@refrakt-md/runes 0.17.0 → 0.19.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.
Files changed (58) hide show
  1. package/dist/aggregate-resolve.d.ts.map +1 -1
  2. package/dist/aggregate-resolve.js +69 -0
  3. package/dist/aggregate-resolve.js.map +1 -1
  4. package/dist/collection-helpers.d.ts +24 -2
  5. package/dist/collection-helpers.d.ts.map +1 -1
  6. package/dist/collection-helpers.js +24 -3
  7. package/dist/collection-helpers.js.map +1 -1
  8. package/dist/collection-resolve.d.ts.map +1 -1
  9. package/dist/collection-resolve.js +15 -8
  10. package/dist/collection-resolve.js.map +1 -1
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +60 -354
  13. package/dist/config.js.map +1 -1
  14. package/dist/drawer-pipeline.d.ts.map +1 -1
  15. package/dist/drawer-pipeline.js +7 -22
  16. package/dist/drawer-pipeline.js.map +1 -1
  17. package/dist/fence-escape.d.ts.map +1 -1
  18. package/dist/fence-escape.js +7 -1
  19. package/dist/fence-escape.js.map +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/lib/component.d.ts.map +1 -1
  25. package/dist/lib/component.js +50 -3
  26. package/dist/lib/component.js.map +1 -1
  27. package/dist/lib/resolver.d.ts.map +1 -1
  28. package/dist/lib/resolver.js +13 -2
  29. package/dist/lib/resolver.js.map +1 -1
  30. package/dist/tags/aggregate.d.ts.map +1 -1
  31. package/dist/tags/aggregate.js +10 -3
  32. package/dist/tags/aggregate.js.map +1 -1
  33. package/dist/tags/budget.d.ts.map +1 -1
  34. package/dist/tags/budget.js +99 -9
  35. package/dist/tags/budget.js.map +1 -1
  36. package/dist/tags/card.d.ts.map +1 -1
  37. package/dist/tags/card.js +26 -43
  38. package/dist/tags/card.js.map +1 -1
  39. package/dist/tags/chart.d.ts.map +1 -1
  40. package/dist/tags/chart.js +31 -64
  41. package/dist/tags/chart.js.map +1 -1
  42. package/dist/tags/common.d.ts +37 -8
  43. package/dist/tags/common.d.ts.map +1 -1
  44. package/dist/tags/common.js +59 -24
  45. package/dist/tags/common.js.map +1 -1
  46. package/dist/tags/diagram.d.ts.map +1 -1
  47. package/dist/tags/diagram.js +33 -7
  48. package/dist/tags/diagram.js.map +1 -1
  49. package/dist/tags/embed.d.ts.map +1 -1
  50. package/dist/tags/embed.js +28 -10
  51. package/dist/tags/embed.js.map +1 -1
  52. package/dist/tags/gallery.d.ts.map +1 -1
  53. package/dist/tags/gallery.js +1 -6
  54. package/dist/tags/gallery.js.map +1 -1
  55. package/dist/tags/sandbox.d.ts.map +1 -1
  56. package/dist/tags/sandbox.js +32 -50
  57. package/dist/tags/sandbox.js.map +1 -1
  58. package/package.json +3 -3
package/dist/config.js CHANGED
@@ -1,4 +1,4 @@
1
- import { isTag, makeTag, findByDataName, readMeta, resolveGap, ratioToFr, resolveOffset, resolveValign, parsePlacement } from '@refrakt-md/transform';
1
+ import { isTag, readField, resolveGap, ratioToFr, resolveOffset, resolveValign, parsePlacement } from '@refrakt-md/transform';
2
2
  import Markdoc from '@markdoc/markdoc';
3
3
  const { Tag } = Markdoc;
4
4
  import { createComponentRenderable } from './lib/index.js';
@@ -15,58 +15,6 @@ import { resolveExpands } from './expand-pipeline.js';
15
15
  import { resolveCollections } from './collection-resolve.js';
