create-portfoliable 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.
Files changed (62) hide show
  1. package/bin/create-portfoliable.mjs +425 -0
  2. package/cli/portfoliable.mjs +836 -0
  3. package/index.html +18 -0
  4. package/package.json +102 -0
  5. package/public/favicon.ico +0 -0
  6. package/public/favicon.png +0 -0
  7. package/scripts/check-updates.mjs +31 -0
  8. package/scripts/ensure-valence-index-css.mjs +245 -0
  9. package/scripts/generate-password-hash.mjs +25 -0
  10. package/scripts/release-orchestrator.mjs +363 -0
  11. package/scripts/scaffold-case.mjs +460 -0
  12. package/scripts/scaffold-consumer.mjs +87 -0
  13. package/scripts/smoke-homeview.mjs +133 -0
  14. package/scripts/smoke-initializer.mjs +129 -0
  15. package/scripts/smoke-packed.mjs +184 -0
  16. package/scripts/switch-valence-source.mjs +162 -0
  17. package/scripts/sync-template-shared.mjs +136 -0
  18. package/scripts/terminal-ui.mjs +274 -0
  19. package/scripts/verify-integration.mjs +72 -0
  20. package/src/App.js +10015 -0
  21. package/src/header-visibility.contract.js +31 -0
  22. package/src/header-visibility.contract.test.js +41 -0
  23. package/src/main.js +12 -0
  24. package/src/parser/markdown.js +855 -0
  25. package/src/style.css +88 -0
  26. package/templates/README.md +242 -0
  27. package/templates/configs/i18n/i18n.config.js +230 -0
  28. package/templates/configs/i18n/i18n.labels.js +188 -0
  29. package/templates/configs/portfoliable.a11y.config.js +262 -0
  30. package/templates/configs/portfoliable.design.config.js +651 -0
  31. package/templates/gitignore +9 -0
  32. package/templates/index.html +20 -0
  33. package/templates/public/api/.htaccess +4 -0
  34. package/templates/public/api/README.md +27 -0
  35. package/templates/public/api/password.config.example.json +16 -0
  36. package/templates/public/api/password.config.json +16 -0
  37. package/templates/public/api/unlock-case.php +56 -0
  38. package/templates/public/favicon.ico +0 -0
  39. package/templates/public/favicon.png +0 -0
  40. package/templates/public/portfoliable-logo.svg +3 -0
  41. package/templates/scripts/add-language.mjs +253 -0
  42. package/templates/scripts/convert-video-to-mp4.mjs +207 -0
  43. package/templates/scripts/convert-wav-to-mp3.mjs +184 -0
  44. package/templates/scripts/delete-case.mjs +320 -0
  45. package/templates/scripts/delete-language.mjs +195 -0
  46. package/templates/scripts/generate-password-hash.mjs +98 -0
  47. package/templates/scripts/i18n-config-utils.mjs +123 -0
  48. package/templates/scripts/scaffold-case.mjs +444 -0
  49. package/templates/scripts/sync-locales.mjs +1048 -0
  50. package/templates/scripts/validate-content.mjs +257 -0
  51. package/templates/scripts/validate-protection.mjs +187 -0
  52. package/templates/src/cases/index.js +188 -0
  53. package/templates/src/content/about/ABOUTME.md +202 -0
  54. package/templates/src/content/cases/template-case/case.md +479 -0
  55. package/templates/src/content/cases/template-case/cover.png +0 -0
  56. package/templates/src/content/cases/template-case/he-cover.png +0 -0
  57. package/templates/src/content/cases/template-case/template-audio.mp3 +0 -0
  58. package/templates/src/content/cases/template-case/template-video.mp4 +0 -0
  59. package/templates/src/content/thumbnail-options.generated.json +1044 -0
  60. package/templates/src/main.js +23 -0
  61. package/templates/src/parser/markdown.js +805 -0
  62. package/vite.config.js +111 -0
