create-dokio 0.1.1

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.
Files changed (3) hide show
  1. package/README.md +137 -0
  2. package/dist/index.js +846 -0
  3. package/package.json +56 -0
package/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # create-dokio
2
+
3
+ CLI scaffold for Dokio templates. Generates the correct file structure, `data.yaml`, HTML boilerplate, and SCSS partials for PDF, General, Email, and Video template types.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ # Interactive — prompts for everything
9
+ npx create-dokio
10
+
11
+ # Pre-fill the template name
12
+ npx create-dokio pool-care
13
+ ```
14
+
15
+ > Using pnpm? `pnpm dlx create-dokio`
16
+
17
+ ---
18
+
19
+ ## Template types
20
+
21
+ ### PDF
22
+ PrinceXML-rendered print documents. Supports multi-page, resizable layouts, orderable exports, and proof downloads.
23
+
24
+ ```
25
+ dokio-pool-care/
26
+ ├── CHANGELOG.md
27
+ ├── data.yaml
28
+ ├── index.html
29
+ ├── assets/
30
+ ├── partials/
31
+ │ ├── page1-cover.html
32
+ │ ├── page2-content.html
33
+ │ └── page3-cta.html
34
+ └── scss/
35
+ ├── style.scss.hbs
36
+ ├── _fonts.scss
37
+ ├── _mixins.scss
38
+ ├── _variables.scss
39
+ └── pages/
40
+ ├── _page1.scss
41
+ ├── _page2.scss
42
+ └── _page3.scss
43
+ ```
44
+
45
+ **Prompts:**
46
+ - Page dimensions (mm)
47
+ - Page count
48
+ - PrinceXML version (11 or 15 — use 15 for flexbox)
49
+ - Resizable template
50
+ - Proof downloads (`draft_proofable`)
51
+ - Orderable export (Print from Snap)
52
+
53
+ ---
54
+
55
+ ### General (image)
56
+ Chrome-rendered image output — JPG and/or PNG. Single or multi-section layouts.
57
+
58
+ **Prompts:**
59
+ - Dimensions (px)
60
+ - Page/section count
61
+ - Export formats (JPG, PNG, or both)
62
+
63
+ ---
64
+
65
+ ### Email
66
+ HTML email scaffold. Includes Outlook conditional comments, `{{{___assembled_css}}}` injection, `{{{subject}}}` title, and Gmail-safe table structure.
67
+
68
+ **Prompts:**
69
+ - Subdomain only — email structure is fixed at 600px
70
+
71
+ ---
72
+
73
+ ### Video *(WIP)*
74
+ 1920×1080 video template scaffold. `data.yaml` includes duration and highlight point fields.
75
+
76
+ ---
77
+
78
+ ## What gets generated
79
+
80
+ | File | Description |
81
+ |---|---|
82
+ | `data.yaml` | Dokio template config — set your template ID and subdomain before upload |
83
+ | `index.html` | Entry HTML with Handlebars partial includes |
84
+ | `partials/*.html` | Per-page/section HTML fragments |
85
+ | `scss/style.scss.hbs` | Main SCSS entry — supports Handlebars syntax |
86
+ | `scss/_fonts.scss` | Empty with commented `@font-face` example |
87
+ | `scss/_variables.scss` | Dimension, colour, and typography variables |
88
+ | `scss/_mixins.scss` | `absolute()`, `flex()`, `truncate()` mixins |
89
+ | `scss/pages/_pageN.scss` | Per-page style partials |
90
+ | `assets/` | Drop fonts, images, and local assets here |
91
+ | `CHANGELOG.md` | Template-level changelog |
92
+
93
+ ---
94
+
95
+ ## After scaffolding
96
+
97
+ ```bash
98
+ cd dokio-<name>
99
+
100
+ # 1. Set your template ID in data.yaml
101
+ # name: TEMPLATEID - dokio-<name> ← replace TEMPLATEID
102
+
103
+ # 2. Change subdomain from 'test' before uploading
104
+ # subdomain: your-hub-subdomain
105
+
106
+ # 3. Add fonts/images to assets/
107
+ # Declare fonts in scss/_fonts.scss
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Development
113
+
114
+ ```bash
115
+ git clone <repo>
116
+ pnpm install
117
+ pnpm dev # watch mode
118
+ pnpm build # compile to dist/
119
+ ```
120
+
121
+ ### Commit format
122
+
123
+ This repo uses [Conventional Commits](https://www.conventionalcommits.org/):
124
+
125
+ ```
126
+ feat: add resizable PDF option
127
+ fix: correct page count in data.yaml
128
+ chore: update dependencies
129
+ ```
130
+
131
+ `CHANGELOG.md` updates automatically on `git push`.
132
+
133
+ ---
134
+
135
+ ## License
136
+
137
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,846 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import kleur3 from "kleur";
5
+
6
+ // src/prompts.ts
7
+ import prompts from "prompts";
8
+ import kleur from "kleur";
9
+
10
+ // src/utils.ts
11
+ function toKebab(str) {
12
+ return str.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
13
+ }
14
+ function getPageName(index, total) {
15
+ if (total === 1) return "page1";
16
+ if (index === 0) return "page1-cover";
17
+ if (index === total - 1) return `page${index + 1}-cta`;
18
+ return `page${index + 1}-content`;
19
+ }
20
+ function buildPages(pageCount) {
21
+ return Array.from({ length: pageCount }, (_, i) => getPageName(i, pageCount));
22
+ }
23
+
24
+ // src/prompts.ts
25
+ var onCancel = () => {
26
+ console.log(kleur.yellow("\n Cancelled.\n"));
27
+ process.exit(0);
28
+ };
29
+ async function runPrompts(nameArg) {
30
+ const base = await prompts(
31
+ [
32
+ {
33
+ type: nameArg ? null : "text",
34
+ name: "name",
35
+ message: 'Template name (kebab-case, no "dokio-" prefix)',
36
+ initial: "my-template",
37
+ validate: (v) => /^[a-z0-9-]+$/.test(v) || "Lowercase letters, numbers, hyphens only"
38
+ },
39
+ {
40
+ type: "select",
41
+ name: "mode",
42
+ message: "Template type",
43
+ choices: [
44
+ { title: "PDF", value: "pdf" },
45
+ { title: "General (image/JPG/PNG)", value: "general" },
46
+ { title: kleur.dim("Video [WIP]"), value: "video" },
47
+ { title: "Email", value: "email" }
48
+ ]
49
+ },
50
+ {
51
+ type: "text",
52
+ name: "subdomain",
53
+ message: "Subdomain",
54
+ initial: "test"
55
+ }
56
+ ],
57
+ { onCancel }
58
+ );
59
+ const name = nameArg ? toKebab(nameArg) : base.name;
60
+ const fullName = `dokio-${name}`;
61
+ const subdomain = base.subdomain;
62
+ if (base.mode === "pdf") {
63
+ const pdf = await prompts(
64
+ [
65
+ { type: "number", name: "width", message: "Page width (mm)", initial: 210, min: 1 },
66
+ { type: "number", name: "height", message: "Page height (mm)", initial: 297, min: 1 },
67
+ { type: "number", name: "pageCount", message: "Number of pages", initial: 1, min: 1 },
68
+ {
69
+ type: "select",
70
+ name: "princeVersion",
71
+ message: "PrinceXML version",
72
+ choices: [
73
+ { title: "15 (flexbox support \u2014 recommended)", value: 15 },
74
+ { title: "11 (legacy)", value: 11 }
75
+ ]
76
+ },
77
+ {
78
+ type: "toggle",
79
+ name: "resizable",
80
+ message: "Resizable template?",
81
+ initial: false,
82
+ active: "yes",
83
+ inactive: "no"
84
+ },
85
+ {
86
+ type: "toggle",
87
+ name: "proofable",
88
+ message: "Enable proof downloads (draft_proofable)?",
89
+ initial: true,
90
+ active: "yes",
91
+ inactive: "no"
92
+ },
93
+ {
94
+ type: "toggle",
95
+ name: "orderable",
96
+ message: "Include orderable export (Print from Snap)?",
97
+ initial: false,
98
+ active: "yes",
99
+ inactive: "no"
100
+ }
101
+ ],
102
+ { onCancel }
103
+ );
104
+ return {
105
+ mode: "pdf",
106
+ name,
107
+ fullName,
108
+ subdomain,
109
+ width: pdf.width,
110
+ height: pdf.height,
111
+ pageCount: pdf.pageCount,
112
+ princeVersion: pdf.princeVersion,
113
+ resizable: pdf.resizable,
114
+ proofable: pdf.proofable,
115
+ orderable: pdf.orderable
116
+ };
117
+ }
118
+ if (base.mode === "general") {
119
+ const gen = await prompts(
120
+ [
121
+ { type: "number", name: "width", message: "Width (px)", initial: 400, min: 1 },
122
+ { type: "number", name: "height", message: "Height (px)", initial: 400, min: 1 },
123
+ {
124
+ type: "number",
125
+ name: "pageCount",
126
+ message: "Number of pages / sections",
127
+ initial: 1,
128
+ min: 1
129
+ },
130
+ {
131
+ type: "multiselect",
132
+ name: "exportFormats",
133
+ message: "Export formats",
134
+ choices: [
135
+ { title: "JPG", value: "jpg", selected: true },
136
+ { title: "PNG", value: "png", selected: true }
137
+ ],
138
+ min: 1
139
+ }
140
+ ],
141
+ { onCancel }
142
+ );
143
+ return {
144
+ mode: "general",
145
+ name,
146
+ fullName,
147
+ subdomain,
148
+ width: gen.width,
149
+ height: gen.height,
150
+ pageCount: gen.pageCount,
151
+ exportFormats: gen.exportFormats
152
+ };
153
+ }
154
+ if (base.mode === "video") {
155
+ return { mode: "video", name, fullName, subdomain };
156
+ }
157
+ return { mode: "email", name, fullName, subdomain };
158
+ }
159
+
160
+ // src/scaffold.ts
161
+ import { join } from "path";
162
+ import fse from "fs-extra";
163
+ import kleur2 from "kleur";
164
+
165
+ // src/templates/shared.ts
166
+ function changelog(fullName) {
167
+ return `# Changelog
168
+
169
+ All notable changes to **${fullName}** will be documented here.
170
+
171
+ ## [Unreleased]
172
+
173
+ ### Added
174
+ - Initial scaffold
175
+ `;
176
+ }
177
+ function gitkeep() {
178
+ return "";
179
+ }
180
+ function fontsScss() {
181
+ const weights = [
182
+ ["Regular", 400],
183
+ ["Light", 300],
184
+ ["Medium", 500],
185
+ ["SemiBold", 600],
186
+ ["Bold", 700],
187
+ ["Black", 900]
188
+ ];
189
+ return weights.map(
190
+ ([variant, weight]) => `@font-face {
191
+ font-family: 'Montserrat';
192
+ src: url('../assets/Montserrat-${variant}.ttf') format('truetype');
193
+ font-weight: ${weight};
194
+ font-style: normal;
195
+ }`
196
+ ).join("\n\n") + "\n";
197
+ }
198
+ function fontsScssEmpty() {
199
+ return `// Add custom fonts here.
200
+ // Place font files in assets/ and declare them below.
201
+ //
202
+ // Example:
203
+ //
204
+ // @font-face {
205
+ // font-family: 'Montserrat';
206
+ // src: url('../assets/Montserrat-Regular.ttf') format('truetype');
207
+ // font-weight: 400;
208
+ // font-style: normal;
209
+ // }
210
+ //
211
+ // @font-face {
212
+ // font-family: 'Montserrat';
213
+ // src: url('../assets/Montserrat-Bold.ttf') format('truetype');
214
+ // font-weight: 700;
215
+ // font-style: normal;
216
+ // }
217
+ `;
218
+ }
219
+ function mixinsScss() {
220
+ return `@mixin absolute($top: auto, $right: auto, $bottom: auto, $left: auto) {
221
+ position: absolute;
222
+ top: $top;
223
+ right: $right;
224
+ bottom: $bottom;
225
+ left: $left;
226
+ }
227
+
228
+ @mixin flex($align: center, $justify: center, $direction: row) {
229
+ display: flex;
230
+ align-items: $align;
231
+ justify-content: $justify;
232
+ flex-direction: $direction;
233
+ }
234
+
235
+ @mixin truncate($lines: 1) {
236
+ @if $lines == 1 {
237
+ overflow: hidden;
238
+ text-overflow: ellipsis;
239
+ white-space: nowrap;
240
+ } @else {
241
+ display: -webkit-box;
242
+ -webkit-line-clamp: $lines;
243
+ -webkit-box-orient: vertical;
244
+ overflow: hidden;
245
+ }
246
+ }
247
+ `;
248
+ }
249
+
250
+ // src/templates/pdf.ts
251
+ function pdfFiles(config, pages) {
252
+ return {
253
+ "CHANGELOG.md": changelog(config.fullName),
254
+ "data.yaml": pdfYaml(config),
255
+ "index.html": pdfHtml(pages),
256
+ "assets/.gitkeep": gitkeep(),
257
+ ...pdfPartials(pages),
258
+ "scss/style.scss.hbs": pdfStyleSass(config, pages),
259
+ "scss/_fonts.scss": fontsScssEmpty(),
260
+ "scss/_variables.scss": pdfVariables(config),
261
+ "scss/_mixins.scss": mixinsScss(),
262
+ ...pdfPageScss(pages)
263
+ };
264
+ }
265
+ function pdfYaml(config) {
266
+ const { fullName, subdomain, width, height, pageCount, princeVersion, resizable, proofable, orderable } = config;
267
+ const lines = [
268
+ `name: TEMPLATEID - ${fullName}`,
269
+ `mode: pdf`,
270
+ `prince_version: ${princeVersion}`,
271
+ `status: 0`,
272
+ `subdomain: ${subdomain}`,
273
+ `dimension_mode: mm`,
274
+ `page_count: ${pageCount}`,
275
+ `dimension_width: ${width}`,
276
+ `dimension_height: ${height}`,
277
+ `compositing_data: {}`
278
+ ];
279
+ if (proofable) lines.push(`draft_proofable: true`);
280
+ if (resizable) lines.push(`resizable: true`);
281
+ lines.push(`export_options:`);
282
+ if (orderable) {
283
+ lines.push(
284
+ ` orderable:`,
285
+ ` title: Print order`,
286
+ ` description: Order from Snap`,
287
+ ` description_complete: Ordered from Snap`,
288
+ ` title_download: Download for your own printer`,
289
+ ` description_download: Download for your own printer`,
290
+ ` description_download_complete: Downloaded for your own printer`,
291
+ ` marks: true`,
292
+ ` bleed: true`,
293
+ ` supplier_choice: true`
294
+ );
295
+ }
296
+ lines.push(
297
+ ` downloadables:`,
298
+ ` hi_res:`,
299
+ ` title: Hi-res Download`,
300
+ ` renderer: pdf`,
301
+ ` marks: true`,
302
+ ` bleed: true`,
303
+ ` slug: false`,
304
+ ` approval_required: false`,
305
+ ` disable_printing: false`,
306
+ ` low-res-pdf:`,
307
+ ` title: Local Printer PDF - Use this version when emailing or printing in-house.`,
308
+ ` renderer: pdf`,
309
+ ` bleed: false`,
310
+ ` slug: false`,
311
+ ` marks: false`,
312
+ ` approval_required: false`,
313
+ ` disable_printing: false`,
314
+ ` resolution: 200`,
315
+ `fields_data:`
316
+ );
317
+ return lines.join("\n") + "\n";
318
+ }
319
+ function pdfHtml(pages) {
320
+ const partialIncludes = pages.map((p) => ` {{> ${p}}}`).join("\n");
321
+ return `{{ pragma "disable-legacy-nesting" }}
322
+ <!DOCTYPE html>
323
+ <html>
324
+ <head>
325
+ <meta charset="UTF-8">
326
+ </head>
327
+ <body>
328
+ ${partialIncludes}
329
+ </body>
330
+ </html>
331
+ `;
332
+ }
333
+ function pdfPartials(pages) {
334
+ return Object.fromEntries(
335
+ pages.map((pageName, i) => {
336
+ const label = pageName.includes("-") ? pageName.split("-").slice(1).join(" ") : `page ${i + 1}`;
337
+ return [
338
+ `partials/${pageName}.html`,
339
+ `<div class="page ${pageName}">
340
+ {{! ${label} }}
341
+ </div>
342
+ `
343
+ ];
344
+ })
345
+ );
346
+ }
347
+ function pdfStyleSass(config, pages) {
348
+ const { width, height, resizable } = config;
349
+ const pageImports = pages.map((_, i) => `@import 'pages/page${i + 1}';`).join("\n");
350
+ if (resizable) {
351
+ return `@import 'fonts';
352
+ @import 'variables';
353
+ @import 'mixins';
354
+
355
+ ${pageImports}
356
+
357
+ {{! Add resizable page sizes below \u2014 update data.yaml select field to match }}
358
+ {{#if_eq page-size 'A4'}}
359
+ @page { size: ${width}mm ${height}mm; }
360
+ $multiplier: 1;
361
+ {{/if_eq}}
362
+
363
+ * {
364
+ box-sizing: border-box;
365
+ margin: 0;
366
+ padding: 0;
367
+ }
368
+
369
+ .page {
370
+ width: ${width}mm * $multiplier;
371
+ height: ${height}mm * $multiplier;
372
+ position: relative;
373
+ overflow: hidden;
374
+ page-break-after: always;
375
+
376
+ &:last-child {
377
+ page-break-after: avoid;
378
+ }
379
+ }
380
+ `;
381
+ }
382
+ return `@import 'fonts';
383
+ @import 'variables';
384
+ @import 'mixins';
385
+
386
+ ${pageImports}
387
+
388
+ @page {
389
+ size: ${width}mm ${height}mm;
390
+ padding: 0;
391
+ margin: 0;
392
+ }
393
+
394
+ * {
395
+ box-sizing: border-box;
396
+ margin: 0;
397
+ padding: 0;
398
+ }
399
+
400
+ body {
401
+ width: ${width}mm;
402
+ }
403
+
404
+ .page {
405
+ width: ${width}mm;
406
+ height: ${height}mm;
407
+ position: relative;
408
+ overflow: hidden;
409
+ page-break-after: always;
410
+
411
+ &:last-child {
412
+ page-break-after: avoid;
413
+ }
414
+ }
415
+ `;
416
+ }
417
+ function pdfVariables(config) {
418
+ return `// Dimensions
419
+ $page-width: ${config.width}mm;
420
+ $page-height: ${config.height}mm;
421
+
422
+ // Typography
423
+ $font-primary: 'Montserrat', Arial, sans-serif;
424
+
425
+ // Colors \u2014 update to match brand
426
+ $color-primary: #000000;
427
+ $color-secondary: #ffffff;
428
+ $color-accent: #0079C8;
429
+ `;
430
+ }
431
+ function pdfPageScss(pages) {
432
+ return Object.fromEntries(
433
+ pages.map((pageName, i) => {
434
+ const label = pageName.includes("-") ? pageName.split("-").slice(1).join(" ") : `page ${i + 1}`;
435
+ return [`scss/pages/_page${i + 1}.scss`, `.${pageName} {
436
+ // ${label} styles
437
+ }
438
+ `];
439
+ })
440
+ );
441
+ }
442
+
443
+ // src/templates/general.ts
444
+ function generalFiles(config, pages) {
445
+ return {
446
+ "CHANGELOG.md": changelog(config.fullName),
447
+ "data.yaml": generalYaml(config),
448
+ "index.html": generalHtml(pages),
449
+ "assets/.gitkeep": gitkeep(),
450
+ ...generalPartials(pages),
451
+ "scss/style.scss.hbs": generalStyleSass(config, pages),
452
+ "scss/_fonts.scss": fontsScssEmpty(),
453
+ "scss/_variables.scss": generalVariables(config),
454
+ "scss/_mixins.scss": mixinsScss(),
455
+ ...generalPageScss(pages)
456
+ };
457
+ }
458
+ function generalYaml(config) {
459
+ const { fullName, subdomain, width, height, pageCount, exportFormats } = config;
460
+ const lines = [
461
+ `name: TEMPLATEID - ${fullName}`,
462
+ `mode: general`,
463
+ `subdomain: ${subdomain}`,
464
+ `dimension_mode: px`,
465
+ `page_count: ${pageCount}`,
466
+ `dimension_width: ${width}`,
467
+ `dimension_height: ${height}`,
468
+ `compositing_data: {}`,
469
+ `export_options:`,
470
+ ` downloadables:`
471
+ ];
472
+ if (exportFormats.includes("jpg")) {
473
+ lines.push(
474
+ ` jpg_chrome:`,
475
+ ` title: JPG`,
476
+ ` renderer: image`,
477
+ ` format: jpg`
478
+ );
479
+ }
480
+ if (exportFormats.includes("png")) {
481
+ lines.push(
482
+ ` png_chrome:`,
483
+ ` title: PNG`,
484
+ ` renderer: image`,
485
+ ` format: png`
486
+ );
487
+ }
488
+ lines.push(`fields_data:`);
489
+ return lines.join("\n") + "\n";
490
+ }
491
+ function generalHtml(pages) {
492
+ if (pages.length > 1) {
493
+ const partialIncludes = pages.map((p) => ` {{> ${p}}}`).join("\n");
494
+ return `{{ pragma "disable-legacy-nesting" }}
495
+ <!DOCTYPE html>
496
+ <html>
497
+ <head>
498
+ <meta charset="UTF-8">
499
+ </head>
500
+ <body>
501
+ ${partialIncludes}
502
+ </body>
503
+ </html>
504
+ `;
505
+ }
506
+ return `{{ pragma "disable-legacy-nesting" }}
507
+ <!DOCTYPE html>
508
+ <html>
509
+ <head>
510
+ <meta charset="UTF-8">
511
+ </head>
512
+ <body>
513
+ <div class="page">
514
+ {{! Template content here }}
515
+ </div>
516
+ </body>
517
+ </html>
518
+ `;
519
+ }
520
+ function generalPartials(pages) {
521
+ if (pages.length <= 1) return {};
522
+ return Object.fromEntries(
523
+ pages.map((pageName, i) => {
524
+ const label = pageName.includes("-") ? pageName.split("-").slice(1).join(" ") : `section ${i + 1}`;
525
+ return [
526
+ `partials/${pageName}.html`,
527
+ `<div class="page ${pageName}">
528
+ {{! ${label} }}
529
+ </div>
530
+ `
531
+ ];
532
+ })
533
+ );
534
+ }
535
+ function generalStyleSass(config, pages) {
536
+ const { width, height } = config;
537
+ const pageImports = pages.map((_, i) => `@import 'pages/page${i + 1}';`).join("\n");
538
+ return `@import 'fonts';
539
+ @import 'variables';
540
+ @import 'mixins';
541
+
542
+ ${pageImports}
543
+
544
+ * {
545
+ box-sizing: border-box;
546
+ margin: 0;
547
+ padding: 0;
548
+ }
549
+
550
+ body {
551
+ width: ${width}px;
552
+ height: ${height}px;
553
+ }
554
+
555
+ .page {
556
+ width: ${width}px;
557
+ height: ${height}px;
558
+ position: relative;
559
+ overflow: hidden;
560
+ }
561
+ `;
562
+ }
563
+ function generalVariables(config) {
564
+ return `// Dimensions
565
+ $page-width: ${config.width}px;
566
+ $page-height: ${config.height}px;
567
+
568
+ // Typography
569
+ $font-primary: 'Montserrat', Arial, sans-serif;
570
+
571
+ // Colors \u2014 use hex or rgb only (no CMYK in general templates)
572
+ $color-primary: #000000;
573
+ $color-secondary: #ffffff;
574
+ $color-accent: #0079C8;
575
+ `;
576
+ }
577
+ function generalPageScss(pages) {
578
+ return Object.fromEntries(
579
+ pages.map((pageName, i) => {
580
+ const label = pageName.includes("-") ? pageName.split("-").slice(1).join(" ") : `section ${i + 1}`;
581
+ return [`scss/pages/_page${i + 1}.scss`, `.${pageName} {
582
+ // ${label} styles
583
+ }
584
+ `];
585
+ })
586
+ );
587
+ }
588
+
589
+ // src/templates/video.ts
590
+ function videoFiles(config) {
591
+ return {
592
+ "CHANGELOG.md": changelog(config.fullName),
593
+ "data.yaml": videoYaml(config),
594
+ "index.html": videoHtml(),
595
+ "assets/.gitkeep": gitkeep(),
596
+ "scss/style.scss.hbs": videoStyleSass(),
597
+ "scss/_fonts.scss": fontsScss(),
598
+ "scss/_variables.scss": videoVariables(),
599
+ "scss/_mixins.scss": mixinsScss(),
600
+ "scss/pages/_page1.scss": `.page {
601
+ // video frame styles
602
+ }
603
+ `
604
+ };
605
+ }
606
+ function videoYaml(config) {
607
+ return `# TODO: Video template support is WIP
608
+ name: TEMPLATEID - ${config.fullName}
609
+ mode: video
610
+ subdomain: ${config.subdomain}
611
+ duration: '30'
612
+ highlight_point: '27'
613
+ dimension_height: 1080
614
+ dimension_width: 1920
615
+ export_options:
616
+ downloadables:
617
+ hi-res:
618
+ title: 1080p
619
+ fields_data:
620
+ `;
621
+ }
622
+ function videoHtml() {
623
+ return `{{ pragma "disable-legacy-nesting" }}
624
+ <!DOCTYPE html>
625
+ <html>
626
+ <head>
627
+ <meta charset="UTF-8">
628
+ </head>
629
+ <body>
630
+ <div class="page">
631
+ {{! TODO: Video template content }}
632
+ </div>
633
+ </body>
634
+ </html>
635
+ `;
636
+ }
637
+ function videoStyleSass() {
638
+ return `@import 'fonts';
639
+ @import 'variables';
640
+ @import 'mixins';
641
+ @import 'pages/page1';
642
+
643
+ * {
644
+ box-sizing: border-box;
645
+ margin: 0;
646
+ padding: 0;
647
+ }
648
+
649
+ body {
650
+ width: 1920px;
651
+ height: 1080px;
652
+ }
653
+
654
+ .page {
655
+ width: 1920px;
656
+ height: 1080px;
657
+ position: relative;
658
+ overflow: hidden;
659
+ }
660
+ `;
661
+ }
662
+ function videoVariables() {
663
+ return `$page-width: 1920px;
664
+ $page-height: 1080px;
665
+
666
+ $font-primary: 'Montserrat', Arial, sans-serif;
667
+
668
+ $color-primary: #000000;
669
+ $color-secondary: #ffffff;
670
+ $color-accent: #0079C8;
671
+ `;
672
+ }
673
+
674
+ // src/templates/email.ts
675
+ function emailFiles(config) {
676
+ return {
677
+ "CHANGELOG.md": changelog(config.fullName),
678
+ "data.yaml": emailYaml(config),
679
+ "index.html": emailHtml(),
680
+ "assets/.gitkeep": gitkeep(),
681
+ "scss/style.scss.hbs": emailStyleSass(),
682
+ "scss/_fonts.scss": fontsScss(),
683
+ "scss/_variables.scss": emailVariables(),
684
+ "scss/_mixins.scss": mixinsScss(),
685
+ "scss/pages/_page1.scss": `.email-body {
686
+ // email body styles
687
+ }
688
+ `
689
+ };
690
+ }
691
+ function emailYaml(config) {
692
+ return `name: TEMPLATEID - ${config.fullName}
693
+ mode: email
694
+ status: 0
695
+ subdomain: ${config.subdomain}
696
+ export_options:
697
+ downloadables:
698
+ a:
699
+ local_zip:
700
+ title: Standalone Download (local assets)
701
+ host_files: false
702
+ output: zip
703
+ b:
704
+ hosted_download:
705
+ title: HTML Download (hosted assets)
706
+ host_files: true
707
+ output: zip
708
+ fields_data:
709
+ `;
710
+ }
711
+ function emailHtml() {
712
+ return `<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
713
+ <html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
714
+ <head>
715
+ <!--[if gte mso 9]>
716
+ <xml>
717
+ <o:OfficeDocumentSettings>
718
+ <o:AllowPNG/>
719
+ <o:PixelsPerInch>96</o:PixelsPerInch>
720
+ </o:OfficeDocumentSettings>
721
+ </xml>
722
+ <![endif]-->
723
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
724
+ <meta name="viewport" content="width=device-width, initial-scale=1">
725
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
726
+ <title>{{{subject}}}</title>
727
+ <style type="text/css">{{{___assembled_css}}}</style>
728
+ </head>
729
+ <body style="margin: 0 0 0 0; padding: 0 0 0 0;" bgcolor="#ededed">
730
+ <table role="none" width="100%" cellpadding="0" cellspacing="0" border="0">
731
+ <tr>
732
+ <td>
733
+ <center>
734
+ <table role="none" width="600" class="full-wd" cellpadding="0" cellspacing="0" border="0">
735
+ <tr>
736
+ <td>
737
+ {{! TODO: Email content here }}
738
+ </td>
739
+ </tr>
740
+ </table>
741
+ </center>
742
+ </td>
743
+ </tr>
744
+ </table>
745
+ </body>
746
+ </html>
747
+ `;
748
+ }
749
+ function emailStyleSass() {
750
+ return `@import 'fonts';
751
+ @import 'variables';
752
+ @import 'mixins';
753
+ @import 'pages/page1';
754
+
755
+ // Use hex/rgb only \u2014 no CMYK in email
756
+
757
+ * {
758
+ box-sizing: border-box;
759
+ }
760
+
761
+ body {
762
+ margin: 0;
763
+ padding: 0;
764
+ background-color: #f4f4f4;
765
+ }
766
+
767
+ img {
768
+ display: block;
769
+ border: 0;
770
+ outline: none;
771
+ text-decoration: none;
772
+ }
773
+
774
+ @media screen and (max-width: 599px) {
775
+ .mobileOff {
776
+ width: 0px !important;
777
+ display: none !important;
778
+ }
779
+ }
780
+ `;
781
+ }
782
+ function emailVariables() {
783
+ return `$email-width: 600px;
784
+
785
+ // Use hex/rgb only (no CMYK)
786
+ $font-primary: 'Montserrat', Arial, sans-serif;
787
+
788
+ $color-primary: #000000;
789
+ $color-secondary: #ffffff;
790
+ $color-background: #f4f4f4;
791
+ $color-accent: #0079C8;
792
+ `;
793
+ }
794
+
795
+ // src/scaffold.ts
796
+ async function scaffold(config) {
797
+ const outDir = join(process.cwd(), config.fullName);
798
+ if (await fse.pathExists(outDir)) {
799
+ console.error(kleur2.red(`
800
+ Error: directory "${config.fullName}" already exists.
801
+ `));
802
+ process.exit(1);
803
+ }
804
+ let files;
805
+ if (config.mode === "pdf") {
806
+ files = pdfFiles(config, buildPages(config.pageCount));
807
+ } else if (config.mode === "general") {
808
+ files = generalFiles(config, buildPages(config.pageCount));
809
+ } else if (config.mode === "video") {
810
+ files = videoFiles(config);
811
+ } else {
812
+ files = emailFiles(config);
813
+ }
814
+ console.log("");
815
+ for (const [rel, content] of Object.entries(files)) {
816
+ const fullPath = join(outDir, rel);
817
+ await fse.ensureDir(join(fullPath, ".."));
818
+ await fse.writeFile(fullPath, content, "utf8");
819
+ console.log(kleur2.dim(` + ${rel}`));
820
+ }
821
+ }
822
+
823
+ // src/index.ts
824
+ async function run(nameArg) {
825
+ console.log(kleur3.bold().cyan("\n \u25C6 dokio create\n"));
826
+ const config = await runPrompts(nameArg);
827
+ await scaffold(config);
828
+ const isWip = config.mode === "video";
829
+ console.log(kleur3.green(`
830
+ \u2713 Created ${kleur3.bold(config.fullName)}
831
+ `));
832
+ if (isWip) {
833
+ console.log(
834
+ kleur3.yellow(` \u26A0 ${config.mode} support is WIP \u2014 check data.yaml for TODOs
835
+ `)
836
+ );
837
+ }
838
+ console.log(kleur3.dim(` Next steps:`));
839
+ console.log(kleur3.dim(` cd ${config.fullName}`));
840
+ console.log(kleur3.dim(` Edit data.yaml \u2014 set your template ID and subdomain`));
841
+ console.log(kleur3.dim(` Add assets to assets/`));
842
+ console.log("");
843
+ }
844
+
845
+ // bin/index.ts
846
+ run(process.argv[2]);
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "create-dokio",
3
+ "version": "0.1.1",
4
+ "description": "CLI scaffold for Dokio templates",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-dokio": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "dependencies": {
16
+ "fs-extra": "^11.2.0",
17
+ "kleur": "^4.1.5",
18
+ "prompts": "^2.4.2"
19
+ },
20
+ "devDependencies": {
21
+ "@commitlint/cli": "^19.0.0",
22
+ "@commitlint/config-conventional": "^19.0.0",
23
+ "@commitlint/types": "^19.0.0",
24
+ "@types/fs-extra": "^11.0.4",
25
+ "@types/node": "^20.0.0",
26
+ "@types/prompts": "^2.4.9",
27
+ "changelogen": "^0.6.2",
28
+ "husky": "^9.0.0",
29
+ "tsup": "^8.0.0",
30
+ "typescript": "^5.4.0"
31
+ },
32
+ "tsup": {
33
+ "entry": {
34
+ "index": "bin/index.ts"
35
+ },
36
+ "format": [
37
+ "esm"
38
+ ],
39
+ "clean": true,
40
+ "banner": {
41
+ "js": "#!/usr/bin/env node"
42
+ }
43
+ },
44
+ "keywords": [
45
+ "dokio",
46
+ "scaffold",
47
+ "template",
48
+ "cli"
49
+ ],
50
+ "license": "MIT",
51
+ "scripts": {
52
+ "build": "tsup",
53
+ "dev": "tsup --watch",
54
+ "changelog": "changelogen --bump --output CHANGELOG.md"
55
+ }
56
+ }