haze-ui 1.34.1 → 1.35.0

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.
Files changed (98) hide show
  1. package/README.md +26 -0
  2. package/dist/THIRD-PARTY-NOTICES.md +2 -1
  3. package/dist/components/Anchor/Anchor.js +84 -65
  4. package/dist/components/BackToTop/BackToTop.js +18 -12
  5. package/dist/components/Combobox/Combobox.js +109 -99
  6. package/dist/components/CopyButton/CopyButton.js +13 -12
  7. package/dist/components/DataTable/DataTable.js +16 -16
  8. package/dist/components/FloatButton/FloatButton.js +11 -19
  9. package/dist/components/FollowUpSuggestions/FollowUpSuggestions.js +45 -0
  10. package/dist/components/FollowUpSuggestions/index.js +3 -0
  11. package/dist/components/LocaleProvider/ar-sa.js +9 -0
  12. package/dist/components/LocaleProvider/de-de.js +9 -0
  13. package/dist/components/LocaleProvider/es-es.js +9 -0
  14. package/dist/components/LocaleProvider/fr-fr.js +9 -0
  15. package/dist/components/LocaleProvider/it-it.js +9 -0
  16. package/dist/components/LocaleProvider/ja-jp.js +9 -0
  17. package/dist/components/LocaleProvider/ko-kr.js +9 -0
  18. package/dist/components/LocaleProvider/locale.js +9 -0
  19. package/dist/components/LocaleProvider/pt-br.js +9 -0
  20. package/dist/components/LocaleProvider/ru-ru.js +9 -0
  21. package/dist/components/LocaleProvider/zh-cn.js +9 -0
  22. package/dist/components/NavigationBar/NavLink.js +15 -16
  23. package/dist/components/NotificationCenter/NotificationCenter.js +294 -0
  24. package/dist/components/NotificationCenter/index.js +3 -0
  25. package/dist/components/Tabs/Tab.js +58 -50
  26. package/dist/components/TaskList/TaskList.js +96 -0
  27. package/dist/components/TaskList/index.js +3 -0
  28. package/dist/components/Toggle/Toggle.js +8 -8
  29. package/dist/components/Toggle/ToggleCore.js +4 -5
  30. package/dist/components/Toolbar/ToolbarButton.js +7 -7
  31. package/dist/components/Toolbar/ToolbarToggle.js +9 -9
  32. package/dist/components/Tree/Tree.js +159 -137
  33. package/dist/components/Tree/index.js +2 -1
  34. package/dist/components/Tree/useTree.js +32 -0
  35. package/dist/components/Tree/utils.js +21 -11
  36. package/dist/css/anchor.css +1 -1
  37. package/dist/css/combobox.css +1 -1
  38. package/dist/css/data-table.css +1 -1
  39. package/dist/css/follow-up-suggestions.css +4 -0
  40. package/dist/css/notification-center.css +5 -0
  41. package/dist/css/task-list.css +4 -0
  42. package/dist/css/tokens.css +1 -0
  43. package/dist/css-manifest.json +21 -0
  44. package/dist/haze-ui.css +17 -3
  45. package/dist/headless/index.js +2 -1
  46. package/dist/hooks/index.js +25 -0
  47. package/dist/hooks/useAnchorSpy.js +49 -0
  48. package/dist/hooks/useElementSize.js +31 -0
  49. package/dist/hooks/useIdle.js +45 -0
  50. package/dist/hooks/useMutationObserver.js +31 -0
  51. package/dist/hooks/useNetwork.js +28 -0
  52. package/dist/hooks/useOrientation.js +27 -0
  53. package/dist/hooks/useResizeObserver.js +16 -0
  54. package/dist/icons/index.js +9 -0
  55. package/dist/index.js +323 -318
  56. package/dist/mcp-docs.json +535 -13
  57. package/dist/registry.json +387 -7
  58. package/dist/sitemap.xml +6 -0
  59. package/dist/tokens/index.js +2 -1
  60. package/dist/tokens/zoom.js +6 -0
  61. package/dist/types/components/Anchor/Anchor.d.ts +5 -1
  62. package/dist/types/components/BackToTop/BackToTop.d.ts +23 -5
  63. package/dist/types/components/Combobox/Combobox.d.ts +17 -1
  64. package/dist/types/components/ConfigProvider/ConfigContext.d.ts +6 -0
  65. package/dist/types/components/CopyButton/CopyButton.d.ts +16 -4
  66. package/dist/types/components/DataTable/DataTable.bench.d.ts +1 -0
  67. package/dist/types/components/FloatButton/FloatButton.d.ts +25 -5
  68. package/dist/types/components/FollowUpSuggestions/FollowUpSuggestions.d.ts +27 -0
  69. package/dist/types/components/FollowUpSuggestions/index.d.ts +2 -0
  70. package/dist/types/components/LocaleProvider/locale.d.ts +9 -0
  71. package/dist/types/components/NavigationBar/NavLink.d.ts +24 -32
  72. package/dist/types/components/NotificationCenter/NotificationCenter.d.ts +111 -0
  73. package/dist/types/components/NotificationCenter/index.d.ts +2 -0
  74. package/dist/types/components/Select/Select.bench.d.ts +1 -0
  75. package/dist/types/components/Tabs/Tab.d.ts +26 -4
  76. package/dist/types/components/TaskList/TaskList.d.ts +50 -0
  77. package/dist/types/components/TaskList/index.d.ts +2 -0
  78. package/dist/types/components/Toggle/Toggle.d.ts +15 -8
  79. package/dist/types/components/Toggle/ToggleCore.d.ts +16 -7
  80. package/dist/types/components/Toolbar/ToolbarButton.d.ts +17 -5
  81. package/dist/types/components/Toolbar/ToolbarToggle.d.ts +22 -5
  82. package/dist/types/components/Tree/Tree.d.ts +1 -1
  83. package/dist/types/components/Tree/index.d.ts +4 -1
  84. package/dist/types/components/Tree/types.d.ts +20 -0
  85. package/dist/types/components/Tree/useTree.d.ts +96 -0
  86. package/dist/types/components/Tree/utils.d.ts +7 -0
  87. package/dist/types/components/VirtualList/VirtualList.bench.d.ts +1 -0
  88. package/dist/types/headless/index.d.ts +16 -0
  89. package/dist/types/hooks/index.d.ts +4 -0
  90. package/dist/types/hooks/useAnchorSpy.d.ts +62 -0
  91. package/dist/types/icons/index.d.ts +138 -0
  92. package/dist/types/index.d.ts +10 -2
  93. package/dist/types/tokens/index.d.ts +1 -0
  94. package/dist/types/tokens/zoom.d.ts +33 -0
  95. package/dist/types/utils/deprecate.d.ts +13 -0
  96. package/dist/types/utils/portal.d.ts +29 -0
  97. package/dist/utils/portal.js +13 -0
  98. package/package.json +40 -8
@@ -66,6 +66,53 @@
66
66
  }
67
67
  ]
68
68
  },
