@uniweb/runtime 0.9.4 → 0.9.6

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.
@@ -16362,6 +16362,8 @@ var __P2 = __toESM(require_jsx_runtime(), 1);
16362
16362
  var src_exports = {};
16363
16363
  __export(src_exports, {
16364
16364
  Block: () => Block,
16365
+ DATA_DIR: () => DATA_DIR,
16366
+ DATA_URL_PREFIX: () => DATA_URL_PREFIX,
16365
16367
  DataStore: () => DataStore,
16366
16368
  EntityStore: () => EntityStore,
16367
16369
  FetcherDispatcher: () => FetcherDispatcher,
@@ -16370,16 +16372,21 @@ __export(src_exports, {
16370
16372
  Theme: () => Theme,
16371
16373
  Uniweb: () => Uniweb,
16372
16374
  Website: () => Website,
16375
+ collectionDataUrl: () => collectionDataUrl,
16376
+ collectionNameFromUrl: () => collectionNameFromUrl,
16373
16377
  createUniweb: () => createUniweb,
16374
16378
  deriveCacheKey: () => deriveCacheKey,
16375
16379
  evaluateWhere: () => evaluate,
16376
16380
  getUniweb: () => getUniweb,
16377
16381
  hasDarkScheme: () => hasDarkScheme,
16382
+ isDataUrl: () => isDataUrl,
16378
16383
  isRichSchema: () => isRichSchema,
16379
16384
  isWildcardLanguages: () => isWildcardLanguages,
16380
16385
  listRequestStyleNames: () => listStyleNames,
16381
16386
  matchWhere: () => match,
16382
16387
  normalizeLanguageList: () => normalizeLanguageList,
16388
+ normalizeSchema: () => normalizeSchema,
16389
+ recordDataUrl: () => recordDataUrl,
16383
16390
  resolveDefaultLocale: () => resolveDefaultLocale,
16384
16391
  resolvePublishableLocales: () => resolvePublishableLocales,
16385
16392
  resolveRequestStyle: () => resolveStyle,
@@ -22732,6 +22739,13 @@ function createSequenceElement(node, options = {}) {
22732
22739
  children: processSequence({ content })
22733
22740
  };
22734
22741
  }
22742
+ case "concept_block": {
22743
+ return {
22744
+ type: "concept_block",
22745
+ tag: attrs?.tag,
22746
+ children: processSequence({ content }, options)
22747
+ };
22748
+ }
22735
22749
  case "dataBlock":
22736
22750
  return {
22737
22751
  type: "dataBlock",
@@ -22773,6 +22787,12 @@ function createSequenceElement(node, options = {}) {
22773
22787
  attrs: parseImgBlock(attrs)
22774
22788
  };
22775
22789
  case "image":
22790
+ if (attrs?.role === "video") {
22791
+ return {
22792
+ type: "video",
22793
+ attrs: parseMarkdownVideo(attrs)
22794
+ };
22795
+ }
22776
22796
  return {
22777
22797
  type: "image",
22778
22798
  attrs: parseImgBlock(attrs || {})
@@ -22798,10 +22818,32 @@ function createSequenceElement(node, options = {}) {
22798
22818
  attrs
22799
22819
  };
22800
22820
  }
22821
+ case "table": {
22822
+ const rows = (content || []).filter((row) => row.type === "tableRow").map((row) => ({
22823
+ cells: (row.content || []).filter((cell) => cell.type === "tableCell").map((cell) => ({
22824
+ children: processSequence({ content: cell.content }, options),
22825
+ header: cell.attrs?.header === true,
22826
+ align: cell.attrs?.align || null,
22827
+ colspan: cell.attrs?.colspan || 1,
22828
+ rowspan: cell.attrs?.rowspan || 1
22829
+ }))
22830
+ }));
22831
+ return { type: "table", rows, attrs };
22832
+ }
22833
+ // `divider` is what content-reader emits; `horizontalRule` and
22834
+ // `DividerBlock` are the editor's spellings. All three are one element.
22835
+ //
22836
+ // The reader's own name was NOT listed here until 2026-07-30 — markdown
22837
+ // dividers reached consumers only by falling through `default:`, which
22838
+ // happens to produce `{ type: 'divider', content: '' }` and happens to
22839
+ // match what everything downstream keys on. It worked by coincidence,
22840
+ // and the coincidence would have broken the moment `default:` changed.
22841
+ case "divider":
22801
22842
  case "DividerBlock":
22802
22843
  case "horizontalRule":
22803
22844
  return {
22804
- type: "divider"
22845
+ type: "divider",
22846
+ attrs
22805
22847
  };
22806
22848
  // Custom TipTap elements
22807
22849
  case "card-group":
@@ -22814,6 +22856,11 @@ function createSequenceElement(node, options = {}) {
22814
22856
  type: "document-group",
22815
22857
  documents: node.content?.filter((c) => c.type === "document").map((doc) => parseDocumentBlock(doc.attrs)) || []
22816
22858
  };
22859
+ // The editor renamed this node to `StructuredContent` (2026-07-31,
22860
+ // editor-internal). Both are accepted: the new name is what the editor
22861
+ // emits now, and the old one still arrives from documents authored
22862
+ // before the rename, which the framework never rewrites.
22863
+ case "StructuredContent":
22817
22864
  case "FormBlock":
22818
22865
  let formData = attrs?.data;
22819
22866
  if (typeof formData === "string") {
@@ -22983,6 +23030,12 @@ function processInlineElements(content) {
22983
23030
  type: "icon",
22984
23031
  attrs: parseUniwebIcon(item.attrs)
22985
23032
  });
23033
+ } else if (item.type === "image" || item.type === "ImageBlock") {
23034
+ const isVideo = item.attrs?.role === "video";
23035
+ items.push({
23036
+ type: isVideo ? "video" : "image",
23037
+ attrs: isVideo ? parseMarkdownVideo(item.attrs) : parseImgBlock(item.attrs || {})
23038
+ });
22986
23039
  } else if (item.type === "math-inline" || item.type === "math_inline") {
22987
23040
  items.push(item);
22988
23041
  } else if (item.type === "inset_placeholder") {
@@ -23057,10 +23110,10 @@ function parseDocumentBlock(itemAttrs) {
23057
23110
  return ele;
23058
23111
  }
23059
23112
  function parseUniwebIcon(itemAttrs) {
23060
- const { svg, url, size, color, preserveColors, href, target, library, name } = itemAttrs || {};
23113
+ const { svg, url, src, size, color, preserveColors, href, target, library, name } = itemAttrs || {};
23061
23114
  const icon = {};
23062
23115
  if (svg) icon.svg = svg;
23063
- if (url) icon.url = url;
23116
+ if (url || src) icon.url = url || src;
23064
23117
  if (size) icon.size = size;
23065
23118
  if (color) icon.color = color;
23066
23119
  if (preserveColors) icon.preserveColors = preserveColors;
@@ -23078,8 +23131,11 @@ function parseUniwebIcon(itemAttrs) {
23078
23131
  return icon;
23079
23132
  }
23080
23133
  function parseIconBlock(itemAttrs) {
23081
- let { svg } = itemAttrs;
23082
- return svg;
23134
+ const { svg, theme } = itemAttrs || {};
23135
+ const icon = {};
23136
+ if (svg) icon.svg = svg;
23137
+ if (theme) icon.theme = theme;
23138
+ return icon;
23083
23139
  }
23084
23140
  function parseImgBlock(itemAttrs) {
23085
23141
  let {
@@ -23096,9 +23152,30 @@ function parseImgBlock(itemAttrs) {
23096
23152
  theme,
23097
23153
  role,
23098
23154
  credit = "",
23099
- id
23155
+ id,
23100
23156
  // {#fig-id} cross-reference label — preserved so Press
23101
23157
  // adapters can emit \label{id} (LaTeX) / <id> (Typst).
23158
+ // ── Markdown-authored attributes ────────────────────────────────
23159
+ // This function was written for the EDITOR's `ImageBlock` node and
23160
+ // later reused for the markdown `image` node (see `case "image"`), so
23161
+ // its vocabulary is the editor's: contentType, direction, filter,
23162
+ // theme, credit. Everything markdown declares and the editor does not
23163
+ // was therefore tokenized, declared, and then dropped one layer before
23164
+ // delivery — the published "Image Attributes" table
23165
+ // (docs/reference/content-structure.md) reached no component, and
23166
+ // neither did the `role=pdf` set added 2026-07-29 for the editor's
23167
+ // `document` fold-in. Carried 2026-07-30.
23168
+ width,
23169
+ height,
23170
+ loading,
23171
+ fit,
23172
+ position,
23173
+ preview,
23174
+ // role=pdf — the preview image
23175
+ author,
23176
+ // role=pdf — resource metadata, rendered beside the preview
23177
+ description
23178
+ // role=pdf — describes the RESOURCE, not the image (≠ alt)
23102
23179
  } = itemAttrs;
23103
23180
  let { contentType, viewType, contentId, identifier } = imgInfo || {};
23104
23181
  const sizes = {
@@ -23123,14 +23200,64 @@ function parseImgBlock(itemAttrs) {
23123
23200
  direction,
23124
23201
  filter,
23125
23202
  imgPos: direction === "left" || direction === "right" ? direction : "",
23203
+ // NOTE: `size` here is the direction-derived LAYOUT size
23204
+ // (basic/lg/full), not the author's `{size=…}` — that attribute is
23205
+ // icon-only and is read by parseUniwebIcon. Do not wire `{size=…}`
23206
+ // through here; the two meanings collide on one key.
23126
23207
  size: sizes[direction] || "basic",
23127
23208
  href,
23128
23209
  target,
23129
23210
  theme,
23130
23211
  role,
23131
23212
  credit,
23132
- id: id || void 0
23213
+ id: id || void 0,
23214
+ // Omitted when absent, so an existing consumer sees no new empty keys.
23215
+ ...width !== void 0 && width !== null && { width },
23216
+ ...height !== void 0 && height !== null && { height },
23217
+ ...loading && { loading },
23218
+ ...fit && { fit },
23219
+ ...position && { position },
23220
+ ...preview && { preview },
23221
+ ...author && { author },
23222
+ ...description && { description }
23223
+ };
23224
+ }
23225
+ function parseMarkdownVideo(itemAttrs) {
23226
+ const {
23227
+ src,
23228
+ url,
23229
+ info,
23230
+ poster,
23231
+ alt = "",
23232
+ caption = "",
23233
+ autoplay,
23234
+ muted,
23235
+ loop,
23236
+ controls,
23237
+ href = "",
23238
+ target = "",
23239
+ role,
23240
+ id
23241
+ // {#fig-id} cross-reference label, same as parseImgBlock
23242
+ } = itemAttrs || {};
23243
+ const video = {
23244
+ // `identifier` (a CDN-resolvable asset id) wins when present, matching
23245
+ // parseImgBlock — an editor-deployed asset with a stale src must not
23246
+ // shadow the CDN copy.
23247
+ src: makeAssetUrl({ src: url || src, ...info }),
23248
+ alt,
23249
+ caption: stripTags(caption),
23250
+ role,
23251
+ href,
23252
+ target
23133
23253
  };
23254
+ if (poster) video.poster = poster;
23255
+ if (autoplay !== void 0) video.autoplay = autoplay;
23256
+ if (muted !== void 0) video.muted = muted;
23257
+ if (loop !== void 0) video.loop = loop;
23258
+ if (controls !== void 0) video.controls = controls;
23259
+ if (id) video.id = id;
23260
+ return video;
23134
23261
  }
23135
23262
  function parseVideoBlock(itemAttrs) {
23136
23263
  let {
@@ -23295,6 +23422,7 @@ function flattenGroup(group) {
23295
23422
  headings: group.body.headings || []
23296
23423
  };
23297
23424
  if (group.body.math && group.body.math.length) flat.math = group.body.math;
23425
+ if (group.body.tables && group.body.tables.length) flat.tables = group.body.tables;
23298
23426
  return flat;
23299
23427
  }
23300
23428
  function processGroups(sequence, options = {}) {
@@ -23317,11 +23445,11 @@ function processGroups(sequence, options = {}) {
23317
23445
  items: []
23318
23446
  };
23319
23447
  }
23320
- const groups = splitBySlices(sequence);
23448
+ const groups = splitBySlices(sequence, options);
23321
23449
  const processedGroups = groups.map((group) => processGroupContent(group));
23322
23450
  let mainGroup = null;
23323
23451
  let itemGroups = [];
23324
- const shouldBeMain = identifyMainContent(processedGroups);
23452
+ const shouldBeMain = options.alwaysItems ? false : identifyMainContent(processedGroups);
23325
23453
  if (shouldBeMain) {
23326
23454
  mainGroup = processedGroups[0];
23327
23455
  itemGroups = processedGroups.slice(1);
@@ -23349,7 +23477,7 @@ function processGroups(sequence, options = {}) {
23349
23477
  items: flatItems
23350
23478
  };
23351
23479
  }
23352
- function splitBySlices(sequence) {
23480
+ function splitBySlices(sequence, options = {}) {
23353
23481
  const groups = [];
23354
23482
  let currentGroup = [];
23355
23483
  for (let i = 0; i < sequence.length; i++) {
@@ -23367,7 +23495,7 @@ function splitBySlices(sequence) {
23367
23495
  groups.push(currentGroup);
23368
23496
  currentGroup = [];
23369
23497
  }
23370
- const headingBlock = readHeadingGroup(sequence, i);
23498
+ const headingBlock = readHeadingGroup(sequence, i, options);
23371
23499
  currentGroup.push(...headingBlock);
23372
23500
  i += headingBlock.length - 1;
23373
23501
  } else {
@@ -23385,7 +23513,7 @@ function isPreTitle(sequence, i) {
23385
23513
  function isBannerImage(sequence, i) {
23386
23514
  return i === 0 && i + 1 < sequence.length && sequence[i].type === "image" && (sequence[i].role === "banner" || sequence[i + 1].type === "heading");
23387
23515
  }
23388
- function readHeadingGroup(sequence, startIdx) {
23516
+ function readHeadingGroup(sequence, startIdx, options = {}) {
23389
23517
  const elements = [sequence[startIdx]];
23390
23518
  let hasGoneDeeper = false;
23391
23519
  for (let i = startIdx + 1; i < sequence.length; i++) {
@@ -23403,7 +23531,7 @@ function readHeadingGroup(sequence, startIdx) {
23403
23531
  elements.push(element);
23404
23532
  continue;
23405
23533
  }
23406
- if (element.level === previousElement.level && !hasGoneDeeper) {
23534
+ if (element.level === previousElement.level && !hasGoneDeeper && !options.alwaysItems) {
23407
23535
  elements.push(element);
23408
23536
  continue;
23409
23537
  }
@@ -23522,6 +23650,14 @@ function processGroupContent(elements) {
23522
23650
  const quoteContent = processGroupContent(element.children);
23523
23651
  body.quotes.push(quoteContent.body);
23524
23652
  break;
23653
+ case "concept_block": {
23654
+ const children = element.children || [];
23655
+ body.data[element.tag] = {
23656
+ items: processGroups(children, { alwaysItems: true }).items,
23657
+ sequence: children
23658
+ };
23659
+ break;
23660
+ }
23525
23661
  case "dataBlock":
23526
23662
  body.data[element.tag] = element.data;
23527
23663
  break;
@@ -23538,6 +23674,12 @@ function processGroupContent(elements) {
23538
23674
  mathml: element.mathml || ""
23539
23675
  });
23540
23676
  break;
23677
+ case "table":
23678
+ (body.tables ||= []).push({
23679
+ rows: element.rows || [],
23680
+ attrs: element.attrs || {}
23681
+ });
23682
+ break;
23541
23683
  case "inset":
23542
23684
  body.insets.push({ refId: element.refId });
23543
23685
  break;
@@ -23596,6 +23738,8 @@ function processInlineElements2(children, body) {
23596
23738
  } else {
23597
23739
  body.images.push(item.attrs);
23598
23740
  }
23741
+ } else if (item.type === "video") {
23742
+ body.videos.push(item.attrs);
23599
23743
  } else if (item.type === "link") {
23600
23744
  body.links.push(item.attrs);
23601
23745
  }
@@ -24228,6 +24372,7 @@ function isGoogleFontsUrl(url) {
24228
24372
  return false;
24229
24373
  }
24230
24374
  }
24375
+ var FONT_LINKS_MARKER = "<!--uniweb-fonts-->";
24231
24376
  function generateFontCSS(fontRoles = {}, fonts = {}, base = "/") {
24232
24377
  const cssLines = [];
24233
24378
  const linkTags = [];
@@ -26324,6 +26469,23 @@ function substitutePlaceholders(value, context, options = {}) {
26324
26469
  return value;
26325
26470
  }
26326
26471
 
26472
+ // ../core/src/data-paths.js
26473
+ var DATA_DIR = "data";
26474
+ var DATA_URL_PREFIX = `/${DATA_DIR}/`;
26475
+ function collectionDataUrl(name) {
26476
+ return `${DATA_URL_PREFIX}${name}.json`;
26477
+ }
26478
+ function recordDataUrl(collection, slug) {
26479
+ return `${DATA_URL_PREFIX}${collection}/${slug}.json`;
26480
+ }
26481
+ function collectionNameFromUrl(path) {
26482
+ if (typeof path !== "string") return "";
26483
+ return path.replace(new RegExp(`^/?${DATA_DIR}/`), "").replace(/\.json$/i, "");
26484
+ }
26485
+ function isDataUrl(path) {
26486
+ return typeof path === "string" && path.startsWith(DATA_URL_PREFIX);
26487
+ }
26488
+
26327
26489
  // ../core/src/fetch-config.js
26328
26490
  function isFetchRefinement(cfg) {
26329
26491
  return cfg?.refine === true || cfg?.inherit === true;
@@ -26331,7 +26493,7 @@ function isFetchRefinement(cfg) {
26331
26493
  function localizeConfig(cfg, locale, defaultLocale) {
26332
26494
  if (!cfg.path) return cfg;
26333
26495
  if (!locale || locale === defaultLocale) return cfg;
26334
- if (!cfg.path.startsWith("/data/")) return cfg;
26496
+ if (!isDataUrl(cfg.path)) return cfg;
26335
26497
  return { ...cfg, path: `/${locale}${cfg.path}` };
26336
26498
  }
26337
26499
  function applyDeferredDetail(cfg, collections) {
@@ -26342,7 +26504,7 @@ function applyDeferredDetail(cfg, collections) {
26342
26504
  if (!collConfig || typeof collConfig !== "object") return cfg;
26343
26505
  const deferred = Array.isArray(collConfig.deferred) ? collConfig.deferred : null;
26344
26506
  if (!deferred || deferred.length === 0) return cfg;
26345
- const pattern = typeof collConfig.detailUrl === "string" ? collConfig.detailUrl : `/data/${schema4}/{slug}.json`;
26507
+ const pattern = typeof collConfig.detailUrl === "string" ? collConfig.detailUrl : recordDataUrl(schema4, "{slug}");
26346
26508
  return { ...cfg, detail: pattern };
26347
26509
  }
26348
26510
  function resolveFetchConfigs(sources, options = {}) {
@@ -27097,6 +27259,32 @@ function validateLanguageConfig(config = {}) {
27097
27259
  return { errors, warnings };
27098
27260
  }
27099
27261
 
27262
+ // ../core/src/route-match.js
27263
+ var PARAM_NAME = "[A-Za-z0-9_-]+";
27264
+ var REGEX_SPECIALS = /[.*+?^${}()|[\]\\]/g;
27265
+ function normalizeRoute(route) {
27266
+ if (typeof route !== "string" || route === "") return "/";
27267
+ return route === "/" ? "/" : route.replace(/\/+$/, "") || "/";
27268
+ }
27269
+ function routePatternToRegex(pattern) {
27270
+ const paramNames = [];
27271
+ const source = normalizeRoute(pattern).replace(REGEX_SPECIALS, "\\$&").replace(new RegExp(`:(${PARAM_NAME})`, "g"), (_, name) => {
27272
+ paramNames.push(name);
27273
+ return "([^/]+)";
27274
+ });
27275
+ return { regex: new RegExp(`^${source}$`), paramNames };
27276
+ }
27277
+ function matchDynamicRoute(pattern, path) {
27278
+ const { regex, paramNames } = routePatternToRegex(pattern);
27279
+ const match2 = normalizeRoute(path).match(regex);
27280
+ if (!match2) return null;
27281
+ const params = {};
27282
+ paramNames.forEach((name, i) => {
27283
+ params[name] = decodeURIComponent(match2[i + 1]);
27284
+ });
27285
+ return { params };
27286
+ }
27287
+
27100
27288
  // ../core/src/website.js
27101
27289
  var Website = class {
27102
27290
  constructor({
@@ -27457,19 +27645,7 @@ var Website = class {
27457
27645
  * @returns {Object|null} Match result with params, or null if no match
27458
27646
  */
27459
27647
  _matchDynamicRoute(pattern, path) {
27460
- const paramNames = [];
27461
- const regexStr = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:(\w+)/g, (_, paramName) => {
27462
- paramNames.push(paramName);
27463
- return "([^/]+)";
27464
- });
27465
- const regex = new RegExp(`^${regexStr}$`);
27466
- const match2 = path.match(regex);
27467
- if (!match2) return null;
27468
- const params = {};
27469
- paramNames.forEach((name, i) => {
27470
- params[name] = decodeURIComponent(match2[i + 1]);
27471
- });
27472
- return { params };
27648
+ return matchDynamicRoute(pattern, path);
27473
27649
  }
27474
27650
  /**
27475
27651
  * Create a dynamic page instance with concrete route and params
@@ -28598,26 +28774,6 @@ var Uniweb = class {
28598
28774
  // ../core/src/theme.js
28599
28775
  var SHADE_LEVELS4 = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
28600
28776
  var VALID_CONTEXTS = ["light", "medium", "dark"];
28601
- var DEFAULT_CONTEXT_TOKENS2 = {
28602
- light: {
28603
- bg: "var(--neutral-50)",
28604
- text: "var(--neutral-950)",
28605
- heading: "var(--neutral-900)",
28606
- link: "var(--primary-600)"
28607
- },
28608
- medium: {
28609
- bg: "var(--neutral-100)",
28610
- text: "var(--neutral-950)",
28611
- heading: "var(--neutral-900)",
28612
- link: "var(--primary-600)"
28613
- },
28614
- dark: {
28615
- bg: "var(--neutral-900)",
28616
- text: "var(--neutral-50)",
28617
- heading: "white",
28618
- link: "var(--primary-400)"
28619
- }
28620
- };
28621
28777
  var Theme = class {
28622
28778
  /**
28623
28779
  * Create a Theme instance
@@ -28721,36 +28877,42 @@ var Theme = class {
28721
28877
  // ============================================================
28722
28878
  // Context Access
28723
28879
  // ============================================================
28724
- /**
28725
- * Get a semantic token value for a context
28880
+ /*
28881
+ * REMOVED 2026-07-28: getContextToken(context, token) and
28882
+ * getContextTokens(context), plus the DEFAULT_CONTEXT_TOKENS table backing
28883
+ * them. Read this before reintroducing either.
28726
28884
  *
28727
- * @param {string} context - Context name ('light', 'medium', 'dark')
28728
- * @param {string} token - Token name (e.g., 'bg', 'text', 'link')
28729
- * @returns {string|null} Token value or null
28885
+ * They were unreachable-by-design rather than merely stale. A lookup keyed on
28886
+ * (context, token) answers "what does the .context-<name> class set by
28887
+ * default?", but the question callers ask is "what colour is --heading HERE",
28888
+ * and those diverge for two reasons no such lookup can see:
28730
28889
  *
28731
- * @example
28732
- * theme.getContextToken('light', 'bg') // → "var(--neutral-50)"
28733
- * theme.getContextToken('dark', 'text') // "var(--neutral-50)"
28734
- */
28735
- getContextToken(context, token) {
28736
- const customContext = this._contexts[context];
28737
- if (customContext && customContext[token]) {
28738
- return customContext[token];
28739
- }
28740
- const defaults = DEFAULT_CONTEXT_TOKENS2[context];
28741
- return defaults?.[token] || null;
28742
- }
28743
- /**
28744
- * Get all tokens for a context
28890
+ * - a section's own `theme:` overrides, emitted as `#section-{id} { … }` by
28891
+ * @uniweb/theming's buildSectionOverrides;
28892
+ * - the active site scheme, where `.scheme-dark` redefines the root tokens.
28745
28893
  *
28746
- * @param {string} context - Context name
28747
- * @returns {Object} Token name value mapping
28894
+ * So a correct-looking answer would still be wrong whenever a section
28895
+ * overrides tokens or the visitor is in dark mode — the failure being a
28896
+ * confident wrong value, not an error. (The table had also drifted: it held
28897
+ * `bg`/`text`, retired in favour of `section`/`body`, and four tokens where
28898
+ * the live set is ~25. Repopulating it would have made a misleading API look
28899
+ * trustworthy, which is worse than leaving it visibly incomplete.)
28900
+ *
28901
+ * What to use instead:
28902
+ * - an actually-resolved value (canvas, SVG, a chart matching the theme):
28903
+ * getComputedStyle(el).getPropertyValue('--heading') — this accounts for
28904
+ * both section overrides and the active scheme, which no static table can;
28905
+ * - the defaults table itself (tooling, a theme editor):
28906
+ * getDefaultContextTokens() from @uniweb/theming, which owns it;
28907
+ * - ordinary component styling: the CSS variables directly. Reading tokens
28908
+ * into JS to branch on them is the `isDark ? … : …` pattern semantic
28909
+ * tokens exist to remove.
28910
+ *
28911
+ * SHADE_LEVELS below is duplicated from @uniweb/theming too, and was left
28912
+ * deliberately: both copies are identical and the 11-step scale is fixed by
28913
+ * the design, so there is no drift to prevent — noted so the next reader
28914
+ * knows it was considered rather than missed.
28748
28915
  */
28749
- getContextTokens(context) {
28750
- const defaults = DEFAULT_CONTEXT_TOKENS2[context] || {};
28751
- const custom = this._contexts[context] || {};
28752
- return { ...defaults, ...custom };
28753
- }
28754
28916
  /**
28755
28917
  * Get the CSS class name for a context
28756
28918
  *
@@ -29075,6 +29237,25 @@ function isRichSchema(schema4) {
29075
29237
  if (schema4.childSchema && typeof schema4.childSchema === "object") return true;
29076
29238
  return false;
29077
29239
  }
29240
+ function normalizeSchema(schema4) {
29241
+ if (!schema4 || typeof schema4 !== "object" || Array.isArray(schema4))
29242
+ return null;
29243
+ if (Array.isArray(schema4.fields)) return schema4;
29244
+ if (schema4.isComposite === true || schema4.childSchema) return schema4;
29245
+ if (schema4.sections !== void 0) return null;
29246
+ const mapToFields = (map2) => Object.entries(map2).map(
29247
+ ([id, spec]) => typeof spec === "string" ? { id, type: spec } : { id, ...spec }
29248
+ );
29249
+ if (schema4.fields && typeof schema4.fields === "object") {
29250
+ const { fields, ...rest } = schema4;
29251
+ return { ...rest, fields: mapToFields(fields) };
29252
+ }
29253
+ const entries = Object.entries(schema4);
29254
+ if (!entries.length) return null;
29255
+ const isFieldSpec = ([, v]) => v && typeof v === "object" && !Array.isArray(v) && v.type !== void 0;
29256
+ if (!entries.every(isFieldSpec)) return null;
29257
+ return { fields: mapToFields(schema4) };
29258
+ }
29078
29259
 
29079
29260
  // ../core/src/request-styles/json-body.js
29080
29261
  var jsonBody = {
@@ -29417,7 +29598,14 @@ function getUniweb() {
29417
29598
  return globalThis.uniweb;
29418
29599
  }
29419
29600
  function createUniweb(content, foundation = null, extensions = [], { defaultFetcher = null, transport = null, dev = false } = {}) {
29420
- const instance = new Uniweb({ content, foundation, extensions, defaultFetcher, transport, dev });
29601
+ const instance = new Uniweb({
29602
+ content,
29603
+ foundation,
29604
+ extensions,
29605
+ defaultFetcher,
29606
+ transport,
29607
+ dev
29608
+ });
29421
29609
  globalThis.uniweb = instance;
29422
29610
  return instance;
29423
29611
  }
@@ -29672,6 +29860,20 @@ function getComponentMeta(componentName) {
29672
29860
  function getComponentDefaults(componentName) {
29673
29861
  return globalThis.uniweb?.getComponentDefaults?.(componentName) || {};
29674
29862
  }
29863
+ var PARAM_NAME2 = "[A-Za-z0-9_-]+";
29864
+ var REGEX_SPECIALS2 = /[.*+?^${}()|[\]\\]/g;
29865
+ function normalizeRoute2(route) {
29866
+ if (typeof route !== "string" || route === "") return "/";
29867
+ return route === "/" ? "/" : route.replace(/\/+$/, "") || "/";
29868
+ }
29869
+ function routePatternToRegex2(pattern) {
29870
+ const paramNames = [];
29871
+ const source = normalizeRoute2(pattern).replace(REGEX_SPECIALS2, "\\$&").replace(new RegExp(`:(${PARAM_NAME2})`, "g"), (_, name) => {
29872
+ paramNames.push(name);
29873
+ return "([^/]+)";
29874
+ });
29875
+ return { regex: new RegExp(`^${source}$`), paramNames };
29876
+ }
29675
29877
  function default404Html(basePath = "") {
29676
29878
  const homeHref = basePath ? `${basePath}/` : "/";
29677
29879
  return `<div class="page-not-found" style="min-height:80vh;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem;text-align:center"><h1 style="font-size:3rem;font-weight:bold;color:#1f2937;margin-bottom:1rem">404</h1><p style="color:#64748b;margin-bottom:2rem">Page not found</p><a href="${homeHref}" style="color:#3b82f6;text-decoration:underline">Go to homepage</a></div>`;
@@ -29735,6 +29937,23 @@ function resolveLayoutTransitions(areaNames, explicit) {
29735
29937
  for (const name of areaNames) transitions[name] = toIdent(name);
29736
29938
  return explicit ? { ...transitions, ...explicit } : transitions;
29737
29939
  }
29940
+ function resolveLayoutLayers(areaNames, explicit) {
29941
+ if (explicit === false) return {};
29942
+ const defaults = {};
29943
+ for (const name of areaNames) defaults[name] = 1;
29944
+ return explicit ? { ...defaults, ...explicit } : defaults;
29945
+ }
29946
+ function areaWrapperStyle(region, transitions, layers) {
29947
+ const style = {};
29948
+ const name = transitions?.[region];
29949
+ if (name) style.viewTransitionName = name;
29950
+ const layer = layers?.[region];
29951
+ if (layer != null) {
29952
+ style.position = "relative";
29953
+ style.zIndex = layer;
29954
+ }
29955
+ return Object.keys(style).length > 0 ? style : null;
29956
+ }
29738
29957
  function applyBootScheme(respectSystem, fallback) {
29739
29958
  var stored = null;
29740
29959
  try {
@@ -29988,15 +30207,17 @@ function renderLayout(page, website) {
29988
30207
  const layoutMeta = website.getLayoutMeta(layoutName);
29989
30208
  const bodyBlocks = page.getBodyBlocks();
29990
30209
  const areas = page.getLayoutAreas();
29991
- const transitions = website.viewTransitions ? resolveLayoutTransitions(Object.keys(areas), layoutMeta?.transitions) : null;
29992
- const wrapTransition = (name, element) => {
29993
- const vtName = transitions?.[name];
29994
- return vtName ? import_react.default.createElement("div", { style: { viewTransitionName: vtName } }, element) : element;
30210
+ const areaNames = Object.keys(areas);
30211
+ const transitions = website.viewTransitions ? resolveLayoutTransitions(areaNames, layoutMeta?.transitions) : null;
30212
+ const layers = resolveLayoutLayers(areaNames, layoutMeta?.layers);
30213
+ const wrapArea = (name, element) => {
30214
+ const style = areaWrapperStyle(name, transitions, layers);
30215
+ return style ? import_react.default.createElement("div", { style }, element) : element;
29995
30216
  };
29996
- const bodyElement = bodyBlocks ? wrapTransition("body", renderBlocks(bodyBlocks)) : null;
30217
+ const bodyElement = bodyBlocks ? wrapArea("body", renderBlocks(bodyBlocks)) : null;
29997
30218
  const areaElements = {};
29998
30219
  for (const [name, blocks] of Object.entries(areas)) {
29999
- areaElements[name] = wrapTransition(name, renderBlocks(blocks));
30220
+ areaElements[name] = wrapArea(name, renderBlocks(blocks));
30000
30221
  }
30001
30222
  if (RemoteLayout) {
30002
30223
  const params = { ...layoutMeta?.defaults || {}, ...page.getLayoutParams() || {} };
@@ -30011,9 +30232,9 @@ function renderLayout(page, website) {
30011
30232
  return import_react.default.createElement(
30012
30233
  import_react.default.Fragment,
30013
30234
  null,
30014
- areaElements.header && import_react.default.createElement("header", { style: { position: "relative", zIndex: 40 } }, areaElements.header),
30235
+ areaElements.header && import_react.default.createElement("header", null, areaElements.header),
30015
30236
  bodyElement && import_react.default.createElement("main", null, bodyElement),
30016
- areaElements.footer && import_react.default.createElement("footer", { style: { position: "relative", zIndex: 30 } }, areaElements.footer)
30237
+ areaElements.footer && import_react.default.createElement("footer", null, areaElements.footer)
30017
30238
  );
30018
30239
  }
30019
30240
  function initPrerenderForLocale(content, foundation, locale, extensionsOrOptions, maybeOptions) {
@@ -30092,6 +30313,9 @@ async function prefetchIcons(siteContent, uniweb, onProgress = () => {
30092
30313
  siteContent._iconCache = Object.fromEntries(uniweb.iconCache);
30093
30314
  }
30094
30315
  }
30316
+ function resolvePage(website, route) {
30317
+ return website.getPage(route);
30318
+ }
30095
30319
  function classifyRenderError(err) {
30096
30320
  const msg = err.message || "";
30097
30321
  if (msg.includes("Invalid hook call") || msg.includes("useState") || msg.includes("useEffect")) {
@@ -30113,6 +30337,14 @@ function classifyRenderError(err) {
30113
30337
  }
30114
30338
  function renderPage(page, website) {
30115
30339
  website.setActivePage(page.route);
30340
+ if (page.hasContent?.() && page.getBodyBlocks().length === 0) {
30341
+ return {
30342
+ error: {
30343
+ type: "content-not-loaded",
30344
+ message: `page "${page.route}" declares content but has no loaded sections \u2014 its sections are not in the payload and this renderer does not fetch them`
30345
+ }
30346
+ };
30347
+ }
30116
30348
  const element = renderLayout(page, website);
30117
30349
  let renderedContent;
30118
30350
  try {
@@ -30137,13 +30369,22 @@ function injectPageContent(html, renderedContent, page, options = {}) {
30137
30369
  </head>`);
30138
30370
  }
30139
30371
  }
30140
- const themeCss = page?.website?.themeData?.css;
30372
+ const themeData = page?.website?.themeData;
30373
+ const themeCss = themeData?.css;
30141
30374
  if (themeCss && !result.includes('id="uniweb-theme"')) {
30142
30375
  result = result.replace(
30143
30376
  "</head>",
30144
30377
  ` <style id="uniweb-theme">
30145
30378
  ${themeCss}
30146
30379
  </style>
30380
+ </head>`
30381
+ );
30382
+ }
30383
+ if (themeData?.links && !result.includes(FONT_LINKS_MARKER)) {
30384
+ result = result.replace(
30385
+ "</head>",
30386
+ ` ${FONT_LINKS_MARKER}
30387
+ ${themeData.links}
30147
30388
  </head>`
30148
30389
  );
30149
30390
  }
@@ -30198,10 +30439,7 @@ ${options.sectionOverrideCSS}
30198
30439
  }
30199
30440
  function generate404Html({ baseHtml, website, siteContent }) {
30200
30441
  const dynamicTemplates = siteContent.pages?.filter((p) => p.isDynamic) || [];
30201
- const routePatterns = dynamicTemplates.map((p) => {
30202
- const escaped = p.route.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[^/]+/g, "[^\\/]+");
30203
- return `^${escaped}\\/?$`;
30204
- });
30442
+ const routePatterns = dynamicTemplates.map((p) => routePatternToRegex2(p.route).regex.source);
30205
30443
  let html = baseHtml;
30206
30444
  const notFoundPage = website.getNotFoundPage();
30207
30445
  if (notFoundPage) {
@@ -30220,7 +30458,7 @@ function generate404Html({ baseHtml, website, siteContent }) {
30220
30458
  }
30221
30459
  if (routePatterns.length > 0) {
30222
30460
  const patternList = routePatterns.map((p) => `/${p}/`).join(",");
30223
- const dynamicScript = `<script>(function(){var p=[${patternList}],r=window.location.pathname;if(p.some(function(x){return x.test(r)})){var el=document.getElementById('root');if(el)el.innerHTML='';}})()<\/script>`;
30461
+ const dynamicScript = `<script>(function(){var p=[${patternList}],r=window.location.pathname.replace(/\\/+$/,'')||'/';if(p.some(function(x){return x.test(r)})){var el=document.getElementById('root');if(el)el.innerHTML='';}})()<\/script>`;
30224
30462
  html = html.replace("</body>", `${dynamicScript}
30225
30463
  </body>`);
30226
30464
  }
@@ -30254,6 +30492,7 @@ export {
30254
30492
  renderBlocks,
30255
30493
  renderLayout,
30256
30494
  renderPage,
30495
+ resolvePage,
30257
30496
  sliceContentForLocale
30258
30497
  };
30259
30498
  /*! Bundled license information: