@lutrin/core 1.0.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.
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Design tokens of the compiler's DEFAULT theme: a generic, neutral
3
+ * system ("Slate") — no branding. The complete JSON mirror lives in
4
+ * design/themes/default.json (a template to copy when theming an
5
+ * organization); an anti-drift test guarantees it stays identical to
6
+ * these values. Organization brands are KITS (kit.json + theme.json
7
+ * + layouts/ + fonts), resolved by resolveTheme (theme.mjs).
8
+ *
9
+ * Units: the 16:9 slide is 13.333 × 7.5 in, that is exactly
10
+ * 1280 × 720 px at 96 dpi. Everything is therefore composed in pixels on
11
+ * the system's 8 px grid, and converted to inches at render time (px / 96).
12
+ *
13
+ * Themability: each group is an object mutated IN PLACE by
14
+ * `applyTheme()` (theme.mjs) — consumers must read the tokens at call
15
+ * time, never copy them when the module loads. The derived groups
16
+ * (PAGE.margin/gutter/footerHeight, LAYER_SHADES, TREND_INK,
17
+ * SEMANTIC) are recomputed by `deriveTokens()` so they follow a theme's
18
+ * palette.
19
+ */
20
+
21
+ export const COLORS = {
22
+ primary: '1D4ED8',
23
+ primaryLighter: '60A5FA',
24
+ primaryDarker: '1E3A8A',
25
+ brand: '1D4ED8',
26
+ brandBlack: '111827',
27
+ neutralPrimary: '212529',
28
+ neutralSecondary: '637381',
29
+ neutralTertiary: 'ADB2BD',
30
+ neutralStroke: 'CED4DA',
31
+ underground1: 'F8F9FA',
32
+ underground2: 'DEE2E6',
33
+ ground: 'FFFFFF',
34
+ highlightLight: 'EFF6FF',
35
+ highlightStrong: 'DBEAFE',
36
+ informative: '0079C4',
37
+ informativeDark: '004B7B',
38
+ informativeLight: 'E6F5F9',
39
+ positive: '0DA566',
40
+ positiveDark: '025D29',
41
+ positiveLight: 'E7F6F0',
42
+ warning: 'FFB833',
43
+ warningDark: '6C4600',
44
+ warningLight: 'FEFAE6',
45
+ negative: 'D3310A',
46
+ negativeDark: '851A00',
47
+ negativeLight: 'FFEBE6',
48
+ };
49
+
50
+ /**
51
+ * Default font: Arial — installed everywhere, identical rendering in
52
+ * PowerPoint and in browsers without embedding anything. A theme supplies
53
+ * its family through `fonts.body` and its files through `fonts.files`.
54
+ */
55
+ export const FONTS = {
56
+ body: 'Arial',
57
+ mono: 'Courier New',
58
+ };
59
+
60
+ /**
61
+ * Files of the embedded font, themable: `regular`/`bold`/`italic` are
62
+ * the .ttf files embedded in the .pptx; the HTML renderer inlines the
63
+ * same names as .woff2 (same file name, .woff2 extension — supply both
64
+ * formats side by side). A theme's paths are resolved relative to the
65
+ * theme file by resolveTheme(). The default embeds nothing (Arial is
66
+ * already on the machine).
67
+ */
68
+ export const FONT_FILES = {
69
+ regular: null,
70
+ bold: null,
71
+ italic: null,
72
+ };
73
+
74
+ /**
75
+ * Chrome signatures (logos), themable: `cover` is placed at the top of the
76
+ * cover slide, `section` (a reversed variant, meant for the primary
77
+ * background) at the bottom of section slides. `cover`/`section` feed the
78
+ * PPTX (bitmap required: png/jpg); `coverSvg`/`sectionSvg` the HTML output
79
+ * (svg accepted, falling back to the bitmap). Absolute paths after
80
+ * resolveTheme(). The default has no signature: both renderers simply omit
81
+ * the logo when the path is null or not found.
82
+ */
83
+ export const LOGOS = {
84
+ cover: null,
85
+ coverSvg: null,
86
+ section: null,
87
+ sectionSvg: null,
88
+ };
89
+
90
+ /** Sizes in points, derived from the DESIGN.md scale (px × 0.75 = pt),
91
+ * bumped up one notch for projection. */
92
+ export const TYPE = {
93
+ coverTitle: 40,
94
+ coverSubtitle: 20,
95
+ sectionTitle: 32,
96
+ slideTitle: 26,
97
+ sectionHeading: 16, // h2 inside a slide (slot title)
98
+ lead: 16,
99
+ body: 14,
100
+ bullet: 14,
101
+ bulletNested: 13,
102
+ small: 11,
103
+ caption: 9,
104
+ code: 11,
105
+ metricValue: 44,
106
+ metricLabel: 12,
107
+ quote: 22,
108
+ tableBody: 12,
109
+ tableHeader: 12,
110
+ };
111
+
112
+ export const LINE_HEIGHT = 1.4; // structural, not themable (scalar export)
113
+
114
+ /** 8 px grid (DESIGN.md: xs 8 · sm 16 · md 24 · lg 32 · xl 40 · xxl 48). */
115
+ export const SPACE = { xs: 8, sm: 16, md: 24, lg: 32, xl: 40, xxl: 48 };
116
+
117
+ /** Slide geometry, in pixels (1280 × 720 @96 dpi).
118
+ * width/height are the physical 16:9 frame — never themable;
119
+ * margin/gutter/footerHeight are derived from SPACE (deriveTokens). */
120
+ export const PAGE = {
121
+ width: 1280,
122
+ height: 720,
123
+ margin: SPACE.xxl, // 48 px
124
+ gutter: SPACE.md, // 24 px
125
+ titleHeight: 96, // 2 × 48: title zone of content slides
126
+ footerHeight: SPACE.lg, // 32 px
127
+ };
128
+
129
+ /** Utility radii (px). PPTX rectangles take a 0–1 ratio. */
130
+ export const ROUNDED = { sm: 2, md: 4, lg: 8, pill: 64 };
131
+
132
+ /**
133
+ * Chrome of the cover/section layouts and of the content masters — geometry
134
+ * shared by BOTH renderers (pptx and html) so that parity is structural
135
+ * rather than two sets of literals to keep in sync.
136
+ * All values in px on the 1280 × 720 grid.
137
+ */
138
+ export const CHROME = {
139
+ cover: {
140
+ barY: 280, // accent bar above the title
141
+ barW: 96,
142
+ barH: 6,
143
+ titleY: 304,
144
+ titleH: 120,
145
+ subtitleY: 424,
146
+ subtitleH: 72,
147
+ bylineBottom: 80, // distance byline → bottom of page
148
+ bylineH: 32,
149
+ logoH: 44,
150
+ },
151
+ section: {
152
+ titleY: 288,
153
+ titleH: 144,
154
+ logoH: 32,
155
+ },
156
+ title: {
157
+ accentW: 64, // accent segment of the title rule
158
+ accentH: 4,
159
+ },
160
+ footer: {
161
+ textW: 600,
162
+ h: 24,
163
+ numW: 64, // page number, right-aligned
164
+ },
165
+ };
166
+
167
+ export const px = (v) => v / 96; // px → inches
168
+
169
+ /** Usable content area of a content slide (below the title, above the footer). */
170
+ export function contentArea() {
171
+ const x = PAGE.margin;
172
+ const y = PAGE.titleHeight + SPACE.sm;
173
+ return {
174
+ x,
175
+ y,
176
+ w: PAGE.width - 2 * PAGE.margin,
177
+ h: PAGE.height - y - PAGE.footerHeight - SPACE.sm,
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Categorical chart palette — six neutral hues (teal, ochre, blue, red,
183
+ * dark blue, brown), lightness and chroma tuned to pass the six dataviz
184
+ * accessibility checks (OKLCH band 0.43–0.77, chroma ≥ 0.10, adjacent CVD
185
+ * ΔE ≥ 12, contrast ≥ 3:1 on white). Fixed order — never assign in a
186
+ * loop: past six series, group the rest under "Other". Palette independent
187
+ * of COLORS: a theme that changes `primary` must supply its own
188
+ * `chartColors`.
189
+ */
190
+ export const CHART_COLORS = ['0A8A76', 'B87F00', '0079C4', 'D3310A', '005E99', '8A5C00'];
191
+
192
+ /**
193
+ * Shades of the `layers` layout (architecture layers), from the base (dark)
194
+ * up to the surface (light) — only hues of the primary token, the system's
195
+ * only accent. `ink` = the ink legible on the shade (contrast ≥ 4.5:1);
196
+ * white on the two dark shades only.
197
+ * Derived from COLORS — recipe in deriveTokens().
198
+ */
199
+ export const LAYER_SHADES = [];
200
+
201
+ /** Trend inks of the `:::metric` cards (dark: small body text).
202
+ * Derived from COLORS — recipe in deriveTokens(). */
203
+ export const TREND_INK = {};
204
+
205
+ /** Tints of the :::info/success/warning/danger callouts.
206
+ * fill/text derived from COLORS (deriveTokens); label localizable. */
207
+ export const SEMANTIC = {};
208
+
209
+ /**
210
+ * Recipes for the groups derived from COLORS/SPACE: run when the module
211
+ * loads, then re-run by applyTheme() AFTER a theme is merged so that
212
+ * LAYER_SHADES, SEMANTIC, TREND_INK and PAGE's margins follow the
213
+ * palette — an explicit theme override on a derived group is then
214
+ * re-merged on top (theme.mjs).
215
+ */
216
+ export function deriveTokens() {
217
+ PAGE.margin = SPACE.xxl;
218
+ PAGE.gutter = SPACE.md;
219
+ PAGE.footerHeight = SPACE.lg;
220
+
221
+ LAYER_SHADES.splice(
222
+ 0,
223
+ LAYER_SHADES.length,
224
+ { fill: COLORS.primaryDarker, ink: COLORS.ground },
225
+ { fill: COLORS.primary, ink: COLORS.ground },
226
+ { fill: COLORS.primaryLighter, ink: COLORS.neutralPrimary },
227
+ { fill: COLORS.highlightStrong, ink: COLORS.primaryDarker },
228
+ { fill: COLORS.highlightLight, ink: COLORS.primaryDarker },
229
+ );
230
+
231
+ Object.assign(TREND_INK, {
232
+ positive: COLORS.positiveDark,
233
+ negative: COLORS.negativeDark,
234
+ neutral: COLORS.neutralSecondary,
235
+ });
236
+
237
+ Object.assign(SEMANTIC, {
238
+ info: { fill: COLORS.informativeLight, text: COLORS.informativeDark, label: 'Info' },
239
+ success: { fill: COLORS.positiveLight, text: COLORS.positiveDark, label: 'Key point' },
240
+ warning: { fill: COLORS.warningLight, text: COLORS.warningDark, label: 'Caution' },
241
+ danger: { fill: COLORS.negativeLight, text: COLORS.negativeDark, label: 'Important' },
242
+ });
243
+ }
244
+ deriveTokens();
245
+
246
+ /**
247
+ * Panel styles of the structured layouts (comparison, pillars, layers,
248
+ * swot): fill + rule per variant — a flat system, no shadow. Shared by
249
+ * both renderers to guarantee identical rendering.
250
+ */
251
+ export function panelStyle(block) {
252
+ switch (block.variant) {
253
+ case 'accent':
254
+ // solid accent bar (focus layout) — same ink as the title rule
255
+ return { fill: COLORS.primary, line: null };
256
+ case 'highlight':
257
+ return { fill: COLORS.highlightLight, line: { color: COLORS.primary, width: 1.25 } };
258
+ case 'pillar':
259
+ return { fill: COLORS.ground, line: { color: COLORS.neutralStroke, width: 1 } };
260
+ case 'semantic':
261
+ return { fill: (SEMANTIC[block.kind] ?? SEMANTIC.info).fill, line: null };
262
+ case 'layer':
263
+ return { fill: (LAYER_SHADES[block.shade] ?? LAYER_SHADES[0]).fill, line: null };
264
+ case 'muted':
265
+ default:
266
+ return { fill: COLORS.underground1, line: { color: COLORS.neutralStroke, width: 1 } };
267
+ }
268
+ }