69
+ {
70
+ "name": "ai-workbench-block",
71
+ "type": "registry:block",
72
+ "title": "AI workbench block (Workbench shell + activity rail + Sources dock)",
73
+ "description": "The `Workbench` shell as a double-column AI desk — an `ActivityRail` + `ConversationList` sidebar, a scripted `ThinkingIndicator` → `ToolCallCard` → `StreamingText` review turn, a docked `Sources` citations panel, and `PromptInput` + `ModelPicker` as the composer.",
74
+ "author": "wmzy <https://github.com/wmzy>",
75
+ "docs": "The `Workbench` shell as a double-column AI desk — an `ActivityRail` + `ConversationList` sidebar, a scripted `ThinkingIndicator` → `ToolCallCard` → `StreamingText` review turn, a docked `Sources` citations panel, and `PromptInput` + `ModelPicker` as the composer.\n\n## Install\n\n```bash\nnpx shadcn@latest add wmzy/haze-ui/ai-workbench-block\n```\n\nThe full-page source lands at `components/block/haze/ai-workbench-block.tsx` (via the `@block/` target placeholder from `components.json`) — vendored, so you own and edit it. Component imports are rewritten to the `haze-ui` npm package, which is installed automatically together with `lucide-react` and `react-use-control`.\n\nThe file imports the stylesheet of every component it renders (`haze-ui/css/tokens.css` + 20 component css files), so no extra stylesheet setup is needed.\n\n## Theming\n\nThis wrapper re-exports the 'lightTheme' (or 'darkTheme'), 'spacing' and\n'typography' classes from 'haze-ui' and imports 'haze-ui/css/tokens.css'.\n\nActivate the design tokens by applying the classes to a root element:\n\n```jsx\nimport { lightTheme, spacing, typography } from 'haze-ui';\n\n// on <body> or any root container — darkTheme is a drop-in swap\n<div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n```\n\nLive preview and source: https://wmzy.github.io/haze-ui/#/blocks — https://github.com/wmzy/haze-ui/tree/main/registry",
76
+ "categories": [
77
+ "blocks",
78
+ "agent",
79
+ "layout"
80
+ ],
81
+ "dependencies": [
82
+ "haze-ui",
83
+ "lucide-react",
84
+ "react-use-control"
85
+ ],
86
+ "registryDependencies": [
87
+ "wmzy/haze-ui/workbench",
88
+ "wmzy/haze-ui/resizable",
89
+ "wmzy/haze-ui/activity-rail",
90
+ "wmzy/haze-ui/status-bar",
91
+ "wmzy/haze-ui/tab-bar",
92
+ "wmzy/haze-ui/conversation-list",
93
+ "wmzy/haze-ui/chat-container",
94
+ "wmzy/haze-ui/chat-message",
95
+ "wmzy/haze-ui/streaming-text",
96
+ "wmzy/haze-ui/thinking-indicator",
97
+ "wmzy/haze-ui/tool-call-card",
98
+ "wmzy/haze-ui/prompt-input",
99
+ "wmzy/haze-ui/model-picker",
100
+ "wmzy/haze-ui/sources",
101
+ "wmzy/haze-ui/avatar",
102
+ "wmzy/haze-ui/badge",
103
+ "wmzy/haze-ui/button",
104
+ "wmzy/haze-ui/empty",
105
+ "wmzy/haze-ui/spinner",
106
+ "wmzy/haze-ui/tooltip"
107
+ ],
108
+ "files": [
109
+ {
110
+ "path": "components/block/haze/ai-workbench-block.tsx",
111
+ "content": "// haze-ui registry block — generated by scripts/generate-registry.mjs\n// from src/views/Blocks/blocks/AiWorkbenchBlock.tsx.\n//\n// Unlike the component items (thin re-export wrappers over the haze-ui npm\n// package), a block is VENDORED source: a full page you own and edit.\n// The component imports are rewritten from the docs site's '@/lib' alias\n// to the published 'haze-ui' package and every stylesheet of the page's\n// component closure is imported below, so the file is self-contained.\n// npm dependencies beyond haze-ui: lucide-react, react-use-control.\n//\n// Theming (once per app): apply the token classes to a root element, e.g.\n// import { lightTheme, spacing, typography } from 'haze-ui';\n// <div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n// (darkTheme is a drop-in swap).\n\nimport 'haze-ui/css/tokens.css';\nimport 'haze-ui/css/activity-rail.css';\nimport 'haze-ui/css/avatar.css';\nimport 'haze-ui/css/badge.css';\nimport 'haze-ui/css/button.css';\nimport 'haze-ui/css/chat-container.css';\nimport 'haze-ui/css/chat-message.css';\nimport 'haze-ui/css/conversation-list.css';\nimport 'haze-ui/css/empty.css';\nimport 'haze-ui/css/model-picker.css';\nimport 'haze-ui/css/prompt-input.css';\nimport 'haze-ui/css/resizable.css';\nimport 'haze-ui/css/sources.css';\nimport 'haze-ui/css/spinner.css';\nimport 'haze-ui/css/status-bar.css';\nimport 'haze-ui/css/streaming-text.css';\nimport 'haze-ui/css/tab-bar.css';\nimport 'haze-ui/css/thinking-indicator.css';\nimport 'haze-ui/css/tool-call-card.css';\nimport 'haze-ui/css/tooltip.css';\nimport 'haze-ui/css/workbench.css';\n\nimport type { ReactNode } from 'react';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { useControl } from 'react-use-control';\nimport {\n MessagesSquare,\n GitPullRequest,\n FileSearch,\n Settings,\n PanelLeft,\n} from 'lucide-react';\n\nimport {\n ActivityRail,\n ActivityRailItem,\n Avatar,\n Badge,\n Button,\n ChatContainer,\n ChatMessage,\n ConversationItem,\n ConversationList,\n Empty,\n ModelPicker,\n PromptInput,\n Sources,\n StatusBar,\n StatusItem,\n StreamingText,\n TabBar,\n TabBarItem,\n ThinkingIndicator,\n ToolCallCard,\n Workbench,\n} from 'haze-ui';\n\n/*\n * AI workbench block: the Workbench shell assembled as a double-column AI\n * coding-review desk — activity rail + ConversationList sidebar on the left,\n * the chat column (ChatContainer + scripted ThinkingIndicator → ToolCallCard\n * → StreamingText turn) in the middle, a docked Sources citations panel on\n * the right, PromptInput + ModelPicker as the composer and a StatusBar\n * under everything. Same no-network script idea as the chat block, but the\n * layout is the point here: resizable columns, collapsible sidebar, mobile\n * overlays — all Workbench features, not local CSS.\n */\n\ntype Phase = 'idle' | 'thinking' | 'tool' | 'answering';\n\ntype Entry =\n | { id: number; kind: 'user'; text: string }\n | { id: number; kind: 'thinking' }\n | { id: number; kind: 'thought'; seconds: number }\n | { id: number; kind: 'tool'; status: 'running' | 'done' }\n | { id: number; kind: 'answer'; text: string; phase: 'streaming' | 'done' };\n\ntype Conversation = { id: string; title: string; subtitle: string; time: string };\n\ntype ToolScript = { thoughtSeconds: number; runMs: number };\n\ntype TurnScript = { thinkMs: number; tool?: ToolScript; answer: string };\n\ntype Model = {\n value: string;\n label: string;\n contextLength: string;\n /** Characters per StreamingText tick — the model card changes pacing. */\n cps: number;\n};\n\nconst AGENT_NAME = 'Nimbus Review Copilot';\nconst USER_NAME = 'Dev';\n\nconst MODELS: Model[] = [\n { value: 'nimbus-4o', label: 'Nimbus 4o', contextLength: '128k', cps: 14 },\n { value: 'nimbus-4o-mini', label: 'Nimbus 4o mini', contextLength: '32k', cps: 26 },\n { value: 'nimbus-reasoner', label: 'Nimbus reasoner', contextLength: '200k', cps: 9 },\n];\n\nconst REVIEWERS = ['Ana Petrova', 'Ben Okafor', 'Chen Wei', 'Dana Silva'];\n\nconst TOUR_QUESTION = 'Review PR #482 — is the new retry loop safe to ship?';\n\nconst TOOL_INPUT = `{ \"repo\": \"nimbus-gateway\",\n \"pull\": 482,\n \"focus\": \"retry.go\" }`;\n\nconst TOOL_OUTPUT = `retry.go +38 −4\nbackoff cap 2s → 8s\npayload policy unchanged (verbatim)\ntests 11 passed, 2 skipped`;\n\nconst TOUR_ANSWER =\n 'Mostly safe, with one sharp edge. The doubled backoff cap is fine and the new jitter prevents thundering herds, but the webhook path still retries the 1.2 MB payload verbatim — exactly the failure mode from the week-42 incident. Key retries on the Idempotency-Key header and fall back to the batched format after two failures; that mirrors the fix that recovered Ferrite Labs. The two skipped tests are the webhook ones, so add coverage there before merge.';\n\nconst FOLLOW_UP_ANSWER =\n 'Checked the CI history for this branch: the skipped webhook tests have been skipped for three weeks (imported flag), not flaked. I can draft the idempotency patch on a side branch and link it to the PR — want the diff posted here or as review comments?';\n\nconst SOURCE_ITEMS = [\n {\n id: 'retry-go',\n title: 'gateway/retry.go @ v2.3',\n url: '#ai-workbench',\n snippet: 'backoff := base * math.Pow(2, float64(attempt)); if backoff > cap { backoff = cap }',\n },\n {\n id: 'postmortem',\n title: 'postmortem/webhook-410.md',\n url: '#ai-workbench',\n snippet: 'The 1.2 MB body is rejected by the gateway before ingest; every retry re-sends it verbatim.',\n },\n {\n id: 'runbook',\n title: 'runbook/retries.md',\n url: '#ai-workbench',\n snippet: 'Retries must be idempotent — key them on Idempotency-Key, never on attempt count.',\n },\n];\n\nconst SEED_CONVERSATIONS: Conversation[] = [\n { id: 'retry-loop', title: 'PR #482 · retry loop safety', subtitle: 'nimbus-gateway', time: '2m' },\n { id: 'flaky-e2e', title: 'Flaky e2e: checkout spec', subtitle: 'nimbus-web', time: '1h' },\n { id: 'migration-notes', title: 'Draft v2.3 migration notes', subtitle: 'docs', time: '3h' },\n { id: 'secrets', title: 'Rotate leaked CI secrets', subtitle: 'infra', time: 'yesterday' },\n];\n\nconst SEED_ENTRIES: Record<string, Entry[]> = {\n 'retry-loop': [{ id: 1, kind: 'user', text: TOUR_QUESTION }],\n 'flaky-e2e': [\n { id: 1, kind: 'user', text: 'The checkout spec failed twice on main last night — same assertion?' },\n {\n id: 2,\n kind: 'answer',\n phase: 'done',\n text: 'Different assertions, same cause: both runs raced the stubbed clock at midnight UTC. Freeze the clock fixture and the pair of failures collapses into one deterministic case.',\n },\n ],\n};\n\nconst TOUR_SCRIPT: TurnScript = {\n thinkMs: 900,\n tool: { thoughtSeconds: 4, runMs: 1100 },\n answer: TOUR_ANSWER,\n};\n\nconst FOLLOW_UP_SCRIPT: TurnScript = {\n thinkMs: 700,\n tool: { thoughtSeconds: 3, runMs: 900 },\n answer: FOLLOW_UP_ANSWER,\n};\n\n/** Deterministic fake clock (chat-block pattern): stable timestamps, no\n * Date in the render. */\nfunction stamp(id: number): string {\n return `14:${String(5 + (id % 50)).padStart(2, '0')}`;\n}\n\nconst sleep = (ms: number) =>\n new Promise<void>((resolve) => {\n setTimeout(resolve, ms);\n });\n\n/** Async reviewer-mention source for PromptInput's @ trigger — stale\n * responses are dropped by the component itself. */\nfunction suggestReviewer(trigger: string, query: string): Promise<string[]> {\n return sleep(200).then(() => {\n if (trigger !== '@') return [];\n const q = query.toLowerCase();\n return REVIEWERS.filter((name) => name.toLowerCase().startsWith(q));\n });\n}\n\nfunction Mono({ children }: { children: ReactNode }) {\n return (\n <code\n style={{\n fontFamily: 'var(--haze-font-mono)',\n fontSize: 'var(--haze-text-xs)',\n whiteSpace: 'pre',\n color: 'var(--haze-color-text-secondary)',\n }}\n >\n {children}\n </code>\n );\n}\n\nexport default function AiWorkbenchBlock() {\n const [conversations, setConversations] = useState(SEED_CONVERSATIONS);\n const [entries, setEntries] = useState(SEED_ENTRIES);\n const [phases, setPhases] = useState<Record<string, Phase>>({});\n // User-facing state, one Control each: active conversation, picked model,\n // composer draft/tags, workbench chrome.\n const [activeId, setActiveId] = useControl(undefined, 'retry-loop');\n const [modelValue, , modelCtrl] = useControl(undefined, MODELS[0]!.value);\n const [draft, , draftCtrl] = useControl(undefined, '');\n const [, setTags, tagsCtrl] = useControl(undefined, ['repo: nimbus-gateway', 'PR #482']);\n const [sidebarCollapsed, setSidebarCollapsed, sidebarCtrl] = useControl(undefined, false);\n const [mobileSidebarOpen, setMobileSidebarOpen] = useControl(undefined, false);\n\n const model = MODELS.find((m) => m.value === modelValue) ?? MODELS[0]!;\n\n const runRef = useRef(0);\n const idRef = useRef(100);\n const nextId = () => (idRef.current += 1);\n\n // Invalidation guard (chat-block pattern): unmount / stop / new run bumps\n // the counter so cancelled sleeps never mutate state.\n useEffect(() => () => {\n runRef.current += 1;\n }, []);\n\n const append = useCallback((conv: string, entry: Entry) => {\n setEntries((prev) => ({ ...prev, [conv]: [...(prev[conv] ?? []), entry] }));\n }, []);\n\n const patch = useCallback(\n (conv: string, id: number, updater: (entry: Entry) => Entry) => {\n setEntries((prev) => ({\n ...prev,\n [conv]: (prev[conv] ?? []).map((entry) =>\n entry.id === id ? updater(entry) : entry\n ),\n }));\n },\n []\n );\n\n const setPhaseFor = useCallback((conv: string, phase: Phase) => {\n setPhases((prev) => ({ ...prev, [conv]: phase }));\n }, []);\n\n const playTurn = useCallback(\n async (conv: string, script: TurnScript) => {\n const run = (runRef.current += 1);\n const alive = () => run === runRef.current;\n\n setPhaseFor(conv, 'thinking');\n const thinkingId = nextId();\n append(conv, { id: thinkingId, kind: 'thinking' });\n await sleep(script.thinkMs);\n if (!alive()) return;\n\n const thoughtSeconds = script.tool\n ? script.tool.thoughtSeconds\n : Math.max(1, Math.round(script.thinkMs / 1000));\n patch(conv, thinkingId, () => ({\n id: thinkingId,\n kind: 'thought',\n seconds: thoughtSeconds,\n }));\n\n if (script.tool) {\n setPhaseFor(conv, 'tool');\n const toolId = nextId();\n append(conv, { id: toolId, kind: 'tool', status: 'running' });\n await sleep(script.tool.runMs);\n if (!alive()) return;\n patch(conv, toolId, () => ({ id: toolId, kind: 'tool', status: 'done' }));\n }\n\n setPhaseFor(conv, 'answering');\n append(conv, { id: nextId(), kind: 'answer', text: script.answer, phase: 'streaming' });\n },\n [append, patch, setPhaseFor]\n );\n\n // The scripted review plays once on mount in the seeded conversation —\n // kicked from a timer so the effect body itself never writes state.\n useEffect(() => {\n const timer = setTimeout(() => {\n void playTurn('retry-loop', TOUR_SCRIPT);\n }, 0);\n return () => clearTimeout(timer);\n }, [playTurn]);\n\n const phase = phases[activeId] ?? 'idle';\n const generating = phase !== 'idle';\n\n const send = (text: string) => {\n if (!text.trim()) return;\n append(activeId, { id: nextId(), kind: 'user', text });\n void playTurn(activeId, FOLLOW_UP_SCRIPT);\n };\n\n const stopGeneration = () => {\n runRef.current += 1;\n setPhaseFor(activeId, 'idle');\n setEntries((prev) => {\n const list = [...(prev[activeId] ?? [])];\n while (list.length > 0) {\n const last = list[list.length - 1]!;\n const incomplete =\n last.kind === 'thinking' ||\n (last.kind === 'tool' && last.status === 'running') ||\n (last.kind === 'answer' && last.phase === 'streaming');\n if (!incomplete) break;\n list.pop();\n }\n return { ...prev, [activeId]: list };\n });\n };\n\n const newChat = () => {\n const id = `review-${conversations.length + 1}`;\n setConversations((prev) => [\n { id, title: 'New review session', subtitle: 'no repo bound', time: 'now' },\n ...prev,\n ]);\n setActiveId(id);\n };\n\n const markAnswerDone = (id: number) => {\n patch(activeId, id, (entry) =>\n entry.kind === 'answer' ? { ...entry, phase: 'done' } : entry\n );\n setPhaseFor(activeId, 'idle');\n };\n\n const renderEntry = (entry: Entry) => {\n switch (entry.kind) {\n case 'user':\n return (\n <ChatMessage\n key={entry.id}\n role='user'\n name={USER_NAME}\n status='sent'\n timestamp={stamp(entry.id)}\n avatar={<Avatar size='sm' fallback='DV' />}\n >\n <div style={{ fontSize: 'var(--haze-text-sm)' }}>{entry.text}</div>\n </ChatMessage>\n );\n case 'thinking':\n return <ThinkingIndicator key={entry.id} text='Reading the diff…' />;\n case 'thought':\n return (\n <div\n key={entry.id}\n data-slot='workbench-thought'\n style={{\n padding: 'var(--haze-space-1) 0',\n fontSize: 'var(--haze-text-xs)',\n color: 'var(--haze-color-text-muted)',\n }}\n >\n Thought for {entry.seconds}s\n </div>\n );\n case 'tool':\n return (\n <ToolCallCard\n key={entry.id}\n name='read_diff'\n status={entry.status}\n input={<Mono>{TOOL_INPUT}</Mono>}\n output={entry.status === 'done' ? <Mono>{TOOL_OUTPUT}</Mono> : undefined}\n />\n );\n case 'answer':\n return (\n <ChatMessage\n key={entry.id}\n role='assistant'\n name={AGENT_NAME}\n timestamp={stamp(entry.id)}\n copyable\n avatar={<Avatar size='sm' fallback='RC' />}\n >\n {entry.phase === 'streaming' ? (\n <StreamingText\n text={entry.text}\n speed={model.cps}\n onComplete={() => markAnswerDone(entry.id)}\n />\n ) : (\n <div style={{ fontSize: 'var(--haze-text-sm)' }}>{entry.text}</div>\n )}\n {entry.phase === 'done' && activeId === 'retry-loop' && (\n <Sources\n compact\n items={SOURCE_ITEMS}\n label='Citations for this review'\n style={{ marginTop: 'var(--haze-space-2)' }}\n />\n )}\n </ChatMessage>\n );\n }\n };\n\n const activeConversation =\n conversations.find((conversation) => conversation.id === activeId) ?? conversations[0]!;\n\n return (\n <div\n data-slot='ai-workbench-block'\n style={{ height: '100%', fontFamily: 'var(--haze-font-sans)', color: 'var(--haze-color-text)' }}\n >\n <Workbench\n style={{ height: '100%' }}\n sidebarCollapsed={sidebarCtrl}\n mobileSidebarOpen={mobileSidebarOpen}\n activityBar={\n <ActivityRail aria-label='Workbench views'>\n <ActivityRailItem\n icon={<MessagesSquare size={20} />}\n label='Conversations'\n active\n badge={3}\n />\n <ActivityRailItem icon={<GitPullRequest size={20} />} label='Open reviews' badge={5} />\n <ActivityRailItem icon={<FileSearch size={20} />} label='Code search' />\n <ActivityRailItem slot='end' icon={<Settings size={20} />} label='Settings' />\n </ActivityRail>\n }\n sidebar={\n <div\n data-slot='workbench-conversations'\n style={{\n height: '100%',\n display: 'flex',\n flexDirection: 'column',\n background: 'var(--haze-color-bg)',\n }}\n >\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: 'var(--haze-space-2)',\n padding: 'var(--haze-space-3) var(--haze-space-3) var(--haze-space-2)',\n }}\n >\n <span\n style={{\n flex: 1,\n fontSize: 'var(--haze-text-xs)',\n fontWeight: 'var(--haze-weight-semibold)',\n letterSpacing: '0.04em',\n textTransform: 'uppercase',\n color: 'var(--haze-color-text-muted)',\n }}\n >\n Conversations\n </span>\n <Button size='sm' variant='outline' onClick={newChat}>\n New\n </Button>\n </div>\n {/* ConversationList takes className only — the rail wraps it and\n owns the overflow. */}\n <div\n style={{\n flex: 1,\n minHeight: 0,\n overflowY: 'auto',\n padding: '0 var(--haze-space-2)',\n }}\n >\n <ConversationList>\n {conversations.map((conversation) => (\n <ConversationItem\n key={conversation.id}\n title={conversation.title}\n subtitle={conversation.subtitle}\n active={conversation.id === activeId}\n onClick={() => setActiveId(conversation.id)}\n end={\n <span\n style={{\n fontSize: 'var(--haze-text-xs)',\n color: 'var(--haze-color-text-muted)',\n }}\n >\n {conversation.time}\n </span>\n }\n />\n ))}\n </ConversationList>\n </div>\n </div>\n }\n auxiliaryBar={\n <div\n data-slot='workbench-sources'\n style={{\n height: '100%',\n display: 'flex',\n flexDirection: 'column',\n background: 'var(--haze-color-bg)',\n }}\n >\n <div\n style={{\n padding: 'var(--haze-space-3)',\n borderBottom: '1px solid var(--haze-color-border)',\n fontSize: 'var(--haze-text-xs)',\n fontWeight: 'var(--haze-weight-semibold)',\n letterSpacing: '0.04em',\n textTransform: 'uppercase',\n color: 'var(--haze-color-text-muted)',\n }}\n >\n Citations\n </div>\n <div style={{ flex: 1, minHeight: 0, overflowY: 'auto', padding: 'var(--haze-space-3)' }}>\n <Sources items={SOURCE_ITEMS} label='Sources cited by the copilot' />\n <p\n style={{\n margin: 'var(--haze-space-3) 0 0',\n fontSize: 'var(--haze-text-xs)',\n color: 'var(--haze-color-text-muted)',\n }}\n >\n Drag the panel edge to resize; the handle collapses it entirely.\n </p>\n </div>\n </div>\n }\n statusBar={\n <StatusBar\n left={\n <>\n <StatusItem priority>{model.label}</StatusItem>\n <StatusItem>ctx {model.contextLength}</StatusItem>\n <StatusItem>grounding: repo + docs</StatusItem>\n </>\n }\n right={\n <>\n <StatusItem>main · clean</StatusItem>\n <StatusItem priority>5 reviews open</StatusItem>\n </>\n }\n />\n }\n tabBar={\n <TabBar aria-label='Workbench navigation'>\n <TabBarItem\n icon={<MessagesSquare size={18} />}\n label='Chats'\n badge={3}\n active={mobileSidebarOpen}\n onSelect={() => setMobileSidebarOpen((v) => !v)}\n />\n <TabBarItem\n icon={<GitPullRequest size={18} />}\n label='Reviews'\n badge={5}\n onSelect={() => setMobileSidebarOpen(true)}\n />\n </TabBar>\n }\n >\n <div\n data-slot='workbench-chat'\n style={{\n height: '100%',\n minWidth: 0,\n display: 'flex',\n flexDirection: 'column',\n background: 'var(--haze-color-bg-subtle)',\n }}\n >\n <header\n data-slot='workbench-chat-header'\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: 'var(--haze-space-2)',\n padding: 'var(--haze-space-2) var(--haze-space-3)',\n borderBottom: '1px solid var(--haze-color-border)',\n background: 'var(--haze-color-bg)',\n }}\n >\n <Button\n size='sm'\n variant='ghost'\n square\n aria-label={sidebarCollapsed ? 'Show conversations' : 'Hide conversations'}\n onClick={() => setSidebarCollapsed((v) => !v)}\n >\n <PanelLeft size={16} aria-hidden='true' />\n </Button>\n <div style={{ minWidth: 0 }}>\n <div\n style={{\n fontSize: 'var(--haze-text-sm)',\n fontWeight: 'var(--haze-weight-semibold)',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n }}\n >\n {activeConversation.title}\n </div>\n <div\n style={{\n fontSize: 'var(--haze-text-xs)',\n color: 'var(--haze-color-text-muted)',\n }}\n >\n {activeConversation.subtitle} · scripted demo, no network\n </div>\n </div>\n <span style={{ flex: 1 }} />\n {generating ? (\n <Badge color='info'>Generating…</Badge>\n ) : (\n <Badge color='success'>ready</Badge>\n )}\n </header>\n\n <ChatContainer\n role='log'\n aria-label='Review conversation transcript'\n style={{ flex: 1, minHeight: 0 }}\n >\n {(entries[activeId] ?? []).length === 0 ? (\n <div\n style={{\n flex: 1,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: '240px',\n }}\n >\n <Empty description='Ask about a pull request, a test or a runbook — replies are scripted, nothing leaves the page.' />\n </div>\n ) : (\n (entries[activeId] ?? []).map(renderEntry)\n )}\n </ChatContainer>\n\n <footer\n data-slot='workbench-composer'\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: 'var(--haze-space-2)',\n padding: 'var(--haze-space-3)',\n borderTop: '1px solid var(--haze-color-border)',\n background: 'var(--haze-color-bg)',\n }}\n >\n <div style={{ width: '180px', flexShrink: 0 }}>\n <ModelPicker\n value={modelCtrl}\n options={MODELS.map(({ value, label, contextLength }) => ({\n value,\n label,\n contextLength,\n }))}\n />\n </div>\n <div style={{ flex: 1, minWidth: 0 }}>\n <PromptInput\n value={draftCtrl}\n tags={tagsCtrl}\n onTagsChange={setTags}\n triggers={['@']}\n getSuggestions={suggestReviewer}\n onSubmit={() => send(draft)}\n placeholder={generating ? 'Copilot is responding…' : 'Ask, or @ a reviewer…'}\n disabled={generating}\n maxRows={4}\n />\n </div>\n {generating && (\n <Button size='sm' variant='outline' onClick={stopGeneration}>\n Stop\n </Button>\n )}\n </footer>\n </div>\n </Workbench>\n </div>\n );\n}\n",
112
+ "type": "registry:block"
113
+ }
114
+ ]
115
+ },
69
116
  {
70
117
  "name": "alert",
71
118
  "type": "registry:ui",
@@ -303,7 +350,7 @@
303
350
  "title": "Haze UI base (tokens + every component wrapper)",
304
351
  "description": "One-command setup of the whole haze-ui design system: theme tokens plus a thin wrapper for every styled component, pulled in through registryDependencies.",
305
352
  "author": "wmzy <https://github.com/wmzy>",
306
- "docs": "One command installs the entire haze-ui design system: the theme tokens\n(this item ships the token-activation wrapper) plus a thin re-export\nwrapper for every styled component — 153 registry items pulled\nin through `registryDependencies`, resolved recursively by the CLI.\n\n## Install\n\n```bash\nnpx shadcn@latest add wmzy/haze-ui/base\n```\n\nEach component wrapper lands at `components/ui/haze/<component>.tsx` and\nre-exports its component from the `haze-ui` npm package (installed\nautomatically), importing its stylesheet — no other setup is needed.\n\n## Theming\n\nThis wrapper re-exports the 'lightTheme' (or 'darkTheme'), 'spacing' and\n'typography' classes from 'haze-ui' and imports 'haze-ui/css/tokens.css'.\n\nActivate the design tokens by applying the classes to a root element:\n\n```jsx\nimport { lightTheme, spacing, typography } from 'haze-ui';\n\n// on <body> or any root container — darkTheme is a drop-in swap\n<div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n```\n\nDetails: https://github.com/wmzy/haze-ui/tree/main/registry",
353
+ "docs": "One command installs the entire haze-ui design system: the theme tokens\n(this item ships the token-activation wrapper) plus a thin re-export\nwrapper for every styled component — 156 registry items pulled\nin through `registryDependencies`, resolved recursively by the CLI.\n\n## Install\n\n```bash\nnpx shadcn@latest add wmzy/haze-ui/base\n```\n\nEach component wrapper lands at `components/ui/haze/<component>.tsx` and\nre-exports its component from the `haze-ui` npm package (installed\nautomatically), importing its stylesheet — no other setup is needed.\n\n## Theming\n\nThis wrapper re-exports the 'lightTheme' (or 'darkTheme'), 'spacing' and\n'typography' classes from 'haze-ui' and imports 'haze-ui/css/tokens.css'.\n\nActivate the design tokens by applying the classes to a root element:\n\n```jsx\nimport { lightTheme, spacing, typography } from 'haze-ui';\n\n// on <body> or any root container — darkTheme is a drop-in swap\n<div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n```\n\nDetails: https://github.com/wmzy/haze-ui/tree/main/registry",
307
354
  "categories": [
308
355
  "theme",
309
356
  "setup"
@@ -371,6 +418,7 @@
371
418
  "wmzy/haze-ui/file-preview",
372
419
  "wmzy/haze-ui/flex",
373
420
  "wmzy/haze-ui/float-button",
421
+ "wmzy/haze-ui/follow-up-suggestions",
374
422
  "wmzy/haze-ui/form",
375
423
  "wmzy/haze-ui/grid",
376
424
  "wmzy/haze-ui/heatmap",
@@ -398,6 +446,7 @@
398
446
  "wmzy/haze-ui/model-picker",
399
447
  "wmzy/haze-ui/navigation-bar",
400
448
  "wmzy/haze-ui/navigation-menu",
449
+ "wmzy/haze-ui/notification-center",
401
450
  "wmzy/haze-ui/number-input",
402
451
  "wmzy/haze-ui/otp-input",
403
452
  "wmzy/haze-ui/pagination",
@@ -442,6 +491,7 @@
442
491
  "wmzy/haze-ui/tag",
443
492
  "wmzy/haze-ui/tag-group",
444
493
  "wmzy/haze-ui/tag-input",
494
+ "wmzy/haze-ui/task-list",
445
495
  "wmzy/haze-ui/textarea",
446
496
  "wmzy/haze-ui/thinking-indicator",
447
497
  "wmzy/haze-ui/time-grid",
@@ -469,7 +519,7 @@
469
519
  "files": [
470
520
  {
471
521
  "path": "components/haze/base.tsx",
472
- "content": "// haze-ui registry base — generated by scripts/generate-registry.mjs.\n//\n// Meta item: 'npx shadcn add wmzy/haze-ui/base' installs the entire\n// design system — this theme-activation wrapper plus, through\n// registryDependencies, a thin wrapper for every styled component under\n// components/ui/haze/. The implementation keeps coming from the haze-ui\n// npm package; edit the wrappers to customize.\n//\n// Theming (once per app): apply the token classes to a root element, e.g.\n// import { lightTheme, spacing, typography } from 'haze-ui';\n// <div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n// (darkTheme is a drop-in swap).\n\nimport { autoTheme, compact, cyanTheme, darkTheme, lightTheme, motion, motionPresets, orangeTheme, roseTheme, spacing, tealTheme, typography, violetTheme } from 'haze-ui';\n\nimport 'haze-ui/css/tokens.css';\n\nexport { autoTheme, compact, cyanTheme, darkTheme, lightTheme, motion, motionPresets, orangeTheme, roseTheme, spacing, tealTheme, typography, violetTheme };\n",
522
+ "content": "// haze-ui registry base — generated by scripts/generate-registry.mjs.\n//\n// Meta item: 'npx shadcn add wmzy/haze-ui/base' installs the entire\n// design system — this theme-activation wrapper plus, through\n// registryDependencies, a thin wrapper for every styled component under\n// components/ui/haze/. The implementation keeps coming from the haze-ui\n// npm package; edit the wrappers to customize.\n//\n// Theming (once per app): apply the token classes to a root element, e.g.\n// import { lightTheme, spacing, typography } from 'haze-ui';\n// <div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n// (darkTheme is a drop-in swap).\n\nimport { autoTheme, compact, cyanTheme, darkTheme, lightTheme, motion, motionPresets, orangeTheme, remScale, roseTheme, spacing, tealTheme, typography, violetTheme } from 'haze-ui';\n\nimport 'haze-ui/css/tokens.css';\n\nexport { autoTheme, compact, cyanTheme, darkTheme, lightTheme, motion, motionPresets, orangeTheme, remScale, roseTheme, spacing, tealTheme, typography, violetTheme };\n",
473
523
  "type": "registry:base"
474
524
  }
475
525
  ]
@@ -663,6 +713,43 @@
663
713
  }
664
714
  ]
665
715
  },
