uniweb 0.79.2 → 0.80.0

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.79.2",
3
+ "version": "0.80.0",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,17 +41,17 @@
41
41
  "js-yaml": "^4.1.0",
42
42
  "prompts": "^2.4.2",
43
43
  "tar": "^7.0.0",
44
- "@uniweb/core": "^0.35.0",
45
- "@uniweb/schemas": "^0.9.3",
46
- "@uniweb/kit": "^0.19.11",
47
- "@uniweb/runtime": "^0.28.3",
44
+ "@uniweb/runtime": "^0.28.4",
48
45
  "@uniweb/content-writer": "^0.3.4",
49
- "@uniweb/semantic-parser": "^1.4.1"
46
+ "@uniweb/core": "^0.35.0",
47
+ "@uniweb/kit": "^0.19.13",
48
+ "@uniweb/schemas": "^0.10.0",
49
+ "@uniweb/semantic-parser": "^1.5.1"
50
50
  },
51
51
  "peerDependencies": {
52
- "@uniweb/build": "^0.73.2",
52
+ "@uniweb/build": "^0.74.0",
53
53
  "@uniweb/content-reader": "^1.2.7",
54
- "@uniweb/semantic-parser": "^1.4.1"
54
+ "@uniweb/semantic-parser": "^1.5.1"
55
55
  },
