@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.
- package/LICENSE +16 -0
- package/README.md +55 -2
- package/package.json +43 -4
- package/plugin.d.mts +30 -0
- package/plugin.mjs +358 -0
- package/src/Accordion.tsx +58 -0
- package/src/Alert.tsx +25 -0
- package/src/Badge.tsx +17 -0
- package/src/Button.tsx +100 -0
- package/src/Calendar.tsx +162 -0
- package/src/Canvas.tsx +110 -0
- package/src/Card.tsx +29 -0
- package/src/Chart.tsx +133 -0
- package/src/Charts.tsx +69 -0
- package/src/Chip.tsx +27 -0
- package/src/Choice.tsx +105 -0
- package/src/CodeBlock.tsx +65 -0
- package/src/CodeInput.tsx +130 -0
- package/src/Combobox.tsx +113 -0
- package/src/CommandPalette.tsx +348 -0
- package/src/DataGrid.tsx +561 -0
- package/src/DateField.tsx +221 -0
- package/src/Dither.tsx +129 -0
- package/src/Drawer.tsx +41 -0
- package/src/DurationField.tsx +98 -0
- package/src/Feed.tsx +74 -0
- package/src/Field.tsx +147 -0
- package/src/Icon.tsx +8 -0
- package/src/Kbd.tsx +6 -0
- package/src/KeyValueEditor.tsx +114 -0
- package/src/Layout.tsx +109 -0
- package/src/Markdown.tsx +63 -0
- package/src/MentionsInput.tsx +150 -0
- package/src/Menu.tsx +111 -0
- package/src/Modal.tsx +40 -0
- package/src/MonthPicker.tsx +190 -0
- package/src/Nav.tsx +20 -0
- package/src/NumberField.tsx +78 -0
- package/src/Pagination.tsx +122 -0
- package/src/PaymentCard.tsx +63 -0
- package/src/Picker.tsx +120 -0
- package/src/PlaceField.tsx +126 -0
- package/src/Popover.tsx +34 -0
- package/src/Progress.tsx +26 -0
- package/src/RecurrenceField.tsx +126 -0
- package/src/RichSelect.tsx +243 -0
- package/src/RichTextEditor.tsx +207 -0
- package/src/ScrollGrid.tsx +190 -0
- package/src/Segments.tsx +70 -0
- package/src/Select.tsx +192 -0
- package/src/Shape.tsx +20 -0
- package/src/Shell.tsx +308 -0
- package/src/SideNav.tsx +147 -0
- package/src/SignaturePad.tsx +120 -0
- package/src/Skeleton.tsx +14 -0
- package/src/Slider.tsx +42 -0
- package/src/Spinner.tsx +7 -0
- package/src/Table.tsx +528 -0
- package/src/Tabs.tsx +67 -0
- package/src/Tag.tsx +15 -0
- package/src/Ticker.tsx +36 -0
- package/src/TimeField.tsx +237 -0
- package/src/Toast.tsx +56 -0
- package/src/Toggle.tsx +42 -0
- package/src/Tooltip.tsx +32 -0
- package/src/Type.tsx +46 -0
- package/src/VirtualList.tsx +386 -0
- package/src/admin/ApiKeys.tsx +136 -0
- package/src/admin/Audit.tsx +93 -0
- package/src/admin/Billing.tsx +123 -0
- package/src/admin/Cart.tsx +96 -0
- package/src/admin/Checkout.tsx +300 -0
- package/src/admin/CsvImport.tsx +174 -0
- package/src/admin/Danger.tsx +110 -0
- package/src/admin/Integrations.tsx +200 -0
- package/src/admin/InviteDialog.tsx +64 -0
- package/src/admin/Members.tsx +186 -0
- package/src/admin/Order.tsx +63 -0
- package/src/admin/PlainTable.tsx +22 -0
- package/src/admin/Plans.tsx +167 -0
- package/src/admin/SaveBar.tsx +112 -0
- package/src/admin/Secret.tsx +37 -0
- package/src/admin/Settings.tsx +76 -0
- package/src/admin/StepBar.tsx +35 -0
- package/src/admin/TagsInput.tsx +79 -0
- package/src/admin/atoms.tsx +33 -0
- package/src/admin/index.ts +16 -0
- package/src/agent/Actions.tsx +85 -0
- package/src/agent/AgentPlan.tsx +49 -0
- package/src/agent/ArtifactPanel.tsx +80 -0
- package/src/agent/Assistant.tsx +96 -0
- package/src/agent/Checkpoint.tsx +14 -0
- package/src/agent/Cite.tsx +39 -0
- package/src/agent/ContextMeter.tsx +31 -0
- package/src/agent/Conversation.tsx +34 -0
- package/src/agent/Message.tsx +53 -0
- package/src/agent/ModelPicker.tsx +65 -0
- package/src/agent/Prompt.tsx +197 -0
- package/src/agent/Prose.tsx +54 -0
- package/src/agent/Question.tsx +52 -0
- package/src/agent/Reasoning.tsx +19 -0
- package/src/agent/Shimmer.tsx +7 -0
- package/src/agent/Sources.tsx +23 -0
- package/src/agent/SpeechInput.tsx +102 -0
- package/src/agent/Suggestions.tsx +22 -0
- package/src/agent/ToolCall.tsx +91 -0
- package/src/agent/TraceViewer.tsx +55 -0
- package/src/agent/bits.tsx +40 -0
- package/src/agent/index.ts +22 -0
- package/src/bind.ts +42 -0
- package/src/collab/AutoTrack.tsx +42 -0
- package/src/collab/Comments.tsx +94 -0
- package/src/collab/Composer.tsx +173 -0
- package/src/collab/ConversationList.tsx +73 -0
- package/src/collab/Cursors.tsx +35 -0
- package/src/collab/DataIntegrity.tsx +45 -0
- package/src/collab/Kanban.tsx +185 -0
- package/src/collab/Lock.tsx +23 -0
- package/src/collab/PresenceStack.tsx +67 -0
- package/src/collab/ShareDialog.tsx +118 -0
- package/src/collab/Status.tsx +42 -0
- package/src/collab/SyncedForm.tsx +19 -0
- package/src/collab/Thread.tsx +273 -0
- package/src/collab/VersionHistory.tsx +49 -0
- package/src/collab/ViewportRail.tsx +49 -0
- package/src/collab/index.ts +16 -0
- package/src/collab/peer.tsx +16 -0
- package/src/fields.ts +5 -0
- package/src/flow/AuthScreen.tsx +40 -0
- package/src/flow/AuthSteps.tsx +120 -0
- package/src/flow/Checklist.tsx +84 -0
- package/src/flow/ConnBanner.tsx +147 -0
- package/src/flow/Flow.tsx +63 -0
- package/src/flow/MfaSetup.tsx +103 -0
- package/src/flow/Otp.tsx +114 -0
- package/src/flow/ReauthDialog.tsx +47 -0
- package/src/flow/ShortcutsOverlay.tsx +60 -0
- package/src/flow/SignGate.tsx +98 -0
- package/src/flow/SignIn.tsx +80 -0
- package/src/flow/SignUp.tsx +71 -0
- package/src/flow/Sso.tsx +38 -0
- package/src/flow/SystemPage.tsx +247 -0
- package/src/flow/Tour.tsx +108 -0
- package/src/flow/index.ts +17 -0
- package/src/flow/kit.tsx +170 -0
- package/src/index.ts +74 -0
- package/src/layout.css +4 -0
- package/src/links.css +4 -0
- package/src/live/ActivityFeed.tsx +49 -0
- package/src/live/Bell.tsx +62 -0
- package/src/live/Connection.tsx +49 -0
- package/src/live/Inbox.tsx +220 -0
- package/src/live/MeetingRoom.tsx +298 -0
- package/src/live/NotificationPreferences.tsx +193 -0
- package/src/live/Prejoin.tsx +140 -0
- package/src/live/PushSoftAsk.tsx +37 -0
- package/src/live/TransportPicker.tsx +163 -0
- package/src/live/index.ts +12 -0
- package/src/live/notify.tsx +16 -0
- package/src/live/shared.tsx +39 -0
- package/src/parts.tsx +93 -0
- package/src/schedule/Agenda.tsx +173 -0
- package/src/schedule/AvailabilityEditor.tsx +164 -0
- package/src/schedule/BookingCalendar.tsx +132 -0
- package/src/schedule/Calendar.tsx +746 -0
- package/src/schedule/EventEditor.tsx +87 -0
- package/src/schedule/EventPopover.tsx +65 -0
- package/src/schedule/MeetingPlanner.tsx +214 -0
- package/src/schedule/MoveSheet.tsx +78 -0
- package/src/schedule/YearView.tsx +163 -0
- package/src/schedule/bits.tsx +51 -0
- package/src/schedule/index.ts +10 -0
- package/src/skeleton-css.ts +4 -0
- package/src/styles.ts +10 -0
- package/src/tokens.d.ts +2 -0
- package/src/type.css +15 -0
- package/src/ui.ts +26 -0
- package/src/use-ui.ts +10 -0
- package/tailwind.css +38 -0
- package/tokens/color.mjs +66 -0
package/src/SideNav.tsx
ADDED
|
@@ -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 => ({ '&': '&', '<': '<', '>': '>' })[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
|
+
}
|
package/src/Skeleton.tsx
ADDED
|
@@ -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
|
+
}
|
package/src/Spinner.tsx
ADDED
|
@@ -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
|
+
}
|