716
+ {
717
+ "name": "chat-block",
718
+ "type": "registry:block",
719
+ "title": "Chat block (ChatContainer + scripted AI turn + ConversationList rail)",
720
+ "description": "AI chat in one screen: a role=\"log\" `ChatContainer` of `ChatMessage`s, a scripted `ThinkingIndicator` → `ToolCallCard` → `StreamingText` turn, a `ChatInput` whose send flips to stop, and a `ConversationList` rail.",
721
+ "author": "wmzy <https://github.com/wmzy>",
722
+ "docs": "AI chat in one screen: a role=\"log\" `ChatContainer` of `ChatMessage`s, a scripted `ThinkingIndicator` → `ToolCallCard` → `StreamingText` turn, a `ChatInput` whose send flips to stop, and a `ConversationList` rail.\n\n## Install\n\n```bash\nnpx shadcn@latest add wmzy/haze-ui/chat-block\n```\n\nThe full-page source lands at `components/block/haze/chat-block.tsx` (via the `@block/` target placeholder from `components.json`) — vendored, so you own and edit it. Component imports are rewritten to the `haze-ui` npm package, which is installed automatically together with `react-use-control`.\n\nThe file imports the stylesheet of every component it renders (`haze-ui/css/tokens.css` + 12 component css files), so no extra stylesheet setup is needed.\n\n## Theming\n\nThis wrapper re-exports the 'lightTheme' (or 'darkTheme'), 'spacing' and\n'typography' classes from 'haze-ui' and imports 'haze-ui/css/tokens.css'.\n\nActivate the design tokens by applying the classes to a root element:\n\n```jsx\nimport { lightTheme, spacing, typography } from 'haze-ui';\n\n// on <body> or any root container — darkTheme is a drop-in swap\n<div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n```\n\nLive preview and source: https://wmzy.github.io/haze-ui/#/blocks — https://github.com/wmzy/haze-ui/tree/main/registry",
723
+ "categories": [
724
+ "blocks",
725
+ "agent"
726
+ ],
727
+ "dependencies": [
728
+ "haze-ui",
729
+ "react-use-control"
730
+ ],
731
+ "registryDependencies": [
732
+ "wmzy/haze-ui/chat-container",
733
+ "wmzy/haze-ui/chat-message",
734
+ "wmzy/haze-ui/streaming-text",
735
+ "wmzy/haze-ui/thinking-indicator",
736
+ "wmzy/haze-ui/tool-call-card",
737
+ "wmzy/haze-ui/chat-input",
738
+ "wmzy/haze-ui/conversation-list",
739
+ "wmzy/haze-ui/avatar",
740
+ "wmzy/haze-ui/badge",
741
+ "wmzy/haze-ui/button",
742
+ "wmzy/haze-ui/empty",
743
+ "wmzy/haze-ui/spinner"
744
+ ],
745
+ "files": [
746
+ {
747
+ "path": "components/block/haze/chat-block.tsx",
748
+ "content": "// haze-ui registry block — generated by scripts/generate-registry.mjs\n// from src/views/Blocks/blocks/ChatBlock.tsx.\n//\n// Unlike the component items (thin re-export wrappers over the haze-ui npm\n// package), a block is VENDORED source: a full page you own and edit.\n// The component imports are rewritten from the docs site's '@/lib' alias\n// to the published 'haze-ui' package and every stylesheet of the page's\n// component closure is imported below, so the file is self-contained.\n// npm dependencies beyond haze-ui: react-use-control.\n//\n// Theming (once per app): apply the token classes to a root element, e.g.\n// import { lightTheme, spacing, typography } from 'haze-ui';\n// <div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n// (darkTheme is a drop-in swap).\n\nimport 'haze-ui/css/tokens.css';\nimport 'haze-ui/css/avatar.css';\nimport 'haze-ui/css/badge.css';\nimport 'haze-ui/css/button.css';\nimport 'haze-ui/css/chat-container.css';\nimport 'haze-ui/css/chat-input.css';\nimport 'haze-ui/css/chat-message.css';\nimport 'haze-ui/css/conversation-list.css';\nimport 'haze-ui/css/empty.css';\nimport 'haze-ui/css/spinner.css';\nimport 'haze-ui/css/streaming-text.css';\nimport 'haze-ui/css/thinking-indicator.css';\nimport 'haze-ui/css/tool-call-card.css';\n\nimport type { ReactNode } from 'react';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { useControl } from 'react-use-control';\n\nimport {\n Avatar,\n Badge,\n Button,\n ChatContainer,\n ChatInput,\n ChatMessage,\n ConversationItem,\n ConversationList,\n Empty,\n StreamingText,\n ThinkingIndicator,\n ToolCallCard,\n} from 'haze-ui';\n\n/*\n * Chat block: the AI conversation family assembled into one screen — a\n * scrolling ChatContainer (role=\"log\") of ChatMessages, a scripted agent\n * turn that plays a ThinkingIndicator, a ToolCallCard (running → done)\n * and a StreamingText answer, plus a ChatInput whose send button flips\n * to a stop control while the turn runs. The right rail is a\n * ConversationList: switching or starting chats swaps the transcript,\n * and every reply is scripted — no network, timers only. All layout\n * glue is inline styles over --haze-* tokens so the block re-themes.\n */\n\ntype Phase = 'idle' | 'thinking' | 'tool' | 'answering';\n\ntype Entry =\n | { id: number; kind: 'user'; text: string }\n | { id: number; kind: 'thinking' }\n | { id: number; kind: 'thought'; seconds: number }\n | { id: number; kind: 'tool'; status: 'running' | 'done' }\n | { id: number; kind: 'answer'; text: string; phase: 'streaming' | 'done' };\n\ntype Conversation = {\n id: string;\n title: string;\n subtitle: string;\n time: string;\n};\n\ntype ToolScript = { thoughtSeconds: number; runMs: number };\n\ntype TurnScript = { thinkMs: number; tool?: ToolScript; answer: string };\n\nconst AGENT_NAME = 'Nimbus Copilot';\nconst USER_NAME = 'Ana';\n\nconst TOUR_QUESTION =\n 'Why did event volume drop for Ferrite Labs last week?';\n\nconst TOOL_INPUT = `{ \"workspace\": \"ferrite-labs\",\n \"metric\": \"ingested_events\",\n \"range\": \"14d\" }`;\n\nconst TOOL_OUTPUT = `wk 41 → 71,980 events\nwk 42 → 44,610 events (−38%)\nsource: webhook/v2, 3 collectors`;\n\nconst TOUR_ANSWER =\n 'Ferrite Labs lost 38% of ingested events in week 42 — all of it from the webhook source, while the SDK collectors stayed flat. The cliff starts Tuesday 09:40, which lines up with their v2.3 webhook rollout. The failing payload now sends a 1.2 MB body and gets rejected by the gateway before ingest. Retrying with the v2 batched format should recover it — want me to draft the migration note?';\n\nconst FOLLOW_UP_ANSWER =\n 'Scanned the last 24 hours across this workspace: ingest is flat everywhere else, so the Ferrite Labs cliff is still the only outlier. I can pin an annotation to the chart and alert you when the webhook error rate clears — say the word.';\n\nconst SEED_CONVERSATIONS: Conversation[] = [\n { id: 'anomaly', title: 'Ferrite Labs volume drop', subtitle: 'Investigating the week-42 cliff', time: '09:41' },\n { id: 'retention', title: 'Weekly retention digest', subtitle: 'Summarized last week', time: 'Yesterday' },\n { id: 'ingest', title: 'Ingest backfill window', subtitle: 'Resolved', time: 'Mon' },\n];\n\nconst SEED_ENTRIES: Record<string, Entry[]> = {\n anomaly: [{ id: 1, kind: 'user', text: TOUR_QUESTION }],\n retention: [\n { id: 2, kind: 'user', text: 'Give me the one-line retention story for last week.' },\n {\n id: 3,\n kind: 'answer',\n text: 'Rolling 7-day retention held at 41% — up 1.2 points. Power users drove the lift; trial workspaces were flat.',\n phase: 'done',\n },\n ],\n ingest: [\n { id: 4, kind: 'user', text: 'Is the October backfill window still open?' },\n { id: 5, kind: 'answer', text: 'It closed Sunday 23:59 UTC. The next window opens Nov 3 — queued replays carry over automatically.', phase: 'done' },\n ],\n};\n\nconst TOUR_SCRIPT: TurnScript = {\n thinkMs: 1300,\n tool: { thoughtSeconds: 4, runMs: 1700 },\n answer: TOUR_ANSWER,\n};\n\nconst FOLLOW_UP_SCRIPT: TurnScript = {\n thinkMs: 900,\n answer: FOLLOW_UP_ANSWER,\n};\n\n/** Deterministic fake clock — stable timestamps keep the demo (and its\n * smoke test) reproducible without dragging Date into the render. */\nfunction stamp(id: number): string {\n return `09:${String(40 + (id % 20)).padStart(2, '0')}`;\n}\n\nconst sleep = (ms: number) =>\n new Promise<void>((resolve) => {\n setTimeout(resolve, ms);\n });\n\nfunction Mono({ children }: { children: ReactNode }) {\n return (\n <code\n style={{\n fontFamily: 'var(--haze-font-mono)',\n fontSize: 'var(--haze-text-xs)',\n whiteSpace: 'pre',\n color: 'var(--haze-color-text-secondary)',\n }}\n >\n {children}\n </code>\n );\n}\n\nexport default function ChatBlock() {\n const [conversations, setConversations] = useState(SEED_CONVERSATIONS);\n const [entries, setEntries] = useState(SEED_ENTRIES);\n const [phases, setPhases] = useState<Record<string, Phase>>({});\n // The active conversation is user-facing state — one Control, and a\n // host app could drive it externally.\n const [activeId, setActiveId] = useControl(undefined, 'anomaly');\n\n const runRef = useRef(0);\n const idRef = useRef(100);\n const nextId = () => (idRef.current += 1);\n\n // Invalidation guard: a new run, a stop or the unmount bumps the\n // counter, so stale sleeps from a cancelled turn never mutate state.\n useEffect(() => () => {\n runRef.current += 1;\n }, []);\n\n const append = useCallback((conv: string, entry: Entry) => {\n setEntries((prev) => ({ ...prev, [conv]: [...(prev[conv] ?? []), entry] }));\n }, []);\n\n const patch = useCallback(\n (conv: string, id: number, updater: (entry: Entry) => Entry) => {\n setEntries((prev) => ({\n ...prev,\n [conv]: (prev[conv] ?? []).map((entry) =>\n entry.id === id ? updater(entry) : entry\n ),\n }));\n },\n []\n );\n\n const setPhaseFor = useCallback((conv: string, phase: Phase) => {\n setPhases((prev) => ({ ...prev, [conv]: phase }));\n }, []);\n\n const playTurn = useCallback(\n async (conv: string, script: TurnScript) => {\n const run = (runRef.current += 1);\n const alive = () => run === runRef.current;\n\n setPhaseFor(conv, 'thinking');\n const thinkingId = nextId();\n append(conv, { id: thinkingId, kind: 'thinking' });\n await sleep(script.thinkMs);\n if (!alive()) return;\n\n const thoughtSeconds = script.tool\n ? script.tool.thoughtSeconds\n : Math.max(1, Math.round(script.thinkMs / 1000));\n patch(conv, thinkingId, () => ({\n id: thinkingId,\n kind: 'thought',\n seconds: thoughtSeconds,\n }));\n\n if (script.tool) {\n setPhaseFor(conv, 'tool');\n const toolId = nextId();\n append(conv, { id: toolId, kind: 'tool', status: 'running' });\n await sleep(script.tool.runMs);\n if (!alive()) return;\n patch(conv, toolId, () => ({ id: toolId, kind: 'tool', status: 'done' }));\n }\n\n setPhaseFor(conv, 'answering');\n append(conv, {\n id: nextId(),\n kind: 'answer',\n text: script.answer,\n phase: 'streaming',\n });\n },\n [append, patch, setPhaseFor]\n );\n\n // The scripted tour plays once on mount in the seeded conversation —\n // kicked from a timer so the effect body itself never writes state.\n useEffect(() => {\n const timer = setTimeout(() => {\n void playTurn('anomaly', TOUR_SCRIPT);\n }, 0);\n return () => clearTimeout(timer);\n }, [playTurn]);\n\n const phase = phases[activeId] ?? 'idle';\n\n const send = (text: string) => {\n append(activeId, { id: nextId(), kind: 'user', text });\n void playTurn(activeId, FOLLOW_UP_SCRIPT);\n };\n\n /** Stop keeps what finished and drops the in-flight tail (thinking\n * dots, a running tool, a half-streamed answer) in one pass. */\n const stopGeneration = () => {\n runRef.current += 1;\n setPhaseFor(activeId, 'idle');\n setEntries((prev) => {\n const list = [...(prev[activeId] ?? [])];\n while (list.length > 0) {\n const last = list[list.length - 1]!;\n const incomplete =\n last.kind === 'thinking' ||\n (last.kind === 'tool' && last.status === 'running') ||\n (last.kind === 'answer' && last.phase === 'streaming');\n if (!incomplete) break;\n list.pop();\n }\n return { ...prev, [activeId]: list };\n });\n };\n\n const newChat = () => {\n const id = `fresh-${conversations.length + 1}`;\n setConversations((prev) => [\n { id, title: 'New conversation', subtitle: 'Just started', time: 'now' },\n ...prev,\n ]);\n setActiveId(id);\n };\n\n const markAnswerDone = (id: number) => {\n patch(activeId, id, (entry) =>\n entry.kind === 'answer' ? { ...entry, phase: 'done' } : entry\n );\n setPhaseFor(activeId, 'idle');\n };\n\n const renderEntry = (entry: Entry) => {\n switch (entry.kind) {\n case 'user':\n return (\n <ChatMessage\n key={entry.id}\n role='user'\n name={USER_NAME}\n status='sent'\n timestamp={stamp(entry.id)}\n avatar={<Avatar size='sm' fallback='AN' />}\n >\n <div style={{ fontSize: 'var(--haze-text-sm)' }}>{entry.text}</div>\n </ChatMessage>\n );\n case 'thinking':\n return (\n <ThinkingIndicator\n key={entry.id}\n text='Pulling workspace metrics…'\n />\n );\n case 'thought':\n return (\n <div\n key={entry.id}\n data-slot='chat-thought'\n style={{\n padding: 'var(--haze-space-1) 0',\n fontSize: 'var(--haze-text-xs)',\n color: 'var(--haze-color-text-muted)',\n }}\n >\n Thought for {entry.seconds}s\n </div>\n );\n case 'tool':\n return (\n <ToolCallCard\n key={entry.id}\n name='query_events'\n status={entry.status}\n input={<Mono>{TOOL_INPUT}</Mono>}\n output={entry.status === 'done' ? <Mono>{TOOL_OUTPUT}</Mono> : undefined}\n />\n );\n case 'answer':\n return (\n <ChatMessage\n key={entry.id}\n role='assistant'\n name={AGENT_NAME}\n timestamp={stamp(entry.id)}\n copyable\n avatar={<Avatar size='sm' fallback='NC' />}\n >\n {entry.phase === 'streaming' ? (\n <StreamingText\n text={entry.text}\n speed={12}\n onComplete={() => markAnswerDone(entry.id)}\n />\n ) : (\n <div style={{ fontSize: 'var(--haze-text-sm)' }}>{entry.text}</div>\n )}\n </ChatMessage>\n );\n }\n };\n\n return (\n <div\n data-slot='chat-block'\n style={{\n display: 'flex',\n height: '100%',\n minHeight: '480px',\n fontFamily: 'var(--haze-font-sans)',\n color: 'var(--haze-color-text)',\n }}\n >\n <section\n aria-label={`Chat with ${AGENT_NAME}`}\n style={{\n flex: 1,\n minWidth: 0,\n display: 'flex',\n flexDirection: 'column',\n }}\n >\n <header\n data-slot='chat-header'\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: 'var(--haze-space-3)',\n padding: 'var(--haze-space-3) var(--haze-space-4)',\n borderBottom: '1px solid var(--haze-color-border)',\n }}\n >\n <Avatar fallback='NC' />\n <div style={{ minWidth: 0 }}>\n <div style={{ fontSize: 'var(--haze-text-sm)', fontWeight: 'var(--haze-weight-semibold)' }}>\n {AGENT_NAME}\n </div>\n <div style={{ fontSize: 'var(--haze-text-xs)', color: 'var(--haze-color-text-muted)' }}>\n Workspace analytics agent · scripted demo\n </div>\n </div>\n <span style={{ flex: 1 }} />\n {phase === 'idle' ? <Badge>Ready</Badge> : <Badge color='info'>Generating…</Badge>}\n </header>\n\n {/* ChatContainer owns the scrollport; role=\"log\" makes the whole\n transcript one live region for assistive tech. */}\n <ChatContainer\n role='log'\n aria-label='Conversation transcript'\n style={{ flex: 1, minHeight: 0, background: 'var(--haze-color-bg-subtle)' }}\n >\n {(entries[activeId] ?? []).length === 0 ? (\n <div\n style={{\n flex: 1,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: '240px',\n }}\n >\n <Empty description='Ask about workspaces, events or alerts — every reply here is scripted, no network involved.' />\n </div>\n ) : (\n (entries[activeId] ?? []).map(renderEntry)\n )}\n </ChatContainer>\n\n <footer\n data-slot='chat-composer'\n style={{\n borderTop: '1px solid var(--haze-color-border)',\n padding: 'var(--haze-space-3)',\n background: 'var(--haze-color-bg)',\n }}\n >\n <ChatInput\n onSend={send}\n generating={phase !== 'idle'}\n onStop={stopGeneration}\n placeholder={\n phase === 'idle' ? 'Ask a follow-up…' : 'Assistant is responding…'\n }\n />\n </footer>\n </section>\n\n <aside\n data-slot='chat-conversations'\n aria-label='Conversations'\n style={{\n width: '264px',\n flexShrink: 0,\n display: 'flex',\n flexDirection: 'column',\n borderInlineStart: '1px solid var(--haze-color-border)',\n background: 'var(--haze-color-bg)',\n }}\n >\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: 'var(--haze-space-2)',\n padding: 'var(--haze-space-3) var(--haze-space-3) var(--haze-space-2)',\n }}\n >\n <span\n style={{\n flex: 1,\n fontSize: 'var(--haze-text-xs)',\n fontWeight: 'var(--haze-weight-semibold)',\n letterSpacing: '0.04em',\n textTransform: 'uppercase',\n color: 'var(--haze-color-text-muted)',\n }}\n >\n Conversations\n </span>\n <Button size='sm' variant='outline' onClick={newChat}>\n New chat\n </Button>\n </div>\n {/* ConversationList has no style prop (className only, for\n Linaria classes) — the rail wraps it and owns the overflow. */}\n <div\n style={{\n flex: 1,\n minHeight: 0,\n overflowY: 'auto',\n padding: '0 var(--haze-space-2)',\n }}\n >\n <ConversationList>\n {conversations.map((conversation) => (\n <ConversationItem\n key={conversation.id}\n title={conversation.title}\n subtitle={conversation.subtitle}\n active={conversation.id === activeId}\n onClick={() => setActiveId(conversation.id)}\n end={\n <span style={{ fontSize: 'var(--haze-text-xs)', color: 'var(--haze-color-text-muted)' }}>\n {conversation.time}\n </span>\n }\n />\n ))}\n </ConversationList>\n </div>\n </aside>\n </div>\n );\n}\n",
749
+ "type": "registry:block"
750
+ }
751
+ ]
752
+ },
666
753
  {
667
754
  "name": "chat-container",
668
755
  "type": "registry:ui",
@@ -1136,6 +1223,50 @@
1136
1223
  }
1137
1224
  ]
1138
1225
  },
