@lucasvu/scope-ui 0.0.3 → 0.0.5

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.
@@ -5,30 +5,264 @@ import { dirname, resolve } from 'node:path'
5
5
  import process from 'node:process'
6
6
 
7
7
  const PACKAGE_NAME = '@lucasvu/scope-ui'
8
+ const DEFAULT_THEME_PRESET = 'ocean'
9
+
10
+ const THEME_PRESETS = {
11
+ ocean: {
12
+ id: 'ocean',
13
+ label: 'Ocean Glass',
14
+ description:
15
+ 'Blue-cyan preset with clean glass surfaces. Matches the current default visual language.',
16
+ recommendedFor: [
17
+ 'Admin dashboards',
18
+ 'SaaS CRUD screens',
19
+ 'Projects that want the existing package look',
20
+ ],
21
+ tokens: {
22
+ light: {
23
+ '--tw-background': '0 0% 100%',
24
+ '--tw-foreground': '222.2 47.4% 11.2%',
25
+ '--tw-primary': '221.2 83.2% 53.3%',
26
+ '--tw-accent': '199 89% 48%',
27
+ '--tw-success': '142.1 76.2% 36.3%',
28
+ '--tw-destructive': '0 84.2% 60.2%',
29
+ '--tw-border': '214.3 31.8% 91.4%',
30
+ '--radius': '0.75rem',
31
+ '--surface': 'rgba(255, 255, 255, 0.92)',
32
+ '--surface-strong': 'rgba(241, 245, 249, 0.96)',
33
+ '--grey': 'rgba(248, 250, 252, 0.96)',
34
+ '--grey-strong': 'rgba(241, 245, 249, 0.98)',
35
+ '--shadow-sm': '0 10px 28px -18px rgba(15, 23, 42, 0.22)',
36
+ '--shadow': '0 24px 60px -28px rgba(15, 23, 42, 0.3)',
37
+ '--primary-grad-from': '199 89% 48%',
38
+ '--primary-grad-to': '221.2 83.2% 53.3%',
39
+ },
40
+ dark: {
41
+ '--tw-background': '222.2 84% 4.9%',
42
+ '--tw-foreground': '210 40% 98%',
43
+ '--tw-primary': '217.2 91.2% 59.8%',
44
+ '--tw-accent': '199 89% 48%',
45
+ '--tw-success': '142.1 70.6% 45.3%',
46
+ '--tw-destructive': '0 62.8% 30.6%',
47
+ '--tw-border': '217.2 32.6% 17.5%',
48
+ '--radius': '0.75rem',
49
+ '--surface': 'rgba(15, 23, 42, 0.78)',
50
+ '--surface-strong': 'rgba(30, 41, 59, 0.92)',
51
+ '--grey': 'rgba(30, 41, 59, 0.88)',
52
+ '--grey-strong': 'rgba(51, 65, 85, 0.92)',
53
+ '--shadow-sm': '0 16px 32px -18px rgba(2, 6, 23, 0.48)',
54
+ '--shadow': '0 28px 80px -32px rgba(2, 6, 23, 0.7)',
55
+ '--primary-grad-from': '198.6 88.7% 48.4%',
56
+ '--primary-grad-to': '221.2 83.2% 53.3%',
57
+ },
58
+ },
59
+ },
60
+ sunset: {
61
+ id: 'sunset',
62
+ label: 'Sunset Ember',
63
+ description:
64
+ 'Warm orange-coral preset with softer cream surfaces for branded landing or growth products.',
65
+ recommendedFor: [
66
+ 'Growth products',
67
+ 'Commerce backoffices',
68
+ 'Projects that want a warmer visual tone',
69
+ ],
70
+ tokens: {
71
+ light: {
72
+ '--tw-background': '30 100% 98%',
73
+ '--tw-foreground': '20 24% 14%',
74
+ '--tw-primary': '14 90% 56%',
75
+ '--tw-accent': '29 100% 58%',
76
+ '--tw-success': '145 63% 38%',
77
+ '--tw-destructive': '0 78% 58%',
78
+ '--tw-border': '24 45% 89%',
79
+ '--radius': '0.85rem',
80
+ '--surface': 'rgba(255, 247, 240, 0.92)',
81
+ '--surface-strong': 'rgba(255, 237, 223, 0.96)',
82
+ '--grey': 'rgba(255, 243, 231, 0.96)',
83
+ '--grey-strong': 'rgba(255, 232, 214, 0.98)',
84
+ '--shadow-sm': '0 12px 30px -18px rgba(194, 65, 12, 0.18)',
85
+ '--shadow': '0 28px 64px -30px rgba(154, 52, 18, 0.24)',
86
+ '--primary-grad-from': '29 100% 58%',
87
+ '--primary-grad-to': '14 90% 56%',
88
+ },
89
+ dark: {
90
+ '--tw-background': '20 24% 8%',
91
+ '--tw-foreground': '40 33% 96%',
92
+ '--tw-primary': '18 100% 62%',
93
+ '--tw-accent': '35 100% 58%',
94
+ '--tw-success': '145 60% 47%',
95
+ '--tw-destructive': '0 73% 52%',
96
+ '--tw-border': '18 24% 22%',
97
+ '--radius': '0.85rem',
98
+ '--surface': 'rgba(41, 24, 18, 0.84)',
99
+ '--surface-strong': 'rgba(59, 34, 24, 0.9)',
100
+ '--grey': 'rgba(70, 42, 29, 0.88)',
101
+ '--grey-strong': 'rgba(92, 54, 38, 0.9)',
102
+ '--shadow-sm': '0 18px 36px -20px rgba(67, 20, 7, 0.48)',
103
+ '--shadow': '0 30px 84px -34px rgba(67, 20, 7, 0.62)',
104
+ '--primary-grad-from': '35 100% 58%',
105
+ '--primary-grad-to': '18 100% 62%',
106
+ },
107
+ },
108
+ },
109
+ forest: {
110
+ id: 'forest',
111
+ label: 'Forest Mist',
112
+ description:
113
+ 'Emerald-teal preset with soft botanical surfaces for calmer productivity products.',
114
+ recommendedFor: [
115
+ 'Operations tools',
116
+ 'Internal platforms',
117
+ 'Products that want a calmer green tone',
118
+ ],
119
+ tokens: {
120
+ light: {
121
+ '--tw-background': '138 40% 98%',
122
+ '--tw-foreground': '160 25% 14%',
123
+ '--tw-primary': '158 64% 40%',
124
+ '--tw-accent': '173 58% 44%',
125
+ '--tw-success': '145 63% 36%',
126
+ '--tw-destructive': '0 78% 58%',
127
+ '--tw-border': '143 21% 88%',
128
+ '--radius': '0.8rem',
129
+ '--surface': 'rgba(245, 252, 249, 0.92)',
130
+ '--surface-strong': 'rgba(232, 245, 239, 0.96)',
131
+ '--grey': 'rgba(240, 248, 244, 0.96)',
132
+ '--grey-strong': 'rgba(225, 240, 232, 0.98)',
133
+ '--shadow-sm': '0 12px 28px -18px rgba(5, 86, 66, 0.18)',
134
+ '--shadow': '0 26px 62px -30px rgba(6, 78, 59, 0.24)',
135
+ '--primary-grad-from': '173 58% 44%',
136
+ '--primary-grad-to': '158 64% 40%',
137
+ },
138
+ dark: {
139
+ '--tw-background': '164 35% 8%',
140
+ '--tw-foreground': '144 35% 96%',
141
+ '--tw-primary': '160 70% 46%',
142
+ '--tw-accent': '174 72% 45%',
143
+ '--tw-success': '145 68% 46%',
144
+ '--tw-destructive': '0 70% 52%',
145
+ '--tw-border': '160 20% 20%',
146
+ '--radius': '0.8rem',
147
+ '--surface': 'rgba(16, 36, 31, 0.84)',
148
+ '--surface-strong': 'rgba(21, 51, 44, 0.9)',
149
+ '--grey': 'rgba(24, 61, 52, 0.88)',
150
+ '--grey-strong': 'rgba(31, 77, 65, 0.9)',
151
+ '--shadow-sm': '0 18px 34px -20px rgba(1, 44, 34, 0.48)',
152
+ '--shadow': '0 30px 82px -34px rgba(1, 44, 34, 0.6)',
153
+ '--primary-grad-from': '174 72% 45%',
154
+ '--primary-grad-to': '160 70% 46%',
155
+ },
156
+ },
157
+ },
158
+ graphite: {
159
+ id: 'graphite',
160
+ label: 'Graphite Pulse',
161
+ description:
162
+ 'Neutral slate preset with restrained blue accents for products that need a steadier enterprise tone.',
163
+ recommendedFor: [
164
+ 'Enterprise admin panels',
165
+ 'B2B internal tools',
166
+ 'Projects that want a more neutral interface',
167
+ ],
168
+ tokens: {
169
+ light: {
170
+ '--tw-background': '220 18% 97%',
171
+ '--tw-foreground': '222 24% 14%',
172
+ '--tw-primary': '221 24% 32%',
173
+ '--tw-accent': '198 83% 44%',
174
+ '--tw-success': '160 56% 38%',
175
+ '--tw-destructive': '0 72% 54%',
176
+ '--tw-border': '218 17% 86%',
177
+ '--radius': '0.7rem',
178
+ '--surface': 'rgba(248, 250, 252, 0.94)',
179
+ '--surface-strong': 'rgba(226, 232, 240, 0.96)',
180
+ '--grey': 'rgba(241, 245, 249, 0.98)',
181
+ '--grey-strong': 'rgba(226, 232, 240, 0.99)',
182
+ '--shadow-sm': '0 12px 30px -20px rgba(15, 23, 42, 0.18)',
183
+ '--shadow': '0 28px 66px -30px rgba(15, 23, 42, 0.24)',
184
+ '--primary-grad-from': '198 83% 44%',
185
+ '--primary-grad-to': '221 24% 32%',
186
+ },
187
+ dark: {
188
+ '--tw-background': '222 32% 8%',
189
+ '--tw-foreground': '210 25% 96%',
190
+ '--tw-primary': '210 24% 82%',
191
+ '--tw-accent': '192 92% 52%',
192
+ '--tw-success': '158 64% 45%',
193
+ '--tw-destructive': '0 72% 56%',
194
+ '--tw-border': '217 19% 24%',
195
+ '--radius': '0.7rem',
196
+ '--surface': 'rgba(15, 23, 42, 0.82)',
197
+ '--surface-strong': 'rgba(30, 41, 59, 0.92)',
198
+ '--grey': 'rgba(30, 41, 59, 0.9)',
199
+ '--grey-strong': 'rgba(51, 65, 85, 0.92)',
200
+ '--shadow-sm': '0 18px 38px -22px rgba(2, 6, 23, 0.48)',
201
+ '--shadow': '0 32px 88px -34px rgba(2, 6, 23, 0.68)',
202
+ '--primary-grad-from': '192 92% 52%',
203
+ '--primary-grad-to': '210 24% 82%',
204
+ },
205
+ },
206
+ },
207
+ }
208
+
209
+ function exitWithMissingValue(optionName) {
210
+ console.error(`Missing value for ${optionName}`)
211
+ process.exit(1)
212
+ }
8
213
 
