@utopia-studio-design/design-system-cli 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +23 -0
  2. package/bin/utopia-ds-mcp.mjs +9 -2
  3. package/bin/utopia-ds.mjs +115 -7
  4. package/data/blocks/activity-feed/ActivityFeedBlock.tsx +255 -0
  5. package/data/blocks/activity-feed/README.md +16 -0
  6. package/data/blocks/activity-feed/activity-feed-contract.ts +96 -0
  7. package/data/blocks/activity-feed/activity-feed-fixture.ts +22 -0
  8. package/data/blocks/activity-feed/activity-feed.css +154 -0
  9. package/data/blocks/agent-status/AgentStatusBlock.tsx +313 -0
  10. package/data/blocks/agent-status/README.md +15 -0
  11. package/data/blocks/agent-status/agent-status-contract.ts +133 -0
  12. package/data/blocks/agent-status/agent-status-fixture.ts +79 -0
  13. package/data/blocks/agent-status/agent-status.css +216 -0
  14. package/data/blocks/analytics-dashboard/AnalyticsDashboardBlocks.tsx +107 -0
  15. package/data/blocks/analytics-dashboard/AnalyticsMetricBlocks.tsx +87 -0
  16. package/data/blocks/analytics-dashboard/README.md +17 -0
  17. package/data/blocks/analytics-dashboard/analytics-dashboard.css +92 -0
  18. package/data/blocks/calendar-application/CalendarApplicationBlocks.tsx +519 -0
  19. package/data/blocks/calendar-application/README.md +20 -0
  20. package/data/blocks/calendar-application/calendar-application.css +216 -0
  21. package/data/blocks/calendar-application/calendar-localization.ts +90 -0
  22. package/data/blocks/calendar-application/calendar-panel-contract.ts +72 -0
  23. package/data/blocks/dashboard-composer/CeramicPuckAdapter.tsx +380 -0
  24. package/data/blocks/dashboard-composer/DashboardComposerBlocks.tsx +337 -0
  25. package/data/blocks/dashboard-composer/README.md +16 -0
  26. package/data/blocks/dashboard-composer/RegistryDataBlocks.tsx +238 -0
  27. package/data/blocks/dashboard-composer/dashboard-composer.css +328 -0
  28. package/data/blocks/database-workspace/DatabaseWorkspaceBlocks.tsx +240 -0
  29. package/data/blocks/database-workspace/README.md +9 -0
  30. package/data/blocks/database-workspace/database-workspace.css +107 -0
  31. package/data/blocks/interaction-workbench/InteractionWorkbench.tsx +271 -0
  32. package/data/blocks/interaction-workbench/README.md +9 -0
  33. package/data/blocks/interaction-workbench/interaction-workbench.css +226 -0
  34. package/data/docs/calendar.md +164 -0
  35. package/data/docs/charts.md +95 -0
  36. package/data/docs/composition-registry.md +174 -0
  37. package/data/docs/dashboard-composer.md +94 -0
  38. package/data/docs/database.md +68 -0
  39. package/data/docs/examples/dashboard-layout.v1.json +65 -0
  40. package/data/docs/foundations.md +2 -1
  41. package/data/docs/interaction-foundation.md +109 -0
  42. package/data/docs/observability.md +165 -0
  43. package/data/docs/quick-start-ai.md +55 -1
  44. package/data/manifests/blocks.json +392 -0
  45. package/data/manifests/catalog.json +7 -4
  46. package/data/manifests/components.json +191 -3
  47. package/data/manifests/composition-registry.json +427 -0
  48. package/data/manifests/motion-profiles.json +1 -1
  49. package/data/manifests/patterns.json +113 -1
  50. package/data/manifests/templates.json +346 -0
  51. package/data/manifests/theme-renacore.json +142 -0
  52. package/data/manifests/themes.json +314 -28
  53. package/data/templates/analytics-dashboard/AnalyticsDashboardBlocks.tsx +107 -0
  54. package/data/templates/analytics-dashboard/AnalyticsMetricBlocks.tsx +87 -0
  55. package/data/templates/analytics-dashboard/README.md +6 -0
  56. package/data/templates/analytics-dashboard/analytics-dashboard.css +92 -0
  57. package/data/templates/analytics-dashboard/index.html +13 -0
  58. package/data/templates/analytics-dashboard/main.tsx +196 -0
  59. package/data/templates/analytics-dashboard/styles.css +120 -0
  60. package/data/templates/analytics-dashboard/template.manifest.json +17 -0
  61. package/data/templates/calendar-application/CalendarApplicationBlocks.tsx +519 -0
  62. package/data/templates/calendar-application/README.md +10 -0
  63. package/data/templates/calendar-application/calendar-application.css +216 -0
  64. package/data/templates/calendar-application/calendar-localization.ts +90 -0
  65. package/data/templates/calendar-application/index.html +14 -0
  66. package/data/templates/calendar-application/main.tsx +409 -0
  67. package/data/templates/calendar-application/styles.css +248 -0
  68. package/data/templates/calendar-application/template.manifest.json +27 -0
  69. package/data/templates/composition-registry-lab/README.md +9 -0
  70. package/data/templates/composition-registry-lab/index.html +14 -0
  71. package/data/templates/composition-registry-lab/main.tsx +217 -0
  72. package/data/templates/composition-registry-lab/sample-layout.json +63 -0
  73. package/data/templates/composition-registry-lab/styles.css +343 -0
  74. package/data/templates/composition-registry-lab/template.manifest.json +34 -0
  75. package/data/templates/dashboard-composer/ActivityFeedBlock.tsx +255 -0
  76. package/data/templates/dashboard-composer/AgentStatusBlock.tsx +313 -0
  77. package/data/templates/dashboard-composer/AnalyticsDashboardBlocks.tsx +107 -0
  78. package/data/templates/dashboard-composer/AnalyticsMetricBlocks.tsx +87 -0
  79. package/data/templates/dashboard-composer/CalendarApplicationBlocks.tsx +519 -0
  80. package/data/templates/dashboard-composer/CeramicPuckAdapter.tsx +380 -0
  81. package/data/templates/dashboard-composer/DashboardComposerBlocks.tsx +337 -0
  82. package/data/templates/dashboard-composer/DatabaseWorkspaceBlocks.tsx +240 -0
  83. package/data/templates/dashboard-composer/README.md +53 -0
  84. package/data/templates/dashboard-composer/RegistryDataBlocks.tsx +238 -0
  85. package/data/templates/dashboard-composer/activity-feed-contract.ts +96 -0
  86. package/data/templates/dashboard-composer/activity-feed-fixture.ts +22 -0
  87. package/data/templates/dashboard-composer/activity-feed.css +154 -0
  88. package/data/templates/dashboard-composer/agent-status-contract.ts +133 -0
  89. package/data/templates/dashboard-composer/agent-status-fixture.ts +79 -0
  90. package/data/templates/dashboard-composer/agent-status-lab/index.html +19 -0
  91. package/data/templates/dashboard-composer/agent-status-lab/main.tsx +151 -0
  92. package/data/templates/dashboard-composer/agent-status-lab/styles.css +139 -0
  93. package/data/templates/dashboard-composer/agent-status.css +216 -0
  94. package/data/templates/dashboard-composer/analytics-dashboard.css +92 -0
  95. package/data/templates/dashboard-composer/calendar-application.css +216 -0
  96. package/data/templates/dashboard-composer/calendar-localization.ts +90 -0
  97. package/data/templates/dashboard-composer/calendar-panel-contract.ts +72 -0
  98. package/data/templates/dashboard-composer/dashboard-composer.css +328 -0
  99. package/data/templates/dashboard-composer/database-workspace.css +107 -0
  100. package/data/templates/dashboard-composer/index.html +14 -0
  101. package/data/templates/dashboard-composer/main.tsx +550 -0
  102. package/data/templates/dashboard-composer/sample-layout.json +147 -0
  103. package/data/templates/dashboard-composer/styles.css +198 -0
  104. package/data/templates/dashboard-composer/template.manifest.json +34 -0
  105. package/data/templates/dashboard-composer/visual-editor/PuckEditorSurface.tsx +132 -0
  106. package/data/templates/dashboard-composer/visual-editor/VisualEditorApp.tsx +343 -0
  107. package/data/templates/dashboard-composer/visual-editor/index.html +17 -0
  108. package/data/templates/dashboard-composer/visual-editor/main.tsx +95 -0
  109. package/data/templates/dashboard-composer/visual-editor/styles.css +310 -0
  110. package/data/templates/database-workspace/DatabaseWorkspaceBlocks.tsx +240 -0
  111. package/data/templates/database-workspace/README.md +5 -0
  112. package/data/templates/database-workspace/database-workspace.css +107 -0
  113. package/data/templates/database-workspace/index.html +14 -0
  114. package/data/templates/database-workspace/main.tsx +247 -0
  115. package/data/templates/database-workspace/styles.css +121 -0
  116. package/data/templates/database-workspace/template.manifest.json +19 -0
  117. package/data/templates/interaction-lab/InteractionWorkbench.tsx +271 -0
  118. package/data/templates/interaction-lab/README.md +7 -0
  119. package/data/templates/interaction-lab/index.html +14 -0
  120. package/data/templates/interaction-lab/interaction-workbench.css +226 -0
  121. package/data/templates/interaction-lab/main.tsx +104 -0
  122. package/data/templates/interaction-lab/styles.css +224 -0
  123. package/data/templates/interaction-lab/template.manifest.json +36 -0
  124. package/lib/api.mjs +72 -5
  125. package/lib/composition-runtime.mjs +382 -0
  126. package/lib/telemetry.mjs +74 -0
  127. package/package.json +4 -1
