@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,118 @@
1
+ import { CopyIcon, LinkIcon } from '@weft-js/warp-icons'
2
+ import { announce, arrivalTracker, installSweep, type Peer } from '@weft-js/warp-core'
3
+ import { derived, state, untrack, type State } from '@weft-js/core'
4
+ import { Button } from '../Button'
5
+ import { ComboField } from '../Combobox'
6
+ import { Modal } from '../Modal'
7
+ import { CompactSelect } from '../Select'
8
+ import { Tag } from '../Tag'
9
+ import { PeerAvatar, read, type Live } from './peer'
10
+ import '../styles'
11
+ import '@weft-js/warp-core/styles/app/tokens.css'
12
+ import '@weft-js/warp-core/styles/app/base.css'
13
+ import '@weft-js/warp-core/styles/app/share.css'
14
+ import '@weft-js/warp-core/styles/components/share.css'
15
+
16
+
17
+ /** What someone may do with the document. */
18
+ export type ShareRole = 'viewer' | 'commenter' | 'editor' | 'owner'
19
+ const ROLES: [ShareRole, string][] = [['viewer', 'Viewer'], ['commenter', 'Commenter'], ['editor', 'Editor']]
20
+ /** Someone the document is shared with. */
21
+ export type ShareMember = Peer & { email: string; role: ShareRole }
22
+ /** Someone it could be shared with. */
23
+ export type ShareContact = Peer & { email: string }
24
+ /** Who a link lets in, and as what. */
25
+ export interface ShareLink { access: 'restricted' | 'anyone'; role: ShareRole }
26
+
27
+ /** Props for ShareDialog. */
28
+ export interface ShareDialogProps {
29
+ open: State<boolean>
30
+ /** The object's title: "Q4 brief". */
31
+ title: string
32
+ members: Live<ShareMember[]>
33
+ /** People the invite field suggests; any email address works too. */
34
+ people?: ShareContact[]
35
+ link: Live<ShareLink>
36
+ /** The URL Copy link writes. */
37
+ url: string
38
+ onInvite: (emails: string[], role: ShareRole) => void
39
+ /** A role, or null to remove the person. */
40
+ onRoleChange: (m: ShareMember, role: ShareRole | null) => void
41
+ onLinkAccess: (link: ShareLink) => void
42
+ }
43
+
44
+ const ROLE_OPTIONS = ROLES.map(([value, label]) => ({ value, label }))
45
+ const MEMBER_OPTIONS = [...ROLE_OPTIONS, { value: 'remove', label: 'Remove' }]
46
+ const ACCESS_OPTIONS = [{ value: 'restricted', label: 'Restricted' }, { value: 'anyone', label: 'Anyone with link' }]
47
+
48
+ /**
49
+ * The share dialog (07 §6.2): the invite combobox with chips, a role per person, link access, Copy link with one
50
+ * confirm sweep, and invited rows arriving.
51
+ */
52
+ export function ShareDialog({ open, title, members, people, link, url, onInvite, onRoleChange, onLinkAccess }: ShareDialogProps) {
53
+ const list = derived(() => read(members))
54
+ const access = derived(() => read(link))
55
+ const mark = arrivalTracker()
56
+ const fresh = derived(() => mark(list.value.map(m => m.id)))
57
+ const typed = state('')
58
+ const chips = state<string[]>([])
59
+ const role = state<ShareRole>('editor')
60
+ const copied = state(false)
61
+ let copy: HTMLElement | undefined
62
+ const add = (email: string) => {
63
+ const e = email.trim()
64
+ if (e && !chips.value.includes(e)) chips.value = [...chips.value, e]
65
+ typed.value = ''
66
+ }
67
+ // oxlint-disable-next-line typescript/require-await -- `find` is typed to return a promise, and Promise.resolve costs bytes for nothing
68
+ const find = async (q: string) => untrack(() => (people ?? []).filter(p => `${p.name} ${p.email}`.toLowerCase().includes(q.toLowerCase())))
69
+ const invite = () => {
70
+ const all = typed.value.includes('@') ? [...new Set([...chips.value, typed.value.trim()])] : chips.value
71
+ if (!all.length) return
72
+ onInvite(all, role.value)
73
+ announce(copy!.ownerDocument, `Invited ${all.length === 1 ? all[0] : `${all.length} people`}`)
74
+ chips.value = []
75
+ typed.value = ''
76
+ }
77
+ return (
78
+ <Modal open={open} title={`Share “${title}”`} footer={<Button onClick={() => (open.value = false)}>Done</Button>}>
79
+ <div data-scope="share">
80
+ <form style="display:flex;gap:8px;align-items:center" onSubmit={(e: Event) => { e.preventDefault(); if (typed.value.includes('@')) add(typed.value); else invite() }}>
81
+ <div style="flex:1;min-inline-size:0">
82
+ <ComboField label="Add people or email" minChars={1} text={() => typed.value} onText={t => (typed.value = t)} find={find} row={(p: ShareContact) => ({ main: p.name, sub: p.email })} onPick={(p: ShareContact) => add(p.email)} />
83
+ </div>
84
+ <CompactSelect label="Role for new people" value={role.value} options={ROLE_OPTIONS} onValueChange={v => (role.value = v as ShareRole)} />
85
+ <Button onClick={invite}>Invite</Button>
86
+ </form>
87
+ {chips.value.length > 0 && <div style="display:flex;flex-wrap:wrap;gap:6px;margin-block-start:8px">{chips.value.map(c => <Tag key={c} onRemove={() => (chips.value = chips.value.filter(x => x !== c))}>{c}</Tag>)}</div>}
88
+ <div>
89
+ {list.value.map(m => (
90
+ <div key={m.id} data-part="person" data-arrive={fresh.value.includes(m.id) ? '' : undefined}>
91
+ <PeerAvatar peer={m} size="sm" />
92
+ <span>{m.name}<small>{m.email}</small></span>
93
+ {m.role === 'owner'
94
+ ? <span>Owner</span>
95
+ : <>
96
+ <CompactSelect label={`Role for ${m.name}`} value={m.role} options={MEMBER_OPTIONS} onValueChange={v => onRoleChange(m, v === 'remove' ? null : (v as ShareRole))} />
97
+ </>}
98
+ </div>
99
+ ))}
100
+ </div>
101
+ <div data-part="link">
102
+ <LinkIcon />
103
+ <CompactSelect label="Link access" value={access.value.access} options={ACCESS_OPTIONS} onValueChange={v => onLinkAccess({ ...access.value, access: v as ShareLink['access'] })} />
104
+ {access.value.access === 'anyone' && <CompactSelect label="Link role" value={access.value.role} options={ROLE_OPTIONS} onValueChange={v => onLinkAccess({ ...access.value, role: v as ShareRole })} />}
105
+ <span data-part="spacer" style="flex:1"></span>
106
+ <span ref={n => (copy = n)} style="display:contents">
107
+ <Button emphasis="outline" tone="neutral" size="sm" onClick={(e: MouseEvent) => {
108
+ void navigator.clipboard?.writeText(url).catch(() => {})
109
+ copied.value = true
110
+ installSweep().confirm(e.currentTarget as Element)
111
+ announce((e.currentTarget as Element).ownerDocument, 'Link copied')
112
+ }}><CopyIcon /> {copied.value ? 'Copied' : 'Copy link'}</Button>
113
+ </span>
114
+ </div>
115
+ </div>
116
+ </Modal>
117
+ )
118
+ }
@@ -0,0 +1,42 @@
1
+ import { AlertIcon, CheckIcon, ClockIcon } from '@weft-js/warp-icons'
2
+ import { announce, firstName, type Peer } from '@weft-js/warp-core'
3
+ import { derived } from '@weft-js/core'
4
+ import { Alert } from '../Alert'
5
+ import { Button } from '../Button'
6
+ import '../styles'
7
+ import '@weft-js/warp-core/styles/app/tokens.css'
8
+ import '@weft-js/warp-core/styles/app/sync.css'
9
+ import '@weft-js/warp-core/styles/app/conflict.css'
10
+
11
+ /** Where a document's saving stands. */
12
+ export type SyncState = 'saved' | 'saving' | 'offline'
13
+ const SYNC = { saved: 'Saved', saving: 'Saving…', offline: 'Offline · changes kept' } as const
14
+
15
+ /** Next to the title (07 §6.1): "Saved", "Saving…", "Offline · changes kept"; map `channel.status` onto it. */
16
+ export function SyncIndicator({ state }: { state: SyncState }) {
17
+ const icon = derived(() => (state === 'saved' ? 'check' : state === 'saving' ? 'clock' : 'alert'))
18
+ return <span data-scope="sync" data-state={state} role="status">{icon.value === 'check' ? <CheckIcon /> : icon.value === 'clock' ? <ClockIcon /> : <AlertIcon />} {SYNC[state]}</span>
19
+ }
20
+
21
+ /**
22
+ * A conflict (07 §6.1): a caution alert with both versions side by side, and Use theirs, Keep mine, Compare. Nothing
23
+ * is discarded silently: the caller holds both values until one of these fires.
24
+ */
25
+ export function ConflictAlert({ field, peer, time, theirs, yours, onUseTheirs, onKeepMine, onCompare }: { field: string; peer: Peer; time: string; theirs: unknown; yours: unknown; onUseTheirs: () => void; onKeepMine: () => void; onCompare?: () => void }) {
26
+ let box: HTMLDivElement | undefined
27
+ const settle = (fn: () => void) => () => { fn(); if (box) announce(box.ownerDocument, 'Conflict resolved') }
28
+ const compare = derived(() => onCompare !== undefined)
29
+ const first = () => firstName(peer.name)
30
+ const actions = derived(() => <><Button size="sm" onClick={settle(onUseTheirs)}>Use theirs</Button><Button size="sm" emphasis="outline" tone="neutral" onClick={settle(onKeepMine)}>Keep mine</Button>{compare.value && <Button size="sm" emphasis="ghost" tone="neutral" onClick={() => onCompare!()}>Compare</Button>}</>)
31
+ return (
32
+ <div ref={n => (box = n)}>
33
+ <Alert tone="caution" live title={`${first()} changed “${field}” while you were editing`} actions={actions.value}>
34
+ Pick a version. Nothing is lost either way.
35
+ <div data-scope="conflict">
36
+ <div><small>Theirs · {first()} · {time}</small><ins>{theirs}</ins></div>
37
+ <div><small>Yours · unsaved</small><del>{yours}</del></div>
38
+ </div>
39
+ </Alert>
40
+ </div>
41
+ )
42
+ }
@@ -0,0 +1,19 @@
1
+ import { syncedForm, type FormOp } from '@weft-js/warp-core'
2
+ import { derived } from '@weft-js/core'
3
+ import { use } from '@weft-js/warp-core/weft'
4
+ import { read, type Live } from './peer'
5
+ import '../styles'
6
+ import '@weft-js/warp-core/styles/app/tokens.css'
7
+ import '@weft-js/warp-core/styles/app/base.css'
8
+ import '@weft-js/warp-core/styles/app/synced-form.css'
9
+
10
+ /**
11
+ * All-inputs sync (07 §6.4): every named control inside is shared. `ops` are the channel's remote edits, oldest first;
12
+ * `onSend` hears each local one. A remote edit rings the field and names the peer for 1.4 s; a focused field queues it.
13
+ */
14
+ export function SyncedForm({ ops, onSend, label, children }: { ops: Live<FormOp[]>; onSend?: ((name: string, value: string | boolean) => void) | undefined; label: string; children?: unknown }) {
15
+ const list = derived(() => read(ops))
16
+ let form: HTMLFormElement | undefined
17
+ use(syncedForm, () => form, () => ({ ops: list.value, onSend }))
18
+ return <form ref={n => (form = n)} data-scope="synced-form" aria-label={label} onSubmit={(e: Event) => e.preventDefault()}>{children}</form>
19
+ }
@@ -0,0 +1,273 @@
1
+ import { AlertIcon, CheckIcon, ChecksIcon, ClockIcon, CopyIcon, QuoteIcon, SmileIcon } from '@weft-js/warp-icons'
2
+ import { announce, arrivalTracker, burst, firstName, joinNames, messageLog, quoteText, reactionName, STICK_PX, textParts, threadRows, typingLine, uid, type ChatMessage, type MessageReaction, type ThreadRow } from '@weft-js/warp-core'
3
+ import { derived, onCleanup, onMount, state, untrack, watch } from '@weft-js/core'
4
+ import { Text } from '@weft-js/core/text'
5
+ import { Button } from '../Button'
6
+ import { use } from '@weft-js/warp-core/weft'
7
+ import { VirtualList, type VirtualControl } from '../VirtualList'
8
+ import { Composer, TypingIndicator, type ComposerProps, type ComposerSend, type QuoteDraftValue } from './Composer'
9
+ import { PeerAvatar, read, SWEEP, type Live } from './peer'
10
+ import '../styles'
11
+ import '@weft-js/warp-core/styles/components/button.css'
12
+ import '@weft-js/warp-core/styles/app/tokens.css'
13
+ import '@weft-js/warp-core/styles/app/base.css'
14
+ import '@weft-js/warp-core/styles/app/thread.css'
15
+ import '@weft-js/warp-core/styles/app/message.css'
16
+ import '@weft-js/warp-core/styles/app/reactions.css'
17
+ import '@weft-js/warp-core/styles/app/thread-replies.css'
18
+ import '@weft-js/warp-core/styles/app/avatar-stack.css'
19
+
20
+
21
+ /** Props for Message. */
22
+ export interface MessageProps {
23
+ message: ChatMessage
24
+ continued?: boolean
25
+ /** The last of mine: only it carries Sent or Read. */
26
+ last?: boolean
27
+ arrive?: boolean
28
+ /** The quoted source, read live. */
29
+ source?: ChatMessage | undefined
30
+ onRetry?: ((m: ChatMessage) => void) | undefined
31
+ onReact?: (m: ChatMessage, r: MessageReaction | null) => void
32
+ onReplies?: (m: ChatMessage) => void
33
+ }
34
+
35
+ /**
36
+ * One message (07 §5.1, §5.2): avatar, author, Actions toolbar, bubble (quote, text with mention chips), Jump to
37
+ * original, attachment, reactions, thread replies, delivery. Continued messages hide avatar and author.
38
+ */
39
+ export function Message({ message, continued, last, arrive, source, onRetry, onReact, onReplies }: MessageProps) {
40
+ const who = () => (message.mine ? 'You' : message.author.name)
41
+ const quoted = derived(() => {
42
+ const q = message.quote
43
+ if (!q) return null
44
+ const src = source
45
+ const author = src ? (src.mine ? 'You' : src.author.name) : q.author
46
+ const deleted = q.deleted || (!src && !q.author && !q.channel)
47
+ return { text: q.text, author, by: deleted ? 'Original message deleted' : q.channel ? `In ${q.channel}${author ? ` · ${author}` : ''}` : `${author} · ${src?.time ?? q.time ?? ''}`, ref: src ? q.ref : undefined, deleted }
48
+ })
49
+ const parts = derived(() => textParts(message.text, message.mentions))
50
+ const state_ = derived(() => message.state)
51
+ const delivery = derived(() => {
52
+ const s = state_.value
53
+ return !message.mine ? null : s === 'failed' || s === 'sending' ? s : last ? (s === 'read' ? 'read' : 'sent') : null
54
+ })
55
+ return (
56
+ <div
57
+ data-scope="message"
58
+ data-focus=""
59
+ role="article"
60
+ tabindex="-1"
61
+ data-mid={message.id}
62
+ aria-label={`${who()}, ${message.time}${quoted.value?.author ? `, quoting ${quoted.value.author}` : ''}`}
63
+ data-mine={message.mine ? '' : undefined}
64
+ data-continued={continued ? '' : undefined}
65
+ data-state={state_.value}
66
+ data-arrive={arrive ? '' : undefined}
67
+ data-mentions-me={message.mentions?.includes('you') ? '' : undefined}
68
+ >
69
+ {!message.mine && <PeerAvatar peer={message.author} />}
70
+ <div data-part="body">
71
+ {!continued && !message.mine && <div data-part="by"><b>{message.author.name}</b><time>{message.time}</time></div>}
72
+ <div data-part="actions" role="toolbar" aria-label="Message actions">
73
+ <Button emphasis="ghost" tone="neutral" size="sm" label="Quote reply" data-icon-only="" data-quote="" tabindex="-1" aria-keyshortcuts="Q"><QuoteIcon /></Button>
74
+ <Button emphasis="ghost" tone="neutral" size="sm" label="Copy text" data-icon-only="" data-copytext="" tabindex="-1"><CopyIcon /></Button>
75
+ </div>
76
+ <div data-part="bubble" data-sweep="">
77
+ <SWEEP />
78
+ {quoted.value && (
79
+ <blockquote data-part="quote">
80
+ <span data-part="quote-by"><span data-part="sr-only">Quoting </span>{quoted.value.by}</span>
81
+ {!quoted.value.deleted && <span data-part="quote-text">{quoted.value.text}</span>}
82
+ </blockquote>
83
+ )}
84
+ {message.streaming
85
+ ? <Text stream as="div">{message.text}</Text>
86
+ : <span data-part="text" data-measure="">{parts.value.map(p => p.mention ? <span key={p.at} data-part="mention">{p.text}</span> : <span key={p.at}>{p.text}</span>)}</span>}
87
+ </div>
88
+ {quoted.value?.ref && <Button emphasis="link" tone="neutral" data-part="quote-jump" data-jump={quoted.value.ref}>Jump to original</Button>}
89
+ {message.attachment && <div data-part="attachment"><i>{message.attachment.kind}</i><b>{message.attachment.name}</b><small>{message.attachment.meta}</small></div>}
90
+ {(message.reactions?.length ?? 0) > 0 && (
91
+ <div data-scope="reactions">
92
+ {message.reactions!.map(r => (
93
+ <button key={r.emoji} type="button" aria-pressed={r.mine ? 'true' : 'false'} aria-label={reactionName(r)} onClick={() => onReact?.(message, r)}>{r.emoji} <span>{r.count}</span></button>
94
+ ))}
95
+ <button type="button" aria-label="Add reaction" onClick={() => onReact?.(message, null)}><SmileIcon /></button>
96
+ </div>
97
+ )}
98
+ {message.replies && (
99
+ <button data-scope="thread-replies" type="button" onClick={() => onReplies?.(message)}>
100
+ <span data-scope="avatar-stack">{(message.replies.people ?? []).map(p => <PeerAvatar key={p.id} peer={p} />)}</span>
101
+ {message.replies.count} {message.replies.count === 1 ? 'reply' : 'replies'}{message.replies.last ? ` · last ${message.replies.last}` : ''}
102
+ </button>
103
+ )}
104
+ {delivery.value === 'failed' && <span data-part="status"><AlertIcon /> Not sent. <Button emphasis="link" tone="critical" onClick={() => onRetry?.(message)}>Retry</Button></span>}
105
+ {delivery.value === 'sending' && <span data-part="status"><ClockIcon /> Sending…</span>}
106
+ {delivery.value === 'sent' && <span data-part="status"><CheckIcon /> Sent {message.time}</span>}
107
+ {delivery.value === 'read' && <span data-part="status"><ChecksIcon /> Read {message.readAt ?? message.time}</span>}
108
+ </div>
109
+ </div>
110
+ )
111
+ }
112
+
113
+ type MessageRow = Extract<ThreadRow, { kind: 'message' }>
114
+ /** One row of the window: a message, and the divider above it if it has one. */
115
+ type Entry = { id: string; row: MessageRow; divider?: Exclude<ThreadRow, MessageRow> | undefined }
116
+
117
+ function entries(rows: readonly ThreadRow[]) {
118
+ const out: Entry[] = []
119
+ let divider: Entry['divider']
120
+ for (const r of rows) {
121
+ if (r.kind !== 'message') { divider = r; continue }
122
+ out.push({ id: r.id, row: r, divider })
123
+ divider = undefined
124
+ }
125
+ return out
126
+ }
127
+
128
+ const plain = (m: ChatMessage) => !m.quote && !m.attachment && !m.reactions?.length && !m.replies && !m.mentions?.length && !m.streaming
129
+ /** Rows that look the same around their text share one measurement. Anything richer is measured when it renders. */
130
+ function shape({ row: { message: m, continued, last }, divider }: Entry) {
131
+ if (!plain(m)) return 'rich'
132
+ const status = !m.mine ? '' : m.state === 'failed' || m.state === 'sending' ? m.state : last ? 'last' : ''
133
+ return `${m.mine ? 'm' : continued ? 'c' : 'h'}${divider?.unread ? 'u' : divider ? 'd' : ''}${status}`
134
+ }
135
+
136
+ function Divider({ d }: { d: NonNullable<Entry['divider']> }) {
137
+ return <div data-part="divider" data-unread={d.unread ? '' : undefined} role="separator" aria-label={d.unread ? 'New messages' : d.label}>{d.label}</div>
138
+ }
139
+
140
+ /** Props for Thread. */
141
+ export interface ThreadProps {
142
+ /** Names the region: "calendar-engine channel". */
143
+ label: string
144
+ messages: Live<ChatMessage[]>
145
+ /** The first unread message: opening lands on the divider before it. */
146
+ unreadFrom?: string | null
147
+ /** Names typing now. */
148
+ typing?: Live<string[]>
149
+ /** The header's content (`data-part="app-bar"`): channel, members, stack, call, search. */
150
+ header?: unknown
151
+ /** A log height, if the parent doesn't size the thread. */
152
+ height?: string
153
+ /** No `onSend`, no composer: a read-only log. */
154
+ onSend?: ((m: ComposerSend) => void) | undefined
155
+ onRetry?: ((m: ChatMessage) => void) | undefined
156
+ onReact?: (m: ChatMessage, r: MessageReaction | null) => void
157
+ onReplies?: (m: ChatMessage) => void
158
+ /** Near the top: load older messages, and prepend them to `messages`. */
159
+ onOlder?: () => void
160
+ onDrift?: ((index: number, predicted: number, actual: number) => void) | undefined
161
+ control?: ((c: VirtualControl) => void) | undefined
162
+ composer?: Omit<ComposerProps, 'onSend' | 'quote' | 'input'>
163
+ }
164
+
165
+ /**
166
+ * A conversation (07 §5, §5.5): the log, a window of rows, so 100,000 messages cost what 20 do (role=log; day and unread
167
+ * dividers, grouping, stick to bottom, Jump to latest with the new count, roving messages and Q), typing line, composer
168
+ * with the quote draft. New messages are announced as one summary after 1 s of quiet, a failed send assertively once.
169
+ * Plain messages have exact heights from `text.measure`; rows are keyed, so live updates patch in place.
170
+ */
171
+ export function Thread({ label, messages, unreadFrom, typing, header, height, onSend, onRetry, onReact, onReplies, onOlder, onDrift, control, composer }: ThreadProps) {
172
+ const id = uid('thread')
173
+ const list = derived(() => read(messages))
174
+ const rows = derived(() => entries(threadRows(list.value, unreadFrom)))
175
+ const ids = derived(() => list.value.map(m => m.id))
176
+ const mark = arrivalTracker()
177
+ const fresh = derived(() => mark(ids.value))
178
+ const stuck = state(true)
179
+ const unseen = state(0)
180
+ const draft = state<QuoteDraftValue | null>(null)
181
+ const typingNames = derived(() => read(typing) ?? [])
182
+ const head = derived(() => header)
183
+ let root: HTMLElement | undefined
184
+ let box: HTMLTextAreaElement | undefined
185
+ let win: VirtualControl | undefined
186
+ const doc = () => root!.ownerDocument
187
+ const find = (mid?: string) => (mid ? list.value.find(m => m.id === mid) : undefined)
188
+
189
+ onMount(() => {
190
+ const told = burst<string>(names => {
191
+ const who = [...new Set(names)]
192
+ announce(doc(), names.length === 1 ? `New message from ${who[0]}` : who.length === 1 ? `${names.length} new messages from ${who[0]}` : `${names.length} new messages from ${joinNames(who)}`)
193
+ })
194
+ const failed = new Set(untrack(() => list.value.filter(m => m.state === 'failed').map(m => m.id)))
195
+ let seen = new Set(untrack(() => ids.value))
196
+ onCleanup(told.cancel)
197
+ watch(() => list.value, l => {
198
+ for (const m of l) {
199
+ if (!seen.has(m.id) && !m.mine) told.push(firstName(m.author.name))
200
+ if (m.state === 'failed' && !failed.has(m.id)) { failed.add(m.id); announce(doc(), 'Message not sent', true) }
201
+ if (m.state !== 'failed') failed.delete(m.id)
202
+ }
203
+ const last = l.at(-1)
204
+ if (last && !seen.has(last.id)) { // a prepend of older messages isn't an arrival
205
+ const known = l.findIndex(m => seen.has(m.id))
206
+ const arrived = l.slice(l.findLastIndex(m => seen.has(m.id)) + 1)
207
+ if (arrived.some(m => m.mine)) queueMicrotask(() => win?.scrollToEnd())
208
+ else if (known >= 0 && !stuck.value) unseen.value += arrived.length
209
+ }
210
+ seen = new Set(l.map(m => m.id))
211
+ })
212
+ })
213
+ use(messageLog, () => root?.querySelector<HTMLElement>('[data-part="log"]') ?? undefined, () => ({
214
+ onQuote(mid: string, selection: string) {
215
+ const m = find(mid)
216
+ if (!m) return
217
+ const author = m.mine ? 'You' : m.author.name
218
+ draft.value = { ref: m.id, author, time: m.time, text: quoteText(m, selection) }
219
+ box?.focus()
220
+ announce(doc(), `Quoting ${author}. Escape removes the quote.`)
221
+ },
222
+ ids: () => untrack(() => ids.value),
223
+ reveal: (mid: string) => win?.focus(mid),
224
+ }))
225
+ const latest = () => {
226
+ win?.scrollToEnd()
227
+ const last = ids.value.at(-1)
228
+ if (last !== undefined) win?.focus(last)
229
+ }
230
+ const take = (c: VirtualControl) => {
231
+ win = c
232
+ control?.(c)
233
+ const open = untrack(() => unreadFrom)
234
+ if (open) queueMicrotask(() => c.scrollTo(open))
235
+ }
236
+ const row = (e: () => Entry) => {
237
+ const source = derived(() => find(e().row.message.quote?.ref))
238
+ return <>
239
+ {e().divider && <Divider d={e().divider!} />}
240
+ <Message message={e().row.message} continued={e().row.continued} last={e().row.last} arrive={fresh.value.includes(e().id)} source={source.value} onRetry={m => onRetry?.(m)} onReact={(m, r) => onReact?.(m, r)} onReplies={m => onReplies?.(m)} />
241
+ </>
242
+ }
243
+ return (
244
+ <section ref={n => (root = n)} data-scope="thread" data-part="root" aria-label={label} id={id} style={height ? `block-size:${height}` : undefined}>
245
+ {head.value !== undefined ? <div data-part="app-bar">{head.value}</div> : <span></span>}
246
+ <VirtualList
247
+ part="log"
248
+ role="log"
249
+ bareRows
250
+ open
251
+ tabindex={-1}
252
+ label="Messages"
253
+ items={rows.value}
254
+ rowKey={e => e.id}
255
+ text={e => (plain(e.row.message) ? e.row.message.text : null)}
256
+ variant={shape}
257
+ estimate={64}
258
+ gap={2}
259
+ stick={STICK_PX}
260
+ from={untrack(() => (unreadFrom ? 'start' : 'end'))}
261
+ onStart={onOlder}
262
+ onStick={s => { stuck.value = s; if (s) unseen.value = 0 }}
263
+ onDrift={onDrift}
264
+ control={take}
265
+ >{row}</VirtualList>
266
+ {!stuck.value && <Button size="sm" data-part="jump" onClick={latest}>Jump to latest{unseen.value > 0 ? ` · ${unseen.value} new` : ''}</Button>}
267
+ <div>
268
+ <TypingIndicator text={typingLine(typingNames.value)} />
269
+ {onSend && <Composer {...(composer ?? { label: `Message ${label}` })} quote={draft} onSend={m => onSend(m)} input={el => (box = el)} />}
270
+ </div>
271
+ </section>
272
+ )
273
+ }
@@ -0,0 +1,49 @@
1
+ import { announce } from '@weft-js/warp-core'
2
+ import { derived, state } from '@weft-js/core'
3
+ import { Button } from '../Button'
4
+ import { Modal } from '../Modal'
5
+ import { read, type Live } from './peer'
6
+ import '../styles'
7
+ import '@weft-js/warp-core/styles/app/tokens.css'
8
+ import '@weft-js/warp-core/styles/app/versions.css'
9
+
10
+ /** One saved version of a document. */
11
+ export interface Version { id: string; title: string; /** "Mei · 10:02" */ meta: string; named?: boolean | undefined; current?: boolean | undefined }
12
+
13
+ /**
14
+ * Version history (07 §6.2): named versions carry the accent dot. A version previews read-only (`preview` draws it,
15
+ * with its diff tint), then Restore asks in a dialog; restoring makes a new version, so `onRestore` loses nothing.
16
+ */
17
+ export function VersionHistory({ versions, preview, onPreview, onRestore, label = 'Version history' }: { versions: Live<Version[]>; preview?: (v: Version) => unknown; onPreview?: (v: Version) => void; onRestore: (v: Version) => void; label?: string | undefined }) {
18
+ const list = derived(() => read(versions))
19
+ const shown = state<Version | null>(null)
20
+ const asking = state<Version | null>(null)
21
+ const viewOpen = state(false)
22
+ const askOpen = state(false)
23
+ let root: HTMLElement | undefined
24
+ const look = (v: Version) => { if (v.current) return; shown.value = v; viewOpen.value = true; onPreview?.(v) }
25
+ const ask = (v: Version) => { asking.value = v; viewOpen.value = false; askOpen.value = true }
26
+ const restore = () => {
27
+ const v = asking.value!
28
+ askOpen.value = false
29
+ onRestore(v)
30
+ if (root) announce(root.ownerDocument, `Restored ${v.title} as a new version`)
31
+ }
32
+ return (
33
+ <>
34
+ <ul ref={n => (root = n)} data-scope="versions" aria-label={label}>
35
+ {list.value.map(v => (
36
+ <li key={v.id} aria-current={v.current ? 'true' : undefined} data-named={v.named ? '' : undefined} onClick={() => look(v)}>
37
+ <i></i>
38
+ <span>{v.title}<small>{v.meta}</small></span>
39
+ {!v.current && <span><Button emphasis="ghost" tone="neutral" size="sm" aria-label={`Preview ${v.title}`} onClick={(e: MouseEvent) => { e.stopPropagation(); look(v) }}>Preview</Button><Button emphasis="ghost" tone="neutral" size="sm" aria-label={`Restore ${v.title}`} onClick={(e: MouseEvent) => { e.stopPropagation(); ask(v) }}>Restore</Button></span>}
40
+ </li>
41
+ ))}
42
+ </ul>
43
+ <Modal open={viewOpen} title={`Preview · ${shown.value?.title ?? ''}`} size="lg" footer={<Button onClick={() => ask(shown.value!)}>Restore</Button>}>
44
+ {shown.value && preview ? <div inert>{preview(shown.value)}</div> : <p>{shown.value?.meta}</p>}
45
+ </Modal>
46
+ <Modal open={askOpen} alert title={`Restore “${asking.value?.title ?? ''}”?`} description="Restoring creates a new version. Nothing is lost." footer={<><Button emphasis="outline" tone="neutral" autofocus onClick={() => (askOpen.value = false)}>Cancel</Button><Button onClick={restore}>Restore</Button></>} />
47
+ </>
48
+ )
49
+ }
@@ -0,0 +1,49 @@
1
+ import { firstName, joinNames, peerColor, type Peer } from '@weft-js/warp-core'
2
+ import { derived } from '@weft-js/core'
3
+ import { PeerAvatar, read, type Live } from './peer'
4
+ import '../styles'
5
+ import '@weft-js/warp-core/styles/app/tokens.css'
6
+ import '@weft-js/warp-core/styles/app/base.css'
7
+ import '@weft-js/warp-core/styles/app/avatar.css'
8
+ import '@weft-js/warp-core/styles/app/viewport-rail.css'
9
+ import '@weft-js/warp-core/styles/app/section-peers.css'
10
+
11
+ /** A peer's place in the document. */
12
+ export type ViewportPeer = Peer & { /** Viewport top and height, 0–1 of the document. */ top: number; height: number }
13
+
14
+ /**
15
+ * LiveViewport (07 §6.1): a rail of the document with a box per viewport in the peer's colour. A picture, unless
16
+ * `onJump` makes each box a button.
17
+ */
18
+ export function ViewportRail({ peers, self, onJump, label = 'Where people are looking' }: { peers: Live<ViewportPeer[]>; self?: { top: number; height: number }; onJump?: (p: ViewportPeer) => void; label?: string | undefined }) {
19
+ const list = derived(() => read(peers))
20
+ const canJump = derived(() => onJump !== undefined)
21
+ return (
22
+ <div data-scope="viewport-rail" role={canJump.value ? 'group' : 'img'} aria-label={label}>
23
+ {self && <i data-self="" style={`--_t:${self.top};--_h:${self.height}`}></i>}
24
+ {list.value.map(p => (
25
+ <i
26
+ key={p.id}
27
+ data-presence-color={peerColor(p)}
28
+ style={`--_t:${p.top};--_h:${p.height}`}
29
+ role={canJump.value ? 'button' : undefined}
30
+ tabindex={canJump.value ? 0 : undefined}
31
+ aria-label={canJump.value ? `Jump to ${p.name}` : undefined}
32
+ onClick={() => onJump?.(p)}
33
+ onKeyDown={(e: KeyboardEvent) => { if (canJump.value && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); onJump?.(p) } }}
34
+ ></i>
35
+ ))}
36
+ </div>
37
+ )
38
+ }
39
+
40
+ /** LiveSection (07 §6.1): who is reading this section, beside its heading. */
41
+ export function SectionPeers({ peers }: { peers: Live<Peer[]> }) {
42
+ const list = derived(() => read(peers))
43
+ const names = derived(() => list.value.map(p => firstName(p.name)))
44
+ return (
45
+ <span data-scope="section-peers" role="img" aria-label={`${joinNames(names.value)} ${names.value.length === 1 ? 'is' : 'are'} here`}>
46
+ {list.value.map(p => <PeerAvatar key={p.id} peer={p} />)}
47
+ </span>
48
+ )
49
+ }
@@ -0,0 +1,16 @@
1
+ // The collab pack: see docs/design/warp. One file per component family; each imports only its own CSS.
2
+ export { AutoTrack } from './AutoTrack'
3
+ export { CommentAnchor, CommentRail, CommentThread, type Comment, type CommentThreadData } from './Comments'
4
+ export { Composer, TypingIndicator, type ComposerFile, type ComposerProps, type ComposerSend, type Mentionable, type QuoteDraftValue } from './Composer'
5
+ export { ConversationList, type ChatConversation } from './ConversationList'
6
+ export { Cursors, PeerMark, PeerSelection } from './Cursors'
7
+ export { DataIntegrity, type IntegrityEvent } from './DataIntegrity'
8
+ export { Kanban, type KanbanCardData, type KanbanColumn } from './Kanban'
9
+ export { LockedField, LockIndicator } from './Lock'
10
+ export { Following, PresenceStack } from './PresenceStack'
11
+ export { ShareDialog, type ShareContact, type ShareDialogProps, type ShareLink, type ShareMember, type ShareRole } from './ShareDialog'
12
+ export { ConflictAlert, SyncIndicator, type SyncState } from './Status'
13
+ export { SyncedForm } from './SyncedForm'
14
+ export { Message, Thread, type MessageProps, type ThreadProps } from './Thread'
15
+ export { SectionPeers, ViewportRail, type ViewportPeer } from './ViewportRail'
16
+ export { VersionHistory, type Version } from './VersionHistory'
@@ -0,0 +1,16 @@
1
+ // What the collab pack shares: a peer's avatar in their presence colour, and the props that may be a State or a value.
2
+ import { peerColor, type Peer } from '@weft-js/warp-core'
3
+ import { Box, type State } from '@weft-js/core'
4
+ import { Avatar } from '../parts'
5
+
6
+ export type Live<T> = State<T> | T
7
+ export const read = <T,>(v: Live<T>): T => (v instanceof Box ? v.value : (v as T))
8
+
9
+ export const tint = (n: number) => `--_bg:color-mix(in oklab,var(--warp-color-data-${n}) 26%,var(--warp-color-surface-raised))`
10
+
11
+ /** A peer's Avatar: the tint and, with `ring`, the presence colour ring. */
12
+ export function PeerAvatar({ peer, size, ring, ...rest }: { peer: Peer; size?: 'sm' | 'md' | 'lg' | undefined; ring?: boolean; [attr: string]: unknown }) {
13
+ return <Avatar name={peer.name} size={size} presence={ring ? peerColor(peer) : undefined} data-presence-color={peerColor(peer)} style={tint(peerColor(peer))} {...rest} />
14
+ }
15
+
16
+ export const SWEEP = () => <><span data-part="sweep-base" aria-hidden="true"></span><span data-part="sweep" aria-hidden="true"></span></>
package/src/fields.ts ADDED
@@ -0,0 +1,5 @@
1
+ // The input family's shared CSS: its plumbing and the field shell (warp-inputs.css §0–1).
2
+ import './styles'
3
+ import '@weft-js/warp-core/styles/inputs/base.css'
4
+ import '@weft-js/warp-core/styles/inputs/field.css'
5
+ import '@weft-js/warp-core/styles/inputs/keyframes.css'
@@ -0,0 +1,40 @@
1
+ import { WeaveIcon } from '@weft-js/warp-icons'
2
+ import '../styles'
3
+ import '@weft-js/warp-core/styles/flows/tokens.css'
4
+ import '@weft-js/warp-core/styles/flows/base.css'
5
+ import '@weft-js/warp-core/styles/flows/auth.css'
6
+
7
+ const THREADS = Array.from({ length: 24 }, (_, i) => ({ x: 12 + i * 26 }))
8
+ const WEFT = [[180, 0.15, 0.75], [300, 0.35, 0.95], [420, 0.05, 0.55], [560, 0.25, 0.85]].map(([y, a, b], i) => ({ i, d: `M${a! * 640} ${y} H${b! * 640}` }))
9
+
10
+ /** The woven lines of the brand panel: warp threads, and weft that draws once. */
11
+ const WeaveArt = () => (
12
+ <svg data-part="weave" viewBox="0 0 640 800" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
13
+ {THREADS.map(t => <line key={t.x} x1={t.x} y1="0" x2={t.x} y2="800" />)}
14
+ {WEFT.map(w => <path key={w.i} pathLength="1" style={`--i:${w.i}`} d={w.d} />)}
15
+ </svg>
16
+ )
17
+
18
+
19
+ /** The product's mark: a logo of the app's, or warp's woven square. */
20
+ export const AuthLogo = ({ logo, name }: { logo?: unknown; name?: unknown }) => <span data-part="logo">{logo ?? <WeaveIcon />}{name}</span>
21
+
22
+ /**
23
+ * The auth layout (09 §2.2), by its own width: full-bleed under 600, a 420 card on the page at 600–999, split with the
24
+ * brand panel from 1000. The panel is decorative, so it is hidden from assistive tech; quiet flattens it by token.
25
+ */
26
+ export function AuthScreen({ logo, name, quote, cite, brand, children, ...rest }: { logo?: unknown; name?: unknown; quote?: unknown; cite?: unknown; brand?: unknown; children?: unknown; [attr: string]: unknown }) {
27
+ return (
28
+ <div data-scope="auth" {...rest}>
29
+ <div data-part="layout">
30
+ <aside data-part="brand" aria-hidden="true">
31
+ <AuthLogo logo={logo} name={name} />
32
+ <WeaveArt />
33
+ {brand}
34
+ {quote !== undefined && <blockquote>{quote}{cite !== undefined && <cite>{cite}</cite>}</blockquote>}
35
+ </aside>
36
+ <div data-part="main"><div data-part="panel">{children}</div></div>
37
+ </div>
38
+ </div>
39
+ )
40
+ }