@formicoidea/labre-core 0.39.3 → 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.
@@ -1,22 +1,30 @@
1
1
  import { ParagraphBlockModel, } from '../affine-model/index.js';
2
+ import { HEADING_LEVELS, HEADING_SCALE, headingLineBox, } from '../affine-shared/consts/index.js';
2
3
  import { matchModels } from '../affine-shared/utils/index.js';
4
+ /** The emoji's box, px (`.affine-callout-emoji-container` in callout-block.ts). */
5
+ const EMOJI_BOX_PX = 24;
3
6
  /**
4
- * How far the emoji has to drop to sit on the first line's baseline.
7
+ * H1's bold caps read as sitting low when the emoji is centred on the line
8
+ * box mathematically; lifting the emoji by 1px levels it optically.
9
+ */
10
+ const H1_OPTICAL_LIFT_PX = 1;
11
+ /**
12
+ * How far the emoji has to drop to sit level with the first line.
5
13
  *
6
14
  * The emoji is a fixed 24px box next to a column whose first line can be
7
15
  * anything from an H1 to an ordinary paragraph. A single margin therefore
8
16
  * cannot be right twice: level with a paragraph it floats above an H1, level
9
- * with an H1 it sinks below a paragraph. These are the drops measured per
10
- * heading level.
17
+ * with an H1 it sinks below a paragraph. Per heading level, the drop centres
18
+ * the emoji on the heading's first line box, below its margin-top:
19
+ * `marginTop + lineBox / 2 - emojiBox / 2`.
11
20
  */
12
- const EMOJI_MARGIN_TOP_BY_TYPE = {
13
- h1: '23px',
14
- h2: '20px',
15
- h3: '16px',
16
- h4: '15px',
17
- h5: '14px',
18
- h6: '13px',
19
- };
21
+ const EMOJI_MARGIN_TOP_BY_TYPE = Object.fromEntries(HEADING_LEVELS.map(level => {
22
+ const centred = HEADING_SCALE[level].marginTop +
23
+ headingLineBox(level) / 2 -
24
+ EMOJI_BOX_PX / 2;
25
+ const drop = level === 'h1' ? centred - H1_OPTICAL_LIFT_PX : centred;
26
+ return [level, `${drop}px`];
27
+ }));
20
28
  /** What a paragraph, a list, or an empty callout gets. */
21
29
  const DEFAULT_EMOJI_MARGIN_TOP = '10px';
22
30
  /**
@@ -1,4 +1,20 @@
1
+ import { type HeadingLevel } from '../../affine-shared/consts/index.js';
1
2
  import type { SlashMenuTooltip } from '../../affine-widget-slash-menu/index.js';
3
+ /**
4
+ * The body lines of a heading preview that fit ENTIRELY in the frame, with the
5
+ * baseline each is set on.
6
+ *
7
+ * The body starts right under the heading's line box, so how many of its lines
8
+ * the card can show depends on the level: derived here from the geometry (line
9
+ * boxes, frame height, bottom padding) rather than tuned per level. At 10px on
10
+ * a 12px line, Inter's descenders end within a twentieth of a pixel of the line
11
+ * box's bottom edge, which the bottom padding covers: a line whose box fits is
12
+ * never clipped.
13
+ */
14
+ export declare function headingPreviewBodyLines(level: HeadingLevel): {
15
+ y: number;
16
+ text: string;
17
+ }[];
2
18
  export declare const BoldTextTooltip: import("lit-html").TemplateResult<1>;
3
19
  export declare const ItalicTooltip: import("lit-html").TemplateResult<1>;
4
20
  export declare const StrikethroughTooltip: import("lit-html").TemplateResult<1>;
@@ -1,4 +1,5 @@
1
- import { html } from 'lit';
1
+ import { HEADING_SCALE, headingLineBox, } from '../../affine-shared/consts/index.js';
2
+ import { html, svg } from 'lit';
2
3
  // prettier-ignore
3
4
  const TextTooltip = html `<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
4
5
  <rect width="170" height="68" rx="2" fill="white"/>
@@ -10,78 +11,110 @@ const TextTooltip = html `<svg width="170" height="68" viewBox="0 0 170 68" fill
10
11
  </g>
11
12
  </svg>