1226
+ {
1227
+ "name": "data-table-block",
1228
+ "type": "registry:block",
1229
+ "title": "Data table block (filterable DataTable console with batch actions)",
1230
+ "description": "The TanStack-backed `DataTable` as a full fleet page — sortable, resizable columns, a per-column filter row, pagination, column visibility, density, `dataTableToCsv` export and checkbox batch selection driving a bulk-action bar.",
1231
+ "author": "wmzy <https://github.com/wmzy>",
1232
+ "docs": "The TanStack-backed `DataTable` as a full fleet page — sortable, resizable columns, a per-column filter row, pagination, column visibility, density, `dataTableToCsv` export and checkbox batch selection driving a bulk-action bar.\n\n## Install\n\n```bash\nnpx shadcn@latest add wmzy/haze-ui/data-table-block\n```\n\nThe full-page source lands at `components/block/haze/data-table-block.tsx` (via the `@block/` target placeholder from `components.json`) — vendored, so you own and edit it. Component imports are rewritten to the `haze-ui` npm package, which is installed automatically together with `@tanstack/react-table` and `react-use-control`.\n\nThe file imports the stylesheet of every component it renders (`haze-ui/css/tokens.css` + 18 component css files), so no extra stylesheet setup is needed.\n\n## Theming\n\nThis wrapper re-exports the 'lightTheme' (or 'darkTheme'), 'spacing' and\n'typography' classes from 'haze-ui' and imports 'haze-ui/css/tokens.css'.\n\nActivate the design tokens by applying the classes to a root element:\n\n```jsx\nimport { lightTheme, spacing, typography } from 'haze-ui';\n\n// on <body> or any root container — darkTheme is a drop-in swap\n<div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n```\n\nLive preview and source: https://wmzy.github.io/haze-ui/#/blocks — https://github.com/wmzy/haze-ui/tree/main/registry",
1233
+ "categories": [
1234
+ "blocks",
1235
+ "data-display"
1236
+ ],
1237
+ "dependencies": [
1238
+ "haze-ui",
1239
+ "@tanstack/react-table",
1240
+ "react-use-control"
1241
+ ],
1242
+ "registryDependencies": [
1243
+ "wmzy/haze-ui/data-table",
1244
+ "wmzy/haze-ui/checkbox",
1245
+ "wmzy/haze-ui/dropdown-menu",
1246
+ "wmzy/haze-ui/empty",
1247
+ "wmzy/haze-ui/input",
1248
+ "wmzy/haze-ui/pagination",
1249
+ "wmzy/haze-ui/progress",
1250
+ "wmzy/haze-ui/segmented",
1251
+ "wmzy/haze-ui/select",
1252
+ "wmzy/haze-ui/skeleton",
1253
+ "wmzy/haze-ui/spinner",
1254
+ "wmzy/haze-ui/table",
1255
+ "wmzy/haze-ui/virtual-list",
1256
+ "wmzy/haze-ui/visually-hidden",
1257
+ "wmzy/haze-ui/avatar",
1258
+ "wmzy/haze-ui/badge",
1259
+ "wmzy/haze-ui/button",
1260
+ "wmzy/haze-ui/chip"
1261
+ ],
1262
+ "files": [
1263
+ {
1264
+ "path": "components/block/haze/data-table-block.tsx",
1265
+ "content": "// haze-ui registry block — generated by scripts/generate-registry.mjs\n// from src/views/Blocks/blocks/DataTableBlock.tsx.\n//\n// Unlike the component items (thin re-export wrappers over the haze-ui npm\n// package), a block is VENDORED source: a full page you own and edit.\n// The component imports are rewritten from the docs site's '@/lib' alias\n// to the published 'haze-ui' package and every stylesheet of the page's\n// component closure is imported below, so the file is self-contained.\n// npm dependencies beyond haze-ui: @tanstack/react-table, react-use-control.\n//\n// Theming (once per app): apply the token classes to a root element, e.g.\n// import { lightTheme, spacing, typography } from 'haze-ui';\n// <div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n// (darkTheme is a drop-in swap).\n\nimport 'haze-ui/css/tokens.css';\nimport 'haze-ui/css/avatar.css';\nimport 'haze-ui/css/badge.css';\nimport 'haze-ui/css/button.css';\nimport 'haze-ui/css/checkbox.css';\nimport 'haze-ui/css/chip.css';\nimport 'haze-ui/css/data-table.css';\nimport 'haze-ui/css/dropdown-menu.css';\nimport 'haze-ui/css/empty.css';\nimport 'haze-ui/css/input.css';\nimport 'haze-ui/css/pagination.css';\nimport 'haze-ui/css/progress.css';\nimport 'haze-ui/css/segmented.css';\nimport 'haze-ui/css/select.css';\nimport 'haze-ui/css/skeleton.css';\nimport 'haze-ui/css/spinner.css';\nimport 'haze-ui/css/table.css';\nimport 'haze-ui/css/virtual-list.css';\nimport 'haze-ui/css/visually-hidden.css';\n\nimport type { RowSelectionState } from '@tanstack/react-table';\nimport type { DataTableColumnDef } from 'haze-ui';\n\nimport { useMemo } from 'react';\nimport { useControl } from 'react-use-control';\n\nimport {\n Avatar,\n Badge,\n Button,\n DataTable,\n dataTableAvg,\n dataTableSum,\n dataTableToCsv,\n Option,\n Progress,\n Segmented,\n Select,\n} from 'haze-ui';\n\n/*\n * Data-table block: the TanStack-backed DataTable carrying a full admin\n * page — sortable headers, per-column filter row, pagination, resizable\n * columns, a toolbar strip (density, column visibility, CSV export), and\n * checkbox batch selection driving a bulk-action bar. Region/status\n * filtering is parent-driven (the top filter strip narrows `data` before\n * the table sees it), while the text filters use the table's own\n * `filterable` row — both idioms in one screen.\n */\n\ntype InstanceStatus = 'healthy' | 'degraded' | 'rebooting' | 'stopped';\ntype Region = 'us-east-1' | 'us-west-2' | 'eu-central-1' | 'ap-southeast-1';\n\ntype InstanceRow = {\n id: string;\n name: string;\n region: Region;\n status: InstanceStatus;\n vcpus: number;\n memoryGb: number;\n cost: number;\n uptime: number;\n owner: string;\n};\n\nconst INSTANCES: InstanceRow[] = [\n { id: 'i-0a11', name: 'gateway-edge-1', region: 'us-east-1', status: 'healthy', vcpus: 8, memoryGb: 32, cost: 214, uptime: 99.98, owner: 'Ana Petrova' },\n { id: 'i-0a12', name: 'gateway-edge-2', region: 'us-west-2', status: 'healthy', vcpus: 8, memoryGb: 32, cost: 218, uptime: 99.91, owner: 'Ana Petrova' },\n { id: 'i-0b21', name: 'ingest-primary', region: 'us-east-1', status: 'degraded', vcpus: 16, memoryGb: 64, cost: 486, uptime: 97.42, owner: 'Ben Okafor' },\n { id: 'i-0b22', name: 'ingest-replica', region: 'us-east-1', status: 'healthy', vcpus: 16, memoryGb: 64, cost: 486, uptime: 99.95, owner: 'Ben Okafor' },\n { id: 'i-0c31', name: 'vector-index-a', region: 'eu-central-1', status: 'healthy', vcpus: 32, memoryGb: 128, cost: 1042, uptime: 99.99, owner: 'Chen Wei' },\n { id: 'i-0c32', name: 'vector-index-b', region: 'eu-central-1', status: 'rebooting', vcpus: 32, memoryGb: 128, cost: 1042, uptime: 98.7, owner: 'Chen Wei' },\n { id: 'i-0d41', name: 'cache-redis-1', region: 'us-west-2', status: 'healthy', vcpus: 4, memoryGb: 16, cost: 96, uptime: 99.99, owner: 'Dana Silva' },\n { id: 'i-0d42', name: 'cache-redis-2', region: 'ap-southeast-1', status: 'healthy', vcpus: 4, memoryGb: 16, cost: 102, uptime: 99.87, owner: 'Dana Silva' },\n { id: 'i-0e51', name: 'worker-batch-1', region: 'us-east-1', status: 'healthy', vcpus: 8, memoryGb: 32, cost: 178, uptime: 99.62, owner: 'Ben Okafor' },\n { id: 'i-0e52', name: 'worker-batch-2', region: 'us-east-1', status: 'stopped', vcpus: 8, memoryGb: 32, cost: 0, uptime: 91.15, owner: 'Ben Okafor' },\n { id: 'i-0f61', name: 'worker-batch-3', region: 'eu-central-1', status: 'healthy', vcpus: 8, memoryGb: 32, cost: 181, uptime: 99.44, owner: 'Chen Wei' },\n { id: 'i-0f71', name: 'search-api-1', region: 'ap-southeast-1', status: 'healthy', vcpus: 12, memoryGb: 48, cost: 305, uptime: 99.93, owner: 'Dana Silva' },\n { id: 'i-0f72', name: 'search-api-2', region: 'ap-southeast-1', status: 'degraded', vcpus: 12, memoryGb: 48, cost: 305, uptime: 96.08, owner: 'Dana Silva' },\n { id: 'i-0g81', name: 'ci-runner-x1', region: 'us-west-2', status: 'healthy', vcpus: 64, memoryGb: 256, cost: 1688, uptime: 99.71, owner: 'Ana Petrova' },\n { id: 'i-0g82', name: 'ci-runner-x2', region: 'us-west-2', status: 'rebooting', vcpus: 64, memoryGb: 256, cost: 1688, uptime: 98.31, owner: 'Ana Petrova' },\n { id: 'i-0h91', name: 'metrics-store', region: 'eu-central-1', status: 'healthy', vcpus: 24, memoryGb: 96, cost: 742, uptime: 99.96, owner: 'Chen Wei' },\n { id: 'i-0h92', name: 'metrics-rollup', region: 'us-east-1', status: 'healthy', vcpus: 6, memoryGb: 24, cost: 133, uptime: 99.88, owner: 'Chen Wei' },\n { id: 'i-0i01', name: 'staging-db', region: 'us-east-1', status: 'stopped', vcpus: 16, memoryGb: 64, cost: 0, uptime: 88.04, owner: 'Dana Silva' },\n];\n\nconst REGIONS: Region[] = ['us-east-1', 'us-west-2', 'eu-central-1', 'ap-southeast-1'];\nconst STATUSES: InstanceStatus[] = ['healthy', 'degraded', 'rebooting', 'stopped'];\n\nconst initials = (name: string) =>\n name\n .split(' ')\n .map((part) => part.charAt(0))\n .join('')\n .slice(0, 2);\n\nconst statusBadge = (status: InstanceStatus) => {\n const color =\n status === 'healthy'\n ? 'success'\n : status === 'degraded'\n ? 'warning'\n : status === 'rebooting'\n ? 'info'\n : 'default';\n return <Badge color={color}>{status}</Badge>;\n};\n\nconst uptimeCell = (value: number) => (\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: 'var(--haze-space-2)',\n minWidth: '120px',\n }}\n >\n <Progress value={value} size='sm' />\n <span style={{ fontSize: 'var(--haze-text-xs)', color: 'var(--haze-color-text-secondary)' }}>\n {value.toFixed(2)}%\n </span>\n </div>\n);\n\nconst columns: DataTableColumnDef<InstanceRow>[] = [\n {\n accessorKey: 'name',\n header: 'Instance',\n cell: (info) => <strong>{info.getValue() as string}</strong>,\n meta: { width: 160 },\n },\n { accessorKey: 'region', header: 'Region' },\n {\n accessorKey: 'status',\n header: 'Status',\n cell: (info) => statusBadge(info.getValue() as InstanceStatus),\n meta: { width: 120 },\n },\n { accessorKey: 'vcpus', header: 'vCPU', meta: { width: 80 } },\n { accessorKey: 'memoryGb', header: 'Memory (GB)', meta: { width: 120 } },\n {\n accessorKey: 'cost',\n header: 'Cost / mo',\n cell: (info) => `$${(info.getValue() as number).toLocaleString('en-US')}`,\n meta: { width: 100 },\n },\n {\n accessorKey: 'uptime',\n header: 'Uptime (30d)',\n cell: (info) => uptimeCell(info.getValue() as number),\n meta: { width: 180 },\n },\n {\n accessorKey: 'owner',\n header: 'Owner',\n cell: (info) => (\n <span style={{ display: 'inline-flex', alignItems: 'center', gap: 'var(--haze-space-2)' }}>\n <Avatar size='sm' fallback={initials(info.getValue() as string)} />\n <span>{info.getValue() as string}</span>\n </span>\n ),\n meta: { excludeFromExport: true, width: 160 },\n },\n];\n\n/** Serialize rows through the table's own csv helper and download them\n * (dashboard-block pattern). */\nfunction downloadCsv(rows: InstanceRow[], file: string): void {\n const csv = dataTableToCsv(rows, columns);\n const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));\n const anchor = document.createElement('a');\n anchor.href = url;\n anchor.download = file;\n document.body.appendChild(anchor);\n anchor.click();\n anchor.remove();\n URL.revokeObjectURL(url);\n}\n\nconst barStyle = {\n display: 'flex',\n alignItems: 'center',\n gap: 'var(--haze-space-2)',\n flexWrap: 'wrap',\n} as const;\n\nexport default function DataTableBlock() {\n // One Control per user-facing state: the two filter strips, the density\n // switcher and the row-selection map.\n const [region, setRegion, regionCtrl] = useControl(undefined, 'all');\n const [status, setStatus, statusCtrl] = useControl(undefined, 'all');\n const [density, setDensity, densityCtrl] = useControl(undefined, 'md');\n const [rowSelection, setRowSelection] = useControl(undefined, {} as RowSelectionState);\n\n // Parent-driven filtering: the strip narrows `data` before the table\n // sees it; the table's own filter row handles per-column text.\n const rows = useMemo(\n () =>\n INSTANCES.filter(\n (row) =>\n (region === 'all' || row.region === region) &&\n (status === 'all' || row.status === status)\n ),\n [region, status]\n );\n\n const selectedCount = Object.values(rowSelection).filter(Boolean).length;\n const selectedRows = useMemo(\n () => rows.filter((row) => rowSelection[row.id] === true),\n [rows, rowSelection]\n );\n\n return (\n <div\n data-slot='data-table-block'\n style={{\n height: '100%',\n display: 'flex',\n flexDirection: 'column',\n gap: 'var(--haze-space-3)',\n padding: 'var(--haze-space-4)',\n fontFamily: 'var(--haze-font-sans)',\n color: 'var(--haze-color-text)',\n background: 'var(--haze-color-bg)',\n }}\n >\n <header\n data-slot='data-table-header'\n style={{ display: 'flex', alignItems: 'center', gap: 'var(--haze-space-3)', flexWrap: 'wrap' }}\n >\n <div>\n <h2 style={{ margin: 0, fontSize: 'var(--haze-text-lg)', fontWeight: 'var(--haze-weight-semibold)' }}>\n Compute fleet\n </h2>\n <p\n style={{\n margin: 0,\n fontSize: 'var(--haze-text-sm)',\n color: 'var(--haze-color-text-secondary)',\n }}\n >\n {rows.length} of {INSTANCES.length} instances · sort, filter, resize, select and export\n </p>\n </div>\n <span style={{ flex: 1 }} />\n <div style={barStyle}>\n <Select\n size='sm'\n value={regionCtrl}\n onValuesChange={(next) => setRegion(typeof next === 'string' ? next : 'all')}\n aria-label='Filter by region'\n >\n <Option value='all'>All regions</Option>\n {REGIONS.map((r) => (\n <Option key={r} value={r}>\n {r}\n </Option>\n ))}\n </Select>\n <Select\n size='sm'\n value={statusCtrl}\n onValuesChange={(next) => setStatus(typeof next === 'string' ? next : 'all')}\n aria-label='Filter by status'\n >\n <Option value='all'>All statuses</Option>\n {STATUSES.map((s) => (\n <Option key={s} value={s}>\n {s}\n </Option>\n ))}\n </Select>\n <Button\n size='sm'\n variant='outline'\n onClick={() => downloadCsv(rows, 'nimbus-fleet.csv')}\n >\n Export CSV\n </Button>\n </div>\n </header>\n\n {selectedCount > 0 && (\n <div data-slot='data-table-bulk-bar' style={barStyle}>\n <Badge color='info'>{selectedCount} selected</Badge>\n <Button\n size='sm'\n variant='outline'\n onClick={() => downloadCsv(selectedRows, 'nimbus-fleet-selection.csv')}\n >\n Export selection\n </Button>\n <Button size='sm' variant='ghost' onClick={() => setRowSelection({})}>\n Clear selection\n </Button>\n </div>\n )}\n\n <DataTable\n columns={columns}\n data={rows}\n getRowId={(row) => row.id}\n sortable\n selectable\n rowSelection={rowSelection}\n filterable\n resizable\n pageSize={8}\n columnToggle\n size={density as 'sm' | 'md' | 'lg'}\n summary={(summaryRows) => [\n {\n label: 'Filtered totals',\n cells: [\n `${summaryRows.length} instances`,\n '',\n '',\n dataTableSum(summaryRows, 'vcpus'),\n `${dataTableSum(summaryRows, 'memoryGb')} GB`,\n `$${dataTableSum(summaryRows, 'cost').toLocaleString('en-US')}`,\n `${(dataTableAvg(summaryRows, 'uptime') ?? 0).toFixed(2)}% avg`,\n '',\n ],\n },\n ]}\n toolbar={\n <Segmented\n size='sm'\n options={[\n { value: 'sm', label: 'S' },\n { value: 'md', label: 'M' },\n { value: 'lg', label: 'L' },\n ]}\n value={densityCtrl}\n onChange={setDensity}\n aria-label='Table density'\n />\n }\n />\n </div>\n );\n}\n",
1266
+ "type": "registry:block"
1267
+ }
1268
+ ]
1269
+ },
1139
1270
  {
1140
1271
  "name": "date-field",
1141
1272
  "type": "registry:ui",
@@ -1472,6 +1603,45 @@
1472
1603
  }
1473
1604
  ]
1474
1605
  },