56
56
  "peerDependenciesMeta": {
57
57
  "@uniweb/build": {
@@ -366,6 +366,7 @@ content = {
366
366
  images: [], // { src, alt, role, href }
367
367
  icons: [], // { library, name, role }
368
368
  videos: [], // { src, alt, role, poster, href }
369
+ documents: [], // { url, alt, caption, role: 'pdf', preview?, author?, description? } — from {role=pdf}
369
370
  insets: [], // Inline @Component references — { refId }
370
371
  lists: [], // [[{ paragraphs, links, lists, … }]] — each item is an object, not a string
371
372
  quotes: [], // Blockquotes
@@ -524,12 +525,13 @@ Optional attributes: `{size=20}`, `{color=red}`. Custom SVGs: `![Logo](./logo.sv
524
525
  ```markdown
525
526
  [text](url){target=_blank} <!-- Open in new tab -->
526
527
  [text](./file.pdf){download} <!-- Download -->
527
- ![alt](./img.jpg){role=banner} <!-- Role determines array: images, icons, or videos -->
528
+ ![alt](./img.jpg){role=banner} <!-- Role determines array: images, icons, videos or documents -->
528
529
  ![alt](./demo.mp4){role=video} <!-- → content.videos[], not content.images[] -->
530
+ ![alt](./report.pdf){role=pdf} <!-- → content.documents[] — a file, drawn as a link -->
529
531
  ![alt](./img.jpg){href=/products} <!-- Clickable media: href + target ride along -->
530
532
  ```
531
533
 
532
- Sizing and loading ride on the image: `{width=800 height=600 loading=lazy fit=cover position=center}`. A video adds `{poster=./thumb.jpg autoplay muted loop controls}`; a `{role=pdf}` document adds `{preview=./cover.jpg author=… description=…}`.
534
+ Sizing and loading ride on the image: `{width=800 height=600 loading=lazy fit=cover position=center}`. A video adds `{poster=./thumb.jpg autoplay muted loop controls}`; a `{role=pdf}` document adds `{preview=./cover.jpg author=… description=…}` and arrives in `content.documents` — `{ url, name, mime, size?, alt, caption, … }` — declare it in `content:` as `documents`.
533
535
 
534
536
  **Quote values containing spaces:** `{note="Ready to go"}`, not `{note=Ready to go}` — unquoted values end at the first space.
535
537
 
@@ -1087,7 +1089,7 @@ Names only — for signatures and props, read the package: it's on disk at `node
1087
1089
 
1088
1090
  **Text:** `H1`–`H6`, `P`, `Span`, `Div`, `Text` (with `as`)
1089
1091
  **Content:** `Section`, `Prose`, `Article`, `Render` (ProseMirror → React), `ChildBlocks`, `splitContent()`
1090
- **Media:** `Visual` (first non-empty: inset/video/image), `Image`, `Media`, `Icon`, `Asset`
1092
+ **Media:** `Visual` (a `content.media` item by its kind; or the first of inset/video/image), `Image`, `Media` (video), `Icon`, `Asset`
1091
1093
  **Navigation:** `Link`, `useActiveRoute()`, `useWebsite()`, `useRouting()`
1092
1094
  **Header/layout:** `useScrolled(threshold)`, `useMobileMenu()`, `useAppearance()`
1093
1095
  **Overlays:** `Overlay` (modals, palettes, drawers, toasts — portals out of the layout, contains focus; read the note below before hand-rolling one)
@@ -1098,7 +1100,7 @@ Names only — for signatures and props, read the package: it's on disk at `node
1098
1100
  @import "@uniweb/kit/prose-tokens.css"; /* the plugin, wired to theme.yml */
1099
1101
  ```
1100
1102
 
1101
- Nothing to install — the import brings the plugin with it. **Skip the import and prose is completely unstyled**, silently: the class is there and no rules are behind it. `uniweb doctor` flags that. Use **one** `prose` container per subtree — the `--tw-prose-*` variables are inherited, so a nested one silently resets them and the outer container goes on looking correct. The section that renders the document is usually the better owner; a layout should supply column width and padding.
1103
+ Nothing to install — the import brings the plugin with it. **Skip the import and prose is completely unstyled** — lists lose their bullets and numbers, and text its typography — while the markup looks right. The foundation build warns about it, and `uniweb doctor` flags it. Use **one** `prose` container per subtree — the `--tw-prose-*` variables are inherited, so a nested one silently resets them and the outer container goes on looking correct. The section that renders the document is usually the better owner; a layout should supply column width and padding.
1102
1104
 
1103
1105
  **Documentation shells:** `useHeadings()` (the page's headings + the one being read, derived from content so it prerenders), `website.getBranchHierarchy({ route, for })` (the page tree for one branch). Kit ships no ready-made layout — a layout is your foundation's design; write it in `src/layouts/` and use these for the behaviour.
1104
1106
  **Layout helpers:** `useGridLayout(columns, { gap })`, `useAccordion({ multiple, defaultOpen })`
@@ -1160,21 +1162,23 @@ Header.as = 'header'
1160
1162
  export default {
1161
1163
  title: 'Feature Grid',
1162
1164
  description: 'Grid of feature cards with icons',
1163
- category: 'showcase', // free-form editor grouping — nothing validates it.
1164
- // impact / showcase / structure is a suggested set.
1165
- // It names the kind of *component*, not the kind of
1166
- // site: a genre like 'marketing' belongs to a template
1167
- purpose: 'Explain', // free-form; a single verb reads well
1165
+ family: 'features', // the standard section type it is — the editor's
1166
+ // illustration and label (`uniweb families`). Optional;
1167
+ // a name like `Features` resolves on its own
1168
1168
  // hidden: true, // Exclude from export entirely (internal helpers)
1169
1169
  // background: 'self', // Component renders its own background
1170
1170
  // inset: true, // Available for @ComponentName in markdown
1171
- // visuals: 1, // Expects 1 visual
1172
1171
  // children: true, // Accepts child sections
1173
1172
 
1174
1173
  content: {
1175
1174
  title: 'Section heading',
1176
1175
  paragraphs: 'Introduction [0-1]',
1177
- items: 'Feature cards with icon, title, description',
1176
+ items: {
1177
+ label: 'Feature cards [3-6]',
1178
+ content: { icon: 'Icon [1]', title: 'Feature', paragraphs: 'Description' }, // one entry
1179
+ },
1180
+ // media: 'Photo, video or diagram [1]', // an image, a video, or an embedded component
1181
+ // sequence: 'Prose and media', // renders content.sequence — the section as written
1178
1182
  },
1179
1183
 
1180
1184
  params: {
@@ -1195,6 +1199,13 @@ export default {
1195
1199
 
1196
1200
  **All defaults belong in `meta.js`, not inline in component code.** `meta.js` is also the catalog entry another agent reads to discover your section type (Part 2, step 2) — write `description` and `content:` for that reader.
1197
1201
 
1202
+ **`content:` names what the component reads**, each as a label for the author with an optional count (`'Cards [3-6]'`): `title`, `pretitle`, `subtitle`, `paragraphs`, `links`, `lists`, `items`, `media`, `icons`, `documents`, `snippets`, `tables`, `math`, `quotes`, `sequence`.
1203
+
1204
+ - **`media` is an image, a video, or an embedded component.** `media: 'Hero media [1]'` takes any of the three; `media: { label, types: ['image'] }` narrows it; `image:`, `videos:` and `insets:` are the same slot narrowed to one type. Read it as `content.media` — the author's order, each item with its `kind` — and render it with `<Visual media={content.media} block={block} />`. Rendering the slot with `<Image>` drops a diagram an author placed there, and kit's `<Media>` is a video player.
1205
+ - **`sequence`** says the component renders `content.sequence` — the whole section as written, headline included. `{ label, except: ['table', 'math'] }` names the kinds it leaves out.
1206
+ - **`items: { label, content: { … } }`** says what each entry holds, in the same vocabulary.
1207
+ - The build warns on what it can't read, and refuses `visuals:` (declare `media`) and the `content:` element `data` (declare the block's tag in `data:`).
1208
+
1198
1209
  > **`meta.js` is a user interface, not just metadata.** When a foundation is published, every `meta.js` is registered as the foundation's schema, and the visual editor builds its controls from it: the build generates a `schema.json` from every `meta.js` in the foundation, and the editor renders it: a `select` param with `options` becomes a dropdown, a `boolean` becomes a toggle, `label` and `description` become the words a non-technical author reads, and each entry in `presets` becomes a one-click choice. That's the real reason param naming matters — `variant: centered` and `renderMode: flex-center` aren't a style preference, they're the difference between a legible control and a baffling one. Write `meta.js` as though someone who will never see your code has to use it, because that's exactly who does.
1199
1210
 
1200
1211
  ### Two worlds: section types and your own components
@@ -1617,7 +1628,7 @@ Back up your database **before** running this. It is not reversible.
1617
1628
 
1618
1629
  That makes one distinction worth holding onto. You **may** declare a schema describing the form *definition's envelope* — `title`, `description`, `fields` as a map — and get build-time validation that an authored form is well-formed. What you can't declare is a schema whose fields are *the form's* fields (`name`, `email`, …); that's describing the visitor's answers, which arrive at runtime and belong to a form you've never seen. Declare the key either way — it is what delivers the block — and give it a schema only if you want it checked, and only of the envelope (`{}` declares the key with none).
1619
1630
 
1620
- **Reading one in a component.** Declare the tag as a key (`data: { faq: {} }`), and `content.data[tag]` gives you both views: `items` for anything row-shaped (an accordion, a step list), `sequence` when you don't recognize the tag and want to render it faithfully in document order. Both are derived, so nothing is stored twice.
1631
+ **Reading one in a component.** Declare it the way the author writes the fence — `data: { 'md:faq': 'Questions and answers [3+]' }`. The value is the author's label (its count counts the entries), or `{ label, hint, content: { title: 'Question', paragraphs: 'Answer' } }` to say what each entry holds. The component still reads `content.data.faq`, which gives you both views: `items` for anything row-shaped (an accordion, a step list), `sequence` when you don't recognize the tag and want to render it faithfully in document order. Both are derived, so nothing is stored twice. (`faq: {}` also declares the key, saying nothing about the block.)
1621
1632
 
1622
1633
  ```jsx
1623
1634
  function Faq({ content }) {
@@ -1635,10 +1646,11 @@ function Faq({ content }) {
1635
1646
  ![Architecture overview](@NetworkDiagram){variant=compact}
1636
1647
  ```
1637
1648
 
1638
- The developer builds `NetworkDiagram` as an ordinary React component with `inset: true` in `meta.js`. Kit's `<Visual>` renders the first non-empty candidate, so one section type works whether the author supplies an image, a video, or an interactive component:
1649
+ The developer builds `NetworkDiagram` as an ordinary React component with `inset: true` in `meta.js`. The section that hosts it declares a `media` slot, and kit's `<Visual>` renders the author's item by its kind — so one section type works whether the author supplies an image, a video, or an interactive component:
1639
1650
 
1640
1651
  ```jsx
1641
- <Visual inset={block.insets[0]} video={content.videos[0]} image={content.images[0]} className="rounded-2xl" />
1652
+ // meta.js: content: { title: 'Headline', media: 'Illustration [1]' }
1653
+ <Visual media={content.media} block={block} className="rounded-2xl" />
1642
1654
  ```
1643
1655
 
1644
1656
  **Insets are full section types** — they receive `{ content, params, block }`. The alt text becomes `content.title` and attributes become `params`: `![npm create uniweb](@CommandBlock){note="Ready to go"}` → `content.title = "npm create uniweb"`, `params.note = "Ready to go"`.
@@ -189,6 +189,24 @@ function loadSiteYml(dir) {
189
189
  * `node_modules` — this answers "did the developer write this", so a match
190
190
  * inside a dependency is the wrong answer.
191
191
  */
192
+ /**
193
+ * Does this foundation render kit's <Prose> or <Article> — under its own name or an alias?
194
+ *
195
+ * The JSX tag alone missed `import { Article as ArticleBody } from '@uniweb/kit'`, which
196
+ * renders `<ArticleBody>`: two official templates shipped that way with no prose styles
197
+ * until 2026-09-29. So the import counts too. (The foundation build checks the same thing
198
+ * from its module graph, which no alias can hide from.)
199
+ *
200
+ * @param {string} dir - the foundation's source directory
201
+ * @returns {boolean}
202
+ */
203
+ export function rendersKitProse(dir) {
204
+ return (
205
+ sourceMatches(dir, /<(Prose|Article)\b/) ||
206
+ sourceMatches(dir, /import\s*\{[^}]*\b(Prose|Article)\b[^}]*\}\s*from\s*['"]@uniweb\/kit['"]/)
207
+ )
208
+ }
209
+
192
210
  function sourceMatches(dir, pattern) {
193
211
  let found = false
194
212
  forEachSourceFile(dir, (text) => {
@@ -1217,8 +1235,8 @@ export async function doctor(args = []) {
1217
1235
 
1218
1236
  // Does anything here actually ask for prose? Either the class in the
1219
1237
  // stylesheet, or kit's components, which render it.
1238
+ const usesProseComponent = rendersKitProse(f.path)
1220
1239
  const usesProseClass = /(^|[\s"'`])prose(\s|["'`]|$)/m.test(styles)
1221
- const usesProseComponent = sourceMatches(f.path, /<(Prose|Article)\b/)
1222
1240
 
1223
1241
  if (!usesProseClass && !usesProseComponent) continue
1224
1242
 
@@ -90,6 +90,8 @@ export async function generateStarter({ name, sectionDir, preset, json, write })
90
90
  const metaPath = join(sectionDir, 'meta.js')
91
91
  const meta = existsSync(metaPath) ? await loadMeta(metaPath) : {}
92
92
  const result = starterContent({ name, ...meta }, { preset })
93
+ // What the declaration says that could not be read — the build warns the same way.
94
+ result.problems = describeContent({ name, ...meta }).problems
93
95
 
94
96
  const doc = buildDoc(result.content)
95
97
  const markdown = doc ? serializeSection(result.params, doc) : ''
@@ -157,11 +159,14 @@ export function reportStarter(result, { write } = {}) {
157
159
  console.log('')
158
160
  console.log(` ${c.yellow}!${c.reset} Not filled: ${result.unfilled.join(', ')}`)
159
161
  console.log(
160
- ` ${c.dim}\`background\` is frontmatter, not content. A video needs an address we cannot invent.${c.reset}`,
161
- )
162
- console.log(
163
- ` ${c.dim}A \`data\` block needs a schema: a @/ ref resolves at build, and an empty {} declares no shape.${c.reset}`,
162
+ ` ${c.dim}A starter makes up no file, video or embedded component, and writes no table, equation,${c.reset}`,
164
163
  )
164
+ console.log(` ${c.dim}quote or concept block (md:<tag>) — add those in the section.${c.reset}`)
165
+ }
166
+
167
+ if (result.problems?.length) {
168
+ console.log('')
169
+ for (const problem of result.problems) console.log(` ${c.yellow}!${c.reset} ${problem}`)
165
170
  }
166
171
 
167
172
  if (write) {
@@ -189,7 +194,6 @@ export async function declarationFor(result) {
189
194
  icons: 'Icon [1]',
190
195
  videos: 'Video [1]',
191
196
  snippets: 'Code sample [1]',
192
- data: 'The data block the author writes',
193
197
  }
194
198
  // ⛔ The slot → declaration spelling comes from @uniweb/schemas, not from a
195
199
  // copy here. `image` declares and `images` delivers; that mapping is the
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-09-28T23:00:27.056Z",
3
+ "generatedAt": "2026-09-29T20:02:35.777Z",
4
4
  "packages": {
5
5
  "@uniweb/api": {
6
6
  "version": "0.6.8",
@@ -10,7 +10,7 @@
10
10
  ]
11
11
  },
12
12
  "@uniweb/build": {
13
- "version": "0.73.2",
13
+ "version": "0.74.0",
14
14
  "path": "framework/build",
15
15
  "deps": [
16
16
  "@uniweb/content-reader",
@@ -55,7 +55,7 @@
55
55
  ]
56
56
  },
57
57
  "@uniweb/kit": {
58
- "version": "0.19.11",
58
+ "version": "0.19.13",
59
59
  "path": "framework/kit",
60
60
  "deps": [
61
61
  "@uniweb/core",
@@ -71,7 +71,7 @@
71
71
  "deps": []
72
72
  },
73
73
  "@uniweb/press": {
74
- "version": "0.4.17",
74
+ "version": "0.4.18",
75
75
  "path": "framework/press",
76
76
  "deps": []
77
77
  },
@@ -84,7 +84,7 @@
84
84
  ]
85
85
  },
86
86
  "@uniweb/runtime": {
87
- "version": "0.28.3",
87
+ "version": "0.28.4",
88
88
  "path": "framework/runtime",
89
89
  "deps": [
90
90
  "@uniweb/core",
@@ -97,7 +97,7 @@
97
97
  "deps": []
98
98
  },
99
99
  "@uniweb/schemas": {
100
- "version": "0.9.3",
100
+ "version": "0.10.0",
101
101
  "path": "framework/schemas",
102
102
  "deps": []
103
103
  },
@@ -107,7 +107,7 @@
107
107
  "deps": []
108
108
  },
109
109
  "@uniweb/semantic-parser": {
110
- "version": "1.4.1",
110
+ "version": "1.5.1",
111
111
  "path": "framework/semantic-parser",
112
112
  "deps": []
113
113
  },
@@ -117,7 +117,7 @@
117
117
  "deps": []
118
118
  },
119
119
  "@uniweb/templates": {
120
- "version": "0.17.7",
120
+ "version": "0.17.9",
121
121
  "path": "framework/templates",
122
122
  "deps": []
123
123
  },
@@ -127,7 +127,7 @@
127
127
  "deps": []
128
128
  },
129
129
  "@uniweb/unipress": {
130
- "version": "0.10.16",
130
+ "version": "0.10.17",
131
131
  "path": "framework/unipress",
132
132
  "deps": [
133
133
  "@uniweb/build",