@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
package/src/Button.tsx ADDED
@@ -0,0 +1,100 @@
1
+ import { derived } from '@weft-js/core'
2
+ import type { Interaction } from '@weft-js/core/interaction'
3
+ import { pressOrigin } from '@weft-js/warp-core'
4
+ import { Icon, type IconRef } from './Icon'
5
+ import { useUi } from './use-ui'
6
+ import './styles'
7
+ import '@weft-js/warp-core/styles/components/button.css'
8
+ import '@weft-js/warp-core/styles/components/spinner.css'
9
+
10
+ /** The colour role a control speaks in. */
11
+ export type Tone = 'neutral' | 'accent' | 'positive' | 'caution' | 'critical' | 'info'
12
+ /** How much weight a control carries, from solid down to a plain link. */
13
+ export type Emphasis = 'solid' | 'soft' | 'outline' | 'ghost' | 'frame' | 'link'
14
+
15
+ /** Props for Button; anything else lands on the <button>. */
16
+ export interface ButtonProps {
17
+ /** solid (default) · soft · outline · ghost · frame (the signature) · link. Fixed at creation: it picks the anatomy. */
18
+ emphasis?: Emphasis | undefined
19
+ tone?: Tone | undefined
20
+ size?: 'sm' | 'md' | 'lg' | undefined
21
+ /** A frame button with the solid fill sweeping in. */
22
+ fill?: boolean
23
+ /** At vivid the fill spreads from where the pointer arrives and parts back out, instead of wiping in from the left. Not on frame or link. */
24
+ follow?: boolean
25
+ full?: boolean
26
+ /** Icon only: `label` is then required (it is the accessible name). */
27
+ icon?: IconRef | undefined
28
+ /** With `icon` and text: on a phone (or a tablet and a phone) only the icon shows, and the text stays the accessible name. */
29
+ collapse?: 'phone' | 'tablet' | undefined
30
+ ref?: ((el: HTMLButtonElement) => unknown) | undefined
31
+ onClick?: ((e: MouseEvent) => unknown) | undefined
32
+ label?: string | undefined
33
+ /** Busy: the label stays the accessible name (hidden by opacity), presses are swallowed, focus stays. */
34
+ loading?: boolean | undefined
35
+ /** `interaction()`'s row: real events fill it, and it draws whoever set it. */
36
+ ui?: Interaction
37
+ children?: unknown
38
+ [attr: string]: unknown
39
+ }
40
+
41
+ const swept = (e: Emphasis) => e !== 'link'
42
+
43
+ /**
44
+ * A native button (05 §2): all but link and frame append one [data-part="sweep"], and ::before paints the fill (the wipe,
45
+ * or with `follow` at vivid the two blobs that close on where the hand arrived); frame carries the six-part signature anatomy (SIGNATURE §1b).
46
+ */
47
+ export function Button({ emphasis = 'solid', tone, size, fill, follow, full, icon, collapse, label, loading, type = 'button', onClick, ui, ref, children, ...rest }: ButtonProps) {
48
+ pressOrigin() // one listener for the page; it only moves buttons that ask to follow
49
+ let host: HTMLButtonElement | undefined
50
+ useUi(() => ui, () => host)
51
+ const canLoad = derived(() => loading !== undefined)
52
+ const glyph = derived(() => icon)
53
+ // a loading or aria-disabled button swallows presses (components.html guard())
54
+ const press = (e: MouseEvent) => {
55
+ const el = e.currentTarget as HTMLElement
56
+ if (el.hasAttribute('data-loading') || el.getAttribute('aria-disabled') === 'true') return e.preventDefault()
57
+ onClick?.(e)
58
+ }
59
+ const content = () => <>{glyph.value && <Icon name={glyph.value} />}{collapse && glyph.value && children ? <span data-part="collapse">{children}</span> : children}</>
60
+ return (
61
+ <button
62
+ type={type}
63
+ data-scope="button"
64
+ data-emphasis={emphasis === 'solid' ? undefined : emphasis}
65
+ data-tone={tone}
66
+ data-size={size === 'md' ? undefined : size}
67
+ data-fill={emphasis === 'frame' && fill ? '' : undefined}
68
+ data-follow={follow && emphasis !== 'frame' && emphasis !== 'link' ? '' : undefined}
69
+ data-full={full ? '' : undefined}
70
+ data-icon-only={glyph.value && !children ? '' : undefined}
71
+ data-collapse={glyph.value && children ? collapse : undefined}
72
+ aria-label={label}
73
+ data-sweep={swept(emphasis) ? '' : undefined}
74
+
75
+ data-loading={loading ? '' : undefined}
76
+ aria-busy={loading ? 'true' : undefined}
77
+ aria-disabled={loading ? 'true' : undefined}
78
+ onClick={press}
79
+ {...rest}
80
+ ref={n => { host = n; ref?.(n) }}
81
+ >
82
+ {emphasis === 'frame' ? (
83
+ <>
84
+ {fill && <span data-part="sweep-fill" aria-hidden="true"></span>}
85
+ <span data-part="frame-corners" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
86
+ <span data-part="sweep-corners" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
87
+ <span data-part="sweep-outline" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i></span>
88
+ <span data-part="sweep-label">{content()}</span>
89
+ <span data-part="sweep-content" aria-hidden="true">{content()}</span>
90
+ </>
91
+ ) : canLoad.value ? (
92
+ <span data-part="label">{content()}</span>
93
+ ) : (
94
+ content()
95
+ )}
96
+ {loading && <span data-scope="spinner" data-part="spinner" aria-hidden="true"></span>}
97
+ {emphasis !== 'frame' && emphasis !== 'link' && <span data-part="sweep" aria-hidden="true"></span>}
98
+ </button>
99
+ )
100
+ }
@@ -0,0 +1,162 @@
1
+ import { LeftIcon, RightIcon } from '@weft-js/warp-icons'
2
+ import { addMonths, dayState, fmt, gridKey, isoWeek, isWeekend, isUnavailable, messages, monthEnd, monthStart, monthWeeks, ymd, weekStart, weekdays, type CalendarContext, type CalendarMode, type CalendarValue, type ChangeReason, type DateRange, type IsoDate } from '@weft-js/warp-core'
3
+ import { derived, flush, state, untrack, watch } from '@weft-js/core'
4
+ import { Button } from './Button'
5
+ import { bindable, uid, usePickerEnv, type Bindable } from './Picker'
6
+ import './styles'
7
+ import '@weft-js/warp-core/styles/rich/calendar-grid.css'
8
+
9
+ export interface GridProps {
10
+ mode: CalendarMode
11
+ months?: number
12
+ value: () => CalendarValue
13
+ compare?: () => DateRange | null | undefined
14
+ locale: string
15
+ today: string
16
+ min?: string | undefined
17
+ max?: string | undefined
18
+ isDateUnavailable?: ((d: IsoDate) => boolean) | undefined
19
+ firstDayOfWeek?: number | undefined
20
+ weekNumbers?: boolean | undefined
21
+ /** Marks the focused day `data-initial`, so a panel opens onto it. */
22
+ autofocus?: boolean
23
+ onPick: (iso: IsoDate, reason: ChangeReason) => void
24
+ onHover?: (iso: IsoDate | null) => void
25
+ onUnavailable?: (iso: IsoDate) => void
26
+ /** A cursor from outside (a preset moved the months). */
27
+ goTo?: () => string | null | undefined
28
+ children?: unknown
29
+ }
30
+
31
+ const startOf = (v: CalendarValue, today: string, mode: CalendarMode) => (mode === 'range' ? (v as DateRange)?.start : mode === 'multiple' ? [...((v as IsoDate[]) ?? [])].sort()[0] : mode === 'week' ? null : (v as string | null)) ?? today
32
+
33
+ /**
34
+ * The APG date grid (04 §1.4): one table per month, one tab stop on the cursor's day. Blank cells stand for days of
35
+ * other months. A day is never named "14" alone: every button carries the full date and its state.
36
+ */
37
+ export function CalendarGrid(p: GridProps) {
38
+ const firstDay = derived(() => p.firstDayOfWeek ?? weekStart(p.locale))
39
+ const count = () => p.months ?? 1
40
+ const cursor = state(untrack(() => startOf(p.value(), p.today, p.mode)))
41
+ const view = state(monthStart(untrack(() => cursor.value)))
42
+ const hover = state<string | null>(null)
43
+ const anim = state<'next' | 'prev' | undefined>(undefined)
44
+ let box: HTMLDivElement | undefined
45
+ const shown = derived(() => Array.from({ length: count() }, (_, n) => ({ month: addMonths(view.value, n) })))
46
+ const heads = derived(() => weekdays(firstDay.value, p.locale))
47
+ const t = derived(() => messages(p.locale))
48
+ const context = (): CalendarContext => ({ mode: p.mode, value: p.value(), compare: p.compare?.(), today: p.today, locale: p.locale, hover: hover.value, min: p.min, max: p.max, isDateUnavailable: p.isDateUnavailable })
49
+ const lastDay = () => monthEnd(addMonths(view.value, count() - 1))
50
+ const moveTo = (iso: string) => {
51
+ if (iso < view.value) { anim.value = 'prev'; view.value = monthStart(iso) }
52
+ else if (iso > lastDay()) { anim.value = 'next'; view.value = monthStart(addMonths(iso, -(count() - 1))) }
53
+ cursor.value = iso
54
+ }
55
+ const focusCursor = () => { flush(); box?.querySelector<HTMLElement>('[data-part="day"][tabindex="0"]')?.focus() }
56
+ const pick = (iso: string, reason: ChangeReason) => {
57
+ if (isUnavailable(iso, context())) return p.onUnavailable?.(iso as IsoDate)
58
+ cursor.value = iso
59
+ hover.value = null
60
+ p.onPick(iso as IsoDate, reason)
61
+ }
62
+ const onKey = (e: KeyboardEvent) => {
63
+ if (!(e.target as Element).closest?.('[data-part="day"]')) return
64
+ const to = gridKey(e.key, e.shiftKey, cursor.value, firstDay.value, getComputedStyle(box!).direction === 'rtl')
65
+ if (!to) return
66
+ e.preventDefault()
67
+ if (to === 'pick') return pick(cursor.value, 'keyboard')
68
+ if (p.mode === 'range') hover.value = to
69
+ moveTo(to)
70
+ focusCursor()
71
+ }
72
+ const onClick = (e: MouseEvent) => {
73
+ const b = (e.target as Element).closest?.<HTMLElement>('[data-part="day"]')
74
+ if (b) pick(b.dataset.date!, 'pointer')
75
+ }
76
+ const onOver = (e: PointerEvent) => {
77
+ const b = (e.target as Element).closest?.<HTMLElement>('[data-part="day"]')
78
+ if (p.mode !== 'range' || !b || hover.value === b.dataset.date) return
79
+ hover.value = b.dataset.date!
80
+ p.onHover?.(hover.value as IsoDate)
81
+ }
82
+ const onLeave = () => { if (p.mode === 'range') { hover.value = null; p.onHover?.(null) } }
83
+ const page = (d: number) => { anim.value = d > 0 ? 'next' : 'prev'; view.value = addMonths(view.value, d); cursor.value = addMonths(cursor.value, d) }
84
+ watch(() => p.goTo?.(), iso => { if (iso) moveTo(iso) })
85
+ return (
86
+ <div ref={n => (box = n)} data-scope="calendar" data-part="root">
87
+ <Button emphasis="ghost" size="sm" icon={LeftIcon} label={t.value.prev} data-part="nav" data-dir="prev" data-icon="" onClick={() => page(-1)} />
88
+ <Button emphasis="ghost" size="sm" icon={RightIcon} label={t.value.next} data-part="nav" data-dir="next" data-icon="" onClick={() => page(1)} />
89
+ <div data-part="months" data-anim={anim.value} onKeyDown={onKey} onClick={onClick} onPointerOver={onOver} onPointerLeave={onLeave}>
90
+ {shown.value.map(m => <Month key={m.month} month={m.month} heads={heads.value} first={firstDay.value} weekNumbers={p.weekNumbers} locale={p.locale} wholeWeek={p.mode === 'week'} ctx={context} cursor={cursor.value} initial={p.autofocus} />)}
91
+ </div>
92
+ {p.children}
93
+ </div>
94
+ )
95
+ }
96
+
97
+ function Month({ month, heads, first, weekNumbers, locale, wholeWeek, ctx, cursor, initial }: { month: string; heads: readonly (readonly [string, string, number])[]; first: number; weekNumbers?: boolean | undefined; locale: string; wholeWeek: boolean; ctx: () => CalendarContext; cursor: string; initial?: boolean | undefined }) {
98
+ const cap = uid('cap')
99
+ const weeks = derived(() => monthWeeks(month, first).map(days => ({ days: days.map((iso, c) => ({ iso, k: iso ?? `b${c}`, weekday: (first + c) % 7 })), k: days.find(Boolean)!, week: isoWeek(days.find(Boolean)!).week })))
100
+ return (
101
+ <div data-part="month">
102
+ <div data-part="caption" id={cap} aria-live="polite">{fmt(month, { month: 'long', year: 'numeric' }, locale)}</div>
103
+ <table role="grid" aria-labelledby={cap}>
104
+ <thead>
105
+ <tr>
106
+ {weekNumbers && <th scope="col" abbr={messages(locale).week}><span class="sr">{messages(locale).week}</span></th>}
107
+ {heads.map(h => <th key={h[0]} scope="col" abbr={h[0]}>{h[1]}</th>)}
108
+ </tr>
109
+ </thead>
110
+ <tbody>
111
+ {weeks.value.map(w => (
112
+ <tr key={w.k}>
113
+ {weekNumbers && <th scope="row" data-part="week">{w.week}</th>}
114
+ {w.days.map(d => (d.iso ? <Day key={d.k} iso={d.iso} weekend={isWeekend(d.weekday, locale)} locale={locale} wholeWeek={wholeWeek} ctx={ctx} cursor={cursor} initial={initial} /> : <td key={d.k} role="gridcell"></td>))}
115
+ </tr>
116
+ ))}
117
+ </tbody>
118
+ </table>
119
+ </div>
120
+ )
121
+ }
122
+
123
+ function Day({ iso, weekend, locale, ctx, cursor, initial }: { iso: IsoDate; weekend: boolean; locale: string; wholeWeek: boolean; ctx: () => CalendarContext; cursor: string; initial?: boolean | undefined }) {
124
+ const s = derived(() => dayState(iso, ctx()))
125
+ const n = derived(() => new Intl.NumberFormat(locale).format(ymd(iso)[2]))
126
+ return (
127
+ <td role="gridcell" aria-selected={s.value.cellSelected ? 'true' : 'false'} data-range={s.value.range} data-preview={s.value.preview ? '' : undefined} data-compare={s.value.compare ? '' : undefined}>
128
+ <button type="button" data-part="day" data-date={iso} tabindex={iso === cursor ? '0' : '-1'} data-initial={initial && iso === cursor ? '' : undefined} data-weekend={weekend ? '' : undefined} data-selected={s.value.selected ? '' : undefined} data-booked={s.value.unavailable && s.value.booked ? '' : undefined} aria-current={s.value.today ? 'date' : undefined} aria-disabled={s.value.unavailable ? 'true' : undefined} aria-label={s.value.name}>{n.value}</button>
129
+ </td>
130
+ )
131
+ }
132
+
133
+ export type InlineCalendarValue = IsoDate | IsoDate[] | DateRange | null
134
+
135
+ /**
136
+ * InlineCalendar (04 §2): the same grid, standalone, for availability and booking. 1–3 months; `single`, `multiple`
137
+ * (toggles days, announces the count) or `range`. No field shell: `aria-label` names it.
138
+ */
139
+ export function InlineCalendar({ selection = 'single', months = 1, value, defaultValue, onValueChange, min, max, isDateUnavailable, firstDayOfWeek, weekNumbers, locale, today, ...rest }: Bindable<InlineCalendarValue> & { selection?: 'single' | 'multiple' | 'range'; months?: 1 | 2 | 3; min?: string | undefined; max?: string | undefined; isDateUnavailable?: ((d: IsoDate) => boolean) | undefined; firstDayOfWeek?: number | undefined; weekNumbers?: boolean | undefined; locale?: string | undefined; today?: string | undefined; 'aria-label': string }) {
140
+ const env = usePickerEnv(() => locale, () => today)
141
+ const empty = (): InlineCalendarValue => (selection === 'multiple' ? [] : selection === 'range' ? { start: null, end: null } : null)
142
+ const v = bindable<InlineCalendarValue>(() => value, () => defaultValue, untrack(empty), () => onValueChange)
143
+ const said = state('')
144
+ const onPick = (iso: IsoDate, reason: ChangeReason) => {
145
+ if (selection === 'single') return v.set(iso, reason)
146
+ if (selection === 'multiple') {
147
+ const now = (v.value as IsoDate[]) ?? []
148
+ const next = now.includes(iso) ? now.filter(d => d !== iso) : [...now, iso]
149
+ v.set(next, reason)
150
+ said.value = next.length === 1 ? '1 day selected' : `${next.length} days selected`
151
+ return
152
+ }
153
+ const r = v.value as DateRange
154
+ v.set(!r?.start || r.end ? { start: iso, end: null } : iso < r.start ? { start: iso, end: r.start } : { start: r.start, end: iso }, reason)
155
+ }
156
+ return (
157
+ <div role="group" {...rest}>
158
+ <CalendarGrid mode={selection} months={months} value={() => v.value} locale={env.locale.value} today={env.today.value} min={min} max={max} isDateUnavailable={isDateUnavailable} firstDayOfWeek={firstDayOfWeek} weekNumbers={weekNumbers} onPick={onPick} />
159
+ <span class="sr" role="status">{said.value}</span>
160
+ </div>
161
+ )
162
+ }
package/src/Canvas.tsx ADDED
@@ -0,0 +1,110 @@
1
+ import { surface, type Frame, type SurfaceOptions } from '@weft-js/warp-core/canvas'
2
+ import { state, untrack } from '@weft-js/core'
3
+ import { uid } from '@weft-js/warp-core'
4
+ import { floatStage, stage, stageSelect, type StageOptions } from '@weft-js/warp-core/stage'
5
+ import { use } from '@weft-js/warp-core/weft'
6
+ import './styles'
7
+ import '@weft-js/warp-core/styles/canvas/canvas.css'
8
+ import '@weft-js/warp-core/styles/canvas/stage.css'
9
+
10
+ export type { Frame, Pointer } from '@weft-js/warp-core/canvas'
11
+ export type { Piece as StagePiece, Press, Scene, Scroll, StageLayout, StagePainter, Word } from '@weft-js/warp-core/stage'
12
+
13
+ /** Props for Canvas. */
14
+ export interface CanvasProps {
15
+ /** Paints one frame: `ctx` is already scaled to CSS pixels. Return true to be called again without `animate`. */
16
+ draw: (frame: Frame) => boolean | void
17
+ /** Moves at balanced and vivid. Quiet and reduced motion paint one frame, with `still` set. */
18
+ animate?: boolean
19
+ /** Caps the frame rate while it animates. Uncapped by default, at the display's rate. */
20
+ fps?: number
21
+ /** Whole-cell mode: `width` and `height` count cells of this many CSS pixels, scaled up without smoothing. */
22
+ cell?: number
23
+ /** Settings for the 2D context, such as `{ alpha: false }`. Read once, when the canvas attaches. */
24
+ context?: SurfaceOptions['context']
25
+ /** Called before the repaint when a theme, tier or motion dial changes around the canvas. Read colours again here. */
26
+ onTheme?: () => void
27
+ /** Track the pointer into `frame.pointer`, and repaint when it moves over the canvas. */
28
+ pointer?: boolean
29
+ /** Names the drawing (role="img"). Without one it is decorative. */
30
+ label?: string | undefined
31
+ [attr: string]: unknown
32
+ }
33
+
34
+ /** A canvas that handles pixel ratio, resize, off-screen and reduced motion, for anything warp doesn't draw itself (08 §6.7). */
35
+ export function Canvas({ draw, animate, fps, cell, context, onTheme, pointer, label, ...rest }: CanvasProps) {
36
+ let node: HTMLCanvasElement | undefined
37
+ use(surface, () => node, () => ({ draw, animate, fps, cell, context, onTheme, pointer }))
38
+ return <div {...rest} data-scope="canvas"><canvas ref={n => (node = n)} role={label ? 'img' : undefined} aria-label={label} aria-hidden={label ? undefined : 'true'}></canvas></div>
39
+ }
40
+
41
+ /** Props for CanvasStage. */
42
+ export interface CanvasStageProps extends StageOptions {
43
+ /** `box` paints its own block; `scroll` is a scroller whose view it paints; `page` paints the viewport, for a whole page. */
44
+ layout?: StageOptions['layout']
45
+ /** A modal's stage: the dialog spans the viewport and the stage centres its panel. */
46
+ veil?: boolean | undefined
47
+ children?: unknown
48
+ [attr: string]: unknown
49
+ }
50
+
51
+ /**
52
+ * A canvas that paints the page laid out inside it (08 §6.8). Mark any element `data-skin="button"`, `card`, or a name of
53
+ * your own: it keeps its layout, focus, typing and semantics, and your painters draw it with a 2D context, states and all.
54
+ */
55
+ export function CanvasStage({ layout = 'box', painters, background, overlay, paint, animate, cell, fps, rail, context, onTheme, veil, children, ...rest }: CanvasStageProps) {
56
+ let node: HTMLCanvasElement | undefined
57
+ use(stage, () => node, () => ({ layout, painters, background, overlay, paint, animate, cell, fps, rail, context, onTheme }))
58
+ return (
59
+ <div {...rest} data-scope="stage" data-layout={layout} data-rail={rail ? '' : undefined} data-veil={veil ? '' : undefined}>
60
+ <div data-part="paint" aria-hidden="true"><canvas ref={n => (node = n)}></canvas></div>
61
+ {children}
62
+ </div>
63
+ )
64
+ }
65
+
66
+ /** One choice of a StageSelect: a string is its own value and label. */
67
+ export type StageOption = string | { value: string; label: string; disabled?: boolean | undefined }
68
+
69
+ /** Props for StageSelect. */
70
+ export interface StageSelectProps {
71
+ options: StageOption[]
72
+ /** The value it starts on; the first option when left out. */
73
+ value?: string | undefined
74
+ /** Puts the value in a form, through a hidden input. */
75
+ name?: string | undefined
76
+ disabled?: boolean | undefined
77
+ /** Hears each value picked. */
78
+ onChange?: ((value: string) => void) | undefined
79
+ [attr: string]: unknown
80
+ }
81
+
82
+ /**
83
+ * A select a stage draws, list and all (APG select-only combobox). Its trigger is a `field` skin; its list floats in the
84
+ * top layer as a stage of its own, in the look of the one around it, with `menu` and `option` skins. Wrap it in a
85
+ * `<label>` to name it.
86
+ */
87
+ export function StageSelect({ options, value, name, disabled, onChange, ...rest }: StageSelectProps) {
88
+ const items = untrack(() => options).map(o => (typeof o === 'string' ? { value: o, label: o } : o))
89
+ const current = state(untrack(() => value) ?? items[0]?.value ?? '')
90
+ const id = uid('stage-sel')
91
+ let trigger: HTMLButtonElement | undefined, paint: HTMLCanvasElement | undefined
92
+ use(stageSelect, () => trigger, () => ({ value: current.value, onPick: (v: string) => { current.value = v; onChange?.(v) } }))
93
+ use(floatStage, () => paint)
94
+ return (
95
+ <span data-part="select">
96
+ <button {...rest} ref={n => (trigger = n)} type="button" data-skin="field" role="combobox" aria-haspopup="listbox" aria-expanded="false" aria-controls={id} disabled={disabled}>
97
+ {items.find(i => i.value === current.value)?.label ?? ''}
98
+ </button>
99
+ <div popover="manual" data-scope="stage" data-layout="box" data-part="float">
100
+ <div data-part="paint" aria-hidden="true"><canvas ref={n => (paint = n)}></canvas></div>
101
+ <div role="listbox" id={id} data-skin="menu">
102
+ {items.map((o, i) => (
103
+ <div key={o.value} role="option" id={`${id}-${i}`} data-skin="option" data-value={o.value} aria-selected={current.value === o.value ? 'true' : 'false'} aria-disabled={o.disabled ? 'true' : undefined}>{o.label}</div>
104
+ ))}
105
+ </div>
106
+ </div>
107
+ {name && <input type="hidden" name={name} value={current.value} />}
108
+ </span>
109
+ )
110
+ }
package/src/Card.tsx ADDED
@@ -0,0 +1,29 @@
1
+ import { untrack } from '@weft-js/core'
2
+ import type { Interaction } from '@weft-js/core/interaction'
3
+ import { useUi } from './use-ui'
4
+ import './styles'
5
+ import '@weft-js/warp-core/styles/components/surface.css'
6
+
7
+ /**
8
+ * A surface (05 §1, §6: Card, Panel). Interactive cards (`href` or `onClick`) are links or buttons and always carry
9
+ * the sweep at every stop; static ones carry `data-sweep="surface"`, which only `data-flair="sweep"` shows.
10
+ */
11
+ export function Card({ eyebrow, title, href, onClick, sweep = true, ui, ref, children, ...rest }: { ref?: ((el: HTMLElement) => void) | undefined; eyebrow?: unknown; title?: unknown; href?: string; onClick?: ((e: MouseEvent) => void) | undefined; sweep?: boolean; ui?: Interaction; children?: unknown; [attr: string]: unknown }) {
12
+ let host: HTMLElement | undefined
13
+ const took = (n: HTMLElement) => { host = n; ref?.(n) }
14
+ useUi(() => ui, () => host)
15
+ const interactive = untrack(() => !!(href || onClick))
16
+ const swept = untrack(() => sweep)
17
+ const parts = () => (
18
+ <>
19
+ {eyebrow !== undefined && <span data-part="eyebrow">{eyebrow}</span>}
20
+ {title !== undefined && <span data-part="title">{title}</span>}
21
+ {children}
22
+ {swept && <span data-part="sweep" aria-hidden="true"></span>}
23
+ </>
24
+ )
25
+ const sweepAttr = swept ? (interactive ? '' : 'surface') : undefined
26
+ if (untrack(() => href)) return <a href={href} data-scope="surface" data-sweep={sweepAttr} onClick={onClick!} {...rest} ref={took}>{parts()}</a>
27
+ if (interactive) return <button type="button" data-scope="surface" data-sweep={sweepAttr} onClick={onClick!} {...rest} ref={took}>{parts()}</button>
28
+ return <div data-scope="surface" data-sweep={sweepAttr} {...rest} ref={took}>{parts()}</div>
29
+ }
package/src/Chart.tsx ADDED
@@ -0,0 +1,133 @@
1
+ import { uid } from '@weft-js/warp-core'
2
+ import { chart, SSR_RADIUS, SSR_WIDTH, tableHTML, toHTML, type Ctx, type Form } from '@weft-js/warp-core/charts'
3
+ import { derived, untrack } from '@weft-js/core'
4
+ import { use } from '@weft-js/warp-core/weft'
5
+ import './styles'
6
+ import '@weft-js/warp-core/styles/data/tokens.css'
7
+ import '@weft-js/warp-core/styles/data/base.css'
8
+ import '@weft-js/warp-core/styles/data/chart.css'
9
+ import '@weft-js/warp-core/styles/data/legend.css'
10
+ import '@weft-js/warp-core/styles/data/tooltip.css'
11
+
12
+ /** What every chart shares around its plot: the caption, the toolbar and the loading, empty and error states. */
13
+ export type ChartFrame = {
14
+ title: string
15
+ /** The title's heading level: one below the heading the chart sits under. Default 3, a chart in a page section. */
16
+ level?: 2 | 3 | 4 | 5 | 6
17
+ description?: unknown
18
+ /** Beside the view toggle: a layout switch, a range picker. */
19
+ toolbar?: unknown
20
+ /** Replaces the plot, in the same frame (06 §12). With no data the frame shows `empty` on its own. */
21
+ state?: 'loading' | 'empty' | 'error' | undefined
22
+ loading?: string | undefined
23
+ /** The title and detail shown in place of the plot when there is no data. */
24
+ empty?: { title: string; detail?: unknown } | undefined
25
+ error?: { title: string; detail?: unknown } | undefined
26
+ /** The next step under an empty or error state: a Retry button. */
27
+ action?: unknown
28
+ }
29
+ type Props<C> = ChartFrame & C & { form: Form<C> }
30
+
31
+ // props that are the frame's, not a form's: a JSX prop builds its element each time it's read, so these are read once
32
+ const FRAME = new Set(['form', 'level', 'description', 'toolbar', 'action', 'empty', 'error', 'loading'])
33
+
34
+ const Drawn = ({ stroke }: { stroke: string }) => (
35
+ <svg width="72" height="40" viewBox="0 0 72 40" aria-hidden="true">
36
+ <path d="M8 0V40M22 0V40M36 0V40M50 0V40M64 0V40" stroke="var(--warp-color-border-strong)" />
37
+ <path d={stroke === 'var(--warp-color-critical-solid)' ? 'M0 20H30' : 'M0 14H72M0 26H72'} stroke={stroke} stroke-width="2" />
38
+ </svg>
39
+ )
40
+
41
+ function Standby({ state, title, text, action }: { state: 'loading' | 'empty' | 'error'; title: string; text?: { title: string; detail?: unknown } | undefined; action?: unknown }) {
42
+ if (untrack(() => state) === 'loading')
43
+ return (
44
+ <div data-part="state" role="status" aria-busy="true">
45
+ <div aria-hidden="true" style="display:flex;align-items:end;gap:10px;block-size:118px;inline-size:100%;padding-inline:6px">
46
+ {[40, 70, 55, 90, 64, 80, 96].map(h => <span key={h} data-part="skeleton" style={`--h:${h}px`}></span>)}
47
+ </div>
48
+ {text?.title ?? `Loading ${title}…`}
49
+ </div>
50
+ )
51
+ const failed = untrack(() => state) === 'error'
52
+ return (
53
+ <div data-part="state" role={failed ? 'alert' : 'status'}>
54
+ <Drawn stroke={failed ? 'var(--warp-color-critical-solid)' : 'var(--warp-color-accent-solid)'} />
55
+ <b style="color:var(--warp-color-text)">{text?.title ?? (failed ? `Couldn’t load ${title}` : `No ${title}`)}</b>
56
+ {text?.detail}
57
+ {action}
58
+ </div>
59
+ )
60
+ }
61
+
62
+ function Ready<C>(p: { form: Form<C>; cfg: () => C; id: string; root: () => HTMLElement | undefined }) {
63
+ const id = () => p.id
64
+ // the first layout, once: from here on `chart` owns these nodes, so nothing below re-renders them
65
+ const first = derived(() => untrack(() => p.form.plot(p.cfg(), SSR_WIDTH, { id: p.id, hidden: new Set(), r: SSR_RADIUS } satisfies Ctx)))
66
+ const legend = derived(() => p.form.legend?.(p.cfg()) ?? [])
67
+ const table = derived(() => tableHTML(p.form.table(p.cfg())))
68
+ const summary = derived(() => {
69
+ const c = p.cfg() as C & { summary?: string | ((c: C) => string) | undefined }
70
+ return typeof c.summary === 'function' ? c.summary(c) : c.summary ?? p.form.summary(c)
71
+ })
72
+ use(chart<C>, () => p.root(), () => ({ form: p.form, cfg: p.cfg(), id: p.id }))
73
+ return (
74
+ <>
75
+ {p.form.ramp !== undefined
76
+ ? <ul data-scope="legend" aria-label="Scale"><li style="display:flex;align-items:center;gap:6px;font-size:var(--warp-text-caption-size);color:var(--warp-color-text-subtle);padding-inline:6px">Fewer {[0, 1, 2, 3, 4, 5].slice(p.form.ramp).map(b => <i key={b} style={`display:inline-block;inline-size:11px;block-size:11px;border-radius:2px;background:var(--warp-color-data-seq-${b})`}></i>)} More</li></ul>
77
+ : legend.value.length > 0 && (
78
+ <ul data-scope="legend" aria-label="Series">
79
+ {legend.value.map((s, i) => <li key={s.name}><button type="button" aria-pressed="true" data-si={i} style={`--_dc:${s.color}`}><i data-shape={p.form.swatch ?? 'box'}></i>{s.name}{s.value !== undefined && <> <b>{s.value}</b></>}</button></li>)}
80
+ </ul>
81
+ )}
82
+ <div data-part="plot">
83
+ <div data-scope="tooltip" data-part="chart-tip" aria-hidden="true"></div>
84
+ <svg data-part="svg" viewBox={`0 0 ${first.value.w ?? SSR_WIDTH} ${first.value.h}`} width={first.value.w ?? SSR_WIDTH} height={first.value.h} style={first.value.style} role="group" aria-roledescription={p.form.kind} aria-labelledby={`${id()}-t`} aria-describedby={`${id()}-s`}
85
+ // oxlint-disable-next-line weft/no-html-sink -- toHTML escapes every text and attribute of the scene
86
+ innerHTML={toHTML(first.value.body)}></svg>
87
+ </div>
88
+ <div data-part="table" tabindex="0" role="region" aria-labelledby={`${id()}-t`}
89
+ // oxlint-disable-next-line weft/no-html-sink -- tableHTML escapes every cell
90
+ innerHTML={table.value}></div>
91
+ <p data-part="summary" id={`${id()}-s`}>{summary.value}</p>
92
+ </>
93
+ )
94
+ }
95
+
96
+ /**
97
+ * The frame every chart shares (06 §12): figure > head · legend · plot (svg, tooltip) · table · summary. The plot is
98
+ * laid out once here, so the server sends a whole chart, and from then on warp-core's `chart` owns it: width and data
99
+ * changes rewrite attributes on the same nodes, and the legend and the view toggle are delegated from the figure.
100
+ */
101
+ export function Chart<C extends { data: unknown; title: string }>(props: Props<C>) {
102
+ const id = uid('chart')
103
+ let figure: HTMLElement | undefined
104
+ const cfg = () => {
105
+ const c: Record<string, unknown> = {}
106
+ for (const k of Object.keys(props)) if (!FRAME.has(k)) c[k] = (props as Record<string, unknown>)[k]
107
+ return c as C
108
+ }
109
+ const hasData = () => { const d = props.data; return d !== undefined && d !== null && (!Array.isArray(d) || d.length > 0) }
110
+ const status = () => props.state ?? (hasData() ? undefined : 'empty')
111
+ return (
112
+ <figure ref={n => (figure = n)} data-scope="chart" data-view="chart" data-enter="wait">
113
+ <div data-part="head">
114
+ <div>
115
+ <h3 id={`${id}-t`} aria-level={props.level === 3 ? undefined : props.level} style="margin:0;font-family:var(--warp-text-heading-font);font-size:calc(var(--warp-text-heading-size) - 3px);font-weight:var(--warp-text-heading-weight);letter-spacing:var(--warp-text-heading-tracking)">{props.title}</h3>
116
+ {props.description !== undefined && <p style="margin:2px 0 0;font-size:var(--warp-text-caption-size);color:var(--warp-color-text-subtle)">{props.description}</p>}
117
+ </div>
118
+ <div style="display:flex;flex-wrap:wrap;align-items:center;gap:8px">
119
+ {props.toolbar}
120
+ {status() === undefined && (
121
+ <div data-part="view-toggle" role="group" aria-label="View">
122
+ <button type="button" aria-pressed="true" data-view="chart" aria-label="Chart view"><svg aria-hidden="true" focusable="false" viewBox="0 0 16 16"><path d="M2 13.5h12M3.5 10.5l3-3.5 2.5 2 3.5-4.5" /></svg></button>
123
+ <button type="button" aria-pressed="false" data-view="table" aria-label="Table view"><svg aria-hidden="true" focusable="false" viewBox="0 0 16 16"><rect x="2" y="2.5" width="12" height="11" rx="1.5" /><path d="M2 6.5h12M2 10h12M6.5 6.5v7" /></svg></button>
124
+ </div>
125
+ )}
126
+ </div>
127
+ </div>
128
+ {status() === undefined
129
+ ? <Ready form={props.form} cfg={cfg} id={id} root={() => figure} />
130
+ : <Standby state={status()!} title={props.title} text={status() === 'loading' ? (props.loading === undefined ? undefined : { title: props.loading }) : status() === 'error' ? props.error : props.empty} action={props.action} />}
131
+ </figure>
132
+ )
133
+ }
package/src/Charts.tsx ADDED
@@ -0,0 +1,69 @@
1
+ // The chart forms (06 §12), one line each: a form is a pure layout in warp-core, and `Chart` is the frame around it.
2
+ // Nothing here runs at import, so an app pays only for the forms it renders.
3
+ import { spark as sparkAction, sparkScene, SSR_WIDTH, toHTML, bar, box, brush, bubble, candlestick, combo, donut, funnel, gauge, heat, histogram, line, radar, radial, sankey, scatter, stream, sunburst, tile, treemap, waterfall, type BarConfig, type BoxConfig, type BrushConfig, type CandleConfig, type ComboConfig, type DonutConfig, type FunnelConfig, type GaugeConfig, type HeatConfig, type HistConfig, type LineConfig, type RadarConfig, type RadialConfig, type SankeyConfig, type ScatterConfig, type StreamConfig, type SunConfig, type TileConfig, type TreemapConfig, type WaterConfig } from '@weft-js/warp-core/charts'
4
+ import { state, untrack } from '@weft-js/core'
5
+ import { Button } from './Button'
6
+ import { Chart, type ChartFrame } from './Chart'
7
+ import { use } from '@weft-js/warp-core/weft'
8
+ import './styles'
9
+ import '@weft-js/warp-core/styles/data/tokens.css'
10
+ import '@weft-js/warp-core/styles/data/base.css'
11
+
12
+ /** Lines over a shared x axis. */
13
+ export const LineChart = (p: ChartFrame & LineConfig) => <Chart form={line} {...p} />
14
+ /** Bars, grouped or stacked. */
15
+ export const BarChart = (p: ChartFrame & BarConfig) => <Chart form={bar} {...p} />
16
+ /** Bars and lines on one plot. */
17
+ export const ComboChart = (p: ChartFrame & ComboConfig) => <Chart form={combo} {...p} />
18
+ /** A ring of parts of a whole. */
19
+ export const DonutChart = (p: ChartFrame & DonutConfig) => <Chart form={donut} {...p} />
20
+ /** Concentric arcs, one per value. */
21
+ export const RadialChart = (p: ChartFrame & RadialConfig) => <Chart form={radial} {...p} />
22
+ /** Stages narrowing from first to last. */
23
+ export const FunnelChart = (p: ChartFrame & FunnelConfig) => <Chart form={funnel} {...p} />
24
+ /** Dots at x and y. */
25
+ export const ScatterChart = (p: ChartFrame & ScatterConfig) => <Chart form={scatter} {...p} />
26
+ /** Dots at x and y, sized by a third value. */
27
+ export const BubbleChart = (p: ChartFrame & ScatterConfig) => <Chart form={bubble} {...p} />
28
+ /** Values binned into equal ranges. */
29
+ export const Histogram = (p: ChartFrame & HistConfig) => <Chart form={histogram} {...p} />
30
+ /** A grid of cells coloured by value. */
31
+ export const HeatGrid = (p: ChartFrame & HeatConfig) => <Chart form={heat} {...p} />
32
+ /** Series on axes spread around a circle. */
33
+ export const RadarChart = (p: ChartFrame & RadarConfig) => <Chart form={radar} {...p} />
34
+ /** Nested rectangles sized by value. */
35
+ export const Treemap = (p: ChartFrame & TreemapConfig) => <Chart form={treemap} {...p} />
36
+ /** Nested rings sized by value. */
37
+ export const Sunburst = (p: ChartFrame & SunConfig) => <Chart form={sunburst} {...p} />
38
+ /** Flows between stages, band width showing the amount. */
39
+ export const SankeyChart = (p: ChartFrame & SankeyConfig) => <Chart form={sankey} {...p} />
40
+ /** Open, high, low and close per period. */
41
+ export const CandlestickChart = (p: ChartFrame & CandleConfig) => <Chart form={candlestick} {...p} />
42
+ /** Quartiles and whiskers per group. */
43
+ export const BoxPlot = (p: ChartFrame & BoxConfig) => <Chart form={box} {...p} />
44
+ /** A running total, each step a rise or a fall. */
45
+ export const WaterfallChart = (p: ChartFrame & WaterConfig) => <Chart form={waterfall} {...p} />
46
+ /** A long series with a window dragged over it. */
47
+ export const BrushChart = (p: ChartFrame & BrushConfig) => <Chart form={brush} {...p} />
48
+ /** Regions drawn as equal tiles. */
49
+ export const TileMap = (p: ChartFrame & TileConfig) => <Chart form={tile} {...p} />
50
+ /** One value on an arc between its bounds. */
51
+ export const GaugeChart = (p: ChartFrame & GaugeConfig) => <Chart form={gauge} {...p} />
52
+
53
+ /** A rolling window (06 §12): one `d` write per tick on a fixed domain. Pausing holds the window and shows it as a table. */
54
+ export function StreamChart(p: ChartFrame & Omit<StreamConfig, 'paused'>) {
55
+ const paused = state(false)
56
+ let held = untrack(() => p.data)
57
+ const flip = () => { held = p.data; paused.value = !paused.value }
58
+ return <Chart form={stream} {...p} data={paused.value ? held : p.data} paused={paused.value} toolbar={<Button emphasis="outline" onClick={flip}>{paused.value ? 'Resume' : 'Pause'}</Button>} />
59
+ }
60
+
61
+ /** No axes, end dot, `aria-hidden`: a sparkline is context inside a labelled parent. */
62
+ /** `format` says a hovered point the way the headline beside it does (`v => \`${v}%\``), so the unit carries over. */
63
+ export function Sparkline({ values, color, format }: { values: number[]; color?: string | undefined; format?: ((value: number) => string) | undefined }) {
64
+ let svg: SVGSVGElement | undefined
65
+ use(sparkAction, () => svg, () => (format ? { values, format } : values))
66
+ return <svg ref={n => (svg = n)} data-part="spark" viewBox={`0 0 ${SSR_WIDTH} 36`} aria-hidden="true" preserveAspectRatio="none" style={`display:block;inline-size:100%;block-size:36px;overflow:visible${color ? `;--_dc:${color}` : ''}`}
67
+ // oxlint-disable-next-line weft/no-html-sink -- toHTML escapes every text and attribute of the scene
68
+ innerHTML={toHTML(sparkScene(untrack(() => values), SSR_WIDTH))}></svg>
69
+ }
package/src/Chip.tsx ADDED
@@ -0,0 +1,27 @@
1
+ import { CheckIcon } from '@weft-js/warp-icons'
2
+ import { derived } from '@weft-js/core'
3
+ import type { Interaction } from '@weft-js/core/interaction'
4
+ import { Icon, type IconRef } from './Icon'
5
+ import { useUi } from './use-ui'
6
+ import './fields'
7
+ import '@weft-js/warp-core/styles/inputs/choice.css'
8
+ // the inputs form sits on the components chip (inputs.html loads both): pressed fill, forced colours
9
+ import '@weft-js/warp-core/styles/components/chip.css'
10
+ import '@weft-js/warp-core/styles/components/toggle-button.css'
11
+
12
+ const sweepParts = () => <><span data-part="sweep-base" aria-hidden="true"></span><span data-part="sweep" aria-hidden="true"></span></>
13
+
14
+ /** A filter chip (03 §4 Chips): a toggle button; pressed pins the sweep on accent-soft and draws the check. */
15
+ export function Chip({ pressed, count, icon, ui, ref, children, ...rest }: { ref?: ((el: HTMLButtonElement) => unknown) | undefined; pressed?: boolean; count?: unknown; icon?: IconRef | undefined; ui?: Interaction; children?: unknown; [attr: string]: unknown }) {
16
+ let host: HTMLButtonElement | undefined
17
+ useUi(() => ui, () => host)
18
+ const glyph = derived(() => icon)
19
+ return (
20
+ <button type="button" data-scope="chip" data-sweep="" aria-pressed={pressed ? 'true' : 'false'} {...rest} ref={n => { host = n; ref?.(n) }}>
21
+ {sweepParts()}
22
+ {pressed ? <CheckIcon data-part="check" /> : glyph.value && <Icon name={glyph.value} />}
23
+ {children}
24
+ {count !== undefined && <span data-part="count">{count}</span>}
25
+ </button>
26
+ )
27
+ }