@estiva-app/ui 0.72.0-widgets2.1 → 0.72.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 (49) hide show
  1. package/dist/CommentHighlights.d.ts +100 -0
  2. package/dist/CommentHighlights.d.ts.map +1 -0
  3. package/dist/ComposeBox.d.ts +55 -0
  4. package/dist/ComposeBox.d.ts.map +1 -0
  5. package/dist/ComposerTriggers.d.ts.map +1 -1
  6. package/dist/SelectionToolbar.d.ts +17 -1
  7. package/dist/SelectionToolbar.d.ts.map +1 -1
  8. package/dist/editor.d.ts +3 -1
  9. package/dist/editor.d.ts.map +1 -1
  10. package/dist/editor.js +618 -217
  11. package/dist/editor.js.map +4 -4
  12. package/dist/index.js.map +1 -1
  13. package/dist/looks.d.ts +6 -0
  14. package/dist/looks.d.ts.map +1 -1
  15. package/package.json +1 -5
  16. package/registry.json +309 -3
  17. package/src/CommentHighlights.mdx +61 -0
  18. package/src/CommentHighlights.stories.tsx +72 -0
  19. package/src/CommentHighlights.test.ts +100 -0
  20. package/src/CommentHighlights.ts +413 -0
  21. package/src/ComposeBox.mdx +58 -0
  22. package/src/ComposeBox.stories.tsx +81 -0
  23. package/src/ComposeBox.tsx +183 -0
  24. package/src/ComposerTriggers.tsx +9 -3
  25. package/src/SelectionToolbar.actions.test.tsx +44 -0
  26. package/src/SelectionToolbar.mdx +1 -0
  27. package/src/SelectionToolbar.tsx +36 -2
  28. package/src/editor.ts +15 -1
  29. package/src/looks.ts +8 -0
  30. package/src/registry/app.test.ts +7 -1
  31. package/src/registry/registry.test.ts +6 -6
  32. package/stories/Choosing.mdx +2 -0
  33. package/dist/widgets-2/fixtures.d.ts +0 -102
  34. package/dist/widgets-2/fixtures.d.ts.map +0 -1
  35. package/dist/widgets-2/places.d.ts +0 -99
  36. package/dist/widgets-2/places.d.ts.map +0 -1
  37. package/dist/widgets-2/proposed.d.ts +0 -130
  38. package/dist/widgets-2/proposed.d.ts.map +0 -1
  39. package/dist/widgets-2.d.ts +0 -12
  40. package/dist/widgets-2.d.ts.map +0 -1
  41. package/dist/widgets-2.js +0 -584
  42. package/dist/widgets-2.js.map +0 -7
  43. package/src/widgets-2/fixtures.ts +0 -161
  44. package/src/widgets-2/places.tsx +0 -309
  45. package/src/widgets-2/proposed.tsx +0 -596
  46. package/src/widgets-2.ts +0 -11
  47. package/stories/widgets-2/ReadMe.mdx +0 -60
  48. package/stories/widgets-2/Widgets.stories.tsx +0 -504
  49. package/stories/widgets-2/today.tsx +0 -352
