@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,102 @@
1
+ import { MicIcon } from '@weft-js/warp-icons'
2
+ import { announce, dictation, formatClock, listMics, speechWave, type DictationEngine, type DictationEvents, type DictationState } from '@weft-js/warp-core'
3
+ import { derived, onCleanup, onMount, state, untrack } from '@weft-js/core'
4
+ import { Alert } from '../Alert'
5
+ import { Button } from '../Button'
6
+ import { Select } from '../Select'
7
+ import { use } from '@weft-js/warp-core/weft'
8
+ import { Sweep, useSweep } from './bits'
9
+ import '../styles'
10
+ import '@weft-js/warp-core/styles/app/speech.css'
11
+
12
+ const BARS = Array.from({ length: 40 }, (_, i) => i)
13
+ const DEFAULT_MIC = [{ value: '', label: 'Default microphone' }]
14
+
15
+ /** Props for SpeechInput. */
16
+ export interface SpeechInputProps {
17
+ /** Writes the transcript somewhere: the Prompt's `control.insert`. Nothing is sent until then. */
18
+ onInsert?: (text: string) => void
19
+ mics?: { value: string; label: string }[]
20
+ /** Replaces the browser's speech recognition. */
21
+ engine?: (on: DictationEvents) => DictationEngine
22
+ /** The state and words to start from (a demo, a test). */
23
+ initial?: { state?: DictationState | undefined; final?: string; interim?: string; seconds?: number } | undefined
24
+ }
25
+
26
+ /**
27
+ * SpeechInput + MicSelector (07 §6.4): the mic Select and input level, then Record and Stop on one button. Listening
28
+ * holds the strip's sweep, moves the wave and counts; words arrive grey while they might change and ink once final. A
29
+ * blocked mic is a caution Alert with the fix, not a dead button. Uses the browser's speech recognition unless given an `engine`.
30
+ */
31
+ export function SpeechInput({ onInsert, mics, engine, initial }: SpeechInputProps) {
32
+ const start = untrack(() => initial)
33
+ const mode = state<DictationState>(start?.state ?? 'idle')
34
+ const final = state(start?.final ?? '')
35
+ const interim = state(start?.interim ?? '')
36
+ const seconds = state(start?.seconds ?? 0)
37
+ const level = state(0)
38
+ const found = state<{ value: string; label: string }[]>(DEFAULT_MIC)
39
+ const mic = state('')
40
+ const options = derived(() => mics ?? found.value)
41
+ const on = derived(() => mode.value === 'listening')
42
+ const words = derived(() => (final.value + ' ' + interim.value).trim())
43
+ let root: HTMLElement | undefined
44
+ let wave: HTMLElement | undefined
45
+ let live: DictationEngine | undefined
46
+ let clock: ReturnType<typeof setInterval> | undefined
47
+ useSweep()
48
+ use(speechWave, () => wave, () => level.value)
49
+ const say = (text: string, urgent = false) => announce(root!.ownerDocument, text, urgent)
50
+ const tick = (run: boolean) => {
51
+ clearInterval(clock)
52
+ if (run) { seconds.value = 0; clock = setInterval(() => seconds.value++, 1000) }
53
+ }
54
+ const events: DictationEvents = {
55
+ onState(s) {
56
+ const was = mode.value
57
+ mode.value = s
58
+ tick(s === 'listening')
59
+ if (s === 'listening') say('Listening.')
60
+ if (s === 'blocked') say('Microphone blocked. Allow it from the site settings, then try again.', true)
61
+ if (s === 'unsupported') say('This browser has no dictation.', true)
62
+ if (s === 'idle' && was === 'listening') { final.value = words.value; interim.value = ''; if (final.value) say('Dictation stopped. Transcript ready.') }
63
+ },
64
+ onTranscript(f, i) { final.value = f; interim.value = i },
65
+ onLevel(v) { level.value = v },
66
+ }
67
+ onMount(() => {
68
+ if (!untrack(() => mics)) void listMics(window).then(list => { if (list.length) found.value = list })
69
+ onCleanup(() => { clearInterval(clock); live?.stop() })
70
+ })
71
+ const toggle = () => {
72
+ if (on.value) return live?.stop()
73
+ live ??= (untrack(() => engine) ?? (e => dictation(window, e)))(events)
74
+ final.value = interim.value = ''
75
+ live.start(mic.value || undefined)
76
+ }
77
+ const clear = () => { if (on.value) live?.stop(); final.value = interim.value = '' }
78
+ const insert = () => { onInsert?.(words.value); clear() }
79
+ return (
80
+ <div ref={n => (root = n)} data-scope="speech" data-state={mode.value}>
81
+ <Select label="Microphone" options={options.value} onChange={(e: Event) => { const s = e.currentTarget as HTMLSelectElement; mic.value = s.value; say(`Microphone: ${s.selectedOptions[0]!.textContent}`) }} />
82
+ <div data-part="mic">
83
+ <span data-part="level" aria-hidden="true" style={`--level:${level.value}`}><i></i><i></i><i></i></span>
84
+ <span>{mode.value === 'blocked' ? 'Blocked by the browser' : 'Input level'}</span>
85
+ </div>
86
+ <div data-part="strip" data-sweep="" data-pin={on.value ? '' : undefined}>
87
+ <Sweep />
88
+ <Button data-icon-only="" tone={on.value ? 'critical' : undefined} aria-pressed={on.value ? 'true' : 'false'} label={on.value ? 'Stop dictation' : 'Start dictation'} onClick={toggle}><MicIcon /></Button>
89
+ <span ref={n => (wave = n)} data-part="wave" data-loop="" aria-hidden="true">{BARS.map(b => <i key={b}></i>)}</span>
90
+ <span data-part="time" role="timer" aria-label="Recording time" data-numeric="">{formatClock(seconds.value)}</span>
91
+ </div>
92
+ <div data-part="transcript" role="log" aria-live="polite" aria-label="Transcript">
93
+ {words.value ? <>{final.value} <span data-part="interim">{interim.value}</span></> : <small>Press the mic and speak. Nothing is sent until you insert it.</small>}
94
+ </div>
95
+ <div>
96
+ <Button size="sm" disabled={!words.value || on.value} onClick={insert}>Insert into message</Button> <Button emphasis="ghost" tone="neutral" size="sm" onClick={clear}>Clear</Button>
97
+ </div>
98
+ {mode.value === 'blocked' && <Alert tone="caution" title="The browser is blocking the microphone">Click the lock in the address bar, allow Microphone, then press the mic again. Typing still works.</Alert>}
99
+ {mode.value === 'unsupported' && <Alert tone="info" title="This browser has no dictation">Typing still works.</Alert>}
100
+ </div>
101
+ )
102
+ }
@@ -0,0 +1,22 @@
1
+ import { Sweep, useSweep } from './bits'
2
+ import '../styles'
3
+ import '@weft-js/warp-core/styles/app/suggestions.css'
4
+
5
+ /** A prompt offered as a starting point. */
6
+ export type Suggestion = { id?: string; title: string; description?: string | undefined }
7
+
8
+ /** Starter prompts (sweep cards) on an empty conversation, chips after a turn (07 §1.3). Picking one sends it. */
9
+ export function Suggestions({ items, variant, onPick, ...rest }: { items: Suggestion[]; variant?: 'cards' | 'chips' | undefined; onPick?: (s: Suggestion) => void; [attr: string]: unknown }) {
10
+ useSweep()
11
+ return (
12
+ <div data-scope="suggestions" data-variant={variant === 'chips' ? 'chips' : undefined} {...rest}>
13
+ {items.map(s => (
14
+ <button key={s.id ?? s.title} type="button" data-sweep="" onClick={() => onPick?.(s)}>
15
+ <Sweep />
16
+ <span>{s.title}</span>
17
+ {s.description !== undefined && <small>{s.description}</small>}
18
+ </button>
19
+ ))}
20
+ </div>
21
+ )
22
+ }
@@ -0,0 +1,91 @@
1
+ import { AlertIcon, CheckIcon, RightIcon, XIcon } from '@weft-js/warp-icons'
2
+ import { announce, argsDiff, installSweep, toolStatus, uid, type AgentTool } from '@weft-js/warp-core'
3
+ import { derived, onMount, state, untrack, watch } from '@weft-js/core'
4
+ import { Button } from '../Button'
5
+ import { Checkbox } from '../Choice'
6
+ import { Sweep } from './bits'
7
+ import '../styles'
8
+ import '@weft-js/warp-core/styles/components/spinner.css'
9
+ import '@weft-js/warp-core/styles/app/tool-call.css'
10
+
11
+ /** Props for ToolCall. */
12
+ export interface ToolCallProps {
13
+ call: AgentTool
14
+ /** Called with the arguments as edited, and whether "always allow" is ticked. Approval never auto-runs. */
15
+ onApprove?: (args: string, always: boolean) => void
16
+ /** Called when the call is denied. */
17
+ onDeny?: () => void
18
+ /** The "always allow" line, scoped to tool and resource: "Always allow edits in src/calendar". */
19
+ alwaysAllow?: string | undefined
20
+ /** Was there on load: takes no focus and announces nothing. */
21
+ quiet?: boolean | undefined
22
+ }
23
+
24
+ /**
25
+ * ToolCall (07 §1.2, §1.6): running carries a light on its bottom edge, and every other state is an icon plus words.
26
+ * Approval opens the card with editable arguments and a diff of the edit, takes focus and announces assertively. The
27
+ * summary reads with its state ("run_tests, failed, exit 1"); settling confirms one sweep.
28
+ */
29
+ export function ToolCall({ call, onApprove, onDeny, alwaysAllow, quiet }: ToolCallProps) {
30
+ const id = uid('tool')
31
+ const meta = derived(() => toolStatus(call))
32
+ const st = derived(() => call.state)
33
+ const args = state(untrack(() => call.args))
34
+ const always = state(false)
35
+ const diff = derived(() => argsDiff(call.args, args.value).join('\n'))
36
+ let frame: HTMLElement | undefined
37
+ let head: HTMLElement | undefined
38
+ let ok: HTMLElement | undefined
39
+ onMount(() => {
40
+ const asked = () => queueMicrotask(() => {
41
+ ok?.focus({ preventScroll: true })
42
+ if (frame) announce(frame.ownerDocument, `The assistant wants to run ${untrack(() => call.name)}. Approve or deny.`, true)
43
+ })
44
+ watch(() => st.value, (s, prev) => {
45
+ if (s === 'approval') { args.value = untrack(() => call.args); if (prev !== undefined || !untrack(() => quiet)) asked() }
46
+ if (prev !== undefined && s === 'done') {
47
+ installSweep().confirm(frame)
48
+ announce(frame!.ownerDocument, `${untrack(() => call.name)} finished.`)
49
+ }
50
+ })
51
+ })
52
+ const leave = (then: () => void) => { head?.focus({ preventScroll: true }); then() }
53
+ return (
54
+ <div ref={n => (frame = n)} data-part="tool-frame" data-sweep="">
55
+ <Sweep />
56
+ <details data-scope="tool-call" data-state={st.value} open={st.value === 'approval'}>
57
+ <summary ref={n => (head = n)}>
58
+ <span>
59
+ {st.value === 'running' && <span data-scope="spinner" style="--_size:13px;color:var(--warp-color-accent-text)"></span>}
60
+ {st.value === 'done' && <CheckIcon style="color:var(--warp-color-positive-text)" />}
61
+ {st.value === 'approval' && <AlertIcon style="color:var(--warp-color-caution-text)" />}
62
+ {(st.value === 'failed' || st.value === 'denied') && <XIcon style="color:var(--warp-color-critical-text)" />}
63
+ </span>
64
+ <span data-part="name">{call.name}</span>
65
+ <span data-part="sr-only">{meta.value.word}</span>
66
+ <span data-part="arg" aria-hidden="true">{call.args}</span>
67
+ <span data-part="dur" data-numeric="">{meta.value.status}</span>
68
+ <RightIcon data-part="chev" />
69
+ </summary>
70
+ <div data-part="io">
71
+ <span>Input</span>
72
+ <pre>{call.args}</pre>
73
+ <span>Output</span>
74
+ <pre>{call.output ?? (st.value === 'approval' ? 'Waiting for approval' : st.value === 'denied' ? 'Denied. Nothing was changed.' : '…')}</pre>
75
+ </div>
76
+ {st.value === 'approval' && (
77
+ <div data-part="approve" role="group" aria-label={`Approve ${call.name}`}>
78
+ <span data-part="eyebrow" id={`${id}-a`}>Arguments · editable</span>
79
+ <textarea aria-labelledby={`${id}-a`} spellcheck="false" value={args.value} onInput={e => (args.value = (e.currentTarget).value)}></textarea>
80
+ {diff.value && <section data-part="io"><span>Your edit</span><pre>{diff.value}</pre></section>}
81
+ <div>
82
+ <Button size="sm" ref={n => (ok = n)} onClick={() => leave(() => onApprove?.(args.value, always.value))}><CheckIcon />Approve</Button>
83
+ <Button emphasis="outline" tone="neutral" size="sm" onClick={() => leave(() => onDeny?.())}>Deny</Button>
84
+ {alwaysAllow !== undefined && <Checkbox checked={always.value} onChange={(e: Event & { currentTarget: HTMLInputElement }) => (always.value = e.currentTarget.checked)}>{alwaysAllow}</Checkbox>}
85
+ </div>
86
+ </div>
87
+ )}
88
+ </details>
89
+ </div>
90
+ )
91
+ }
@@ -0,0 +1,55 @@
1
+ import { formatSeconds, traceKey, traceRows, traceSummary, type TraceSpan } from '@weft-js/warp-core'
2
+ import { derived, state, untrack } from '@weft-js/core'
3
+ import '../styles'
4
+ import '@weft-js/warp-core/styles/app/tokens.css'
5
+ import '@weft-js/warp-core/styles/app/base.css'
6
+ import '@weft-js/warp-core/styles/app/trace.css'
7
+
8
+ /**
9
+ * TraceViewer (07 §1.3): nested spans on a shared time axis, an APG treegrid. One tab stop; ↑/↓, Home/End move, →/←
10
+ * expand, collapse and step in and out, Enter toggles. The line above follows the focused span's time, tokens and cost.
11
+ * Live: pass new `spans`, and rows patch by id.
12
+ */
13
+ export function TraceViewer({ spans, label = 'Spans' }: { spans: TraceSpan[]; label?: string | undefined }) {
14
+ const shut = state<ReadonlySet<string>>(new Set())
15
+ const rows = derived(() => traceRows(spans, shut.value))
16
+ const at = state<string | undefined>(undefined)
17
+ const stop = derived(() => rows.value.find(r => r.id === at.value) ?? rows.value[0])
18
+ let grid: HTMLElement | undefined
19
+ const toggle = (id: string) => {
20
+ const next = new Set(shut.value)
21
+ if (!next.delete(id)) next.add(id)
22
+ shut.value = next
23
+ }
24
+ const onKey = (e: KeyboardEvent) => {
25
+ const list = rows.value
26
+ const i = list.findIndex(r => r.id === stop.value?.id)
27
+ if (i < 0) return
28
+ const rtl = getComputedStyle(e.currentTarget as Element).direction === 'rtl'
29
+ const go = traceKey(e.key, list, i, rtl)
30
+ if (!go) return
31
+ e.preventDefault()
32
+ if (go.toggle) toggle(go.toggle)
33
+ if (go.focus) {
34
+ at.value = go.focus
35
+ queueMicrotask(() => grid?.querySelector<HTMLElement>(`[data-id="${untrack(() => at.value)}"]`)?.focus())
36
+ }
37
+ }
38
+ return (
39
+ <div style="display:grid;gap:14px">
40
+ <div style="display:flex;justify-content:space-between;gap:8px">
41
+ <span data-part="eyebrow">{label}</span>
42
+ <small data-numeric="">{stop.value ? traceSummary(stop.value) : ''}</small>
43
+ </div>
44
+ <div ref={n => (grid = n)} data-scope="trace" role="treegrid" aria-label={label} aria-readonly="true" onKeyDown={onKey}>
45
+ {rows.value.map(r => (
46
+ <div key={r.id} data-id={r.id} role="row" aria-level={r.level} aria-posinset={r.pos} aria-setsize={r.size} aria-expanded={r.branch ? (r.open ? 'true' : 'false') : undefined} aria-label={`${r.name}, ${traceSummary(r)}`} tabindex={r.id === stop.value?.id ? '0' : '-1'} onFocus={() => (at.value = r.id)} onClick={() => r.branch && toggle(r.id)}>
47
+ <span role="gridcell" style={`--_d:${r.level - 1}`}>{r.branch ? (r.open ? '▾ ' : '▸ ') : ''}{r.name}</span>
48
+ <i role="gridcell" style={`--_x:${r.x};--_w:${r.w};--_c:var(--warp-color-data-${r.color})`}></i>
49
+ <em role="gridcell" data-numeric="">{formatSeconds(r.ms)}</em>
50
+ </div>
51
+ ))}
52
+ </div>
53
+ </div>
54
+ )
55
+ }
@@ -0,0 +1,40 @@
1
+ // What the agent pack's components share: glyphs from the app specimen, the sweep's two parts, an icon button, a value
2
+ // held in a State or a plain prop, and the thin progress bar.
3
+ import { type IconComponent } from '@weft-js/warp-icons'
4
+ import { installSweep } from '@weft-js/warp-core'
5
+ import { Box, onMount, state, untrack, type State } from '@weft-js/core'
6
+ import { Button } from '../Button'
7
+ import '@weft-js/warp-core/styles/app/tokens.css'
8
+ import '@weft-js/warp-core/styles/app/base.css'
9
+ import '@weft-js/warp-core/styles/components/progress.css'
10
+
11
+ /** The two sweep parts a [data-sweep] host carries. */
12
+ export const Sweep = () => <><span data-part="sweep-base" aria-hidden="true"></span><span data-part="sweep" aria-hidden="true"></span></>
13
+
14
+ /** Follow the sweep machine; for hosts this pack draws by hand. */
15
+ export const useSweep = () => onMount(() => { installSweep() })
16
+
17
+ /** A small ghost icon button, named by `label`. */
18
+ export function IconButton({ icon, label, ...rest }: { icon: IconComponent; label: string; [attr: string]: unknown }) {
19
+ return <Button emphasis="ghost" tone="neutral" size="sm" label={label} data-icon-only="" {...rest}>{icon({})}</Button>
20
+ }
21
+
22
+ /** A thin determinate bar; `value` is 0 to 1. */
23
+ export function Bar({ label, value }: { label: string; value: number }) {
24
+ return <div data-scope="progress" data-part="root" role="progressbar" aria-label={label} aria-valuemin="0" aria-valuemax="100" aria-valuenow={Math.round(value * 100)} style={`--_v:${value}`}><div data-part="track"><div data-part="range"></div></div></div>
25
+ }
26
+
27
+ /** One read and write over a State to bind or a plain value with its change callback. */
28
+ export function held<T>(value: () => State<T> | T | undefined, fallback: T, onChange?: () => ((v: T) => void) | undefined) {
29
+ const box = (v: unknown): v is State<T> => v instanceof Box
30
+ const own = state<T>(untrack(() => { const v = value(); return box(v) || v === undefined ? fallback : v }))
31
+ return {
32
+ get value(): T { const v = value(); return box(v) ? v.value : v !== undefined ? v : own.value },
33
+ set(next: T) {
34
+ const v = untrack(value)
35
+ if (box(v)) v.value = next
36
+ else own.value = next
37
+ untrack(onChange ?? (() => undefined))?.(next)
38
+ },
39
+ }
40
+ }
@@ -0,0 +1,22 @@
1
+ // The agent pack: see docs/design/warp. One file per component family; each imports only its own CSS.
2
+ export { Conversation } from './Conversation'
3
+ export { Message as UserMessage } from './Message'
4
+ export { Assistant, type AssistantProps } from './Assistant'
5
+ export { Reasoning } from './Reasoning'
6
+ export { ToolCall, type ToolCallProps } from './ToolCall'
7
+ export { Cite } from './Cite'
8
+ export { Sources } from './Sources'
9
+ export { Prose } from './Prose'
10
+ export { AgentPlan, type QueuedMessage } from './AgentPlan'
11
+ export { Prompt, type PromptProps, type PromptTool, type PromptFile, type PromptSuggestion, type PromptControl } from './Prompt'
12
+ export { ModelPicker, type ModelOption } from './ModelPicker'
13
+ export { MessageActions, BranchSwitcher, type MessageActionsProps, type Feedback } from './Actions'
14
+ export { Suggestions, type Suggestion } from './Suggestions'
15
+ export { Question } from './Question'
16
+ export { Checkpoint } from './Checkpoint'
17
+ export { Shimmer } from './Shimmer'
18
+ export { ContextMeter, type ContextUse } from './ContextMeter'
19
+ export { TraceViewer } from './TraceViewer'
20
+ export { ArtifactPanel, type ArtifactVersion } from './ArtifactPanel'
21
+ export { SpeechInput, type SpeechInputProps } from './SpeechInput'
22
+ export { foldAnswer, type Answer, type AgentEvent, type AgentTool, type AgentSource, type AgentQuestion, type PlanStep, type TraceSpan } from '@weft-js/warp-core'
package/src/bind.ts ADDED
@@ -0,0 +1,42 @@
1
+ import type { ChangeReason } from '@weft-js/warp-core'
2
+ import { Box, state, untrack, type State } from '@weft-js/core'
3
+
4
+ /** 04 §1.1: a State to bind, or a plain value (controlled) or defaultValue; `onValueChange` hears every change. */
5
+ export interface Bindable<T> {
6
+ value?: State<T> | T
7
+ defaultValue?: T
8
+ /** Called with each new value, and whether a pointer, a key or code made the change. */
9
+ onValueChange?: (v: T, reason: ChangeReason) => void
10
+ }
11
+ /** An overlay's open state, held by the caller or by the component. */
12
+ export interface Openable {
13
+ open?: State<boolean> | boolean
14
+ /** Starts open, when the component keeps its own open state. */
15
+ defaultOpen?: boolean
16
+ /** Called with `true` when it opens and `false` when it closes. */
17
+ onOpenChange?: (o: boolean) => void
18
+ }
19
+
20
+ const isState = (v: unknown): v is State<unknown> => v instanceof Box
21
+
22
+ /** One read/write over the three ways a value can be held. */
23
+ export function bindable<T>(value: () => State<T> | T | undefined, initial: () => T | undefined, fallback: T, onChange?: () => ((v: T, r: ChangeReason) => void) | undefined) {
24
+ const own = state<T>(untrack(() => { const v = value(); return (isState(v) ? undefined : (v)) ?? initial() ?? fallback }))
25
+ const read = (): T => {
26
+ const v = value()
27
+ return isState(v) ? v.value : v !== undefined ? (v) : own.value
28
+ }
29
+ const first = untrack(read)
30
+ const set = (next: T, reason: ChangeReason = 'programmatic') => {
31
+ const v = untrack(value)
32
+ if (isState(v)) v.value = next
33
+ else own.value = next
34
+ untrack(onChange ?? (() => undefined))?.(next, reason)
35
+ }
36
+ return {
37
+ get value(): T { return read() },
38
+ set,
39
+ /** Back to the value it started with: a form's reset. */
40
+ reset: () => { set(first) },
41
+ }
42
+ }
@@ -0,0 +1,42 @@
1
+ import { announce, autoTrack, firstName, type Peer } from '@weft-js/warp-core'
2
+ import { derived, state } from '@weft-js/core'
3
+ import { Button } from '../Button'
4
+ import { Switch } from '../Choice'
5
+ import { use } from '@weft-js/warp-core/weft'
6
+ import { PeerAvatar } from './peer'
7
+ import '../styles'
8
+ import '@weft-js/warp-core/styles/app/tokens.css'
9
+ import '@weft-js/warp-core/styles/app/auto-track.css'
10
+
11
+ type Track = 'following' | 'paused' | 'off'
12
+
13
+ /**
14
+ * MultiplayerAutoTrack (07 §6.4): a Switch, a status line, and a scroll region that follows the presenter's section
15
+ * (`[data-section]` children). A manual scroll pauses it with Resume; presenter changes are shown, not announced.
16
+ */
17
+ export function AutoTrack({ presenter, section, label, height = 180, children }: { presenter: Peer | null; section?: string | undefined; label: string; height?: number; children?: unknown }) {
18
+ const mode = state<Track>('following')
19
+ const name = derived(() => (presenter ? firstName(presenter.name) : ''))
20
+ let region: HTMLDivElement | undefined
21
+ use(autoTrack, () => region, () => ({
22
+ following: mode.value === 'following',
23
+ section,
24
+ onPause() {
25
+ mode.value = 'paused'
26
+ announce(region!.ownerDocument, 'Auto-track paused. Resume to follow the presenter again.')
27
+ },
28
+ }))
29
+ return (
30
+ <div>
31
+ <Switch checked={mode.value !== 'off'} onChange={(e: Event) => (mode.value = (e.currentTarget as HTMLInputElement).checked ? 'following' : 'off')}>Follow whoever presents</Switch>
32
+ <div>
33
+ <span data-scope="auto-track" data-state={mode.value} role="status">
34
+ {mode.value === 'following' && presenter && <PeerAvatar peer={presenter} size="sm" />}
35
+ {mode.value === 'following' ? (presenter ? `Following ${name.value} · presenting` : 'Nobody is presenting') : mode.value === 'paused' ? 'Paused: you scrolled' : 'Off'}
36
+ </span>
37
+ {mode.value === 'paused' && <Button emphasis="outline" tone="neutral" size="sm" onClick={() => { mode.value = 'following'; announce(region!.ownerDocument, `Following ${presenter?.name ?? 'the presenter'}`) }}>Resume</Button>}
38
+ </div>
39
+ <div ref={n => (region = n)} tabindex="0" role="region" aria-label={label} style={`block-size:${height}px;overflow:auto`}>{children}</div>
40
+ </div>
41
+ )
42
+ }
@@ -0,0 +1,94 @@
1
+ import { CheckIcon } from '@weft-js/warp-icons'
2
+ import { announce, arrivalTracker, isSheet, uid, type Peer } from '@weft-js/warp-core'
3
+ import { derived, onMount, state, untrack, watch, type State } from '@weft-js/core'
4
+ import { Button } from '../Button'
5
+ import { Drawer } from '../Drawer'
6
+ import { PeerAvatar, read, SWEEP, type Live } from './peer'
7
+ import '../styles'
8
+ import '@weft-js/warp-core/styles/app/tokens.css'
9
+ import '@weft-js/warp-core/styles/app/base.css'
10
+ import '@weft-js/warp-core/styles/app/comment-thread.css'
11
+ import '@weft-js/warp-core/styles/app/comment-anchor.css'
12
+
13
+ /** One comment in a thread. */
14
+ export interface Comment { id: string; author: Peer; time: string; text: string }
15
+ /** A comment thread and whether it's resolved. */
16
+ export interface CommentThreadData { id: string; comments: Comment[]; resolved?: boolean }
17
+
18
+ /**
19
+ * A comment thread (07 §6.2): an article of comments, a reply field and Resolve; resolved collapses to one line with
20
+ * Reopen. A new comment arrives with `data-arrive`; `active` raises it and holds its sweep.
21
+ */
22
+ export function CommentThread({ thread, active, onReply, onResolve }: { thread: Live<CommentThreadData>; active?: boolean; onReply?: (text: string) => void; onResolve?: (resolved: boolean) => void }) {
23
+ const id = uid('reply')
24
+ const data = derived(() => read(thread))
25
+ const own = state(untrack(() => !!data.value.resolved))
26
+ const resolved = derived(() => own.value)
27
+ const mark = arrivalTracker()
28
+ const fresh = derived(() => mark(data.value.comments.map(c => c.id)))
29
+ const first = derived(() => data.value.comments[0])
30
+ let root: HTMLElement | undefined
31
+ const flip = (on: boolean) => {
32
+ own.value = on
33
+ onResolve?.(on)
34
+ if (root) announce(root.ownerDocument, on ? 'Comment resolved' : 'Comment reopened')
35
+ }
36
+ const reply = (e: KeyboardEvent) => {
37
+ const el = e.currentTarget as HTMLInputElement
38
+ if (e.key !== 'Enter' || !el.value.trim()) return
39
+ e.preventDefault()
40
+ onReply?.(el.value.trim())
41
+ el.value = ''
42
+ }
43
+ return (
44
+ <article ref={n => (root = n)} data-scope="comment-thread" data-c={data.value.id} data-sweep="surface" data-active={active ? '' : undefined} data-pin={active ? '' : undefined} data-resolved={resolved.value ? '' : undefined} aria-label={`Comment from ${first.value?.author.name}`}>
45
+ <SWEEP />
46
+ {resolved.value
47
+ ? <div data-part="reply"><span>{first.value?.author.name}: {first.value?.text}</span><Button emphasis="ghost" tone="neutral" size="sm" onClick={() => flip(false)}>Reopen</Button></div>
48
+ : <>
49
+ {data.value.comments.map(c => (
50
+ <div key={c.id} data-part="comment" data-arrive={fresh.value.includes(c.id) ? '' : undefined}>
51
+ <PeerAvatar peer={c.author} size="sm" /><b>{c.author.name}</b><time>{c.time}</time><p>{c.text}</p>
52
+ </div>
53
+ ))}
54
+ <div data-part="reply">
55
+ <label data-part="sr-only" for={id}>Reply to {first.value?.author.name}</label>
56
+ <input id={id} placeholder="Reply…" onKeyDown={reply} />
57
+ <Button emphasis="ghost" tone="neutral" size="sm" onClick={() => flip(true)}><CheckIcon /> Resolve</Button>
58
+ </div>
59
+ </>}
60
+ </article>
61
+ )
62
+ }
63
+
64
+ /** The highlighted range a thread hangs from (07 §6.2): activates its thread on press and on focus. */
65
+ export function CommentAnchor({ thread, active, children }: { thread: string; active: State<string | null>; children?: unknown }) {
66
+ const on = () => (active.value = thread)
67
+ return <span data-scope="comment-anchor" data-c={thread} data-active={active.value === thread ? '' : undefined} role="button" tabindex="0" aria-pressed={active.value === thread ? 'true' : 'false'} onClick={on} onFocus={on} onKeyDown={(e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); on() } }}>{children}</span>
68
+ }
69
+
70
+ /**
71
+ * The margin rail of anchored threads (07 §6.2): a complementary landmark of articles. On a phone the rail gives way
72
+ * to a bottom Sheet holding the active thread.
73
+ */
74
+ export function CommentRail({ threads, active, onReply, onResolve, label = 'Comments' }: { threads: Live<CommentThreadData[]>; active: State<string | null>; onReply?: (thread: string, text: string) => void; onResolve?: (thread: string, resolved: boolean) => void; label?: string | undefined }) {
75
+ const list = derived(() => read(threads))
76
+ const phone = state(false)
77
+ let rail: HTMLElement | undefined
78
+ const sheet = state(false)
79
+ onMount(() => {
80
+ queueMicrotask(() => { if (rail && isSheet(rail)) phone.value = true })
81
+ watch(() => active.value, a => { if (phone.value) sheet.value = a !== null })
82
+ watch(() => sheet.value, on => { if (!on) active.value = null })
83
+ })
84
+ const open = derived(() => list.value.find(t => t.id === active.value))
85
+ const body = (t: CommentThreadData) => <CommentThread thread={t} active={active.value === t.id} onReply={x => onReply?.(t.id, x)} onResolve={r => onResolve?.(t.id, r)} />
86
+ return (
87
+ <>
88
+ <aside ref={n => (rail = n)} aria-label={label} hidden={phone.value}>
89
+ {!phone.value && list.value.map(t => <div key={t.id}>{body(t)}</div>)}
90
+ </aside>
91
+ {phone.value && <Drawer open={sheet} title={label} side="bottom">{open.value ? body(open.value) : null}</Drawer>}
92
+ </>
93
+ )
94
+ }