16
16
  import { resolveRelationships } from './relationships-resolve.js';
17
17
  import { resolveAggregates } from './aggregate-resolve.js';
18
- // ─── Budget postTransform helpers ───
19
- const BUDGET_CURRENCY_SYMBOLS = {
20
- USD: '$', EUR: '€', GBP: '£', JPY: '¥', CNY: '¥',
21
- AUD: 'A$', CAD: 'C$', CHF: 'CHF ', SEK: 'kr', NOK: 'kr', DKK: 'kr',
22
- INR: '₹', KRW: '₩', BRL: 'R$', MXN: 'MX$', ZAR: 'R',
23
- };
24
- function formatBudgetAmount(amount, symbol) {
25
- const parts = (amount % 1 === 0 ? String(amount) : amount.toFixed(2)).split('.');
26
- parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
27
- return symbol + parts.join('.');
28
- }
29
- function parseBudgetDays(duration) {
30
- let days = 0;
31
- const dayMatch = duration.match(/(\d+)\s*day/i);
32
- const weekMatch = duration.match(/(\d+)\s*week/i);
33
- const monthMatch = duration.match(/(\d+)\s*month/i);
34
- if (dayMatch)
35
- days += parseInt(dayMatch[1]);
36
- if (weekMatch)
37
- days += parseInt(weekMatch[1]) * 7;
38
- if (monthMatch)
39
- days += parseInt(monthMatch[1]) * 30;
40
- if (days === 0) {
41
- const num = parseInt(duration);
42
- if (!isNaN(num))
43
- days = num;
44
- }
45
- return days;
46
- }
47
- function parseBudgetAmount(str) {
48
- const cleaned = str.replace(/[€$£¥₹₩\s]/g, '').replace(/,/g, '');
49
- const range = cleaned.match(/^([\d.]+)\s*[-–]\s*([\d.]+)/);
50
- if (range)
51
- return (parseFloat(range[1]) + parseFloat(range[2])) / 2;
52
- const num = parseFloat(cleaned);
53
- return isNaN(num) ? 0 : num;
54
- }
55
- /** Recursively find all nodes with a specific data-rune attribute */
56
- function collectByRune(children, typeName) {
57
- const results = [];
58
- for (const c of children) {
59
- if (isTag(c)) {
60
- if (c.attributes?.['data-rune'] === typeName) {
61
- results.push(c);
62
- }
63
- else {
64
- results.push(...collectByRune(c.children, typeName));
65
- }
66
- }
67
- }
68
- return results;
69
- }
70
18
  /** Read text content from a property span child */