1606
+ {
1607
+ "name": "filter-list-block",
1608
+ "type": "registry:block",
1609
+ "title": "Filter list block (filter rail + card catalog + Pagination)",
1610
+ "description": "A browsing page, not a table page: search `Input`, category `Checkbox`es, region and sort `Select`s drive a stack of result `Card`s with `Avatar`, status `Badge` and `Rating`; active facets render as closable `Chip`s, `Empty` covers no-match and `Pagination` slices the list.",
1611
+ "author": "wmzy <https://github.com/wmzy>",
1612
+ "docs": "A browsing page, not a table page: search `Input`, category `Checkbox`es, region and sort `Select`s drive a stack of result `Card`s with `Avatar`, status `Badge` and `Rating`; active facets render as closable `Chip`s, `Empty` covers no-match and `Pagination` slices the list.\n\n## Install\n\n```bash\nnpx shadcn@latest add wmzy/haze-ui/filter-list-block\n```\n\nThe full-page source lands at `components/block/haze/filter-list-block.tsx` (via the `@block/` target placeholder from `components.json`) — vendored, so you own and edit it. Component imports are rewritten to the `haze-ui` npm package, which is installed automatically together with `react-use-control`.\n\nThe file imports the stylesheet of every component it renders (`haze-ui/css/tokens.css` + 13 component css files), so no extra stylesheet setup is needed.\n\n## Theming\n\nThis wrapper re-exports the 'lightTheme' (or 'darkTheme'), 'spacing' and\n'typography' classes from 'haze-ui' and imports 'haze-ui/css/tokens.css'.\n\nActivate the design tokens by applying the classes to a root element:\n\n```jsx\nimport { lightTheme, spacing, typography } from 'haze-ui';\n\n// on <body> or any root container — darkTheme is a drop-in swap\n<div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n```\n\nLive preview and source: https://wmzy.github.io/haze-ui/#/blocks — https://github.com/wmzy/haze-ui/tree/main/registry",
1613
+ "categories": [
1614
+ "blocks",
1615
+ "data-display",
1616
+ "form"
1617
+ ],
1618
+ "dependencies": [
1619
+ "haze-ui",
1620
+ "react-use-control"
1621
+ ],
1622
+ "registryDependencies": [
1623
+ "wmzy/haze-ui/card",
1624
+ "wmzy/haze-ui/input",
1625
+ "wmzy/haze-ui/checkbox",
1626
+ "wmzy/haze-ui/select",
1627
+ "wmzy/haze-ui/chip",
1628
+ "wmzy/haze-ui/badge",
1629
+ "wmzy/haze-ui/avatar",
1630
+ "wmzy/haze-ui/rating",
1631
+ "wmzy/haze-ui/empty",
1632
+ "wmzy/haze-ui/pagination",
1633
+ "wmzy/haze-ui/button",
1634
+ "wmzy/haze-ui/spinner",
1635
+ "wmzy/haze-ui/virtual-list"
1636
+ ],
1637
+ "files": [
1638
+ {
1639
+ "path": "components/block/haze/filter-list-block.tsx",
1640
+ "content": "// haze-ui registry block — generated by scripts/generate-registry.mjs\n// from src/views/Blocks/blocks/FilterListBlock.tsx.\n//\n// Unlike the component items (thin re-export wrappers over the haze-ui npm\n// package), a block is VENDORED source: a full page you own and edit.\n// The component imports are rewritten from the docs site's '@/lib' alias\n// to the published 'haze-ui' package and every stylesheet of the page's\n// component closure is imported below, so the file is self-contained.\n// npm dependencies beyond haze-ui: react-use-control.\n//\n// Theming (once per app): apply the token classes to a root element, e.g.\n// import { lightTheme, spacing, typography } from 'haze-ui';\n// <div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n// (darkTheme is a drop-in swap).\n\nimport 'haze-ui/css/tokens.css';\nimport 'haze-ui/css/avatar.css';\nimport 'haze-ui/css/badge.css';\nimport 'haze-ui/css/button.css';\nimport 'haze-ui/css/card.css';\nimport 'haze-ui/css/checkbox.css';\nimport 'haze-ui/css/chip.css';\nimport 'haze-ui/css/empty.css';\nimport 'haze-ui/css/input.css';\nimport 'haze-ui/css/pagination.css';\nimport 'haze-ui/css/rating.css';\nimport 'haze-ui/css/select.css';\nimport 'haze-ui/css/spinner.css';\nimport 'haze-ui/css/virtual-list.css';\n\nimport type { ReactNode } from 'react';\n\nimport { useMemo } from 'react';\nimport { useControl } from 'react-use-control';\n\nimport {\n Avatar,\n Badge,\n Button,\n Card,\n Checkbox,\n Chip,\n Empty,\n Input,\n Option,\n Pagination,\n Rating,\n Select,\n} from 'haze-ui';\n\n/*\n * Filter list block: a catalog page with a filter rail. The rail (search\n * Input, category Checkboxes, region Select, sort Select) and the result\n * list never share components with the Crud/DataTable blocks — the list\n * itself is a stack of Cards, so this reads as a browsing page, not a\n * table page. Every filter is a Control (query / categories / region /\n * sort / page), active facets render as closable Chips, and an Empty\n * state plus Pagination close the loop.\n */\n\ntype CatalogItem = {\n id: string;\n name: string;\n description: string;\n category: 'automation' | 'data' | 'design' | 'docs';\n region: 'eu-central' | 'us-east' | 'ap-northeast';\n rating: number;\n installs: number;\n status: 'ready' | 'beta';\n updated: string;\n};\n\nconst CATEGORY_LABELS: Record<CatalogItem['category'], string> = {\n automation: 'Automation',\n data: 'Data',\n design: 'Design',\n docs: 'Docs',\n};\n\nconst CATALOG: CatalogItem[] = [\n { id: 'pipeline-watch', name: 'Pipeline Watch', description: 'Streaming health board for ingest pipelines, with SLO burn alerts.', category: 'data', region: 'eu-central', rating: 5, installs: 12840, status: 'ready', updated: '2 days ago' },\n { id: 'release-notes', name: 'Release Notes Bot', description: 'Turns merged PR labels into a drafted changelog every Friday.', category: 'automation', region: 'us-east', rating: 4, installs: 9310, status: 'ready', updated: '1 week ago' },\n { id: 'token-studio', name: 'Token Studio Sync', description: 'Two-way sync between the design token pipeline and the codebase.', category: 'design', region: 'eu-central', rating: 5, installs: 7725, status: 'ready', updated: '3 days ago' },\n { id: 'runbook-search', name: 'Runbook Search', description: 'Full-text search across on-call runbooks with incident tags.', category: 'docs', region: 'us-east', rating: 4, installs: 5410, status: 'ready', updated: '2 weeks ago' },\n { id: 'fleet-mapper', name: 'Fleet Mapper', description: 'Maps device fleets onto regions and utilization heat bands.', category: 'data', region: 'ap-northeast', rating: 4, installs: 4980, status: 'beta', updated: '5 days ago' },\n { id: 'handoff-kit', name: 'Handoff Kit', description: 'Figma frame to component scaffold, with a11y annotations intact.', category: 'design', region: 'ap-northeast', rating: 5, installs: 4102, status: 'ready', updated: '4 days ago' },\n { id: 'triage-rules', name: 'Triage Rules', description: 'Routes incoming issues to owners with a small rules DSL.', category: 'automation', region: 'eu-central', rating: 3, installs: 3611, status: 'ready', updated: '3 weeks ago' },\n { id: 'schema-docs', name: 'Schema Docs', description: 'Generates readable docs from protobuf and SQL schemas.', category: 'docs', region: 'eu-central', rating: 4, installs: 2870, status: 'beta', updated: '1 week ago' },\n { id: 'cohort-lens', name: 'Cohort Lens', description: 'Retention cohorts over the warehouse, no notebook required.', category: 'data', region: 'us-east', rating: 5, installs: 2614, status: 'ready', updated: '6 days ago' },\n { id: 'brand-lint', name: 'Brand Lint', description: 'CI check that new UI stays inside the brand token scales.', category: 'design', region: 'us-east', rating: 3, installs: 1980, status: 'ready', updated: '2 weeks ago' },\n { id: 'oncall-brief', name: 'Oncall Brief', description: 'Morning digest of what changed while the on-call slept.', category: 'automation', region: 'ap-northeast', rating: 4, installs: 1502, status: 'beta', updated: '3 days ago' },\n { id: 'policy-wiki', name: 'Policy Wiki', description: 'Versioned policy pages with review expirations and owners.', category: 'docs', region: 'ap-northeast', rating: 3, installs: 940, status: 'ready', updated: '1 month ago' },\n];\n\nconst PAGE_SIZE = 5;\n\nconst SORTERS: Record<string, (a: CatalogItem, b: CatalogItem) => number> = {\n popular: (a, b) => b.installs - a.installs,\n rating: (a, b) => b.rating - a.rating || b.installs - a.installs,\n name: (a, b) => a.name.localeCompare(b.name),\n};\n\nfunction FilterSection({ title, children }: { title: string; children: ReactNode }) {\n return (\n <div\n data-slot='filter-section'\n style={{\n display: 'flex',\n flexDirection: 'column',\n gap: 'var(--haze-space-2)',\n paddingTop: 'var(--haze-space-3)',\n borderTop: '1px solid var(--haze-color-border)',\n }}\n >\n <span\n style={{\n fontSize: 'var(--haze-text-xs)',\n fontWeight: 'var(--haze-weight-semibold)',\n letterSpacing: '0.04em',\n textTransform: 'uppercase',\n color: 'var(--haze-color-text-muted)',\n }}\n >\n {title}\n </span>\n {children}\n </div>\n );\n}\n\nfunction CatalogCard({ item }: { item: CatalogItem }) {\n return (\n <Card data-slot='catalog-card' variant='outlined'>\n <div\n style={{\n display: 'flex',\n alignItems: 'flex-start',\n gap: 'var(--haze-space-3)',\n }}\n >\n <Avatar fallback={item.name.slice(0, 2).toUpperCase()} />\n <div style={{ flex: 1, minWidth: 0 }}>\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: 'var(--haze-space-2)',\n flexWrap: 'wrap',\n }}\n >\n <span\n style={{\n fontSize: 'var(--haze-text-base)',\n fontWeight: 'var(--haze-weight-medium)',\n }}\n >\n {item.name}\n </span>\n <Badge color={item.status === 'ready' ? 'success' : 'info'}>\n {item.status === 'ready' ? 'Ready' : 'Beta'}\n </Badge>\n <span style={{ marginLeft: 'auto', whiteSpace: 'nowrap' }}>\n <Rating value={item.rating} allowHalf={false} />\n </span>\n </div>\n <p\n style={{\n margin: 'var(--haze-space-1) 0 0',\n fontSize: 'var(--haze-text-sm)',\n color: 'var(--haze-color-text-secondary)',\n }}\n >\n {item.description}\n </p>\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: 'var(--haze-space-3)',\n marginTop: 'var(--haze-space-2)',\n fontSize: 'var(--haze-text-xs)',\n color: 'var(--haze-color-text-muted)',\n }}\n >\n <span>{CATEGORY_LABELS[item.category]}</span>\n <span>{item.installs.toLocaleString()} installs</span>\n <span>Updated {item.updated}</span>\n </div>\n </div>\n </div>\n </Card>\n );\n}\n\nexport default function FilterListBlock() {\n // The whole filter state is user-facing — Controls, so a host app can\n // preset, observe or drive this page from a URL.\n const [query, setQuery, queryCtrl] = useControl(undefined, '');\n const [categories, setCategories] = useControl<string[]>(undefined, []);\n const [region, setRegion, regionCtrl] = useControl(undefined, 'all');\n const [sort, , sortCtrl] = useControl(undefined, 'popular');\n const [page, setPage, pageCtrl] = useControl(undefined, 1);\n\n const filtered = useMemo(() => {\n const q = query.trim().toLowerCase();\n const matched = CATALOG.filter((item) => {\n if (\n q &&\n !item.name.toLowerCase().includes(q) &&\n !item.description.toLowerCase().includes(q)\n ) {\n return false;\n }\n if (categories.length > 0 && !categories.includes(item.category)) {\n return false;\n }\n if (region !== 'all' && item.region !== region) return false;\n return true;\n });\n return [...matched].sort(SORTERS[sort] ?? SORTERS.popular);\n }, [query, categories, region, sort]);\n\n const pageCount = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));\n const safePage = Math.min(page, pageCount);\n const pageItems = filtered.slice(\n (safePage - 1) * PAGE_SIZE,\n safePage * PAGE_SIZE\n );\n\n function toggleCategory(category: CatalogItem['category'], checked: boolean) {\n setCategories((prev) =>\n checked ? [...prev, category] : prev.filter((c) => c !== category)\n );\n setPage(1);\n }\n\n return (\n <div\n data-slot='filter-list-block'\n style={{\n display: 'flex',\n gap: 'var(--haze-space-5)',\n alignItems: 'flex-start',\n padding: 'var(--haze-space-5)',\n fontFamily: 'var(--haze-font-sans)',\n color: 'var(--haze-color-text)',\n background: 'var(--haze-color-bg)',\n }}\n >\n <Card data-slot='filter-rail' style={{ width: '260px', flexShrink: 0 }}>\n <div\n style={{\n display: 'flex',\n flexDirection: 'column',\n gap: 'var(--haze-space-3)',\n }}\n >\n <Input\n value={queryCtrl}\n size='sm'\n placeholder='Search the catalog…'\n aria-label='Search the catalog'\n onChange={(e) => {\n setQuery(e.currentTarget.value);\n setPage(1);\n }}\n />\n <FilterSection title='Category'>\n {(Object.keys(CATEGORY_LABELS) as CatalogItem['category'][]).map(\n (category) => (\n <Checkbox\n key={category}\n checked={categories.includes(category)}\n label={CATEGORY_LABELS[category]}\n onChange={(e) => toggleCategory(category, e.currentTarget.checked)}\n />\n )\n )}\n </FilterSection>\n <FilterSection title='Region'>\n <Select\n value={regionCtrl}\n size='sm'\n aria-label='Filter by region'\n onChange={(e) => {\n setRegion(e.currentTarget.value);\n setPage(1);\n }}\n >\n <Option value='all'>All regions</Option>\n <Option value='eu-central'>Frankfurt</Option>\n <Option value='us-east'>N. Virginia</Option>\n <Option value='ap-northeast'>Tokyo</Option>\n </Select>\n </FilterSection>\n <FilterSection title='Sort'>\n <Select value={sortCtrl} size='sm' aria-label='Sort order'>\n <Option value='popular'>Most installed</Option>\n <Option value='rating'>Highest rated</Option>\n <Option value='name'>Name A–Z</Option>\n </Select>\n </FilterSection>\n <Button\n size='sm'\n variant='outline'\n onClick={() => {\n setQuery('');\n setCategories([]);\n setRegion('all');\n setPage(1);\n }}\n >\n Reset filters\n </Button>\n </div>\n </Card>\n\n <div\n data-slot='filter-results'\n style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 'var(--haze-space-3)' }}\n >\n <div\n data-slot='results-header'\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: 'var(--haze-space-2)',\n flexWrap: 'wrap',\n minHeight: '32px',\n }}\n >\n <span style={{ fontSize: 'var(--haze-text-sm)', color: 'var(--haze-color-text-secondary)' }}>\n {filtered.length} of {CATALOG.length} templates\n </span>\n {categories.map((category) => (\n <Chip\n key={category}\n variant='outline'\n onClose={() => toggleCategory(category as CatalogItem['category'], false)}\n >\n {CATEGORY_LABELS[category as CatalogItem['category']]}\n </Chip>\n ))}\n {region !== 'all' && (\n <Chip variant='outline' onClose={() => setRegion('all')}>\n Region: {region}\n </Chip>\n )}\n </div>\n\n {pageItems.length === 0 ? (\n <Empty description='No templates match these filters — reset them or try a shorter query.' />\n ) : (\n pageItems.map((item) => <CatalogCard key={item.id} item={item} />)\n )}\n\n <div style={{ display: 'flex', justifyContent: 'flex-end' }}>\n <Pagination\n page={pageCtrl}\n total={filtered.length}\n pageSize={PAGE_SIZE}\n size='sm'\n />\n </div>\n </div>\n </div>\n );\n}\n",
1641
+ "type": "registry:block"
1642
+ }
1643
+ ]
1644
+ },
1475
1645
  {
1476
1646
  "name": "flex",
1477
1647
  "type": "registry:ui",
@@ -1514,6 +1684,27 @@
1514
1684
  }
1515
1685
  ]
1516
1686
  },
1687
+ {
1688
+ "name": "follow-up-suggestions",
1689
+ "type": "registry:ui",
1690
+ "title": "FollowUpSuggestions",
1691
+ "description": "Wrapping row of clickable AI-chat follow-up suggestion chips with loading skeleton and disabled states.",
1692
+ "author": "wmzy <https://github.com/wmzy>",
1693
+ "docs": "Wrapping row of clickable AI-chat follow-up suggestion chips with loading skeleton and disabled states.\n\nExports: `FollowUpSuggestions`.\n\n## Install\n\n```bash\nnpx shadcn@latest add wmzy/haze-ui/follow-up-suggestions\n```\n\nThe wrapper lands at `components/ui/haze/follow-up-suggestions.tsx` (via the `@ui/` target placeholder from `components.json`) and re-exports the component(s) from the `haze-ui` npm package, which is installed automatically.\n\n## Theming\n\nhaze-ui ships JS and CSS separately — the installed wrapper already imports\n`haze-ui/css/tokens.css` and `haze-ui/css/follow-up-suggestions.css`, so no extra stylesheet setup is needed.\n\nActivate the design tokens by applying the classes to a root element:\n\n```jsx\nimport { lightTheme, spacing, typography } from 'haze-ui';\n\n// on <body> or any root container — darkTheme is a drop-in swap\n<div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n```\n\nDetails: https://github.com/wmzy/haze-ui/tree/main/registry",
1694
+ "categories": [
1695
+ "agent"
1696
+ ],
1697
+ "dependencies": [
1698
+ "haze-ui"
1699
+ ],
1700
+ "files": [
1701
+ {
1702
+ "path": "components/haze/follow-up-suggestions.tsx",
1703
+ "content": "// haze-ui registry wrapper — generated by scripts/generate-registry.mjs.\n//\n// This is a thin wrapper around the published haze-ui npm package, not\n// vendored component source: it re-exports the component(s) from\n// 'haze-ui' and imports their stylesheet, so the package keeps updating\n// underneath. Edit this file to customize — wrap, restyle, compose; that\n// is what it is for. (haze-ui is compiled CSS-in-JS (linaria) styled\n// against --haze-* design tokens, so vendoring the source would neither\n// build nor style — forking this wrapper is the intended customization\n// path.)\n//\n// Theming (once per app): apply the token classes to a root element, e.g.\n// import { lightTheme, spacing, typography } from 'haze-ui';\n// <div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n// (darkTheme is a drop-in swap). haze-ui dist modules ship their own\n// 'use client' directive, so imports resolve cleanly from RSC; the\n// wrapper inherits client semantics either way.\n\nimport { FollowUpSuggestions } from 'haze-ui';\n\nimport 'haze-ui/css/tokens.css';\nimport 'haze-ui/css/follow-up-suggestions.css';\nimport 'haze-ui/css/button.css';\nimport 'haze-ui/css/skeleton.css';\nimport 'haze-ui/css/spinner.css';\n\nexport { FollowUpSuggestions };\nexport type { FollowUpSuggestionsProps } from 'haze-ui';\n",
1704
+ "type": "registry:ui"
1705
+ }
1706
+ ]
1707
+ },
1517
1708
  {
1518
1709
  "name": "form",
1519
1710
  "type": "registry:ui",
@@ -1535,6 +1726,43 @@
1535
1726
  }
1536
1727
  ]
1537
1728
  },
1729
+ {
1730
+ "name": "form-wizard-block",
1731
+ "type": "registry:block",
1732
+ "title": "Form wizard block (Stepper over one zod Form, gated steps)",
1733
+ "description": "Three-step workspace setup in a centered `Card`: a `Stepper` whose activeStep is a Control rides one react-f0rm `Form` so values survive step changes, `Next` awaits `trigger()` on the current step fields before advancing, and the final submit lands in an `Alert` receipt.",
1734
+ "author": "wmzy <https://github.com/wmzy>",
1735
+ "docs": "Three-step workspace setup in a centered `Card`: a `Stepper` whose activeStep is a Control rides one react-f0rm `Form` so values survive step changes, `Next` awaits `trigger()` on the current step fields before advancing, and the final submit lands in an `Alert` receipt.\n\n## Install\n\n```bash\nnpx shadcn@latest add wmzy/haze-ui/form-wizard-block\n```\n\nThe full-page source lands at `components/block/haze/form-wizard-block.tsx` (via the `@block/` target placeholder from `components.json`) — vendored, so you own and edit it. Component imports are rewritten to the `haze-ui` npm package, which is installed automatically together with `zod` and `react-use-control`.\n\nThe file imports the stylesheet of every component it renders (`haze-ui/css/tokens.css` + 11 component css files), so no extra stylesheet setup is needed.\n\n## Theming\n\nThis wrapper re-exports the 'lightTheme' (or 'darkTheme'), 'spacing' and\n'typography' classes from 'haze-ui' and imports 'haze-ui/css/tokens.css'.\n\nActivate the design tokens by applying the classes to a root element:\n\n```jsx\nimport { lightTheme, spacing, typography } from 'haze-ui';\n\n// on <body> or any root container — darkTheme is a drop-in swap\n<div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n```\n\nLive preview and source: https://wmzy.github.io/haze-ui/#/blocks — https://github.com/wmzy/haze-ui/tree/main/registry",
1736
+ "categories": [
1737
+ "blocks",
1738
+ "form"
1739
+ ],
1740
+ "dependencies": [
1741
+ "haze-ui",
1742
+ "zod",
1743
+ "react-use-control"
1744
+ ],
1745
+ "registryDependencies": [
1746
+ "wmzy/haze-ui/stepper",
1747
+ "wmzy/haze-ui/card",
1748
+ "wmzy/haze-ui/form",
1749
+ "wmzy/haze-ui/input",
1750
+ "wmzy/haze-ui/select",
1751
+ "wmzy/haze-ui/checkbox",
1752
+ "wmzy/haze-ui/button",
1753
+ "wmzy/haze-ui/alert",
1754
+ "wmzy/haze-ui/chip",
1755
+ "wmzy/haze-ui/spinner",
1756
+ "wmzy/haze-ui/virtual-list"
1757
+ ],
1758
+ "files": [
1759
+ {
1760
+ "path": "components/block/haze/form-wizard-block.tsx",
1761
+ "content": "// haze-ui registry block — generated by scripts/generate-registry.mjs\n// from src/views/Blocks/blocks/FormWizardBlock.tsx.\n//\n// Unlike the component items (thin re-export wrappers over the haze-ui npm\n// package), a block is VENDORED source: a full page you own and edit.\n// The component imports are rewritten from the docs site's '@/lib' alias\n// to the published 'haze-ui' package and every stylesheet of the page's\n// component closure is imported below, so the file is self-contained.\n// npm dependencies beyond haze-ui: zod, react-use-control.\n//\n// Theming (once per app): apply the token classes to a root element, e.g.\n// import { lightTheme, spacing, typography } from 'haze-ui';\n// <div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n// (darkTheme is a drop-in swap).\n\nimport 'haze-ui/css/tokens.css';\nimport 'haze-ui/css/alert.css';\nimport 'haze-ui/css/button.css';\nimport 'haze-ui/css/card.css';\nimport 'haze-ui/css/checkbox.css';\nimport 'haze-ui/css/chip.css';\nimport 'haze-ui/css/form.css';\nimport 'haze-ui/css/input.css';\nimport 'haze-ui/css/select.css';\nimport 'haze-ui/css/spinner.css';\nimport 'haze-ui/css/stepper.css';\nimport 'haze-ui/css/virtual-list.css';\n\nimport { z } from 'zod';\nimport { useControl } from 'react-use-control';\n\nimport {\n Alert,\n Button,\n Card,\n CheckboxCore,\n Form,\n FormItem,\n InputCore,\n Option,\n SelectCore,\n Step,\n Stepper,\n trigger,\n useForm,\n zodResolver,\n} from 'haze-ui';\n\n/*\n * Form wizard block: a centered Card walking a three-step workspace setup.\n * One react-f0rm Form wraps every step so values survive step changes, a\n * single zod schema defines the record (each field validated by its slice\n * through zodResolver — field-level, so the per-step trigger() actually\n * runs it), and the Stepper's activeStep is a Control the host app can\n * drive or observe. Next awaits trigger() on just the current step's\n * fields (gated advancement, first error focused), and the final submit\n * lands in an Alert receipt.\n */\n\ntype WizardValues = {\n fullName: string;\n workEmail: string;\n workspaceName: string;\n teamSize: string;\n region: string;\n productNews: boolean;\n weeklyDigest: boolean;\n};\n\nconst wizardSchema = z.object({\n fullName: z.string().min(2, 'Tell us your full name'),\n workEmail: z.email('Enter a valid work email'),\n workspaceName: z.string().min(3, 'At least 3 characters'),\n teamSize: z.string().min(1, 'Pick a team size'),\n region: z.string().min(1, 'Pick a data region'),\n productNews: z.boolean(),\n weeklyDigest: z.boolean(),\n});\n\n// Field-level validators sliced out of the one schema — trigger() with\n// explicit names runs field validators only (form-level validate is\n// skipped), so this is the channel per-step gating has to go through.\nconst validateFullName = zodResolver(wizardSchema.shape.fullName);\nconst validateWorkEmail = zodResolver(wizardSchema.shape.workEmail);\nconst validateWorkspaceName = zodResolver(wizardSchema.shape.workspaceName);\nconst validateTeamSize = zodResolver(wizardSchema.shape.teamSize);\nconst validateRegion = zodResolver(wizardSchema.shape.region);\n\nconst STEPS = [\n { title: 'Account', description: 'Who you are', fields: ['fullName', 'workEmail'] },\n {\n title: 'Workspace',\n description: 'Where the team lands',\n fields: ['workspaceName', 'teamSize', 'region'],\n },\n { title: 'Preferences', description: 'Stay in the loop', fields: [] },\n] as const;\n\nfunction WizardForm() {\n const form = useForm<WizardValues>({\n initialValues: {\n fullName: '',\n workEmail: '',\n workspaceName: '',\n teamSize: '',\n region: '',\n productNews: true,\n weeklyDigest: false,\n },\n // Step fields unmount on every step change; the library default\n // (tombstone) would drop their values, but a wizard's whole point is\n // one record accumulating across steps.\n shouldUnregister: false,\n });\n // Step cursor is user-facing state — a Control, so a host app can deep\n // link or observe progress. `created` is demo-local display state.\n const [step, setStep, stepCtrl] = useControl(undefined, 0);\n const [created, setCreated] = useControl(undefined, false);\n const lastStep = STEPS.length - 1;\n\n async function goNext() {\n const fields = STEPS[step]?.fields ?? [];\n // Validate just this step's slice of the schema; only a clean step\n // advances (the first errored field takes focus via shouldFocus).\n const ok =\n fields.length === 0\n ? true\n : await trigger(form, [...fields] as string[], { shouldFocus: true });\n if (ok) setStep(Math.min(step + 1, lastStep));\n }\n\n return (\n <Form form={form} onValidSubmit={() => setCreated(true)}>\n <div\n data-slot='wizard-body'\n style={{\n display: 'flex',\n flexDirection: 'column',\n gap: 'var(--haze-space-5)',\n }}\n >\n <Stepper activeStep={stepCtrl}>\n {STEPS.map((s) => (\n <Step key={s.title} title={s.title} description={s.description} />\n ))}\n </Stepper>\n\n {created ? (\n <Alert color='success'>\n Workspace created — the whole schema passed. Close and reopen the\n preview to run the wizard again.\n </Alert>\n ) : step === 0 ? (\n <div\n data-slot='wizard-step-account'\n style={{\n display: 'flex',\n flexDirection: 'column',\n gap: 'var(--haze-space-4)',\n }}\n >\n <FormItem\n form={form}\n name='fullName'\n label='Full name'\n validate={validateFullName}\n mode='onBlur'\n input={InputCore}\n placeholder='Ada Lovelace'\n />\n <FormItem\n form={form}\n name='workEmail'\n label='Work email'\n validate={validateWorkEmail}\n mode='onBlur'\n input={InputCore}\n type='email'\n placeholder='you@company.com'\n />\n </div>\n ) : step === 1 ? (\n <div\n data-slot='wizard-step-workspace'\n style={{\n display: 'flex',\n flexDirection: 'column',\n gap: 'var(--haze-space-4)',\n }}\n >\n <FormItem\n form={form}\n name='workspaceName'\n label='Workspace name'\n validate={validateWorkspaceName}\n mode='onBlur'\n input={InputCore}\n placeholder='Platform team'\n />\n <FormItem\n form={form}\n name='teamSize'\n label='Team size'\n validate={validateTeamSize}\n mode='onBlur'\n input={SelectCore}\n >\n <Option value=''>Pick a size…</Option>\n <Option value='2-9'>2–9 people</Option>\n <Option value='10-49'>10–49 people</Option>\n <Option value='50-199'>50–199 people</Option>\n <Option value='200+'>200+ people</Option>\n </FormItem>\n <FormItem\n form={form}\n name='region'\n label='Data region'\n validate={validateRegion}\n mode='onBlur'\n input={SelectCore}\n >\n <Option value=''>Pick a region…</Option>\n <Option value='eu-central'>Frankfurt (eu-central)</Option>\n <Option value='us-east'>N. Virginia (us-east)</Option>\n <Option value='ap-northeast'>Tokyo (ap-northeast)</Option>\n </FormItem>\n </div>\n ) : (\n <div\n data-slot='wizard-step-preferences'\n style={{\n display: 'flex',\n flexDirection: 'column',\n gap: 'var(--haze-space-4)',\n }}\n >\n <FormItem\n form={form}\n name='productNews'\n label='Product news'\n input={CheckboxCore}\n valueToProps={(checked) => ({ checked: !!checked })}\n />\n <FormItem\n form={form}\n name='weeklyDigest'\n label='Weekly workspace digest'\n input={CheckboxCore}\n valueToProps={(checked) => ({ checked: !!checked })}\n />\n <p\n style={{\n margin: 0,\n fontSize: 'var(--haze-text-xs)',\n color: 'var(--haze-color-text-muted)',\n }}\n >\n Both toggles are plain form values — they submit with everything\n else and can be preset from the host app.\n </p>\n </div>\n )}\n\n <div\n data-slot='wizard-footer'\n style={{\n display: 'flex',\n justifyContent: 'space-between',\n gap: 'var(--haze-space-2)',\n borderTop: '1px solid var(--haze-color-border)',\n paddingTop: 'var(--haze-space-4)',\n }}\n >\n <Button\n variant='outline'\n disabled={step === 0 || created}\n onClick={() => setStep(Math.max(step - 1, 0))}\n >\n Back\n </Button>\n {step < lastStep ? (\n <Button onClick={() => void goNext()}>Next step</Button>\n ) : (\n /* Button's native `type` is pinned to 'button' — submit through\n the owning form so the zod chain runs first. */\n <Button disabled={created} onClick={(e) => e.currentTarget.form?.requestSubmit()}>\n Create workspace\n </Button>\n )}\n </div>\n </div>\n </Form>\n );\n}\n\nexport default function FormWizardBlock() {\n return (\n <div\n data-slot='form-wizard-block'\n style={{\n minHeight: '520px',\n display: 'flex',\n justifyContent: 'center',\n alignItems: 'flex-start',\n padding: 'var(--haze-space-6)',\n background: 'var(--haze-color-bg)',\n fontFamily: 'var(--haze-font-sans)',\n color: 'var(--haze-color-text)',\n }}\n >\n <Card style={{ width: '100%', maxWidth: '680px' }}>\n <div\n data-slot='wizard-header'\n style={{\n display: 'flex',\n flexDirection: 'column',\n gap: 'var(--haze-space-1)',\n marginBottom: 'var(--haze-space-5)',\n }}\n >\n <h2\n style={{\n margin: 0,\n fontSize: 'var(--haze-text-xl)',\n fontWeight: 'var(--haze-weight-semibold)',\n }}\n >\n Create your workspace\n </h2>\n <p\n style={{\n margin: 0,\n fontSize: 'var(--haze-text-sm)',\n color: 'var(--haze-color-text-secondary)',\n }}\n >\n Three steps, one zod schema — values persist across steps and only\n a valid step advances.\n </p>\n </div>\n <WizardForm />\n </Card>\n </div>\n );\n}\n",
1762
+ "type": "registry:block"
1763
+ }
1764
+ ]
1765
+ },
1538
1766
  {
1539
1767
  "name": "grid",
1540
1768
  "type": "registry:ui",
@@ -1808,6 +2036,46 @@
1808
2036
  }
