@glw907/cairn-cms 0.6.0-rc.0 → 0.6.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.
@@ -9,8 +9,8 @@ named `?/setRole`, `?/remove`, and `?/add` actions.
9
9
  import type { Editor } from '../auth/types.js';
10
10
 
11
11
  interface Props {
12
- /** The editors load's data, plus the site name. */
13
- data: { editors: Editor[]; self: string; siteName: string };
12
+ /** The editors load's data: the allowlist and the acting owner's email. */
13
+ data: { editors: Editor[]; self: string };
14
14
  /** The last action's result (an error message when it failed). */
15
15
  form: { error?: string; ok?: boolean } | null;
16
16
  }
@@ -1,10 +1,9 @@
1
1
  import type { Editor } from '../auth/types.js';
2
2
  interface Props {
3
- /** The editors load's data, plus the site name. */
3
+ /** The editors load's data: the allowlist and the acting owner's email. */
4
4
  data: {
5
5
  editors: Editor[];
6
6
  self: string;
7
- siteName: string;
8
7
  };
9
8
  /** The last action's result (an error message when it failed). */
10
9
  form: {
@@ -1 +1 @@
1
- {"version":3,"file":"ManageEditors.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ManageEditors.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAG7C,UAAU,KAAK;IACb,mDAAmD;IACnD,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5D,kEAAkE;IAClE,IAAI,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;CAC/C;AAyEH;;;;;GAKG;AACH,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"ManageEditors.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ManageEditors.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAG7C,UAAU,KAAK;IACb,2EAA2E;IAC3E,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,kEAAkE;IAClE,IAAI,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;CAC/C;AAyEH;;;;;GAKG;AACH,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -1,9 +1,10 @@
1
1
  import { type PluggableList } from 'unified';
2
2
  import type { ComponentRegistry } from './registry.js';
3
3
  export interface RendererOptions {
4
- /** A site's per-index motion formula for the top-level rise stagger
5
- * (e.g. ecnordic's `(i) => '--rise:' + …`). Omit for no stagger. */
6
- rise?: (idx: number) => string;
4
+ /** Stamp a `data-rise` ordinal (0, 1, 2, …) on each top-level component so a site's
5
+ * CSS can drive an entrance-cascade delay off it. Omit for no stagger. The ordinal
6
+ * is inert, so a consumer's sanitize floor can keep `data-rise` and drop `style`. */
7
+ stagger?: boolean;
7
8
  }
8
9
  /** Compose a site's render pipeline from its component registry: directive syntax to
9
10
  * stamped markers to registry-built hast. Returns `renderMarkdown` plus the remark/
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/lib/render/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAUtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEvD,MAAM,WAAW,eAAe;IAC9B;yEACqE;IACrE,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;CAChC;AAED;;uFAEuF;AACvF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,iBAAiB,EAAE,OAAO,GAAE,eAAoB;;;8BAarD,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;EAE3D"}
1
+ {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/lib/render/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAUtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEvD,MAAM,WAAW,eAAe;IAC9B;;0FAEsF;IACtF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;uFAEuF;AACvF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,iBAAiB,EAAE,OAAO,GAAE,eAAoB;;;8BAarD,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;EAE3D"}
@@ -13,7 +13,7 @@ import { rehypeDispatch } from './rehype-dispatch.js';
13
13
  * rehype plugin arrays (so the Carta editor preview can reuse the exact same set). */
14
14
  export function createRenderer(registry, options = {}) {
15
15
  const remarkPlugins = [remarkDirective, [remarkDirectiveStamp, registry]];
16
- const rehypePlugins = [rehypeRaw, [rehypeDispatch, registry, options.rise], rehypeSlug];
16
+ const rehypePlugins = [rehypeRaw, [rehypeDispatch, registry, options.stagger], rehypeSlug];
17
17
  const processor = unified()
18
18
  .use(remarkParse)
19
19
  .use(remarkGfm)
@@ -9,8 +9,10 @@ export interface ComponentDef {
9
9
  description: string;
10
10
  /** Markdown scaffold inserted at the cursor by the editor palette. */
11
11
  insertTemplate: string;
12
- /** Build the final hast element from the stamped directive element. */
13
- build: (node: Element, rise?: string) => Element;
12
+ /** Build the final hast element from the stamped directive element. The engine
13
+ * stamps the entrance-stagger ordinal (`data-rise`) on the top-level result, so a
14
+ * build fn stays free of any motion concern. */
15
+ build: (node: Element) => Element;
14
16
  /** Optional role-to-default-icon, e.g. `{ caution: 'warning' }`. */
15
17
  defaultIconByRole?: Record<string, string>;
16
18
  }
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/lib/render/registry.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;IACb,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,cAAc,EAAE,MAAM,CAAC;IACvB,uEAAuE;IACvE,KAAK,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;IACjD,oEAAoE;IACpE,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC5C;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,YAAY,EAAE,CAAC;IACrB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAAC;IAC5C,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CAC9D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAAE,UAAU,EAAE,EAAE;IAAE,UAAU,EAAE,YAAY,EAAE,CAAA;CAAE,GAAG,iBAAiB,CAQhG"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/lib/render/registry.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;IACb,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,cAAc,EAAE,MAAM,CAAC;IACvB;;qDAEiD;IACjD,KAAK,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC;IAClC,oEAAoE;IACpE,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC5C;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,YAAY,EAAE,CAAC;IACrB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAAC;IAC5C,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CAC9D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAAE,UAAU,EAAE,EAAE;IAAE,UAAU,EAAE,YAAY,EAAE,CAAA;CAAE,GAAG,iBAAiB,CAQhG"}
@@ -10,15 +10,16 @@ export declare function splitHead(node: Element, makeIcon?: MakeIcon): {
10
10
  head: Element;
11
11
  rest: ElementContent[];
12
12
  };
13
- /** Section wrapper: `<section class=…><div class="card-body">…</div></section>`,
14
- * with an optional inline rise style. */
15
- export declare function cardShell(classes: string[], rise: string | undefined, body: ElementContent[]): Element;
13
+ /** Section wrapper: `<section class=…><div class="card-body">…</div></section>`. */
14
+ export declare function cardShell(classes: string[], body: ElementContent[]): Element;
16
15
  /** Tag the first <ul> among children with `ec-grid` and strip its whitespace-only
17
16
  * text nodes so the bare list serializes without newlines. Returns that <ul>. */
18
17
  export declare function markFirstList(children: ElementContent[]): Element | undefined;
19
18
  /** Rehype transformer: dispatch each stamped element through its registry `build`
20
- * fn. Top-level primitives get a document-order rise stagger when `rise` is
21
- * supplied (a site's per-index motion formula); nested ones don't. Non-primitive
19
+ * fn. When `stagger` is on, each top-level primitive gets a `data-rise` attribute
20
+ * carrying its document-order index (0, 1, 2, …); the site's CSS maps that ordinal
21
+ * to an entrance delay. The index is inert, so a consumer's sanitize floor can keep
22
+ * `data-rise` while dropping `style`. Nested primitives never get it. Non-primitive
22
23
  * content (lede, intro paragraphs, the page-toc nav) passes through untouched. */
23
- export declare function rehypeDispatch(registry: ComponentRegistry, rise?: (idx: number) => string): (tree: Root) => void;
24
+ export declare function rehypeDispatch(registry: ComponentRegistry, stagger?: boolean): (tree: Root) => void;
24
25
  //# sourceMappingURL=rehype-dispatch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rehype-dispatch.d.ts","sourceRoot":"","sources":["../../src/lib/render/rehype-dispatch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,cAAc,EAAc,MAAM,MAAM,CAAC;AAEtE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEvD,wBAAgB,SAAS,CAAC,IAAI,EAAE,cAAc,GAAG,SAAS,GAAG,IAAI,IAAI,OAAO,CAE3E;AAKD,wBAAgB,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGvE;AAED,mFAAmF;AACnF,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAGjE;AAED,kFAAkF;AAClF,MAAM,MAAM,QAAQ,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;AAMhE,wBAAgB,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,cAAc,EAAE,CAAA;CAAE,CAYvG;AAED;0CAC0C;AAC1C,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,OAAO,CAItG;AAED;kFACkF;AAClF,wBAAgB,aAAa,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,OAAO,GAAG,SAAS,CAS7E;AAmBD;;;mFAGmF;AACnF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,iBAAiB,EAAE,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,IAChF,MAAM,IAAI,UAUnB"}
1
+ {"version":3,"file":"rehype-dispatch.d.ts","sourceRoot":"","sources":["../../src/lib/render/rehype-dispatch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,MAAM,CAAC;AAE1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEvD,wBAAgB,SAAS,CAAC,IAAI,EAAE,cAAc,GAAG,SAAS,GAAG,IAAI,IAAI,OAAO,CAE3E;AAKD,wBAAgB,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGvE;AAED,mFAAmF;AACnF,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAGjE;AAED,kFAAkF;AAClF,MAAM,MAAM,QAAQ,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;AAMhE,wBAAgB,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,cAAc,EAAE,CAAA;CAAE,CAYvG;AAED,oFAAoF;AACpF,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,OAAO,CAE5E;AAED;kFACkF;AAClF,wBAAgB,aAAa,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,OAAO,GAAG,SAAS,CAS7E;AAoBD;;;;;mFAKmF;AACnF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,iBAAiB,EAAE,OAAO,CAAC,EAAE,OAAO,IACnE,MAAM,IAAI,UAYnB"}
@@ -32,13 +32,9 @@ export function splitHead(node, makeIcon) {
32
32
  headKids.push(h2);
33
33
  return { head: h('div', { className: ['ec-head'] }, headKids), rest };
34
34
  }
35
- /** Section wrapper: `<section class=…><div class="card-body">…</div></section>`,
36
- * with an optional inline rise style. */
37
- export function cardShell(classes, rise, body) {
38
- const properties = { className: classes };
39
- if (rise)
40
- properties.style = rise;
41
- return h('section', properties, [h('div', { className: ['card-body'] }, body)]);
35
+ /** Section wrapper: `<section class=…><div class="card-body">…</div></section>`. */
36
+ export function cardShell(classes, body) {
37
+ return h('section', { className: classes }, [h('div', { className: ['card-body'] }, body)]);
42
38
  }
43
39
  /** Tag the first <ul> among children with `ec-grid` and strip its whitespace-only
44
40
  * text nodes so the bare list serializes without newlines. Returns that <ul>. */
@@ -51,7 +47,8 @@ export function markFirstList(children) {
51
47
  return ul;
52
48
  }
53
49
  // Recurse into a node's children, transforming any nested primitive sections
54
- // (a grid inside a card, panels inside a split) WITHOUT a rise stagger.
50
+ // (a grid inside a card, panels inside a split). Nested primitives never carry the
51
+ // entrance stagger; only top-level ones do (stamped in the transformer below).
55
52
  function transformChildren(children, registry) {
56
53
  return children.map((c) => {
57
54
  if (isElement(c) && c.properties?.dataPrimitive)
@@ -61,22 +58,27 @@ function transformChildren(children, registry) {
61
58
  return c;
62
59
  });
63
60
  }
64
- function transformNode(node, registry, rise) {
61
+ function transformNode(node, registry) {
65
62
  node.children = transformChildren(node.children, registry);
66
63
  const name = strProp(node, 'dataPrimitive');
67
64
  const def = name ? registry.get(name) : undefined;
68
- return def ? def.build(node, rise) : node;
65
+ return def ? def.build(node) : node;
69
66
  }
70
67
  /** Rehype transformer: dispatch each stamped element through its registry `build`
71
- * fn. Top-level primitives get a document-order rise stagger when `rise` is
72
- * supplied (a site's per-index motion formula); nested ones don't. Non-primitive
68
+ * fn. When `stagger` is on, each top-level primitive gets a `data-rise` attribute
69
+ * carrying its document-order index (0, 1, 2, …); the site's CSS maps that ordinal
70
+ * to an entrance delay. The index is inert, so a consumer's sanitize floor can keep
71
+ * `data-rise` while dropping `style`. Nested primitives never get it. Non-primitive
73
72
  * content (lede, intro paragraphs, the page-toc nav) passes through untouched. */
74
- export function rehypeDispatch(registry, rise) {
73
+ export function rehypeDispatch(registry, stagger) {
75
74
  return (tree) => {
76
75
  let idx = 0;
77
76
  tree.children = tree.children.map((child) => {
78
77
  if (isElement(child) && child.properties?.dataPrimitive) {
79
- return transformNode(child, registry, rise ? rise(idx++) : undefined);
78
+ const el = transformNode(child, registry);
79
+ if (stagger)
80
+ el.properties = { ...el.properties, dataRise: String(idx++) };
81
+ return el;
80
82
  }
81
83
  if (isElement(child))
82
84
  child.children = transformChildren(child.children, registry);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glw907/cairn-cms",
3
- "version": "0.6.0-rc.0",
3
+ "version": "0.6.0",
4
4
  "description": "Embedded, magic-link, GitHub-committing CMS for SvelteKit/Cloudflare sites.",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -85,6 +85,7 @@
85
85
  "@sveltejs/kit": "^2.61",
86
86
  "@sveltejs/package": "^2",
87
87
  "@sveltejs/vite-plugin-svelte": "^7.1",
88
+ "@types/node": "^22.19.19",
88
89
  "@vitest/browser": "^4.1.7",
89
90
  "@vitest/browser-playwright": "^4.1.7",
90
91
  "carta-md": "^4.11",
@@ -9,8 +9,8 @@ named `?/setRole`, `?/remove`, and `?/add` actions.
9
9
  import type { Editor } from '../auth/types.js';
10
10
 
11
11
  interface Props {
12
- /** The editors load's data, plus the site name. */
13
- data: { editors: Editor[]; self: string; siteName: string };
12
+ /** The editors load's data: the allowlist and the acting owner's email. */
13
+ data: { editors: Editor[]; self: string };
14
14
  /** The last action's result (an error message when it failed). */
15
15
  form: { error?: string; ok?: boolean } | null;
16
16
  }
@@ -11,9 +11,10 @@ import { rehypeDispatch } from './rehype-dispatch.js';
11
11
  import type { ComponentRegistry } from './registry.js';
12
12
 
13
13
  export interface RendererOptions {
14
- /** A site's per-index motion formula for the top-level rise stagger
15
- * (e.g. ecnordic's `(i) => '--rise:' + …`). Omit for no stagger. */
16
- rise?: (idx: number) => string;
14
+ /** Stamp a `data-rise` ordinal (0, 1, 2, …) on each top-level component so a site's
15
+ * CSS can drive an entrance-cascade delay off it. Omit for no stagger. The ordinal
16
+ * is inert, so a consumer's sanitize floor can keep `data-rise` and drop `style`. */
17
+ stagger?: boolean;
17
18
  }
18
19
 
19
20
  /** Compose a site's render pipeline from its component registry: directive syntax to
@@ -21,7 +22,7 @@ export interface RendererOptions {
21
22
  * rehype plugin arrays (so the Carta editor preview can reuse the exact same set). */
22
23
  export function createRenderer(registry: ComponentRegistry, options: RendererOptions = {}) {
23
24
  const remarkPlugins: PluggableList = [remarkDirective, [remarkDirectiveStamp, registry]];
24
- const rehypePlugins: PluggableList = [rehypeRaw, [rehypeDispatch, registry, options.rise], rehypeSlug];
25
+ const rehypePlugins: PluggableList = [rehypeRaw, [rehypeDispatch, registry, options.stagger], rehypeSlug];
25
26
  const processor = unified()
26
27
  .use(remarkParse)
27
28
  .use(remarkGfm)
@@ -15,8 +15,10 @@ export interface ComponentDef {
15
15
  description: string;
16
16
  /** Markdown scaffold inserted at the cursor by the editor palette. */
17
17
  insertTemplate: string;
18
- /** Build the final hast element from the stamped directive element. */
19
- build: (node: Element, rise?: string) => Element;
18
+ /** Build the final hast element from the stamped directive element. The engine
19
+ * stamps the entrance-stagger ordinal (`data-rise`) on the top-level result, so a
20
+ * build fn stays free of any motion concern. */
21
+ build: (node: Element) => Element;
20
22
  /** Optional role-to-default-icon, e.g. `{ caution: 'warning' }`. */
21
23
  defaultIconByRole?: Record<string, string>;
22
24
  }
@@ -1,4 +1,4 @@
1
- import type { Root, Element, ElementContent, Properties } from 'hast';
1
+ import type { Root, Element, ElementContent } from 'hast';
2
2
  import { h } from 'hastscript';
3
3
  import type { ComponentRegistry } from './registry.js';
4
4
 
@@ -41,12 +41,9 @@ export function splitHead(node: Element, makeIcon?: MakeIcon): { head: Element;
41
41
  return { head: h('div', { className: ['ec-head'] }, headKids), rest };
42
42
  }
43
43
 
44
- /** Section wrapper: `<section class=…><div class="card-body">…</div></section>`,
45
- * with an optional inline rise style. */
46
- export function cardShell(classes: string[], rise: string | undefined, body: ElementContent[]): Element {
47
- const properties: Properties = { className: classes };
48
- if (rise) properties.style = rise;
49
- return h('section', properties, [h('div', { className: ['card-body'] }, body)]);
44
+ /** Section wrapper: `<section class=…><div class="card-body">…</div></section>`. */
45
+ export function cardShell(classes: string[], body: ElementContent[]): Element {
46
+ return h('section', { className: classes }, [h('div', { className: ['card-body'] }, body)]);
50
47
  }
51
48
 
52
49
  /** Tag the first <ul> among children with `ec-grid` and strip its whitespace-only
@@ -63,7 +60,8 @@ export function markFirstList(children: ElementContent[]): Element | undefined {
63
60
  }
64
61
 
65
62
  // Recurse into a node's children, transforming any nested primitive sections
66
- // (a grid inside a card, panels inside a split) WITHOUT a rise stagger.
63
+ // (a grid inside a card, panels inside a split). Nested primitives never carry the
64
+ // entrance stagger; only top-level ones do (stamped in the transformer below).
67
65
  function transformChildren(children: ElementContent[], registry: ComponentRegistry): ElementContent[] {
68
66
  return children.map((c) => {
69
67
  if (isElement(c) && c.properties?.dataPrimitive) return transformNode(c, registry);
@@ -72,23 +70,27 @@ function transformChildren(children: ElementContent[], registry: ComponentRegist
72
70
  });
73
71
  }
74
72
 
75
- function transformNode(node: Element, registry: ComponentRegistry, rise?: string): Element {
73
+ function transformNode(node: Element, registry: ComponentRegistry): Element {
76
74
  node.children = transformChildren(node.children as ElementContent[], registry);
77
75
  const name = strProp(node, 'dataPrimitive');
78
76
  const def = name ? registry.get(name) : undefined;
79
- return def ? def.build(node, rise) : node;
77
+ return def ? def.build(node) : node;
80
78
  }
81
79
 
82
80
  /** Rehype transformer: dispatch each stamped element through its registry `build`
83
- * fn. Top-level primitives get a document-order rise stagger when `rise` is
84
- * supplied (a site's per-index motion formula); nested ones don't. Non-primitive
81
+ * fn. When `stagger` is on, each top-level primitive gets a `data-rise` attribute
82
+ * carrying its document-order index (0, 1, 2, …); the site's CSS maps that ordinal
83
+ * to an entrance delay. The index is inert, so a consumer's sanitize floor can keep
84
+ * `data-rise` while dropping `style`. Nested primitives never get it. Non-primitive
85
85
  * content (lede, intro paragraphs, the page-toc nav) passes through untouched. */
86
- export function rehypeDispatch(registry: ComponentRegistry, rise?: (idx: number) => string) {
86
+ export function rehypeDispatch(registry: ComponentRegistry, stagger?: boolean) {
87
87
  return (tree: Root) => {
88
88
  let idx = 0;
89
89
  tree.children = (tree.children as ElementContent[]).map((child) => {
90
90
  if (isElement(child) && child.properties?.dataPrimitive) {
91
- return transformNode(child, registry, rise ? rise(idx++) : undefined);
91
+ const el = transformNode(child, registry);
92
+ if (stagger) el.properties = { ...el.properties, dataRise: String(idx++) };
93
+ return el;
92
94
  }
93
95
  if (isElement(child)) child.children = transformChildren(child.children as ElementContent[], registry);
94
96
  return child;