@weft-js/warp 0.0.0-stage → 0.1.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.
Files changed (180) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +55 -2
  3. package/package.json +43 -4
  4. package/plugin.d.mts +30 -0
  5. package/plugin.mjs +358 -0
  6. package/src/Accordion.tsx +58 -0
  7. package/src/Alert.tsx +25 -0
  8. package/src/Badge.tsx +17 -0
  9. package/src/Button.tsx +100 -0
  10. package/src/Calendar.tsx +162 -0
  11. package/src/Canvas.tsx +110 -0
  12. package/src/Card.tsx +29 -0
  13. package/src/Chart.tsx +133 -0
  14. package/src/Charts.tsx +69 -0
  15. package/src/Chip.tsx +27 -0
  16. package/src/Choice.tsx +105 -0
  17. package/src/CodeBlock.tsx +65 -0
  18. package/src/CodeInput.tsx +130 -0
  19. package/src/Combobox.tsx +113 -0
  20. package/src/CommandPalette.tsx +348 -0
  21. package/src/DataGrid.tsx +561 -0
  22. package/src/DateField.tsx +221 -0
  23. package/src/Dither.tsx +129 -0
  24. package/src/Drawer.tsx +41 -0
  25. package/src/DurationField.tsx +98 -0
  26. package/src/Feed.tsx +74 -0
  27. package/src/Field.tsx +147 -0
  28. package/src/Icon.tsx +8 -0
  29. package/src/Kbd.tsx +6 -0
  30. package/src/KeyValueEditor.tsx +114 -0
  31. package/src/Layout.tsx +109 -0
  32. package/src/Markdown.tsx +63 -0
  33. package/src/MentionsInput.tsx +150 -0
  34. package/src/Menu.tsx +111 -0
  35. package/src/Modal.tsx +40 -0
  36. package/src/MonthPicker.tsx +190 -0
  37. package/src/Nav.tsx +20 -0
  38. package/src/NumberField.tsx +78 -0
  39. package/src/Pagination.tsx +122 -0
  40. package/src/PaymentCard.tsx +63 -0
  41. package/src/Picker.tsx +120 -0
  42. package/src/PlaceField.tsx +126 -0
  43. package/src/Popover.tsx +34 -0
  44. package/src/Progress.tsx +26 -0
  45. package/src/RecurrenceField.tsx +126 -0
  46. package/src/RichSelect.tsx +243 -0
  47. package/src/RichTextEditor.tsx +207 -0
  48. package/src/ScrollGrid.tsx +190 -0
  49. package/src/Segments.tsx +70 -0
  50. package/src/Select.tsx +192 -0
  51. package/src/Shape.tsx +20 -0
  52. package/src/Shell.tsx +308 -0
  53. package/src/SideNav.tsx +147 -0
  54. package/src/SignaturePad.tsx +120 -0
  55. package/src/Skeleton.tsx +14 -0
  56. package/src/Slider.tsx +42 -0
  57. package/src/Spinner.tsx +7 -0
  58. package/src/Table.tsx +528 -0
  59. package/src/Tabs.tsx +67 -0
  60. package/src/Tag.tsx +15 -0
  61. package/src/Ticker.tsx +36 -0
  62. package/src/TimeField.tsx +237 -0
  63. package/src/Toast.tsx +56 -0
  64. package/src/Toggle.tsx +42 -0
  65. package/src/Tooltip.tsx +32 -0
  66. package/src/Type.tsx +46 -0
  67. package/src/VirtualList.tsx +386 -0
  68. package/src/admin/ApiKeys.tsx +136 -0
  69. package/src/admin/Audit.tsx +93 -0
  70. package/src/admin/Billing.tsx +123 -0
  71. package/src/admin/Cart.tsx +96 -0
  72. package/src/admin/Checkout.tsx +300 -0
  73. package/src/admin/CsvImport.tsx +174 -0
  74. package/src/admin/Danger.tsx +110 -0
  75. package/src/admin/Integrations.tsx +200 -0
  76. package/src/admin/InviteDialog.tsx +64 -0
  77. package/src/admin/Members.tsx +186 -0
  78. package/src/admin/Order.tsx +63 -0
  79. package/src/admin/PlainTable.tsx +22 -0
  80. package/src/admin/Plans.tsx +167 -0
  81. package/src/admin/SaveBar.tsx +112 -0
  82. package/src/admin/Secret.tsx +37 -0
  83. package/src/admin/Settings.tsx +76 -0
  84. package/src/admin/StepBar.tsx +35 -0
  85. package/src/admin/TagsInput.tsx +79 -0
  86. package/src/admin/atoms.tsx +33 -0
  87. package/src/admin/index.ts +16 -0
  88. package/src/agent/Actions.tsx +85 -0
  89. package/src/agent/AgentPlan.tsx +49 -0
  90. package/src/agent/ArtifactPanel.tsx +80 -0
  91. package/src/agent/Assistant.tsx +96 -0
  92. package/src/agent/Checkpoint.tsx +14 -0
  93. package/src/agent/Cite.tsx +39 -0
  94. package/src/agent/ContextMeter.tsx +31 -0
  95. package/src/agent/Conversation.tsx +34 -0
  96. package/src/agent/Message.tsx +53 -0
  97. package/src/agent/ModelPicker.tsx +65 -0
  98. package/src/agent/Prompt.tsx +197 -0
  99. package/src/agent/Prose.tsx +54 -0
  100. package/src/agent/Question.tsx +52 -0
  101. package/src/agent/Reasoning.tsx +19 -0
  102. package/src/agent/Shimmer.tsx +7 -0
  103. package/src/agent/Sources.tsx +23 -0
  104. package/src/agent/SpeechInput.tsx +102 -0
  105. package/src/agent/Suggestions.tsx +22 -0
  106. package/src/agent/ToolCall.tsx +91 -0
  107. package/src/agent/TraceViewer.tsx +55 -0
  108. package/src/agent/bits.tsx +40 -0
  109. package/src/agent/index.ts +22 -0
  110. package/src/bind.ts +42 -0
  111. package/src/collab/AutoTrack.tsx +42 -0
  112. package/src/collab/Comments.tsx +94 -0
  113. package/src/collab/Composer.tsx +173 -0
  114. package/src/collab/ConversationList.tsx +73 -0
  115. package/src/collab/Cursors.tsx +35 -0
  116. package/src/collab/DataIntegrity.tsx +45 -0
  117. package/src/collab/Kanban.tsx +185 -0
  118. package/src/collab/Lock.tsx +23 -0
  119. package/src/collab/PresenceStack.tsx +67 -0
  120. package/src/collab/ShareDialog.tsx +118 -0
  121. package/src/collab/Status.tsx +42 -0
  122. package/src/collab/SyncedForm.tsx +19 -0
  123. package/src/collab/Thread.tsx +273 -0
  124. package/src/collab/VersionHistory.tsx +49 -0
  125. package/src/collab/ViewportRail.tsx +49 -0
  126. package/src/collab/index.ts +16 -0
  127. package/src/collab/peer.tsx +16 -0
  128. package/src/fields.ts +5 -0
  129. package/src/flow/AuthScreen.tsx +40 -0
  130. package/src/flow/AuthSteps.tsx +120 -0
  131. package/src/flow/Checklist.tsx +84 -0
  132. package/src/flow/ConnBanner.tsx +147 -0
  133. package/src/flow/Flow.tsx +63 -0
  134. package/src/flow/MfaSetup.tsx +103 -0
  135. package/src/flow/Otp.tsx +114 -0
  136. package/src/flow/ReauthDialog.tsx +47 -0
  137. package/src/flow/ShortcutsOverlay.tsx +60 -0
  138. package/src/flow/SignGate.tsx +98 -0
  139. package/src/flow/SignIn.tsx +80 -0
  140. package/src/flow/SignUp.tsx +71 -0
  141. package/src/flow/Sso.tsx +38 -0
  142. package/src/flow/SystemPage.tsx +247 -0
  143. package/src/flow/Tour.tsx +108 -0
  144. package/src/flow/index.ts +17 -0
  145. package/src/flow/kit.tsx +170 -0
  146. package/src/index.ts +74 -0
  147. package/src/layout.css +4 -0
  148. package/src/links.css +4 -0
  149. package/src/live/ActivityFeed.tsx +49 -0
  150. package/src/live/Bell.tsx +62 -0
  151. package/src/live/Connection.tsx +49 -0
  152. package/src/live/Inbox.tsx +220 -0
  153. package/src/live/MeetingRoom.tsx +298 -0
  154. package/src/live/NotificationPreferences.tsx +193 -0
  155. package/src/live/Prejoin.tsx +140 -0
  156. package/src/live/PushSoftAsk.tsx +37 -0
  157. package/src/live/TransportPicker.tsx +163 -0
  158. package/src/live/index.ts +12 -0
  159. package/src/live/notify.tsx +16 -0
  160. package/src/live/shared.tsx +39 -0
  161. package/src/parts.tsx +93 -0
  162. package/src/schedule/Agenda.tsx +173 -0
  163. package/src/schedule/AvailabilityEditor.tsx +164 -0
  164. package/src/schedule/BookingCalendar.tsx +132 -0
  165. package/src/schedule/Calendar.tsx +746 -0
  166. package/src/schedule/EventEditor.tsx +87 -0
  167. package/src/schedule/EventPopover.tsx +65 -0
  168. package/src/schedule/MeetingPlanner.tsx +214 -0
  169. package/src/schedule/MoveSheet.tsx +78 -0
  170. package/src/schedule/YearView.tsx +163 -0
  171. package/src/schedule/bits.tsx +51 -0
  172. package/src/schedule/index.ts +10 -0
  173. package/src/skeleton-css.ts +4 -0
  174. package/src/styles.ts +10 -0
  175. package/src/tokens.d.ts +2 -0
  176. package/src/type.css +15 -0
  177. package/src/ui.ts +26 -0
  178. package/src/use-ui.ts +10 -0
  179. package/tailwind.css +38 -0
  180. package/tokens/color.mjs +66 -0