1809
2037
  ]
1810
2038
  },
2039
+ {
2040
+ "name": "kanban-block",
2041
+ "type": "registry:block",
2042
+ "title": "Kanban block (four-column board with priority badges and progress)",
2043
+ "description": "Four columns of issue cards with priority `Badge`s, labels, assignee `Avatar`s and `Progress` bars; cards move through the `DropdownMenu` ⋯ menu — haze-ui ships single-container sorting, not cross-column drag, so the board is static by design.",
2044
+ "author": "wmzy <https://github.com/wmzy>",
2045
+ "docs": "Four columns of issue cards with priority `Badge`s, labels, assignee `Avatar`s and `Progress` bars; cards move through the `DropdownMenu` ⋯ menu — haze-ui ships single-container sorting, not cross-column drag, so the board is static by design.\n\n## Install\n\n```bash\nnpx shadcn@latest add wmzy/haze-ui/kanban-block\n```\n\nThe full-page source lands at `components/block/haze/kanban-block.tsx` (via the `@block/` target placeholder from `components.json`) — vendored, so you own and edit it. Component imports are rewritten to the `haze-ui` npm package, which is installed automatically together with `lucide-react` and `react-use-control`.\n\nThe file imports the stylesheet of every component it renders (`haze-ui/css/tokens.css` + 14 component css files), so no extra stylesheet setup is needed.\n\n## Theming\n\nThis wrapper re-exports the 'lightTheme' (or 'darkTheme'), 'spacing' and\n'typography' classes from 'haze-ui' and imports 'haze-ui/css/tokens.css'.\n\nActivate the design tokens by applying the classes to a root element:\n\n```jsx\nimport { lightTheme, spacing, typography } from 'haze-ui';\n\n// on <body> or any root container — darkTheme is a drop-in swap\n<div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n```\n\nLive preview and source: https://wmzy.github.io/haze-ui/#/blocks — https://github.com/wmzy/haze-ui/tree/main/registry",
2046
+ "categories": [
2047
+ "blocks",
2048
+ "data-display"
2049
+ ],
2050
+ "dependencies": [
2051
+ "haze-ui",
2052
+ "lucide-react",
2053
+ "react-use-control"
2054
+ ],
2055
+ "registryDependencies": [
2056
+ "wmzy/haze-ui/avatar",
2057
+ "wmzy/haze-ui/avatar-group",
2058
+ "wmzy/haze-ui/badge",
2059
+ "wmzy/haze-ui/button",
2060
+ "wmzy/haze-ui/chip",
2061
+ "wmzy/haze-ui/dropdown-menu",
2062
+ "wmzy/haze-ui/empty",
2063
+ "wmzy/haze-ui/input",
2064
+ "wmzy/haze-ui/progress",
2065
+ "wmzy/haze-ui/select",
2066
+ "wmzy/haze-ui/spinner",
2067
+ "wmzy/haze-ui/tooltip",
2068
+ "wmzy/haze-ui/virtual-list",
2069
+ "wmzy/haze-ui/visually-hidden"
2070
+ ],
2071
+ "files": [
2072
+ {
2073
+ "path": "components/block/haze/kanban-block.tsx",
2074
+ "content": "// haze-ui registry block — generated by scripts/generate-registry.mjs\n// from src/views/Blocks/blocks/KanbanBlock.tsx.\n//\n// Unlike the component items (thin re-export wrappers over the haze-ui npm\n// package), a block is VENDORED source: a full page you own and edit.\n// The component imports are rewritten from the docs site's '@/lib' alias\n// to the published 'haze-ui' package and every stylesheet of the page's\n// component closure is imported below, so the file is self-contained.\n// npm dependencies beyond haze-ui: lucide-react, react-use-control.\n//\n// Theming (once per app): apply the token classes to a root element, e.g.\n// import { lightTheme, spacing, typography } from 'haze-ui';\n// <div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n// (darkTheme is a drop-in swap).\n\nimport 'haze-ui/css/tokens.css';\nimport 'haze-ui/css/avatar.css';\nimport 'haze-ui/css/avatar-group.css';\nimport 'haze-ui/css/badge.css';\nimport 'haze-ui/css/button.css';\nimport 'haze-ui/css/chip.css';\nimport 'haze-ui/css/dropdown-menu.css';\nimport 'haze-ui/css/empty.css';\nimport 'haze-ui/css/input.css';\nimport 'haze-ui/css/progress.css';\nimport 'haze-ui/css/select.css';\nimport 'haze-ui/css/spinner.css';\nimport 'haze-ui/css/tooltip.css';\nimport 'haze-ui/css/virtual-list.css';\nimport 'haze-ui/css/visually-hidden.css';\n\nimport { useMemo, useState } from 'react';\nimport { useControl } from 'react-use-control';\nimport { MoreHorizontal, Plus, Search } from 'lucide-react';\n\nimport {\n Avatar,\n AvatarGroup,\n Badge,\n Button,\n Chip,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n Empty,\n Input,\n Progress,\n Select,\n Option,\n Tooltip,\n VisuallyHidden,\n} from 'haze-ui';\n\n/*\n * Kanban block: a sprint board — four columns of issue cards with priority\n * badges, assignee avatars, labels and progress bars.\n *\n * Drag-and-drop was deliberately left out: haze-ui ships dnd only inside\n * single-container wrappers (SortableTagGroup / SortableTabList /\n * SortableTree), none of which compose into cross-column card dragging, and\n * views never import @dnd-kit directly — a raw DndContext here would break\n * the \"blocks are pure haze-ui compositions\" premise the page (and its\n * StackBlitz css-family mechanism) is built on. Card movement is real state\n * instead: every card's ⋯ menu moves it between columns, and the source\n * stays honest about what the library gives you.\n */\n\ntype ColumnId = 'backlog' | 'progress' | 'review' | 'done';\ntype Priority = 'P0' | 'P1' | 'P2';\n\ntype Issue = {\n id: string;\n title: string;\n column: ColumnId;\n priority: Priority;\n labels: string[];\n assignees: string[];\n points: number;\n progress: number;\n};\n\nconst COLUMNS: { id: ColumnId; name: string; hint: string; color: string }[] = [\n { id: 'backlog', name: 'Backlog', hint: 'groomed next sprint', color: 'var(--haze-color-text-muted)' },\n { id: 'progress', name: 'In progress', hint: 'WIP limit 4', color: 'var(--haze-color-primary)' },\n { id: 'review', name: 'Review', hint: 'needs a reviewer', color: 'var(--haze-color-warning)' },\n { id: 'done', name: 'Done', hint: 'this sprint', color: 'var(--haze-color-success)' },\n];\n\nconst INITIAL_ISSUES: Issue[] = [\n { id: 'NB-412', title: 'Idempotency-Key on webhook retries', column: 'progress', priority: 'P0', labels: ['gateway', 'reliability'], assignees: ['Ana Petrova', 'Ben Okafor'], points: 5, progress: 60 },\n { id: 'NB-415', title: 'Batched payload fallback after 2 failures', column: 'progress', priority: 'P1', labels: ['gateway'], assignees: ['Ben Okafor'], points: 3, progress: 25 },\n { id: 'NB-401', title: 'Freeze clock fixture in checkout e2e', column: 'review', priority: 'P1', labels: ['flaky', 'e2e'], assignees: ['Dana Silva', 'Chen Wei', 'Ana Petrova'], points: 2, progress: 90 },\n { id: 'NB-408', title: 'Trim ingest payload snapshot to 1 MB', column: 'review', priority: 'P2', labels: ['ingest'], assignees: ['Chen Wei'], points: 1, progress: 100 },\n { id: 'NB-419', title: 'Alert on webhook error-rate cliff', column: 'backlog', priority: 'P1', labels: ['observability'], assignees: ['Dana Silva'], points: 3, progress: 0 },\n { id: 'NB-421', title: 'Runbook: collector rollbacks', column: 'backlog', priority: 'P2', labels: ['docs'], assignees: [], points: 2, progress: 0 },\n { id: 'NB-422', title: 'Rotate CI secrets quarterly', column: 'backlog', priority: 'P0', labels: ['security', 'infra'], assignees: ['Ana Petrova'], points: 1, progress: 0 },\n { id: 'NB-418', title: 'Dashboard: 30d uptime column', column: 'backlog', priority: 'P2', labels: ['console'], assignees: ['Chen Wei', 'Dana Silva'], points: 2, progress: 0 },\n { id: 'NB-396', title: 'v2.3 migration notes', column: 'done', priority: 'P1', labels: ['docs'], assignees: ['Ben Okafor'], points: 2, progress: 100 },\n { id: 'NB-399', title: 'Backoff cap 2s → 8s', column: 'done', priority: 'P0', labels: ['gateway'], assignees: ['Ana Petrova', 'Chen Wei'], points: 3, progress: 100 },\n { id: 'NB-403', title: 'Retry jitter across collectors', column: 'done', priority: 'P1', labels: ['gateway', 'reliability'], assignees: ['Ben Okafor'], points: 2, progress: 100 },\n];\n\nconst NEXT_POOL: Omit<Issue, 'id' | 'column'>[] = [\n { title: 'Spike: QUIC for collector transport', priority: 'P2', labels: ['spike'], assignees: [], points: 1, progress: 0 },\n { title: 'Reduce cold-start of ingest workers', priority: 'P1', labels: ['ingest', 'perf'], assignees: ['Chen Wei'], points: 3, progress: 0 },\n { title: 'Audit PII in payload snapshots', priority: 'P0', labels: ['security'], assignees: ['Dana Silva', 'Ana Petrova'], points: 2, progress: 0 },\n { title: 'Changelog page for v2.4', priority: 'P2', labels: ['docs'], assignees: [], points: 1, progress: 0 },\n];\n\nconst initials = (name: string) =>\n name\n .split(' ')\n .map((part) => part.charAt(0))\n .join('')\n .slice(0, 2);\n\nconst priorityBadge = (priority: Priority) => (\n <Badge\n color={priority === 'P0' ? 'danger' : priority === 'P1' ? 'warning' : 'default'}\n >\n {priority}\n </Badge>\n);\n\nfunction IssueMenu({ issue, onMove, onDelete }: {\n issue: Issue;\n onMove: (id: string, column: ColumnId) => void;\n onDelete: (id: string) => void;\n}) {\n return (\n <DropdownMenu>\n <DropdownMenuTrigger>\n <VisuallyHidden>Actions for {issue.id}</VisuallyHidden>\n <MoreHorizontal size={14} aria-hidden='true' />\n </DropdownMenuTrigger>\n <DropdownMenuContent align='end'>\n {COLUMNS.filter((column) => column.id !== issue.column).map((column) => (\n <DropdownMenuItem key={column.id} onClick={() => onMove(issue.id, column.id)}>\n Move to {column.name}\n </DropdownMenuItem>\n ))}\n <DropdownMenuSeparator />\n <DropdownMenuItem danger onClick={() => onDelete(issue.id)}>\n Delete issue\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n\nfunction IssueCard({ issue, onMove, onDelete }: {\n issue: Issue;\n onMove: (id: string, column: ColumnId) => void;\n onDelete: (id: string) => void;\n}) {\n return (\n <article\n data-slot='kanban-card'\n style={{\n display: 'flex',\n flexDirection: 'column',\n gap: 'var(--haze-space-2)',\n padding: 'var(--haze-space-3)',\n border: '1px solid var(--haze-color-border)',\n borderRadius: 'var(--haze-radius-md)',\n background: 'var(--haze-color-bg)',\n }}\n >\n <div style={{ display: 'flex', alignItems: 'flex-start', gap: 'var(--haze-space-2)' }}>\n <div\n style={{\n flex: 1,\n minWidth: 0,\n fontSize: 'var(--haze-text-sm)',\n fontWeight: 'var(--haze-weight-medium)',\n lineHeight: 'var(--haze-leading-snug)',\n }}\n >\n <span\n style={{\n fontFamily: 'var(--haze-font-mono)',\n fontSize: 'var(--haze-text-xs)',\n color: 'var(--haze-color-text-muted)',\n display: 'block',\n }}\n >\n {issue.id}\n </span>\n {issue.title}\n </div>\n <IssueMenu issue={issue} onMove={onMove} onDelete={onDelete} />\n </div>\n\n {issue.labels.length > 0 && (\n <div style={{ display: 'flex', gap: 'var(--haze-space-1)', flexWrap: 'wrap' }}>\n {issue.labels.map((label) => (\n <Chip key={label} variant='outline'>\n {label}\n </Chip>\n ))}\n </div>\n )}\n\n {issue.progress > 0 && <Progress value={issue.progress} size='sm' color='success' />}\n\n <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--haze-space-2)' }}>\n {issue.assignees.length > 0 ? (\n issue.assignees.length > 1 ? (\n <AvatarGroup max={2} total={issue.assignees.length}>\n {issue.assignees.map((name) => (\n <Tooltip key={name} content={name}>\n <Avatar size='sm' fallback={initials(name)} />\n </Tooltip>\n ))}\n </AvatarGroup>\n ) : (\n <Avatar size='sm' fallback={initials(issue.assignees[0]!)} />\n )\n ) : (\n <span style={{ fontSize: 'var(--haze-text-xs)', color: 'var(--haze-color-text-muted)' }}>\n unassigned\n </span>\n )}\n <span style={{ flex: 1 }} />\n <Tooltip content='Story points'>\n <span\n style={{\n fontFamily: 'var(--haze-font-mono)',\n fontSize: 'var(--haze-text-xs)',\n color: 'var(--haze-color-text-secondary)',\n }}\n >\n {issue.points} pt\n </span>\n </Tooltip>\n {priorityBadge(issue.priority)}\n </div>\n </article>\n );\n}\n\nexport default function KanbanBlock() {\n const [issues, setIssues] = useState(INITIAL_ISSUES);\n const [nextIndex, setNextIndex] = useState(0);\n // User-facing filter state as Controls: priority + title search.\n const [priority, setPriority, priorityCtrl] = useControl(undefined, 'all');\n const [search, setSearch, searchCtrl] = useControl(undefined, '');\n\n const visible = useMemo(\n () =>\n issues.filter(\n (issue) =>\n (priority === 'all' || issue.priority === priority) &&\n (search.trim() === '' ||\n `${issue.id} ${issue.title}`.toLowerCase().includes(search.trim().toLowerCase()))\n ),\n [issues, priority, search]\n );\n\n const byColumn = (column: ColumnId) => visible.filter((issue) => issue.column === column);\n\n const moveIssue = (id: string, column: ColumnId) => {\n setIssues((prev) => prev.map((issue) => (issue.id === id ? { ...issue, column } : issue)));\n };\n\n const deleteIssue = (id: string) => {\n setIssues((prev) => prev.filter((issue) => issue.id !== id));\n };\n\n const addIssue = () => {\n // Cycled pool keeps \"New issue\" infinitely demoable without a form;\n // ids stay unique per click via the counter.\n const seed = NEXT_POOL[nextIndex % NEXT_POOL.length]!;\n const id = `NB-${430 + nextIndex}`;\n setNextIndex((prev) => prev + 1);\n setIssues((prev) => [\n { ...seed, id, column: 'backlog' },\n ...prev,\n ]);\n };\n\n return (\n <div\n data-slot='kanban-block'\n style={{\n height: '100%',\n display: 'flex',\n flexDirection: 'column',\n fontFamily: 'var(--haze-font-sans)',\n color: 'var(--haze-color-text)',\n background: 'var(--haze-color-bg-subtle)',\n }}\n >\n <header\n data-slot='kanban-header'\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: 'var(--haze-space-2)',\n flexWrap: 'wrap',\n padding: 'var(--haze-space-3) var(--haze-space-4)',\n borderBottom: '1px solid var(--haze-color-border)',\n background: 'var(--haze-color-bg)',\n }}\n >\n <div>\n <h2 style={{ margin: 0, fontSize: 'var(--haze-text-lg)', fontWeight: 'var(--haze-weight-semibold)' }}>\n Nimbus · sprint 42 board\n </h2>\n <p\n style={{\n margin: 0,\n fontSize: 'var(--haze-text-sm)',\n color: 'var(--haze-color-text-secondary)',\n }}\n >\n {visible.length} issues · move cards from the ⋯ menu\n </p>\n </div>\n <span style={{ flex: 1 }} />\n <Input\n size='sm'\n value={searchCtrl}\n onChange={(event) => setSearch(event.target.value)}\n placeholder='Search issues…'\n aria-label='Search issues'\n style={{ width: '180px' }}\n />\n <Select\n size='sm'\n value={priorityCtrl}\n onValuesChange={(next) => setPriority(typeof next === 'string' ? next : 'all')}\n aria-label='Filter by priority'\n >\n <Option value='all'>All priorities</Option>\n <Option value='P0'>P0</Option>\n <Option value='P1'>P1</Option>\n <Option value='P2'>P2</Option>\n </Select>\n <Button size='sm' onClick={addIssue}>\n <Plus size={14} aria-hidden='true' />\n New issue\n </Button>\n </header>\n\n <div\n data-slot='kanban-columns'\n style={{\n flex: 1,\n minHeight: 0,\n display: 'flex',\n gap: 'var(--haze-space-3)',\n padding: 'var(--haze-space-3) var(--haze-space-4)',\n overflowX: 'auto',\n }}\n >\n {COLUMNS.map((column) => {\n const columnIssues = byColumn(column.id);\n return (\n <section\n key={column.id}\n data-slot='kanban-column'\n aria-label={`${column.name} — ${columnIssues.length} issues`}\n style={{\n flex: '1 1 260px',\n minWidth: '240px',\n display: 'flex',\n flexDirection: 'column',\n minHeight: 0,\n border: '1px solid var(--haze-color-border)',\n borderRadius: 'var(--haze-radius-lg)',\n background: 'var(--haze-color-bg)',\n }}\n >\n <header\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: 'var(--haze-space-2)',\n padding: 'var(--haze-space-2) var(--haze-space-3)',\n borderBottom: '1px solid var(--haze-color-border)',\n }}\n >\n <span\n aria-hidden='true'\n style={{\n width: 'var(--haze-space-2)',\n height: 'var(--haze-space-2)',\n borderRadius: 'var(--haze-radius-full)',\n background: column.color,\n flexShrink: 0,\n }}\n />\n <strong style={{ fontSize: 'var(--haze-text-sm)' }}>{column.name}</strong>\n <Badge>{columnIssues.length}</Badge>\n <span style={{ flex: 1 }} />\n <span\n style={{\n fontSize: 'var(--haze-text-xs)',\n color: 'var(--haze-color-text-muted)',\n }}\n >\n {column.hint}\n </span>\n </header>\n <div\n data-slot='kanban-column-cards'\n style={{\n flex: 1,\n minHeight: 0,\n overflowY: 'auto',\n display: 'flex',\n flexDirection: 'column',\n gap: 'var(--haze-space-2)',\n padding: 'var(--haze-space-3)',\n }}\n >\n {columnIssues.length === 0 ? (\n <Empty description={`Nothing in ${column.name.toLowerCase()}`} />\n ) : (\n columnIssues.map((issue) => (\n <IssueCard\n key={issue.id}\n issue={issue}\n onMove={moveIssue}\n onDelete={deleteIssue}\n />\n ))\n )}\n </div>\n </section>\n );\n })}\n </div>\n\n <footer\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: 'var(--haze-space-2)',\n padding: 'var(--haze-space-2) var(--haze-space-4)',\n borderTop: '1px solid var(--haze-color-border)',\n background: 'var(--haze-color-bg)',\n fontSize: 'var(--haze-text-xs)',\n color: 'var(--haze-color-text-muted)',\n }}\n >\n <Search size={12} aria-hidden='true' />\n Static board by design — haze-ui ships single-container sorting\n (SortableTagGroup / SortableTabList / SortableTree), not cross-column drag.\n </footer>\n </div>\n );\n}\n",
2075
+ "type": "registry:block"
2076
+ }
2077
+ ]
2078
+ },
1811
2079
  {
1812
2080
  "name": "kbd",
1813
2081
  "type": "registry:ui",
@@ -2115,6 +2383,27 @@
2115
2383
  }
