@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,147 @@
1
+ import { RightIcon, XIcon } from '@weft-js/warp-icons'
2
+ import { navIndicator, readCollapsed, readFolds, sideNav, uid, writeCollapsed, writeFolds, type Folds } from '@weft-js/warp-core'
3
+ import { derived, onMount, state, untrack, watch } 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, without a bar
7
+ import '@weft-js/warp-core/styles/layout/navlist.css'
8
+ import '@weft-js/warp-core/styles/layout/sidenav.css'
9
+
10
+ /** One page in a SideNav section. */
11
+ export interface SideNavLink {
12
+ href: string
13
+ label: string
14
+ }
15
+
16
+ /** A titled run of pages. `pinned` keeps it open, with a plain heading instead of a fold. */
17
+ export interface SideNavSection {
18
+ title: string
19
+ items: SideNavLink[]
20
+ pinned?: boolean | undefined
21
+ }
22
+
23
+ /** Props for SideNav. */
24
+ export interface SideNavProps {
25
+ /** Names the navigation landmark, and titles the panel when it is laid over the page. */
26
+ label: string
27
+ sections: SideNavSection[]
28
+ /** The current page's href: its link is marked, its section starts open, and the panel scrolls to it. */
29
+ current?: string | undefined
30
+ /** A filter box at the top, for a long list. A string is its placeholder. */
31
+ filter?: boolean | string | undefined
32
+ /** The localStorage key for what a viewer folded, and `<key>:collapsed` for the docked panel's fold; `false` keeps
33
+ * nothing. Default `warp:sidenav:<label>`. */
34
+ remember?: string | false | undefined
35
+ /** A collapse bar at the panel's edge: on a phone it is all that shows until pressed, and it opens the panel over the
36
+ * page; docked, it folds the sidebar to its own width. Default true. `false` leaves the panel to an outside button. */
37
+ collapsible?: boolean | undefined
38
+ /** The panel's id. Any button with `aria-controls` naming it also opens the panel on a phone. */
39
+ id?: string | undefined
40
+ /** Links shown above the sections only while the panel is laid over the page, such as a site's top links. */
41
+ lead?: SideNavLink[] | undefined
42
+ }
43
+
44
+ /**
45
+ * A docs sidebar: one left panel whose sections fold, with a collapse bar at its edge. The section holding the current
46
+ * page starts open, the rest closed, and a viewer's own choices are remembered. From 900px the panel is docked: sticky,
47
+ * scrolling on its own, and its bar folds it to a slim strip and back, remembered too. Below that only the bar shows, a
48
+ * slim strip down the left edge; pressing it brings the panel over the page, full height, with focus kept inside, and
49
+ * the bar, Escape, the backdrop or a link closing it. Links are plain anchors, so a router that handles `<a>` clicks
50
+ * navigates as usual.
51
+ */
52
+ export function SideNav(props: SideNavProps) {
53
+ const id = untrack(() => props.id) ?? uid('sidenav')
54
+ const chosen = state<Folds>({})
55
+ const query = state('')
56
+ const open = state(false)
57
+ const collapsible = untrack(() => props.collapsible) !== false
58
+ const collapsed = state(false)
59
+ let root: HTMLElement | undefined
60
+ let list: HTMLElement | undefined
61
+
62
+ const key = () => (props.remember === false ? undefined : props.remember ?? `warp:sidenav:${props.label}`)
63
+ const holds = (s: SideNavSection) => s.items.some(i => i.href === props.current)
64
+ const words = derived(() => query.value.trim().toLowerCase())
65
+ const shown = derived(() => props.sections
66
+ .map((section, n) => ({ section, n, items: words.value ? section.items.filter(i => i.label.toLowerCase().includes(words.value)) : section.items }))
67
+ .filter(x => x.items.length > 0))
68
+ const unfolded = (s: SideNavSection) => words.value !== '' || !!s.pinned || (chosen.value[s.title] ?? holds(s))
69
+ const toggle = (s: SideNavSection) => {
70
+ chosen.value = { ...chosen.value, [s.title]: !unfolded(s) }
71
+ const k = key()
72
+ if (k) writeFolds(k, chosen.value)
73
+ }
74
+ // Arriving on a page opens its section, whatever was remembered, so the current link is never folded away.
75
+ const reveal = () => {
76
+ const s = props.sections.find(holds)
77
+ if (s && chosen.value[s.title] === false) chosen.value = { ...chosen.value, [s.title]: true }
78
+ }
79
+ const fold = (v: boolean) => {
80
+ collapsed.value = v
81
+ const k = key()
82
+ if (k) writeCollapsed(`${k}:collapsed`, v)
83
+ }
84
+ onMount(() => {
85
+ const k = key()
86
+ if (k) chosen.value = readFolds(k)
87
+ if (k && collapsible) collapsed.value = readCollapsed(`${k}:collapsed`)
88
+ reveal()
89
+ })
90
+ watch(() => props.current, () => { reveal(); open.value = false })
91
+ use(sideNav, () => root, () => ({ current: props.current, open: open.value, onOpenChange: (v: boolean) => (open.value = v), collapsed: collapsed.value, onCollapsedChange: fold }))
92
+ // warp's sliding bar, as Nav and the on-page rail have it: it follows the pointer and keyboard, and slides to a new page.
93
+ use(navIndicator, () => list, () => ({ axis: 'y' as const }))
94
+
95
+ const filterText = () => (typeof props.filter === 'string' ? props.filter : `Filter ${props.label.toLowerCase()}`)
96
+ return (
97
+ <div ref={n => (root = n)} data-scope="sidenav" data-open={open.value ? '' : undefined} data-collapsible={collapsible ? '' : undefined} data-collapsed={collapsible && collapsed.value ? '' : undefined}>
98
+ <div data-part="backdrop" aria-hidden="true"></div>
99
+ <div data-part="panel" id={id} tabindex="-1">
100
+ <div data-part="head">
101
+ <p data-part="title" id={`${id}-t`}>{props.label}</p>
102
+ {!collapsible && <button type="button" data-scope="button" data-emphasis="ghost" data-tone="neutral" data-icon-only="" data-size="sm" aria-label="Close" data-part="close"><XIcon /></button>}
103
+ </div>
104
+ <div data-part="scroll">
105
+ {props.lead && props.lead.length > 0 && (
106
+ <div data-part="lead" data-scope="navlist">
107
+ <ul>
108
+ {props.lead.map(i => <li key={i.href}><a data-part="item" href={i.href} aria-current={props.current?.startsWith(i.href) ? 'true' : undefined}>{i.label}</a></li>)}
109
+ </ul>
110
+ </div>
111
+ )}
112
+ {props.filter && (
113
+ <input type="search" data-part="filter" placeholder={filterText()} aria-label={filterText()} value={query.value} onInput={(e: Event) => (query.value = (e.currentTarget as HTMLInputElement).value)} />
114
+ )}
115
+ <nav ref={n => (list = n)} data-scope="navlist" data-bar="" aria-label={props.label}>
116
+ <ul data-part="sections">
117
+ {shown.value.map(x => (
118
+ <li key={x.section.title} data-current={holds(x.section) ? '' : undefined}>
119
+ {x.section.pinned
120
+ ? <p data-part="group-label">{x.section.title}</p>
121
+ : (
122
+ <button type="button" data-part="item" data-fold="" aria-expanded={unfolded(x.section) ? 'true' : 'false'} aria-controls={`${id}-${x.n}`} onClick={() => toggle(x.section)}>
123
+ <span data-part="fold-label">{x.section.title}</span>
124
+ <span data-part="chevron" aria-hidden="true"><RightIcon /></span>
125
+ </button>
126
+ )}
127
+ <ul id={`${id}-${x.n}`} hidden={!unfolded(x.section)}>
128
+ {x.items.map(i => <li key={i.href}><a data-part="item" href={i.href} aria-current={i.href === props.current ? 'page' : undefined}>{i.label}</a></li>)}
129
+ </ul>
130
+ </li>
131
+ ))}
132
+ </ul>
133
+ <span data-part="indicator" aria-hidden="true"></span>
134
+ </nav>
135
+ {words.value !== '' && shown.value.length === 0 && <p data-part="empty">No pages match “{query.value.trim()}”.</p>}
136
+ </div>
137
+ {/* Last, as Shell's toggle is at its nav's foot. Its name is the label; aria-expanded, kept by sideNav, says the rest. */}
138
+ {collapsible && (
139
+ <button type="button" data-part="bar" aria-controls={id} aria-expanded="false">
140
+ <RightIcon />
141
+ <span data-part="bar-label">{props.label}</span>
142
+ </button>
143
+ )}
144
+ </div>
145
+ </div>
146
+ )
147
+ }
@@ -0,0 +1,120 @@
1
+ import { UndoIcon, XIcon } from '@weft-js/warp-icons'
2
+ import { paintStrokes, signatureSvg, strokeWidth, uid, type Stroke } from '@weft-js/warp-core'
3
+ import { derived, flush, state, untrack } from '@weft-js/core'
4
+ import { Button } from './Button'
5
+ import { Field, type FieldShell } from './Field'
6
+ import { bindable, type Bindable } from './Picker'
7
+ import { use } from '@weft-js/warp-core/weft'
8
+ import '@weft-js/warp-core/styles/rich/signature.css'
9
+
10
+ /** A signature, drawn or typed, with the SVG to show it. */
11
+ export type SignatureValue = { method: 'drawn' | 'typed'; strokes?: Stroke[]; text?: string | undefined; svg: string }
12
+ /** Props for SignaturePad. */
13
+ export type SignaturePadProps = FieldShell & Bindable<SignatureValue | null>
14
+
15
+ /**
16
+ * SignaturePad (04 §4): a canvas redrawn from its stored points on resize and theme change, with Undo and Clear. Typing
17
+ * the name is the mandatory alternative (WCAG 2.1.1, 2.5.1), so keyboard and switch users can sign.
18
+ */
19
+ export function SignaturePad({ value, defaultValue, onValueChange, label, hint, ...rest }: SignaturePadProps) {
20
+ const id = uid('sig')
21
+ const v = bindable<SignatureValue | null>(() => value, () => defaultValue, null, () => onValueChange)
22
+ const start = untrack(() => v.value)
23
+ const strokes = state<Stroke[]>(start?.strokes ?? [])
24
+ const typed = state(start?.text ?? '')
25
+ const method = state<'draw' | 'type'>(start?.method === 'typed' ? 'type' : 'draw')
26
+ const count = derived(() => strokes.value.length)
27
+ const has = derived(() => (method.value === 'draw' ? count.value > 0 : typed.value.trim().length > 0))
28
+ const name = derived(() => (method.value === 'type' ? 'Typed signature' : count.value ? `Signature, ${count.value} stroke${count.value === 1 ? '' : 's'}` : 'Signature, empty'))
29
+ const status = derived(() => (!has.value ? 'Empty' : method.value === 'type' ? 'Typed' : `${count.value} stroke${count.value === 1 ? '' : 's'} · drawn`))
30
+ let pad: HTMLDivElement | undefined
31
+ let canvas: HTMLCanvasElement | undefined
32
+ let box: HTMLInputElement | undefined
33
+ let cur: Stroke | null = null
34
+ let last = { x: 0, y: 0, t: 0, w: 2.6 }
35
+ const draw = () => {
36
+ const ctx = canvas?.getContext?.('2d')
37
+ if (!ctx) return
38
+ const dpr = devicePixelRatio || 1, r = pad!.getBoundingClientRect()
39
+ if (canvas!.width !== Math.round(r.width * dpr) || canvas!.height !== Math.round(r.height * dpr)) { canvas!.width = Math.max(1, Math.round(r.width * dpr)); canvas!.height = Math.max(1, Math.round(r.height * dpr)) }
40
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
41
+ ctx.strokeStyle = ctx.fillStyle = getComputedStyle(pad!).color
42
+ paintStrokes(ctx, strokes.value, r.width, r.height)
43
+ }
44
+ const emit = () => {
45
+ if (!has.value) return v.set(null, 'programmatic')
46
+ if (method.value === 'draw') v.set({ method: 'drawn', strokes: strokes.value, svg: signatureSvg(strokes.value) }, 'pointer')
47
+ else { const text = typed.value.trim(); v.set({ method: 'typed', text, svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 200"><text x="24" y="130" font-size="64" font-style="italic" fill="currentColor">${text.replace(/[&<>]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' })[c]!)}</text></svg>` }, 'keyboard') }
48
+ }
49
+ const down = (e: PointerEvent) => {
50
+ if (method.value !== 'draw') return
51
+ canvas!.setPointerCapture?.(e.pointerId)
52
+ const r = canvas!.getBoundingClientRect()
53
+ cur = [[(e.clientX - r.left) / r.width, (e.clientY - r.top) / r.height, e.pointerType === 'pen' && e.pressure ? 1 + e.pressure * 3 : 2.6]]
54
+ strokes.value = [...strokes.value, cur]
55
+ last = { x: e.clientX, y: e.clientY, t: e.timeStamp, w: 2.6 }
56
+ draw()
57
+ }
58
+ const move = (e: PointerEvent) => {
59
+ if (!cur) return
60
+ const r = canvas!.getBoundingClientRect()
61
+ for (const ev of e.getCoalescedEvents?.().length ? e.getCoalescedEvents() : [e]) {
62
+ const speed = Math.hypot(ev.clientX - last.x, ev.clientY - last.y) / Math.max(1, ev.timeStamp - last.t)
63
+ const w = strokeWidth(last.w, speed, ev.pointerType === 'pen' ? ev.pressure : undefined)
64
+ cur.push([(ev.clientX - r.left) / r.width, (ev.clientY - r.top) / r.height, w])
65
+ last = { x: ev.clientX, y: ev.clientY, t: ev.timeStamp, w }
66
+ }
67
+ draw()
68
+ }
69
+ const up = () => { if (cur) { cur = null; strokes.value = [...strokes.value]; emit() } }
70
+ const undo = () => { strokes.value = strokes.value.slice(0, -1); flush(); draw(); emit() }
71
+ const clear = () => { if (method.value === 'draw') strokes.value = []; else typed.value = ''; flush(); draw(); emit() }
72
+ const pick = (m: 'draw' | 'type') => { method.value = m; flush(); if (m === 'type') box?.focus(); else draw(); emit() }
73
+ use((el: HTMLDivElement) => {
74
+ const size = typeof ResizeObserver === 'undefined' ? undefined : new ResizeObserver(draw)
75
+ size?.observe(el)
76
+ const theme = new MutationObserver(() => requestAnimationFrame(draw))
77
+ theme.observe(el.ownerDocument.documentElement, { attributes: true, attributeFilter: ['data-theme', 'data-expression'] })
78
+ return { update() {}, destroy() { size?.disconnect(); theme.disconnect() } }
79
+ }, () => pad)
80
+ return (
81
+ <div data-scope="signature" class="stack">
82
+ <div class="row" style="justify-content:flex-end">
83
+ <div class="seg" role="radiogroup" aria-label="Signing method">
84
+ <label><input type="radio" name={`${id}-m`} checked={method.value === 'draw'} onChange={() => pick('draw')} /><i>Draw</i></label>
85
+ <label><input type="radio" name={`${id}-m`} checked={method.value === 'type'} onChange={() => pick('type')} /><i>Type</i></label>
86
+ </div>
87
+ </div>
88
+ <Field
89
+ {...rest}
90
+ group
91
+ label={label}
92
+ hint={hint ?? 'Draw with a finger, a pen or a mouse, or switch to Type.'}
93
+ filled={has.value}
94
+ reset={() => {
95
+ v.reset()
96
+ strokes.value = start?.strokes ?? []
97
+ typed.value = start?.text ?? ''
98
+ method.value = start?.method === 'typed' ? 'type' : 'draw'
99
+ flush()
100
+ draw()
101
+ }}
102
+ control={() => (
103
+ <div ref={n => (pad = n)} data-part="pad">
104
+ <canvas ref={n => (canvas = n)} role="img" aria-label={name.value} style={method.value === 'type' ? 'visibility:hidden' : undefined} onPointerDown={down} onPointerMove={move} onPointerUp={up} onPointerCancel={up}></canvas>
105
+ <span data-part="empty"></span>
106
+ <span data-part="baseline" aria-hidden="true">×</span>
107
+ <input ref={n => (box = n)} data-part="typed" hidden={method.value !== 'type'} aria-label="Type your full name" placeholder="Type your full name" autocomplete="name" value={typed.value} onInput={e => { typed.value = (e.currentTarget).value; emit() }} />
108
+ </div>
109
+ )}
110
+ />
111
+ <div class="row" style="justify-content:space-between">
112
+ <span class="muted" aria-live="polite">{status.value}</span>
113
+ <span class="row">
114
+ {method.value === 'draw' && <Button emphasis="ghost" size="sm" icon={UndoIcon} disabled={!count.value} onClick={undo}>Undo</Button>}
115
+ <Button emphasis="ghost" size="sm" icon={XIcon} disabled={!has.value} onClick={clear}>Clear</Button>
116
+ </span>
117
+ </div>
118
+ </div>
119
+ )
120
+ }
@@ -0,0 +1,14 @@
1
+ import { untrack } from '@weft-js/core'
2
+ import './styles'
3
+ import '@weft-js/warp-core/styles/components/skeleton.css'
4
+
5
+ /**
6
+ * A skeleton (05 §5): the shimmer is the sweep, left to right; still under reduced motion. `lines` stacks text
7
+ * lines (the last one shorter).
8
+ */
9
+ export function Skeleton({ shape = 'text', lines = 1, width, height }: { shape?: 'text' | 'circle' | 'block' | undefined; lines?: number | undefined; width?: string | undefined; height?: string | undefined }) {
10
+ const size = (last: boolean) => [width ? `inline-size:${width}` : last && lines > 1 ? 'inline-size:60%' : '', height ? `block-size:${height}` : ''].filter(Boolean).join(';') || undefined
11
+ if (untrack(() => lines) <= 1) return <span data-scope="skeleton" data-shape={shape === 'text' ? undefined : shape} style={size(false)}></span>
12
+ return <>{Array.from({ length: lines }, (_, i) => <span data-scope="skeleton" data-shape={shape === 'text' ? undefined : shape} style={size(i === lines - 1)}></span>)}</>
13
+ }
14
+
package/src/Slider.tsx ADDED
@@ -0,0 +1,42 @@
1
+ import { slider as action, uid } from '@weft-js/warp-core'
2
+ import { derived } from '@weft-js/core'
3
+ import { use } from '@weft-js/warp-core/weft'
4
+ import './fields'
5
+ import '@weft-js/warp-core/styles/inputs/range.css'
6
+
7
+ /** Props for Slider. */
8
+ export interface SliderProps<V extends number | [number, number] = number | [number, number]> {
9
+ label: string
10
+ /** One number, or two for a range. */
11
+ value: V
12
+ onChange: (value: V) => void
13
+ min?: number | undefined
14
+ max?: number | undefined
15
+ step?: number | undefined
16
+ format?: ((v: number) => string) | undefined
17
+ /** A range's thumbs, for screen readers: "Minimum price", "Maximum price". */
18
+ names?: [string, string] | undefined
19
+ disabled?: boolean | undefined
20
+ /** Posts with a form: the number, or one entry per thumb. */
21
+ name?: string | undefined
22
+ }
23
+
24
+ /** A slider (03 §5): role=slider thumbs on warp's rail, keyboard per APG, a pointer anywhere on the track. */
25
+ export function Slider<V extends number | [number, number]>({ label, value, onChange, min = 0, max = 100, step = 1, format = String, names, disabled, name }: SliderProps<V>) {
26
+ const id = uid('slider')
27
+ const values = derived<number[]>(() => (typeof value === 'number' ? [value] : value))
28
+ let el: HTMLDivElement | undefined
29
+ use(action, () => el, () => ({ values: values.value, min, max, step, format, names, disabled, onChange: (v: number[]) => onChange((v.length === 1 ? v[0] : v) as V) }))
30
+ return (
31
+ <div style="display:grid;gap:6px">
32
+ <div data-scope="range-head"><span id={`${id}-l`}>{label}</span><output>{values.value.map(format).join(' – ')}</output></div>
33
+ <div ref={n => (el = n)} data-scope="slider" data-disabled={disabled ? '' : undefined}>
34
+ <div data-part="track" data-on=""><div data-part="range"></div></div>
35
+ {values.value.map((v, i) => (
36
+ <div key={i} data-part="thumb" role="slider" tabindex={disabled ? '-1' : '0'} aria-labelledby={`${id}-l`} aria-valuemin={min} aria-valuemax={max} aria-valuenow={v} aria-disabled={disabled ? 'true' : undefined}></div>
37
+ ))}
38
+ </div>
39
+ {name && values.value.map((v, i) => <input key={i} type="hidden" name={name} value={v} />)}
40
+ </div>
41
+ )
42
+ }
@@ -0,0 +1,7 @@
1
+ import './styles'
2
+ import '@weft-js/warp-core/styles/components/spinner.css'
3
+
4
+ /** A spinner: one arc, 0.8s; a pulse under reduced motion. `label` names it (role="status"); without one it is decorative. */
5
+ export function Spinner({ label, size }: { label?: string | undefined; size?: 'sm' | 'md' | 'lg' | undefined }) {
6
+ return <span data-scope="spinner" data-size={size === 'md' ? undefined : size} role={label ? 'status' : undefined} aria-label={label} aria-hidden={label ? undefined : 'true'}></span>
7
+ }