fix: tighten sharing and shadcn composition
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
# React and shadcn Fixes Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Fix the security-sensitive share-link authorization gap and the highest-confidence shadcn Base UI composition issues from the audit.
|
||||
|
||||
**Architecture:** Add registry-level share authorization callbacks and require them in the generic share service before token creation and shared data loading. Update current modules to provide household/user-scoped authorization and share loaders, then align selected UI call sites with shadcn Base API.
|
||||
|
||||
**Tech Stack:** Next.js 15 App Router, TypeScript, Drizzle, shadcn/ui Base UI, Tailwind v4, Node test runner via `tsx --test`.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Share Authorization Helper
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `tests/unit/share-authorization.test.ts`
|
||||
- Create: `src/modules/_core/share-authorization.ts`
|
||||
- Modify: `src/modules/_core/module.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create tests for missing callbacks, denied callbacks, and allowed callbacks.
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `pnpm exec tsx --test tests/unit/share-authorization.test.ts`
|
||||
Expected: failure because `share-authorization.ts` does not exist.
|
||||
|
||||
- [ ] **Step 3: Implement the helper and registry types**
|
||||
|
||||
Add `ShareContext`, `ensureEntityShareAuthorized`, `EntityTypeRegistration.canShareEntity`, and scoped `loadForShare` context typing.
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `pnpm exec tsx --test tests/unit/share-authorization.test.ts`
|
||||
Expected: all tests pass.
|
||||
|
||||
### Task 2: Wire Authorization Into Share Service
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/modules/_core/share.ts`
|
||||
- Modify: `src/app/s/[token]/page.tsx`
|
||||
|
||||
- [ ] **Step 1: Call `ensureEntityShareAuthorized` in `createShareLink`**
|
||||
|
||||
Require the active user and household to be authorized before token insertion.
|
||||
|
||||
- [ ] **Step 2: Pass household context into public share loading**
|
||||
|
||||
Call `entityReg.loadForShare(resolved.entityId, { householdId: resolved.householdId })`.
|
||||
|
||||
- [ ] **Step 3: Run typecheck**
|
||||
|
||||
Run: `pnpm typecheck`
|
||||
Expected: type errors until module adapters are updated.
|
||||
|
||||
### Task 3: Add Module Share Adapters
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/modules/calendar/manifest.tsx`
|
||||
- Modify: `src/modules/calendar/server/share-queries.ts`
|
||||
- Modify: `src/modules/lists/manifest.tsx`
|
||||
- Modify: `src/modules/lists/server/share-queries.ts`
|
||||
- Modify: `src/modules/notes/manifest.tsx`
|
||||
- Modify: `src/modules/notes/server/share-queries.ts`
|
||||
- Modify: `src/modules/garden/manifest.tsx`
|
||||
- Modify: `src/modules/garden/server/share-queries.ts`
|
||||
|
||||
- [ ] **Step 1: Implement `canShareEntity` for every shareable entity**
|
||||
|
||||
Use household scoping and existing private-calendar rules.
|
||||
|
||||
- [ ] **Step 2: Scope `loadForShare` queries by token household**
|
||||
|
||||
Require the public token household to match the entity household.
|
||||
|
||||
- [ ] **Step 3: Run typecheck**
|
||||
|
||||
Run: `pnpm typecheck`
|
||||
Expected: pass.
|
||||
|
||||
### Task 4: Fix Base UI Select Composition
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/modules/calendar/components/calendar-shell.tsx`
|
||||
- Modify: `src/components/theme-picker.tsx`
|
||||
|
||||
- [ ] **Step 1: Add `items` arrays and `SelectGroup` wrappers**
|
||||
|
||||
Keep existing labels and values; do not redesign layout.
|
||||
|
||||
- [ ] **Step 2: Run typecheck**
|
||||
|
||||
Run: `pnpm typecheck`
|
||||
Expected: pass.
|
||||
|
||||
### Task 5: Targeted shadcn Menu and Icon Cleanup
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/components/dashboard-switcher.tsx`
|
||||
- Modify: `src/components/dashboard-editor.tsx`
|
||||
- Modify: `src/components/share-button.tsx`
|
||||
|
||||
- [ ] **Step 1: Wrap dropdown items in `DropdownMenuGroup`**
|
||||
|
||||
Preserve menu behavior.
|
||||
|
||||
- [ ] **Step 2: Replace icon sizing inside shadcn buttons/menu items**
|
||||
|
||||
Use `data-icon` where icons sit in `Button`; remove explicit `size-4 mr-*` from dropdown item icons.
|
||||
|
||||
- [ ] **Step 3: Run lint**
|
||||
|
||||
Run: `pnpm lint`
|
||||
Expected: no new errors; existing warnings may remain.
|
||||
|
||||
### Task 6: Final Verification
|
||||
|
||||
**Files:**
|
||||
|
||||
- No direct edits.
|
||||
|
||||
- [ ] **Step 1: Run targeted test**
|
||||
|
||||
Run: `pnpm exec tsx --test tests/unit/share-authorization.test.ts`
|
||||
Expected: pass.
|
||||
|
||||
- [ ] **Step 2: Run typecheck**
|
||||
|
||||
Run: `pnpm typecheck`
|
||||
Expected: pass.
|
||||
|
||||
- [ ] **Step 3: Run lint**
|
||||
|
||||
Run: `pnpm lint`
|
||||
Expected: exit 0; warnings only if pre-existing.
|
||||
@@ -0,0 +1,35 @@
|
||||
# React and shadcn Fixes Design
|
||||
|
||||
## Goal
|
||||
|
||||
Fix the audit findings that have security or correctness impact, and make a small targeted pass on shadcn Base UI composition without turning this into a broad visual rewrite.
|
||||
|
||||
## Approaches Considered
|
||||
|
||||
1. **Security-first, targeted UI cleanup.** Add a registry authorization hook for share links, implement it for current modules, fix Base `Select` usage, and clean up the compact dropdown/button issues found in the audit.
|
||||
2. **Full shadcn migration sweep.** Replace all `.btn`, raw forms, raw colors, and hand-rolled overlays in one pass. This would touch many dirty files and blur behavior changes with style churn.
|
||||
3. **Security only.** Fix share authorization and defer UI. This leaves known Base UI composition drift in place.
|
||||
|
||||
Chosen approach: **Option 1**. It fixes the exploitable path, handles the highest-confidence shadcn correctness issue, and leaves broad styling convergence to the existing shadcn-tier task docs.
|
||||
|
||||
## Architecture
|
||||
|
||||
Share authorization belongs in the entity registry, not in `_core` switch statements. Each shareable entity registration will expose `canShareEntity(id, ctx)` and the generic `createShareLink` action will require it to return true before inserting a token.
|
||||
|
||||
Public share rendering will also pass the token household into `loadForShare(id, ctx)` so loaders can scope database reads. This prevents a bad token row from loading an unrelated entity by id alone.
|
||||
|
||||
## UI Composition
|
||||
|
||||
The project uses shadcn `base-nova`, so `Select` roots need an `items` prop and `SelectItem` children should be wrapped in `SelectGroup`. The calendar and theme picker selects will be updated to that shape.
|
||||
|
||||
Compact menu/button drift will be cleaned where it does not require redesign: dropdown items should sit in `DropdownMenuGroup`, menu icons should rely on component icon sizing, and button icons should use `data-icon`.
|
||||
|
||||
## Testing
|
||||
|
||||
Add a small Node test around the new share authorization helper first. The test will prove that missing or false registry authorization rejects share creation and that approved entities pass through. Typecheck and lint will cover module hook signatures and Base UI prop usage.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Full replacement of `.btn` and raw garden forms.
|
||||
- Reworking command palette and quick-add sheet into shadcn dialogs.
|
||||
- Measuring bundle deltas or adding dynamic imports for global overlays.
|
||||
@@ -0,0 +1,104 @@
|
||||
# Tier 1: Garden badges → Badge, topbar avatar → Avatar, + fallback, share-green → semantic token
|
||||
|
||||
## Goal
|
||||
|
||||
Replace ad-hoc badge and avatar styling across the app with shadcn/ui component primitives. Standardize color tokens for green success/warning indicators. Create a reusable avatar fallback pattern.
|
||||
|
||||
## Affected files (6)
|
||||
|
||||
### Task 1a: Garden health badges → Badge variant
|
||||
|
||||
**File:** `src/modules/garden/components/plant-detail.tsx`
|
||||
|
||||
- **Current state (line ~44-48):** `healthBadgeClass(status)` returns hand-crafted class names (`badge-success`, `badge-danger`, `badge-warning`) not from shadcn.
|
||||
- **Line 119-123:** Badge rendered as a span with those classes.
|
||||
- **Change:** Replace with `<Badge>` component:
|
||||
- "healthy" → `variant="default"` (primary color)
|
||||
- "sick" → `variant="destructive"`
|
||||
- "sick/other" → `variant="secondary"`
|
||||
- Delete `healthBadgeClass()` function. Remove the span and replace with Badge import + usage.
|
||||
|
||||
### Task 1b: Topbar avatar → `<Avatar>`
|
||||
|
||||
**File:** `src/components/topbar.tsx`
|
||||
|
||||
- **Current state (line ~60-75):** Manual `<span className="avatar">` with inline width/height/background styles, plus conditional img tag or initial text fallback.
|
||||
- **Change:** Replace with shadcn `<Avatar>`:
|
||||
|
||||
```tsx
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
||||
|
||||
<Avatar style={{ width: 28, height: 28 }}>
|
||||
{userRow?.image ? (
|
||||
<AvatarImage src={userRow.image} alt="" />
|
||||
) : (
|
||||
<AvatarFallback>{initial}</AvatarFallback>
|
||||
)}
|
||||
</Avatar>;
|
||||
```
|
||||
|
||||
- Remove the `getUserAvatar` function — already fetches name/email/image. Just pass the image directly.
|
||||
|
||||
### Task 1c: HouseholdPill avatars → Avatar with fallback
|
||||
|
||||
**File:** `src/components/sidebar.tsx`
|
||||
|
||||
- **Current state (line ~106-120):** Manual span-based avatar circles for household members. Uses `avatarColor()` hash function.
|
||||
- **Change:** Replace inner spans with `<Avatar>`:
|
||||
```tsx
|
||||
<Avatar
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
marginLeft: i > 0 ? -6 : 0,
|
||||
boxShadow: "0 0 0 1.5px var(--card)",
|
||||
}}
|
||||
>
|
||||
{m.image ? (
|
||||
<AvatarImage src={m.image} alt={m.name ?? m.email ?? ""} />
|
||||
) : (
|
||||
<AvatarFallback style={{ background: avatarColor(m.id) }}>{initial}</AvatarFallback>
|
||||
)}
|
||||
</Avatar>
|
||||
```
|
||||
- The `avatarColor()` function stays as-is — it produces the color variable for the fallback background.
|
||||
|
||||
### Task 1d: Create a generic Avatar fallback wrapper (reusable component)
|
||||
|
||||
**File:** `src/components/avatar-fallback.tsx` (new)
|
||||
|
||||
- **Purpose:** A thin wrapper that takes `(name|initial, image?)` and renders the right Avatar/AvatarImage/Fallback pattern with proper fallback initials.
|
||||
- **Export:** `AvatarFallbackWithName` — accepts `{ name?: string; initial?: string; image?: string | null; size?: "sm" | "default" | "lg" }`
|
||||
- Used by both topbar and sidebar avatars, removing code duplication.
|
||||
|
||||
### Task 1e: share-green → semantic token
|
||||
|
||||
**Files affected:**
|
||||
|
||||
1. `src/components/push-opt-in.tsx` line ~104: `text-green-600 dark:text-green-400` → `text-[var(--c-success)] dark:text-[var(--c-success)]`
|
||||
2. `src/modules/garden/components/care-schedule-editor.tsx` line ~185: green borders for "Active" button → use a semantic border class like `border-[var(--c-success)] text-[var(--c-success)]`
|
||||
3. `src/modules/garden/components/plant-widget.tsx` urgency colors: ensure overdue/due-today colors use semantic CSS vars rather than hardcoded greens/red/amber
|
||||
|
||||
**Changes:**
|
||||
|
||||
- In `src/lib/themes.ts` or the global CSS root, add: `--c-success: #16a34a; dark: --c-success: #4ade80;` (or pick from existing palette)
|
||||
- Replace all `text-green-600`/`dark:text-green-400` → `text-[var(--c-success)] dark:text-[var(--c-success)]`
|
||||
- Similarly for border-green-400 → `border-[var(--c-success)]`
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. No hardcoded badge-success/badge-danger/badge-warning class names remain in plant-detail.tsx
|
||||
2. Topbar avatar renders via shadcn Avatar components (verified by DOM inspection)
|
||||
3. HouseholdPill avatars use shadcn Avatar components
|
||||
4. AvatarFallbackWithName component exists and is imported by both topbar.tsx and sidebar.tsx
|
||||
5. All green-600/green-500 references replaced with --c-success semantic variable
|
||||
6. Zero TypeScript errors, zero lint errors
|
||||
|
||||
## Steps
|
||||
|
||||
1. Edit plant-detail.tsx: replace healthBadgeClass usage with <Badge> component
|
||||
2. Create src/components/avatar-fallback.tsx
|
||||
3. Edit topbar.tsx: use Avatar from shadcn + AvatarFallbackWithName
|
||||
4. Edit sidebar.tsx: update HouseholdPill to use Avatar components
|
||||
5. Search for remaining green-600/green-500/border-green references and replace with --c-success
|
||||
6. Run `pnpm lint` and `pnpm typecheck` to verify no errors
|
||||
@@ -0,0 +1,183 @@
|
||||
# Tier 2: Skeleton loading states, settings Tabs navigation, toast notifications via sonner, form FieldGroup audit, manual separators → <Separator>
|
||||
|
||||
## Goal
|
||||
|
||||
Standardize loading UX, navigation, and toast patterns across the app using shadcn components. Audit form field consistency. Replace hand-crafted separator borders with the shadcn Separator component.
|
||||
|
||||
---
|
||||
|
||||
### Task 2a: Skeleton loading states
|
||||
|
||||
**Files affected:**
|
||||
|
||||
1. `src/app/d/[slug]/page.tsx` line ~120 — inline `<Suspense fallback>` with animate-pulse divs (3 lines). Convert to `<Skeleton>` components.
|
||||
2. `src/modules/garden/components/plant-detail.tsx` line 55 — `uploadingImage` state during image upload, no visual feedback other than button text. Add a skeleton overlay or shimmer on the gallery card area.
|
||||
3. `src/modules/garden/components/container-detail.tsx` line 30 — same pattern: `uploadingImage` state without skeleton.
|
||||
4. Any garden page with loading states that use animate-pulse instead of Skeleton.
|
||||
|
||||
**Changes:**
|
||||
|
||||
1. **d/[slug]/page.tsx:** Replace inline fallback divs:
|
||||
```tsx
|
||||
<Suspense fallback={
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
</div>
|
||||
}>
|
||||
```
|
||||
2. **plant-detail.tsx / container-detail.tsx:** When `uploadingImage` is true, wrap the image area with a Skeleton overlay (absolute positioned, full-width). Or add a shimmer effect using the existing skeleton component.
|
||||
|
||||
---
|
||||
|
||||
### Task 2b: Settings Tabs navigation → `<Tabs>`
|
||||
|
||||
**File:** `src/app/settings/page.tsx` + `src/components/settings-section.tsx`
|
||||
|
||||
- **Current state:** Uses a sidebar nav (vertical list with URL hash sync). This is fine for desktop but doesn't use `<Tabs>`.
|
||||
- **What needs to change:** The **garden detail pages** that have manual tab buttons need to use shadcn Tabs, NOT the settings page. The tier says "settings Tabs navigation" — checking the context: plant-detail.tsx line 164 and container-detail.tsx line 139 both have hand-crafted tab buttons with border-bottom active state. These should become `<Tabs>` + `<TabsList>` + `<TabsTrigger>` + `<TabsContent>`.
|
||||
|
||||
**Changes:**
|
||||
|
||||
1. **plant-detail.tsx (lines 162-180):** Replace manual tab buttons:
|
||||
|
||||
```tsx
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
|
||||
<Tabs value={tab} onValueChange={(v) => setTab(v as Tab)} className="mt-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="info">Info</TabsTrigger>
|
||||
<TabsTrigger value="gallery">Gallery</TabsTrigger>
|
||||
<TabsTrigger value="care">Care</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="info">{/* existing info section */}</TabsContent>
|
||||
<TabsContent value="gallery">{/* existing gallery section */}</TabsContent>
|
||||
<TabsContent value="care">{/* existing care section */}</TabsContent>
|
||||
</Tabs>;
|
||||
```
|
||||
|
||||
2. **container-detail.tsx (lines 138-155):** Same pattern — info/gallery tabs:
|
||||
```tsx
|
||||
<Tabs value={tab} onValueChange={(v) => setTab(v as Tab)} className="mt-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="info">Info</TabsTrigger>
|
||||
<TabsTrigger value="gallery">Gallery</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="info">{/* ... */}</TabsContent>
|
||||
<TabsContent value="gallery">{/* ... */}</TabsContent>
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2c: Toast notifications via sonner
|
||||
|
||||
**Files affected:** Need to find all manual error/success messages and convert to toast calls.
|
||||
|
||||
1. **`src/app/layout.tsx`:** Add `<Toaster />` component to the app layout root so toasts are globally available.
|
||||
2. **Places that need toast conversion (search for inline `setError` / success messages):**
|
||||
- `src/modules/garden/components/plant-detail.tsx` — delete confirmation success/error → `toast.success()` / `toast.error()`
|
||||
- `src/modules/garden/components/container-detail.tsx` — same pattern
|
||||
- `src/modules/garden/components/care-schedule-editor.tsx` line 192 (`calendarSuccess`) and error states → toast
|
||||
- `src/components/share-button.tsx` — "Share link created" dialog could be a toast instead (or keep as dialog but add toast on copy)
|
||||
|
||||
**Changes:**
|
||||
|
||||
```tsx
|
||||
// In any component that needs toasts:
|
||||
import { toast } from "sonner";
|
||||
|
||||
// On success:
|
||||
toast.success("Event added to calendar");
|
||||
|
||||
// On error:
|
||||
toast.error("Failed to create share link");
|
||||
|
||||
// In layout.tsx (client component or client wrapper):
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
|
||||
return <Toaster position="bottom-right" />;
|
||||
```
|
||||
|
||||
**Acceptance for 2c:**
|
||||
|
||||
- `<Toaster>` is present in the app shell layout
|
||||
- All delete confirmations show toasts on success/error
|
||||
- The share dialog can remain as-is (it's a distinct UX), but copy action adds toast confirmation
|
||||
|
||||
---
|
||||
|
||||
### Task 2d: Form FieldGroup audit
|
||||
|
||||
**Scope:** Audit all form field components across the app. Check for consistency in label-input-error patterns.
|
||||
|
||||
**Files to audit:**
|
||||
|
||||
- `src/modules/garden/components/container-form.tsx` — garden container form fields
|
||||
- `src/modules/garden/components/plant-form.tsx` — garden plant form fields
|
||||
- `src/app/settings/household/*` — household edit forms
|
||||
- Any other server/action-driven forms
|
||||
|
||||
**Audit checklist:**
|
||||
|
||||
1. Do all form inputs have `<Label>` from shadcn? (Not just plain text labels)
|
||||
2. Are error messages rendered consistently below each field?
|
||||
3. Are `Input` components from shadcn used everywhere? (No native `<input>` tags with manual styles)
|
||||
4. Is there a FieldGroup wrapper pattern being used, or is it ad-hoc div nesting?
|
||||
|
||||
**Deliverable:** A list of inconsistencies found and fixes applied. If no major issues, document the findings. Do not introduce a FieldGroup abstraction unless one already exists in the codebase.
|
||||
|
||||
---
|
||||
|
||||
### Task 2e: Manual separators → `<Separator>`
|
||||
|
||||
**Files affected:**
|
||||
|
||||
1. `src/modules/garden/components/plant-detail.tsx` — border-bottom on tabs bar (`border-b border-[var(--ink-faint)]`) → use `<Separator>` instead
|
||||
2. `src/modules/garden/components/container-detail.tsx` — same pattern for info/gallery tab separator
|
||||
3. Any other manual `border-b`, `border-t`, or `border-l` borders used purely as visual separators (not structural layout borders)
|
||||
|
||||
**Changes:**
|
||||
|
||||
```tsx
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
|
||||
// Replace:
|
||||
<div className="flex gap-6 border-b border-[var(--ink-faint)]">
|
||||
{/* tab buttons */}
|
||||
</div>
|
||||
|
||||
// With:
|
||||
<TabsList>
|
||||
<TabsTrigger value="info">Info</TabsTrigger>
|
||||
<TabsTrigger value="gallery">Gallery</TabsTrigger>
|
||||
</TabsList>
|
||||
<Separator className="my-2" />
|
||||
|
||||
// For the garden tab bar:
|
||||
{tab === "gallery" && (
|
||||
<Separator className="my-4" orientation="horizontal" />
|
||||
)}
|
||||
```
|
||||
|
||||
**Note:** The settings sidebar uses a `.nav-divider` class which is handled by CSS — leave that alone. Only convert inline border divs to Separator components.
|
||||
|
||||
---
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. All loading states use `<Skeleton>` (no animate-pulse patterns outside skeleton.tsx)
|
||||
2. Garden detail page tabs use shadcn Tabs/TabsList/TabsTrigger/TabsContent
|
||||
3. Toaster component rendered in app layout, delete/cancel actions show toasts
|
||||
4. Form fields audit complete with findings documented or fixed
|
||||
5. Manual border separators replaced with `<Separator>` where appropriate
|
||||
|
||||
## Steps
|
||||
|
||||
1. Add Skeleton imports and replace inline animation patterns (2a)
|
||||
2. Replace garden detail page tabs with shadcn Tabs (2b)
|
||||
3. Add Toaster to layout, add toast() calls to action handlers (2c)
|
||||
4. Audit form fields for consistency (2d)
|
||||
5. Replace border separators with Separator component (2e)
|
||||
6. Run `pnpm lint` and `pnpm typecheck`
|
||||
@@ -0,0 +1,143 @@
|
||||
# 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`
|
||||
@@ -0,0 +1,114 @@
|
||||
# 76 — Shadcn component implementation (Tiers 1–3)
|
||||
|
||||
## Goal
|
||||
|
||||
Implement the three shadcn component adoption tiers that replace ad-hoc UI patterns across the app with shadcn/ui primitives. This is the **execution** phase — components are already installed (see `2026-06-13-shadcn-component-install.md`).
|
||||
|
||||
| Tier | Name | Tasks | Files touched |
|
||||
| ---- | ------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| 1 | Garden badges → Badge, topbar avatar → Avatar, + fallback, semantic token | 5 sub-tasks | topbar.tsx, sidebar.tsx, plant-detail.tsx, push-opt-in.tsx, care-schedule-editor.tsx, plant-widget.tsx |
|
||||
| 2 | Skeleton loading, Tabs nav, Sonner toasts, FieldGroup audit, Separator | 5 sub-tasks | d/[slug]/page.tsx, plant-detail.tsx, container-detail.tsx, layout.tsx, notify-channel-toggles.tsx |
|
||||
| 3 | Garden empty-state, Switch for notify channels | 2 sub-tasks | plant-list.tsx, container-list.tsx, notify-channel-toggles.tsx |
|
||||
|
||||
## Depends on
|
||||
|
||||
- `2026-06-13-shadcn-component-install.md` — components are already in `src/components/ui/`
|
||||
- The shadcn migration map at `2026-06-13-shadcn-migration.md`
|
||||
|
||||
## Scope by tier
|
||||
|
||||
### Tier 1 (detailed brief: `docs/tasks/1a-badges-avatars.md`)
|
||||
|
||||
**Task 1a — Garden health badges → `<Badge>`**
|
||||
|
||||
- File: `src/modules/garden/components/plant-detail.tsx` lines 44-48, 119-123
|
||||
- Replace hand-crafted `badge-success/danger/warning` classes with shadcn `<Badge variant="default|destructive|secondary">`
|
||||
- Delete the `healthBadgeClass()` helper function
|
||||
|
||||
**Task 1b — Topbar avatar → `<Avatar>`**
|
||||
|
||||
- File: `src/components/topbar.tsx` lines 60-75
|
||||
- Replace manual `<span className="avatar">` with `<Avatar><AvatarImage/><AvatarFallback/></Avatar>`
|
||||
|
||||
**Task 1c — HouseholdPill avatars → `<Avatar>`**
|
||||
|
||||
- File: `src/components/sidebar.tsx` lines 106-120
|
||||
- Replace inner span elements with Avatar components (keep avatarColor hash for fallback background)
|
||||
|
||||
**Task 1d — Generic AvatarFallbackWithName component**
|
||||
|
||||
- New file: `src/components/avatar-fallback.tsx`
|
||||
- Thin wrapper that handles the Image/Fallback pattern with name/initial logic
|
||||
- Imported by both topbar.tsx and sidebar.tsx to remove duplication
|
||||
|
||||
**Task 1e — share-green → semantic token**
|
||||
|
||||
- Files: push-opt-in.tsx (line ~104), care-schedule-editor.tsx (line ~185), plant-widget.tsx urgencyLabel, plus any other green-600/green-500 in garden components
|
||||
- Add `--c-success` CSS variable to the global theme
|
||||
- Replace all hardcoded green-600 references with `var(--c-success)`
|
||||
|
||||
### Tier 2 (detailed brief: `docs/tasks/1b-skeleton-tabs-toasts.md`)
|
||||
|
||||
**Task 2a — Skeleton loading states**
|
||||
|
||||
- d/[slug]/page.tsx inline animate-pulse → `<Skeleton>` components
|
||||
- plant-detail.tsx & container-detail.tsx uploadingImage → skeleton overlay
|
||||
- Any other animate-pulse patterns outside skeleton.tsx itself
|
||||
|
||||
**Task 2b — Settings/garden Tabs navigation → `<Tabs>`**
|
||||
|
||||
- File: `src/modules/garden/components/plant-detail.tsx` lines 162-180
|
||||
- File: `src/modules/garden/components/container-detail.tsx` lines 138-155
|
||||
- Replace manual button-based tabs with shadcn Tabs/TabsList/TabsTrigger/TabsContent
|
||||
|
||||
**Task 2c — Toast notifications via sonner**
|
||||
|
||||
- Add `<Toaster>` to app layout (`src/app/layout.tsx`)
|
||||
- Convert delete confirmations, calendar schedule success/error, and share-link actions to `toast.success()`/`toast.error()` calls
|
||||
|
||||
**Task 2d — Form FieldGroup audit**
|
||||
|
||||
- Audit: container-form.tsx, plant-form.tsx, household edit forms
|
||||
- Check consistency of Label + Input + error pattern
|
||||
- Fix any non-shadcn form fields found
|
||||
|
||||
**Task 2e — Manual separators → `<Separator>`**
|
||||
|
||||
- Remove border-bottom on garden detail tab bars, replace with shadcn Separator (usually alongside the Tabs component)
|
||||
|
||||
### Tier 3 (detailed brief: `docs/tasks/1c-emptystate-switch.md`)
|
||||
|
||||
**Task 3a — Garden empty-state component**
|
||||
|
||||
- File: `src/modules/garden/components/plant-list.tsx` line 22 → use EmptyState from ui/empty.tsx
|
||||
- File: `src/modules/garden/components/container-list.tsx` line 39 → use EmptyState
|
||||
- Use appropriate icons (Sprout for plants, Container for containers)
|
||||
|
||||
**Task 3b — Notify channel toggles → `<Switch>`**
|
||||
|
||||
- File: `src/components/notify-channel-toggles.tsx`
|
||||
- Replace native `<input type="checkbox">` with shadcn `<Switch>` component
|
||||
- Keep server action integration (setNotifChannel)
|
||||
- Disabled state for ntfy when unconfigured
|
||||
|
||||
## Acceptance criteria (all tiers)
|
||||
|
||||
1. Zero `badge-success`, `badge-danger`, `badge-warning` class names remain in garden code
|
||||
2. All avatars rendered via shadcn Avatar components (verified by DOM/JSX inspection)
|
||||
3. All loading states use `<Skeleton>` — no animate-pulse patterns outside skeleton.tsx
|
||||
4. Garden detail pages use shadcn Tabs component (not manual buttons)
|
||||
5. Toaster rendered in app layout, delete/cancel actions show toasts
|
||||
6. EmptyState used in all garden list pages (no ad-hoc empty text paragraphs)
|
||||
7. Notification channels use shadcn Switch components
|
||||
8. `pnpm lint` passes with zero errors/warnings
|
||||
9. `pnpm typecheck` passes with zero errors
|
||||
|
||||
## Execution order
|
||||
|
||||
Execute tiers sequentially: 1 → 2 → 3. Each tier builds on the previous one's patterns and fixes foundational issues before adding polish.
|
||||
|
||||
Detailed task briefs are in:
|
||||
|
||||
- Tier 1: `docs/tasks/1a-badges-avatars.md`
|
||||
- Tier 2: `docs/tasks/1b-skeleton-tabs-toasts.md`
|
||||
- Tier 3: `docs/tasks/1c-emptystate-switch.md`
|
||||
Reference in New Issue
Block a user