@@ -0,0 +1,274 @@
1
+ // File: scripts/terminal-ui.mjs
2
+ // Purpose: Shared terminal UI helpers for boxed output, progress rails, and first-run interactive prompts.
3
+ // Author: Lio Schimanko
4
+
5
+ // MARK: IMPORTS
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import readline from 'node:readline';
9
+
10
+ // MARK: STYLE TOKENS
11
+ export const ui = {
12
+ green: '\x1b[32m',
13
+ yellow: '\x1b[33m',
14
+ cyan: '\x1b[36m',
15
+ blue: '\x1b[34m',
16
+ bold: '\x1b[1m',
17
+ dim: '\x1b[2m',
18
+ red: '\x1b[31m',
19
+ reset: '\x1b[0m'
20
+ };
21
+
22
+ const ANSI_ESCAPE_PATTERN = /[\u001B\u009B][[\]()#;?]*(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~])/g;
23
+ const PROJECT_UI_MARKER = '.portfoliable-ui.json';
24
+
25
+ // MARK: WIDTH AND BOX RENDERING
26
+ // Removes ANSI escape sequences so visible width can be measured correctly.
27
+ export function stripAnsi(input) {
28
+ return String(input).replace(ANSI_ESCAPE_PATTERN, '');
29
+ }
30
+
31
+ // Computes user-visible width while ignoring ANSI style codes.
32
+ export function visibleLength(input) {
33
+ return Array.from(stripAnsi(input)).length;
34
+ }
35
+
36
+ // Right pads a string by visible width and preserves ANSI styling.
37
+ export function padVisible(input, targetWidth) {
38
+ const text = String(input);
39
+ const fill = Math.max(0, targetWidth - visibleLength(text));
40
+ return text + ' '.repeat(fill);
41
+ }
42
+
43
+ // Builds a top or bottom border line for boxed terminal UI.
44
+ function border(width, left, right) {
45
+ return `${left}${'─'.repeat(width - 2)}${right}`;
46
+ }
47
+
48
+ // Builds a full box from content lines and matching border width.
49
+ function buildBoxLines({ width, lines }) {
50
+ return [
51
+ border(width, '╭', '╮'),
52
+ ...lines.map((line) => boxLine(line, width)),
53
+ border(width, '╰', '╯')
54
+ ];
55
+ }
56
+
57
+ // Returns one boxed content line with ANSI-safe padding.
58
+ export function boxLine(text = '', width = 72) {
59
+ return `│ ${padVisible(text, width - 4)} │`;
60
+ }
61
+
62
+ // Prints a boxed block with consistent borders and line padding.
63
+ export function printBox({ width = 72, lines = [] }) {
64
+ const block = buildBoxLines({ width, lines });
65
+ for (const line of block) {
66
+ console.log(line);
67
+ }
68
+ }
69
+
70
+ // Prints a box attached to the right of the rail column.
71
+ export function printRailAttachedBox({ width = 72, lines = [] }) {
72
+ const block = buildBoxLines({ width, lines });
73
+ for (const line of block) {
74
+ console.log(`${ui.dim}│${ui.reset} ${line}`);
75
+ }
76
+ }
77
+
78
+ // Prints one or more empty rail lines.
79
+ export function printRailSpacer(lines = 1) {
80
+ for (let i = 0; i < lines; i += 1) {
81
+ console.log(railOnlyLine());
82
+ }
83
+ }
84
+
85
+ // Returns a standalone rail line used as vertical spacing.
86
+ function railOnlyLine() {
87
+ return `${ui.dim}│${ui.reset}`;
88
+ }
89
+
90
+ // Returns a content line visually attached to the rail column.
91
+ function railAttachedLine(content) {
92
+ return `${ui.dim}│${ui.reset} ${content}`;
93
+ }
94
+
95
+ // MARK: TERMINAL PREFERENCES
96
+ // Prints a rail line segment used to emulate vertical progress connectors.
97
+ export function printRailSegment({ dot = false, label = '', color = ui.cyan } = {}) {
98
+ if (dot) {
99
+ console.log(`${color}●${ui.reset} ${label}`.trimEnd());
100
+ return;
101
+ }
102
+ console.log(`${ui.dim}│${ui.reset}${label ? ` ${label}` : ''}`);
103
+ }
104
+
105
+ // Resolves marker path for project-level terminal UI preferences.
106
+ export function resolveUiMarkerPath(projectDir) {
107
+ return path.resolve(projectDir, PROJECT_UI_MARKER);
108
+ }
109
+
110
+ // Reads project-level terminal UI preferences with safe defaults.
111
+ export function readProjectUiPreferences(projectDir) {
112
+ const markerPath = resolveUiMarkerPath(projectDir);
113
+ if (!fs.existsSync(markerPath)) {
114
+ return null;
115
+ }
116
+
117
+ try {
118
+ const parsed = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
119
+ if (typeof parsed?.fancyDots === 'boolean') {
120
+ return parsed;
121
+ }
122
+ } catch {
123
+ // Falls back to null so callers can recover with defaults.
124
+ }
125
+
126
+ return null;
127
+ }
128
+
129
+ // Persists project-level terminal UI preferences.
130
+ export function writeProjectUiPreferences(projectDir, fancyDots) {
131
+ const markerPath = resolveUiMarkerPath(projectDir);
132
+ const payload = {
133
+ version: 1,
134
+ fancyDots: Boolean(fancyDots),
135
+ updatedAt: new Date().toISOString()
136
+ };
137
+
138
+ fs.writeFileSync(markerPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
139
+ }
140
+
141
+ // Returns true only when stdin/stdout are interactive terminals.
142
+ export function isInteractiveTerminal() {
143
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
144
+ }
145
+
146
+ // Renders selectable yes/no dot prompt content with optional rail attachment.
147
+ function renderDotPrompt({ selected, question, width, yesLabel, noLabel, attachedToRail }) {
148
+ const yesDot = selected === 0 ? `${ui.blue}●${ui.reset}` : '○';
149
+ const noDot = selected === 1 ? `${ui.blue}●${ui.reset}` : '○';
150
+
151
+ const rawLines = [
152
+ `${ui.bold}${question}${ui.reset}`,
153
+ `${ui.dim}Use Up/Down arrows and press Enter.${ui.reset}`,
154
+ `${yesDot} ${yesLabel}`,
155
+ `${noDot} ${noLabel}`
156
+ ];
157
+
158
+ const boxLines = buildBoxLines({ width, lines: rawLines });
159
+
160
+ if (!attachedToRail) {
161
+ return boxLines;
162
+ }
163
+
164
+ return boxLines.map((line) => railAttachedLine(line));
165
+ }
166
+
167
+ // Repaints an interactive prompt in place without stacking old frames.
168
+ function repaintPrompt(lines, renderedLineCount) {
169
+ if (renderedLineCount > 0) {
170
+ readline.moveCursor(process.stdout, 0, -renderedLineCount);
171
+ for (let i = 0; i < renderedLineCount; i += 1) {
172
+ readline.clearLine(process.stdout, 0);
173
+ readline.cursorTo(process.stdout, 0);
174
+ if (i < renderedLineCount - 1) {
175
+ readline.moveCursor(process.stdout, 0, 1);
176
+ }
177
+ }
178
+ readline.moveCursor(process.stdout, 0, -(renderedLineCount - 1));
179
+ }
180
+
181
+ for (let i = 0; i < lines.length; i += 1) {
182
+ process.stdout.write(lines[i]);
183
+ process.stdout.write('\n');
184
+ }
185
+
186
+ return lines.length;
187
+ }
188
+
189
+ // MARK: INTERACTIVE PROMPTS
190
+ // Shows an interactive yes/no prompt rendered with selectable dot options.
191
+ export async function promptYesNoDots({
192
+ question,
193
+ yesLabel = 'Yes',
194
+ noLabel = 'No',
195
+ defaultValue = true,
196
+ width = 86,
197
+ attachedToRail = true
198
+ } = {}) {
199
+ if (!isInteractiveTerminal()) {
200
+ return defaultValue;
201
+ }
202
+
203
+ let selected = defaultValue ? 0 : 1;
204
+ let renderedLineCount = 0;
205
+
206
+ readline.emitKeypressEvents(process.stdin);
207
+ process.stdin.setRawMode(true);
208
+ process.stdin.resume();
209
+
210
+ return new Promise((resolve) => {
211
+ // Renders current prompt frame whenever selection state changes.
212
+ const render = () => {
213
+ const lines = renderDotPrompt({
214
+ selected,
215
+ question,
216
+ width,
217
+ yesLabel,
218
+ noLabel,
219
+ attachedToRail
220
+ });
221
+ renderedLineCount = repaintPrompt(lines, renderedLineCount);
222
+ };
223
+
224
+ // Finalizes prompt, restores terminal state, and resolves selected value.
225
+ const finish = (value) => {
226
+ process.stdin.removeListener('keypress', onKeypress);
227
+ process.stdin.setRawMode(false);
228
+ process.stdin.pause();
229
+ if (attachedToRail) {
230
+ console.log(railOnlyLine());
231
+ }
232
+ resolve(value);
233
+ };
234
+
235
+ // Handles arrow navigation, confirmation, and escape/cancel shortcuts.
236
+ const onKeypress = (_str, key = {}) => {
237
+ if (key.name === 'up' || key.name === 'left') {
238
+ selected = 0;
239
+ render();
240
+ return;
241
+ }
242
+
243
+ if (key.name === 'down' || key.name === 'right') {
244
+ selected = 1;
245
+ render();
246
+ return;
247
+ }
248
+
249
+ if (key.name === 'return' || key.name === 'enter') {
250
+ finish(selected === 0);
251
+ return;
252
+ }
253
+
254
+ if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
255
+ finish(defaultValue);
256
+ }
257
+ };
258
+
259
+ process.stdin.on('keypress', onKeypress);
260
+ render();
261
+ });
262
+ }
263
+
264
+ // Shows a first-run yes/no prompt rendered with selectable dot options.
265
+ export async function promptFancyDotsPreference({ question = 'Enable decorative terminal lines and dots?', defaultValue = true, width = 86 } = {}) {
266
+ return promptYesNoDots({
267
+ question,
268
+ yesLabel: 'Yes, keep the line-and-dot terminal style',
269
+ noLabel: 'No, use simpler terminal output',
270
+ defaultValue,
271
+ width,
272
+ attachedToRail: true
273
+ });
274
+ }
@@ -0,0 +1,72 @@
1
+ // File: create-portfoliable/scripts/verify-integration.mjs
2
+ // Purpose: Verify local runtime and optional consumer integration build workflows.
3
+ // Author: Lio Schimanko
4
+
5
+ // MARK: IMPORTS
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { spawnSync } from 'node:child_process';
10
+
11
+ // MARK: PATH CONSTANTS
12
+ // Resolves the current script filename for deterministic root-path calculation.
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ // Resolves the directory containing this script.
15
+ const __dirname = path.dirname(__filename);
16
+ // Resolves canonical runtime package root.
17
+ const portfoliableRoot = path.resolve(__dirname, '..');
18
+ // Resolves a local sibling consumer repository path used for optional integration checks.
19
+ const portfolioRoot = path.resolve(portfoliableRoot, '..', '..', 'portfolio');
20
+
21
+ // MARK: COMMAND EXECUTOR
22
+ // Executes one labeled integration step and throws on non-zero status.
23
+ function runStep(label, command, args, cwd) {
24
+ console.log(`\n[verify] ${label}`);
25
+ // Executes verification command step.
26
+ const result = spawnSync(command, args, {
27
+ cwd,
28
+ stdio: 'inherit',
29
+ shell: process.platform === 'win32'
30
+ });
31
+
32
+ if (result.status !== 0) {
33
+ throw new Error(`Step failed: ${label}`);
34
+ }
35
+ }
36
+
37
+ // MARK: ENVIRONMENT DETECTION
38
+ // Detects whether a local portfolio consumer repository is available for cross-repo checks.
39
+ function hasPortfolioConsumer() {
40
+ // Points to package manifest used as existence marker for the consumer repo.
41
+ const packageJsonPath = path.join(portfolioRoot, 'package.json');
42
+ return fs.existsSync(packageJsonPath);
43
+ }
44
+
45
+ // MARK: VERIFICATION ORCHESTRATION
46
+ // Runs validation/build checks for runtime package and optionally for local consumer project.
47
+ function main() {
48
+ // Parses optional CLI flags used to control consumer verification behavior.
49
+ const args = new Set(process.argv.slice(2));
50
+ // Enables skipping local consumer build when flag is provided.
51
+ const skipConsumer = args.has('--skip-consumer');
52
+
53
+ runStep('Validate markdown content', 'npm', ['run', 'validate:content'], portfoliableRoot);
54
+ runStep('Build portfoliable', 'npm', ['run', 'build'], portfoliableRoot);
55
+
56
+ if (!skipConsumer && hasPortfolioConsumer()) {
57
+ runStep('Build portfolio consumer', 'npm', ['run', 'build'], portfolioRoot);
58
+ } else if (!skipConsumer) {
59
+ console.log('[verify] portfolio consumer not found. Skipping consumer build.');
60
+ }
61
+
62
+ console.log('\n[verify] Integration verification passed.');
63
+ }
64
+
65
+ // MARK: SCRIPT ENTRYPOINT
66
+ // Executes integration verification and converts thrown errors to exit code 1.
67
+ try {
68
+ main();
69
+ } catch (error) {
70
+ console.error(`\n[verify] ${error.message}`);
71
+ process.exit(1);
72
+ }