@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.
- package/dist/CardUnder.d.ts +10 -0
- package/dist/CardUnder.d.ts.map +1 -0
- package/dist/InlineChip.d.ts +2 -0
- package/dist/InlineChip.d.ts.map +1 -1
- package/dist/LinkCard.d.ts +17 -0
- package/dist/LinkCard.d.ts.map +1 -0
- package/dist/LinkChip.d.ts +8 -0
- package/dist/LinkChip.d.ts.map +1 -0
- package/dist/LinkParts.d.ts +45 -0
- package/dist/LinkParts.d.ts.map +1 -0
- package/dist/LinkPreview.d.ts +21 -0
- package/dist/LinkPreview.d.ts.map +1 -0
- package/dist/LinkRow.d.ts +24 -0
- package/dist/LinkRow.d.ts.map +1 -0
- package/dist/PartCard.d.ts +32 -0
- package/dist/PartCard.d.ts.map +1 -0
- package/dist/Skeleton.d.ts +12 -2
- package/dist/Skeleton.d.ts.map +1 -1
- package/dist/eslint/index.js +1 -0
- package/dist/eslint/index.js.map +2 -2
- package/dist/eslint/no-restyled-part.d.ts.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +480 -71
- package/dist/index.js.map +4 -4
- package/dist/linked.d.ts +168 -0
- package/dist/linked.d.ts.map +1 -0
- package/dist/linked.examples.d.ts +27 -0
- package/dist/linked.examples.d.ts.map +1 -0
- package/package.json +1 -1
- package/registry.json +784 -7
- package/src/CardUnder.mdx +53 -0
- package/src/CardUnder.stories.tsx +45 -0
- package/src/CardUnder.test.tsx +40 -0
- package/src/CardUnder.tsx +45 -0
- package/src/InlineChip.tsx +2 -0
- package/src/LinkCard.mdx +58 -0
- package/src/LinkCard.stories.tsx +68 -0
- package/src/LinkCard.test.tsx +103 -0
- package/src/LinkCard.tsx +209 -0
- package/src/LinkChip.mdx +51 -0
- package/src/LinkChip.stories.tsx +40 -0
- package/src/LinkChip.test.tsx +83 -0
- package/src/LinkChip.tsx +51 -0
- package/src/LinkParts.mdx +49 -0
- package/src/LinkParts.stories.tsx +61 -0
- package/src/LinkParts.tsx +147 -0
- package/src/LinkPreview.mdx +47 -0
- package/src/LinkPreview.stories.tsx +38 -0
- package/src/LinkPreview.tsx +55 -0
- package/src/LinkRow.mdx +47 -0
- package/src/LinkRow.stories.tsx +49 -0
- package/src/LinkRow.test.tsx +38 -0
- package/src/LinkRow.tsx +107 -0
- package/src/PartCard.mdx +43 -0
- package/src/PartCard.stories.tsx +35 -0
- package/src/PartCard.test.tsx +30 -0
- package/src/PartCard.tsx +59 -0
- package/src/Skeleton.tsx +7 -3
- package/src/eslint/no-native-title.test.ts +3 -2
- package/src/eslint/no-restyled-part.ts +1 -0
- package/src/index.ts +25 -0
- package/src/linked.examples.ts +107 -0
- package/src/linked.test.ts +58 -0
- package/src/linked.ts +217 -0
- package/src/registry/registry.test.ts +8 -8
- 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
|
|
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:
|
|
58
|
-
expect(new Set(registry.entries.map((one) => one.sourceFile)).size).toBe(
|
|
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(
|
|
64
|
-
expect(registry.entries.filter((one) => one.purposeFrom === 'comment').length).toBe(
|
|
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,
|
|
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
|
|
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]))
|
package/stories/Choosing.mdx
CHANGED
|
@@ -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 |
|