uniweb 0.56.2 → 0.56.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "0.56.2",
3
+ "version": "0.56.3",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,15 +41,16 @@
41
41
  "js-yaml": "^4.1.0",
42
42
  "prompts": "^2.4.2",
43
43
  "tar": "^7.0.0",
44
- "@uniweb/core": "^0.29.3",
45
- "@uniweb/kit": "^0.19.1",
44
+ "@uniweb/core": "^0.29.4",
45
+ "@uniweb/kit": "^0.19.2",
46
+ "@uniweb/schemas": "^0.2.14",
46
47
  "@uniweb/semantic-parser": "^1.4.0",
47
- "@uniweb/runtime": "^0.26.3"
48
+ "@uniweb/runtime": "^0.26.4"
48
49
  },
49
50
  "peerDependencies": {
51
+ "@uniweb/build": "^0.52.2",
50
52
  "@uniweb/content-reader": "^1.2.5",
51
- "@uniweb/semantic-parser": "^1.4.0",
52
- "@uniweb/build": "^0.52.1"
53
+ "@uniweb/semantic-parser": "^1.4.0"
53
54
  },
54
55
  "peerDependenciesMeta": {
55
56
  "@uniweb/build": {
@@ -236,10 +236,32 @@ cat <foundation>/sections/Hero/meta.js # what one expects and accepts
236
236
 
237
237
  > **A site-relative URL (`/effects/entry.js`) only works where the site serves its own files** — `uniweb export` and `uniweb deploy --host=<adapter>`. A site published to Uniweb hosting ships no JS, so nothing serves that path, and `uniweb publish` rejects it with a pointer to the catalog-ref form. Register the extension (`uniweb register` in its directory) and reference it like any other foundation. When you reference a workspace-local extension, `uniweb publish` brings it along exactly as it does the primary — releasing it if its code changed, and pinning the released version on the published site.
238
238
 
239
- Each `meta.js` is a catalog entry: `description` (what the type is for), `content:` (what markdown it expects), `params:` (what frontmatter it accepts, with defaults), `presets:` (named param bundles). Read them as a menu — that is what they are. There is no CLI command that lists them; reading the folder *is* the discovery step.
239
+ Each `meta.js` is a catalog entry: `description` (what the type is for), `content:` (what markdown it expects), `params:` (what frontmatter it accepts, with defaults), `presets:` (named param bundles), and optionally `family:` (the standard section family it belongs to). Read them as a menu — that is what they are. There is no CLI command that lists them; reading the folder *is* the discovery step.
240
240
 
241
241
  > **Never write a `type:` or a param you haven't confirmed exists.** Both failures are silent (Part 0) — invisible from the terminal, visible only on the page.
242
242
 
243
+ #### `family:` — so an editor can draw your section
244
+
245
+ An editor showing an author a section picker has to know what each section type
246
+ **is**, to pick its illustration and a label it can translate. Two ways to tell
247
+ it, in this order:
248
+
249
+ 1. **Name the component for its family.** Run `uniweb families`, and use one of
250
+ those names when it fits — `Hero`, `Footer`, `Pricing`, `FAQ`, `CardGrid`.
251
+ Then there is nothing to declare: the name resolves on its own.
252
+ 2. **Declare `family:`** when the name is yours — a variant, or your own
253
+ vocabulary:
254
+ ```js
255
+ // sections/ProfileHero/meta.js
256
+ export default { title: 'Researcher Profile', family: 'profile' }
257
+ ```
258
+ One value, the dominant shape: a `HeroWithEstimate` is `family: 'hero'`.
259
+
260
+ **Leave it off when nothing fits.** A section that is genuinely specific to this
261
+ site gets a generic illustration, and that is the right outcome — do not force it
262
+ into the nearest family. `uniweb doctor` shows what each of your sections
263
+ resolved to, and suggests a family for any it recognises by another name.
264
+
243
265
  ### 3. Find your lane
244
266
 
245
267
  The architecture exists to keep content and code separate. Your task sits in one of them. Decide before you edit, then stay there.
@@ -175,8 +175,12 @@ Component metadata in <foundation>/components/[Name]/meta.js (or sections/[Name]
175
175
  ${colors.bright}Identity:${colors.reset}
176
176
  ${colors.cyan}title${colors.reset} Display name in editor
177
177
  ${colors.cyan}description${colors.reset} What the component does
178
- ${colors.cyan}category${colors.reset} Grouping: "impact", "showcase", "structure"
179
- ${colors.cyan}purpose${colors.reset} Single verb: Introduce, Express, Explain
178
+ ${colors.cyan}family${colors.reset} The standard section family this is — "hero", "pricing",
179
+ "faq". Picks the editor's illustration and a translated
180
+ label. Only needed when the component NAME is not already
181
+ one: \`Hero\` resolves on its own, \`ProfileHero\` does not.
182
+ An unrecognized value is legal and falls back.
183
+ ${colors.dim}uniweb families${colors.reset}
180
184
  ${colors.cyan}hidden${colors.reset} If true, not selectable in frontmatter
181
185
 
182
186
  ${colors.bright}Content Expectations:${colors.reset}
@@ -202,7 +206,7 @@ ${colors.bright}Example:${colors.reset}
202
206
  ${colors.dim}export default {
203
207
  title: 'Hero Banner',
204
208
  description: 'Bold hero section with headline and CTA',
205
- category: 'impact',
209
+ family: 'hero',
206
210
  background: true,
207
211
 
208
212
  content: {
@@ -27,6 +27,8 @@ import { discoverFoundations, discoverSites } from '../utils/discover.js'
27
27
  import { checkSiteInstall } from '../utils/install-integrity.js'
28
28
  import { findWorkspaceRoot } from '../utils/workspace.js'
29
29
  import { DATA_DIR } from '@uniweb/core/data-paths'
30
+ import { FAMILIES, resolveFamily } from '@uniweb/schemas/families'
31
+ import { suggestFamily } from '@uniweb/schemas/family-aliases'
30
32
 
31
33
  /**
32
34
  * Parse the `--fix [<issue-id>]` flag.
@@ -99,10 +101,21 @@ function loadFoundationJs(dir) {
99
101
  }
100
102
 
101
103
  /**
102
- * Load built schema.json from a directory
104
+ * Load a foundation's built schema.json.
105
+ *
106
+ * ⛔ THE PATH IS `dist/meta/schema.json`, and it read `dist/schema.json` from
107
+ * 2026-02-06 until 2026-09-15 — a file the build has never written. So this
108
+ * returned `null` every time, and its one caller (the extension `vars`/layouts
109
+ * warning, which `framework/CLAUDE.md` gotcha #18 says doctor performs) could
110
+ * not fire at all.
111
+ *
112
+ * ⭐ THE CLASS IS WORTH MORE THAN THE BUG: a wrong PATH returns empty, it does
113
+ * not throw. `existsSync` said no, the function returned `null`, every caller
114
+ * used `?.` and skipped — and a check that never fires looks exactly like a
115
+ * codebase with nothing to report.
103
116
  */
104
117
  function loadSchemaJson(dir) {
105
- const schemaPath = join(dir, 'dist', 'schema.json')
118
+ const schemaPath = join(dir, 'dist', 'meta', 'schema.json')
106
119
  if (!existsSync(schemaPath)) return null
107
120
  try {
108
121
  return JSON.parse(readFileSync(schemaPath, 'utf8'))
@@ -498,6 +511,175 @@ export async function checkFoundationSupports({
498
511
  log(` ${colors.dim}uniweb doctor --fix ${id}${colors.reset}`)
499
512
  }
500
513
 
514
+ /**
515
+ * Every section type's standard family — what it resolved to, and HOW.
516
+ *
517
+ * ⭐ THIS IS WHERE THE TAXONOMY IS ACTUALLY LEARNED. Nobody reads a word list in
518
+ * advance; they read the warning that names their own component and proposes the
519
+ * edit. A foundation developer sees their own picker here before an author does.
520
+ *
521
+ * ⛔ AN UNRECOGNIZED NAME IS NOT AN ERROR, and this check must never imply it is.
522
+ * A foundation with its own vocabulary is a supported choice: the section falls
523
+ * back to a generic illustration and nothing breaks. What is reported is the
524
+ * OPPORTUNITY — and only where there is a concrete suggestion to act on.
525
+ *
526
+ * ⚠️ READS THE BUILT `schema.json`, so it says nothing before a build. That is
527
+ * deliberate: schema.json is what a picker actually receives, and inferring from
528
+ * unbuilt source would report on a state no consumer sees.
529
+ */
530
+ export function checkSectionFamilies({
531
+ foundationName,
532
+ folderName,
533
+ foundationDir,
534
+ issues,
535
+ shouldFixExplicitly,
536
+ fixed,
537
+ }) {
538
+ const schema = loadSchemaJson(foundationDir)
539
+ if (!schema) return
540
+
541
+ const familyIds = FAMILIES.map((f) => f.id)
542
+ const sections = Object.entries(schema)
543
+ .filter(([key, value]) => !key.startsWith('_') && key !== 'dataSchemas' && value && typeof value === 'object')
544
+ .map(([name, entry]) => ({ name, entry, ...resolveFamily({ name, family: entry.family }) }))
545
+
546
+ if (sections.length === 0) return
547
+
548
+ const declared = sections.filter((s) => s.source === 'declared')
549
+ const byName = sections.filter((s) => s.source === 'name')
550
+ const misses = sections.filter((s) => !s.id)
551
+
552
+ info(
553
+ `${folderName}: ${sections.length - misses.length}/${sections.length} sections have a standard family ` +
554
+ `${colors.dim}(${declared.length} declared, ${byName.length} from the name)${colors.reset}`
555
+ )
556
+
557
+ // ── a `family:` that names nothing we know ────────────────────────────────
558
+ const bogus = misses.filter((s) => s.unknown)
559
+ for (const s of bogus) {
560
+ const id = 'section-family-unknown'
561
+ const near = suggestFamily(s.unknown, familyIds)
562
+ issues.push({
563
+ id,
564
+ type: 'warning',
565
+ foundation: foundationName,
566
+ message: `${s.name} declares family '${s.unknown}', which is not a standard family`,
567
+ })
568
+ warn(`[${id}] ${folderName}/${s.name}: family '${s.unknown}' is not a standard family`)
569
+ if (near.id) log(` Did you mean ${colors.blue}${near.id}${colors.reset}?`)
570
+ log(` ${colors.dim}An unrecognized value is legal — it falls back to a generic illustration.${colors.reset}`)
571
+ log(` ${colors.dim}uniweb families${colors.reset}`)
572
+ }
573
+
574
+ // ── a name we do not know, WITH something to suggest ──────────────────────
575
+ const undeclared = misses.filter((s) => !s.unknown)
576
+ const proposals = undeclared
577
+ .map((s) => ({ ...s, suggestion: suggestFamily(s.name, familyIds) }))
578
+ .filter((s) => s.suggestion.id || s.suggestion.ambiguous)
579
+ if (proposals.length === 0) return
580
+
581
+ const id = 'section-family-unrecognized'
582
+ const writable = proposals.filter((p) => p.suggestion.fixable && p.suggestion.id)
583
+
584
+ // ⛔ EXPLICIT ID ONLY — never a bare `--fix`. Every id below is a GUESS from a
585
+ // table that exists here, rather than in the resolver, precisely so a person
586
+ // approves it. A bare `--fix` writing them would put the silent-wrong-guess
587
+ // failure back, one directory deeper.
588
+ if (shouldFixExplicitly?.(id) && writable.length > 0) {
589
+ const srcDir = resolveFoundationSrcPath(foundationDir)
590
+ for (const p of writable) {
591
+ const metaPath = join(srcDir, p.entry.path || '', 'meta.js')
592
+ const written = writeFamilyIntoMeta(metaPath, p.suggestion.id)
593
+ if (written) fixed?.(`${folderName}/${p.entry.path}/meta.js now declares family: '${p.suggestion.id}'`)
594
+ else warn(`[${id}] could not write ${p.name}'s meta.js — add \`family: '${p.suggestion.id}'\` by hand`)
595
+ }
596
+ return
597
+ }
598
+
599
+ issues.push({
600
+ id,
601
+ type: 'info',
602
+ foundation: foundationName,
603
+ message: `${proposals.length} section(s) in ${foundationName} could declare a standard family`,
604
+ details: proposals.map((p) => ({ name: p.name, suggested: p.suggestion.id, via: p.suggestion.via })),
605
+ fixable: writable.length > 0,
606
+ })
607
+
608
+ warn(`[${id}] ${folderName}: ${proposals.length} section(s) have no standard family`)
609
+ const width = Math.max(...proposals.map((p) => p.name.length))
610
+ for (const p of proposals) {
611
+ const { id: hit, via, ambiguous } = p.suggestion
612
+ if (ambiguous) {
613
+ log(` ${p.name.padEnd(width)} ${colors.dim}ambiguous — ${ambiguous}${colors.reset}`)
614
+ } else {
615
+ const how = via === 'near' ? 'possible typo' : via === 'suffix' ? 'name + shape suffix' : 'known alias'
616
+ log(` ${p.name.padEnd(width)} → ${colors.blue}${hit}${colors.reset} ${colors.dim}(${how})${colors.reset}`)
617
+ }
618
+ }
619
+ log(` ${colors.dim}Nothing is broken — an unrecognized name falls back to a generic illustration.${colors.reset}`)
620
+ if (writable.length > 0) {
621
+ log(` ${colors.dim}uniweb doctor --fix ${id}${colors.reset} ${colors.dim}— writes ${writable.length} \`family:\` line(s). Read the diff: these are guesses.${colors.reset}`)
622
+ }
623
+ }
624
+
625
+ /**
626
+ * Insert `family: '<id>'` as the first key of a meta.js default export.
627
+ *
628
+ * ⛔ REFUSES ANYTHING IT DOES NOT RECOGNIZE. A meta.js is hand-written source,
629
+ * and a half-understood rewrite of someone's file is far worse than telling them
630
+ * to add one line. A `false` return is reported as "could not write", never as
631
+ * fixed.
632
+ */
633
+ function writeFamilyIntoMeta(metaPath, familyId) {
634
+ if (!existsSync(metaPath)) return false
635
+ try {
636
+ const src = readFileSync(metaPath, 'utf8')
637
+ if (/^\s*family\s*:/m.test(src)) return false // already has one — never overwrite
638
+ const m = src.match(/export\s+default\s*\{[ \t]*\r?\n/)
639
+ if (!m) return false
640
+ const at = m.index + m[0].length
641
+ const indent = src.slice(at).match(/^([ \t]+)/)?.[1] ?? ' '
642
+ writeFileSync(metaPath, `${src.slice(0, at)}${indent}family: '${familyId}',\n${src.slice(at)}`)
643
+ return true
644
+ } catch {
645
+ return false
646
+ }
647
+ }
648
+
649
+ /**
650
+ * `category:` and `purpose:` were retired when `family:` landed.
651
+ *
652
+ * ⚖️ INFO, NOT A WARNING. Both are inert — nothing in the build, the runtime or
653
+ * the wire has ever read either — so a foundation carrying one is not broken and
654
+ * does not need to hurry. It is worth saying once, because a developer who sees
655
+ * `category:` in a neighbour's meta.js will reasonably copy it.
656
+ */
657
+ export function checkRetiredMetaKeys({ foundationName, folderName, foundationDir, issues }) {
658
+ const schema = loadSchemaJson(foundationDir)
659
+ if (!schema) return
660
+ const stale = Object.entries(schema)
661
+ .filter(([key, v]) => !key.startsWith('_') && key !== 'dataSchemas' && v && typeof v === 'object')
662
+ .filter(([, v]) => v.category !== undefined || v.purpose !== undefined)
663
+ if (stale.length === 0) return
664
+
665
+ const id = 'section-retired-meta-key'
666
+ const keys = [...new Set(stale.flatMap(([, v]) => [
667
+ v.category !== undefined ? 'category' : null,
668
+ v.purpose !== undefined ? 'purpose' : null,
669
+ ].filter(Boolean)))]
670
+ issues.push({
671
+ id,
672
+ type: 'info',
673
+ foundation: foundationName,
674
+ message: `${stale.length} section(s) still declare ${keys.join(' / ')}`,
675
+ })
676
+ info(
677
+ `[${id}] ${folderName}: ${stale.length} section(s) still declare ` +
678
+ `${colors.dim}${keys.join(' / ')}${colors.reset} — retired, and read by nothing`
679
+ )
680
+ log(` ${colors.dim}\`family:\` replaced them. Removing them changes no behaviour.${colors.reset}`)
681
+ }
682
+
501
683
  /**
502
684
  * The gates kit exposes for a host service, and the field that IS the gate.
503
685
  *
@@ -888,6 +1070,11 @@ export async function doctor(args = []) {
888
1070
  // each diagnostic below so the rewrite happens with full context.
889
1071
  const fixFlag = parseFixFlag(args)
890
1072
  const shouldFix = (id) => fixFlag === 'all' || fixFlag === id
1073
+ // ⛔ SOME FIXES MUST NOT RIDE A BARE `--fix`. A fix derived from a GUESS — the
1074
+ // family aliases, which exist in the CLI precisely so a human approves each
1075
+ // one — becomes a silent write the moment `--fix` alone applies it, which is
1076
+ // the failure the guess was moved here to avoid. Those checks ask for the id.
1077
+ const shouldFixExplicitly = (id) => fixFlag === id
891
1078
  const fixed = (msg) =>
892
1079
  console.log(` ${colors.green}↳ Fixed:${colors.reset} ${msg}`)
893
1080
 
@@ -1054,6 +1241,26 @@ export async function doctor(args = []) {
1054
1241
  log(` ${colors.dim}@import "@uniweb/kit/prose-tokens.css";${colors.reset}`)
1055
1242
  }
1056
1243
 
1244
+ // ⛔ SAY WHEN A CHECK DID NOT RUN. Every foundation check below reads the
1245
+ // BUILT `dist/meta/schema.json` — what a consumer actually receives — and
1246
+ // returns silently when there is none. In a workspace with a site, "Foundation
1247
+ // not built yet" turns up later under that site; in a FOUNDATION-ONLY
1248
+ // workspace, which is the normal shape when a foundation is the product, it
1249
+ // never does. The report then looks clean because nothing ran.
1250
+ //
1251
+ // That is the same class as the `dist/schema.json` path bug (a check that
1252
+ // never fires is indistinguishable from a codebase with nothing to report),
1253
+ // so it is named here rather than left to be rediscovered.
1254
+ for (const f of foundations) {
1255
+ if (!loadSchemaJson(f.path)) {
1256
+ info(
1257
+ `${f.folderName}: not built, so nothing was checked here ` +
1258
+ `${colors.dim}(services, section families, retired keys)${colors.reset}`
1259
+ )
1260
+ log(` ${colors.dim}Run \`uniweb build\` in ${f.folderName}, then doctor again.${colors.reset}`)
1261
+ }
1262
+ }
1263
+
1057
1264
  // ── uniweb.supports — the services this foundation says it is built against ──
1058
1265
  for (const f of foundations) {
1059
1266
  await checkFoundationSupports({
@@ -1071,6 +1278,20 @@ export async function doctor(args = []) {
1071
1278
  srcDir: resolveFoundationSrcPath(f.path),
1072
1279
  issues
1073
1280
  })
1281
+ checkSectionFamilies({
1282
+ foundationName: f.name,
1283
+ folderName: f.folderName,
1284
+ foundationDir: f.path,
1285
+ issues,
1286
+ shouldFixExplicitly,
1287
+ fixed
1288
+ })
1289
+ checkRetiredMetaKeys({
1290
+ foundationName: f.name,
1291
+ folderName: f.folderName,
1292
+ foundationDir: f.path,
1293
+ issues
1294
+ })
1074
1295
  }
1075
1296
 
1076
1297
  // Extensions carry `uniweb.supports` too — same key, same field, and a site's
@@ -1093,6 +1314,22 @@ export async function doctor(args = []) {
1093
1314
  srcDir: resolveFoundationSrcPath(e.path),
1094
1315
  issues
1095
1316
  })
1317
+ // An extension contributes section types by URL and lands in the same
1318
+ // picker, so its sections need a family for exactly the same reason.
1319
+ checkSectionFamilies({
1320
+ foundationName: e.name,
1321
+ folderName: e.folderName,
1322
+ foundationDir: e.path,
1323
+ issues,
1324
+ shouldFixExplicitly,
1325
+ fixed
1326
+ })
1327
+ checkRetiredMetaKeys({
1328
+ foundationName: e.name,
1329
+ folderName: e.folderName,
1330
+ foundationDir: e.path,
1331
+ issues
1332
+ })
1096
1333
  }
1097
1334
 
1098
1335
  if (extensions.length > 0) {
@@ -0,0 +1,82 @@
1
+ /**
2
+ * `uniweb families` — the standard section families this CLI knows.
3
+ *
4
+ * ⭐ NAMED AFTER THE FIELD, not after the things it lists. A developer's question
5
+ * is "what can I put in `family:`?", and they will type the word they just read
6
+ * in a `meta.js` or a doctor warning.
7
+ *
8
+ * ⚠️ IT REPORTS THE ROSTER THIS CLI SHIPS WITH, not a live fetch — the list comes
9
+ * from the `@uniweb/schemas` version this CLI resolved. That is the right answer,
10
+ * because it is also the list `uniweb doctor` matched against. A newer family
11
+ * than your CLI will not appear, and declaring one is not fatal either way: an
12
+ * unrecognized value falls back to a generic illustration.
13
+ */
14
+
15
+ import { FAMILIES, GROUPS } from '@uniweb/schemas/families'
16
+ import { getCliVersion } from '../versions.js'
17
+
18
+ const colors = {
19
+ reset: '\x1b[0m',
20
+ bright: '\x1b[1m',
21
+ dim: '\x1b[2m',
22
+ cyan: '\x1b[36m'
23
+ }
24
+
25
+ export async function families(args = []) {
26
+ if (args.includes('--json')) {
27
+ // ⛔ stdout carries JSON and nothing else, so it pipes. Every human-facing
28
+ // line in this command goes to stdout only in the non-JSON branch.
29
+ process.stdout.write(
30
+ JSON.stringify(
31
+ { cliVersion: getCliVersion(), count: FAMILIES.length, groups: GROUPS, families: FAMILIES },
32
+ null,
33
+ 2
34
+ ) + '\n'
35
+ )
36
+ return
37
+ }
38
+
39
+ const filter = args.find((a) => !a.startsWith('-'))
40
+ const shown = filter
41
+ ? FAMILIES.filter(
42
+ (f) =>
43
+ f.id.includes(filter.toLowerCase()) ||
44
+ f.label.toLowerCase().includes(filter.toLowerCase()) ||
45
+ f.group === filter.toLowerCase()
46
+ )
47
+ : FAMILIES
48
+
49
+ console.log('')
50
+ console.log(
51
+ `${colors.bright}Section families${colors.reset} ${colors.dim}(uniweb ${getCliVersion()})${colors.reset}`
52
+ )
53
+
54
+ if (shown.length === 0) {
55
+ console.log('')
56
+ console.log(` ${colors.dim}Nothing matches "${filter}".${colors.reset}`)
57
+ console.log(
58
+ ` ${colors.dim}A name of your own is fine — it falls back to a generic illustration.${colors.reset}`
59
+ )
60
+ console.log('')
61
+ return
62
+ }
63
+
64
+ const width = Math.max(...shown.map((f) => f.id.length))
65
+ for (const group of GROUPS) {
66
+ const rows = shown.filter((f) => f.group === group.id)
67
+ if (rows.length === 0) continue
68
+ console.log('')
69
+ console.log(` ${colors.bright}${group.label}${colors.reset} ${colors.dim}— ${group.move}${colors.reset}`)
70
+ for (const f of rows) {
71
+ console.log(` ${colors.cyan}${f.id.padEnd(width)}${colors.reset} ${colors.dim}${f.label}${colors.reset}`)
72
+ }
73
+ }
74
+
75
+ console.log('')
76
+ console.log(` ${colors.dim}Declare one in a section's meta.js: family: 'hero'${colors.reset}`)
77
+ console.log(
78
+ ` ${colors.dim}A component already named for its family needs no declaration.${colors.reset}`
79
+ )
80
+ console.log(` ${colors.dim}--json for scripts. uniweb doctor reports what your sections resolved to.${colors.reset}`)
81
+ console.log('')
82
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-09-15T03:16:10.815Z",
3
+ "generatedAt": "2026-09-16T03:38:43.897Z",
4
4
  "packages": {
5
5
  "@uniweb/api": {
6
6
  "version": "0.3.6",
@@ -10,7 +10,7 @@
10
10
  ]
11
11
  },
12
12
  "@uniweb/build": {
13
- "version": "0.52.1",
13
+ "version": "0.52.2",
14
14
  "path": "framework/build",
15
15
  "deps": [
16
16
  "@uniweb/content-reader",
@@ -34,7 +34,7 @@
34
34
  "deps": []
35
35
  },
36
36
  "@uniweb/core": {
37
- "version": "0.29.3",
37
+ "version": "0.29.4",
38
38
  "path": "framework/core",
39
39
  "deps": [
40
40
  "@uniweb/semantic-parser",
@@ -54,7 +54,7 @@
54
54
  ]
55
55
  },
56
56
  "@uniweb/kit": {
57
- "version": "0.19.1",
57
+ "version": "0.19.2",
58
58
  "path": "framework/kit",
59
59
  "deps": [
60
60
  "@uniweb/core",
@@ -82,7 +82,7 @@
82
82
  ]
83
83
  },
84
84
  "@uniweb/runtime": {
85
- "version": "0.26.3",
85
+ "version": "0.26.4",
86
86
  "path": "framework/runtime",
87
87
  "deps": [
88
88
  "@uniweb/core",
@@ -95,7 +95,7 @@
95
95
  "deps": []
96
96
  },
97
97
  "@uniweb/schemas": {
98
- "version": "0.2.13",
98
+ "version": "0.2.14",
99
99
  "path": "framework/schemas",
100
100
  "deps": []
101
101
  },
@@ -110,7 +110,7 @@
110
110
  "deps": []
111
111
  },
112
112
  "@uniweb/templates": {
113
- "version": "0.14.3",
113
+ "version": "0.14.4",
114
114
  "path": "framework/templates",
115
115
  "deps": []
116
116
  },
package/src/index.js CHANGED
@@ -956,6 +956,13 @@ async function main() {
956
956
  return
957
957
  }
958
958
 
959
+ // Handle families command — the standard section families this CLI ships with
960
+ if (command === 'families') {
961
+ const { families } = await import('./commands/families.js')
962
+ await families(args.slice(1))
963
+ return
964
+ }
965
+
959
966
  // Handle template command
960
967
  if (command === 'template') {
961
968
  await template(args.slice(1))
@@ -1705,6 +1712,33 @@ ${colors.cyan}${colors.bright}uniweb handoff${colors.reset} ${colors.dim}— (re
1705
1712
 
1706
1713
  The site-handoff flow was retired with the legacy backend.
1707
1714
  Manage client sites from the Uniweb app for now.
1715
+ `,
1716
+ families: `
1717
+ ${colors.cyan}${colors.bright}uniweb families${colors.reset} ${colors.dim}— The standard section families${colors.reset}
1718
+
1719
+ ${colors.bright}Usage:${colors.reset}
1720
+ uniweb families List them, grouped
1721
+ uniweb families <filter> Match an id, a label or a group name
1722
+ uniweb families --json Machine-readable (for scripts)
1723
+
1724
+ ${colors.bright}What a family is for:${colors.reset}
1725
+ It tells an editor which illustration to show for a section type, and gives
1726
+ it a label it can translate. Declare it in a section's meta.js:
1727
+
1728
+ ${colors.dim}export default { title: 'Researcher Profile', family: 'profile' }${colors.reset}
1729
+
1730
+ ${colors.bright}Most components need no declaration.${colors.reset} The section type IS the component
1731
+ name, so \`Hero\`, \`Footer\` and \`Pricing\` already resolve. \`family:\` is for the
1732
+ ones whose name does not give it away — \`ProfileHero\`, \`CvEntry\`.
1733
+
1734
+ An unrecognized value is legal: it falls back to a generic illustration and
1735
+ nothing breaks, so a foundation with its own vocabulary is never blocked.
1736
+
1737
+ \`uniweb doctor\` reports what each of your sections resolved to, and suggests
1738
+ a family for the ones it recognizes by another name.
1739
+
1740
+ ${colors.bright}Note:${colors.reset} this is the roster ${colors.bright}this CLI ships with${colors.reset}, not a live fetch — and it is
1741
+ the same list \`doctor\` matches against.
1708
1742
  `,
1709
1743
  template: `
1710
1744
  ${colors.cyan}${colors.bright}uniweb template${colors.reset} ${colors.dim}— Official templates${colors.reset}
@@ -1835,6 +1869,7 @@ ${colors.bright}Commands:${colors.reset}
1835
1869
  status Show a site's sync state (unpushed content, foundation)
1836
1870
  inspect <path> Inspect parsed content shape of a markdown file or folder
1837
1871
  docs Generate component documentation
1872
+ families List the standard section families (for meta.js family:)
1838
1873
  doctor Diagnose project configuration issues
1839
1874
  validate Check your content against your foundation's data schemas
1840
1875
  update Align workspace deps + AGENTS.md to the running CLI
@@ -1,8 +1,6 @@
1
1
  export default {
2
2
  title: 'Section',
3
3
  description: 'A versatile content section for headings, text, and links',
4
- category: 'content',
5
- purpose: 'Inform',
6
4
 
7
5
  content: {
8
6
  pretitle: 'Eyebrow text',