@@ -0,0 +1,519 @@
1
+ import * as React from 'react'
2
+ import { X } from '@phosphor-icons/react'
3
+ import { Button } from '@utopia-studio-design/design-system/Button'
4
+ import { Calendar } from '@utopia-studio-design/design-system/DataDisplay'
5
+ import {
6
+ Checkbox,
7
+ Field,
8
+ FieldError,
9
+ FieldLabel,
10
+ NativeSelect,
11
+ NativeSelectOption,
12
+ Select,
13
+ SelectContent,
14
+ SelectItem,
15
+ SelectTrigger,
16
+ SelectValue,
17
+ TextInput,
18
+ } from '@utopia-studio-design/design-system/Forms'
19
+ import { PhosphorIcon } from '@utopia-studio-design/design-system/PhosphorIcon'
20
+ import {
21
+ AgendaView,
22
+ SchedulerRoot,
23
+ addSchedulerDays,
24
+ expandRecurringEvents,
25
+ startOfSchedulerWeek,
26
+ type SchedulerEvent,
27
+ type SchedulerEventEditHandler,
28
+ type SchedulerLabelOverrides,
29
+ type SchedulerResource,
30
+ type SchedulerRootProps,
31
+ type SchedulerWeekDay,
32
+ } from '@utopia-studio-design/design-system/Calendar'
33
+ import './calendar-application.css'
34
+
35
+ export type SchedulerShellProps = SchedulerRootProps
36
+
37
+ export function SchedulerShell(props: SchedulerShellProps) {
38
+ return <SchedulerRoot {...props} />
39
+ }
40
+
41
+ export interface CalendarSidebarProps extends React.HTMLAttributes<HTMLElement> {
42
+ date: Date
43
+ labels?: {
44
+ ariaLabel?: string
45
+ jumpToDate?: string
46
+ schedule?: string
47
+ calendars?: string
48
+ }
49
+ locale?: string
50
+ onDateChange?: (date: Date) => void
51
+ resources?: SchedulerResource[]
52
+ selectedResourceIds?: string[]
53
+ onResourceToggle?: (resourceId: string) => void
54
+ weekStartsOn?: SchedulerWeekDay
55
+ }
56
+
57
+ export function CalendarSidebar({
58
+ date,
59
+ labels = {},
60
+ locale = 'en-US',
61
+ onDateChange,
62
+ onResourceToggle,
63
+ resources = [],
64
+ selectedResourceIds = resources.map((resource) => resource.id),
65
+ weekStartsOn = 1,
66
+ ...props
67
+ }: CalendarSidebarProps) {
68
+ return (
69
+ <aside aria-label={labels.ariaLabel ?? 'Calendar sidebar'} className="ceramic-calendar-sidebar" {...props}>
70
+ <div className="ceramic-calendar-sidebar-heading">
71
+ <span>{labels.schedule ?? 'Schedule'}</span>
72
+ <strong>{new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }).format(date)}</strong>
73
+ </div>
74
+ <Calendar
75
+ aria-label={labels.jumpToDate ?? 'Jump to date'}
76
+ captionLayout="label"
77
+ className="ceramic-calendar-mini"
78
+ locale={locale}
79
+ month={date}
80
+ selectedDate={date}
81
+ weekStartsOn={weekStartsOn}
82
+ onMonthChange={onDateChange}
83
+ onSelect={onDateChange}
84
+ />
85
+ <fieldset>
86
+ <legend>{labels.calendars ?? 'My calendars'}</legend>
87
+ {resources.map((resource) => (
88
+ <label key={resource.id}>
89
+ <Checkbox
90
+ aria-label={resource.label}
91
+ checked={selectedResourceIds.includes(resource.id)}
92
+ onCheckedChange={() => onResourceToggle?.(resource.id)}
93
+ />
94
+ <span>
95
+ <i aria-hidden="true" data-resource={resource.id} />
96
+ {resource.label}
97
+ </span>
98
+ </label>
99
+ ))}
100
+ </fieldset>
101
+ </aside>
102
+ )
103
+ }
104
+
105
+ export interface TimezoneSelectorProps {
106
+ className?: string
107
+ label?: string
108
+ onValueChange?: (value: string) => void
109
+ timezones: string[]
110
+ value: string
111
+ }
112
+
113
+ export function TimezoneSelector({
114
+ className,
115
+ label = 'Display timezone',
116
+ onValueChange,
117
+ timezones,
118
+ value,
119
+ }: TimezoneSelectorProps) {
120
+ const labelId = React.useId()
121
+ return (
122
+ <Field className={`ceramic-timezone-selector ${className ?? ''}`.trim()}>
123
+ <FieldLabel id={labelId}>{label}</FieldLabel>
124
+ <Select value={value} onValueChange={onValueChange}>
125
+ <SelectTrigger aria-labelledby={labelId}>
126
+ <SelectValue />
127
+ </SelectTrigger>
128
+ <SelectContent>
129
+ {timezones.map((timezone) => <SelectItem key={timezone} value={timezone}>{timezone}</SelectItem>)}
130
+ </SelectContent>
131
+ </Select>
132
+ </Field>
133
+ )
134
+ }
135
+
136
+ export interface AgendaPanelProps extends React.HTMLAttributes<HTMLElement> {
137
+ calendarSystem?: string
138
+ date: Date
139
+ events: SchedulerEvent[]
140
+ labels?: SchedulerLabelOverrides
141
+ locale?: string
142
+ numberingSystem?: string
143
+ onEventEdit?: SchedulerEventEditHandler
144
+ timezone: string
145
+ weekendDays?: SchedulerWeekDay[]
146
+ weekStartsOn?: SchedulerWeekDay
147
+ }
148
+
149
+ export function AgendaPanel({
150
+ calendarSystem,
151
+ date,
152
+ events,
153
+ labels,
154
+ locale,
155
+ numberingSystem,
156
+ onEventEdit,
157
+ timezone,
158
+ weekendDays,
159
+ weekStartsOn,
160
+ ...props
161
+ }: AgendaPanelProps) {
162
+ const rangeStart = startOfSchedulerWeek(date, weekStartsOn)
163
+ const expandedEvents = expandRecurringEvents(events, rangeStart, addSchedulerDays(rangeStart, 7))
164
+ return (
165
+ <aside className="ceramic-agenda-panel" {...props}>
166
+ <AgendaView
167
+ calendarSystem={calendarSystem}
168
+ date={date}
169
+ events={expandedEvents}
170
+ labels={labels}
171
+ locale={locale}
172
+ numberingSystem={numberingSystem}
173
+ timezone={timezone}
174
+ weekendDays={weekendDays}
175
+ weekStartsOn={weekStartsOn}
176
+ visible
177
+ onEventEdit={onEventEdit}
178
+ />
179
+ </aside>
180
+ )
181
+ }
182
+
183
+ export interface EventEditorDrawerProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onSave'> {
184
+ canDelete?: boolean
185
+ event: SchedulerEvent | null
186
+ labels?: Partial<EventEditorLabels>
187
+ onClose?: () => void
188
+ onDelete?: (event: SchedulerEvent) => void
189
+ onSave?: (event: SchedulerEvent, scope: 'occurrence' | 'series') => void | Promise<void>
190
+ open: boolean
191
+ returnFocusRef?: React.RefObject<HTMLElement | null>
192
+ }
193
+
194
+ export interface EventEditorLabels {
195
+ ariaLabel: string
196
+ eyebrow: string
197
+ newEvent: string
198
+ close: string
199
+ title: string
200
+ starts: string
201
+ ends: string
202
+ allDay: string
203
+ applyTo: string
204
+ occurrence: string
205
+ series: string
206
+ save: string
207
+ saving: string
208
+ cancel: string
209
+ delete: string
210
+ validationSummary: string
211
+ titleRequired: string
212
+ startRequired: string
213
+ endRequired: string
214
+ invalidDateTime: string
215
+ endAfterStart: string
216
+ saveError: string
217
+ }
218
+
219
+ const defaultEditorLabels: EventEditorLabels = {
220
+ ariaLabel: 'Event editor',
221
+ eyebrow: 'Calendar event',
222
+ newEvent: 'New event',
223
+ close: 'Close event editor',
224
+ title: 'Title',
225
+ starts: 'Starts',
226
+ ends: 'Ends',
227
+ allDay: 'All-day event',
228
+ applyTo: 'Apply change to',
229
+ occurrence: 'This occurrence',
230
+ series: 'Entire series',
231
+ save: 'Save event',
232
+ saving: 'Saving event…',
233
+ cancel: 'Cancel',
234
+ delete: 'Delete event',
235
+ validationSummary: 'Check the highlighted fields.',
236
+ titleRequired: 'Enter an event title.',
237
+ startRequired: 'Enter a start date and time.',
238
+ endRequired: 'Enter an end date and time.',
239
+ invalidDateTime: 'Enter a valid date and time.',
240
+ endAfterStart: 'End must be after start.',
241
+ saveError: "The event couldn't be saved. Your changes are still here.",
242
+ }
243
+
244
+ function toLocalInput(value: string) {
245
+ const date = new Date(value)
246
+ if (Number.isNaN(date.valueOf())) return ''
247
+ const offset = date.getTimezoneOffset() * 60_000
248
+ return new Date(date.valueOf() - offset).toISOString().slice(0, 16)
249
+ }
250
+
251
+ function parseLocalInput(value: string) {
252
+ if (!value) return null
253
+ const date = new Date(value)
254
+ return Number.isNaN(date.valueOf()) ? null : date
255
+ }
256
+
257
+ type EventEditorErrors = Partial<Record<'title' | 'start' | 'end', string>>
258
+
259
+ export function EventEditorDrawer({
260
+ canDelete = true,
261
+ event,
262
+ labels: labelOverrides,
263
+ onClose,
264
+ onDelete,
265
+ onSave,
266
+ open,
267
+ returnFocusRef,
268
+ ...props
269
+ }: EventEditorDrawerProps) {
270
+ const labels = { ...defaultEditorLabels, ...labelOverrides }
271
+ const drawerRef = React.useRef<HTMLElement>(null)
272
+ const closeRef = React.useRef<HTMLButtonElement>(null)
273
+ const titleRef = React.useRef<HTMLInputElement>(null)
274
+ const startRef = React.useRef<HTMLInputElement>(null)
275
+ const endRef = React.useRef<HTMLInputElement>(null)
276
+ const saveErrorRef = React.useRef<HTMLDivElement>(null)
277
+ const triggerRef = React.useRef<HTMLElement | null>(null)
278
+ const openRef = React.useRef(open)
279
+ openRef.current = open
280
+ const titleId = React.useId()
281
+ const startId = React.useId()
282
+ const endId = React.useId()
283
+ const scopeId = React.useId()
284
+ const [draft, setDraft] = React.useState(event)
285
+ const [titleInput, setTitleInput] = React.useState(event?.title ?? '')
286
+ const [startInput, setStartInput] = React.useState(event ? toLocalInput(event.start) : '')
287
+ const [endInput, setEndInput] = React.useState(event ? toLocalInput(event.end) : '')
288
+ const [scope, setScope] = React.useState<'occurrence' | 'series'>('occurrence')
289
+ const [errors, setErrors] = React.useState<EventEditorErrors>({})
290
+ const [saveError, setSaveError] = React.useState(false)
291
+ const [isSaving, setIsSaving] = React.useState(false)
292
+ React.useEffect(() => {
293
+ setDraft(event)
294
+ setTitleInput(event?.title ?? '')
295
+ setStartInput(event ? toLocalInput(event.start) : '')
296
+ setEndInput(event ? toLocalInput(event.end) : '')
297
+ setScope('occurrence')
298
+ setErrors({})
299
+ setSaveError(false)
300
+ setIsSaving(false)
301
+ }, [event])
302
+ React.useLayoutEffect(() => {
303
+ if (!open) return
304
+ const activeElement = returnFocusRef?.current
305
+ ?? (document.activeElement instanceof HTMLElement ? document.activeElement : null)
306
+ if (activeElement && !drawerRef.current?.contains(activeElement)) triggerRef.current = activeElement
307
+ const scrollX = window.scrollX
308
+ const scrollY = window.scrollY
309
+ const bodyStyles = {
310
+ inlineSize: document.body.style.inlineSize,
311
+ inset: document.body.style.inset,
312
+ overflow: document.body.style.overflow,
313
+ position: document.body.style.position,
314
+ }
315
+ document.body.style.position = 'fixed'
316
+ document.body.style.inset = '0'
317
+ document.body.style.inlineSize = '100%'
318
+ document.body.style.overflow = 'hidden'
319
+ return () => {
320
+ document.body.style.position = bodyStyles.position
321
+ document.body.style.inset = bodyStyles.inset
322
+ document.body.style.inlineSize = bodyStyles.inlineSize
323
+ document.body.style.overflow = bodyStyles.overflow
324
+ window.scrollTo(scrollX, scrollY)
325
+ const trigger = triggerRef.current
326
+ window.requestAnimationFrame(() => {
327
+ if (!openRef.current && trigger?.isConnected && !document.querySelector('.ceramic-event-editor[role="dialog"]')) {
328
+ trigger.focus()
329
+ if (triggerRef.current === trigger) triggerRef.current = null
330
+ }
331
+ })
332
+ }
333
+ }, [open, returnFocusRef])
334
+ React.useLayoutEffect(() => {
335
+ if (open && draft) closeRef.current?.focus()
336
+ }, [draft?.id, open])
337
+ React.useLayoutEffect(() => {
338
+ if (saveError) saveErrorRef.current?.focus()
339
+ }, [saveError])
340
+ if (!open || !draft) return null
341
+ const focusableSelector = 'button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
342
+ const closeEditor = () => {
343
+ if (!isSaving) onClose?.()
344
+ }
345
+ const clearError = (field: keyof EventEditorErrors) => {
346
+ setErrors((current) => {
347
+ if (!current[field]) return current
348
+ const next = { ...current }
349
+ delete next[field]
350
+ return next
351
+ })
352
+ setSaveError(false)
353
+ }
354
+ const handleSubmit = async (submitEvent: React.FormEvent<HTMLFormElement>) => {
355
+ submitEvent.preventDefault()
356
+ if (isSaving) return
357
+
358
+ const nextErrors: EventEditorErrors = {}
359
+ const title = titleInput.trim()
360
+ const start = parseLocalInput(startInput)
361
+ const end = parseLocalInput(endInput)
362
+ if (!title) nextErrors.title = labels.titleRequired
363
+ if (!startInput) nextErrors.start = labels.startRequired
364
+ else if (!start) nextErrors.start = labels.invalidDateTime
365
+ if (!endInput) nextErrors.end = labels.endRequired
366
+ else if (!end) nextErrors.end = labels.invalidDateTime
367
+ else if (start && end <= start) nextErrors.end = labels.endAfterStart
368
+
369
+ setErrors(nextErrors)
370
+ setSaveError(false)
371
+ if (Object.keys(nextErrors).length > 0) {
372
+ window.requestAnimationFrame(() => {
373
+ if (nextErrors.title) titleRef.current?.focus()
374
+ else if (nextErrors.start) startRef.current?.focus()
375
+ else endRef.current?.focus()
376
+ })
377
+ return
378
+ }
379
+
380
+ const nextDraft = {
381
+ ...draft,
382
+ title,
383
+ start: start!.toISOString(),
384
+ end: end!.toISOString(),
385
+ }
386
+ setDraft(nextDraft)
387
+ setIsSaving(true)
388
+ try {
389
+ await onSave?.(nextDraft, scope)
390
+ } catch {
391
+ setSaveError(true)
392
+ } finally {
393
+ setIsSaving(false)
394
+ }
395
+ }
396
+ return (
397
+ <>
398
+ <div aria-hidden="true" className="ceramic-event-editor-backdrop" onClick={closeEditor} />
399
+ <aside
400
+ aria-label={labels.ariaLabel}
401
+ aria-modal="true"
402
+ aria-busy={isSaving || undefined}
403
+ className="ceramic-event-editor"
404
+ ref={drawerRef}
405
+ role="dialog"
406
+ onKeyDown={(keyboardEvent) => {
407
+ if (keyboardEvent.key === 'Escape') {
408
+ keyboardEvent.preventDefault()
409
+ closeEditor()
410
+ }
411
+ if (keyboardEvent.key !== 'Tab') return
412
+ const focusable = [...(drawerRef.current?.querySelectorAll<HTMLElement>(focusableSelector) ?? [])]
413
+ const first = focusable[0]
414
+ const last = focusable.at(-1)
415
+ if (keyboardEvent.shiftKey && document.activeElement === first) {
416
+ keyboardEvent.preventDefault()
417
+ last?.focus()
418
+ } else if (!keyboardEvent.shiftKey && document.activeElement === last) {
419
+ keyboardEvent.preventDefault()
420
+ first?.focus()
421
+ }
422
+ }}
423
+ {...props}
424
+ >
425
+ <header>
426
+ <div><span>{labels.eyebrow}</span><h2><bdi>{draft.title || labels.newEvent}</bdi></h2></div>
427
+ <Button ref={closeRef} aria-label={labels.close} disabled={isSaving} isIconOnly variant="ghost" onClick={closeEditor}>
428
+ <PhosphorIcon decorative icon={X} size="sm" />
429
+ </Button>
430
+ </header>
431
+ <form aria-busy={isSaving || undefined} noValidate onSubmit={handleSubmit}>
432
+ {Object.keys(errors).length > 0 ? (
433
+ <div className="ceramic-event-editor-alert" role="alert">{labels.validationSummary}</div>
434
+ ) : null}
435
+ {saveError ? (
436
+ <div className="ceramic-event-editor-alert" ref={saveErrorRef} role="alert" tabIndex={-1}>
437
+ {labels.saveError}
438
+ </div>
439
+ ) : null}
440
+ <Field>
441
+ <FieldLabel htmlFor={titleId}>{labels.title}</FieldLabel>
442
+ <TextInput
443
+ aria-describedby={errors.title ? `${titleId}-error` : undefined}
444
+ aria-invalid={Boolean(errors.title)}
445
+ disabled={isSaving}
446
+ id={titleId}
447
+ ref={titleRef}
448
+ required
449
+ value={titleInput}
450
+ onChange={(changeEvent) => {
451
+ setTitleInput(changeEvent.target.value)
452
+ clearError('title')
453
+ }}
454
+ />
455
+ <FieldError id={`${titleId}-error`}>{errors.title}</FieldError>
456
+ </Field>
457
+ <Field>
458
+ <FieldLabel htmlFor={startId}>{labels.starts}</FieldLabel>
459
+ <TextInput
460
+ aria-describedby={errors.start ? `${startId}-error` : undefined}
461
+ aria-invalid={Boolean(errors.start)}
462
+ disabled={isSaving}
463
+ id={startId}
464
+ ref={startRef}
465
+ required
466
+ type="datetime-local"
467
+ value={startInput}
468
+ onChange={(changeEvent) => {
469
+ setStartInput(changeEvent.target.value)
470
+ clearError('start')
471
+ }}
472
+ />
473
+ <FieldError id={`${startId}-error`}>{errors.start}</FieldError>
474
+ </Field>
475
+ <Field>
476
+ <FieldLabel htmlFor={endId}>{labels.ends}</FieldLabel>
477
+ <TextInput
478
+ aria-describedby={errors.end ? `${endId}-error` : undefined}
479
+ aria-invalid={Boolean(errors.end)}
480
+ disabled={isSaving}
481
+ id={endId}
482
+ ref={endRef}
483
+ required
484
+ type="datetime-local"
485
+ value={endInput}
486
+ onChange={(changeEvent) => {
487
+ setEndInput(changeEvent.target.value)
488
+ clearError('end')
489
+ }}
490
+ />
491
+ <FieldError id={`${endId}-error`}>{errors.end}</FieldError>
492
+ </Field>
493
+ <label className="ceramic-event-checkbox">
494
+ <Checkbox disabled={isSaving} checked={Boolean(draft.allDay)} onCheckedChange={(checked) => setDraft({ ...draft, allDay: checked === true })} />
495
+ <span>{labels.allDay}</span>
496
+ </label>
497
+ {draft.recurrenceId || draft.recurrence ? (
498
+ <Field>
499
+ <FieldLabel htmlFor={scopeId}>{labels.applyTo}</FieldLabel>
500
+ <NativeSelect disabled={isSaving} id={scopeId} value={scope} onChange={(changeEvent) => setScope(changeEvent.target.value as 'occurrence' | 'series')}>
501
+ <NativeSelectOption value="occurrence">{labels.occurrence}</NativeSelectOption>
502
+ <NativeSelectOption value="series">{labels.series}</NativeSelectOption>
503
+ </NativeSelect>
504
+ </Field>
505
+ ) : null}
506
+ <div className="ceramic-event-editor-actions">
507
+ <div>
508
+ <Button loading={isSaving} loadingText={labels.saving} type="submit">{labels.save}</Button>
509
+ <Button disabled={isSaving} type="button" variant="outline" onClick={closeEditor}>{labels.cancel}</Button>
510
+ </div>
511
+ {canDelete ? (
512
+ <Button disabled={isSaving} type="button" variant="destructive" onClick={() => onDelete?.(draft)}>{labels.delete}</Button>
513
+ ) : null}
514
+ </div>
515
+ </form>
516
+ </aside>
517
+ </>
518
+ )
519
+ }
@@ -0,0 +1,10 @@
1
+ # Calendar Application
2
+
3
+ Product-first Ceramic scheduling workspace composed from the Shadcn-derived
4
+ Ceramic Calendar, Button, ButtonGroup, IconButton, Select, and form families.
5
+ It includes Month, Week, Day, and Agenda views, overlaps, recurrence, event
6
+ editing, conflict recovery, resources, permissions, timezone switching, RTL,
7
+ dark mode, and mobile day/agenda transformation.
8
+
9
+ The collapsed `Test states` section is a QA lab and is not part of the primary
10
+ calendar workflow.