The frontend is rebuilt screen by screen from the handoff: warm cream ground, terracotta + sage accents, Caprasimo over Figtree, every control a pill. Same React/Vite/TanStack stack and the same lib/ data layer; the presentation is new. - Tokens: web/src/styles/index.css declares the handoff's styles.css variables through Tailwind's @theme under the same names; dark mode is those variables overridden on <html> by the handoff's pansy-theme.js, inlined in index.html so it runs before first paint. Lucide glyphs at stroke 2.75; a small pill kit (Button, Dialog, Field, Seg, Toggle, Tag, toast). - Login / Register: the centered column over soft accent circles; OIDC button and signup footer still follow /auth/providers. - Gardens: cards with a real SVG plot thumbnail (objects + plant-colored dots from /full), a `plan` tag for "<name> — <year>" copies, shares line, Open + share/copy/edit/delete; New garden / Share / Plan-a-season dialogs. - Plants: monogram markers derived from the name (collision-resolved across the catalog — replaces emoji icons), category chips, expandable lot cards, the scan-packet flow as a two-step dialog that never auto-creates. - Settings: Appearance (theme seg), Who gets in (read-only sign-in config), Garden assistant (self-saving toggle + chat/vision model fields), You. - Editor: a new canvas with the prototype's pointer model (wheel-to-cursor, pinch about the centroid, 3″ snap, one PATCH per drop, semantic-zoom monograms/labels), plus corner resize handles; desktop three-card workspace (toolkit | plan | rail with Plot/Journal/History/Assistant) and, below 760px of container width, the phone chrome (header, peek panel, tool strip, mode bar). Seasons as a segmented control over the years with data plus plan copies; Undo re-reads history before reverting the newest step. - Public read-only view and the register page restyled to match. - GET /settings gains a read-only `auth` view (registration mode, local auth, OIDC issuer) so the Settings page can show what's in force. - README / DESIGN.md / CLAUDE.md updated; @use-gesture/react dropped. Co-Authored-By: Claude Fable 5 <[email protected]>
93 lines
3.9 KiB
TypeScript
93 lines
3.9 KiB
TypeScript
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
import { Link, getRouteApi } from '@tanstack/react-router'
|
|
import { Brand } from '@/components/layout/Nav'
|
|
import { ThemeButton } from '@/components/layout/ThemeButton'
|
|
import { Alert } from '@/components/ui/Alert'
|
|
import { IconButton } from '@/components/ui/Button'
|
|
import { Tag } from '@/components/ui/Tag'
|
|
import { Canvas, type CanvasHandle } from '@/editor/Canvas'
|
|
import { PHONE_BREAKPOINT } from '@/editor/shared'
|
|
import { useEditorStore } from '@/editor/store'
|
|
import type { EditorGarden } from '@/editor/types'
|
|
import { cn } from '@/lib/cn'
|
|
import { monogramMap } from '@/lib/monogram'
|
|
import { toEditorObject } from '@/lib/objects'
|
|
import { toEditorPlanting } from '@/lib/plantings'
|
|
import { usePublicGarden } from '@/lib/publicGarden'
|
|
import { usePageTitle } from '@/lib/usePageTitle'
|
|
|
|
const routeApi = getRouteApi('/g/$token')
|
|
|
|
/**
|
|
* The public, read-only garden behind a share token — the unauthenticated
|
|
* `/g/$token` route (no auth guard), so a logged-out visitor never hits /login
|
|
* or OIDC. The same canvas, with every mutation entry point off.
|
|
*/
|
|
export function PublicGardenPage() {
|
|
const { token } = routeApi.useParams()
|
|
const full = usePublicGarden(token)
|
|
usePageTitle(full.data?.garden.name ?? 'Shared garden')
|
|
const canvasRef = useRef<CanvasHandle>(null)
|
|
const rootRef = useRef<HTMLDivElement>(null)
|
|
const [vw, setVw] = useState(() => (typeof window !== 'undefined' ? window.innerWidth : 1024))
|
|
|
|
useEffect(() => {
|
|
useEditorStore.getState().reset()
|
|
}, [token])
|
|
|
|
useEffect(() => {
|
|
const el = rootRef.current
|
|
if (!el) return
|
|
const ro = new ResizeObserver(([entry]) => setVw(entry.contentRect.width))
|
|
ro.observe(el)
|
|
return () => ro.disconnect()
|
|
}, [])
|
|
const isMobile = vw < PHONE_BREAKPOINT
|
|
|
|
const objects = useMemo(() => full.data?.objects.map(toEditorObject) ?? [], [full.data?.objects])
|
|
const plantings = useMemo(() => full.data?.plantings.map(toEditorPlanting) ?? [], [full.data?.plantings])
|
|
const plants = useMemo(() => full.data?.plants ?? [], [full.data?.plants])
|
|
const plantsById = useMemo(() => new Map(plants.map((p) => [p.id, p])), [plants])
|
|
const letters = useMemo(() => monogramMap(plants), [plants])
|
|
|
|
if (full.isPending) return <p className="p-7 text-[13px] font-semibold text-ink-mute">Loading the garden…</p>
|
|
if (full.isError) {
|
|
return (
|
|
<div className="flex min-h-dvh flex-col bg-bg">
|
|
<div className="flex items-center gap-3 px-3.5 py-2.5">
|
|
<Link to="/" className="no-underline">
|
|
<Brand />
|
|
</Link>
|
|
</div>
|
|
<div className="p-7">
|
|
<Alert>This shared link isn't available. The owner may have turned it off or changed it.</Alert>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const g = full.data.garden
|
|
const garden: EditorGarden = { id: g.id, name: g.name, widthCm: g.widthCm, heightCm: g.heightCm, unitPref: g.unitPref, gridSizeCm: g.gridSizeCm, snapToGrid: g.snapToGrid }
|
|
|
|
return (
|
|
<div ref={rootRef} className="flex h-dvh flex-col bg-bg">
|
|
<div className="flex flex-none items-center gap-3 px-3.5 py-2.5">
|
|
<Link to="/" className="no-underline">
|
|
<Brand />
|
|
</Link>
|
|
<span className="min-w-0 truncate font-heading text-[17px]" title={g.name}>
|
|
{g.name}
|
|
</span>
|
|
<Tag tone="accent-2" className="flex-none">
|
|
shared · read-only
|
|
</Tag>
|
|
<ThemeButton className="ml-auto" />
|
|
</div>
|
|
<div className={cn('relative min-h-0 flex-1', !isMobile && 'panel m-3.5 mt-0 overflow-hidden')}>
|
|
<Canvas ref={canvasRef} garden={garden} objects={objects} plantings={plantings} plantsById={plantsById} letters={letters} canEdit={false} isMobile={isMobile} />
|
|
<IconButton label="Fit the garden" icon="maximize" iconSize={16} variant="plain" size={40} className="elev-sm absolute bottom-3 right-3 border border-divider bg-surface" onClick={() => canvasRef.current?.zoomFit()} />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|