@hachej/boring-ui-kit 0.2.0 → 0.2.2
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/dist/canvas-editor-native.js +2 -2
- package/dist/experience-document-viewer.js +1 -1
- package/dist/file-tree-view.d.ts +3 -1
- package/dist/file-tree-view.js +5 -5
- package/dist/html-viewer.js +1 -1
- package/dist/markdown-editor.js +2 -2
- package/package.json +32 -31
- package/r/button.json +29 -0
- package/r/feedback-preview.json +20 -0
- package/r/feedback.json +688 -0
- package/r/file-tree.json +26 -0
- package/r/html-viewer.json +94 -0
- package/r/markdown-editor.json +216 -0
- package/r/pi-ambient.json +66 -0
- package/r/pi-app.json +102 -0
- package/r/pi-chat.json +246 -0
- package/r/pi-workspace.json +29 -0
- package/r/provider-setup.json +24 -0
- package/r/registry.json +2274 -0
- package/r/task-list-viewer.json +71 -0
- package/r/theme.json +76 -0
- package/r/utils.json +19 -0
- package/r/viewers.json +653 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "task-list-viewer",
|
|
4
|
+
"title": "Consumer task-list viewer",
|
|
5
|
+
"description": "A consumer-defined task-list schema and controller over the shared text buffer, with a restylable borrowed React renderer.",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"@hachej/boring-ui-kit@^0.2.2",
|
|
8
|
+
"@hachej/boring-files@^0.2.2",
|
|
9
|
+
"react@^19.0.0",
|
|
10
|
+
"react-dom@^19.0.0",
|
|
11
|
+
"zod@^4.6.5"
|
|
12
|
+
],
|
|
13
|
+
"files": [
|
|
14
|
+
{
|
|
15
|
+
"path": "registry/task-list-viewer/task-list-document.ts",
|
|
16
|
+
"content": "import { z } from 'zod';\n\nexport const taskListKind = 'fictional.task-list';\nexport const taskListVersion = 1;\nexport const taskListMediaType = 'application/vnd.fictional.task-list+json';\nconst encoder = new TextEncoder();\nconst text = (maximum: number) => z.string().min(1).refine(value => encoder.encode(value).length <= maximum\n && !/[\\uD800-\\uDFFF]/u.test(value) && value.trim().length > 0);\nconst id = text(128), title = text(2048);\nconst item = z.object({ id, title, completed: z.boolean() }).strict();\nconst documentSchema = z.object({ kind: z.literal(taskListKind), version: z.literal(taskListVersion), items: z.array(item).max(100) }).strict()\n .refine(value => new Set(value.items.map(entry => entry.id)).size === value.items.length, 'Duplicate task ID');\nconst operationsSchema = z.array(z.discriminatedUnion('kind', [\n z.object({ kind: z.literal('add'), id, title }).strict(),\n z.object({ kind: z.literal('rename'), id, title }).strict(),\n z.object({ kind: z.literal('set-completed'), id, completed: z.boolean() }).strict(),\n z.object({ kind: z.literal('remove'), id }).strict(),\n])).min(1).max(100);\nexport interface TaskListDocument {\n readonly kind: typeof taskListKind;\n readonly version: typeof taskListVersion;\n readonly items: readonly Readonly<z.infer<typeof item>>[];\n}\nexport type TaskListOperation = Readonly<z.infer<typeof operationsSchema>[number]>;\nfunction bounded(text: string): string {\n if (encoder.encode(text).length > 65_536) throw new TypeError('Task list exceeds 65536 bytes');\n return text;\n}\nexport function parseTaskList(value: unknown): TaskListDocument {\n const parsed = documentSchema.parse(value);\n bounded(JSON.stringify(parsed));\n return Object.freeze({ ...parsed, items: Object.freeze(parsed.items.map(entry => Object.freeze(entry))) });\n}\nexport function parseTaskListOperations(value: unknown): readonly TaskListOperation[] {\n const parsed = operationsSchema.parse(value);\n bounded(JSON.stringify(parsed));\n return Object.freeze(parsed.map(operation => Object.freeze(operation)));\n}\nexport function readTaskList(text: string): TaskListDocument { return parseTaskList(JSON.parse(bounded(text))); }\nexport function serializeTaskList(document: TaskListDocument): string { return bounded(JSON.stringify(parseTaskList(document))); }\nexport function emptyTaskList(): TaskListDocument { return parseTaskList({ kind: taskListKind, version: taskListVersion, items: [] }); }\nexport function applyTaskListOperations(document: TaskListDocument, operations: readonly TaskListOperation[]): TaskListDocument {\n const items = parseTaskList(document).items.map(entry => ({ ...entry }));\n for (const operation of parseTaskListOperations(operations)) {\n const position = items.findIndex(entry => entry.id === operation.id);\n if (operation.kind === 'add') {\n if (position !== -1) throw new TypeError('Task ID already exists');\n items.push({ id: operation.id, title: operation.title, completed: false });\n } else {\n const current = items[position];\n if (!current) throw new TypeError('Task ID does not exist');\n switch (operation.kind) {\n case 'rename': items[position] = { ...current, title: operation.title }; break;\n case 'set-completed': items[position] = { ...current, completed: operation.completed }; break;\n case 'remove': items.splice(position, 1); break;\n }\n }\n }\n return parseTaskList({ kind: taskListKind, version: taskListVersion, items });\n}\n",
|
|
17
|
+
"type": "registry:lib",
|
|
18
|
+
"target": "components/task-list-viewer/task-list-document.ts"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"path": "registry/task-list-viewer/task-list-controller.ts",
|
|
22
|
+
"content": "import { z } from 'zod';\nimport { createTextBuffer } from '@hachej/boring-ui-kit/text-buffer';\nimport type { TextBufferOptions, TextBufferState } from '@hachej/boring-ui-kit/text-buffer';\nimport type { EditableViewerController, SaveSelection, SaveResult } from '@hachej/boring-ui-kit/resources';\nimport type { PresentationCommand, ViewerFeature } from '@hachej/boring-ui-kit/contracts';\nimport type { ReadResult, ResourceExpectation, ResourceLocator } from '@hachej/boring-files';\nimport { applyTaskListOperations, emptyTaskList, readTaskList, serializeTaskList, taskListKind, taskListMediaType, taskListVersion } from './task-list-document';\nimport type { TaskListDocument, TaskListOperation } from './task-list-document';\n\nexport interface TaskListState extends TextBufferState { readonly document: TaskListDocument }\nexport type TaskListOptions = Pick<TextBufferOptions, 'identity' | 'instanceId' | 'epoch' | 'source' | 'client' | 'readOnly' | 'onListenerError'>;\ntype EditResult = { readonly kind: 'applied' } | { readonly kind: 'stale' | 'conflict' | 'denied' | 'unavailable'; readonly reason: string };\nexport interface TaskListActions {\n readonly selection: () => SaveSelection;\n readonly edit: (selection: SaveSelection, operations: readonly TaskListOperation[]) => EditResult;\n readonly refresh: () => Promise<ReadResult>;\n readonly discardToRemote: () => Promise<ReadResult>;\n readonly reconcile: () => Promise<SaveResult>;\n readonly abandon: () => Promise<ReadResult>;\n}\nexport interface TaskListTools {\n readonly inspect: PresentationCommand<{ readonly expiresAt: number }, {\n readonly document: TaskListDocument; readonly dirty: boolean; readonly bufferVersion: number; readonly base: ResourceExpectation;\n }, SaveSelection['target']['subject']>;\n}\nexport type TaskListController = EditableViewerController<TaskListState, TaskListActions, TaskListTools>;\nconst expiry = z.object({ expiresAt: z.number().int().min(Number.MIN_SAFE_INTEGER).max(Number.MAX_SAFE_INTEGER) }).strict();\nconst locator = z.object({ resource: z.object({ providerId: z.string().min(1).max(256), path: z.string().min(1).max(2048) }).strict(), view: z.object({ kind: z.literal('published') }).strict() }).strict();\nconst descriptorSchema = z.object({ kind: z.literal(taskListKind), version: z.literal(taskListVersion), title: z.string().max(2048).optional(), source: locator }).strict();\nexport interface TaskListDescriptor { readonly kind: typeof taskListKind; readonly version: typeof taskListVersion; readonly title?: string; readonly source: ResourceLocator }\n\nfunction sameBase(left: ResourceExpectation, right: ResourceExpectation): boolean {\n return left.kind === right.kind && left.target.resource.providerId === right.target.resource.providerId && left.target.resource.path === right.target.resource.path\n && left.target.view.kind === right.target.view.kind && (left.target.view.kind === 'published' || right.target.view.kind === 'working' && left.target.view.viewId === right.target.view.viewId)\n && (left.kind === 'absent' || right.kind === 'revision' && left.target.revision === right.target.revision);\n}\n\nexport function createTaskListController(options: TaskListOptions): TaskListController {\n locator.parse(options.source.kind === 'saved'\n ? { resource: options.source.snapshot.ref.resource, view: options.source.snapshot.ref.view } : options.source.target);\n const source = options.source.kind === 'saved' ? options.source : { ...options.source, text: options.source.text ?? serializeTaskList(emptyTaskList()) };\n if (source.kind === 'new') readTaskList(source.text);\n const buffer = createTextBuffer({ ...options, source, emptyText: serializeTaskList(emptyTaskList()), mediaType: taskListMediaType, readText: snapshot => {\n if (snapshot.mediaType !== taskListMediaType) throw new TypeError('Expected a task-list document');\n const text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(snapshot.bytes);\n readTaskList(text); return text;\n } });\n let state: TaskListState = Object.freeze({ ...buffer.getSnapshot(), document: readTaskList(buffer.getSnapshot().text) });\n const unsubscribe = buffer.subscribe(() => {\n const next = buffer.getSnapshot();\n state = Object.freeze({ ...next, document: next.text === state.text ? state.document : readTaskList(next.text) });\n });\n const selected = (selection: SaveSelection) => {\n const current = buffer.selection().target, proposed = selection.target;\n return proposed.instanceId === current.instanceId && proposed.epoch === current.epoch && proposed.subject.scopeId === current.subject.scopeId\n && proposed.subject.bufferVersion === current.subject.bufferVersion && sameBase(proposed.subject.base, current.subject.base);\n };\n return {\n getSnapshot: () => state, subscribe: buffer.subscribe, flush: buffer.flush,\n dispose: () => { buffer.dispose(); unsubscribe(); },\n actions: {\n selection: buffer.selection,\n edit: (selection, operations) => {\n if (state.lifecycle === 'disposed') return { kind: 'unavailable', reason: 'Viewer is disposed' };\n if (state.readOnly) return { kind: 'denied', reason: 'Viewer is read-only' };\n if (!selected(selection)) return { kind: 'stale', reason: 'Task-list target has changed' };\n let document;\n try { document = applyTaskListOperations(state.document, operations); }\n catch { return { kind: 'conflict', reason: 'Task-list operations are invalid or conflict with the current list' }; }\n buffer.edit(serializeTaskList(document));\n return { kind: 'applied' };\n },\n refresh: () => buffer.refresh(false), discardToRemote: () => buffer.refresh(true), reconcile: buffer.reconcile, abandon: buffer.abandon,\n },\n tools: { inspect: { name: 'inspect_task_list_buffer', input: { jsonSchema: z.toJSONSchema(expiry), parse: value => expiry.parse(value) }, invoke: async (target, input, signal) => {\n if (state.lifecycle === 'disposed') return { kind: 'unavailable', reason: 'Viewer is disposed' };\n if (signal?.aborted) return { kind: 'denied', reason: 'Inspection cancelled' };\n if (expiry.parse(input).expiresAt <= Date.now() || !selected({ target })) return { kind: 'stale', reason: 'Inspection target changed or expired' };\n return { kind: 'applied', value: { document: state.document, dirty: state.dirty, bufferVersion: state.bufferVersion, base: state.base } };\n } } },\n };\n}\n\nexport function createTaskListFeature(options: TaskListOptions): ViewerFeature<TaskListDescriptor, TaskListController> {\n const captured = { ...options, identity: structuredClone(options.identity), source: structuredClone(options.source) };\n const configured = locator.parse(captured.source.kind === 'saved'\n ? { resource: captured.source.snapshot.ref.resource, view: captured.source.snapshot.ref.view } : captured.source.target);\n return { kind: taskListKind, version: taskListVersion,\n descriptor: { jsonSchema: z.toJSONSchema(descriptorSchema), parse: value => {\n const parsed = descriptorSchema.parse(value);\n return { kind: parsed.kind, version: parsed.version, source: parsed.source, ...(parsed.title === undefined ? {} : { title: parsed.title }) };\n } },\n createController: descriptor => {\n const parsed = descriptorSchema.parse(descriptor);\n if (JSON.stringify(parsed.source) !== JSON.stringify(configured)) throw new TypeError('Descriptor names another task-list source');\n return createTaskListController(captured);\n },\n };\n}\n",
|
|
23
|
+
"type": "registry:lib",
|
|
24
|
+
"target": "components/task-list-viewer/task-list-controller.ts"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"path": "registry/task-list-viewer/task-list-viewer.tsx",
|
|
28
|
+
"content": "'use client';\n\nimport { randomUUID } from '@hachej/boring-files/platform';\nimport React, { useId, useRef, useState, useSyncExternalStore } from 'react';\nimport type { ReadResult } from '@hachej/boring-files';\nimport type { SaveResult } from '@hachej/boring-ui-kit/resources';\nimport type { TaskListController } from './task-list-controller';\nimport type { TaskListOperation } from './task-list-document';\n\nexport interface TaskListViewerProps {\n readonly controller: TaskListController;\n readonly title?: string;\n readonly className?: string;\n}\n\nexport function TaskListViewer({ controller, title = 'Tasks', className }: TaskListViewerProps) {\n const state = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);\n const current = useRef({ controller });\n if (current.current.controller !== controller) current.current = { controller };\n const owner = current.current;\n const [form, setForm] = useState({ owner, draftTitle: '', notice: '' });\n const draftTitle = form.owner === owner ? form.draftTitle : '';\n const notice = form.owner === owner ? form.notice : '';\n const updateForm = (patch: Partial<{ draftTitle: string; notice: string }>) => {\n if (current.current !== owner) return;\n setForm(previous => ({ ...(previous.owner === owner ? previous : { owner, draftTitle: '', notice: '' }), ...patch }));\n };\n const setDraftTitle = (value: string) => updateForm({ draftTitle: value });\n const setNotice = (value: string) => updateForm({ notice: value });\n const inputId = useId();\n const disabled = state.readOnly || state.lifecycle === 'disposed';\n const edit = (operations: readonly TaskListOperation[]) => {\n const result = controller.actions.edit(controller.actions.selection(), operations);\n setNotice(result.kind === 'applied' ? '' : `${result.kind}: ${result.reason}`);\n return result;\n };\n const run = async (action: () => Promise<ReadResult | SaveResult>) => {\n try {\n const result = await action();\n setNotice(result.kind === 'available' || result.kind === 'saved' ? ''\n : result.kind === 'missing' ? 'missing: Resource is missing'\n : `${result.kind}${'reason' in result ? `: ${result.reason}` : ''}`);\n }\n catch (error) { setNotice(String(error)); }\n };\n const save = state.save.kind === 'settled' ? state.save.result : undefined;\n return <section className={['boring-task-list-recipe', className].filter(Boolean).join(' ')} data-boring=\"task-list-viewer\">\n <h2>{title}</h2>\n <p role=\"status\" aria-live=\"polite\">{state.lifecycle === 'disposed' ? 'Closed' : state.readOnly ? 'Read only' : state.dirty ? 'Unsaved changes' : 'Saved'}\n {state.save.kind === 'pending' ? ' · Saving' : save ? ` · ${save.kind}${'reason' in save ? `: ${save.reason}` : ''}` : ''}\n {state.remote ? ' · Remote changes available' : ''}</p>\n <ul>{state.document.items.map(item => <li key={item.id} data-task-id={item.id}>\n <label><input type=\"checkbox\" aria-label={`Complete ${item.title}`} checked={item.completed} disabled={disabled}\n onChange={event => edit([{ kind: 'set-completed', id: item.id, completed: event.target.checked }])} />{item.title}</label>\n <button type=\"button\" aria-label={`Remove ${item.title}`} disabled={disabled} onClick={() => edit([{ kind: 'remove', id: item.id }])}>Remove</button>\n </li>)}</ul>\n <form onSubmit={event => { event.preventDefault(); if (edit([{ kind: 'add', id: randomUUID(), title: draftTitle }]).kind === 'applied') setDraftTitle(''); }}>\n <label htmlFor={inputId}>New task</label><input id={inputId} value={draftTitle} disabled={disabled} onChange={event => setDraftTitle(event.target.value)} />\n <button type=\"submit\" disabled={disabled || !draftTitle.trim()}>Add task</button>\n </form>\n <div className=\"boring-task-list-actions\">\n <button type=\"button\" disabled={disabled || !state.dirty || state.save.kind === 'pending'} onClick={() => void run(() => controller.flush(controller.actions.selection()))}>Save</button>\n <button type=\"button\" disabled={state.lifecycle === 'disposed'} onClick={() => void run(controller.actions.refresh)}>Refresh</button>\n <button type=\"button\" disabled={disabled || (!state.dirty && !state.remote)} onClick={() => void run(controller.actions.discardToRemote)}>Discard</button>\n <button type=\"button\" disabled={state.lifecycle === 'disposed' || save?.kind !== 'unknown'} onClick={() => void run(controller.actions.reconcile)}>Reconcile</button>\n <button type=\"button\" disabled={state.lifecycle === 'disposed' || save?.kind !== 'unknown'} onClick={() => void run(controller.actions.abandon)}>Abandon save</button>\n </div>\n {notice && <p role=\"alert\">{notice}</p>}\n </section>;\n}\n",
|
|
29
|
+
"type": "registry:component",
|
|
30
|
+
"target": "components/task-list-viewer/task-list-viewer.tsx"
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"css": {
|
|
34
|
+
".boring-task-list-recipe[data-boring=\"task-list-viewer\"]": {
|
|
35
|
+
"padding": "1rem",
|
|
36
|
+
"border": "1px solid var(--border, #d1d5db)",
|
|
37
|
+
"border-radius": "var(--boring-task-list-radius, 0.75rem)",
|
|
38
|
+
"background": "var(--background, #fff)",
|
|
39
|
+
"color": "var(--foreground, #111827)"
|
|
40
|
+
},
|
|
41
|
+
".boring-task-list-recipe[data-boring=\"task-list-viewer\"] ul": {
|
|
42
|
+
"list-style": "none",
|
|
43
|
+
"padding": "0",
|
|
44
|
+
"display": "grid",
|
|
45
|
+
"gap": "0.5rem"
|
|
46
|
+
},
|
|
47
|
+
".boring-task-list-recipe[data-boring=\"task-list-viewer\"] li": {
|
|
48
|
+
"display": "flex",
|
|
49
|
+
"align-items": "center",
|
|
50
|
+
"gap": "0.5rem",
|
|
51
|
+
"flex-wrap": "wrap"
|
|
52
|
+
},
|
|
53
|
+
".boring-task-list-recipe[data-boring=\"task-list-viewer\"] button": {
|
|
54
|
+
"min-height": "2.25rem",
|
|
55
|
+
"padding": "0.375rem 0.75rem"
|
|
56
|
+
},
|
|
57
|
+
".boring-task-list-recipe[data-boring=\"task-list-viewer\"] input[type=\"text\"]": {
|
|
58
|
+
"min-height": "2.25rem",
|
|
59
|
+
"max-width": "100%"
|
|
60
|
+
},
|
|
61
|
+
".boring-task-list-recipe[data-boring=\"task-list-viewer\"] [role=\"alert\"]": {
|
|
62
|
+
"color": "var(--destructive, #b91c1c)"
|
|
63
|
+
},
|
|
64
|
+
".boring-task-list-recipe[data-boring=\"task-list-viewer\"] :focus-visible": {
|
|
65
|
+
"outline": "2px solid var(--ring, #2563eb)",
|
|
66
|
+
"outline-offset": "2px"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"docs": "The installed task-list schema/controller are trusted consumer code; publication and draft recovery come from @hachej/boring-ui-kit/text-buffer. Construct a feature with a host-authenticated ResourceClient and exact source, then pass its concrete controller to TaskListViewer. Install native tools separately on the server. No host credentials or native kernel are copied into this item. Local tarball qualification only; no hosted release endpoint is advertised.",
|
|
70
|
+
"type": "registry:component"
|
|
71
|
+
}
|
package/r/theme.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "theme",
|
|
4
|
+
"title": "Theme",
|
|
5
|
+
"description": "The shadcn tokens every Tailwind item is styled with (neutral base, light and dark) and their Tailwind v4 `@theme inline` bindings. The CLI adds only the tokens the host does not define yet. The examples compile the same values (examples/studio/tailwind.mjs).",
|
|
6
|
+
"cssVars": {
|
|
7
|
+
"theme": {
|
|
8
|
+
"color-background": "var(--background)",
|
|
9
|
+
"color-foreground": "var(--foreground)",
|
|
10
|
+
"color-card": "var(--card)",
|
|
11
|
+
"color-card-foreground": "var(--card-foreground)",
|
|
12
|
+
"color-popover": "var(--popover)",
|
|
13
|
+
"color-popover-foreground": "var(--popover-foreground)",
|
|
14
|
+
"color-primary": "var(--primary)",
|
|
15
|
+
"color-primary-foreground": "var(--primary-foreground)",
|
|
16
|
+
"color-secondary": "var(--secondary)",
|
|
17
|
+
"color-secondary-foreground": "var(--secondary-foreground)",
|
|
18
|
+
"color-muted": "var(--muted)",
|
|
19
|
+
"color-muted-foreground": "var(--muted-foreground)",
|
|
20
|
+
"color-accent": "var(--accent)",
|
|
21
|
+
"color-accent-foreground": "var(--accent-foreground)",
|
|
22
|
+
"color-destructive": "var(--destructive)",
|
|
23
|
+
"color-border": "var(--border)",
|
|
24
|
+
"color-input": "var(--input)",
|
|
25
|
+
"color-ring": "var(--ring)",
|
|
26
|
+
"radius-sm": "calc(var(--radius) - 4px)",
|
|
27
|
+
"radius-md": "calc(var(--radius) - 2px)",
|
|
28
|
+
"radius-lg": "var(--radius)",
|
|
29
|
+
"radius-xl": "calc(var(--radius) + 4px)"
|
|
30
|
+
},
|
|
31
|
+
"light": {
|
|
32
|
+
"radius": "0.75rem",
|
|
33
|
+
"background": "oklch(1 0 0)",
|
|
34
|
+
"foreground": "oklch(0.141 0.005 285.823)",
|
|
35
|
+
"card": "oklch(1 0 0)",
|
|
36
|
+
"card-foreground": "oklch(0.141 0.005 285.823)",
|
|
37
|
+
"popover": "oklch(1 0 0)",
|
|
38
|
+
"popover-foreground": "oklch(0.141 0.005 285.823)",
|
|
39
|
+
"primary": "oklch(0.21 0.006 285.885)",
|
|
40
|
+
"primary-foreground": "oklch(0.985 0 0)",
|
|
41
|
+
"secondary": "oklch(0.967 0.001 286.375)",
|
|
42
|
+
"secondary-foreground": "oklch(0.21 0.006 285.885)",
|
|
43
|
+
"muted": "oklch(0.967 0.001 286.375)",
|
|
44
|
+
"muted-foreground": "oklch(0.52 0.016 285.938)",
|
|
45
|
+
"accent": "oklch(0.955 0.003 286.375)",
|
|
46
|
+
"accent-foreground": "oklch(0.21 0.006 285.885)",
|
|
47
|
+
"destructive": "oklch(0.577 0.245 27.325)",
|
|
48
|
+
"border": "oklch(0.92 0.004 286.32)",
|
|
49
|
+
"input": "oklch(0.89 0.005 286.32)",
|
|
50
|
+
"ring": "oklch(0.55 0.02 286.067)",
|
|
51
|
+
"shell": "oklch(0.975 0.002 286)"
|
|
52
|
+
},
|
|
53
|
+
"dark": {
|
|
54
|
+
"background": "oklch(0.175 0.006 285.885)",
|
|
55
|
+
"foreground": "oklch(0.985 0 0)",
|
|
56
|
+
"card": "oklch(0.205 0.006 285.885)",
|
|
57
|
+
"card-foreground": "oklch(0.985 0 0)",
|
|
58
|
+
"popover": "oklch(0.205 0.006 285.885)",
|
|
59
|
+
"popover-foreground": "oklch(0.985 0 0)",
|
|
60
|
+
"primary": "oklch(0.92 0.004 286.32)",
|
|
61
|
+
"primary-foreground": "oklch(0.21 0.006 285.885)",
|
|
62
|
+
"secondary": "oklch(0.274 0.006 286.033)",
|
|
63
|
+
"secondary-foreground": "oklch(0.985 0 0)",
|
|
64
|
+
"muted": "oklch(0.255 0.006 286.033)",
|
|
65
|
+
"muted-foreground": "oklch(0.72 0.015 286.067)",
|
|
66
|
+
"accent": "oklch(0.3 0.006 286.033)",
|
|
67
|
+
"accent-foreground": "oklch(0.985 0 0)",
|
|
68
|
+
"destructive": "oklch(0.704 0.191 22.216)",
|
|
69
|
+
"border": "oklch(1 0 0 / 11%)",
|
|
70
|
+
"input": "oklch(1 0 0 / 16%)",
|
|
71
|
+
"ring": "oklch(0.65 0.02 286.067)",
|
|
72
|
+
"shell": "oklch(0.13 0.005 285.823)"
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"type": "registry:theme"
|
|
76
|
+
}
|
package/r/utils.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "utils",
|
|
4
|
+
"title": "Shared helpers",
|
|
5
|
+
"description": "The class merge helper `cn` and `copyText` (host callback, Clipboard API, then a selection copy that works over plain HTTP) shared by every Tailwind item, installed once in components/utils/.",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx@^2.1.1",
|
|
8
|
+
"tailwind-merge@^3.7.0"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "registry/utils/utils.ts",
|
|
13
|
+
"content": "import { clsx } from 'clsx';\nimport type { ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]): string { return twMerge(clsx(inputs)); }\n\n/** Thrown when no automatic copy worked; `text` is what the person must copy by hand (a viewer shows it selected). */\nexport class ManualCopyError extends Error {\n constructor(readonly text: string) { super('Copy the text by hand'); this.name = 'ManualCopyError'; }\n}\n\n/**\n * Copies `text` using the host's callback when given, else the async Clipboard API (secure contexts only), else a hidden\n * textarea and `document.execCommand('copy')` (works over plain HTTP). Throws `ManualCopyError` when none worked.\n * This is the one place this item touches the clipboard; `npm run check` keeps it that way.\n */\nexport async function copyText(text: string, onCopy?: (text: string) => Promise<void>): Promise<void> {\n if (onCopy) return onCopy(text);\n try { const clipboard = globalThis.navigator?.clipboard; if (clipboard) { await clipboard.writeText(text); return; } }\n catch { /* denied or unfocused: use the selection route */ }\n const doc = globalThis.document;\n if (doc?.body && typeof doc.execCommand === 'function') {\n const area = doc.createElement('textarea');\n area.value = text;\n area.setAttribute('readonly', '');\n area.style.cssText = 'position:fixed;top:0;left:0;width:1px;height:1px;opacity:0;pointer-events:none';\n const focus = doc.activeElement instanceof HTMLElement ? doc.activeElement : undefined;\n doc.body.append(area);\n try { area.select(); area.setSelectionRange(0, text.length); if (doc.execCommand('copy')) return; }\n catch { /* fall through */ }\n finally { area.remove(); focus?.focus({ preventScroll: true }); }\n }\n throw new ManualCopyError(text);\n}\n\n/**\n * A block's `labels` or `icons`: `defaults` with each partial override on top, later ones winning. A key an override leaves `undefined`\n * keeps the value below it, so a host can pass `{ title: maybe }` safely.\n */\nexport function withDefaults<T extends object>(defaults: T, ...overrides: readonly (Partial<T> | undefined)[]): T {\n const merged: Record<string, unknown> = { ...defaults as Record<string, unknown> };\n for (const override of overrides) for (const [key, value] of Object.entries(override ?? {})) if (value !== undefined) merged[key] = value;\n return merged as T;\n}\n",
|
|
14
|
+
"type": "registry:lib",
|
|
15
|
+
"target": "components/utils/utils.ts"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"type": "registry:lib"
|
|
19
|
+
}
|