12
13
  `;
14
+ /** The preview frame: every slash-menu figure is a 170×68 card. */
15
+ const PREVIEW_HEIGHT = 68;
16
+ /** Space above the first line of a preview, px. */
17
+ const PREVIEW_PADDING_TOP = 6;
18
+ /**
19
+ * Space a preview keeps free under its last line, px. A body line is drawn only
20
+ * if its whole line box ends above it, so no line is ever cut by the frame.
21
+ */
22
+ const PREVIEW_PADDING_BOTTOM = 2;
23
+ /** The body text under a heading preview, shrunk to 10px on a 12px line. */
24
+ const BODY_FONT_SIZE = 10;
25
+ const BODY_LINE_HEIGHT = 12;
26
+ /** Extra space between two paragraphs of the body, px. */
27
+ const BODY_PARAGRAPH_GAP = 4;
28
+ /**
29
+ * The body, as the lines it is set in (the `text` preview's own wording). A
30
+ * paragraph ends with a newline, as the Figma export wrote it.
31
+ */
32
+ const BODY_PARAGRAPHS = [
33
+ [
34
+ 'In a decentralized system, we can have a kaleidoscopic ',
35
+ 'complexity to our data.\n',
36
+ ],
37
+ [
38
+ 'Any user may have a different perspective on what data they ',
39
+ 'either have, choose to share, or accept.\n',
40
+ ],
41
+ [
42
+ 'For example, one user\u2019s edits to a document might be on ',
43
+ 'their laptop on an airplane; when the plane lands and the ',
44
+ 'computer reconnects, those changes are distributed to ',
45
+ 'other users.\n',
46
+ ],
47
+ [
48
+ 'Other users might choose to accept all, some, or none of ',
49
+ 'those changes to their version of the document.',
50
+ ],
51
+ ];
52
+ /**
53
+ * Where Figma sets an Inter baseline in a line box: 4/11 em below the box's
54
+ * centre (half of Inter's ascent minus descent).
55
+ */
56
+ function interBaseline(top, lineHeight, fontSize) {
57
+ return Number((top + lineHeight / 2 + (fontSize * 4) / 11).toFixed(4));
58
+ }
59
+ /**
60
+ * The body lines of a heading preview that fit ENTIRELY in the frame, with the
61
+ * baseline each is set on.
62
+ *
63
+ * The body starts right under the heading's line box, so how many of its lines
64
+ * the card can show depends on the level: derived here from the geometry (line
65
+ * boxes, frame height, bottom padding) rather than tuned per level. At 10px on
66
+ * a 12px line, Inter's descenders end within a twentieth of a pixel of the line
67
+ * box's bottom edge, which the bottom padding covers: a line whose box fits is
68
+ * never clipped.
69
+ */
70
+ export function headingPreviewBodyLines(level) {
71
+ const limit = PREVIEW_HEIGHT - PREVIEW_PADDING_BOTTOM;
72
+ const lines = [];
73
+ let top = PREVIEW_PADDING_TOP + headingLineBox(level);
74
+ for (const [index, paragraph] of BODY_PARAGRAPHS.entries()) {
75
+ if (index > 0)
76
+ top += BODY_PARAGRAPH_GAP;
77
+ for (const text of paragraph) {
78
+ if (top + BODY_LINE_HEIGHT > limit)
79
+ return lines;
80
+ lines.push({
81
+ y: interBaseline(top, BODY_LINE_HEIGHT, BODY_FONT_SIZE),
82
+ text,
83
+ });
84
+ top += BODY_LINE_HEIGHT;
85
+ }
86
+ }
87
+ return lines;
88
+ }
89
+ /**
90
+ * A heading preview drawn at the heading's real size, weight and line box
91
+ * (from `HEADING_SCALE`), above as many lines of body text as the card shows
92
+ * whole (`headingPreviewBodyLines`).
93
+ */
13
94
  // prettier-ignore
14
- const Heading1Tooltip = html `<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
95
+ function headingTooltip(level) {
96
+ const { fontSize, fontWeight, letterSpacing } = HEADING_SCALE[level];
97
+ const lineBox = headingLineBox(level);
98
+ const maskId = `mask_heading_tooltip_${level}`;
99
+ const body = headingPreviewBodyLines(level).map(line => svg `<tspan x="8" y=${line.y}>${line.text}</tspan>`);
100
+ return html `<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
15
101
  <rect width="170" height="68" rx="2" fill="white"/>
16
- <mask id="mask0_16460_873" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
102
+ <mask id=${maskId} style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
17
103
  <rect width="170" height="68" rx="2" fill="white"/>
18
104
  </mask>
19
- <g mask="url(#mask0_16460_873)">
20
- <text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="28" font-weight="bold" letter-spacing="-0.24px"><tspan x="8" y="34.1818">Heading 1</tspan></text>
21
- <text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="51.6364">In a decentralized system, we can have a kaleidoscopic </tspan><tspan x="8" y="63.6364">complexity to our data.&#10;</tspan><tspan x="8" y="79.6364">Any user may have a different perspective on what data they </tspan><tspan x="8" y="91.6364">either have, choose to share, or accept.&#10;</tspan><tspan x="8" y="107.636">For example, one user&#x2019;s edits to a document might be on </tspan><tspan x="8" y="119.636">their laptop on an airplane; when the plane lands and the </tspan><tspan x="8" y="131.636">computer reconnects, those changes are distributed to </tspan><tspan x="8" y="143.636">other users.&#10;</tspan><tspan x="8" y="159.636">Other users might choose to accept all, some, or none of </tspan><tspan x="8" y="171.636">those changes to their version of the document.</tspan></text>
22
- </g>
23
- </svg>
24
- `;
25
- // prettier-ignore
26
- const Heading2Tooltip = html `<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
27
- <rect width="170" height="68" rx="2" fill="white"/>
28
- <mask id="mask0_16460_880" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
29
- <rect width="170" height="68" rx="2" fill="white"/>
30
- </mask>
31
- <g mask="url(#mask0_16460_880)">
32
- <text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="26" font-weight="600" letter-spacing="-0.24px"><tspan x="8" y="33.4545">Heading 2</tspan></text>
33
- <text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="51.6364">In a decentralized system, we can have a kaleidoscopic </tspan><tspan x="8" y="63.6364">complexity to our data.&#10;</tspan><tspan x="8" y="79.6364">Any user may have a different perspective on what data they </tspan><tspan x="8" y="91.6364">either have, choose to share, or accept.&#10;</tspan><tspan x="8" y="107.636">For example, one user&#x2019;s edits to a document might be on </tspan><tspan x="8" y="119.636">their laptop on an airplane; when the plane lands and the </tspan><tspan x="8" y="131.636">computer reconnects, those changes are distributed to </tspan><tspan x="8" y="143.636">other users.&#10;</tspan><tspan x="8" y="159.636">Other users might choose to accept all, some, or none of </tspan><tspan x="8" y="171.636">those changes to their version of the document.</tspan></text>
34
- </g>
35
- </svg>
36
- `;
37
- // prettier-ignore
38
- const Heading3Tooltip = html `<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
39
- <rect width="170" height="68" rx="2" fill="white"/>
40
- <mask id="mask0_16460_887" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
41
- <rect width="170" height="68" rx="2" fill="white"/>
42
- </mask>
43
- <g mask="url(#mask0_16460_887)">
44
- <text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="24" font-weight="600" letter-spacing="-0.24px"><tspan x="8" y="30.7273">Heading 3</tspan></text>
45
- <text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="47.6364">In a decentralized system, we can have a kaleidoscopic </tspan><tspan x="8" y="59.6364">complexity to our data.&#10;</tspan><tspan x="8" y="75.6364">Any user may have a different perspective on what data they </tspan><tspan x="8" y="87.6364">either have, choose to share, or accept.&#10;</tspan><tspan x="8" y="103.636">For example, one user&#x2019;s edits to a document might be on </tspan><tspan x="8" y="115.636">their laptop on an airplane; when the plane lands and the </tspan><tspan x="8" y="127.636">computer reconnects, those changes are distributed to </tspan><tspan x="8" y="139.636">other users.&#10;</tspan><tspan x="8" y="155.636">Other users might choose to accept all, some, or none of </tspan><tspan x="8" y="167.636">those changes to their version of the document.</tspan></text>
46
- </g>
47
- </svg>
48
- `;
49
- // prettier-ignore
50
- const Heading4Tooltip = html `<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
51
- <rect width="170" height="68" rx="2" fill="white"/>
52
- <mask id="mask0_16460_894" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
53
- <rect width="170" height="68" rx="2" fill="white"/>
54
- </mask>
55
- <g mask="url(#mask0_16460_894)">
56
- <text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="22" font-weight="600" letter-spacing="0.24px"><tspan x="8" y="29">Heading 4</tspan></text>
57
- <text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="45.6364">In a decentralized system, we can have a kaleidoscopic </tspan><tspan x="8" y="57.6364">complexity to our data.&#10;</tspan><tspan x="8" y="73.6364">Any user may have a different perspective on what data they </tspan><tspan x="8" y="85.6364">either have, choose to share, or accept.&#10;</tspan><tspan x="8" y="101.636">For example, one user&#x2019;s edits to a document might be on </tspan><tspan x="8" y="113.636">their laptop on an airplane; when the plane lands and the </tspan><tspan x="8" y="125.636">computer reconnects, those changes are distributed to </tspan><tspan x="8" y="137.636">other users.&#10;</tspan><tspan x="8" y="153.636">Other users might choose to accept all, some, or none of </tspan><tspan x="8" y="165.636">those changes to their version of the document.</tspan></text>
58
- </g>
59
- </svg>
60
- `;
61
- // prettier-ignore
62
- const Heading5Tooltip = html `<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
63
- <rect width="170" height="68" rx="2" fill="white"/>
64
- <mask id="mask0_16460_901" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
65
- <rect width="170" height="68" rx="2" fill="white"/>
66
- </mask>
67
- <g mask="url(#mask0_16460_901)">
68
- <text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="20" font-weight="600" letter-spacing="0.24px"><tspan x="8" y="27.2727">Heading 5</tspan></text>
69
- <text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="43.6364">In a decentralized system, we can have a kaleidoscopic </tspan><tspan x="8" y="55.6364">complexity to our data.&#10;</tspan><tspan x="8" y="71.6364">Any user may have a different perspective on what data they </tspan><tspan x="8" y="83.6364">either have, choose to share, or accept.&#10;</tspan><tspan x="8" y="99.6364">For example, one user&#x2019;s edits to a document might be on </tspan><tspan x="8" y="111.636">their laptop on an airplane; when the plane lands and the </tspan><tspan x="8" y="123.636">computer reconnects, those changes are distributed to </tspan><tspan x="8" y="135.636">other users.&#10;</tspan><tspan x="8" y="151.636">Other users might choose to accept all, some, or none of </tspan><tspan x="8" y="163.636">those changes to their version of the document.</tspan></text>
70
- </g>
71
- </svg>
72
- `;
73
- // prettier-ignore
74
- const Heading6Tooltip = html `<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
75
- <rect width="170" height="68" rx="2" fill="white"/>
76
- <mask id="mask0_16460_908" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
77
- <rect width="170" height="68" rx="2" fill="white"/>
78
- </mask>
79
- <g mask="url(#mask0_16460_908)">
80
- <text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="18" font-weight="600" letter-spacing="0.24px"><tspan x="8" y="25.5455">Heading 6</tspan></text>
81
- <text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="41.6364">In a decentralized system, we can have a kaleidoscopic </tspan><tspan x="8" y="53.6364">complexity to our data.&#10;</tspan><tspan x="8" y="69.6364">Any user may have a different perspective on what data they </tspan><tspan x="8" y="81.6364">either have, choose to share, or accept.&#10;</tspan><tspan x="8" y="97.6364">For example, one user&#x2019;s edits to a document might be on </tspan><tspan x="8" y="109.636">their laptop on an airplane; when the plane lands and the </tspan><tspan x="8" y="121.636">computer reconnects, those changes are distributed to </tspan><tspan x="8" y="133.636">other users.&#10;</tspan><tspan x="8" y="149.636">Other users might choose to accept all, some, or none of </tspan><tspan x="8" y="161.636">those changes to their version of the document.</tspan></text>
105
+ <g mask="url(#${maskId})">
106
+ <text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size=${fontSize} font-weight=${fontWeight} letter-spacing="${letterSpacing}em"><tspan x="8" y=${interBaseline(PREVIEW_PADDING_TOP, lineBox, fontSize)}>Heading ${level.slice(1)}</tspan></text>
107
+ <text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size=${BODY_FONT_SIZE} letter-spacing="0px">${body}</text>
82
108
  </g>
83
109
  </svg>
84
110
  `;
111
+ }
112
+ const Heading1Tooltip = headingTooltip('h1');
113
+ const Heading2Tooltip = headingTooltip('h2');
114
+ const Heading3Tooltip = headingTooltip('h3');
115
+ const Heading4Tooltip = headingTooltip('h4');
116
+ const Heading5Tooltip = headingTooltip('h5');
117
+ const Heading6Tooltip = headingTooltip('h6');
85
118
  // prettier-ignore