@@ -0,0 +1,221 @@
1
+ import { CalendarIcon } from '@weft-js/warp-icons'
2
+ import { addDays, daysIn, fmt, fmtRange, isoOf, isUnavailable, pickRange, readDate, toIso, daysBetween, defaultPresets, listKey, weekStart, type ChangeReason, type DateRange, type IsoDate, type RangePreset, type SegmentPart } from '@weft-js/warp-core'
3
+ import { derived, state, untrack } from '@weft-js/core'
4
+ import { Button } from './Button'
5
+ import { CalendarGrid } from './Calendar'
6
+ import { Composite, Hidden, isSheet, messages, Panel, Trigger, bindable, useOpen, usePickerEnv, type Bindable, type Openable, type RichFieldProps } from './Picker'
7
+ import { Segments, type SegmentState } from './Segments'
8
+
9
+ /** Props for DateField. */
10
+ export type DateFieldProps = RichFieldProps & Bindable<IsoDate | null> & Openable & {
11
+ min?: IsoDate | undefined
12
+ max?: IsoDate | undefined
13
+ isDateUnavailable?: ((d: IsoDate) => boolean) | undefined
14
+ /** The day each calendar row starts on, 0 for Sunday to 6. The locale's otherwise. */
15
+ firstDayOfWeek?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined
16
+ /** Shows the week number at the start of each calendar row. */
17
+ weekNumbers?: boolean | undefined
18
+ parse?: ((text: string, today: IsoDate) => IsoDate | null) | undefined
19
+ }
20
+
21
+ /**
22
+ * DateField (04 §2): segments in the field shell and a calendar in the popover (a sheet on phones), Today and Clear in
23
+ * the foot. `onValueChange` fires only with a complete, valid date or null; a half-typed date stays in the segments
24
+ * and `error` explains it on blur. The shell's `error` prop, when given, wins.
25
+ */
26
+ export function DateField({ value, defaultValue, onValueChange, open, defaultOpen, onOpenChange, min, max, isDateUnavailable, firstDayOfWeek, weekNumbers, parse: _parse, locale, today, name, presentation, label, hint, error, disabled, readonly, ...rest }: DateFieldProps) {
27
+ const env = usePickerEnv(() => locale, () => today)
28
+ const v = bindable<IsoDate | null>(() => value, () => defaultValue, null, () => onValueChange)
29
+ const pop = useOpen(() => open, () => defaultOpen, () => onOpenChange)
30
+ const own = state<string | null>(null)
31
+ const started = state(false)
32
+ const clears = state(0)
33
+ const t = derived(() => messages(env.locale.value))
34
+ let trigger: HTMLButtonElement | undefined
35
+ const ctx = () => ({ mode: 'single' as const, value: v.value, today: env.today.value, locale: env.locale.value, min, max, isDateUnavailable })
36
+ const onEdit = (st: SegmentState, parts: SegmentPart[]) => {
37
+ started.value = st !== 'empty'
38
+ own.value = null
39
+ const take = (iso: IsoDate | null) => { if (iso !== untrack(() => v.value)) v.set(iso, 'keyboard') }
40
+ if (st !== 'complete') return take(null)
41
+ const d = readDate(parts)
42
+ if (d.day! > daysIn(d.year!, d.month!)) { own.value = `${fmt(toIso(d.year!, d.month!, 1), { month: 'long', year: 'numeric' }, env.locale.value)} has ${daysIn(d.year!, d.month!)} days.`; return take(null) }
43
+ const iso = isoOf(d)
44
+ if (isUnavailable(iso, ctx())) { own.value = `${fmt(iso, { dateStyle: 'medium' }, env.locale.value)} is ${t.value.unavailable}.`; return take(null) }
45
+ take(iso)
46
+ }
47
+ const choose = (iso: IsoDate | null, reason: ChangeReason) => { own.value = null; v.set(iso, reason); pop.set(false); if (iso === null) { clears.value++; started.value = false } }
48
+ return (
49
+ <Composite
50
+ {...rest}
51
+ label={label}
52
+ hint={hint}
53
+ error={error ?? own.value ?? undefined}
54
+ disabled={disabled}
55
+ readonly={readonly}
56
+ bound={v}
57
+ onReset={() => { own.value = null; started.value = false }}
58
+ filled={v.value !== null || started.value}
59
+ hold={pop.value}
60
+ attrs={{ onClick: (e: MouseEvent) => { if (!disabled && isSheet(e.currentTarget as Element, presentation) && (e.target as Element).closest?.('[data-part="segment"]')) pop.set(true) } }}
61
+ control={() => (
62
+ <>
63
+ <Segments required={rest.required} kind="date" locale={env.locale.value} value={v.value} today={env.today.value} clears={clears.value} disabled={disabled} readonly={readonly} onEdit={onEdit} onBlurPartial={() => (own.value = 'Enter a complete date, or pick one from the calendar.')} />
64
+ <Trigger el={(n: HTMLButtonElement) => (trigger = n)} icon={CalendarIcon} label={t.value.choose + (v.value ? `, ${fmt(v.value, { dateStyle: 'full' }, env.locale.value)}` : '')} open={pop.value} disabled={disabled} onToggle={() => pop.set(!pop.value)} />
65
+ <Hidden name={name} value={() => v.value ?? ''} />
66
+ </>
67
+ )}
68
+ end={
69
+ <Panel label={t.value.choose} open={pop} trigger={() => trigger!} presentation={presentation}>
70
+ {() => (
71
+ <CalendarGrid mode="single" value={() => v.value} locale={env.locale.value} today={env.today.value} min={min} max={max} isDateUnavailable={isDateUnavailable} firstDayOfWeek={firstDayOfWeek} weekNumbers={weekNumbers} autofocus onPick={choose}>
72
+ <div data-part="foot">
73
+ <Button emphasis="ghost" size="sm" onClick={() => choose(env.today.value as IsoDate, 'pointer')}>{t.value.todayBtn}</Button>
74
+ <Button emphasis="ghost" size="sm" onClick={() => choose(null, 'pointer')}>{t.value.clear}</Button>
75
+ </div>
76
+ </CalendarGrid>
77
+ )}
78
+ </Panel>
79
+ }
80
+ />
81
+ )
82
+ }
83
+
84
+ export type RangeCompare = 'previous-period' | 'previous-year' | DateRange
85
+ /** Props for DateRangeField. */
86
+ export type DateRangeFieldProps = RichFieldProps & Bindable<DateRange> & Openable & {
87
+ presets?: RangePreset[] | 'default' | false
88
+ /** The id of the chosen preset, such as "Last 7 days". Null when the dates were picked by hand. */
89
+ preset?: string | null
90
+ /** Called with the preset's id when one is picked, and null when the dates are changed by hand. */
91
+ onPresetChange?: (id: string | null) => void
92
+ min?: IsoDate | undefined
93
+ max?: IsoDate | undefined
94
+ maxDays?: number
95
+ /** Return true for a day that can't start or end the range: it shows struck through. */
96
+ isDateUnavailable?: (d: IsoDate) => boolean
97
+ months?: 1 | 2 | 'auto'
98
+ compare?: RangeCompare | null | undefined
99
+ }
100
+
101
+ /** The comparison range of a selection, for a dashboard's "vs" (04 DateRangeField). */
102
+ export function compareRange(v: DateRange, c: RangeCompare | null | undefined): DateRange | null {
103
+ if (!c || !v.start || !v.end) return typeof c === 'object' ? c : null
104
+ if (typeof c === 'object') return c
105
+ if (c === 'previous-year') return { start: `${+v.start.slice(0, 4) - 1}${v.start.slice(4)}` as IsoDate, end: `${+v.end.slice(0, 4) - 1}${v.end.slice(4)}` as IsoDate }
106
+ const n = daysBetween(v.start, v.end) + 1
107
+ return { start: addDays(v.start, -n), end: addDays(v.start, -1) }
108
+ }
109
+
110
+ /**
111
+ * DateRangeField (04 §2): two segment groups and one trigger; the popover shows two months (one when narrow) beside a
112
+ * preset rail. The chosen preset is kept apart from the frozen dates, so a dashboard's "Last 7 days" can roll forward.
113
+ */
114
+ export function DateRangeField({ value, defaultValue, onValueChange, open, defaultOpen, onOpenChange, presets = 'default', preset, onPresetChange, min, max, maxDays, isDateUnavailable, months = 'auto', compare, locale, today, name, presentation, label, hint, error, disabled, readonly, ...rest }: DateRangeFieldProps) {
115
+ const env = usePickerEnv(() => locale, () => today)
116
+ const v = bindable<DateRange>(() => value, () => defaultValue, { start: null, end: null }, () => onValueChange)
117
+ const pop = useOpen(() => open, () => defaultOpen, () => onOpenChange)
118
+ const chosen = state<string | null>(untrack(() => preset) ?? null)
119
+ const jump = state<string | null>(null)
120
+ const started = state(false)
121
+ const narrow = state(false)
122
+ const t = derived(() => messages(env.locale.value))
123
+ const list = derived(() => (presets === false ? [] : presets === 'default' ? defaultPresets(env.locale.value) : presets))
124
+ const count = derived(() => (months === 'auto' ? (narrow.value ? 1 : 2) : months))
125
+ const compared = derived(() => compareRange(v.value, compare))
126
+ const trouble = derived(() => {
127
+ const r = v.value
128
+ if (r.start && r.end && r.end < r.start) return 'The end is before the start.'
129
+ if (r.start && r.end && maxDays && daysBetween(r.start, r.end) + 1 > maxDays) return `Choose at most ${maxDays} days.`
130
+ return undefined
131
+ })
132
+ const human = (d: string) => fmt(d, { dateStyle: 'medium' }, env.locale.value)
133
+ const summary = derived(() => {
134
+ const r = v.value
135
+ if (!r.start) return 'Pick a start date'
136
+ if (!r.end) return 'Pick an end date'
137
+ const n = daysBetween(r.start, r.end) + 1
138
+ return `${r.start > r.end ? `${human(r.start)} – ${human(r.end)}` : fmtRange(r.start, r.end, env.locale.value)} · ${new Intl.NumberFormat(env.locale.value, { style: 'unit', unit: 'day', unitDisplay: 'long' }).format(n)}`
139
+ })
140
+ const said = state('')
141
+ let trigger: HTMLButtonElement | undefined
142
+ const set = (r: DateRange, reason: ChangeReason, id: string | null = null) => {
143
+ chosen.value = id
144
+ onPresetChange?.(id)
145
+ v.set(r, reason)
146
+ }
147
+ const edit = (which: 'start' | 'end') => (st: SegmentState, parts: SegmentPart[]) => {
148
+ started.value = st !== 'empty' || !!(which === 'start' ? v.value.end : v.value.start)
149
+ if (st === 'empty') return set({ ...v.value, [which]: null }, 'keyboard')
150
+ if (st !== 'complete') return
151
+ const d = readDate(parts)
152
+ if (d.day! <= daysIn(d.year!, d.month!)) set({ ...v.value, [which]: isoOf(d) }, 'keyboard')
153
+ }
154
+ const editStart = edit('start'), editEnd = edit('end')
155
+ const applyPreset = (p: RangePreset) => {
156
+ const r = p.range(env.today.value as IsoDate, weekStart(env.locale.value))
157
+ set(r, 'pointer', p.id)
158
+ jump.value = r.end
159
+ said.value = `${p.label}: ${summary.value}`
160
+ }
161
+ const onRail = (e: KeyboardEvent) => {
162
+ const opts = list.value
163
+ const at = opts.findIndex(p => p.id === (e.target as HTMLElement).dataset.id)
164
+ if (at < 0) return
165
+ const step = narrow.value ? ({ ArrowRight: 'ArrowDown', ArrowLeft: 'ArrowUp' } as Record<string, string>)[e.key] ?? e.key : e.key
166
+ const m = listKey(step, at, opts.length)
167
+ if (!m.handled) return
168
+ e.preventDefault()
169
+ if (m.pick) return applyPreset(opts[at]!)
170
+ ;(e.currentTarget as HTMLElement).querySelector<HTMLElement>(`[data-id="${opts[m.to]!.id}"]`)?.focus()
171
+ }
172
+ const toggle = () => {
173
+ narrow.value = isSheet(trigger!, presentation) || (trigger!.ownerDocument.defaultView?.innerWidth ?? 1024) < 640
174
+ pop.set(!pop.value)
175
+ }
176
+ return (
177
+ <Composite
178
+ {...rest}
179
+ label={label}
180
+ hint={hint}
181
+ error={error ?? trouble.value}
182
+ disabled={disabled}
183
+ readonly={readonly}
184
+ bound={v}
185
+ onReset={() => { started.value = false; chosen.value = untrack(() => preset) ?? null }}
186
+ filled={!!(v.value.start || v.value.end) || started.value}
187
+ hold={pop.value}
188
+ control={() => (
189
+ <>
190
+ <Segments required={rest.required} kind="date" label="Start" locale={env.locale.value} value={v.value.start} today={env.today.value} disabled={disabled} readonly={readonly} onEdit={editStart} />
191
+ <span data-part="range-sep" aria-hidden="true">–</span>
192
+ <Segments required={rest.required} kind="date" label="End" locale={env.locale.value} value={v.value.end} today={env.today.value} disabled={disabled} readonly={readonly} onEdit={editEnd} />
193
+ <Trigger el={(n: HTMLButtonElement) => (trigger = n)} icon={CalendarIcon} label={t.value.chooseRange + (v.value.start && v.value.end ? `, ${summary.value}` : '')} open={pop.value} disabled={disabled} onToggle={toggle} />
194
+ <Hidden name={name} value={() => (v.value.start && v.value.end ? `${v.value.start}/${v.value.end}` : '')} />
195
+ </>
196
+ )}
197
+ end={
198
+ <Panel label={t.value.chooseRange} open={pop} trigger={() => trigger!} presentation={presentation}>
199
+ {() => (
200
+ <div data-scope="range-panel" data-narrow={narrow.value ? '' : undefined}>
201
+ {list.value.length > 0 && (
202
+ <ul role="listbox" aria-label="Presets" aria-orientation={narrow.value ? 'horizontal' : 'vertical'} onKeyDown={onRail}>
203
+ {list.value.map(p => <li key={p.id} role="option" data-id={p.id} tabindex={p.id === (chosen.value ?? list.value[0]!.id) ? '0' : '-1'} aria-selected={p.id === chosen.value ? 'true' : 'false'} onClick={() => applyPreset(p)}>{p.label}</li>)}
204
+ </ul>
205
+ )}
206
+ <div>
207
+ <CalendarGrid mode="range" months={count.value} value={() => v.value} compare={() => compared.value} goTo={() => jump.value} locale={env.locale.value} today={env.today.value} min={min} max={max} isDateUnavailable={isDateUnavailable} autofocus onPick={(iso, r) => set(pickRange(v.value, iso), r)}>
208
+ <div data-part="foot">
209
+ <span aria-live="polite">{summary.value}</span>
210
+ <Button emphasis="ghost" size="sm" onClick={() => set({ start: null, end: null }, 'pointer')}>{t.value.clear}</Button>
211
+ </div>
212
+ </CalendarGrid>
213
+ </div>
214
+ <span class="sr" role="status">{said.value}</span>
215
+ </div>
216
+ )}
217
+ </Panel>
218
+ }
219
+ />
220
+ )
221
+ }
package/src/Dither.tsx ADDED
@@ -0,0 +1,129 @@
1
+ import { ditherField, ditherImage, ditherStage, type Algorithm, type FieldOptions, type Gradient, type MaskSource, type Ramp, type DitherStageOptions } from '@weft-js/warp-core/dither'
2
+ import { derived } from '@weft-js/core'
3
+ import { use } from '@weft-js/warp-core/weft'
4
+ import './styles'
5
+ import '@weft-js/warp-core/styles/canvas/dither.css'
6
+ import '@weft-js/warp-core/styles/canvas/stage.css'
7
+
8
+ export type { Algorithm, Brush, DitherPainter, DitherPiece, Field as FieldFn, FieldName, Gradient, MaskSource, Pattern, PointerMode, Ramp, Skin } from '@weft-js/warp-core/dither'
9
+ export { LEVEL, PAINTERS } from '@weft-js/warp-core/dither'
10
+
11
+ const box = (ratio: string | number | undefined, style: unknown) => [ratio === undefined ? undefined : `aspect-ratio:${ratio}`, style].filter(Boolean).join(';') || undefined
12
+
13
+ /** Props for DitherField. */
14
+ export interface DitherFieldProps extends FieldOptions {
15
+ /** Fills the nearest positioned ancestor: a section's background. */
16
+ fill?: boolean
17
+ /** CSS aspect-ratio. Default 16 / 9 unless `fill`. */
18
+ ratio?: string | number
19
+ /** Names the field (role="img"). Without one it is decorative. */
20
+ label?: string | undefined
21
+ children?: unknown
22
+ [attr: string]: unknown
23
+ }
24
+
25
+ /** A live dithered field (08 §6.7): moves at vivid and balanced, holds still at quiet and under reduced motion. Children sit over it. */
26
+ export function DitherField({ kind, smooth, ramp, spark, pattern, cell, inks, speed, gain, seed, spread, pointer, mask, fade, reveal, fps, fill, ratio, label, children, style, ...rest }: DitherFieldProps) {
27
+ let node: HTMLCanvasElement | undefined
28
+ use(ditherField, () => node, () => ({ kind, smooth, ramp, spark, pattern, cell, inks, speed, gain, seed, spread, pointer, mask, fade, reveal, fps }))
29
+ return (
30
+ <div {...rest} data-scope="dither" data-fill={fill ? '' : undefined} role={label ? 'img' : undefined} aria-label={label} style={box(ratio ?? (fill ? undefined : '16 / 9'), style)}>
31
+ <canvas ref={n => (node = n)} aria-hidden="true"></canvas>
32
+ {children}
33
+ </div>
34
+ )
35
+ }
36
+
37
+ /** Props for Dither. */
38
+ export interface DitherProps {
39
+ /** An image to dither. The real <img> stays underneath, for alt text, for no script and for a cross-origin failure. */
40
+ src?: string
41
+ /** The image's alt text, kept on the real <img> underneath. */
42
+ alt?: string
43
+ /** The image's CORS mode. A cross-origin image needs it to be read and dithered. */
44
+ crossorigin?: 'anonymous' | 'use-credentials'
45
+ /** With no `src`: the gradient to dither. Default a soft diagonal. */
46
+ gradient?: Gradient | undefined
47
+ /** `atkinson` by default for images, `bayer8` for gradients. */
48
+ algorithm?: Algorithm
49
+ /** The inks: a tone such as `accent`, or your own list of colours. */
50
+ ramp?: Ramp
51
+ /** CSS pixels per dither cell. Larger cells give a coarser, chunkier picture. */
52
+ cell?: number
53
+ /** How many inks of the ramp to use. Default: the tier's. */
54
+ inks?: number
55
+ contrast?: number
56
+ invert?: boolean
57
+ /** Fills the nearest positioned parent instead of keeping a ratio, for a section background. */
58
+ fill?: boolean
59
+ ratio?: string | number
60
+ label?: string | undefined
61
+ [attr: string]: unknown
62
+ }
63
+
64
+ /** A still image or gradient, dithered to warp's inks once, and again only when its size or theme changes (08 §6.7). */
65
+ export function Dither({ src, alt, crossorigin, gradient, algorithm, ramp, cell, inks, contrast, invert, fill, ratio, label, style, ...rest }: DitherProps) {
66
+ let node: HTMLCanvasElement | undefined
67
+ let img: HTMLImageElement | undefined
68
+ use(ditherImage, () => node, () => ({ source: img, gradient, algorithm, ramp, cell, inks, contrast, invert }))
69
+ return (
70
+ <div {...rest} data-scope="dither" data-fill={fill ? '' : undefined} role={label ? 'img' : undefined} aria-label={label} style={box(ratio ?? (fill ? undefined : '16 / 9'), style)}>
71
+ {src && <img ref={n => (img = n)} src={src} alt={alt ?? ''} crossorigin={crossorigin} loading="lazy" decoding="async" />}
72
+ <canvas ref={n => (node = n)} aria-hidden="true"></canvas>
73
+ </div>
74
+ )
75
+ }
76
+
77
+ /** Props for DitherGlyph. */
78
+ export interface DitherGlyphProps extends Omit<FieldOptions, 'mask'> {
79
+ /** The word to set. It stays in the page as real text for readers and for search. */
80
+ text?: string | undefined
81
+ /** Or an SVG path and the size of its box, for a logo or an icon. */
82
+ path?: string
83
+ box?: [width: number, height: number]
84
+ /** Stroke width in the box's units, for a path made of lines. */
85
+ stroke?: number | undefined
86
+ /** Or any drawing, in cells. */
87
+ draw?: (ctx: CanvasRenderingContext2D, cols: number, rows: number) => void
88
+ /** Names a `path` or `draw` glyph. */
89
+ label?: string | undefined
90
+ font?: string
91
+ weight?: number | string
92
+ /** Share of the box the shape fills, 0–1. */
93
+ fit?: number | undefined
94
+ ratio?: string | number
95
+ [attr: string]: unknown
96
+ }
97
+
98
+ /** A word or a logo woven out of a dithered field (08 §6.7). */
99
+ export function DitherGlyph({ text, path, box: viewBox, stroke, draw, label, font, weight, fit, cell = 4, ratio, kind = 'flow', ...rest }: DitherGlyphProps) {
100
+ const mask = derived<MaskSource>(() => draw ?? (path ? { path, box: viewBox ?? [24, 24], fit, stroke } : { text: text ?? '', font, weight, fit }))
101
+ return (
102
+ <DitherField {...rest} kind={kind} cell={cell} mask={mask.value} ratio={ratio ?? '3 / 1'} label={text ? undefined : label}>
103
+ {text && <span data-part="glyph-text">{text}</span>}
104
+ </DitherField>
105
+ )
106
+ }
107
+
108
+ /** Props for DitherStage. */
109
+ export interface DitherStageProps extends DitherStageOptions {
110
+ /** `box` paints its own block; `scroll` is a scroller whose view it paints; `page` paints the viewport, for a whole page. */
111
+ layout?: DitherStageOptions['layout']
112
+ children?: unknown
113
+ [attr: string]: unknown
114
+ }
115
+
116
+ /**
117
+ * A CanvasStage in the dither look (08 §6.8). Mark any element `data-skin="button"`, `card`, `field` and so on: it keeps
118
+ * its layout, focus, typing and semantics, and the stage draws it in dithered cells, states and all.
119
+ */
120
+ export function DitherStage({ layout = 'box', ramp, kind, ground, pattern, cell, speed, drift, rail, veil, ripple, painters, fps, children, ...rest }: DitherStageProps) {
121
+ let node: HTMLCanvasElement | undefined
122
+ use(ditherStage, () => node, () => ({ layout, ramp, kind, ground, pattern, cell, speed, drift, rail, veil, ripple, painters, fps }))
123
+ return (
124
+ <div {...rest} data-scope="stage" data-paint="dither" data-layout={layout} data-rail={rail ? '' : undefined} data-veil={veil ? '' : undefined}>
125
+ <div data-part="paint" aria-hidden="true"><canvas ref={n => (node = n)}></canvas></div>
126
+ {children}
127
+ </div>
128
+ )
129
+ }
package/src/Drawer.tsx ADDED
@@ -0,0 +1,41 @@
1
+ import { XIcon } from '@weft-js/warp-icons'
2
+ import { dialog, uid } from '@weft-js/warp-core'
3
+ import { derived, type State, untrack } from '@weft-js/core'
4
+ import { use } from '@weft-js/warp-core/weft'
5
+ import './styles'
6
+ import '@weft-js/warp-core/styles/components/button.css' // the close button's markup
7
+ import '@weft-js/warp-core/styles/components/sheet.css'
8
+ import '@weft-js/warp-core/styles/components/dialog.css' // the backdrop and the parts a sheet shares with the dialog
9
+
10
+ /**
11
+ * A sheet (05 §3 Drawer/Sheet; components.html:1092-1106): on a native <dialog>, at the side on wide screens and from
12
+ * the bottom on phones (`side="auto"`: bottom under 600px, decided when it opens). The handle shows only at the
13
+ * bottom. Initial focus goes to [autofocus], else the title, never the handle. `inline` draws it open in the page's flow,
14
+ * for docs and previews: a non-modal <dialog open> at its side's shape, with no top layer, scrim or focus trap.
15
+ */
16
+ export function Drawer({ open, title, description, footer, side = 'auto', inline, children }: { open: State<boolean>; title: unknown; description?: unknown; footer?: unknown; side?: 'auto' | 'right' | 'left' | 'bottom' | 'top'; /** Drawn open in the page's flow, not over it: no top layer, scrim or focus trap. For docs and previews. */ inline?: boolean; children?: unknown }) {
17
+ const id = uid('sheet')
18
+ const hasDescription = derived(() => description !== undefined)
19
+ const foot = derived(() => footer) // one build of the footer, however often it is read
20
+ let el: HTMLDialogElement | undefined
21
+ const placed = () => (side === 'auto' ? (typeof innerWidth === 'number' && innerWidth < 600 ? 'bottom' : 'right') : side)
22
+ const flat = untrack(() => !!inline)
23
+ if (!flat) use(dialog, () => el, () => {
24
+ const on = open.value
25
+ if (on && el) el.dataset.side = placed()
26
+ return { open: on, onClose: () => (open.value = false) }
27
+ })
28
+ const closer = (e: Event) => { if (flat && (e.target as Element).closest?.('[data-close]')) open.value = false }
29
+ return (
30
+ <dialog ref={n => (el = n)} open={flat ? open.value : undefined} data-presentation={flat ? 'inline' : undefined} onClick={closer} data-scope="sheet" aria-labelledby={`${id}-t`} aria-describedby={hasDescription.value ? `${id}-d` : undefined} data-side={untrack(() => (side === 'auto' ? 'right' : side))}>
31
+ <button type="button" data-part="handle" aria-label="Sheet height: tall. Press to shrink."></button>
32
+ <div data-part="header">
33
+ <h2 data-part="title" id={`${id}-t`}>{title}</h2>
34
+ {hasDescription.value && <p data-part="description" id={`${id}-d`}>{description}</p>}
35
+ <button type="button" data-scope="button" data-emphasis="ghost" data-tone="neutral" data-icon-only="" aria-label="Close" data-part="close" data-close=""><XIcon /></button>
36
+ </div>
37
+ <div data-part="body">{children}</div>
38
+ {foot.value !== undefined && <div data-part="footer">{foot.value}</div>}
39
+ </dialog>
40
+ )
41
+ }
@@ -0,0 +1,98 @@
1
+ import { TimerIcon } from '@weft-js/warp-icons'
2
+ import { balance, formatDuration, pad, parseDuration, totalSeconds, type DurationParts, type IsoDuration } from '@weft-js/warp-core'
3
+ import { derived, state, untrack } from '@weft-js/core'
4
+ import { bindable, Composite, Hidden, usePickerEnv, type Bindable, type RichFieldProps } from './Picker'
5
+
6
+ type Unit = 'd' | 'h' | 'm' | 's'
7
+ const INTL = { d: 'day', h: 'hour', m: 'minute', s: 'second' } as const
8
+ const NAMES = { d: 'Days', h: 'Hours', m: 'Minutes', s: 'Seconds' } as const
9
+ const SHORT = { d: 'd', h: 'h', m: 'min', s: 's' } as const
10
+ const SIZE = { d: 86400, h: 3600, m: 60, s: 1 } as const
11
+ const MAX = { d: 99, h: 99, m: 59, s: 59 } as const
12
+
13
+ /** Props for DurationField. */
14
+ export type DurationFieldProps = RichFieldProps & Bindable<IsoDuration | null> & {
15
+ units?: Unit[]
16
+ max?: IsoDuration | undefined
17
+ step?: Partial<Record<'h' | 'm' | 's', number>>
18
+ balance?: boolean
19
+ }
20
+
21
+ /**
22
+ * DurationField (04 §2): a spinbutton for each allowed unit; ↑/↓ step with carry into the next unit (Shift for three
23
+ * steps), and digits fill. The value is an ISO duration, balanced (`PT90M` → `PT1H30M`) unless `balance: false`.
24
+ */
25
+ export function DurationField({ value, defaultValue, onValueChange, units = ['h', 'm'], max, step, balance: carry = true, locale, today, name, label, hint, error, disabled, ...rest }: DurationFieldProps) {
26
+ const env = usePickerEnv(() => locale, () => today)
27
+ const v = bindable<IsoDuration | null>(() => value, () => defaultValue, null, () => onValueChange)
28
+ const parts = derived(() => parseDuration(v.value))
29
+ const limit = untrack(() => (max ? totalSeconds(parseDuration(max)) : Infinity))
30
+ const buf = state('')
31
+ let group: HTMLDivElement | undefined
32
+ const say = (u: Unit, n: number) => new Intl.NumberFormat(env.locale.value, { style: 'unit', unit: INTL[u], unitDisplay: 'long' }).format(n)
33
+ const words = derived(() => {
34
+ const p = parts.value
35
+ const on = units.filter(u => p[u])
36
+ return on.length ? on.map(u => say(u, p[u])).join(' ') : 'No time'
37
+ })
38
+ const write = (next: DurationParts, reason: 'keyboard' = 'keyboard') => {
39
+ const t = Math.max(0, Math.min(limit, totalSeconds(next)))
40
+ v.set(formatDuration(carry ? balance(t, units) : next), reason)
41
+ }
42
+ const onKey = (e: KeyboardEvent) => {
43
+ const el = (e.target as HTMLElement).closest?.<HTMLElement>('[data-u]')
44
+ if (!el) return
45
+ const u = el.dataset.u as Unit
46
+ const at = units.indexOf(u)
47
+ const p = parts.value
48
+ const size = (step?.[u as 'h'] ?? (u === 'm' ? 5 : 1)) * SIZE[u] * (e.shiftKey ? 3 : 1)
49
+ if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
50
+ e.preventDefault()
51
+ buf.value = ''
52
+ write(balance(totalSeconds(p) + (e.key === 'ArrowUp' ? size : -size), units))
53
+ } else if (/^\d$/.test(e.key)) {
54
+ e.preventDefault()
55
+ const b = (buf.value + e.key).slice(-2)
56
+ buf.value = b
57
+ const n = +b
58
+ write(n > MAX[u] && !carry ? p : { ...p, [u]: n })
59
+ if (b.length === 2 && at < units.length - 1) { buf.value = ''; (group!.querySelector(`[data-u="${units[at + 1]}"]`) as HTMLElement).focus() }
60
+ } else if (e.key === 'Backspace') {
61
+ e.preventDefault()
62
+ buf.value = ''
63
+ write({ ...p, [u]: 0 })
64
+ } else if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
65
+ const to = units[at + (e.key === 'ArrowRight' ? 1 : -1)]
66
+ if (to) { e.preventDefault(); (group!.querySelector(`[data-u="${to}"]`) as HTMLElement).focus() }
67
+ }
68
+ }
69
+ return (
70
+ <Composite
71
+ icon={TimerIcon}
72
+ {...rest}
73
+ label={label}
74
+ hint={hint ?? words.value}
75
+ error={error}
76
+ disabled={disabled}
77
+ bound={v}
78
+ onReset={() => (buf.value = '')}
79
+ filled={totalSeconds(parts.value) > 0}
80
+ control={() => (
81
+ <>
82
+ <div ref={n => (group = n)} data-part="segments" onKeyDown={onKey} onFocusOut={() => (buf.value = '')}>
83
+ {units.map(u => {
84
+ const n = derived(() => parts.value[u])
85
+ return (
86
+ <>
87
+ <span key={u} data-part="segment" data-u={u} role="spinbutton" tabindex={disabled ? undefined : '0'} aria-label={NAMES[u]} aria-required={rest.required ? 'true' : undefined} aria-valuemin="0" aria-valuemax={MAX[u]} aria-valuenow={n.value} aria-valuetext={say(u, n.value)}>{u === units[0] ? n.value : pad(n.value)}</span>
88
+ <span data-part="literal" class="unit" aria-hidden="true">{SHORT[u]}</span>
89
+ </>
90
+ )
91
+ })}
92
+ </div>
93
+ <Hidden name={name} value={() => v.value ?? ''} />
94
+ </>
95
+ )}
96
+ />
97
+ )
98
+ }
package/src/Feed.tsx ADDED
@@ -0,0 +1,74 @@
1
+ import { untrack } from '@weft-js/core'
2
+ import { VirtualList, type VirtualControl } from './VirtualList'
3
+ import './styles'
4
+ import '@weft-js/warp-core/styles/data/timeline.css'
5
+
6
+ /** One row of a Feed. */
7
+ export interface FeedItem {
8
+ id: string | number
9
+ /** Actor, verb and object: the bold line. */
10
+ what: unknown
11
+ when?: string
12
+ /** The body, measured. */
13
+ detail?: string
14
+ tone?: string | undefined
15
+ }
16
+
17
+ /** Props for Feed. */
18
+ export interface FeedProps<T extends FeedItem> {
19
+ items: readonly T[]
20
+ label: string
21
+ height?: string
22
+ busy?: boolean | undefined
23
+ /** Near the end: load more. */
24
+ onEnd?: () => void
25
+ /** Called when an item's measured height differs from the estimate, with both heights. For tuning. */
26
+ onDrift?: (index: number, predicted: number, actual: number) => void
27
+ /** Hands over a control to scroll to, focus or move between items from outside. */
28
+ control?: (c: VirtualControl) => void
29
+ /** An item's whole content. Default: knot, what, when, detail. */
30
+ children?: (item: () => T) => unknown
31
+ }
32
+
33
+ /** A long activity feed (07 §4): the APG feed pattern on a window. Page Down and Up move between articles. */
34
+ export function Feed<T extends FeedItem>(props: FeedProps<T>) {
35
+ const render = untrack(() => props.children)
36
+ const keys = (e: KeyboardEvent, c: VirtualControl) => {
37
+ if (e.key === 'PageDown' || e.key === 'PageUp') {
38
+ e.preventDefault()
39
+ c.move(e.key === 'PageDown' ? 1 : -1)
40
+ }
41
+ }
42
+ return (
43
+ <VirtualList
44
+ tag="ol"
45
+ inner
46
+ role="feed"
47
+ rowRole="article"
48
+ listScope="timeline"
49
+ label={props.label}
50
+ height={props.height ?? '480px'}
51
+ items={props.items}
52
+ rowKey={i => i.id}
53
+ text={i => i.detail ?? ''}
54
+ estimate={56}
55
+ busy={props.busy}
56
+ onEnd={props.onEnd}
57
+ onDrift={props.onDrift}
58
+ control={props.control}
59
+ roving
60
+ onRowKey={keys}
61
+ >
62
+ {item => (render
63
+ ? render(item)
64
+ : (
65
+ <>
66
+ <span data-part="knot" data-tone={item().tone}></span>
67
+ <span data-part="what">{item().what}</span>
68
+ <time data-part="when">{item().when}</time>
69
+ <span data-part="detail" data-measure="">{item().detail}</span>
70
+ </>
71
+ ))}
72
+ </VirtualList>
73
+ )
74
+ }