makersclaw 0.3.2 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/package.json +1 -1
- package/r/annotate.json +3 -3
- package/r/approval-request.json +4 -4
- package/r/audio-player.json +2 -2
- package/r/auth-form.json +2 -2
- package/r/board.json +2 -2
- package/r/calendar-view.json +9 -9
- package/r/cart.json +2 -2
- package/r/chart.json +7 -7
- package/r/chat.json +3 -3
- package/r/code-editor.json +2 -2
- package/r/comment-box.json +5 -5
- package/r/contact-section.json +2 -2
- package/r/core.json +17 -11
- package/r/csv-import.json +3 -3
- package/r/data-table.json +4 -4
- package/r/diagram.json +2 -2
- package/r/doc-editor.json +2 -2
- package/r/doc-page.json +3 -3
- package/r/email-render.json +2 -2
- package/r/faq.json +2 -2
- package/r/file-upload.json +2 -2
- package/r/flow-canvas.json +4 -4
- package/r/footer.json +2 -2
- package/r/gantt.json +3 -3
- package/r/job-panel.json +8 -5
- package/r/json-editor.json +3 -3
- package/r/map.json +2 -2
- package/r/menubar.json +2 -2
- package/r/mind-map.json +6 -6
- package/r/navigation-menu.json +2 -2
- package/r/order-detail.json +2 -2
- package/r/org-chart.json +4 -4
- package/r/page-builder.json +2 -2
- package/r/payment-methods.json +2 -2
- package/r/pdf-document.json +2 -2
- package/r/pdf-viewer.json +3 -3
- package/r/product-detail.json +2 -2
- package/r/product-grid.json +3 -3
- package/r/record-form.json +2 -2
- package/r/record-notes.json +6 -6
- package/r/registry.json +131 -123
- package/r/search-dialog.json +2 -2
- package/r/settings-list.json +2 -2
- package/r/setup-checklist.json +2 -2
- package/r/share-dialog.json +2 -2
- package/r/sidebar.json +4 -4
- package/r/signature-pad.json +3 -3
- package/r/sketch-canvas.json +2 -2
- package/r/spreadsheet.json +3 -3
- package/r/stat-cards.json +3 -3
- package/r/task-list.json +3 -3
- package/r/task-model.json +2 -2
- package/r/testimonials.json +2 -2
- package/r/transaction-list.json +2 -2
- package/r/whiteboard.json +3 -3
- package/registry/blocks/approval-request/README.md +2 -1
- package/registry/blocks/approval-request/approval-request.jsx +5 -3
- package/registry/blocks/calendar-view/README.md +8 -0
- package/registry/blocks/calendar-view/agenda.jsx +5 -4
- package/registry/blocks/calendar-view/calendar.jsx +23 -8
- package/registry/blocks/calendar-view/demo.jsx +2 -0
- package/registry/blocks/calendar-view/month.jsx +6 -5
- package/registry/blocks/calendar-view/time-grid.jsx +9 -7
- package/registry/blocks/calendar-view/tray.jsx +5 -4
- package/registry/blocks/chart/README.md +9 -4
- package/registry/blocks/chart/cartesian.jsx +31 -24
- package/registry/blocks/chart/core.jsx +21 -0
- package/registry/blocks/chart/pie.jsx +15 -10
- package/registry/blocks/chart/sparkline.jsx +32 -14
- package/registry/blocks/chat/message.jsx +4 -4
- package/registry/blocks/comment-box/README.md +1 -1
- package/registry/blocks/comment-box/comment-time.jsx +4 -6
- package/registry/blocks/data-table/README.md +4 -0
- package/registry/blocks/data-table/data-table.jsx +20 -6
- package/registry/blocks/flow-canvas/edges.jsx +4 -2
- package/registry/blocks/flow-canvas/theme.js +59 -48
- package/registry/blocks/job-panel/README.md +40 -3
- package/registry/blocks/job-panel/block.json +2 -2
- package/registry/blocks/job-panel/demo.jsx +75 -6
- package/registry/blocks/job-panel/job-panel.jsx +60 -11
- package/registry/blocks/mind-map/README.md +9 -2
- package/registry/blocks/mind-map/mind-map.jsx +51 -12
- package/registry/blocks/mind-map/tree.js +33 -0
- package/registry/blocks/org-chart/org-chart.jsx +1 -1
- package/registry/blocks/pdf-viewer/notes.jsx +4 -3
- package/registry/blocks/record-notes/README.md +1 -1
- package/registry/blocks/record-notes/record-notes.jsx +4 -5
- package/registry/blocks/sidebar/sidebar-panel.jsx +4 -4
- package/registry/blocks/sidebar/sidebar.jsx +13 -13
- package/registry/blocks/signature-pad/signature.js +8 -1
- package/registry/blocks/spreadsheet/cells.jsx +2 -2
- package/src/shadcn.mjs +1 -1
- package/templates/next/files/README.md +410 -86
- package/templates/next/files/app/auto-refresh.jsx +42 -15
- package/templates/next/files/app/claw-alive/route.js +17 -0
- package/templates/next/files/app/error-relay.jsx +9 -4
- package/templates/next/files/app/escape-relay.jsx +27 -0
- package/templates/next/files/app/frame.js +409 -0
- package/templates/next/files/app/layout.jsx +18 -3
- package/templates/next/files/check/run.mjs +51 -18
- package/templates/next/files/components/parts/fixture.jsx +29 -4
- package/templates/next/files/components/parts/index.js +2 -1
- package/templates/next/files/components/parts/live-work.jsx +235 -0
- package/templates/next/files/components/ui/button.jsx +6 -1
- package/templates/next/files/components/ui/calendar.jsx +24 -0
- package/templates/next/files/components/ui/date-picker.jsx +15 -2
- package/templates/next/files/components/ui/field.jsx +12 -3
- package/templates/next/files/components/ui/icon.jsx +37 -2
- package/templates/next/files/components/ui/index.js +1 -0
- package/templates/next/files/components/ui/sheet.jsx +38 -2
- package/templates/next/files/components/ui/toggle.jsx +20 -2
- package/templates/next/files/lib/actions.js +63 -5
- package/templates/next/files/lib/claw.js +127 -28
- package/templates/next/files/lib/data.js +4 -1
- package/templates/next/files/lib/db.js +18 -1
- package/templates/next/files/lib/hooks.js +493 -37
- package/templates/next/files/lib/platform.js +113 -15
- package/templates/next/files/surfaces/README.md +3 -0
- package/templates/next/template.json +19 -8
package/r/core.json
CHANGED
|
@@ -3,19 +3,25 @@
|
|
|
3
3
|
"name": "core",
|
|
4
4
|
"type": "registry:ui",
|
|
5
5
|
"title": "Makersclaw kit",
|
|
6
|
-
"description": "The kit every Makersclaw block is built from — Button, Field, Select, Sheet, Tabs, Menu, Notice, Icon, Depiction — and the parts bound to data (RecordSwitcher, useFixture). Native elements under the project's own theme tokens.",
|
|
6
|
+
"description": "The kit every Makersclaw block is built from — Button, Field, Select, Sheet, Tabs, Menu, Notice, Icon, Depiction — and the parts bound to data (RecordSwitcher, useFixture, LiveEmpty). Native elements under the project's own theme tokens.",
|
|
7
7
|
"files": [
|
|
8
8
|
{
|
|
9
9
|
"path": "templates/next/files/components/parts/fixture.jsx",
|
|
10
10
|
"type": "registry:ui",
|
|
11
11
|
"target": "@components/makersclaw/parts/fixture.jsx",
|
|
12
|
-
"content": "\"use client\";\n\nimport { useMemo, useState } from \"react\";\nimport { markSample } from \"./sample\";\nimport { Button, Notice } from \"../ui\";\n\n// The worked example, never persisted. A surface opens populated (README\n// §3: the template test) — but populated is not persisted. The example is\n// rows in the app's code, shown while the real collection is empty and\n// never INSERTed: the workspace database is shared by every app, so an\n// invented row there is a real row somewhere else. Clearing writes\n// nothing; the first real write is the user's.\n//\n// const { rows, isFixture, clear } = useFixture(realRows, SAMPLE);\n// {isFixture ? <FixtureNotice onClear={clear} /> : null}\n//\n// A fixture row carries `_sample: true` and a `sample-N` id. Editing one\n// promotes it: the surface copies its values into a new draft and saves\n// THAT — it never updateRow()s a sample id (the row does not exist).\n//\n// `rows` keeps its identity between renders (an effect may depend on it);\n// declare SAMPLE at module level, not inside the component, so it does too.\n\nconst NONE = [];\n\nexport function useFixture(rows, sample, { idColumn = \"id\" } = {}) {\n const [cleared, setCleared] = useState(false);\n const isFixture = !cleared && (!rows || rows.length === 0) && Array.isArray(sample) && sample.length > 0;\n const fixtureRows = useMemo(() => markSample(sample, idColumn), [sample, idColumn]);\n return {\n rows: isFixture ? fixtureRows : rows ?? NONE,\n isFixture,\n clear: () => setCleared(true),\n restore: () => setCleared(false),\n };\n}\n\nexport function isSample(row) {\n return Boolean(row && row._sample);\n}\n\n/** The line above a fixture, with the one control it earns. */\nexport function FixtureNotice({ onClear, children, className }) {\n return (\n <Notice\n tone=\"info\"\n title=\"Example only\"\n className={className}\n action={\n onClear ? (\n <Button variant=\"quiet\" size=\"sm\" onClick={onClear}>\n Clear the example\n </Button>\n ) : null\n }\n >\n {children ?? \"Edit it or start your own. Nothing here is saved until you save it.\"}\n </Notice>\n );\n}\n"
|
|
12
|
+
"content": "\"use client\";\n\nimport { createContext, useContext, useMemo, useState } from \"react\";\nimport { markSample } from \"./sample\";\nimport { Button, Notice } from \"../ui\";\n\n// The worked example, never persisted. A surface opens populated (README\n// §3: the template test) — but populated is not persisted. The example is\n// rows in the app's code, shown while the real collection is empty and\n// never INSERTed: the workspace database is shared by every app, so an\n// invented row there is a real row somewhere else. Clearing writes\n// nothing; the first real write is the user's.\n//\n// const { rows, isFixture, clear } = useFixture(realRows, SAMPLE);\n// {isFixture ? <FixtureNotice onClear={clear} /> : null}\n//\n// A fixture row carries `_sample: true` and a `sample-N` id. Editing one\n// promotes it: the surface copies its values into a new draft and saves\n// THAT — it never updateRow()s a sample id (the row does not exist).\n//\n// `rows` keeps its identity between renders (an effect may depend on it);\n// declare SAMPLE at module level, not inside the component, so it does too.\n//\n// Examples show only OUTSIDE a Makersclaw workspace: the store's authoring\n// loop, a shadcn project. Inside one, an app never shows example data (up\n// to 0.3.x it did, until its table had a row): an empty table stays empty\n// and the screen shows what its agent is doing instead. app/layout.jsx\n// decides it per request and says so through <Examples allowed>; with no\n// provider at all — a shadcn project — the answer is yes. The provider\n// lives here, not in lib/, so the parts still travel as one folder.\n\nconst NONE = [];\nconst nothing = () => {};\n\nconst ExamplesContext = createContext(true);\n\n/** Whether the parts below may show an example. The layout wraps the app\n * in it; `allowed` is false inside a workspace. */\nexport function Examples({ allowed, children }) {\n return <ExamplesContext.Provider value={Boolean(allowed)}>{children}</ExamplesContext.Provider>;\n}\n\n/** true unless an <Examples allowed={false}> is above. */\nexport function useExamples() {\n return useContext(ExamplesContext);\n}\n\nexport function useFixture(rows, sample, { idColumn = \"id\" } = {}) {\n const allowed = useExamples();\n const [cleared, setCleared] = useState(false);\n const isFixture = allowed && !cleared && (!rows || rows.length === 0) && Array.isArray(sample) && sample.length > 0;\n const fixtureRows = useMemo(() => markSample(sample, idColumn), [sample, idColumn]);\n // With examples off there is no example to clear or bring back: the\n // real rows, however few, are all a surface gets.\n return {\n rows: isFixture ? fixtureRows : rows ?? NONE,\n isFixture,\n clear: allowed ? () => setCleared(true) : nothing,\n restore: allowed ? () => setCleared(false) : nothing,\n };\n}\n\nexport function isSample(row) {\n return Boolean(row && row._sample);\n}\n\n/** The line above a fixture, with the one control it earns. */\nexport function FixtureNotice({ onClear, children, className }) {\n return (\n <Notice\n tone=\"info\"\n title=\"Example only\"\n className={className}\n action={\n onClear ? (\n <Button variant=\"quiet\" size=\"sm\" onClick={onClear}>\n Clear the example\n </Button>\n ) : null\n }\n >\n {children ?? \"Edit it or start your own. Nothing here is saved until you save it.\"}\n </Notice>\n );\n}\n"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
"path": "templates/next/files/components/parts/index.js",
|
|
16
16
|
"type": "registry:ui",
|
|
17
17
|
"target": "@components/makersclaw/parts/index.js",
|
|
18
|
-
"content": "// Parts: the kit's pieces bound to this template's data layer. Each\n// exists because an app got it wrong once; each carries the behaviour\n// that makes it right. README §3b.\nexport { useFixture, isSample, FixtureNotice } from \"./fixture\";\nexport { RecordSwitcher } from \"./record-switcher\";\n"
|
|
18
|
+
"content": "// Parts: the kit's pieces bound to this template's data layer. Each\n// exists because an app got it wrong once; each carries the behaviour\n// that makes it right. README §3b.\nexport { Examples, useExamples, useFixture, isSample, FixtureNotice } from \"./fixture\";\nexport { LiveWork, LiveEmpty, liveWords } from \"./live-work\";\nexport { RecordSwitcher } from \"./record-switcher\";\n"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"path": "templates/next/files/components/parts/live-work.jsx",
|
|
22
|
+
"type": "registry:ui",
|
|
23
|
+
"target": "@components/makersclaw/parts/live-work.jsx",
|
|
24
|
+
"content": "\"use client\";\n\nimport { isValidElement, useState } from \"react\";\nimport { Button, Icon, StatusDot, cx } from \"../ui\";\n\n// The empty state that shows the work coming, never an example. Inside a\n// Makersclaw workspace a table opens empty (fixture.jsx: examples show\n// only outside one), and a table is usually empty because its agent has\n// not got to it yet — or is on it right now. So the empty block says\n// which: the ask in flight on this surface (its line, whose ask it is,\n// what it waits behind, the one press it needs), or, with nothing in\n// flight, what would appear here and the one way to make it. Up to 0.3.x\n// an example filled that gap; since 0.4.0 no app shows example data in a\n// workspace (claw's docs/JARVIS-PLATFORM.md §9.6 and §12, promise 6).\n//\n// const work = useRequests({ surface: \"deals\" }); // lib/hooks.js\n// if (!rows.length) {\n// return <LiveEmpty work={work} surface=\"deals\" icon=\"inbox\" title=\"No deals yet\"\n// line=\"Ask the agent to find some, or add one.\" action={{ label: \"Add a deal\", onClick: add }} />;\n// }\n//\n// Props in, callbacks out: `work` is useRequests()'s answer as it stands,\n// and the row calls back through it — work.open(request) opens the\n// conversation in the desk's agent column (where a need is answered),\n// work.stop(id) stops an ask. The part imports nothing from lib/, so the\n// parts still travel as one folder; in a shadcn project, with no `work`,\n// it is the plain empty block.\n\n// A request's status is one of eleven (claw's shared/src/requests.ts;\n// lib/hooks.js keeps the same list — this folder may not import it).\n// Every step before the agent is on it reads as Working: the member asked\n// for the result, not for its plumbing.\nconst WORDS = new Map([\n [\"routing\", \"Working\"],\n [\"queued\", \"Working\"],\n [\"installing\", \"Working\"],\n [\"setting_up\", \"Working\"],\n [\"building\", \"Working\"],\n [\"working\", \"Working\"],\n [\"needs_you\", \"Needs you\"],\n [\"answered\", \"Answered\"],\n [\"done\", \"Done\"],\n [\"stopped\", \"Stopped\"],\n [\"didnt_finish\", \"Didn't finish\"],\n]);\n\n/** The seven statuses in which the work still goes. `answered` is open\n * but not live: the agent answered and waits, and no row is coming. */\nconst LIVE = new Set([\"routing\", \"queued\", \"installing\", \"setting_up\", \"building\", \"working\", \"needs_you\"]);\n\n/** A scheduled run's statuses while it goes (a brief's, not a request's:\n * _claw/requests returns the run only in one of these). */\nconst RUN_LIVE = new Set([\"queued\", \"planning\", \"awaiting_approval\", \"in_progress\"]);\n\n/** The status word for a request or a bare status: \"Working\", \"Needs\n * you\", \"Answered\", \"Done\", \"Stopped\", \"Didn't finish\". An unknown\n * status is \"\" — a word the platform has not taught this part yet is\n * better unsaid than guessed. */\nexport function liveWords(requestOrStatus) {\n const status = typeof requestOrStatus === \"string\" ? requestOrStatus : requestOrStatus?.status;\n return WORDS.get(status) ?? \"\";\n}\n\nfunction runGoing(run) {\n return Boolean(run && typeof run === \"object\" && (run.status == null || RUN_LIVE.has(run.status)));\n}\n\n/**\n * One piece of work in flight, as a row: a pulsing dot, the request's\n * line, whose request it is when it is not yours, what it waits behind.\n * A need gets the warning tone and Answer (it is answered in the desk's\n * column, where the conversation lives); your own ask gets Stop. With no\n * request, a scheduled run that is going shows its title.\n *\n * request one of useRequests().live (lib/hooks.js)\n * run useRequests().run, shown when there is no request\n * onOpen (request) → open its conversation; Answer needs it\n * onStop (id) → stop it; Stop needs it, and shows only to the asker\n */\nexport function LiveWork({ request, run, onOpen, onStop, className }) {\n const [stopping, setStopping] = useState(false);\n\n if (request && typeof request === \"object\") {\n const status = request.status;\n const word = liveWords(request);\n const needs = status === \"needs_you\";\n const queued = status === \"queued\";\n const theirs = request.mine === false;\n const main = request.line || word || request.title || \"\";\n const meta = [];\n if (theirs) meta.push(`from ${request.asker_name || \"a teammate\"}'s request`);\n // claw's line for a queued request already says \"Queued · after …\"\n // (lineOf, shared/src/requests.ts); say it once, under a line that\n // does not.\n if (queued && !/^Queued\\b/.test(main)) meta.push(`Queued · after ${request.queued?.after_title || \"the job before it\"}`);\n // Only the asker answers: a teammate's need opens on \"Waiting on …\"\n // in the desk, so it is shown, not offered.\n const answer = needs && !theirs && onOpen;\n const going = LIVE.has(status);\n const stop = !needs && going && request.mine === true && onStop;\n const onStopClick = async () => {\n setStopping(true);\n try {\n await onStop(request.id);\n } finally {\n setStopping(false);\n }\n };\n return (\n <Row\n tone={needs ? \"warning\" : going && !queued ? \"primary\" : \"neutral\"}\n pulse={going && !queued}\n word={word}\n warn={needs}\n main={main}\n meta={meta}\n className={className}\n >\n {answer ? (\n <Button size=\"sm\" variant=\"primary\" onClick={() => onOpen(request)}>\n Answer\n </Button>\n ) : stop ? (\n <Button size=\"sm\" variant=\"quiet\" loading={stopping} onClick={onStopClick}>\n Stop\n </Button>\n ) : null}\n </Row>\n );\n }\n\n if (runGoing(run)) {\n return (\n <Row tone=\"primary\" pulse word=\"Working\" main={run.title || \"Working\"} meta={run.title ? [\"Working\"] : []} className={className} />\n );\n }\n return null;\n}\n\nfunction Row({ tone, pulse, word, warn, main, meta, className, children }) {\n return (\n <div\n className={cx(\n \"flex items-center gap-3 rounded-lg border bg-card px-3 py-2.5 text-left text-sm text-card-foreground\",\n warn ? \"border-warning/50\" : \"border-border\",\n className,\n )}\n >\n <StatusDot tone={tone} pulse={pulse} label={word || \"Working\"} hideLabel />\n <div aria-live=\"polite\" className=\"min-w-0 flex-1\">\n <p className=\"truncate font-medium\">{main}</p>\n {meta.length ? <p className=\"truncate text-xs text-muted-foreground\">{meta.join(\" · \")}</p> : null}\n </div>\n {children ? <div className=\"flex shrink-0 items-center gap-2\">{children}</div> : null}\n </div>\n );\n}\n\n/** Does a request belong to this empty state? Each filter given is a way\n * in (lib/hooks.js reads it the same way): an ask made on this surface,\n * or work on this job wherever it was asked for. No filter: any. */\nfunction belongs(request, surface, job) {\n if (!surface && !job) return true;\n return Boolean((surface && request.surface === surface) || (job && request.job_key === job));\n}\n\n// The one to show when several are in flight: what needs an answer, then\n// what is working, then what waits in the queue; yours before a\n// teammate's; the newest first.\nconst rank = (r) => (r.status === \"needs_you\" ? 0 : r.status === \"queued\" ? 2 : 1);\n\nfunction pickRequest(live, surface, job) {\n const list = (Array.isArray(live) ? live : [])\n .map((r, i) => ({ r, i }))\n .filter(({ r }) => r && typeof r === \"object\" && LIVE.has(r.status) && belongs(r, surface, job));\n list.sort(\n (a, b) =>\n rank(a.r) - rank(b.r) ||\n Number(b.r.mine === true) - Number(a.r.mine === true) ||\n String(b.r.created_at ?? \"\").localeCompare(String(a.r.created_at ?? \"\")) ||\n a.i - b.i,\n );\n return list[0]?.r ?? null;\n}\n\nconst FRAME = \"grid place-items-center rounded-lg border border-dashed border-border px-6 py-10 text-center\";\n\n/**\n * What an empty collection shows in a workspace. With work in flight for\n * this surface or job — or the app's scheduled run going — the work, as\n * a LiveWork row, and \"Rows appear here as they land\". With none, the\n * empty block: icon, title, line and the one action that fills it.\n *\n * work useRequests()'s answer ({ live, run, open, stop }); absent\n * in a shadcn project, where only the empty block shows\n * surface the surface key its asks carry; job — the job key whose\n * work lands here. Either is a way in; neither: any live work\n * icon the empty block's glyph (inbox)\n * title what would be here: \"No deals yet\"\n * line how it gets here: \"Ask the agent to find some, or add one.\"\n * action { label, onClick, href, icon, variant } for a Button, or an\n * element as it is\n * children anything the surface adds under either state\n */\nexport function LiveEmpty({ work, surface, job, icon = \"inbox\", title, line, action, children, className }) {\n const request = pickRequest(work?.live, surface, job);\n const run = !request && runGoing(work?.run) ? work.run : null;\n\n if (request || run) {\n return (\n <div className={cx(FRAME, \"gap-3\", className)}>\n <LiveWork request={request} run={run} onOpen={work.open} onStop={work.stop} className=\"w-full max-w-md\" />\n <p className=\"text-sm text-muted-foreground\">Rows appear here as they land</p>\n {children}\n </div>\n );\n }\n\n const button = isValidElement(action) ? (\n action\n ) : action && typeof action === \"object\" && action.label ? (\n <Button variant={action.variant ?? \"secondary\"} icon={action.icon} href={action.href} onClick={action.onClick}>\n {action.label}\n </Button>\n ) : null;\n return (\n <div className={cx(FRAME, \"gap-2\", className)}>\n <Icon name={icon} size={22} className=\"text-muted-foreground\" />\n {title ? <p className=\"text-sm font-medium\">{title}</p> : null}\n {line ? <p className=\"max-w-prose text-sm text-muted-foreground\">{line}</p> : null}\n {button ? <div className=\"mt-2\">{button}</div> : null}\n {children}\n </div>\n );\n}\n"
|
|
19
25
|
},
|
|
20
26
|
{
|
|
21
27
|
"path": "templates/next/files/components/parts/record-switcher.jsx",
|
|
@@ -51,13 +57,13 @@
|
|
|
51
57
|
"path": "templates/next/files/components/ui/button.jsx",
|
|
52
58
|
"type": "registry:ui",
|
|
53
59
|
"target": "@components/makersclaw/ui/button.jsx",
|
|
54
|
-
"content": "import Link from \"next/link\";\nimport { cx } from \"./cx\";\nimport { Icon } from \"./icon\";\nimport { Spinner } from \"./feedback\";\n\n// Button — every action in the app. One shape, 44px targets\n// (mobile first: a thumb hits it). `href` makes it a next/link (base\n// path applied); `icon` names one from ./icon. Never style a <button>\n// by hand in a surface: the look is the workspace's, the behaviour is\n// the browser's, and this is the only place both meet.\n//\n// variant primary (the one main action per surface) · secondary ·\n// quiet (no fill) · danger (destructive, red text) ·\n// link (text only: inline, no box, padding or min-height — it\n// wraps and truncates with the sentence it sits in) · nav (a navigation row: quiet, full\n// width, start-aligned, normal weight; aria-current=\"page\"\n// fills it and sets it medium)\n// size md (44px) · sm (36px, for dense rows and rails); a link has\n// no height, so for it md and sm are text-sm and `inherit`\n// takes the surrounding text's size and weight\n// align center (the default) · start (label at the leading edge, for\n// a full-width row of your own; nav is always start)\n// loading true while its action runs: a spinner over the label (the\n// width does not jump), aria-busy, and disabled so a second\n// click cannot fire it again\n// tone a link's colour: primary (the default, text-primary) · muted\n// (text-muted-foreground, foreground on hover — a secondary\n// link beside a heading, \"What's new\", \"View all\"); only a\n// link reads it\n// iconOnly a square at the size (IconButton sets it; a Button whose\n// only child is an <Icon> should too)\n//\n// cx does not merge classes: a className can add, never override, what a\n// variant or size already sets. Anything a surface needs to vary is a\n// prop here, not a className fight.\n\nconst VARIANTS = {\n primary: \"bg-primary text-primary-foreground hover:opacity-90 border-transparent\",\n secondary: \"bg-secondary text-secondary-foreground border-border hover:bg-accent\",\n quiet: \"bg-transparent text-foreground border-transparent hover:bg-accent\",\n danger: \"bg-transparent text-destructive border-border hover:bg-accent\",\n link: \"underline-offset-4 hover:underline\",\n nav:\n \"w-full min-w-0 bg-transparent text-foreground border-transparent hover:bg-accent \" +\n \"[&[aria-current]:not([aria-current=false])]:bg-accent [&[aria-current]:not([aria-current=false])]:text-accent-foreground\",\n};\n\n// A link's colour, apart from its variant so a secondary link is a prop\n// and not a text-muted-foreground laid over text-primary.\nconst LINK_TONES = {\n primary: \"text-primary\",\n muted: \"text-muted-foreground hover:text-foreground\",\n};\n\n// The shape each variant is drawn in. A nav row spaces its icon wider.\n// A link is text in a line: inline, not a flex box — no border, padding\n// or height, it wraps where the sentence wraps and truncates with it. Its\n// icons sit on the text's line: `icon` leads, an <Icon> child trails.\nconst SHAPE = {\n box: \"inline-flex items-center gap-2 rounded-md border whitespace-nowrap\",\n nav: \"inline-flex items-center gap-3 rounded-md border whitespace-nowrap\",\n link: \"inline rounded-sm [&>svg]:ml-1 [&>svg]:inline-block [&>svg]:align-[-0.125em]\",\n};\n\n// Weight lives apart from the shape so a nav row can be normal weight\n// (its current row medium) without laying one font weight over another.\nconst WEIGHT = {\n box: \"font-medium\",\n nav: \"font-normal [&[aria-current]:not([aria-current=false])]:font-medium\",\n};\n\nconst SIZES = {\n md: \"min-h-11 px-4 text-sm\",\n sm: \"min-h-9 px-3 text-sm\",\n};\n\n// A link's sizes are only its text: md and sm as every other button,\n// inherit for a link inside a sentence.\nconst LINK_SIZES = {\n md: \"text-sm font-medium\",\n sm: \"text-sm font-medium\",\n inherit: \"\",\n};\n\n// IconButton's squares. Their own entries, never a px-0 laid over a\n// size's padding: cx does not merge classes, so which of two paddings\n// wins is CSS order, not the order they were written in.\nconst ICON_SIZES = {\n md: \"size-11 px-0 text-sm\",\n sm: \"size-9 px-0 text-sm\",\n};\n\nconst ALIGN = {\n center: \"justify-center\",\n start: \"justify-start text-left\",\n};\n\nexport function Button({\n variant = \"secondary\",\n size = \"md\",\n icon,\n href,\n className,\n children,\n type = \"button\",\n loading = false,\n disabled,\n iconOnly = false,\n align = \"center\",\n tone = \"primary\",\n ...rest\n}) {\n const look = VARIANTS[variant] ? variant : \"secondary\";\n const link = look === \"link\";\n const nav = look === \"nav\";\n const sizeClass = link\n ? LINK_SIZES[size] ?? LINK_SIZES.md\n : iconOnly\n ? ICON_SIZES[size] ?? ICON_SIZES.md\n : SIZES[size] ?? SIZES.md;\n const classes = cx(\n SHAPE[link ? \"link\" : nav ? \"nav\" : \"box\"],\n !link && (nav ? WEIGHT.nav : WEIGHT.box),\n !link && !iconOnly && (ALIGN[nav ? \"start\" : align] ?? ALIGN.center),\n iconOnly && ALIGN.center,\n \"outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n VARIANTS[look],\n link && (LINK_TONES[tone] ?? LINK_TONES.primary),\n sizeClass,\n // Busy is not unavailable: the button keeps its full colour.\n loading && \"relative disabled:aria-busy:opacity-100 disabled:aria-busy:cursor-progress\",\n loading && href && \"pointer-events-none opacity-50\",\n className,\n );\n const lead = icon ? (\n link ? (\n <span className=\"mr-1 inline-block align-[-0.125em]\">\n <Icon name={icon} />\n </span>\n ) : (\n <Icon name={icon} />\n )\n ) : null;\n const content = (\n <>\n {lead}\n {children}\n </>\n );\n // Loading: the label stays in place (transparent, still the accessible\n // name) so the button keeps its width; the spinner sits on top of it.\n const body = loading ? (\n <>\n <span className=\"inline-flex items-center gap-2 opacity-0\">{content}</span>\n <span className=\"absolute inset-0 grid place-items-center\">\n <Spinner size=\"sm\" label={null} />\n </span>\n </>\n ) : (\n content\n );\n if (href) {\n return (\n <Link\n href={href}\n className={classes}\n aria-busy={loading || undefined}\n aria-disabled={loading || undefined}\n tabIndex={loading ? -1 : undefined}\n disabled={disabled}\n {...rest}\n >\n {body}\n </Link>\n );\n }\n return (\n <button type={type} className={classes} disabled={loading || disabled} aria-busy={loading || undefined} {...rest}>\n {body}\n </button>\n );\n}\n\n/** A square button holding only an icon; `label` is required and is what\n * a screen reader says. */\nexport function IconButton({ icon, label, size = \"md\", variant = \"quiet\", className, ...rest }) {\n return (\n <Button\n variant={variant}\n size={size}\n aria-label={label}\n title={label}\n iconOnly\n className={cx(\"shrink-0\", className)}\n {...rest}\n >\n <Icon name={icon} size={size === \"sm\" ? 16 : 18} />\n </Button>\n );\n}\n"
|
|
60
|
+
"content": "import Link from \"next/link\";\nimport { cx } from \"./cx\";\nimport { Icon } from \"./icon\";\nimport { Spinner } from \"./feedback\";\n\n// Button — every action in the app. One shape, 44px targets\n// (mobile first: a thumb hits it). `href` makes it a next/link (base\n// path applied); `icon` names one from ./icon. Never style a <button>\n// by hand in a surface: the look is the workspace's, the behaviour is\n// the browser's, and this is the only place both meet.\n//\n// variant primary (the one main action per surface, filled with\n// --primary, the app's accent) · secondary (the default, the\n// workspace's neutral --secondary: a Button that does not say\n// primary is never the accent — a surface's \"Add guest\"\n// written without variant=\"primary\" came out dark grey on a\n// coral app, 2026-09-25) ·\n// quiet (no fill) · danger (destructive, red text) ·\n// link (text only: inline, no box, padding or min-height — it\n// wraps and truncates with the sentence it sits in) · nav (a navigation row: quiet, full\n// width, start-aligned, normal weight; aria-current=\"page\"\n// fills it and sets it medium)\n// size md (44px) · sm (36px, for dense rows and rails); a link has\n// no height, so for it md and sm are text-sm and `inherit`\n// takes the surrounding text's size and weight\n// align center (the default) · start (label at the leading edge, for\n// a full-width row of your own; nav is always start)\n// loading true while its action runs: a spinner over the label (the\n// width does not jump), aria-busy, and disabled so a second\n// click cannot fire it again\n// tone a link's colour: primary (the default, text-primary) · muted\n// (text-muted-foreground, foreground on hover — a secondary\n// link beside a heading, \"What's new\", \"View all\"); only a\n// link reads it\n// iconOnly a square at the size (IconButton sets it; a Button whose\n// only child is an <Icon> should too)\n//\n// cx does not merge classes: a className can add, never override, what a\n// variant or size already sets. Anything a surface needs to vary is a\n// prop here, not a className fight.\n\nconst VARIANTS = {\n primary: \"bg-primary text-primary-foreground hover:opacity-90 border-transparent\",\n secondary: \"bg-secondary text-secondary-foreground border-border hover:bg-accent\",\n quiet: \"bg-transparent text-foreground border-transparent hover:bg-accent\",\n danger: \"bg-transparent text-destructive border-border hover:bg-accent\",\n link: \"underline-offset-4 hover:underline\",\n nav:\n \"w-full min-w-0 bg-transparent text-foreground border-transparent hover:bg-accent \" +\n \"[&[aria-current]:not([aria-current=false])]:bg-accent [&[aria-current]:not([aria-current=false])]:text-accent-foreground\",\n};\n\n// A link's colour, apart from its variant so a secondary link is a prop\n// and not a text-muted-foreground laid over text-primary.\nconst LINK_TONES = {\n primary: \"text-primary\",\n muted: \"text-muted-foreground hover:text-foreground\",\n};\n\n// The shape each variant is drawn in. A nav row spaces its icon wider.\n// A link is text in a line: inline, not a flex box — no border, padding\n// or height, it wraps where the sentence wraps and truncates with it. Its\n// icons sit on the text's line: `icon` leads, an <Icon> child trails.\nconst SHAPE = {\n box: \"inline-flex items-center gap-2 rounded-md border whitespace-nowrap\",\n nav: \"inline-flex items-center gap-3 rounded-md border whitespace-nowrap\",\n link: \"inline rounded-sm [&>svg]:ml-1 [&>svg]:inline-block [&>svg]:align-[-0.125em]\",\n};\n\n// Weight lives apart from the shape so a nav row can be normal weight\n// (its current row medium) without laying one font weight over another.\nconst WEIGHT = {\n box: \"font-medium\",\n nav: \"font-normal [&[aria-current]:not([aria-current=false])]:font-medium\",\n};\n\nconst SIZES = {\n md: \"min-h-11 px-4 text-sm\",\n sm: \"min-h-9 px-3 text-sm\",\n};\n\n// A link's sizes are only its text: md and sm as every other button,\n// inherit for a link inside a sentence.\nconst LINK_SIZES = {\n md: \"text-sm font-medium\",\n sm: \"text-sm font-medium\",\n inherit: \"\",\n};\n\n// IconButton's squares. Their own entries, never a px-0 laid over a\n// size's padding: cx does not merge classes, so which of two paddings\n// wins is CSS order, not the order they were written in.\nconst ICON_SIZES = {\n md: \"size-11 px-0 text-sm\",\n sm: \"size-9 px-0 text-sm\",\n};\n\nconst ALIGN = {\n center: \"justify-center\",\n start: \"justify-start text-left\",\n};\n\nexport function Button({\n variant = \"secondary\",\n size = \"md\",\n icon,\n href,\n className,\n children,\n type = \"button\",\n loading = false,\n disabled,\n iconOnly = false,\n align = \"center\",\n tone = \"primary\",\n ...rest\n}) {\n const look = VARIANTS[variant] ? variant : \"secondary\";\n const link = look === \"link\";\n const nav = look === \"nav\";\n const sizeClass = link\n ? LINK_SIZES[size] ?? LINK_SIZES.md\n : iconOnly\n ? ICON_SIZES[size] ?? ICON_SIZES.md\n : SIZES[size] ?? SIZES.md;\n const classes = cx(\n SHAPE[link ? \"link\" : nav ? \"nav\" : \"box\"],\n !link && (nav ? WEIGHT.nav : WEIGHT.box),\n !link && !iconOnly && (ALIGN[nav ? \"start\" : align] ?? ALIGN.center),\n iconOnly && ALIGN.center,\n \"outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n VARIANTS[look],\n link && (LINK_TONES[tone] ?? LINK_TONES.primary),\n sizeClass,\n // Busy is not unavailable: the button keeps its full colour.\n loading && \"relative disabled:aria-busy:opacity-100 disabled:aria-busy:cursor-progress\",\n loading && href && \"pointer-events-none opacity-50\",\n className,\n );\n const lead = icon ? (\n link ? (\n <span className=\"mr-1 inline-block align-[-0.125em]\">\n <Icon name={icon} />\n </span>\n ) : (\n <Icon name={icon} />\n )\n ) : null;\n const content = (\n <>\n {lead}\n {children}\n </>\n );\n // Loading: the label stays in place (transparent, still the accessible\n // name) so the button keeps its width; the spinner sits on top of it.\n const body = loading ? (\n <>\n <span className=\"inline-flex items-center gap-2 opacity-0\">{content}</span>\n <span className=\"absolute inset-0 grid place-items-center\">\n <Spinner size=\"sm\" label={null} />\n </span>\n </>\n ) : (\n content\n );\n if (href) {\n return (\n <Link\n href={href}\n className={classes}\n aria-busy={loading || undefined}\n aria-disabled={loading || undefined}\n tabIndex={loading ? -1 : undefined}\n disabled={disabled}\n {...rest}\n >\n {body}\n </Link>\n );\n }\n return (\n <button type={type} className={classes} disabled={loading || disabled} aria-busy={loading || undefined} {...rest}>\n {body}\n </button>\n );\n}\n\n/** A square button holding only an icon; `label` is required and is what\n * a screen reader says. */\nexport function IconButton({ icon, label, size = \"md\", variant = \"quiet\", className, ...rest }) {\n return (\n <Button\n variant={variant}\n size={size}\n aria-label={label}\n title={label}\n iconOnly\n className={cx(\"shrink-0\", className)}\n {...rest}\n >\n <Icon name={icon} size={size === \"sm\" ? 16 : 18} />\n </Button>\n );\n}\n"
|
|
55
61
|
},
|
|
56
62
|
{
|
|
57
63
|
"path": "templates/next/files/components/ui/calendar.jsx",
|
|
58
64
|
"type": "registry:ui",
|
|
59
65
|
"target": "@components/makersclaw/ui/calendar.jsx",
|
|
60
|
-
"content": "\"use client\";\n\nimport { useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport { cx } from \"./cx\";\nimport { IconButton } from \"./button\";\nimport { Select } from \"./field\";\n\n// Calendar — a month grid to pick a day, several days, or a span of days.\n// It is the WAI-ARIA date grid: one Tab stop (the focused day), arrows\n// move a day or a week, Page Up/Down a month (with Shift, a year), Home\n// and End the week's edges, Enter or Space picks. Names, week start and\n// digits come from Intl for the reader's locale. No date library.\n//\n// mode single (value \"2026-03-12\" | null) · multiple (value\n// [\"2026-03-12\", …], kept sorted) · range (value { from, to }\n// | null; `to` is null until the second click; a second click\n// before `from` swaps them)\n// value, onChange(value) — controlled; or `defaultValue`\n// required single: clicking the chosen day again keeps it (default:\n// it clears)\n// month the month shown, \"2026-03\" (controlled, with onMonthChange);\n// or `defaultMonth`; else the first chosen day's, else today's\n// numberOfMonths side by side from 640px, stacked on a phone\n// min, max \"YYYY-MM-DD\", inclusive; days past them are disabled and\n// the arrows stop there\n// disabled (date) → true for a day that cannot be picked (weekends,\n// booked days); true disables every day\n// captionLayout label (default) · dropdown (month and year Selects for\n// far jumps; the arrows stay) · none (no caption and no\n// arrows: the parent shows the month and moves it, passing a\n// controlled `month`; a screen reader still hears the month)\n// fromYear, toYear the dropdown's years (default: min/max, else today\n// −100 … +10)\n// showOutsideDays the neighbouring months' days, muted (true)\n// showWeekNumbers ISO 8601 week numbers down the side\n// marked days that hold something (posts, bookings): a list of\n// \"YYYY-MM-DD\", or { \"2026-03-04\": { tone, label } } — a dot\n// under the number in `tone` (primary, the default · neutral ·\n// success · warning · danger · info); `label` joins the day's\n// name for a screen reader (\"Wednesday, March 4, 2026, 3\n// posts\"). Selection is unchanged: open the day's items from\n// onChange\n// emphasizeMarked marked days drawn bold, the rest muted — a calendar\n// that exists to show where the posts are (false)\n// onlyMarked unmarked days cannot be picked, with no strike-through: they\n// are empty, not forbidden (that stays `disabled`'s look).\n// Keys still walk them (false)\n// weekStartsOn 0 (Sunday) … 6; default: the locale's\n// locale a BCP 47 tag; default: the browser's\n// today \"YYYY-MM-DD\", drawn ringed; default: the browser's today.\n// Pass it where a screen must not change from day to day (a\n// demo, a test)\n// size md (44px days) · sm (36px days from 640px, 44px on a phone)\n// autoFocus when it turns true, focus moves to the grid (a picker\n// passes its open state)\n// label what a screen reader calls the whole calendar (\"Calendar\")\n//\n// Dates are plain \"YYYY-MM-DD\" strings in and out, never Date objects. A\n// date is a day on a wall calendar, not an instant: a Date is an instant,\n// and \"2026-03-12\" parsed as one is midnight UTC — the 11th in New York.\n// Strings compare in order (\"2026-03-02\" < \"2026-03-12\"), survive JSON\n// and a database's `date` column unchanged, and mean the same day in\n// every timezone. Inside, arithmetic runs on UTC midnights, where no\n// daylight-saving shift can add or lose a day. The helpers below are\n// exported for the same work in a surface.\n\n// ── Dates as strings ────────────────────────────────────────────────────\n\nconst ISO = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\nconst DAY = 86400000;\nconst pad = (n, w = 2) => String(n).padStart(w, \"0\");\n\n/** \"YYYY-MM-DD\" → a Date at that day's UTC midnight; null when it is not\n * a real day (\"2026-02-30\", \"\", undefined). */\nexport function parseDate(s) {\n const m = typeof s === \"string\" ? ISO.exec(s) : null;\n if (!m) return null;\n const d = new Date(0);\n d.setUTCFullYear(+m[1], +m[2] - 1, +m[3]);\n return d.getUTCMonth() === +m[2] - 1 && d.getUTCDate() === +m[3] ? d : null;\n}\n\nconst iso = (d) => `${pad(d.getUTCFullYear(), 4)}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;\n\n/** True for a real \"YYYY-MM-DD\" day. */\nexport const isDateString = (s) => parseDate(s) !== null;\n\n/** A JS Date → the \"YYYY-MM-DD\" it is on the reader's own calendar\n * (local time: new Date() at 23:30 in Tokyo is Tokyo's day). */\nexport function toDateString(date = new Date()) {\n return `${pad(date.getFullYear(), 4)}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;\n}\n\n/** Today on the reader's calendar, \"YYYY-MM-DD\". */\nexport const todayString = () => toDateString(new Date());\n\n/** The day `n` days on (negative: back). */\nexport function addDays(s, n) {\n const d = parseDate(s);\n return d ? iso(new Date(d.getTime() + n * DAY)) : null;\n}\n\n/** The same day `n` months on, clamped to the month's end (Jan 31 + 1 →\n * Feb 28). */\nexport function addMonths(s, n) {\n const d = parseDate(s);\n if (!d) return null;\n const y = d.getUTCFullYear();\n const m = d.getUTCMonth() + n;\n const last = new Date(Date.UTC(2000, 0, 1));\n last.setUTCFullYear(y, m + 1, 0);\n const out = new Date(0);\n out.setUTCFullYear(y, m, Math.min(d.getUTCDate(), last.getUTCDate()));\n return iso(out);\n}\n\n/** The first day of `s`'s month; \"2026-03\" works as well as a full date. */\nexport const startOfMonth = (s) => (typeof s === \"string\" && /^\\d{4}-\\d{2}/.test(s) ? `${s.slice(0, 7)}-01` : null);\n\n/** The last day of `s`'s month. */\nexport const endOfMonth = (s) => {\n const first = startOfMonth(s);\n return first ? addDays(addMonths(first, 1), -1) : null;\n};\n\n/** The day of the week, 0 (Sunday) … 6. */\nexport const dayOfWeek = (s) => parseDate(s)?.getUTCDay() ?? null;\n\n// Intl's spacing differs between ICU builds (Node's thin and no-break\n// spaces around a range's dash, a browser's plain ones): the server's\n// HTML and the browser's first render must be the same text, so every\n// formatted string is written with plain spaces.\nexport const plainSpaces = (s) => s.replace(/[\\u00a0\\u2009\\u202f]/g, \" \");\n\n/** A day in words for the locale: formatDate(\"2026-03-12\") → \"Mar 12,\n * 2026\"; `options` are Intl.DateTimeFormat's ({ dateStyle: \"full\" }). */\nexport function formatDate(s, options = { dateStyle: \"medium\" }, locale) {\n const d = parseDate(s);\n return d ? plainSpaces(new Intl.DateTimeFormat(locale, { ...options, timeZone: \"UTC\" }).format(d)) : \"\";\n}\n\n// The week's first day for a locale: Intl's own answer where the browser\n// has it (getWeekInfo), else CLDR's regions that start on Sunday or\n// Saturday; everyone else starts on Monday.\nconst SUNDAY = new Set(\"AG AS BD BR BS BT BW BZ CA CO DM DO ET GT GU HK HN ID IL IN JM JP KE KH KR LA MH MM MO MT MX MZ NI NP PA PE PH PK PR PT PY SA SG SV TH TT TW UM US VE VI WS YE ZA ZW\".split(\" \"));\nconst SATURDAY = new Set(\"AE AF BH DJ DZ EG IQ IR JO KW LY OM QA SD SY\".split(\" \"));\n\n/** 0 (Sunday) … 6: the day a week starts on in `locale`. */\nexport function firstDayOfWeek(locale) {\n try {\n const loc = new Intl.Locale(locale ?? Intl.DateTimeFormat().resolvedOptions().locale);\n const info = loc.getWeekInfo?.() ?? loc.weekInfo;\n if (info?.firstDay) return info.firstDay % 7;\n const region = loc.maximize().region;\n return SUNDAY.has(region) ? 0 : SATURDAY.has(region) ? 6 : 1;\n } catch {\n return 1;\n }\n}\n\n// ISO 8601 week number of the week holding `thursday` (a Thursday's\n// week number is its day of the year, in sevens, rounded up).\nfunction isoWeek(thursday) {\n const d = parseDate(thursday);\n const jan1 = Date.UTC(d.getUTCFullYear(), 0, 1);\n return Math.floor((d.getTime() - jan1) / DAY / 7) + 1;\n}\n\n// ── The reader's locale and today, without a hydration mismatch ────────\n// The server does not know either. useSyncExternalStore renders the\n// server's answer (null) while hydrating, then the browser's — so the\n// first paint matches the HTML and nothing warns.\n\nconst never = () => () => {};\nfunction useBrowser(get) {\n return useSyncExternalStore(never, get, () => null);\n}\n\n/** The locale to format in: `locale`, else the browser's (after\n * hydration; \"en-US\" in the server's HTML). */\nexport function useLocale(locale) {\n const browser = useBrowser(() => navigator.language);\n return locale ?? browser ?? \"en-US\";\n}\n\n/** `today`, else the browser's today once hydrated (null before). */\nexport function useToday(today) {\n const browser = useBrowser(todayString);\n return today ?? browser;\n}\n\n// ── The grid ────────────────────────────────────────────────────────────\n\n// A marked day's dot. On a picked day (the primary fill) the primary and\n// neutral dots turn to the fill's own foreground so they still show.\nconst DOT = {\n primary: \"bg-primary\",\n neutral: \"bg-muted-foreground\",\n success: \"bg-success\",\n warning: \"bg-warning\",\n danger: \"bg-destructive\",\n info: \"bg-info\",\n};\n\nconst SIZES = {\n md: { cell: \"size-11\", caption: \"h-11\", nav: \"md\" },\n sm: { cell: \"size-11 sm:size-9\", caption: \"h-11 sm:h-9\", nav: \"sm\" },\n};\n\nfunction useControlled(value, fallback) {\n const [own, setOwn] = useState(fallback);\n return value !== undefined ? [value, false] : [own, setOwn];\n}\n\nfunction listOf(mode, value) {\n if (!value) return [];\n if (mode === \"multiple\") return Array.isArray(value) ? value : [];\n if (mode === \"range\") return [value.from, value.to].filter(Boolean);\n return [value];\n}\n\nexport function Calendar({\n mode = \"single\",\n value: valueProp,\n defaultValue,\n onChange,\n required = false,\n month: monthProp,\n defaultMonth,\n onMonthChange,\n numberOfMonths = 1,\n min,\n max,\n disabled,\n captionLayout = \"label\",\n fromYear,\n toYear,\n showOutsideDays = true,\n showWeekNumbers = false,\n marked,\n emphasizeMarked = false,\n onlyMarked = false,\n weekStartsOn,\n locale: localeProp,\n today: todayProp,\n size = \"md\",\n autoFocus = false,\n label = \"Calendar\",\n className,\n}) {\n const uid = useId();\n const root = useRef(null);\n const pending = useRef(false);\n const locale = useLocale(localeProp);\n const today = useToday(todayProp);\n const s = SIZES[size] ?? SIZES.md;\n const n = Math.max(1, numberOfMonths | 0);\n\n const [own, setOwnValue] = useControlled(valueProp, defaultValue ?? (mode === \"multiple\" ? [] : null));\n const value = valueProp !== undefined ? valueProp : own;\n const chosen = listOf(mode, value);\n\n const [ownMonth, setOwnMonth] = useState(() => startOfMonth(defaultMonth) ?? startOfMonth(chosen[0]));\n const first = startOfMonth(monthProp) ?? ownMonth ?? startOfMonth(today);\n const last = first ? addMonths(first, n - 1) : null;\n const [focus, setFocus] = useState(null);\n const [hover, setHover] = useState(null);\n\n const marks = useMemo(() => {\n if (Array.isArray(marked)) return Object.fromEntries(marked.map((d) => [d, {}]));\n return marked && typeof marked === \"object\" ? marked : {};\n }, [marked]);\n\n const ws = weekStartsOn ?? firstDayOfWeek(locale);\n const fmt = useMemo(() => {\n const f = (o) => {\n const intl = new Intl.DateTimeFormat(locale, { ...o, timeZone: \"UTC\" });\n return { format: (d) => plainSpaces(intl.format(d)) };\n };\n return {\n caption: f({ month: \"long\", year: \"numeric\" }),\n day: f({ day: \"numeric\" }),\n full: f({ dateStyle: \"full\" }),\n monthShort: f({ month: \"short\" }),\n year: f({ year: \"numeric\" }),\n weekShort: f({ weekday: \"short\" }),\n weekLong: f({ weekday: \"long\" }),\n };\n }, [locale]);\n // 2023-01-01 was a Sunday: the seven names from the week's first day.\n const weekdays = useMemo(\n () =>\n Array.from({ length: 7 }, (_, i) => {\n const d = parseDate(addDays(\"2023-01-01\", (ws + i) % 7));\n return { short: fmt.weekShort.format(d), long: fmt.weekLong.format(d) };\n }),\n [fmt, ws],\n );\n\n // Focus follows the roving day after the render that shows it.\n useEffect(() => {\n if (!pending.current) return;\n pending.current = false;\n root.current?.querySelector('button[data-day][tabindex=\"0\"]')?.focus();\n });\n useEffect(() => {\n if (!autoFocus) return;\n const go = () => root.current?.querySelector('button[data-day][tabindex=\"0\"]')?.focus({ preventScroll: true });\n // Inside a popover not shown yet (one open on load), wait for it: the\n // Popover focuses its first control on toggle, and the day wins after.\n const pop = root.current?.closest(\"[popover]\");\n if (pop && !pop.matches(\":popover-open\")) {\n let t;\n const on = () => (t = setTimeout(go));\n pop.addEventListener(\"toggle\", on, { once: true });\n return () => {\n pop.removeEventListener(\"toggle\", on);\n clearTimeout(t);\n };\n }\n go();\n }, [autoFocus]);\n\n function setMonth(m) {\n if (monthProp === undefined) setOwnMonth(m);\n onMonthChange?.(m.slice(0, 7));\n }\n\n function setValue(v) {\n if (setOwnValue) setOwnValue(v);\n onChange?.(v);\n }\n\n const isDisabled = (d) =>\n disabled === true || (min && d < min) || (max && d > max) || (typeof disabled === \"function\" && !!disabled(d));\n const inView = (d) => !!(d && first && startOfMonth(d) >= first && startOfMonth(d) <= last);\n\n const isEmpty = (d) => onlyMarked && !marks[d];\n\n function pick(d) {\n if (isDisabled(d) || isEmpty(d)) return;\n if (mode === \"multiple\") {\n setValue(chosen.includes(d) ? chosen.filter((x) => x !== d) : [...chosen, d].sort());\n } else if (mode === \"range\") {\n const { from, to } = value ?? {};\n if (!from || to) setValue({ from: d, to: null });\n else if (d < from) setValue({ from: d, to: from });\n else setValue({ from, to: d });\n } else {\n setValue(value === d && !required ? null : d);\n }\n }\n\n function moveTo(d) {\n if (min && d < min) d = min;\n if (max && d > max) d = max;\n const m = startOfMonth(d);\n if (m < first) setMonth(m);\n else if (m > last) setMonth(addMonths(m, -(n - 1)));\n setFocus(d);\n pending.current = true;\n }\n\n function onKeyDown(e) {\n const day = e.target.dataset?.day;\n if (!day || e.altKey || e.ctrlKey || e.metaKey) return;\n const rtl = getComputedStyle(e.currentTarget).direction === \"rtl\";\n const back = (dayOfWeek(day) - ws + 7) % 7;\n const step = {\n ArrowLeft: () => addDays(day, rtl ? 1 : -1),\n ArrowRight: () => addDays(day, rtl ? -1 : 1),\n ArrowUp: () => addDays(day, -7),\n ArrowDown: () => addDays(day, 7),\n PageUp: () => addMonths(day, e.shiftKey ? -12 : -1),\n PageDown: () => addMonths(day, e.shiftKey ? 12 : 1),\n Home: () => addDays(day, -back),\n End: () => addDays(day, 6 - back),\n }[e.key];\n if (!step) return;\n e.preventDefault();\n moveTo(step());\n }\n\n // Range display: the committed span, or — between the two clicks —\n // a preview from `from` to the day under the pointer or focus.\n const range = mode === \"range\" ? value ?? {} : {};\n const previewing = !!(range.from && !range.to && hover);\n const ends = range.from ? [range.from, range.to ?? (previewing ? hover : range.from)].sort() : null;\n\n const tabbable = [focus, ...chosen, today].find((d) => inView(d) && !(min && d < min) && !(max && d > max)) ?? (inView(min) ? min : first);\n\n if (!first) {\n // Hydrating with no month to show yet: hold the space.\n return <div ref={root} role=\"group\" aria-label={label} aria-busy=\"true\" className={cx(\"min-h-80 min-w-72\", className)} />;\n }\n\n const months = Array.from({ length: n }, (_, i) => addMonths(first, i));\n const prevOff = !!(min && startOfMonth(min) >= first);\n const nextOff = !!(max && startOfMonth(max) <= last);\n\n function Caption({ m, i }) {\n const id = `${uid}-cap-${i}`;\n if (captionLayout === \"none\") {\n return (\n <span id={id} className=\"sr-only\" aria-live={i === 0 ? \"polite\" : undefined}>\n {fmt.caption.format(parseDate(m))}\n </span>\n );\n }\n if (captionLayout === \"dropdown\" && i === 0) {\n const y = +m.slice(0, 4);\n const ty = +(today ?? m).slice(0, 4);\n const lo = Math.min(fromYear ?? (min ? +min.slice(0, 4) : ty - 100), y);\n const hi = Math.max(toYear ?? (max ? +max.slice(0, 4) : ty + 10), y);\n const monthOff = (k) => {\n const mm = `${pad(y, 4)}-${pad(k + 1)}-01`;\n return (min && endOfMonth(mm) < min) || (max && mm > max);\n };\n return (\n <div className={cx(\"flex items-center justify-center gap-1 px-12\", s.caption)}>\n <span id={id} className=\"sr-only\" aria-live=\"polite\">\n {fmt.caption.format(parseDate(m))}\n </span>\n <span className=\"min-w-0 flex-[3]\">\n <Select aria-label=\"Month\" value={String(+m.slice(5, 7))} onChange={(e) => setMonth(`${pad(y, 4)}-${pad(+e.target.value)}-01`)}>\n {Array.from({ length: 12 }, (_, k) => (\n <option key={k} value={k + 1} disabled={monthOff(k)}>\n {fmt.monthShort.format(parseDate(`2023-${pad(k + 1)}-01`))}\n </option>\n ))}\n </Select>\n </span>\n <span className=\"min-w-0 flex-[2]\">\n <Select aria-label=\"Year\" value={String(y)} onChange={(e) => setMonth(`${pad(+e.target.value, 4)}${m.slice(4)}`)}>\n {Array.from({ length: hi - lo + 1 }, (_, k) => (\n <option key={k} value={lo + k}>\n {fmt.year.format(parseDate(`${pad(lo + k, 4)}-06-15`))}\n </option>\n ))}\n </Select>\n </span>\n </div>\n );\n }\n return (\n <div id={id} aria-live={i === 0 ? \"polite\" : undefined} className={cx(\"flex items-center justify-center px-12 font-medium\", s.caption)}>\n {fmt.caption.format(parseDate(m))}\n </div>\n );\n }\n\n function Month({ m, i }) {\n const lead = (dayOfWeek(m) - ws + 7) % 7;\n const start = addDays(m, -lead);\n const dim = +endOfMonth(m).slice(8);\n const rows = Math.ceil((lead + dim) / 7);\n return (\n <div className=\"grid content-start gap-2\">\n {Caption({ m, i })}\n <table role=\"grid\" aria-labelledby={`${uid}-cap-${i}`} className=\"border-collapse\" onKeyDown={onKeyDown}>\n <thead>\n <tr>\n {showWeekNumbers ? (\n <th scope=\"col\" className=\"w-8 text-xs font-normal text-muted-foreground\">\n <span className=\"sr-only\">Week</span>\n </th>\n ) : null}\n {weekdays.map((w) => (\n <th key={w.long} scope=\"col\" abbr={w.long} className=\"h-8 p-0 text-xs font-normal text-muted-foreground\">\n <span aria-hidden=\"true\">{w.short}</span>\n <span className=\"sr-only\">{w.long}</span>\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {Array.from({ length: rows }, (_, r) => {\n const rowStart = addDays(start, r * 7);\n return (\n <tr key={rowStart}>\n {showWeekNumbers ? (\n <th scope=\"row\" className=\"w-8 p-0 text-center text-xs font-normal tabular-nums text-muted-foreground\">\n {isoWeek(addDays(rowStart, (4 - dayOfWeek(rowStart) + 7) % 7))}\n </th>\n ) : null}\n {Array.from({ length: 7 }, (_, c) => Day({ d: addDays(rowStart, c), m }))}\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n );\n }\n\n function Day({ d, m }) {\n const outside = startOfMonth(d) !== m;\n if (outside && !showOutsideDays) return <td key={d} className=\"p-0\" />;\n const off = isDisabled(d);\n const empty = !off && isEmpty(d);\n const picked = mode === \"range\" ? d === range.from || d === range.to : chosen.includes(d);\n // Beside another month, an outside day repeats a day shown there: only\n // the real one carries the band.\n const band = ends && ends[0] < ends[1] && d >= ends[0] && d <= ends[1] && !(outside && n > 1);\n const isToday = d === today;\n const mark = marks[d];\n const tone = mark ? (DOT[mark.tone] ? mark.tone : \"primary\") : null;\n // cx does not merge: one text colour and one weight per day.\n const quiet = !picked && !band && !outside && ((emphasizeMarked && !mark) || empty);\n return (\n <td\n key={d}\n aria-selected={picked || (band && !previewing) || undefined}\n className={cx(\n \"p-0 text-center\",\n band && (previewing ? \"bg-accent/50\" : \"bg-accent\"),\n band && d === ends[0] && \"rounded-l-md\",\n band && d === ends[1] && \"rounded-r-md\",\n )}\n >\n <button\n type=\"button\"\n data-day={d}\n data-outside={outside || undefined}\n tabIndex={!outside && d === tabbable ? 0 : -1}\n aria-label={[fmt.full.format(parseDate(d)), isToday && \"today\", mark?.label].filter(Boolean).join(\", \")}\n aria-current={isToday ? \"date\" : undefined}\n aria-disabled={off || empty || undefined}\n onClick={() => {\n if (off || empty) return;\n pick(d);\n setFocus(d);\n if (!inView(d)) setMonth(startOfMonth(d) < first ? startOfMonth(d) : addMonths(startOfMonth(d), -(n - 1)));\n }}\n onMouseEnter={() => mode === \"range\" && setHover(d)}\n onFocus={() => mode === \"range\" && setHover(d)}\n className={cx(\n \"relative inline-flex items-center justify-center rounded-md text-sm tabular-nums outline-none\",\n \"focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-ring\",\n s.cell,\n picked\n ? \"bg-primary font-medium text-primary-foreground\"\n : band\n ? \"text-accent-foreground hover:bg-accent\"\n : quiet\n ? \"text-muted-foreground\"\n : \"text-foreground\",\n !picked && !band && !empty && \"hover:bg-accent\",\n emphasizeMarked && mark && !picked && !isToday && \"font-semibold\",\n outside && !picked && \"text-muted-foreground opacity-60\",\n outside && picked && \"opacity-70\",\n off && \"cursor-not-allowed line-through opacity-40 hover:bg-transparent\",\n empty && \"cursor-default\",\n // Today: a ring and a heavier number — never colour alone.\n isToday && \"font-semibold ring-1 ring-inset ring-muted-foreground/60\",\n )}\n >\n {fmt.day.format(parseDate(d))}\n {tone ? (\n <span\n aria-hidden=\"true\"\n className={cx(\n \"absolute bottom-1 left-1/2 size-1.5 -translate-x-1/2 rounded-full\",\n picked && (tone === \"primary\" || tone === \"neutral\") ? \"bg-primary-foreground\" : DOT[tone],\n )}\n />\n ) : null}\n </button>\n </td>\n );\n }\n\n return (\n <div\n ref={root}\n role=\"group\"\n aria-label={label}\n onMouseLeave={() => setHover(null)}\n onBlur={(e) => !root.current?.contains(e.relatedTarget) && setHover(null)}\n className={cx(\"relative inline-block max-w-full text-sm\", className)}\n >\n {captionLayout === \"none\" ? null : (\n <>\n <IconButton\n icon=\"chevron-left\"\n label=\"Previous month\"\n size={s.nav}\n disabled={prevOff}\n onClick={() => setMonth(addMonths(first, -1))}\n className=\"absolute left-0 top-0 z-10\"\n />\n <IconButton\n icon=\"chevron-right\"\n label=\"Next month\"\n size={s.nav}\n disabled={nextOff}\n onClick={() => setMonth(addMonths(first, 1))}\n className=\"absolute right-0 top-0 z-10\"\n />\n </>\n )}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:gap-6\">\n {months.map((m, i) => (\n <div key={m}>{Month({ m, i })}</div>\n ))}\n </div>\n </div>\n );\n}\n"
|
|
66
|
+
"content": "\"use client\";\n\nimport { useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport { cx } from \"./cx\";\nimport { IconButton } from \"./button\";\nimport { Select } from \"./field\";\n\n// Calendar — a month grid to pick a day, several days, or a span of days.\n// It is the WAI-ARIA date grid: one Tab stop (the focused day), arrows\n// move a day or a week, Page Up/Down a month (with Shift, a year), Home\n// and End the week's edges, Enter or Space picks. Names, week start and\n// digits come from Intl for the reader's locale. No date library.\n//\n// mode single (value \"2026-03-12\" | null) · multiple (value\n// [\"2026-03-12\", …], kept sorted) · range (value { from, to }\n// | null; `to` is null until the second click; a second click\n// before `from` swaps them)\n// value, onChange(value) — controlled; or `defaultValue`\n// required single: clicking the chosen day again keeps it (default:\n// it clears)\n// month the month shown, \"2026-03\" (controlled, with onMonthChange);\n// or `defaultMonth`; else the first chosen day's, else today's\n// numberOfMonths side by side from 640px, stacked on a phone\n// min, max \"YYYY-MM-DD\", inclusive; days past them are disabled and\n// the arrows stop there\n// disabled (date) → true for a day that cannot be picked (weekends,\n// booked days); true disables every day\n// captionLayout label (default) · dropdown (month and year Selects for\n// far jumps; the arrows stay) · none (no caption and no\n// arrows: the parent shows the month and moves it, passing a\n// controlled `month`; a screen reader still hears the month)\n// fromYear, toYear the dropdown's years (default: min/max, else today\n// −100 … +10)\n// showOutsideDays the neighbouring months' days, muted (true)\n// showWeekNumbers ISO 8601 week numbers down the side\n// marked days that hold something (posts, bookings): a list of\n// \"YYYY-MM-DD\", or { \"2026-03-04\": { tone, label } } — a dot\n// under the number in `tone` (primary, the default · neutral ·\n// success · warning · danger · info); `label` joins the day's\n// name for a screen reader (\"Wednesday, March 4, 2026, 3\n// posts\"). Selection is unchanged: open the day's items from\n// onChange\n// emphasizeMarked marked days drawn bold, the rest muted — a calendar\n// that exists to show where the posts are (false)\n// onlyMarked unmarked days cannot be picked, with no strike-through: they\n// are empty, not forbidden (that stays `disabled`'s look).\n// Keys still walk them (false)\n// weekStartsOn 0 (Sunday) … 6; default: the locale's\n// locale a BCP 47 tag; default: the browser's\n// today \"YYYY-MM-DD\", drawn ringed; default: the browser's today.\n// Pass it where a screen must not change from day to day (a\n// demo, a test)\n// size md (44px days) · sm (36px days from 640px, 44px on a phone)\n// autoFocus when it turns true, focus moves to the grid (a picker\n// passes its open state)\n// label what a screen reader calls the whole calendar (\"Calendar\")\n//\n// Dates are plain \"YYYY-MM-DD\" strings in and out, never Date objects. A\n// date is a day on a wall calendar, not an instant: a Date is an instant,\n// and \"2026-03-12\" parsed as one is midnight UTC — the 11th in New York.\n// Strings compare in order (\"2026-03-02\" < \"2026-03-12\"), survive JSON\n// and a database's `date` column unchanged, and mean the same day in\n// every timezone. Inside, arithmetic runs on UTC midnights, where no\n// daylight-saving shift can add or lose a day. The helpers below are\n// exported for the same work in a surface.\n\n// ── Dates as strings ────────────────────────────────────────────────────\n\nconst ISO = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\nconst DAY = 86400000;\nconst pad = (n, w = 2) => String(n).padStart(w, \"0\");\n\n/** \"YYYY-MM-DD\" → a Date at that day's UTC midnight; null when it is not\n * a real day (\"2026-02-30\", \"\", undefined). */\nexport function parseDate(s) {\n const m = typeof s === \"string\" ? ISO.exec(s) : null;\n if (!m) return null;\n const d = new Date(0);\n d.setUTCFullYear(+m[1], +m[2] - 1, +m[3]);\n return d.getUTCMonth() === +m[2] - 1 && d.getUTCDate() === +m[3] ? d : null;\n}\n\nconst iso = (d) => `${pad(d.getUTCFullYear(), 4)}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;\n\n/** True for a real \"YYYY-MM-DD\" day. */\nexport const isDateString = (s) => parseDate(s) !== null;\n\n/** A JS Date → the \"YYYY-MM-DD\" it is on the reader's own calendar\n * (local time: new Date() at 23:30 in Tokyo is Tokyo's day). */\nexport function toDateString(date = new Date()) {\n return `${pad(date.getFullYear(), 4)}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;\n}\n\n/** Today on the reader's calendar, \"YYYY-MM-DD\". */\nexport const todayString = () => toDateString(new Date());\n\n/** The day `n` days on (negative: back). */\nexport function addDays(s, n) {\n const d = parseDate(s);\n return d ? iso(new Date(d.getTime() + n * DAY)) : null;\n}\n\n/** The same day `n` months on, clamped to the month's end (Jan 31 + 1 →\n * Feb 28). */\nexport function addMonths(s, n) {\n const d = parseDate(s);\n if (!d) return null;\n const y = d.getUTCFullYear();\n const m = d.getUTCMonth() + n;\n const last = new Date(Date.UTC(2000, 0, 1));\n last.setUTCFullYear(y, m + 1, 0);\n const out = new Date(0);\n out.setUTCFullYear(y, m, Math.min(d.getUTCDate(), last.getUTCDate()));\n return iso(out);\n}\n\n/** The first day of `s`'s month; \"2026-03\" works as well as a full date. */\nexport const startOfMonth = (s) => (typeof s === \"string\" && /^\\d{4}-\\d{2}/.test(s) ? `${s.slice(0, 7)}-01` : null);\n\n/** The last day of `s`'s month. */\nexport const endOfMonth = (s) => {\n const first = startOfMonth(s);\n return first ? addDays(addMonths(first, 1), -1) : null;\n};\n\n/** The day of the week, 0 (Sunday) … 6. */\nexport const dayOfWeek = (s) => parseDate(s)?.getUTCDay() ?? null;\n\n// Intl's spacing differs between ICU builds (Node's thin and no-break\n// spaces around a range's dash, a browser's plain ones): the server's\n// HTML and the browser's first render must be the same text, so every\n// formatted string is written with plain spaces.\nexport const plainSpaces = (s) => s.replace(/[\\u00a0\\u2009\\u202f]/g, \" \");\n\n/** A day in words for the locale: formatDate(\"2026-03-12\") → \"Mar 12,\n * 2026\"; `options` are Intl.DateTimeFormat's ({ dateStyle: \"full\" }). */\nexport function formatDate(s, options = { dateStyle: \"medium\" }, locale) {\n const d = parseDate(s);\n return d ? plainSpaces(new Intl.DateTimeFormat(locale, { ...options, timeZone: \"UTC\" }).format(d)) : \"\";\n}\n\n/** Any date a row holds → a Date to show in the reader's own time; null\n * when it is none. A day (\"2026-09-28\", what a `date` column reads as)\n * is that day's LOCAL midnight: new Date(\"2026-09-28\") is UTC's\n * midnight, the 27th everywhere west of Greenwich, and a screen that\n * shows it, then saves what it shows, moves the day back by one. A Date\n * stays as it is; an ISO timestamp or epoch milliseconds go through\n * new Date(). For a day alone, formatDate and the helpers above need no\n * Date at all; this is for code that mixes days with instants (a\n * comment's time, \"due\" beside \"sent at\"). */\nexport function toLocalDate(value) {\n if (value === null || value === undefined || value === \"\") return null;\n if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;\n const day = typeof value === \"string\" ? parseDate(value) : null;\n if (day) {\n // setFullYear, not new Date(y, m, d): that one reads years 0–99 as 19xx.\n const local = new Date(0);\n local.setFullYear(day.getUTCFullYear(), day.getUTCMonth(), day.getUTCDate());\n local.setHours(0, 0, 0, 0);\n return local;\n }\n const d = new Date(value);\n return Number.isNaN(d.getTime()) ? null : d;\n}\n\n// The week's first day for a locale: Intl's own answer where the browser\n// has it (getWeekInfo), else CLDR's regions that start on Sunday or\n// Saturday; everyone else starts on Monday.\nconst SUNDAY = new Set(\"AG AS BD BR BS BT BW BZ CA CO DM DO ET GT GU HK HN ID IL IN JM JP KE KH KR LA MH MM MO MT MX MZ NI NP PA PE PH PK PR PT PY SA SG SV TH TT TW UM US VE VI WS YE ZA ZW\".split(\" \"));\nconst SATURDAY = new Set(\"AE AF BH DJ DZ EG IQ IR JO KW LY OM QA SD SY\".split(\" \"));\n\n/** 0 (Sunday) … 6: the day a week starts on in `locale`. */\nexport function firstDayOfWeek(locale) {\n try {\n const loc = new Intl.Locale(locale ?? Intl.DateTimeFormat().resolvedOptions().locale);\n const info = loc.getWeekInfo?.() ?? loc.weekInfo;\n if (info?.firstDay) return info.firstDay % 7;\n const region = loc.maximize().region;\n return SUNDAY.has(region) ? 0 : SATURDAY.has(region) ? 6 : 1;\n } catch {\n return 1;\n }\n}\n\n// ISO 8601 week number of the week holding `thursday` (a Thursday's\n// week number is its day of the year, in sevens, rounded up).\nfunction isoWeek(thursday) {\n const d = parseDate(thursday);\n const jan1 = Date.UTC(d.getUTCFullYear(), 0, 1);\n return Math.floor((d.getTime() - jan1) / DAY / 7) + 1;\n}\n\n// ── The reader's locale and today, without a hydration mismatch ────────\n// The server does not know either. useSyncExternalStore renders the\n// server's answer (null) while hydrating, then the browser's — so the\n// first paint matches the HTML and nothing warns.\n\nconst never = () => () => {};\nfunction useBrowser(get) {\n return useSyncExternalStore(never, get, () => null);\n}\n\n/** The locale to format in: `locale`, else the browser's (after\n * hydration; \"en-US\" in the server's HTML). */\nexport function useLocale(locale) {\n const browser = useBrowser(() => navigator.language);\n return locale ?? browser ?? \"en-US\";\n}\n\n/** `today`, else the browser's today once hydrated (null before). */\nexport function useToday(today) {\n const browser = useBrowser(todayString);\n return today ?? browser;\n}\n\n// ── The grid ────────────────────────────────────────────────────────────\n\n// A marked day's dot. On a picked day (the primary fill) the primary and\n// neutral dots turn to the fill's own foreground so they still show.\nconst DOT = {\n primary: \"bg-primary\",\n neutral: \"bg-muted-foreground\",\n success: \"bg-success\",\n warning: \"bg-warning\",\n danger: \"bg-destructive\",\n info: \"bg-info\",\n};\n\nconst SIZES = {\n md: { cell: \"size-11\", caption: \"h-11\", nav: \"md\" },\n sm: { cell: \"size-11 sm:size-9\", caption: \"h-11 sm:h-9\", nav: \"sm\" },\n};\n\nfunction useControlled(value, fallback) {\n const [own, setOwn] = useState(fallback);\n return value !== undefined ? [value, false] : [own, setOwn];\n}\n\nfunction listOf(mode, value) {\n if (!value) return [];\n if (mode === \"multiple\") return Array.isArray(value) ? value : [];\n if (mode === \"range\") return [value.from, value.to].filter(Boolean);\n return [value];\n}\n\nexport function Calendar({\n mode = \"single\",\n value: valueProp,\n defaultValue,\n onChange,\n required = false,\n month: monthProp,\n defaultMonth,\n onMonthChange,\n numberOfMonths = 1,\n min,\n max,\n disabled,\n captionLayout = \"label\",\n fromYear,\n toYear,\n showOutsideDays = true,\n showWeekNumbers = false,\n marked,\n emphasizeMarked = false,\n onlyMarked = false,\n weekStartsOn,\n locale: localeProp,\n today: todayProp,\n size = \"md\",\n autoFocus = false,\n label = \"Calendar\",\n className,\n}) {\n const uid = useId();\n const root = useRef(null);\n const pending = useRef(false);\n const locale = useLocale(localeProp);\n const today = useToday(todayProp);\n const s = SIZES[size] ?? SIZES.md;\n const n = Math.max(1, numberOfMonths | 0);\n\n const [own, setOwnValue] = useControlled(valueProp, defaultValue ?? (mode === \"multiple\" ? [] : null));\n const value = valueProp !== undefined ? valueProp : own;\n const chosen = listOf(mode, value);\n\n const [ownMonth, setOwnMonth] = useState(() => startOfMonth(defaultMonth) ?? startOfMonth(chosen[0]));\n const first = startOfMonth(monthProp) ?? ownMonth ?? startOfMonth(today);\n const last = first ? addMonths(first, n - 1) : null;\n const [focus, setFocus] = useState(null);\n const [hover, setHover] = useState(null);\n\n const marks = useMemo(() => {\n if (Array.isArray(marked)) return Object.fromEntries(marked.map((d) => [d, {}]));\n return marked && typeof marked === \"object\" ? marked : {};\n }, [marked]);\n\n const ws = weekStartsOn ?? firstDayOfWeek(locale);\n const fmt = useMemo(() => {\n const f = (o) => {\n const intl = new Intl.DateTimeFormat(locale, { ...o, timeZone: \"UTC\" });\n return { format: (d) => plainSpaces(intl.format(d)) };\n };\n return {\n caption: f({ month: \"long\", year: \"numeric\" }),\n day: f({ day: \"numeric\" }),\n full: f({ dateStyle: \"full\" }),\n monthShort: f({ month: \"short\" }),\n year: f({ year: \"numeric\" }),\n weekShort: f({ weekday: \"short\" }),\n weekLong: f({ weekday: \"long\" }),\n };\n }, [locale]);\n // 2023-01-01 was a Sunday: the seven names from the week's first day.\n const weekdays = useMemo(\n () =>\n Array.from({ length: 7 }, (_, i) => {\n const d = parseDate(addDays(\"2023-01-01\", (ws + i) % 7));\n return { short: fmt.weekShort.format(d), long: fmt.weekLong.format(d) };\n }),\n [fmt, ws],\n );\n\n // Focus follows the roving day after the render that shows it.\n useEffect(() => {\n if (!pending.current) return;\n pending.current = false;\n root.current?.querySelector('button[data-day][tabindex=\"0\"]')?.focus();\n });\n useEffect(() => {\n if (!autoFocus) return;\n const go = () => root.current?.querySelector('button[data-day][tabindex=\"0\"]')?.focus({ preventScroll: true });\n // Inside a popover not shown yet (one open on load), wait for it: the\n // Popover focuses its first control on toggle, and the day wins after.\n const pop = root.current?.closest(\"[popover]\");\n if (pop && !pop.matches(\":popover-open\")) {\n let t;\n const on = () => (t = setTimeout(go));\n pop.addEventListener(\"toggle\", on, { once: true });\n return () => {\n pop.removeEventListener(\"toggle\", on);\n clearTimeout(t);\n };\n }\n go();\n }, [autoFocus]);\n\n function setMonth(m) {\n if (monthProp === undefined) setOwnMonth(m);\n onMonthChange?.(m.slice(0, 7));\n }\n\n function setValue(v) {\n if (setOwnValue) setOwnValue(v);\n onChange?.(v);\n }\n\n const isDisabled = (d) =>\n disabled === true || (min && d < min) || (max && d > max) || (typeof disabled === \"function\" && !!disabled(d));\n const inView = (d) => !!(d && first && startOfMonth(d) >= first && startOfMonth(d) <= last);\n\n const isEmpty = (d) => onlyMarked && !marks[d];\n\n function pick(d) {\n if (isDisabled(d) || isEmpty(d)) return;\n if (mode === \"multiple\") {\n setValue(chosen.includes(d) ? chosen.filter((x) => x !== d) : [...chosen, d].sort());\n } else if (mode === \"range\") {\n const { from, to } = value ?? {};\n if (!from || to) setValue({ from: d, to: null });\n else if (d < from) setValue({ from: d, to: from });\n else setValue({ from, to: d });\n } else {\n setValue(value === d && !required ? null : d);\n }\n }\n\n function moveTo(d) {\n if (min && d < min) d = min;\n if (max && d > max) d = max;\n const m = startOfMonth(d);\n if (m < first) setMonth(m);\n else if (m > last) setMonth(addMonths(m, -(n - 1)));\n setFocus(d);\n pending.current = true;\n }\n\n function onKeyDown(e) {\n const day = e.target.dataset?.day;\n if (!day || e.altKey || e.ctrlKey || e.metaKey) return;\n const rtl = getComputedStyle(e.currentTarget).direction === \"rtl\";\n const back = (dayOfWeek(day) - ws + 7) % 7;\n const step = {\n ArrowLeft: () => addDays(day, rtl ? 1 : -1),\n ArrowRight: () => addDays(day, rtl ? -1 : 1),\n ArrowUp: () => addDays(day, -7),\n ArrowDown: () => addDays(day, 7),\n PageUp: () => addMonths(day, e.shiftKey ? -12 : -1),\n PageDown: () => addMonths(day, e.shiftKey ? 12 : 1),\n Home: () => addDays(day, -back),\n End: () => addDays(day, 6 - back),\n }[e.key];\n if (!step) return;\n e.preventDefault();\n moveTo(step());\n }\n\n // Range display: the committed span, or — between the two clicks —\n // a preview from `from` to the day under the pointer or focus.\n const range = mode === \"range\" ? value ?? {} : {};\n const previewing = !!(range.from && !range.to && hover);\n const ends = range.from ? [range.from, range.to ?? (previewing ? hover : range.from)].sort() : null;\n\n const tabbable = [focus, ...chosen, today].find((d) => inView(d) && !(min && d < min) && !(max && d > max)) ?? (inView(min) ? min : first);\n\n if (!first) {\n // Hydrating with no month to show yet: hold the space.\n return <div ref={root} role=\"group\" aria-label={label} aria-busy=\"true\" className={cx(\"min-h-80 min-w-72\", className)} />;\n }\n\n const months = Array.from({ length: n }, (_, i) => addMonths(first, i));\n const prevOff = !!(min && startOfMonth(min) >= first);\n const nextOff = !!(max && startOfMonth(max) <= last);\n\n function Caption({ m, i }) {\n const id = `${uid}-cap-${i}`;\n if (captionLayout === \"none\") {\n return (\n <span id={id} className=\"sr-only\" aria-live={i === 0 ? \"polite\" : undefined}>\n {fmt.caption.format(parseDate(m))}\n </span>\n );\n }\n if (captionLayout === \"dropdown\" && i === 0) {\n const y = +m.slice(0, 4);\n const ty = +(today ?? m).slice(0, 4);\n const lo = Math.min(fromYear ?? (min ? +min.slice(0, 4) : ty - 100), y);\n const hi = Math.max(toYear ?? (max ? +max.slice(0, 4) : ty + 10), y);\n const monthOff = (k) => {\n const mm = `${pad(y, 4)}-${pad(k + 1)}-01`;\n return (min && endOfMonth(mm) < min) || (max && mm > max);\n };\n return (\n <div className={cx(\"flex items-center justify-center gap-1 px-12\", s.caption)}>\n <span id={id} className=\"sr-only\" aria-live=\"polite\">\n {fmt.caption.format(parseDate(m))}\n </span>\n <span className=\"min-w-0 flex-[3]\">\n <Select aria-label=\"Month\" value={String(+m.slice(5, 7))} onChange={(e) => setMonth(`${pad(y, 4)}-${pad(+e.target.value)}-01`)}>\n {Array.from({ length: 12 }, (_, k) => (\n <option key={k} value={k + 1} disabled={monthOff(k)}>\n {fmt.monthShort.format(parseDate(`2023-${pad(k + 1)}-01`))}\n </option>\n ))}\n </Select>\n </span>\n <span className=\"min-w-0 flex-[2]\">\n <Select aria-label=\"Year\" value={String(y)} onChange={(e) => setMonth(`${pad(+e.target.value, 4)}${m.slice(4)}`)}>\n {Array.from({ length: hi - lo + 1 }, (_, k) => (\n <option key={k} value={lo + k}>\n {fmt.year.format(parseDate(`${pad(lo + k, 4)}-06-15`))}\n </option>\n ))}\n </Select>\n </span>\n </div>\n );\n }\n return (\n <div id={id} aria-live={i === 0 ? \"polite\" : undefined} className={cx(\"flex items-center justify-center px-12 font-medium\", s.caption)}>\n {fmt.caption.format(parseDate(m))}\n </div>\n );\n }\n\n function Month({ m, i }) {\n const lead = (dayOfWeek(m) - ws + 7) % 7;\n const start = addDays(m, -lead);\n const dim = +endOfMonth(m).slice(8);\n const rows = Math.ceil((lead + dim) / 7);\n return (\n <div className=\"grid content-start gap-2\">\n {Caption({ m, i })}\n <table role=\"grid\" aria-labelledby={`${uid}-cap-${i}`} className=\"border-collapse\" onKeyDown={onKeyDown}>\n <thead>\n <tr>\n {showWeekNumbers ? (\n <th scope=\"col\" className=\"w-8 text-xs font-normal text-muted-foreground\">\n <span className=\"sr-only\">Week</span>\n </th>\n ) : null}\n {weekdays.map((w) => (\n <th key={w.long} scope=\"col\" abbr={w.long} className=\"h-8 p-0 text-xs font-normal text-muted-foreground\">\n <span aria-hidden=\"true\">{w.short}</span>\n <span className=\"sr-only\">{w.long}</span>\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {Array.from({ length: rows }, (_, r) => {\n const rowStart = addDays(start, r * 7);\n return (\n <tr key={rowStart}>\n {showWeekNumbers ? (\n <th scope=\"row\" className=\"w-8 p-0 text-center text-xs font-normal tabular-nums text-muted-foreground\">\n {isoWeek(addDays(rowStart, (4 - dayOfWeek(rowStart) + 7) % 7))}\n </th>\n ) : null}\n {Array.from({ length: 7 }, (_, c) => Day({ d: addDays(rowStart, c), m }))}\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n );\n }\n\n function Day({ d, m }) {\n const outside = startOfMonth(d) !== m;\n if (outside && !showOutsideDays) return <td key={d} className=\"p-0\" />;\n const off = isDisabled(d);\n const empty = !off && isEmpty(d);\n const picked = mode === \"range\" ? d === range.from || d === range.to : chosen.includes(d);\n // Beside another month, an outside day repeats a day shown there: only\n // the real one carries the band.\n const band = ends && ends[0] < ends[1] && d >= ends[0] && d <= ends[1] && !(outside && n > 1);\n const isToday = d === today;\n const mark = marks[d];\n const tone = mark ? (DOT[mark.tone] ? mark.tone : \"primary\") : null;\n // cx does not merge: one text colour and one weight per day.\n const quiet = !picked && !band && !outside && ((emphasizeMarked && !mark) || empty);\n return (\n <td\n key={d}\n aria-selected={picked || (band && !previewing) || undefined}\n className={cx(\n \"p-0 text-center\",\n band && (previewing ? \"bg-accent/50\" : \"bg-accent\"),\n band && d === ends[0] && \"rounded-l-md\",\n band && d === ends[1] && \"rounded-r-md\",\n )}\n >\n <button\n type=\"button\"\n data-day={d}\n data-outside={outside || undefined}\n tabIndex={!outside && d === tabbable ? 0 : -1}\n aria-label={[fmt.full.format(parseDate(d)), isToday && \"today\", mark?.label].filter(Boolean).join(\", \")}\n aria-current={isToday ? \"date\" : undefined}\n aria-disabled={off || empty || undefined}\n onClick={() => {\n if (off || empty) return;\n pick(d);\n setFocus(d);\n if (!inView(d)) setMonth(startOfMonth(d) < first ? startOfMonth(d) : addMonths(startOfMonth(d), -(n - 1)));\n }}\n onMouseEnter={() => mode === \"range\" && setHover(d)}\n onFocus={() => mode === \"range\" && setHover(d)}\n className={cx(\n \"relative inline-flex items-center justify-center rounded-md text-sm tabular-nums outline-none\",\n \"focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-ring\",\n s.cell,\n picked\n ? \"bg-primary font-medium text-primary-foreground\"\n : band\n ? \"text-accent-foreground hover:bg-accent\"\n : quiet\n ? \"text-muted-foreground\"\n : \"text-foreground\",\n !picked && !band && !empty && \"hover:bg-accent\",\n emphasizeMarked && mark && !picked && !isToday && \"font-semibold\",\n outside && !picked && \"text-muted-foreground opacity-60\",\n outside && picked && \"opacity-70\",\n off && \"cursor-not-allowed line-through opacity-40 hover:bg-transparent\",\n empty && \"cursor-default\",\n // Today: a ring and a heavier number — never colour alone.\n isToday && \"font-semibold ring-1 ring-inset ring-muted-foreground/60\",\n )}\n >\n {fmt.day.format(parseDate(d))}\n {tone ? (\n <span\n aria-hidden=\"true\"\n className={cx(\n \"absolute bottom-1 left-1/2 size-1.5 -translate-x-1/2 rounded-full\",\n picked && (tone === \"primary\" || tone === \"neutral\") ? \"bg-primary-foreground\" : DOT[tone],\n )}\n />\n ) : null}\n </button>\n </td>\n );\n }\n\n return (\n <div\n ref={root}\n role=\"group\"\n aria-label={label}\n onMouseLeave={() => setHover(null)}\n onBlur={(e) => !root.current?.contains(e.relatedTarget) && setHover(null)}\n className={cx(\"relative inline-block max-w-full text-sm\", className)}\n >\n {captionLayout === \"none\" ? null : (\n <>\n <IconButton\n icon=\"chevron-left\"\n label=\"Previous month\"\n size={s.nav}\n disabled={prevOff}\n onClick={() => setMonth(addMonths(first, -1))}\n className=\"absolute left-0 top-0 z-10\"\n />\n <IconButton\n icon=\"chevron-right\"\n label=\"Next month\"\n size={s.nav}\n disabled={nextOff}\n onClick={() => setMonth(addMonths(first, 1))}\n className=\"absolute right-0 top-0 z-10\"\n />\n </>\n )}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:gap-6\">\n {months.map((m, i) => (\n <div key={m}>{Month({ m, i })}</div>\n ))}\n </div>\n </div>\n );\n}\n"
|
|
61
67
|
},
|
|
62
68
|
{
|
|
63
69
|
"path": "templates/next/files/components/ui/carousel.jsx",
|
|
@@ -99,7 +105,7 @@
|
|
|
99
105
|
"path": "templates/next/files/components/ui/date-picker.jsx",
|
|
100
106
|
"type": "registry:ui",
|
|
101
107
|
"target": "@components/makersclaw/ui/date-picker.jsx",
|
|
102
|
-
"content": "\"use client\";\n\nimport { useState, useSyncExternalStore } from \"react\";\nimport { cx } from \"./cx\";\nimport { Icon } from \"./icon\";\nimport { Button, IconButton } from \"./button\";\nimport { Input } from \"./field\";\nimport { Popover } from \"./popover\";\nimport {\n Calendar,\n addDays,\n addMonths,\n endOfMonth,\n formatDate,\n parseDate,\n plainSpaces,\n startOfMonth,\n useLocale,\n useToday,\n} from \"./calendar\";\n\n// DatePicker — a field that reads like an input and opens a Calendar in a\n// Popover: a due date, a report's span. The trigger shows the day in the\n// reader's words (\"Mar 12, 2026\"); the value stays a \"YYYY-MM-DD\" string\n// (calendar.jsx says why). Put it in <Field id> with the same `id`: the\n// label points at the trigger.\n//\n// mode single (value \"2026-03-12\" | null) · range (value\n// { from, to } | null)\n// value, onChange(value) — controlled; or `defaultValue`\n// placeholder \"Pick a date\" · \"Pick a range\"\n// presets true (the defaults for the mode: Today, Tomorrow, In a\n// week… · Today, Last 7 days, This month…) or your own\n// [{ label, value }] — `value` a date, a { from, to }, or\n// (today) → one. A list beside the calendar; a row above\n// it on a phone\n// clearable an × in the field once it holds a date (true)\n// format Intl.DateTimeFormat options for the field ({ dateStyle:\n// \"medium\" }); a range is written with formatRange\n// disabled true: the whole field; a function (date) → true: those days\n// min, max, numberOfMonths (range: 2 from 640px; always 1 on a phone),\n// captionLayout, showWeekNumbers, weekStartsOn, locale, today — as\n// Calendar\n// name a hidden input for a form; a range submits two under it\n// invalid the red edge — pair it with <Field error>\n// size md (44px) · sm (36px)\n// label what a screen reader calls the panel (\"Choose a date\")\n// defaultOpen open on first render (a demo; a form never needs it)\n//\n// It closes when a day, a finished range or a preset is picked; the focus\n// lands on the chosen day when it opens and back on the field when it\n// closes.\n//\n// DateInput below is the typed way in: the browser's own date field\n// (segments in the reader's order, digits only, never a free-text parse)\n// with this Calendar one button away. Typing a date into a text box and\n// parsing it is not robust across locales (\"03/04\" is two different\n// days), which is why there is no text DateInput.\n\nconst FIELD =\n \"flex w-full items-center gap-2 rounded-md border border-input bg-background px-3 text-left text-sm text-foreground \" +\n \"outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 \" +\n \"aria-[invalid=true]:border-destructive\";\nconst HEIGHT = { md: \"min-h-11\", sm: \"min-h-9\" };\n// Presets: on a phone a scrolling row exactly as wide as the calendar (w-0\n// plus a min width of its box: it never widens the panel); a column beside\n// the calendar from 640px.\nconst PRESETS =\n \"-mx-3 flex w-0 min-w-[calc(100%+1.5rem)] gap-1 overflow-x-auto px-3 pb-1 \" +\n \"sm:m-0 sm:w-36 sm:min-w-0 sm:flex-col sm:overflow-visible sm:border-r sm:border-border sm:p-0 sm:pr-3\";\n\nfunction useWide() {\n return useSyncExternalStore(\n (cb) => {\n const q = window.matchMedia(\"(min-width: 640px)\");\n q.addEventListener(\"change\", cb);\n return () => q.removeEventListener(\"change\", cb);\n },\n () => window.matchMedia(\"(min-width: 640px)\").matches,\n () => false,\n );\n}\n\n/** The default presets. Each value is (today) → a date or a { from, to }. */\nexport const DATE_PRESETS = {\n single: [\n { label: \"Today\", value: (t) => t },\n { label: \"Tomorrow\", value: (t) => addDays(t, 1) },\n { label: \"In a week\", value: (t) => addDays(t, 7) },\n { label: \"In a month\", value: (t) => addMonths(t, 1) },\n ],\n range: [\n { label: \"Today\", value: (t) => ({ from: t, to: t }) },\n { label: \"Last 7 days\", value: (t) => ({ from: addDays(t, -6), to: t }) },\n { label: \"Last 30 days\", value: (t) => ({ from: addDays(t, -29), to: t }) },\n { label: \"This month\", value: (t) => ({ from: startOfMonth(t), to: endOfMonth(t) }) },\n { label: \"Last month\", value: (t) => ({ from: startOfMonth(addMonths(t, -1)), to: endOfMonth(addMonths(t, -1)) }) },\n { label: \"This year\", value: (t) => ({ from: `${t.slice(0, 4)}-01-01`, to: `${t.slice(0, 4)}-12-31` }) },\n ],\n};\n\nconst same = (a, b) => (a && b && typeof a === \"object\" ? a.from === b.from && a.to === b.to : a === b);\n\n/** The value in words: a day, or a span through formatRange. */\nfunction words(mode, value, format, locale) {\n if (mode !== \"range\") return value ? formatDate(value, format, locale) : \"\";\n const { from, to } = value ?? {};\n if (!from) return \"\";\n if (!to) return `${formatDate(from, format, locale)} – …`;\n const f = new Intl.DateTimeFormat(locale, { ...format, timeZone: \"UTC\" });\n return plainSpaces(f.formatRange ? f.formatRange(parseDate(from), parseDate(to)) : `${f.format(parseDate(from))} – ${f.format(parseDate(to))}`);\n}\n\nexport function DatePicker({\n id,\n mode = \"single\",\n value: valueProp,\n defaultValue = null,\n onChange,\n placeholder,\n presets = false,\n clearable = true,\n format = { dateStyle: \"medium\" },\n disabled,\n min,\n max,\n numberOfMonths,\n captionLayout,\n showWeekNumbers,\n weekStartsOn,\n locale: localeProp,\n today: todayProp,\n name,\n invalid,\n size = \"md\",\n align = \"start\",\n label,\n defaultOpen = false,\n className,\n}) {\n const locale = useLocale(localeProp);\n const today = useToday(todayProp);\n const wide = useWide();\n const [own, setOwn] = useState(defaultValue);\n const value = valueProp !== undefined ? valueProp : own;\n const [open, setOpen] = useState(defaultOpen);\n const [month, setMonth] = useState(null);\n const range = mode === \"range\";\n const off = disabled === true;\n\n const first = range ? value?.from : value;\n const text = words(mode, value, format, locale);\n const list = presets === true ? DATE_PRESETS[range ? \"range\" : \"single\"] : Array.isArray(presets) ? presets : [];\n const shown = list.map((p) => ({ label: p.label, value: typeof p.value === \"function\" ? (today ? p.value(today) : null) : p.value }));\n\n function set(v) {\n if (valueProp === undefined) setOwn(v);\n onChange?.(v);\n }\n\n // Each opening starts on the chosen day's month (or today's).\n function onOpenChange(next) {\n if (next) setMonth(startOfMonth(first) ?? startOfMonth(today));\n setOpen(next);\n }\n\n function choose(v) {\n set(v);\n if (!range || (v?.from && v?.to)) setOpen(false);\n }\n\n const months = wide ? numberOfMonths ?? (range ? 2 : 1) : 1;\n const showClear = clearable && !off && (range ? !!value?.from : !!value);\n\n return (\n <div className={cx(\"relative w-full\", className)}>\n <Popover\n open={open}\n onOpenChange={onOpenChange}\n align={align}\n width=\"auto\"\n className=\"p-3\"\n label={label ?? (range ? \"Choose a date range\" : \"Choose a date\")}\n trigger={(props) => (\n <button\n {...props}\n id={id}\n type=\"button\"\n disabled={off}\n aria-invalid={invalid ? true : undefined}\n className={cx(FIELD, HEIGHT[size] ?? HEIGHT.md, showClear && \"pr-11\")}\n >\n <Icon name=\"calendar\" className=\"shrink-0 text-muted-foreground\" />\n <span className={cx(\"min-w-0 flex-1 truncate\", !text && \"text-muted-foreground\")}>\n {text || placeholder || (range ? \"Pick a range\" : \"Pick a date\")}\n </span>\n </button>\n )}\n >\n <div className=\"flex flex-col gap-3 sm:flex-row\">\n {shown.length ? (\n <div\n role=\"group\"\n aria-label=\"Presets\"\n className={PRESETS}\n >\n {shown.map((p) => {\n const on = !!p.value && same(p.value, value);\n return (\n <Button\n key={p.label}\n size={wide ? \"sm\" : \"md\"}\n variant={on ? \"secondary\" : \"quiet\"}\n aria-pressed={on}\n disabled={!p.value}\n className=\"shrink-0 justify-start\"\n onClick={() => {\n choose(p.value);\n setMonth(startOfMonth(range ? p.value.from : p.value));\n }}\n >\n {p.label}\n </Button>\n );\n })}\n </div>\n ) : null}\n <Calendar\n mode={range ? \"range\" : \"single\"}\n value={value}\n onChange={choose}\n month={month ?? undefined}\n onMonthChange={(m) => setMonth(`${m}-01`)}\n numberOfMonths={months}\n min={min}\n max={max}\n disabled={typeof disabled === \"function\" ? disabled : undefined}\n captionLayout={captionLayout}\n showWeekNumbers={showWeekNumbers}\n weekStartsOn={weekStartsOn}\n locale={locale}\n today={today ?? undefined}\n autoFocus={open}\n label={label ?? (range ? \"Choose a date range\" : \"Choose a date\")}\n />\n </div>\n </Popover>\n {showClear ? (\n <IconButton\n icon=\"x\"\n label=\"Clear date\"\n size=\"sm\"\n onClick={() => set(null)}\n className={cx(\"absolute right-1\", size === \"sm\" ? \"top-0\" : \"top-1\")}\n />\n ) : null}\n {name && !range ? <input type=\"hidden\" name={name} value={value ?? \"\"} /> : null}\n {name && range ? (\n <>\n <input type=\"hidden\" name={name} value={value?.from ?? \"\"} />\n <input type=\"hidden\" name={name} value={value?.to ?? \"\"} />\n </>\n ) : null}\n </div>\n );\n}\n\n/**\n * A date typed or picked. The browser's own date field — segments in the\n * reader's order, digits only, the value a \"YYYY-MM-DD\" string or \"\" while\n * incomplete — with its built-in picker swapped for a button that opens\n * this Calendar. onChange(date | null). Chrome and Safari hide their own\n * picker icon; Firefox keeps its own beside ours.\n *\n * value, onChange(date | null), defaultValue, min, max, disabled\n * (true, or a function for the Calendar's days), name, invalid, size,\n * locale (the Calendar's; the field itself follows the browser), today\n */\nexport function DateInput({\n id,\n value: valueProp,\n defaultValue = null,\n onChange,\n min,\n max,\n disabled,\n name,\n invalid,\n size = \"md\",\n locale,\n today,\n weekStartsOn,\n className,\n ...rest\n}) {\n const [own, setOwn] = useState(defaultValue);\n const value = valueProp !== undefined ? valueProp : own;\n const [open, setOpen] = useState(false);\n const [month, setMonth] = useState(null);\n const off = disabled === true;\n const out = !!value && ((min && value < min) || (max && value > max));\n\n function set(v) {\n if (valueProp === undefined) setOwn(v);\n onChange?.(v);\n }\n\n return (\n <div className={cx(\"relative w-full\", className)}>\n <Input\n id={id}\n type=\"date\"\n name={name}\n value={value ?? \"\"}\n min={min}\n max={max}\n disabled={off}\n invalid={invalid || out}\n onChange={(e) => set(e.target.value || null)}\n className={cx(\n \"pr-12 [&::-webkit-calendar-picker-indicator]:hidden tabular-nums\",\n size === \"sm\" && \"min-h-9\",\n )}\n {...rest}\n />\n <span className={cx(\"absolute right-1\", size === \"sm\" ? \"top-0\" : \"top-1\")}>\n <Popover\n open={open}\n onOpenChange={(next) => {\n if (next) setMonth(startOfMonth(value) ?? null);\n setOpen(next);\n }}\n align=\"end\"\n width=\"auto\"\n className=\"p-3\"\n label=\"Choose a date\"\n trigger={(props) => <IconButton {...props} icon=\"calendar\" label=\"Open calendar\" size=\"sm\" disabled={off} />}\n >\n <Calendar\n value={value}\n onChange={(v) => {\n set(v);\n setOpen(false);\n }}\n month={month ?? undefined}\n onMonthChange={(m) => setMonth(`${m}-01`)}\n min={min}\n max={max}\n disabled={typeof disabled === \"function\" ? disabled : undefined}\n locale={locale}\n today={today}\n weekStartsOn={weekStartsOn}\n autoFocus={open}\n label=\"Choose a date\"\n />\n </Popover>\n </span>\n </div>\n );\n}\n"
|
|
108
|
+
"content": "\"use client\";\n\nimport { useState, useSyncExternalStore } from \"react\";\nimport { cx } from \"./cx\";\nimport { Icon } from \"./icon\";\nimport { Button, IconButton } from \"./button\";\nimport { Input } from \"./field\";\nimport { Popover } from \"./popover\";\nimport {\n Calendar,\n addDays,\n addMonths,\n endOfMonth,\n formatDate,\n parseDate,\n plainSpaces,\n startOfMonth,\n useLocale,\n useToday,\n} from \"./calendar\";\n\n// DatePicker — a field that reads like an input and opens a Calendar in a\n// Popover: a due date, a report's span. The trigger shows the day in the\n// reader's words (\"Mar 12, 2026\"); the value stays a \"YYYY-MM-DD\" string\n// (calendar.jsx says why). Put it in <Field id> with the same `id`: the\n// label points at the trigger.\n//\n// mode single (value \"2026-03-12\" | null) · range (value\n// { from, to } | null)\n// value, onChange(value) — controlled; or `defaultValue`\n// placeholder \"Pick a date\" · \"Pick a range\"\n// presets true (the defaults for the mode: Today, Tomorrow, In a\n// week… · Today, Last 7 days, This month…) or your own\n// [{ label, value }] — `value` a date, a { from, to }, or\n// (today) → one. A list beside the calendar; a row above\n// it on a phone\n// clearable an × in the field once it holds a date (true)\n// format Intl.DateTimeFormat options for the field ({ dateStyle:\n// \"medium\" }); a range is written with formatRange\n// disabled true: the whole field; a function (date) → true: those days\n// min, max, numberOfMonths (range: 2 from 640px; always 1 on a phone),\n// captionLayout, showWeekNumbers, weekStartsOn, locale, today — as\n// Calendar\n// name a hidden input for a form; a range submits two under it\n// invalid the red edge — pair it with <Field error>\n// size md (44px) · sm (36px)\n// label what a screen reader calls the panel (\"Choose a date\")\n// defaultOpen open on first render (a demo; a form never needs it)\n//\n// It closes when a day, a finished range or a preset is picked; the focus\n// lands on the chosen day when it opens and back on the field when it\n// closes.\n//\n// DateInput below is the typed way in: the browser's own date field\n// (segments in the reader's order, digits only, never a free-text parse)\n// with this Calendar one button away. Typing a date into a text box and\n// parsing it is not robust across locales (\"03/04\" is two different\n// days), which is why there is no text DateInput.\n\nconst FIELD =\n \"flex w-full items-center gap-2 rounded-md border border-input bg-background px-3 text-left text-sm text-foreground \" +\n \"outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 \" +\n \"aria-[invalid=true]:border-destructive\";\nconst HEIGHT = { md: \"min-h-11\", sm: \"min-h-9\" };\n// Presets: on a phone a scrolling row exactly as wide as the calendar (w-0\n// plus a min width of its box: it never widens the panel); a column beside\n// the calendar from 640px.\nconst PRESETS =\n \"-mx-3 flex w-0 min-w-[calc(100%+1.5rem)] gap-1 overflow-x-auto px-3 pb-1 \" +\n \"sm:m-0 sm:w-36 sm:min-w-0 sm:flex-col sm:overflow-visible sm:border-r sm:border-border sm:p-0 sm:pr-3\";\n\nfunction useWide() {\n return useSyncExternalStore(\n (cb) => {\n const q = window.matchMedia(\"(min-width: 640px)\");\n q.addEventListener(\"change\", cb);\n return () => q.removeEventListener(\"change\", cb);\n },\n () => window.matchMedia(\"(min-width: 640px)\").matches,\n () => false,\n );\n}\n\n/** The default presets. Each value is (today) → a date or a { from, to }. */\nexport const DATE_PRESETS = {\n single: [\n { label: \"Today\", value: (t) => t },\n { label: \"Tomorrow\", value: (t) => addDays(t, 1) },\n { label: \"In a week\", value: (t) => addDays(t, 7) },\n { label: \"In a month\", value: (t) => addMonths(t, 1) },\n ],\n range: [\n { label: \"Today\", value: (t) => ({ from: t, to: t }) },\n { label: \"Last 7 days\", value: (t) => ({ from: addDays(t, -6), to: t }) },\n { label: \"Last 30 days\", value: (t) => ({ from: addDays(t, -29), to: t }) },\n { label: \"This month\", value: (t) => ({ from: startOfMonth(t), to: endOfMonth(t) }) },\n { label: \"Last month\", value: (t) => ({ from: startOfMonth(addMonths(t, -1)), to: endOfMonth(addMonths(t, -1)) }) },\n { label: \"This year\", value: (t) => ({ from: `${t.slice(0, 4)}-01-01`, to: `${t.slice(0, 4)}-12-31` }) },\n ],\n};\n\nconst same = (a, b) => (a && b && typeof a === \"object\" ? a.from === b.from && a.to === b.to : a === b);\n\n/** The value in words: a day, or a span through formatRange. */\nfunction words(mode, value, format, locale) {\n if (mode !== \"range\") return value ? formatDate(value, format, locale) : \"\";\n const { from, to } = value ?? {};\n if (!from) return \"\";\n if (!to) return `${formatDate(from, format, locale)} – …`;\n const f = new Intl.DateTimeFormat(locale, { ...format, timeZone: \"UTC\" });\n return plainSpaces(f.formatRange ? f.formatRange(parseDate(from), parseDate(to)) : `${f.format(parseDate(from))} – ${f.format(parseDate(to))}`);\n}\n\nexport function DatePicker({\n id,\n mode = \"single\",\n value: valueProp,\n defaultValue = null,\n onChange,\n placeholder,\n presets = false,\n clearable = true,\n format = { dateStyle: \"medium\" },\n disabled,\n min,\n max,\n numberOfMonths,\n captionLayout,\n showWeekNumbers,\n weekStartsOn,\n locale: localeProp,\n today: todayProp,\n name,\n invalid,\n size = \"md\",\n align = \"start\",\n label,\n defaultOpen = false,\n className,\n}) {\n const locale = useLocale(localeProp);\n const today = useToday(todayProp);\n const wide = useWide();\n const [own, setOwn] = useState(defaultValue);\n const value = valueProp !== undefined ? valueProp : own;\n const [open, setOpen] = useState(defaultOpen);\n const [month, setMonth] = useState(null);\n const range = mode === \"range\";\n const off = disabled === true;\n\n const first = range ? value?.from : value;\n const text = words(mode, value, format, locale);\n const list = presets === true ? DATE_PRESETS[range ? \"range\" : \"single\"] : Array.isArray(presets) ? presets : [];\n const shown = list.map((p) => ({ label: p.label, value: typeof p.value === \"function\" ? (today ? p.value(today) : null) : p.value }));\n\n function set(v) {\n if (valueProp === undefined) setOwn(v);\n onChange?.(v);\n }\n\n // Each opening starts on the chosen day's month (or today's).\n function onOpenChange(next) {\n if (next) setMonth(startOfMonth(first) ?? startOfMonth(today));\n setOpen(next);\n }\n\n function choose(v) {\n set(v);\n if (!range || (v?.from && v?.to)) setOpen(false);\n }\n\n const months = wide ? numberOfMonths ?? (range ? 2 : 1) : 1;\n const showClear = clearable && !off && (range ? !!value?.from : !!value);\n\n return (\n <div className={cx(\"relative w-full\", className)}>\n <Popover\n open={open}\n onOpenChange={onOpenChange}\n align={align}\n width=\"auto\"\n className=\"p-3\"\n label={label ?? (range ? \"Choose a date range\" : \"Choose a date\")}\n trigger={(props) => (\n <button\n {...props}\n id={id}\n type=\"button\"\n disabled={off}\n aria-invalid={invalid ? true : undefined}\n className={cx(FIELD, HEIGHT[size] ?? HEIGHT.md, showClear && \"pr-11\")}\n >\n <Icon name=\"calendar\" className=\"shrink-0 text-muted-foreground\" />\n <span className={cx(\"min-w-0 flex-1 truncate\", !text && \"text-muted-foreground\")}>\n {text || placeholder || (range ? \"Pick a range\" : \"Pick a date\")}\n </span>\n </button>\n )}\n >\n <div className=\"flex flex-col gap-3 sm:flex-row\">\n {shown.length ? (\n <div\n role=\"group\"\n aria-label=\"Presets\"\n className={PRESETS}\n >\n {shown.map((p) => {\n const on = !!p.value && same(p.value, value);\n return (\n <Button\n key={p.label}\n size={wide ? \"sm\" : \"md\"}\n variant={on ? \"secondary\" : \"quiet\"}\n aria-pressed={on}\n disabled={!p.value}\n className=\"shrink-0 justify-start\"\n onClick={() => {\n choose(p.value);\n setMonth(startOfMonth(range ? p.value.from : p.value));\n }}\n >\n {p.label}\n </Button>\n );\n })}\n </div>\n ) : null}\n <Calendar\n mode={range ? \"range\" : \"single\"}\n value={value}\n onChange={choose}\n month={month ?? undefined}\n onMonthChange={(m) => setMonth(`${m}-01`)}\n numberOfMonths={months}\n min={min}\n max={max}\n disabled={typeof disabled === \"function\" ? disabled : undefined}\n captionLayout={captionLayout}\n showWeekNumbers={showWeekNumbers}\n weekStartsOn={weekStartsOn}\n locale={locale}\n today={today ?? undefined}\n autoFocus={open}\n label={label ?? (range ? \"Choose a date range\" : \"Choose a date\")}\n />\n </div>\n </Popover>\n {showClear ? (\n <IconButton\n icon=\"x\"\n label=\"Clear date\"\n size=\"sm\"\n onClick={() => set(null)}\n className={cx(\"absolute right-1\", size === \"sm\" ? \"top-0\" : \"top-1\")}\n />\n ) : null}\n {name && !range ? <input type=\"hidden\" name={name} value={value ?? \"\"} /> : null}\n {name && range ? (\n <>\n <input type=\"hidden\" name={name} value={value?.from ?? \"\"} />\n <input type=\"hidden\" name={name} value={value?.to ?? \"\"} />\n </>\n ) : null}\n </div>\n );\n}\n\n/**\n * A date typed or picked. The browser's own date field — segments in the\n * reader's order, digits only, the value a \"YYYY-MM-DD\" string or \"\" while\n * incomplete — with its built-in picker swapped for a button that opens\n * this Calendar. onChange(date | null). Chrome and Safari hide their own\n * picker icon; Firefox keeps its own beside ours.\n *\n * value, onChange(date | null), defaultValue, min, max, disabled\n * (true, or a function for the Calendar's days), name, invalid, size,\n * locale (the Calendar's; the field itself follows the browser), today\n * clearable an × beside the calendar button once the field holds a\n * date (false — DatePicker's is true)\n *\n * Every other prop goes onto the <input>, so a prop this part names must\n * be taken out here: until 0.3.3 `clearable` was DatePicker's alone, and\n * a surface that wrote <DateInput clearable> put it on the <input> (\"Received\n * `true` for a non-boolean attribute clearable\", a podcast guest sheet,\n * 2026-09-25). It is DateInput's now too, off unless asked, so a field\n * that did not ask keeps its width.\n */\nexport function DateInput({\n id,\n value: valueProp,\n defaultValue = null,\n onChange,\n min,\n max,\n disabled,\n name,\n invalid,\n size = \"md\",\n locale,\n today,\n weekStartsOn,\n clearable = false,\n className,\n ...rest\n}) {\n const [own, setOwn] = useState(defaultValue);\n const value = valueProp !== undefined ? valueProp : own;\n const [open, setOpen] = useState(false);\n const [month, setMonth] = useState(null);\n const off = disabled === true;\n const out = !!value && ((min && value < min) || (max && value > max));\n const showClear = clearable && !off && !!value;\n\n function set(v) {\n if (valueProp === undefined) setOwn(v);\n onChange?.(v);\n }\n\n return (\n <div className={cx(\"relative w-full\", className)}>\n <Input\n id={id}\n type=\"date\"\n name={name}\n value={value ?? \"\"}\n min={min}\n max={max}\n disabled={off}\n invalid={invalid || out}\n onChange={(e) => set(e.target.value || null)}\n className={cx(\n showClear ? \"pr-20\" : \"pr-12\",\n \"[&::-webkit-calendar-picker-indicator]:hidden tabular-nums\",\n size === \"sm\" && \"min-h-9\",\n )}\n {...rest}\n />\n <span className={cx(\"absolute right-1 flex\", size === \"sm\" ? \"top-0\" : \"top-1\")}>\n {showClear ? <IconButton icon=\"x\" label=\"Clear date\" size=\"sm\" onClick={() => set(null)} /> : null}\n <Popover\n open={open}\n onOpenChange={(next) => {\n if (next) setMonth(startOfMonth(value) ?? null);\n setOpen(next);\n }}\n align=\"end\"\n width=\"auto\"\n className=\"p-3\"\n label=\"Choose a date\"\n trigger={(props) => <IconButton {...props} icon=\"calendar\" label=\"Open calendar\" size=\"sm\" disabled={off} />}\n >\n <Calendar\n value={value}\n onChange={(v) => {\n set(v);\n setOpen(false);\n }}\n month={month ?? undefined}\n onMonthChange={(m) => setMonth(`${m}-01`)}\n min={min}\n max={max}\n disabled={typeof disabled === \"function\" ? disabled : undefined}\n locale={locale}\n today={today}\n weekStartsOn={weekStartsOn}\n autoFocus={open}\n label=\"Choose a date\"\n />\n </Popover>\n </span>\n </div>\n );\n}\n"
|
|
103
109
|
},
|
|
104
110
|
{
|
|
105
111
|
"path": "templates/next/files/components/ui/depiction.jsx",
|
|
@@ -123,7 +129,7 @@
|
|
|
123
129
|
"path": "templates/next/files/components/ui/field.jsx",
|
|
124
130
|
"type": "registry:ui",
|
|
125
131
|
"target": "@components/makersclaw/ui/field.jsx",
|
|
126
|
-
"content": "\"use client\";\n\nimport { useCallback, useLayoutEffect, useRef } from \"react\";\nimport { cx } from \"./cx\";\n\n// Form parts. Each is the native element with the workspace's look and a\n// 44px target; the browser supplies the behaviour (focus, keyboard,\n// validation, autofill). Wrap one in <Field> for its label, hint and\n// error — never a bare <input> with a <div> label beside it.\n\nconst CONTROL =\n \"w-full min-h-11 rounded-md border border-input bg-background px-3 text-sm text-foreground \" +\n \"placeholder:text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring \" +\n \"disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid=true]:border-destructive\";\n\n/** Label, control, hint and error in one stack. Pass `id`; the label\n * points at it. `error` (a string) marks the control invalid. */\nexport function Field({ id, label, hint, error, required, className, children }) {\n return (\n <div className={cx(\"grid gap-1.5\", className)}>\n {label ? (\n <label htmlFor={id} className=\"text-sm font-medium text-foreground\">\n {label}\n {required ? <span aria-hidden=\"true\" className=\"text-destructive\"> *</span> : null}\n </label>\n ) : null}\n {children}\n {error ? (\n <p id={id ? `${id}-error` : undefined} role=\"alert\" className=\"text-xs text-destructive\">\n {error}\n </p>\n ) : hint ? (\n <p id={id ? `${id}-hint` : undefined} className=\"text-xs text-muted-foreground\">\n {hint}\n </p>\n ) : null}\n </div>\n );\n}\n\nexport function Input({ className, invalid, ...rest }) {\n return <input className={cx(CONTROL, className)} aria-invalid={invalid ? true : undefined} {...rest} />;\n}\n\n/**\n * A time of day: the native <input type=\"time\"> (the browser's segments,\n * keyboard and picker; `value` is \"HH:MM\", or \"HH:MM:SS\" with `step` under\n * 60), in the workspace's look. The browser's own clock button is drawn\n * by the engine in its own grey and ignores the theme, so it is laid\n * invisibly over the kit's clock glyph: the glyph is what shows, the\n * native button is what a press opens. min, max, step, required and every\n * other prop go to the input.\n */\nexport function TimeInput({ className, invalid, ...rest }) {\n return (\n <span className=\"relative block\">\n <input\n type=\"time\"\n className={cx(\n CONTROL,\n \"appearance-none pr-10 tabular-nums [&::-webkit-date-and-time-value]:text-left\",\n \"[&::-webkit-calendar-picker-indicator]:absolute [&::-webkit-calendar-picker-indicator]:right-3 [&::-webkit-calendar-picker-indicator]:top-1/2\",\n \"[&::-webkit-calendar-picker-indicator]:size-5 [&::-webkit-calendar-picker-indicator]:-translate-y-1/2 [&::-webkit-calendar-picker-indicator]:m-0\",\n \"[&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-0\",\n className,\n )}\n aria-invalid={invalid ? true : undefined}\n {...rest}\n />\n <svg\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.8\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 4v5l3 3\" />\n </svg>\n </span>\n );\n}\n\n/**\n * Keeps a <textarea> as tall as its text, between `minRows` and `maxRows`\n * lines; past `maxRows` it scrolls. Returns the ref callback to put on the\n * textarea (it also sets `outer`, a ref of the caller's). It measures on\n * every value change, typed or set (a composer clearing after send), and\n * when the width changes the wrapping. Textarea autoGrow and InputGroup\n * multiline use it; it is not a surface's tool.\n */\nexport function useAutoGrow({ enabled = true, minRows = 1, maxRows = 8, value, outer }) {\n const node = useRef(null);\n\n const fit = useCallback(() => {\n const el = node.current;\n if (!el || !enabled) return;\n const cs = getComputedStyle(el);\n const line = parseFloat(cs.lineHeight) || parseFloat(cs.fontSize) * 1.5 || 20;\n const frame =\n parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom) +\n (cs.boxSizing === \"border-box\" ? parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth) : 0);\n const low = line * Math.max(1, minRows) + frame;\n const high = Number.isFinite(maxRows) && maxRows > 0 ? line * Math.max(minRows, maxRows) + frame : Infinity;\n el.style.height = \"auto\";\n const content = el.scrollHeight + (cs.boxSizing === \"border-box\" ? parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth) : 0);\n el.style.height = `${Math.min(Math.max(content, low), high)}px`;\n el.style.overflowY = content > high ? \"auto\" : \"hidden\";\n }, [enabled, minRows, maxRows]);\n\n useLayoutEffect(fit, [fit, value]);\n\n // A width change re-wraps the text; a height change is our own doing.\n useLayoutEffect(() => {\n const el = node.current;\n if (!el || !enabled || typeof ResizeObserver === \"undefined\") return;\n let width = el.clientWidth;\n const ro = new ResizeObserver(() => {\n if (el.clientWidth === width) return;\n width = el.clientWidth;\n fit();\n });\n ro.observe(el);\n return () => ro.disconnect();\n }, [enabled, fit]);\n\n const ref = useCallback(\n (el) => {\n node.current = el;\n if (typeof outer === \"function\") return outer(el);\n if (outer) outer.current = el;\n },\n [outer],\n );\n return { ref, fit };\n}\n\n/**\n * The native <textarea>. `rows` sets its height (default 4) and it\n * resizes by its corner. `autoGrow` makes it as tall as its text instead:\n * from `minRows` (default 1) to `maxRows` (default 8) lines, then it\n * scrolls; no resize corner.\n */\nexport function Textarea({ className, invalid, rows = 4, autoGrow = false, minRows = 1, maxRows = 8, onInput, ref, ...rest }) {\n const grow = useAutoGrow({ enabled: autoGrow, minRows, maxRows, value: rest.value, outer: ref });\n return (\n <textarea\n ref={grow.ref}\n rows={autoGrow ? minRows : rows}\n className={cx(CONTROL, autoGrow ? \"resize-none py-2.5 leading-normal\" : \"min-h-24 resize-y py-2.5 leading-normal\", className)}\n aria-invalid={invalid ? true : undefined}\n onInput={(e) => {\n if (autoGrow) grow.fit();\n onInput?.(e);\n }}\n {...rest}\n />\n );\n}\n\n/**\n * The native <select>, styled. `options` is [{ value, label }]; children\n * work too. A select with nothing to choose from is not a control — when\n * `options` is empty it renders `empty` (a short muted line, or nothing\n * when `empty` is null) rather than a disabled dropdown. For \"pick a\n * saved record or make a new one\" use RecordSwitcher (components/parts).\n */\nexport function Select({ options, empty = \"Nothing to choose yet.\", className, invalid, children, ...rest }) {\n if (options && options.length === 0 && !children) {\n return empty ? <p className=\"text-sm text-muted-foreground\">{empty}</p> : null;\n }\n return (\n <span className=\"relative block\">\n <select\n className={cx(CONTROL, \"appearance-none pr-9\", className)}\n aria-invalid={invalid ? true : undefined}\n {...rest}\n >\n {options ? options.map((o) => <option key={o.value} value={o.value}>{o.label}</option>) : children}\n </select>\n <svg\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.8\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </span>\n );\n}\n\n/** An on/off. A checkbox underneath (role=switch), so forms, labels and\n * keyboards work; the track is drawn from tokens. */\nexport function Switch({ id, checked, onChange, label, disabled, className }) {\n return (\n <label htmlFor={id} className={cx(\"inline-flex min-h-11 cursor-pointer items-center gap-3 text-sm\", disabled && \"cursor-not-allowed opacity-50\", className)}>\n <span className=\"relative inline-flex h-6 w-10 shrink-0\">\n <input\n id={id}\n type=\"checkbox\"\n role=\"switch\"\n className=\"peer sr-only\"\n checked={checked}\n onChange={onChange}\n disabled={disabled}\n />\n <span className=\"absolute inset-0 rounded-full border border-input bg-muted transition-colors peer-checked:bg-primary peer-focus-visible:ring-2 peer-focus-visible:ring-ring\" />\n <span className=\"absolute left-0.5 top-0.5 h-5 w-5 rounded-full bg-foreground transition-transform peer-checked:translate-x-4 peer-checked:bg-primary-foreground\" />\n </span>\n {label ? <span>{label}</span> : null}\n </label>\n );\n}\n\nexport function Checkbox({ id, label, className, ...rest }) {\n return (\n <label htmlFor={id} className={cx(\"inline-flex min-h-11 cursor-pointer items-center gap-2 text-sm\", className)}>\n <input\n id={id}\n type=\"checkbox\"\n className=\"h-4 w-4 rounded-sm border-input accent-[var(--color-primary)] outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n {...rest}\n />\n {label ? <span>{label}</span> : null}\n </label>\n );\n}\n"
|
|
132
|
+
"content": "\"use client\";\n\nimport { useCallback, useLayoutEffect, useRef } from \"react\";\nimport { cx } from \"./cx\";\n\n// Form parts. Each is the native element with the workspace's look and a\n// 44px target; the browser supplies the behaviour (focus, keyboard,\n// validation, autofill). Wrap one in <Field> for its label, hint and\n// error — never a bare <input> with a <div> label beside it.\n\nconst BOX =\n \"min-h-11 rounded-md border border-input bg-background px-3 text-sm text-foreground \" +\n \"placeholder:text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring \" +\n \"disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid=true]:border-destructive\";\nconst CONTROL = `w-full ${BOX}`;\n\n// An Input is as wide as its column unless the caller's className gives\n// it a width (w-24, w-auto, w-[12rem], size-10), which then stands in\n// for w-full. cx does not merge classes: with both, the rule later in the\n// stylesheet wins, and Tailwind writes w-full after w-24, w-auto, w-fit\n// and w-[…] — until 0.5.1 those did nothing on an Input. A width under a\n// breakpoint (sm:w-40) keeps w-full below it.\nconst ownWidth = (className) => /(?:^|\\s)!?(?:w|size)-/.test(className ?? \"\");\n\n/** Label, control, hint and error in one stack. Pass `id`; the label\n * points at it. `error` (a string) marks the control invalid. */\nexport function Field({ id, label, hint, error, required, className, children }) {\n return (\n <div className={cx(\"grid gap-1.5\", className)}>\n {label ? (\n <label htmlFor={id} className=\"text-sm font-medium text-foreground\">\n {label}\n {required ? <span aria-hidden=\"true\" className=\"text-destructive\"> *</span> : null}\n </label>\n ) : null}\n {children}\n {error ? (\n <p id={id ? `${id}-error` : undefined} role=\"alert\" className=\"text-xs text-destructive\">\n {error}\n </p>\n ) : hint ? (\n <p id={id ? `${id}-hint` : undefined} className=\"text-xs text-muted-foreground\">\n {hint}\n </p>\n ) : null}\n </div>\n );\n}\n\nexport function Input({ className, invalid, ...rest }) {\n return <input className={cx(!ownWidth(className) && \"w-full\", BOX, className)} aria-invalid={invalid ? true : undefined} {...rest} />;\n}\n\n/**\n * A time of day: the native <input type=\"time\"> (the browser's segments,\n * keyboard and picker; `value` is \"HH:MM\", or \"HH:MM:SS\" with `step` under\n * 60), in the workspace's look. The browser's own clock button is drawn\n * by the engine in its own grey and ignores the theme, so it is laid\n * invisibly over the kit's clock glyph: the glyph is what shows, the\n * native button is what a press opens. min, max, step, required and every\n * other prop go to the input.\n */\nexport function TimeInput({ className, invalid, ...rest }) {\n return (\n <span className=\"relative block\">\n <input\n type=\"time\"\n className={cx(\n CONTROL,\n \"appearance-none pr-10 tabular-nums [&::-webkit-date-and-time-value]:text-left\",\n \"[&::-webkit-calendar-picker-indicator]:absolute [&::-webkit-calendar-picker-indicator]:right-3 [&::-webkit-calendar-picker-indicator]:top-1/2\",\n \"[&::-webkit-calendar-picker-indicator]:size-5 [&::-webkit-calendar-picker-indicator]:-translate-y-1/2 [&::-webkit-calendar-picker-indicator]:m-0\",\n \"[&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-0\",\n className,\n )}\n aria-invalid={invalid ? true : undefined}\n {...rest}\n />\n <svg\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.8\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 4v5l3 3\" />\n </svg>\n </span>\n );\n}\n\n/**\n * Keeps a <textarea> as tall as its text, between `minRows` and `maxRows`\n * lines; past `maxRows` it scrolls. Returns the ref callback to put on the\n * textarea (it also sets `outer`, a ref of the caller's). It measures on\n * every value change, typed or set (a composer clearing after send), and\n * when the width changes the wrapping. Textarea autoGrow and InputGroup\n * multiline use it; it is not a surface's tool.\n */\nexport function useAutoGrow({ enabled = true, minRows = 1, maxRows = 8, value, outer }) {\n const node = useRef(null);\n\n const fit = useCallback(() => {\n const el = node.current;\n if (!el || !enabled) return;\n const cs = getComputedStyle(el);\n const line = parseFloat(cs.lineHeight) || parseFloat(cs.fontSize) * 1.5 || 20;\n const frame =\n parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom) +\n (cs.boxSizing === \"border-box\" ? parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth) : 0);\n const low = line * Math.max(1, minRows) + frame;\n const high = Number.isFinite(maxRows) && maxRows > 0 ? line * Math.max(minRows, maxRows) + frame : Infinity;\n el.style.height = \"auto\";\n const content = el.scrollHeight + (cs.boxSizing === \"border-box\" ? parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth) : 0);\n el.style.height = `${Math.min(Math.max(content, low), high)}px`;\n el.style.overflowY = content > high ? \"auto\" : \"hidden\";\n }, [enabled, minRows, maxRows]);\n\n useLayoutEffect(fit, [fit, value]);\n\n // A width change re-wraps the text; a height change is our own doing.\n useLayoutEffect(() => {\n const el = node.current;\n if (!el || !enabled || typeof ResizeObserver === \"undefined\") return;\n let width = el.clientWidth;\n const ro = new ResizeObserver(() => {\n if (el.clientWidth === width) return;\n width = el.clientWidth;\n fit();\n });\n ro.observe(el);\n return () => ro.disconnect();\n }, [enabled, fit]);\n\n const ref = useCallback(\n (el) => {\n node.current = el;\n if (typeof outer === \"function\") return outer(el);\n if (outer) outer.current = el;\n },\n [outer],\n );\n return { ref, fit };\n}\n\n/**\n * The native <textarea>. `rows` sets its height (default 4) and it\n * resizes by its corner. `autoGrow` makes it as tall as its text instead:\n * from `minRows` (default 1) to `maxRows` (default 8) lines, then it\n * scrolls; no resize corner.\n */\nexport function Textarea({ className, invalid, rows = 4, autoGrow = false, minRows = 1, maxRows = 8, onInput, ref, ...rest }) {\n const grow = useAutoGrow({ enabled: autoGrow, minRows, maxRows, value: rest.value, outer: ref });\n return (\n <textarea\n ref={grow.ref}\n rows={autoGrow ? minRows : rows}\n className={cx(CONTROL, autoGrow ? \"resize-none py-2.5 leading-normal\" : \"min-h-24 resize-y py-2.5 leading-normal\", className)}\n aria-invalid={invalid ? true : undefined}\n onInput={(e) => {\n if (autoGrow) grow.fit();\n onInput?.(e);\n }}\n {...rest}\n />\n );\n}\n\n/**\n * The native <select>, styled. `options` is [{ value, label }]; children\n * work too. A select with nothing to choose from is not a control — when\n * `options` is empty it renders `empty` (a short muted line, or nothing\n * when `empty` is null) rather than a disabled dropdown. For \"pick a\n * saved record or make a new one\" use RecordSwitcher (components/parts).\n */\nexport function Select({ options, empty = \"Nothing to choose yet.\", className, invalid, children, ...rest }) {\n if (options && options.length === 0 && !children) {\n return empty ? <p className=\"text-sm text-muted-foreground\">{empty}</p> : null;\n }\n return (\n <span className=\"relative block\">\n <select\n className={cx(CONTROL, \"appearance-none pr-9\", className)}\n aria-invalid={invalid ? true : undefined}\n {...rest}\n >\n {options ? options.map((o) => <option key={o.value} value={o.value}>{o.label}</option>) : children}\n </select>\n <svg\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.8\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </span>\n );\n}\n\n/** An on/off. A checkbox underneath (role=switch), so forms, labels and\n * keyboards work; the track is drawn from tokens. */\nexport function Switch({ id, checked, onChange, label, disabled, className }) {\n return (\n <label htmlFor={id} className={cx(\"inline-flex min-h-11 cursor-pointer items-center gap-3 text-sm\", disabled && \"cursor-not-allowed opacity-50\", className)}>\n <span className=\"relative inline-flex h-6 w-10 shrink-0\">\n <input\n id={id}\n type=\"checkbox\"\n role=\"switch\"\n className=\"peer sr-only\"\n checked={checked}\n onChange={onChange}\n disabled={disabled}\n />\n <span className=\"absolute inset-0 rounded-full border border-input bg-muted transition-colors peer-checked:bg-primary peer-focus-visible:ring-2 peer-focus-visible:ring-ring\" />\n <span className=\"absolute left-0.5 top-0.5 h-5 w-5 rounded-full bg-foreground transition-transform peer-checked:translate-x-4 peer-checked:bg-primary-foreground\" />\n </span>\n {label ? <span>{label}</span> : null}\n </label>\n );\n}\n\nexport function Checkbox({ id, label, className, ...rest }) {\n return (\n <label htmlFor={id} className={cx(\"inline-flex min-h-11 cursor-pointer items-center gap-2 text-sm\", className)}>\n <input\n id={id}\n type=\"checkbox\"\n className=\"h-4 w-4 rounded-sm border-input accent-[var(--color-primary)] outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n {...rest}\n />\n {label ? <span>{label}</span> : null}\n </label>\n );\n}\n"
|
|
127
133
|
},
|
|
128
134
|
{
|
|
129
135
|
"path": "templates/next/files/components/ui/file-input.jsx",
|
|
@@ -135,7 +141,7 @@
|
|
|
135
141
|
"path": "templates/next/files/components/ui/icon.jsx",
|
|
136
142
|
"type": "registry:ui",
|
|
137
143
|
"target": "@components/makersclaw/ui/icon.jsx",
|
|
138
|
-
"content": "// The kit's icons: one inline sprite, no icon package to install. Every\n// glyph is a 24-box stroke path (1.8 wide, round caps, currentColor); add\n// one here, never an <svg> in a surface. `name` is a key below; unknown\n// names render nothing (and `npm run check` names them).\n//\n// BRANDS holds the brand marks: google, github, apple, microsoft,\n// x-logo (the X / Twitter mark; `x` is the close glyph), linkedin,\n// instagram, youtube, facebook. Unlike PATHS they are FILLED shapes\n// (currentColor, even-odd so letters and holes knock out), one colour,\n// drawn by hand and simplified, never copied from an icon library.\n\nconst PATHS = {\n plus: \"M12 5v14M5 12h14\",\n x: \"M18 6 6 18M6 6l12 12\",\n check: \"m5 12 5 5L20 7\",\n \"chevron-down\": \"m6 9 6 6 6-6\",\n \"chevron-up\": \"m18 15-6-6-6 6\",\n \"chevron-left\": \"m15 18-6-6 6-6\",\n \"chevron-right\": \"m9 18 6-6-6-6\",\n \"chevrons-up-down\": \"m7 15 5 5 5-5M7 9l5-5 5 5\",\n \"arrow-left\": \"M19 12H5m7-7-7 7 7 7\",\n \"arrow-right\": \"M5 12h14m-7-7 7 7-7 7\",\n \"arrow-up\": \"M12 19V5m-7 7 7-7 7 7\",\n \"arrow-down\": \"M12 5v14m7-7-7 7-7-7\",\n search: \"M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14ZM21 21l-4.3-4.3\",\n trash: \"M3 6h18M8 6V4h8v2m-9 0 1 14h8l1-14\",\n pencil: \"M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z\",\n copy: \"M8 8h12v12H8zM4 16V4h12\",\n more: \"M5 12h.01M12 12h.01M19 12h.01\",\n external: \"M14 3h7v7m0-7L10 14M21 14v7H3V3h7\",\n refresh: \"M21 12a9 9 0 1 1-2.6-6.4M21 3v6h-6\",\n filter: \"M3 5h18l-7 8v6l-4 2v-8Z\",\n calendar: \"M3 5h18v16H3zM16 3v4M8 3v4M3 10h18\",\n clock: \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 4v5l3 3\",\n user: \"M20 21a8 8 0 0 0-16 0M12 3a5 5 0 1 0 0 10 5 5 0 0 0 0-10Z\",\n users: \"M17 21a4 4 0 0 0-8 0M13 4a4 4 0 1 0 0 8 4 4 0 0 0 0-8ZM23 21a4 4 0 0 0-3-3.9M16 3.1a4 4 0 0 1 0 7.8M3 21a4 4 0 0 1 3-3.9M8 3.1a4 4 0 0 0 0 7.8\",\n mail: \"M3 5h18v14H3zM3 7l9 6 9-6\",\n link: \"M10 14a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1M14 10a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1\",\n grip: \"M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01\",\n info: \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 5h.01M12 12v4\",\n warning: \"M12 3 2 20h20L12 3Zm0 7v4m0 3h.01\",\n sparkles: \"m12 3 2 6 6 2-6 2-2 6-2-6-6-2 6-2 2-6ZM19 15l1 2 2 1-2 1-1 2-1-2-2-1 2-1 1-2Z\",\n play: \"m6 4 14 8-14 8V4Z\",\n pause: \"M6 4h4v16H6zM14 4h4v16h-4z\",\n download: \"M12 3v12m-5-5 5 5 5-5M4 21h16\",\n upload: \"M12 21V9m-5 5 5-5 5 5M4 3h16\",\n eye: \"M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12Zm10-3a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z\",\n \"eye-off\": \"M10.6 5.1A10 10 0 0 1 12 5c6 0 10 7 10 7a17 17 0 0 1-2.4 3.2M6.6 6.6C3.8 8.4 2 12 2 12s4 7 10 7a9.7 9.7 0 0 0 5.4-1.6M9.9 9.9a3 3 0 0 0 4.2 4.2M2 2l20 20\",\n send: \"m22 2-7 20-4-9-9-4 20-7Z\",\n home: \"m3 11 9-8 9 8v10h-6v-6H9v6H3V11Z\",\n settings: \"M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8Zm8 4-1.5-.5.3-1.6 1.4-.9-1-1.7-1.6.5-1.2-1.2.5-1.6-1.7-1-.9 1.4-1.6-.3L12 4l-.5 1.5-1.6.3-.9-1.4-1.7 1 .5 1.6-1.2 1.2-1.6-.5-1 1.7 1.4.9.3 1.6L4 12l1.5.5-.3 1.6-1.4.9 1 1.7 1.6-.5 1.2 1.2-.5 1.6 1.7 1 .9-1.4 1.6.3L12 20l.5-1.5 1.6-.3.9 1.4 1.7-1-.5-1.6 1.2-1.2 1.6.5 1-1.7-1.4-.9-.3-1.6L20 12Z\",\n reply: \"M9 17 4 12l5-5M4 12h10a6 6 0 0 1 6 6v1\",\n repeat: \"m17 2 4 4-4 4M3 11V9a3 3 0 0 1 3-3h15M7 22l-4-4 4-4M21 13v2a3 3 0 0 1-3 3H3\",\n heart: \"M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1.1-1.1a5.5 5.5 0 0 0-7.8 7.8l1.1 1.1L12 21l7.8-7.5 1.1-1.1a5.5 5.5 0 0 0-.1-7.8Z\",\n chart: \"M3 3v18h18M7 16l4-5 4 3 5-7\",\n share: \"M12 3v12M7 8l5-5 5 5M5 14v6h14v-6\",\n star: \"m12 3 2.8 5.7 6.2.9-4.5 4.4 1.1 6.2L12 17.3 6.4 20.2l1.1-6.2L3 9.6l6.2-.9L12 3Z\",\n bell: \"M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10 21a2 2 0 0 0 4 0\",\n tag: \"M20 12 12 20l-9-9V3h8l9 9ZM7 7h.01\",\n folder: \"M3 7V5h6l2 2h10v13H3V7Z\",\n file: \"M14 3H6v18h12V7l-4-4ZM14 3v4h4\",\n image: \"M3 5h18v14H3zM8 11a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM21 15l-5-5-9 9\",\n phone: \"M22 16.9v3a2 2 0 0 1-2.2 2A19.8 19.8 0 0 1 2.1 4.2 2 2 0 0 1 4.1 2h3a2 2 0 0 1 2 1.7l.8 3.6a2 2 0 0 1-.5 1.9L8.1 10.5a16 16 0 0 0 5.4 5.4l1.3-1.3a2 2 0 0 1 1.9-.5l3.6.8a2 2 0 0 1 1.7 2Z\",\n globe: \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18\",\n lock: \"M5 11h14v10H5zM8 11V7a4 4 0 0 1 8 0v4\",\n zap: \"M13 2 3 14h8l-1 8 10-12h-8l1-8Z\",\n dollar: \"M12 2v20M17 6H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6\",\n map: \"m3 6 6-3 6 3 6-3v15l-6 3-6-3-6 3V6ZM9 3v15M15 6v15\",\n minus: \"M5 12h14\",\n archive: \"M3 4h18v4H3zM5 8v12h14V8M10 12h4\",\n inbox: \"M3 13h5l2 3h4l2-3h5M3 13V5h18v8M3 13v6h18v-6\",\n pin: \"M12 17v5M8 3h8M9 3v7l-3 4v3h12v-3l-3-4V3\",\n bold: \"M7 4h6a4 4 0 0 1 0 8H7zM7 12h7a4 4 0 0 1 0 8H7z\",\n italic: \"M19 4h-9M14 20H5M15 4 9 20\",\n underline: \"M6 4v6a6 6 0 0 0 12 0V4M4 21h16\",\n list: \"M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01\",\n grid: \"M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3zM14 14h7v7h-7z\",\n \"align-left\": \"M3 6h18M3 12h12M3 18h16\",\n \"align-center\": \"M3 6h18M6 12h12M4 18h16\",\n \"align-right\": \"M3 6h18M9 12h12M5 18h16\",\n paperclip: \"m21 11-9 9a6 6 0 0 1-8.5-8.5l8.6-8.6a4 4 0 0 1 5.7 5.7l-8.6 8.5a2 2 0 0 1-2.8-2.8L15 6.9\",\n stop: \"M6 6h12v12H6z\",\n menu: \"M4 6h16M4 12h16M4 18h16\",\n \"panel-left\": \"M3 4h18v16H3zM9 4v16\",\n \"log-out\": \"M9 21H4V3h5M16 17l5-5-5-5M21 12H9\",\n code: \"m16 18 6-6-6-6M8 6l-6 6 6 6\",\n shield: \"M12 3 4 6v6c0 5 3.4 8.2 8 9 4.6-.8 8-4 8-9V6l-8-3Z\",\n \"shield-check\": \"M12 3 4 6v6c0 5 3.4 8.2 8 9 4.6-.8 8-4 8-9V6l-8-3ZM9 12l2 2 4-4\",\n \"circle-check\": \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm-4 9 3 3 5-5\",\n // The same glyph under its older name, so either one renders.\n \"check-circle\": \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm-4 9 3 3 5-5\",\n \"circle-x\": \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM15 9l-6 6M9 9l6 6\",\n \"message-square\": \"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2Z\",\n \"git-branch\": \"M6 3v12M18 3a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM6 15a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM18 9a9 9 0 0 1-9 9\",\n \"git-pull-request\": \"M6 3a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM18 15a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM6 9v12M12 6h4a2 2 0 0 1 2 2v7m-3-12-3 3 3 3\",\n server: \"M3 3h18v8H3zM3 13h18v8H3zM7 7h.01M7 17h.01\",\n leaf: \"M5 19C5 10 10 4 20 4c0 10-6 15-15 15ZM3 21 14 10\",\n monitor: \"M3 4h18v12H3zM12 16v4M8 20h8\",\n smartphone: \"M8 3h8v18H8zM11 17.5h2\",\n video: \"M3 6h12v12H3zM15 10l6-3v10l-6-3\",\n tablet: \"M5 2h14v20H5zM11 18.5h2\",\n sun: \"M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8ZM12 2v2M12 20v2M2 12h2M20 12h2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4\",\n moon: \"M20 14.5A8.5 8.5 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5Z\",\n database: \"M4 6a8 3 0 1 0 16 0 8 3 0 1 0-16 0ZM4 6v12a8 3 0 0 0 16 0V6M4 12a8 3 0 0 0 16 0\",\n \"git-commit\": \"M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM3 12h6M15 12h6\",\n lightbulb: \"M12 3a6 6 0 0 0-3.5 10.9c.6.4.9 1.1.9 1.8V16h5.2v-.3c0-.7.3-1.4.9-1.8A6 6 0 0 0 12 3ZM9.5 19h5M10.5 22h3\",\n \"life-buoy\": \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8ZM5.6 5.6l3.6 3.6M14.8 14.8l3.6 3.6M18.4 5.6l-3.6 3.6M9.2 14.8l-3.6 3.6\",\n \"log-in\": \"M15 3h5v18h-5M10 17l5-5-5-5M15 12H3\",\n \"user-plus\": \"M2 21a7 7 0 0 1 14 0M9 4a4 4 0 1 0 0 8 4 4 0 0 0 0-8ZM19 8v6M16 11h6\",\n palette: \"M12 3a9 9 0 0 0 0 18c1.1 0 2-.9 2-2 0-.5-.2-1-.5-1.3-.3-.4-.5-.8-.5-1.3 0-1.1.9-1.9 2-1.9h2a4 4 0 0 0 4-4C21 6.4 17 3 12 3ZM7.5 11.5h.01M9 7.5h.01M14 6.5h.01M17.5 9.5h.01\",\n layers: \"M12 3 2 8l10 5 10-5-10-5ZM2 12l10 5 10-5M2 16l10 5 10-5\",\n type: \"M4 7V4h16v3M12 4v16M9 20h6\",\n package: \"M12 2 3 7v10l9 5 9-5V7l-9-5ZM3 7l9 5 9-5M12 12v10M7.5 4.5l9 5\",\n // Task states: an empty ring is \"to do\", a broken ring is \"doing\".\n circle: \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Z\",\n \"circle-dashed\": \"M13.6 3.1A9 9 0 0 1 17.2 4.6M19.4 6.8A9 9 0 0 1 20.9 10.4M20.9 13.6A9 9 0 0 1 19.4 17.2M17.2 19.4A9 9 0 0 1 13.6 20.9M10.4 20.9A9 9 0 0 1 6.8 19.4M4.6 17.2A9 9 0 0 1 3.1 13.6M3.1 10.4A9 9 0 0 1 4.6 6.8M6.8 4.6A9 9 0 0 1 10.4 3.1\",\n loader: \"M12 2v4M12 18v4M2 12h4M18 12h4M4.9 4.9l2.8 2.8M16.3 16.3l2.8 2.8M4.9 19.1l2.8-2.8M16.3 7.7l2.8-2.8\",\n \"trending-up\": \"m3 17 6-6 4 4 8-8M15 7h6v6\",\n \"trending-down\": \"m3 7 6 6 4-4 8 8M15 17h6v-6\",\n \"message-circle\": \"M21 12a9 9 0 0 1-13.3 7.9L3 21l1.1-4.7A9 9 0 1 1 21 12Z\",\n \"credit-card\": \"M2 5h20v14H2zM2 10h20M6 15h4\",\n \"shopping-cart\": \"M2 3h3l2.4 12h11.2L21 7H5.8M8 19.5a1.5 1.5 0 1 0 3 0 1.5 1.5 0 1 0-3 0ZM16 19.5a1.5 1.5 0 1 0 3 0 1.5 1.5 0 1 0-3 0Z\",\n key: \"M7.5 11a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9ZM10.7 12.3 21 2M16 7l3 3M18.5 4.5l2 2\",\n quote: \"M4 18h5v-6H4v6Zm0-6c0-4 1.5-6 5-7M15 18h5v-6h-5v6Zm0-6c0-4 1.5-6 5-7\",\n truck: \"M2 5h12v11H2zM14 9h4.5L21 12v4h-7M4 18a2 2 0 1 0 4 0 2 2 0 1 0-4 0ZM15 18a2 2 0 1 0 4 0 2 2 0 1 0-4 0Z\",\n laptop: \"M4 5h16v11H4zM2 19h20\",\n receipt: \"M5 3h14v18l-2.3-1.5-2.4 1.5-2.3-1.5-2.3 1.5-2.4-1.5L5 21V3ZM9 8h6M9 12h6M9 16h3\",\n mic: \"M12 2a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3ZM5 11a7 7 0 0 0 14 0M12 18v4M8 22h8\",\n \"thumbs-up\": \"M7 10v11H3V10h4Zm0 0 4-8a3 3 0 0 1 3 3v4h5.5a2 2 0 0 1 2 2.3l-1.4 8a2 2 0 0 1-2 1.7H7\",\n \"thumbs-down\": \"M7 14V3H3v11h4Zm0 0 4 8a3 3 0 0 0 3-3v-4h5.5a2 2 0 0 0 2-2.3l-1.4-8a2 2 0 0 0-2-1.7H7\",\n history: \"M3 12a9 9 0 1 0 2.6-6.4L3 8M3 3v5h5M12 7v5l3 2\",\n wand: \"M3 21 14 10M16 3v4M14 5h4M20 9v3M18.5 10.5h3M9 3v2M8 4h2\",\n building: \"M4 21V3h12v18M16 9h4v12M2 21h20M8 7h4M8 11h4M8 15h4\",\n // Canvas tools (flow-canvas): the rail, the zoom bar, the arrange buttons.\n \"mouse-pointer\": \"M5 3l5.8 16.5 2.4-6.8 6.8-2.4L5 3ZM13.2 12.7 19 18.5\",\n hand: \"M8 12V5.5a1.5 1.5 0 0 1 3 0V11M11 10.5V4a1.5 1.5 0 0 1 3 0v6.5M14 10.5V5.5a1.5 1.5 0 0 1 3 0V14a7 7 0 0 1-7 7h-.4a6 6 0 0 1-4.7-2.3l-2.3-2.9a1.6 1.6 0 0 1 2.4-2.1L8 16\",\n \"sticky-note\": \"M4 4h16v10l-6 6H4V4ZM14 20v-6h6\",\n square: \"M4 4h16v16H4z\",\n frame: \"M7 3v18M17 3v18M3 7h18M3 17h18\",\n connector: \"M4 20C4 11 10 5 19 5m-4-3.5L19 5l-4 3.5M4 20h.01\",\n maximize: \"M8 3H3v5M16 3h5v5M8 21H3v-5M16 21h5v-5\",\n undo: \"M9 14 4 9l5-5M4 9h11a5 5 0 0 1 0 10h-3\",\n redo: \"m15 14 5-5-5-5M20 9H9a5 5 0 0 0 0 10h3\",\n keyboard: \"M2 6h20v12H2zM6 10h.01M10 10h.01M14 10h.01M18 10h.01M7 14h10\",\n // Sketch tools (sketch-canvas): the diamond shape and the eraser.\n diamond: \"M12 3 21 12 12 21 3 12Z\",\n eraser: \"M20 20H8.5l-4-4a2 2 0 0 1 0-2.8l8.8-8.8a2 2 0 0 1 2.8 0l3.5 3.5a2 2 0 0 1 0 2.8L12 20M8.5 9.5l6 6\",\n highlighter: \"m14.5 4.5 5 5-7.5 7.5H8.5L7 15.5v-3.5ZM11 8l5 5M7 15.5 4 18.5l2 1.5h5M14 21h6\",\n \"lock-open\": \"M5 11h14v10H5zM8 11V7a4 4 0 0 1 7.7-1.5\",\n \"bring-front\": \"M12 14V3M8 7l4-4 4 4M4 15l8 5 8-5\",\n \"send-back\": \"M12 3v11M8 10l4 4 4-4M4 15l8 5 8-5\",\n // Column types (spreadsheet): a number field, a checkbox field.\n hash: \"M4 9h16M4 15h16M10 3 8 21M16 3l-2 18\",\n \"square-check\": \"M4 4h16v16H4zM8 12l3 3 5-6\",\n target: \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM12 7a5 5 0 1 0 0 10 5 5 0 0 0 0-10ZM12 11a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z\",\n flag: \"M5 21V4m0 0h11l-2 4 2 4H5\",\n \"circle-help\": \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM9.5 9.2a2.6 2.6 0 0 1 5 .9c0 1.7-2.5 2.3-2.5 3.9M12 17h.01\",\n // Playback (audio-player): skip back / forward, sound on / off.\n \"rotate-ccw\": \"M3 12a9 9 0 1 0 3-6.7L3 8M3 3v5h5\",\n \"rotate-cw\": \"M21 12a9 9 0 1 1-3-6.7L21 8M21 3v5h-5\",\n volume: \"M11 5 6 9H2v6h4l5 4V5ZM15.5 8.5a5 5 0 0 1 0 7M19 5a10 10 0 0 1 0 14\",\n \"volume-x\": \"M11 5 6 9H2v6h4l5 4V5ZM22 9l-6 6M16 9l6 6\",\n navigation: \"M3 11 21 3l-8 18-2-8Z\",\n locate: \"M12 5a7 7 0 1 0 0 14 7 7 0 0 0 0-14Zm0 4a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM12 2v3M12 19v3M2 12h3M19 12h3\",\n route: \"M6 16a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM18 2a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15\",\n // View kinds (work-views): a board's columns, a table's grid, a gantt's bars.\n kanban: \"M4 4h4v14H4zM10 4h4v9h-4zM16 4h4v16h-4z\",\n table: \"M3 4h18v16H3zM3 10h18M3 15h18M9 10v10\",\n gantt: \"M4 4v16h16M8 8h6M11 12h7M9 16h5\",\n};\n\nconst BRANDS = {\n google: \"M18.9 6.2A9 9 0 1 0 21 12v-1.8h-8.8v3.4h4.96A5.4 5.4 0 1 1 16.14 8.53Z\",\n github: \"M12 2.5a9.5 9.5 0 1 0 0 19 9.5 9.5 0 0 0 0-19ZM10 21.3v-2.9c-2.8-.4-4.5-2-4.5-5 0-1.1.4-2.1 1.1-2.9-.2-.7-.2-1.7.2-2.7 1 0 2 .5 2.8 1.1a8 8 0 0 1 4.8 0c.8-.6 1.8-1.1 2.8-1.1.4 1 .4 2 .2 2.7.7.8 1.1 1.8 1.1 2.9 0 3-1.7 4.6-4.5 5v2.9ZM10 20.2c-2.4.4-3.5-.5-4.3-2l1-.4c.6 1.1 1.4 1.7 3.3 1.3Z\",\n apple: \"M12 7.8C10.5 6.8 8.8 6.6 7.4 7.4 4.8 8.9 4.4 12.8 5.9 16.2c1 2.4 2.5 4.4 4.1 4.4.9 0 1.3-.5 2-.5s1.1.5 2 .5c1.6 0 3-1.7 4-3.6.5-.8.9-1.4 1.3-2A3.2 3.2 0 0 1 18.3 9.3c-.8-1.2-2-2.1-3.5-2.2-1 0-1.9.3-2.8.7ZM12.2 6.3c0-1.8 1.3-3.2 3.1-3.3 0 1.8-1.3 3.2-3.1 3.3Z\",\n microsoft: \"M3 3h8.5v8.5H3zM12.5 3H21v8.5h-8.5zM3 12.5h8.5V21H3zM12.5 12.5H21V21h-8.5z\",\n \"x-logo\": \"M2.8 3h6l12.4 18h-6ZM5.3 4.3l10.6 15.4h2.8L8.1 4.3ZM19.3 3h1.9l-6.6 7.4-.8-1ZM4.7 21H2.8l6.6-7.4.8 1Z\",\n linkedin: \"M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2ZM6.4 7.6a1.3 1.3 0 1 0 2.6 0 1.3 1.3 0 1 0-2.6 0ZM6.6 10h2.3v7.5H6.6ZM10.8 10H13v1.1c.5-.8 1.4-1.3 2.5-1.3 2 0 3 1.2 3 3.4v4.3h-2.3v-3.9c0-1.1-.4-1.8-1.4-1.8s-1.7.7-1.7 1.8v3.9h-2.3Z\",\n instagram: \"M7.5 2.5h9a5 5 0 0 1 5 5v9a5 5 0 0 1-5 5h-9a5 5 0 0 1-5-5v-9a5 5 0 0 1 5-5ZM7.5 4.3a3.2 3.2 0 0 0-3.2 3.2v9a3.2 3.2 0 0 0 3.2 3.2h9a3.2 3.2 0 0 0 3.2-3.2v-9a3.2 3.2 0 0 0-3.2-3.2ZM12 7.3a4.7 4.7 0 1 0 0 9.4 4.7 4.7 0 0 0 0-9.4Zm0 1.8a2.9 2.9 0 1 1 0 5.8 2.9 2.9 0 0 1 0-5.8ZM16.2 6.7a1.1 1.1 0 1 0 2.2 0 1.1 1.1 0 1 0-2.2 0Z\",\n youtube: \"M2.5 7.5c.2-1.6 1.2-2.6 2.8-2.7 2.1-.2 4.4-.3 6.7-.3s4.6.1 6.7.3c1.6.1 2.6 1.1 2.8 2.7.2 1.5.3 3 .3 4.5s-.1 3-.3 4.5c-.2 1.6-1.2 2.6-2.8 2.7-2.1.2-4.4.3-6.7.3s-4.6-.1-6.7-.3c-1.6-.1-2.6-1.1-2.8-2.7-.2-1.5-.3-3-.3-4.5s.1-3 .3-4.5ZM10 8.8v6.4l5.5-3.2Z\",\n facebook: \"M12 2.5a9.5 9.5 0 1 0 0 19 9.5 9.5 0 0 0 0-19ZM10.3 21.4v-7.6H8.4v-2.5h1.9V9.4c0-2.5 1.4-3.9 3.7-3.9.9 0 1.6.1 1.9.1v2.2h-1.3c-1 0-1.4.6-1.4 1.4v2.1h2.6l-.4 2.5h-2.2v7.6Z\",\n};\n\nexport const ICON_NAMES = [...Object.keys(PATHS), ...Object.keys(BRANDS)];\n\nexport function Icon({ name, size = 16, className, label }) {\n const brand = BRANDS[name];\n const d = PATHS[name] || brand;\n if (!d) return null;\n return (\n <svg\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill={brand ? \"currentColor\" : \"none\"}\n fillRule={brand ? \"evenodd\" : undefined}\n stroke={brand ? \"none\" : \"currentColor\"}\n strokeWidth={brand ? undefined : \"1.8\"}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden={label ? undefined : true}\n role={label ? \"img\" : undefined}\n aria-label={label}\n className={className}\n >\n <path d={d} />\n </svg>\n );\n}\n"
|
|
144
|
+
"content": "// The kit's icons: one inline sprite, no icon package to install. Every\n// glyph is a 24-box stroke path (1.8 wide, round caps, currentColor); add\n// one here, never an <svg> in a surface. `name` is a key below; unknown\n// names render nothing (and `npm run check` names them).\n//\n// `size` is pixels (16, the default) or one of the kit's size names:\n// xs 12 · sm 14 · md 16 · lg 20 · xl 24. Every other part of the kit\n// takes size=\"sm\", so a surface writes it here too — and until 0.3.3 a\n// name went onto the <svg> as width=\"sm\", which is not a length: the\n// browser logged \"Expected length, 'sm'\" and drew the glyph at its\n// 300×150 default, a calendar as big as the card around it (a podcast\n// guest board, 2026-09-25).\n//\n// A glyph added for a build takes lucide's name when lucide has one\n// (calendar-check, trash-2), because that is the name a build reaches for\n// first; the path is our own, drawn to the same 24 box, never copied.\n//\n// BRANDS holds the brand marks: google, github, apple, microsoft,\n// x-logo (the X / Twitter mark; `x` is the close glyph), linkedin,\n// instagram, youtube, facebook. Unlike PATHS they are FILLED shapes\n// (currentColor, even-odd so letters and holes knock out), one colour,\n// drawn by hand and simplified, never copied from an icon library.\n\nconst PATHS = {\n plus: \"M12 5v14M5 12h14\",\n x: \"M18 6 6 18M6 6l12 12\",\n check: \"m5 12 5 5L20 7\",\n \"chevron-down\": \"m6 9 6 6 6-6\",\n \"chevron-up\": \"m18 15-6-6-6 6\",\n \"chevron-left\": \"m15 18-6-6 6-6\",\n \"chevron-right\": \"m9 18 6-6-6-6\",\n \"chevrons-up-down\": \"m7 15 5 5 5-5M7 9l5-5 5 5\",\n \"arrow-left\": \"M19 12H5m7-7-7 7 7 7\",\n \"arrow-right\": \"M5 12h14m-7-7 7 7-7 7\",\n \"arrow-up\": \"M12 19V5m-7 7 7-7 7 7\",\n \"arrow-down\": \"M12 5v14m7-7-7 7-7-7\",\n search: \"M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14ZM21 21l-4.3-4.3\",\n trash: \"M3 6h18M8 6V4h8v2m-9 0 1 14h8l1-14\",\n pencil: \"M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z\",\n copy: \"M8 8h12v12H8zM4 16V4h12\",\n more: \"M5 12h.01M12 12h.01M19 12h.01\",\n external: \"M14 3h7v7m0-7L10 14M21 14v7H3V3h7\",\n refresh: \"M21 12a9 9 0 1 1-2.6-6.4M21 3v6h-6\",\n filter: \"M3 5h18l-7 8v6l-4 2v-8Z\",\n calendar: \"M3 5h18v16H3zM16 3v4M8 3v4M3 10h18\",\n clock: \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 4v5l3 3\",\n user: \"M20 21a8 8 0 0 0-16 0M12 3a5 5 0 1 0 0 10 5 5 0 0 0 0-10Z\",\n users: \"M17 21a4 4 0 0 0-8 0M13 4a4 4 0 1 0 0 8 4 4 0 0 0 0-8ZM23 21a4 4 0 0 0-3-3.9M16 3.1a4 4 0 0 1 0 7.8M3 21a4 4 0 0 1 3-3.9M8 3.1a4 4 0 0 0 0 7.8\",\n mail: \"M3 5h18v14H3zM3 7l9 6 9-6\",\n link: \"M10 14a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1M14 10a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1\",\n grip: \"M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01\",\n info: \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 5h.01M12 12v4\",\n warning: \"M12 3 2 20h20L12 3Zm0 7v4m0 3h.01\",\n sparkles: \"m12 3 2 6 6 2-6 2-2 6-2-6-6-2 6-2 2-6ZM19 15l1 2 2 1-2 1-1 2-1-2-2-1 2-1 1-2Z\",\n play: \"m6 4 14 8-14 8V4Z\",\n pause: \"M6 4h4v16H6zM14 4h4v16h-4z\",\n download: \"M12 3v12m-5-5 5 5 5-5M4 21h16\",\n upload: \"M12 21V9m-5 5 5-5 5 5M4 3h16\",\n eye: \"M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12Zm10-3a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z\",\n \"eye-off\": \"M10.6 5.1A10 10 0 0 1 12 5c6 0 10 7 10 7a17 17 0 0 1-2.4 3.2M6.6 6.6C3.8 8.4 2 12 2 12s4 7 10 7a9.7 9.7 0 0 0 5.4-1.6M9.9 9.9a3 3 0 0 0 4.2 4.2M2 2l20 20\",\n send: \"m22 2-7 20-4-9-9-4 20-7Z\",\n home: \"m3 11 9-8 9 8v10h-6v-6H9v6H3V11Z\",\n settings: \"M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8Zm8 4-1.5-.5.3-1.6 1.4-.9-1-1.7-1.6.5-1.2-1.2.5-1.6-1.7-1-.9 1.4-1.6-.3L12 4l-.5 1.5-1.6.3-.9-1.4-1.7 1 .5 1.6-1.2 1.2-1.6-.5-1 1.7 1.4.9.3 1.6L4 12l1.5.5-.3 1.6-1.4.9 1 1.7 1.6-.5 1.2 1.2-.5 1.6 1.7 1 .9-1.4 1.6.3L12 20l.5-1.5 1.6-.3.9 1.4 1.7-1-.5-1.6 1.2-1.2 1.6.5 1-1.7-1.4-.9-.3-1.6L20 12Z\",\n reply: \"M9 17 4 12l5-5M4 12h10a6 6 0 0 1 6 6v1\",\n repeat: \"m17 2 4 4-4 4M3 11V9a3 3 0 0 1 3-3h15M7 22l-4-4 4-4M21 13v2a3 3 0 0 1-3 3H3\",\n heart: \"M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1.1-1.1a5.5 5.5 0 0 0-7.8 7.8l1.1 1.1L12 21l7.8-7.5 1.1-1.1a5.5 5.5 0 0 0-.1-7.8Z\",\n chart: \"M3 3v18h18M7 16l4-5 4 3 5-7\",\n share: \"M12 3v12M7 8l5-5 5 5M5 14v6h14v-6\",\n star: \"m12 3 2.8 5.7 6.2.9-4.5 4.4 1.1 6.2L12 17.3 6.4 20.2l1.1-6.2L3 9.6l6.2-.9L12 3Z\",\n bell: \"M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10 21a2 2 0 0 0 4 0\",\n tag: \"M20 12 12 20l-9-9V3h8l9 9ZM7 7h.01\",\n folder: \"M3 7V5h6l2 2h10v13H3V7Z\",\n file: \"M14 3H6v18h12V7l-4-4ZM14 3v4h4\",\n image: \"M3 5h18v14H3zM8 11a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM21 15l-5-5-9 9\",\n phone: \"M22 16.9v3a2 2 0 0 1-2.2 2A19.8 19.8 0 0 1 2.1 4.2 2 2 0 0 1 4.1 2h3a2 2 0 0 1 2 1.7l.8 3.6a2 2 0 0 1-.5 1.9L8.1 10.5a16 16 0 0 0 5.4 5.4l1.3-1.3a2 2 0 0 1 1.9-.5l3.6.8a2 2 0 0 1 1.7 2Z\",\n globe: \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18\",\n lock: \"M5 11h14v10H5zM8 11V7a4 4 0 0 1 8 0v4\",\n zap: \"M13 2 3 14h8l-1 8 10-12h-8l1-8Z\",\n dollar: \"M12 2v20M17 6H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6\",\n map: \"m3 6 6-3 6 3 6-3v15l-6 3-6-3-6 3V6ZM9 3v15M15 6v15\",\n minus: \"M5 12h14\",\n archive: \"M3 4h18v4H3zM5 8v12h14V8M10 12h4\",\n inbox: \"M3 13h5l2 3h4l2-3h5M3 13V5h18v8M3 13v6h18v-6\",\n pin: \"M12 17v5M8 3h8M9 3v7l-3 4v3h12v-3l-3-4V3\",\n bold: \"M7 4h6a4 4 0 0 1 0 8H7zM7 12h7a4 4 0 0 1 0 8H7z\",\n italic: \"M19 4h-9M14 20H5M15 4 9 20\",\n underline: \"M6 4v6a6 6 0 0 0 12 0V4M4 21h16\",\n list: \"M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01\",\n grid: \"M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3zM14 14h7v7h-7z\",\n \"align-left\": \"M3 6h18M3 12h12M3 18h16\",\n \"align-center\": \"M3 6h18M6 12h12M4 18h16\",\n \"align-right\": \"M3 6h18M9 12h12M5 18h16\",\n paperclip: \"m21 11-9 9a6 6 0 0 1-8.5-8.5l8.6-8.6a4 4 0 0 1 5.7 5.7l-8.6 8.5a2 2 0 0 1-2.8-2.8L15 6.9\",\n stop: \"M6 6h12v12H6z\",\n menu: \"M4 6h16M4 12h16M4 18h16\",\n \"panel-left\": \"M3 4h18v16H3zM9 4v16\",\n \"log-out\": \"M9 21H4V3h5M16 17l5-5-5-5M21 12H9\",\n code: \"m16 18 6-6-6-6M8 6l-6 6 6 6\",\n shield: \"M12 3 4 6v6c0 5 3.4 8.2 8 9 4.6-.8 8-4 8-9V6l-8-3Z\",\n \"shield-check\": \"M12 3 4 6v6c0 5 3.4 8.2 8 9 4.6-.8 8-4 8-9V6l-8-3ZM9 12l2 2 4-4\",\n \"circle-check\": \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm-4 9 3 3 5-5\",\n // The same glyph under its older name, so either one renders.\n \"check-circle\": \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm-4 9 3 3 5-5\",\n \"circle-x\": \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM15 9l-6 6M9 9l6 6\",\n \"message-square\": \"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2Z\",\n \"git-branch\": \"M6 3v12M18 3a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM6 15a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM18 9a9 9 0 0 1-9 9\",\n \"git-pull-request\": \"M6 3a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM18 15a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM6 9v12M12 6h4a2 2 0 0 1 2 2v7m-3-12-3 3 3 3\",\n server: \"M3 3h18v8H3zM3 13h18v8H3zM7 7h.01M7 17h.01\",\n leaf: \"M5 19C5 10 10 4 20 4c0 10-6 15-15 15ZM3 21 14 10\",\n monitor: \"M3 4h18v12H3zM12 16v4M8 20h8\",\n smartphone: \"M8 3h8v18H8zM11 17.5h2\",\n video: \"M3 6h12v12H3zM15 10l6-3v10l-6-3\",\n tablet: \"M5 2h14v20H5zM11 18.5h2\",\n sun: \"M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8ZM12 2v2M12 20v2M2 12h2M20 12h2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4\",\n moon: \"M20 14.5A8.5 8.5 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5Z\",\n database: \"M4 6a8 3 0 1 0 16 0 8 3 0 1 0-16 0ZM4 6v12a8 3 0 0 0 16 0V6M4 12a8 3 0 0 0 16 0\",\n \"git-commit\": \"M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM3 12h6M15 12h6\",\n lightbulb: \"M12 3a6 6 0 0 0-3.5 10.9c.6.4.9 1.1.9 1.8V16h5.2v-.3c0-.7.3-1.4.9-1.8A6 6 0 0 0 12 3ZM9.5 19h5M10.5 22h3\",\n \"life-buoy\": \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8ZM5.6 5.6l3.6 3.6M14.8 14.8l3.6 3.6M18.4 5.6l-3.6 3.6M9.2 14.8l-3.6 3.6\",\n \"log-in\": \"M15 3h5v18h-5M10 17l5-5-5-5M15 12H3\",\n \"user-plus\": \"M2 21a7 7 0 0 1 14 0M9 4a4 4 0 1 0 0 8 4 4 0 0 0 0-8ZM19 8v6M16 11h6\",\n palette: \"M12 3a9 9 0 0 0 0 18c1.1 0 2-.9 2-2 0-.5-.2-1-.5-1.3-.3-.4-.5-.8-.5-1.3 0-1.1.9-1.9 2-1.9h2a4 4 0 0 0 4-4C21 6.4 17 3 12 3ZM7.5 11.5h.01M9 7.5h.01M14 6.5h.01M17.5 9.5h.01\",\n layers: \"M12 3 2 8l10 5 10-5-10-5ZM2 12l10 5 10-5M2 16l10 5 10-5\",\n type: \"M4 7V4h16v3M12 4v16M9 20h6\",\n package: \"M12 2 3 7v10l9 5 9-5V7l-9-5ZM3 7l9 5 9-5M12 12v10M7.5 4.5l9 5\",\n // Task states: an empty ring is \"to do\", a broken ring is \"doing\".\n circle: \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Z\",\n \"circle-dashed\": \"M13.6 3.1A9 9 0 0 1 17.2 4.6M19.4 6.8A9 9 0 0 1 20.9 10.4M20.9 13.6A9 9 0 0 1 19.4 17.2M17.2 19.4A9 9 0 0 1 13.6 20.9M10.4 20.9A9 9 0 0 1 6.8 19.4M4.6 17.2A9 9 0 0 1 3.1 13.6M3.1 10.4A9 9 0 0 1 4.6 6.8M6.8 4.6A9 9 0 0 1 10.4 3.1\",\n loader: \"M12 2v4M12 18v4M2 12h4M18 12h4M4.9 4.9l2.8 2.8M16.3 16.3l2.8 2.8M4.9 19.1l2.8-2.8M16.3 7.7l2.8-2.8\",\n \"trending-up\": \"m3 17 6-6 4 4 8-8M15 7h6v6\",\n \"trending-down\": \"m3 7 6 6 4-4 8 8M15 17h6v-6\",\n \"message-circle\": \"M21 12a9 9 0 0 1-13.3 7.9L3 21l1.1-4.7A9 9 0 1 1 21 12Z\",\n \"credit-card\": \"M2 5h20v14H2zM2 10h20M6 15h4\",\n \"shopping-cart\": \"M2 3h3l2.4 12h11.2L21 7H5.8M8 19.5a1.5 1.5 0 1 0 3 0 1.5 1.5 0 1 0-3 0ZM16 19.5a1.5 1.5 0 1 0 3 0 1.5 1.5 0 1 0-3 0Z\",\n key: \"M7.5 11a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9ZM10.7 12.3 21 2M16 7l3 3M18.5 4.5l2 2\",\n quote: \"M4 18h5v-6H4v6Zm0-6c0-4 1.5-6 5-7M15 18h5v-6h-5v6Zm0-6c0-4 1.5-6 5-7\",\n truck: \"M2 5h12v11H2zM14 9h4.5L21 12v4h-7M4 18a2 2 0 1 0 4 0 2 2 0 1 0-4 0ZM15 18a2 2 0 1 0 4 0 2 2 0 1 0-4 0Z\",\n laptop: \"M4 5h16v11H4zM2 19h20\",\n receipt: \"M5 3h14v18l-2.3-1.5-2.4 1.5-2.3-1.5-2.3 1.5-2.4-1.5L5 21V3ZM9 8h6M9 12h6M9 16h3\",\n mic: \"M12 2a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3ZM5 11a7 7 0 0 0 14 0M12 18v4M8 22h8\",\n \"thumbs-up\": \"M7 10v11H3V10h4Zm0 0 4-8a3 3 0 0 1 3 3v4h5.5a2 2 0 0 1 2 2.3l-1.4 8a2 2 0 0 1-2 1.7H7\",\n \"thumbs-down\": \"M7 14V3H3v11h4Zm0 0 4 8a3 3 0 0 0 3-3v-4h5.5a2 2 0 0 0 2-2.3l-1.4-8a2 2 0 0 0-2-1.7H7\",\n history: \"M3 12a9 9 0 1 0 2.6-6.4L3 8M3 3v5h5M12 7v5l3 2\",\n wand: \"M3 21 14 10M16 3v4M14 5h4M20 9v3M18.5 10.5h3M9 3v2M8 4h2\",\n building: \"M4 21V3h12v18M16 9h4v12M2 21h20M8 7h4M8 11h4M8 15h4\",\n // Canvas tools (flow-canvas): the rail, the zoom bar, the arrange buttons.\n \"mouse-pointer\": \"M5 3l5.8 16.5 2.4-6.8 6.8-2.4L5 3ZM13.2 12.7 19 18.5\",\n hand: \"M8 12V5.5a1.5 1.5 0 0 1 3 0V11M11 10.5V4a1.5 1.5 0 0 1 3 0v6.5M14 10.5V5.5a1.5 1.5 0 0 1 3 0V14a7 7 0 0 1-7 7h-.4a6 6 0 0 1-4.7-2.3l-2.3-2.9a1.6 1.6 0 0 1 2.4-2.1L8 16\",\n \"sticky-note\": \"M4 4h16v10l-6 6H4V4ZM14 20v-6h6\",\n square: \"M4 4h16v16H4z\",\n frame: \"M7 3v18M17 3v18M3 7h18M3 17h18\",\n connector: \"M4 20C4 11 10 5 19 5m-4-3.5L19 5l-4 3.5M4 20h.01\",\n maximize: \"M8 3H3v5M16 3h5v5M8 21H3v-5M16 21h5v-5\",\n undo: \"M9 14 4 9l5-5M4 9h11a5 5 0 0 1 0 10h-3\",\n redo: \"m15 14 5-5-5-5M20 9H9a5 5 0 0 0 0 10h3\",\n keyboard: \"M2 6h20v12H2zM6 10h.01M10 10h.01M14 10h.01M18 10h.01M7 14h10\",\n // Sketch tools (sketch-canvas): the diamond shape and the eraser.\n diamond: \"M12 3 21 12 12 21 3 12Z\",\n eraser: \"M20 20H8.5l-4-4a2 2 0 0 1 0-2.8l8.8-8.8a2 2 0 0 1 2.8 0l3.5 3.5a2 2 0 0 1 0 2.8L12 20M8.5 9.5l6 6\",\n highlighter: \"m14.5 4.5 5 5-7.5 7.5H8.5L7 15.5v-3.5ZM11 8l5 5M7 15.5 4 18.5l2 1.5h5M14 21h6\",\n \"lock-open\": \"M5 11h14v10H5zM8 11V7a4 4 0 0 1 7.7-1.5\",\n \"bring-front\": \"M12 14V3M8 7l4-4 4 4M4 15l8 5 8-5\",\n \"send-back\": \"M12 3v11M8 10l4 4 4-4M4 15l8 5 8-5\",\n // Column types (spreadsheet): a number field, a checkbox field.\n hash: \"M4 9h16M4 15h16M10 3 8 21M16 3l-2 18\",\n \"square-check\": \"M4 4h16v16H4zM8 12l3 3 5-6\",\n target: \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM12 7a5 5 0 1 0 0 10 5 5 0 0 0 0-10ZM12 11a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z\",\n flag: \"M5 21V4m0 0h11l-2 4 2 4H5\",\n \"circle-help\": \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM9.5 9.2a2.6 2.6 0 0 1 5 .9c0 1.7-2.5 2.3-2.5 3.9M12 17h.01\",\n // Playback (audio-player): skip back / forward, sound on / off.\n \"rotate-ccw\": \"M3 12a9 9 0 1 0 3-6.7L3 8M3 3v5h5\",\n \"rotate-cw\": \"M21 12a9 9 0 1 1-3-6.7L21 8M21 3v5h-5\",\n volume: \"M11 5 6 9H2v6h4l5 4V5ZM15.5 8.5a5 5 0 0 1 0 7M19 5a10 10 0 0 1 0 14\",\n \"volume-x\": \"M11 5 6 9H2v6h4l5 4V5ZM22 9l-6 6M16 9l6 6\",\n navigation: \"M3 11 21 3l-8 18-2-8Z\",\n locate: \"M12 5a7 7 0 1 0 0 14 7 7 0 0 0 0-14Zm0 4a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM12 2v3M12 19v3M2 12h3M19 12h3\",\n route: \"M6 16a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM18 2a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15\",\n // View kinds (work-views): a board's columns, a table's grid, a gantt's bars.\n kanban: \"M4 4h4v14H4zM10 4h4v9h-4zM16 4h4v16h-4z\",\n table: \"M3 4h18v16H3zM3 10h18M3 15h18M9 10v10\",\n gantt: \"M4 4v16h16M8 8h6M11 12h7M9 16h5\",\n // Names builds reached for that the sprite lacked, so `npm run check`\n // refused the ship and the build spent a round renaming them (the\n // launch build test, 2026-09-25, and the local build logs before it):\n // a day that is booked, the bin with its slats, a small tree of nodes.\n \"calendar-check\": \"M3 5h18v16H3zM16 3v4M8 3v4M3 10h18M9 15.5l2 2 4-4\",\n \"trash-2\": \"M3 6h18M8 6V4h8v2m-9 0 1 14h8l1-14M10 11v6M14 11v6\",\n network: \"M9 2h6v6H9zM2 16h6v6H2zM16 16h6v6h-6zM12 8v4M5 16v-4h14v4\",\n};\n\nconst BRANDS = {\n google: \"M18.9 6.2A9 9 0 1 0 21 12v-1.8h-8.8v3.4h4.96A5.4 5.4 0 1 1 16.14 8.53Z\",\n github: \"M12 2.5a9.5 9.5 0 1 0 0 19 9.5 9.5 0 0 0 0-19ZM10 21.3v-2.9c-2.8-.4-4.5-2-4.5-5 0-1.1.4-2.1 1.1-2.9-.2-.7-.2-1.7.2-2.7 1 0 2 .5 2.8 1.1a8 8 0 0 1 4.8 0c.8-.6 1.8-1.1 2.8-1.1.4 1 .4 2 .2 2.7.7.8 1.1 1.8 1.1 2.9 0 3-1.7 4.6-4.5 5v2.9ZM10 20.2c-2.4.4-3.5-.5-4.3-2l1-.4c.6 1.1 1.4 1.7 3.3 1.3Z\",\n apple: \"M12 7.8C10.5 6.8 8.8 6.6 7.4 7.4 4.8 8.9 4.4 12.8 5.9 16.2c1 2.4 2.5 4.4 4.1 4.4.9 0 1.3-.5 2-.5s1.1.5 2 .5c1.6 0 3-1.7 4-3.6.5-.8.9-1.4 1.3-2A3.2 3.2 0 0 1 18.3 9.3c-.8-1.2-2-2.1-3.5-2.2-1 0-1.9.3-2.8.7ZM12.2 6.3c0-1.8 1.3-3.2 3.1-3.3 0 1.8-1.3 3.2-3.1 3.3Z\",\n microsoft: \"M3 3h8.5v8.5H3zM12.5 3H21v8.5h-8.5zM3 12.5h8.5V21H3zM12.5 12.5H21V21h-8.5z\",\n \"x-logo\": \"M2.8 3h6l12.4 18h-6ZM5.3 4.3l10.6 15.4h2.8L8.1 4.3ZM19.3 3h1.9l-6.6 7.4-.8-1ZM4.7 21H2.8l6.6-7.4.8 1Z\",\n linkedin: \"M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2ZM6.4 7.6a1.3 1.3 0 1 0 2.6 0 1.3 1.3 0 1 0-2.6 0ZM6.6 10h2.3v7.5H6.6ZM10.8 10H13v1.1c.5-.8 1.4-1.3 2.5-1.3 2 0 3 1.2 3 3.4v4.3h-2.3v-3.9c0-1.1-.4-1.8-1.4-1.8s-1.7.7-1.7 1.8v3.9h-2.3Z\",\n instagram: \"M7.5 2.5h9a5 5 0 0 1 5 5v9a5 5 0 0 1-5 5h-9a5 5 0 0 1-5-5v-9a5 5 0 0 1 5-5ZM7.5 4.3a3.2 3.2 0 0 0-3.2 3.2v9a3.2 3.2 0 0 0 3.2 3.2h9a3.2 3.2 0 0 0 3.2-3.2v-9a3.2 3.2 0 0 0-3.2-3.2ZM12 7.3a4.7 4.7 0 1 0 0 9.4 4.7 4.7 0 0 0 0-9.4Zm0 1.8a2.9 2.9 0 1 1 0 5.8 2.9 2.9 0 0 1 0-5.8ZM16.2 6.7a1.1 1.1 0 1 0 2.2 0 1.1 1.1 0 1 0-2.2 0Z\",\n youtube: \"M2.5 7.5c.2-1.6 1.2-2.6 2.8-2.7 2.1-.2 4.4-.3 6.7-.3s4.6.1 6.7.3c1.6.1 2.6 1.1 2.8 2.7.2 1.5.3 3 .3 4.5s-.1 3-.3 4.5c-.2 1.6-1.2 2.6-2.8 2.7-2.1.2-4.4.3-6.7.3s-4.6-.1-6.7-.3c-1.6-.1-2.6-1.1-2.8-2.7-.2-1.5-.3-3-.3-4.5s.1-3 .3-4.5ZM10 8.8v6.4l5.5-3.2Z\",\n facebook: \"M12 2.5a9.5 9.5 0 1 0 0 19 9.5 9.5 0 0 0 0-19ZM10.3 21.4v-7.6H8.4v-2.5h1.9V9.4c0-2.5 1.4-3.9 3.7-3.9.9 0 1.6.1 1.9.1v2.2h-1.3c-1 0-1.4.6-1.4 1.4v2.1h2.6l-.4 2.5h-2.2v7.6Z\",\n};\n\nexport const ICON_NAMES = [...Object.keys(PATHS), ...Object.keys(BRANDS)];\n\n// The kit's size names in pixels. A number, or a length the <svg> can\n// read (\"18\", \"1.25em\"), is kept as it was; anything else is the default,\n// never an attribute the browser cannot read.\nconst SIZES = { xs: 12, sm: 14, md: 16, lg: 20, xl: 24 };\nconst LENGTH = /^(\\d+\\.?\\d*|\\.\\d+)(px|em|rem|%)?$/;\n\nfunction pixels(size) {\n if (typeof size === \"number\" && size > 0) return size;\n if (typeof size === \"string\") {\n if (SIZES[size]) return SIZES[size];\n if (LENGTH.test(size)) return size;\n }\n return SIZES.md;\n}\n\nexport function Icon({ name, size = 16, className, label }) {\n const brand = BRANDS[name];\n const d = PATHS[name] || brand;\n if (!d) return null;\n const px = pixels(size);\n return (\n <svg\n width={px}\n height={px}\n viewBox=\"0 0 24 24\"\n fill={brand ? \"currentColor\" : \"none\"}\n fillRule={brand ? \"evenodd\" : undefined}\n stroke={brand ? \"none\" : \"currentColor\"}\n strokeWidth={brand ? undefined : \"1.8\"}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden={label ? undefined : true}\n role={label ? \"img\" : undefined}\n aria-label={label}\n className={className}\n >\n <path d={d} />\n </svg>\n );\n}\n"
|
|
139
145
|
},
|
|
140
146
|
{
|
|
141
147
|
"path": "templates/next/files/components/ui/image.jsx",
|
|
@@ -147,7 +153,7 @@
|
|
|
147
153
|
"path": "templates/next/files/components/ui/index.js",
|
|
148
154
|
"type": "registry:ui",
|
|
149
155
|
"target": "@components/makersclaw/ui/index.js",
|
|
150
|
-
"content": "// The kit. Import from \"../components/ui\" (a surface) or \"./ui\" (a\n// component). Every part is the workspace's look over the browser's own\n// behaviour; none of them takes a colour. README §3b.\nexport { cx } from \"./cx\";\nexport { Icon, ICON_NAMES } from \"./icon\";\nexport { Button, IconButton } from \"./button\";\nexport { Field, Input, TimeInput, Textarea, Select, Switch, Checkbox } from \"./field\";\nexport { Card, Notice, Badge, Kicker, Empty } from \"./notice\";\nexport { Sheet, Confirm } from \"./sheet\";\nexport { Tabs, TabPanel } from \"./tabs\";\nexport { Menu, MenuPanel } from \"./menu\";\nexport { ContextMenu } from \"./context-menu\";\nexport { Popover, usePopoverPosition, panelClass } from \"./popover\";\nexport {\n Calendar,\n parseDate,\n isDateString,\n toDateString,\n todayString,\n addDays,\n addMonths,\n startOfMonth,\n endOfMonth,\n dayOfWeek,\n formatDate,\n firstDayOfWeek,\n} from \"./calendar\";\nexport { DatePicker, DateInput, DATE_PRESETS } from \"./date-picker\";\nexport { Tooltip, HoverCard } from \"./tooltip\";\nexport { Depiction } from \"./depiction\";\nexport { toast, Toaster } from \"./toast\";\nexport { Carousel } from \"./carousel\";\nexport { ResizablePanelGroup, ResizablePanel, ResizableHandle } from \"./resizable\";\nexport { Avatar, AvatarGroup } from \"./avatar\";\nexport { Breadcrumb } from \"./breadcrumb\";\nexport { Pagination } from \"./pagination\";\nexport { RadioGroup, Radio } from \"./radio\";\nexport { Toggle, ToggleGroup } from \"./toggle\";\nexport { ButtonGroup, ButtonGroupSeparator } from \"./button-group\";\nexport { Collapsible, Accordion } from \"./collapsible\";\nexport { Item, ItemGroup } from \"./item\";\nexport { Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption } from \"./table\";\nexport { Spinner, Skeleton, Progress } from \"./feedback\";\nexport { Distribution } from \"./distribution\";\nexport { Swatch } from \"./swatch\";\nexport { Stat, StatGroup, Delta, StatusDot, Legend, Meter } from \"./stat\";\nexport { Steps } from \"./steps\";\nexport { Separator, Kbd, KbdGroup, AspectRatio } from \"./separator\";\nexport { ScrollArea } from \"./scroll-area\";\nexport { Slider } from \"./slider\";\nexport { InputGroup, PasswordInput } from \"./input-group\";\nexport { Image } from \"./image\";\nexport { FileInput, FileDrop, fileMatches } from \"./file-input\";\nexport { InputOTP } from \"./input-otp\";\nexport { Command, CommandDialog, commandScore, commandFilter } from \"./command\";\nexport { Combobox } from \"./combobox\";\nexport { Rating } from \"./rating\";\n"
|
|
156
|
+
"content": "// The kit. Import from \"../components/ui\" (a surface) or \"./ui\" (a\n// component). Every part is the workspace's look over the browser's own\n// behaviour; none of them takes a colour. README §3b.\nexport { cx } from \"./cx\";\nexport { Icon, ICON_NAMES } from \"./icon\";\nexport { Button, IconButton } from \"./button\";\nexport { Field, Input, TimeInput, Textarea, Select, Switch, Checkbox } from \"./field\";\nexport { Card, Notice, Badge, Kicker, Empty } from \"./notice\";\nexport { Sheet, Confirm } from \"./sheet\";\nexport { Tabs, TabPanel } from \"./tabs\";\nexport { Menu, MenuPanel } from \"./menu\";\nexport { ContextMenu } from \"./context-menu\";\nexport { Popover, usePopoverPosition, panelClass } from \"./popover\";\nexport {\n Calendar,\n parseDate,\n isDateString,\n toDateString,\n todayString,\n addDays,\n addMonths,\n startOfMonth,\n endOfMonth,\n dayOfWeek,\n formatDate,\n toLocalDate,\n firstDayOfWeek,\n} from \"./calendar\";\nexport { DatePicker, DateInput, DATE_PRESETS } from \"./date-picker\";\nexport { Tooltip, HoverCard } from \"./tooltip\";\nexport { Depiction } from \"./depiction\";\nexport { toast, Toaster } from \"./toast\";\nexport { Carousel } from \"./carousel\";\nexport { ResizablePanelGroup, ResizablePanel, ResizableHandle } from \"./resizable\";\nexport { Avatar, AvatarGroup } from \"./avatar\";\nexport { Breadcrumb } from \"./breadcrumb\";\nexport { Pagination } from \"./pagination\";\nexport { RadioGroup, Radio } from \"./radio\";\nexport { Toggle, ToggleGroup } from \"./toggle\";\nexport { ButtonGroup, ButtonGroupSeparator } from \"./button-group\";\nexport { Collapsible, Accordion } from \"./collapsible\";\nexport { Item, ItemGroup } from \"./item\";\nexport { Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption } from \"./table\";\nexport { Spinner, Skeleton, Progress } from \"./feedback\";\nexport { Distribution } from \"./distribution\";\nexport { Swatch } from \"./swatch\";\nexport { Stat, StatGroup, Delta, StatusDot, Legend, Meter } from \"./stat\";\nexport { Steps } from \"./steps\";\nexport { Separator, Kbd, KbdGroup, AspectRatio } from \"./separator\";\nexport { ScrollArea } from \"./scroll-area\";\nexport { Slider } from \"./slider\";\nexport { InputGroup, PasswordInput } from \"./input-group\";\nexport { Image } from \"./image\";\nexport { FileInput, FileDrop, fileMatches } from \"./file-input\";\nexport { InputOTP } from \"./input-otp\";\nexport { Command, CommandDialog, commandScore, commandFilter } from \"./command\";\nexport { Combobox } from \"./combobox\";\nexport { Rating } from \"./rating\";\n"
|
|
151
157
|
},
|
|
152
158
|
{
|
|
153
159
|
"path": "templates/next/files/components/ui/input-group.jsx",
|
|
@@ -225,7 +231,7 @@
|
|
|
225
231
|
"path": "templates/next/files/components/ui/sheet.jsx",
|
|
226
232
|
"type": "registry:ui",
|
|
227
233
|
"target": "@components/makersclaw/ui/sheet.jsx",
|
|
228
|
-
"content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { cx } from \"./cx\";\nimport { Button, IconButton } from \"./button\";\nimport { Checkbox, Input } from \"./field\";\n\n// Sheet — the one overlay. A native <dialog> (focus trap, Esc, scroll\n// lock and the top layer come from the browser), docked to the side the\n// surface asks for. Mobile first: on a narrow window every side becomes\n// a bottom sheet the thumb can reach. Controlled: `open` + `onClose`.\n//\n// side end (a rail beside the work; the default) · start (a drawer\n// from the leading edge, as navigation opens) · center (a\n// dialog) · bottom\n// title, description the header; the title names the dialog\n// footer a row of buttons under the body\n// bare true: no header and a flush body (no padding) — for a panel\n// that draws its own top, as CommandDialog does. Esc and a\n// press outside still close it. Give it `label`.\n// label the dialog's name for a screen reader when no `title` shows\n//\n// Confirm below is the \"delete this?\" case — never window.confirm().\n\nconst SIDES = {\n end:\n \"inset-y-0 right-0 left-auto h-dvh max-h-none w-full max-w-md rounded-none border-l \" +\n \"max-sm:inset-x-0 max-sm:top-auto max-sm:bottom-0 max-sm:h-auto max-sm:max-h-[85dvh] max-sm:max-w-none max-sm:rounded-t-xl max-sm:border-l-0 max-sm:border-t\",\n start:\n \"inset-y-0 left-0 right-auto h-dvh max-h-none w-full max-w-md rounded-none border-r \" +\n \"max-sm:inset-x-0 max-sm:top-auto max-sm:bottom-0 max-sm:h-auto max-sm:max-h-[85dvh] max-sm:max-w-none max-sm:rounded-t-xl max-sm:border-r-0 max-sm:border-t\",\n center: \"m-auto w-[calc(100%-2rem)] max-w-lg rounded-lg border max-sm:mb-0 max-sm:mt-auto max-sm:w-full max-sm:max-w-none max-sm:rounded-b-none max-sm:rounded-t-xl\",\n bottom: \"inset-x-0 top-auto bottom-0 w-full max-w-none max-h-[85dvh] rounded-t-xl border-t\",\n};\n\nexport function Sheet({ open, onClose, title, description, side = \"end\", footer, bare = false, label, className, children }) {\n const ref = useRef(null);\n const titleId = useId();\n const named = Boolean(title) && !bare;\n\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n if (open && !el.open) el.showModal();\n else if (!open && el.open) el.close();\n }, [open]);\n\n return (\n <dialog\n ref={ref}\n onClose={() => onClose?.()}\n onClick={(e) => {\n if (e.target === ref.current) ref.current.close();\n }}\n className={cx(\n \"fixed m-0 flex-col bg-popover p-0 text-popover-foreground shadow-xl border-border open:flex\",\n SIDES[side] ?? SIDES.end,\n className,\n )}\n aria-labelledby={named ? titleId : undefined}\n aria-label={named ? undefined : label}\n >\n <div className=\"flex min-h-0 flex-1 flex-col\">\n {!bare && (title || onClose) ? (\n <header className=\"flex items-start justify-between gap-3 border-b border-border px-4 py-3\">\n <div className=\"min-w-0\">\n {title ? <h2 id={titleId} className=\"text-base font-semibold leading-tight\">{title}</h2> : null}\n {description ? <p className=\"mt-0.5 text-sm text-muted-foreground\">{description}</p> : null}\n </div>\n <IconButton icon=\"x\" label=\"Close\" size=\"sm\" onClick={() => ref.current?.close()} className=\"-mr-2\" />\n </header>\n ) : null}\n <div className={cx(\"min-h-0 flex-1 overflow-y-auto\", !bare && \"px-4 py-4\")}>{children}</div>\n {footer ? <footer className=\"flex flex-wrap justify-end gap-2 border-t border-border px-4 py-3\">{footer}</footer> : null}\n </div>\n </dialog>\n );\n}\n\n/** A yes/no with the consequence spelled out. `tone=\"danger\"` for a\n * destructive confirm. Resolves through `onConfirm`; closes either way.\n *\n * consequences lines that will happen (\"14 contacts are deleted\"), a\n * muted bullet list under the body\n * requireText the exact text to type (\"acme-corp\") before Confirm\n * unlocks — for the one delete that cannot come back\n * acknowledge a checkbox label that must be ticked first (\"I have\n * exported what I need\")\n *\n * Confirm stays disabled until each gate given is met; both reset when\n * the dialog opens again. Without them it is the plain yes/no. */\nexport function Confirm({\n open,\n onClose,\n onConfirm,\n title,\n body,\n confirmLabel = \"Confirm\",\n cancelLabel = \"Cancel\",\n tone = \"neutral\",\n busy,\n consequences,\n requireText,\n acknowledge,\n}) {\n const [typed, setTyped] = useState(\"\");\n const [ticked, setTicked] = useState(false);\n const typeId = useId();\n const tickId = useId();\n\n useEffect(() => {\n if (!open) return;\n setTyped(\"\");\n setTicked(false);\n }, [open]);\n\n const lines = Array.isArray(consequences) ? consequences.filter(Boolean) : [];\n const held = (requireText ? typed !== requireText : false) || (acknowledge ? !ticked : false);\n\n return (\n <Sheet\n open={open}\n onClose={onClose}\n side=\"center\"\n title={title}\n footer={\n <>\n <Button variant=\"quiet\" onClick={onClose} disabled={busy}>\n {cancelLabel}\n </Button>\n <Button variant={tone === \"danger\" ? \"danger\" : \"primary\"} onClick={onConfirm} disabled={busy || held}>\n {busy ? \"Working…\" : confirmLabel}\n </Button>\n </>\n }\n >\n {body ? <p className=\"text-sm text-muted-foreground\">{body}</p> : null}\n {lines.length ? (\n <ul className={cx(\"list-disc space-y-1 ps-5 text-sm text-muted-foreground marker:text-muted-foreground\", body && \"mt-3\")}>\n {lines.map((line, i) => (\n <li key={i}>{line}</li>\n ))}\n </ul>\n ) : null}\n {requireText ? (\n <div className={cx(\"grid gap-1.5\", (body || lines.length) && \"mt-4\")}>\n <label htmlFor={typeId} className=\"text-sm text-foreground\">\n Type <span className=\"break-all font-mono font-medium\">{requireText}</span> to confirm\n </label>\n <Input\n id={typeId}\n value={typed}\n onChange={(e) => setTyped(e.target.value)}\n autoComplete=\"off\"\n autoCapitalize=\"off\"\n autoCorrect=\"off\"\n spellCheck={false}\n disabled={busy}\n />\n </div>\n ) : null}\n {acknowledge ? (\n <Checkbox\n id={tickId}\n checked={ticked}\n onChange={(e) => setTicked(e.target.checked)}\n label={acknowledge}\n disabled={busy}\n className={cx((body || lines.length || requireText) && \"mt-2\")}\n />\n ) : null}\n </Sheet>\n );\n}\n"
|
|
234
|
+
"content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { cx } from \"./cx\";\nimport { Button, IconButton } from \"./button\";\nimport { Checkbox, Input } from \"./field\";\n\n// Sheet — the one overlay. A native <dialog> (focus trap, Esc, scroll\n// lock and the top layer come from the browser), docked to the side the\n// surface asks for. Mobile first: on a narrow window every side becomes\n// a bottom sheet the thumb can reach. Controlled: `open` + `onClose`.\n//\n// side end (a rail beside the work; the default) · start (a drawer\n// from the leading edge, as navigation opens) · center (a\n// dialog) · bottom\n// title, description the header; the title names the dialog\n// footer a row of buttons under the body\n// bare true: no header and a flush body (no padding) — for a panel\n// that draws its own top, as CommandDialog does. Esc and a\n// press outside still close it. Give it `label`.\n// label the dialog's name for a screen reader when no `title` shows\n//\n// A field with autoFocus inside it takes focus once the sheet is open\n// (otherwise the browser gives it to the first control: Close).\n//\n// Confirm below is the \"delete this?\" case — never window.confirm().\n\nconst SIDES = {\n end:\n \"inset-y-0 right-0 left-auto h-dvh max-h-none w-full max-w-md rounded-none border-l \" +\n \"max-sm:inset-x-0 max-sm:top-auto max-sm:bottom-0 max-sm:h-auto max-sm:max-h-[85dvh] max-sm:max-w-none max-sm:rounded-t-xl max-sm:border-l-0 max-sm:border-t\",\n start:\n \"inset-y-0 left-0 right-auto h-dvh max-h-none w-full max-w-md rounded-none border-r \" +\n \"max-sm:inset-x-0 max-sm:top-auto max-sm:bottom-0 max-sm:h-auto max-sm:max-h-[85dvh] max-sm:max-w-none max-sm:rounded-t-xl max-sm:border-r-0 max-sm:border-t\",\n center: \"m-auto w-[calc(100%-2rem)] max-w-lg rounded-lg border max-sm:mb-0 max-sm:mt-auto max-sm:w-full max-sm:max-w-none max-sm:rounded-b-none max-sm:rounded-t-xl\",\n bottom: \"inset-x-0 top-auto bottom-0 w-full max-w-none max-h-[85dvh] rounded-t-xl border-t\",\n};\n\n// autoFocus inside a Sheet. React focuses an autoFocus field as the field\n// mounts, and the dialog opens after that, in the effect below: a field\n// mounted with the sheet, or long before it (a sheet's body is rendered\n// while it is closed), was focused inside a closed dialog, where focus\n// goes nowhere. showModal() then focused the first thing it could, the\n// header's Close button, and typing in \"New piece\" went nowhere\n// (weekly-content-report, 2026-09-23; CLAW-FIXES #18). So once it is\n// open, the Sheet puts focus where a field asked for it.\n//\n// Which field: React renders no autofocus attribute in the browser. It\n// keeps a host element's props on the element, under \"__reactProps$…\"\n// (react-dom's own key since React 17; test:app holds the installed\n// react-dom to that name), so that is read, for the elements React\n// autofocuses. The attribute counts too: a server-rendered field has it.\nconst AUTOFOCUSABLE = \"input, textarea, select, button, [autofocus]\";\n\nfunction reactProps(node) {\n for (const key of Object.keys(node)) if (key.startsWith(\"__reactProps$\")) return node[key];\n return null;\n}\n\n/** The field in `dialog` that asked for focus (autoFocus), or null — never\n * one inside a sheet nested in this one, never a disabled one. */\nexport function autoFocusTarget(dialog) {\n for (const node of dialog.querySelectorAll(AUTOFOCUSABLE)) {\n if (node.disabled || node.closest(\"dialog\") !== dialog) continue;\n if (node.hasAttribute(\"autofocus\") || reactProps(node)?.autoFocus) return node;\n }\n return null;\n}\n\nexport function Sheet({ open, onClose, title, description, side = \"end\", footer, bare = false, label, className, children }) {\n const ref = useRef(null);\n const titleId = useId();\n const named = Boolean(title) && !bare;\n\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n if (open && !el.open) {\n el.showModal();\n autoFocusTarget(el)?.focus();\n } else if (!open && el.open) el.close();\n }, [open]);\n\n return (\n <dialog\n ref={ref}\n onClose={() => onClose?.()}\n onClick={(e) => {\n if (e.target === ref.current) ref.current.close();\n }}\n className={cx(\n \"fixed m-0 flex-col bg-popover p-0 text-popover-foreground shadow-xl border-border open:flex\",\n SIDES[side] ?? SIDES.end,\n className,\n )}\n aria-labelledby={named ? titleId : undefined}\n aria-label={named ? undefined : label}\n >\n <div className=\"flex min-h-0 flex-1 flex-col\">\n {!bare && (title || onClose) ? (\n <header className=\"flex items-start justify-between gap-3 border-b border-border px-4 py-3\">\n <div className=\"min-w-0\">\n {title ? <h2 id={titleId} className=\"text-base font-semibold leading-tight\">{title}</h2> : null}\n {description ? <p className=\"mt-0.5 text-sm text-muted-foreground\">{description}</p> : null}\n </div>\n <IconButton icon=\"x\" label=\"Close\" size=\"sm\" onClick={() => ref.current?.close()} className=\"-mr-2\" />\n </header>\n ) : null}\n <div className={cx(\"min-h-0 flex-1 overflow-y-auto\", !bare && \"px-4 py-4\")}>{children}</div>\n {footer ? <footer className=\"flex flex-wrap justify-end gap-2 border-t border-border px-4 py-3\">{footer}</footer> : null}\n </div>\n </dialog>\n );\n}\n\n/** A yes/no with the consequence spelled out. `tone=\"danger\"` for a\n * destructive confirm. Resolves through `onConfirm`; closes either way.\n *\n * consequences lines that will happen (\"14 contacts are deleted\"), a\n * muted bullet list under the body\n * requireText the exact text to type (\"acme-corp\") before Confirm\n * unlocks — for the one delete that cannot come back\n * acknowledge a checkbox label that must be ticked first (\"I have\n * exported what I need\")\n *\n * Confirm stays disabled until each gate given is met; both reset when\n * the dialog opens again. Without them it is the plain yes/no. */\nexport function Confirm({\n open,\n onClose,\n onConfirm,\n title,\n body,\n confirmLabel = \"Confirm\",\n cancelLabel = \"Cancel\",\n tone = \"neutral\",\n busy,\n consequences,\n requireText,\n acknowledge,\n}) {\n const [typed, setTyped] = useState(\"\");\n const [ticked, setTicked] = useState(false);\n const typeId = useId();\n const tickId = useId();\n\n useEffect(() => {\n if (!open) return;\n setTyped(\"\");\n setTicked(false);\n }, [open]);\n\n const lines = Array.isArray(consequences) ? consequences.filter(Boolean) : [];\n const held = (requireText ? typed !== requireText : false) || (acknowledge ? !ticked : false);\n\n return (\n <Sheet\n open={open}\n onClose={onClose}\n side=\"center\"\n title={title}\n footer={\n <>\n <Button variant=\"quiet\" onClick={onClose} disabled={busy}>\n {cancelLabel}\n </Button>\n <Button variant={tone === \"danger\" ? \"danger\" : \"primary\"} onClick={onConfirm} disabled={busy || held}>\n {busy ? \"Working…\" : confirmLabel}\n </Button>\n </>\n }\n >\n {body ? <p className=\"text-sm text-muted-foreground\">{body}</p> : null}\n {lines.length ? (\n <ul className={cx(\"list-disc space-y-1 ps-5 text-sm text-muted-foreground marker:text-muted-foreground\", body && \"mt-3\")}>\n {lines.map((line, i) => (\n <li key={i}>{line}</li>\n ))}\n </ul>\n ) : null}\n {requireText ? (\n <div className={cx(\"grid gap-1.5\", (body || lines.length) && \"mt-4\")}>\n <label htmlFor={typeId} className=\"text-sm text-foreground\">\n Type <span className=\"break-all font-mono font-medium\">{requireText}</span> to confirm\n </label>\n <Input\n id={typeId}\n value={typed}\n onChange={(e) => setTyped(e.target.value)}\n autoComplete=\"off\"\n autoCapitalize=\"off\"\n autoCorrect=\"off\"\n spellCheck={false}\n disabled={busy}\n />\n </div>\n ) : null}\n {acknowledge ? (\n <Checkbox\n id={tickId}\n checked={ticked}\n onChange={(e) => setTicked(e.target.checked)}\n label={acknowledge}\n disabled={busy}\n className={cx((body || lines.length || requireText) && \"mt-2\")}\n />\n ) : null}\n </Sheet>\n );\n}\n"
|
|
229
235
|
},
|
|
230
236
|
{
|
|
231
237
|
"path": "templates/next/files/components/ui/slider.jsx",
|
|
@@ -273,7 +279,7 @@
|
|
|
273
279
|
"path": "templates/next/files/components/ui/toggle.jsx",
|
|
274
280
|
"type": "registry:ui",
|
|
275
281
|
"target": "@components/makersclaw/ui/toggle.jsx",
|
|
276
|
-
"content": "\"use client\";\n\nimport { useRef, useState } from \"react\";\nimport { cx } from \"./cx\";\nimport { Icon } from \"./icon\";\n\n// Toggle — a button that stays down: bold on, pinned, \"show archived\".\n// A real <button aria-pressed>, so Space and Enter work and a screen\n// reader says \"pressed\". Controlled (`pressed` + `onPressedChange(next)`)\n// or not (`defaultPressed`). A setting that saves is a Switch; a toggle\n// changes what THIS view shows or does. Off reads muted, on reads lit\n// (full text, the accent fill, a stronger edge) — never colour alone.\n//\n// variant secondary (bordered; the default) · quiet (no fill until pressed)\n// size md (44px) · sm (36px)\n// icon a name from ./icon; with no children the toggle is square\n// and `label` is required (what a screen reader says)\n//\n// ToggleGroup below is a row of them as one control — the segmented\n// control. Arrow keys move between items (roving focus: the row is one\n// Tab stop), Space or Enter presses.\n\nconst VARIANTS = {\n secondary:\n \"bg-secondary text-muted-foreground border-border hover:bg-accent hover:text-foreground \" +\n \"aria-pressed:bg-accent aria-pressed:text-accent-foreground aria-pressed:border-foreground/50\",\n quiet:\n \"bg-transparent text-muted-foreground border-transparent hover:bg-accent hover:text-foreground \" +\n \"aria-pressed:bg-accent aria-pressed:text-accent-foreground\",\n};\n\n// `square` is the icon-only toggle's own box, used INSTEAD of `box` —\n// never a px-0 laid over box's padding: cx does not merge classes, so\n// which of two paddings wins is CSS order, not the order written.\nconst SIZES = {\n md: { box: \"min-h-11 px-3 text-sm\", square: \"size-11 px-0 text-sm\", glyph: 18 },\n sm: { box: \"min-h-9 px-2.5 text-sm\", square: \"size-9 px-0 text-sm\", glyph: 16 },\n};\n\nexport function Toggle({\n pressed,\n defaultPressed = false,\n onPressedChange,\n variant = \"secondary\",\n size = \"md\",\n icon,\n label,\n className,\n children,\n onClick,\n ...rest\n}) {\n const [own, setOwn] = useState(defaultPressed);\n const on = pressed ?? own;\n const s = SIZES[size] ?? SIZES.md;\n const square = icon && !children;\n return (\n <button\n type=\"button\"\n aria-pressed={on}\n aria-label={square ? label : undefined}\n title={square ? label : undefined}\n onClick={(e) => {\n onClick?.(e);\n if (e.defaultPrevented) return;\n if (pressed === undefined) setOwn(!on);\n onPressedChange?.(!on);\n }}\n className={cx(\n \"inline-flex items-center justify-center gap-2 rounded-md border font-medium whitespace-nowrap transition-colors\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n VARIANTS[variant] ?? VARIANTS.secondary,\n square ? s.square : s.box,\n className,\n )}\n {...rest}\n >\n {icon ? <Icon name={icon} size={square ? s.glyph : 16} /> : null}\n {children}\n </button>\n );\n}\n\n// As SIZES: `square` replaces `item` for an icon-only item.\nconst SEG = {\n md: { track: \"p-1\", item: \"min-h-9 px-3\", square: \"min-h-9 min-w-9 px-0\", glyph: 18 },\n sm: { track: \"p-0.5\", item: \"min-h-8 px-2.5\", square: \"min-h-8 min-w-8 px-0\", glyph: 16 },\n};\n\n/**\n * A row of toggles as one control, the segmented look: a muted track,\n * the pressed items raised. `type` single (exactly one pressed — pressing\n * it again keeps it; a filter that can be off is `multiple`) · multiple.\n * `value` is a string (single) or an array (multiple); `onChange` gets\n * the same shape. md is 44px tall, sm 36px. Stretches when given a width\n * (className=\"w-full\"): the items share it.\n *\n * items [{ value, label, icon?, iconOnly?, disabled? }] — `iconOnly`\n * shows the icon alone; `label` is then what a screen reader says\n */\nexport function ToggleGroup({\n type = \"single\",\n items,\n value,\n defaultValue,\n onChange,\n size = \"md\",\n disabled,\n className,\n \"aria-label\": ariaLabel,\n}) {\n const multiple = type === \"multiple\";\n const [own, setOwn] = useState(defaultValue ?? (multiple ? [] : undefined));\n const current = value !== undefined ? value : own;\n const refs = useRef([]);\n const s = SEG[size] ?? SEG.md;\n const live = (items ?? []).filter(Boolean);\n\n const isOn = (v) => (multiple ? (current ?? []).includes(v) : current === v);\n const usable = (i) => !disabled && !live[i]?.disabled;\n\n function press(v) {\n let next;\n if (multiple) next = isOn(v) ? current.filter((x) => x !== v) : [...(current ?? []), v];\n else if (current === v) return;\n else next = v;\n if (value === undefined) setOwn(next);\n onChange?.(next);\n }\n\n // One Tab stop: the first pressed item, else the first usable one.\n let stop = live.findIndex((it, i) => usable(i) && isOn(it.value));\n if (stop < 0) stop = live.findIndex((_, i) => usable(i));\n\n function onKeyDown(e, index) {\n const step = e.key === \"ArrowRight\" || e.key === \"ArrowDown\" ? 1 : e.key === \"ArrowLeft\" || e.key === \"ArrowUp\" ? -1 : 0;\n let next = null;\n if (step) {\n for (let k = 1; k <= live.length; k++) {\n const i = (index + step * k + live.length) % live.length;\n if (usable(i)) {\n next = i;\n break;\n }\n }\n } else if (e.key === \"Home\") next = live.findIndex((_, i) => usable(i));\n else if (e.key === \"End\") next = live.findLastIndex((_, i) => usable(i));\n if (next === null || next < 0) return;\n e.preventDefault();\n refs.current[next]?.focus();\n }\n\n if (live.length === 0) return null;\n\n return (\n <div\n role=\"group\"\n aria-label={ariaLabel}\n className={cx(\"inline-flex max-w-full gap-1 rounded-md bg-muted\"
|
|
282
|
+
"content": "\"use client\";\n\nimport { useRef, useState } from \"react\";\nimport { cx } from \"./cx\";\nimport { Icon } from \"./icon\";\n\n// Toggle — a button that stays down: bold on, pinned, \"show archived\".\n// A real <button aria-pressed>, so Space and Enter work and a screen\n// reader says \"pressed\". Controlled (`pressed` + `onPressedChange(next)`)\n// or not (`defaultPressed`). A setting that saves is a Switch; a toggle\n// changes what THIS view shows or does. Off reads muted, on reads lit\n// (full text, the accent fill, a stronger edge) — never colour alone.\n//\n// variant secondary (bordered; the default) · quiet (no fill until pressed)\n// size md (44px) · sm (36px)\n// icon a name from ./icon; with no children the toggle is square\n// and `label` is required (what a screen reader says)\n//\n// ToggleGroup below is a row of them as one control — the segmented\n// control. Arrow keys move between items (roving focus: the row is one\n// Tab stop), Space or Enter presses.\n\nconst VARIANTS = {\n secondary:\n \"bg-secondary text-muted-foreground border-border hover:bg-accent hover:text-foreground \" +\n \"aria-pressed:bg-accent aria-pressed:text-accent-foreground aria-pressed:border-foreground/50\",\n quiet:\n \"bg-transparent text-muted-foreground border-transparent hover:bg-accent hover:text-foreground \" +\n \"aria-pressed:bg-accent aria-pressed:text-accent-foreground\",\n};\n\n// `square` is the icon-only toggle's own box, used INSTEAD of `box` —\n// never a px-0 laid over box's padding: cx does not merge classes, so\n// which of two paddings wins is CSS order, not the order written.\nconst SIZES = {\n md: { box: \"min-h-11 px-3 text-sm\", square: \"size-11 px-0 text-sm\", glyph: 18 },\n sm: { box: \"min-h-9 px-2.5 text-sm\", square: \"size-9 px-0 text-sm\", glyph: 16 },\n};\n\nexport function Toggle({\n pressed,\n defaultPressed = false,\n onPressedChange,\n variant = \"secondary\",\n size = \"md\",\n icon,\n label,\n className,\n children,\n onClick,\n ...rest\n}) {\n const [own, setOwn] = useState(defaultPressed);\n const on = pressed ?? own;\n const s = SIZES[size] ?? SIZES.md;\n const square = icon && !children;\n return (\n <button\n type=\"button\"\n aria-pressed={on}\n aria-label={square ? label : undefined}\n title={square ? label : undefined}\n onClick={(e) => {\n onClick?.(e);\n if (e.defaultPrevented) return;\n if (pressed === undefined) setOwn(!on);\n onPressedChange?.(!on);\n }}\n className={cx(\n \"inline-flex items-center justify-center gap-2 rounded-md border font-medium whitespace-nowrap transition-colors\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n VARIANTS[variant] ?? VARIANTS.secondary,\n square ? s.square : s.box,\n className,\n )}\n {...rest}\n >\n {icon ? <Icon name={icon} size={square ? s.glyph : 16} /> : null}\n {children}\n </button>\n );\n}\n\n// As SIZES: `square` replaces `item` for an icon-only item.\nconst SEG = {\n md: { track: \"p-1\", item: \"min-h-9 px-3\", square: \"min-h-9 min-w-9 px-0\", glyph: 18 },\n sm: { track: \"p-0.5\", item: \"min-h-8 px-2.5\", square: \"min-h-8 min-w-8 px-0\", glyph: 16 },\n};\n\n// The group is a scroll container, so its row never makes its parent\n// wider (in a plain grid it pushed the page wider on a phone, 0.5.0).\n// That also takes away the minimums a flex item gets from its content,\n// so each comes back unless the className sizes that axis itself:\n// shrink-0 keeps its width in a flex row (its neighbours give way, as\n// before), min-h-min its height in a flex column.\nconst FLEXES = /(?:^|\\s)!?(?:w-|flex-|grow|shrink|basis-)/;\nconst TALL = /(?:^|\\s)!?(?:h-|min-h-|size-)/;\n\n/**\n * A row of toggles as one control, the segmented look: a muted track,\n * the pressed items raised. `type` single (exactly one pressed — pressing\n * it again keeps it; a filter that can be off is `multiple`) · multiple.\n * `value` is a string (single) or an array (multiple); `onChange` gets\n * the same shape. md is 44px tall, sm 36px. Stretches when given a width\n * (className=\"w-full\"): the items share it. Never wider than its\n * container: past that it scrolls sideways inside itself (it does not\n * wrap unless className=\"flex-wrap\").\n *\n * items [{ value, label, icon?, iconOnly?, disabled? }] — `iconOnly`\n * shows the icon alone; `label` is then what a screen reader says\n */\nexport function ToggleGroup({\n type = \"single\",\n items,\n value,\n defaultValue,\n onChange,\n size = \"md\",\n disabled,\n className,\n \"aria-label\": ariaLabel,\n}) {\n const multiple = type === \"multiple\";\n const [own, setOwn] = useState(defaultValue ?? (multiple ? [] : undefined));\n const current = value !== undefined ? value : own;\n const refs = useRef([]);\n const s = SEG[size] ?? SEG.md;\n const live = (items ?? []).filter(Boolean);\n\n const isOn = (v) => (multiple ? (current ?? []).includes(v) : current === v);\n const usable = (i) => !disabled && !live[i]?.disabled;\n\n function press(v) {\n let next;\n if (multiple) next = isOn(v) ? current.filter((x) => x !== v) : [...(current ?? []), v];\n else if (current === v) return;\n else next = v;\n if (value === undefined) setOwn(next);\n onChange?.(next);\n }\n\n // One Tab stop: the first pressed item, else the first usable one.\n let stop = live.findIndex((it, i) => usable(i) && isOn(it.value));\n if (stop < 0) stop = live.findIndex((_, i) => usable(i));\n\n function onKeyDown(e, index) {\n const step = e.key === \"ArrowRight\" || e.key === \"ArrowDown\" ? 1 : e.key === \"ArrowLeft\" || e.key === \"ArrowUp\" ? -1 : 0;\n let next = null;\n if (step) {\n for (let k = 1; k <= live.length; k++) {\n const i = (index + step * k + live.length) % live.length;\n if (usable(i)) {\n next = i;\n break;\n }\n }\n } else if (e.key === \"Home\") next = live.findIndex((_, i) => usable(i));\n else if (e.key === \"End\") next = live.findLastIndex((_, i) => usable(i));\n if (next === null || next < 0) return;\n e.preventDefault();\n refs.current[next]?.focus();\n }\n\n if (live.length === 0) return null;\n\n return (\n <div\n role=\"group\"\n aria-label={ariaLabel}\n className={cx(\n \"inline-flex max-w-full gap-1 overflow-x-auto rounded-md bg-muted\",\n !FLEXES.test(className ?? \"\") && \"shrink-0\",\n !TALL.test(className ?? \"\") && \"min-h-min\",\n s.track,\n disabled && \"opacity-50\",\n className,\n )}\n >\n {live.map((it, i) => {\n const on = isOn(it.value);\n const square = it.icon && it.iconOnly;\n return (\n <button\n key={it.value}\n ref={(el) => {\n refs.current[i] = el;\n }}\n type=\"button\"\n aria-pressed={on}\n aria-label={square ? it.label : undefined}\n title={square ? it.label : undefined}\n disabled={!usable(i)}\n tabIndex={i === stop ? 0 : -1}\n onClick={() => press(it.value)}\n onKeyDown={(e) => onKeyDown(e, i)}\n className={cx(\n \"inline-flex flex-1 items-center justify-center gap-2 rounded-sm text-sm font-medium whitespace-nowrap transition-colors\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n \"disabled:cursor-not-allowed\",\n !disabled && \"disabled:opacity-50\",\n square ? s.square : s.item,\n on ? \"bg-background text-foreground shadow-sm ring-1 ring-border\" : \"text-muted-foreground hover:text-foreground\",\n )}\n >\n {it.icon ? <Icon name={it.icon} size={square ? s.glyph : 16} /> : null}\n {square ? null : it.label}\n </button>\n );\n })}\n </div>\n );\n}\n"
|
|
277
283
|
},
|
|
278
284
|
{
|
|
279
285
|
"path": "templates/next/files/components/ui/tooltip.jsx",
|