5.7 KiB
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:
src/modules/garden/components/plant-list.tsxline 22:<p className="text-sm text-[var(--ink-mute)]">No plants yet.</p>→<EmptyState ... />src/modules/garden/components/container-list.tsxline 39:"No containers yet. Add one to start organising your plants."→<EmptyState ... />src/modules/garden/components/container-detail.tsxline 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:
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/emptyin 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:
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:
- Detect which channels were added/removed from the selection
- Call
setNotifChannel(channel, enabled)for each change - 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:
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
- EmptyState component from
@/components/ui/emptyused in all garden list pages - All ad-hoc "No plants yet." / "No containers yet." text replaced with EmptyState or kept as contextual inline text where appropriate
- Notification channels use shadcn
<Switch>components - Visual consistency across garden empty states
Steps
- Use existing
src/components/ui/empty.tsxEmptyState in plant-list.tsx and container-list.tsx - Replace Switch component imports in notify-channel-toggles.tsx
- Verify server action integration for toggles
- Run
pnpm lintandpnpm typecheck