@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,108 @@
1
+ import { announce, coachSpot, tourSpot, uid } from '@weft-js/warp-core'
2
+ import { derived, state, untrack, watch, type State } from '@weft-js/core'
3
+ import { Button } from '../Button'
4
+ import { bindable } from '../Picker'
5
+ import { use } from '@weft-js/warp-core/weft'
6
+ import '../styles'
7
+ import '@weft-js/warp-core/styles/flows/tokens.css'
8
+ import '@weft-js/warp-core/styles/flows/base.css'
9
+ import '@weft-js/warp-core/styles/components/tour.css'
10
+ import '@weft-js/warp-core/styles/flows/tour.css'
11
+
12
+ /** One stop of a tour. */
13
+ export interface TourStep {
14
+ /** A CSS selector: `[data-tour=new]`. */
15
+ target: string
16
+ title: unknown
17
+ body: unknown
18
+ /** The target stays pressable ("Try it: click New"). */
19
+ interactive?: boolean | undefined
20
+ }
21
+
22
+ /** Props for Tour. */
23
+ export interface TourProps {
24
+ id?: string
25
+ steps: TourStep[]
26
+ open?: State<boolean> | boolean
27
+ /** Ran to the end. */
28
+ onDone?: () => void
29
+ /** Skip or Escape. */
30
+ onSkip?: () => void
31
+ onStepChange?: ((index: number) => void) | undefined
32
+ }
33
+
34
+ function Running({ steps, onEnd, onStepChange }: { steps: TourStep[]; onEnd: (done: boolean) => void; onStepChange?: ((i: number) => void) | undefined }) {
35
+ const id = uid('tour')
36
+ const list = untrack(() => steps)
37
+ const at = state(0)
38
+ const way = state(1)
39
+ const step = derived(() => list[at.value])
40
+ const many = list.length > 1
41
+ const go = (i: number, dir = 1) => {
42
+ way.value = dir
43
+ if (i < 0) return
44
+ if (i >= list.length) return onEnd(true)
45
+ at.value = i
46
+ onStepChange?.(i)
47
+ }
48
+ watch(() => at.value, i => { if (typeof document === 'object') announce(document, `${many ? `Step ${i + 1} of ${list.length}. ` : ''}${typeof list[i]!.title === 'string' ? list[i]!.title : ''}`) })
49
+ let root: HTMLDivElement | undefined
50
+ use(tourSpot, () => root, () => ({
51
+ target: step.value!.target, step: at.value, interactive: step.value!.interactive,
52
+ onNext: () => go(at.value + 1), onPrev: () => go(at.value - 1, -1), onClose: () => onEnd(false),
53
+ onMissing: () => (way.value < 0 && at.value === 0 ? onEnd(false) : go(at.value + way.value, way.value)),
54
+ onBlocked: () => onEnd(false),
55
+ }))
56
+ return (
57
+ <div ref={n => (root = n)} data-scope="tour">
58
+ <div data-part="spotlight" data-sweep="" data-hold=""><span data-part="sweep-base" aria-hidden="true"></span><span data-part="sweep" aria-hidden="true"></span></div>
59
+ <div data-part="card" role="dialog" aria-modal="false" aria-labelledby={`${id}-t`} aria-describedby={`${id}-d`}>
60
+ <span data-part="step-count">{many ? `Step ${at.value + 1} of ${list.length}` : 'Tip'}</span>
61
+ <h4 id={`${id}-t`} tabindex="-1" aria-level="2">{step.value!.title}</h4>
62
+ <p id={`${id}-d`}>{step.value!.body}</p>
63
+ <footer>
64
+ <span data-part="pips" aria-hidden="true">{many && list.map((s, i) => <i key={s.target} data-on={i === at.value ? '' : undefined}></i>)}</span>
65
+ <Button emphasis="ghost" tone="neutral" size="sm" onClick={() => onEnd(false)}>Skip tour</Button>
66
+ {at.value > 0 && <Button emphasis="ghost" tone="neutral" size="sm" onClick={() => go(at.value - 1, -1)}>Back</Button>}
67
+ <Button size="sm" onClick={() => go(at.value + 1)}>{at.value === list.length - 1 ? 'Done' : 'Next'}</Button>
68
+ </footer>
69
+ </div>
70
+ </div>
71
+ )
72
+ }
73
+
74
+ /**
75
+ * The product tour (09 §3.2): a spotlight cut by the scrim, a ring that draws on arrival, a card beside the target
76
+ * (docked on a phone). A missing target skips its step; it starts only on request and never over another overlay; Esc
77
+ * ends it and focus returns to where it began.
78
+ */
79
+ export function Tour({ steps, open, onDone, onSkip, onStepChange }: TourProps) {
80
+ const on = bindable<boolean>(() => open, () => false, false)
81
+ const end = (done: boolean) => {
82
+ on.set(false)
83
+ ;(done ? onDone : onSkip)?.()
84
+ }
85
+ return <>{on.value && <Running steps={steps} onEnd={end} onStepChange={onStepChange} />}</>
86
+ }
87
+
88
+ /** A coachmark (09 §3.2): one tip with no scrim: a beacon on the target, the card on a press. Persisting the dismissal is the app's (`onDismiss`). */
89
+ export function Coachmark({ target, title, body, onDismiss }: { target: string; title: unknown; body: unknown; onDismiss?: () => void }) {
90
+ const id = uid('coach')
91
+ const open = state(false)
92
+ const gone = state(false)
93
+ let root: HTMLDivElement | undefined
94
+ use(coachSpot, () => root, () => ({ target, open: open.value, onClose: () => (open.value = false) }))
95
+ return (
96
+ <div ref={n => (root = n)} data-scope="tour" hidden={gone.value || undefined}>
97
+ <button type="button" data-scope="button" data-emphasis="ghost" data-tone="neutral" data-size="sm" data-icon-only="" data-beacon="" aria-label="Show tip" aria-expanded={open.value ? 'true' : 'false'} onClick={() => (open.value = !open.value)}><span data-part="beacon" aria-hidden="true"></span></button>
98
+ {open.value && (
99
+ <div data-part="card" role="dialog" aria-modal="false" aria-labelledby={`${id}-t`}>
100
+ <span data-part="step-count">Tip</span>
101
+ <h4 id={`${id}-t`} tabindex="-1" aria-level="2">{title}</h4>
102
+ <p>{body}</p>
103
+ <footer><span data-part="pips"></span><Button size="sm" onClick={() => { open.value = false; gone.value = true; onDismiss?.() }}>Got it</Button></footer>
104
+ </div>
105
+ )}
106
+ </div>
107
+ )
108
+ }
@@ -0,0 +1,17 @@
1
+ // The flow pack: see docs/design/warp. One file per component family; each imports only its own CSS.
2
+ export { Flow, type FlowProps, type FlowStep } from './Flow'
3
+ export { AuthScreen } from './AuthScreen'
4
+ export { SignIn, type SignInProps, type SignInValues } from './SignIn'
5
+ export { SignUp, PasswordMeter, type SignUpProps, type SignUpValues } from './SignUp'
6
+ export { OtpField, OtpStep, type OtpFieldProps, type OtpStepProps, type OtpValues } from './Otp'
7
+ export { MagicLinkSent, ResetPassword, InviteAccept, AuthDone, type ResetPasswordProps, type ResetValues } from './AuthSteps'
8
+ export { ReauthDialog, type ReauthProps } from './ReauthDialog'
9
+ export { PasskeyButton, SsoButtons, type SsoProvider } from './Sso'
10
+ export { MfaSetup, type MfaSetupProps } from './MfaSetup'
11
+ export { SignGate, SignGateBadge, type SignGateProps, type SignStatus } from './SignGate'
12
+ export { Checklist, type ChecklistProps, type ChecklistItem } from './Checklist'
13
+ export { Tour, Coachmark, type TourProps, type TourStep } from './Tour'
14
+ export { SystemPage, type SystemPageProps, type AccessRequest, type SwitchAccount } from './SystemPage'
15
+ export { ConnBanner, RateLimited, type ConnBannerProps, type RateLimitedProps } from './ConnBanner'
16
+ export { ShortcutsOverlay } from './ShortcutsOverlay'
17
+ export { useCooldown, type FlowForm, type FormLike } from './kit'
@@ -0,0 +1,170 @@
1
+ // What the flow screens share (09 §1): the title, mark and rail parts, the step's form frame and its error surfaces.
2
+ // Internal, except what the barrel names.
3
+ import { announce, mapFlowError, secondsClock, type FlowError, type FlowErrorOptions } from '@weft-js/warp-core'
4
+ import { derived, flush, onCleanup, onMount, state, untrack, watch } from '@weft-js/core'
5
+ import { Alert } from '../Alert'
6
+ import { Button, type ButtonProps } from '../Button'
7
+ import { Banner } from '../parts'
8
+ import { toast } from '../Toast'
9
+ import '../styles'
10
+ import '@weft-js/warp-core/styles/flows/tokens.css'
11
+ import '@weft-js/warp-core/styles/flows/base.css'
12
+ import '@weft-js/warp-core/styles/flows/meter.css' // the result mark
13
+ import '@weft-js/warp-core/styles/layout/stack.css'
14
+
15
+ export type Heading = 'h1' | 'h2' | 'h3'
16
+
17
+ /** A step's title: focus lands here on arrival (09 §1.1). */
18
+ export function Title({ as, id, children }: { as?: Heading | undefined; id?: string; children?: unknown }) {
19
+ const level = derived(() => as ?? 'h1')
20
+ return (
21
+ <>
22
+ {level.value === 'h3' ? <h3 data-part="title" data-part-title="" tabindex="-1" id={id}>{children}</h3>
23
+ : level.value === 'h2' ? <h2 data-part="title" data-part-title="" tabindex="-1" id={id}>{children}</h2>
24
+ : <h1 data-part="title" data-part-title="" tabindex="-1" id={id}>{children}</h1>}
25
+ </>
26
+ )
27
+ }
28
+
29
+ /** The result mark; `draw` strokes a check in. */
30
+ export function Mark({ tone, draw, children }: { tone?: 'positive' | 'caution' | 'critical' | undefined; draw?: boolean; children?: unknown }) {
31
+ return <span data-part="mark" data-tone={tone}>{draw ? <svg viewBox="0 0 24 24" aria-hidden="true"><path pathLength="1" data-draw="" d="m5 12.5 4.5 4.5L19 7.5" /></svg> : children}</span>
32
+ }
33
+
34
+ /** The flows rail: `value` in percent. */
35
+ export function Rail({ value, on = true, ...rest }: { value: number; on?: boolean; [attr: string]: unknown }) {
36
+ return <span data-part="rail" data-on={on ? '' : undefined} style={`--v:${value}%`} {...rest}><span data-part="rail-fill"></span></span>
37
+ }
38
+
39
+ /**
40
+ * Seconds counting down to zero while `seconds()` holds a number; a new number restarts it. Nothing runs on the server
41
+ * or before mount, so the first paint is the full count.
42
+ */
43
+ export function useCooldown(seconds: () => number | undefined, onDone?: () => void) {
44
+ const left = state(untrack(seconds) ?? 0)
45
+ let stop: (() => void) | undefined
46
+ const start = (n: number | undefined) => {
47
+ stop?.()
48
+ stop = undefined
49
+ left.value = n ?? 0
50
+ if (n) stop = secondsClock(n, s => (left.value = s), onDone)
51
+ }
52
+ onMount(() => {
53
+ // a fresh object each run: a restart with the same seconds still restarts
54
+ watch(() => ({ n: seconds() }), v => start(v.n))
55
+ onCleanup(() => stop?.())
56
+ })
57
+ return left
58
+ }
59
+
60
+ /** What `<Form of={mutation}>` gives its children; a step reads it, so warp never imports the server's Form. */
61
+ export interface FormLike {
62
+ readonly pending: { readonly value: boolean }
63
+ readonly errors: { readonly value: Record<string, string> }
64
+ readonly message: { readonly value: string | undefined }
65
+ value?(name: string): unknown
66
+ }
67
+
68
+ /** How a screen is driven: inside `<Form of>` (pass `form`), or on its own with `onSubmit` and `errors`. */
69
+ export interface FlowForm<V> {
70
+ /** The `<Form of={mutation}>` render argument: the screen then renders no <form> of its own. */
71
+ form?: FormLike
72
+ /** Called with the form's values once its own checks pass. */
73
+ onSubmit?: (values: V, e: SubmitEvent) => void
74
+ /** Messages by field name. */
75
+ errors?: Record<string, string> | undefined
76
+ /** The failure: anything `mapFlowError` takes (a `FnError`, a typed error, a string). */
77
+ error?: unknown
78
+ /** Options for `mapFlowError`: copy by code, and `online`. */
79
+ errorOptions?: FlowErrorOptions | undefined
80
+ pending?: boolean | undefined
81
+ /** The Retry on a network failure's toast. */
82
+ onRetry?: (() => void) | undefined
83
+ /** The title's level (default h1: an auth step is the page). */
84
+ heading?: Heading
85
+ }
86
+
87
+ /** One step's form state: errors from all three sources, the mapped failure and its cooldown, its surfaces' side effects. */
88
+ export function useStepForm<V>(p: FlowForm<V>, check?: (values: Record<string, string>) => Record<string, string | undefined>) {
89
+ const own = state<Record<string, string>>({})
90
+ const failure = derived(() => {
91
+ const e = p.form ? p.form.message.value : p.error
92
+ return e ? mapFlowError(e, untrack(() => p.errorOptions)) : undefined
93
+ })
94
+ const errors = derived<Record<string, string>>(() => ({ ...own.value, ...failure.value?.fields, ...(p.form ? p.form.errors.value : p.errors) }))
95
+ const pending = derived(() => !!(p.form ? p.form.pending.value : p.pending))
96
+ const cooling = useCooldown(() => failure.value?.retryAfter, () => announce(document, 'You can try again now.'))
97
+ let root: HTMLElement | undefined
98
+ const focusInvalid = (say?: string) => {
99
+ flush()
100
+ root?.querySelector<HTMLElement>('[aria-invalid="true"]')?.focus()
101
+ if (say) announce(document, say)
102
+ }
103
+ onMount(() => {
104
+ watch(() => failure.value, (m: FlowError | undefined) => {
105
+ if (!m) return
106
+ if (m.surface === 'field') focusInvalid(m.summary)
107
+ if (m.surface === 'alert') { flush(); root?.querySelector<HTMLElement>('[data-part="formerr"]')?.focus() }
108
+ if (m.surface === 'rate-limit') announce(document, `${m.title} Try again in ${m.retryAfter}s.`)
109
+ if (m.surface === 'toast') toast.show({ title: m.title, description: m.description, tone: 'critical', action: p.onRetry && { label: 'Retry', onClick: () => p.onRetry!() } })
110
+ })
111
+ let sent = 0
112
+ watch(() => Object.keys(p.form ? p.form.errors.value : (p.errors ?? {})).length, n => {
113
+ if (n && n !== sent) focusInvalid()
114
+ sent = n
115
+ })
116
+ })
117
+ const submit = (e: SubmitEvent) => {
118
+ e.preventDefault()
119
+ const f = e.currentTarget as HTMLFormElement
120
+ const values = Object.fromEntries(new FormData(f)) as Record<string, string>
121
+ const bad = Object.fromEntries(Object.entries(check?.(values) ?? {}).filter(([, m]) => m)) as Record<string, string>
122
+ own.value = bad
123
+ const n = Object.keys(bad).length
124
+ if (n) return focusInvalid(n > 1 ? `${n} fields need attention` : undefined)
125
+ p.onSubmit?.(values as V, e)
126
+ }
127
+ return {
128
+ errors, failure, pending, cooling, submit,
129
+ /** Set one field's message from the client (format on blur), or clear it. */
130
+ flag(name: string, message: string | undefined) {
131
+ const { [name]: _, ...rest } = own.value
132
+ own.value = message ? { ...rest, [name]: message } : rest
133
+ },
134
+ bind(n: HTMLElement) { root = n },
135
+ }
136
+ }
137
+ export type StepForm = ReturnType<typeof useStepForm>
138
+
139
+ /** The step's frame: a <form> of its own, or a plain stack inside `<Form of>`. */
140
+ export function StepFrame({ ctl, embedded, labelledby, children }: { ctl: StepForm; embedded: boolean; labelledby?: string; children?: unknown }) {
141
+ const plain = derived(() => embedded)
142
+ return (
143
+ <>
144
+ {plain.value
145
+ ? <div ref={n => ctl.bind(n)} data-scope="stack" role="group" aria-labelledby={labelledby}>{children}</div>
146
+ : <form ref={n => ctl.bind(n)} data-scope="stack" novalidate aria-labelledby={labelledby} onSubmit={ctl.submit}>{children}</form>}
147
+ </>
148
+ )
149
+ }
150
+
151
+ /** The failure at the top of the step (09 §1.3): the Alert takes focus; a rate limit counts down; offline is a Banner. */
152
+ export function FormError({ ctl, actions }: { ctl: StepForm; actions?: unknown }) {
153
+ const m = derived(() => ctl.failure.value)
154
+ return (
155
+ <div data-part="formerr" tabindex="-1">
156
+ {m.value?.surface === 'alert' && <Alert tone="critical" live title={m.value.title} actions={actions}>{m.value.description}</Alert>}
157
+ {m.value?.surface === 'rate-limit' && <Alert tone="caution" title={m.value.title}>{ctl.cooling.value > 0 ? `Try again in ${ctl.cooling.value}s.` : 'You can try again now.'}</Alert>}
158
+ {m.value?.surface === 'banner' && <Banner tone="caution" live={m.value.live}><b>{m.value.title}</b> {m.value.description}</Banner>}
159
+ </div>
160
+ )
161
+ }
162
+
163
+ /** The one solid: keeps its label while pending; a rate limit disables it and says why. */
164
+ export function Submit({ ctl, disabled, children, ...rest }: { ctl: StepForm; disabled?: boolean | undefined; children?: unknown } & ButtonProps) {
165
+ const wait = derived(() => ctl.cooling.value)
166
+ return <Button type="submit" size="lg" full loading={ctl.pending.value} disabled={disabled || wait.value > 0} {...rest}>{wait.value > 0 ? `Try again in ${wait.value}s` : children}</Button>
167
+ }
168
+
169
+ /** A text button in a step's row. */
170
+ export const TextLink = ({ children, ...rest }: ButtonProps) => <Button emphasis="link" data-hit="text" {...rest}>{children}</Button>
package/src/index.ts ADDED
@@ -0,0 +1,74 @@
1
+ // @weft-js/warp: the weft binding of warp. Each component renders the spec's specimen markup with weft's compiled
2
+ // templates and attaches warp-core's behaviour; each imports only its own CSS (warp-core/styles, split per component).
3
+ export { Stack, Row, Grid, Split, Center, Cover, Frame, Spacer, Inset, type Step } from './Layout'
4
+ export { Heading, Text, Link } from './Type'
5
+ export { Button, type ButtonProps, type Tone, type Emphasis } from './Button'
6
+ export { Toggle, type ToggleProps } from './Toggle'
7
+ export { Field, Input, Search, Textarea, type FieldProps } from './Field'
8
+ export { NumberField, type NumberFieldProps } from './NumberField'
9
+ export { CompactSelect, Select, type Option, type OptionGroup } from './Select'
10
+ export { Switch, Checkbox, Radio, RadioGroup, Segmented, type SegmentedOption, type RadioOption } from './Choice'
11
+ export { Slider, type SliderProps } from './Slider'
12
+ export { Accordion, type AccordionItem, type AccordionProps } from './Accordion'
13
+ export { Ticker, type TickerProps } from './Ticker'
14
+ export { Menu, type MenuEntry as MenuItem, type MenuProps } from './Menu'
15
+ export { Chip } from './Chip'
16
+ export { Tabs, type TabOption } from './Tabs'
17
+ export { Nav } from './Nav'
18
+ export { Card } from './Card'
19
+ export { Badge } from './Badge'
20
+ export { Tag } from './Tag'
21
+ export { Kbd } from './Kbd'
22
+ export { Alert } from './Alert'
23
+ export { Progress } from './Progress'
24
+ export { Spinner } from './Spinner'
25
+ export { Skeleton } from './Skeleton'
26
+ export { Shape } from './Shape'
27
+ export { Tooltip } from './Tooltip'
28
+ export { Popover } from './Popover'
29
+ export { Modal } from './Modal'
30
+ export { Drawer } from './Drawer'
31
+ export { Toaster, toast } from './Toast'
32
+ export { Table, type Column, type TableProps, type TableSort, type TableStatus, type ListPage } from './Table'
33
+ export { DataGrid, PivotTable, type DataGridProps, type GridColumn, type GridView, type GridTree, type GridPaste, type PivotMeasure } from './DataGrid'
34
+ export { PaginationBar, LoadMore, type PaginationBarProps } from './Pagination'
35
+ export { CommandPalette, type Command, type CommandMode, type CommandSource, type CommandPaletteProps } from './CommandPalette'
36
+ export { MentionsInput, type MentionsInputProps } from './MentionsInput'
37
+ export { CodeBlock, Code, Snippet, type CodeBlockProps } from './CodeBlock'
38
+ export { CodeInput, JsonEditor, type CodeInputProps, type JsonEditorProps } from './CodeInput'
39
+ export { KeyValueEditor, type KeyValue, type KeyValueEditorProps } from './KeyValueEditor'
40
+ export { RichTextEditor, type RichTextEditorProps, type SlashCommand } from './RichTextEditor'
41
+ export { Markdown, MarkdownEditor, type MarkdownEditorProps } from './Markdown'
42
+ export { Shell, type ShellProps, type NavItem, type NavGroup, type NavIcon, type Crumb, type MenuEntry, type Workspace } from './Shell'
43
+ /** A docs sidebar: one left panel whose sections fold and remember; docked from 900px, opened over the page on phones. */
44
+ export { SideNav, type SideNavProps, type SideNavSection, type SideNavLink } from './SideNav'
45
+ export { Icon, type IconName, type IconRef } from './Icon'
46
+ export { Chart, type ChartFrame } from './Chart'
47
+ export { LineChart, BarChart, ComboChart, DonutChart, RadialChart, FunnelChart, ScatterChart, BubbleChart, Histogram, HeatGrid, RadarChart, Treemap, Sunburst, SankeyChart, CandlestickChart, BoxPlot, WaterfallChart, StreamChart, BrushChart, TileMap, GaugeChart, Sparkline } from './Charts'
48
+ export { type Bindable, type Openable } from './Picker'
49
+ export { InlineCalendar } from './Calendar'
50
+ export { DateField, DateRangeField, compareRange, type DateFieldProps, type DateRangeFieldProps } from './DateField'
51
+ export { TimeField, DateTimeField, type TimeFieldProps, type DateTimeFieldProps } from './TimeField'
52
+ export { MonthPicker, WeekPicker, PeriodPicker, type MonthPickerProps, type WeekPickerProps, type PeriodPickerProps } from './MonthPicker'
53
+ export { TimeZoneSelect, CountrySelect, LanguageSelect, CurrencySelect, PhoneField, type TimeZoneSelectProps, type PhoneFieldProps } from './RichSelect'
54
+ export { DurationField, type DurationFieldProps } from './DurationField'
55
+ export { RecurrenceField, type RecurrenceFieldProps } from './RecurrenceField'
56
+ export { PlaceLookupInput, AddressField, type Place, type PlaceBias, type PlaceLookupInputProps, type AddressFieldProps, type AddressProvider } from './PlaceField'
57
+ export { PaymentCardInput, CardNumberText, type PaymentCardInputProps, type ProviderField, type ProviderAppearance, type CardNumberTextProps } from './PaymentCard'
58
+ export { SignaturePad, type SignaturePadProps, type SignatureValue } from './SignaturePad'
59
+ export { Canvas, CanvasStage, StageSelect, type CanvasProps, type CanvasStageProps, type StageOption, type StageSelectProps, type Frame as CanvasFrame, type Press, type Scene, type Scroll, type StageLayout, type StagePainter, type StagePiece, type Word } from './Canvas'
60
+ export { Dither, DitherField, DitherGlyph, DitherStage, LEVEL, PAINTERS, type DitherProps, type DitherFieldProps, type DitherGlyphProps, type DitherStageProps, type Algorithm, type Brush, type DitherPainter, type DitherPiece, type FieldFn, type FieldName, type Gradient, type MaskSource, type Pattern, type PointerMode, type Ramp, type Skin } from './Dither'
61
+ export * from './agent/index'
62
+ export * from './schedule/index'
63
+ export * from './live/index'
64
+ export * from './collab/index'
65
+ export { Thread, Kanban } from './collab/index' // the one chat and the one board: both are windows (VirtualList)
66
+ export * from './flow/index'
67
+ export * from './admin/index'
68
+ export { VirtualList, type VirtualListProps, type VirtualControl } from './VirtualList'
69
+ export { Feed, type FeedProps, type FeedItem } from './Feed'
70
+ export { ScrollGrid, type ScrollGridProps, type GridColumn as ScrollGridColumn } from './ScrollGrid'
71
+ export type { Address, MentionItem } from '@weft-js/warp-core'
72
+ export type { Peer, CursorPeer, IntegrityStats, ChatMessage, MessageReaction } from '@weft-js/warp-core'
73
+ export type { IsoDate, IsoTime, IsoDateTime, IsoMonth, IsoWeek, IsoDuration, IsoZoned, TimeZoneId, DateRange, ChangeReason } from '@weft-js/warp-core'
74
+ export { sweep, setDial, getDial } from '@weft-js/warp-core'
package/src/layout.css ADDED
@@ -0,0 +1,4 @@
1
+ /* 02 §3 lists `page` in the Space scale; the spec's layout CSS stops at `section`. */
2
+ @layer warp.layout {
3
+ :where([data-gap="page"]) { --_gap: var(--warp-space-page); }
4
+ }
package/src/links.css ADDED
@@ -0,0 +1,4 @@
1
+ /* A bare `<a>` is never the browser's blue: warp's link ink and hairline underline (05 §4), in warp.base so every component's own link and app CSS win. */
2
+ @layer warp.base {
3
+ :where(a:any-link) { color: var(--warp-color-accent-text); text-decoration-thickness: var(--warp-border-hairline); text-underline-offset: 0.22em; }
4
+ }
@@ -0,0 +1,49 @@
1
+ import { arrivals, groupByDay, uid } from '@weft-js/warp-core'
2
+ import { derived } from '@weft-js/core'
3
+ import { Avatar, type LiveState } from '../parts'
4
+ import { use } from '@weft-js/warp-core/weft'
5
+ import { avatarTint, G, val } from './shared'
6
+ import '../styles'
7
+ import '@weft-js/warp-core/styles/app/base.css'
8
+ import '@weft-js/warp-core/styles/app/activity.css'
9
+ import '@weft-js/warp-core/styles/app/activity-feed.css'
10
+ import '@weft-js/warp-core/styles/app/sync.css'
11
+
12
+ /** One line of the activity feed. */
13
+ export interface ActivityItem { id: string; actor: string; verb: string; object?: string; detail?: string; time: string; datetime?: string; day?: string }
14
+
15
+ /**
16
+ * Activity feed (07 §4.2): actor, verb, object, time, grouped per day on a hairline spine, the quiet sibling of the inbox
17
+ * row. The header shows a "Live" check while `live` is live; new ids arrive.
18
+ */
19
+ export function ActivityFeed({ items, live, title = 'Activity', level = 3 }: { items: ActivityItem[] | { value: ActivityItem[] } | (() => ActivityItem[]); live?: LiveState | { value: LiveState } | (() => LiveState) | undefined; title?: string; /** The title's heading level: one below the heading the feed sits under. */ level?: 2 | 3 | 4 | 5 | 6 }) {
20
+ const id = uid('activity')
21
+ const data = derived(() => (val<ActivityItem[]>(items) ?? []))
22
+ const days = derived(() => groupByDay(data.value))
23
+ const many = derived(() => days.value.length > 1 || data.value.some(n => n.day))
24
+ const on = derived(() => val<LiveState | undefined>(live) === 'live')
25
+ let root: HTMLElement | undefined
26
+ use(arrivals, () => root, () => ({ items: '[data-scope="activity"] > li', ids: data.value.map(n => n.id) }))
27
+ return (
28
+ <section ref={n => (root = n)} data-scope="activity-feed" aria-labelledby={`${id}-t`}>
29
+ <header>
30
+ <h3 id={`${id}-t`} aria-level={level === 3 ? undefined : level}>{title}</h3>
31
+ {on.value && <span data-scope="sync" data-state="saved"><G name="check" /> Live</span>}
32
+ </header>
33
+ {days.value.map(g => (
34
+ <div key={g.day}>
35
+ {many.value && <span data-part="eyebrow">{g.day}</span>}
36
+ <ol data-scope="activity">
37
+ {g.rows.map(n => (
38
+ <li key={n.id} data-id={n.id}>
39
+ <Avatar name={n.actor} size="sm" style={avatarTint(n.actor)} />
40
+ <span><b>{n.actor}</b> {n.verb}{n.object ? ' ' : ''}{n.object && <b>{n.object}</b>}{n.detail ? ` ${n.detail}` : ''}</span>
41
+ <time datetime={n.datetime}>{n.time}</time>
42
+ </li>
43
+ ))}
44
+ </ol>
45
+ </div>
46
+ ))}
47
+ </section>
48
+ )
49
+ }
@@ -0,0 +1,62 @@
1
+ import { bellCount, bellLabel, bump, popover, uid, type Align, type Side } from '@weft-js/warp-core'
2
+ import { derived, onMount, state, untrack, watch, type State } from '@weft-js/core'
3
+ import { Button } from '../Button'
4
+ import { Badge } from '../Badge'
5
+ import { use } from '@weft-js/warp-core/weft'
6
+ import { G, val } from './shared'
7
+ import '../styles'
8
+ import '@weft-js/warp-core/styles/components/popover.css'
9
+ import '@weft-js/warp-core/styles/app/bell.css'
10
+ import '@weft-js/warp-core/styles/app/badge.css'
11
+
12
+ /** Props for Bell. */
13
+ export interface BellProps {
14
+ /** Unread count: a number, a State or a getter. */
15
+ count?: number | { value: number } | (() => number | undefined)
16
+ onOpen?: () => void
17
+ /** The panel, usually `open => <Inbox variant="popover" open={open.value} … />`, shown in a Popover from the bell. */
18
+ inbox?: (open: State<boolean>) => unknown
19
+ /** On a phone the inbox is a page, not a popover (07 §4.3): below 600px the bell calls this instead of opening. */
20
+ route?: () => void
21
+ side?: Side | undefined
22
+ align?: Align
23
+ }
24
+
25
+ /** Bell (07 §4.1): icon button and count badge, which bumps once per batch; its name says the count (§4.5). */
26
+ export function Bell({ count, onOpen, inbox, route, side = 'bottom', align = 'end' }: BellProps) {
27
+ const id = uid('bell')
28
+ const n = derived(() => Number(val<unknown>(count) ?? 0))
29
+ const open = state(false)
30
+ const panel = untrack(() => inbox)
31
+ let button: HTMLButtonElement | undefined
32
+ let box: HTMLDivElement | undefined
33
+ let badge: HTMLElement | undefined
34
+ use(popover, () => box, () => ({ open: open.value, trigger: button!, side, align, onClose: () => (open.value = false) }))
35
+ onMount(() => watch(() => n.value, (v, prev) => { if (prev !== undefined && v > prev && badge) bump(badge) }))
36
+ const press = () => {
37
+ const toPage = route && button!.ownerDocument.defaultView!.matchMedia?.('(max-width: 599.98px)').matches
38
+ onOpen?.()
39
+ if (toPage) route()
40
+ else if (panel) open.value = !open.value
41
+ }
42
+ return (
43
+ <span data-scope="bell">
44
+ <Button
45
+ emphasis="ghost"
46
+ tone="neutral"
47
+ data-icon-only=""
48
+ ref={(el: HTMLButtonElement) => (button = el)}
49
+ id={`${id}-b`}
50
+ aria-label={bellLabel(n.value)}
51
+ aria-haspopup={panel ? 'dialog' : undefined}
52
+ aria-expanded={panel ? (open.value ? 'true' : 'false') : undefined}
53
+ aria-controls={panel ? id : undefined}
54
+ onClick={press}
55
+ >
56
+ <G name="bell" />
57
+ </Button>
58
+ <Badge emphasis="solid" count ref={(el: HTMLElement) => (badge = el)} hidden={n.value === 0} aria-hidden="true">{bellCount(n.value)}</Badge>
59
+ {panel && <div ref={el => (box = el)} data-scope="popover" data-part="content" role="dialog" popover="manual" id={id} aria-label="Notifications">{panel(open)}</div>}
60
+ </span>
61
+ )
62
+ }
@@ -0,0 +1,49 @@
1
+ import { announce, connectionAnnouncement, liveState, type LiveState } from '@weft-js/warp-core'
2
+ import { derived, onCleanup, onMount, state, untrack, watch } from '@weft-js/core'
3
+ import { Banner, LiveStatus } from '../parts'
4
+ import { Button } from '../Button'
5
+ import { val } from './shared'
6
+
7
+ const TEXT: Record<LiveState, string> = { connecting: 'Connecting…', live: 'Live', reconnecting: 'Reconnecting…', offline: 'Offline' }
8
+
9
+ type Status = string | undefined | { value: string } | (() => string | undefined)
10
+
11
+ /**
12
+ * The one pill (07 §0, §4.4): `state` is `.live`, `stream().status` or `channel.status`, mapped to the four states. It
13
+ * announces each change once; `queued` is the count in "Back online. 4 updates."
14
+ */
15
+ export function ConnectionPill({ state: status, label = 'Connection', queued = 0 }: { state: Status; label?: string | undefined; queued?: number }) {
16
+ const now = derived(() => liveState(val<string | undefined>(status)))
17
+ const words = derived(() => TEXT[now.value])
18
+ let root: HTMLSpanElement | undefined
19
+ onMount(() => watch(() => now.value, (next, prev) => {
20
+ const say = connectionAnnouncement(prev, next, untrack(() => queued))
21
+ if (say && root) announce(root.ownerDocument, say.text, say.assertive)
22
+ }))
23
+ return <span ref={n => (root = n)}><LiveStatus state={now.value} label={label}>{words.value}</LiveStatus></span>
24
+ }
25
+
26
+ /**
27
+ * The trouble banner (07 §4.4): nothing while live or connecting, after `delay` ms of reconnecting (data stays, edits
28
+ * queue), at once when offline, with Retry. Quiet for assistive tech: the pill does the announcing.
29
+ */
30
+ export function ConnectionBanner({ state: status, delay = 3000, onRetry, offline = "You're offline. Showing what was saved; new notifications will arrive when you reconnect.", reconnecting = 'Reconnecting… changes you make are queued.' }: { state: Status; delay?: number; onRetry?: (() => void) | undefined; offline?: unknown; reconnecting?: unknown }) {
31
+ const now = derived(() => liveState(val<string | undefined>(status)))
32
+ const late = state(false)
33
+ const retry = untrack(() => onRetry)
34
+ onMount(() => {
35
+ let t: ReturnType<typeof setTimeout> | undefined
36
+ watch(() => now.value, next => {
37
+ clearTimeout(t)
38
+ if (next !== 'reconnecting') { late.value = false; return }
39
+ t = setTimeout(() => (late.value = true), untrack(() => delay))
40
+ })
41
+ onCleanup(() => clearTimeout(t))
42
+ })
43
+ return (
44
+ <>
45
+ {now.value === 'offline' && <Banner tone="critical" actions={retry ? () => <Button emphasis="outline" tone="neutral" size="sm" onClick={retry}>Retry now</Button> : undefined}>{offline}</Banner>}
46
+ {now.value === 'reconnecting' && late.value && <Banner tone="caution">{reconnecting}</Banner>}
47
+ </>
48
+ )
49
+ }