2116
2384
  ]
2117
2385
  },
2386
+ {
2387
+ "name": "notification-center",
2388
+ "type": "registry:ui",
2389
+ "title": "NotificationCenter",
2390
+ "description": "Persistent notification feed behind a bell trigger: unread-count badge, severity/kind rows, mark-all-read and clear-all — declarative `items`, not an imperative open() API.",
2391
+ "author": "wmzy <https://github.com/wmzy>",
2392
+ "docs": "Persistent notification feed behind a bell trigger: unread-count badge, severity/kind rows, mark-all-read and clear-all — declarative `items`, not an imperative open() API.\n\nExports: `NotificationCenter`.\n\n## Install\n\n```bash\nnpx shadcn@latest add wmzy/haze-ui/notification-center\n```\n\nThe wrapper lands at `components/ui/haze/notification-center.tsx` (via the `@ui/` target placeholder from `components.json`) and re-exports the component(s) from the `haze-ui` npm package, which is installed automatically.\n\n## Theming\n\nhaze-ui ships JS and CSS separately — the installed wrapper already imports\n`haze-ui/css/tokens.css` and `haze-ui/css/notification-center.css`, so no extra stylesheet setup is needed.\n\nActivate the design tokens by applying the classes to a root element:\n\n```jsx\nimport { lightTheme, spacing, typography } from 'haze-ui';\n\n// on <body> or any root container — darkTheme is a drop-in swap\n<div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n```\n\nDetails: https://github.com/wmzy/haze-ui/tree/main/registry",
2393
+ "categories": [
2394
+ "overlay"
2395
+ ],
2396
+ "dependencies": [
2397
+ "haze-ui"
2398
+ ],
2399
+ "files": [
2400
+ {
2401
+ "path": "components/haze/notification-center.tsx",
2402
+ "content": "// haze-ui registry wrapper — generated by scripts/generate-registry.mjs.\n//\n// This is a thin wrapper around the published haze-ui npm package, not\n// vendored component source: it re-exports the component(s) from\n// 'haze-ui' and imports their stylesheet, so the package keeps updating\n// underneath. Edit this file to customize — wrap, restyle, compose; that\n// is what it is for. (haze-ui is compiled CSS-in-JS (linaria) styled\n// against --haze-* design tokens, so vendoring the source would neither\n// build nor style — forking this wrapper is the intended customization\n// path.)\n//\n// Theming (once per app): apply the token classes to a root element, e.g.\n// import { lightTheme, spacing, typography } from 'haze-ui';\n// <div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n// (darkTheme is a drop-in swap). haze-ui dist modules ship their own\n// 'use client' directive, so imports resolve cleanly from RSC; the\n// wrapper inherits client semantics either way.\n\nimport { NotificationCenter } from 'haze-ui';\n\nimport 'haze-ui/css/tokens.css';\nimport 'haze-ui/css/notification-center.css';\nimport 'haze-ui/css/badge.css';\nimport 'haze-ui/css/icon.css';\n\nexport { NotificationCenter };\nexport type { NotificationCenterProps, NotificationCenterClassNames, NotificationCenterItem, NotificationCenterSeverity, NotificationCenterKind } from 'haze-ui';\n",
2403
+ "type": "registry:ui"
2404
+ }
2405
+ ]
2406
+ },
2118
2407
  {
2119
2408
  "name": "number-input",
2120
2409
  "type": "registry:ui",
@@ -2283,6 +2572,40 @@
2283
2572
  }
2284
2573
  ]
2285
2574
  },
2575
+ {
2576
+ "name": "profile-block",
2577
+ "type": "registry:block",
2578
+ "title": "Profile block (identity header + Descriptions + Timeline)",
2579
+ "description": "A member detail page: a large `Avatar` header with status `Badge`s and a controllable follow toggle, a `Descriptions` fact sheet, a `TagGroup` skill row, an `AvatarGroup` teammates strip and a `Timeline` activity feed.",
2580
+ "author": "wmzy <https://github.com/wmzy>",
2581
+ "docs": "A member detail page: a large `Avatar` header with status `Badge`s and a controllable follow toggle, a `Descriptions` fact sheet, a `TagGroup` skill row, an `AvatarGroup` teammates strip and a `Timeline` activity feed.\n\n## Install\n\n```bash\nnpx shadcn@latest add wmzy/haze-ui/profile-block\n```\n\nThe full-page source lands at `components/block/haze/profile-block.tsx` (via the `@block/` target placeholder from `components.json`) — vendored, so you own and edit it. Component imports are rewritten to the `haze-ui` npm package, which is installed automatically together with `react-use-control`.\n\nThe file imports the stylesheet of every component it renders (`haze-ui/css/tokens.css` + 9 component css files), so no extra stylesheet setup is needed.\n\n## Theming\n\nThis wrapper re-exports the 'lightTheme' (or 'darkTheme'), 'spacing' and\n'typography' classes from 'haze-ui' and imports 'haze-ui/css/tokens.css'.\n\nActivate the design tokens by applying the classes to a root element:\n\n```jsx\nimport { lightTheme, spacing, typography } from 'haze-ui';\n\n// on <body> or any root container — darkTheme is a drop-in swap\n<div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n```\n\nLive preview and source: https://wmzy.github.io/haze-ui/#/blocks — https://github.com/wmzy/haze-ui/tree/main/registry",
2582
+ "categories": [
2583
+ "blocks",
2584
+ "data-display"
2585
+ ],
2586
+ "dependencies": [
2587
+ "haze-ui",
2588
+ "react-use-control"
2589
+ ],
2590
+ "registryDependencies": [
2591
+ "wmzy/haze-ui/card",
2592
+ "wmzy/haze-ui/avatar",
2593
+ "wmzy/haze-ui/avatar-group",
2594
+ "wmzy/haze-ui/badge",
2595
+ "wmzy/haze-ui/button",
2596
+ "wmzy/haze-ui/descriptions",
2597
+ "wmzy/haze-ui/timeline",
2598
+ "wmzy/haze-ui/tag-group",
2599
+ "wmzy/haze-ui/spinner"
2600
+ ],
2601
+ "files": [
2602
+ {
2603
+ "path": "components/block/haze/profile-block.tsx",
2604
+ "content": "// haze-ui registry block — generated by scripts/generate-registry.mjs\n// from src/views/Blocks/blocks/ProfileBlock.tsx.\n//\n// Unlike the component items (thin re-export wrappers over the haze-ui npm\n// package), a block is VENDORED source: a full page you own and edit.\n// The component imports are rewritten from the docs site's '@/lib' alias\n// to the published 'haze-ui' package and every stylesheet of the page's\n// component closure is imported below, so the file is self-contained.\n// npm dependencies beyond haze-ui: react-use-control.\n//\n// Theming (once per app): apply the token classes to a root element, e.g.\n// import { lightTheme, spacing, typography } from 'haze-ui';\n// <div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n// (darkTheme is a drop-in swap).\n\nimport 'haze-ui/css/tokens.css';\nimport 'haze-ui/css/avatar.css';\nimport 'haze-ui/css/avatar-group.css';\nimport 'haze-ui/css/badge.css';\nimport 'haze-ui/css/button.css';\nimport 'haze-ui/css/card.css';\nimport 'haze-ui/css/descriptions.css';\nimport 'haze-ui/css/spinner.css';\nimport 'haze-ui/css/tag-group.css';\nimport 'haze-ui/css/timeline.css';\n\nimport type { ReactNode } from 'react';\n\nimport { useControl } from 'react-use-control';\n\nimport {\n Avatar,\n AvatarGroup,\n Badge,\n Button,\n Card,\n Descriptions,\n TagGroup,\n TagGroupItem,\n Timeline,\n TimelineItem,\n} from 'haze-ui';\n\n/*\n * Profile block: a member detail page — identity header (large Avatar,\n * status Badges, action Buttons with a controllable follow toggle) over a\n * Descriptions fact sheet, a TagGroup skill row, an AvatarGroup teammates\n * strip and a Timeline activity feed. Where the Settings block is a\n * control surface, this is a read-mostly detail page that still keeps its\n * one piece of user state (following) behind a Control.\n */\n\nfunction ProfileSection({ title, children }: { title: string; children: ReactNode }) {\n return (\n <Card data-slot='profile-section' variant='outlined'>\n <h3\n style={{\n margin: '0 0 var(--haze-space-3)',\n fontSize: 'var(--haze-text-sm)',\n fontWeight: 'var(--haze-weight-semibold)',\n letterSpacing: '0.04em',\n textTransform: 'uppercase',\n color: 'var(--haze-color-text-muted)',\n }}\n >\n {title}\n </h3>\n {children}\n </Card>\n );\n}\n\nconst TEAMMATES = ['AK', 'JD', 'RS', 'TW', 'YN', 'MZ'];\n\nexport default function ProfileBlock() {\n // Follow state is user-facing — a Control, so the host app can seed it\n // from its social graph and observe the toggle.\n const [following, setFollowing] = useControl(undefined, false);\n\n return (\n <div\n data-slot='profile-block'\n style={{\n display: 'flex',\n flexDirection: 'column',\n gap: 'var(--haze-space-5)',\n padding: 'var(--haze-space-5)',\n fontFamily: 'var(--haze-font-sans)',\n color: 'var(--haze-color-text)',\n background: 'var(--haze-color-bg)',\n }}\n >\n <Card data-slot='profile-header'>\n <div\n style={{\n display: 'flex',\n alignItems: 'flex-start',\n gap: 'var(--haze-space-4)',\n flexWrap: 'wrap',\n }}\n >\n <Avatar size='lg' fallback='ML' />\n <div style={{ flex: 1, minWidth: '220px' }}>\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: 'var(--haze-space-2)',\n flexWrap: 'wrap',\n }}\n >\n <span\n style={{\n fontSize: 'var(--haze-text-xl)',\n fontWeight: 'var(--haze-weight-semibold)',\n }}\n >\n Mira Lindqvist\n </span>\n <Badge color='primary'>Admin</Badge>\n <Badge color='success'>Online</Badge>\n </div>\n <p\n style={{\n margin: 'var(--haze-space-1) 0 0',\n fontSize: 'var(--haze-text-sm)',\n color: 'var(--haze-color-text-secondary)',\n }}\n >\n Staff engineer · Platform infrastructure · @mira\n </p>\n <p\n style={{\n margin: 'var(--haze-space-2) 0 0',\n fontSize: 'var(--haze-text-xs)',\n color: 'var(--haze-color-text-muted)',\n }}\n >\n 1,284 commits this year · 5,930 reviews · 42 on-call weeks\n </p>\n </div>\n <div\n style={{\n display: 'flex',\n gap: 'var(--haze-space-2)',\n }}\n >\n <Button size='sm'>Message</Button>\n <Button\n size='sm'\n variant='outline'\n aria-pressed={following}\n onClick={() => setFollowing(!following)}\n >\n {following ? 'Following' : 'Follow'}\n </Button>\n </div>\n </div>\n </Card>\n\n <div\n data-slot='profile-columns'\n style={{\n display: 'grid',\n gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',\n gap: 'var(--haze-space-5)',\n alignItems: 'start',\n }}\n >\n <div\n data-slot='profile-left'\n style={{\n display: 'flex',\n flexDirection: 'column',\n gap: 'var(--haze-space-5)',\n }}\n >\n <ProfileSection title='Details'>\n <Descriptions\n size='sm'\n columns={1}\n items={[\n { key: 'email', label: 'Email', children: 'mira@nimbus.dev' },\n { key: 'team', label: 'Team', children: 'Platform infrastructure' },\n { key: 'location', label: 'Location', children: 'Stockholm, SE' },\n { key: 'timezone', label: 'Timezone', children: 'CET (UTC+1)' },\n { key: 'joined', label: 'Joined', children: 'March 2021' },\n { key: 'manager', label: 'Reports to', children: 'Jonas Ekström' },\n ]}\n />\n </ProfileSection>\n\n <ProfileSection title='Skills'>\n <TagGroup>\n <TagGroupItem>TypeScript</TagGroupItem>\n <TagGroupItem>React</TagGroupItem>\n <TagGroupItem>Rust</TagGroupItem>\n <TagGroupItem>Kubernetes</TagGroupItem>\n <TagGroupItem>Observability</TagGroupItem>\n <TagGroupItem>Design systems</TagGroupItem>\n </TagGroup>\n </ProfileSection>\n\n <ProfileSection title='Also in Platform infrastructure'>\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: 'var(--haze-space-3)',\n }}\n >\n <AvatarGroup max={5} total={TEAMMATES.length}>\n {TEAMMATES.map((initials) => (\n <Avatar key={initials} fallback={initials} />\n ))}\n </AvatarGroup>\n <span\n style={{\n fontSize: 'var(--haze-text-xs)',\n color: 'var(--haze-color-text-muted)',\n }}\n >\n 6 engineers · 2 open seats\n </span>\n </div>\n </ProfileSection>\n </div>\n\n <ProfileSection title='Recent activity'>\n <Timeline>\n <TimelineItem\n color='success'\n title='Shipped token pipeline v3'\n description='Migrated the OKLCH palette build to the new generator.'\n time='Today, 09:41'\n />\n <TimelineItem\n color='primary'\n title='Reviewed #4821'\n description='Streaming table virtualization — approved with notes.'\n time='Yesterday, 17:02'\n />\n <TimelineItem\n color='warning'\n title='On-call handover'\n description='Flagged a rising p99 on the ingest path for the next rotation.'\n time='Mon, 08:15'\n />\n <TimelineItem\n title='Joined the a11y guild'\n description='Sponsoring the forced-colors audit across the library.'\n time='Last week'\n />\n <TimelineItem\n color='danger'\n title='Rolled back 2.14.1'\n description='A regression in the dark theme focus ring; patched within the hour.'\n time='Last week'\n />\n </Timeline>\n </ProfileSection>\n </div>\n </div>\n );\n}\n",
2605
+ "type": "registry:block"
2606
+ }
2607
+ ]
2608
+ },
2286
2609
  {
2287
2610
  "name": "progress",
2288
2611
  "type": "registry:ui",
@@ -2577,6 +2900,42 @@
2577
2900
  }
2578
2901
  ]
2579
2902
  },
