@utopia-studio-design/design-system-cli 0.4.1 → 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.
- package/README.md +23 -0
- package/bin/utopia-ds-mcp.mjs +9 -2
- package/bin/utopia-ds.mjs +106 -8
- package/data/blocks/activity-feed/ActivityFeedBlock.tsx +255 -0
- package/data/blocks/activity-feed/README.md +16 -0
- package/data/blocks/activity-feed/activity-feed-contract.ts +96 -0
- package/data/blocks/activity-feed/activity-feed-fixture.ts +22 -0
- package/data/blocks/activity-feed/activity-feed.css +154 -0
- package/data/blocks/agent-status/AgentStatusBlock.tsx +313 -0
- package/data/blocks/agent-status/README.md +15 -0
- package/data/blocks/agent-status/agent-status-contract.ts +133 -0
- package/data/blocks/agent-status/agent-status-fixture.ts +79 -0
- package/data/blocks/agent-status/agent-status.css +216 -0
- package/data/blocks/analytics-dashboard/AnalyticsDashboardBlocks.tsx +107 -0
- package/data/blocks/analytics-dashboard/AnalyticsMetricBlocks.tsx +87 -0
- package/data/blocks/analytics-dashboard/README.md +17 -0
- package/data/blocks/analytics-dashboard/analytics-dashboard.css +92 -0
- package/data/blocks/calendar-application/CalendarApplicationBlocks.tsx +519 -0
- package/data/blocks/calendar-application/README.md +20 -0
- package/data/blocks/calendar-application/calendar-application.css +216 -0
- package/data/blocks/calendar-application/calendar-localization.ts +90 -0
- package/data/blocks/calendar-application/calendar-panel-contract.ts +72 -0
- package/data/blocks/dashboard-composer/CeramicPuckAdapter.tsx +380 -0
- package/data/blocks/dashboard-composer/DashboardComposerBlocks.tsx +337 -0
- package/data/blocks/dashboard-composer/README.md +16 -0
- package/data/blocks/dashboard-composer/RegistryDataBlocks.tsx +238 -0
- package/data/blocks/dashboard-composer/dashboard-composer.css +328 -0
- package/data/blocks/database-workspace/DatabaseWorkspaceBlocks.tsx +240 -0
- package/data/blocks/database-workspace/README.md +9 -0
- package/data/blocks/database-workspace/database-workspace.css +107 -0
- package/data/blocks/interaction-workbench/InteractionWorkbench.tsx +271 -0
- package/data/blocks/interaction-workbench/README.md +9 -0
- package/data/blocks/interaction-workbench/interaction-workbench.css +226 -0
- package/data/docs/calendar.md +164 -0
- package/data/docs/charts.md +95 -0
- package/data/docs/composition-registry.md +174 -0
- package/data/docs/dashboard-composer.md +94 -0
- package/data/docs/database.md +68 -0
- package/data/docs/examples/dashboard-layout.v1.json +65 -0
- package/data/docs/foundations.md +1 -1
- package/data/docs/interaction-foundation.md +109 -0
- package/data/docs/quick-start-ai.md +23 -1
- package/data/manifests/blocks.json +392 -0
- package/data/manifests/catalog.json +7 -4
- package/data/manifests/components.json +179 -0
- package/data/manifests/composition-registry.json +427 -0
- package/data/manifests/motion-profiles.json +1 -1
- package/data/manifests/patterns.json +113 -1
- package/data/manifests/templates.json +346 -0
- package/data/manifests/theme-renacore.json +142 -0
- package/data/manifests/themes.json +314 -28
- package/data/templates/analytics-dashboard/AnalyticsDashboardBlocks.tsx +107 -0
- package/data/templates/analytics-dashboard/AnalyticsMetricBlocks.tsx +87 -0
- package/data/templates/analytics-dashboard/README.md +6 -0
- package/data/templates/analytics-dashboard/analytics-dashboard.css +92 -0
- package/data/templates/analytics-dashboard/index.html +13 -0
- package/data/templates/analytics-dashboard/main.tsx +196 -0
- package/data/templates/analytics-dashboard/styles.css +120 -0
- package/data/templates/analytics-dashboard/template.manifest.json +17 -0
- package/data/templates/calendar-application/CalendarApplicationBlocks.tsx +519 -0
- package/data/templates/calendar-application/README.md +10 -0
- package/data/templates/calendar-application/calendar-application.css +216 -0
- package/data/templates/calendar-application/calendar-localization.ts +90 -0
- package/data/templates/calendar-application/index.html +14 -0
- package/data/templates/calendar-application/main.tsx +409 -0
- package/data/templates/calendar-application/styles.css +248 -0
- package/data/templates/calendar-application/template.manifest.json +27 -0
- package/data/templates/composition-registry-lab/README.md +9 -0
- package/data/templates/composition-registry-lab/index.html +14 -0
- package/data/templates/composition-registry-lab/main.tsx +217 -0
- package/data/templates/composition-registry-lab/sample-layout.json +63 -0
- package/data/templates/composition-registry-lab/styles.css +343 -0
- package/data/templates/composition-registry-lab/template.manifest.json +34 -0
- package/data/templates/dashboard-composer/ActivityFeedBlock.tsx +255 -0
- package/data/templates/dashboard-composer/AgentStatusBlock.tsx +313 -0
- package/data/templates/dashboard-composer/AnalyticsDashboardBlocks.tsx +107 -0
- package/data/templates/dashboard-composer/AnalyticsMetricBlocks.tsx +87 -0
- package/data/templates/dashboard-composer/CalendarApplicationBlocks.tsx +519 -0
- package/data/templates/dashboard-composer/CeramicPuckAdapter.tsx +380 -0
- package/data/templates/dashboard-composer/DashboardComposerBlocks.tsx +337 -0
- package/data/templates/dashboard-composer/DatabaseWorkspaceBlocks.tsx +240 -0
- package/data/templates/dashboard-composer/README.md +53 -0
- package/data/templates/dashboard-composer/RegistryDataBlocks.tsx +238 -0
- package/data/templates/dashboard-composer/activity-feed-contract.ts +96 -0
- package/data/templates/dashboard-composer/activity-feed-fixture.ts +22 -0
- package/data/templates/dashboard-composer/activity-feed.css +154 -0
- package/data/templates/dashboard-composer/agent-status-contract.ts +133 -0
- package/data/templates/dashboard-composer/agent-status-fixture.ts +79 -0
- package/data/templates/dashboard-composer/agent-status-lab/index.html +19 -0
- package/data/templates/dashboard-composer/agent-status-lab/main.tsx +151 -0
- package/data/templates/dashboard-composer/agent-status-lab/styles.css +139 -0
- package/data/templates/dashboard-composer/agent-status.css +216 -0
- package/data/templates/dashboard-composer/analytics-dashboard.css +92 -0
- package/data/templates/dashboard-composer/calendar-application.css +216 -0
- package/data/templates/dashboard-composer/calendar-localization.ts +90 -0
- package/data/templates/dashboard-composer/calendar-panel-contract.ts +72 -0
- package/data/templates/dashboard-composer/dashboard-composer.css +328 -0
- package/data/templates/dashboard-composer/database-workspace.css +107 -0
- package/data/templates/dashboard-composer/index.html +14 -0
- package/data/templates/dashboard-composer/main.tsx +550 -0
- package/data/templates/dashboard-composer/sample-layout.json +147 -0
- package/data/templates/dashboard-composer/styles.css +198 -0
- package/data/templates/dashboard-composer/template.manifest.json +34 -0
- package/data/templates/dashboard-composer/visual-editor/PuckEditorSurface.tsx +132 -0
- package/data/templates/dashboard-composer/visual-editor/VisualEditorApp.tsx +343 -0
- package/data/templates/dashboard-composer/visual-editor/index.html +17 -0
- package/data/templates/dashboard-composer/visual-editor/main.tsx +95 -0
- package/data/templates/dashboard-composer/visual-editor/styles.css +310 -0
- package/data/templates/database-workspace/DatabaseWorkspaceBlocks.tsx +240 -0
- package/data/templates/database-workspace/README.md +5 -0
- package/data/templates/database-workspace/database-workspace.css +107 -0
- package/data/templates/database-workspace/index.html +14 -0
- package/data/templates/database-workspace/main.tsx +247 -0
- package/data/templates/database-workspace/styles.css +121 -0
- package/data/templates/database-workspace/template.manifest.json +19 -0
- package/data/templates/interaction-lab/InteractionWorkbench.tsx +271 -0
- package/data/templates/interaction-lab/README.md +7 -0
- package/data/templates/interaction-lab/index.html +14 -0
- package/data/templates/interaction-lab/interaction-workbench.css +226 -0
- package/data/templates/interaction-lab/main.tsx +104 -0
- package/data/templates/interaction-lab/styles.css +224 -0
- package/data/templates/interaction-lab/template.manifest.json +36 -0
- package/lib/api.mjs +72 -5
- package/lib/composition-runtime.mjs +382 -0
- 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,20 @@
|
|
|
1
|
+
# Calendar application blocks
|
|
2
|
+
|
|
3
|
+
Copyable Ceramic blocks for application scheduling: `SchedulerShell`,
|
|
4
|
+
`CalendarSidebar`, `EventEditorDrawer`, `AgendaPanel`, and
|
|
5
|
+
`TimezoneSelector`. Product code owns persistence through controlled events and
|
|
6
|
+
the scheduler mutation adapter.
|
|
7
|
+
|
|
8
|
+
`EventEditorDrawer` keeps incomplete date/time text in a local draft, validates
|
|
9
|
+
required values and chronological order, and accepts synchronous or asynchronous
|
|
10
|
+
`onSave` handlers. Rejected saves preserve the draft for retry. Pass
|
|
11
|
+
`canDelete={false}` for unsaved events and connect `returnFocusRef` to the
|
|
12
|
+
invoking event or create control so pointer-triggered drawers restore focus in
|
|
13
|
+
Safari as well as Chromium.
|
|
14
|
+
|
|
15
|
+
`calendar-panel-contract.ts` is the registry-to-runtime boundary used by both
|
|
16
|
+
the Dashboard Composer and Puck adapter. It maps only registered semantic
|
|
17
|
+
configuration—view, timezone, locale, direction mode, week start, secondary
|
|
18
|
+
calendar, initial scroll hour, and agenda alternative—to `SchedulerRoot`.
|
|
19
|
+
Events, permissions, persistence adapters, and localized product data remain
|
|
20
|
+
runtime-owned.
|