@estiva-app/ui 0.74.1 → 0.75.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 (67) hide show
  1. package/dist/CardUnder.d.ts +10 -0
  2. package/dist/CardUnder.d.ts.map +1 -0
  3. package/dist/InlineChip.d.ts +2 -0
  4. package/dist/InlineChip.d.ts.map +1 -1
  5. package/dist/LinkCard.d.ts +17 -0
  6. package/dist/LinkCard.d.ts.map +1 -0
  7. package/dist/LinkChip.d.ts +8 -0
  8. package/dist/LinkChip.d.ts.map +1 -0
  9. package/dist/LinkParts.d.ts +45 -0
  10. package/dist/LinkParts.d.ts.map +1 -0
  11. package/dist/LinkPreview.d.ts +21 -0
  12. package/dist/LinkPreview.d.ts.map +1 -0
  13. package/dist/LinkRow.d.ts +24 -0
  14. package/dist/LinkRow.d.ts.map +1 -0
  15. package/dist/PartCard.d.ts +32 -0
  16. package/dist/PartCard.d.ts.map +1 -0
  17. package/dist/Skeleton.d.ts +12 -2
  18. package/dist/Skeleton.d.ts.map +1 -1
  19. package/dist/eslint/index.js +1 -0
  20. package/dist/eslint/index.js.map +2 -2
  21. package/dist/eslint/no-restyled-part.d.ts.map +1 -1
  22. package/dist/index.d.ts +8 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +480 -71
  25. package/dist/index.js.map +4 -4
  26. package/dist/linked.d.ts +168 -0
  27. package/dist/linked.d.ts.map +1 -0
  28. package/dist/linked.examples.d.ts +27 -0
  29. package/dist/linked.examples.d.ts.map +1 -0
  30. package/package.json +1 -1
  31. package/registry.json +784 -7
  32. package/src/CardUnder.mdx +53 -0
  33. package/src/CardUnder.stories.tsx +45 -0
  34. package/src/CardUnder.test.tsx +40 -0
  35. package/src/CardUnder.tsx +45 -0
  36. package/src/InlineChip.tsx +2 -0
  37. package/src/LinkCard.mdx +58 -0
  38. package/src/LinkCard.stories.tsx +68 -0
  39. package/src/LinkCard.test.tsx +103 -0
  40. package/src/LinkCard.tsx +209 -0
  41. package/src/LinkChip.mdx +51 -0
  42. package/src/LinkChip.stories.tsx +40 -0
  43. package/src/LinkChip.test.tsx +83 -0
  44. package/src/LinkChip.tsx +51 -0
  45. package/src/LinkParts.mdx +49 -0
  46. package/src/LinkParts.stories.tsx +61 -0
  47. package/src/LinkParts.tsx +147 -0
  48. package/src/LinkPreview.mdx +47 -0
  49. package/src/LinkPreview.stories.tsx +38 -0
  50. package/src/LinkPreview.tsx +55 -0
  51. package/src/LinkRow.mdx +47 -0
  52. package/src/LinkRow.stories.tsx +49 -0
  53. package/src/LinkRow.test.tsx +38 -0
  54. package/src/LinkRow.tsx +107 -0
  55. package/src/PartCard.mdx +43 -0
  56. package/src/PartCard.stories.tsx +35 -0
  57. package/src/PartCard.test.tsx +30 -0
  58. package/src/PartCard.tsx +59 -0
  59. package/src/Skeleton.tsx +7 -3
  60. package/src/eslint/no-native-title.test.ts +3 -2
  61. package/src/eslint/no-restyled-part.ts +1 -0
  62. package/src/index.ts +25 -0
  63. package/src/linked.examples.ts +107 -0
  64. package/src/linked.test.ts +58 -0
  65. package/src/linked.ts +217 -0
  66. package/src/registry/registry.test.ts +8 -8
  67. package/stories/Choosing.mdx +7 -0