71
19
  function readPropText(node, prop) {
72
20
  for (const c of node.children) {
@@ -93,7 +41,7 @@ export const coreConfig = {
93
41
  runes: {
94
42
  // ─── Simple runes (block name only, engine adds BEM classes) ───
95
43
  Accordion: { block: 'accordion', defaultDensity: 'full', sections: { preamble: 'preamble', headline: 'title', blurb: 'description' }, autoLabel: pageSectionAutoLabel, editHints: { headline: 'inline', eyebrow: 'inline', blurb: 'inline' } },
96
- AccordionItem: { block: 'accordion-item', parent: 'Accordion', rootAttributes: { 'data-state': 'closed' }, autoLabel: { name: 'header' }, editHints: { header: 'inline', body: 'none' } },
44
+ AccordionItem: { block: 'accordion-item', parent: 'Accordion', requiresParent: 'Accordion', rootAttributes: { 'data-state': 'closed' }, autoLabel: { name: 'header' }, editHints: { header: 'inline', body: 'none' } },
97
45
  Details: { block: 'details', autoLabel: { summary: 'summary' }, editHints: { summary: 'inline', body: 'none' } },
98
46
  Grid: {
99
47
  block: 'grid',
@@ -135,12 +83,13 @@ export const coreConfig = {
135
83
  },
136
84
  layout: { root: ['topbar'] },
137
85
  sections: { topbar: 'header' },
86
+ // Opt in to the highlight transform's `theme.code.colorScheme` cascade
87
+ // (topbar + tab chrome flip with the inner code). Static flag → declared
88
+ // via rootAttributes rather than a postTransform. The `data-code-host`
89
+ // consumer reads it truthily, so `"true"` is equivalent to the old
90
+ // valueless boolean.
91
+ rootAttributes: { 'data-code-host': 'true' },
138
92
  editHints: { panel: 'code' },
139
- postTransform(node) {
140
- // Opt in to the highlight transform's `theme.code.colorScheme`
141
- // cascade so the topbar + tab chrome flip with the inner code.
142
- return { ...node, attributes: { ...node.attributes, 'data-code-host': true } };
143
- },
144
93
  },
145
94
  PageSection: { block: 'page-section' },
146
95
  TableOfContents: { block: 'toc' },
@@ -188,59 +137,41 @@ export const coreConfig = {
188
137
  block: 'progress',
189
138
  modifiers: { sentiment: { source: 'meta' } },
190
139
  },
191
- /* card — generic content card. The `layout` meta drives the shared
192
- * split layout (split.css, data-attribute-keyed); data-media-position
193
- * hoists the media to a full-bleed header on mobile. Named parts
140
+ /* card — generic content card. The shared `media-position` modifier
141
+ * places the media zone above, below, or beside the content; `media-ratio`
142
+ * controls media's share of the row in beside layouts. Named parts
194
143
  * (media/content/body/footer/link) get rf-card__* from data-name. */
195
144
  Card: {
196
145
  block: 'card',
197
- rootAttributes: { 'data-media-position': 'top' },
198
146
  modifiers: {
199
- layout: { source: 'meta', default: 'stacked' },
147
+ 'media-position': { source: 'meta', default: 'top', noBemClass: true },
148
+ 'media-ratio': { source: 'meta', noBemClass: true },
149
+ valign: { source: 'meta', noBemClass: true },
150
+ collapse: { source: 'meta', noBemClass: true },
151
+ },
152
+ sections: { media: 'media' },
153
+ // SPEC-081/091: the transform emits flat slots; `layout` builds the
154
+ // skeleton — media beside a `content` wrapper grouping eyebrow/body/
155
+ // footer. A base `layout` is the prerequisite for the cover variant
156
+ // ({% ref "SPEC-089" /%}).
157
+ layout: {
158
+ root: ['media', 'content'],
159
+ content: { tag: 'div', children: ['eyebrow', 'body', 'footer'] },
160
+ },
161
+ styles: {
162
+ valign: { prop: '--split-valign', transform: resolveValign },
200
163
  },
201
164
  },
202
165
  Embed: {
203
166
  block: 'embed',
204
167
  defaultDensity: 'compact',
205
- editHints: { fallback: 'none' },
206
- postTransform(node) {
207
- const block = node.attributes.class?.split(' ')[0] || 'rf-embed';
208
- const embedUrl = readMeta(node, 'embedUrl') || readMeta(node, 'url') || '';
209
- const title = readMeta(node, 'title') || 'Embedded content';
210
- const aspect = readMeta(node, 'aspect') || '16:9';
211
- const provider = readMeta(node, 'provider') || '';
212
- const [w, h] = aspect.split(':').map(Number);
213
- const paddingPercent = h && w ? (h / w) * 100 : 56.25;
214
- // Filter out consumed meta tags
215
- const contentChildren = node.children.filter(child => {
216
- if (!isTag(child) || child.name !== 'meta')
217
- return true;
218
- const prop = child.attributes['data-field'];
219
- return !['embedUrl', 'url', 'title', 'aspect', 'provider', 'type'].includes(prop);
220
- });
221
- const children = [];
222
- if (embedUrl) {
223
- children.push(makeTag('div', { class: `${block}__wrapper`, style: `padding-bottom: ${paddingPercent}%` }, [
224
- makeTag('iframe', {
225
- src: embedUrl,
226
- title,
227
- frameborder: '0',
228
- allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',
229
- allowfullscreen: '',
230
- loading: 'lazy',
231
- }, []),
232
- ]));
233
- }
234
- children.push(makeTag('div', { class: `${block}__fallback` }, contentChildren));
235
- return {
236
- ...node,
237
- attributes: {
238
- ...node.attributes,
239
- ...(provider ? { 'data-provider': provider } : {}),
240
- },
241
- children,
242
- };
168
+ // SPEC-081: the rune transform builds the wrapper/iframe/fallback
169
+ // structure directly; `provider` is a bag-only modifier that surfaces
170
+ // as `data-provider`. No postTransform.
171
+ modifiers: {
172
+ provider: { source: 'meta', default: 'generic', noBemClass: true },
243
173
  },
174
+ editHints: { fallback: 'none' },
244
175
  },
245
176
  Breadcrumb: {
246
177
  block: 'breadcrumb',
@@ -253,7 +184,7 @@ export const coreConfig = {
253
184
  separator: { prop: '--separator', template: '"{}"' },
254
185
  },
255
186
  },
256
- BreadcrumbItem: { block: 'breadcrumb-item', parent: 'Breadcrumb' },
187
+ BreadcrumbItem: { block: 'breadcrumb-item', parent: 'Breadcrumb', requiresParent: 'Breadcrumb' },
257
188
  Blog: {
258
189
  block: 'blog',
259
190
  defaultDensity: 'full',
@@ -290,53 +221,12 @@ export const coreConfig = {
290
221
  blocks: {
291
222
  meta: { fields: ['duration', { field: 'currency', align: 'end' }], layout: 'bar' },
292
223
  },
293
- layout: { root: ['meta', 'preamble'] },
294
- postTransform(node) {
295
- const block = 'rf-budget';
296
- const catBlock = 'rf-budget-category';
297
- // Read from data-* attributes (set by engine after consuming meta tags)
298
- const currency = node.attributes['data-currency'] || 'USD';
299
- const duration = node.attributes['data-duration'] || '';
300
- const showPerDay = node.attributes['data-show-per-day'] !== 'false';
301
- const symbol = BUDGET_CURRENCY_SYMBOLS[currency.toUpperCase()] || currency + ' ';
302
- // Find all BudgetCategory children and compute totals
303
- const categories = collectByRune(node.children, 'budget-category');
304
- let grandTotal = 0;
305
- for (const cat of categories) {
306
- // Read from data attributes set by engine from label/subtotal modifiers
307
- const label = cat.attributes['data-label'] || '';
308
- const subtotalStr = cat.attributes['data-subtotal'] || '0';
309
- const subtotal = parseFloat(subtotalStr) || 0;
310
- grandTotal += subtotal;
311
- // Inject category header with label and formatted subtotal
312
- const catHeader = makeTag('div', { class: `${catBlock}__header` }, [
313
- makeTag('span', { class: `${catBlock}__label` }, [label]),
314
- makeTag('span', { class: `${catBlock}__subtotal` }, [formatBudgetAmount(subtotal, symbol)]),
315
- ]);
316
- cat.children.unshift(catHeader);
317
- }
318
- // Build footer with totals
319
- const footerChildren = [
320
- makeTag('div', { class: `${block}__total` }, [
321
- makeTag('span', { class: `${block}__total-label` }, ['Total']),
322
- makeTag('span', { class: `${block}__total-amount` }, [formatBudgetAmount(grandTotal, symbol)]),
323
- ]),
324
- ];
325
- if (duration && showPerDay) {
326
- const days = parseBudgetDays(duration);
327
- if (days > 0) {
328
- const perDay = grandTotal / days;
329
- footerChildren.push(makeTag('div', { class: `${block}__per-day` }, [
330
- makeTag('span', { class: `${block}__per-day-label` }, ['Per day']),
331
- makeTag('span', { class: `${block}__per-day-amount` }, [formatBudgetAmount(perDay, symbol)]),
332
- ]));
333
- }
334
- }
335
- const footer = makeTag('div', { class: `${block}__footer` }, footerChildren);
336
- return {
337
- ...node,
338
- children: [...node.children, footer],
339
- };
224
+ // SPEC-081: the transform emits flat header slots and derives the
225
+ // totals (footer + category headers built there); `layout` builds the
226
+ // preamble <header>, and the categories / footer append after it.
227
+ layout: {
228
+ root: ['meta', 'preamble'],
229
+ preamble: { tag: 'header', children: ['headline', 'blurb', 'image'] },
340
230
  },
341
231
  },
342
232
  BudgetCategory: {
@@ -396,12 +286,10 @@ export const coreConfig = {
396
286
  modifiers: {
397
287
  layout: { source: 'meta', default: 'grid' },
398
288
  lightbox: { source: 'meta', default: 'true', noBemClass: true },
399
- gap: { source: 'meta', default: 'default', noBemClass: true },
400
289
  columns: { source: 'meta', noBemClass: true },
401
290
  },
402
291
  styles: {
403
292
  columns: '--gallery-columns',
404
- gap: { prop: '--gallery-gap', transform: resolveGap },
405
293
  },
406
294
  editHints: { items: 'none' },
407
295
  },
@@ -493,115 +381,14 @@ export const coreConfig = {
493
381
  Chart: {
494
382
  block: 'chart',
495
383
  defaultDensity: 'compact',
496
- editHints: { data: 'none' },
497
- postTransform(node) {
498
- const block = node.attributes.class?.split(' ')[0] || 'rf-chart';
499
- const chartType = readMeta(node, 'type') || 'bar';
500
- const title = readMeta(node, 'title') || '';
501
- const dataJson = findByDataName(node, 'data')?.attributes?.content || '{}';
502
- let chartData = { headers: [], rows: [] };
503
- try {
504
- chartData = JSON.parse(dataJson);
505
- }
506
- catch { /* fallback */ }
507
- const colors = [
508
- 'var(--rf-color-info)', 'var(--rf-color-success)',
509
- 'var(--rf-color-warning)', 'var(--rf-color-danger)',
510
- '#7c3aed', '#0891b2',
511
- ];
512
- const svgW = 600, svgH = 300;
513
- const pad = { top: 30, right: 20, bottom: 40, left: 50 };
514
- const cw = svgW - pad.left - pad.right;
515
- const ch = svgH - pad.top - pad.bottom;
516
- const labels = chartData.rows.map(r => r[0] || '');
517
- const series = chartData.headers.slice(1);
518
- const values = chartData.rows.map(r => r.slice(1).map(v => parseFloat(v) || 0));
519
- const maxVal = Math.max(...values.flat(), 1);
520
- const bgw = cw / Math.max(labels.length, 1);
521
- const bw = bgw / Math.max(series.length + 1, 2);
522
- // Build SVG children
523
- const svgChildren = [];
524
- // Axes
525
- svgChildren.push(makeTag('line', {
526
- x1: String(pad.left), y1: String(pad.top),
527
- x2: String(pad.left), y2: String(svgH - pad.bottom),
528
- stroke: 'var(--rf-color-border)', 'stroke-width': '1',
529
- }, []));
530
- svgChildren.push(makeTag('line', {
531
- x1: String(pad.left), y1: String(svgH - pad.bottom),
532
- x2: String(svgW - pad.right), y2: String(svgH - pad.bottom),
533
- stroke: 'var(--rf-color-border)', 'stroke-width': '1',
534
- }, []));
535
- if (chartType === 'bar') {
536
- for (let i = 0; i < labels.length; i++) {
537
- for (let si = 0; si < series.length; si++) {
538
- const h = (values[i][si] / maxVal) * ch;
539
- svgChildren.push(makeTag('rect', {
540
- x: String(pad.left + i * bgw + si * bw + bw * 0.25),
541
- y: String(pad.top + ch - h),
542
- width: String(bw * 0.75),
543
- height: String(h),
544
- style: `fill: ${colors[si % colors.length]}`,
545
- rx: '2',
546
- }, []));
547
- }
548
- svgChildren.push(makeTag('text', {
549
- x: String(pad.left + i * bgw + bgw / 2),
550
- y: String(svgH - pad.bottom + 20),
551
- 'text-anchor': 'middle', 'font-size': '12',
552
- fill: 'var(--rf-color-muted)',
553
- }, [labels[i]]));
554
- }
555
- }
556
- else if (chartType === 'line') {
557
- for (let si = 0; si < series.length; si++) {
558
- const pts = labels.map((_, i) => `${pad.left + i * bgw + bgw / 2},${pad.top + ch - (values[i][si] / maxVal) * ch}`).join(' ');
559
- svgChildren.push(makeTag('polyline', {
560
- points: pts, fill: 'none',
561
- style: `stroke: ${colors[si % colors.length]}`,
562
- 'stroke-width': '2',
563
- }, []));
564
- for (let i = 0; i < labels.length; i++) {
565
- svgChildren.push(makeTag('circle', {
566
- cx: String(pad.left + i * bgw + bgw / 2),
567
- cy: String(pad.top + ch - (values[i][si] / maxVal) * ch),
568
- r: '4',
569
- style: `fill: ${colors[si % colors.length]}`,
570
- }, []));
571
- }
572
- }
573
- for (let i = 0; i < labels.length; i++) {
574
- svgChildren.push(makeTag('text', {
575
- x: String(pad.left + i * bgw + bgw / 2),
576
- y: String(svgH - pad.bottom + 20),
577
- 'text-anchor': 'middle', 'font-size': '12',
578
- fill: 'var(--rf-color-muted)',
579
- }, [labels[i]]));
580
- }
581
- }
582
- const children = [];
583
- if (title) {
584
- children.push(makeTag('figcaption', { class: `${block}__title` }, [title]));
585
- }
586
- children.push(makeTag('div', { class: `${block}__container` }, [
587
- makeTag('svg', {
588
- viewBox: `0 0 ${svgW} ${svgH}`,
589
- class: `${block}__svg`,
590
- }, svgChildren),
591
- ]));
592
- // Legend
593
- if (series.length > 1) {
594
- const legendItems = series.map((name, i) => makeTag('span', { class: `${block}__legend-item` }, [
595
- makeTag('span', {
596
- class: `${block}__legend-color`,
597
- style: `background: ${colors[i % colors.length]};`,
598
- }, []),
599
- name,
600
- ]));
601
- children.push(makeTag('div', { class: `${block}__legend` }, legendItems));
602
- }
603
- return { ...node, children };
384
+ // SPEC-083: the transform emits the rf-chart element wrapping the data
385
+ // `<table>`; `type` / `stacked` are bag-only modifiers ( data-type /
386
+ // data-stacked) the web component reads. No postTransform.
387
+ modifiers: {
388
+ type: { source: 'meta', default: 'bar', noBemClass: true },
389
+ stacked: { source: 'meta', noBemClass: true },
604
390
  },
391
+ editHints: { data: 'none' },
605
392
  },
606
393
  // ─── Text formatting & layout runes ───
607
394
  PullQuote: {
@@ -669,8 +456,8 @@ export const coreConfig = {
669
456
  },
670
457
  // ─── Interactive runes (still get BEM classes, components add behavior) ───
671
458
  TabGroup: { block: 'tabs', defaultDensity: 'full', sections: { preamble: 'preamble', headline: 'title', blurb: 'description' }, autoLabel: pageSectionAutoLabel, editHints: { headline: 'inline', eyebrow: 'inline', blurb: 'inline' } },
672
- Tab: { block: 'tab', parent: 'TabGroup', rootAttributes: { 'data-state': 'inactive' }, editHints: { name: 'inline' } },
673
- TabPanel: { block: 'tab-panel', parent: 'TabGroup', rootAttributes: { 'data-state': 'inactive' } },
459
+ Tab: { block: 'tab', parent: 'TabGroup', requiresParent: 'TabGroup', rootAttributes: { 'data-state': 'inactive' }, editHints: { name: 'inline' } },
460
+ TabPanel: { block: 'tab-panel', parent: 'TabGroup', requiresParent: 'TabGroup', rootAttributes: { 'data-state': 'inactive' } },
674
461
  DataTable: {
675
462
  block: 'datatable',
676
463
  defaultDensity: 'compact',
@@ -730,36 +517,15 @@ export const coreConfig = {
730
517
  },
731
518
  editHints: { panels: 'none' },
732
519
  },
733
- JuxtaposePanel: { block: 'juxtapose-panel', parent: 'Juxtapose', rootAttributes: { 'data-state': 'inactive' }, editHints: { body: 'none' } },
520
+ JuxtaposePanel: { block: 'juxtapose-panel', parent: 'Juxtapose', requiresParent: 'Juxtapose', rootAttributes: { 'data-state': 'inactive' }, editHints: { body: 'none' } },
734
521
  Diagram: {
735
522
  block: 'diagram',
736
523
  defaultDensity: 'compact',
737
524
  editHints: { source: 'code' },
738
- postTransform(node) {
739
- const block = node.attributes.class?.split(' ')[0] || 'rf-diagram';
740
- const language = readMeta(node, 'language') || 'mermaid';
741
- const title = readMeta(node, 'title') || '';
742
- const sourceMeta = findByDataName(node, 'source');
743
- const source = sourceMeta?.attributes?.content || '';
744
- // Build fallback HTML (visible in SSR, replaced by web component)
745
- const children = [];
746
- if (title) {
747
- children.push(makeTag('figcaption', { class: `${block}__title` }, [title]));
748
- }
749
- const containerChildren = source
750
- ? [makeTag('pre', { class: `${block}__source` }, [makeTag('code', {}, [source])])]
751
- : [];
752
- children.push(makeTag('div', { class: `${block}__container` }, containerChildren));
753
- // Hidden source for web component to read
754
- if (source) {
755
- children.push(makeTag('div', { 'data-content': 'source', style: 'display:none' }, [source]));
756
- }
757
- return {
758
- ...node,
759
- name: 'rf-diagram',
760
- attributes: { ...node.attributes, 'data-language': language },
761
- children,
762
- };
525
+ // SPEC-081: the rune transform emits the `rf-diagram` element + SSR
526
+ // fallback; `language` is a bag-only modifier (→ data-language).
527
+ modifiers: {
528
+ language: { source: 'meta', default: 'mermaid', noBemClass: true },
763
529
  },
764
530
  },
765
531
  Tint: { block: 'tint', parent: '*' },
@@ -769,64 +535,6 @@ export const coreConfig = {
769
535
  block: 'sandbox',
770
536
  defaultDensity: 'compact',
771
537
  editHints: { source: 'code' },
772
- postTransform(node) {
773
- // Read meta values
774
- const content = readMeta(node, 'content') || '';
775
- const framework = readMeta(node, 'framework') || '';
776
- const dependencies = readMeta(node, 'dependencies') || '';
777
- const label = readMeta(node, 'label') || '';
778
- const height = readMeta(node, 'height') || 'auto';
779
- const designTokens = readMeta(node, 'design-tokens') || '';
780
- const securityMode = readMeta(node, 'security-mode') || 'trusted';
781
- const allowJs = readMeta(node, 'allow-js') || 'true';
782
- const sandboxOrigin = readMeta(node, 'sandbox-origin') || '';
783
- // Keep non-meta children (fallback pre) and extract source panels
784
- const fallbackChildren = [];
785
- const sourcePanelOrigins = [];
786
- for (const child of node.children) {
787
- if (!isTag(child)) {
788
- fallbackChildren.push(child);
789
- continue;
790
- }
791
- if (child.name === 'meta') {
792
- // Collect origin data from source panels
793
- if (child.attributes?.['data-field'] === 'source-panel' && child.attributes?.['data-origin']) {
794
- sourcePanelOrigins.push(`${child.attributes['data-label'] || ''}\t${child.attributes['data-origin']}`);
795
- }
796
- continue;
797
- }
798
- fallbackChildren.push(child);
799
- }
800
- // Wrap fallback and source in <template> tags (inert/invisible).
801
- // Using <template> instead of <div> avoids HTML parser issues:
802
- // when <rf-sandbox> is inside <p>, block elements like <pre> or
803
- // <div> cause <p> to auto-close, pushing children out of the
804
- // custom element. <template> is parsed but never rendered.
805
- const children = [
806
- ...(fallbackChildren.length > 0
807
- ? [makeTag('template', { 'data-content': 'fallback' }, fallbackChildren)]
808
- : []),
809
- makeTag('template', { 'data-content': 'source' }, [content]),
810
- ];
811
- return {
812
- ...node,
813
- name: 'rf-sandbox',
814
- attributes: {
815
- ...node.attributes,
816
- 'data-source-content': content,
817
- ...(framework ? { 'data-framework': framework } : {}),
818
- ...(dependencies ? { 'data-dependencies': dependencies } : {}),
819
- ...(label ? { 'data-label': label } : {}),
820
- 'data-height': height,
821
- ...(designTokens ? { 'data-design-tokens': designTokens } : {}),
822
- ...(sourcePanelOrigins.length > 0 ? { 'data-source-origins': sourcePanelOrigins.join('\n') } : {}),
823
- 'data-security-mode': securityMode,
824
- 'data-allow-js': allowJs,
825
- ...(sandboxOrigin ? { 'data-sandbox-origin': sandboxOrigin } : {}),
826
- },
827
- children,
828
- };
829
- },
830
538
  },
831
539
  },
832
540
  };
@@ -2007,14 +1715,12 @@ function resolveBlogPosts(renderable, allPosts, ctx, pageUrl) {
2007
1715
  const result = mapBlogTags(renderable, (tag) => {
2008
1716
  if (tag.attributes['data-rune'] !== 'blog')
2009
1717
  return tag;
2010
- const folderMeta = tag.children.find((c) => Tag.isTag(c) && c.attributes['data-field'] === 'folder');
2011
- const sortMeta = tag.children.find((c) => Tag.isTag(c) && c.attributes['data-field'] === 'sort');
2012
- const filterMeta = tag.children.find((c) => Tag.isTag(c) && c.attributes['data-field'] === 'filter');
2013
- const limitMeta = tag.children.find((c) => Tag.isTag(c) && c.attributes['data-field'] === 'limit');
2014
- const folder = Tag.isTag(folderMeta) ? folderMeta.attributes.content : '';
2015
- const sort = Tag.isTag(sortMeta) ? sortMeta.attributes.content : 'date-desc';
2016
- const filterStr = Tag.isTag(filterMeta) ? filterMeta.attributes.content : '';
2017
- const limitStr = Tag.isTag(limitMeta) ? limitMeta.attributes.content : '';
1718
+ // SPEC-082: read field values from the bag (bag-first, meta-fallback).
1719
+ // The cross-page tree still carries the `data-rune-fields` attribute.
1720
+ const folder = readField(tag, 'folder') ?? '';
1721
+ const sort = readField(tag, 'sort') || 'date-desc';
1722
+ const filterStr = readField(tag, 'filter') ?? '';
1723
+ const limitStr = readField(tag, 'limit') ?? '';
2018
1724
  const limit = limitStr ? parseInt(limitStr, 10) : undefined;
2019
1725
  if (!folder) {
2020
1726
  ctx.warn('Blog rune missing folder attribute', pageUrl);