86
119
  const CodeBlockTooltip = html `<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
87
120
  <rect width="170" height="68" rx="2" fill="white"/>
@@ -1,5 +1,32 @@
1
+ import { HEADING_LEVELS, HEADING_SCALE } from '../affine-shared/consts/index.js';
1
2
  import { unsafeCSSVarV2 } from '../affine-shared/theme/index.js';
2
- import { css } from 'lit';
3
+ import { css, unsafeCSS } from 'lit';
4
+ /** Inline code is set this much smaller than the text around it. */
5
+ const INLINE_CODE_SHRINK_PX = 3;
6
+ /**
7
+ * H1…H6, generated from `HEADING_SCALE` so every size, line box and margin has
8
+ * one source. Inline code in a heading stays `INLINE_CODE_SHRINK_PX` below the
9
+ * heading's size; H6, the level closest to body text, keeps the body's code
10
+ * padding (with its 2px bottom).
11
+ */
12
+ const headingStyles = unsafeCSS(HEADING_LEVELS.map(level => {
13
+ const { fontSize, fontWeight, letterSpacing, lineHeightExtra, marginTop } = HEADING_SCALE[level];
14
+ return `
15
+ .${level} {
16
+ font-size: ${fontSize}px;
17
+ font-weight: ${fontWeight};
18
+ letter-spacing: ${letterSpacing}em;
19
+ line-height: calc(1em + ${lineHeightExtra}px);
20
+ margin-top: ${marginTop}px;
21
+ margin-bottom: 10px;
22
+ }
23
+
24
+ .${level} code {
25
+ font-size: ${fontSize - INLINE_CODE_SHRINK_PX}px;
26
+ padding: ${level === 'h6' ? '0px 4px 2px' : '0px 4px'};
27
+ }
28
+ `;
29
+ }).join(''));
3
30
  export const paragraphBlockStyles = css `
