@pyreon/document-primitives 0.13.0 → 0.14.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/lib/index.d.ts CHANGED
@@ -81,10 +81,10 @@ declare const DocButton: _pyreon_rocketstyle0.RocketStyleComponent<Partial<{
81
81
  }> & _pyreon_core0.PyreonHTMLAttributes<HTMLElement>, {
82
82
  href?: string;
83
83
  }, {}, {
84
- padding: string;
85
84
  borderRadius: number;
86
85
  fontSize: number;
87
86
  fontWeight: string;
87
+ padding: string;
88
88
  textAlign: string;
89
89
  textDecoration: string;
90
90
  }, {
@@ -118,10 +118,10 @@ declare const DocCode: _pyreon_rocketstyle0.RocketStyleComponent<Partial<{
118
118
  language?: string;
119
119
  }, {}, {
120
120
  backgroundColor: string;
121
- padding: string;
122
121
  borderRadius: number;
123
122
  fontFamily: string;
124
123
  fontSize: number;
124
+ padding: string;
125
125
  }, {
126
126
  _documentType: "code";
127
127
  }, {}, {
@@ -354,9 +354,9 @@ declare const DocHeading: _pyreon_rocketstyle0.RocketStyleComponent<Partial<{
354
354
  }> & _pyreon_core0.PyreonHTMLAttributes<HTMLElement>, {
355
355
  level?: string;
356
356
  }, {}, {
357
+ marginBottom: number;
357
358
  color: string;
358
359
  fontWeight: string;
359
- marginBottom: number;
360
360
  }, {
361
361
  _documentType: "heading";
362
362
  }, {}, {
@@ -423,9 +423,9 @@ declare const DocImage: _pyreon_rocketstyle0.RocketStyleComponent<Partial<{
423
423
  beforeContentCss: _pyreon_elements0.ExtendCss;
424
424
  afterContentCss: _pyreon_elements0.ExtendCss;
425
425
  }> & _pyreon_core0.PyreonHTMLAttributes<HTMLElement>, {
426
- width?: number | string;
427
426
  caption?: string;
428
427
  height?: number | string;
428
+ width?: number | string;
429
429
  src?: string;
430
430
  alt?: string;
431
431
  }, {}, {}, {
@@ -694,10 +694,10 @@ declare const DocQuote: _pyreon_rocketstyle0.RocketStyleComponent<Partial<{
694
694
  }> & _pyreon_core0.PyreonHTMLAttributes<HTMLElement>, {
695
695
  borderColor?: string;
696
696
  }, {}, {
697
- padding: string;
698
697
  borderColor: string;
699
698
  color: string;
700
699
  fontStyle: string;
700
+ padding: string;
701
701
  }, {
702
702
  _documentType: "quote";
703
703
  }, {}, {
@@ -944,9 +944,9 @@ declare const DocText: _pyreon_rocketstyle0.RocketStyleComponent<Partial<{
944
944
  tag: _pyreon_ui_core0.HTMLTextTags;
945
945
  css: _pyreon_elements0.ExtendCss;
946
946
  }> & _pyreon_core0.PyreonHTMLAttributes<HTMLElement>, {}, {}, {
947
+ marginBottom: number;
947
948
  color: string;
948
949
  lineHeight: number;
949
- marginBottom: number;
950
950
  }, {
951
951
  _documentType: "text";
952
952
  }, {}, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/document-primitives",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Rocketstyle document components — render in browser, export to 18 formats",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -41,28 +41,29 @@
41
41
  "typecheck": "tsc --noEmit"
42
42
  },
43
43
  "dependencies": {
44
- "@pyreon/connector-document": "^0.13.0"
44
+ "@pyreon/connector-document": "^0.14.0"
45
45
  },
46
46
  "devDependencies": {
47
- "@pyreon/core": "^0.13.0",
48
- "@pyreon/elements": "^0.13.0",
49
- "@pyreon/reactivity": "^0.13.0",
50
- "@pyreon/rocketstyle": "^0.13.0",
51
- "@pyreon/runtime-dom": "^0.13.0",
52
- "@pyreon/styler": "^0.13.0",
53
- "@pyreon/test-utils": "^0.13.0",
54
- "@pyreon/typescript": "^0.13.0",
55
- "@pyreon/ui-core": "^0.13.0",
47
+ "@pyreon/core": "^0.14.0",
48
+ "@pyreon/elements": "^0.14.0",
49
+ "@pyreon/manifest": "0.13.1",
50
+ "@pyreon/reactivity": "^0.14.0",
51
+ "@pyreon/rocketstyle": "^0.14.0",
52
+ "@pyreon/runtime-dom": "^0.14.0",
53
+ "@pyreon/styler": "^0.14.0",
54
+ "@pyreon/test-utils": "^0.13.2",
55
+ "@pyreon/typescript": "^0.14.0",
56
+ "@pyreon/ui-core": "^0.14.0",
56
57
  "@vitest/browser-playwright": "^4.1.4",
57
58
  "@vitus-labs/tools-rolldown": "^1.15.4"
58
59
  },
59
60
  "peerDependencies": {
60
- "@pyreon/core": "^0.13.0",
61
- "@pyreon/document": "^0.13.0",
62
- "@pyreon/elements": "^0.13.0",
63
- "@pyreon/rocketstyle": "^0.13.0",
64
- "@pyreon/styler": "^0.13.0",
65
- "@pyreon/ui-core": "^0.13.0"
61
+ "@pyreon/core": "^0.14.0",
62
+ "@pyreon/document": "^0.14.0",
63
+ "@pyreon/elements": "^0.14.0",
64
+ "@pyreon/rocketstyle": "^0.14.0",
65
+ "@pyreon/styler": "^0.14.0",
66
+ "@pyreon/ui-core": "^0.14.0"
66
67
  },
67
68
  "engines": {
68
69
  "node": ">= 22"
@@ -0,0 +1,45 @@
1
+ import {
2
+ renderApiReferenceEntries,
3
+ renderLlmsFullSection,
4
+ renderLlmsTxtLine,
5
+ } from '@pyreon/manifest'
6
+ import manifest from '../manifest'
7
+
8
+ describe('gen-docs — document-primitives snapshot', () => {
9
+ it('renders a llms.txt bullet starting with the package prefix', () => {
10
+ const line = renderLlmsTxtLine(manifest)
11
+ expect(line.startsWith('- @pyreon/document-primitives —')).toBe(true)
12
+ })
13
+
14
+ it('renders a llms-full.txt section with the right header', () => {
15
+ const section = renderLlmsFullSection(manifest)
16
+ expect(section.startsWith('## @pyreon/document-primitives —')).toBe(true)
17
+ expect(section).toContain('```typescript')
18
+ })
19
+
20
+ it('renders MCP api-reference entries for every api[] item', () => {
21
+ const record = renderApiReferenceEntries(manifest)
22
+ expect(Object.keys(record).sort()).toEqual([
23
+ 'document-primitives/DocButton',
24
+ 'document-primitives/DocCode',
25
+ 'document-primitives/DocColumn',
26
+ 'document-primitives/DocDivider',
27
+ 'document-primitives/DocDocument',
28
+ 'document-primitives/DocHeading',
29
+ 'document-primitives/DocImage',
30
+ 'document-primitives/DocLink',
31
+ 'document-primitives/DocList',
32
+ 'document-primitives/DocListItem',
33
+ 'document-primitives/DocPage',
34
+ 'document-primitives/DocPageBreak',
35
+ 'document-primitives/DocQuote',
36
+ 'document-primitives/DocRow',
37
+ 'document-primitives/DocSection',
38
+ 'document-primitives/DocSpacer',
39
+ 'document-primitives/DocTable',
40
+ 'document-primitives/DocText',
41
+ 'document-primitives/createDocumentExport',
42
+ 'document-primitives/extractDocNode',
43
+ ])
44
+ })
45
+ })
@@ -14,7 +14,7 @@ describe('DocDocument attrs', () => {
14
14
  it('sets tag to div', async () => {
15
15
  const DocDocument = (await import('../primitives/DocDocument')).default
16
16
  const result = renderProps(DocDocument, { children: 'test' })
17
- expect(result.tag).toBe('div')
17
+ expect(result.as).toBe('div')
18
18
  })
19
19
 
20
20
  it('passes title to _documentProps', async () => {
@@ -158,7 +158,7 @@ describe('DocTable attrs', () => {
158
158
  it('sets tag to table', async () => {
159
159
  const DocTable = (await import('../primitives/DocTable')).default
160
160
  const result = renderProps(DocTable, { children: null })
161
- expect(result.tag).toBe('table')
161
+ expect(result.as).toBe('table')
162
162
  })
163
163
 
164
164
  it('defaults columns and rows to empty arrays', async () => {
@@ -195,13 +195,13 @@ describe('DocList attrs', () => {
195
195
  it('sets tag to ul by default', async () => {
196
196
  const DocList = (await import('../primitives/DocList')).default
197
197
  const result = renderProps(DocList, { children: null })
198
- expect(result.tag).toBe('ul')
198
+ expect(result.as).toBe('ul')
199
199
  })
200
200
 
201
201
  it('sets tag to ol when ordered is true', async () => {
202
202
  const DocList = (await import('../primitives/DocList')).default
203
203
  const result = renderProps(DocList, { ordered: true, children: null })
204
- expect(result.tag).toBe('ol')
204
+ expect(result.as).toBe('ol')
205
205
  expect(result._documentProps.ordered).toBe(true)
206
206
  })
207
207
 
@@ -278,7 +278,7 @@ describe('DocPage attrs', () => {
278
278
  it('sets tag to div', async () => {
279
279
  const DocPage = (await import('../primitives/DocPage')).default
280
280
  const result = renderProps(DocPage, { children: 'page' })
281
- expect(result.tag).toBe('div')
281
+ expect(result.as).toBe('div')
282
282
  })
283
283
 
284
284
  it('passes size and orientation when provided', async () => {
@@ -306,7 +306,7 @@ describe('DocPageBreak attrs', () => {
306
306
  it('sets tag to div with empty _documentProps', async () => {
307
307
  const DocPageBreak = (await import('../primitives/DocPageBreak')).default
308
308
  const result = renderProps(DocPageBreak, { children: null })
309
- expect(result.tag).toBe('div')
309
+ expect(result.as).toBe('div')
310
310
  expect(result._documentProps).toEqual({})
311
311
  })
312
312
  })
@@ -318,7 +318,7 @@ describe('DocQuote attrs', () => {
318
318
  it('sets tag to blockquote', async () => {
319
319
  const DocQuote = (await import('../primitives/DocQuote')).default
320
320
  const result = renderProps(DocQuote, { children: 'quote' })
321
- expect(result.tag).toBe('blockquote')
321
+ expect(result.as).toBe('blockquote')
322
322
  })
323
323
 
324
324
  it('passes borderColor when provided', async () => {
@@ -341,7 +341,7 @@ describe('DocRow attrs', () => {
341
341
  it('sets tag to div with empty _documentProps', async () => {
342
342
  const DocRow = (await import('../primitives/DocRow')).default
343
343
  const result = renderProps(DocRow, { children: null })
344
- expect(result.tag).toBe('div')
344
+ expect(result.as).toBe('div')
345
345
  expect(result._documentProps).toEqual({})
346
346
  })
347
347
  })
@@ -353,7 +353,7 @@ describe('DocColumn attrs', () => {
353
353
  it('sets tag to div', async () => {
354
354
  const DocColumn = (await import('../primitives/DocColumn')).default
355
355
  const result = renderProps(DocColumn, { children: null })
356
- expect(result.tag).toBe('div')
356
+ expect(result.as).toBe('div')
357
357
  })
358
358
 
359
359
  it('passes width to _documentProps when provided', async () => {
@@ -376,7 +376,7 @@ describe('DocSpacer attrs', () => {
376
376
  it('sets tag to div', async () => {
377
377
  const DocSpacer = (await import('../primitives/DocSpacer')).default
378
378
  const result = renderProps(DocSpacer, { children: null })
379
- expect(result.tag).toBe('div')
379
+ expect(result.as).toBe('div')
380
380
  })
381
381
 
382
382
  it('defaults height to 16', async () => {
@@ -399,7 +399,7 @@ describe('DocSection attrs', () => {
399
399
  it('sets tag to div', async () => {
400
400
  const DocSection = (await import('../primitives/DocSection')).default
401
401
  const result = renderProps(DocSection, { children: null })
402
- expect(result.tag).toBe('div')
402
+ expect(result.as).toBe('div')
403
403
  })
404
404
 
405
405
  it('defaults direction to column', async () => {
@@ -445,7 +445,7 @@ describe('DocumentPreview attrs', () => {
445
445
  it('sets tag to div', async () => {
446
446
  const DocumentPreview = (await import('../DocumentPreview')).default
447
447
  const result = renderProps(DocumentPreview, { children: null })
448
- expect(result.tag).toBe('div')
448
+ expect(result.as).toBe('div')
449
449
  })
450
450
 
451
451
  it('defaults size to A4 when not provided', async () => {
@@ -1,17 +1,22 @@
1
1
  import type { DocumentMarker } from '@pyreon/connector-document'
2
+ import { h } from '@pyreon/core'
2
3
  import { describe, expect, it } from 'vitest'
3
4
  import { createDocumentExport, extractDocNode } from '../useDocumentExport'
4
5
 
5
- // Mock VNode
6
- const vnode = (
6
+ // Helper: build a real VNode via @pyreon/core's h(). The third arg
7
+ // is an array (kept for parity with the prior mock helper) and
8
+ // spreads into h()'s varargs. Tests run real-h() VNodes through
9
+ // the export pipeline — the mock-only path masked PR #197's silent
10
+ // metadata drop bug.
11
+ const node = (
7
12
  type: string | ((...args: any[]) => any),
8
13
  props: Record<string, any> = {},
9
14
  children: unknown[] = [],
10
- ) => ({ type, props, children })
15
+ ) => h(type as any, props, ...(children as any[])) as any
11
16
 
12
- // Mock document-marked component
17
+ // Document-marked component
13
18
  const docComponent = (docType: string) => {
14
- const fn = (props: any) => vnode('div', props, props.children ? [props.children] : [])
19
+ const fn = (props: any) => node('div', props, props.children ? [props.children] : [])
15
20
  ;(fn as any)._documentType = docType
16
21
  return fn as ((...args: any[]) => any) & DocumentMarker
17
22
  }
@@ -23,8 +28,8 @@ const DocText = docComponent('text')
23
28
  describe('createDocumentExport', () => {
24
29
  it('extracts a document tree from template function', () => {
25
30
  const doc = createDocumentExport(() =>
26
- vnode(DocDocument, { _documentProps: { title: 'Test' } }, [
27
- vnode(
31
+ node(DocDocument, { _documentProps: { title: 'Test' } }, [
32
+ node(
28
33
  DocHeading,
29
34
  {
30
35
  $rocketstyle: { fontSize: 24, fontWeight: 'bold' },
@@ -32,7 +37,7 @@ describe('createDocumentExport', () => {
32
37
  },
33
38
  ['Hello'],
34
39
  ),
35
- vnode(
40
+ node(
36
41
  DocText,
37
42
  {
38
43
  $rocketstyle: { fontSize: 14, color: '#333' },
@@ -62,7 +67,7 @@ describe('createDocumentExport', () => {
62
67
 
63
68
  it('can be called multiple times', () => {
64
69
  const doc = createDocumentExport(() =>
65
- vnode(DocText, { $rocketstyle: { fontSize: 14 } }, ['Static']),
70
+ node(DocText, { $rocketstyle: { fontSize: 14 } }, ['Static']),
66
71
  )
67
72
 
68
73
  const tree1 = doc.getDocNode()
@@ -74,7 +79,7 @@ describe('createDocumentExport', () => {
74
79
 
75
80
  it('respects includeStyles option', () => {
76
81
  const doc = createDocumentExport(
77
- () => vnode(DocHeading, { $rocketstyle: { fontSize: 24 } }, ['Hello']),
82
+ () => node(DocHeading, { $rocketstyle: { fontSize: 24 } }, ['Hello']),
78
83
  { includeStyles: false },
79
84
  )
80
85
 
@@ -99,8 +104,8 @@ describe('extractDocNode (one-step alias)', () => {
99
104
 
100
105
  it('extracts a tree from a template function in one call', () => {
101
106
  const tree = extractDocNode(() =>
102
- vnode(DocDocument, { _documentProps: { title: 'Test' } }, [
103
- vnode(DocHeading, { _documentProps: { level: 1 } }, ['Hello']),
107
+ node(DocDocument, { _documentProps: { title: 'Test' } }, [
108
+ node(DocHeading, { _documentProps: { level: 1 } }, ['Hello']),
104
109
  ]),
105
110
  )
106
111
 
@@ -114,7 +119,7 @@ describe('extractDocNode (one-step alias)', () => {
114
119
 
115
120
  it('respects extraction options (includeStyles: false)', () => {
116
121
  const tree = extractDocNode(
117
- () => vnode(DocHeading, { $rocketstyle: { fontSize: 24 } }, ['Hello']),
122
+ () => node(DocHeading, { $rocketstyle: { fontSize: 24 } }, ['Hello']),
118
123
  { includeStyles: false },
119
124
  )
120
125
  expect(tree.styles).toBeUndefined()
@@ -125,7 +130,7 @@ describe('extractDocNode (one-step alias)', () => {
125
130
  // one-step form internally, so they MUST produce identical
126
131
  // output for identical input.
127
132
  const template = () =>
128
- vnode(DocText, { $rocketstyle: { fontSize: 14 } }, ['Hello'])
133
+ node(DocText, { $rocketstyle: { fontSize: 14 } }, ['Hello'])
129
134
 
130
135
  const oneStep = extractDocNode(template)
131
136
  const twoStep = createDocumentExport(template).getDocNode()
@@ -150,12 +155,12 @@ describe('extractDocNode (one-step alias)', () => {
150
155
  // catch the regression. The three extractions should be deeply
151
156
  // equal under any change to the primitive's setup path.
152
157
  const template = () =>
153
- vnode(
158
+ node(
154
159
  DocDocument,
155
160
  { _documentProps: { title: 'Idempotent', author: 'Test' } },
156
161
  [
157
- vnode(DocHeading, { _documentProps: { level: 1 } }, ['Hello']),
158
- vnode(DocText, { $rocketstyle: { fontSize: 14 } }, ['World']),
162
+ node(DocHeading, { _documentProps: { level: 1 } }, ['Hello']),
163
+ node(DocText, { $rocketstyle: { fontSize: 14 } }, ['World']),
159
164
  ],
160
165
  )
161
166
 
@@ -174,7 +179,7 @@ describe('extractDocNode (one-step alias)', () => {
174
179
  // value of any accessor in _documentProps.
175
180
  let counter = 0
176
181
  const template = () =>
177
- vnode(
182
+ node(
178
183
  DocDocument,
179
184
  {
180
185
  _documentProps: {
@@ -312,4 +317,50 @@ describe('DocDocument reactive metadata (D1 integration)', () => {
312
317
  cleanup()
313
318
  }
314
319
  })
320
+
321
+ it('extractDocNode does NOT invoke a real DocDocument component (T3.1 Path C)', { timeout: 30_000 }, async () => {
322
+ // The architectural invariant locked in by T3.1 (PR #321):
323
+ // `extractDocumentTree` consumes a real rocketstyle primitive's
324
+ // `__rs_attrs` chain directly, never invoking the wrapped component
325
+ // function. The previous Path B workaround had to call the full
326
+ // styled wrapper per export to read post-attrs `_documentProps`.
327
+ //
328
+ // Spy mechanism: wrap the imported component in a Proxy whose
329
+ // `apply` trap counts function-call invocations. Property reads
330
+ // (IS_ROCKETSTYLE, __rs_attrs, _documentType, .meta, .displayName,
331
+ // etc.) flow through to the original via the default `get` handler,
332
+ // so extractDocumentTree's contract still works — but any code
333
+ // path that CALLS the spied component bumps the counter.
334
+ //
335
+ // The connector-document tests already pin this with a
336
+ // `FakeRocketDoc` fixture; this test pins it for a real
337
+ // rocketstyle primitive end-to-end, closing the abstract /
338
+ // concrete coverage gap.
339
+ const { initTestConfig } = await import('@pyreon/test-utils')
340
+ const { h } = await import('@pyreon/core')
341
+ const cleanup = initTestConfig()
342
+ try {
343
+ const RealDocDocument = (await import('../primitives/DocDocument')).default
344
+
345
+ let callCount = 0
346
+ const SpiedDoc = new Proxy(RealDocDocument, {
347
+ apply(target, thisArg, args) {
348
+ callCount++
349
+ return Reflect.apply(target as (...a: unknown[]) => unknown, thisArg, args as unknown[])
350
+ },
351
+ })
352
+
353
+ const tree = extractDocNode(() =>
354
+ h(SpiedDoc as never, { title: 'Hoisted', author: 'Aisha' } as never),
355
+ )
356
+
357
+ expect(tree.type).toBe('document')
358
+ expect(tree.props.title).toBe('Hoisted')
359
+ expect(tree.props.author).toBe('Aisha')
360
+ // The architectural assertion — the styled wrapper must NOT run.
361
+ expect(callCount).toBe(0)
362
+ } finally {
363
+ cleanup()
364
+ }
365
+ })
315
366
  })
@@ -0,0 +1,388 @@
1
+ import { defineManifest } from '@pyreon/manifest'
2
+
3
+ export default defineManifest({
4
+ name: '@pyreon/document-primitives',
5
+ title: 'Document Primitives',
6
+ tagline:
7
+ '18 rocketstyle document components — render in browser AND export to 14+ formats via the same tree',
8
+ description:
9
+ '18 rocketstyle-based document primitives — `DocDocument`, `DocPage`, `DocSection`, `DocRow`, `DocColumn`, `DocHeading`, `DocText`, `DocLink`, `DocImage`, `DocTable`, `DocList`, `DocListItem`, `DocCode`, `DocDivider`, `DocSpacer`, `DocButton`, `DocQuote`, `DocPageBreak`. The same JSX tree renders in the browser AND exports to 14+ output formats (PDF, DOCX, XLSX, PPTX, HTML, Markdown, email, Slack, Teams, etc.). Primitives carry `_documentType` static markers; `extractDocumentTree` (from `@pyreon/connector-document`) walks the tree to produce a `DocNode` for `@pyreon/document`\\\'s `render()` to consume. `DocDocument` accepts reactive accessors for `title` / `author` / `subject` — function values are stored in `_documentProps` and resolved at extraction time so each export click reads the LIVE value from the underlying signal.',
10
+ category: 'browser',
11
+ features: [
12
+ '18 primitives covering structure, text, lists, tables, code, layout',
13
+ 'Same component tree renders in browser AND exports to 14+ formats',
14
+ 'extractDocNode(templateFn) — one-step extraction (recommended)',
15
+ 'createDocumentExport(templateFn) — two-step form (backward compat)',
16
+ 'DocDocument accepts reactive accessors for title / author / subject',
17
+ 'PR #197 fix: extractDocumentTree now calls rocketstyle components to read post-attrs metadata',
18
+ 'Layout props in .attrs() (direction / gap), CSS in .theme()',
19
+ ],
20
+ longExample: `import {
21
+ DocDocument, DocPage, DocSection, DocRow, DocColumn,
22
+ DocHeading, DocText, DocLink, DocImage, DocTable,
23
+ DocList, DocListItem, DocCode, DocDivider, DocSpacer,
24
+ DocButton, DocQuote, DocPageBreak,
25
+ extractDocNode,
26
+ } from '@pyreon/document-primitives'
27
+ import { download } from '@pyreon/document'
28
+
29
+ interface Resume { name: string; headline: string }
30
+
31
+ function ResumeTemplate(props: { resume: () => Resume }) {
32
+ return (
33
+ // title and author accept reactive accessors — extractDocNode
34
+ // resolves them at extraction time, so each export click reads
35
+ // the LIVE value from the underlying signal
36
+ <DocDocument
37
+ title={() => \`\${props.resume().name} — Resume\`}
38
+ author={() => props.resume().name}
39
+ >
40
+ <DocPage>
41
+ <DocSection>
42
+ <DocHeading level="h1">{() => props.resume().name}</DocHeading>
43
+ <DocText>{() => props.resume().headline}</DocText>
44
+ </DocSection>
45
+ </DocPage>
46
+ </DocDocument>
47
+ )
48
+ }
49
+
50
+ // One-step extraction → render to any of 14+ formats
51
+ const tree = extractDocNode(() => <ResumeTemplate resume={store.resume} />)
52
+ await download(tree, 'resume.pdf')
53
+ await download(tree, 'resume.docx')
54
+ await download(tree, 'resume.html')
55
+ await download(tree, 'resume.md')`,
56
+ api: [
57
+ {
58
+ name: 'extractDocNode',
59
+ kind: 'function',
60
+ signature: 'extractDocNode(templateFn: () => VNode, options?: ExtractOptions): DocNode',
61
+ summary:
62
+ "18 primitives: `DocDocument`, `DocPage`, `DocSection`, `DocRow`, `DocColumn`, `DocHeading`, `DocText`, `DocLink`, `DocImage`, `DocTable`, `DocList`, `DocListItem`, `DocCode`, `DocDivider`, `DocSpacer`, `DocButton`, `DocQuote`, `DocPageBreak`. Same component tree renders in browser AND exports — primitives carry `_documentType` statics that `extractDocumentTree` (from `@pyreon/connector-document`) walks to produce a `DocNode` for `@pyreon/document`\\\'s `render()` to consume. `DocDocument`\\\'s `title` / `author` / `subject` accept either a string OR a `() => string` accessor; function values are stored in `_documentProps` and resolved at extraction time so reactive metadata works without `const initial = get()` workarounds. PR #197 also fixed a latent bug in `extractDocumentTree`: it now CALLS rocketstyle component functions to read post-attrs `_documentProps`, where before it only looked at the JSX vnode\\\'s props directly — every primitive\\\'s metadata was silently dropped during export until that fix landed.",
63
+ example: `import {
64
+ DocDocument, DocPage, DocHeading, DocText,
65
+ extractDocNode,
66
+ } from '@pyreon/document-primitives'
67
+ import { download } from '@pyreon/document'
68
+
69
+ const tree = extractDocNode(() => (
70
+ <DocDocument title="Quarterly Report" author="Aisha">
71
+ <DocPage>
72
+ <DocHeading level="h1">Q4 Results</DocHeading>
73
+ <DocText>Revenue grew 23% YoY.</DocText>
74
+ </DocPage>
75
+ </DocDocument>
76
+ ))
77
+ await download(tree, 'report.pdf')
78
+ await download(tree, 'report.docx')`,
79
+ mistakes: [
80
+ 'Calling `props.title()` at the top of a template body to "fix" reactivity — components run ONCE at mount, so this captures the initial value forever. Pass the accessor through to DocDocument as-is: `<DocDocument title={() => get().name}>`',
81
+ "DocRow direction: layout props (direction, gap) go in `.attrs()` not `.theme()`. Element accepts `'inline'` | `'rows'` | `'reverseInline'` | `'reverseRows'` — `'row'` is NOT valid",
82
+ "For text children reactivity, pass a signal accessor and read inside body: `<DocText>{() => store.field()}</DocText>`",
83
+ "Don't declare runtime-filled fields (`tag`, `_documentProps`) in the rocketstyle `.attrs<P>()` generic — they leak as required JSX props",
84
+ 'Using `createDocumentExport(...).getDocNode()` in new code — prefer `extractDocNode(fn)` which is one call instead of two. `createDocumentExport` is kept for backward compat',
85
+ ],
86
+ seeAlso: ['createDocumentExport'],
87
+ },
88
+ {
89
+ name: 'createDocumentExport',
90
+ kind: 'function',
91
+ signature:
92
+ 'createDocumentExport(templateFn: () => VNode): { getDocNode(): DocNode }',
93
+ summary:
94
+ 'Wrapper around `extractDocNode`. The wrapper-object form is kept for callers that want to pass the helper around (e.g. to wrapper components that take a `DocumentExport` instance). New code should use `extractDocNode(templateFn)` which is one call instead of two.',
95
+ example: `// Two-step form (kept for backward compat). New code should
96
+ // prefer the one-step extractDocNode helper.
97
+ import { createDocumentExport } from '@pyreon/document-primitives'
98
+
99
+ const helper = createDocumentExport(() => <Resume name="Aisha" />)
100
+ const tree = helper.getDocNode()`,
101
+ seeAlso: ['extractDocNode'],
102
+ },
103
+ {
104
+ name: 'DocDocument',
105
+ kind: 'component',
106
+ signature:
107
+ '(props: { title?: string | (() => string); author?: string | (() => string); subject?: string | (() => string); children: VNodeChild }) => VNodeChild',
108
+ summary:
109
+ 'Root container for a document tree — produces a `_documentType: "document"` node. Accepts optional metadata: `title`, `author`, `subject`. Each accepts either a plain string OR a `() => string` accessor; function values are stored in `_documentProps` and resolved at extraction time so each export call reads the LIVE value from any underlying signal.',
110
+ example: `<DocDocument title="Quarterly Report" author="Aisha" subject="Q4 2025">
111
+ <DocPage>...</DocPage>
112
+ </DocDocument>
113
+
114
+ // Reactive metadata via accessor
115
+ <DocDocument title={() => \`\${user().name} — Resume\`}>
116
+ <DocPage>...</DocPage>
117
+ </DocDocument>`,
118
+ seeAlso: ['DocPage', 'extractDocNode'],
119
+ },
120
+ {
121
+ name: 'DocPage',
122
+ kind: 'component',
123
+ signature:
124
+ "(props: { size?: string; orientation?: 'portrait' | 'landscape'; children: VNodeChild }) => VNodeChild",
125
+ summary:
126
+ 'A page boundary inside a `DocDocument`. Paginated outputs (PDF, DOCX) treat each `DocPage` as a separate page; flow outputs (HTML, Markdown) render the contents inline with no page boundary. `size` and `orientation` configure paginated formats — common values: `"A4"`, `"Letter"`, `"Legal"`.',
127
+ example: `<DocDocument>
128
+ <DocPage size="A4" orientation="portrait">
129
+ <DocHeading level="h1">Page 1</DocHeading>
130
+ </DocPage>
131
+ <DocPage size="A4" orientation="landscape">
132
+ <DocHeading level="h1">Page 2 — landscape</DocHeading>
133
+ </DocPage>
134
+ </DocDocument>`,
135
+ seeAlso: ['DocDocument', 'DocPageBreak'],
136
+ },
137
+ {
138
+ name: 'DocSection',
139
+ kind: 'component',
140
+ signature:
141
+ "(props: { direction?: 'column' | 'row'; children: VNodeChild }) => VNodeChild",
142
+ summary:
143
+ 'Semantic grouping inside a page. Default `direction` is `"column"` (children stack vertically); `"row"` arranges them horizontally. Use to group related content for visual rhythm and for export targets that emit semantic section markers (HTML `<section>`, DOCX section breaks).',
144
+ example: `<DocPage>
145
+ <DocSection direction="column">
146
+ <DocHeading level="h2">Introduction</DocHeading>
147
+ <DocText>Background paragraph.</DocText>
148
+ </DocSection>
149
+ </DocPage>`,
150
+ seeAlso: ['DocRow', 'DocColumn'],
151
+ },
152
+ {
153
+ name: 'DocRow',
154
+ kind: 'component',
155
+ signature: '(props: { children: VNodeChild }) => VNodeChild',
156
+ summary:
157
+ 'Horizontal layout container — children flow inline with a fixed 8px gap. Use for side-by-side content (label + value pairs, columns of metadata, button rows). Layout-only — no user-configurable props on this primitive; for columns with custom widths use `DocColumn` inside.',
158
+ example: `<DocRow>
159
+ <DocText>Name:</DocText>
160
+ <DocText>Aisha Patel</DocText>
161
+ </DocRow>`,
162
+ seeAlso: ['DocColumn', 'DocSection'],
163
+ },
164
+ {
165
+ name: 'DocColumn',
166
+ kind: 'component',
167
+ signature:
168
+ '(props: { width?: number | string; children: VNodeChild }) => VNodeChild',
169
+ summary:
170
+ 'A column inside a row layout. Optional `width` controls the column\\\'s share of the row — accepts a number (interpreted as pixels) or a string (`"50%"`, `"1fr"`). When omitted, columns share available width equally. Most common shape is `<DocRow><DocColumn width="30%" /> <DocColumn width="70%" /></DocRow>`.',
171
+ example: `<DocRow>
172
+ <DocColumn width="30%">
173
+ <DocText>Label</DocText>
174
+ </DocColumn>
175
+ <DocColumn width="70%">
176
+ <DocText>Value</DocText>
177
+ </DocColumn>
178
+ </DocRow>`,
179
+ seeAlso: ['DocRow', 'DocSection'],
180
+ },
181
+ {
182
+ name: 'DocHeading',
183
+ kind: 'component',
184
+ signature:
185
+ "(props: { level?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; children: VNodeChild }) => VNodeChild",
186
+ summary:
187
+ 'Heading text — `level` (`"h1"` through `"h6"`) controls both visual size and the semantic level emitted to outputs (HTML `<h1>...<h6>`, DOCX heading styles, Markdown `#`...`######`). Default `level` is `"h1"`. Used for document structure that downstream tooling can build a TOC from.',
188
+ example: `<DocHeading level="h1">Quarterly Report</DocHeading>
189
+ <DocHeading level="h2">Q4 Results</DocHeading>
190
+ <DocHeading level="h3">Revenue Breakdown</DocHeading>`,
191
+ seeAlso: ['DocText', 'DocSection'],
192
+ },
193
+ {
194
+ name: 'DocText',
195
+ kind: 'component',
196
+ signature: '(props: { children: VNodeChild }) => VNodeChild',
197
+ summary:
198
+ 'Paragraph / inline text. The most common primitive — wraps any text content for the document. Children may be string literals OR signal accessors (`{() => store.field()}`) for reactive content. Visual styling (font weight, variant) is controlled via rocketstyle dimension props on the wrapping component definition.',
199
+ example: `<DocText>Static paragraph content.</DocText>
200
+
201
+ // Reactive children
202
+ <DocText>{() => \`Hello, \${user().name}\`}</DocText>`,
203
+ seeAlso: ['DocHeading', 'DocLink'],
204
+ },
205
+ {
206
+ name: 'DocLink',
207
+ kind: 'component',
208
+ signature: '(props: { href?: string; children: VNodeChild }) => VNodeChild',
209
+ summary:
210
+ 'Hyperlink within text. `href` is the URL — defaults to `"#"`. Outputs that support hyperlinks (HTML, PDF, DOCX, email) render this as a clickable link; flat outputs (plain text, certain Slack variants) render the link target inline as `text (href)`.',
211
+ example: `<DocText>
212
+ Read more on
213
+ <DocLink href="https://pyreon.dev">our blog</DocLink>
214
+ for the latest releases.
215
+ </DocText>`,
216
+ seeAlso: ['DocText'],
217
+ },
218
+ {
219
+ name: 'DocImage',
220
+ kind: 'component',
221
+ signature:
222
+ '(props: { src: string; alt?: string; width?: number; height?: number; caption?: string }) => VNodeChild',
223
+ summary:
224
+ 'An image embedded in the document. `src` is the image URL or data URI. `alt` is the accessible description (also used as fallback text in non-visual outputs). `width` / `height` constrain dimensions in pixels. Optional `caption` renders a caption beneath the image.',
225
+ example: `<DocImage
226
+ src="/charts/q4-revenue.png"
227
+ alt="Revenue grew 23% in Q4"
228
+ width={600}
229
+ height={400}
230
+ caption="Figure 1: Quarterly revenue, 2024-2025"
231
+ />`,
232
+ seeAlso: ['DocCode'],
233
+ },
234
+ {
235
+ name: 'DocTable',
236
+ kind: 'component',
237
+ signature:
238
+ '(props: { columns: TableColumn[]; rows: TableRow[]; headerStyle?: object; striped?: boolean; bordered?: boolean; caption?: string }) => VNodeChild',
239
+ summary:
240
+ 'Tabular data. `columns` defines the header cells (label, key, optional alignment). `rows` is an array of data rows keyed by column key. `striped` adds alternating row backgrounds; `bordered` adds cell borders; `caption` renders an accessible table caption. Both `rows` and `columns` are filtered before reaching the DOM via `.attrs(..., { filter: [...] })` because `HTMLTableElement.rows` / `.cells` are read-only DOM properties — assignment would crash.',
241
+ example: `<DocTable
242
+ caption="Q4 results by region"
243
+ bordered
244
+ striped
245
+ columns={[
246
+ { key: 'region', label: 'Region', align: 'left' },
247
+ { key: 'revenue', label: 'Revenue', align: 'right' },
248
+ { key: 'growth', label: 'YoY Growth', align: 'right' },
249
+ ]}
250
+ rows={[
251
+ { region: 'NA', revenue: '$12.4M', growth: '+23%' },
252
+ { region: 'EU', revenue: '$8.7M', growth: '+18%' },
253
+ { region: 'APAC', revenue: '$5.1M', growth: '+41%' },
254
+ ]}
255
+ />`,
256
+ seeAlso: ['DocList', 'DocSection'],
257
+ },
258
+ {
259
+ name: 'DocList',
260
+ kind: 'component',
261
+ signature: '(props: { ordered?: boolean; children: VNodeChild }) => VNodeChild',
262
+ summary:
263
+ 'Bulleted (default) or numbered (`ordered`) list. Children are typically `DocListItem` instances. Outputs map this to the right native list type — HTML `<ul>` / `<ol>`, Markdown `-` / `1.`, DOCX list styles.',
264
+ example: `<DocList>
265
+ <DocListItem>First bullet</DocListItem>
266
+ <DocListItem>Second bullet</DocListItem>
267
+ </DocList>
268
+
269
+ <DocList ordered>
270
+ <DocListItem>First step</DocListItem>
271
+ <DocListItem>Second step</DocListItem>
272
+ </DocList>`,
273
+ seeAlso: ['DocListItem'],
274
+ },
275
+ {
276
+ name: 'DocListItem',
277
+ kind: 'component',
278
+ signature: '(props: { children: VNodeChild }) => VNodeChild',
279
+ summary:
280
+ 'Single item inside a `DocList`. Children may be plain text, `DocText`, nested `DocList` for sublists, or any other inline primitive. Visual marker (bullet vs number) is decided by the parent list\\\'s `ordered` prop, not by the item.',
281
+ example: `<DocList>
282
+ <DocListItem>Top-level item</DocListItem>
283
+ <DocListItem>
284
+ Item with nested list
285
+ <DocList>
286
+ <DocListItem>Nested A</DocListItem>
287
+ <DocListItem>Nested B</DocListItem>
288
+ </DocList>
289
+ </DocListItem>
290
+ </DocList>`,
291
+ seeAlso: ['DocList'],
292
+ },
293
+ {
294
+ name: 'DocCode',
295
+ kind: 'component',
296
+ signature: '(props: { language?: string; children: VNodeChild }) => VNodeChild',
297
+ summary:
298
+ 'Monospace code block. Optional `language` hint enables syntax highlighting in outputs that support it (HTML via Prism / Shiki, Markdown fenced code blocks with language tag). Whitespace is preserved verbatim — pass code as a single string child to keep newlines.',
299
+ example: `<DocCode language="typescript">{
300
+ \`const flow = createFlow({
301
+ nodes: [{ id: '1', position: { x: 0, y: 0 } }],
302
+ edges: [],
303
+ })\`
304
+ }</DocCode>`,
305
+ seeAlso: ['DocText'],
306
+ },
307
+ {
308
+ name: 'DocDivider',
309
+ kind: 'component',
310
+ signature: '(props: { color?: string; thickness?: number }) => VNodeChild',
311
+ summary:
312
+ 'Horizontal rule — visual section separator. `color` controls the line color (any CSS color string); `thickness` controls the line thickness in pixels. Outputs map this to native dividers — HTML `<hr>`, Markdown `---`, DOCX horizontal rule.',
313
+ example: `<DocText>Above the divider.</DocText>
314
+ <DocDivider color="#e5e7eb" thickness={1} />
315
+ <DocText>Below the divider.</DocText>`,
316
+ seeAlso: ['DocSpacer'],
317
+ },
318
+ {
319
+ name: 'DocSpacer',
320
+ kind: 'component',
321
+ signature: '(props: { height?: number }) => VNodeChild',
322
+ summary:
323
+ 'Vertical whitespace — adds a blank vertical gap. `height` is in pixels (default 16). Use to space out content beyond what `DocSection` / `DocPage` margins provide. In flow outputs this becomes a styled blank block; in plain-text outputs, a sequence of newlines.',
324
+ example: `<DocSection>
325
+ <DocHeading level="h2">Section A</DocHeading>
326
+ <DocText>Content...</DocText>
327
+ <DocSpacer height={32} />
328
+ <DocHeading level="h2">Section B</DocHeading>
329
+ <DocText>More content...</DocText>
330
+ </DocSection>`,
331
+ seeAlso: ['DocDivider'],
332
+ },
333
+ {
334
+ name: 'DocButton',
335
+ kind: 'component',
336
+ signature: '(props: { href?: string; children: VNodeChild }) => VNodeChild',
337
+ summary:
338
+ 'Call-to-action button. Renders as a styled clickable element in HTML / email outputs (mail-safe button table layout for email), and as a labeled link in PDF / DOCX. `href` is the action URL — defaults to `"#"`. Visual style (variant) is controlled via rocketstyle dimensions on the component definition.',
339
+ example: `<DocButton href="https://pyreon.dev/signup">
340
+ Get started
341
+ </DocButton>`,
342
+ seeAlso: ['DocLink'],
343
+ },
344
+ {
345
+ name: 'DocQuote',
346
+ kind: 'component',
347
+ signature: '(props: { borderColor?: string; children: VNodeChild }) => VNodeChild',
348
+ summary:
349
+ 'Block quote — sets off a quoted passage with an indented left border. `borderColor` controls the indicator stripe (any CSS color). Outputs map this to native quote styling — HTML `<blockquote>`, Markdown `> ...`, DOCX quote style.',
350
+ example: `<DocQuote borderColor="#3b82f6">
351
+ <DocText>"The best way to predict the future is to build it."</DocText>
352
+ <DocText>— Aisha Patel, Q4 keynote</DocText>
353
+ </DocQuote>`,
354
+ seeAlso: ['DocText'],
355
+ },
356
+ {
357
+ name: 'DocPageBreak',
358
+ kind: 'component',
359
+ signature: '() => VNodeChild',
360
+ summary:
361
+ 'Explicit page boundary inside a `DocPage`. Forces the renderer to start a new page at this point in paginated outputs (PDF, DOCX). In flow outputs (HTML, Markdown), it renders as visible whitespace or is omitted entirely. Use for explicit pagination control beyond what `DocPage` boundaries already provide.',
362
+ example: `<DocPage>
363
+ <DocHeading level="h1">Section 1</DocHeading>
364
+ <DocText>...long content...</DocText>
365
+ <DocPageBreak />
366
+ <DocHeading level="h1">Section 2 — new page</DocHeading>
367
+ </DocPage>`,
368
+ seeAlso: ['DocPage'],
369
+ },
370
+ ],
371
+ gotchas: [
372
+ {
373
+ label: 'Reactive metadata',
374
+ note:
375
+ '`DocDocument` `title` / `author` / `subject` accept either strings or `() => string` accessors. Function values are stored in `_documentProps` and resolved by `extractDocumentTree` at extraction time, so each export click reads the LIVE value from any underlying signal — no `const initial = get()` workaround needed.',
376
+ },
377
+ {
378
+ label: 'PR #197 framework fix',
379
+ note:
380
+ 'Before PR #197, `extractDocumentTree` only looked at the JSX vnode\\\'s direct props for `_documentProps` — but rocketstyle\\\'s attrs HOC stamps that field AFTER the component runs, so every real primitive\\\'s metadata was silently dropped during export. The extractor now CALLS the component function to capture the post-attrs vnode and reads `_documentProps` from there.',
381
+ },
382
+ {
383
+ label: 'DocTable read-only DOM property collision',
384
+ note:
385
+ '`HTMLTableElement.rows` and `.cells` are read-only DOM properties — assigning to them throws. `DocTable` uses `.attrs(callback, { filter: ["rows", "columns", ...] })` to strip these props before they reach the DOM. Watch for similar collisions when adding new primitives that accept prop names matching native HTML element properties.',
386
+ },
387
+ ],
388
+ })