@@ -1,352 +0,0 @@
1
- /**
2
- * Widgets 2 prototype: how Peek, Ship and Leaf draw a link's widget today,
3
- * redrawn as static markup, class for class, from each app's `origin/main`
4
- * (15 Oct: peek-app 92e0cb9, estiva-ship df0136c, leaf 70a5627). Nothing here
5
- * fetches; every field is a prop. These exist only so the proposed kit can be
6
- * judged beside them, and go away with the prototype.
7
- */
8
- import { IconCircle, IconCircleCheck, IconCircleDashed, IconCircleHalf2, IconCircleX, IconExternalLink, IconMessage2 } from '@tabler/icons-react'
9
- import { AttachmentCard } from '../../src/AttachmentCard'
10
- import { Avatar } from '../../src/Avatar'
11
- import { Button } from '../../src/Button'
12
- import { Card } from '../../src/Card'
13
- import { Chip } from '../../src/Chip'
14
- import { cn } from '../../src/cn'
15
- import { Divider } from '../../src/Divider'
16
- import { InlineChip } from '../../src/InlineChip'
17
- import { KindIcon, type ThingKind } from '../../src/KindIcon'
18
- import { Link } from '../../src/Link'
19
- import { Person } from '../../src/Person'
20
- import { SectionLabel } from '../../src/SectionLabel'
21
- import { Select } from '../../src/Select'
22
- import { SkeletonBar } from '../../src/Skeleton'
23
- import { PART, PICTURE, type Status, type StatusColour, type Thing } from '../../src/widgets-2/fixtures'
24
-
25
- const HREF = '#'
26
- const noop = () => {}
27
-
28
- /** KindIcon has no page; every app draws a Leaf page as another app's file today. */
29
- export const iconKind = (kind: Thing['kind']): ThingKind => (kind === 'page' ? 'file' : kind)
30
-
31
- /* ------------------------------------------------------------------ Peek */
32
-
33
- const PEEK_STATUS: Record<StatusColour, string> = {
34
- neutral: 'text-text-secondary',
35
- blue: 'text-info-default',
36
- green: 'text-success-default',
37
- muted: 'text-text-muted',
38
- }
39
-
40
- /** Peek's `Reference`: a chip with the kind's icon, cut at 24ch, its status in coloured words. */
41
- export function PeekChip({ thing }: { thing: Thing }) {
42
- if (thing.kind === 'message') {
43
- return (
44
- <InlineChip icon={<KindIcon kind="message" size={14} tone="inherit" />} href={HREF} style={{ maxWidth: '24ch' }}>
45
- <span className="truncate">
46
- {thing.author}: {thing.body}
47
- </span>
48
- </InlineChip>
49
- )
50
- }
51
- return (
52
- <InlineChip icon={<KindIcon kind={iconKind(thing.kind)} size={14} tone="inherit" />} href={HREF} style={{ maxWidth: '24ch' }}>
53
- <span className="truncate">{thing.title}</span>
54
- {thing.status && <span className={cn('shrink-0 text-caption font-medium', PEEK_STATUS[thing.status.colour])}>{thing.status.label}</span>}
55
- </InlineChip>
56
- )
57
- }
58
-
59
- /** Peek's chip before the lookup answers, and when it never does: the address's first 12 characters. */
60
- export function PeekChipQuiet({ address }: { address: string }) {
61
- return <InlineChip tone="quiet">{address.slice(0, 12)}…</InlineChip>
62
- }
63
-
64
- /**
65
- * Peek's `ForeignObjectCard`. `row` (compact) drops the subtitle and the body
66
- * but keeps the meta line and the actions, which is why an issue's row and
67
- * card read alike.
68
- */
69
- export function PeekCard({ thing, layout }: { thing: Thing; layout: 'card' | 'row' }) {
70
- const whole = layout === 'card'
71
- const actions = thing.kind === 'issue'
72
- return (
73
- <Card fill="inset" className={cn('mt-1', whole ? 'p-3' : 'px-3 py-2')}>
74
- <div className="flex items-baseline gap-2">
75
- <div className="flex min-w-0 flex-1 items-center gap-2">
76
- {thing.kind === 'message' ? (
77
- <Person name={thing.author} size={16} />
78
- ) : (
79
- <span className="contents text-body-2 font-semibold text-text-primary">
80
- <Link href={HREF} variant="quiet" external truncate>
81
- {thing.title}
82
- </Link>
83
- </span>
84
- )}
85
- {thing.status && !actions && <span className={cn('inline-flex items-center text-caption font-medium', PEEK_STATUS[thing.status.colour])}>{thing.status.label}</span>}
86
- </div>
87
- {thing.kind !== 'message' && <SectionLabel className="shrink-0">{thing.kind === 'page' ? thing.app : thing.noun}</SectionLabel>}
88
- </div>
89
- {whole && thing.subtitle && <p className="mt-1 text-caption text-text-secondary">{thing.subtitle}</p>}
90
- {whole && thing.body && <p className="mt-1 line-clamp-4 text-body-2 text-text-primary">{thing.body}</p>}
91
- {(thing.assignee || thing.progress) && !actions && (
92
- <div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5">
93
- {thing.assignee && (
94
- <span className="contents text-caption text-text-secondary">
95
- <Person name={thing.assignee} size={16} />
96
- </span>
97
- )}
98
- {thing.progress && <span className="text-caption text-text-secondary">Progress: {thing.progress}</span>}
99
- </div>
100
- )}
101
- {actions && thing.status && (
102
- <div className="mt-2">
103
- <Divider className="mx-0" />
104
- <div className="flex flex-wrap items-center gap-2 pt-2">
105
- <Select size="small" className="w-auto" value="s" onChange={noop} ariaLabel="Status" options={[{ value: 's', label: thing.status.label }]} />
106
- <Select
107
- size="small"
108
- className="w-auto"
109
- value="a"
110
- onChange={noop}
111
- ariaLabel="Assignee"
112
- options={[{ value: 'a', label: thing.assignee ?? 'Unassigned', leading: <Avatar name={thing.assignee} size={16} /> }]}
113
- />
114
- </div>
115
- </div>
116
- )}
117
- </Card>
118
- )
119
- }
120
-
121
- /** The height Peek holds a chosen card at while it loads (measured: row 97, topic card 46, others 229). */
122
- const PEEK_HELD = { row: 97, card: { topic: 46, other: 229 } }
123
-
124
- /** Peek's `ForeignObjectSkeleton`. */
125
- export function PeekSkeleton({ thing, layout }: { thing: Thing; layout: 'card' | 'row' }) {
126
- const height = layout === 'row' ? PEEK_HELD.row : thing.kind === 'topic' ? PEEK_HELD.card.topic : PEEK_HELD.card.other
127
- return (
128
- <div aria-hidden style={{ height }} className={cn('mt-1 overflow-hidden rounded-lg border border-border-subtle bg-bg-inset animate-skeleton-in', layout === 'row' ? 'px-3 py-2' : 'p-3')}>
129
- <SkeletonBar className="h-3.5 w-1/2" />
130
- <div className="mt-2 flex items-center gap-2">
131
- <SkeletonBar className="h-3 w-16" />
132
- <SkeletonBar className="h-3 w-24" />
133
- </div>
134
- </div>
135
- )
136
- }
137
-
138
- /** Peek's `NotAvailableRow`, the same in row and card. */
139
- export function PeekNotAvailable({ noun, app }: { noun: string; app: string }) {
140
- return (
141
- <Card fill="none" unreadable className="mt-1 px-3 py-2">
142
- <div className="text-caption text-text-secondary">
143
- {noun} — not available.{' '}
144
- <Link href={HREF} variant="underlined" external>
145
- Open in {app} ↗
146
- </Link>
147
- </div>
148
- </Card>
149
- )
150
- }
151
-
152
- /** Peek's and Ship's `PartCard`, resolved: the part drawn live, under "{Noun} · {title}". */
153
- export function TodayPartCard({ app }: { app: 'Peek' | 'Ship' }) {
154
- return (
155
- <Card fill="inset" className={cn('flex flex-col gap-3 p-3', app === 'Peek' && 'mt-1')}>
156
- <span className="contents text-h5 text-text-primary">
157
- <Link href={HREF} variant="quiet" external truncate>
158
- {PART.noun} · {PART.heading}
159
- </Link>
160
- </span>
161
- <PartBlocks />
162
- <div>
163
- <Button size="small" leadingIcon={<IconExternalLink stroke={1.5} className="size-3.5" />}>
164
- {app === 'Ship' ? 'Open' : `Open in ${PART.app}`}
165
- </Button>
166
- </div>
167
- </Card>
168
- )
169
- }
170
-
171
- /** The part's blocks, as a small rich text draws them. */
172
- export function PartBlocks() {
173
- const [heading, paragraph, ...points] = PART.blocks
174
- return (
175
- <div className="flex flex-col gap-1 text-body-2 text-text-primary">
176
- <p className="text-body-2-strong">{heading}</p>
177
- <p>{paragraph}</p>
178
- <ul className="list-disc pl-5">
179
- {points.map((point) => (
180
- <li key={point}>{point}</li>
181
- ))}
182
- </ul>
183
- </div>
184
- )
185
- }
186
-
187
- /** Peek's `PartCard` when the reader cannot read the part. */
188
- export function PeekPartNotAvailable() {
189
- return (
190
- <Card fill="inset" unreadable className="mt-1 px-3 py-2">
191
- <span className="text-caption italic text-text-muted">Not available to you</span>
192
- </Card>
193
- )
194
- }
195
-
196
- /** Peek's `CommentAnchor` and Ship's `BlockAnchorNote`: the same classes. */
197
- export function TodayCommentAnchor({ text }: { text?: string }) {
198
- if (text === undefined) {
199
- return <div className="border-l-2 border-border-default pl-2 text-caption text-text-muted">The part this refers to has since been deleted.</div>
200
- }
201
- return (
202
- <div className="border-l-2 border-border-strong pl-2 text-caption text-text-muted">
203
- <span className="line-clamp-2 italic">{text}</span>
204
- </div>
205
- )
206
- }
207
-
208
- /* ------------------------------------------------------------------ Ship */
209
-
210
- const SHIP_STATUS_ICON = {
211
- neutral: { Icon: IconCircleDashed, tone: 'text-text-secondary' },
212
- blue: { Icon: IconCircleHalf2, tone: 'text-info-default' },
213
- green: { Icon: IconCircleCheck, tone: 'text-success-default' },
214
- muted: { Icon: IconCircleX, tone: 'text-text-muted' },
215
- } as const
216
-
217
- /** Ship's `StatusLabel` at `small`: the status's shape icon and its words. */
218
- export function ShipStatus({ status, words = true }: { status: Status; words?: boolean }) {
219
- const { Icon, tone } = SHIP_STATUS_ICON[status.colour] ?? { Icon: IconCircle, tone: 'text-text-muted' }
220
- return (
221
- <span className="inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap text-caption text-text-secondary">
222
- <Icon size={14} stroke={1.5} className={tone} aria-hidden />
223
- {words && status.label}
224
- </span>
225
- )
226
- }
227
-
228
- /** Ship's `Reference`: Peek's chip shape, with the status as an icon and words. */
229
- export function ShipChip({ thing }: { thing: Thing }) {
230
- return (
231
- <InlineChip icon={<KindIcon kind={iconKind(thing.kind)} size={14} tone="inherit" />} href={HREF} style={{ maxWidth: '24ch' }}>
232
- <span className="truncate">{thing.kind === 'message' ? `${thing.author}: ${thing.body}` : thing.title}</span>
233
- {thing.status && <ShipStatus status={thing.status} />}
234
- </InlineChip>
235
- )
236
- }
237
-
238
- /** Ship's `ForeignObject`. Row and card differ only by `py-2`; neither has a body or actions. */
239
- export function ShipCard({ thing, layout }: { thing: Thing; layout: 'card' | 'row' }) {
240
- return (
241
- <Card href={HREF} fill="surface" className={cn('flex flex-col gap-2 p-3', layout === 'row' && 'py-2')}>
242
- <div className="flex items-start gap-2">
243
- <span className="min-w-0 flex-1 truncate text-body-2-strong text-text-primary">{thing.title}</span>
244
- <span className="flex shrink-0 items-center gap-2">
245
- {thing.comments !== undefined && (
246
- <span className="flex shrink-0 items-center gap-1 text-caption tabular-nums text-text-muted">
247
- <IconMessage2 size={14} stroke={1.5} aria-hidden />
248
- {thing.comments}
249
- </span>
250
- )}
251
- <Chip label={thing.kind === 'page' ? thing.app : thing.noun} />
252
- </span>
253
- </div>
254
- {thing.subtitle && <p className="line-clamp-2 text-body-2 text-text-secondary">{thing.subtitle}</p>}
255
- {(thing.status || thing.assignee) && (
256
- <div className="flex flex-wrap items-center gap-2.5 text-caption text-text-muted">
257
- {thing.status && <ShipStatus status={thing.status} />}
258
- {thing.assignee && (
259
- <span className="flex min-w-0 items-center gap-1.5">
260
- <span aria-hidden className="text-text-muted">
261
- ·
262
- </span>
263
- Assignee
264
- <Person name={thing.assignee} size={16} />
265
- </span>
266
- )}
267
- </div>
268
- )}
269
- {thing.children && (
270
- <div className="mt-1 flex flex-col divide-y divide-border-default">
271
- {thing.children.map((child) => (
272
- <div key={child.title} className="flex items-center gap-2 py-2">
273
- <span className="min-w-0 flex-1 truncate text-body-2-strong text-text-primary">{child.title}</span>
274
- <ShipStatus status={child.status} />
275
- </div>
276
- ))}
277
- </div>
278
- )}
279
- </Card>
280
- )
281
- }
282
-
283
- /** Ship's linked message: no frame, a face, the author, the words. It reads as a quote. */
284
- export function ShipQuote({ thing }: { thing: Thing }) {
285
- return (
286
- <div className="flex gap-2 py-1.5">
287
- <Avatar name={thing.author} size={24} />
288
- <div className="min-w-0 flex-1">
289
- <div className="flex items-center gap-2 text-caption text-text-muted">
290
- <span className="truncate text-text-secondary">{thing.author}</span>
291
- </div>
292
- <p className="text-body-2 text-text-primary">{thing.body}</p>
293
- </div>
294
- </div>
295
- )
296
- }
297
-
298
- /** Ship's loading placeholder: one bar, whatever is coming. */
299
- export function ShipSkeleton() {
300
- return <SkeletonBar className="h-16 w-full" />
301
- }
302
-
303
- /** Ship's unreachable card. */
304
- export function ShipNotAvailable({ noun }: { noun: string }) {
305
- return (
306
- <Card fill="surface" unreadable className="p-3">
307
- <div className="text-body-2 text-text-muted">
308
- You may not have access to this {noun.toLowerCase()}.{' '}
309
- <span className="text-text-secondary">
310
- <Link href={HREF} variant="underlined">
311
- Open it there
312
- </Link>
313
- </span>
314
- .
315
- </div>
316
- </Card>
317
- )
318
- }
319
-
320
- /** Ship's attachment: the package's card, as Ship passes it. */
321
- export function TodayAttachment({ name, size, contentType }: { name: string; size: number; contentType: string }) {
322
- return <AttachmentCard name={name} size={size} contentType={contentType} src={contentType.startsWith('image/') ? PICTURE : undefined} download />
323
- }
324
-
325
- /* ------------------------------------------------------------------ Leaf */
326
-
327
- /** Leaf's `ReferenceChip`: no status. */
328
- export function LeafChip({ thing }: { thing: Thing }) {
329
- return (
330
- <InlineChip icon={<KindIcon kind={iconKind(thing.kind)} size={14} tone="inherit" />} className="max-w-[24ch]">
331
- <span className="truncate">{thing.kind === 'message' ? thing.author : thing.title}</span>
332
- </InlineChip>
333
- )
334
- }
335
-
336
- /** Leaf's `ReferenceObject`: an icon and the title; detailed adds the noun under it. No status, body or app. */
337
- export function LeafCard({ thing, layout }: { thing: Thing; layout: 'compact' | 'detailed' }) {
338
- return (
339
- <Card hover="fill" className="flex items-center gap-2 px-3 py-2">
340
- <KindIcon kind={iconKind(thing.kind)} />
341
- <span className="flex min-w-0 flex-col">
342
- <span className="truncate text-body-2-strong text-text-primary">{thing.kind === 'message' ? thing.author : thing.title}</span>
343
- {layout === 'detailed' && <span className="truncate text-body-2 text-text-secondary">{thing.noun}</span>}
344
- </span>
345
- </Card>
346
- )
347
- }
348
-
349
- /** Leaf's loading placeholder. */
350
- export function LeafSkeleton() {
351
- return <SkeletonBar className="h-12 w-full" />
352
- }