Files
famapp/docs/tasks/1c-emptystate-switch.md
ginnoir a312d4ce39
CI / checks (push) Successful in 12m55s
CI / build (push) Successful in 15m16s
fix: tighten sharing and shadcn composition
2026-06-13 05:20:01 -05:00

144 lines
5.7 KiB
Markdown

# Tier 3: Garden empty-state component, toggle-group for notify channels
## Goal
Create a reusable EmptyState component for garden (and potentially other modules), and replace manual checkbox patterns with shadcn ToggleGroup for notification channel selection.
---
### Task 3a: Garden empty-state component
**New file:** `src/modules/garden/components/empty-state.tsx`
**Purpose:** A shared empty-state component for the garden module — replacing ad-hoc "No plants yet." / "No containers yet." paragraphs scattered across garden list/detail pages.
**Design (base on shadcn's `<EmptyState>` from `src/components/ui/empty.tsx`):**
The empty component already exists at `src/components/ui/empty.tsx` — it provides a generic EmptyState wrapper with icon + title + description slots. We should **use it directly**, not create a garden-specific one, unless the garden needs garden-themed defaults.
**Garden-specific variants needed:**
- "No plants yet" — icon: leaf/plant, title: "No plants yet", description: "Add your first plant to get started."
- "No containers yet" — icon: box/pot, title: "No containers yet", description: "Add a container to group your plants."
**Files to update:**
1. `src/modules/garden/components/plant-list.tsx` line 22: `<p className="text-sm text-[var(--ink-mute)]">No plants yet.</p>``<EmptyState ... />`
2. `src/modules/garden/components/container-list.tsx` line 39: `"No containers yet. Add one to start organising your plants."``<EmptyState ... />`
3. `src/modules/garden/components/container-detail.tsx` line 169: `<p className="text-sm text-[var(--ink-mute)]">No plants in this container yet.</p>` → inline EmptyState or keep as brief fallback (it's a sub-section, not a full page)
**Import:**
```tsx
import { EmptyState } from "@/components/ui/empty"
import { Sprout, Container } from "lucide-react"
// In plant-list:
<EmptyState
icon={Sprout}
title="No plants yet"
description="Add your first plant to get started."
/>
// In container-list:
<EmptyState
icon={Container}
title="No containers yet"
description="Add a container to start organising your plants."
/>
```
**Acceptance for 3a:**
- EmptyState imported from `@/components/ui/empty` in all garden list pages
- Zero ad-hoc empty text paragraphs remain in garden component files (check plant-list, container-list, and any other garden page)
- Garden empty states are visually consistent (same icon size, title weight, description color)
---
### Task 3b: Toggle-group for notify channels
**File:** `src/components/notify-channel-toggles.tsx`
**Current state:** Each channel is a native `<input type="checkbox">` wrapped in a label with custom styling. No grouping or visual cohesion.
**Change to ToggleGroup + ToggleGroupItem:**
```tsx
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
export function NotifyChannelToggles({
push, inapp, ntfy, ntfyConfigured,
}: { ... }) {
const channels: { key: Channel; value: boolean; label: string; disabled?: boolean }[] = [
{ key: "push", value: push, label: "Web push" },
{ key: "inapp", value: inapp, label: "In-app inbox" },
{ key: "ntfy", value: ntfy, label: "ntfy", disabled: !ntfyConfigured },
];
const enabledKeys = channels.filter(c => c.value && !c.disabled).map(c => c.key);
return (
<ToggleGroup type="multiple" defaultValue={enabledKeys} onValueChange={(v) => handleToggle(v)}>
{channels.map(({ key, value, label, disabled }) => (
<ToggleGroupItem
key={key}
value={key}
disabled={disabled || !("PushManager" in window) && key === "push"}
aria-label={label}
>
{label}
</ToggleGroupItem>
))}
</ToggleGroup>
);
}
```
**Important:** The ToggleGroup needs to integrate with the existing server action flow (`setNotifChannel`). The `onValueChange` handler should:
1. Detect which channels were added/removed from the selection
2. Call `setNotifChannel(channel, enabled)` for each change
3. Still trigger an optimistic UI update
**Alternative approach** (safer — use individual ToggleGroupItems as independent switches): Since these are truly independent toggles (enabling push doesn't require disabling inapp), we keep the current per-channel rendering but swap `<input type="checkbox">` to shadcn's `<Switch>` component from `@/components/ui/switch`:
```tsx
import { Switch } from "@/components/ui/switch";
// Each channel:
<label className="flex items-center justify-between gap-4">
<span className="text-sm">{label}</span>
<Switch
checked={value}
disabled={disabled || isPending}
onCheckedChange={(v) => toggle(key, v)}
/>
</label>;
```
**Decision:** The `<Switch>` approach is more appropriate here. ToggleGroup is for mutually-exclusive selection; Switch is for independent on/off toggles — which matches the notification channels use case perfectly. **Use `Switch` from shadcn, not ToggleGroup.**
**Acceptance for 3b:**
- Each channel uses shadcn `<Switch>` component (not native checkbox)
- Visual appearance matches the app's design system
- Server action integration works correctly (toggle enables/disables on the server)
- Disabled state handled properly for ntfy when unconfigured
---
## Acceptance criteria
1. EmptyState component from `@/components/ui/empty` used in all garden list pages
2. All ad-hoc "No plants yet." / "No containers yet." text replaced with EmptyState or kept as contextual inline text where appropriate
3. Notification channels use shadcn `<Switch>` components
4. Visual consistency across garden empty states
## Steps
1. Use existing `src/components/ui/empty.tsx` EmptyState in plant-list.tsx and container-list.tsx
2. Replace Switch component imports in notify-channel-toggles.tsx
3. Verify server action integration for toggles
4. Run `pnpm lint` and `pnpm typecheck`