uniweb 0.2.42 → 0.2.43

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.
@@ -7,15 +7,24 @@
7
7
  * uniweb docs # Generate docs for current directory
8
8
  * uniweb docs --output README.md # Custom output filename
9
9
  * uniweb docs --from-source # Build schema from source (no build required)
10
+ * uniweb docs --target <path> # Specify foundation directory explicitly
10
11
  *
11
12
  * When run from a site directory, automatically finds and documents the
12
13
  * linked foundation, placing COMPONENTS.md in the site folder.
14
+ *
15
+ * When run from workspace root, auto-detects foundations. If multiple exist,
16
+ * prompts for selection.
13
17
  */
14
18
 
15
19
  import { existsSync } from 'node:fs'
16
20
  import { readFile } from 'node:fs/promises'
17
21
  import { resolve, join, dirname } from 'node:path'
18
22
  import { generateDocs } from '@uniweb/build'
23
+ import {
24
+ isWorkspaceRoot,
25
+ findFoundations,
26
+ promptSelect,
27
+ } from '../utils/workspace.js'
19
28
 
20
29
  // Colors for terminal output
21
30
  const colors = {
@@ -51,6 +60,7 @@ function parseArgs(args) {
51
60
  const options = {
52
61
  output: 'COMPONENTS.md',
53
62
  fromSource: false,
63
+ target: null,
54
64
  }
55
65
 
56
66
  for (let i = 0; i < args.length; i++) {
@@ -60,6 +70,8 @@ function parseArgs(args) {
60
70
  options.output = args[++i]
61
71
  } else if (arg === '--from-source' || arg === '-s') {
62
72
  options.fromSource = true
73
+ } else if (arg === '--target' || arg === '-t') {
74
+ options.target = args[++i]
63
75
  } else if (arg === '--help' || arg === '-h') {
64
76
  options.help = true
65
77
  }
@@ -79,16 +91,19 @@ ${colors.dim}Usage:${colors.reset}
79
91
  uniweb docs Generate COMPONENTS.md
80
92
  uniweb docs --output DOCS.md Custom output filename
81
93
  uniweb docs --from-source Build schema from source (no build required)
94
+ uniweb docs --target foundation Specify foundation directory explicitly
82
95
 
83
96
  ${colors.dim}Options:${colors.reset}
84
97
  -o, --output <file> Output filename (default: COMPONENTS.md)
85
98
  -s, --from-source Read meta.js files directly instead of schema.json
99
+ -t, --target <path> Foundation directory (auto-detected if not specified)
86
100
  -h, --help Show this help message
87
101
 
88
102
  ${colors.dim}Notes:${colors.reset}
89
103
  Run from a foundation directory to generate docs there.
90
104
  Run from a site directory to auto-detect the linked foundation
91
105
  and generate docs in the site folder for convenience.
106
+ Run from workspace root to auto-detect foundations (prompts if multiple).
92
107
  `)
93
108
  }
94
109
 
@@ -157,8 +172,45 @@ export async function docs(args) {
157
172
  let foundationDir = projectDir
158
173
  let outputDir = projectDir
159
174
 
175
+ // If --target specified, use it directly
176
+ if (options.target) {
177
+ foundationDir = resolve(projectDir, options.target)
178
+ outputDir = foundationDir
179
+ if (!isFoundation(foundationDir)) {
180
+ error(`Target directory does not appear to be a foundation: ${options.target}`)
181
+ log(`${colors.dim}Foundations have a src/components/ directory.${colors.reset}`)
182
+ process.exit(1)
183
+ }
184
+ info(`Using foundation: ${options.target}`)
185
+ }
186
+ // Check if we're at workspace root
187
+ else if (isWorkspaceRoot(projectDir)) {
188
+ const foundations = await findFoundations(projectDir)
189
+
190
+ if (foundations.length === 0) {
191
+ error('No foundations found in this workspace.')
192
+ log(`${colors.dim}Foundations have @uniweb/build in devDependencies.${colors.reset}`)
193
+ process.exit(1)
194
+ }
195
+
196
+ let targetFoundation
197
+ if (foundations.length === 1) {
198
+ targetFoundation = foundations[0]
199
+ info(`Found foundation: ${targetFoundation}`)
200
+ } else {
201
+ log(`${colors.dim}Multiple foundations found in workspace.${colors.reset}\n`)
202
+ targetFoundation = await promptSelect('Select foundation:', foundations)
203
+ if (!targetFoundation) {
204
+ log('Cancelled.')
205
+ process.exit(0)
206
+ }
207
+ }
208
+
209
+ foundationDir = resolve(projectDir, targetFoundation)
210
+ outputDir = foundationDir
211
+ }
160
212
  // Check if we're in a site directory
161
- if (isSite(projectDir)) {
213
+ else if (isSite(projectDir)) {
162
214
  const foundation = await resolveFoundationFromSite(projectDir)
163
215
  if (!foundation) {
164
216
  error('Could not find a linked foundation in this site.')
@@ -4,15 +4,24 @@
4
4
  * Commands for managing site content internationalization.
5
5
  *
6
6
  * Usage:
7
- * uniweb i18n extract Extract translatable strings to manifest
8
- * uniweb i18n sync Sync manifest with content changes
9
- * uniweb i18n status Show translation coverage per locale
7
+ * uniweb i18n extract Extract translatable strings to manifest
8
+ * uniweb i18n sync Sync manifest with content changes
9
+ * uniweb i18n status Show translation coverage per locale
10
+ * uniweb i18n --target <path> Specify site directory explicitly
11
+ *
12
+ * When run from workspace root, auto-detects sites. If multiple exist,
13
+ * prompts for selection.
10
14
  */
11
15
 
12
16
  import { resolve, join } from 'path'
13
17
  import { existsSync } from 'fs'
14
18
  import { readFile } from 'fs/promises'
15
19
  import yaml from 'js-yaml'
20
+ import {
21
+ isWorkspaceRoot,
22
+ findSites,
23
+ promptSelect,
24
+ } from '../utils/workspace.js'
16
25
 
17
26
  // Colors for terminal output
18
27
  const colors = {
@@ -41,6 +50,21 @@ function error(message) {
41
50
  console.error(`${colors.red}✗${colors.reset} ${message}`)
42
51
  }
43
52
 
53
+ /**
54
+ * Parse --target option from args
55
+ */
56
+ function parseTargetOption(args) {
57
+ for (let i = 0; i < args.length; i++) {
58
+ if (args[i] === '--target' || args[i] === '-t') {
59
+ return {
60
+ target: args[i + 1],
61
+ remainingArgs: [...args.slice(0, i), ...args.slice(i + 2)],
62
+ }
63
+ }
64
+ }
65
+ return { target: null, remainingArgs: args }
66
+ }
67
+
44
68
  /**
45
69
  * Main i18n command handler
46
70
  * @param {string[]} args - Command arguments
@@ -48,13 +72,21 @@ function error(message) {
48
72
  export async function i18n(args) {
49
73
  const subcommand = args[0]
50
74
 
51
- if (!subcommand || subcommand === '--help' || subcommand === '-h') {
75
+ if (subcommand === '--help' || subcommand === '-h') {
52
76
  showHelp()
53
77
  return
54
78
  }
55
79
 
80
+ // Parse --target option
81
+ const { target, remainingArgs } = parseTargetOption(args)
82
+
83
+ // Default to 'sync' if no subcommand (or if first arg is an option)
84
+ const firstArg = remainingArgs[0]
85
+ const effectiveSubcommand = !firstArg || firstArg.startsWith('-') ? 'sync' : firstArg
86
+ const effectiveArgs = !firstArg || firstArg.startsWith('-') ? remainingArgs : remainingArgs.slice(1)
87
+
56
88
  // Find site root
57
- const siteRoot = await findSiteRoot()
89
+ const siteRoot = await findSiteRoot(target)
58
90
  if (!siteRoot) {
59
91
  error('Could not find site root. Make sure you are in a Uniweb site directory.')
60
92
  process.exit(1)
@@ -63,18 +95,18 @@ export async function i18n(args) {
63
95
  // Load site config for locale settings
64
96
  const config = await loadSiteConfig(siteRoot)
65
97
 
66
- switch (subcommand) {
98
+ switch (effectiveSubcommand) {
67
99
  case 'extract':
68
- await runExtract(siteRoot, config, args.slice(1))
100
+ await runExtract(siteRoot, config, effectiveArgs)
69
101
  break
70
102
  case 'sync':
71
- await runSync(siteRoot, config, args.slice(1))
103
+ await runSync(siteRoot, config, effectiveArgs)
72
104
  break
73
105
  case 'status':
74
- await runStatus(siteRoot, config, args.slice(1))
106
+ await runStatus(siteRoot, config, effectiveArgs)
75
107
  break
76
108
  default:
77
- error(`Unknown subcommand: ${subcommand}`)
109
+ error(`Unknown subcommand: ${effectiveSubcommand}`)
78
110
  showHelp()
79
111
  process.exit(1)
80
112
  }
@@ -82,11 +114,52 @@ export async function i18n(args) {
82
114
 
83
115
  /**
84
116
  * Find site root by looking for site.yml
117
+ * Handles: explicit target, workspace root detection, or walking up directories
118
+ *
119
+ * @param {string|null} target - Explicit target path (from --target option)
85
120
  */
86
- async function findSiteRoot() {
87
- let dir = process.cwd()
121
+ async function findSiteRoot(target) {
122
+ const cwd = process.cwd()
123
+
124
+ // If explicit target specified, use it
125
+ if (target) {
126
+ const targetPath = resolve(cwd, target)
127
+ if (existsSync(join(targetPath, 'site.yml'))) {
128
+ return targetPath
129
+ }
130
+ error(`Target directory does not appear to be a site: ${target}`)
131
+ log(`${colors.dim}Sites have a site.yml file.${colors.reset}`)
132
+ process.exit(1)
133
+ }
134
+
135
+ // Check if we're at workspace root
136
+ if (isWorkspaceRoot(cwd)) {
137
+ const sites = await findSites(cwd)
138
+
139
+ if (sites.length === 0) {
140
+ error('No sites found in this workspace.')
141
+ log(`${colors.dim}Sites have @uniweb/runtime in dependencies.${colors.reset}`)
142
+ process.exit(1)
143
+ }
88
144
 
89
- // Check current directory and parents
145
+ let targetSite
146
+ if (sites.length === 1) {
147
+ targetSite = sites[0]
148
+ log(`${colors.cyan}→${colors.reset} Found site: ${targetSite}`)
149
+ } else {
150
+ log(`${colors.dim}Multiple sites found in workspace.${colors.reset}\n`)
151
+ targetSite = await promptSelect('Select site:', sites)
152
+ if (!targetSite) {
153
+ log('Cancelled.')
154
+ process.exit(0)
155
+ }
156
+ }
157
+
158
+ return resolve(cwd, targetSite)
159
+ }
160
+
161
+ // Walk up directories looking for site.yml
162
+ let dir = cwd
90
163
  for (let i = 0; i < 5; i++) {
91
164
  if (existsSync(join(dir, 'site.yml'))) {
92
165
  return dir
@@ -278,16 +351,18 @@ ${colors.cyan}${colors.bright}Uniweb i18n${colors.reset}
278
351
  Site content internationalization commands.
279
352
 
280
353
  ${colors.bright}Usage:${colors.reset}
281
- uniweb i18n <command> [options]
354
+ uniweb i18n [command] [options]
282
355
 
283
356
  ${colors.bright}Commands:${colors.reset}
357
+ (default) Same as sync - extract/update strings (runs if no command given)
284
358
  extract Extract translatable strings to locales/manifest.json
285
359
  sync Update manifest with content changes (detects moved/changed content)
286
360
  status Show translation coverage per locale
287
361
 
288
362
  ${colors.bright}Options:${colors.reset}
289
- --verbose Show detailed output
290
- --dry-run (sync) Show changes without writing files
363
+ -t, --target <path> Site directory (auto-detected if not specified)
364
+ --verbose Show detailed output
365
+ --dry-run (sync) Show changes without writing files
291
366
 
292
367
  ${colors.bright}Configuration:${colors.reset}
293
368
  Optional site.yml settings:
@@ -301,7 +376,7 @@ ${colors.bright}Configuration:${colors.reset}
301
376
 
302
377
  ${colors.bright}Workflow:${colors.reset}
303
378
  1. Build your site: uniweb build
304
- 2. Extract strings: uniweb i18n extract
379
+ 2. Extract strings: uniweb i18n
305
380
  3. Translate locale files: Edit locales/es.json, locales/fr.json, etc.
306
381
  4. Build with translations: uniweb build (generates locale-specific output)
307
382
 
@@ -319,6 +394,11 @@ ${colors.bright}Examples:${colors.reset}
319
394
  uniweb i18n sync --dry-run # Preview changes without writing
320
395
  uniweb i18n status # Show coverage for all locales
321
396
  uniweb i18n status es # Show coverage for Spanish only
397
+ uniweb i18n --target site # Specify site directory explicitly
398
+
399
+ ${colors.bright}Notes:${colors.reset}
400
+ Run from a site directory to operate on that site.
401
+ Run from workspace root to auto-detect sites (prompts if multiple).
322
402
  `)
323
403
  }
324
404
 
@@ -197,15 +197,16 @@ async function processFile(sourcePath, targetPath, data, options = {}) {
197
197
  * @param {Object} options - Processing options
198
198
  * @param {string|null} options.variant - Template variant to use
199
199
  * @param {string|null} options.basePath - Base template to merge with (files copied first)
200
+ * @param {boolean} options.isBase - Internal: true when processing base template (allows overwriting)
200
201
  * @param {Function} options.onWarning - Warning callback
201
202
  * @param {Function} options.onProgress - Progress callback
202
203
  */
203
204
  export async function copyTemplateDirectory(sourcePath, targetPath, data, options = {}) {
204
- const { variant = null, basePath = null, onWarning, onProgress } = options
205
+ const { variant = null, basePath = null, isBase = false, onWarning, onProgress } = options
205
206
 
206
- // If a base template is specified, copy it first (without the basePath option to avoid recursion)
207
+ // If a base template is specified, copy it first (with isBase=true so main can overwrite)
207
208
  if (basePath && existsSync(basePath)) {
208
- await copyTemplateDirectory(basePath, targetPath, data, { variant, onWarning, onProgress })
209
+ await copyTemplateDirectory(basePath, targetPath, data, { variant, isBase: true, onWarning, onProgress })
209
210
  }
210
211
 
211
212
  await fs.mkdir(targetPath, { recursive: true })
@@ -223,7 +224,7 @@ export async function copyTemplateDirectory(sourcePath, targetPath, data, option
223
224
  }
224
225
 
225
226
  // Options for recursive calls (without basePath to avoid re-copying base at each level)
226
- const recursionOptions = { variant, onWarning, onProgress }
227
+ const recursionOptions = { variant, isBase, onWarning, onProgress }
227
228
 
228
229
  for (const entry of entries) {
229
230
  const sourceName = entry.name
@@ -279,11 +280,9 @@ export async function copyTemplateDirectory(sourcePath, targetPath, data, option
279
280
  const sourceFullPath = path.join(sourcePath, sourceName)
280
281
  const targetFullPath = path.join(targetPath, targetName)
281
282
 
282
- // Skip if target already exists (don't overwrite)
283
- if (existsSync(targetFullPath)) {
284
- if (onWarning) {
285
- onWarning(`Skipping ${targetFullPath} - file already exists`)
286
- }
283
+ // When processing base template, skip if target exists (main template files take precedence)
284
+ // When processing main template, overwrite any files from base
285
+ if (isBase && existsSync(targetFullPath)) {
287
286
  continue
288
287
  }
289
288
 
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Workspace Detection Utilities
3
+ *
4
+ * Detects pnpm workspace structure and classifies packages as foundations or sites.
5
+ * Used by commands to auto-detect targets when run from workspace root.
6
+ */
7
+
8
+ import { existsSync, readdirSync } from 'node:fs'
9
+ import { readFile } from 'node:fs/promises'
10
+ import { resolve, dirname, join } from 'node:path'
11
+ import yaml from 'js-yaml'
12
+
13
+ /**
14
+ * Find workspace root by looking for pnpm-workspace.yaml
15
+ * @param {string} startDir - Directory to start searching from
16
+ * @returns {string|null} - Workspace root path or null
17
+ */
18
+ export function findWorkspaceRoot(startDir = process.cwd()) {
19
+ let dir = startDir
20
+ while (dir !== dirname(dir)) {
21
+ if (existsSync(join(dir, 'pnpm-workspace.yaml'))) {
22
+ return dir
23
+ }
24
+ dir = dirname(dir)
25
+ }
26
+ return null
27
+ }
28
+
29
+ /**
30
+ * Resolve workspace package patterns to actual directories
31
+ * Handles patterns like: "foundation", "site", "foundations/*", "sites/*"
32
+ *
33
+ * @param {string[]} patterns - Array of patterns from pnpm-workspace.yaml
34
+ * @param {string} workspaceRoot - Workspace root directory
35
+ * @returns {string[]} - Array of existing package directories (relative paths)
36
+ */
37
+ function resolvePatterns(patterns, workspaceRoot) {
38
+ const packages = []
39
+
40
+ for (const pattern of patterns) {
41
+ // Remove quotes if present
42
+ const cleanPattern = pattern.replace(/^["']|["']$/g, '')
43
+
44
+ if (cleanPattern.endsWith('/*')) {
45
+ // Glob pattern like "foundations/*" - list subdirectories
46
+ const baseDir = cleanPattern.slice(0, -2)
47
+ const fullPath = join(workspaceRoot, baseDir)
48
+
49
+ if (existsSync(fullPath)) {
50
+ try {
51
+ const entries = readdirSync(fullPath, { withFileTypes: true })
52
+ for (const entry of entries) {
53
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
54
+ packages.push(join(baseDir, entry.name))
55
+ }
56
+ }
57
+ } catch {
58
+ // Ignore read errors
59
+ }
60
+ }
61
+ } else {
62
+ // Direct path like "foundation" or "site"
63
+ const fullPath = join(workspaceRoot, cleanPattern)
64
+ if (existsSync(fullPath)) {
65
+ packages.push(cleanPattern)
66
+ }
67
+ }
68
+ }
69
+
70
+ return packages
71
+ }
72
+
73
+ /**
74
+ * Get workspace packages from pnpm-workspace.yaml
75
+ * @param {string} workspaceRoot
76
+ * @returns {Promise<string[]>} - Array of package directories (relative paths)
77
+ */
78
+ export async function getWorkspacePackages(workspaceRoot) {
79
+ const configPath = join(workspaceRoot, 'pnpm-workspace.yaml')
80
+ const content = await readFile(configPath, 'utf-8')
81
+ const config = yaml.load(content)
82
+
83
+ if (!config?.packages || !Array.isArray(config.packages)) {
84
+ return []
85
+ }
86
+
87
+ return resolvePatterns(config.packages, workspaceRoot)
88
+ }
89
+
90
+ /**
91
+ * Classify a package as foundation, site, or unknown
92
+ *
93
+ * Classification logic:
94
+ * - Site: has @uniweb/runtime in dependencies (checked first, more specific)
95
+ * - Foundation: has @uniweb/build in devDependencies but NOT @uniweb/runtime
96
+ *
97
+ * Note: Sites also have @uniweb/build for the Vite plugin, so we check
98
+ * for @uniweb/runtime first to distinguish them.
99
+ *
100
+ * @param {string} packagePath - Full path to package directory
101
+ * @returns {Promise<'foundation'|'site'|null>}
102
+ */
103
+ export async function classifyPackage(packagePath) {
104
+ const pkgJsonPath = join(packagePath, 'package.json')
105
+ if (!existsSync(pkgJsonPath)) return null
106
+
107
+ try {
108
+ const pkg = JSON.parse(await readFile(pkgJsonPath, 'utf-8'))
109
+
110
+ // Site: has @uniweb/runtime in dependencies (check first - more specific)
111
+ if (pkg.dependencies?.['@uniweb/runtime']) {
112
+ return 'site'
113
+ }
114
+ // Foundation: has @uniweb/build in devDependencies (and not a site)
115
+ if (pkg.devDependencies?.['@uniweb/build']) {
116
+ return 'foundation'
117
+ }
118
+ } catch {
119
+ // Ignore parse errors
120
+ }
121
+
122
+ return null
123
+ }
124
+
125
+ /**
126
+ * Find all foundations in workspace
127
+ * @param {string} workspaceRoot
128
+ * @returns {Promise<string[]>} - Array of foundation paths (relative to workspace root)
129
+ */
130
+ export async function findFoundations(workspaceRoot) {
131
+ const packages = await getWorkspacePackages(workspaceRoot)
132
+ const foundations = []
133
+
134
+ for (const pkg of packages) {
135
+ const fullPath = join(workspaceRoot, pkg)
136
+ if ((await classifyPackage(fullPath)) === 'foundation') {
137
+ foundations.push(pkg)
138
+ }
139
+ }
140
+
141
+ return foundations
142
+ }
143
+
144
+ /**
145
+ * Find all sites in workspace
146
+ * @param {string} workspaceRoot
147
+ * @returns {Promise<string[]>} - Array of site paths (relative to workspace root)
148
+ */
149
+ export async function findSites(workspaceRoot) {
150
+ const packages = await getWorkspacePackages(workspaceRoot)
151
+ const sites = []
152
+
153
+ for (const pkg of packages) {
154
+ const fullPath = join(workspaceRoot, pkg)
155
+ if ((await classifyPackage(fullPath)) === 'site') {
156
+ sites.push(pkg)
157
+ }
158
+ }
159
+
160
+ return sites
161
+ }
162
+
163
+ /**
164
+ * Check if current directory is the workspace root
165
+ * @param {string} dir - Directory to check
166
+ * @returns {boolean}
167
+ */
168
+ export function isWorkspaceRoot(dir = process.cwd()) {
169
+ return existsSync(join(dir, 'pnpm-workspace.yaml'))
170
+ }
171
+
172
+ /**
173
+ * Interactive prompt to select from multiple options
174
+ * @param {string} message - Prompt message
175
+ * @param {string[]} choices - Array of choices
176
+ * @returns {Promise<string|null>} - Selected choice or null if cancelled
177
+ */
178
+ export async function promptSelect(message, choices) {
179
+ const prompts = (await import('prompts')).default
180
+
181
+ const response = await prompts({
182
+ type: 'select',
183
+ name: 'value',
184
+ message,
185
+ choices: choices.map(c => ({ title: c, value: c })),
186
+ })
187
+
188
+ return response.value || null
189
+ }
@@ -9,7 +9,8 @@
9
9
  "preview": "pnpm --filter site preview"
10
10
  },
11
11
  "devDependencies": {
12
- "@types/node": "^22.0.0"
12
+ "@types/node": "^22.0.0",
13
+ "uniweb": "{{version "uniweb"}}"
13
14
  },
14
15
  "workspaces": [
15
16
  "site",
@@ -31,7 +31,6 @@
31
31
  "react": "^18.2.0",
32
32
  "react-dom": "^18.2.0",
33
33
  "tailwindcss": "^4.0.0",
34
- "uniweb": "{{version "uniweb"}}",
35
34
  "vite": "^7.0.0",
36
35
  "vite-plugin-svgr": "^4.2.0"
37
36
  }
@@ -10,4 +10,5 @@ export { Section }
10
10
 
11
11
  export const capabilities = null
12
12
 
13
+ // Per-component runtime metadata (from meta.js)
13
14
  export const meta = {}
@@ -1,6 +1,7 @@
1
1
  @import "tailwindcss";
2
2
 
3
3
  @source "./components/**/*.{js,jsx}";
4
+ @source "../node_modules/@uniweb/kit/src/**/*.{js,jsx}";
4
5
 
5
6
  @theme {
6
7
  --color-primary: #3b82f6;
@@ -11,7 +11,8 @@
11
11
  "preview": "pnpm --filter main preview"
12
12
  },
13
13
  "devDependencies": {
14
- "@types/node": "^22.0.0"
14
+ "@types/node": "^22.0.0",
15
+ "uniweb": "{{version "uniweb"}}"
15
16
  },
16
17
  "workspaces": [
17
18
  "site",
@@ -6,7 +6,7 @@
6
6
  "scripts": {
7
7
  "dev": "vite",
8
8
  "dev:runtime": "VITE_FOUNDATION_MODE=runtime vite",
9
- "build": "npx uniweb@latest build",
9
+ "build": "uniweb build",
10
10
  "preview": "vite preview"
11
11
  },
12
12
  "dependencies": {
@@ -1,4 +1,5 @@
1
1
  {
2
2
  "name": "Multi-Site Workspace",
3
- "description": "A Uniweb workspace supporting multiple sites and foundations. Use this when you need to manage several related sites or share foundations across projects."
3
+ "description": "A Uniweb workspace supporting multiple sites and foundations. Use this when you need to manage several related sites or share foundations across projects.",
4
+ "base": "_shared"
4
5
  }
@@ -31,7 +31,6 @@
31
31
  "react": "^18.2.0",
32
32
  "react-dom": "^18.2.0",
33
33
  "tailwindcss": "^4.0.0",
34
- "uniweb": "{{version "uniweb"}}",
35
34
  "vite": "^7.0.0",
36
35
  "vite-plugin-svgr": "^4.2.0"
37
36
  }
@@ -10,4 +10,5 @@ export { Section }
10
10
 
11
11
  export const capabilities = null
12
12
 
13
+ // Per-component runtime metadata (from meta.js)
13
14
  export const meta = {}
@@ -1,6 +1,7 @@
1
1
  @import "tailwindcss";
2
2
 
3
3
  @source "./components/**/*.{js,jsx}";
4
+ @source "../node_modules/@uniweb/kit/src/**/*.{js,jsx}";
4
5
 
5
6
  @theme {
6
7
  --color-primary: #3b82f6;
@@ -6,7 +6,7 @@
6
6
  "scripts": {
7
7
  "dev": "vite",
8
8
  "dev:runtime": "VITE_FOUNDATION_MODE=runtime vite",
9
- "build": "npx uniweb@latest build",
9
+ "build": "uniweb build",
10
10
  "preview": "vite preview"
11
11
  },
12
12
  "dependencies": {