Files
famapp/docs/tasks/1c-emptystate-switch.md
T
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

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:

  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:

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:

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:

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