package/src/linked.ts ADDED
@@ -0,0 +1,217 @@
1
+ /**
2
+ * What a link stands for, as the link widgets draw it (LinkChip, LinkRow,
3
+ * LinkCard, LinkPreview, CardUnder). The app builds it from its own lookup —
4
+ * every Estiva app from `@estiva-app/interop`'s projection — and hands it in
5
+ * whole: the words, the addresses, and the changes the reader may make. The
6
+ * widgets look nothing up, and keep nothing but what a reader does to them.
7
+ *
8
+ * The shape follows the prototype Miky approved (Widgets 2, 15 Oct; why:
9
+ * https://github.com/estiva-app/estiva-docs/blob/main/decisions/where-a-link-sits-and-which-file-gets-the-card.md).
10
+ */
11
+ import type { ThingKind } from './KindIcon'
12
+
13
+ /** What a link stands for, by its icon. `page` draws a page of text. */
14
+ export type LinkedKind = ThingKind | 'page'
15
+
16
+ /** The colours a status draws in, as an app's manifest names them. */
17
+ export type StatusColour = 'neutral' | 'blue' | 'green' | 'muted'
18
+
19
+ /** One value of a status, as the reader sees it. */
20
+ export interface LinkedStatus {
21
+ label: string
22
+ colour: StatusColour
23
+ }
24
+
25
+ /** A person, by the name the app shows and, when it has one, their picture. */
26
+ export interface LinkedPerson {
27
+ name: string
28
+ picture?: string
29
+ }
30
+
31
+ /** One choice a picker offers. */
32
+ export interface PickerOption {
33
+ value: string
34
+ label: string
35
+ /** A status's colour: the option draws its shape. */
36
+ colour?: StatusColour
37
+ /** A person's picture: the option draws their face. */
38
+ picture?: string
39
+ }
40
+
41
+ /**
42
+ * A value the reader may be able to change from the card: a status, or who
43
+ * owns it. With `onChange` it is a picker; without it, the reader may not
44
+ * change it, and it reads as words in the same place.
45
+ */
46
+ export interface LinkedPicker {
47
+ /** The chosen option's `value`; `undefined` is nobody, for an owner. */
48
+ value: string | undefined
49
+ options: PickerOption[]
50
+ onChange?: (value: string) => void
51
+ }
52
+
53
+ /** The count of what was said about it, and where that is read. */
54
+ export interface LinkedConversation {
55
+ count: number
56
+ /** Opens the conversation, at the first unread when there is one. */
57
+ href: string
58
+ /**
59
+ * The reader has something unread there. Only a member of it has unread
60
+ * (SPEC §11.8; why: https://github.com/estiva-app/estiva-docs/blob/main/decisions/membership-replaces-following.md),
61
+ * so the app passes `true` only for one.
62
+ */
63
+ unread?: boolean
64
+ }
65
+
66
+ /** One item listed inside a card, such as a group's members, each opening on its own. */
67
+ export interface LinkedChild {
68
+ id: string
69
+ title: string
70
+ href: string
71
+ key?: string
72
+ status: LinkedStatus
73
+ owner?: LinkedPerson
74
+ }
75
+
76
+ /** Everything a link's widgets may draw about what it stands for. Only `id`, `kind`, `title` and `href` are always there. */
77
+ export interface Linked {
78
+ /** Its address. Two `Linked` with the same `id` are the same thing (identity by value, not by object). */
79
+ id: string
80
+ kind: LinkedKind
81
+ title: string
82
+ /** Opens it. An address in this app goes through `AppProvider`'s `navigate`. */
83
+ href: string
84
+ /** What the reader calls it, quietly before the title: "ABC-2". */
85
+ key?: string
86
+ /** The line under a card's title: what it is in. Drawn after the key. */
87
+ subtitle?: string
88
+ /** Its short summary, a sentence or two. Without one a card shows the title alone. */
89
+ summary?: string
90
+ /** Its status: each option carries its `colour`. */
91
+ status?: LinkedPicker
92
+ /** Who owns it, by the word the app uses (`ownerLabel`: "Assignee", "Lead"). */
93
+ owner?: LinkedPicker
94
+ ownerLabel?: string
95
+ /** What was said about it. `noun` names it for a screen reader: "comments" by default. */
96
+ conversation?: LinkedConversation & { noun?: string }
97
+ /** Items inside it, drawn as rows in its card. */
98
+ children?: LinkedChild[]
99
+ /** How far along it is, drawn as a ring. */
100
+ done?: { done: number; total: number }
101
+ /** Who is in it, as faces. */
102
+ members?: LinkedPerson[]
103
+ /** Who wrote it, as faces. */
104
+ authors?: LinkedPerson[]
105
+ /** When it last changed, in the app's words: "2h ago". */
106
+ updated?: string
107
+ /** The newest thing said in it, drawn only when the reader may read it. */
108
+ latest?: { author: LinkedPerson; body: string; at: string }
109
+ /** Words a person wrote, linked as themselves: drawn with `author`'s face, not an icon. */
110
+ body?: string
111
+ author?: LinkedPerson
112
+ }
113
+
114
+ /** What a card's last line holds: pickers, faces, a line of words, or nothing. */
115
+ export type LoadingFooter = 'pickers' | 'people' | 'line' | 'none'
116
+
117
+ /** The parts of a card known before it loads, so its placeholder takes the card's height. */
118
+ export interface LoadingShape {
119
+ subtitle?: boolean
120
+ summary?: boolean
121
+ rows?: number
122
+ latest?: boolean
123
+ ring?: boolean
124
+ footer?: LoadingFooter
125
+ }
126
+
127
+ /**
128
+ * Where a link's lookup stands. `loading` knows the kind from the address, and
129
+ * a shape when the app remembers one; `unavailable` is deleted or not the
130
+ * reader's to see, which read the same (Miky, 15 Oct).
131
+ */
132
+ export type LinkState =
133
+ | { state: 'loading'; kind: LinkedKind; shape?: LoadingShape }
134
+ | {
135
+ state: 'unavailable'
136
+ kind: LinkedKind
137
+ /** What it is, in a word: "Item". */
138
+ noun: string
139
+ /** Where it may still open for someone who can: the app that owns it, and its address. */
140
+ openIn?: { app: string; href: string }
141
+ }
142
+ | { state: 'ready'; linked: Linked }
143
+
144
+ /** A picker's chosen option, or none. */
145
+ export function chosenOf(picker: LinkedPicker | undefined): PickerOption | undefined {
146
+ return picker?.value === undefined ? undefined : picker.options.find((option) => option.value === picker.value)
147
+ }
148
+
149
+ /** The shape `linked`'s card takes, for its placeholder the next time it loads. */
150
+ export function loadingShapeOf(linked: Linked): LoadingShape {
151
+ return {
152
+ subtitle: !!(linked.subtitle || linked.key),
153
+ summary: !!linked.summary,
154
+ rows: linked.children?.length ?? 0,
155
+ latest: !!linked.latest,
156
+ ring: !!linked.done,
157
+ footer: linked.status || linked.owner ? 'pickers' : linked.authors ? 'people' : 'line',
158
+ }
159
+ }
160
+
161
+ /**
162
+ * What this device last found it could not show, kept so the next open draws
163
+ * "not available." from the first paint instead of a card's height that then
164
+ * closes (Peek's OPT-10; Miky, 15 Oct). One per app, under its own key, in
165
+ * localStorage because the first paint reads it synchronously. The app clears
166
+ * it with the rest of the device's copy at sign-out.
167
+ */
168
+ export interface UnavailableMemory {
169
+ /** Whether this device last found `id` not available. */
170
+ has: (id: string) => boolean
171
+ /** The lookup's answer: remembered when not available, forgotten when it is. */
172
+ note: (id: string, available: boolean) => void
173
+ /** Sign-out. */
174
+ clear: () => void
175
+ }
176
+
177
+ /** The device's memory of links it could not show, under the app's own `storageKey`, keeping the newest `limit`. */
178
+ export function unavailableMemory(storageKey: string, limit = 300): UnavailableMemory {
179
+ // Read once per page: a screen of cards asks many times.
180
+ let held: Set<string> | undefined
181
+ const list = () => {
182
+ if (held) return held
183
+ try {
184
+ const stored: unknown = JSON.parse(localStorage.getItem(storageKey) ?? '[]')
185
+ held = new Set(Array.isArray(stored) ? stored.filter((item): item is string => typeof item === 'string') : [])
186
+ } catch {
187
+ held = new Set()
188
+ }
189
+ return held
190
+ }
191
+ return {
192
+ has: (id) => list().has(id),
193
+ note: (id, available) => {
194
+ // Re-read before writing: another memory on this key, or another tab's sign-out, may have changed it since.
195
+ held = undefined
196
+ const current = list()
197
+ if (available !== current.has(id)) return
198
+ const next = new Set(current)
199
+ if (available) next.delete(id)
200
+ else next.add(id)
201
+ held = new Set([...next].slice(-limit))
202
+ try {
203
+ localStorage.setItem(storageKey, JSON.stringify([...held]))
204
+ } catch {
205
+ // Full or blocked storage: the next page draws the card as if never seen, which is only the first-open jump.
206
+ }
207
+ },
208
+ clear: () => {
209
+ held = undefined
210
+ try {
211
+ localStorage.removeItem(storageKey)
212
+ } catch {
213
+ // Nothing to clear where storage is blocked.
214
+ }
215
+ },
216
+ }
217
+ }
@@ -48,20 +48,20 @@ describe('the registry builds', () => {
48
48
  expect(registry.entries.map((one) => one.name).sort()).toEqual(values.map((one) => one.name).sort())
49
49
  })
