@glw907/cairn-cms 0.52.1 → 0.54.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/CHANGELOG.md +46 -0
- package/dist/components/AdminLayout.svelte +58 -23
- package/dist/components/EditPage.svelte +456 -124
- package/dist/components/EditPage.svelte.d.ts +4 -2
- package/dist/components/EditorToolbar.svelte +29 -53
- package/dist/components/EditorToolbar.svelte.d.ts +3 -11
- package/dist/components/MarkdownEditor.svelte +163 -24
- package/dist/components/MarkdownEditor.svelte.d.ts +3 -0
- package/dist/components/MarkdownHelpDialog.svelte +5 -0
- package/dist/components/ShortcutsDialog.svelte +37 -0
- package/dist/components/ShortcutsDialog.svelte.d.ts +13 -0
- package/dist/components/ShortcutsGrid.svelte +18 -0
- package/dist/components/ShortcutsGrid.svelte.d.ts +23 -0
- package/dist/components/cairn-admin.css +199 -99
- package/dist/components/editor-folding.d.ts +7 -0
- package/dist/components/editor-folding.js +331 -0
- package/dist/components/editor-highlight.js +55 -6
- package/dist/components/editor-shortcuts.d.ts +16 -0
- package/dist/components/editor-shortcuts.js +36 -0
- package/dist/components/fonts/{Figtree-OFL.txt → IBMPlexSans-OFL.txt} +2 -2
- package/dist/components/fonts/ibm-plex-sans.woff2 +0 -0
- package/dist/components/markdown-directives.d.ts +17 -0
- package/dist/components/markdown-directives.js +41 -0
- package/dist/components/topbar-context.d.ts +13 -0
- package/dist/components/topbar-context.js +17 -0
- package/dist/sveltekit/static-admin-page.js +2 -2
- package/package.json +1 -1
- package/src/lib/components/AdminLayout.svelte +58 -23
- package/src/lib/components/EditPage.svelte +456 -124
- package/src/lib/components/EditorToolbar.svelte +29 -53
- package/src/lib/components/MarkdownEditor.svelte +163 -24
- package/src/lib/components/MarkdownHelpDialog.svelte +5 -0
- package/src/lib/components/ShortcutsDialog.svelte +37 -0
- package/src/lib/components/ShortcutsGrid.svelte +18 -0
- package/src/lib/components/cairn-admin.css +51 -14
- package/src/lib/components/editor-folding.ts +356 -0
- package/src/lib/components/editor-highlight.ts +54 -4
- package/src/lib/components/editor-shortcuts.ts +42 -0
- package/src/lib/components/fonts/{Figtree-OFL.txt → IBMPlexSans-OFL.txt} +2 -2
- package/src/lib/components/fonts/ibm-plex-sans.woff2 +0 -0
- package/src/lib/components/markdown-directives.ts +42 -0
- package/src/lib/components/topbar-context.ts +30 -0
- package/src/lib/sveltekit/static-admin-page.ts +2 -2
- package/dist/components/fonts/figtree.woff2 +0 -0
- package/src/lib/components/fonts/figtree.woff2 +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Cairn's own typefaces, self-hosted so the admin needs no external font request.
|
|
1
|
+
/* Cairn's own typefaces, self-hosted so the admin needs no external font request. IBM Plex Sans carries
|
|
2
2
|
the body and UI (friendly, highly legible at small sizes); Bricolage Grotesque gives the brand and
|
|
3
3
|
the page headings a distinct voice; iA Writer Mono is the editor writing surface. The first two are
|
|
4
4
|
variable (one file spans the weight range), the editor face ships four static files, and all three
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
of use, so this fully overrides the host's theme with no @plugin and no host build step. */
|
|
11
11
|
[data-theme='cairn-admin'] {
|
|
12
12
|
color-scheme: light;
|
|
13
|
-
--font-body: '
|
|
13
|
+
--font-body: 'IBM Plex Sans Variable', system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
14
14
|
--font-display: 'Bricolage Grotesque Variable', var(--font-body);
|
|
15
15
|
--font-editor: 'iA Writer Mono', ui-monospace, monospace;
|
|
16
16
|
font-family: var(--font-body);
|
|
@@ -47,8 +47,9 @@
|
|
|
47
47
|
--cairn-directive-rail-2: 82%;
|
|
48
48
|
--cairn-directive-rail-3: 92%;
|
|
49
49
|
/* Cursor-aware emphasis: the caret container's own bar steps to a full-strength accent rail
|
|
50
|
-
(
|
|
51
|
-
accent (5.28:1, non-text floor 3:1), ink 46% (7.47:1). Do not
|
|
50
|
+
at the same 2px width (strength only) and an ink one notch past depth 3. Locked pairs on
|
|
51
|
+
base-100: rail = solid accent (5.28:1, non-text floor 3:1), ink 46% (7.47:1). Do not
|
|
52
|
+
lighten without re-checking. */
|
|
52
53
|
--cairn-directive-rail-active: 100%;
|
|
53
54
|
--cairn-directive-ink-active: oklch(46% 0.16 300);
|
|
54
55
|
/* The editor's inline-code chip, a quiet tint beside base-200. Locked pair: base-content ink
|
|
@@ -63,6 +64,14 @@
|
|
|
63
64
|
ever sits on; on the chips it measured as low as 2.63:1. Do not lighten without
|
|
64
65
|
re-checking. */
|
|
65
66
|
--cairn-focus-dim-ink: oklch(66% 0.01 75);
|
|
67
|
+
/* Focus mode's rail percentages: the directive bars dim with their text (about a third of the
|
|
68
|
+
quiet 72/82/92 ramp), so a dimmed block keeps its structure without staying the one
|
|
69
|
+
chromatic object in the field. Deliberately sub-3:1, the same transient-state call as the
|
|
70
|
+
dim ink; the quiet ramp is one toggle away. */
|
|
71
|
+
--cairn-focus-dim-rail-1: 24%;
|
|
72
|
+
--cairn-focus-dim-rail-2: 28%;
|
|
73
|
+
--cairn-focus-dim-rail-3: 32%;
|
|
74
|
+
--cairn-focus-dim-rail-active: 36%;
|
|
66
75
|
--color-neutral: oklch(32% 0.012 75);
|
|
67
76
|
--color-neutral-content: oklch(96% 0.004 75);
|
|
68
77
|
|
|
@@ -103,7 +112,7 @@
|
|
|
103
112
|
>= 4.5:1 contrast on the dark bases. The polish pass tunes these values against the reference. */
|
|
104
113
|
[data-theme='cairn-admin-dark'] {
|
|
105
114
|
color-scheme: dark;
|
|
106
|
-
--font-body: '
|
|
115
|
+
--font-body: 'IBM Plex Sans Variable', system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
107
116
|
--font-display: 'Bricolage Grotesque Variable', var(--font-body);
|
|
108
117
|
--font-editor: 'iA Writer Mono', ui-monospace, monospace;
|
|
109
118
|
font-family: var(--font-body);
|
|
@@ -119,6 +128,9 @@
|
|
|
119
128
|
--color-base-300: oklch(30% 0.014 75);
|
|
120
129
|
--color-base-content: oklch(93% 0.006 75);
|
|
121
130
|
|
|
131
|
+
/* Locked pair: pressed footer-toggle text (text-primary on the bg-primary/10 plate over
|
|
132
|
+
dark base-100) measures 4.66:1, only 0.16 above the AA floor. Do not darken dark
|
|
133
|
+
--color-primary without re-checking that pair. */
|
|
122
134
|
--color-primary: oklch(68% 0.18 293);
|
|
123
135
|
--color-primary-content: oklch(20% 0.04 293);
|
|
124
136
|
--color-secondary: oklch(72% 0.02 75);
|
|
@@ -139,10 +151,10 @@
|
|
|
139
151
|
--cairn-directive-rail-2: 74%;
|
|
140
152
|
--cairn-directive-rail-3: 86%;
|
|
141
153
|
/* Cursor-aware emphasis on dark: the caret container's own bar steps to a solid accent rail
|
|
142
|
-
(
|
|
143
|
-
accent (5.86:1, non-text floor 3:1), ink 82% (8.84:1, priced on the
|
|
144
|
-
render; the 0.14 chroma sits outside sRGB at this lightness). Do not
|
|
145
|
-
re-checking. */
|
|
154
|
+
at the same 2px width (strength only) and an ink one notch past depth 3. Locked pairs on
|
|
155
|
+
base-100: rail = solid accent (5.86:1, non-text floor 3:1), ink 82% (8.84:1, priced on the
|
|
156
|
+
gamut-clipped sRGB render; the 0.14 chroma sits outside sRGB at this lightness). Do not
|
|
157
|
+
darken without re-checking. */
|
|
146
158
|
--cairn-directive-rail-active: 100%;
|
|
147
159
|
--cairn-directive-ink-active: oklch(82% 0.14 300);
|
|
148
160
|
/* The inline-code chip on dark sits one step above base-100 so it reads raised. Locked pair:
|
|
@@ -153,6 +165,12 @@
|
|
|
153
165
|
the proposed 50% measured 2.74:1, under the 3:1 floor, so it sits at 53%. Locked pair on
|
|
154
166
|
base-100: 3.12:1. Do not darken without re-checking. */
|
|
155
167
|
--cairn-focus-dim-ink: oklch(53% 0.01 75);
|
|
168
|
+
/* Focus mode's rail percentages on dark, a third of the quiet 62/74/86 ramp; the same
|
|
169
|
+
deliberate sub-3:1 transient-state call as the dim ink. */
|
|
170
|
+
--cairn-focus-dim-rail-1: 21%;
|
|
171
|
+
--cairn-focus-dim-rail-2: 25%;
|
|
172
|
+
--cairn-focus-dim-rail-3: 29%;
|
|
173
|
+
--cairn-focus-dim-rail-active: 33%;
|
|
156
174
|
--color-neutral: oklch(80% 0.01 75);
|
|
157
175
|
--color-neutral-content: oklch(22% 0.008 75);
|
|
158
176
|
|
|
@@ -260,6 +278,13 @@
|
|
|
260
278
|
outline-offset: -1px;
|
|
261
279
|
}
|
|
262
280
|
|
|
281
|
+
/* Under focus mode the document title eases back with the rest of the context: at 30px bold
|
|
282
|
+
it would otherwise be the strongest ink on the page and pull the eye off the lit
|
|
283
|
+
paragraph. Its own focus restores full ink, so editing the title is never dimmed. */
|
|
284
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .cairn-doc-title-dim:not(:focus) {
|
|
285
|
+
color: var(--cairn-focus-dim-ink);
|
|
286
|
+
}
|
|
287
|
+
|
|
263
288
|
/* Menu items come as anchors or buttons, and the omitted Preflight is what made them match:
|
|
264
289
|
without it a button keeps the UA chrome (outset border, gray fill, centered system-font
|
|
265
290
|
text) while its anchor siblings render flat. This scoped substitute levels the buttons to
|
|
@@ -274,12 +299,24 @@
|
|
|
274
299
|
text-align: start;
|
|
275
300
|
}
|
|
276
301
|
|
|
277
|
-
/* The
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
302
|
+
/* The same leveling for every bare admin button, not only menu items: a button styled with
|
|
303
|
+
utilities (the footer's posture segments and mode toggles, a list's sort-column headers, the
|
|
304
|
+
zen chip's exit) otherwise keeps the UA outset border and gray fill, since the admin omits
|
|
305
|
+
global Preflight. A .btn keeps its full DaisyUI chrome, and a utility (a bg-*, a border-l for
|
|
306
|
+
a segment divider) still wins from the utilities layer, so a control opts back into a border
|
|
307
|
+
or fill by naming it. */
|
|
308
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) button:not(.btn) {
|
|
309
|
+
border: 0 solid;
|
|
310
|
+
background-color: transparent;
|
|
311
|
+
font: inherit;
|
|
312
|
+
color: inherit;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* The one header band is the 4rem topbar, and a control the browser scrolls into view could land
|
|
316
|
+
hidden beneath it (WCAG 2.4.11 Focus Not Obscured). The scroll margin keeps any focus or
|
|
317
|
+
fragment scroll clear of the sticky band, with a little slack. */
|
|
281
318
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) :is(a, button, input, textarea, select, summary, [tabindex]) {
|
|
282
|
-
scroll-margin-top:
|
|
319
|
+
scroll-margin-top: 5.5rem;
|
|
283
320
|
}
|
|
284
321
|
}
|
|
285
322
|
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
// Container folding: CodeMirror's fold system driven by cairn's directive grammar, with the rail
|
|
2
|
+
// band as the affordance. Client-only like editor-highlight and editor-modes; MarkdownEditor
|
|
3
|
+
// reaches this module through a dynamic import, so the static @codemirror imports here never enter
|
|
4
|
+
// a server bundle (guarded by the editor-boundary test).
|
|
5
|
+
//
|
|
6
|
+
// The architecture (plan decision 4): @codemirror/language's codeFolding plus foldEffect/
|
|
7
|
+
// unfoldEffect, never a custom fold store. Fold ranges come only from containerRanges, the pure
|
|
8
|
+
// pairing helper beside fenceScan. The safety invariant lives in one transactionExtender that
|
|
9
|
+
// appends unfold effects when a change or selection touches a folded range. The chevrons are
|
|
10
|
+
// widget decorations from this module's own ViewPlugin (the rails are box-shadows, there is no CM
|
|
11
|
+
// gutter element to hang a foldGutter on).
|
|
12
|
+
//
|
|
13
|
+
// The safety invariant: an author never edits, deletes, or fails to see hidden text.
|
|
14
|
+
import {
|
|
15
|
+
Decoration,
|
|
16
|
+
EditorView,
|
|
17
|
+
ViewPlugin,
|
|
18
|
+
WidgetType,
|
|
19
|
+
keymap,
|
|
20
|
+
type DecorationSet,
|
|
21
|
+
type ViewUpdate,
|
|
22
|
+
} from '@codemirror/view';
|
|
23
|
+
import { EditorState, Prec, RangeSetBuilder, StateEffect, StateField, type Extension } from '@codemirror/state';
|
|
24
|
+
import { codeFolding, foldEffect, foldedRanges, unfoldEffect } from '@codemirror/language';
|
|
25
|
+
import { caretContainerRange, containerRanges, fenceScan, type ContainerRange } from './markdown-directives.js';
|
|
26
|
+
|
|
27
|
+
// Deeper nesting shares the third visual step, matching the rail stepping in editor-highlight.
|
|
28
|
+
const DEPTH_STEPS = 3;
|
|
29
|
+
// The chevron sits over the container's own innermost bar: depth 1 at x0, depth 2 at x8, depth 3
|
|
30
|
+
// at x16, so indentation telegraphs the nesting and depth 3 never collides with depth 2.
|
|
31
|
+
const chevronX = (depth: number) => (Math.min(depth, DEPTH_STEPS) - 1) * 8;
|
|
32
|
+
|
|
33
|
+
// The two chevron glyphs from the gold-standard mockup: down (caret inside) and right (folded).
|
|
34
|
+
const CHEVRON_DOWN = 'm6 9 6 6 6-6';
|
|
35
|
+
const CHEVRON_RIGHT = 'm9 6 6 6-6 6';
|
|
36
|
+
|
|
37
|
+
function chevronSvg(direction: 'down' | 'right'): SVGSVGElement {
|
|
38
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
39
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
40
|
+
svg.setAttribute('fill', 'none');
|
|
41
|
+
svg.setAttribute('stroke', 'currentColor');
|
|
42
|
+
svg.setAttribute('stroke-width', '2.5');
|
|
43
|
+
svg.setAttribute('stroke-linecap', 'round');
|
|
44
|
+
svg.setAttribute('stroke-linejoin', 'round');
|
|
45
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
46
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
47
|
+
path.setAttribute('d', direction === 'down' ? CHEVRON_DOWN : CHEVRON_RIGHT);
|
|
48
|
+
svg.appendChild(path);
|
|
49
|
+
return svg;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The opener-row band: the whole 28px gutter is the click target (cursor pointer over the band
|
|
53
|
+
// only, the opener text never folds), and the chevron lives inside it at the container's bar x.
|
|
54
|
+
// The widget knows its own container and whether it is folded, so the click toggles the right
|
|
55
|
+
// range; the depth class lets the theme step the chevron ink.
|
|
56
|
+
class FoldBandWidget extends WidgetType {
|
|
57
|
+
constructor(
|
|
58
|
+
readonly range: ContainerRange,
|
|
59
|
+
readonly folded: boolean,
|
|
60
|
+
readonly caretInside: boolean,
|
|
61
|
+
) {
|
|
62
|
+
super();
|
|
63
|
+
}
|
|
64
|
+
eq(other: FoldBandWidget) {
|
|
65
|
+
return (
|
|
66
|
+
other.range.fromLine === this.range.fromLine &&
|
|
67
|
+
other.range.toLine === this.range.toLine &&
|
|
68
|
+
other.range.depth === this.range.depth &&
|
|
69
|
+
other.folded === this.folded &&
|
|
70
|
+
other.caretInside === this.caretInside
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
toDOM(view: EditorView): HTMLElement {
|
|
74
|
+
const band = document.createElement('span');
|
|
75
|
+
const depth = Math.min(this.range.depth, DEPTH_STEPS);
|
|
76
|
+
// Folded rows always show the chevron (right); an open container shows it down while the caret
|
|
77
|
+
// is inside; otherwise it fades in on rail-band hover (the band's own :hover, in the theme).
|
|
78
|
+
// The depth class carries the stepped ink.
|
|
79
|
+
const state = this.folded ? ' cm-cairn-fold-folded' : this.caretInside ? ' cm-cairn-fold-active' : '';
|
|
80
|
+
band.className = `cm-cairn-fold-band cm-cairn-fold-depth-${depth}${state}`;
|
|
81
|
+
const chevron = chevronSvg(this.folded ? 'right' : 'down');
|
|
82
|
+
chevron.style.left = `${chevronX(this.range.depth)}px`;
|
|
83
|
+
band.appendChild(chevron);
|
|
84
|
+
band.addEventListener('mousedown', (e) => {
|
|
85
|
+
// mousedown, not click: a click would first move the caret into the line. preventDefault
|
|
86
|
+
// keeps the caret where it is and stops the band from stealing focus from the editor.
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
e.stopPropagation();
|
|
89
|
+
toggleFold(view, this.range);
|
|
90
|
+
});
|
|
91
|
+
return band;
|
|
92
|
+
}
|
|
93
|
+
ignoreEvent() {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// The pill placeholder: a real focusable button counting the hidden lines, the screen-reader story
|
|
99
|
+
// for a fold. preparePlaceholder computes the count off the folded char range so placeholderDOM
|
|
100
|
+
// renders it without re-deriving. Clicking unfolds through CodeMirror's own onclick handler.
|
|
101
|
+
function preparePlaceholder(state: EditorState, range: { from: number; to: number }): number {
|
|
102
|
+
return state.doc.lineAt(range.to).number - state.doc.lineAt(range.from).number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function placeholderDOM(view: EditorView, onclick: (event: Event) => void, lines: number): HTMLElement {
|
|
106
|
+
const pill = document.createElement('button');
|
|
107
|
+
pill.type = 'button';
|
|
108
|
+
pill.className = 'cm-cairn-fold-pill';
|
|
109
|
+
pill.textContent = `${lines} lines`;
|
|
110
|
+
pill.setAttribute('aria-label', `Show ${lines} hidden lines`);
|
|
111
|
+
pill.addEventListener('click', onclick);
|
|
112
|
+
return pill;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// The char range a container folds: end-of-opener-line to end-of-closer-line, so the bare closer
|
|
116
|
+
// never dangles. Null when the opener and closer share a line (nothing to hide). The one place
|
|
117
|
+
// that turns a line range into the fold range, shared by the toggle, the keymap, and the
|
|
118
|
+
// decoration build.
|
|
119
|
+
function foldCharRange(state: EditorState, range: ContainerRange): { from: number; to: number } | null {
|
|
120
|
+
const opener = state.doc.line(range.fromLine + 1);
|
|
121
|
+
const closer = state.doc.line(range.toLine + 1);
|
|
122
|
+
if (closer.to <= opener.to) return null;
|
|
123
|
+
return { from: opener.to, to: closer.to };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Fold one container, or unfold it if already folded. The range comes from containerRanges, so a
|
|
127
|
+
// half-typed fence can never fold. A no-op when the container has nothing to hide.
|
|
128
|
+
function toggleFold(view: EditorView, range: ContainerRange): void {
|
|
129
|
+
const span = foldCharRange(view.state, range);
|
|
130
|
+
if (!span) return;
|
|
131
|
+
const effect = foldExists(view.state, span.from, span.to) ? unfoldEffect : foldEffect;
|
|
132
|
+
view.dispatch({ effects: effect.of(span) });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function foldExists(state: EditorState, from: number, to: number): boolean {
|
|
136
|
+
let found = false;
|
|
137
|
+
foldedRanges(state).between(from, from, (a, b) => {
|
|
138
|
+
if (a === from && b === to) found = true;
|
|
139
|
+
});
|
|
140
|
+
return found;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// The innermost container at the caret, the unit the keymap folds and unfolds. caretContainerRange
|
|
144
|
+
// returns the nearest enclosing container, but a container that never closes (an unbalanced
|
|
145
|
+
// opener) must not fold, so the result is only honored when containerRanges actually pairs it.
|
|
146
|
+
function caretFoldRange(view: EditorView): ContainerRange | null {
|
|
147
|
+
const scan = fenceScan(docLines(view));
|
|
148
|
+
const caretLine = view.state.doc.lineAt(view.state.selection.main.head).number - 1;
|
|
149
|
+
const inner = caretContainerRange(scan, caretLine);
|
|
150
|
+
if (!inner) return null;
|
|
151
|
+
return (
|
|
152
|
+
containerRanges(scan).find((r) => r.fromLine === inner.fromLine && r.toLine === inner.toLine) ?? null
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function docLines(view: EditorView): string[] {
|
|
157
|
+
const doc = view.state.doc;
|
|
158
|
+
const lines: string[] = [];
|
|
159
|
+
for (let n = 1; n <= doc.lines; n++) lines.push(doc.line(n).text);
|
|
160
|
+
return lines;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// The keymap: Ctrl+Shift+[ folds, Ctrl+Shift+] unfolds, the innermost container at the caret. No
|
|
164
|
+
// fold-all, no chords. (CodeMirror's own foldKeymap binds the same keys to its tree-folding
|
|
165
|
+
// commands; this module replaces them with the container-aware versions and never adds foldKeymap.)
|
|
166
|
+
// At Prec.high so it resolves the shifted bracket ahead of the default keymap's Ctrl-[ indentLess,
|
|
167
|
+
// which the same keystroke also matches on the non-shift lookup.
|
|
168
|
+
const foldKeymap = Prec.high(
|
|
169
|
+
keymap.of([
|
|
170
|
+
{
|
|
171
|
+
key: 'Mod-Shift-[',
|
|
172
|
+
run: (view) => {
|
|
173
|
+
const range = caretFoldRange(view);
|
|
174
|
+
const span = range && foldCharRange(view.state, range);
|
|
175
|
+
if (!span || foldExists(view.state, span.from, span.to)) return false;
|
|
176
|
+
view.dispatch({ effects: foldEffect.of(span) });
|
|
177
|
+
return true;
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
key: 'Mod-Shift-]',
|
|
182
|
+
run: (view) => {
|
|
183
|
+
const range = caretFoldRange(view);
|
|
184
|
+
const span = range && foldCharRange(view.state, range);
|
|
185
|
+
if (!span || !foldExists(view.state, span.from, span.to)) return false;
|
|
186
|
+
view.dispatch({ effects: unfoldEffect.of(span) });
|
|
187
|
+
return true;
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
]),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// The unfold flash: a one-time low-alpha accent line decoration on the revealed lines, faded out
|
|
194
|
+
// over ~400ms. A StateEffect carries the revealed char range; the field decorates those lines
|
|
195
|
+
// until a follow-up effect clears it. Driven by the plugin, which schedules the clear.
|
|
196
|
+
const flashEffect = StateEffect.define<{ from: number; to: number } | null>();
|
|
197
|
+
const flashLine = Decoration.line({ class: 'cm-cairn-fold-flash' });
|
|
198
|
+
|
|
199
|
+
const flashField = StateField.define<DecorationSet>({
|
|
200
|
+
create() {
|
|
201
|
+
return Decoration.none;
|
|
202
|
+
},
|
|
203
|
+
update(deco, tr) {
|
|
204
|
+
deco = deco.map(tr.changes);
|
|
205
|
+
for (const e of tr.effects) {
|
|
206
|
+
if (e.is(flashEffect)) {
|
|
207
|
+
if (!e.value) {
|
|
208
|
+
deco = Decoration.none;
|
|
209
|
+
} else {
|
|
210
|
+
const builder = new RangeSetBuilder<Decoration>();
|
|
211
|
+
const first = tr.state.doc.lineAt(e.value.from).number;
|
|
212
|
+
const last = tr.state.doc.lineAt(e.value.to).number;
|
|
213
|
+
for (let n = first; n <= last; n++) {
|
|
214
|
+
const line = tr.state.doc.line(n);
|
|
215
|
+
builder.add(line.from, line.from, flashLine);
|
|
216
|
+
}
|
|
217
|
+
deco = builder.finish();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return deco;
|
|
222
|
+
},
|
|
223
|
+
provide: (f) => EditorView.decorations.from(f),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const FLASH_MS = 400;
|
|
227
|
+
|
|
228
|
+
// The safety invariant, in one transactionExtender. CodeMirror's own fold field already clears a
|
|
229
|
+
// fold the selection head sits inside and a fold a delete touches; this covers the rest: an insert
|
|
230
|
+
// touching a fold boundary, a paste across it, an undo/redo landing inside, and a selection range
|
|
231
|
+
// (not just its head) extending into hidden text. It reads the start state's folds, maps them
|
|
232
|
+
// forward, and appends an unfold effect for any the change or new selection touches. A replace
|
|
233
|
+
// inside a fold leaves it open afterward, which falls out of the same rule.
|
|
234
|
+
function safetyExtender(): Extension {
|
|
235
|
+
return EditorState.transactionExtender.of((tr) => {
|
|
236
|
+
if (!tr.docChanged && !tr.selection) return null;
|
|
237
|
+
const startFolds = foldedRanges(tr.startState);
|
|
238
|
+
if (startFolds.size === 0) return null;
|
|
239
|
+
const effects: StateEffect<unknown>[] = [];
|
|
240
|
+
startFolds.between(0, tr.startState.doc.length, (from, to) => {
|
|
241
|
+
// The fold's position after the change, for the selection test.
|
|
242
|
+
const mappedFrom = tr.changes.mapPos(from, 1);
|
|
243
|
+
const mappedTo = tr.changes.mapPos(to, -1);
|
|
244
|
+
let touched = false;
|
|
245
|
+
if (tr.docChanged) {
|
|
246
|
+
tr.changes.iterChangedRanges((fromA, toA) => {
|
|
247
|
+
if (fromA <= to && toA >= from) touched = true;
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (!touched && tr.selection) {
|
|
251
|
+
for (const range of tr.selection.ranges) {
|
|
252
|
+
if (range.from < mappedTo && range.to > mappedFrom) touched = true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (touched) effects.push(unfoldEffect.of({ from, to }));
|
|
256
|
+
});
|
|
257
|
+
return effects.length ? { effects } : null;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// The chevron-and-wash plugin: a widget on each opener row of a paired container (the band with
|
|
262
|
+
// the chevron) and the folded-row wash line decoration. Rebuilds on doc change (the container set
|
|
263
|
+
// moves), selection change (the caret-inside chevron state), viewport change, and any fold change
|
|
264
|
+
// (a fold flips a chevron and adds a wash). The hover reveal is the band's own CSS :hover in the
|
|
265
|
+
// theme, so it costs no rebuild.
|
|
266
|
+
function foldDecorations(view: EditorView, scan: ReturnType<typeof fenceScan>, ranges: ContainerRange[]): DecorationSet {
|
|
267
|
+
const builder = new RangeSetBuilder<Decoration>();
|
|
268
|
+
const caretLine = view.state.doc.lineAt(view.state.selection.main.head).number - 1;
|
|
269
|
+
const inner = caretContainerRange(scan, caretLine);
|
|
270
|
+
// One opener row may host several enclosing containers' bars, but only its OWN container opens
|
|
271
|
+
// there, so a single band per opener row. Sort by opener line so the builder receives ascending
|
|
272
|
+
// positions; equal openers cannot happen (one open per line).
|
|
273
|
+
const byOpener = [...ranges].sort((a, b) => a.fromLine - b.fromLine);
|
|
274
|
+
for (const range of byOpener) {
|
|
275
|
+
const span = foldCharRange(view.state, range);
|
|
276
|
+
if (!span) continue;
|
|
277
|
+
const opener = view.state.doc.line(range.fromLine + 1);
|
|
278
|
+
const folded = foldExists(view.state, span.from, span.to);
|
|
279
|
+
// The folded opener row carries the wash; a line decoration so the rails (box-shadows on the
|
|
280
|
+
// same element) run through it unbroken.
|
|
281
|
+
if (folded) builder.add(opener.from, opener.from, washLine);
|
|
282
|
+
const caretInside = !!inner && inner.fromLine === range.fromLine && inner.toLine === range.toLine;
|
|
283
|
+
builder.add(
|
|
284
|
+
opener.from,
|
|
285
|
+
opener.from,
|
|
286
|
+
Decoration.widget({ widget: new FoldBandWidget(range, folded, caretInside), side: -1 }),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
return builder.finish();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const washLine = Decoration.line({ class: 'cm-cairn-folded-row' });
|
|
293
|
+
|
|
294
|
+
function foldPlugin() {
|
|
295
|
+
return ViewPlugin.fromClass(
|
|
296
|
+
class {
|
|
297
|
+
decorations: DecorationSet;
|
|
298
|
+
scan: ReturnType<typeof fenceScan>;
|
|
299
|
+
ranges: ContainerRange[];
|
|
300
|
+
constructor(view: EditorView) {
|
|
301
|
+
this.scan = fenceScan(docLines(view));
|
|
302
|
+
this.ranges = containerRanges(this.scan);
|
|
303
|
+
this.decorations = foldDecorations(view, this.scan, this.ranges);
|
|
304
|
+
}
|
|
305
|
+
update(update: ViewUpdate) {
|
|
306
|
+
if (update.docChanged) {
|
|
307
|
+
this.scan = fenceScan(docLines(update.view));
|
|
308
|
+
this.ranges = containerRanges(this.scan);
|
|
309
|
+
}
|
|
310
|
+
const foldChanged = update.transactions.some((tr) =>
|
|
311
|
+
tr.effects.some((e) => e.is(foldEffect) || e.is(unfoldEffect)),
|
|
312
|
+
);
|
|
313
|
+
if (update.docChanged || update.viewportChanged || update.selectionSet || foldChanged) {
|
|
314
|
+
this.decorations = foldDecorations(update.view, this.scan, this.ranges);
|
|
315
|
+
}
|
|
316
|
+
// Flash the revealed lines on an unfold, then schedule the clear. Folding adds no flash.
|
|
317
|
+
for (const tr of update.transactions) {
|
|
318
|
+
for (const e of tr.effects) {
|
|
319
|
+
if (e.is(unfoldEffect)) {
|
|
320
|
+
const { from, to } = e.value;
|
|
321
|
+
const view = update.view;
|
|
322
|
+
queueMicrotask(() => {
|
|
323
|
+
if (!view.dom.isConnected) return;
|
|
324
|
+
view.dispatch({ effects: flashEffect.of({ from, to }) });
|
|
325
|
+
setTimeout(() => {
|
|
326
|
+
if (view.dom.isConnected) view.dispatch({ effects: flashEffect.of(null) });
|
|
327
|
+
}, FLASH_MS);
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
{ decorations: (v) => v.decorations },
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// The design notes' diagnostics rules (a deliberately refolded erroring container keeps its fold
|
|
339
|
+
// and tints the pill warning ink instead of re-springing on every lint) have no trigger today: the
|
|
340
|
+
// editor carries no lint source, so there is nothing to refold around or to tint. Deliberately not
|
|
341
|
+
// built; do not invent a lint system to satisfy a rule with no input.
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The cairn fold extension: the CodeMirror fold system with the pill placeholder, the chevron and
|
|
345
|
+
* wash affordance, the safety invariant, and the Ctrl+Shift+[ / ] keymap. Session-local and never
|
|
346
|
+
* persisted: the fold state lives in CodeMirror's foldState field, which this never serializes.
|
|
347
|
+
*/
|
|
348
|
+
export function cairnFolding(): Extension {
|
|
349
|
+
return [
|
|
350
|
+
codeFolding({ preparePlaceholder, placeholderDOM }),
|
|
351
|
+
flashField,
|
|
352
|
+
safetyExtender(),
|
|
353
|
+
foldPlugin(),
|
|
354
|
+
foldKeymap,
|
|
355
|
+
];
|
|
356
|
+
}
|
|
@@ -7,10 +7,12 @@ import { Decoration, ViewPlugin, type DecorationSet, type EditorView, type ViewU
|
|
|
7
7
|
import { RangeSetBuilder } from '@codemirror/state';
|
|
8
8
|
import {
|
|
9
9
|
caretContainerRange,
|
|
10
|
+
containerRanges,
|
|
10
11
|
directiveLineKind,
|
|
11
12
|
fenceScan,
|
|
12
13
|
fenceTokens,
|
|
13
14
|
findInlineDirectives,
|
|
15
|
+
markerPrefix,
|
|
14
16
|
type FenceScan,
|
|
15
17
|
} from './markdown-directives.js';
|
|
16
18
|
|
|
@@ -25,7 +27,9 @@ export function cairnHighlightStyle(): HighlightStyle {
|
|
|
25
27
|
{ tag: tags.heading1, fontSize: '1.5em', fontWeight: '700', color: 'var(--color-base-content)' },
|
|
26
28
|
{ tag: tags.heading2, fontSize: '1.3em', fontWeight: '700', color: 'var(--color-base-content)' },
|
|
27
29
|
{ tag: tags.heading3, fontSize: '1.17em', fontWeight: '700', color: 'var(--color-base-content)' },
|
|
28
|
-
//
|
|
30
|
+
// A real step for h4, between h3 and body, so a hand-typed #### reads as a heading.
|
|
31
|
+
{ tag: tags.heading4, fontSize: '1.05em', fontWeight: '700', color: 'var(--color-base-content)' },
|
|
32
|
+
// h5 and deeper share the weight only; body size keeps the low levels from outranking h4.
|
|
29
33
|
{ tag: tags.heading, fontWeight: '700', color: 'var(--color-base-content)' },
|
|
30
34
|
{ tag: tags.strong, fontWeight: '700' },
|
|
31
35
|
{ tag: tags.emphasis, fontStyle: 'italic' },
|
|
@@ -59,6 +63,11 @@ const fenceLines = DEPTH_STEPS.map((d) =>
|
|
|
59
63
|
);
|
|
60
64
|
const contentLines = DEPTH_STEPS.map((d) => Decoration.line({ class: `cm-cairn-directive-content cm-cairn-depth-${d}` }));
|
|
61
65
|
const leafLine = Decoration.line({ class: 'cm-cairn-directive-leaf', attributes: { title: MACHINERY_HINT } });
|
|
66
|
+
// A paired container's opener row, where the fold chevron replaces the container's own innermost
|
|
67
|
+
// rail bar. The class is additive over the fence depth class, so the theme drops only the deepest
|
|
68
|
+
// bar on this row while the outer bars stay; an unbalanced opener never gets it (no chevron, bar
|
|
69
|
+
// intact). Added just after the fence depth line decoration so the row carries both classes.
|
|
70
|
+
const openerLine = Decoration.line({ class: 'cm-cairn-directive-opener' });
|
|
62
71
|
const inlineMark = Decoration.mark({ class: 'cm-cairn-directive-inline' });
|
|
63
72
|
// Within a fence line, machinery (colons, brackets, braces) dims to the marker tone while the
|
|
64
73
|
// directive name and label keep a depth-stepped ink: meaning over machinery.
|
|
@@ -69,6 +78,26 @@ const fenceLabels = DEPTH_STEPS.map((d) => Decoration.mark({ class: `cm-cairn-di
|
|
|
69
78
|
// label ink up one notch while the other containers sit quieter.
|
|
70
79
|
const caretBlockLine = Decoration.line({ class: 'cm-cairn-caret-block' });
|
|
71
80
|
|
|
81
|
+
// The hanging-indent line decoration for a quote or list line, keyed by the marker's character
|
|
82
|
+
// width. The width rides a --cairn-hang custom property so the theme's padding-left rule adds it
|
|
83
|
+
// to the directive gutter rather than replacing it (an inline padding-left would win the cascade
|
|
84
|
+
// and erase the gutter). The equal negative text-indent pulls the first line back by the marker
|
|
85
|
+
// width, so the marker sits in the indent and a wrapped continuation resumes under the content
|
|
86
|
+
// (the Obsidian/HyperMD idiom). The surface is iA Writer Mono, fixed pitch, so n chars is exactly
|
|
87
|
+
// n ch. Built lazily and memoized; marker widths repeat.
|
|
88
|
+
const hangLines = new Map<number, Decoration>();
|
|
89
|
+
function hangLine(width: number): Decoration {
|
|
90
|
+
let deco = hangLines.get(width);
|
|
91
|
+
if (!deco) {
|
|
92
|
+
deco = Decoration.line({
|
|
93
|
+
class: 'cm-cairn-hang',
|
|
94
|
+
attributes: { style: `--cairn-hang:${width}ch;text-indent:-${width}ch` },
|
|
95
|
+
});
|
|
96
|
+
hangLines.set(width, deco);
|
|
97
|
+
}
|
|
98
|
+
return deco;
|
|
99
|
+
}
|
|
100
|
+
|
|
72
101
|
// Depth needs the whole document, since a visible line's containers can open above the viewport.
|
|
73
102
|
// One regex pass per line, linear in the document; at admin entry sizes (tens of kilobytes) that
|
|
74
103
|
// is well under a millisecond. The plugin caches the fence scan, so it reruns only when the
|
|
@@ -89,6 +118,9 @@ function buildDirectiveDecorations(view: EditorView, scan: FenceScan): Decoratio
|
|
|
89
118
|
// as its own line decoration just ahead of the row's depth decoration.
|
|
90
119
|
const caretLine = view.state.doc.lineAt(view.state.selection.main.head).number - 1;
|
|
91
120
|
const caret = caretContainerRange(scan, caretLine);
|
|
121
|
+
// The opener rows that carry a fold chevron, so the rail rule drops their innermost bar. Only
|
|
122
|
+
// paired containers fold, so an unbalanced opener is absent here and keeps its full rail.
|
|
123
|
+
const openerLines = new Set(containerRanges(scan).map((r) => r.fromLine));
|
|
92
124
|
for (const { from, to } of view.visibleRanges) {
|
|
93
125
|
for (let pos = from; pos <= to; ) {
|
|
94
126
|
const line = view.state.doc.lineAt(pos);
|
|
@@ -101,6 +133,26 @@ function buildDirectiveDecorations(view: EditorView, scan: FenceScan): Decoratio
|
|
|
101
133
|
// inside a code block, outside any container); it gets no machinery treatment.
|
|
102
134
|
if (kind === 'fence' && depth > 0) {
|
|
103
135
|
builder.add(line.from, line.from, fenceLines[depth - 1]);
|
|
136
|
+
// A paired opener row gets the opener class just after its fence depth class, so the rail
|
|
137
|
+
// rule below drops the innermost bar where the chevron renders.
|
|
138
|
+
if (openerLines.has(line.number - 1)) builder.add(line.from, line.from, openerLine);
|
|
139
|
+
} else if (kind === 'leaf') {
|
|
140
|
+
builder.add(line.from, line.from, leafLine);
|
|
141
|
+
} else if (kind === null && depth > 0) {
|
|
142
|
+
builder.add(line.from, line.from, contentLines[depth - 1]);
|
|
143
|
+
}
|
|
144
|
+
// A quote or list line hangs its wrapped continuation under the content. The decoration is
|
|
145
|
+
// a line decoration too, so it enters at line.from after the depth and caret-block lines
|
|
146
|
+
// and before any mark decoration on the same row; inside a container it composes with the
|
|
147
|
+
// gutter padding. A fence or leaf machinery line is never a quote or list, so this only
|
|
148
|
+
// fires on prose and content rows.
|
|
149
|
+
if (kind === null) {
|
|
150
|
+
const prefix = markerPrefix(line.text);
|
|
151
|
+
if (prefix) builder.add(line.from, line.from, hangLine(prefix.length));
|
|
152
|
+
}
|
|
153
|
+
// Mark decorations start at offsets past line.from, so they enter after every line
|
|
154
|
+
// decoration on the row.
|
|
155
|
+
if (kind === 'fence' && depth > 0) {
|
|
104
156
|
for (const token of fenceTokens(line.text)) {
|
|
105
157
|
builder.add(
|
|
106
158
|
line.from + token.from,
|
|
@@ -108,9 +160,7 @@ function buildDirectiveDecorations(view: EditorView, scan: FenceScan): Decoratio
|
|
|
108
160
|
token.kind === 'mark' ? fenceMark : fenceLabels[depth - 1],
|
|
109
161
|
);
|
|
110
162
|
}
|
|
111
|
-
} else if (kind ===
|
|
112
|
-
else if (kind === null) {
|
|
113
|
-
if (depth > 0) builder.add(line.from, line.from, contentLines[depth - 1]);
|
|
163
|
+
} else if (kind === null) {
|
|
114
164
|
for (const r of findInlineDirectives(line.text)) {
|
|
115
165
|
builder.add(line.from + r.from, line.from + r.to, inlineMark);
|
|
116
166
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// The one keyboard-shortcut table, the single source the shortcuts sheet (ShortcutsDialog) and the
|
|
2
|
+
// Markdown help dialog both render. Order and content follow the gold-standard mockup's screen 5
|
|
3
|
+
// (docs/internal/design/2026-06-12-editor-shell-gold-standard.html), one change: Write / Preview
|
|
4
|
+
// shows Ctrl Alt P, the binding actually wired in EditPage (Firefox reserves Ctrl Shift P for
|
|
5
|
+
// private browsing at the browser level, below preventDefault), and the fold pair the editor wires
|
|
6
|
+
// in editor-folding.ts joins the editor-structure rows. The keys read with literal "Ctrl" to match
|
|
7
|
+
// the toolbar tooltips, which never localize to Cmd.
|
|
8
|
+
|
|
9
|
+
/** One shortcut row: a human label and the chord that triggers it. */
|
|
10
|
+
export type ShortcutRow = { label: string; keys: string };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The shortcut vocabulary, in the mockup's reading order. Each entry is verified against the
|
|
14
|
+
* handler that implements it: Save / Publish / Details panel / Zen / Write-Preview / Focus mode and
|
|
15
|
+
* This sheet ride EditPage's window keydown; Bold / Italic / Inline code / Web link / the heading
|
|
16
|
+
* pair / Quote / the list pair ride EditPage's card keydown; Fold / unfold ride editor-folding's
|
|
17
|
+
* CodeMirror keymap; the command palette rides AdminLayout's Ctrl K; Continue list / quote is the
|
|
18
|
+
* built-in markdown keymap on Enter.
|
|
19
|
+
*/
|
|
20
|
+
export const editorShortcuts: ShortcutRow[] = [
|
|
21
|
+
{ label: 'Save', keys: 'Ctrl S' },
|
|
22
|
+
{ label: 'Bold', keys: 'Ctrl B' },
|
|
23
|
+
{ label: 'Publish', keys: 'Ctrl Shift S' },
|
|
24
|
+
{ label: 'Italic', keys: 'Ctrl I' },
|
|
25
|
+
{ label: 'Details panel', keys: 'Ctrl .' },
|
|
26
|
+
{ label: 'Web link', keys: 'Ctrl K' },
|
|
27
|
+
{ label: 'Zen', keys: 'Ctrl Shift .' },
|
|
28
|
+
{ label: 'Inline code', keys: 'Ctrl E' },
|
|
29
|
+
{ label: 'Write / Preview', keys: 'Ctrl Alt P' },
|
|
30
|
+
{ label: 'Heading / smaller', keys: 'Ctrl Alt 2 / 3' },
|
|
31
|
+
{ label: 'Focus mode', keys: 'Ctrl Shift F' },
|
|
32
|
+
{ label: 'Quote', keys: 'Ctrl Shift 9' },
|
|
33
|
+
{ label: 'Command palette', keys: 'Ctrl K (global)' },
|
|
34
|
+
{ label: 'Bulleted / numbered list', keys: 'Ctrl Shift 8 / 7' },
|
|
35
|
+
{ label: 'Fold / unfold', keys: 'Ctrl Shift [ / ]' },
|
|
36
|
+
{ label: 'This sheet', keys: 'Ctrl /' },
|
|
37
|
+
{ label: 'Continue list / quote', keys: 'Enter' },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/** The closing reassurance under the grid: the keys never gate the markdown that authors type. */
|
|
41
|
+
export const shortcutsClosingLine =
|
|
42
|
+
'Typing markdown always works; the keys are conveniences, never requirements.';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copyright
|
|
1
|
+
Copyright 2019 IBM Corp. All rights reserved. IBMPlexSans-Italic[wdth,wght].ttf: Copyright 2019 IBM Corp. All rights reserved.
|
|
2
2
|
|
|
3
3
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
4
|
This license is copied below, and is also available with a FAQ at:
|
|
@@ -18,7 +18,7 @@ with others.
|
|
|
18
18
|
|
|
19
19
|
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
20
|
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
-
fonts, including any derivative works, can be bundled, embedded,
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
22
|
redistributed and/or sold with any software provided that any reserved
|
|
23
23
|
names are not used by derivative works. The fonts and derivatives,
|
|
24
24
|
however, cannot be released under any other type of license. The
|
|
Binary file
|