9
214
  function parseArgs(argv) {
10
215
  const args = {
11
216
  force: false,
12
217
  agentsFile: 'AGENTS.md',
13
218
  themeFile: 'src/styles/ui-theme.css',
219
+ theme: DEFAULT_THEME_PRESET,
220
+ listThemes: false,
14
221
  }
15
222
 
16
223
  for (let index = 0; index < argv.length; index += 1) {
17
224
  const arg = argv[index]
225
+
18
226
  if (arg === '--force') {
19
227
  args.force = true
20
228
  continue
21
229
  }
230
+
22
231
  if (arg === '--agents-file') {
23
- args.agentsFile = argv[index + 1] ?? args.agentsFile
232
+ const value = argv[index + 1]
233
+ if (!value || value.startsWith('--')) {
234
+ exitWithMissingValue('--agents-file')
235
+ }
236
+ args.agentsFile = value
24
237
  index += 1
25
238
  continue
26
239
  }
240
+
27
241
  if (arg === '--theme-file') {
28
- args.themeFile = argv[index + 1] ?? args.themeFile
242
+ const value = argv[index + 1]
243
+ if (!value || value.startsWith('--')) {
244
+ exitWithMissingValue('--theme-file')
245
+ }
246
+ args.themeFile = value
29
247
  index += 1
30
248
  continue
31
249
  }
250
+
251
+ if (arg === '--theme') {
252
+ const value = argv[index + 1]
253
+ if (!value || value.startsWith('--')) {
254
+ exitWithMissingValue('--theme')
255
+ }
256
+ args.theme = value
257
+ index += 1
258
+ continue
259
+ }
260
+
261
+ if (arg === '--list-themes') {
262
+ args.listThemes = true
263
+ continue
264
+ }
265
+
32
266
  if (arg === '--help' || arg === '-h') {
33
267
  printHelp()
34
268
  process.exit(0)
@@ -45,38 +279,85 @@ Create bootstrap files for using ${PACKAGE_NAME} with AI/codegen.
45
279
 
46
280
  Usage:
47
281
  npx scope-ui-init
48
- npx scope-ui-init --force
282
+ Run this after @lucasvu/scope-ui has been installed in the consumer project.
283
+
284
+ npx scope-ui-init --list-themes
285
+ npx scope-ui-init --theme sunset
286
+ npx scope-ui-init --theme forest --force
49
287
  npx scope-ui-init --agents-file AGENTS.md --theme-file src/styles/ui-theme.css
50
288
 
51
289
  Options:
52
290
  --force overwrite existing files
53
291
  --agents-file target path for the generated AGENTS.md file
54
292
  --theme-file target path for the generated theme override file
293
+ --theme theme preset id (${Object.keys(THEME_PRESETS).join(', ')})
294
+ --list-themes show the approved preset list
55
295
  --help, -h show this help
56
296
  `)
57
297
  }
58
298
 
59
- function createAgentsTemplate({ themeFile }) {
299
+ function printThemeList() {
300
+ console.log('Approved theme presets:\n')
301
+
302
+ for (const themePreset of Object.values(THEME_PRESETS)) {
303
+ console.log(`${themePreset.id} - ${themePreset.label}`)
304
+ console.log(` ${themePreset.description}`)
305
+ console.log(` Recommended for: ${themePreset.recommendedFor.join(', ')}`)
306
+ console.log('')
307
+ }
308
+ }
309
+
310
+ function getThemePreset(themeId) {
311
+ return THEME_PRESETS[themeId]
312
+ }
313
+
314
+ function resolveThemePreset(themeId) {
315
+ const themePreset = getThemePreset(themeId)
316
+
317
+ if (themePreset) {
318
+ return themePreset
319
+ }
320
+
321
+ console.error(`Unknown theme preset: ${themeId}\n`)
322
+ printThemeList()
323
+ process.exit(1)
324
+ }
325
+
326
+ function renderTokenBlock(tokens) {
327
+ return Object.entries(tokens)
328
+ .map(([token, value]) => ` ${token}: ${value};`)
329
+ .join('\n')
330
+ }
331
+
332
+ function createAgentsTemplate({ themeFile, themePreset }) {
60
333
  return `# Agent Rules
61
334
 
62
335
  This repo uses \`${PACKAGE_NAME}\` as the default UI library.
336
+ This repo is locked to the \`${themePreset.label}\` theme preset (\`${themePreset.id}\`).
63
337
 
64
338
  Primary references:
65
339
  - \`node_modules/${PACKAGE_NAME}/README.md\`
66
340
  - \`node_modules/${PACKAGE_NAME}/AI_SETUP.md\`
67
341
  - \`${themeFile}\`
68
342
 
343
+ Theme preset summary:
344
+ - ${themePreset.description}
345
+ - Recommended for: ${themePreset.recommendedFor.join(', ')}
346
+
69
347
  Always:
70
348
  - import \`${PACKAGE_NAME}/styles.css\` once at the app entry
71
349
  - import the project theme override file after the package stylesheet
72
350
  - use root exports from \`${PACKAGE_NAME}\`
351
+ - use \`${themeFile}\` as the only source of truth for colors, radius, surfaces, and shadows
352
+ - keep layouts and component choices aligned with the shared preset-driven UI used across projects
73
353
  - read \`uiAiManifest\` before choosing components
74
- - read \`uiThemeContract\` before changing colors, surfaces, borders, or shadows
354
+ - read \`uiThemeContract\` before changing colors, surfaces, borders, radius, or shadows
75
355
  - keep UI token-driven and theme-driven
76
356
 
77
357
  Do not:
78
358
  - import from \`MainFe\` unless the task explicitly targets a legacy screen
79
- - hardcode brand colors inside page components when a theme token already exists
359
+ - invent a second palette or one-off brand colors outside \`${themeFile}\`
360
+ - restyle individual pages if the preset tokens can solve it globally
80
361
  - create duplicate Button/Input/Select wrappers unless a project-specific behavior is required
81
362
 
82
363
  Component choice:
@@ -92,20 +373,18 @@ Component choice:
92
373
  `
93
374
  }
94
375
 
95
- function createThemeTemplate() {
96
- return `:root {
97
- /* Override only the tokens you need for the project brand. */
98
- /* --tw-primary: 221.2 83.2% 53.3%; */
99
- /* --tw-accent: 199 89% 48%; */
100
- /* --primary-grad-from: 199 89% 48%; */
101
- /* --primary-grad-to: 221.2 83.2% 53.3%; */
102
- /* --surface: rgba(255, 255, 255, 0.92); */
376
+ function createThemeTemplate({ themePreset }) {
377
+ return `/* Generated by scope-ui-init. */
378
+ /* Theme preset: ${themePreset.label} (${themePreset.id}) */
379
+ /* Re-run \`npx scope-ui-init --theme <preset> --force\` to switch preset. */
380
+
381
+ :root {
382
+ ${renderTokenBlock(themePreset.tokens.light)}
103
383
  }
104
384
 
105
- .dark {
106
- /* --tw-primary: 217.2 91.2% 59.8%; */
107
- /* --tw-accent: 199 89% 48%; */
108
- /* --surface: rgba(15, 23, 42, 0.78); */
385
+ .dark,
386
+ [data-ui-theme='dark'] {
387
+ ${renderTokenBlock(themePreset.tokens.dark)}
109
388
  }
110
389
  `
111
390
  }
@@ -125,19 +404,27 @@ function writeFile(targetPath, content, force) {
125
404
 
126
405
  const options = parseArgs(process.argv.slice(2))
127
406
 
407
+ if (options.listThemes) {
408
+ printThemeList()
409
+ process.exit(0)
410
+ }
411
+
412
+ const themePreset = resolveThemePreset(options.theme)
413
+
128
414
  const agentsResult = writeFile(
129
415
  options.agentsFile,
130
- createAgentsTemplate({ themeFile: options.themeFile }),
416
+ createAgentsTemplate({ themeFile: options.themeFile, themePreset }),
131
417
  options.force,
132
418
  )
133
419
 
134
420
  const themeResult = writeFile(
135
421
  options.themeFile,
136
- createThemeTemplate(),
422
+ createThemeTemplate({ themePreset }),
137
423
  options.force,
138
424
  )
139
425
 
140
426
  console.log(`Initialized ${PACKAGE_NAME}`)
427
+ console.log(`- theme preset: ${themePreset.label} (${themePreset.id})`)
141
428
  console.log(`- ${agentsResult.path}: ${agentsResult.status}`)
142
429
  console.log(`- ${themeResult.path}: ${themeResult.status}`)
143
430
  console.log('')
@@ -145,3 +432,4 @@ console.log('Next steps:')
145
432
  console.log(`1. Import \`${PACKAGE_NAME}/styles.css\` once at your app entry.`)
146
433
  console.log(`2. Import \`${options.themeFile}\` right after the package stylesheet.`)
147
434
  console.log(`3. Let your agent read \`${options.agentsFile}\` before generating UI.`)
435
+ console.log(`4. Re-run this command with \`--theme <preset> --force\` if you want another approved preset.`)