@weft-js/warp 0.0.0-stage → 0.1.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 (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,237 @@
1
+ import { CalendarIcon, ClockIcon } from '@weft-js/warp-icons'
2
+ import { daysIn, dayPeriods, fieldName, fmt, fmtTime, fromMinutes, hour12, isoOf, listKey, minutes, pad, readDate, readTime, uid, wallTimeIssue, type ChangeReason, type IsoDate, type IsoDateTime, type IsoTime, type SegmentPart, type TimeZoneId } from '@weft-js/warp-core'
3
+ import { derived, flush, onMount, state, untrack } from '@weft-js/core'
4
+ import { CalendarGrid } from './Calendar'
5
+ import { Composite, Hidden, isSheet, messages, Panel, Trigger, bindable, useOpen, usePickerEnv, type Bindable, type Openable, type RichFieldProps } from './Picker'
6
+ import { Segments, type SegmentState } from './Segments'
7
+ import '@weft-js/warp-core/styles/rich/listbox.css'
8
+ import '@weft-js/warp-core/styles/rich/datetime.css'
9
+
10
+ /** The slot list (04 TimeField): a listbox with an active option, not a row of tab stops. */
11
+ function Slots({ slots, selected, locale, hourCycle, label, onPick, meta }: { slots: string[]; selected: string | null; locale: string; hourCycle?: string | undefined; label: string; onPick: (t: string, r: ChangeReason) => void; meta?: boolean | undefined }) {
12
+ const id = uid('slots')
13
+ const near = () => Math.max(0, slots.findIndex(x => x >= (untrack(() => selected) ?? '09:00')))
14
+ const active = state(untrack(near))
15
+ let list: HTMLUListElement | undefined
16
+ const show = (block: ScrollLogicalPosition) => list?.querySelector<HTMLElement>('[data-active]')?.scrollIntoView?.({ block })
17
+ const onKey = (e: KeyboardEvent) => {
18
+ const m = listKey(e.key, active.value, slots.length)
19
+ if (!m.handled) return
20
+ e.preventDefault()
21
+ if (m.pick) return onPick(slots[active.value]!, 'keyboard')
22
+ active.value = m.to
23
+ flush()
24
+ show('nearest')
25
+ }
26
+ onMount(() => show('center'))
27
+ return (
28
+ <ul ref={n => (list = n)} data-part="listbox" role="listbox" tabindex="0" data-initial="" aria-label={label} aria-activedescendant={`${id}-${active.value}`} onKeyDown={onKey}>
29
+ {slots.map((x, n) => <Slot key={x} id={`${id}-${n}`} time={x} text={fmtTime(x, locale, hourCycle)} selected={x === selected} active={n === active.value} meta={meta} onPick={onPick} />)}
30
+ </ul>
31
+ )
32
+ }
33
+ const Slot = ({ id, time, text, selected, active, meta, onPick }: { id: string; time: string; text: string; selected: boolean; active: boolean; meta?: boolean | undefined; onPick: (t: string, r: ChangeReason) => void }) => (
34
+ <li role="option" id={id} aria-selected={selected ? 'true' : 'false'} data-active={active ? '' : undefined} onClick={() => onPick(time, 'pointer')}>{text}{meta && <span data-part="opt-meta">{time}</span>}</li>
35
+ )
36
+
37
+ /** The wheel sheet of a phone (04 §1.2): scroll-snap columns; arrows and presses work too. */
38
+ function Wheels({ value, locale, hourCycle, onChange }: { value: string | null; locale: string; hourCycle?: 'h12' | 'h23' | 'auto' | undefined; onChange: (t: IsoTime) => void }) {
39
+ const h12 = untrack(() => hour12(locale, hourCycle))
40
+ const start = untrack(() => value) ?? '09:30'
41
+ const h0 = +start.slice(0, 2), m0 = Math.round(+start.slice(3, 5) / 5) * 5 % 60
42
+ const periods = untrack(() => dayPeriods(locale))
43
+ const cols = [
44
+ { key: 'hour', name: untrack(() => fieldName('hour', locale)), vals: h12 ? [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] : Array.from({ length: 24 }, (_, i) => i), cur: state(h12 ? h0 % 12 || 12 : h0), show: (v: number) => (h12 ? String(v) : pad(v)) },
45
+ { key: 'minute', name: untrack(() => fieldName('minute', locale)), vals: Array.from({ length: 12 }, (_, i) => i * 5), cur: state(m0), show: (v: number) => pad(v) },
46
+ ...(h12 ? [{ key: 'dayPeriod', name: untrack(() => fieldName('dayPeriod', locale)), vals: [0, 1], cur: state(h0 >= 12 ? 1 : 0), show: (v: number) => periods[v] }] : []),
47
+ ]
48
+ const commit = () => {
49
+ let h = cols[0]!.cur.value
50
+ if (h12) h = h % 12 + (cols[2]!.cur.value ? 12 : 0)
51
+ onChange(`${pad(h)}:${pad(cols[1]!.cur.value)}` as IsoTime)
52
+ }
53
+ return (
54
+ <div data-scope="wheels">
55
+ {cols.map(c => {
56
+ let list: HTMLUListElement | undefined
57
+ let timer: ReturnType<typeof setTimeout> | undefined
58
+ const row = () => (list!.firstElementChild as HTMLElement | null)?.getBoundingClientRect().height || 40
59
+ const select = (i: number, smooth: boolean) => {
60
+ i = Math.max(0, Math.min(c.vals.length - 1, i))
61
+ c.cur.value = c.vals[i]!
62
+ list?.scrollTo?.({ top: i * row(), behavior: smooth ? 'smooth' : 'instant' })
63
+ commit()
64
+ }
65
+ onMount(() => list?.scrollTo?.({ top: c.vals.indexOf(c.cur.value) * row(), behavior: 'instant' as ScrollBehavior }))
66
+ const onScroll = () => {
67
+ clearTimeout(timer)
68
+ timer = setTimeout(() => {
69
+ const i = Math.round(list!.scrollTop / row())
70
+ if (c.vals[i] !== undefined && c.vals[i] !== c.cur.value) { c.cur.value = c.vals[i]; commit() }
71
+ }, 90)
72
+ }
73
+ const onKey = (e: KeyboardEvent) => {
74
+ const d = ({ ArrowDown: 1, ArrowUp: -1 } as Record<string, number>)[e.key]
75
+ if (!d) return
76
+ e.preventDefault()
77
+ select(c.vals.indexOf(c.cur.value) + d, true)
78
+ }
79
+ return (
80
+ <ul key={c.key} ref={n => (list = n)} role="listbox" tabindex="0" aria-label={c.name} onScroll={onScroll} onKeyDown={onKey}>
81
+ {c.vals.map(v => <li key={v} role="option" aria-selected={c.cur.value === v ? 'true' : 'false'} onClick={() => select(c.vals.indexOf(v), true)}>{c.show(v)}</li>)}
82
+ </ul>
83
+ )
84
+ })}
85
+ </div>
86
+ )
87
+ }
88
+
89
+ export const stepSlots = (step: number, min?: string , max?: string ) => {
90
+ const from = min ? minutes(min) : 0, to = max ? minutes(max) : 1439
91
+ const out: string[] = []
92
+ for (let m = from; m <= to; m += step) out.push(fromMinutes(m))
93
+ return out
94
+ }
95
+
96
+ /** Props for TimeField. */
97
+ export type TimeFieldProps = RichFieldProps & Bindable<IsoTime | null> & Openable & {
98
+ /** `h12` shows AM and PM, `h23` a 24-hour clock, `auto` the locale's. */
99
+ hourCycle?: 'h12' | 'h23' | 'auto' | undefined
100
+ step?: number | undefined
101
+ /** Adds a seconds segment. */
102
+ seconds?: boolean | undefined
103
+ min?: IsoTime | undefined
104
+ max?: IsoTime | undefined
105
+ slots?: IsoTime[] | undefined
106
+ /** Only for the DST hint. */
107
+ date?: IsoDate | undefined
108
+ timeZone?: TimeZoneId | undefined
109
+ }
110
+
111
+ /**
112
+ * TimeField (04 §2): segments in the locale's order and cycle; the trigger opens a slot list (wheels in a phone
113
+ * sheet). With `date` and `timeZone`, a wall time that doesn't exist or happens twice gets a hint, and a missing one
114
+ * resolves with `disambiguation: "compatible"`.
115
+ */
116
+ export function TimeField({ value, defaultValue, onValueChange, open, defaultOpen, onOpenChange, hourCycle, step = 15, seconds, min, max, slots, date, timeZone, locale, today, name, presentation, label, hint, error, disabled, readonly, ...rest }: TimeFieldProps) {
117
+ const env = usePickerEnv(() => locale, () => today)
118
+ const v = bindable<IsoTime | null>(() => value, () => defaultValue, null, () => onValueChange)
119
+ const pop = useOpen(() => open, () => defaultOpen, () => onOpenChange)
120
+ const started = state(false)
121
+ const sheet = state(false)
122
+ const t = derived(() => messages(env.locale.value))
123
+ const options = derived(() => slots ?? stepSlots(step, min, max))
124
+ let trigger: HTMLButtonElement | undefined
125
+ const dst = derived(() => {
126
+ if (!v.value || !date || !timeZone) return undefined
127
+ const issue = wallTimeIssue(date, v.value, timeZone)
128
+ if (!issue) return undefined
129
+ const day = fmt(date, { month: 'short', day: 'numeric' }, env.locale.value)
130
+ const city = timeZone.split('/').pop()!.replace(/_/g, ' ')
131
+ const at = fmtTime(v.value, env.locale.value, hourCycle)
132
+ return issue.kind === 'gap' ? `${at} doesn't exist on ${day} in ${city}; using ${fmtTime(issue.resolved, env.locale.value, hourCycle)}.` : `${at} happens twice on ${day} in ${city}; using the first.`
133
+ })
134
+ const onEdit = (st: SegmentState, parts: SegmentPart[]) => {
135
+ started.value = st !== 'empty'
136
+ if (st === 'empty') return v.set(null, 'keyboard')
137
+ if (st === 'complete') { const x = readTime(parts); if (x) v.set(x, 'keyboard') }
138
+ }
139
+ const open_ = () => { sheet.value = isSheet(trigger!, presentation); pop.set(true) }
140
+ return (
141
+ <Composite
142
+ {...rest}
143
+ label={label}
144
+ hint={dst.value ?? hint}
145
+ error={error}
146
+ disabled={disabled}
147
+ readonly={readonly}
148
+ bound={v}
149
+ onReset={() => (started.value = false)}
150
+ filled={v.value !== null || started.value}
151
+ hold={pop.value}
152
+ control={() => (
153
+ <>
154
+ <Segments required={rest.required} kind="time" options={{ hourCycle, seconds }} locale={env.locale.value} value={v.value} today={env.today.value} disabled={disabled} readonly={readonly} onEdit={onEdit} />
155
+ <Trigger el={(n: HTMLButtonElement) => (trigger = n)} icon={ClockIcon} label={t.value.chooseTime + (v.value ? `, ${fmtTime(v.value, env.locale.value, hourCycle)}` : '')} open={pop.value} disabled={disabled} onToggle={() => (pop.value ? pop.set(false) : open_())} />
156
+ <Hidden name={name} value={() => v.value ?? ''} />
157
+ </>
158
+ )}
159
+ end={
160
+ <Panel label={t.value.chooseTime} open={pop} trigger={() => trigger!} presentation={presentation} align="end">
161
+ {() => (sheet.value
162
+ ? <Wheels value={v.value} locale={env.locale.value} hourCycle={hourCycle} onChange={x => v.set(x, 'pointer')} />
163
+ : <Slots slots={options.value} selected={v.value} locale={env.locale.value} hourCycle={hourCycle} label={t.value.chooseTime} meta onPick={(x, r) => { v.set(x as IsoTime, r); pop.set(false) }} />)}
164
+ </Panel>
165
+ }
166
+ />
167
+ )
168
+ }
169
+
170
+ /** Props for DateTimeField. */
171
+ export type DateTimeFieldProps = RichFieldProps & Bindable<IsoDateTime | null> & Openable & {
172
+ min?: IsoDate | undefined
173
+ max?: IsoDate | undefined
174
+ /** Return true for a day that can't be picked: it shows struck through. */
175
+ isDateUnavailable?: ((d: IsoDate) => boolean) | undefined
176
+ /** The day each calendar row starts on, 0 for Sunday to 6. The locale's otherwise. */
177
+ firstDayOfWeek?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined
178
+ step?: number | undefined
179
+ }
180
+
181
+ /**
182
+ * DateTimeField (04 §2): a date group and a time group in one control and one popover, calendar beside the slot list.
183
+ * The value is a wall time with no zone; the hint spells it out in full.
184
+ */
185
+ export function DateTimeField({ value, defaultValue, onValueChange, open, defaultOpen, onOpenChange, min, max, isDateUnavailable, firstDayOfWeek, step = 30, locale, today, name, presentation, label, hint, error, disabled, readonly, ...rest }: DateTimeFieldProps) {
186
+ const env = usePickerEnv(() => locale, () => today)
187
+ const v = bindable<IsoDateTime | null>(() => value, () => defaultValue, null, () => onValueChange)
188
+ const pop = useOpen(() => open, () => defaultOpen, () => onOpenChange)
189
+ const date = state<string | null>(untrack(() => v.value?.slice(0, 10) ?? null))
190
+ const time = state<string | null>(untrack(() => v.value?.slice(11) ?? null))
191
+ let trigger: HTMLButtonElement | undefined
192
+ const emit = (r: ChangeReason) => v.set(date.value && time.value ? `${date.value}T${time.value}` as IsoDateTime : null, r)
193
+ const said = derived(() => (v.value ? `${fmt(v.value, { dateStyle: 'full', timeStyle: 'short' }, env.locale.value)} · wall clock, no zone` : hint))
194
+ const onDate = (st: SegmentState, parts: SegmentPart[]) => {
195
+ const d = readDate(parts)
196
+ date.value = st === 'complete' && d.day! <= daysIn(d.year!, d.month!) ? isoOf(d) : null
197
+ emit('keyboard')
198
+ }
199
+ const onTime = (st: SegmentState, parts: SegmentPart[]) => { time.value = st === 'complete' ? readTime(parts) : null; emit('keyboard') }
200
+ return (
201
+ <Composite
202
+ {...rest}
203
+ label={label}
204
+ hint={said.value}
205
+ error={error}
206
+ disabled={disabled}
207
+ readonly={readonly}
208
+ bound={v}
209
+ onReset={() => { date.value = v.value?.slice(0, 10) ?? null; time.value = v.value?.slice(11) ?? null }}
210
+ filled={!!(date.value || time.value)}
211
+ hold={pop.value}
212
+ control={() => (
213
+ <>
214
+ <Segments required={rest.required} kind="date" label="Date" locale={env.locale.value} value={date.value} today={env.today.value} disabled={disabled} readonly={readonly} onEdit={onDate} />
215
+ <span data-part="literal" class="dt-sep" aria-hidden="true">·</span>
216
+ <Segments required={rest.required} kind="time" label="Time" locale={env.locale.value} value={time.value} today={env.today.value} disabled={disabled} readonly={readonly} onEdit={onTime} />
217
+ <Trigger el={(n: HTMLButtonElement) => (trigger = n)} icon={CalendarIcon} label="Choose date and time" open={pop.value} disabled={disabled} onToggle={() => pop.set(!pop.value)} />
218
+ <Hidden name={name} value={() => v.value ?? ''} />
219
+ </>
220
+ )}
221
+ end={
222
+ <Panel label="Choose date and time" open={pop} trigger={() => trigger!} presentation={presentation}>
223
+ {() => (
224
+ <div class="dt-pop">
225
+ <CalendarGrid mode="single" value={() => date.value as IsoDate | null} locale={env.locale.value} today={env.today.value} min={min} max={max} isDateUnavailable={isDateUnavailable} firstDayOfWeek={firstDayOfWeek} autofocus onPick={(iso, r) => { date.value = iso; emit(r) }} />
226
+ <div class="dt-side">
227
+ <p class="dt-cap">Time</p>
228
+ <Slots slots={stepSlots(step)} selected={time.value} locale={env.locale.value} label="Time" onPick={(x, r) => { time.value = x; emit(r); pop.set(false) }} />
229
+ </div>
230
+ </div>
231
+ )}
232
+ </Panel>
233
+ }
234
+ />
235
+ )
236
+ }
237
+
package/src/Toast.tsx ADDED
@@ -0,0 +1,56 @@
1
+ import { XIcon } from '@weft-js/warp-icons'
2
+ import { toaster, toastStore, type Toast, type ToastInput, type ToastPromise } from '@weft-js/warp-core'
3
+ import { onCleanup, state } from '@weft-js/core'
4
+ import { Icon, toneIcon } from './Icon'
5
+ import { use } from '@weft-js/warp-core/weft'
6
+ import './styles'
7
+ import '@weft-js/warp-core/styles/components/button.css' // the action and dismiss buttons' markup
8
+ import '@weft-js/warp-core/styles/components/spinner.css'
9
+ import '@weft-js/warp-core/styles/components/toast.css'
10
+
11
+ let store: ReturnType<typeof toastStore> | undefined
12
+ const toasts = () => (store ??= toastStore())
13
+
14
+ /**
15
+ * Show a toast from anywhere (05 §5): `toast.show({ title, description, tone, action })`. Needs one <Toaster />.
16
+ * Life is 5s + 60ms a word, 10s with an action; a critical toast with an action stays until dismissed.
17
+ */
18
+ export const toast = {
19
+ show: (t: ToastInput) => toasts().show(t),
20
+ dismiss: (id: string) => toasts().dismiss(id),
21
+ /** One toast through a promise's life: `pending`, then `done` or `error`, in place. Returns the promise. */
22
+ promise: <T,>(p: Promise<T>, messages: ToastPromise<T>) => toasts().promise(p, messages),
23
+ }
24
+
25
+ /**
26
+ * The toast region and the two pre-mounted live regions (05 §5; components.html:1115-1117). Mount once, near the
27
+ * root. While a modal is open they move into it, or it would make them inert. Newest first; three stack, collapsed, and fan out on hover or focus (which pauses every timer); swipe or
28
+ * Dismiss removes one; F8 focuses the newest.
29
+ */
30
+ export function Toaster({ position }: { /** Which corner or edge. Bottom right by default. */ position?: 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right' } = {}) {
31
+ const list = state<Toast[]>(toasts().get())
32
+ onCleanup(toasts().subscribe(v => (list.value = v)))
33
+ let region: HTMLElement | undefined
34
+ use(toaster, () => region, () => toasts())
35
+ return (
36
+ <>
37
+ <section ref={n => (region = n)} data-scope="toast" data-part="region" data-position={position === 'bottom-right' ? undefined : position} aria-label="Notifications (F8)" tabindex="-1">
38
+ {list.value.map(t => (
39
+ <div key={t.id} data-scope="toast" data-part="root" data-id={t.id} data-tone={t.tone ?? 'neutral'} data-state={t.state} role="group" aria-label={t.title}>
40
+ {t.loading ? <span data-scope="spinner" data-part="icon" aria-hidden="true"></span> : <Icon name={toneIcon[t.tone ?? 'neutral']} data-part="icon" />}
41
+ <div data-part="title">{t.title}</div>
42
+ {t.description && <div data-part="description">{t.description}</div>}
43
+ <div data-part="actions">
44
+ {/* ghost <Button>s as their markup: they need no JS */}
45
+ {t.action && <button type="button" data-scope="button" data-emphasis="ghost" data-size="sm" data-act="" onClick={() => { t.action!.onClick(); toast.dismiss(t.id) }}>{t.action.label}</button>}
46
+ <button type="button" data-scope="button" data-emphasis="ghost" data-tone="neutral" data-size="sm" data-icon-only="" aria-label="Dismiss" data-x="" onClick={() => toast.dismiss(t.id)}><XIcon /></button>
47
+ </div>
48
+ {t.life > 0 && <span data-part="progress" aria-hidden="true"></span>}
49
+ </div>
50
+ ))}
51
+ </section>
52
+ <div data-scope="announcer" role="status" aria-live="polite" aria-atomic="true"></div>
53
+ <div data-scope="announcer" role="alert" aria-live="assertive" aria-atomic="true"></div>
54
+ </>
55
+ )
56
+ }
package/src/Toggle.tsx ADDED
@@ -0,0 +1,42 @@
1
+ import { derived } from '@weft-js/core'
2
+ import type { Interaction } from '@weft-js/core/interaction'
3
+ import { Icon, type IconRef } from './Icon'
4
+ import { useUi } from './use-ui'
5
+ import './styles'
6
+ import '@weft-js/warp-core/styles/components/toggle-button.css'
7
+ import '@weft-js/warp-core/styles/components/chip.css' // the pressed toggle's forced-colours rule lives with the chip
8
+
9
+ /** Props for Toggle. */
10
+ export type ToggleProps = {
11
+ ref?: ((el: HTMLButtonElement) => void) | undefined
12
+ onClick?: ((e: MouseEvent) => void) | undefined
13
+ /** Called as the pointer comes over the button. */
14
+ onPointerEnter?: ((e: PointerEvent) => void) | undefined
15
+ pressed: boolean
16
+ icon?: IconRef | undefined
17
+ label?: string | undefined
18
+ size?: 'sm' | 'md' | 'lg' | undefined
19
+ ui?: Interaction
20
+ children?: unknown
21
+ [attr: string]: unknown
22
+ }
23
+
24
+ /** A toggle button (05 §2): `aria-pressed`, the sweep held while pressed. */
25
+ export function Toggle({ pressed, icon, label, size, ui, ref, children, onClick, onPointerEnter, ...rest }: ToggleProps) {
26
+ let host: HTMLButtonElement | undefined
27
+ useUi(() => ui, () => host)
28
+ const glyph = derived(() => icon)
29
+ // Released from pressed, the fill goes at once; the held stroke goes with it when the pointer and focus leave, so it
30
+ // doesn't retract across the face as a stray bracket. A plain hover keeps the sweep's own exit.
31
+ const click = (e: MouseEvent) => {
32
+ if (host?.getAttribute('aria-pressed') === 'true') host.dataset.released = ''
33
+ else delete host?.dataset.released
34
+ onClick?.(e)
35
+ }
36
+ const enter = (e: PointerEvent) => { if (host?.getAttribute('aria-pressed') !== 'true') delete host?.dataset.released; onPointerEnter?.(e) }
37
+ return (
38
+ <button type="button" data-scope="toggle" aria-pressed={pressed ? 'true' : 'false'} aria-label={label} data-size={size === 'md' ? undefined : size} data-sweep="" {...rest} onClick={click} onPointerEnter={enter} ref={n => { host = n; ref?.(n) }}>
39
+ {glyph.value && <Icon name={glyph.value} />}{children}<span data-part="sweep" aria-hidden="true"></span>
40
+ </button>
41
+ )
42
+ }
@@ -0,0 +1,32 @@
1
+ import { tooltip, uid, type Align, type Side } from '@weft-js/warp-core'
2
+ import { untrack } from '@weft-js/core'
3
+ import { use } from '@weft-js/warp-core/weft'
4
+ import './styles'
5
+ import '@weft-js/warp-core/styles/components/tooltip.css'
6
+
7
+ /**
8
+ * A hint on hover and keyboard focus, never essential (05 §3, §10 H1): `children` is the trigger (one focusable
9
+ * element); the tip follows it in the DOM as a manual popover in the top layer. The trigger is described by the tip
10
+ * unless its accessible name already says the same words (an icon button named "Settings" with tip "Settings").
11
+ * `inline` draws the tip shown, under its trigger in the page's flow, for docs and previews: no top layer, no hover wait.
12
+ */
13
+ export function Tooltip({ content, kbd, side, align, inline, children }: { content: string; kbd?: string; side?: Side; align?: Align; /** Shown under the trigger, in the page's flow: no top layer or hover wait. For docs and previews. */ inline?: boolean; children: unknown }) {
14
+ const id = uid('tip')
15
+ const flat = untrack(() => !!inline)
16
+ let tip: HTMLDivElement | undefined
17
+ // the trigger is the element before the tip; it is described by the tip unless its name already says the same
18
+ const attach = (el: HTMLElement, o: { side?: Side | undefined; align?: Align | undefined }) => {
19
+ const trigger = el.previousElementSibling as HTMLElement
20
+ if (trigger.getAttribute('aria-label') !== el.textContent) trigger.setAttribute('aria-describedby', [trigger.getAttribute('aria-describedby'), id].filter(Boolean).join(' '))
21
+ if (flat) return { update() {}, destroy() {} } // shown in flow: nothing to place, open or close
22
+ const action = tooltip(el, { trigger, side: o.side, align: o.align })
23
+ return { update: (n: { side?: Side | undefined; align?: Align | undefined }) => action.update({ trigger, side: n.side, align: n.align }), destroy: () => action.destroy() }
24
+ }
25
+ use(attach, () => tip, () => ({ side, align }))
26
+ return (
27
+ <>
28
+ {children}
29
+ <div ref={n => (tip = n)} data-scope="tooltip" data-part="content" role="tooltip" popover={flat ? undefined : 'manual'} data-presentation={flat ? 'inline' : undefined} id={id}>{content}{kbd && <kbd>{kbd}</kbd>}</div>
30
+ </>
31
+ )
32
+ }
package/src/Type.tsx ADDED
@@ -0,0 +1,46 @@
1
+ // Type by role (01 §2, PLAN §6): the spec defines the roles as tokens and leaves the element to the app, so these
2
+ // are what an app reads them through. A size, weight or colour is never a prop.
3
+ import { untrack } from '@weft-js/core'
4
+ import type { TextRole, Tone } from '@weft-js/core/ds'
5
+ import './styles'
6
+ import '@weft-js/warp-core/styles/components/link.css'
7
+ import './type.css'
8
+
9
+ type Open = { children?: unknown; [attr: string]: unknown }
10
+
11
+ type HeadingRole = 'display' | 'title' | 'heading' | 'label'
12
+
13
+ /** The role each level takes by default, so a page's `h1` is one size everywhere (`weft check` keeps the same table: D006). */
14
+ const ROLE: Record<number, HeadingRole> = { 1: 'title', 2: 'heading', 3: 'heading', 4: 'label', 5: 'label', 6: 'label' }
15
+
16
+ /**
17
+ * A real heading: `level` is the document outline (default 2) and picks the role: `title` for h1, `heading` for h2 and
18
+ * h3, `label` below. `role` overrides the look, so the two can differ, and `weft check` flags a level that renders at
19
+ * two roles in one app (D006). It follows `level` for skips too (D035).
20
+ */
21
+ export function Heading({ level = 2, role, children, ...rest }: Open & { level?: 1 | 2 | 3 | 4 | 5 | 6 | undefined; role?: HeadingRole | undefined }) {
22
+ const n = untrack(() => level)
23
+ const look = () => role ?? ROLE[n]
24
+ if (n === 1) return <h1 data-scope="text" data-role={look()} {...rest}>{children}</h1>
25
+ if (n === 2) return <h2 data-scope="text" data-role={look()} {...rest}>{children}</h2>
26
+ if (n === 3) return <h3 data-scope="text" data-role={look()} {...rest}>{children}</h3>
27
+ if (n === 4) return <h4 data-scope="text" data-role={look()} {...rest}>{children}</h4>
28
+ if (n === 5) return <h5 data-scope="text" data-role={look()} {...rest}>{children}</h5>
29
+ return <h6 data-scope="text" data-role={look()} {...rest}>{children}</h6>
30
+ }
31
+
32
+ /** Text in a role. A paragraph by default; `inline` makes it a span. `tone` colours it, `subtle` and `faint` quieten it. */
33
+ export function Text({ role, tone, subtle, faint, inline, children, ...rest }: Open & { role?: TextRole; tone?: Tone | undefined; subtle?: boolean; faint?: boolean; inline?: boolean }) {
34
+ const ink = () => (subtle ? 'subtle' : faint ? 'faint' : undefined)
35
+ if (untrack(() => inline)) return <span data-scope="text" data-role={role} data-tone={tone} data-ink={ink()} {...rest}>{children}</span>
36
+ return <p data-scope="text" data-role={role} data-tone={tone} data-ink={ink()} {...rest}>{children}</p>
37
+ }
38
+
39
+ /**
40
+ * A link (05 §4): underlined, with the underline sweep. `subtle` takes the surrounding ink, and `external` opens a new
41
+ * tab and says so. A bare `<a>` gets warp's colour from the base layer; this adds the rest, and D007 points to it.
42
+ */
43
+ export function Link({ href, subtle, external, children, ...rest }: Open & { href: string; subtle?: boolean | undefined; external?: boolean | undefined }) {
44
+ if (untrack(() => external)) return <a data-scope="link" data-variant={subtle ? 'subtle' : undefined} href={href} target="_blank" rel="noopener noreferrer" {...rest}>{children}<span class="vh"> (opens in a new tab)</span></a>
45
+ return <a data-scope="link" data-variant={subtle ? 'subtle' : undefined} href={href} {...rest}>{children}</a>
46
+ }