@estiva-app/ui 0.39.0 → 0.40.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/RichText.d.ts +78 -0
- package/dist/RichText.d.ts.map +1 -0
- 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/gates/app-checks.d.ts.map +1 -1
- package/dist/gates/index.js +11 -1
- package/dist/gates/index.js.map +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +350 -209
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/registry.json +90 -3
- package/src/RichText.mdx +79 -0
- package/src/RichText.stories.tsx +91 -0
- package/src/RichText.test.tsx +149 -0
- package/src/RichText.tsx +294 -0
- package/src/eslint/no-restyled-part.ts +1 -0
- package/src/gates/app-checks.ts +7 -0
- package/src/index.ts +1 -0
- package/src/registry/registry.test.ts +5 -5
- package/stories/Choosing.mdx +1 -0
- package/tailwind-preset.js +1 -0
- package/tokens.css +15 -0
package/src/RichText.tsx
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import type { MouseEvent, ReactNode } from 'react'
|
|
2
|
+
import { cn } from './cn'
|
|
3
|
+
import { Link } from './Link'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Formatted text, drawn: headings, paragraphs, lists, a quote, code, a divider,
|
|
7
|
+
* a table and links.
|
|
8
|
+
*
|
|
9
|
+
* Both apps drew the same thing twice, from the same source — the text tree the
|
|
10
|
+
* protocol package builds (`toRenderTree` → `RenderBlock`, `RenderInline`) —
|
|
11
|
+
* and the two had drifted: headings, list markers, quotes and code differed,
|
|
12
|
+
* one app had tables and the other dropped them, one had links and the other
|
|
13
|
+
* printed the brackets (UIG-30). The look here is Katerina's picks of
|
|
14
|
+
* 28 September, one per difference, most of them Peek's.
|
|
15
|
+
*
|
|
16
|
+
* **It takes the tree, not the text.** The caller parses (the format comes from
|
|
17
|
+
* the item's tag, never from the body) and hands the blocks in. So this package
|
|
18
|
+
* does not depend on the protocol, and a caller can leave out a block it draws
|
|
19
|
+
* elsewhere before passing the rest. The types below are the tree's shape; the
|
|
20
|
+
* protocol's own types fit them.
|
|
21
|
+
*
|
|
22
|
+
* **Safe by construction.** The tree has nothing left to parse, and every
|
|
23
|
+
* string here reaches the page as a React child, so it is text. Nothing is set
|
|
24
|
+
* as HTML.
|
|
25
|
+
*
|
|
26
|
+
* **What a word points at stays the app's.** A person, an item from another
|
|
27
|
+
* app, a name the app recognises in plain text: `renderReference`,
|
|
28
|
+
* `renderText` and `renderBlock` are where each app draws its own.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** A run of text and the marks on it — the protocol's `RenderInline`. */
|
|
32
|
+
export interface RichTextRun {
|
|
33
|
+
text: string
|
|
34
|
+
marks: readonly string[]
|
|
35
|
+
/** A `nostr:` address this run stands for. `text` is the address itself. */
|
|
36
|
+
reference?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A block of the tree — the protocol's `RenderBlock`. */
|
|
40
|
+
export interface RichTextBlock {
|
|
41
|
+
type: string
|
|
42
|
+
/** What the document called a block the tree does not know. */
|
|
43
|
+
typeName?: string
|
|
44
|
+
/** The block's address, when it has one. Drawn as `data-block-id`. */
|
|
45
|
+
id?: string
|
|
46
|
+
level?: 1 | 2 | 3
|
|
47
|
+
language?: string
|
|
48
|
+
/** A numbered list's first number. */
|
|
49
|
+
start?: number
|
|
50
|
+
attrs?: Record<string, unknown>
|
|
51
|
+
inline?: readonly RichTextRun[]
|
|
52
|
+
children?: readonly RichTextBlock[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type RichTextSize = 'default' | 'small'
|
|
56
|
+
|
|
57
|
+
export interface RichTextProps {
|
|
58
|
+
/** The tree, already parsed. */
|
|
59
|
+
blocks: readonly RichTextBlock[]
|
|
60
|
+
/**
|
|
61
|
+
* Default `default`, the size of text you read. `small` is the same text one
|
|
62
|
+
* step down, with every heading, list, quote and table kept: an item shown
|
|
63
|
+
* inside a card.
|
|
64
|
+
*/
|
|
65
|
+
size?: RichTextSize
|
|
66
|
+
/**
|
|
67
|
+
* Draws plain text: what is left of a run once its links are drawn. Where an
|
|
68
|
+
* app turns words it recognises into chips. By default the text as it is.
|
|
69
|
+
*/
|
|
70
|
+
renderText?: (text: string) => ReactNode
|
|
71
|
+
/** Draws a run that stands for an address. By default its text. */
|
|
72
|
+
renderReference?: (reference: string, run: RichTextRun) => ReactNode
|
|
73
|
+
/**
|
|
74
|
+
* Draws a block this part has no design for (an attachment, a widget). Return
|
|
75
|
+
* `undefined` to leave it to the part, which draws its words.
|
|
76
|
+
*/
|
|
77
|
+
renderBlock?: (block: RichTextBlock) => ReactNode | undefined
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The whole look, as classes on the box around the text: the elements inside
|
|
82
|
+
* carry none. That is so an editor can wear the same classes on its own box and
|
|
83
|
+
* look the same as what it writes — ProseMirror draws the same elements, with a
|
|
84
|
+
* paragraph inside each list item, quote and table cell (the `> p` lines).
|
|
85
|
+
*/
|
|
86
|
+
const RICH_TEXT_CLASSES = {
|
|
87
|
+
base: 'flex flex-col gap-1 break-words text-text-secondary',
|
|
88
|
+
/* A heading binds to what follows it: 12px above (8 and the 4px gap), 8px below (4 and the gap); two headings in a row, 12px apart. Level 3 keeps the text's colour, one step lower. */
|
|
89
|
+
headings: '[&>:is(h1,h2,h3)]:mb-1 [&>:is(h1,h2,h3)]:mt-2 [&>:is(h1,h2,h3):first-child]:mt-0 [&>:is(h1,h2,h3):last-child]:mb-0 [&>:is(h1,h2,h3)+:is(h1,h2,h3)]:mt-1 [&>:is(h1,h2)]:text-text-primary',
|
|
90
|
+
lists: '[&_:is(ul,ol)]:pl-5 [&_li>p]:m-0 [&_li]:break-words [&_li]:marker:text-text-muted [&_ol]:list-decimal [&_ul]:list-disc',
|
|
91
|
+
// eslint-disable-next-line token-spacing/no-restricted-classes -- @estiva-escape: the quote line is Peek's, 3px, between the ramp's 2 and 4
|
|
92
|
+
quote: '[&_blockquote>p]:m-0 [&_blockquote]:border-l-[3px] [&_blockquote]:border-border-strong [&_blockquote]:pl-2.5',
|
|
93
|
+
// eslint-disable-next-line token-values/no-restricted-classes -- @estiva-escape: code is 0.9 of the text around it, so it scales with a heading and with `small`; a named size is fixed
|
|
94
|
+
code: '[&_:not(pre)>code]:rounded-sm [&_:not(pre)>code]:bg-bg-code [&_:not(pre)>code]:px-1 [&_:not(pre)>code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.9em] [&_code]:text-text-primary',
|
|
95
|
+
// eslint-disable-next-line token-values/no-restricted-classes -- @estiva-escape: code is 0.9 of the text around it; the code inside the block is the block's own size, not 0.9 of it again
|
|
96
|
+
codeBlock: '[&_pre>code]:text-[1em] [&_pre]:whitespace-pre-wrap [&_pre]:break-words [&_pre]:rounded-md [&_pre]:bg-bg-code [&_pre]:p-2 [&_pre]:font-mono [&_pre]:text-[0.9em] [&_pre]:text-text-primary',
|
|
97
|
+
divider: '[&_hr]:border-border-default',
|
|
98
|
+
table: '[&_:is(th,td)>p]:m-0 [&_:is(th,td)]:border [&_:is(th,td)]:border-border-default [&_:is(th,td)]:px-2 [&_:is(th,td)]:py-1 [&_:is(th,td)]:text-left [&_:is(th,td)]:align-top [&_table]:w-full [&_table]:border-collapse [&_th]:bg-bg-code [&_th]:font-medium [&_th]:text-text-primary',
|
|
99
|
+
marks: '[&_em]:italic [&_strong]:font-semibold [&_u]:underline [&_u]:underline-offset-2',
|
|
100
|
+
default: 'text-body-2 [&>:is(h2,h3)]:text-body-2 [&>:is(h2,h3)]:font-semibold [&>h1]:text-h4',
|
|
101
|
+
small: 'text-caption leading-snug [&>:is(h2,h3)]:text-caption [&>:is(h2,h3)]:font-semibold [&>h1]:text-body-2 [&>h1]:font-semibold',
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* RichText's look, for a box you draw yourself: an editor's, so that what is
|
|
106
|
+
* being written looks the same as what is read. Put it on the editor's root.
|
|
107
|
+
*/
|
|
108
|
+
export function richTextClassName(size: RichTextSize = 'default', className?: string) {
|
|
109
|
+
const { base, headings, lists, quote, code, codeBlock, divider, table, marks } = RICH_TEXT_CLASSES
|
|
110
|
+
return cn(base, headings, lists, quote, code, codeBlock, divider, table, marks, RICH_TEXT_CLASSES[size], className)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A link written as `[words](address)`, then a bare address. The written form
|
|
115
|
+
* first, so its address is not found a second time on its own.
|
|
116
|
+
*/
|
|
117
|
+
const LINK_SPLIT = /(\[[^[\]\n]+\]\((?:https?:\/\/|mailto:)[^\s)]+\)|https?:\/\/[^\s]+)/g
|
|
118
|
+
const WRITTEN_LINK = /^\[([^[\]\n]+)\]\(((?:https?:\/\/|mailto:)[^\s)]+)\)$/
|
|
119
|
+
/** Punctuation after a bare address ends the sentence, not the address. */
|
|
120
|
+
const BARE_LINK = /^(https?:\/\/\S+?)([.,;:!?)\]]*)$/
|
|
121
|
+
|
|
122
|
+
/** A link in text is not also a click on what the text sits in, such as a card. */
|
|
123
|
+
const keepToLink = (event: MouseEvent) => event.stopPropagation()
|
|
124
|
+
|
|
125
|
+
function textWithLinks(text: string, renderText: (text: string) => ReactNode): ReactNode {
|
|
126
|
+
const parts = text.split(LINK_SPLIT)
|
|
127
|
+
if (parts.length === 1) return renderText(text)
|
|
128
|
+
return parts.map((part, i) => {
|
|
129
|
+
const written = part.match(WRITTEN_LINK)
|
|
130
|
+
if (written) {
|
|
131
|
+
return (
|
|
132
|
+
<span key={i} className="break-words">
|
|
133
|
+
<Link href={written[2]} external onClick={keepToLink}>{written[1]}</Link>
|
|
134
|
+
</span>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
const bare = i % 2 === 1 ? part.match(BARE_LINK) : null
|
|
138
|
+
if (bare) {
|
|
139
|
+
return (
|
|
140
|
+
// An address has no spaces to wrap at, so it may break anywhere.
|
|
141
|
+
<span key={i} className="break-all">
|
|
142
|
+
<Link href={bare[1]} external onClick={keepToLink}>{bare[1]}</Link>
|
|
143
|
+
{bare[2]}
|
|
144
|
+
</span>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
return part ? <span key={i}>{renderText(part)}</span> : null
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface Draw {
|
|
152
|
+
renderText: (text: string) => ReactNode
|
|
153
|
+
renderReference: (reference: string, run: RichTextRun) => ReactNode
|
|
154
|
+
renderBlock?: (block: RichTextBlock) => ReactNode | undefined
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function Runs({ runs, draw }: { runs?: readonly RichTextRun[]; draw: Draw }) {
|
|
158
|
+
return (
|
|
159
|
+
<>
|
|
160
|
+
{(runs ?? []).map((run, i) => {
|
|
161
|
+
if (run.reference) return <span key={i}>{draw.renderReference(run.reference, run)}</span>
|
|
162
|
+
// Code carries no other mark, and its words are never read for links.
|
|
163
|
+
if (run.marks.includes('code')) {
|
|
164
|
+
return <code key={i}>{run.text}</code>
|
|
165
|
+
}
|
|
166
|
+
// A line break inside a run is a line break.
|
|
167
|
+
let node: ReactNode = run.text.split('\n').map((line, j) => (
|
|
168
|
+
<span key={j}>
|
|
169
|
+
{j > 0 && <br />}
|
|
170
|
+
{textWithLinks(line, draw.renderText)}
|
|
171
|
+
</span>
|
|
172
|
+
))
|
|
173
|
+
if (run.marks.includes('underline')) node = <u>{node}</u>
|
|
174
|
+
if (run.marks.includes('italic')) node = <em>{node}</em>
|
|
175
|
+
if (run.marks.includes('bold')) node = <strong>{node}</strong>
|
|
176
|
+
return <span key={i}>{node}</span>
|
|
177
|
+
})}
|
|
178
|
+
</>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const named = (block: RichTextBlock) => block.typeName ?? block.type
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* A table's rows and cells are named by whatever wrote the document; these are
|
|
186
|
+
* the editor's names. A first row of header cells is the header. Returns null
|
|
187
|
+
* for a nesting it does not recognise, which is then drawn as words.
|
|
188
|
+
*/
|
|
189
|
+
function Table({ block, draw }: { block: RichTextBlock; draw: Draw }) {
|
|
190
|
+
const rows = (block.children ?? []).filter((row) => named(row) === 'tableRow')
|
|
191
|
+
if (!rows.length) return null
|
|
192
|
+
const cells = (row: RichTextBlock) =>
|
|
193
|
+
(row.children ?? []).map((cell, i) => {
|
|
194
|
+
const header = named(cell) === 'tableHeader'
|
|
195
|
+
const Cell = header ? 'th' : 'td'
|
|
196
|
+
const { colspan, rowspan } = (cell.attrs ?? {}) as { colspan?: number; rowspan?: number }
|
|
197
|
+
return (
|
|
198
|
+
<Cell
|
|
199
|
+
key={cell.id ?? i}
|
|
200
|
+
data-block-id={cell.id}
|
|
201
|
+
colSpan={colspan && colspan > 1 ? colspan : undefined}
|
|
202
|
+
rowSpan={rowspan && rowspan > 1 ? rowspan : undefined}
|
|
203
|
+
>
|
|
204
|
+
{(cell.children ?? []).map((child, j) => <Runs key={j} runs={child.inline} draw={draw} />)}
|
|
205
|
+
<Runs runs={cell.inline} draw={draw} />
|
|
206
|
+
</Cell>
|
|
207
|
+
)
|
|
208
|
+
})
|
|
209
|
+
const first = rows[0].children ?? []
|
|
210
|
+
const headerRow = first.length > 0 && first.every((cell) => named(cell) === 'tableHeader')
|
|
211
|
+
return (
|
|
212
|
+
<table data-block-id={block.id}>
|
|
213
|
+
{headerRow && (
|
|
214
|
+
<thead>
|
|
215
|
+
<tr data-block-id={rows[0].id}>{cells(rows[0])}</tr>
|
|
216
|
+
</thead>
|
|
217
|
+
)}
|
|
218
|
+
<tbody>
|
|
219
|
+
{(headerRow ? rows.slice(1) : rows).map((row, i) => (
|
|
220
|
+
<tr key={row.id ?? i} data-block-id={row.id}>{cells(row)}</tr>
|
|
221
|
+
))}
|
|
222
|
+
</tbody>
|
|
223
|
+
</table>
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function Block({ block, draw }: { block: RichTextBlock; draw: Draw }): ReactNode {
|
|
228
|
+
const own = draw.renderBlock?.(block)
|
|
229
|
+
if (own !== undefined) return own
|
|
230
|
+
const runs = <Runs runs={block.inline} draw={draw} />
|
|
231
|
+
switch (block.type) {
|
|
232
|
+
case 'heading': {
|
|
233
|
+
if (block.level === 1) return <h1 data-block-id={block.id}>{runs}</h1>
|
|
234
|
+
if (block.level === 3) return <h3 data-block-id={block.id}>{runs}</h3>
|
|
235
|
+
return <h2 data-block-id={block.id}>{runs}</h2>
|
|
236
|
+
}
|
|
237
|
+
case 'paragraph':
|
|
238
|
+
return <p data-block-id={block.id}>{runs}</p>
|
|
239
|
+
case 'bulletList':
|
|
240
|
+
case 'orderedList': {
|
|
241
|
+
const items = (block.children ?? []).map((item, i) => (
|
|
242
|
+
<li key={item.id ?? i} data-block-id={item.id}>
|
|
243
|
+
<Runs runs={item.inline} draw={draw} />
|
|
244
|
+
</li>
|
|
245
|
+
))
|
|
246
|
+
return block.type === 'bulletList'
|
|
247
|
+
? <ul data-block-id={block.id}>{items}</ul>
|
|
248
|
+
: <ol data-block-id={block.id} start={block.start}>{items}</ol>
|
|
249
|
+
}
|
|
250
|
+
case 'blockquote':
|
|
251
|
+
return <blockquote data-block-id={block.id}>{runs}</blockquote>
|
|
252
|
+
case 'codeBlock':
|
|
253
|
+
// Literal: no marks, no links, no chips. A long line wraps; nothing scrolls sideways.
|
|
254
|
+
return (
|
|
255
|
+
<pre data-block-id={block.id} data-language={block.language}>
|
|
256
|
+
<code>{(block.inline ?? []).map((run) => run.text).join('')}</code>
|
|
257
|
+
</pre>
|
|
258
|
+
)
|
|
259
|
+
case 'horizontalRule':
|
|
260
|
+
return <hr data-block-id={block.id} />
|
|
261
|
+
case 'table': {
|
|
262
|
+
const table = <Table block={block} draw={draw} />
|
|
263
|
+
if ((block.children ?? []).some((row) => named(row) === 'tableRow')) return table
|
|
264
|
+
break
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/*
|
|
268
|
+
Anything else — a type this part has no design for, or one published after
|
|
269
|
+
it was written — is drawn as its words, never dropped: a block that
|
|
270
|
+
vanishes looks like one you are not allowed to see.
|
|
271
|
+
*/
|
|
272
|
+
return (
|
|
273
|
+
<div data-block-id={block.id} data-block-type={named(block)}>
|
|
274
|
+
{runs}
|
|
275
|
+
{(block.children ?? []).map((child, i) => <Block key={child.id ?? i} block={child} draw={draw} />)}
|
|
276
|
+
</div>
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function RichText({
|
|
281
|
+
blocks,
|
|
282
|
+
size = 'default',
|
|
283
|
+
renderText = (text) => text,
|
|
284
|
+
renderReference = (_reference, run) => run.text,
|
|
285
|
+
renderBlock,
|
|
286
|
+
}: RichTextProps) {
|
|
287
|
+
if (blocks.length === 0) return null
|
|
288
|
+
const draw: Draw = { renderText, renderReference, renderBlock }
|
|
289
|
+
return (
|
|
290
|
+
<div data-rich-text className={richTextClassName(size)}>
|
|
291
|
+
{blocks.map((block, i) => <Block key={block.id ?? i} block={block} draw={draw} />)}
|
|
292
|
+
</div>
|
|
293
|
+
)
|
|
294
|
+
}
|
|
@@ -129,6 +129,7 @@ export const PART_LOOK_PROPS: Record<string, string[]> = {
|
|
|
129
129
|
RailItem: ['active'],
|
|
130
130
|
Reaction: ['pressed'],
|
|
131
131
|
ReactionPicker: ['surface'],
|
|
132
|
+
RichText: ['size'],
|
|
132
133
|
SectionHeader: ['hover'],
|
|
133
134
|
SectionLabel: ['tone', 'truncate'],
|
|
134
135
|
Select: ['size'],
|
package/src/gates/app-checks.ts
CHANGED
|
@@ -254,8 +254,15 @@ export function appChecks(h: GateHelpers, { app = '.', page, chain = { ref: 'UIG
|
|
|
254
254
|
return probe(code, 'none', undefined, 'src/components/__gates_probe__.test.tsx', config)()
|
|
255
255
|
}), 'a test file is not checked for hand-written values, while source is') },
|
|
256
256
|
]),
|
|
257
|
+
// Built 28 September: both apps drew the protocol's text tree by hand, and drifted apart.
|
|
258
|
+
// A file that reads the protocol and draws a quote or a numbered list itself is a third copy.
|
|
257
259
|
ticket('UIG-30', [
|
|
258
260
|
{ what: 'the installed package has RichText', run: () => h.contains(installed, /\bRichText\b/, 'the installed @estiva-app/ui exports RichText') },
|
|
261
|
+
{ what: "no file draws the protocol's text tree by hand", run: () => {
|
|
262
|
+
const sources = h.listFiles(at('src'), (n) => /\.tsx$/.test(n) && !/\.(test|stories)\.tsx$/.test(n))
|
|
263
|
+
const own = sources.filter((f) => { const text = h.read(f); return /from ['"]@estiva-app\/protocol['"]/.test(text) && /<(?:blockquote|ol)\b/.test(text) })
|
|
264
|
+
return own.length ? h.FAIL(`draws the text tree itself (${own.join(', ')}): pass the tree to RichText`) : h.PASS('formatted text is drawn by RichText only')
|
|
265
|
+
} },
|
|
259
266
|
]),
|
|
260
267
|
// Katerina's ruling of 17 September (docs/GATES.md §23): one copy, in the package. An app made
|
|
261
268
|
// by create-estiva-app passes this from its first commit; Peek and Ship do once UIG-32 lands.
|
package/src/index.ts
CHANGED
|
@@ -46,6 +46,7 @@ export {
|
|
|
46
46
|
} from './InlineChip'
|
|
47
47
|
export { Lightbox, type LightboxProps } from './Lightbox'
|
|
48
48
|
export { Link, type LinkProps, type LinkVariant } from './Link'
|
|
49
|
+
export { RichText, richTextClassName, type RichTextProps, type RichTextBlock, type RichTextRun, type RichTextSize } from './RichText'
|
|
49
50
|
export { IconButton, type IconButtonProps, type IconButtonVariant } from './IconButton'
|
|
50
51
|
export { Kbd, type KbdProps } from './Kbd'
|
|
51
52
|
export { IdentityMenu, IdentityPanel, type Identity, type IdentityMenuProps, type IdentityPanelProps } from './IdentityMenu'
|
|
@@ -45,17 +45,17 @@ describe('the registry builds', () => {
|
|
|
45
45
|
expect(registry.entries.map((one) => one.name).sort()).toEqual(values.map((one) => one.name).sort())
|
|
46
46
|
})
|
|
47
47
|
|
|
48
|
-
it('counts
|
|
48
|
+
it('counts 90 names over 59 files: 82 components, 7 helpers and 1 hook (RichText and richTextClassName joined on 28 September)', () => {
|
|
49
49
|
// The reconciliation GATES.md §24 explains. If this changes, that changes.
|
|
50
50
|
const kinds = registry.entries.reduce<Record<string, number>>((all, one) => ({ ...all, [one.kind]: (all[one.kind] ?? 0) + 1 }), {})
|
|
51
|
-
expect(kinds).toEqual({ component:
|
|
52
|
-
expect(new Set(registry.entries.map((one) => one.sourceFile)).size).toBe(
|
|
51
|
+
expect(kinds).toEqual({ component: 82, helper: 7, hook: 1 })
|
|
52
|
+
expect(new Set(registry.entries.map((one) => one.sourceFile)).size).toBe(59)
|
|
53
53
|
})
|
|
54
54
|
|
|
55
55
|
it('gives every entry a purpose, from its own page or from the comment above it', () => {
|
|
56
56
|
expect(registry.entries.filter((one) => one.purpose.trim() === '')).toEqual([])
|
|
57
|
-
expect(registry.entries.filter((one) => one.purposeFrom === 'page').length).toBe(
|
|
58
|
-
expect(registry.entries.filter((one) => one.purposeFrom === 'comment').length).toBe(
|
|
57
|
+
expect(registry.entries.filter((one) => one.purposeFrom === 'page').length).toBe(59)
|
|
58
|
+
expect(registry.entries.filter((one) => one.purposeFrom === 'comment').length).toBe(31)
|
|
59
59
|
})
|
|
60
60
|
|
|
61
61
|
it('takes a name with a page of its own from the page, and a name without one from the code', () => {
|
package/stories/Choosing.mdx
CHANGED
|
@@ -87,6 +87,7 @@ fork freely, owe nothing back, mention it on the package's ticket.
|
|
|
87
87
|
| A link — in text, on a title, beside a note, or a whole box | **Link** — `text`, `quiet`, `underlined`, `plain`; `external` for a new tab |
|
|
88
88
|
| A document or an image attached to something, posted or waiting to be sent | **AttachmentCard** — the file decides the shape; `pending` in a composer |
|
|
89
89
|
| A person or a thing named inside a sentence | **InlineChip** — one line high, level with the words; `href` makes it a link |
|
|
90
|
+
| Formatted text: headings, lists, a quote, code, a table, links | **RichText** — pass the parsed tree; `small` inside a card |
|
|
90
91
|
| The key that does this too | **Kbd** — or the `shortcut` prop on **MenuItem**, **Tooltip** and **SearchInput** |
|
|
91
92
|
|
|
92
93
|
## Feedback
|
package/tailwind-preset.js
CHANGED
|
@@ -153,6 +153,7 @@ export default {
|
|
|
153
153
|
'bg-tooltip': 'var(--bg-tooltip)',
|
|
154
154
|
'bg-tooltip-key': 'var(--bg-tooltip-key)',
|
|
155
155
|
'bg-field': 'var(--bg-field)',
|
|
156
|
+
'bg-code': 'var(--bg-code)',
|
|
156
157
|
// text
|
|
157
158
|
'text-primary': 'var(--text-primary)',
|
|
158
159
|
'text-secondary': 'var(--text-secondary)',
|
package/tokens.css
CHANGED
|
@@ -94,6 +94,9 @@
|
|
|
94
94
|
--bg-field: #F4F4F5;
|
|
95
95
|
--border-field: #E4E4E7;
|
|
96
96
|
--shadow-field: 0 0 #0000;
|
|
97
|
+
/* Code, in a sentence and as a block, and a table's header row (RichText, UIG-30).
|
|
98
|
+
Here, the hover fill: bg-active, #D4D4D8, was the darkest thing on a white card (Katerina, 28 September). */
|
|
99
|
+
--bg-code: #E9E9EB;
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
/* ─── dark ─── Peek's dark theme (peek/src/index.css .dark, 2026-08-28). */
|
|
@@ -172,6 +175,9 @@
|
|
|
172
175
|
--bg-field: #27272A;
|
|
173
176
|
--border-field: #2D2D31;
|
|
174
177
|
--shadow-field: 0 0 #0000;
|
|
178
|
+
/* Code, in a sentence and as a block, and a table's header row (RichText, UIG-30).
|
|
179
|
+
Here, bg-active, as Peek draws code (Katerina, 28 September). */
|
|
180
|
+
--bg-code: #2D2D31;
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
|
|
@@ -254,6 +260,9 @@
|
|
|
254
260
|
--bg-field: #20242d;
|
|
255
261
|
--border-field: rgba(255, 255, 255, 0.12);
|
|
256
262
|
--shadow-field: 0 0 #0000;
|
|
263
|
+
/* Code, in a sentence and as a block, and a table's header row (RichText, UIG-30).
|
|
264
|
+
Here, bg-active, as Peek draws code (Katerina, 28 September). */
|
|
265
|
+
--bg-code: rgba(255, 255, 255, 0.08);
|
|
257
266
|
}
|
|
258
267
|
|
|
259
268
|
/* ─── ship ─── Ship's palette (Katerina's ruling, 2026-08-28; ship/web/src/index.css). */
|
|
@@ -342,6 +351,9 @@
|
|
|
342
351
|
--bg-field: #1f2330;
|
|
343
352
|
--border-field: rgba(255, 255, 255, 0.12);
|
|
344
353
|
--shadow-field: 0 0 #0000;
|
|
354
|
+
/* Code, in a sentence and as a block, and a table's header row (RichText, UIG-30).
|
|
355
|
+
Here, bg-active, as Peek draws code (Katerina, 28 September). */
|
|
356
|
+
--bg-code: rgba(255, 255, 255, 0.08);
|
|
345
357
|
}
|
|
346
358
|
|
|
347
359
|
/* ─── leaf ─── Leaf's theme: Linear's light theme on these same tokens (Katerina,
|
|
@@ -430,4 +442,7 @@
|
|
|
430
442
|
--bg-field: #ffffff;
|
|
431
443
|
--border-field: #dedede; /* the default line, so a field shows on a white dialog (Katerina, 28 September); Linear's own is #f7f7f7 */
|
|
432
444
|
--shadow-field: 0 3px 6px -2px rgba(0, 0, 0, 0.02), 0 1px 1px 0 rgba(0, 0, 0, 0.04);
|
|
445
|
+
/* Code, in a sentence and as a block, and a table's header row (RichText, UIG-30).
|
|
446
|
+
Here, the inset fill: lighter than bg-active on a white card (Katerina, 28 September). */
|
|
447
|
+
--bg-code: #e9e9ea;
|
|
433
448
|
}
|