50
50
 
51
- it('counts 178 names over 84 files: 119 components, 57 helpers and 2 hooks (InlineText, a title or the line under it edited where it stands (SUM-4), on 16 October; ComposeBox with ComposeKeys and its classes, CommentHighlights with its five helpers, and keepSelection and releaseSelection, from the Leaf prototype (LEAF-4), on 16 October;NavTree, NavTreeSection, the composer triggers and UnreadDot joined on 2 October; BlockHandle and EditorBlockHandle (RIC-18) on 2 October, moveBlock and blockDropCursor, the drop in the gutter, on 5 October, MembersPill and MembersDialog, moved from Peek (SHI-31), on 6 October, ShowMore on 7 October, AppProvider on 8 October, and KindIcon, thingKindOf and PastedImages with its two helpers, moved from Peek, on 8 October; CodeBlockColours, the coloured code block of the editor, on 9 October; Checklist, Callout, isChecklistTick and calloutLook (RIC-19) on 13 October; EditorTableControls and its four actions (RIC-21) on 13 October; ReferenceCards, useReferenceCards and its three helpers, the card a chip in a document asks for, on 14 October; LinkChips and its two helpers, a URL in a document read as its chip, on 14 October; ReferenceMenu, ReferenceMenuView and three helpers, the menu of a chip while writing, on 14 October; PastedMarkdown, markdown pasted as the blocks it spells (LEAF-10), on 15 October; turnIntoRows, turnListInto and turnQuoteInto, the block menu’s Turn into for a list and a quote (LEAF-11), on 15 October)', () => {
51
+ it('counts 197 names over 92 files: 134 components, 60 helpers and 3 hooks (the link widgets — LinkChip, LinkRow, LinkCard, CardUnder, PartCard, LinkPreview and their parts, with useCanHover, chosenOf, loadingShapeOf and unavailableMemory — from the Widgets 2 prototype, on 16 October; InlineText, a title or the line under it edited where it stands (SUM-4), on 16 October; ComposeBox with ComposeKeys and its classes, CommentHighlights with its five helpers, and keepSelection and releaseSelection, from the Leaf prototype (LEAF-4), on 16 October;NavTree, NavTreeSection, the composer triggers and UnreadDot joined on 2 October; BlockHandle and EditorBlockHandle (RIC-18) on 2 October, moveBlock and blockDropCursor, the drop in the gutter, on 5 October, MembersPill and MembersDialog, moved from Peek (SHI-31), on 6 October, ShowMore on 7 October, AppProvider on 8 October, and KindIcon, thingKindOf and PastedImages with its two helpers, moved from Peek, on 8 October; CodeBlockColours, the coloured code block of the editor, on 9 October; Checklist, Callout, isChecklistTick and calloutLook (RIC-19) on 13 October; EditorTableControls and its four actions (RIC-21) on 13 October; ReferenceCards, useReferenceCards and its three helpers, the card a chip in a document asks for, on 14 October; LinkChips and its two helpers, a URL in a document read as its chip, on 14 October; ReferenceMenu, ReferenceMenuView and three helpers, the menu of a chip while writing, on 14 October; PastedMarkdown, markdown pasted as the blocks it spells (LEAF-10), on 15 October; turnIntoRows, turnListInto and turnQuoteInto, the block menu’s Turn into for a list and a quote (LEAF-11), on 15 October)', () => {
52
52
  // The reconciliation GATES.md §24 explains. If this changes, that changes.
53
53
  // The `EDITOR_EXTENSIONS` count as components by their names; they are
54
54
  // editor extensions, documented on SelectionToolbar's, BlockNodes' and
55
55
  // ComposerTriggers' pages.
56
56
  const kinds = registry.entries.reduce<Record<string, number>>((all, one) => ({ ...all, [one.kind]: (all[one.kind] ?? 0) + 1 }), {})
57
- expect(kinds).toEqual({ component: 119, helper: 57, hook: 2 })
58
- expect(new Set(registry.entries.map((one) => one.sourceFile)).size).toBe(84)
57
+ expect(kinds).toEqual({ component: 134, helper: 60, hook: 3 })
58
+ expect(new Set(registry.entries.map((one) => one.sourceFile)).size).toBe(92)
59
59
  })
60
60
 
61
61
  it('gives every entry a purpose, from its own page or from the comment above it', () => {
62
62
  expect(registry.entries.filter((one) => one.purpose.trim() === '')).toEqual([])
63
- expect(registry.entries.filter((one) => one.purposeFrom === 'page').length).toBe(80)
64
- expect(registry.entries.filter((one) => one.purposeFrom === 'comment').length).toBe(98)
63
+ expect(registry.entries.filter((one) => one.purposeFrom === 'page').length).toBe(86)
64
+ expect(registry.entries.filter((one) => one.purposeFrom === 'comment').length).toBe(111)
65
65
  })
66
66
 
67
67
  it('takes a name with a page of its own from the page, and a name without one from the code', () => {
@@ -262,8 +262,8 @@ describe('the ids point at something', () => {
262
262
 
263
263
  it('gives every component a page and a story, and only a helper none', () => {
264
264
  const without = registry.entries.filter((one) => one.docsId === null || one.storyId === null)
265
- // The helpers in a file with no page: cn, the shortcut names, and the editor's suggestion wiring (documented on SuggestionMenu's page).
266
- expect(without.map((one) => one.name).sort()).toEqual(['cn', 'isApplePlatform', 'isSuggestionActive', 'isSuggestionOpen', 'shortcutLabel', 'suggestionPopup'])
265
+ // The helpers in a file with no page: cn, the shortcut names, the editor's suggestion wiring (documented on SuggestionMenu's page), and the link widgets' data helpers (documented on LinkCard's and LinkChip's pages).
266
+ expect(without.map((one) => one.name).sort()).toEqual(['chosenOf', 'cn', 'isApplePlatform', 'isSuggestionActive', 'isSuggestionOpen', 'loadingShapeOf', 'shortcutLabel', 'suggestionPopup', 'unavailableMemory'])
267
267
  expect(without.every((one) => one.kind !== 'component')).toBe(true)
268
268
  })
269
269
 
@@ -299,7 +299,7 @@ describe('the ids point at something', () => {
299
299
  describe('a second parser agrees', async () => {
300
300
  const docgen = await import('react-docgen').catch(() => null)
301
301
 
302
- it.skipIf(!docgen)('finds the same props on all 80 components', async () => {
302
+ it.skipIf(!docgen)('finds the same props on all 86 components', async () => {
303
303
  if (!docgen) return
304
304
  const resolver = new docgen.builtinResolvers.FindExportedDefinitionsResolver({ limit: 0 })
305
305
  const byName = new Map(registry.entries.map((one) => [one.name, one]))
@@ -106,6 +106,13 @@ fork freely, owe nothing back, mention it on the package's ticket.
106
106
  | A URL in a document that reads as the chip of what it points at | **LinkChips** — from `@estiva-app/ui/editor`, beside `ReferenceNode`; the app says what each URL names |
107
107
  | Open a chip in a document, or choose its card, while writing | **ReferenceMenu** — from `@estiva-app/ui/editor`, beside `ReferenceCards`; a click on the chip, or putting one in, opens it |
108
108
  | A code block in an editor, coloured by its language as you type | **CodeBlockColours** — from `@estiva-app/ui/editor`, in place of the starter kit's code block |
109
+ | A link to something an app looked up, in a sentence | **LinkChip** — hover shows its card; loading and "not available" drawn for you. **LinkChips** (editor) turns a URL in a document into one |
110
+ | A link alone on its line | **LinkRow** — one 36px line for every kind; **LinkUnavailable** when it cannot be read |
111
+ | A link's card: what a row cannot hold, with the pickers the reader may use | **LinkCard** — `onChange` on a picker only for a reader who may change it; **LinkCardLoading** at its height |
112
+ | The one card under a piece of text, with the ✕ that removes it | **CardUnder** — `revealOnHover` once it is sent |
113
+ | A link's card on hover, read only | **LinkPreview** — LinkChip and LinkRow already wrap themselves in it |
114
+ | A part of something longer, shared live | **PartCard** — the app draws the blocks |
115
+ | A status and an owner changed in place, a conversation count with its unread dot, a ring | **LinkParts** — `LinkPickers`, `CommentCount`, `StatusMark`, `ProgressRing` |
109
116
  | A person or a thing named inside a sentence | **InlineChip** — one line high, level with the words; `href` makes it a link |
110
117
  | What kind of thing a name stands for — a topic, a project, an issue, a file, a message | **KindIcon** — before the name in a row, or inside an **InlineChip**; `thingKindOf` turns an event kind into one |
111
118
  | Formatted text: headings, lists, a quote, code, a table, links | **RichText** — pass the parsed tree; `small` inside a card |