4
31
  affine-paragraph {
5
32
  box-sizing: border-box;
@@ -21,91 +48,11 @@ export const paragraphBlockStyles = css `
21
48
  }
22
49
 
23
50
  affine-paragraph code {
24
- font-size: calc(var(--affine-font-base) - 3px);
51
+ font-size: calc(var(--affine-font-base) - ${INLINE_CODE_SHRINK_PX}px);
25
52
  padding: 0px 4px 2px;
26
53
  }
27
54
 
28
- .h1 {
29
- font-size: var(--affine-font-h-1);
30
- font-weight: 700;
31
- letter-spacing: -0.02em;
32
- line-height: calc(1em + 8px);
33
- margin-top: 18px;
34
- margin-bottom: 10px;
35
- }
36
-
37
- .h1 code {
38
- font-size: calc(var(--affine-font-base) + 10px);
39
- padding: 0px 4px;
40
- }
41
-
42
- .h2 {
43
- font-size: var(--affine-font-h-2);
44
- font-weight: 600;
45
- letter-spacing: -0.02em;
46
- line-height: calc(1em + 10px);
47
- margin-top: 14px;
48
- margin-bottom: 10px;
49
- }
50
-
51
- .h2 code {
52
- font-size: calc(var(--affine-font-base) + 8px);
53
- padding: 0px 4px;
54
- }
55
-
56
- .h3 {
57
- font-size: var(--affine-font-h-3);
58
- font-weight: 600;
59
- letter-spacing: -0.02em;
60
- line-height: calc(1em + 8px);
61
- margin-top: 12px;
62
- margin-bottom: 10px;
63
- }
64
-
65
- .h3 code {
66
- font-size: calc(var(--affine-font-base) + 6px);
67
- padding: 0px 4px;
68
- }
69
-
70
- .h4 {
71
- font-size: var(--affine-font-h-4);
72
- font-weight: 600;
73
- letter-spacing: -0.015em;
74
- line-height: calc(1em + 8px);
75
- margin-top: 12px;
76
- margin-bottom: 10px;
77
- }
78
- .h4 code {
79
- font-size: calc(var(--affine-font-base) + 4px);
80
- padding: 0px 4px;
81
- }
82
-
83
- .h5 {
84
- font-size: var(--affine-font-h-5);
85
- font-weight: 600;
86
- letter-spacing: -0.015em;
87
- line-height: calc(1em + 8px);
88
- margin-top: 12px;
89
- margin-bottom: 10px;
90
- }
91
- .h5 code {
92
- font-size: calc(var(--affine-font-base) + 2px);
93
- padding: 0px 4px;
94
- }
95
-
96
- .h6 {
97
- font-size: var(--affine-font-h-6);
98
- font-weight: 600;
99
- letter-spacing: -0.015em;
100
- line-height: calc(1em + 8px);
101
- margin-top: 12px;
102
- margin-bottom: 10px;
103
- }
104
-
105
- .h6 code {
106
- font-size: var(--affine-font-base);
107
- padding: 0px 4px 2px;
108
- }
55
+ ${headingStyles}
109
56
 
110
57
  .quote {
111
58
  line-height: 26px;
@@ -19,6 +19,7 @@ export class ParagraphLayoutHandlerExtension extends BlockLayoutHandlerExtension
19
19
  const { zoom, viewScale } = viewportRecord;
20
20
  const paragraph = {
21
21
  type: 'affine:paragraph',
22
+ paragraphType: model.props.type,
22
23
  sentences: [],
23
24
  blockId: model.id,
24
25
  rect: { x: 0, y: 0, w: 0, h: 0 },
@@ -6,6 +6,8 @@ interface SentenceLayout {
6
6
  }
7
7
  export interface ParagraphLayout extends BlockLayout {
8
8
  type: 'affine:paragraph';
9
+ /** The paragraph block's own `props.type`: `text`, `quote`, `h1`…`h6`. */
10
+ paragraphType: string;
9
11
  sentences: SentenceLayout[];
10
12
  }
11
13
  export declare const ParagraphLayoutPainterExtension: import("../../store/index.js").ExtensionType;
@@ -39,12 +39,17 @@ class ParagraphLayoutPainter {
39
39
  console.warn('Expected paragraph layout but received different format:', layout);
40
40
  return;
41
41
  }
42
+ // Headings are not painted (TODO: fine-tune the baseline for heading
43
+ // sizes). Skipped on the block TYPE, not on the font size: H6 is set at
44
+ // the body's 15px (`HEADING_SCALE`), so a size test would paint it as text.
45
+ if (layout.paragraphType.startsWith('h'))
46
+ return;
42
47
  const renderedPositions = new Set();
43
48
  layout.sentences.forEach(sentence => {
44
49
  const fontSize = sentence.fontSize;
45
50
  const baselineY = getBaseline(fontSize);
46
51
  if (fontSize !== 15)
47
- return; // TODO: fine-tune for heading font sizes
52
+ return; // the baseline is tuned for body text only
48
53
  ctx.font = `${fontSize}px Inter`;
49
54
  ctx.strokeStyle = 'yellow';
50
55
  sentence.rects.forEach(textRect => {
@@ -494,7 +494,10 @@ export interface FrameworkBackgroundDef {
494
494
  transitionBandWidth?: Ratio;
495
495
  chrome?: BackgroundChromeDef;
496
496
  }
497
- /** The default card: a plain white rectangle, no border, no radius. */
497
+ /**
498
+ * The default card: a plain rectangle in the shared notation scale's card
499
+ * fill, no border, no radius.
500
+ */
498
501
  export declare const DEFAULT_BACKGROUND_SURFACE: BackgroundSurfaceDef;
499
502
  /** The plot rectangle of a background of size `w × h`, in element-local units. */
500
503
  export declare function backgroundPlot(def: FrameworkBackgroundDef, w: number, h: number): {
@@ -1,6 +1,10 @@
1
- /** The default card: a plain white rectangle, no border, no radius. */
1
+ import { NOTATION_NEUTRALS } from '../../affine-shared/consts/index.js';
2
+ /**
3
+ * The default card: a plain rectangle in the shared notation scale's card
4
+ * fill, no border, no radius.
5
+ */
2
6
  export const DEFAULT_BACKGROUND_SURFACE = {
3
- fill: '#ffffff',
7
+ fill: NOTATION_NEUTRALS.cardFill,
4
8
  };
5
9
  /** The plot rectangle of a background of size `w × h`, in element-local units. */
6
10
  export function backgroundPlot(def, w, h) {
@@ -1,18 +1,24 @@
1
1
  import { ConnectorMode, FontFamily, FontWeight, PointStyle, ShapeStyle, StrokeStyle, TextAlign, } from '../../affine-model/index.js';
2
+ import { NOTATION_NEUTRALS } from '../../affine-shared/consts/index.js';
2
3
  import { makeTemplateSnapshot, surfaceText, } from '../make-snapshot.js';
3
4
  /**
4
5
  * The generic ("Other") diagrams: built ONLY from general BlockSuite shapes
5
6
  * (`shape` rect/polygon, `text`, `connector`) per the composition principle —
6
7
  * they belong to no framework, so they ship from the template package itself.
7
8
  */
8
- const DARK = '#262626';
9
- const MUTED = '#9aa0a6';
9
+ /**
10
+ * Their neutrals are the shared notation scale's, like every framework's:
11
+ * the ink, the divider grey, the label grey and the card white. Only
12
+ * the hues (kanban cards, gantt bars) are the diagrams' own.
13
+ */
14
+ const DARK = NOTATION_NEUTRALS.ink;
15
+ const MUTED = NOTATION_NEUTRALS.divider;
10
16
  function rect(x, y, w, h, opts = {}) {
11
17
  const el = {
12
18
  type: 'shape',
13
19
  shapeType: opts.shapeType ?? 'rect',
14
20
  filled: true,
15
- fillColor: opts.fill ?? '#ffffff',
21
+ fillColor: opts.fill ?? NOTATION_NEUTRALS.cardFill,
16
22
  strokeColor: opts.stroke ?? DARK,
17
23
  strokeWidth: opts.sw ?? 2,
18
24
  strokeStyle: opts.dash ? StrokeStyle.Dash : StrokeStyle.Solid,
@@ -36,7 +42,7 @@ function label(x, y, w, h, str, opts = {}) {
36
42
  return {
37
43
  type: 'text',
38
44
  text: surfaceText(str),
39
- color: opts.color ?? '#1a1a1a',
45
+ color: opts.color ?? DARK,
40
46
  fontFamily: FontFamily.Inter,
41
47
  fontSize: opts.fontSize ?? 16,
42
48
  fontWeight: opts.weight ?? FontWeight.Regular,
@@ -72,7 +78,12 @@ function swot() {
72
78
  }
73
79
  // ── Kanban (To do / Doing / Done) ─────────────────────────────────────
74
80
  function kanban() {
75
- const colOpts = { fill: '#f4f4f5', stroke: MUTED, sw: 1.5, radius: 10 };
81
+ const colOpts = {
82
+ fill: NOTATION_NEUTRALS.cardFill,
83
+ stroke: MUTED,
84
+ sw: 1.5,
85
+ radius: 10,
86
+ };
76
87
  const head = {
77
88
  fontSize: 18,
78
89
  weight: FontWeight.Medium,
@@ -109,7 +120,10 @@ function bmc() {
109
120
  });
110
121
  const hdr = (x, s) => ({
111
122
  box: rect(x, 8, 128, 34, { stroke: MUTED, sw: 1 }),
112
- txt: label(x + 8, 16, 120, 18, s, { fontSize: 11, color: '#5f6368' }),
123
+ txt: label(x + 8, 16, 120, 18, s, {
124
+ fontSize: 11,
125
+ color: NOTATION_NEUTRALS.label,
126
+ }),
113
127
  });
114
128
  const h1 = hdr(470, 'Designed for');
115
129
  const h2 = hdr(602, 'Designed by');
@@ -156,7 +170,7 @@ function fishbone() {
156
170
  sw: 1.4,
157
171
  dash: true,
158
172
  text: n,
159
- textColor: '#5f6368',
173
+ textColor: NOTATION_NEUTRALS.label,
160
174
  fontSize: 13,
161
175
  });
162
176
  const out = {
@@ -190,10 +204,13 @@ function gantt() {
190
204
  // week gridlines + labels
191
205
  for (let i = 0; i < 6; i++) {
192
206
  const x = 220 + i * 130;
193
- out[`g${i}`] = line(x, 40, x, 268, { stroke: '#e0e0e0', sw: 1 });
207
+ out[`g${i}`] = line(x, 40, x, 268, {
208
+ stroke: NOTATION_NEUTRALS.cardBorder,
209
+ sw: 1,
210
+ });
194
211
  out[`w${i}`] = label(x - 16, 12, 40, 20, `W${i + 1}`, {
195
212
  fontSize: 12,
196
- color: '#5f6368',
213
+ color: NOTATION_NEUTRALS.label,
197
214
  align: TextAlign.Center,
198
215
  });
199
216
  }
@@ -212,11 +229,11 @@ function gantt() {
212
229
  }
213
230
  const ATTRS = 'width="100%" height="100%" viewBox="0 0 135 80" xmlns="http://www.w3.org/2000/svg"';
214
231
  const previews = {
215
- swot: `<svg ${ATTRS} fill="none"><rect x="20" y="12" width="95" height="56" stroke="#262626" stroke-width="2"/><path d="M67.5 12 V68 M20 40 H115" stroke="#262626" stroke-width="1.6"/></svg>`,
216
- kanban: `<svg ${ATTRS} fill="none"><rect x="10" y="12" width="35" height="56" rx="4" stroke="#9aa0a6"/><rect x="50" y="12" width="35" height="56" rx="4" stroke="#9aa0a6"/><rect x="90" y="12" width="35" height="56" rx="4" stroke="#9aa0a6"/><rect x="15" y="22" width="25" height="11" rx="2" fill="#fde6c8"/><rect x="55" y="22" width="25" height="11" rx="2" fill="#d6e4fb"/><rect x="95" y="22" width="25" height="11" rx="2" fill="#d5efd9"/></svg>`,
217
- bmc: `<svg ${ATTRS} fill="none"><g stroke="#262626" stroke-width="1.2"><rect x="8" y="14" width="22" height="40"/><rect x="32" y="14" width="22" height="20"/><rect x="32" y="35" width="22" height="19"/><rect x="56" y="14" width="22" height="40"/><rect x="80" y="14" width="22" height="20"/><rect x="80" y="35" width="22" height="19"/><rect x="104" y="14" width="22" height="40"/><rect x="8" y="56" width="57" height="14"/><rect x="68" y="56" width="58" height="14"/></g></svg>`,
218
- fishbone: `<svg ${ATTRS} fill="none"><path d="M14 40 H112" stroke="#262626" stroke-width="3"/><rect x="112" y="33" width="20" height="14" stroke="#262626" stroke-width="1.4"/><path d="M40 18 L52 40 M40 62 L52 40 M84 18 L96 40 M84 62 L96 40" stroke="#262626" stroke-width="1.4"/></svg>`,
219
- gantt: `<svg ${ATTRS} fill="none"><path d="M40 14 V70 M62 14 V70 M84 14 V70 M106 14 V70" stroke="#e0e0e0"/><rect x="40" y="22" width="34" height="8" rx="2" fill="#4574c4"/><rect x="52" y="36" width="44" height="8" rx="2" fill="#2f9e95"/><rect x="62" y="50" width="50" height="8" rx="2" fill="#d99a2b"/><rect x="84" y="64" width="28" height="8" rx="2" fill="#43a06b"/></svg>`,
232
+ swot: `<svg ${ATTRS} fill="none"><rect x="20" y="12" width="95" height="56" stroke="${DARK}" stroke-width="2"/><path d="M67.5 12 V68 M20 40 H115" stroke="${DARK}" stroke-width="1.6"/></svg>`,
233
+ kanban: `<svg ${ATTRS} fill="none"><rect x="10" y="12" width="35" height="56" rx="4" stroke="${MUTED}"/><rect x="50" y="12" width="35" height="56" rx="4" stroke="${MUTED}"/><rect x="90" y="12" width="35" height="56" rx="4" stroke="${MUTED}"/><rect x="15" y="22" width="25" height="11" rx="2" fill="#fde6c8"/><rect x="55" y="22" width="25" height="11" rx="2" fill="#d6e4fb"/><rect x="95" y="22" width="25" height="11" rx="2" fill="#d5efd9"/></svg>`,
234
+ bmc: `<svg ${ATTRS} fill="none"><g stroke="${DARK}" stroke-width="1.2"><rect x="8" y="14" width="22" height="40"/><rect x="32" y="14" width="22" height="20"/><rect x="32" y="35" width="22" height="19"/><rect x="56" y="14" width="22" height="40"/><rect x="80" y="14" width="22" height="20"/><rect x="80" y="35" width="22" height="19"/><rect x="104" y="14" width="22" height="40"/><rect x="8" y="56" width="57" height="14"/><rect x="68" y="56" width="58" height="14"/></g></svg>`,
235
+ fishbone: `<svg ${ATTRS} fill="none"><path d="M14 40 H112" stroke="${DARK}" stroke-width="3"/><rect x="112" y="33" width="20" height="14" stroke="${DARK}" stroke-width="1.4"/><path d="M40 18 L52 40 M40 62 L52 40 M84 18 L96 40 M84 62 L96 40" stroke="${DARK}" stroke-width="1.4"/></svg>`,
236
+ gantt: `<svg ${ATTRS} fill="none"><path d="M40 14 V70 M62 14 V70 M84 14 V70 M106 14 V70" stroke="${NOTATION_NEUTRALS.cardBorder}"/><rect x="40" y="22" width="34" height="8" rx="2" fill="#4574c4"/><rect x="52" y="36" width="44" height="8" rx="2" fill="#2f9e95"/><rect x="62" y="50" width="50" height="8" rx="2" fill="#d99a2b"/><rect x="84" y="64" width="28" height="8" rx="2" fill="#43a06b"/></svg>`,
220
237
  };
221
238
  function t(name, preview, elements) {
222
239
  return {
@@ -18,4 +18,7 @@ export declare const UnderthroughExtension: ExtensionType & {
18
18
  export declare const CodeExtension: ExtensionType & {
19
19
  identifier: import("../global/di/types.js").ServiceIdentifier<import("../std/inline/index.js").InlineMarkdownMatch<AffineTextAttributes>>;
20
20
  };
21
+ export declare const EmDashExtension: ExtensionType & {
22
+ identifier: import("../global/di/types.js").ServiceIdentifier<import("../std/inline/index.js").InlineMarkdownMatch<AffineTextAttributes>>;
23
+ };
21
24
  export declare const MarkdownExtensions: ExtensionType[];
@@ -193,6 +193,24 @@ export const CodeExtension = InlineMarkdownExtension({
193
193
  });
194
194
  },
195
195
  });
196
+ // typographic replacement: word -- + space => word — + space
197
+ // not convert: --- + space (divider shortcut), --flag + space, inside inline code
198
+ export const EmDashExtension = InlineMarkdownExtension({
199
+ name: 'em-dash',
200
+ pattern: /(?:^|[^-])--\s$/,
201
+ action: ({ inlineEditor, inlineRange, undoManager }) => {
202
+ const dashes = { index: inlineRange.index - 3, length: 2 };
203
+ const format = inlineEditor.getFormat(dashes);
204
+ if (format.code)
205
+ return;
206
+ undoManager.stopCapturing();
207
+ inlineEditor.insertText(dashes, '—', format);
208
+ inlineEditor.setInlineRange({
209
+ index: inlineRange.index - 1,
210
+ length: 0,
211
+ });
212
+ },
213
+ });
196
214
  export const MarkdownExtensions = [
197
215
  BoldItalicMarkdown,
198
216
  BoldMarkdown,
@@ -201,4 +219,5 @@ export const MarkdownExtensions = [
201
219
  UnderthroughExtension,
202
220
  CodeExtension,
203
221
  LatexExtension,
222
+ EmDashExtension,
204
223
  ];
@@ -0,0 +1,29 @@
1
+ export type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
2
+ export interface HeadingMetrics {
3
+ /** Font size, px. */
4
+ readonly fontSize: number;
5
+ readonly fontWeight: number;
6
+ /** Letter spacing, em. */
7
+ readonly letterSpacing: number;
8
+ /** What the line box adds to the font size, px: `line-height: calc(1em + Npx)`. */
9
+ readonly lineHeightExtra: number;
10
+ /** Space above the heading, px. */
11
+ readonly marginTop: number;
12
+ }
13
+ export declare const HEADING_LEVELS: readonly HeadingLevel[];
14
+ /**
15
+ * Labre's document heading scale — the single source of truth for H1…H6.
16
+ *
17
+ * The sizes step down by a ratio of about 1.25 from the doc title (40px/700)
18
+ * to H3, so each level reads as clearly smaller than the one above and H1
19
+ * never competes with the title. Below H3 the steps tighten: H4–H6 only need
20
+ * to stay above body text. The paragraph styles, the callout emoji and the
21
+ * slash-menu previews derive from this table rather than from the upstream
22
+ * `--affine-font-h-N` theme variables, which no longer drive document headings.
23
+ * The hover affordances (drag-handle grabber, heading-level icon) keep their
24
+ * upstream tuning on purpose: the PO prefers them aligned with each other as
25
+ * they were (recette 11/09/2026), not re-centred on the text line.
26
+ */
27
+ export declare const HEADING_SCALE: Readonly<Record<HeadingLevel, HeadingMetrics>>;
28
+ /** Height of a heading's (single) line box, px: font size + line-height extra. */
29
+ export declare function headingLineBox(level: HeadingLevel): number;
@@ -0,0 +1,70 @@
1
+ export const HEADING_LEVELS = [
2
+ 'h1',
3
+ 'h2',
4
+ 'h3',
5
+ 'h4',
6
+ 'h5',
7
+ 'h6',
8
+ ];
9
+ /**
10
+ * Labre's document heading scale — the single source of truth for H1…H6.
11
+ *
12
+ * The sizes step down by a ratio of about 1.25 from the doc title (40px/700)
13
+ * to H3, so each level reads as clearly smaller than the one above and H1
14
+ * never competes with the title. Below H3 the steps tighten: H4–H6 only need
15
+ * to stay above body text. The paragraph styles, the callout emoji and the
16
+ * slash-menu previews derive from this table rather than from the upstream
17
+ * `--affine-font-h-N` theme variables, which no longer drive document headings.
18
+ * The hover affordances (drag-handle grabber, heading-level icon) keep their
19
+ * upstream tuning on purpose: the PO prefers them aligned with each other as
20
+ * they were (recette 11/09/2026), not re-centred on the text line.
21
+ */
22
+ export const HEADING_SCALE = {
23
+ h1: {
24
+ fontSize: 32,
25
+ fontWeight: 700,
26
+ letterSpacing: -0.02,
27
+ lineHeightExtra: 8,
28
+ marginTop: 18,
29
+ },
30
+ h2: {
31
+ fontSize: 26,
32
+ fontWeight: 600,
33
+ letterSpacing: -0.02,
34
+ lineHeightExtra: 10,
35
+ marginTop: 14,
36
+ },
37
+ h3: {
38
+ fontSize: 20,
39
+ fontWeight: 600,
40
+ letterSpacing: -0.02,
41
+ lineHeightExtra: 8,
42
+ marginTop: 12,
43
+ },
44
+ h4: {
45
+ fontSize: 18,
46
+ fontWeight: 600,
47
+ letterSpacing: -0.015,
48
+ lineHeightExtra: 8,
49
+ marginTop: 12,
50
+ },
51
+ h5: {
52
+ fontSize: 16,
53
+ fontWeight: 600,
54
+ letterSpacing: -0.015,
55
+ lineHeightExtra: 8,
56
+ marginTop: 12,
57
+ },
58
+ h6: {
59
+ fontSize: 15,
60
+ fontWeight: 600,
61
+ letterSpacing: -0.015,
62
+ lineHeightExtra: 8,
63
+ marginTop: 12,
64
+ },
65
+ };
66
+ /** Height of a heading's (single) line box, px: font size + line-height extra. */
67
+ export function headingLineBox(level) {
68
+ const { fontSize, lineHeightExtra } = HEADING_SCALE[level];
69
+ return fontSize + lineHeightExtra;
70
+ }
@@ -22,5 +22,7 @@ export declare const DEFAULT_IMAGE_PROXY_ENDPOINT = "https://affine-worker.toeve
22
22
  export declare const DEFAULT_LINK_PREVIEW_ENDPOINT = "https://affine-worker.toeverything.workers.dev/api/worker/link-preview";
23
23
  export declare const CANVAS_EXPORT_IGNORE_TAGS: string[];
24
24
  export * from './bracket-pairs.js';
25
+ export * from './heading.js';
26
+ export * from './notation.js';
25
27
  export * from './note.js';
26
28
  export * from './text.js';
@@ -73,5 +73,7 @@ export const CANVAS_EXPORT_IGNORE_TAGS = [
73
73
  'AFFINE-BLOCK-SELECTION',
74
74
  ];
75
75
  export * from './bracket-pairs.js';
76
+ export * from './heading.js';
77
+ export * from './notation.js';
76
78
  export * from './note.js';
77
79
  export * from './text.js';
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The neutral scale every framework notation on the canvas shares: inks,
3
+ * greys, borders and the card fill.
4
+ *
5
+ * **Wardley is the reference.** The values are the Wardley map's own (its
6
+ * background `COLORS`, its node stroke and fill, its legend frame), so a
7
+ * Wardley board is unchanged by this module existing; every other framework
8
+ * (BPMN, C4, EDGY, the DDD notations, the generic templates) reads its neutrals
9
+ * from here instead of restating a near-identical grey of its own. One scale,
10
+ * one place to change it.
11
+ *
12
+ * **Hues stay per framework.** An event ring, a C4 container blue, a sticky
13
+ * colour or an evolution red is the notation's meaning and lives in that
14
+ * framework's `consts.ts`. So do the neutrals a notation borrows from an
15
+ * official source (the C4 stencil's `#444444`, the Cynefin and Estuarine SVGs,
16
+ * the EDGY facet picto) and the greys that carry a meaning (a future bounded
17
+ * context, a Core Domain zone): those are content of the notation, not chrome.
18
+ *
19
+ * **No band tint.** The title or header strip of a board (BPMN participant
20
+ * band, C4 board header, a generic template's column) is plain `cardFill`
21
+ * white, set apart by its divider line (PO, 11/09/2026). The Wardley phase
22
+ * bands are not chrome: a graded wash across four zones is the map's meaning,
23
+ * so they stay Wardley's own.
24
+ *
25
+ * **Stored colours are never rewritten.** A creation default is copied into
26
+ * the element when it is placed, so changing a value here repaints only the
27
+ * render-time chrome of existing boards (backgrounds, cards, frames) and the
28
+ * elements created from now on. Any code that recognises an element by its
29
+ * stored colour must keep accepting the value it was created with.
30
+ */
31
+ declare const SCALE: {
32
+ /**
33
+ * Artefact ink: node strokes, text inside or under an artefact, labels,
34
+ * legend text, the default connector stroke (Wardley `NODE_STROKE`).
35
+ */
36
+ ink: string;
37
+ /**
38
+ * Background structure: axes, frame lines, board titles, pool frames and
39
+ * names, axis titles (Wardley `COLORS.axis`).
40
+ */
41
+ frameInk: string;
42
+ /**
43
+ * Secondary labels drawn on a background: headings, tick labels (Wardley
44
+ * `COLORS.label`).
45
+ */
46
+ label: string;
47
+ /**
48
+ * Dashed dividers, group strokes, secondary strokes (Wardley
49
+ * `COLORS.divider`).
50
+ */
51
+ divider: string;
52
+ /** Board, card and node fill (Wardley `COLORS.card` / `NODE_FILL`). */
53
+ cardFill: string;
54
+ /** The board card's border (Wardley `COLORS.cardBorder`). */
55
+ cardBorder: string;
56
+ /** A legend's frame and separators (the Wardley legend). */
57
+ legendBorder: string;
58
+ };
59
+ /**
60
+ * Typed as plain strings on purpose, not `as const`: a framework constant set
61
+ * from the scale (`NODE_STROKE = NOTATION_NEUTRALS.ink`) must stay a `string`,
62
+ * as the hex literal it replaces was, so it can still be passed wherever any
63
+ * colour is (a helper whose parameter type is inferred from a default).
64
+ */
65
+ export declare const NOTATION_NEUTRALS: Readonly<typeof SCALE>;
66
+ export type NotationNeutral = keyof typeof NOTATION_NEUTRALS;
67
+ export {};
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The neutral scale every framework notation on the canvas shares: inks,
3
+ * greys, borders and the card fill.
4
+ *
5
+ * **Wardley is the reference.** The values are the Wardley map's own (its
6
+ * background `COLORS`, its node stroke and fill, its legend frame), so a
7
+ * Wardley board is unchanged by this module existing; every other framework
8
+ * (BPMN, C4, EDGY, the DDD notations, the generic templates) reads its neutrals
9
+ * from here instead of restating a near-identical grey of its own. One scale,
10
+ * one place to change it.
11
+ *
12
+ * **Hues stay per framework.** An event ring, a C4 container blue, a sticky
13
+ * colour or an evolution red is the notation's meaning and lives in that
14
+ * framework's `consts.ts`. So do the neutrals a notation borrows from an
15
+ * official source (the C4 stencil's `#444444`, the Cynefin and Estuarine SVGs,
16
+ * the EDGY facet picto) and the greys that carry a meaning (a future bounded
17
+ * context, a Core Domain zone): those are content of the notation, not chrome.
18
+ *
19
+ * **No band tint.** The title or header strip of a board (BPMN participant
20
+ * band, C4 board header, a generic template's column) is plain `cardFill`
21
+ * white, set apart by its divider line (PO, 11/09/2026). The Wardley phase
22
+ * bands are not chrome: a graded wash across four zones is the map's meaning,
23
+ * so they stay Wardley's own.
24
+ *
25
+ * **Stored colours are never rewritten.** A creation default is copied into
26
+ * the element when it is placed, so changing a value here repaints only the
27
+ * render-time chrome of existing boards (backgrounds, cards, frames) and the
28
+ * elements created from now on. Any code that recognises an element by its
29
+ * stored colour must keep accepting the value it was created with.
30
+ */
31
+ const SCALE = {
32
+ /**
33
+ * Artefact ink: node strokes, text inside or under an artefact, labels,
34
+ * legend text, the default connector stroke (Wardley `NODE_STROKE`).
35
+ */
36
+ ink: '#1f2328',
37
+ /**
38
+ * Background structure: axes, frame lines, board titles, pool frames and
39
+ * names, axis titles (Wardley `COLORS.axis`).
40
+ */
41
+ frameInk: '#3b3d42',
42
+ /**
43
+ * Secondary labels drawn on a background: headings, tick labels (Wardley
44
+ * `COLORS.label`).
45
+ */
46
+ label: '#6b7280',
47
+ /**
48
+ * Dashed dividers, group strokes, secondary strokes (Wardley
49
+ * `COLORS.divider`).
50
+ */
51
+ divider: '#9aa0a6',
52
+ /** Board, card and node fill (Wardley `COLORS.card` / `NODE_FILL`). */
53
+ cardFill: '#ffffff',
54
+ /** The board card's border (Wardley `COLORS.cardBorder`). */
55
+ cardBorder: '#e3e2e4',
56
+ /** A legend's frame and separators (the Wardley legend). */
57
+ legendBorder: '#cfd2d6',
58
+ };
59
+ /**
60
+ * Typed as plain strings on purpose, not `as const`: a framework constant set
61
+ * from the scale (`NODE_STROKE = NOTATION_NEUTRALS.ink`) must stay a `string`,
62
+ * as the hex literal it replaces was, so it can still be passed wherever any
63
+ * colour is (a helper whose parameter type is inferred from a default).
64
+ */
65
+ export const NOTATION_NEUTRALS = Object.freeze(SCALE);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@formicoidea/labre-core",
3
3
  "description": "Labre editor — the full BlockSuite-derived editor as one package.",
4
- "version": "0.39.3",
4
+ "version": "0.40.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "author": "lajola",