2903
+ {
2904
+ "name": "settings-block",
2905
+ "type": "registry:block",
2906
+ "title": "Settings block (card Tabs over Switch/RadioGroup/Select/Combobox rows)",
2907
+ "description": "Workspace preferences page: a card-variant `Tabs` hosting Notifications / Appearance / Security sections, every row a live control — `Switch`, `RadioGroup`, `Select` and a multiple `Combobox`, all through the controllable-state pattern.",
2908
+ "author": "wmzy <https://github.com/wmzy>",
2909
+ "docs": "Workspace preferences page: a card-variant `Tabs` hosting Notifications / Appearance / Security sections, every row a live control — `Switch`, `RadioGroup`, `Select` and a multiple `Combobox`, all through the controllable-state pattern.\n\n## Install\n\n```bash\nnpx shadcn@latest add wmzy/haze-ui/settings-block\n```\n\nThe full-page source lands at `components/block/haze/settings-block.tsx` (via the `@block/` target placeholder from `components.json`) — vendored, so you own and edit it. Component imports are rewritten to the `haze-ui` npm package, which is installed automatically together with `react-use-control`.\n\nThe file imports the stylesheet of every component it renders (`haze-ui/css/tokens.css` + 10 component css files), so no extra stylesheet setup is needed.\n\n## Theming\n\nThis wrapper re-exports the 'lightTheme' (or 'darkTheme'), 'spacing' and\n'typography' classes from 'haze-ui' and imports 'haze-ui/css/tokens.css'.\n\nActivate the design tokens by applying the classes to a root element:\n\n```jsx\nimport { lightTheme, spacing, typography } from 'haze-ui';\n\n// on <body> or any root container — darkTheme is a drop-in swap\n<div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n```\n\nLive preview and source: https://wmzy.github.io/haze-ui/#/blocks — https://github.com/wmzy/haze-ui/tree/main/registry",
2910
+ "categories": [
2911
+ "blocks",
2912
+ "settings",
2913
+ "form"
2914
+ ],
2915
+ "dependencies": [
2916
+ "haze-ui",
2917
+ "react-use-control"
2918
+ ],
2919
+ "registryDependencies": [
2920
+ "wmzy/haze-ui/tabs",
2921
+ "wmzy/haze-ui/switch",
2922
+ "wmzy/haze-ui/radio",
2923
+ "wmzy/haze-ui/select",
2924
+ "wmzy/haze-ui/combobox",
2925
+ "wmzy/haze-ui/chip",
2926
+ "wmzy/haze-ui/spinner",
2927
+ "wmzy/haze-ui/virtual-list",
2928
+ "wmzy/haze-ui/button",
2929
+ "wmzy/haze-ui/card"
2930
+ ],
2931
+ "files": [
2932
+ {
2933
+ "path": "components/block/haze/settings-block.tsx",
2934
+ "content": "// haze-ui registry block — generated by scripts/generate-registry.mjs\n// from src/views/Blocks/blocks/SettingsBlock.tsx.\n//\n// Unlike the component items (thin re-export wrappers over the haze-ui npm\n// package), a block is VENDORED source: a full page you own and edit.\n// The component imports are rewritten from the docs site's '@/lib' alias\n// to the published 'haze-ui' package and every stylesheet of the page's\n// component closure is imported below, so the file is self-contained.\n// npm dependencies beyond haze-ui: react-use-control.\n//\n// Theming (once per app): apply the token classes to a root element, e.g.\n// import { lightTheme, spacing, typography } from 'haze-ui';\n// <div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n// (darkTheme is a drop-in swap).\n\nimport 'haze-ui/css/tokens.css';\nimport 'haze-ui/css/button.css';\nimport 'haze-ui/css/card.css';\nimport 'haze-ui/css/chip.css';\nimport 'haze-ui/css/combobox.css';\nimport 'haze-ui/css/radio.css';\nimport 'haze-ui/css/select.css';\nimport 'haze-ui/css/spinner.css';\nimport 'haze-ui/css/switch.css';\nimport 'haze-ui/css/tabs.css';\nimport 'haze-ui/css/virtual-list.css';\n\nimport type { ReactNode } from 'react';\n\nimport { useControl } from 'react-use-control';\n\nimport {\n Button,\n Card,\n Combobox,\n Option,\n Radio,\n RadioGroup,\n Select,\n Switch,\n Tabs,\n} from 'haze-ui';\n\n/*\n * Settings block: one data-driven <Tabs variant='card' items={…}> hosting\n * three preference sections (Notifications / Appearance / Security). Every\n * control is a live ControlOrValue — Switches, the theme RadioGroup, the\n * timezone Select and the alert-routes Combobox all run through\n * useControl so a host app can own any piece of this state.\n */\n\nfunction SettingRow({\n title,\n description,\n children,\n}: {\n title: string;\n description: string;\n children: ReactNode;\n}) {\n return (\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: 'var(--haze-space-4)',\n padding: 'var(--haze-space-3) 0',\n borderBottom: '1px solid var(--haze-color-border)',\n }}\n >\n <div style={{ flex: 1, minWidth: 0 }}>\n <div style={{ fontSize: 'var(--haze-text-sm)', color: 'var(--haze-color-text)' }}>\n {title}\n </div>\n <div\n style={{\n fontSize: 'var(--haze-text-xs)',\n color: 'var(--haze-color-text-muted)',\n }}\n >\n {description}\n </div>\n </div>\n {children}\n </div>\n );\n}\n\nfunction NotificationsPanel() {\n const [productNews, , productNewsCtrl] = useControl(undefined, true);\n const [, , weeklyDigestCtrl] = useControl(undefined, true);\n const [, , anomalyAlertsCtrl] = useControl(undefined, false);\n\n return (\n <div data-slot='settings-notifications'>\n <SettingRow title='Product updates' description='New features and changelog highlights.'>\n <Switch checked={productNewsCtrl} aria-label='Product updates' />\n </SettingRow>\n <SettingRow title='Weekly digest' description='A Monday summary of workspace activity.'>\n <Switch checked={weeklyDigestCtrl} aria-label='Weekly digest' />\n </SettingRow>\n <SettingRow title='Anomaly alerts' description='Email me when a metric breaks its band.'>\n <Switch checked={anomalyAlertsCtrl} aria-label='Anomaly alerts' />\n </SettingRow>\n <p\n style={{\n margin: 'var(--haze-space-3) 0 0',\n fontSize: 'var(--haze-text-xs)',\n color: 'var(--haze-color-text-muted)',\n }}\n >\n {productNews ? 'Product updates are on.' : 'Product updates are off.'}\n </p>\n </div>\n );\n}\n\nfunction AppearancePanel() {\n const [theme, , themeCtrl] = useControl(undefined, 'system');\n const [, , densityCtrl] = useControl(undefined, 'comfortable');\n\n return (\n <div data-slot='settings-appearance'>\n <SettingRow title='Theme' description='Follow the system, or pin light / dark.'>\n <RadioGroup value={themeCtrl} name='settings-theme'>\n <Radio value='system'>System</Radio>\n <Radio value='light'>Light</Radio>\n <Radio value='dark'>Dark</Radio>\n </RadioGroup>\n </SettingRow>\n <SettingRow title='Density' description='Comfortable spacing or the compact scale.'>\n <Select value={densityCtrl} size='sm' aria-label='Density'>\n <Option value='comfortable'>Comfortable</Option>\n <Option value='compact'>Compact</Option>\n </Select>\n </SettingRow>\n <p\n style={{\n margin: 'var(--haze-space-3) 0 0',\n fontSize: 'var(--haze-text-xs)',\n color: 'var(--haze-color-text-muted)',\n }}\n >\n Theme: {theme}\n </p>\n </div>\n );\n}\n\nfunction SecurityPanel() {\n const [, , twoFactorCtrl] = useControl(undefined, true);\n const [routes, , routesCtrl] = useControl(undefined, ['email'] as string[]);\n\n return (\n <div data-slot='settings-security'>\n <SettingRow title='Two-factor authentication' description='Require a TOTP code at sign-in.'>\n <Switch checked={twoFactorCtrl} aria-label='Two-factor authentication' />\n </SettingRow>\n <SettingRow title='Alert routes' description='Where security notices are delivered.'>\n <div style={{ minWidth: '240px' }}>\n <Combobox\n multiple\n value={routesCtrl}\n options={[\n { value: 'email', label: 'Email' },\n { value: 'sms', label: 'SMS' },\n { value: 'slack', label: 'Slack' },\n { value: 'pagerduty', label: 'PagerDuty' },\n ]}\n placeholder='Add a route…'\n />\n </div>\n </SettingRow>\n <p\n style={{\n margin: 'var(--haze-space-3) 0 0',\n fontSize: 'var(--haze-text-xs)',\n color: 'var(--haze-color-text-muted)',\n }}\n >\n Active routes: {routes.join(', ')}\n </p>\n </div>\n );\n}\n\nexport default function SettingsBlock() {\n const [, , activeCtrl] = useControl(undefined, 'notifications');\n\n return (\n <div\n data-slot='settings-block'\n style={{\n padding: 'var(--haze-space-5)',\n background: 'var(--haze-color-bg)',\n fontFamily: 'var(--haze-font-sans)',\n color: 'var(--haze-color-text)',\n }}\n >\n <h2 style={{ margin: '0 0 var(--haze-space-1)', fontSize: 'var(--haze-text-xl)' }}>\n Workspace settings\n </h2>\n <p\n style={{\n margin: '0 0 var(--haze-space-4)',\n fontSize: 'var(--haze-text-sm)',\n color: 'var(--haze-color-text-secondary)',\n }}\n >\n Preferences apply to everyone in acme-robotics.\n </p>\n <Card variant='outlined'>\n <Tabs\n value={activeCtrl}\n variant='card'\n items={[\n { value: 'notifications', label: 'Notifications', panel: <NotificationsPanel /> },\n { value: 'appearance', label: 'Appearance', panel: <AppearancePanel /> },\n { value: 'security', label: 'Security', panel: <SecurityPanel /> },\n ]}\n />\n <div\n style={{\n display: 'flex',\n justifyContent: 'flex-end',\n gap: 'var(--haze-space-2)',\n marginTop: 'var(--haze-space-4)',\n }}\n >\n <Button variant='ghost'>Discard</Button>\n <Button>Save changes</Button>\n </div>\n </Card>\n </div>\n );\n}\n",
2935
+ "type": "registry:block"
2936
+ }
2937
+ ]
2938
+ },
2580
2939
  {
2581
2940
  "name": "sidebar",
2582
2941
  "type": "registry:ui",
@@ -3039,6 +3398,27 @@
3039
3398
  }
3040
3399
  ]
3041
3400
  },
3401
+ {
3402
+ "name": "task-list",
3403
+ "type": "registry:ui",
3404
+ "title": "TaskList / TaskListItem",
3405
+ "description": "Agent task checklist (`TaskList` + `TaskListItem`): checkbox rows strike through and dim when done; compound children or `items` data-driven.",
3406
+ "author": "wmzy <https://github.com/wmzy>",
3407
+ "docs": "Agent task checklist (`TaskList` + `TaskListItem`): checkbox rows strike through and dim when done; compound children or `items` data-driven.\n\nExports: `TaskList`, `TaskListItem`.\n\n## Install\n\n```bash\nnpx shadcn@latest add wmzy/haze-ui/task-list\n```\n\nThe wrapper lands at `components/ui/haze/task-list.tsx` (via the `@ui/` target placeholder from `components.json`) and re-exports the component(s) from the `haze-ui` npm package, which is installed automatically.\n\n## Theming\n\nhaze-ui ships JS and CSS separately — the installed wrapper already imports\n`haze-ui/css/tokens.css` and `haze-ui/css/task-list.css`, so no extra stylesheet setup is needed.\n\nActivate the design tokens by applying the classes to a root element:\n\n```jsx\nimport { lightTheme, spacing, typography } from 'haze-ui';\n\n// on <body> or any root container — darkTheme is a drop-in swap\n<div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n```\n\nDetails: https://github.com/wmzy/haze-ui/tree/main/registry",
3408
+ "categories": [
3409
+ "agent"
3410
+ ],
3411
+ "dependencies": [
3412
+ "haze-ui"
3413
+ ],
3414
+ "files": [
3415
+ {
3416
+ "path": "components/haze/task-list.tsx",
3417
+ "content": "// haze-ui registry wrapper — generated by scripts/generate-registry.mjs.\n//\n// This is a thin wrapper around the published haze-ui npm package, not\n// vendored component source: it re-exports the component(s) from\n// 'haze-ui' and imports their stylesheet, so the package keeps updating\n// underneath. Edit this file to customize — wrap, restyle, compose; that\n// is what it is for. (haze-ui is compiled CSS-in-JS (linaria) styled\n// against --haze-* design tokens, so vendoring the source would neither\n// build nor style — forking this wrapper is the intended customization\n// path.)\n//\n// Theming (once per app): apply the token classes to a root element, e.g.\n// import { lightTheme, spacing, typography } from 'haze-ui';\n// <div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n// (darkTheme is a drop-in swap). haze-ui dist modules ship their own\n// 'use client' directive, so imports resolve cleanly from RSC; the\n// wrapper inherits client semantics either way.\n\nimport { TaskList, TaskListItem } from 'haze-ui';\n\nimport 'haze-ui/css/tokens.css';\nimport 'haze-ui/css/task-list.css';\nimport 'haze-ui/css/checkbox.css';\n\nexport { TaskList, TaskListItem };\nexport type { TaskListProps, TaskListItemProps, TaskData } from 'haze-ui';\n",
3418
+ "type": "registry:ui"
3419
+ }
3420
+ ]
3421
+ },
3042
3422
  {
3043
3423
  "name": "textarea",
3044
3424
  "type": "registry:ui",
@@ -3213,7 +3593,7 @@
3213
3593
  "title": "Theme tokens (lightTheme / darkTheme / autoTheme / spacing / typography)",
3214
3594
  "description": "Theme classes for haze-ui: `lightTheme`/`darkTheme`/`autoTheme` (zero-JS system-following) plus `spacing` and `typography`, applied to a root element to activate the `--haze-*` design tokens.",
3215
3595
  "author": "wmzy <https://github.com/wmzy>",
3216
- "docs": "Theme classes for haze-ui: `lightTheme`/`darkTheme`/`autoTheme` (zero-JS system-following) plus `spacing` and `typography`, applied to a root element to activate the `--haze-*` design tokens.\n\nExports: `lightTheme`, `darkTheme`, `autoTheme`, `violetTheme`, `tealTheme`, `cyanTheme`, `orangeTheme`, `roseTheme`, `spacing`, `compact`, `typography`, `motion`, `motionPresets`.\n\n## Install\n\n```bash\nnpx shadcn@latest add wmzy/haze-ui/tokens\n```\n\nThe wrapper lands at `lib/haze/tokens.tsx` (via the `@lib/` target placeholder from `components.json`); the `haze-ui` npm package is installed automatically as a dependency.\n\n## Theming\n\nThis wrapper re-exports the 'lightTheme' (or 'darkTheme'), 'spacing' and\n'typography' classes from 'haze-ui' and imports 'haze-ui/css/tokens.css'.\n\nActivate the design tokens by applying the classes to a root element:\n\n```jsx\nimport { lightTheme, spacing, typography } from 'haze-ui';\n\n// on <body> or any root container — darkTheme is a drop-in swap\n<div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n```\n\nDetails: https://github.com/wmzy/haze-ui/tree/main/registry",
3596
+ "docs": "Theme classes for haze-ui: `lightTheme`/`darkTheme`/`autoTheme` (zero-JS system-following) plus `spacing` and `typography`, applied to a root element to activate the `--haze-*` design tokens.\n\nExports: `lightTheme`, `darkTheme`, `autoTheme`, `violetTheme`, `tealTheme`, `cyanTheme`, `orangeTheme`, `roseTheme`, `spacing`, `compact`, `typography`, `motion`, `motionPresets`, `remScale`.\n\n## Install\n\n```bash\nnpx shadcn@latest add wmzy/haze-ui/tokens\n```\n\nThe wrapper lands at `lib/haze/tokens.tsx` (via the `@lib/` target placeholder from `components.json`); the `haze-ui` npm package is installed automatically as a dependency.\n\n## Theming\n\nThis wrapper re-exports the 'lightTheme' (or 'darkTheme'), 'spacing' and\n'typography' classes from 'haze-ui' and imports 'haze-ui/css/tokens.css'.\n\nActivate the design tokens by applying the classes to a root element:\n\n```jsx\nimport { lightTheme, spacing, typography } from 'haze-ui';\n\n// on <body> or any root container — darkTheme is a drop-in swap\n<div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n```\n\nDetails: https://github.com/wmzy/haze-ui/tree/main/registry",
3217
3597
  "categories": [
3218
3598
  "theme"
3219
3599
  ],
@@ -3223,7 +3603,7 @@
3223
3603
  "files": [
3224
3604
  {
3225
3605
  "path": "components/haze/tokens.tsx",
3226
- "content": "// haze-ui registry wrapper — generated by scripts/generate-registry.mjs.\n//\n// This is a thin wrapper around the published haze-ui npm package, not\n// vendored component source: it re-exports the component(s) from\n// 'haze-ui' and imports their stylesheet, so the package keeps updating\n// underneath. Edit this file to customize — wrap, restyle, compose; that\n// is what it is for. (haze-ui is compiled CSS-in-JS (linaria) styled\n// against --haze-* design tokens, so vendoring the source would neither\n// build nor style — forking this wrapper is the intended customization\n// path.)\n//\n// Theming (once per app): apply the token classes to a root element, e.g.\n// import { lightTheme, spacing, typography } from 'haze-ui';\n// <div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n// (darkTheme is a drop-in swap). haze-ui dist modules ship their own\n// 'use client' directive, so imports resolve cleanly from RSC; the\n// wrapper inherits client semantics either way.\n\nimport { lightTheme, darkTheme, autoTheme, violetTheme, tealTheme, cyanTheme, orangeTheme, roseTheme, spacing, compact, typography, motion, motionPresets } from 'haze-ui';\n\nimport 'haze-ui/css/tokens.css';\n\nexport { lightTheme, darkTheme, autoTheme, violetTheme, tealTheme, cyanTheme, orangeTheme, roseTheme, spacing, compact, typography, motion, motionPresets };\nexport type { BrandTheme, MotionPreset } from 'haze-ui';\n",
3606
+ "content": "// haze-ui registry wrapper — generated by scripts/generate-registry.mjs.\n//\n// This is a thin wrapper around the published haze-ui npm package, not\n// vendored component source: it re-exports the component(s) from\n// 'haze-ui' and imports their stylesheet, so the package keeps updating\n// underneath. Edit this file to customize — wrap, restyle, compose; that\n// is what it is for. (haze-ui is compiled CSS-in-JS (linaria) styled\n// against --haze-* design tokens, so vendoring the source would neither\n// build nor style — forking this wrapper is the intended customization\n// path.)\n//\n// Theming (once per app): apply the token classes to a root element, e.g.\n// import { lightTheme, spacing, typography } from 'haze-ui';\n// <div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n// (darkTheme is a drop-in swap). haze-ui dist modules ship their own\n// 'use client' directive, so imports resolve cleanly from RSC; the\n// wrapper inherits client semantics either way.\n\nimport { lightTheme, darkTheme, autoTheme, violetTheme, tealTheme, cyanTheme, orangeTheme, roseTheme, spacing, compact, typography, motion, motionPresets, remScale } from 'haze-ui';\n\nimport 'haze-ui/css/tokens.css';\n\nexport { lightTheme, darkTheme, autoTheme, violetTheme, tealTheme, cyanTheme, orangeTheme, roseTheme, spacing, compact, typography, motion, motionPresets, remScale };\nexport type { BrandTheme, MotionPreset } from 'haze-ui';\n",
3227
3607
  "type": "registry:lib"
3228
3608
  }
3229
3609
  ]
@@ -3357,10 +3737,10 @@
3357
3737
  {
3358
3738
  "name": "tree",
3359
3739
  "type": "registry:ui",
3360
- "title": "Tree / SortableTree",
3740
+ "title": "Tree / SortableTree / useTree",
3361
3741
  "description": "Hierarchical tree with expand, select, check state, async `loadData` and `searchValue` filtering.",
3362
3742
  "author": "wmzy <https://github.com/wmzy>",
3363
- "docs": "Hierarchical tree with expand, select, check state, async `loadData` and `searchValue` filtering.\n\nExports: `Tree`, `SortableTree`.\n\n## Install\n\n```bash\nnpx shadcn@latest add wmzy/haze-ui/tree\n```\n\nThe wrapper lands at `components/ui/haze/tree.tsx` (via the `@ui/` target placeholder from `components.json`) and re-exports the component(s) from the `haze-ui` npm package, which is installed automatically.\n\n## Theming\n\nhaze-ui ships JS and CSS separately — the installed wrapper already imports\n`haze-ui/css/tokens.css` and `haze-ui/css/tree.css`, so no extra stylesheet setup is needed.\n\nActivate the design tokens by applying the classes to a root element:\n\n```jsx\nimport { lightTheme, spacing, typography } from 'haze-ui';\n\n// on <body> or any root container — darkTheme is a drop-in swap\n<div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n```\n\nDetails: https://github.com/wmzy/haze-ui/tree/main/registry",
3743
+ "docs": "Hierarchical tree with expand, select, check state, async `loadData` and `searchValue` filtering.\n\nExports: `Tree`, `SortableTree`, `useTree`.\n\n## Install\n\n```bash\nnpx shadcn@latest add wmzy/haze-ui/tree\n```\n\nThe wrapper lands at `components/ui/haze/tree.tsx` (via the `@ui/` target placeholder from `components.json`) and re-exports the component(s) from the `haze-ui` npm package, which is installed automatically.\n\n## Theming\n\nhaze-ui ships JS and CSS separately — the installed wrapper already imports\n`haze-ui/css/tokens.css` and `haze-ui/css/tree.css`, so no extra stylesheet setup is needed.\n\nActivate the design tokens by applying the classes to a root element:\n\n```jsx\nimport { lightTheme, spacing, typography } from 'haze-ui';\n\n// on <body> or any root container — darkTheme is a drop-in swap\n<div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n```\n\nDetails: https://github.com/wmzy/haze-ui/tree/main/registry",
3364
3744
  "categories": [
3365
3745
  "data-display"
3366
3746
  ],
@@ -3370,7 +3750,7 @@
3370
3750
  "files": [
3371
3751
  {
3372
3752
  "path": "components/haze/tree.tsx",
3373
- "content": "// haze-ui registry wrapper — generated by scripts/generate-registry.mjs.\n//\n// This is a thin wrapper around the published haze-ui npm package, not\n// vendored component source: it re-exports the component(s) from\n// 'haze-ui' and imports their stylesheet, so the package keeps updating\n// underneath. Edit this file to customize — wrap, restyle, compose; that\n// is what it is for. (haze-ui is compiled CSS-in-JS (linaria) styled\n// against --haze-* design tokens, so vendoring the source would neither\n// build nor style — forking this wrapper is the intended customization\n// path.)\n//\n// Theming (once per app): apply the token classes to a root element, e.g.\n// import { lightTheme, spacing, typography } from 'haze-ui';\n// <div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n// (darkTheme is a drop-in swap). haze-ui dist modules ship their own\n// 'use client' directive, so imports resolve cleanly from RSC; the\n// wrapper inherits client semantics either way.\n\nimport { Tree, SortableTree } from 'haze-ui';\n\nimport 'haze-ui/css/tokens.css';\nimport 'haze-ui/css/tree.css';\nimport 'haze-ui/css/virtual-list.css';\n\nexport { Tree, SortableTree };\nexport type { TreeProps, TreeNodeData, SortableTreeProps, SortableTreeMoveInfo } from 'haze-ui';\n",
3753
+ "content": "// haze-ui registry wrapper — generated by scripts/generate-registry.mjs.\n//\n// This is a thin wrapper around the published haze-ui npm package, not\n// vendored component source: it re-exports the component(s) from\n// 'haze-ui' and imports their stylesheet, so the package keeps updating\n// underneath. Edit this file to customize — wrap, restyle, compose; that\n// is what it is for. (haze-ui is compiled CSS-in-JS (linaria) styled\n// against --haze-* design tokens, so vendoring the source would neither\n// build nor style — forking this wrapper is the intended customization\n// path.)\n//\n// Theming (once per app): apply the token classes to a root element, e.g.\n// import { lightTheme, spacing, typography } from 'haze-ui';\n// <div className={`${lightTheme} ${spacing} ${typography}`}>…</div>\n// (darkTheme is a drop-in swap). haze-ui dist modules ship their own\n// 'use client' directive, so imports resolve cleanly from RSC; the\n// wrapper inherits client semantics either way.\n\nimport { Tree, SortableTree, useTree } from 'haze-ui';\n\nimport 'haze-ui/css/tokens.css';\nimport 'haze-ui/css/tree.css';\nimport 'haze-ui/css/virtual-list.css';\n\nexport { Tree, SortableTree, useTree };\nexport type { TreeProps, TreeNodeData, TreeScrollHandle, SortableTreeProps, SortableTreeMoveInfo, UseTreeOptions, UseTreeResult, TreeScrollToOptions, VisibleTreeRow } from 'haze-ui';\n",
3374
3754
  "type": "registry:ui"
3375
3755
  }
3376
3756
  ]