@estiva-app/ui 0.63.0 → 0.64.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/CodeBlockColours.d.ts +11 -0
- package/dist/CodeBlockColours.d.ts.map +1 -0
- package/dist/Link.d.ts +1 -1
- package/dist/RichText.d.ts +2 -1
- package/dist/RichText.d.ts.map +1 -1
- package/dist/codeColours.d.ts +29 -0
- package/dist/codeColours.d.ts.map +1 -0
- package/dist/editor.d.ts +1 -0
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +12 -0
- package/dist/editor.js.map +3 -3
- package/dist/index.js +55 -31
- package/dist/index.js.map +4 -4
- package/package.json +8 -2
- package/registry.json +33 -14
- package/src/CodeBlockColours.mdx +48 -0
- package/src/CodeBlockColours.stories.tsx +37 -0
- package/src/CodeBlockColours.ts +14 -0
- package/src/Link.mdx +1 -1
- package/src/Link.stories.tsx +1 -1
- package/src/Link.test.tsx +2 -2
- package/src/Link.tsx +2 -2
- package/src/RichText.stories.tsx +24 -0
- package/src/RichText.tsx +31 -17
- package/src/codeColours.ts +33 -0
- package/src/editor.ts +1 -0
- package/src/pages.test.ts +1 -0
- package/src/registry/registry.test.ts +6 -6
- package/stories/Choosing.mdx +1 -0
- package/tailwind-preset.js +8 -0
- package/tokens.css +50 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estiva-app/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.64.0",
|
|
4
4
|
"description": "Estiva's design tokens (the contract) and a small set of primitives (a convenience) for every Estiva app.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -91,7 +91,8 @@
|
|
|
91
91
|
"@tiptap/extension-mention": "^3.22.1",
|
|
92
92
|
"@tiptap/pm": "^3.22.1",
|
|
93
93
|
"@tiptap/react": "^3.22.1",
|
|
94
|
-
"@tiptap/suggestion": "^3.22.1"
|
|
94
|
+
"@tiptap/suggestion": "^3.22.1",
|
|
95
|
+
"@tiptap/extension-code-block-lowlight": "^3.22.1"
|
|
95
96
|
},
|
|
96
97
|
"peerDependenciesMeta": {
|
|
97
98
|
"eslint": {
|
|
@@ -117,6 +118,9 @@
|
|
|
117
118
|
},
|
|
118
119
|
"@tiptap/suggestion": {
|
|
119
120
|
"optional": true
|
|
121
|
+
},
|
|
122
|
+
"@tiptap/extension-code-block-lowlight": {
|
|
123
|
+
"optional": true
|
|
120
124
|
}
|
|
121
125
|
},
|
|
122
126
|
"devDependencies": {
|
|
@@ -130,6 +134,7 @@
|
|
|
130
134
|
"@testing-library/react": "^16.3.3",
|
|
131
135
|
"@testing-library/user-event": "^14.6.7",
|
|
132
136
|
"@tiptap/core": "^3.31.3",
|
|
137
|
+
"@tiptap/extension-code-block-lowlight": "^3.31.3",
|
|
133
138
|
"@tiptap/extension-mention": "^3.31.3",
|
|
134
139
|
"@tiptap/pm": "^3.31.3",
|
|
135
140
|
"@tiptap/react": "^3.31.3",
|
|
@@ -162,6 +167,7 @@
|
|
|
162
167
|
"dependencies": {
|
|
163
168
|
"@base-ui/react": "^1.8.0",
|
|
164
169
|
"clsx": "^2.1.1",
|
|
170
|
+
"lowlight": "^3.3.0",
|
|
165
171
|
"tailwind-merge": "^3.5.0"
|
|
166
172
|
},
|
|
167
173
|
"main": "./dist/index.js",
|
package/registry.json
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
"kind": "package",
|
|
5
5
|
"repo": "estiva-ui",
|
|
6
6
|
"package": "@estiva-app/ui",
|
|
7
|
-
"packageVersion": "0.
|
|
8
|
-
"exports":
|
|
7
|
+
"packageVersion": "0.64.0",
|
|
8
|
+
"exports": 140,
|
|
9
9
|
"typeExports": 145,
|
|
10
10
|
"files": null
|
|
11
11
|
},
|
|
@@ -1196,6 +1196,25 @@
|
|
|
1196
1196
|
"app": null,
|
|
1197
1197
|
"looks": []
|
|
1198
1198
|
},
|
|
1199
|
+
{
|
|
1200
|
+
"name": "CodeBlockColours",
|
|
1201
|
+
"repo": "estiva-ui",
|
|
1202
|
+
"kind": "component",
|
|
1203
|
+
"importPath": "@estiva-app/ui/editor",
|
|
1204
|
+
"sourceFile": "src/CodeBlockColours.ts",
|
|
1205
|
+
"purpose": "A code block in an editor, coloured by its language as you type.",
|
|
1206
|
+
"purposeFrom": "page",
|
|
1207
|
+
"props": [],
|
|
1208
|
+
"variants": [],
|
|
1209
|
+
"ownsBehaviours": [],
|
|
1210
|
+
"status": "stable",
|
|
1211
|
+
"migrationStage": null,
|
|
1212
|
+
"docsId": "primitives-codeblockcolours--docs",
|
|
1213
|
+
"storyId": "primitives-codeblockcolours--in-an-editor",
|
|
1214
|
+
"docPage": "src/CodeBlockColours.mdx",
|
|
1215
|
+
"app": null,
|
|
1216
|
+
"looks": []
|
|
1217
|
+
},
|
|
1199
1218
|
{
|
|
1200
1219
|
"name": "CollapsibleSection",
|
|
1201
1220
|
"repo": "estiva-ui",
|
|
@@ -3439,10 +3458,10 @@
|
|
|
3439
3458
|
"docPage": "src/Link.mdx",
|
|
3440
3459
|
"app": null,
|
|
3441
3460
|
"looks": [
|
|
3442
|
-
"text-
|
|
3443
|
-
"text-
|
|
3444
|
-
"text-
|
|
3445
|
-
"text-
|
|
3461
|
+
"text-text-interactive underline underline-offset-2 hover:opacity-80",
|
|
3462
|
+
"text-text-interactive underline underline-offset-2 hover:opacity-80 after:rounded-md focus-visible:outline-none",
|
|
3463
|
+
"text-text-interactive underline underline-offset-2 hover:opacity-80 truncate",
|
|
3464
|
+
"text-text-interactive underline underline-offset-2 hover:opacity-80 truncate after:rounded-md focus-visible:outline-none",
|
|
3446
3465
|
"hover:underline truncate after:rounded-md focus-visible:outline-none",
|
|
3447
3466
|
"underline underline-offset-2 hover:text-text-primary after:rounded-md focus-visible:outline-none",
|
|
3448
3467
|
"underline underline-offset-2 hover:text-text-primary truncate",
|
|
@@ -5394,15 +5413,15 @@
|
|
|
5394
5413
|
"docPage": "src/RichText.mdx",
|
|
5395
5414
|
"app": null,
|
|
5396
5415
|
"looks": [
|
|
5397
|
-
"[
|
|
5398
|
-
"[&
|
|
5399
|
-
"[&
|
|
5400
|
-
"[&
|
|
5401
|
-
"[&
|
|
5416
|
+
"[&_:is(ul,ol)]:pl-5 [&_li>p]:m-0 [&_li]:break-words [&_li]:marker:text-text-secondary [&_ol]:list-decimal [&_ul]:list-disc",
|
|
5417
|
+
"[&_blockquote>p]:m-0 [&_blockquote]:border-l-[3px] [&_blockquote]:border-text-muted [&_blockquote]:pl-2.5",
|
|
5418
|
+
"[&_:not(pre)>code]:rounded-sm [&_:not(pre)>code]:bg-bg-code [&_:not(pre)>code]:px-1 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:text-text-code [&_code]:font-mono [&_code]:text-[0.85em] [&_code]:text-text-primary",
|
|
5419
|
+
"[&_pre>code]:text-[1em] [&_pre]:whitespace-pre-wrap [&_pre]:break-words [&_pre]:rounded-md [&_pre]:bg-bg-code [&_pre]:px-3 [&_pre]:py-2.5 [&_pre]:font-mono [&_pre]:text-[0.85em] [&_pre]:leading-relaxed [&_pre]:text-text-primary",
|
|
5420
|
+
"[&_:is(.hljs-keyword,.hljs-doctag,.hljs-type,.hljs-template-tag,.hljs-template-variable)]:text-syntax-keyword [&_.hljs-title]:text-syntax-function [&_:is(.hljs-number,.hljs-literal,.hljs-attr,.hljs-attribute,.hljs-meta,.hljs-variable,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id)]:text-syntax-constant [&_:is(.hljs-string,.hljs-regexp)]:text-syntax-string [&_:is(.hljs-built_in,.hljs-symbol)]:text-syntax-variable [&_.hljs-comment]:text-syntax-comment [&_:is(.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo)]:text-syntax-tag",
|
|
5402
5421
|
"[&_: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",
|
|
5403
|
-
"[&_em]:italic [&_strong]:font-semibold [&_u]:underline [&_u]:underline-offset-2",
|
|
5404
|
-
"text-body-2 [&>:is(h2,h3)]:text-body-2 [&>:is(h2,h3)]:font-semibold [&>h1]:text-h4",
|
|
5405
|
-
"text-caption leading-snug [&>:is(h2,h3)]:text-caption [&>:is(h2,h3)]:font-semibold [&>h1]:text-body-2 [&>h1]:font-semibold"
|
|
5422
|
+
"[&_em]:italic [&_strong]:font-semibold [&_strong]:text-text-primary [&_u]:underline [&_u]:decoration-text-muted [&_u]:underline-offset-2",
|
|
5423
|
+
"text-body-2 leading-normal [&>:is(h2,h3)]:text-body-2 [&>:is(h2,h3)]:font-semibold [&>h1]:text-h4",
|
|
5424
|
+
"text-caption leading-snug [&>:is(h1,h2,h3)]:mb-0.5 [&>:is(h1,h2,h3)]:mt-1.5 [&>:is(h1,h2,h3)+:is(h1,h2,h3)]:mt-1 [&>:is(h1,h2,h3):last-child]:mb-0 [&>:is(h2,h3)]:text-caption [&>:is(h2,h3)]:font-semibold [&>h1]:text-body-2 [&>h1]:font-semibold"
|
|
5406
5425
|
]
|
|
5407
5426
|
},
|
|
5408
5427
|
{
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Meta, Canvas } from '@storybook/addon-docs/blocks'
|
|
2
|
+
import * as CodeBlockColoursStories from './CodeBlockColours.stories'
|
|
3
|
+
|
|
4
|
+
<Meta of={CodeBlockColoursStories} />
|
|
5
|
+
|
|
6
|
+
# CodeBlockColours
|
|
7
|
+
|
|
8
|
+
A code block in an editor, coloured by its language as you type. The colours
|
|
9
|
+
are the ones RichText reads it in, so writing looks the same as reading.
|
|
10
|
+
|
|
11
|
+
<Canvas of={CodeBlockColoursStories.InAnEditor} />
|
|
12
|
+
|
|
13
|
+
## When
|
|
14
|
+
|
|
15
|
+
- **Any editor that writes RichText**: a message box, an edit box, a
|
|
16
|
+
description. Use it in place of the starter kit's code block.
|
|
17
|
+
|
|
18
|
+
## When not
|
|
19
|
+
|
|
20
|
+
- Drawing text that is already written → **RichText**, which colours its code
|
|
21
|
+
blocks itself.
|
|
22
|
+
- Code in a sentence → the code mark. It is amber and takes no other colours.
|
|
23
|
+
|
|
24
|
+
## How
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import StarterKit from '@tiptap/starter-kit'
|
|
28
|
+
import { CodeBlockColours } from '@estiva-app/ui/editor'
|
|
29
|
+
import { richTextClassName } from '@estiva-app/ui'
|
|
30
|
+
|
|
31
|
+
useEditor({
|
|
32
|
+
extensions: [StarterKit.configure({ codeBlock: false }), CodeBlockColours],
|
|
33
|
+
editorProps: { attributes: { class: richTextClassName() } },
|
|
34
|
+
})
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- **The editor's root must wear `richTextClassName`.** The colours live in its
|
|
38
|
+
class list; without it the words are named but drawn in one colour.
|
|
39
|
+
- The block keeps the name `codeBlock` and its `language`, so text written
|
|
40
|
+
before it was added reads the same.
|
|
41
|
+
- It colours highlight.js's 37 everyday languages. A block in any other
|
|
42
|
+
language, or in none, stays one colour.
|
|
43
|
+
- The app installs `@tiptap/extension-code-block-lowlight`, as it installs the
|
|
44
|
+
other Tiptap parts.
|
|
45
|
+
|
|
46
|
+
## What it owns
|
|
47
|
+
|
|
48
|
+
Nothing. It names each word of a code block; the RichText class list colours them.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { EditorContent, useEditor } from '@tiptap/react'
|
|
3
|
+
import StarterKit from '@tiptap/starter-kit'
|
|
4
|
+
import { Card } from './Card'
|
|
5
|
+
import { CodeBlockColours } from './CodeBlockColours'
|
|
6
|
+
import { richTextClassName } from './RichText'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Type in the code block: each word takes its colour as you write, the same
|
|
10
|
+
* colours RichText reads it in. Change the theme to see each set.
|
|
11
|
+
*/
|
|
12
|
+
const meta = {
|
|
13
|
+
title: 'Primitives/CodeBlockColours',
|
|
14
|
+
parameters: { layout: 'padded' },
|
|
15
|
+
} satisfies Meta
|
|
16
|
+
|
|
17
|
+
export default meta
|
|
18
|
+
type Story = StoryObj
|
|
19
|
+
|
|
20
|
+
function Box() {
|
|
21
|
+
const editor = useEditor({
|
|
22
|
+
extensions: [StarterKit.configure({ codeBlock: false }), CodeBlockColours],
|
|
23
|
+
content: [
|
|
24
|
+
'<p>A code block in a language takes its colours:</p>',
|
|
25
|
+
'<pre><code class="language-ts">// Count the items that are done\nexport function countDone(items: Item[]): number {\n const done = items.filter((item) => item.state === \'done\')\n return done.length + 0\n}</code></pre>',
|
|
26
|
+
].join(''),
|
|
27
|
+
editorProps: { attributes: { class: richTextClassName('default', 'outline-none'), 'aria-label': 'Text' } },
|
|
28
|
+
})
|
|
29
|
+
return (
|
|
30
|
+
<Card fill="surface" className="w-full max-w-lg p-3">
|
|
31
|
+
<EditorContent editor={editor} />
|
|
32
|
+
</Card>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** An editor with a code block in TypeScript. Type in it. */
|
|
37
|
+
export const InAnEditor: Story = { render: () => <Box /> }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** The editor's code block, coloured by language like RichText's (`@estiva-app/ui/editor`). */
|
|
2
|
+
import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight'
|
|
3
|
+
import { lowlight } from './codeColours'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A code block in the editor, with the colours RichText reads it in (Katerina,
|
|
7
|
+
* 9 October): the same highlight.js languages, the same classes, and the
|
|
8
|
+
* editor's root wears `richTextClassName`, which colours them.
|
|
9
|
+
*
|
|
10
|
+
* It replaces the starter kit's code block: `StarterKit.configure({ codeBlock: false })`,
|
|
11
|
+
* then add `CodeBlockColours`. The block keeps its name, `codeBlock`, and its
|
|
12
|
+
* `language` attribute, so nothing already written changes.
|
|
13
|
+
*/
|
|
14
|
+
export const CodeBlockColours = CodeBlockLowlight.configure({ lowlight })
|
package/src/Link.mdx
CHANGED
|
@@ -14,7 +14,7 @@ the app decides how it navigates.
|
|
|
14
14
|
|
|
15
15
|
- **It takes the size and colour of the text around it**: put it inside that
|
|
16
16
|
text.
|
|
17
|
-
- **`text`** — a link written inside text: the
|
|
17
|
+
- **`text`** — a link written inside text: the theme's link colour, always
|
|
18
18
|
underlined, dimming on hover.
|
|
19
19
|
- **`quiet`** — a title or a time that is also a link. It takes the colour and
|
|
20
20
|
size of the text it sits in, and underlines on hover.
|
package/src/Link.stories.tsx
CHANGED
|
@@ -15,7 +15,7 @@ const meta = {
|
|
|
15
15
|
export default meta
|
|
16
16
|
type Story = StoryObj<typeof meta>
|
|
17
17
|
|
|
18
|
-
/** Inside written text: the
|
|
18
|
+
/** Inside written text: the theme's link colour, always underlined. Hover dims it. */
|
|
19
19
|
export const Text: Story = {
|
|
20
20
|
render: (args) => (
|
|
21
21
|
<p className="max-w-[480px] text-body-2 text-text-primary">
|
package/src/Link.test.tsx
CHANGED
|
@@ -53,10 +53,10 @@ describe('Link', () => {
|
|
|
53
53
|
expect(screen.getByRole('link', { name: 'Documents' }).getAttribute('href')).toBe('/documents')
|
|
54
54
|
})
|
|
55
55
|
|
|
56
|
-
it('text is the
|
|
56
|
+
it('text is the link colour, always underlined, and is the default', () => {
|
|
57
57
|
render(<Link href="#">Label</Link>)
|
|
58
58
|
const classes = classesOf(screen.getByRole('link'))
|
|
59
|
-
expect(classes).toContain('text-
|
|
59
|
+
expect(classes).toContain('text-text-interactive')
|
|
60
60
|
expect(classes).toContain('underline')
|
|
61
61
|
expect(classes).toContain('underline-offset-2')
|
|
62
62
|
})
|
package/src/Link.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import { cn } from './cn'
|
|
|
9
9
|
* on 13 September 2026, in three jobs (UIG-27). The looks are the ones those
|
|
10
10
|
* links already had, reduced by Katerina's ruling:
|
|
11
11
|
*
|
|
12
|
-
* - `text` — a link inside written text: the
|
|
12
|
+
* - `text` — a link inside written text: the theme's link colour, always underlined,
|
|
13
13
|
* dimming on hover. The look a link in a body of text already had.
|
|
14
14
|
* - `quiet` — a title or a timestamp that is also a link. It takes the colour
|
|
15
15
|
* and size of the text it sits in and underlines on hover. Two looks that
|
|
@@ -43,7 +43,7 @@ export type LinkVariant = 'text' | 'quiet' | 'underlined' | 'plain'
|
|
|
43
43
|
const COVER_CLASSES = 'after:absolute after:inset-0 after:rounded-md focus-visible:outline-none focus-visible:after:[outline-style:auto]'
|
|
44
44
|
|
|
45
45
|
const VARIANT_CLASSES: Record<LinkVariant, string> = {
|
|
46
|
-
text: 'text-
|
|
46
|
+
text: 'text-text-interactive underline underline-offset-2 hover:opacity-80',
|
|
47
47
|
quiet: 'hover:underline',
|
|
48
48
|
underlined: 'underline underline-offset-2 hover:text-text-primary',
|
|
49
49
|
plain: '',
|
package/src/RichText.stories.tsx
CHANGED
|
@@ -57,6 +57,7 @@ export const Links: Story = {
|
|
|
57
57
|
blocks: [
|
|
58
58
|
p(t('A link with its own words: [the guide](https://example.com/guide). A bare address: https://example.com/a/long/address/that/breaks/where/it/must.')),
|
|
59
59
|
p(t('Inside code it stays text: '), t('[not a link](https://example.com)', 'code')),
|
|
60
|
+
p(t('An '), t('underlined', 'underline'), t(' word beside [a link](https://example.com), and '), t('item.count', 'code'), t(' in a sentence.')),
|
|
60
61
|
],
|
|
61
62
|
},
|
|
62
63
|
}
|
|
@@ -89,3 +90,26 @@ export const UnknownBlock: Story = {
|
|
|
89
90
|
blocks: [p(t('The block below is of a kind this part does not know.')), { type: 'unknown', typeName: 'later', inline: [t('Its words are still here.')] }],
|
|
90
91
|
},
|
|
91
92
|
}
|
|
93
|
+
|
|
94
|
+
/** A code block in a named language. */
|
|
95
|
+
export const CodeInALanguage: Story = {
|
|
96
|
+
args: {
|
|
97
|
+
blocks: [
|
|
98
|
+
{
|
|
99
|
+
type: 'codeBlock',
|
|
100
|
+
language: 'ts',
|
|
101
|
+
inline: [
|
|
102
|
+
t(
|
|
103
|
+
[
|
|
104
|
+
'// Count the items that are done',
|
|
105
|
+
'export function countDone(items: Item[]): number {',
|
|
106
|
+
" const done = items.filter((item) => item.state === 'done')",
|
|
107
|
+
' return done.length + 0',
|
|
108
|
+
'}',
|
|
109
|
+
].join('\n'),
|
|
110
|
+
),
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
}
|
package/src/RichText.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { MouseEvent, ReactNode } from 'react'
|
|
2
2
|
import { cn } from './cn'
|
|
3
3
|
import { Link } from './Link'
|
|
4
|
+
import { codeColours } from './codeColours'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Formatted text, drawn: headings, paragraphs, lists, a quote, code, a divider,
|
|
@@ -11,7 +12,8 @@ import { Link } from './Link'
|
|
|
11
12
|
* and the two had drifted: headings, list markers, quotes and code differed,
|
|
12
13
|
* one app had tables and the other dropped them, one had links and the other
|
|
13
14
|
* 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
|
+
* 28 September, one per difference, most of them Peek's, with the room and
|
|
16
|
+
* colours she changed on 9 October.
|
|
15
17
|
*
|
|
16
18
|
* **It takes the tree, not the text.** The caller parses (the format comes from
|
|
17
19
|
* the item's tag, never from the body) and hands the blocks in. So this package
|
|
@@ -84,21 +86,32 @@ export interface RichTextProps {
|
|
|
84
86
|
* paragraph inside each list item, quote and table cell (the `> p` lines).
|
|
85
87
|
*/
|
|
86
88
|
const RICH_TEXT_CLASSES = {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
/* Blocks 8px apart (Katerina, 9 October; 4 before). A list's own items stay together. */
|
|
90
|
+
base: 'flex flex-col gap-2 break-words text-text-secondary',
|
|
91
|
+
/* A heading binds to what follows it: 12px above (4 and the 8px gap), 8px below (the gap); two headings in a row, 12px apart. The same as before the gap grew (Katerina, 9 October: headings do not change). Level 3 keeps the text's colour, one step lower: at the same size as level 2, colour is what tells them apart (Katerina, 9 October). */
|
|
92
|
+
headings: '[&>:is(h1,h2,h3)]:mt-1 [&>:is(h1,h2,h3):first-child]:mt-0 [&>:is(h1,h2)]:text-text-primary',
|
|
93
|
+
/* Bullets and numbers in the text's own colour, so a number can be read (Katerina, 9 October; they were the muted grey, 2.4 : 1 in Leaf). */
|
|
94
|
+
lists: '[&_:is(ul,ol)]:pl-5 [&_li>p]:m-0 [&_li]:break-words [&_li]:marker:text-text-secondary [&_ol]:list-decimal [&_ul]:list-disc',
|
|
95
|
+
/* The line in the muted text grey, not the border grey: a border grey was 1.3 : 1 in Leaf (Katerina, 9 October). */
|
|
91
96
|
// 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-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
quote: '[&_blockquote>p]:m-0 [&_blockquote]:border-l-[3px] [&_blockquote]:border-text-muted [&_blockquote]:pl-2.5',
|
|
98
|
+
/* Code in a sentence is amber, its own colour (Katerina, 9 October: option I). */
|
|
99
|
+
// eslint-disable-next-line token-values/no-restricted-classes -- @estiva-escape: code is 0.85 of the text around it, so it scales with a heading and with `small`; a named size is fixed
|
|
100
|
+
code: '[&_:not(pre)>code]:rounded-sm [&_:not(pre)>code]:bg-bg-code [&_:not(pre)>code]:px-1 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:text-text-code [&_code]:font-mono [&_code]:text-[0.85em] [&_code]:text-text-primary',
|
|
101
|
+
/* 12px either side, 10px above and below, looser lines than the text (Katerina, 9 October; 8px all round before). */
|
|
102
|
+
// eslint-disable-next-line token-values/no-restricted-classes -- @estiva-escape: code is 0.85 of the text around it; the code inside the block is the block's own size, not 0.85 of it again
|
|
103
|
+
codeBlock: '[&_pre>code]:text-[1em] [&_pre]:whitespace-pre-wrap [&_pre]:break-words [&_pre]:rounded-md [&_pre]:bg-bg-code [&_pre]:px-3 [&_pre]:py-2.5 [&_pre]:font-mono [&_pre]:text-[0.85em] [&_pre]:leading-relaxed [&_pre]:text-text-primary',
|
|
104
|
+
/* Colours inside a code block, by language: the classes `codeColours` gives each word, after highlight.js's own GitHub theme (Katerina, 9 October). */
|
|
105
|
+
syntax: '[&_:is(.hljs-keyword,.hljs-doctag,.hljs-type,.hljs-template-tag,.hljs-template-variable)]:text-syntax-keyword [&_.hljs-title]:text-syntax-function [&_:is(.hljs-number,.hljs-literal,.hljs-attr,.hljs-attribute,.hljs-meta,.hljs-variable,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id)]:text-syntax-constant [&_:is(.hljs-string,.hljs-regexp)]:text-syntax-string [&_:is(.hljs-built_in,.hljs-symbol)]:text-syntax-variable [&_.hljs-comment]:text-syntax-comment [&_:is(.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo)]:text-syntax-tag',
|
|
106
|
+
/* A divider splits the text in two: 16px either side (Katerina, 9 October; 4 before). */
|
|
107
|
+
divider: '[&_hr]:my-2 [&_hr]:border-border-default',
|
|
98
108
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
/* Bold in the heading colour, not only heavier. An underlined word draws a faint line, so it does not read as a link (Katerina, 9 October). */
|
|
110
|
+
marks: '[&_em]:italic [&_strong]:font-semibold [&_strong]:text-text-primary [&_u]:underline [&_u]:decoration-text-muted [&_u]:underline-offset-2',
|
|
111
|
+
/* Lines 1.5 apart (Katerina, 9 October; 1.4 before). */
|
|
112
|
+
default: 'text-body-2 leading-normal [&>:is(h2,h3)]:text-body-2 [&>:is(h2,h3)]:font-semibold [&>h1]:text-h4',
|
|
113
|
+
/* Blocks 6px apart; a heading keeps its 12px above and 8px below. */
|
|
114
|
+
small: 'gap-1.5 text-caption leading-snug [&>:is(h1,h2,h3)]:mb-0.5 [&>:is(h1,h2,h3)]:mt-1.5 [&>:is(h1,h2,h3)+:is(h1,h2,h3)]:mt-1 [&>:is(h1,h2,h3):last-child]:mb-0 [&>:is(h2,h3)]:text-caption [&>:is(h2,h3)]:font-semibold [&>h1]:text-body-2 [&>h1]:font-semibold',
|
|
102
115
|
}
|
|
103
116
|
|
|
104
117
|
/**
|
|
@@ -106,8 +119,8 @@ const RICH_TEXT_CLASSES = {
|
|
|
106
119
|
* being written looks the same as what is read. Put it on the editor's root.
|
|
107
120
|
*/
|
|
108
121
|
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)
|
|
122
|
+
const { base, headings, lists, quote, code, codeBlock, syntax, divider, table, marks } = RICH_TEXT_CLASSES
|
|
123
|
+
return cn(base, headings, lists, quote, code, codeBlock, syntax, divider, table, marks, RICH_TEXT_CLASSES[size], className)
|
|
111
124
|
}
|
|
112
125
|
|
|
113
126
|
/**
|
|
@@ -251,9 +264,10 @@ function Block({ block, draw }: { block: RichTextBlock; draw: Draw }): ReactNode
|
|
|
251
264
|
return <blockquote data-block-id={block.id}>{runs}</blockquote>
|
|
252
265
|
case 'codeBlock':
|
|
253
266
|
// Literal: no marks, no links, no chips. A long line wraps; nothing scrolls sideways.
|
|
267
|
+
// In a language highlight.js knows, each word takes its colour; in any other, the text as it is.
|
|
254
268
|
return (
|
|
255
269
|
<pre data-block-id={block.id} data-language={block.language}>
|
|
256
|
-
<code>{(block.inline ?? []).map((run) => run.text).join('')}</code>
|
|
270
|
+
<code>{codeColours((block.inline ?? []).map((run) => run.text).join(''), block.language)}</code>
|
|
257
271
|
</pre>
|
|
258
272
|
)
|
|
259
273
|
case 'horizontalRule':
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createElement, type ReactNode } from 'react'
|
|
2
|
+
import { common, createLowlight } from 'lowlight'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Colours inside a code block, by language (Katerina, 9 October).
|
|
6
|
+
*
|
|
7
|
+
* highlight.js reads the code and names each word (a keyword, a string, a
|
|
8
|
+
* comment); RichText's class list gives each name its colour from the theme's
|
|
9
|
+
* `syntax-*` tokens. The same instance colours the editor's code block
|
|
10
|
+
* (`CodeBlockColours`), so what is written looks the same as what is read.
|
|
11
|
+
*
|
|
12
|
+
* `common` is highlight.js's 37 everyday languages. A language outside them, or
|
|
13
|
+
* none, leaves the code in one colour.
|
|
14
|
+
*/
|
|
15
|
+
export const lowlight = createLowlight(common)
|
|
16
|
+
|
|
17
|
+
interface HastNode {
|
|
18
|
+
type: string
|
|
19
|
+
value?: string
|
|
20
|
+
properties?: { className?: string[] }
|
|
21
|
+
children?: HastNode[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function draw(node: HastNode, key: number): ReactNode {
|
|
25
|
+
if (node.type === 'text') return node.value
|
|
26
|
+
return createElement('span', { key, className: node.properties?.className?.join(' ') }, (node.children ?? []).map(draw))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The code, its words wrapped in highlight.js's classes; the text as it is when the language is unknown. */
|
|
30
|
+
export function codeColours(text: string, language?: string): ReactNode {
|
|
31
|
+
if (!language || !lowlight.registered(language)) return text
|
|
32
|
+
return (lowlight.highlight(language, text).children as HastNode[]).map(draw)
|
|
33
|
+
}
|
package/src/editor.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export { KeptSelection, SelectionToolbar, normalizeHref, type MarkId, type SelectionToolbarProps } from './SelectionToolbar'
|
|
11
11
|
export { PastedImages, pastedImages, pastedImageName, type PastedImagesOptions, type ClipboardSource } from './PastedImages'
|
|
12
|
+
export { CodeBlockColours } from './CodeBlockColours'
|
|
12
13
|
export { suggestionPopup, isSuggestionActive, isSuggestionOpen, type SuggestionPopupOptions } from './suggestionPopup'
|
|
13
14
|
export { EditorBlockHandle, blockDropCursor, deleteBlock, moveBlock, duplicateBlock, startBlockDrag, type EditorBlockHandleProps, type BlockTurnInto } from './EditorBlockHandle'
|
|
14
15
|
export {
|
package/src/pages.test.ts
CHANGED
|
@@ -41,6 +41,7 @@ const NOTHING = 'Nothing. It only draws. Clicks and keys are yours.'
|
|
|
41
41
|
const HOLDS_ONLY: Record<string, string> = {
|
|
42
42
|
AppProvider: 'Nothing. It draws nothing; it holds two answers for the parts below it.',
|
|
43
43
|
PastedImages: 'Nothing. It draws nothing; it hands a pasted picture to the app, named.',
|
|
44
|
+
CodeBlockColours: 'Nothing. It names each word of a code block; the RichText class list colours them.',
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
const dir = new URL('.', import.meta.url)
|
|
@@ -26,7 +26,7 @@ import { SCHEMA_VERSION, validateRegistry, type Registry, type RegistryEntry } f
|
|
|
26
26
|
const root = process.cwd()
|
|
27
27
|
const registry = buildRegistry({ root })
|
|
28
28
|
/** Editor extensions: named like components by the catalogue, and not React components. */
|
|
29
|
-
const EDITOR_EXTENSIONS = ['KeptSelection', 'BlockId', 'UnknownBlock', 'ReferenceNode', 'AttachmentNode', 'PersonMention', 'UrgentPersonMention', 'CaptionedReference', 'ReferenceTrigger', 'SlashCommands', 'PastedImages']
|
|
29
|
+
const EDITOR_EXTENSIONS = ['KeptSelection', 'BlockId', 'UnknownBlock', 'ReferenceNode', 'AttachmentNode', 'PersonMention', 'UrgentPersonMention', 'CaptionedReference', 'ReferenceTrigger', 'SlashCommands', 'PastedImages', 'CodeBlockColours']
|
|
30
30
|
const entry = (name: string): RegistryEntry => {
|
|
31
31
|
const found = registry.entries.find((candidate) => candidate.name === name)
|
|
32
32
|
if (!found) throw new Error(`no entry for ${name}`)
|
|
@@ -48,19 +48,19 @@ 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 140 names over 76 files: 107 components, 32 helpers and 1 hook (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)', () => {
|
|
52
52
|
// The reconciliation GATES.md §24 explains. If this changes, that changes.
|
|
53
|
-
// The
|
|
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: 107, helper: 32, hook: 1 })
|
|
58
|
+
expect(new Set(registry.entries.map((one) => one.sourceFile)).size).toBe(76)
|
|
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(
|
|
63
|
+
expect(registry.entries.filter((one) => one.purposeFrom === 'page').length).toBe(72)
|
|
64
64
|
expect(registry.entries.filter((one) => one.purposeFrom === 'comment').length).toBe(68)
|
|
65
65
|
})
|
|
66
66
|
|
package/stories/Choosing.mdx
CHANGED
|
@@ -97,6 +97,7 @@ fork freely, owe nothing back, mention it on the package's ticket.
|
|
|
97
97
|
| A link — in text, on a title, beside a note, or a whole box | **Link** — `text`, `quiet`, `underlined`, `plain`; `external` for a new tab |
|
|
98
98
|
| A document or an image attached to something, posted or waiting to be sent | **AttachmentCard** — the file decides the shape; `pending` in a composer |
|
|
99
99
|
| A picture pasted into an editor, attached rather than pasted | **PastedImages** — from `@estiva-app/ui/editor`; the app hands it its upload |
|
|
100
|
+
| 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 |
|
|
100
101
|
| A person or a thing named inside a sentence | **InlineChip** — one line high, level with the words; `href` makes it a link |
|
|
101
102
|
| 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 |
|
|
102
103
|
| Formatted text: headings, lists, a quote, code, a table, links | **RichText** — pass the parsed tree; `small` inside a card |
|
package/tailwind-preset.js
CHANGED
|
@@ -155,6 +155,14 @@ export default {
|
|
|
155
155
|
'bg-tooltip-key': 'var(--bg-tooltip-key)',
|
|
156
156
|
'bg-field': 'var(--bg-field)',
|
|
157
157
|
'bg-code': 'var(--bg-code)',
|
|
158
|
+
'text-code': 'var(--text-code)',
|
|
159
|
+
'syntax-keyword': 'var(--syntax-keyword)',
|
|
160
|
+
'syntax-function': 'var(--syntax-function)',
|
|
161
|
+
'syntax-constant': 'var(--syntax-constant)',
|
|
162
|
+
'syntax-string': 'var(--syntax-string)',
|
|
163
|
+
'syntax-variable': 'var(--syntax-variable)',
|
|
164
|
+
'syntax-comment': 'var(--syntax-comment)',
|
|
165
|
+
'syntax-tag': 'var(--syntax-tag)',
|
|
158
166
|
// text
|
|
159
167
|
'text-primary': 'var(--text-primary)',
|
|
160
168
|
'text-secondary': 'var(--text-secondary)',
|
package/tokens.css
CHANGED
|
@@ -101,6 +101,16 @@
|
|
|
101
101
|
/* Code, in a sentence and as a block, and a table's header row (RichText, UIG-30).
|
|
102
102
|
Here, the hover fill: bg-active, #D4D4D8, was the darkest thing on a white card (Katerina, 28 September). */
|
|
103
103
|
--bg-code: #E9E9EB;
|
|
104
|
+
/* Inline code's text: amber, on --bg-code (Katerina, 9 October: option I). Darker than the warning amber, which is 3.8 : 1 on the code fill; this is 4.8. */
|
|
105
|
+
--text-code: #855f00;
|
|
106
|
+
/* Colours inside a code block, by language (Katerina, 9 October). GitHub's light set; the keyword one shade darker (#cf222e is 4.4 : 1 on our fill, this 4.9). */
|
|
107
|
+
--syntax-keyword: #c21f2b;
|
|
108
|
+
--syntax-function: #6639ba;
|
|
109
|
+
--syntax-constant: #0550ae;
|
|
110
|
+
--syntax-string: #0a3069;
|
|
111
|
+
--syntax-variable: #953800;
|
|
112
|
+
--syntax-comment: #59636e;
|
|
113
|
+
--syntax-tag: #116329;
|
|
104
114
|
}
|
|
105
115
|
|
|
106
116
|
/* ─── dark ─── Peek's dark theme (peek/src/index.css .dark, 2026-08-28). */
|
|
@@ -184,6 +194,16 @@
|
|
|
184
194
|
/* Code, in a sentence and as a block, and a table's header row (RichText, UIG-30).
|
|
185
195
|
Here, bg-active, as Peek draws code (Katerina, 28 September). */
|
|
186
196
|
--bg-code: #2D2D31;
|
|
197
|
+
/* Inline code's text: amber, on --bg-code (Katerina, 9 October: option I). Amber 400, as on GitHub's dark ground. */
|
|
198
|
+
--text-code: #fbbf24;
|
|
199
|
+
/* Colours inside a code block, by language (Katerina, 9 October). GitHub's dark set. */
|
|
200
|
+
--syntax-keyword: #ff7b72;
|
|
201
|
+
--syntax-function: #d2a8ff;
|
|
202
|
+
--syntax-constant: #79c0ff;
|
|
203
|
+
--syntax-string: #a5d6ff;
|
|
204
|
+
--syntax-variable: #ffa657;
|
|
205
|
+
--syntax-comment: #9198a1;
|
|
206
|
+
--syntax-tag: #7ee787;
|
|
187
207
|
}
|
|
188
208
|
|
|
189
209
|
|
|
@@ -271,6 +291,16 @@
|
|
|
271
291
|
/* Code, in a sentence and as a block, and a table's header row (RichText, UIG-30).
|
|
272
292
|
Here, bg-active, as Peek draws code (Katerina, 28 September). */
|
|
273
293
|
--bg-code: rgba(255, 255, 255, 0.08);
|
|
294
|
+
/* Inline code's text: amber, on --bg-code (Katerina, 9 October: option I). Signal's warning amber. */
|
|
295
|
+
--text-code: #ffb020;
|
|
296
|
+
/* Colours inside a code block, by language (Katerina, 9 October). GitHub's dark set. */
|
|
297
|
+
--syntax-keyword: #ff7b72;
|
|
298
|
+
--syntax-function: #d2a8ff;
|
|
299
|
+
--syntax-constant: #79c0ff;
|
|
300
|
+
--syntax-string: #a5d6ff;
|
|
301
|
+
--syntax-variable: #ffa657;
|
|
302
|
+
--syntax-comment: #9198a1;
|
|
303
|
+
--syntax-tag: #7ee787;
|
|
274
304
|
}
|
|
275
305
|
|
|
276
306
|
/* ─── ship ─── Ship's palette (Katerina's ruling, 2026-08-28; ship/web/src/index.css). */
|
|
@@ -372,6 +402,16 @@
|
|
|
372
402
|
/* Code, in a sentence and as a block, and a table's header row (RichText, UIG-30).
|
|
373
403
|
Here, bg-active, as Peek draws code (Katerina, 28 September). */
|
|
374
404
|
--bg-code: rgba(255, 255, 255, 0.08);
|
|
405
|
+
/* Inline code's text: amber, on --bg-code (Katerina, 9 October: option I). Ship's warning amber. */
|
|
406
|
+
--text-code: #e0b04a;
|
|
407
|
+
/* Colours inside a code block, by language (Katerina, 9 October). GitHub's dark set. */
|
|
408
|
+
--syntax-keyword: #ff7b72;
|
|
409
|
+
--syntax-function: #d2a8ff;
|
|
410
|
+
--syntax-constant: #79c0ff;
|
|
411
|
+
--syntax-string: #a5d6ff;
|
|
412
|
+
--syntax-variable: #ffa657;
|
|
413
|
+
--syntax-comment: #9198a1;
|
|
414
|
+
--syntax-tag: #7ee787;
|
|
375
415
|
}
|
|
376
416
|
|
|
377
417
|
/* ─── leaf ─── Leaf's theme: Linear's light theme on these same tokens (Katerina,
|
|
@@ -465,4 +505,14 @@
|
|
|
465
505
|
/* Code, in a sentence and as a block, and a table's header row (RichText, UIG-30).
|
|
466
506
|
Here, the inset fill: lighter than bg-active on a white card (Katerina, 28 September). */
|
|
467
507
|
--bg-code: #e9e9ea;
|
|
508
|
+
/* Inline code's text: amber, on --bg-code (Katerina, 9 October: option I). Darker than Leaf's warning amber (#967000, 3.8 : 1 on the code fill); this is 4.8. */
|
|
509
|
+
--text-code: #855f00;
|
|
510
|
+
/* Colours inside a code block, by language (Katerina, 9 October). GitHub's light set; the keyword one shade darker (#cf222e is 4.4 : 1 on our fill, this 4.9). */
|
|
511
|
+
--syntax-keyword: #c21f2b;
|
|
512
|
+
--syntax-function: #6639ba;
|
|
513
|
+
--syntax-constant: #0550ae;
|
|
514
|
+
--syntax-string: #0a3069;
|
|
515
|
+
--syntax-variable: #953800;
|
|
516
|
+
--syntax-comment: #59636e;
|
|
517
|
+
--syntax-tag: #116329;
|
|
468
518
|
}
|