@hachej/boring-ui-kit 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/r/file-tree.json CHANGED
@@ -4,27 +4,28 @@
4
4
  "title": "File tree",
5
5
  "description": "Browse, search, upload and inspect retained versions through one revisionProvider binding. Uses the consumer's shadcn Button and dropdown-menu with shared version-history rows; Base UI style only.",
6
6
  "dependencies": [
7
- "@hachej/boring-ui-kit@^0.4.1",
8
- "@hachej/boring-files@^0.4.1",
7
+ "@hachej/boring-ui-kit@^0.4.3",
8
+ "@hachej/boring-files@^0.4.3",
9
9
  "react@^19.0.0",
10
10
  "lucide-react@^1.51.0"
11
11
  ],
12
12
  "registryDependencies": [
13
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.1/r/theme.json",
14
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.1/r/utils.json",
15
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.1/r/version-history.json",
13
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.3/r/theme.json",
14
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.3/r/utils.json",
15
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.3/r/version-history.json",
16
16
  "button",
17
17
  "dropdown-menu",
18
18
  "collapsible",
19
19
  "empty",
20
20
  "input",
21
+ "progress",
21
22
  "separator",
22
23
  "sidebar"
23
24
  ],
24
25
  "files": [
25
26
  {
26
27
  "path": "registry/file-tree/file-tree.tsx",
27
- "content": "'use client';\n\nimport '@hachej/boring-ui-kit/file-tree.css';\nimport { FileTree as PackageFileTree, FileTreeView as PackageFileTreeView } from '@hachej/boring-ui-kit/file-tree-view';\nimport type { FileTreeComponents, FileTreeActionsProps, FileTreeButtonProps, FileTreeVersionsProps, FileTreeProps as PackageFileTreeProps, FileTreeViewProps as PackageFileTreeViewProps } from '@hachej/boring-ui-kit/file-tree-view';\nimport { useRef } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';\nimport { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty';\nimport { Input } from '@/components/ui/input';\nimport { Separator } from '@/components/ui/separator';\nimport { SidebarMenu, SidebarMenuItem, SidebarMenuSub } from '@/components/ui/sidebar';\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';\nimport { VersionHistoryRows, frenchVersionHistoryLabels } from '../version-history/version-history';\nimport { cn } from '../utils/utils';\n\n// Only touch-target layout is added; appearance remains the consumer's shadcn Button.\nfunction LibraryButton({ className, ...props }: FileTreeButtonProps) {\n return <Button {...props} className={cn('max-sm:min-h-11 max-sm:min-w-11 pointer-coarse:min-h-11 pointer-coarse:min-w-11', className)} />;\n}\n\nfunction FileActions({ label, icon, items }: FileTreeActionsProps) {\n const trigger = useRef<HTMLButtonElement>(null);\n const portal = useRef<HTMLElement | null>(null);\n return <DropdownMenu>\n <DropdownMenuTrigger ref={trigger} render={<LibraryButton type=\"button\" variant=\"ghost\" size=\"icon\" ref={node => { portal.current = node?.ownerDocument.body ?? null; }} />} data-file-more aria-label={label}>{icon}</DropdownMenuTrigger>\n <DropdownMenuPortal container={portal}>\n <DropdownMenuContent data-boring=\"file-actions\" align=\"end\" finalFocus={trigger} className=\"w-max max-w-(--available-width)\">\n {items.map(item => <DropdownMenuItem key={item.id} data-action={item.id} onClick={item.onSelect} className=\"max-sm:min-h-11 pointer-coarse:min-h-11\">{item.icon}{item.label}</DropdownMenuItem>)}\n </DropdownMenuContent>\n </DropdownMenuPortal>\n </DropdownMenu>;\n}\n\nfunction FileVersions({ versions, current, onSelect, locale, earlierVersion }: FileTreeVersionsProps) {\n return <VersionHistoryRows versions={versions.map(version => ({ id: version.revision, label: earlierVersion, savedAt: version.savedAt, ...(version.source ? { source: version.source } : {}) }))}\n current={current} onSelect={onSelect} testId=\"file-history-versions\" {...(locale === 'fr' ? { labels: frenchVersionHistoryLabels } : {})} />;\n}\n\nfunction FileTreeEmpty({ icon, title, description, className, children }: Parameters<FileTreeComponents['Empty']>[0]) {\n return <Empty className={className}>\n <EmptyHeader>\n {icon && <EmptyMedia variant=\"icon\">{icon}</EmptyMedia>}\n <EmptyTitle>{title}</EmptyTitle>\n {description && <EmptyDescription>{description}</EmptyDescription>}\n </EmptyHeader>\n {children && <EmptyContent>{children}</EmptyContent>}\n </Empty>;\n}\n\nexport const fileTreeComponents = {\n Button: LibraryButton, Actions: FileActions, Versions: FileVersions,\n Menu: SidebarMenu, MenuItem: SidebarMenuItem, MenuSub: SidebarMenuSub, Input, Separator: () => <Separator />,\n Collapsible, CollapsibleTrigger, CollapsibleContent, Empty: FileTreeEmpty,\n} satisfies FileTreeComponents;\nexport type FileTreeProps = Omit<PackageFileTreeProps, 'components'>;\nexport type FileTreeViewProps = Omit<PackageFileTreeViewProps, 'components'>;\n\nexport function FileTree(props: FileTreeProps) {\n return <PackageFileTree {...props} components={fileTreeComponents} />;\n}\nexport function FileTreeView(props: FileTreeViewProps) {\n return <PackageFileTreeView {...props} components={fileTreeComponents} />;\n}\nexport { createFileTreeController } from '@hachej/boring-ui-kit/file-tree';\nexport type { FileTreeController } from '@hachej/boring-ui-kit/file-tree';\n",
28
+ "content": "'use client';\n\nimport '@hachej/boring-ui-kit/file-tree.css';\nimport { FileTree as PackageFileTree, FileTreeView as PackageFileTreeView } from '@hachej/boring-ui-kit/file-tree-view';\nimport type { FileTreeComponents, FileTreeActionsProps, FileTreeButtonProps, FileTreeVersionsProps, FileTreeProps as PackageFileTreeProps, FileTreeViewProps as PackageFileTreeViewProps } from '@hachej/boring-ui-kit/file-tree-view';\nimport { useRef } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';\nimport { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty';\nimport { Input } from '@/components/ui/input';\nimport { Progress } from '@/components/ui/progress';\nimport { Separator } from '@/components/ui/separator';\nimport { SidebarMenu, SidebarMenuItem, SidebarMenuSub } from '@/components/ui/sidebar';\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';\nimport { VersionHistoryRows, frenchVersionHistoryLabels } from '../version-history/version-history';\nimport { cn } from '../utils/utils';\n\n// Only touch-target layout is added; appearance remains the consumer's shadcn Button.\nfunction LibraryButton({ className, ...props }: FileTreeButtonProps) {\n return <Button {...props} className={cn('max-sm:min-h-11 max-sm:min-w-11 pointer-coarse:min-h-11 pointer-coarse:min-w-11', className)} />;\n}\n\nfunction FileActions({ label, icon, items }: FileTreeActionsProps) {\n const trigger = useRef<HTMLButtonElement>(null);\n const portal = useRef<HTMLElement | null>(null);\n return <DropdownMenu>\n <DropdownMenuTrigger ref={trigger} render={<LibraryButton type=\"button\" variant=\"ghost\" size=\"icon\" ref={node => { portal.current = node?.ownerDocument.body ?? null; }} />} data-file-more aria-label={label}>{icon}</DropdownMenuTrigger>\n <DropdownMenuPortal container={portal}>\n <DropdownMenuContent data-boring=\"file-actions\" align=\"end\" finalFocus={trigger} className=\"w-max max-w-(--available-width)\">\n {items.map(item => <DropdownMenuItem key={item.id} data-action={item.id} onClick={item.onSelect} className=\"max-sm:min-h-11 pointer-coarse:min-h-11\">{item.icon}{item.label}</DropdownMenuItem>)}\n </DropdownMenuContent>\n </DropdownMenuPortal>\n </DropdownMenu>;\n}\n\nfunction FileVersions({ versions, current, onSelect, locale, earlierVersion }: FileTreeVersionsProps) {\n return <VersionHistoryRows versions={versions.map(version => ({ id: version.revision, label: earlierVersion, savedAt: version.savedAt, ...(version.source ? { source: version.source } : {}) }))}\n current={current} onSelect={onSelect} testId=\"file-history-versions\" {...(locale === 'fr' ? { labels: frenchVersionHistoryLabels } : {})} />;\n}\n\nfunction FileTreeEmpty({ icon, title, description, className, children }: Parameters<FileTreeComponents['Empty']>[0]) {\n return <Empty className={className}>\n <EmptyHeader>\n {icon && <EmptyMedia variant=\"icon\">{icon}</EmptyMedia>}\n <EmptyTitle>{title}</EmptyTitle>\n {description && <EmptyDescription>{description}</EmptyDescription>}\n </EmptyHeader>\n {children && <EmptyContent>{children}</EmptyContent>}\n </Empty>;\n}\n\nexport const fileTreeComponents = {\n Button: LibraryButton, Actions: FileActions, Versions: FileVersions,\n Menu: SidebarMenu, MenuItem: SidebarMenuItem, MenuSub: SidebarMenuSub, Input, Separator: () => <Separator />,\n Collapsible, CollapsibleTrigger, CollapsibleContent, Empty: FileTreeEmpty,\n Progress: ({ value, 'aria-label': label }: { readonly value: number | null; readonly 'aria-label': string }) => <Progress value={value} aria-label={label} />,\n} satisfies FileTreeComponents;\nexport type FileTreeProps = Omit<PackageFileTreeProps, 'components'>;\nexport type FileTreeViewProps = Omit<PackageFileTreeViewProps, 'components'>;\n\nexport function FileTree(props: FileTreeProps) {\n return <PackageFileTree {...props} components={fileTreeComponents} />;\n}\nexport function FileTreeView(props: FileTreeViewProps) {\n return <PackageFileTreeView {...props} components={fileTreeComponents} />;\n}\nexport { createFileTreeController } from '@hachej/boring-ui-kit/file-tree';\nexport type { FileTreeController } from '@hachej/boring-ui-kit/file-tree';\n",
28
29
  "type": "registry:component",
29
30
  "target": "components/file-tree/file-tree.tsx"
30
31
  }
@@ -4,13 +4,13 @@
4
4
  "title": "HTML source and passive preview",
5
5
  "description": "Exact HTML source editing with a reconstructed passive preview and borrowed controller. Browser security qualification remains separate.",
6
6
  "dependencies": [
7
- "@hachej/boring-ui-kit@^0.4.1",
8
- "@hachej/boring-files@^0.4.1",
7
+ "@hachej/boring-ui-kit@^0.4.3",
8
+ "@hachej/boring-files@^0.4.3",
9
9
  "react@^19.0.0",
10
10
  "react-dom@^19.0.0"
11
11
  ],
12
12
  "registryDependencies": [
13
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.1/r/button.json"
13
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.3/r/button.json"
14
14
  ],
15
15
  "files": [
16
16
  {
@@ -0,0 +1,87 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "maquette-viewer",
4
+ "title": "Maquette viewer",
5
+ "description": "MaquetteViewer and maquetteKind: a PM agent's *.maquette.json mock-up (format v1, versioned) one screen at a time, drawn from the host's shadcn components, under one thin icon bar: Back, previous and next (also arrow keys and swipe), an optional comment icon (point mode of the feedback item: a note pinned on a block names the maquette path, screen id and block id) and a menu with guided tours and How it works (the workflow, inline or a linked *.diagram.json, drawn by the diagram-viewer item in a dialog). Optional phone or desktop frame and #screen=<id> deep links. The pi-app artifact panel shows .maquette.json files with it by default; app artifactKinds win.",
6
+ "dependencies": [
7
+ "react@^19.0.0",
8
+ "lucide-react@^1.51.0"
9
+ ],
10
+ "registryDependencies": [
11
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.3/r/theme.json",
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.3/r/diagram-viewer.json",
13
+ "alert",
14
+ "badge",
15
+ "bubble",
16
+ "button",
17
+ "card",
18
+ "empty",
19
+ "input",
20
+ "item",
21
+ "label",
22
+ "native-select",
23
+ "separator",
24
+ "table",
25
+ "dialog",
26
+ "dropdown-menu",
27
+ "popover",
28
+ "toggle",
29
+ "tooltip"
30
+ ],
31
+ "files": [
32
+ {
33
+ "path": "registry/maquette-viewer/maquette.ts",
34
+ "content": "/*\n * The maquette format: a lo-fi mock-up a PM agent writes as `<name>.maquette.json` (screens built from a small catalogue of blocks, goto\n * links between them, an optional workflow and its cases). Plain data drawn by `MaquetteViewer`: nothing in it is built or runs.\n * The format is documented in registry/README.md (\"Maquette viewer\"). Model output is untrusted: `parseMaquette` reads it defensively,\n * keeps what it understands, bounds every list and string, and never throws. The workflow is a diagram (`parseDiagram` of the\n * diagram-viewer item reads it), written inline or linked as a `*.diagram.json` path.\n */\nimport { parseDiagram } from '../diagram-viewer/diagram';\nimport type { Diagram, DiagramEdge, DiagramNode } from '../diagram-viewer/diagram';\n\n/** The format version this viewer writes and reads. A file without `version` is read as version 1. */\nexport const MAQUETTE_FORMAT_VERSION = 1;\n\n/** A block as written in format v1. `type` picks the drawing; the other fields are read by the types that use them. */\nexport interface MaquetteBlockV1 {\n readonly type: string;\n /** A stable name for the block, used when someone comments on it; its position (`2`, `2.0` for a child) otherwise. */\n readonly id?: string;\n /** The block's words (`text` and `title` are accepted as aliases). */\n readonly label?: string;\n readonly text?: string;\n readonly title?: string;\n /** The screen id a click on this block opens (buttons, links, list items, placeholders). */\n readonly goto?: string;\n /** A primary button. */\n readonly primary?: boolean;\n /** Nested blocks of `card`, `row`, `stack` (`blocks` is accepted as an alias). */\n readonly children?: readonly MaquetteBlockV1[];\n readonly blocks?: readonly MaquetteBlockV1[];\n /** `list` rows, or `select` options. */\n readonly items?: readonly string[];\n readonly options?: readonly string[];\n /** `table` header and rows. */\n readonly columns?: readonly string[];\n readonly rows?: readonly (readonly string[])[];\n /** `chat` messages; `from: \"user\"` (or `person`, `me`, `moi`) is the person's side. */\n readonly messages?: readonly { readonly from?: string; readonly text?: string }[];\n /** `document` lines. */\n readonly lines?: readonly string[];\n /** `input`, `select` and `chat` hint and value. */\n readonly placeholder?: string;\n readonly value?: string;\n /** A placeholder's note under its name. */\n readonly note?: string;\n}\n\n/** A maquette file as written in format v1. */\nexport interface MaquetteFileV1 {\n /** Absent means 1. */\n readonly version?: 1;\n readonly title?: string;\n /** The screen id play mode opens first; the first screen otherwise. */\n readonly start?: string;\n /** The frame screens are drawn in: a light phone or desktop frame. Absent: the panel's width. */\n readonly device?: 'phone' | 'desktop';\n /** Screens by id, in the order the stepper walks them. `note` says why the screen exists and what the person does there. */\n readonly screens: Readonly<Record<string, { readonly title?: string; readonly note?: string; readonly blocks: readonly MaquetteBlockV1[] }>>;\n /** The app's navigation bar, shown above every screen. */\n readonly nav?: readonly { readonly label: string; readonly goto: string }[];\n /**\n * The workflow understood: states and the transitions between them (`on` is accepted as an alias of `label`). It is a diagram (format v1\n * of `*.diagram.json`: `nodes`/`edges` are the same as `states`/`transitions`, plus optional `groups`, `path` and `note`), or the path of a\n * `*.diagram.json` file, relative to the maquette's folder.\n */\n readonly workflow?: string | {\n readonly states?: readonly ({ readonly id: string; readonly label?: string; readonly group?: string } | string)[];\n readonly transitions?: readonly { readonly from: string; readonly to: string; readonly label?: string; readonly on?: string; readonly by?: string }[];\n readonly nodes?: readonly ({ readonly id: string; readonly label?: string; readonly group?: string } | string)[];\n readonly edges?: readonly { readonly from: string; readonly to: string; readonly label?: string; readonly on?: string; readonly by?: string }[];\n readonly groups?: readonly ({ readonly id: string; readonly label?: string } | string)[];\n readonly path?: readonly string[];\n readonly note?: string;\n };\n /**\n * Cases. `steps` makes a guided tour: its screens in order, each with a caption (\"Camille receives the invoice and approves it\").\n * `path` (workflow state ids) highlights the case in the workflow diagram. A case may carry both.\n */\n readonly cases?: readonly { readonly name: string; readonly steps?: readonly { readonly screen: string; readonly caption?: string }[]; readonly path?: readonly string[] }[];\n}\n\n/** A block as the viewer draws it: every field read, bounded and defaulted. */\nexport interface MaquetteBlock {\n readonly type: string;\n /** The block's `id`, or its position among the screen's blocks (`2`, `2.0` for the first child of the third block). */\n readonly id: string;\n readonly label: string;\n readonly goto: string | undefined;\n readonly primary: boolean;\n readonly children: readonly MaquetteBlock[];\n readonly items: readonly string[];\n readonly columns: readonly string[];\n readonly rows: readonly (readonly string[])[];\n readonly messages: readonly { readonly mine: boolean; readonly text: string }[];\n readonly lines: readonly string[];\n readonly placeholder: string;\n readonly value: string;\n readonly note: string;\n}\nexport interface MaquetteScreen { readonly id: string; readonly title: string; readonly note: string; readonly blocks: readonly MaquetteBlock[] }\nexport type MaquetteState = DiagramNode;\nexport type MaquetteTransition = DiagramEdge;\nexport interface MaquetteWorkflow { readonly states: readonly MaquetteState[]; readonly transitions: readonly MaquetteTransition[] }\nexport interface MaquetteCase { readonly name: string; readonly steps: readonly { readonly screen: string; readonly caption: string }[]; readonly path: readonly string[] }\n/** A maquette ready to draw. */\nexport interface Maquette {\n readonly title: string;\n readonly start: string;\n readonly device: 'phone' | 'desktop' | undefined;\n readonly screens: readonly MaquetteScreen[];\n readonly nav: readonly { readonly label: string; readonly goto: string }[];\n readonly workflow: MaquetteWorkflow | null;\n /** The inline workflow as a diagram (with its groups, path and note), for `DiagramView`. */\n readonly diagram: Diagram | null;\n /** The `*.diagram.json` path the workflow links to, as written (relative to the maquette's folder). */\n readonly diagramLink: string | undefined;\n readonly cases: readonly MaquetteCase[];\n}\n\n/**\n * The outcome of reading a file. `version` is the file's declared version (1 when absent); a version above `MAQUETTE_FORMAT_VERSION`\n * is read as v1, best effort. `invalid` is not JSON (`json`) or has no screen to show (`shape`).\n */\nexport type MaquetteParse =\n | { readonly kind: 'ok'; readonly maquette: Maquette; readonly version: number }\n | { readonly kind: 'invalid'; readonly reason: 'json' | 'shape'; readonly version: number | undefined };\n\nconst MINE = new Set(['user', 'person', 'me', 'moi']);\nconst str = (value: unknown, max = 400): string => typeof value === 'string' ? value.slice(0, max) : typeof value === 'number' && Number.isFinite(value) ? String(value) : '';\nconst arr = (value: unknown, max = 60): readonly unknown[] => Array.isArray(value) ? value.slice(0, max) : [];\nconst obj = (value: unknown): Record<string, unknown> => value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};\nconst strings = (value: unknown, max: number, length = 120): string[] => arr(value, max).map(item => str(item, length));\n\nfunction block(value: unknown, depth: number, at: string): MaquetteBlock | null {\n const raw = obj(value);\n const type = str(raw['type'], 40).trim().toLowerCase();\n if (!type || depth > 6) return null;\n const goto = str(raw['goto'], 60);\n return {\n type, id: str(raw['id'], 60).trim() || at, label: str(raw['label'] ?? raw['text'] ?? raw['title']), goto: goto || undefined, primary: raw['primary'] === true,\n children: arr(raw['children'] ?? raw['blocks'], 40).map((child, index) => block(child, depth + 1, `${at}.${index}`)).filter((child): child is MaquetteBlock => child !== null),\n items: strings(raw['items'] ?? raw['options'], 12),\n columns: strings(raw['columns'], 8, 40),\n rows: arr(raw['rows'], 12).map(row => strings(row, 8, 80)),\n messages: arr(raw['messages'], 10).map(item => { const message = obj(item); return { mine: MINE.has(str(message['from'], 20).toLowerCase()), text: str(message['text'], 600) }; }).filter(message => message.text),\n lines: strings(raw['lines'], 16, 300),\n placeholder: str(raw['placeholder'], 120), value: str(raw['value'], 120), note: str(raw['note'], 200),\n };\n}\n\n/** Reads a maquette (its JSON text or an already parsed value). Never throws. */\nexport function parseMaquette(input: unknown): MaquetteParse {\n let raw: unknown = input;\n if (typeof raw === 'string') {\n try { raw = JSON.parse(raw); } catch { return { kind: 'invalid', reason: 'json', version: undefined }; }\n }\n const spec = obj(raw);\n const declared = spec['version'];\n const version = declared === undefined ? 1 : typeof declared === 'number' && Number.isSafeInteger(declared) && declared >= 1 ? declared : undefined;\n if (version === undefined) return { kind: 'invalid', reason: 'shape', version: undefined };\n const screens = Object.entries(obj(spec['screens'])).slice(0, 30).map(([id, value]): MaquetteScreen => {\n const screen = obj(value);\n return { id: id.slice(0, 60), title: str(screen['title'], 120) || id.slice(0, 60), note: str(screen['note'], 300),\n blocks: arr(screen['blocks'] ?? screen['children']).map((item, index) => block(item, 0, String(index))).filter((item): item is MaquetteBlock => item !== null) };\n });\n if (!screens.length) return { kind: 'invalid', reason: 'shape', version };\n const ids = new Set(screens.map(screen => screen.id));\n const link = typeof spec['workflow'] === 'string' ? str(spec['workflow'], 200).trim() : '';\n const flow = link ? undefined : parseDiagram(obj(spec['workflow']));\n const diagram = flow?.kind === 'ok' ? flow.diagram : null;\n // A linked diagram is read later: its node ids are not known yet, so a case's path is kept as written.\n const known = (id: string) => link ? true : !!diagram?.nodes.some(node => node.id === id);\n const start = str(spec['start'], 60), device = spec['device'];\n return { kind: 'ok', version, maquette: {\n title: str(spec['title'], 120), start: ids.has(start) ? start : screens[0]!.id, device: device === 'phone' || device === 'desktop' ? device : undefined, screens,\n nav: arr(spec['nav'], 8).map(value => { const item = obj(value); return { label: str(item['label'], 40), goto: str(item['goto'], 60) }; }).filter(item => item.label && ids.has(item.goto)),\n workflow: diagram ? { states: diagram.nodes, transitions: diagram.edges } : null, diagram, diagramLink: isDiagramLink(link) ? link : undefined,\n cases: arr(spec['cases'], 10).map((value): MaquetteCase => {\n const item = obj(value);\n const steps = arr(item['steps'], 20).map(step => { const raw = obj(step); return { screen: str(raw['screen'], 60), caption: str(raw['caption'], 300) }; }).filter(step => ids.has(step.screen));\n return { name: str(item['name'], 80), steps, path: strings(item['path'], 20, 60).filter(known) };\n }).filter(item => item.name && (item.steps.length > 0 || item.path.length > 0)),\n } };\n}\n\n/** Every screen id a block tree links to, in order, without repeats. */\nexport function linksOf(blocks: readonly MaquetteBlock[]): string[] {\n const found = new Set<string>();\n const walk = (list: readonly MaquetteBlock[]) => { for (const item of list) { if (item.goto) found.add(item.goto); walk(item.children); } };\n walk(blocks);\n return [...found];\n}\n\n/** A workflow link the viewer follows: a relative `*.diagram.json` path (no scheme, no leading slash, no `..`). */\nconst isDiagramLink = (link: string): boolean => /\\.diagram\\.json$/i.test(link) && !/^[a-z][a-z0-9+.-]*:|^\\/|(^|\\/)\\.\\.(\\/|$)/i.test(link);\n\n/** The path of a linked diagram: `link` resolved against the folder of the maquette at `path`. */\nexport const diagramPathOf = (path: string, link: string): string => `${path.includes('/') ? path.slice(0, path.lastIndexOf('/') + 1) : ''}${link.replace(/^\\.\\//, '')}`;\n\n/** True for a path the built-in maquette kind draws: a `.maquette.json` file. */\nexport const isMaquettePath = (path: string): boolean => /\\.maquette\\.json$/i.test(path);\n\n/** The screen id a URL fragment names (`#screen=<id>`, alone or among other `&`-separated keys), or undefined. */\nexport function screenFromFragment(fragment: string | undefined): string | undefined {\n if (!fragment) return undefined;\n try { return new URLSearchParams(fragment.replace(/^#/, '')).get('screen') ?? undefined; } catch { return undefined; }\n}\n",
35
+ "type": "registry:lib",
36
+ "target": "components/maquette-viewer/maquette.ts"
37
+ },
38
+ {
39
+ "path": "registry/maquette-viewer/maquette-labels.ts",
40
+ "content": "/* The maquette viewer's words in English and French. `locale` picks the base set; a host's `labels` still override any of them. */\n\nexport type MaquetteLocale = 'en' | 'fr';\n\nexport const defaultMaquetteLabels = {\n /** The kind's name on the artifact card and in the panel bar. */\n kind: 'Mock-up',\n screens: 'Screens',\n previous: 'Previous screen',\n next: 'Next screen',\n back: 'Back',\n more: 'More',\n appNavigation: 'App navigation',\n howItWorks: 'How it works',\n comment: 'Comment on this screen',\n loading: 'Loading…',\n diagramUnavailable: 'The linked diagram cannot be read',\n endTour: 'End tour',\n aboutScreen: 'About this screen',\n opens: (title: string) => `Opens “${title}”`,\n tourStep: (index: number, count: number) => `Step ${index} of ${count}`,\n transitionBy: (by: string) => `by ${by}`,\n unknownBlock: 'Not drawn in this mock-up',\n showing: (title: string) => `Showing ${title}`,\n newerTitle: 'Made with a newer version',\n newer: (version: number) => `This mock-up uses format version ${version}. This viewer reads version 1: parts it does not know may be missing.`,\n errorTitle: 'This mock-up cannot be shown',\n invalidJson: 'The file is not valid JSON. Ask the PM to present it again.',\n invalidShape: 'The file has no screen to show (a mock-up needs \"screens\": { \"<id>\": { \"title\", \"blocks\" } }).',\n};\nexport type MaquetteLabels = typeof defaultMaquetteLabels;\n\n/** Typed as `MaquetteLabels`, so a key added in English must be added here too. */\nexport const frenchMaquetteLabels: MaquetteLabels = {\n kind: 'Maquette',\n screens: 'Écrans',\n previous: 'Écran précédent',\n next: 'Écran suivant',\n back: 'Retour',\n more: 'Plus',\n appNavigation: 'Navigation de l’application',\n howItWorks: 'Comment ça marche',\n comment: 'Commenter cet écran',\n loading: 'Chargement…',\n diagramUnavailable: 'Le diagramme lié ne peut pas être lu',\n endTour: 'Terminer la visite',\n aboutScreen: 'À propos de cet écran',\n opens: title => `Ouvre « ${title} »`,\n tourStep: (index, count) => `Étape ${index} sur ${count}`,\n transitionBy: by => `par ${by}`,\n unknownBlock: 'Pas dessiné dans cette maquette',\n showing: title => `Écran affiché : ${title}`,\n newerTitle: 'Faite avec une version plus récente',\n newer: version => `Cette maquette utilise la version ${version} du format. Cet afficheur lit la version 1 : ce qu’il ne connaît pas peut manquer.`,\n errorTitle: 'Cette maquette ne peut pas être affichée',\n invalidJson: 'Le fichier n’est pas du JSON valide. Demandez au PM de la présenter à nouveau.',\n invalidShape: 'Le fichier n’a aucun écran à montrer (une maquette a besoin de « screens » : { \"<id>\": { \"title\", \"blocks\" } }).',\n};\n\n/** The viewer's words for `locale` (English by default) with the host's overrides. */\nexport const maquetteLabelsFor = (locale: MaquetteLocale | undefined, labels?: Partial<MaquetteLabels>): MaquetteLabels =>\n ({ ...(locale === 'fr' ? frenchMaquetteLabels : defaultMaquetteLabels), ...labels });\n",
41
+ "type": "registry:lib",
42
+ "target": "components/maquette-viewer/maquette-labels.ts"
43
+ },
44
+ {
45
+ "path": "registry/maquette-viewer/maquette-blocks.tsx",
46
+ "content": "'use client';\n\n// One maquette screen drawn from the host's own shadcn components: each catalogue type is a real Button, Input, NativeSelect, Badge, Table,\n// Item, Card or Bubble, so a mock-up looks like the app it announces. A type the catalogue does not know becomes a named placeholder\n// (a grey dashed Empty). Blocks with a goto open their screen; the item's CSS gives them a soft ring on hover and focus. Each block sits\n// in a box marked `data-feedback-id` (the maquette path, screen id and block id), so a note pinned on it with the feedback item's point mode\n// says exactly which block it is about; the screen is a `data-feedback-visible` region, so the block's words go with the note.\nimport type { ReactNode, Ref } from 'react';\nimport { BoxIcon } from 'lucide-react';\nimport { Badge } from '@/components/ui/badge';\nimport { Bubble, BubbleContent } from '@/components/ui/bubble';\nimport { Button } from '@/components/ui/button';\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty';\nimport { Input } from '@/components/ui/input';\nimport { Item, ItemContent, ItemGroup, ItemTitle } from '@/components/ui/item';\nimport { Label } from '@/components/ui/label';\nimport { NativeSelect, NativeSelectOption } from '@/components/ui/native-select';\nimport { Separator } from '@/components/ui/separator';\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';\nimport type { Maquette, MaquetteBlock, MaquetteScreen } from './maquette';\nimport type { MaquetteLabels } from './maquette-labels';\n\n/** Opens a screen by id. */\nexport type MaquetteGo = (screen: string) => void;\n/**\n * How a block reaches other screens: `go` opens one, `hint` names where a goto leads (the button's tooltip). `target` is a block's\n * `data-feedback-id` and `pins` the numbers of the comment pins on it (none while point mode draws its own).\n */\nexport interface MaquetteLinks {\n readonly go: MaquetteGo;\n readonly hint: (screen: string) => string;\n readonly target: (block: MaquetteBlock) => string;\n readonly pins: (block: MaquetteBlock) => readonly number[];\n}\n\nconst SKELETON = [92, 100, 64];\n\nfunction Placeholder({ block, links, labels }: { readonly block: MaquetteBlock; readonly links: MaquetteLinks; readonly labels: MaquetteLabels }) {\n return <div data-testid=\"maquette-placeholder\" data-type={block.type}>\n <Empty data-maquette=\"placeholder\">\n <EmptyHeader>\n <EmptyMedia variant=\"icon\"><BoxIcon aria-hidden=\"true\" /></EmptyMedia>\n <EmptyTitle>{block.label || block.type}</EmptyTitle>\n <EmptyDescription>{block.note || labels.unknownBlock}</EmptyDescription>\n </EmptyHeader>\n {block.goto && <EmptyContent><GotoButton block={block} links={links} label={block.label || block.type} /></EmptyContent>}\n </Empty>\n </div>;\n}\n\n// A block with a goto opens its screen (its tooltip names it); one without is drawn but does nothing.\nfunction GotoButton({ block, links, label }: { readonly block: MaquetteBlock; readonly links: MaquetteLinks; readonly label: string }) {\n const target = block.goto;\n return <Button type=\"button\" variant={block.type === 'link' ? 'link' : block.primary ? 'default' : 'outline'} className=\"w-fit max-w-full pointer-coarse:min-h-11\"\n data-testid={target ? 'maquette-goto' : 'maquette-button'} data-goto={target} title={target ? links.hint(target) : undefined} onClick={() => { if (target) links.go(target); }}>\n <span className=\"truncate\">{label || '…'}</span>\n </Button>;\n}\n\n/** One block and its children, in its marked box with its comment pins. */\nexport function MaquetteBlockView({ block, links, labels }: { readonly block: MaquetteBlock; readonly links: MaquetteLinks; readonly labels: MaquetteLabels }): ReactNode {\n const pins = links.pins(block);\n return <div data-testid=\"maquette-block\" data-block={block.id} data-feedback-id={links.target(block)} className=\"relative min-w-0\">\n <BlockBody block={block} links={links} labels={labels} />\n {pins.length > 0 && <Badge data-testid=\"maquette-pin\" data-feedback-ignore=\"\" aria-hidden=\"true\" className=\"absolute -top-2 -left-2 tabular-nums\">{pins.join(' ')}</Badge>}\n </div>;\n}\n\nfunction BlockBody({ block, links, labels }: { readonly block: MaquetteBlock; readonly links: MaquetteLinks; readonly labels: MaquetteLabels }): ReactNode {\n const children = block.children.map((child, index) => <MaquetteBlockView key={index} block={child} links={links} labels={labels} />);\n switch (block.type) {\n case 'heading': return <h4 className=\"m-0 text-base font-semibold text-balance\">{block.label}</h4>;\n case 'text': return <p className=\"m-0 text-sm text-pretty text-muted-foreground\">{block.label}</p>;\n case 'button': case 'link': return <GotoButton block={block} links={links} label={block.label} />;\n case 'input': return <Label className=\"flex-col items-stretch gap-1.5\">{block.label}\n <Input className=\"pointer-coarse:min-h-11\" placeholder={block.placeholder} defaultValue={block.value} /></Label>;\n case 'select': return <Label className=\"flex-col items-stretch gap-1.5\">{block.label}\n <NativeSelect className=\"w-full pointer-coarse:[&_select]:min-h-11\" defaultValue={block.value}>\n {(block.items.length ? block.items : [block.value || block.placeholder || '…']).map((option, index) => <NativeSelectOption key={index} value={option}>{option}</NativeSelectOption>)}\n </NativeSelect></Label>;\n case 'badge': return <Badge variant=\"secondary\">{block.label}</Badge>;\n case 'list': return <ItemGroup className=\"gap-2\">\n {block.label && <p className=\"m-0 text-sm font-medium\">{block.label}</p>}\n {block.items.map((item, index) => <Item key={index} variant=\"outline\" size=\"sm\"><ItemContent><ItemTitle>{item}</ItemTitle></ItemContent></Item>)}\n </ItemGroup>;\n case 'table': return <div className=\"min-w-0 overflow-x-auto\"><Table>\n <TableHeader><TableRow>{block.columns.map((column, index) => <TableHead key={index}>{column}</TableHead>)}</TableRow></TableHeader>\n <TableBody>{block.rows.map((row, index) => <TableRow key={index}>{row.map((cell, column) => <TableCell key={column}>{cell}</TableCell>)}</TableRow>)}</TableBody>\n </Table></div>;\n case 'card': return <Card size=\"sm\">\n {block.label && <CardHeader><CardTitle>{block.label}</CardTitle></CardHeader>}\n {children.length > 0 && <CardContent className=\"flex flex-col gap-3\">{children}</CardContent>}\n </Card>;\n case 'row': return <div className=\"flex flex-wrap items-start gap-3 *:min-w-40 *:flex-1\">{children}</div>;\n case 'stack': return <div className=\"flex flex-col gap-3\">{children}</div>;\n case 'chat': return <Card size=\"sm\">\n {block.label && <CardHeader><CardTitle>{block.label}</CardTitle></CardHeader>}\n <CardContent className=\"flex flex-col gap-2\">\n {block.messages.map((message, index) => <Bubble key={index} align={message.mine ? 'end' : 'start'} variant={message.mine ? 'default' : 'muted'}><BubbleContent>{message.text}</BubbleContent></Bubble>)}\n <Input className=\"pointer-coarse:min-h-11\" placeholder={block.placeholder || '…'} aria-label={block.placeholder || block.label || 'chat'} />\n {children}\n </CardContent>\n </Card>;\n case 'document': return <Card size=\"sm\">\n {block.label && <CardHeader><CardTitle>{block.label}</CardTitle></CardHeader>}\n <CardContent className=\"flex flex-col gap-2\">\n {block.lines.length ? block.lines.map((line, index) => <p key={index} className=\"m-0 text-sm text-muted-foreground\">{line}</p>)\n : SKELETON.map(width => <span key={width} aria-hidden=\"true\" className=\"block h-2 bg-muted\" style={{ width: `${width}%` }} />)}\n {children}\n </CardContent>\n </Card>;\n case 'placeholder': default: return <Placeholder block={block} links={links} labels={labels} />;\n }\n}\n\n/** A screen as the app would show it: the app's navigation bar (when the maquette has one), then its blocks. Its name is for screen readers and focus. */\nexport function MaquetteScreenView({ maquette, screen, links, labels, headingRef }: {\n readonly maquette: Maquette; readonly screen: MaquetteScreen; readonly links: MaquetteLinks; readonly labels: MaquetteLabels;\n readonly headingRef?: Ref<HTMLHeadingElement> | undefined;\n}) {\n return <div data-testid=\"maquette-screen\" data-screen={screen.id} data-feedback-visible=\"\" className=\"flex min-w-0 flex-col gap-4\">\n {maquette.nav.length > 0 && <>\n <nav aria-label={labels.appNavigation} className=\"flex flex-wrap gap-1\">\n {maquette.nav.map(item => <Button key={item.goto} type=\"button\" size=\"sm\" variant={item.goto === screen.id ? 'secondary' : 'ghost'} className=\"pointer-coarse:min-h-11\"\n aria-current={item.goto === screen.id ? 'page' : undefined} data-testid=\"maquette-nav\" title={links.hint(item.goto)} onClick={() => links.go(item.goto)}>{item.label}</Button>)}\n </nav>\n <Separator />\n </>}\n <h3 ref={headingRef} tabIndex={-1} data-testid=\"maquette-screen-title\" className=\"sr-only\">{screen.title}</h3>\n {screen.blocks.map((block, index) => <MaquetteBlockView key={index} block={block} links={links} labels={labels} />)}\n </div>;\n}\n",
47
+ "type": "registry:component",
48
+ "target": "components/maquette-viewer/maquette-blocks.tsx"
49
+ },
50
+ {
51
+ "path": "registry/maquette-viewer/maquette-viewer.tsx",
52
+ "content": "'use client';\n\n// The maquette viewer: a PM agent's `*.maquette.json` mock-up, one screen at a time. The screen is the content; the chrome is one thin bar\n// of icons: Back, ‹ 2 / 5 › (also arrow keys and swipe) and a menu holding the guided tours and \"How it works\" (the workflow, in a dialog).\n// During a tour the bar shows the step dots and ×, with the step's caption under it. `maquetteKind` is the kind the pi-app artifact panel\n// uses by default for `.maquette.json` files; an app's own kind in `artifactKinds` wins. It draws data only: nothing in the file runs.\n// The workflow is drawn by the diagram-viewer item's `DiagramView`, inline or from a linked `*.diagram.json`. With `comments` (the feedback\n// item's `usePointComments`, structurally) one more icon enters point mode on the screen: a tap on a block pins a note that names the\n// maquette path, the screen id and the block id; the pins of the current screen show as small numbers on their blocks.\nimport { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';\nimport type { ComponentType, KeyboardEvent, ReactNode, TouchEvent } from 'react';\nimport { ArrowLeftIcon, ChevronLeftIcon, ChevronRightIcon, CompassIcon, EllipsisIcon, InfoIcon, LayoutTemplateIcon, MessageSquarePlusIcon, TriangleAlertIcon, WorkflowIcon, XIcon } from 'lucide-react';\nimport { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';\nimport { Button } from '@/components/ui/button';\nimport { Card, CardContent } from '@/components/ui/card';\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';\nimport { Toggle } from '@/components/ui/toggle';\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';\nimport { parseDiagram } from '../diagram-viewer/diagram';\nimport type { Diagram } from '../diagram-viewer/diagram';\nimport { DiagramView } from '../diagram-viewer/diagram-view';\nimport { MAQUETTE_FORMAT_VERSION, diagramPathOf, isMaquettePath, parseMaquette, screenFromFragment } from './maquette';\nimport type { Maquette, MaquetteBlock, MaquetteCase, MaquetteScreen } from './maquette';\nimport { maquetteLabelsFor } from './maquette-labels';\nimport type { MaquetteLabels, MaquetteLocale } from './maquette-labels';\nimport { MaquetteScreenView } from './maquette-blocks';\nimport type { MaquetteLinks } from './maquette-blocks';\n\nexport { MAQUETTE_FORMAT_VERSION, diagramPathOf, isMaquettePath, linksOf, parseMaquette, screenFromFragment } from './maquette';\nexport type { Maquette, MaquetteBlock, MaquetteBlockV1, MaquetteCase, MaquetteFileV1, MaquetteParse, MaquetteScreen, MaquetteState, MaquetteTransition, MaquetteWorkflow } from './maquette';\nexport { defaultMaquetteLabels, frenchMaquetteLabels, maquetteLabelsFor } from './maquette-labels';\nexport type { MaquetteLabels, MaquetteLocale } from './maquette-labels';\n\n/**\n * Comment pins, structurally the feedback item's `usePointComments(session)`: pass it and the bar shows a comment icon; omit it (no feedback\n * session in the host) and nothing shows.\n */\nexport interface MaquetteComments {\n /** Point mode is on. */\n readonly active: boolean;\n /** Enters point mode (the person taps blocks to pin notes), or ends it when it is on. */\n readonly point: () => void;\n /** The pins taken: each one's number and the `data-feedback-id` its element sits in. */\n readonly pins: readonly { readonly number: number; readonly target: string }[];\n}\n\n/** Reads another file of the workspace as text (the artifact panel passes one): a linked `*.diagram.json`. Undefined when it cannot. */\nexport type MaquetteRead = (path: string) => Promise<string | undefined>;\n\n/** A block's `data-feedback-id`: `maquette:<path>#screen=<screen id>&block=<block id>`. */\nexport const maquetteTarget = (path: string | undefined, screen: string, block: string): string => `maquette:${path ?? ''}#screen=${screen}&block=${block}`;\n\nexport interface MaquetteViewerProps {\n /** The file's text (JSON). */\n readonly text: string;\n /** The file's path: named by comment pins, and the folder a linked diagram is read from. */\n readonly path?: string | undefined;\n /** Comment pins on blocks (the feedback item's `usePointComments`). */\n readonly comments?: MaquetteComments | undefined;\n /** Reads a linked `*.diagram.json`; without it a linked workflow is not offered. */\n readonly read?: MaquetteRead | undefined;\n /** The page's URL fragment: `screen=<id>` opens that screen (and follows later changes). */\n readonly fragment?: string | undefined;\n /** The base words: English by default. */\n readonly locale?: MaquetteLocale | undefined;\n /** Any word over the locale's. */\n readonly labels?: Partial<MaquetteLabels> | undefined;\n}\n\n/** Where the person is: the screens visited (the last is shown) and the guided tour under way. */\ninterface Walk { readonly history: readonly string[]; readonly tour: { readonly name: string; readonly index: number } | null }\n\n/**\n * A host bar that owns the one \"…\" menu (the pi-app artifact panel) provides this: the viewer then publishes its tours and \"How it works\"\n * there instead of drawing a second \"…\" menu beside the screen counter. Without a provider the viewer keeps its own menu.\n */\nexport interface MaquetteMenuEntry { readonly id: string; readonly label: string; readonly icon: ReactNode; readonly testId: string; readonly separatorBefore: boolean; readonly onSelect: () => void }\nexport const MaquetteMenuContext = createContext<((items: readonly MaquetteMenuEntry[]) => void) | undefined>(undefined);\n\nconst TYPING = 'input, select, textarea, [contenteditable=\"true\"]';\nconst ICON = 'pointer-coarse:min-h-11 pointer-coarse:min-w-11';\n\n/** One dot per tour step, the current one in the primary colour. */\nfunction Dots({ count, index, label }: { readonly count: number; readonly index: number; readonly label: string }) {\n return <svg role=\"img\" aria-label={label} data-testid=\"maquette-dots\" width={count * 12} height=\"8\" viewBox={`0 0 ${count * 12} 8`} className=\"shrink-0\">\n {Array.from({ length: count }, (_, i) => <circle key={i} cx={i * 12 + 6} cy=\"4\" r=\"3\" className={i === index ? 'fill-primary' : 'fill-muted-foreground/30'} />)}\n </svg>;\n}\n\n/** The diagram of a linked `*.diagram.json`, read when the dialog opens: undefined while reading, null when it cannot be read. */\nfunction useLinked(read: MaquetteRead | undefined, path: string | undefined, open: boolean): Diagram | null | undefined {\n const [loaded, setLoaded] = useState<{ readonly path: string; readonly diagram: Diagram | null }>();\n useEffect(() => {\n if (!open || !path || !read || loaded?.path === path) return undefined;\n let off = false;\n read(path).then(text => text === undefined ? null : parseDiagram(text), () => null).then(parsed => {\n if (!off) setLoaded({ path, diagram: parsed?.kind === 'ok' ? parsed.diagram : null });\n });\n return () => { off = true; };\n }, [open, path, read, loaded?.path]);\n return loaded && loaded.path === path ? loaded.diagram : undefined;\n}\n\n/** The workflow diagram, its legend and the cases' `path` highlights, in a dialog opened from the menu. */\nfunction HowItWorks({ maquette, source, labels, open, onOpenChange }: {\n readonly maquette: Maquette; readonly source: DiagramSource; readonly labels: MaquetteLabels; readonly open: boolean; readonly onOpenChange: (open: boolean) => void;\n}) {\n const [shown, setShown] = useState<string | null>(null);\n const loaded = useLinked(source.read, source.path, open);\n const diagram = maquette.diagram ?? loaded;\n const highlighted = maquette.cases.filter(item => item.path.length > 0);\n if (!maquette.diagram && !source.path) return null;\n return <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent data-testid=\"maquette-how\" className=\"max-h-[85dvh] overflow-y-auto\">\n <DialogHeader><DialogTitle>{labels.howItWorks}</DialogTitle></DialogHeader>\n {diagram ? <DiagramView diagram={diagram} highlight={highlighted.find(item => item.name === shown)?.path ?? diagram.path} labels={labels} />\n : diagram === null ? <Alert variant=\"destructive\" data-testid=\"maquette-how-error\"><TriangleAlertIcon aria-hidden=\"true\" /><AlertTitle>{labels.diagramUnavailable}</AlertTitle></Alert>\n : <p role=\"status\" className=\"m-0 text-sm text-muted-foreground\">{labels.loading}</p>}\n {highlighted.length > 0 && <div role=\"group\" className=\"flex flex-wrap gap-2\">\n {highlighted.map(item => <Button key={item.name} type=\"button\" size=\"sm\" variant={shown === item.name ? 'default' : 'outline'} aria-pressed={shown === item.name}\n data-testid=\"maquette-case\" className=\"pointer-coarse:min-h-11\" onClick={() => setShown(current => current === item.name ? null : item.name)}>{item.name}</Button>)}\n </div>}\n </DialogContent>\n </Dialog>;\n}\n\n/** Where a linked diagram is read from: its resolved path and the reader (both absent for an inline workflow, or with no reader). */\ninterface DiagramSource { readonly path: string | undefined; readonly read: MaquetteRead | undefined }\n\nfunction Walkthrough({ maquette, labels, fragment, path, comments, read }: {\n readonly maquette: Maquette; readonly labels: MaquetteLabels; readonly fragment: string | undefined;\n readonly path: string | undefined; readonly comments: MaquetteComments | undefined; readonly read: MaquetteRead | undefined;\n}) {\n const exists = (id: string | undefined): id is string => !!id && maquette.screens.some(screen => screen.id === id);\n const linked = screenFromFragment(fragment);\n const [walk, setWalk] = useState<Walk>(() => ({ history: [exists(linked) ? linked : maquette.start], tour: null }));\n const [how, setHow] = useState(false);\n const seen = useRef(new Set<string>());\n const heading = useRef<HTMLHeadingElement>(null);\n const moved = useRef(false);\n const touch = useRef<{ x: number; y: number } | null>(null);\n const screens = maquette.screens;\n const current: MaquetteScreen = screens.find(screen => screen.id === walk.history.at(-1)) ?? screens[0]!;\n const position = screens.indexOf(current);\n const tour: MaquetteCase | undefined = walk.tour ? maquette.cases.find(item => item.name === walk.tour!.name) : undefined;\n const tours = useMemo(() => maquette.cases.filter(item => item.steps.length > 0), [maquette]);\n const titleOf = (id: string) => screens.find(screen => screen.id === id)?.title ?? id;\n // Goto blocks pulse once, the first time their screen is shown.\n // Decided once per shown screen, so a re-render of the same screen (the host menu updating) does not end the pulse.\n const shown = useRef<{ readonly id: string; readonly first: boolean } | undefined>(undefined);\n const view = shown.current?.id === current.id ? shown.current : { id: current.id, first: !seen.current.has(current.id) };\n shown.current = view;\n const firstView = view.first;\n useEffect(() => { seen.current.add(current.id); }, [current.id]);\n\n const move = (next: Walk) => { moved.current = true; setWalk(next); };\n const go = (id: string) => { if (exists(id) && id !== current.id) move({ history: [...walk.history, id], tour: null }); };\n const step = (delta: number) => {\n if (tour && walk.tour) {\n const index = walk.tour.index + delta, target = tour.steps[index];\n if (target) move({ history: [...walk.history, target.screen], tour: { name: tour.name, index } });\n return;\n }\n const target = screens[position + delta];\n if (target) move({ history: [...walk.history, target.id], tour: null });\n };\n const back = () => { if (walk.history.length > 1) move({ history: walk.history.slice(0, -1), tour: null }); };\n const startTour = (item: MaquetteCase) => move({ history: [...walk.history, item.steps[0]!.screen], tour: { name: item.name, index: 0 } });\n // A later `#screen=<id>` (the PM's \"see screen 2\" link) opens that screen.\n useEffect(() => { if (exists(linked) && linked !== walk.history.at(-1)) setWalk(previous => ({ history: [...previous.history, linked], tour: null })); }, [linked]); // eslint-disable-line react-hooks/exhaustive-deps\n // After a move, keyboard focus starts on the new screen, so Tab continues inside the screen just opened.\n useEffect(() => { if (moved.current) { moved.current = false; heading.current?.focus(); } }, [walk]);\n\n const index = tour && walk.tour ? walk.tour.index : position, count = tour ? tour.steps.length : screens.length;\n const target = (block: MaquetteBlock) => maquetteTarget(path, current.id, block.id);\n // While pointing, the session draws its own numbered pins over the page; the small numbers on the blocks are for afterwards.\n const pins = (block: MaquetteBlock) => comments && !comments.active ? comments.pins.filter(pin => pin.target === target(block)).map(pin => pin.number) : [];\n const links: MaquetteLinks = { go, hint: id => labels.opens(titleOf(id)), target, pins };\n const source: DiagramSource = { path: maquette.diagramLink && read ? diagramPathOf(path ?? '', maquette.diagramLink) : undefined, read };\n const workflow = !!maquette.diagram || !!source.path;\n // Inside a host bar with its own \"…\" menu, the tours and \"How it works\" go there (one menu, not two).\n const publish = useContext(MaquetteMenuContext);\n const latestTour = useRef(startTour); latestTour.current = startTour;\n useEffect(() => {\n if (!publish) return undefined;\n const items: MaquetteMenuEntry[] = [\n ...(tour ? [] : tours).map((item, i) => ({ id: `tour-${i}`, label: item.name, icon: <CompassIcon aria-hidden=\"true\" />, testId: 'maquette-tour', separatorBefore: i === 0, onSelect: () => latestTour.current(item) })),\n ...(workflow && !tour ? [{ id: 'how-it-works', label: labels.howItWorks, icon: <WorkflowIcon aria-hidden=\"true\" />, testId: 'maquette-how-open', separatorBefore: true, onSelect: () => setHow(true) }] : []),\n ];\n publish(items);\n return () => publish([]);\n }, [publish, tours, workflow, tour, labels.howItWorks]);\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.defaultPrevented || event.ctrlKey || event.metaKey || event.shiftKey || (event.target as Element).closest?.(TYPING)) return;\n if (event.altKey) { if (event.key === 'ArrowLeft') { event.preventDefault(); back(); } return; }\n if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') { event.preventDefault(); step(event.key === 'ArrowLeft' ? -1 : 1); }\n };\n const onTouchStart = (event: TouchEvent) => { const point = event.touches[0]; touch.current = point ? { x: point.clientX, y: point.clientY } : null; };\n const onTouchEnd = (event: TouchEvent) => {\n const start = touch.current, point = event.changedTouches[0];\n touch.current = null;\n if (!start || !point) return;\n const dx = point.clientX - start.x, dy = point.clientY - start.y;\n if (Math.abs(dx) > 56 && Math.abs(dx) > Math.abs(dy) * 1.5) step(dx < 0 ? 1 : -1);\n };\n const caption = tour && walk.tour ? tour.steps[walk.tour.index]?.caption : undefined;\n\n return <div data-testid=\"maquette-walk\" data-screen={current.id} data-tour={tour?.name} className=\"flex min-w-0 flex-col gap-2\" onKeyDown={onKeyDown}>\n {/* The viewer's own chrome is never pinned, and keeps working while pointing: walk to another screen and pin there too. */}\n <nav aria-label={labels.screens} data-testid=\"maquette-stepper\" data-feedback-ignore=\"\" className=\"flex items-center gap-1\">\n {walk.history.length > 1 && <Button type=\"button\" variant=\"ghost\" size=\"icon-sm\" className={ICON} aria-label={labels.back} data-testid=\"maquette-back\" onClick={back}><ArrowLeftIcon aria-hidden=\"true\" /></Button>}\n <div className=\"flex flex-1 items-center justify-center gap-1\">\n <Button type=\"button\" variant=\"ghost\" size=\"icon-sm\" className={ICON} aria-label={labels.previous} data-testid=\"maquette-previous\" disabled={index <= 0} onClick={() => step(-1)}><ChevronLeftIcon aria-hidden=\"true\" /></Button>\n {tour ? <Dots count={count} index={index} label={labels.tourStep(index + 1, count)} />\n : <span data-testid=\"maquette-position\" className=\"min-w-10 text-center text-sm text-muted-foreground tabular-nums\">{index + 1} / {count}</span>}\n <Button type=\"button\" variant=\"ghost\" size=\"icon-sm\" className={ICON} aria-label={labels.next} data-testid=\"maquette-next\" disabled={index >= count - 1} onClick={() => step(1)}><ChevronRightIcon aria-hidden=\"true\" /></Button>\n </div>\n {comments && <Tooltip><TooltipTrigger render={<Toggle className={ICON} aria-label={labels.comment} pressed={comments.active}\n data-testid=\"maquette-comment\" onPressedChange={() => comments.point()} />}><MessageSquarePlusIcon aria-hidden=\"true\" /></TooltipTrigger><TooltipContent>{labels.comment}</TooltipContent></Tooltip>}\n {tour ? <Button type=\"button\" variant=\"ghost\" size=\"icon-sm\" className={ICON} aria-label={labels.endTour} data-testid=\"maquette-end-tour\" onClick={() => setWalk(previous => ({ ...previous, tour: null }))}><XIcon aria-hidden=\"true\" /></Button>\n : !publish && (tours.length > 0 || workflow) && <DropdownMenu>\n <DropdownMenuTrigger render={<Button type=\"button\" variant=\"ghost\" size=\"icon-sm\" className={ICON} aria-label={labels.more} data-testid=\"maquette-menu\" />}><EllipsisIcon aria-hidden=\"true\" /></DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" data-testid=\"maquette-menu-list\">\n {tours.map(item => <DropdownMenuItem key={item.name} className=\"pointer-coarse:min-h-11\" data-testid=\"maquette-tour\" onClick={() => startTour(item)}><CompassIcon aria-hidden=\"true\" />{item.name}</DropdownMenuItem>)}\n {tours.length > 0 && workflow && <DropdownMenuSeparator />}\n {workflow && <DropdownMenuItem className=\"pointer-coarse:min-h-11\" data-testid=\"maquette-how-open\" onClick={() => setHow(true)}><WorkflowIcon aria-hidden=\"true\" />{labels.howItWorks}</DropdownMenuItem>}\n </DropdownMenuContent>\n </DropdownMenu>}\n </nav>\n {caption && <p data-testid=\"maquette-caption\" className=\"m-0 line-clamp-2 px-1 text-sm text-pretty text-muted-foreground\">{caption}</p>}\n\n <div className={maquette.device === 'phone' ? 'mx-auto flex w-full max-w-sm flex-col gap-2' : 'flex min-w-0 flex-col gap-2'}>\n <div className=\"min-w-0\" data-hint={firstView ? 'pulse' : undefined} onTouchStart={onTouchStart} onTouchEnd={onTouchEnd}>\n <Card data-testid=\"maquette-frame\" data-device={maquette.device ?? 'panel'}><CardContent>\n <MaquetteScreenView key={current.id} maquette={maquette} screen={current} links={links} labels={labels} headingRef={heading} />\n </CardContent></Card>\n </div>\n {current.note && <div className=\"flex min-w-0 items-center gap-1 px-1\">\n <p data-testid=\"maquette-note\" title={current.note} className=\"m-0 min-w-0 flex-1 truncate text-xs text-muted-foreground\">{current.note}</p>\n <Popover>\n <PopoverTrigger render={<Button type=\"button\" variant=\"ghost\" size=\"icon-xs\" className={ICON} aria-label={labels.aboutScreen} data-testid=\"maquette-note-more\" />}><InfoIcon aria-hidden=\"true\" /></PopoverTrigger>\n <PopoverContent align=\"end\" data-testid=\"maquette-note-full\">{current.note}</PopoverContent>\n </Popover>\n </div>}\n </div>\n <p aria-live=\"polite\" className=\"m-0 sr-only\">{labels.showing(current.title)}</p>\n <HowItWorks maquette={maquette} source={source} labels={labels} open={how} onOpenChange={setHow} />\n </div>;\n}\n\n/** A maquette file drawn one screen at a time; a newer format version shows a notice, an unreadable file an inline error. */\nexport function MaquetteViewer({ text, fragment, locale, labels: given, path, comments, read }: MaquetteViewerProps) {\n const labels = useMemo(() => maquetteLabelsFor(locale, given), [locale, given]);\n const parsed = useMemo(() => parseMaquette(text), [text]);\n const newer = parsed.version !== undefined && parsed.version > MAQUETTE_FORMAT_VERSION;\n const notice = newer && <Alert data-testid=\"maquette-newer\"><InfoIcon aria-hidden=\"true\" /><AlertTitle>{labels.newerTitle}</AlertTitle><AlertDescription>{labels.newer(parsed.version!)}</AlertDescription></Alert>;\n return <section data-boring=\"maquette-viewer\" data-state={parsed.kind} aria-label={parsed.kind === 'ok' ? parsed.maquette.title || labels.kind : labels.kind}\n className=\"mx-auto flex w-full max-w-5xl min-w-0 flex-col gap-2 px-3 pt-1 pb-4 sm:px-6\">\n {notice}\n {parsed.kind === 'ok' ? <Walkthrough maquette={parsed.maquette} labels={labels} fragment={fragment} path={path} comments={comments} read={read} />\n : <Alert variant=\"destructive\" data-testid=\"maquette-error\" data-reason={parsed.reason}><TriangleAlertIcon aria-hidden=\"true\" /><AlertTitle>{labels.errorTitle}</AlertTitle>\n <AlertDescription>{parsed.reason === 'json' ? labels.invalidJson : labels.invalidShape}</AlertDescription></Alert>}\n </section>;\n}\n\n/**\n * The artifact kind of a maquette file (structurally an `ArtifactKind` of pi-chat): it matches `*.maquette.json`, names it, gives its icon\n * and draws it with `MaquetteViewer`. Pass it, or your own, in `artifactKinds`; kinds an app passes are consulted before the built-in one.\n */\nexport interface MaquetteKind {\n readonly match: (target: { readonly resource: { readonly path: string } }) => boolean;\n readonly label: string;\n readonly icon: ComponentType<{ readonly className?: string; readonly 'aria-hidden'?: boolean | 'true' }>;\n readonly renderer: (props: {\n readonly text: string; readonly fragment?: string | undefined;\n readonly artifact?: { readonly target: { readonly resource: { readonly path: string } } } | undefined; readonly read?: MaquetteRead | undefined;\n }) => ReactNode;\n}\n\n/**\n * A maquette kind in `locale` (\"Mock-up\" in English, \"Maquette\" in French) with any word overridden. `comments` (the feedback item's\n * `usePointComments(session)`) adds the comment icon; pass a fresh kind when it changes.\n */\nexport function createMaquetteKind({ locale, labels, comments }: {\n readonly locale?: MaquetteLocale | undefined; readonly labels?: Partial<MaquetteLabels> | undefined; readonly comments?: MaquetteComments | undefined;\n} = {}): MaquetteKind {\n return {\n match: target => isMaquettePath(target.resource.path),\n label: maquetteLabelsFor(locale, labels).kind,\n icon: LayoutTemplateIcon,\n renderer: ({ text, fragment, artifact, read }) => <MaquetteViewer text={text} fragment={fragment} locale={locale} labels={labels}\n path={artifact?.target.resource.path} comments={comments} read={read} />,\n };\n}\n\n/** The built-in maquette kind, in English. */\nexport const maquetteKind: MaquetteKind = createMaquetteKind();\n",
53
+ "type": "registry:component",
54
+ "target": "components/maquette-viewer/maquette-viewer.tsx"
55
+ }
56
+ ],
57
+ "css": {
58
+ "@layer components": {
59
+ "[data-boring=\"maquette-viewer\"] [data-maquette=\"placeholder\"]": {
60
+ "border-width": "var(--default-border-width, 1px)",
61
+ "border-style": "dashed",
62
+ "border-color": "var(--border)",
63
+ "background-color": "var(--muted)"
64
+ },
65
+ "[data-boring=\"maquette-viewer\"] [data-hint=\"pulse\"] [data-goto]": {
66
+ "animation": "var(--animate-pulse)",
67
+ "animation-iteration-count": "1"
68
+ },
69
+ "[data-boring=\"maquette-viewer\"] [data-goto]:focus-visible": {
70
+ "outline": "2px solid color-mix(in oklch, var(--ring) 55%, transparent)",
71
+ "outline-offset": "2px"
72
+ },
73
+ "@media (hover: hover) and (pointer: fine)": {
74
+ "[data-boring=\"maquette-viewer\"] [data-goto]:hover": {
75
+ "outline": "2px solid color-mix(in oklch, var(--ring) 55%, transparent)",
76
+ "outline-offset": "2px"
77
+ }
78
+ },
79
+ "@media (prefers-reduced-motion: reduce)": {
80
+ "[data-boring=\"maquette-viewer\"] [data-hint=\"pulse\"] [data-goto]": {
81
+ "animation": "none"
82
+ }
83
+ }
84
+ }
85
+ },
86
+ "type": "registry:component"
87
+ }
@@ -4,8 +4,8 @@
4
4
  "title": "Markdown editor",
5
5
  "description": "A host-owned styled wrapper over the concrete borrowed Markdown controller.",
6
6
  "dependencies": [
7
- "@hachej/boring-ui-kit@^0.4.1",
8
- "@hachej/boring-files@^0.4.1",
7
+ "@hachej/boring-ui-kit@^0.4.3",
8
+ "@hachej/boring-files@^0.4.3",
9
9
  "react@^19.0.0",
10
10
  "react-dom@^19.0.0",
11
11
  "marked@^17.0.6",
@@ -23,7 +23,7 @@
23
23
  "@tiptap/extension-floating-menu@^3.31.4"
24
24
  ],
25
25
  "registryDependencies": [
26
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.1/r/button.json"
26
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.3/r/button.json"
27
27
  ],
28
28
  "files": [
29
29
  {
@@ -0,0 +1,45 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "mockup-viewer",
4
+ "title": "HTML mock-up viewer",
5
+ "description": "Sandboxed *.mockup.html artifacts with host theme variables, lazy Tailwind compilation and feedback point comments. Host presentation only; artifact scripts run in an opaque-origin InteractiveFrame, never in the host.",
6
+ "dependencies": [
7
+ "react@^19.0.0",
8
+ "lucide-react@^1.51.0",
9
+ "tailwindcss@4.3.3",
10
+ "zod@^4.6.5"
11
+ ],
12
+ "registryDependencies": [
13
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.3/r/viewers.json",
14
+ "badge",
15
+ "toggle",
16
+ "tooltip"
17
+ ],
18
+ "files": [
19
+ {
20
+ "path": "registry/mockup-viewer/mockup-viewer.tsx",
21
+ "content": "'use client';\n\nimport { useEffect, useLayoutEffect, useRef, useState } from 'react';\nimport type { ComponentType, ReactNode, RefObject } from 'react';\nimport { CodeXmlIcon, MessageSquarePlusIcon } from 'lucide-react';\nimport { Badge } from '@/components/ui/badge';\nimport { Toggle } from '@/components/ui/toggle';\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';\nimport { InteractiveFrame } from '../viewers/interactive-html';\nimport { mockupContent, readMockupPick } from './mockup-frame';\nimport type { MockupPick } from './mockup-frame';\n\nexport const isMockupPath = (path: string): boolean => /\\.mockup\\.html$/i.test(path);\nexport const mockupTarget = (path: string, selector: string): string => `mockup:${path}#element=${selector}`;\n\n/** Structurally the feedback item's usePointComments. Picks are host-owned proxy elements, never frame DOM. */\nexport interface MockupComments {\n readonly active: boolean;\n readonly picking?: boolean | undefined;\n readonly point: () => void;\n readonly pins: readonly { readonly number: number; readonly target: string }[];\n readonly pick?: ((element: Element) => void) | undefined;\n readonly reposition?: (() => void) | undefined;\n}\n\nexport interface MockupViewerProps {\n readonly text: string;\n readonly path?: string | undefined;\n readonly comments?: MockupComments | undefined;\n readonly locale?: 'en' | 'fr' | undefined;\n readonly onReady?: (() => void) | undefined;\n}\n\nfunction useTheme(root: RefObject<HTMLDivElement | null>) {\n const [theme, setTheme] = useState({ variables: '', dark: false });\n useEffect(() => {\n const element = root.current;\n if (!element) return undefined;\n const view = element.ownerDocument.defaultView;\n if (!view) return undefined;\n const media = view.matchMedia('(prefers-color-scheme: dark)');\n const read = () => {\n const computed = view.getComputedStyle(element);\n const variables = Array.from(computed).filter(name => name.startsWith('--')).map(name => `${name}:${computed.getPropertyValue(name)}`).join(';');\n const dark = !!element.closest('.dark') || (!element.closest('.light') && media.matches);\n setTheme(current => current.variables === variables && current.dark === dark ? current : { variables, dark });\n };\n read();\n const observer = typeof MutationObserver === 'undefined' ? undefined : new MutationObserver(read);\n for (let node: Element | null = element; node; node = node.parentElement) observer?.observe(node, { attributes: true, attributeFilter: ['class', 'style'] });\n media.addEventListener('change', read);\n return () => { observer?.disconnect(); media.removeEventListener('change', read); };\n }, [root]);\n return theme;\n}\n\nexport function MockupViewer({ text, path = '', comments, locale, onReady }: MockupViewerProps) {\n const root = useRef<HTMLDivElement>(null);\n const frame = useRef<HTMLIFrameElement>(null);\n const proxies = useRef(new Map<string, HTMLSpanElement>());\n const pending = useRef<string | undefined>(undefined);\n const theme = useTheme(root);\n const [compiled, setCompiled] = useState<{ text: string; css: string } | { text: string; error: true }>();\n const [picks, setPicks] = useState<readonly MockupPick[]>([]);\n const label = locale === 'fr' ? 'Commenter un élément' : 'Comment on an element';\n useEffect(() => {\n let off = false;\n setPicks([]);\n void import('./mockup-compile').then(({ compileMockup }) => compileMockup(text)).then(css => { if (!off) setCompiled({ text, css }); }, () => { if (!off) setCompiled({ text, error: true }); });\n return () => { off = true; };\n }, [text, path]);\n const ready = compiled?.text === text && 'css' in compiled;\n const content = ready ? mockupContent(text, compiled.css) : undefined;\n const picking = comments?.picking ?? comments?.active ?? false;\n const mode = () => frame.current?.contentWindow?.postMessage({ type: 'mockup:pick-mode', on: picking, theme }, '*');\n useEffect(mode, [picking, content, theme]);\n useLayoutEffect(() => { pending.current = undefined; setPicks([]); }, [content]);\n useEffect(() => {\n const view = root.current?.ownerDocument.defaultView;\n const onMessage = (event: MessageEvent<unknown>) => {\n if (!frame.current?.contentWindow || event.source !== frame.current.contentWindow || event.origin !== 'null' || !comments?.pick) return;\n const pick = readMockupPick(event.data);\n if (!pick || (pick.type === 'mockup:pick' && !picking)) return;\n if (pick.type === 'mockup:pick') pending.current = pick.selector;\n setPicks(current => {\n const exists = current.some(item => item.selector === pick.selector);\n if (!exists && (pick.type !== 'mockup:pick' || current.length >= 50)) { pending.current = undefined; return current; }\n return exists ? current.map(item => item.selector === pick.selector ? pick : item) : [...current, pick];\n });\n };\n view?.addEventListener('message', onMessage);\n return () => view?.removeEventListener('message', onMessage);\n }, [comments, picking]);\n useLayoutEffect(() => {\n const selector = pending.current;\n pending.current = undefined;\n const proxy = selector === undefined ? undefined : proxies.current.get(selector);\n if (proxy) comments?.pick?.(proxy);\n else comments?.reposition?.();\n }, [picks, comments?.pick, comments?.reposition]);\n return <div ref={root} data-boring=\"mockup-viewer\" className=\"flex h-full min-h-0 min-w-0 flex-col\">\n {comments?.pick && <nav data-feedback-ignore=\"\" className=\"flex shrink-0 justify-end px-2 py-1\">\n <Tooltip><TooltipTrigger render={<Toggle aria-label={label} pressed={comments.active} onPressedChange={comments.point} data-testid=\"mockup-comment\" className=\"pointer-coarse:min-h-11 pointer-coarse:min-w-11\" />}><MessageSquarePlusIcon aria-hidden=\"true\" /></TooltipTrigger><TooltipContent>{label}</TooltipContent></Tooltip>\n </nav>}\n <div className=\"relative flex min-h-0 min-w-0 flex-1 overflow-hidden\">\n {content === undefined ? <p role=\"status\" className=\"p-3 text-muted-foreground\">{compiled && 'error' in compiled ? 'HTML mock-up unavailable.' : 'Loading HTML mock-up…'}</p>\n : <InteractiveFrame key={content} text={content} sources={[]} title=\"HTML mock-up\" testId=\"mockup\" frameRef={frame} onLoad={() => { mode(); onReady?.(); }} />}\n {picks.map(pick => <span key={pick.selector} ref={element => { if (element) proxies.current.set(pick.selector, element); else proxies.current.delete(pick.selector); }}\n data-testid=\"mockup-element\" data-feedback-id={mockupTarget(path, pick.selector)} role=\"img\" aria-label=\"HTML mock-up element\"\n className=\"pointer-events-none absolute overflow-hidden\" style={{ left: pick.rect.x, top: pick.rect.y, width: pick.rect.w, height: pick.rect.h }}>\n {!comments?.active && comments?.pins.filter(pin => pin.target === mockupTarget(path, pick.selector)).map(pin => <Badge key={pin.number}>{pin.number}</Badge>)}\n </span>)}\n </div>\n </div>;\n}\n\nexport interface MockupKind {\n readonly match: (target: { readonly resource: { readonly path: string } }) => boolean;\n readonly label: string;\n readonly icon: ComponentType<{ readonly className?: string; readonly 'aria-hidden'?: boolean | 'true' }>;\n readonly renderer: (props: { readonly text: string; readonly onReady?: (() => void) | undefined; readonly artifact?: { readonly target: { readonly resource: { readonly path: string } } } | undefined }) => ReactNode;\n}\n\nexport function createMockupKind({ comments, locale }: { readonly comments?: MockupComments | undefined; readonly locale?: 'en' | 'fr' | undefined } = {}): MockupKind {\n return { match: target => isMockupPath(target.resource.path), label: locale === 'fr' ? 'Maquette HTML' : 'HTML mock-up', icon: CodeXmlIcon,\n renderer: ({ text, artifact, onReady }) => <MockupViewer key={artifact?.target.resource.path} text={text} path={artifact?.target.resource.path} comments={comments} locale={locale} onReady={onReady} /> };\n}\nexport const mockupKind: MockupKind = createMockupKind();\n",
22
+ "type": "registry:component",
23
+ "target": "components/mockup-viewer/mockup-viewer.tsx"
24
+ },
25
+ {
26
+ "path": "registry/mockup-viewer/mockup-frame.ts",
27
+ "content": "// Ported from spike/canvas-board, examples/canvas-board/mockup-frame.ts (3b838e3a5).\nimport { z } from 'zod';\n\nconst rectangle = z.object({ x: z.number().finite().min(-10000).max(10000), y: z.number().finite().min(-10000).max(10000), w: z.number().finite().min(0).max(10000), h: z.number().finite().min(0).max(10000) });\nconst message = z.object({ type: z.enum(['mockup:pick', 'mockup:layout']), selector: z.string().max(300).regex(/^:root(?: > :nth-child\\([1-9]\\d{0,3}\\)){1,12}$/), rect: rectangle });\nexport type MockupPick = z.infer<typeof message>;\nexport function readMockupPick(data: unknown): MockupPick | undefined {\n const parsed = message.safeParse(data);\n return parsed.success ? parsed.data : undefined;\n}\n\n// No text or form values cross the frame boundary. The host's proxy element passes through the normal feedback privacy policy.\nconst PICK = `<script>(() => {\n let picking = false;\n const picked = new Map();\n addEventListener('message', event => {\n if (event.source !== parent || !event.data || event.data.type !== 'mockup:pick-mode') return;\n picking = event.data.on === true;\n const theme = event.data.theme;\n if (theme && typeof theme.variables === 'string') {\n document.getElementById('mockup-theme').textContent = ':root{' + theme.variables + ';color-scheme:' + (theme.dark ? 'dark' : 'light') + '}';\n document.documentElement.classList.toggle('dark', theme.dark === true);\n }\n document.documentElement.style.cursor = picking ? 'crosshair' : '';\n });\n const selectorOf = element => {\n const parts = [];\n for (let node = element; node && node !== document.documentElement; node = node.parentElement) {\n if (parts.length >= 12) return '';\n let index = 1; for (let sibling = node; (sibling = sibling.previousElementSibling);) index++;\n parts.unshift(':nth-child(' + index + ')');\n }\n return [':root', ...parts].join(' > ');\n };\n const send = (type, selector, element) => {\n const r = element.getBoundingClientRect();\n parent.postMessage({ type, selector, rect: { x: r.x, y: r.y, w: r.width, h: r.height } }, '*');\n };\n addEventListener('click', event => {\n if (!(event.target instanceof Element)) return;\n const link = event.target.closest('a[href]');\n if (link && !link.getAttribute('href').startsWith('#')) event.preventDefault();\n if (!picking) return;\n event.preventDefault(); event.stopImmediatePropagation();\n const element = event.target, selector = selectorOf(element);\n if (!selector || (!picked.has(selector) && picked.size >= 50)) return;\n picked.set(selector, element);\n send('mockup:pick', selector, element);\n }, true);\n const layout = () => picked.forEach((element, selector) => send('mockup:layout', selector, element));\n addEventListener('scroll', layout, true);\n addEventListener('resize', layout);\n})();</script>`;\n\n/** InteractiveFrame adds the existing policy before these styles, script and artifact bytes. */\nexport function mockupContent(html: string, css: string): string {\n return `<html><head><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><style>${css.replace(/<\\/style/gi, '<\\\\/style')}</style><style id=\"mockup-theme\"></style>${PICK}</head><body class=\"bg-background text-foreground\">${html}</body></html>`;\n}\n",
28
+ "type": "registry:lib",
29
+ "target": "components/mockup-viewer/mockup-frame.ts"
30
+ },
31
+ {
32
+ "path": "registry/mockup-viewer/mockup-compile.ts",
33
+ "content": "// Ported from examples/canvas-board/mockup-frame.ts on spike/canvas-board (3b838e3a5).\n// This entire module is lazy. No document markup is parsed in the host.\nimport { compile } from 'tailwindcss';\nimport { sources } from './mockup-tailwind';\n\nexport async function compileMockup(html: string): Promise<string> {\n // A compiler per document keeps its accumulated candidates out of unrelated viewers.\n const compiler = await compile(`@layer theme, base, components, utilities;\n@import \"theme\" layer(theme);\n@import \"preflight\" layer(base);\n@import \"utilities\" layer(utilities);\n${sources.bindings}\n@custom-variant dark (&:where(.dark, .dark *));`, {\n base: '/',\n loadStylesheet: async id => {\n if (id !== 'theme' && id !== 'preflight' && id !== 'utilities') throw new Error('Unsupported stylesheet');\n return { path: id, base: '/', content: sources[id] };\n },\n });\n const classes = [...html.matchAll(/\\bclass\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s>]+))/g)].flatMap(match => (match[1] ?? match[2] ?? match[3] ?? '').split(/\\s+/));\n return compiler.build([...new Set([...classes, ...html.split(/[\\s\"'`]+/), 'bg-background', 'text-foreground'])]);\n}\n",
34
+ "type": "registry:lib",
35
+ "target": "components/mockup-viewer/mockup-compile.ts"
36
+ },
37
+ {
38
+ "path": "registry/mockup-viewer/mockup-tailwind.ts",
39
+ "content": "// Generated by build-tailwind.mjs from tailwindcss@4.3.3 and registry.json.\n/* MIT License\n\nCopyright (c) Tailwind Labs, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n */\nexport const sources = {\n \"theme\": \"@theme default {\\n --font-sans:\\n -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', Arial,\\n sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';\\n --font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;\\n --font-mono:\\n ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',\\n monospace;\\n\\n --color-red-50: oklch(97.1% 0.013 17.38);\\n --color-red-100: oklch(93.6% 0.032 17.717);\\n --color-red-200: oklch(88.5% 0.062 18.334);\\n --color-red-300: oklch(80.8% 0.114 19.571);\\n --color-red-400: oklch(70.4% 0.191 22.216);\\n --color-red-500: oklch(63.7% 0.237 25.331);\\n --color-red-600: oklch(57.7% 0.245 27.325);\\n --color-red-700: oklch(50.5% 0.213 27.518);\\n --color-red-800: oklch(44.4% 0.177 26.899);\\n --color-red-900: oklch(39.6% 0.141 25.723);\\n --color-red-950: oklch(25.8% 0.092 26.042);\\n\\n --color-orange-50: oklch(98% 0.016 73.684);\\n --color-orange-100: oklch(95.4% 0.038 75.164);\\n --color-orange-200: oklch(90.1% 0.076 70.697);\\n --color-orange-300: oklch(83.7% 0.128 66.29);\\n --color-orange-400: oklch(75% 0.183 55.934);\\n --color-orange-500: oklch(70.5% 0.213 47.604);\\n --color-orange-600: oklch(64.6% 0.222 41.116);\\n --color-orange-700: oklch(55.3% 0.195 38.402);\\n --color-orange-800: oklch(47% 0.157 37.304);\\n --color-orange-900: oklch(40.8% 0.123 38.172);\\n --color-orange-950: oklch(26.6% 0.079 36.259);\\n\\n --color-amber-50: oklch(98.7% 0.022 95.277);\\n --color-amber-100: oklch(96.2% 0.059 95.617);\\n --color-amber-200: oklch(92.4% 0.12 95.746);\\n --color-amber-300: oklch(87.9% 0.169 91.605);\\n --color-amber-400: oklch(82.8% 0.189 84.429);\\n --color-amber-500: oklch(76.9% 0.188 70.08);\\n --color-amber-600: oklch(66.6% 0.179 58.318);\\n --color-amber-700: oklch(55.5% 0.163 48.998);\\n --color-amber-800: oklch(47.3% 0.137 46.201);\\n --color-amber-900: oklch(41.4% 0.112 45.904);\\n --color-amber-950: oklch(27.9% 0.077 45.635);\\n\\n --color-yellow-50: oklch(98.7% 0.026 102.212);\\n --color-yellow-100: oklch(97.3% 0.071 103.193);\\n --color-yellow-200: oklch(94.5% 0.129 101.54);\\n --color-yellow-300: oklch(90.5% 0.182 98.111);\\n --color-yellow-400: oklch(85.2% 0.199 91.936);\\n --color-yellow-500: oklch(79.5% 0.184 86.047);\\n --color-yellow-600: oklch(68.1% 0.162 75.834);\\n --color-yellow-700: oklch(55.4% 0.135 66.442);\\n --color-yellow-800: oklch(47.6% 0.114 61.907);\\n --color-yellow-900: oklch(42.1% 0.095 57.708);\\n --color-yellow-950: oklch(28.6% 0.066 53.813);\\n\\n --color-lime-50: oklch(98.6% 0.031 120.757);\\n --color-lime-100: oklch(96.7% 0.067 122.328);\\n --color-lime-200: oklch(93.8% 0.127 124.321);\\n --color-lime-300: oklch(89.7% 0.196 126.665);\\n --color-lime-400: oklch(84.1% 0.238 128.85);\\n --color-lime-500: oklch(76.8% 0.233 130.85);\\n --color-lime-600: oklch(64.8% 0.2 131.684);\\n --color-lime-700: oklch(53.2% 0.157 131.589);\\n --color-lime-800: oklch(45.3% 0.124 130.933);\\n --color-lime-900: oklch(40.5% 0.101 131.063);\\n --color-lime-950: oklch(27.4% 0.072 132.109);\\n\\n --color-green-50: oklch(98.2% 0.018 155.826);\\n --color-green-100: oklch(96.2% 0.044 156.743);\\n --color-green-200: oklch(92.5% 0.084 155.995);\\n --color-green-300: oklch(87.1% 0.15 154.449);\\n --color-green-400: oklch(79.2% 0.209 151.711);\\n --color-green-500: oklch(72.3% 0.219 149.579);\\n --color-green-600: oklch(62.7% 0.194 149.214);\\n --color-green-700: oklch(52.7% 0.154 150.069);\\n --color-green-800: oklch(44.8% 0.119 151.328);\\n --color-green-900: oklch(39.3% 0.095 152.535);\\n --color-green-950: oklch(26.6% 0.065 152.934);\\n\\n --color-emerald-50: oklch(97.9% 0.021 166.113);\\n --color-emerald-100: oklch(95% 0.052 163.051);\\n --color-emerald-200: oklch(90.5% 0.093 164.15);\\n --color-emerald-300: oklch(84.5% 0.143 164.978);\\n --color-emerald-400: oklch(76.5% 0.177 163.223);\\n --color-emerald-500: oklch(69.6% 0.17 162.48);\\n --color-emerald-600: oklch(59.6% 0.145 163.225);\\n --color-emerald-700: oklch(50.8% 0.118 165.612);\\n --color-emerald-800: oklch(43.2% 0.095 166.913);\\n --color-emerald-900: oklch(37.8% 0.077 168.94);\\n --color-emerald-950: oklch(26.2% 0.051 172.552);\\n\\n --color-teal-50: oklch(98.4% 0.014 180.72);\\n --color-teal-100: oklch(95.3% 0.051 180.801);\\n --color-teal-200: oklch(91% 0.096 180.426);\\n --color-teal-300: oklch(85.5% 0.138 181.071);\\n --color-teal-400: oklch(77.7% 0.152 181.912);\\n --color-teal-500: oklch(70.4% 0.14 182.503);\\n --color-teal-600: oklch(60% 0.118 184.704);\\n --color-teal-700: oklch(51.1% 0.096 186.391);\\n --color-teal-800: oklch(43.7% 0.078 188.216);\\n --color-teal-900: oklch(38.6% 0.063 188.416);\\n --color-teal-950: oklch(27.7% 0.046 192.524);\\n\\n --color-cyan-50: oklch(98.4% 0.019 200.873);\\n --color-cyan-100: oklch(95.6% 0.045 203.388);\\n --color-cyan-200: oklch(91.7% 0.08 205.041);\\n --color-cyan-300: oklch(86.5% 0.127 207.078);\\n --color-cyan-400: oklch(78.9% 0.154 211.53);\\n --color-cyan-500: oklch(71.5% 0.143 215.221);\\n --color-cyan-600: oklch(60.9% 0.126 221.723);\\n --color-cyan-700: oklch(52% 0.105 223.128);\\n --color-cyan-800: oklch(45% 0.085 224.283);\\n --color-cyan-900: oklch(39.8% 0.07 227.392);\\n --color-cyan-950: oklch(30.2% 0.056 229.695);\\n\\n --color-sky-50: oklch(97.7% 0.013 236.62);\\n --color-sky-100: oklch(95.1% 0.026 236.824);\\n --color-sky-200: oklch(90.1% 0.058 230.902);\\n --color-sky-300: oklch(82.8% 0.111 230.318);\\n --color-sky-400: oklch(74.6% 0.16 232.661);\\n --color-sky-500: oklch(68.5% 0.169 237.323);\\n --color-sky-600: oklch(58.8% 0.158 241.966);\\n --color-sky-700: oklch(50% 0.134 242.749);\\n --color-sky-800: oklch(44.3% 0.11 240.79);\\n --color-sky-900: oklch(39.1% 0.09 240.876);\\n --color-sky-950: oklch(29.3% 0.066 243.157);\\n\\n --color-blue-50: oklch(97% 0.014 254.604);\\n --color-blue-100: oklch(93.2% 0.032 255.585);\\n --color-blue-200: oklch(88.2% 0.059 254.128);\\n --color-blue-300: oklch(80.9% 0.105 251.813);\\n --color-blue-400: oklch(70.7% 0.165 254.624);\\n --color-blue-500: oklch(62.3% 0.214 259.815);\\n --color-blue-600: oklch(54.6% 0.245 262.881);\\n --color-blue-700: oklch(48.8% 0.243 264.376);\\n --color-blue-800: oklch(42.4% 0.199 265.638);\\n --color-blue-900: oklch(37.9% 0.146 265.522);\\n --color-blue-950: oklch(28.2% 0.091 267.935);\\n\\n --color-indigo-50: oklch(96.2% 0.018 272.314);\\n --color-indigo-100: oklch(93% 0.034 272.788);\\n --color-indigo-200: oklch(87% 0.065 274.039);\\n --color-indigo-300: oklch(78.5% 0.115 274.713);\\n --color-indigo-400: oklch(67.3% 0.182 276.935);\\n --color-indigo-500: oklch(58.5% 0.233 277.117);\\n --color-indigo-600: oklch(51.1% 0.262 276.966);\\n --color-indigo-700: oklch(45.7% 0.24 277.023);\\n --color-indigo-800: oklch(39.8% 0.195 277.366);\\n --color-indigo-900: oklch(35.9% 0.144 278.697);\\n --color-indigo-950: oklch(25.7% 0.09 281.288);\\n\\n --color-violet-50: oklch(96.9% 0.016 293.756);\\n --color-violet-100: oklch(94.3% 0.029 294.588);\\n --color-violet-200: oklch(89.4% 0.057 293.283);\\n --color-violet-300: oklch(81.1% 0.111 293.571);\\n --color-violet-400: oklch(70.2% 0.183 293.541);\\n --color-violet-500: oklch(60.6% 0.25 292.717);\\n --color-violet-600: oklch(54.1% 0.281 293.009);\\n --color-violet-700: oklch(49.1% 0.27 292.581);\\n --color-violet-800: oklch(43.2% 0.232 292.759);\\n --color-violet-900: oklch(38% 0.189 293.745);\\n --color-violet-950: oklch(28.3% 0.141 291.089);\\n\\n --color-purple-50: oklch(97.7% 0.014 308.299);\\n --color-purple-100: oklch(94.6% 0.033 307.174);\\n --color-purple-200: oklch(90.2% 0.063 306.703);\\n --color-purple-300: oklch(82.7% 0.119 306.383);\\n --color-purple-400: oklch(71.4% 0.203 305.504);\\n --color-purple-500: oklch(62.7% 0.265 303.9);\\n --color-purple-600: oklch(55.8% 0.288 302.321);\\n --color-purple-700: oklch(49.6% 0.265 301.924);\\n --color-purple-800: oklch(43.8% 0.218 303.724);\\n --color-purple-900: oklch(38.1% 0.176 304.987);\\n --color-purple-950: oklch(29.1% 0.149 302.717);\\n\\n --color-fuchsia-50: oklch(97.7% 0.017 320.058);\\n --color-fuchsia-100: oklch(95.2% 0.037 318.852);\\n --color-fuchsia-200: oklch(90.3% 0.076 319.62);\\n --color-fuchsia-300: oklch(83.3% 0.145 321.434);\\n --color-fuchsia-400: oklch(74% 0.238 322.16);\\n --color-fuchsia-500: oklch(66.7% 0.295 322.15);\\n --color-fuchsia-600: oklch(59.1% 0.293 322.896);\\n --color-fuchsia-700: oklch(51.8% 0.253 323.949);\\n --color-fuchsia-800: oklch(45.2% 0.211 324.591);\\n --color-fuchsia-900: oklch(40.1% 0.17 325.612);\\n --color-fuchsia-950: oklch(29.3% 0.136 325.661);\\n\\n --color-pink-50: oklch(97.1% 0.014 343.198);\\n --color-pink-100: oklch(94.8% 0.028 342.258);\\n --color-pink-200: oklch(89.9% 0.061 343.231);\\n --color-pink-300: oklch(82.3% 0.12 346.018);\\n --color-pink-400: oklch(71.8% 0.202 349.761);\\n --color-pink-500: oklch(65.6% 0.241 354.308);\\n --color-pink-600: oklch(59.2% 0.249 0.584);\\n --color-pink-700: oklch(52.5% 0.223 3.958);\\n --color-pink-800: oklch(45.9% 0.187 3.815);\\n --color-pink-900: oklch(40.8% 0.153 2.432);\\n --color-pink-950: oklch(28.4% 0.109 3.907);\\n\\n --color-rose-50: oklch(96.9% 0.015 12.422);\\n --color-rose-100: oklch(94.1% 0.03 12.58);\\n --color-rose-200: oklch(89.2% 0.058 10.001);\\n --color-rose-300: oklch(81% 0.117 11.638);\\n --color-rose-400: oklch(71.2% 0.194 13.428);\\n --color-rose-500: oklch(64.5% 0.246 16.439);\\n --color-rose-600: oklch(58.6% 0.253 17.585);\\n --color-rose-700: oklch(51.4% 0.222 16.935);\\n --color-rose-800: oklch(45.5% 0.188 13.697);\\n --color-rose-900: oklch(41% 0.159 10.272);\\n --color-rose-950: oklch(27.1% 0.105 12.094);\\n\\n --color-slate-50: oklch(98.4% 0.003 247.858);\\n --color-slate-100: oklch(96.8% 0.007 247.896);\\n --color-slate-200: oklch(92.9% 0.013 255.508);\\n --color-slate-300: oklch(86.9% 0.022 252.894);\\n --color-slate-400: oklch(70.4% 0.04 256.788);\\n --color-slate-500: oklch(55.4% 0.046 257.417);\\n --color-slate-600: oklch(44.6% 0.043 257.281);\\n --color-slate-700: oklch(37.2% 0.044 257.287);\\n --color-slate-800: oklch(27.9% 0.041 260.031);\\n --color-slate-900: oklch(20.8% 0.042 265.755);\\n --color-slate-950: oklch(12.9% 0.042 264.695);\\n\\n --color-gray-50: oklch(98.5% 0.002 247.839);\\n --color-gray-100: oklch(96.7% 0.003 264.542);\\n --color-gray-200: oklch(92.8% 0.006 264.531);\\n --color-gray-300: oklch(87.2% 0.01 258.338);\\n --color-gray-400: oklch(70.7% 0.022 261.325);\\n --color-gray-500: oklch(55.1% 0.027 264.364);\\n --color-gray-600: oklch(44.6% 0.03 256.802);\\n --color-gray-700: oklch(37.3% 0.034 259.733);\\n --color-gray-800: oklch(27.8% 0.033 256.848);\\n --color-gray-900: oklch(21% 0.034 264.665);\\n --color-gray-950: oklch(13% 0.028 261.692);\\n\\n --color-zinc-50: oklch(98.5% 0 none);\\n --color-zinc-100: oklch(96.7% 0.001 286.375);\\n --color-zinc-200: oklch(92% 0.004 286.32);\\n --color-zinc-300: oklch(87.1% 0.006 286.286);\\n --color-zinc-400: oklch(70.5% 0.015 286.067);\\n --color-zinc-500: oklch(55.2% 0.016 285.938);\\n --color-zinc-600: oklch(44.2% 0.017 285.786);\\n --color-zinc-700: oklch(37% 0.013 285.805);\\n --color-zinc-800: oklch(27.4% 0.006 286.033);\\n --color-zinc-900: oklch(21% 0.006 285.885);\\n --color-zinc-950: oklch(14.1% 0.005 285.823);\\n\\n --color-neutral-50: oklch(98.5% 0 none);\\n --color-neutral-100: oklch(97% 0 none);\\n --color-neutral-200: oklch(92.2% 0 none);\\n --color-neutral-300: oklch(87% 0 none);\\n --color-neutral-400: oklch(70.8% 0 none);\\n --color-neutral-500: oklch(55.6% 0 none);\\n --color-neutral-600: oklch(43.9% 0 none);\\n --color-neutral-700: oklch(37.1% 0 none);\\n --color-neutral-800: oklch(26.9% 0 none);\\n --color-neutral-900: oklch(20.5% 0 none);\\n --color-neutral-950: oklch(14.5% 0 none);\\n\\n --color-stone-50: oklch(98.5% 0.001 106.423);\\n --color-stone-100: oklch(97% 0.001 106.424);\\n --color-stone-200: oklch(92.3% 0.003 48.717);\\n --color-stone-300: oklch(86.9% 0.005 56.366);\\n --color-stone-400: oklch(70.9% 0.01 56.259);\\n --color-stone-500: oklch(55.3% 0.013 58.071);\\n --color-stone-600: oklch(44.4% 0.011 73.639);\\n --color-stone-700: oklch(37.4% 0.01 67.558);\\n --color-stone-800: oklch(26.8% 0.007 34.298);\\n --color-stone-900: oklch(21.6% 0.006 56.043);\\n --color-stone-950: oklch(14.7% 0.004 49.25);\\n\\n --color-mauve-50: oklch(98.5% 0 none);\\n --color-mauve-100: oklch(96% 0.003 325.6);\\n --color-mauve-200: oklch(92.2% 0.005 325.62);\\n --color-mauve-300: oklch(86.5% 0.012 325.68);\\n --color-mauve-400: oklch(71.1% 0.019 323.02);\\n --color-mauve-500: oklch(54.2% 0.034 322.5);\\n --color-mauve-600: oklch(43.5% 0.029 321.78);\\n --color-mauve-700: oklch(36.4% 0.029 323.89);\\n --color-mauve-800: oklch(26.3% 0.024 320.12);\\n --color-mauve-900: oklch(21.2% 0.019 322.12);\\n --color-mauve-950: oklch(14.5% 0.008 326);\\n\\n --color-olive-50: oklch(98.8% 0.003 106.5);\\n --color-olive-100: oklch(96.6% 0.005 106.5);\\n --color-olive-200: oklch(93% 0.007 106.5);\\n --color-olive-300: oklch(88% 0.011 106.6);\\n --color-olive-400: oklch(73.7% 0.021 106.9);\\n --color-olive-500: oklch(58% 0.031 107.3);\\n --color-olive-600: oklch(46.6% 0.025 107.3);\\n --color-olive-700: oklch(39.4% 0.023 107.4);\\n --color-olive-800: oklch(28.6% 0.016 107.4);\\n --color-olive-900: oklch(22.8% 0.013 107.4);\\n --color-olive-950: oklch(15.3% 0.006 107.1);\\n\\n --color-mist-50: oklch(98.7% 0.002 197.1);\\n --color-mist-100: oklch(96.3% 0.002 197.1);\\n --color-mist-200: oklch(92.5% 0.005 214.3);\\n --color-mist-300: oklch(87.2% 0.007 219.6);\\n --color-mist-400: oklch(72.3% 0.014 214.4);\\n --color-mist-500: oklch(56% 0.021 213.5);\\n --color-mist-600: oklch(45% 0.017 213.2);\\n --color-mist-700: oklch(37.8% 0.015 216);\\n --color-mist-800: oklch(27.5% 0.011 216.9);\\n --color-mist-900: oklch(21.8% 0.008 223.9);\\n --color-mist-950: oklch(14.8% 0.004 228.8);\\n\\n --color-taupe-50: oklch(98.6% 0.002 67.8);\\n --color-taupe-100: oklch(96% 0.002 17.2);\\n --color-taupe-200: oklch(92.2% 0.005 34.3);\\n --color-taupe-300: oklch(86.8% 0.007 39.5);\\n --color-taupe-400: oklch(71.4% 0.014 41.2);\\n --color-taupe-500: oklch(54.7% 0.021 43.1);\\n --color-taupe-600: oklch(43.8% 0.017 39.3);\\n --color-taupe-700: oklch(36.7% 0.016 35.7);\\n --color-taupe-800: oklch(26.8% 0.011 36.5);\\n --color-taupe-900: oklch(21.4% 0.009 43.1);\\n --color-taupe-950: oklch(14.7% 0.004 49.3);\\n\\n --color-black: #000;\\n --color-white: #fff;\\n\\n --spacing: 0.25rem;\\n\\n --breakpoint-sm: 40rem;\\n --breakpoint-md: 48rem;\\n --breakpoint-lg: 64rem;\\n --breakpoint-xl: 80rem;\\n --breakpoint-2xl: 96rem;\\n\\n --container-3xs: 16rem;\\n --container-2xs: 18rem;\\n --container-xs: 20rem;\\n --container-sm: 24rem;\\n --container-md: 28rem;\\n --container-lg: 32rem;\\n --container-xl: 36rem;\\n --container-2xl: 42rem;\\n --container-3xl: 48rem;\\n --container-4xl: 56rem;\\n --container-5xl: 64rem;\\n --container-6xl: 72rem;\\n --container-7xl: 80rem;\\n\\n --text-xs: 0.75rem;\\n --text-xs--line-height: calc(1 / 0.75);\\n --text-sm: 0.875rem;\\n --text-sm--line-height: calc(1.25 / 0.875);\\n --text-base: 1rem;\\n --text-base--line-height: calc(1.5 / 1);\\n --text-lg: 1.125rem;\\n --text-lg--line-height: calc(1.75 / 1.125);\\n --text-xl: 1.25rem;\\n --text-xl--line-height: calc(1.75 / 1.25);\\n --text-2xl: 1.5rem;\\n --text-2xl--line-height: calc(2 / 1.5);\\n --text-3xl: 1.875rem;\\n --text-3xl--line-height: calc(2.25 / 1.875);\\n --text-4xl: 2.25rem;\\n --text-4xl--line-height: calc(2.5 / 2.25);\\n --text-5xl: 3rem;\\n --text-5xl--line-height: 1;\\n --text-6xl: 3.75rem;\\n --text-6xl--line-height: 1;\\n --text-7xl: 4.5rem;\\n --text-7xl--line-height: 1;\\n --text-8xl: 6rem;\\n --text-8xl--line-height: 1;\\n --text-9xl: 8rem;\\n --text-9xl--line-height: 1;\\n\\n --font-weight-thin: 100;\\n --font-weight-extralight: 200;\\n --font-weight-light: 300;\\n --font-weight-normal: 400;\\n --font-weight-medium: 500;\\n --font-weight-semibold: 600;\\n --font-weight-bold: 700;\\n --font-weight-extrabold: 800;\\n --font-weight-black: 900;\\n\\n --tracking-tighter: -0.05em;\\n --tracking-tight: -0.025em;\\n --tracking-normal: 0em;\\n --tracking-wide: 0.025em;\\n --tracking-wider: 0.05em;\\n --tracking-widest: 0.1em;\\n\\n --leading-tight: 1.25;\\n --leading-snug: 1.375;\\n --leading-normal: 1.5;\\n --leading-relaxed: 1.625;\\n --leading-loose: 2;\\n\\n --radius-xs: 0.125rem;\\n --radius-sm: 0.25rem;\\n --radius-md: 0.375rem;\\n --radius-lg: 0.5rem;\\n --radius-xl: 0.75rem;\\n --radius-2xl: 1rem;\\n --radius-3xl: 1.5rem;\\n --radius-4xl: 2rem;\\n\\n --shadow-2xs: 0 1px rgb(0 0 0 / 0.05);\\n --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05);\\n --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);\\n --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);\\n --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);\\n --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);\\n --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);\\n\\n --inset-shadow-2xs: inset 0 1px rgb(0 0 0 / 0.05);\\n --inset-shadow-xs: inset 0 1px 1px rgb(0 0 0 / 0.05);\\n --inset-shadow-sm: inset 0 2px 4px rgb(0 0 0 / 0.05);\\n\\n --drop-shadow-xs: 0 1px 1px rgb(0 0 0 / 0.05);\\n --drop-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.15);\\n --drop-shadow-md: 0 3px 3px rgb(0 0 0 / 0.12);\\n --drop-shadow-lg: 0 4px 4px rgb(0 0 0 / 0.15);\\n --drop-shadow-xl: 0 9px 7px rgb(0 0 0 / 0.1);\\n --drop-shadow-2xl: 0 25px 25px rgb(0 0 0 / 0.15);\\n\\n --text-shadow-2xs: 0px 1px 0px rgb(0 0 0 / 0.15);\\n --text-shadow-xs: 0px 1px 1px rgb(0 0 0 / 0.2);\\n --text-shadow-sm:\\n 0px 1px 0px rgb(0 0 0 / 0.075), 0px 1px 1px rgb(0 0 0 / 0.075), 0px 2px 2px rgb(0 0 0 / 0.075);\\n --text-shadow-md:\\n 0px 1px 1px rgb(0 0 0 / 0.1), 0px 1px 2px rgb(0 0 0 / 0.1), 0px 2px 4px rgb(0 0 0 / 0.1);\\n --text-shadow-lg:\\n 0px 1px 2px rgb(0 0 0 / 0.1), 0px 3px 2px rgb(0 0 0 / 0.1), 0px 4px 8px rgb(0 0 0 / 0.1);\\n\\n --ease-in: cubic-bezier(0.4, 0, 1, 1);\\n --ease-out: cubic-bezier(0, 0, 0.2, 1);\\n --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);\\n\\n --animate-spin: spin 1s linear infinite;\\n --animate-ping: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;\\n --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;\\n --animate-bounce: bounce 1s infinite;\\n\\n @keyframes spin {\\n to {\\n transform: rotate(360deg);\\n }\\n }\\n\\n @keyframes ping {\\n 75%,\\n 100% {\\n transform: scale(2);\\n opacity: 0;\\n }\\n }\\n\\n @keyframes pulse {\\n 50% {\\n opacity: 0.5;\\n }\\n }\\n\\n @keyframes bounce {\\n 0%,\\n 100% {\\n transform: translateY(-25%);\\n animation-timing-function: cubic-bezier(0.8, 0, 1, 1);\\n }\\n\\n 50% {\\n transform: none;\\n animation-timing-function: cubic-bezier(0, 0, 0.2, 1);\\n }\\n }\\n\\n --blur-xs: 4px;\\n --blur-sm: 8px;\\n --blur-md: 12px;\\n --blur-lg: 16px;\\n --blur-xl: 24px;\\n --blur-2xl: 40px;\\n --blur-3xl: 64px;\\n\\n --perspective-dramatic: 100px;\\n --perspective-near: 300px;\\n --perspective-normal: 500px;\\n --perspective-midrange: 800px;\\n --perspective-distant: 1200px;\\n\\n --aspect-video: 16 / 9;\\n\\n --default-transition-duration: 150ms;\\n --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\\n --default-font-family: --theme(--font-sans, initial);\\n --default-font-feature-settings: --theme(--font-sans--font-feature-settings, initial);\\n --default-font-variation-settings: --theme(--font-sans--font-variation-settings, initial);\\n --default-mono-font-family: --theme(--font-mono, initial);\\n --default-mono-font-feature-settings: --theme(--font-mono--font-feature-settings, initial);\\n --default-mono-font-variation-settings: --theme(--font-mono--font-variation-settings, initial);\\n}\\n\\n/* Deprecated */\\n@theme default inline reference {\\n --blur: 8px;\\n --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);\\n --shadow-inner: inset 0 2px 4px 0 rgb(0 0 0 / 0.05);\\n --drop-shadow: 0 1px 2px rgb(0 0 0 / 0.1), 0 1px 1px rgb(0 0 0 / 0.06);\\n --radius: 0.25rem;\\n --max-width-prose: 65ch;\\n}\\n\",\n \"preflight\": \"/*\\n 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)\\n 2. Remove default margins and padding\\n 3. Reset all borders.\\n*/\\n\\n*,\\n::after,\\n::before,\\n::backdrop,\\n::file-selector-button {\\n box-sizing: border-box; /* 1 */\\n margin: 0; /* 2 */\\n padding: 0; /* 2 */\\n border: 0 solid; /* 3 */\\n}\\n\\n/*\\n 1. Use a consistent sensible line-height in all browsers.\\n 2. Prevent adjustments of font size after orientation changes in iOS.\\n 3. Use a more readable tab size.\\n 4. Use the user's configured `sans` font-family by default.\\n 5. Use the user's configured `sans` font-feature-settings by default.\\n 6. Use the user's configured `sans` font-variation-settings by default.\\n 7. Disable tap highlights on iOS.\\n*/\\n\\nhtml,\\n:host {\\n line-height: 1.5; /* 1 */\\n -webkit-text-size-adjust: 100%; /* 2 */\\n tab-size: 4; /* 3 */\\n font-family: --theme(\\n --default-font-family,\\n -apple-system,\\n BlinkMacSystemFont,\\n 'Segoe UI',\\n Roboto,\\n 'Helvetica Neue',\\n 'Noto Sans',\\n Arial,\\n sans-serif,\\n 'Apple Color Emoji',\\n 'Segoe UI Emoji',\\n 'Segoe UI Symbol',\\n 'Noto Color Emoji'\\n ); /* 4 */\\n font-feature-settings: --theme(--default-font-feature-settings, normal); /* 5 */\\n font-variation-settings: --theme(--default-font-variation-settings, normal); /* 6 */\\n -webkit-tap-highlight-color: transparent; /* 7 */\\n}\\n\\n/*\\n 1. Add the correct height in Firefox.\\n 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)\\n 3. Reset the default border style to a 1px solid border.\\n*/\\n\\nhr {\\n height: 0; /* 1 */\\n color: inherit; /* 2 */\\n border-top-width: 1px; /* 3 */\\n}\\n\\n/*\\n Add the correct text decoration in Chrome, Edge, and Safari.\\n*/\\n\\nabbr:where([title]) {\\n -webkit-text-decoration: underline dotted;\\n text-decoration: underline dotted;\\n}\\n\\n/*\\n Remove the default font size and weight for headings.\\n*/\\n\\nh1,\\nh2,\\nh3,\\nh4,\\nh5,\\nh6 {\\n font-size: inherit;\\n font-weight: inherit;\\n}\\n\\n/*\\n Reset links to optimize for opt-in styling instead of opt-out.\\n*/\\n\\na {\\n color: inherit;\\n -webkit-text-decoration: inherit;\\n text-decoration: inherit;\\n}\\n\\n/*\\n Add the correct font weight in Edge and Safari.\\n*/\\n\\nb,\\nstrong {\\n font-weight: bolder;\\n}\\n\\n/*\\n 1. Use the user's configured `mono` font-family by default.\\n 2. Use the user's configured `mono` font-feature-settings by default.\\n 3. Use the user's configured `mono` font-variation-settings by default.\\n 4. Correct the odd `em` font sizing in all browsers.\\n*/\\n\\ncode,\\nkbd,\\nsamp,\\npre {\\n font-family: --theme(\\n --default-mono-font-family,\\n ui-monospace,\\n SFMono-Regular,\\n Menlo,\\n Monaco,\\n Consolas,\\n 'Liberation Mono',\\n 'Courier New',\\n monospace\\n ); /* 1 */\\n font-feature-settings: --theme(--default-mono-font-feature-settings, normal); /* 2 */\\n font-variation-settings: --theme(--default-mono-font-variation-settings, normal); /* 3 */\\n font-size: 1em; /* 4 */\\n}\\n\\n/*\\n Add the correct font size in all browsers.\\n*/\\n\\nsmall {\\n font-size: 80%;\\n}\\n\\n/*\\n Prevent `sub` and `sup` elements from affecting the line height in all browsers.\\n*/\\n\\nsub,\\nsup {\\n font-size: 75%;\\n line-height: 0;\\n position: relative;\\n vertical-align: baseline;\\n}\\n\\nsub {\\n bottom: -0.25em;\\n}\\n\\nsup {\\n top: -0.5em;\\n}\\n\\n/*\\n 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)\\n 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)\\n 3. Remove gaps between table borders by default.\\n*/\\n\\ntable {\\n text-indent: 0; /* 1 */\\n border-color: inherit; /* 2 */\\n border-collapse: collapse; /* 3 */\\n}\\n\\n/*\\n Use the modern Firefox focus style for all focusable elements.\\n*/\\n\\n:-moz-focusring:where(:not(iframe)) {\\n outline: auto;\\n}\\n\\n/*\\n Add the correct vertical alignment in Chrome and Firefox.\\n*/\\n\\nprogress {\\n vertical-align: baseline;\\n}\\n\\n/*\\n Add the correct display in Chrome and Safari.\\n*/\\n\\nsummary {\\n display: list-item;\\n}\\n\\n/*\\n Make lists unstyled by default.\\n*/\\n\\nol,\\nul,\\nmenu {\\n list-style: none;\\n}\\n\\n/*\\n 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)\\n 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)\\n This can trigger a poorly considered lint error in some tools but is included by design.\\n*/\\n\\nimg,\\nsvg,\\nvideo,\\ncanvas,\\naudio,\\niframe,\\nembed,\\nobject {\\n display: block; /* 1 */\\n vertical-align: middle; /* 2 */\\n}\\n\\n/*\\n Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)\\n*/\\n\\nimg,\\nvideo {\\n max-width: 100%;\\n height: auto;\\n}\\n\\n/*\\n 1. Inherit font styles in all browsers.\\n 2. Remove border radius in all browsers.\\n 3. Remove background color in all browsers.\\n 4. Ensure consistent opacity for disabled states in all browsers.\\n*/\\n\\nbutton,\\ninput,\\nselect,\\noptgroup,\\ntextarea,\\n::file-selector-button {\\n font: inherit; /* 1 */\\n font-feature-settings: inherit; /* 1 */\\n font-variation-settings: inherit; /* 1 */\\n letter-spacing: inherit; /* 1 */\\n color: inherit; /* 1 */\\n border-radius: 0; /* 2 */\\n background-color: transparent; /* 3 */\\n opacity: 1; /* 4 */\\n}\\n\\n/*\\n Restore default font weight.\\n*/\\n\\n:where(select:is([multiple], [size])) optgroup {\\n font-weight: bolder;\\n}\\n\\n/*\\n Restore indentation.\\n*/\\n\\n:where(select:is([multiple], [size])) optgroup option {\\n padding-inline-start: 20px;\\n}\\n\\n/*\\n Restore space after button.\\n*/\\n\\n::file-selector-button {\\n margin-inline-end: 4px;\\n}\\n\\n/*\\n Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)\\n*/\\n\\n::placeholder {\\n opacity: 1;\\n}\\n\\n/*\\n Set the default placeholder color to a semi-transparent version of the current text color in browsers that do not\\n crash when using `color-mix(…)` with `currentcolor`. (https://github.com/tailwindlabs/tailwindcss/issues/17194)\\n*/\\n\\n@supports (not (-webkit-appearance: -apple-pay-button)) /* Not Safari */ or\\n (contain-intrinsic-size: 1px) /* Safari 17+ */ {\\n ::placeholder {\\n color: color-mix(in oklab, currentcolor 50%, transparent);\\n }\\n}\\n\\n/*\\n Prevent resizing textareas horizontally by default.\\n*/\\n\\ntextarea {\\n resize: vertical;\\n}\\n\\n/*\\n Remove the inner padding in Chrome and Safari on macOS.\\n*/\\n\\n::-webkit-search-decoration {\\n -webkit-appearance: none;\\n}\\n\\n/*\\n 1. Ensure date/time inputs have the same height when empty in iOS Safari.\\n 2. Ensure text alignment can be changed on date/time inputs in iOS Safari.\\n*/\\n\\n::-webkit-date-and-time-value {\\n min-height: 1lh; /* 1 */\\n text-align: inherit; /* 2 */\\n}\\n\\n/*\\n Prevent height from changing on date/time inputs in macOS Safari when the input is set to `display: block`.\\n*/\\n\\n::-webkit-datetime-edit {\\n display: inline-flex;\\n}\\n\\n/*\\n Remove excess padding from pseudo-elements in date/time inputs to ensure consistent height across browsers.\\n*/\\n\\n::-webkit-datetime-edit-fields-wrapper {\\n padding: 0;\\n}\\n\\n::-webkit-datetime-edit,\\n::-webkit-datetime-edit-year-field,\\n::-webkit-datetime-edit-month-field,\\n::-webkit-datetime-edit-day-field,\\n::-webkit-datetime-edit-hour-field,\\n::-webkit-datetime-edit-minute-field,\\n::-webkit-datetime-edit-second-field,\\n::-webkit-datetime-edit-millisecond-field,\\n::-webkit-datetime-edit-meridiem-field {\\n padding-block: 0;\\n}\\n\\n/*\\n Center dropdown marker shown on inputs with paired `<datalist>`s in Chrome. (https://github.com/tailwindlabs/tailwindcss/issues/18499)\\n*/\\n\\n::-webkit-calendar-picker-indicator {\\n line-height: 1;\\n}\\n\\n/*\\n Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)\\n*/\\n\\n:-moz-ui-invalid {\\n box-shadow: none;\\n}\\n\\n/*\\n Correct the inability to style the border radius in iOS Safari.\\n*/\\n\\nbutton,\\ninput:where([type='button'], [type='reset'], [type='submit']),\\n::file-selector-button {\\n appearance: button;\\n}\\n\\n/*\\n Correct the cursor style of increment and decrement buttons in Safari.\\n*/\\n\\n::-webkit-inner-spin-button,\\n::-webkit-outer-spin-button {\\n height: auto;\\n}\\n\\n/*\\n Make elements with the HTML hidden attribute stay hidden by default.\\n*/\\n\\n[hidden]:where(:not([hidden='until-found'])) {\\n display: none !important;\\n}\\n\",\n \"utilities\": \"@tailwind utilities;\\n\",\n \"bindings\": \"@theme inline { --color-background: var(--background);\\n--color-foreground: var(--foreground);\\n--color-card: var(--card);\\n--color-card-foreground: var(--card-foreground);\\n--color-popover: var(--popover);\\n--color-popover-foreground: var(--popover-foreground);\\n--color-primary: var(--primary);\\n--color-primary-foreground: var(--primary-foreground);\\n--color-secondary: var(--secondary);\\n--color-secondary-foreground: var(--secondary-foreground);\\n--color-muted: var(--muted);\\n--color-muted-foreground: var(--muted-foreground);\\n--color-accent: var(--accent);\\n--color-accent-foreground: var(--accent-foreground);\\n--color-destructive: var(--destructive);\\n--color-border: var(--border);\\n--color-input: var(--input);\\n--color-ring: var(--ring);\\n--radius-sm: calc(var(--radius) - 4px);\\n--radius-md: calc(var(--radius) - 2px);\\n--radius-lg: var(--radius);\\n--radius-xl: calc(var(--radius) + 4px);\\n--radius-2xl: calc(var(--radius) * 1.8);\\n--radius-3xl: calc(var(--radius) * 2.2);\\n--default-border-width: var(--border-width, 1px);\\n--shadow-xs: var(--shadow-xs);\\n--shadow-sm: var(--shadow-sm);\\n--shadow-md: var(--shadow-md);\\n--shadow-lg: var(--shadow-lg);\\n--shadow-xl: var(--shadow-xl);\\n--shadow-2xl: var(--shadow-2xl); }\"\n};\n",
40
+ "type": "registry:lib",
41
+ "target": "components/mockup-viewer/mockup-tailwind.ts"
42
+ }
43
+ ],
44
+ "type": "registry:block"
45
+ }
package/r/pi-ambient.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "title": "Pi ambient agent",
5
5
  "description": "AmbientChat, a compact floating agent bar that grows into a chat window over an existing page, and AgentNotifications, toasts for finished, failed and waiting tasks (with the one guarded browser Notification helper). A block over the pi-chat item; its in-window artifact panel uses the pi-workspace layout.",
6
6
  "dependencies": [
7
- "@hachej/boring-ui-kit@^0.4.1",
7
+ "@hachej/boring-ui-kit@^0.4.3",
8
8
  "react@^19.0.0",
9
9
  "react-dom@^19.0.0",
10
10
  "@earendil-works/pi-durable@~1.2.0",
@@ -12,11 +12,11 @@
12
12
  "lucide-react@^1.51.0"
13
13
  ],
14
14
  "registryDependencies": [
15
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.1/r/theme.json",
16
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.1/r/pi-chat.json",
17
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.1/r/pi-workspace.json",
18
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.1/r/utils.json",
19
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.1/r/button.json",
15
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.3/r/theme.json",
16
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.3/r/pi-chat.json",
17
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.3/r/pi-workspace.json",
18
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.3/r/utils.json",
19
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.3/r/button.json",
20
20
  "message-scroller",
21
21
  "button",
22
22
  "toggle",