@vettvangur/design-system 1.0.1 → 1.0.3
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,477 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { promises } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { p as parseColors, a as parseTypography, i as inputsSpec } from './inputs-RHH3YuXh.js';
|
|
5
|
+
import { p as parseButtons } from './buttons-o2a9tpOe.js';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import 'node:url';
|
|
8
|
+
import 'node:process';
|
|
9
|
+
|
|
10
|
+
// core/razor/generate-razor.mjs
|
|
11
|
+
const CWD = process.cwd();
|
|
12
|
+
const OUT_DIR = path.resolve(CWD, '../Vettvangur.Site/Views/Partials/DesignSystem');
|
|
13
|
+
const FILE_COLORS = path.join(OUT_DIR, 'Colors.cshtml');
|
|
14
|
+
const FILE_TYPO = path.join(OUT_DIR, 'Typography.cshtml');
|
|
15
|
+
const FILE_BUTTONS = path.join(OUT_DIR, 'Buttons.cshtml');
|
|
16
|
+
const FILE_INPUTS = path.join(OUT_DIR, 'Inputs.cshtml');
|
|
17
|
+
const FILE_RICHTEXT = path.join(OUT_DIR, 'Richtext.cshtml');
|
|
18
|
+
const FILE_Table = path.join(OUT_DIR, 'Tables.cshtml');
|
|
19
|
+
const tag = chalk.cyan('[design-system]');
|
|
20
|
+
async function generateRazor(figma) {
|
|
21
|
+
console.log(`${tag} generating razor files...`);
|
|
22
|
+
await promises.mkdir(OUT_DIR, {
|
|
23
|
+
recursive: true
|
|
24
|
+
});
|
|
25
|
+
const [colors, typography, buttons] = await Promise.all([parseColors(), parseTypography(), parseButtons(figma)]);
|
|
26
|
+
|
|
27
|
+
// Colors
|
|
28
|
+
if (colors?.length) {
|
|
29
|
+
await promises.writeFile(FILE_COLORS, renderColors(colors), 'utf8');
|
|
30
|
+
log(FILE_COLORS, colors.length);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Typography (one file)
|
|
34
|
+
if ((typography?.headlines?.length ?? 0) + (typography?.bodies?.length ?? 0) > 0) {
|
|
35
|
+
await promises.writeFile(FILE_TYPO, renderTypography(typography), 'utf8');
|
|
36
|
+
log(FILE_TYPO, (typography.headlines?.length ?? 0) + (typography.bodies?.length ?? 0));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Buttons
|
|
40
|
+
if (buttons?.length) {
|
|
41
|
+
await promises.writeFile(FILE_BUTTONS, renderButtons(buttons), 'utf8');
|
|
42
|
+
log(FILE_BUTTONS, buttons.length);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Inputs
|
|
46
|
+
const hasAnyInputs = Object.values(inputsSpec).some(v => Array.isArray(v) && v.length);
|
|
47
|
+
if (hasAnyInputs) {
|
|
48
|
+
await promises.writeFile(FILE_INPUTS, renderInputs(inputsSpec), 'utf8');
|
|
49
|
+
log(FILE_INPUTS, countInputs(inputsSpec));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Richtext & Tables (static demos)
|
|
53
|
+
await promises.writeFile(FILE_RICHTEXT, renderRichtext(), 'utf8');
|
|
54
|
+
log(FILE_RICHTEXT, 1);
|
|
55
|
+
await promises.writeFile(FILE_Table, renderRichtextTables(), 'utf8');
|
|
56
|
+
log(FILE_Table, 1);
|
|
57
|
+
console.log(`${tag} great success!`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* -------------------- RENDERERS -------------------- */
|
|
61
|
+
|
|
62
|
+
function renderColors(colors) {
|
|
63
|
+
const items = colors.map(({
|
|
64
|
+
title,
|
|
65
|
+
color
|
|
66
|
+
}) => `
|
|
67
|
+
<div class="styleguide-color">
|
|
68
|
+
<span class="styleguide-color__label label">${h(title)}</span>
|
|
69
|
+
<div class="styleguide-color__swatch" style="background: ${a(color)};"></div>
|
|
70
|
+
<div class="styleguide-color__info">
|
|
71
|
+
<span class="styleguide-color__info-hex label">${h(color)}</span>
|
|
72
|
+
</div>
|
|
73
|
+
</div>`).join('\n');
|
|
74
|
+
return `@* Auto-generated — do not edit by hand *@
|
|
75
|
+
<section class="styleguide-colors styleguide-spacing">
|
|
76
|
+
<div class="container">
|
|
77
|
+
<vv-headline
|
|
78
|
+
identifier="styleguide__headline",
|
|
79
|
+
modifier="headline-h1",
|
|
80
|
+
text="Colors"
|
|
81
|
+
/>
|
|
82
|
+
|
|
83
|
+
<div class="styleguide-colors-grid grid gap-[20px]">
|
|
84
|
+
${items}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</section>
|
|
88
|
+
`;
|
|
89
|
+
}
|
|
90
|
+
function renderTypography({
|
|
91
|
+
headlines = [],
|
|
92
|
+
bodies = []
|
|
93
|
+
}) {
|
|
94
|
+
const headlineItems = headlines.map(hItem => {
|
|
95
|
+
const mod = classToModifier(hItem.class, 'headline');
|
|
96
|
+
const info = infoBlocks(hItem);
|
|
97
|
+
return `
|
|
98
|
+
<div class="styleguide-typography__item grid gap-[20px]">
|
|
99
|
+
<span class="styleguide-typography__item-name">${h(hItem.title)}</span>
|
|
100
|
+
<vv-headline
|
|
101
|
+
identifier="styleguide__headline"
|
|
102
|
+
modifier="${a(mod)}"
|
|
103
|
+
text="The quick brown fox jumps over the lazy dog."
|
|
104
|
+
/>
|
|
105
|
+
${info ? renderInfo(info) : ''}
|
|
106
|
+
</div>`;
|
|
107
|
+
}).join('\n');
|
|
108
|
+
const bodyItems = bodies.map(b => {
|
|
109
|
+
const info = infoBlocks(b);
|
|
110
|
+
return `
|
|
111
|
+
<div class="styleguide-typography__item grid gap-[20px]>
|
|
112
|
+
<span class="styleguide-typography__item-name">${h(b.title)}</span>
|
|
113
|
+
<p class="${a(b.class)} styleguide-typography__item-example">
|
|
114
|
+
The quick brown fox jumps over the lazy dog.
|
|
115
|
+
</p>
|
|
116
|
+
${info ? renderInfo(info) : ''}
|
|
117
|
+
</div>`;
|
|
118
|
+
}).join('\n');
|
|
119
|
+
return `@* Auto-generated — do not edit by hand *@
|
|
120
|
+
<section class="styleguide-typography styleguide-spacing">
|
|
121
|
+
<div class="container">
|
|
122
|
+
<vv-headline
|
|
123
|
+
identifier="styleguide__headline"
|
|
124
|
+
modifier="headline-h1"
|
|
125
|
+
text="Typography"
|
|
126
|
+
/>
|
|
127
|
+
|
|
128
|
+
${headlines.length ? `
|
|
129
|
+
<div class="styleguide-typography__group grid gap-[20px]>
|
|
130
|
+
<vv-headline
|
|
131
|
+
identifier="styleguide__headline"
|
|
132
|
+
modifier="headline-h3"
|
|
133
|
+
text="Headlines"
|
|
134
|
+
/>
|
|
135
|
+
<div class="styleguide-typography__list grid gap-[20px]>
|
|
136
|
+
${headlineItems}
|
|
137
|
+
</div>
|
|
138
|
+
</div>` : ''}
|
|
139
|
+
|
|
140
|
+
${bodies.length ? `
|
|
141
|
+
<div class="styleguide-typography__group grid gap-[20px]>
|
|
142
|
+
<vv-headline
|
|
143
|
+
identifier="styleguide__headline"
|
|
144
|
+
modifier="headline-h3"
|
|
145
|
+
text="Bodies"
|
|
146
|
+
/>
|
|
147
|
+
<div class="styleguide-typography__list grid gap-[20px]>
|
|
148
|
+
${bodyItems}
|
|
149
|
+
</div>
|
|
150
|
+
</div>` : ''}
|
|
151
|
+
</div>
|
|
152
|
+
</section>
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
155
|
+
function renderInfo(info) {
|
|
156
|
+
const {
|
|
157
|
+
base,
|
|
158
|
+
desktop,
|
|
159
|
+
mobile
|
|
160
|
+
} = info;
|
|
161
|
+
return `
|
|
162
|
+
<div class="styleguide-typography__info">
|
|
163
|
+
${base ? `
|
|
164
|
+
<div class="styleguide-typography__info-item">
|
|
165
|
+
<span>Base</span>
|
|
166
|
+
<span><strong>${h(base)}</strong></span>
|
|
167
|
+
</div>` : ''}
|
|
168
|
+
${desktop ? `
|
|
169
|
+
<div class="styleguide-typography__info-item">
|
|
170
|
+
<span>Desktop</span>
|
|
171
|
+
<span><strong>${h(desktop)}</strong></span>
|
|
172
|
+
</div>` : ''}
|
|
173
|
+
${mobile ? `
|
|
174
|
+
<div class="styleguide-typography__info-item">
|
|
175
|
+
<span>Mobile</span>
|
|
176
|
+
<span><strong>${h(mobile)}</strong></span>
|
|
177
|
+
</div>` : ''}
|
|
178
|
+
</div>`;
|
|
179
|
+
}
|
|
180
|
+
function renderButtons(buttons) {
|
|
181
|
+
const items = buttons.map(b => {
|
|
182
|
+
const label = pickButtonText(b);
|
|
183
|
+
return `
|
|
184
|
+
<div class="styleguide__button-wrap">
|
|
185
|
+
<vv-button
|
|
186
|
+
identifier="styleguide__button"
|
|
187
|
+
modifier="${a(b)}"
|
|
188
|
+
type="@ButtonType.Button"
|
|
189
|
+
text="${a(label)}"
|
|
190
|
+
/>
|
|
191
|
+
</div>`;
|
|
192
|
+
}).join('\n');
|
|
193
|
+
return `@* Auto-generated — do not edit by hand *@
|
|
194
|
+
<section class="styleguide-buttons styleguide-spacing">
|
|
195
|
+
<div class="container">
|
|
196
|
+
<vv-headline
|
|
197
|
+
identifier="styleguide__headline",
|
|
198
|
+
modifier="headline-h1",
|
|
199
|
+
text="Buttons"
|
|
200
|
+
/>
|
|
201
|
+
|
|
202
|
+
<div class="grid gap-[20px]>
|
|
203
|
+
${items}
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</section>
|
|
207
|
+
`;
|
|
208
|
+
}
|
|
209
|
+
function pickButtonText(modifier) {
|
|
210
|
+
const m = (modifier || '').toLowerCase();
|
|
211
|
+
|
|
212
|
+
// intent-based overrides
|
|
213
|
+
if (/\b(danger|destructive|error|delete|remove)\b/.test(m)) return 'Delete';
|
|
214
|
+
if (/\b(success|confirm|ok|apply|save)\b/.test(m)) return 'Confirm';
|
|
215
|
+
if (/\b(warn|warning|alert)\b/.test(m)) return 'Proceed with Caution';
|
|
216
|
+
if (/\b(primary|cta|proceed|next)\b/.test(m)) return 'Continue';
|
|
217
|
+
if (/\b(secondary|ghost|tertiary|neutral)\b/.test(m)) return 'Learn More';
|
|
218
|
+
if (/\b(buy|checkout|pay|purchase|cart)\b/.test(m)) return 'Buy Now';
|
|
219
|
+
if (/\b(download|install|get)\b/.test(m)) return 'Download';
|
|
220
|
+
if (/\b(subscribe|signup|register)\b/.test(m)) return 'Subscribe';
|
|
221
|
+
if (/\b(login|signin)\b/.test(m)) return 'Sign In';
|
|
222
|
+
if (/\b(contact|email|message)\b/.test(m)) return 'Contact Us';
|
|
223
|
+
|
|
224
|
+
// size-only or shape-only variants keep a neutral verb
|
|
225
|
+
if (/\b(xs|sm|md|lg|xl|square|pill|round|circle|wide|tall)\b/.test(m)) return seededPick(m, ['Continue', 'Details', 'Open', 'Select', 'View', 'Explore', 'Try', 'Start']);
|
|
226
|
+
|
|
227
|
+
// icon-only hints (still provide text for a11y)
|
|
228
|
+
if (/\b(icon|only|icon-only)\b/.test(m)) return 'Open';
|
|
229
|
+
|
|
230
|
+
// default: deterministic pick
|
|
231
|
+
return seededPick(m, DEFAULT_WORDS);
|
|
232
|
+
}
|
|
233
|
+
const DEFAULT_WORDS = ['Continue', 'Details', 'Select', 'Open', 'Start', 'Try', 'Explore', 'Next', 'View', 'Confirm', 'Apply', 'Proceed', 'Get Quote', 'Book Now', 'Add to Cart', 'Learn More', 'Download', 'Subscribe'];
|
|
234
|
+
|
|
235
|
+
// Deterministic selection from a list based on the modifier string
|
|
236
|
+
function seededPick(seed, list) {
|
|
237
|
+
let h = 0;
|
|
238
|
+
for (let i = 0; i < seed.length; i++) h = h * 131 + seed.charCodeAt(i) >>> 0;
|
|
239
|
+
return list[h % list.length];
|
|
240
|
+
}
|
|
241
|
+
function renderInputs(spec) {
|
|
242
|
+
const {
|
|
243
|
+
inputs = [],
|
|
244
|
+
selects = [],
|
|
245
|
+
checkboxes = [],
|
|
246
|
+
radios = [],
|
|
247
|
+
textareas = []
|
|
248
|
+
} = spec;
|
|
249
|
+
const inputsPartials = inputs.map(i => `
|
|
250
|
+
<vv-input
|
|
251
|
+
id="${a(i.id || '')}"
|
|
252
|
+
label="${a(i.Label ?? i.label ?? 'Input')}"
|
|
253
|
+
placeholder="${a(i.placeholder ?? 'Input placeholder')}"
|
|
254
|
+
is-disabled="${bool(i.isDisabled)}"
|
|
255
|
+
is-readonly="${bool(i.isReadonly)}"
|
|
256
|
+
is-error="${bool(i.isError)}"
|
|
257
|
+
/>
|
|
258
|
+
`).join('\n');
|
|
259
|
+
const selectsPartials = selects.map(s => `
|
|
260
|
+
<vv-select
|
|
261
|
+
id="${a(s.id || '')}"
|
|
262
|
+
label="${a(s.Label ?? 'Select')}"
|
|
263
|
+
options="@selectOptions"
|
|
264
|
+
is-disabled="${bool(s.isDisabled)}"
|
|
265
|
+
is-readOnly="${bool(s.isReadonly)}"
|
|
266
|
+
is-error="${bool(s.isError)}"
|
|
267
|
+
/>
|
|
268
|
+
`).join('\n');
|
|
269
|
+
const checkboxRows = checkboxes.map(c => `
|
|
270
|
+
<div class="input-group">
|
|
271
|
+
<vv-selection
|
|
272
|
+
id="${a(c.id || '')}"
|
|
273
|
+
label="${a(c.Label ?? c.label ?? 'Checkbox')}"
|
|
274
|
+
type="@HTMLInputType.Checkbox"
|
|
275
|
+
is-disabled="${bool(c.isDisabled)}"
|
|
276
|
+
is-error="${bool(c.isError)}"
|
|
277
|
+
/>
|
|
278
|
+
</div>
|
|
279
|
+
`).join('\n');
|
|
280
|
+
const radioRows = radios.map(r => `
|
|
281
|
+
<div class="input-group">
|
|
282
|
+
<vv-selection
|
|
283
|
+
id="${a(r.id || '')}"
|
|
284
|
+
type="@HTMLInputType.Radio"
|
|
285
|
+
name="${a(r.Name ?? r.name ?? 'prettyradio')}"
|
|
286
|
+
label="${a(r.Label ?? r.label ?? 'Radio')}"
|
|
287
|
+
is-disabled="${bool(r.isDisabled)}"
|
|
288
|
+
is-error="${bool(r.isError)}"
|
|
289
|
+
/>
|
|
290
|
+
</div>`).join('\n');
|
|
291
|
+
const textareasPartials = textareas.map(t => `
|
|
292
|
+
<vv-textarea
|
|
293
|
+
id="${a(t.id || '')}"
|
|
294
|
+
label="${a(t.Label ?? 'Textarea')}"
|
|
295
|
+
placeholder="${a(t.placeholder ?? 'Textarea placeholder')}"
|
|
296
|
+
is-disabled="${bool(t.isDisabled)}"
|
|
297
|
+
is-readonly="${bool(t.isReadonly)}"
|
|
298
|
+
is-error="${bool(t.isError)}"
|
|
299
|
+
/>
|
|
300
|
+
`).join('\n');
|
|
301
|
+
return `@* Auto-generated — do not edit by hand *@
|
|
302
|
+
@{
|
|
303
|
+
var selectOptions = new Dictionary<string, string>() {
|
|
304
|
+
{ "option1", "Option value" },
|
|
305
|
+
{ "option2", "Option value" },
|
|
306
|
+
{ "option3", "Option value" },
|
|
307
|
+
{ "option4", "Option value" },
|
|
308
|
+
{ "option5", "Option value" },
|
|
309
|
+
{ "option6", "Option value" },
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
<section class="styleguide-inputs styleguide-spacing">
|
|
314
|
+
<div class="container">
|
|
315
|
+
<vv-headline
|
|
316
|
+
identifier="styleguide__headline",
|
|
317
|
+
modifier="headline-h1",
|
|
318
|
+
text="Inputs"
|
|
319
|
+
/>
|
|
320
|
+
|
|
321
|
+
<div class="styleguide-inputs__scrollitem grid grid--gap grid--align-start">
|
|
322
|
+
${inputsPartials}
|
|
323
|
+
${selectsPartials}
|
|
324
|
+
${checkboxRows}
|
|
325
|
+
${radioRows}
|
|
326
|
+
${textareasPartials}
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
</section>
|
|
330
|
+
`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* ------------ Richtext (static demo) ------------ */
|
|
334
|
+
function renderRichtext() {
|
|
335
|
+
return `@* Auto-generated — do not edit by hand *@
|
|
336
|
+
<section class="w-full">
|
|
337
|
+
<vv-headline
|
|
338
|
+
identifier="styleguide__headline",
|
|
339
|
+
modifier="headline-h1",
|
|
340
|
+
text="Richtext"
|
|
341
|
+
/>
|
|
342
|
+
|
|
343
|
+
<div class="richtext">
|
|
344
|
+
<h1>h1</h1>
|
|
345
|
+
<h2>h2</h2>
|
|
346
|
+
<h3>h3</h3>
|
|
347
|
+
<h4>h4</h4>
|
|
348
|
+
<h5>h5</h5>
|
|
349
|
+
<p>
|
|
350
|
+
Ófróðlegt þorviðar <a href="https://www.vettvangur.is/" target="_blank">látist kærðu</a>, allfeginn því að kaupmanni, <strong>hraðasta</strong> ættleri <em>farminum</em> þuríði brústeinunum konungsþræll grænar. Hólmganga hrísflekkur, lifum skalla-grímur tíkr. Meiðinn hatast oddleifsdóttir, dólglegast dyflini, torfafóstri torfunni rak jarðarmenið stirðir. Býsn hvé ölseljunni gerði, jafnvitur lög vaskligr, sám hvortveggja ófrelsi trúfasti hallkelsdóttur sneri mund guðmundarsonum. Sekum meginmerkurinnar, arfsali allmikilli sjaldgæf. Uggasyni sættu konungsþræll gæs, alþýðuskap orðtækið stokkar, orkar mund ormláðs skrumaði hriflusonar virðak. Biðjum meiðinn keppa, taliðr uggasyni, skerðan manngjarnlega hofgyðja vilk allmikilli. Ógott hirðum samdóma, heimamaðr endilangt, erlingur kaldur gjósa seimþollr hallkell landkaupi. Torfunni tungu-oddur þorviðar gufárós, ævifús sekur tröllskessa, umbanda rétttrúaður bráðan svörfuði nýt íþrótt dalalönd halli. Flyðruness haldnar aðsóknar skerðan, auðhnykkjanda mannvandur smálöndum sauðarhöfðinu ferjunni.
|
|
351
|
+
</p>
|
|
352
|
+
<ul>
|
|
353
|
+
<li>List item</li>
|
|
354
|
+
<li>List item</li>
|
|
355
|
+
<li>
|
|
356
|
+
List item
|
|
357
|
+
<ul>
|
|
358
|
+
<li>Nested item</li>
|
|
359
|
+
<li>Nested item</li>
|
|
360
|
+
<li>
|
|
361
|
+
Nested item
|
|
362
|
+
<ul>
|
|
363
|
+
<li>Nested even more item</li>
|
|
364
|
+
<li>Nested even more item</li>
|
|
365
|
+
<li>Nested even more item</li>
|
|
366
|
+
</ul>
|
|
367
|
+
</li>
|
|
368
|
+
</ul>
|
|
369
|
+
</li>
|
|
370
|
+
<li>List item</li>
|
|
371
|
+
</ul>
|
|
372
|
+
<ol>
|
|
373
|
+
<li>List item</li>
|
|
374
|
+
<li>
|
|
375
|
+
List item
|
|
376
|
+
<ol>
|
|
377
|
+
<li>Nested item</li>
|
|
378
|
+
<li>Nested item</li>
|
|
379
|
+
<li>Nested item</li>
|
|
380
|
+
</ol>
|
|
381
|
+
</li>
|
|
382
|
+
<li>List item</li>
|
|
383
|
+
<li>List item</li>
|
|
384
|
+
</ol>
|
|
385
|
+
<p>
|
|
386
|
+
Ófróðlegt þorviðar <a href="https://www.vettvangur.is/" target="_blank">látist kærðu</a>, allfeginn því að kaupmanni, hraðasta ættleri farminum þuríði brústeinunum konungsþræll grænar. Hólmganga hrísflekkur, lifum skalla-grímur tíkr. Meiðinn hatast oddleifsdóttir, dólglegast dyflini, torfafóstri torfunni rak jarðarmenið stirðir. Býsn hvé ölseljunni gerði, jafnvitur lög vaskligr, sám hvortveggja ófrelsi trúfasti hallkelsdóttur sneri mund guðmundarsonum. Sekum meginmerkurinnar, arfsali allmikilli sjaldgæf. Uggasyni sættu konungsþræll gæs, alþýðuskap orðtækið stokkar, orkar mund ormláðs skrumaði hriflusonar virðak. Biðjum meiðinn keppa, taliðr uggasyni, skerðan manngjarnlega hofgyðja vilk allmikilli. Ógott hirðum samdóma, heimamaðr endilangt, erlingur kaldur gjósa seimþollr hallkell landkaupi. Torfunni tungu-oddur þorviðar gufárós, ævifús sekur tröllskessa, umbanda rétttrúaður bráðan svörfuði nýt íþrótt dalalönd halli. Flyðruness haldnar aðsóknar skerðan, auðhnykkjanda mannvandur smálöndum sauðarhöfðinu ferjunni.
|
|
387
|
+
</p>
|
|
388
|
+
</div>
|
|
389
|
+
</section>
|
|
390
|
+
`;
|
|
391
|
+
}
|
|
392
|
+
function renderRichtextTables() {
|
|
393
|
+
return `@* Auto-generated — do not edit by hand *@
|
|
394
|
+
<section class="w-full">
|
|
395
|
+
<vv-headline
|
|
396
|
+
identifier="styleguide__headline",
|
|
397
|
+
modifier="headline-h1",
|
|
398
|
+
text="Tables"
|
|
399
|
+
/>
|
|
400
|
+
|
|
401
|
+
<div class="table">
|
|
402
|
+
<table class="table__element">
|
|
403
|
+
<thead>
|
|
404
|
+
<tr>
|
|
405
|
+
<th>Head</th>
|
|
406
|
+
<th>Head</th>
|
|
407
|
+
<th>Head</th>
|
|
408
|
+
<th>Head</th>
|
|
409
|
+
<th>Head</th>
|
|
410
|
+
</tr>
|
|
411
|
+
</thead>
|
|
412
|
+
<tbody>
|
|
413
|
+
<tr>
|
|
414
|
+
<td>1</td><td>2</td><td>3</td><td>4</td><td>5</td>
|
|
415
|
+
</tr>
|
|
416
|
+
<tr>
|
|
417
|
+
<td>1</td><td>2</td><td>3</td><td>4</td><td>5</td>
|
|
418
|
+
</tr>
|
|
419
|
+
<tr>
|
|
420
|
+
<td>1</td><td>2</td><td>3</td><td>4</td><td>5</td>
|
|
421
|
+
</tr>
|
|
422
|
+
</tbody>
|
|
423
|
+
</table>
|
|
424
|
+
</div>
|
|
425
|
+
</section>
|
|
426
|
+
`;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/* -------------------- HELPERS -------------------- */
|
|
430
|
+
function infoBlocks(item) {
|
|
431
|
+
const base = pair(item.size, item.lineheight);
|
|
432
|
+
const desktop = item?.config?.desktop ? pair(item.config.desktop.size, item.config.desktop.lineHeight) : '';
|
|
433
|
+
const mobile = item?.config?.mobile ? pair(item.config.mobile.size, item.config.mobile.lineHeight) : '';
|
|
434
|
+
if (!base && !desktop && !mobile) {
|
|
435
|
+
return '';
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
base,
|
|
439
|
+
desktop,
|
|
440
|
+
mobile
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
function pair(size, lh) {
|
|
444
|
+
if (!size && !lh) {
|
|
445
|
+
return '';
|
|
446
|
+
}
|
|
447
|
+
return `Size: ${size ?? '-'} / Line height: ${lh ?? '-'}`;
|
|
448
|
+
}
|
|
449
|
+
function classToModifier(cls = '', prefix = 'headline') {
|
|
450
|
+
// Accept:
|
|
451
|
+
// - "headline-h1" (preferred)
|
|
452
|
+
// - "headline1" (legacy)
|
|
453
|
+
// - "headline_1" (legacy)
|
|
454
|
+
// Always return "headline-h1".
|
|
455
|
+
const re = new RegExp(`^${prefix}[-_]?h?(\\d+)$`, 'i');
|
|
456
|
+
const m = re.exec(cls.trim());
|
|
457
|
+
if (m) return `${prefix}-h${m[1]}`;
|
|
458
|
+
return cls; // do not mutilate unknown strings
|
|
459
|
+
}
|
|
460
|
+
function countInputs(spec) {
|
|
461
|
+
return ['inputs', 'selects', 'checkboxes', 'radios', 'textareas'].reduce((sum, k) => sum + (spec[k]?.length || 0), 0);
|
|
462
|
+
}
|
|
463
|
+
function a(s = '') {
|
|
464
|
+
return String(s).replaceAll('"', '"');
|
|
465
|
+
}
|
|
466
|
+
function h(s = '') {
|
|
467
|
+
return String(s).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
468
|
+
}
|
|
469
|
+
function bool(v) {
|
|
470
|
+
return v ? 'true' : 'false';
|
|
471
|
+
}
|
|
472
|
+
function log(file, n) {
|
|
473
|
+
const name = path.basename(file);
|
|
474
|
+
console.log(`${tag} ✅ ${name} (${n})`);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export { generateRazor as default };
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { p as paths } from './index.esm.js';
|
|
7
|
+
import { p as parseButtons } from './buttons-o2a9tpOe.js';
|
|
8
|
+
import 'node:process';
|
|
9
|
+
import 'boxen';
|
|
10
|
+
import 'fs';
|
|
11
|
+
import 'path';
|
|
12
|
+
import 'os';
|
|
13
|
+
import 'crypto';
|
|
14
|
+
import 'node:url';
|
|
15
|
+
|
|
16
|
+
const tag = chalk.cyan('[design-system]');
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* This needs to genrate:
|
|
20
|
+
* variables:
|
|
21
|
+
* color.css
|
|
22
|
+
* font.css
|
|
23
|
+
* radius.css
|
|
24
|
+
* shadow.css
|
|
25
|
+
* typography.css
|
|
26
|
+
* core:
|
|
27
|
+
* body.css
|
|
28
|
+
* headline.css
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
async function generateTailwind(type, figmaStyles) {
|
|
32
|
+
console.log(`${tag} starting tailwind generation...`);
|
|
33
|
+
await generateColors(figmaStyles.paint);
|
|
34
|
+
await generateTypography(figmaStyles.text);
|
|
35
|
+
|
|
36
|
+
// core utilities
|
|
37
|
+
await generateBodies(figmaStyles.text);
|
|
38
|
+
await generateHeadlines(figmaStyles.text);
|
|
39
|
+
await generateButtons(figmaStyles);
|
|
40
|
+
console.log(`${tag} finished tailwind generation`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ------- VARIABLES -------
|
|
44
|
+
async function generateColors(data) {
|
|
45
|
+
console.log(`${tag} generating colors...`);
|
|
46
|
+
const outDir = path.join(paths.styles, 'config');
|
|
47
|
+
await fs.mkdir(outDir, {
|
|
48
|
+
recursive: true
|
|
49
|
+
});
|
|
50
|
+
const lines = [];
|
|
51
|
+
for (const [key, entry] of Object.entries(data ?? {})) {
|
|
52
|
+
const paints = entry?.paints ?? [];
|
|
53
|
+
const p = paints[0] ?? {};
|
|
54
|
+
// exporter already normalizes; prefer .hex/.value, else stringify fallback
|
|
55
|
+
const value = (p.hex && String(p.hex)) ?? (p.value && String(p.value)) ?? (p.css && String(p.css)) ?? String(p);
|
|
56
|
+
if (!value) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
lines.push(` --color-${key}: ${value};`);
|
|
60
|
+
}
|
|
61
|
+
const css = `/* Auto-generated. Do not edit by hand. */
|
|
62
|
+
@theme {
|
|
63
|
+
${lines.join('\n')}
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
66
|
+
const filePath = path.join(outDir, 'color.css');
|
|
67
|
+
await fs.writeFile(filePath, css, 'utf8');
|
|
68
|
+
console.log(`${tag} finished generating colors`);
|
|
69
|
+
}
|
|
70
|
+
async function generateButtons(figma) {
|
|
71
|
+
console.log(`${tag} generating buttons...`);
|
|
72
|
+
const outDir = path.join(paths.styles, "core");
|
|
73
|
+
const outFile = path.join(outDir, "button-variants.css");
|
|
74
|
+
|
|
75
|
+
// If file already exists, skip generation
|
|
76
|
+
try {
|
|
77
|
+
await fs.access(outFile, constants.F_OK);
|
|
78
|
+
console.log(`${tag} button-variants.css already exists, skipping`);
|
|
79
|
+
return;
|
|
80
|
+
} catch {
|
|
81
|
+
// access threw -> file does not exist, continue
|
|
82
|
+
}
|
|
83
|
+
const getButtons = await parseButtons(figma);
|
|
84
|
+
const buttons = getButtons.map(b => `@utility ${b} {
|
|
85
|
+
@apply button;
|
|
86
|
+
|
|
87
|
+
}`).join("\n\n");
|
|
88
|
+
const out = `/* Auto-generated only once - Feel free to edit by hand. */
|
|
89
|
+
${buttons}
|
|
90
|
+
`;
|
|
91
|
+
await fs.writeFile(outFile, out, "utf8");
|
|
92
|
+
console.log(`${tag} finished generating buttons`);
|
|
93
|
+
}
|
|
94
|
+
async function generateTypography(data) {
|
|
95
|
+
console.log(`${tag} generating typography...`);
|
|
96
|
+
const outDir = path.join(paths.styles, 'config');
|
|
97
|
+
await fs.mkdir(outDir, {
|
|
98
|
+
recursive: true
|
|
99
|
+
});
|
|
100
|
+
const lines = [];
|
|
101
|
+
for (const [key, entry] of Object.entries(data ?? {})) {
|
|
102
|
+
if (!entry || typeof entry !== 'object') {
|
|
103
|
+
console.log(`${tag} [typography] skip "${key}" – entry is not an object`, entry);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const fontSize = entry.fontSize;
|
|
107
|
+
const lh = entry.lineHeight ?? {};
|
|
108
|
+
const lhVal = lh && typeof lh === 'object' ? lh.value : undefined;
|
|
109
|
+
const lhUnit = lh && typeof lh === 'object' ? lh.unit ?? '' : '';
|
|
110
|
+
const hasFontSize = typeof fontSize === 'number' && !Number.isNaN(fontSize);
|
|
111
|
+
const hasLineHeight = typeof lhVal === 'number' && !Number.isNaN(lhVal) && lhUnit !== '';
|
|
112
|
+
if (!hasFontSize && !hasLineHeight) {
|
|
113
|
+
// This will tell you exactly which keys are coming in “empty”
|
|
114
|
+
console.log(`${tag} [typography] "${key}" has no usable fontSize/lineHeight`, {
|
|
115
|
+
fontSize,
|
|
116
|
+
lineHeight: lh
|
|
117
|
+
});
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (hasFontSize) {
|
|
121
|
+
lines.push(` --text-${key}: ${fontSize}px;`);
|
|
122
|
+
}
|
|
123
|
+
if (hasLineHeight) {
|
|
124
|
+
lines.push(` --leading-${key}: ${lhVal}${lhUnit};`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const css = `/* Auto-generated - Do not edit by hand. */
|
|
128
|
+
@theme {
|
|
129
|
+
${lines.join('\n')}
|
|
130
|
+
}
|
|
131
|
+
`;
|
|
132
|
+
const filePath = path.join(outDir, 'typography.css');
|
|
133
|
+
await fs.writeFile(filePath, css, 'utf8');
|
|
134
|
+
console.log(`${tag} finished generating typography`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ------- CORE (fixed grouping incl. semibold) -------
|
|
138
|
+
|
|
139
|
+
async function generateBodies(textMap = {}) {
|
|
140
|
+
console.log(`${tag} generating bodies...`);
|
|
141
|
+
const outDir = path.join(paths.styles, 'core');
|
|
142
|
+
await fs.mkdir(outDir, {
|
|
143
|
+
recursive: true
|
|
144
|
+
});
|
|
145
|
+
const keys = Object.keys(textMap || {}).filter(k => k.startsWith('body-'));
|
|
146
|
+
|
|
147
|
+
// Normalize base: remove a single "mobile" segment after "body-"
|
|
148
|
+
function baseKey(k) {
|
|
149
|
+
const parts = k.split('-');
|
|
150
|
+
const idx = parts.indexOf('mobile');
|
|
151
|
+
if (idx !== -1) parts.splice(idx, 1);
|
|
152
|
+
return parts.join('-');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** @type {Map<string,{desktop:string|null,mobile:string|null}>} */
|
|
156
|
+
const groups = new Map();
|
|
157
|
+
for (const k of keys) {
|
|
158
|
+
const base = baseKey(k);
|
|
159
|
+
const g = groups.get(base) ?? {
|
|
160
|
+
desktop: null,
|
|
161
|
+
mobile: null
|
|
162
|
+
};
|
|
163
|
+
if (k.includes('-mobile-')) g.mobile = k;else g.desktop = k;
|
|
164
|
+
groups.set(base, g);
|
|
165
|
+
}
|
|
166
|
+
const blocks = [];
|
|
167
|
+
for (const [base, {
|
|
168
|
+
desktop,
|
|
169
|
+
mobile
|
|
170
|
+
}] of [...groups.entries()].sort()) {
|
|
171
|
+
const hasMobile = !!mobile;
|
|
172
|
+
const hasDesktop = !!desktop;
|
|
173
|
+
const mobileRef = hasMobile ? mobile : desktop;
|
|
174
|
+
const desktopUpgrade = hasMobile && hasDesktop ? `\n @apply desktop-xs:text-${desktop} desktop-xs:leading-${desktop};` : ``;
|
|
175
|
+
|
|
176
|
+
// collapse “body-body-x” → “body-x”
|
|
177
|
+
const isDouble = base.startsWith('body-body-');
|
|
178
|
+
const utilName = isDouble ? base.replace(/^body-body-/, 'body-') : base;
|
|
179
|
+
|
|
180
|
+
// main utility
|
|
181
|
+
blocks.push(`@utility ${utilName} {
|
|
182
|
+
@apply text-${mobileRef} leading-${mobileRef};${desktopUpgrade}
|
|
183
|
+
}`);
|
|
184
|
+
|
|
185
|
+
// only add numeric alias if no collapse happened
|
|
186
|
+
if (!isDouble) {
|
|
187
|
+
const m = /^body-body-(\d+)$/i.exec(base);
|
|
188
|
+
if (m) {
|
|
189
|
+
const n = m[1];
|
|
190
|
+
blocks.push(`@utility body-${n} {
|
|
191
|
+
@apply text-${mobileRef} leading-${mobileRef};${desktopUpgrade}
|
|
192
|
+
}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const out = `/* Auto-generated - Do not edit by hand. */
|
|
197
|
+
${blocks.join('\n\n')}
|
|
198
|
+
`;
|
|
199
|
+
await fs.writeFile(path.join(outDir, 'body.css'), out, 'utf8');
|
|
200
|
+
console.log(`${tag} finished generating bodies`);
|
|
201
|
+
}
|
|
202
|
+
async function generateHeadlines(textMap = {}) {
|
|
203
|
+
console.log(`${tag} generating headlines...`);
|
|
204
|
+
const outDir = path.join(paths.styles, 'core');
|
|
205
|
+
await fs.mkdir(outDir, {
|
|
206
|
+
recursive: true
|
|
207
|
+
});
|
|
208
|
+
const keys = Object.keys(textMap || {}).filter(k => k.startsWith('headline-'));
|
|
209
|
+
|
|
210
|
+
// collect ids present in either mobile or desktop
|
|
211
|
+
const ids = new Set();
|
|
212
|
+
for (const k of keys) {
|
|
213
|
+
let m = /^headline-h(\d+)$/i.exec(k);
|
|
214
|
+
if (m) {
|
|
215
|
+
ids.add(+m[1]);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
m = /^headline-mobile-h(\d+)$/i.exec(k);
|
|
219
|
+
if (m) {
|
|
220
|
+
ids.add(+m[1]);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const blocks = [];
|
|
225
|
+
for (const n of [...ids].sort((a, b) => a - b)) {
|
|
226
|
+
const mobileKey = keys.includes(`headline-mobile-h${n}`) ? `headline-mobile-h${n}` : `headline-h${n}`;
|
|
227
|
+
const hasDesktop = keys.includes(`headline-h${n}`);
|
|
228
|
+
const desktopKey = hasDesktop ? `headline-h${n}` : null;
|
|
229
|
+
const desktopUpgrade = hasDesktop ? `\n @apply desktop-xs:text-${desktopKey} desktop-xs:leading-${desktopKey};` : ``;
|
|
230
|
+
blocks.push(`@utility headline-h${n} {
|
|
231
|
+
@apply text-${mobileKey} leading-${mobileKey};
|
|
232
|
+
@apply font-default;${desktopUpgrade}
|
|
233
|
+
}`);
|
|
234
|
+
}
|
|
235
|
+
const out = `/* Auto-generated. Do not edit by hand. */
|
|
236
|
+
${blocks.join('\n\n')}
|
|
237
|
+
`;
|
|
238
|
+
await fs.writeFile(path.join(outDir, 'headline.css'), out, 'utf8');
|
|
239
|
+
console.log(`${tag} finished generating headlines`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export { generateTailwind as default, generateTailwind };
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { p as paths } from './index.esm.js';
|
|
7
|
+
import { p as parseButtons } from './buttons-o2a9tpOe.js';
|
|
8
|
+
import 'node:process';
|
|
9
|
+
import 'boxen';
|
|
10
|
+
import 'fs';
|
|
11
|
+
import 'path';
|
|
12
|
+
import 'os';
|
|
13
|
+
import 'crypto';
|
|
14
|
+
import 'node:url';
|
|
15
|
+
|
|
16
|
+
const tag = chalk.cyan('[design-system]');
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* This needs to genrate:
|
|
20
|
+
* variables:
|
|
21
|
+
* color.css
|
|
22
|
+
* font.css
|
|
23
|
+
* radius.css
|
|
24
|
+
* shadow.css
|
|
25
|
+
* typography.css
|
|
26
|
+
* core:
|
|
27
|
+
* body.css
|
|
28
|
+
* headline.css
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
async function generateTailwind(type, figmaStyles) {
|
|
32
|
+
console.log(`${tag} starting tailwind generation...`);
|
|
33
|
+
await generateColors(figmaStyles.paint);
|
|
34
|
+
await generateTypography(figmaStyles.text);
|
|
35
|
+
|
|
36
|
+
// core utilities
|
|
37
|
+
await generateBodies(figmaStyles.text);
|
|
38
|
+
await generateHeadlines(figmaStyles.text);
|
|
39
|
+
await generateButtons(figmaStyles);
|
|
40
|
+
console.log(`${tag} finished tailwind generation`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ------- VARIABLES -------
|
|
44
|
+
async function generateColors(data) {
|
|
45
|
+
console.log(`${tag} generating colors...`);
|
|
46
|
+
const outDir = path.join(paths.styles, 'config');
|
|
47
|
+
await fs.mkdir(outDir, {
|
|
48
|
+
recursive: true
|
|
49
|
+
});
|
|
50
|
+
const lines = [];
|
|
51
|
+
for (const [key, entry] of Object.entries(data ?? {})) {
|
|
52
|
+
const paints = entry?.paints ?? [];
|
|
53
|
+
const p = paints[0] ?? {};
|
|
54
|
+
// exporter already normalizes; prefer .hex/.value, else stringify fallback
|
|
55
|
+
const value = (p.hex && String(p.hex)) ?? (p.value && String(p.value)) ?? (p.css && String(p.css)) ?? String(p);
|
|
56
|
+
if (!value) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
lines.push(` --color-${key}: ${value};`);
|
|
60
|
+
}
|
|
61
|
+
const css = `/* Auto-generated. Do not edit by hand. */
|
|
62
|
+
@theme {
|
|
63
|
+
${lines.join('\n')}
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
66
|
+
const filePath = path.join(outDir, 'color.css');
|
|
67
|
+
await fs.writeFile(filePath, css, 'utf8');
|
|
68
|
+
console.log(`${tag} finished generating colors`);
|
|
69
|
+
}
|
|
70
|
+
async function generateButtons(figma) {
|
|
71
|
+
console.log(`${tag} generating buttons...`);
|
|
72
|
+
const outDir = path.join(paths.styles, "core");
|
|
73
|
+
const outFile = path.join(outDir, "button-variants.css");
|
|
74
|
+
|
|
75
|
+
// If file already exists, skip generation
|
|
76
|
+
try {
|
|
77
|
+
await fs.access(outFile, constants.F_OK);
|
|
78
|
+
console.log(`${tag} button-variants.css already exists, skipping`);
|
|
79
|
+
return;
|
|
80
|
+
} catch {
|
|
81
|
+
// access threw -> file does not exist, continue
|
|
82
|
+
}
|
|
83
|
+
const getButtons = await parseButtons(figma);
|
|
84
|
+
const buttons = getButtons.map(b => `@utility ${b} {
|
|
85
|
+
@apply button;
|
|
86
|
+
|
|
87
|
+
}`).join("\n\n");
|
|
88
|
+
const out = `/* Auto-generated. Do not edit by hand. */
|
|
89
|
+
${buttons}
|
|
90
|
+
`;
|
|
91
|
+
await fs.writeFile(outFile, out, "utf8");
|
|
92
|
+
console.log(`${tag} finished generating buttons`);
|
|
93
|
+
}
|
|
94
|
+
async function generateTypography(data) {
|
|
95
|
+
console.log(`${tag} generating typography...`);
|
|
96
|
+
const outDir = path.join(paths.styles, 'config');
|
|
97
|
+
await fs.mkdir(outDir, {
|
|
98
|
+
recursive: true
|
|
99
|
+
});
|
|
100
|
+
const lines = [];
|
|
101
|
+
for (const [key, entry] of Object.entries(data ?? {})) {
|
|
102
|
+
if (!entry || typeof entry !== 'object') {
|
|
103
|
+
console.log(`${tag} [typography] skip "${key}" – entry is not an object`, entry);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const fontSize = entry.fontSize;
|
|
107
|
+
const lh = entry.lineHeight ?? {};
|
|
108
|
+
const lhVal = lh && typeof lh === 'object' ? lh.value : undefined;
|
|
109
|
+
const lhUnit = lh && typeof lh === 'object' ? lh.unit ?? '' : '';
|
|
110
|
+
const hasFontSize = typeof fontSize === 'number' && !Number.isNaN(fontSize);
|
|
111
|
+
const hasLineHeight = typeof lhVal === 'number' && !Number.isNaN(lhVal) && lhUnit !== '';
|
|
112
|
+
if (!hasFontSize && !hasLineHeight) {
|
|
113
|
+
// This will tell you exactly which keys are coming in “empty”
|
|
114
|
+
console.log(`${tag} [typography] "${key}" has no usable fontSize/lineHeight`, {
|
|
115
|
+
fontSize,
|
|
116
|
+
lineHeight: lh
|
|
117
|
+
});
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (hasFontSize) {
|
|
121
|
+
lines.push(` --text-${key}: ${fontSize}px;`);
|
|
122
|
+
}
|
|
123
|
+
if (hasLineHeight) {
|
|
124
|
+
lines.push(` --leading-${key}: ${lhVal}${lhUnit};`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const css = `/* Auto-generated. Do not edit by hand. */
|
|
128
|
+
@theme {
|
|
129
|
+
${lines.join('\n')}
|
|
130
|
+
}
|
|
131
|
+
`;
|
|
132
|
+
const filePath = path.join(outDir, 'typography.css');
|
|
133
|
+
await fs.writeFile(filePath, css, 'utf8');
|
|
134
|
+
console.log(`${tag} finished generating typography`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ------- CORE (fixed grouping incl. semibold) -------
|
|
138
|
+
|
|
139
|
+
async function generateBodies(textMap = {}) {
|
|
140
|
+
console.log(`${tag} generating bodies...`);
|
|
141
|
+
const outDir = path.join(paths.styles, 'core');
|
|
142
|
+
await fs.mkdir(outDir, {
|
|
143
|
+
recursive: true
|
|
144
|
+
});
|
|
145
|
+
const keys = Object.keys(textMap || {}).filter(k => k.startsWith('body-'));
|
|
146
|
+
|
|
147
|
+
// Normalize base: remove a single "mobile" segment after "body-"
|
|
148
|
+
function baseKey(k) {
|
|
149
|
+
const parts = k.split('-');
|
|
150
|
+
const idx = parts.indexOf('mobile');
|
|
151
|
+
if (idx !== -1) parts.splice(idx, 1);
|
|
152
|
+
return parts.join('-');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** @type {Map<string,{desktop:string|null,mobile:string|null}>} */
|
|
156
|
+
const groups = new Map();
|
|
157
|
+
for (const k of keys) {
|
|
158
|
+
const base = baseKey(k);
|
|
159
|
+
const g = groups.get(base) ?? {
|
|
160
|
+
desktop: null,
|
|
161
|
+
mobile: null
|
|
162
|
+
};
|
|
163
|
+
if (k.includes('-mobile-')) g.mobile = k;else g.desktop = k;
|
|
164
|
+
groups.set(base, g);
|
|
165
|
+
}
|
|
166
|
+
const blocks = [];
|
|
167
|
+
for (const [base, {
|
|
168
|
+
desktop,
|
|
169
|
+
mobile
|
|
170
|
+
}] of [...groups.entries()].sort()) {
|
|
171
|
+
const hasMobile = !!mobile;
|
|
172
|
+
const hasDesktop = !!desktop;
|
|
173
|
+
const mobileRef = hasMobile ? mobile : desktop;
|
|
174
|
+
const desktopUpgrade = hasMobile && hasDesktop ? `\n @apply desktop-xs:text-${desktop} desktop-xs:leading-${desktop};` : ``;
|
|
175
|
+
|
|
176
|
+
// collapse “body-body-x” → “body-x”
|
|
177
|
+
const isDouble = base.startsWith('body-body-');
|
|
178
|
+
const utilName = isDouble ? base.replace(/^body-body-/, 'body-') : base;
|
|
179
|
+
|
|
180
|
+
// main utility
|
|
181
|
+
blocks.push(`@utility ${utilName} {
|
|
182
|
+
@apply text-${mobileRef} leading-${mobileRef};${desktopUpgrade}
|
|
183
|
+
}`);
|
|
184
|
+
|
|
185
|
+
// only add numeric alias if no collapse happened
|
|
186
|
+
if (!isDouble) {
|
|
187
|
+
const m = /^body-body-(\d+)$/i.exec(base);
|
|
188
|
+
if (m) {
|
|
189
|
+
const n = m[1];
|
|
190
|
+
blocks.push(`@utility body-${n} {
|
|
191
|
+
@apply text-${mobileRef} leading-${mobileRef};${desktopUpgrade}
|
|
192
|
+
}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const out = `/* Auto-generated. Do not edit by hand. */
|
|
197
|
+
${blocks.join('\n\n')}
|
|
198
|
+
`;
|
|
199
|
+
await fs.writeFile(path.join(outDir, 'body.css'), out, 'utf8');
|
|
200
|
+
console.log(`${tag} finished generating bodies`);
|
|
201
|
+
}
|
|
202
|
+
async function generateHeadlines(textMap = {}) {
|
|
203
|
+
console.log(`${tag} generating headlines...`);
|
|
204
|
+
const outDir = path.join(paths.styles, 'core');
|
|
205
|
+
await fs.mkdir(outDir, {
|
|
206
|
+
recursive: true
|
|
207
|
+
});
|
|
208
|
+
const keys = Object.keys(textMap || {}).filter(k => k.startsWith('headline-'));
|
|
209
|
+
|
|
210
|
+
// collect ids present in either mobile or desktop
|
|
211
|
+
const ids = new Set();
|
|
212
|
+
for (const k of keys) {
|
|
213
|
+
let m = /^headline-h(\d+)$/i.exec(k);
|
|
214
|
+
if (m) {
|
|
215
|
+
ids.add(+m[1]);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
m = /^headline-mobile-h(\d+)$/i.exec(k);
|
|
219
|
+
if (m) {
|
|
220
|
+
ids.add(+m[1]);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const blocks = [];
|
|
225
|
+
for (const n of [...ids].sort((a, b) => a - b)) {
|
|
226
|
+
const mobileKey = keys.includes(`headline-mobile-h${n}`) ? `headline-mobile-h${n}` : `headline-h${n}`;
|
|
227
|
+
const hasDesktop = keys.includes(`headline-h${n}`);
|
|
228
|
+
const desktopKey = hasDesktop ? `headline-h${n}` : null;
|
|
229
|
+
const desktopUpgrade = hasDesktop ? `\n @apply desktop-xs:text-${desktopKey} desktop-xs:leading-${desktopKey};` : ``;
|
|
230
|
+
blocks.push(`@utility headline-h${n} {
|
|
231
|
+
@apply text-${mobileKey} leading-${mobileKey};
|
|
232
|
+
@apply font-default;${desktopUpgrade}
|
|
233
|
+
}`);
|
|
234
|
+
}
|
|
235
|
+
const out = `/* Auto-generated. Do not edit by hand. */
|
|
236
|
+
${blocks.join('\n\n')}
|
|
237
|
+
`;
|
|
238
|
+
await fs.writeFile(path.join(outDir, 'headline.css'), out, 'utf8');
|
|
239
|
+
console.log(`${tag} finished generating headlines`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export { generateTailwind as default, generateTailwind };
|
package/dist/index.esm.js
CHANGED
|
@@ -1131,7 +1131,7 @@ async function readFigma() {
|
|
|
1131
1131
|
try {
|
|
1132
1132
|
const {
|
|
1133
1133
|
default: generateTailwind
|
|
1134
|
-
} = await import('./generate-tailwind-
|
|
1134
|
+
} = await import('./generate-tailwind-DGiO1-km.js');
|
|
1135
1135
|
const figma = await readFigma();
|
|
1136
1136
|
switch (cmd) {
|
|
1137
1137
|
case 'astro':
|
|
@@ -1157,7 +1157,7 @@ async function readFigma() {
|
|
|
1157
1157
|
}
|
|
1158
1158
|
const {
|
|
1159
1159
|
default: generateRazor
|
|
1160
|
-
} = await import('./generate-razor-
|
|
1160
|
+
} = await import('./generate-razor-U3IALi0o.js');
|
|
1161
1161
|
await generateTailwind('razor', figma);
|
|
1162
1162
|
await generateRazor(ns, figma);
|
|
1163
1163
|
break;
|