@vettvangur/design-system 0.0.21 → 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,501 @@
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 chalk from 'chalk';
6
+ import { readFigma } from './generate-tailwind-Dri2RfD_.js';
7
+ import 'node:url';
8
+ import 'node:process';
9
+ import 'node:fs/promises';
10
+ import 'fs';
11
+ import 'path';
12
+ import 'os';
13
+ import 'crypto';
14
+
15
+ /**
16
+ * Return a list of button names from an exporter payload.
17
+ * @param {{components?: Record<string, any>}} payload
18
+ * @param {{stripPrefix?: boolean, sort?: boolean}} opts
19
+ * @returns {string[]}
20
+ */
21
+ function parseButtons(payload, {
22
+ stripPrefix = false,
23
+ sort = true
24
+ } = {}) {
25
+ const src = payload?.components || {};
26
+ let names = Object.keys(src).filter(k => k.startsWith('button-'));
27
+ if (stripPrefix) names = names.map(k => k.slice('button-'.length));
28
+ if (sort) names.sort();
29
+ console.log('BUTTONS', names);
30
+ return names;
31
+ }
32
+
33
+ // core/razor/generate-razor.mjs
34
+ const CWD = process.cwd();
35
+ const OUT_DIR = path.resolve(CWD, '../Vettvangur.Site/Views/Partials/DesignSystem');
36
+ const FILE_COLORS = path.join(OUT_DIR, 'Colors.cshtml');
37
+ const FILE_TYPO = path.join(OUT_DIR, 'Typography.cshtml');
38
+ const FILE_BUTTONS = path.join(OUT_DIR, 'Buttons.cshtml');
39
+ const FILE_INPUTS = path.join(OUT_DIR, 'Inputs.cshtml');
40
+ const FILE_RICHTEXT = path.join(OUT_DIR, 'Richtext.cshtml');
41
+ const FILE_Table = path.join(OUT_DIR, 'Tables.cshtml');
42
+ const tag = chalk.cyan('[design-system]');
43
+ async function generateRazor() {
44
+ console.log(`${tag} generating razor files...`);
45
+ await promises.mkdir(OUT_DIR, {
46
+ recursive: true
47
+ });
48
+ const figma = await readFigma();
49
+ const [colors, typography, buttons] = await Promise.all([parseColors(), parseTypography(), parseButtons(figma)]);
50
+
51
+ // Colors
52
+ if (colors?.length) {
53
+ await promises.writeFile(FILE_COLORS, renderColors(colors), 'utf8');
54
+ log(FILE_COLORS, colors.length);
55
+ }
56
+
57
+ // Typography (one file)
58
+ if ((typography?.headlines?.length ?? 0) + (typography?.bodies?.length ?? 0) > 0) {
59
+ await promises.writeFile(FILE_TYPO, renderTypography(typography), 'utf8');
60
+ log(FILE_TYPO, (typography.headlines?.length ?? 0) + (typography.bodies?.length ?? 0));
61
+ }
62
+
63
+ // Buttons
64
+ if (buttons?.length) {
65
+ await promises.writeFile(FILE_BUTTONS, renderButtons(buttons), 'utf8');
66
+ log(FILE_BUTTONS, buttons.length);
67
+ }
68
+
69
+ // Inputs
70
+ const hasAnyInputs = Object.values(inputsSpec).some(v => Array.isArray(v) && v.length);
71
+ if (hasAnyInputs) {
72
+ await promises.writeFile(FILE_INPUTS, renderInputs(inputsSpec), 'utf8');
73
+ log(FILE_INPUTS, countInputs(inputsSpec));
74
+ }
75
+
76
+ // Richtext & Tables (static demos)
77
+ await promises.writeFile(FILE_RICHTEXT, renderRichtext(), 'utf8');
78
+ log(FILE_RICHTEXT, 1);
79
+ await promises.writeFile(FILE_Table, renderRichtextTables(), 'utf8');
80
+ log(FILE_Table, 1);
81
+ console.log(`${tag} great success!`);
82
+ }
83
+
84
+ /* -------------------- RENDERERS -------------------- */
85
+
86
+ function renderColors(colors) {
87
+ const items = colors.map(({
88
+ title,
89
+ color
90
+ }) => `
91
+ <div class="styleguide-color">
92
+ <span class="styleguide-color__label label">${h(title)}</span>
93
+ <div class="styleguide-color__swatch" style="background: ${a(color)};"></div>
94
+ <div class="styleguide-color__info">
95
+ <span class="styleguide-color__info-hex label">${h(color)}</span>
96
+ </div>
97
+ </div>`).join('\n');
98
+ return `@* Auto-generated — do not edit by hand *@
99
+ <section class="styleguide-colors styleguide-spacing">
100
+ <div class="container">
101
+ <vv-headline
102
+ identifier="styleguide__headline",
103
+ modifier="headline-h1",
104
+ text="Colors"
105
+ />
106
+
107
+ <div class="styleguide-colors-grid grid grid--gap">
108
+ ${items}
109
+ </div>
110
+ </div>
111
+ </section>
112
+ `;
113
+ }
114
+ function renderTypography({
115
+ headlines = [],
116
+ bodies = []
117
+ }) {
118
+ const headlineItems = headlines.map(hItem => {
119
+ const mod = classToModifier(hItem.class, 'headline');
120
+ const info = infoBlocks(hItem);
121
+ return `
122
+ <div class="styleguide-typography__item grid grid--gap">
123
+ <span class="styleguide-typography__item-name">${h(hItem.title)}</span>
124
+ <vv-headline
125
+ identifier="styleguide__headline"
126
+ modifier="${a(mod)}"
127
+ text="The quick brown fox jumps over the lazy dog."
128
+ />
129
+ ${info ? renderInfo(info) : ''}
130
+ </div>`;
131
+ }).join('\n');
132
+ const bodyItems = bodies.map(b => {
133
+ const info = infoBlocks(b);
134
+ return `
135
+ <div class="styleguide-typography__item grid grid--gap">
136
+ <span class="styleguide-typography__item-name">${h(b.title)}</span>
137
+ <p class="${a(b.class)} styleguide-typography__item-example">
138
+ The quick brown fox jumps over the lazy dog.
139
+ </p>
140
+ ${info ? renderInfo(info) : ''}
141
+ </div>`;
142
+ }).join('\n');
143
+ return `@* Auto-generated — do not edit by hand *@
144
+ <section class="styleguide-typography styleguide-spacing">
145
+ <div class="container">
146
+ <vv-headline
147
+ identifier="styleguide__headline"
148
+ modifier="headline-h1"
149
+ text="Typography"
150
+ />
151
+
152
+ ${headlines.length ? `
153
+ <div class="styleguide-typography__group grid grid--gap">
154
+ <vv-headline
155
+ identifier="styleguide__headline"
156
+ modifier="headline-3"
157
+ text="Headlines"
158
+ />
159
+ <div class="styleguide-typography__list grid grid--gap">
160
+ ${headlineItems}
161
+ </div>
162
+ </div>` : ''}
163
+
164
+ ${bodies.length ? `
165
+ <div class="styleguide-typography__group grid grid--gap">
166
+ <vv-headline
167
+ identifier="styleguide__headline"
168
+ modifier="headline-3"
169
+ text="Body text"
170
+ />
171
+ <div class="styleguide-typography__list grid grid--gap">
172
+ ${bodyItems}
173
+ </div>
174
+ </div>` : ''}
175
+ </div>
176
+ </section>
177
+ `;
178
+ }
179
+ function renderInfo(info) {
180
+ const {
181
+ base,
182
+ desktop,
183
+ mobile
184
+ } = info;
185
+ return `
186
+ <div class="styleguide-typography__info">
187
+ ${base ? `
188
+ <div class="styleguide-typography__info-item">
189
+ <span>Base</span>
190
+ <span><strong>${h(base)}</strong></span>
191
+ </div>` : ''}
192
+ ${desktop ? `
193
+ <div class="styleguide-typography__info-item">
194
+ <span>Desktop</span>
195
+ <span><strong>${h(desktop)}</strong></span>
196
+ </div>` : ''}
197
+ ${mobile ? `
198
+ <div class="styleguide-typography__info-item">
199
+ <span>Mobile</span>
200
+ <span><strong>${h(mobile)}</strong></span>
201
+ </div>` : ''}
202
+ </div>`;
203
+ }
204
+ function renderButtons(buttons) {
205
+ const items = buttons.map(b => {
206
+ const label = pickButtonText(b);
207
+ return `
208
+ <div class="styleguide__button-wrap">
209
+ <vv-button
210
+ identifier="styleguide__button"
211
+ modifier="${a(b)}"
212
+ type="@ButtonType.Button"
213
+ text="${a(label)}"
214
+ />
215
+ </div>`;
216
+ }).join('\n');
217
+ return `@* Auto-generated — do not edit by hand *@
218
+ <section class="styleguide-buttons styleguide-spacing">
219
+ <div class="container">
220
+ <vv-headline
221
+ identifier="styleguide__headline",
222
+ modifier="headline-h1",
223
+ text="Buttons"
224
+ />
225
+
226
+ <div class="grid grid--gap">
227
+ ${items}
228
+ </div>
229
+ </div>
230
+ </section>
231
+ `;
232
+ }
233
+ function pickButtonText(modifier) {
234
+ const m = (modifier || '').toLowerCase();
235
+
236
+ // intent-based overrides
237
+ if (/\b(danger|destructive|error|delete|remove)\b/.test(m)) return 'Delete';
238
+ if (/\b(success|confirm|ok|apply|save)\b/.test(m)) return 'Confirm';
239
+ if (/\b(warn|warning|alert)\b/.test(m)) return 'Proceed with Caution';
240
+ if (/\b(primary|cta|proceed|next)\b/.test(m)) return 'Continue';
241
+ if (/\b(secondary|ghost|tertiary|neutral)\b/.test(m)) return 'Learn More';
242
+ if (/\b(buy|checkout|pay|purchase|cart)\b/.test(m)) return 'Buy Now';
243
+ if (/\b(download|install|get)\b/.test(m)) return 'Download';
244
+ if (/\b(subscribe|signup|register)\b/.test(m)) return 'Subscribe';
245
+ if (/\b(login|signin)\b/.test(m)) return 'Sign In';
246
+ if (/\b(contact|email|message)\b/.test(m)) return 'Contact Us';
247
+
248
+ // size-only or shape-only variants keep a neutral verb
249
+ 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']);
250
+
251
+ // icon-only hints (still provide text for a11y)
252
+ if (/\b(icon|only|icon-only)\b/.test(m)) return 'Open';
253
+
254
+ // default: deterministic pick
255
+ return seededPick(m, DEFAULT_WORDS);
256
+ }
257
+ 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'];
258
+
259
+ // Deterministic selection from a list based on the modifier string
260
+ function seededPick(seed, list) {
261
+ let h = 0;
262
+ for (let i = 0; i < seed.length; i++) h = h * 131 + seed.charCodeAt(i) >>> 0;
263
+ return list[h % list.length];
264
+ }
265
+ function renderInputs(spec) {
266
+ const {
267
+ inputs = [],
268
+ selects = [],
269
+ checkboxes = [],
270
+ radios = [],
271
+ textareas = []
272
+ } = spec;
273
+ const inputsPartials = inputs.map(i => `
274
+ <vv-input
275
+ id="${a(i.id || '')}"
276
+ label="${a(i.Label ?? i.label ?? 'Input')}"
277
+ placeholder="${a(i.placeholder ?? 'Input placeholder')}"
278
+ is-disabled="${bool(i.isDisabled)}"
279
+ is-readonly="${bool(i.isReadonly)}"
280
+ is-error="${bool(i.isError)}"
281
+ />
282
+ `).join('\n');
283
+ const selectsPartials = selects.map(s => `
284
+ <vv-select
285
+ id="${a(s.id || '')}"
286
+ label="${a(s.Label ?? 'Select')}"
287
+ options="@selectOptions"
288
+ is-disabled="${bool(s.isDisabled)}"
289
+ is-readOnly="${bool(s.isReadonly)}"
290
+ is-error="${bool(s.isError)}"
291
+ />
292
+ `).join('\n');
293
+ const checkboxRows = checkboxes.map(c => `
294
+ <div class="input-group">
295
+ <vv-selection
296
+ id="${a(c.id || '')}"
297
+ label="${a(c.Label ?? c.label ?? 'Checkbox')}"
298
+ type="@HTMLInputType.Checkbox"
299
+ is-disabled="${bool(c.isDisabled)}"
300
+ is-error="${bool(c.isError)}"
301
+ />
302
+ </div>
303
+ `).join('\n');
304
+ const radioRows = radios.map(r => `
305
+ <div class="input-group">
306
+ <vv-selection
307
+ id="${a(r.id || '')}"
308
+ type="@HTMLInputType.Radio"
309
+ name="${a(r.Name ?? r.name ?? 'prettyradio')}"
310
+ label="${a(r.Label ?? r.label ?? 'Radio')}"
311
+ is-disabled="${bool(r.isDisabled)}"
312
+ is-error="${bool(r.isError)}"
313
+ />
314
+ </div>`).join('\n');
315
+ const textareasPartials = textareas.map(t => `
316
+ <vv-textarea
317
+ id="${a(t.id || '')}"
318
+ label="${a(t.Label ?? 'Textarea')}"
319
+ placeholder="${a(t.placeholder ?? 'Textarea placeholder')}"
320
+ is-disabled="${bool(t.isDisabled)}"
321
+ is-readonly="${bool(t.isReadonly)}"
322
+ is-error="${bool(t.isError)}"
323
+ />
324
+ `).join('\n');
325
+ return `@* Auto-generated — do not edit by hand *@
326
+ @{
327
+ var selectOptions = new Dictionary<string, string>() {
328
+ { "option1", "Option value" },
329
+ { "option2", "Option value" },
330
+ { "option3", "Option value" },
331
+ { "option4", "Option value" },
332
+ { "option5", "Option value" },
333
+ { "option6", "Option value" },
334
+ };
335
+ }
336
+
337
+ <section class="styleguide-inputs styleguide-spacing">
338
+ <div class="container">
339
+ <vv-headline
340
+ identifier="styleguide__headline",
341
+ modifier="headline-h1",
342
+ text="Inputs"
343
+ />
344
+
345
+ <div class="styleguide-inputs__scrollitem grid grid--gap grid--align-start">
346
+ ${inputsPartials}
347
+ ${selectsPartials}
348
+ ${checkboxRows}
349
+ ${radioRows}
350
+ ${textareasPartials}
351
+ </div>
352
+ </div>
353
+ </section>
354
+ `;
355
+ }
356
+
357
+ /* ------------ Richtext (static demo) ------------ */
358
+ function renderRichtext() {
359
+ return `@* Auto-generated — do not edit by hand *@
360
+ <section class="w-full">
361
+ <vv-headline
362
+ identifier="styleguide__headline",
363
+ modifier="headline-h1",
364
+ text="Richtext"
365
+ />
366
+
367
+ <div class="richtext">
368
+ <h1>h1</h1>
369
+ <h2>h2</h2>
370
+ <h3>h3</h3>
371
+ <h4>h4</h4>
372
+ <h5>h5</h5>
373
+ <p>
374
+ Ó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.
375
+ </p>
376
+ <ul>
377
+ <li>List item</li>
378
+ <li>List item</li>
379
+ <li>
380
+ List item
381
+ <ul>
382
+ <li>Nested item</li>
383
+ <li>Nested item</li>
384
+ <li>
385
+ Nested item
386
+ <ul>
387
+ <li>Nested even more item</li>
388
+ <li>Nested even more item</li>
389
+ <li>Nested even more item</li>
390
+ </ul>
391
+ </li>
392
+ </ul>
393
+ </li>
394
+ <li>List item</li>
395
+ </ul>
396
+ <ol>
397
+ <li>List item</li>
398
+ <li>
399
+ List item
400
+ <ol>
401
+ <li>Nested item</li>
402
+ <li>Nested item</li>
403
+ <li>Nested item</li>
404
+ </ol>
405
+ </li>
406
+ <li>List item</li>
407
+ <li>List item</li>
408
+ </ol>
409
+ <p>
410
+ Ó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.
411
+ </p>
412
+ </div>
413
+ </section>
414
+ `;
415
+ }
416
+ function renderRichtextTables() {
417
+ return `@* Auto-generated — do not edit by hand *@
418
+ <section class="w-full">
419
+ <vv-headline
420
+ identifier="styleguide__headline",
421
+ modifier="headline-h1",
422
+ text="Tables"
423
+ />
424
+
425
+ <div class="table">
426
+ <table class="table__element">
427
+ <thead>
428
+ <tr>
429
+ <th>Head</th>
430
+ <th>Head</th>
431
+ <th>Head</th>
432
+ <th>Head</th>
433
+ <th>Head</th>
434
+ </tr>
435
+ </thead>
436
+ <tbody>
437
+ <tr>
438
+ <td>1</td><td>2</td><td>3</td><td>4</td><td>5</td>
439
+ </tr>
440
+ <tr>
441
+ <td>1</td><td>2</td><td>3</td><td>4</td><td>5</td>
442
+ </tr>
443
+ <tr>
444
+ <td>1</td><td>2</td><td>3</td><td>4</td><td>5</td>
445
+ </tr>
446
+ </tbody>
447
+ </table>
448
+ </div>
449
+ </section>
450
+ `;
451
+ }
452
+
453
+ /* -------------------- HELPERS -------------------- */
454
+ function infoBlocks(item) {
455
+ const base = pair(item.size, item.lineheight);
456
+ const desktop = item?.config?.desktop ? pair(item.config.desktop.size, item.config.desktop.lineHeight) : '';
457
+ const mobile = item?.config?.mobile ? pair(item.config.mobile.size, item.config.mobile.lineHeight) : '';
458
+ if (!base && !desktop && !mobile) {
459
+ return '';
460
+ }
461
+ return {
462
+ base,
463
+ desktop,
464
+ mobile
465
+ };
466
+ }
467
+ function pair(size, lh) {
468
+ if (!size && !lh) {
469
+ return '';
470
+ }
471
+ return `Size: ${size ?? '-'} / Line height: ${lh ?? '-'}`;
472
+ }
473
+ function classToModifier(cls = '', prefix = 'headline') {
474
+ // Accept:
475
+ // - "headline-h1" (preferred)
476
+ // - "headline1" (legacy)
477
+ // - "headline_1" (legacy)
478
+ // Always return "headline-h1".
479
+ const re = new RegExp(`^${prefix}[-_]?h?(\\d+)$`, 'i');
480
+ const m = re.exec(cls.trim());
481
+ if (m) return `${prefix}-h${m[1]}`;
482
+ return cls; // do not mutilate unknown strings
483
+ }
484
+ function countInputs(spec) {
485
+ return ['inputs', 'selects', 'checkboxes', 'radios', 'textareas'].reduce((sum, k) => sum + (spec[k]?.length || 0), 0);
486
+ }
487
+ function a(s = '') {
488
+ return String(s).replaceAll('"', '&quot;');
489
+ }
490
+ function h(s = '') {
491
+ return String(s).replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
492
+ }
493
+ function bool(v) {
494
+ return v ? 'true' : 'false';
495
+ }
496
+ function log(file, n) {
497
+ const name = path.basename(file);
498
+ console.log(`${tag} ✅ ${name} (${n})`);
499
+ }
500
+
501
+ export { generateRazor as default };