@miethe/ui 0.2.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.
Files changed (192) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +863 -9
  3. package/dist/components/content-viewer/ArticleViewer.d.ts +42 -0
  4. package/dist/components/content-viewer/ArticleViewer.d.ts.map +1 -0
  5. package/dist/components/content-viewer/ArticleViewer.js +321 -0
  6. package/dist/components/content-viewer/ArticleViewer.js.map +1 -0
  7. package/dist/components/content-viewer/FrontmatterHeader.d.ts +32 -0
  8. package/dist/components/content-viewer/FrontmatterHeader.d.ts.map +1 -0
  9. package/dist/components/content-viewer/FrontmatterHeader.js +95 -0
  10. package/dist/components/content-viewer/FrontmatterHeader.js.map +1 -0
  11. package/dist/components/content-viewer/callouts/Callout.d.ts +43 -0
  12. package/dist/components/content-viewer/callouts/Callout.d.ts.map +1 -0
  13. package/dist/components/content-viewer/callouts/Callout.js +86 -0
  14. package/dist/components/content-viewer/callouts/Callout.js.map +1 -0
  15. package/dist/components/content-viewer/callouts/index.d.ts +2 -0
  16. package/dist/components/content-viewer/callouts/index.d.ts.map +1 -0
  17. package/dist/components/content-viewer/callouts/index.js +2 -0
  18. package/dist/components/content-viewer/callouts/index.js.map +1 -0
  19. package/dist/components/content-viewer/index.d.ts +21 -0
  20. package/dist/components/content-viewer/index.d.ts.map +1 -0
  21. package/dist/components/content-viewer/index.js +29 -0
  22. package/dist/components/content-viewer/index.js.map +1 -0
  23. package/dist/components/content-viewer/plugins/index.d.ts +5 -0
  24. package/dist/components/content-viewer/plugins/index.d.ts.map +1 -0
  25. package/dist/components/content-viewer/plugins/index.js +5 -0
  26. package/dist/components/content-viewer/plugins/index.js.map +1 -0
  27. package/dist/components/content-viewer/plugins/lowlightLoader.d.ts +63 -0
  28. package/dist/components/content-viewer/plugins/lowlightLoader.d.ts.map +1 -0
  29. package/dist/components/content-viewer/plugins/lowlightLoader.js +120 -0
  30. package/dist/components/content-viewer/plugins/lowlightLoader.js.map +1 -0
  31. package/dist/components/content-viewer/plugins/rehypeCodeHighlight.d.ts +44 -0
  32. package/dist/components/content-viewer/plugins/rehypeCodeHighlight.d.ts.map +1 -0
  33. package/dist/components/content-viewer/plugins/rehypeCodeHighlight.js +122 -0
  34. package/dist/components/content-viewer/plugins/rehypeCodeHighlight.js.map +1 -0
  35. package/dist/components/content-viewer/plugins/rehypeExternalLinks.d.ts +59 -0
  36. package/dist/components/content-viewer/plugins/rehypeExternalLinks.d.ts.map +1 -0
  37. package/dist/components/content-viewer/plugins/rehypeExternalLinks.js +79 -0
  38. package/dist/components/content-viewer/plugins/rehypeExternalLinks.js.map +1 -0
  39. package/dist/components/content-viewer/plugins/rehypeHeadingIds.d.ts +37 -0
  40. package/dist/components/content-viewer/plugins/rehypeHeadingIds.d.ts.map +1 -0
  41. package/dist/components/content-viewer/plugins/rehypeHeadingIds.js +82 -0
  42. package/dist/components/content-viewer/plugins/rehypeHeadingIds.js.map +1 -0
  43. package/dist/components/content-viewer/plugins/remarkCallouts.d.ts +39 -0
  44. package/dist/components/content-viewer/plugins/remarkCallouts.d.ts.map +1 -0
  45. package/dist/components/content-viewer/plugins/remarkCallouts.js +77 -0
  46. package/dist/components/content-viewer/plugins/remarkCallouts.js.map +1 -0
  47. package/dist/components/content-viewer/plugins/slugify.d.ts +24 -0
  48. package/dist/components/content-viewer/plugins/slugify.d.ts.map +1 -0
  49. package/dist/components/content-viewer/plugins/slugify.js +31 -0
  50. package/dist/components/content-viewer/plugins/slugify.js.map +1 -0
  51. package/dist/components/content-viewer/sanitize.d.ts +75 -0
  52. package/dist/components/content-viewer/sanitize.d.ts.map +1 -0
  53. package/dist/components/content-viewer/sanitize.js +252 -0
  54. package/dist/components/content-viewer/sanitize.js.map +1 -0
  55. package/dist/components/content-viewer/types.d.ts +315 -0
  56. package/dist/components/content-viewer/types.d.ts.map +1 -0
  57. package/dist/components/content-viewer/types.js +8 -0
  58. package/dist/components/content-viewer/types.js.map +1 -0
  59. package/dist/components/content-viewer/variants.d.ts +71 -0
  60. package/dist/components/content-viewer/variants.d.ts.map +1 -0
  61. package/dist/components/content-viewer/variants.js +105 -0
  62. package/dist/components/content-viewer/variants.js.map +1 -0
  63. package/dist/content-viewer/ContentPane.d.ts +44 -1
  64. package/dist/content-viewer/ContentPane.d.ts.map +1 -1
  65. package/dist/content-viewer/ContentPane.js +139 -5
  66. package/dist/content-viewer/ContentPane.js.map +1 -1
  67. package/dist/content-viewer/FileTree.d.ts +23 -1
  68. package/dist/content-viewer/FileTree.d.ts.map +1 -1
  69. package/dist/content-viewer/FileTree.js +20 -5
  70. package/dist/content-viewer/FileTree.js.map +1 -1
  71. package/dist/content-viewer/index.d.ts +2 -0
  72. package/dist/content-viewer/index.d.ts.map +1 -1
  73. package/dist/content-viewer/index.js +2 -0
  74. package/dist/content-viewer/index.js.map +1 -1
  75. package/dist/diff/DiffViewer.js +3 -3
  76. package/dist/diff/DiffViewer.js.map +1 -1
  77. package/dist/discovery/discovery-card.d.ts +25 -0
  78. package/dist/discovery/discovery-card.d.ts.map +1 -0
  79. package/dist/discovery/discovery-card.js +265 -0
  80. package/dist/discovery/discovery-card.js.map +1 -0
  81. package/dist/discovery/index.d.ts +3 -0
  82. package/dist/discovery/index.d.ts.map +1 -0
  83. package/dist/discovery/index.js +3 -0
  84. package/dist/discovery/index.js.map +1 -0
  85. package/dist/display/ContextInfoCard.d.ts +61 -0
  86. package/dist/display/ContextInfoCard.d.ts.map +1 -0
  87. package/dist/display/ContextInfoCard.js +45 -0
  88. package/dist/display/ContextInfoCard.js.map +1 -0
  89. package/dist/display/index.d.ts +2 -0
  90. package/dist/display/index.d.ts.map +1 -1
  91. package/dist/display/index.js +1 -0
  92. package/dist/display/index.js.map +1 -1
  93. package/dist/editor/CodeEditor.d.ts +39 -0
  94. package/dist/editor/CodeEditor.d.ts.map +1 -0
  95. package/dist/editor/CodeEditor.js +114 -0
  96. package/dist/editor/CodeEditor.js.map +1 -0
  97. package/dist/editor/MarkdownEditor.d.ts +3 -2
  98. package/dist/editor/MarkdownEditor.d.ts.map +1 -1
  99. package/dist/editor/MarkdownEditor.js +32 -80
  100. package/dist/editor/MarkdownEditor.js.map +1 -1
  101. package/dist/editor/SplitPreview.d.ts +10 -1
  102. package/dist/editor/SplitPreview.d.ts.map +1 -1
  103. package/dist/editor/SplitPreview.js +4 -2
  104. package/dist/editor/SplitPreview.js.map +1 -1
  105. package/dist/editor/codeLanguages.d.ts +28 -0
  106. package/dist/editor/codeLanguages.d.ts.map +1 -0
  107. package/dist/editor/codeLanguages.js +54 -0
  108. package/dist/editor/codeLanguages.js.map +1 -0
  109. package/dist/editor/index.d.ts +2 -0
  110. package/dist/editor/index.d.ts.map +1 -1
  111. package/dist/editor/index.js +6 -0
  112. package/dist/editor/index.js.map +1 -1
  113. package/dist/editor/theme.d.ts +16 -0
  114. package/dist/editor/theme.d.ts.map +1 -0
  115. package/dist/editor/theme.js +82 -0
  116. package/dist/editor/theme.js.map +1 -0
  117. package/dist/filters/filter-bar.d.ts +14 -0
  118. package/dist/filters/filter-bar.d.ts.map +1 -0
  119. package/dist/filters/filter-bar.js +47 -0
  120. package/dist/filters/filter-bar.js.map +1 -0
  121. package/dist/filters/filter-slot-config.d.ts +239 -0
  122. package/dist/filters/filter-slot-config.d.ts.map +1 -0
  123. package/dist/filters/filter-slot-config.js +24 -0
  124. package/dist/filters/filter-slot-config.js.map +1 -0
  125. package/dist/filters/index.d.ts +2 -0
  126. package/dist/filters/index.d.ts.map +1 -1
  127. package/dist/filters/index.js +1 -0
  128. package/dist/filters/index.js.map +1 -1
  129. package/dist/primitives/BatchReadinessPill.d.ts +22 -0
  130. package/dist/primitives/BatchReadinessPill.d.ts.map +1 -0
  131. package/dist/primitives/BatchReadinessPill.js +20 -0
  132. package/dist/primitives/BatchReadinessPill.js.map +1 -0
  133. package/dist/primitives/Card.d.ts +28 -0
  134. package/dist/primitives/Card.d.ts.map +1 -0
  135. package/dist/primitives/Card.js +30 -0
  136. package/dist/primitives/Card.js.map +1 -0
  137. package/dist/primitives/CollectionPicker.d.ts +47 -0
  138. package/dist/primitives/CollectionPicker.d.ts.map +1 -0
  139. package/dist/primitives/CollectionPicker.js +105 -0
  140. package/dist/primitives/CollectionPicker.js.map +1 -0
  141. package/dist/primitives/CreateEntityDialog.d.ts +144 -0
  142. package/dist/primitives/CreateEntityDialog.d.ts.map +1 -0
  143. package/dist/primitives/CreateEntityDialog.js +379 -0
  144. package/dist/primitives/CreateEntityDialog.js.map +1 -0
  145. package/dist/primitives/EffectiveStatusChips.d.ts +43 -0
  146. package/dist/primitives/EffectiveStatusChips.d.ts.map +1 -0
  147. package/dist/primitives/EffectiveStatusChips.js +23 -0
  148. package/dist/primitives/EffectiveStatusChips.js.map +1 -0
  149. package/dist/primitives/FormField.d.ts +29 -0
  150. package/dist/primitives/FormField.d.ts.map +1 -0
  151. package/dist/primitives/FormField.js +27 -0
  152. package/dist/primitives/FormField.js.map +1 -0
  153. package/dist/primitives/Label.d.ts +20 -0
  154. package/dist/primitives/Label.d.ts.map +1 -0
  155. package/dist/primitives/Label.js +21 -0
  156. package/dist/primitives/Label.js.map +1 -0
  157. package/dist/primitives/MismatchBadge.d.ts +34 -0
  158. package/dist/primitives/MismatchBadge.d.ts.map +1 -0
  159. package/dist/primitives/MismatchBadge.js +28 -0
  160. package/dist/primitives/MismatchBadge.js.map +1 -0
  161. package/dist/primitives/PlanningNodeTypeIcon.d.ts +33 -0
  162. package/dist/primitives/PlanningNodeTypeIcon.d.ts.map +1 -0
  163. package/dist/primitives/PlanningNodeTypeIcon.js +35 -0
  164. package/dist/primitives/PlanningNodeTypeIcon.js.map +1 -0
  165. package/dist/primitives/SecretField.d.ts +28 -0
  166. package/dist/primitives/SecretField.d.ts.map +1 -0
  167. package/dist/primitives/SecretField.js +65 -0
  168. package/dist/primitives/SecretField.js.map +1 -0
  169. package/dist/primitives/Spinner.d.ts +16 -0
  170. package/dist/primitives/Spinner.d.ts.map +1 -0
  171. package/dist/primitives/Spinner.js +34 -0
  172. package/dist/primitives/Spinner.js.map +1 -0
  173. package/dist/primitives/StatusChip.d.ts +17 -0
  174. package/dist/primitives/StatusChip.d.ts.map +1 -0
  175. package/dist/primitives/StatusChip.js +22 -0
  176. package/dist/primitives/StatusChip.js.map +1 -0
  177. package/dist/primitives/Switch.d.ts +32 -0
  178. package/dist/primitives/Switch.d.ts.map +1 -0
  179. package/dist/primitives/Switch.js +43 -0
  180. package/dist/primitives/Switch.js.map +1 -0
  181. package/dist/primitives/index.d.ts +28 -0
  182. package/dist/primitives/index.d.ts.map +1 -1
  183. package/dist/primitives/index.js +16 -0
  184. package/dist/primitives/index.js.map +1 -1
  185. package/dist/primitives/variants.d.ts +18 -0
  186. package/dist/primitives/variants.d.ts.map +1 -0
  187. package/dist/primitives/variants.js +33 -0
  188. package/dist/primitives/variants.js.map +1 -0
  189. package/dist/utils/type-colors.d.ts.map +1 -1
  190. package/dist/utils/type-colors.js +4 -0
  191. package/dist/utils/type-colors.js.map +1 -1
  192. package/package.json +40 -6
@@ -0,0 +1,42 @@
1
+ import type { ArticleViewerProps } from './types';
2
+ /**
3
+ * Detect whether content is markdown or HTML.
4
+ *
5
+ * Heuristic: content is treated as HTML if, after stripping optional YAML
6
+ * frontmatter (lines between `---` delimiters at the very start), the
7
+ * remaining text begins with an HTML opening tag.
8
+ *
9
+ * Examples that resolve to "html":
10
+ * `<p>Hello</p>`
11
+ * ` <div class="callout">...</div>`
12
+ * `---\ntitle: doc\n---\n<article>...</article>`
13
+ */
14
+ export declare function detectFormat(content: string): 'markdown' | 'html';
15
+ /**
16
+ * ArticleViewer renders a markdown or pre-compiled HTML string with full GFM
17
+ * support, callout directive handling, typography variants, and frontmatter
18
+ * display.
19
+ *
20
+ * **Markdown rendering pipeline**:
21
+ * 1. `gray-matter` — extracts YAML frontmatter; strips from body
22
+ * 2. `remark-gfm` — tables, task lists, strikethrough, autolinks
23
+ * 3. `remark-directive` — parses `:::type` container directives
24
+ * 4. `remarkCallouts` — converts container directives to `<callout-type>` elements
25
+ * 5. `ReactMarkdown` + `components` map — maps custom elements to React components
26
+ * (also applies external link hardening via the `a` component override)
27
+ *
28
+ * **HTML rendering pipeline** (`format="html"` or auto-detected):
29
+ * 1. When `sanitize={true}` (default): `sanitizeHtml()` runs the content through
30
+ * `rehypeParse → rehypeExternalLinks → rehypeSanitize → rehypeStringify`
31
+ * 2. When `sanitize={false}`: content is rendered as-is via `dangerouslySetInnerHTML`
32
+ * (only safe for fully trusted, engine-compiled HTML)
33
+ *
34
+ * **Variant**: a CSS class is applied based on `variant` prop; consumer defines
35
+ * `--cv-{variant}-*` variables at document root for theming.
36
+ *
37
+ * **Accessibility**: external links get `rel="noopener noreferrer"` on both paths.
38
+ * Error boundary catches pipeline failures and calls `onError`.
39
+ */
40
+ export declare function ArticleViewer({ content, format, variant, frontmatter: frontmatterMode, components: componentOverrides, calloutComponent, sanitize: sanitizeProp, useDOMPurify, codeHighlight, generateHeadingIds, isLoading, error, onError, className, }: ArticleViewerProps): import("react/jsx-runtime").JSX.Element;
41
+ export default ArticleViewer;
42
+ //# sourceMappingURL=ArticleViewer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ArticleViewer.d.ts","sourceRoot":"","sources":["../../../src/components/content-viewer/ArticleViewer.tsx"],"names":[],"mappings":"AA6EA,OAAO,KAAK,EACV,kBAAkB,EAGnB,MAAM,SAAS,CAAC;AAqIjB;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAejE;AA+GD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EACP,MAAe,EACf,OAAO,EACP,WAAW,EAAE,eAAwB,EACrC,UAAU,EAAE,kBAAkB,EAC9B,gBAAgB,EAChB,QAAQ,EAAE,YAAY,EACtB,YAAoB,EACpB,aAAqB,EACrB,kBAAyB,EACzB,SAAiB,EACjB,KAAK,EACL,OAAO,EACP,SAAS,GACV,EAAE,kBAAkB,2CAkHpB;AAED,eAAe,aAAa,CAAC"}
@@ -0,0 +1,321 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * ArticleViewer — read-only markdown/HTML rendering component (PU1–PU4)
5
+ *
6
+ * Renders a markdown string through the full remark pipeline:
7
+ * - CommonMark (base markdown)
8
+ * - GitHub Flavored Markdown via remark-gfm (tables, task lists, strikethrough)
9
+ * - Callout directives via remark-directive + custom remarkCallouts plugin
10
+ *
11
+ * Also accepts pre-compiled HTML strings (`format="html"`) for Portal artifacts.
12
+ * HTML input is sanitized by default via `rehype-sanitize@6` (PU3-02).
13
+ *
14
+ * YAML frontmatter is extracted via `gray-matter` (PU2-01). It is stripped
15
+ * from the body before rendering and optionally displayed as a collapsible
16
+ * header via the `frontmatter` prop (PU2-03).
17
+ *
18
+ * Typography variants are applied as CSS classes (`cv-variant-{name}`) that
19
+ * read CSS custom properties from document root (PU2-05). See the
20
+ * CSS-Variable Contract section in the content-viewer README.
21
+ *
22
+ * External links are hardened with `target="_blank" rel="noopener noreferrer"`
23
+ * on both markdown and HTML paths (PU3-04).
24
+ *
25
+ * Callout components and FrontmatterHeader can be overridden per-type via the
26
+ * `components` prop, allowing Portal and other consumers to inject their own
27
+ * renderers without forking the component.
28
+ *
29
+ * Phase 4 additions:
30
+ * - `codeHighlight` (default false) — opt-in syntax highlighting via lowlight
31
+ * - `generateHeadingIds` (default true) — auto-generates anchor IDs on headings
32
+ * - `isLoading` — shows accessible skeleton placeholder; suppresses content render
33
+ * - `error` — shows accessible error message; takes priority over normal render
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * // Basic usage
38
+ * <ArticleViewer content={markdownString} />
39
+ *
40
+ * // Loading skeleton
41
+ * <ArticleViewer content="" isLoading />
42
+ *
43
+ * // Error state
44
+ * <ArticleViewer content="" error="Failed to load document" />
45
+ *
46
+ * // With code highlighting + variant + frontmatter display
47
+ * <ArticleViewer
48
+ * content={markdownWithFrontmatter}
49
+ * variant="editorial"
50
+ * frontmatter="show"
51
+ * codeHighlight
52
+ * />
53
+ *
54
+ * // Pre-compiled HTML (sanitized by default)
55
+ * <ArticleViewer content={htmlString} format="html" />
56
+ *
57
+ * // Trusted engine output — skip sanitization (Portal only)
58
+ * <ArticleViewer content={trustedHtml} format="html" sanitize={false} />
59
+ *
60
+ * // With callout + header overrides
61
+ * <ArticleViewer
62
+ * content={markdownString}
63
+ * components={{
64
+ * warning: MyWarningBanner,
65
+ * FrontmatterHeader: MyFrontmatterHeader,
66
+ * }}
67
+ * />
68
+ * ```
69
+ */
70
+ import { Component, useMemo } from 'react';
71
+ import ReactMarkdown from 'react-markdown';
72
+ import remarkGfm from 'remark-gfm';
73
+ import remarkDirective from 'remark-directive';
74
+ import matter from 'gray-matter';
75
+ import { cn } from '../../primitives/utils';
76
+ import { remarkCallouts as _remarkCallouts, createHighlightPlugin, createHeadingIdsPlugin, } from './plugins';
77
+ import { variantClass } from './variants';
78
+ import { FrontmatterHeader as DefaultFrontmatterHeader } from './FrontmatterHeader';
79
+ import { sanitizeHtml } from './sanitize';
80
+ // Cast avoids unified vfile version mismatch between remark-directive@2 and react-markdown@9
81
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
82
+ const remarkCallouts = _remarkCallouts;
83
+ import { NoteCallout, ReferenceCallout, WarningCallout, InfoCallout, } from './callouts';
84
+ /**
85
+ * Error boundary that catches rendering errors from the remark pipeline
86
+ * and reports them via the `onError` callback.
87
+ */
88
+ class ArticleViewerErrorBoundary extends Component {
89
+ constructor(props) {
90
+ super(props);
91
+ this.state = { hasError: false, error: null };
92
+ }
93
+ static getDerivedStateFromError(error) {
94
+ return { hasError: true, error };
95
+ }
96
+ componentDidCatch(error, _errorInfo) {
97
+ this.props.onError?.(error);
98
+ }
99
+ render() {
100
+ if (this.state.hasError) {
101
+ return (_jsxs("div", { role: "alert", className: "rounded-md border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive", children: [_jsx("p", { className: "font-semibold", children: "Failed to render content" }), this.state.error && (_jsx("p", { className: "mt-1 font-mono text-xs opacity-75", children: this.state.error.message }))] }));
102
+ }
103
+ return this.props.children;
104
+ }
105
+ }
106
+ // ============================================================================
107
+ // Loading skeleton (PU4-02)
108
+ // ============================================================================
109
+ /**
110
+ * Accessible skeleton placeholder rendered when `isLoading=true`.
111
+ * Uses Tailwind `animate-pulse` for the shimmer effect.
112
+ * Suppresses content rendering while active.
113
+ */
114
+ function ArticleViewerSkeleton() {
115
+ return (_jsxs("div", { role: "status", "aria-busy": "true", "aria-label": "Loading content", className: "article-viewer-skeleton space-y-3", children: [_jsx("div", { className: "h-6 w-2/3 rounded bg-muted animate-pulse" }), _jsxs("div", { className: "space-y-2", children: [_jsx("div", { className: "h-4 w-full rounded bg-muted animate-pulse" }), _jsx("div", { className: "h-4 w-full rounded bg-muted animate-pulse" }), _jsx("div", { className: "h-4 w-5/6 rounded bg-muted animate-pulse" })] }), _jsxs("div", { className: "space-y-2 pt-2", children: [_jsx("div", { className: "h-4 w-full rounded bg-muted animate-pulse" }), _jsx("div", { className: "h-4 w-4/5 rounded bg-muted animate-pulse" })] }), _jsx("span", { className: "sr-only", children: "Loading article content, please wait." })] }));
116
+ }
117
+ // ============================================================================
118
+ // Error display (PU4-03)
119
+ // ============================================================================
120
+ /**
121
+ * Accessible error message rendered when the `error` prop is set.
122
+ * Takes priority over normal render and boundary-caught errors.
123
+ */
124
+ function ArticleViewerError({ error }) {
125
+ const message = error instanceof Error ? error.message : error;
126
+ return (_jsxs("div", { role: "alert", "aria-live": "assertive", className: "article-viewer-error rounded-md border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive", children: [_jsx("p", { className: "font-semibold", children: "Failed to load content" }), message && (_jsx("p", { className: "mt-1 font-mono text-xs opacity-75", children: message }))] }));
127
+ }
128
+ // ============================================================================
129
+ // Format detection
130
+ // ============================================================================
131
+ /**
132
+ * Detect whether content is markdown or HTML.
133
+ *
134
+ * Heuristic: content is treated as HTML if, after stripping optional YAML
135
+ * frontmatter (lines between `---` delimiters at the very start), the
136
+ * remaining text begins with an HTML opening tag.
137
+ *
138
+ * Examples that resolve to "html":
139
+ * `<p>Hello</p>`
140
+ * ` <div class="callout">...</div>`
141
+ * `---\ntitle: doc\n---\n<article>...</article>`
142
+ */
143
+ export function detectFormat(content) {
144
+ let trimmed = content.trimStart();
145
+ // Strip YAML frontmatter before detection so `---\ntitle: x\n---\n<p>` → html
146
+ if (trimmed.startsWith('---')) {
147
+ const endFm = trimmed.indexOf('\n---', 3);
148
+ if (endFm !== -1) {
149
+ trimmed = trimmed.slice(endFm + 4).trimStart();
150
+ }
151
+ }
152
+ if (trimmed.startsWith('<') && /^<[a-zA-Z]/.test(trimmed)) {
153
+ return 'html';
154
+ }
155
+ return 'markdown';
156
+ }
157
+ /**
158
+ * Extract YAML frontmatter from a markdown string using `gray-matter`.
159
+ *
160
+ * - If no frontmatter is present, returns the original string as body.
161
+ * - If frontmatter is malformed, logs a warning and returns the original string.
162
+ * - Never throws; calling code always receives a valid `ParsedContent`.
163
+ */
164
+ function parseMarkdownFrontmatter(content) {
165
+ try {
166
+ const result = matter(content);
167
+ return {
168
+ body: result.content,
169
+ frontmatterData: result.data,
170
+ };
171
+ }
172
+ catch (err) {
173
+ // Malformed YAML — warn and fall through to render body as-is
174
+ console.warn('[ArticleViewer] Failed to parse frontmatter:', err);
175
+ return { body: content, frontmatterData: {} };
176
+ }
177
+ }
178
+ // ============================================================================
179
+ // Callout component builder
180
+ // ============================================================================
181
+ /** The 4 callout types we handle */
182
+ const CALLOUT_TYPES = ['note', 'reference', 'warning', 'info'];
183
+ /** Default callout component map */
184
+ const DEFAULT_CALLOUT_COMPONENTS = {
185
+ note: NoteCallout,
186
+ reference: ReferenceCallout,
187
+ warning: WarningCallout,
188
+ info: InfoCallout,
189
+ };
190
+ // ============================================================================
191
+ // ReactMarkdown component map
192
+ // ============================================================================
193
+ /**
194
+ * Build the `components` map for ReactMarkdown.
195
+ * Maps custom element names (e.g. `callout-note`) to the appropriate callout component.
196
+ * Also wires up link hardening for external URLs.
197
+ */
198
+ function buildComponentMap(overrides, legacyFallback) {
199
+ const map = {};
200
+ // Wire callout elements: `callout-note`, `callout-reference`, etc.
201
+ for (const calloutType of CALLOUT_TYPES) {
202
+ const elementName = `callout-${calloutType}`;
203
+ // Priority: `components[type]` > `calloutComponent` (legacy) > default
204
+ const CalloutComponent = overrides?.[calloutType] ??
205
+ legacyFallback ??
206
+ DEFAULT_CALLOUT_COMPONENTS[calloutType];
207
+ // ReactMarkdown passes all hast element props; we forward children + className
208
+ const Wrapper = ({ children, className }) => (_jsx(CalloutComponent, { type: calloutType, className: className ?? undefined, children: children }));
209
+ Wrapper.displayName = `CalloutWrapper(${calloutType})`;
210
+ map[elementName] = Wrapper;
211
+ }
212
+ // External link hardening (A-UCV-06)
213
+ const LinkComponent = ({ href, children, ...rest }) => {
214
+ const isExternal = typeof href === 'string' && (href.startsWith('http://') || href.startsWith('https://'));
215
+ return (_jsx("a", { href: href, ...(isExternal
216
+ ? { target: '_blank', rel: 'noopener noreferrer' }
217
+ : {}), ...rest, children: children }));
218
+ };
219
+ LinkComponent.displayName = 'ArticleViewerLink';
220
+ map['a'] = LinkComponent;
221
+ return map;
222
+ }
223
+ // ============================================================================
224
+ // ArticleViewer
225
+ // ============================================================================
226
+ /**
227
+ * ArticleViewer renders a markdown or pre-compiled HTML string with full GFM
228
+ * support, callout directive handling, typography variants, and frontmatter
229
+ * display.
230
+ *
231
+ * **Markdown rendering pipeline**:
232
+ * 1. `gray-matter` — extracts YAML frontmatter; strips from body
233
+ * 2. `remark-gfm` — tables, task lists, strikethrough, autolinks
234
+ * 3. `remark-directive` — parses `:::type` container directives
235
+ * 4. `remarkCallouts` — converts container directives to `<callout-type>` elements
236
+ * 5. `ReactMarkdown` + `components` map — maps custom elements to React components
237
+ * (also applies external link hardening via the `a` component override)
238
+ *
239
+ * **HTML rendering pipeline** (`format="html"` or auto-detected):
240
+ * 1. When `sanitize={true}` (default): `sanitizeHtml()` runs the content through
241
+ * `rehypeParse → rehypeExternalLinks → rehypeSanitize → rehypeStringify`
242
+ * 2. When `sanitize={false}`: content is rendered as-is via `dangerouslySetInnerHTML`
243
+ * (only safe for fully trusted, engine-compiled HTML)
244
+ *
245
+ * **Variant**: a CSS class is applied based on `variant` prop; consumer defines
246
+ * `--cv-{variant}-*` variables at document root for theming.
247
+ *
248
+ * **Accessibility**: external links get `rel="noopener noreferrer"` on both paths.
249
+ * Error boundary catches pipeline failures and calls `onError`.
250
+ */
251
+ export function ArticleViewer({ content, format = 'auto', variant, frontmatter: frontmatterMode = 'hide', components: componentOverrides, calloutComponent, sanitize: sanitizeProp, useDOMPurify = false, codeHighlight = false, generateHeadingIds = true, isLoading = false, error, onError, className, }) {
252
+ // All hooks must be called unconditionally (Rules of Hooks).
253
+ // Loading/error early returns happen AFTER all hooks.
254
+ const resolvedFormat = useMemo(() => {
255
+ if (format === 'auto')
256
+ return detectFormat(content);
257
+ return format;
258
+ }, [content, format]);
259
+ // PU2-01: Parse frontmatter out of markdown content
260
+ const { body, frontmatterData } = useMemo(() => {
261
+ // Only parse frontmatter from markdown (HTML passthrough has no frontmatter)
262
+ if (resolvedFormat === 'html') {
263
+ return { body: content, frontmatterData: {} };
264
+ }
265
+ return parseMarkdownFrontmatter(content);
266
+ }, [content, resolvedFormat]);
267
+ // PU3-02: Resolve sanitize flag.
268
+ // Default: ON for HTML input, OFF for markdown (remark never emits raw HTML).
269
+ const shouldSanitize = sanitizeProp !== undefined
270
+ ? sanitizeProp
271
+ : resolvedFormat === 'html';
272
+ // PU3-02 / PU3-03: Sanitize HTML when required (synchronous via rehype or warm DOMPurify cache)
273
+ const htmlContent = useMemo(() => {
274
+ if (resolvedFormat !== 'html')
275
+ return '';
276
+ if (!shouldSanitize)
277
+ return content;
278
+ return sanitizeHtml(content, { useDOMPurify });
279
+ }, [content, resolvedFormat, shouldSanitize, useDOMPurify]);
280
+ const componentMap = useMemo(() => buildComponentMap(componentOverrides, calloutComponent), [componentOverrides, calloutComponent]);
281
+ // PU4-01: Build rehype plugin list (opt-in code highlighting + heading IDs)
282
+ // createHighlightPlugin() and createHeadingIdsPlugin() both create fresh instances
283
+ // per render to prevent cross-render state contamination.
284
+ const rehypePlugins = useMemo(() => {
285
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
286
+ const plugins = [];
287
+ if (generateHeadingIds) {
288
+ plugins.push(createHeadingIdsPlugin());
289
+ }
290
+ if (codeHighlight) {
291
+ plugins.push(createHighlightPlugin());
292
+ }
293
+ return plugins;
294
+ }, [codeHighlight, generateHeadingIds]);
295
+ // PU2-05: Resolve variant class
296
+ const variantClassName = variantClass(variant);
297
+ // PU2-03: Resolve frontmatter header visibility
298
+ const hasFrontmatter = Object.keys(frontmatterData).length > 0;
299
+ const showHeader = hasFrontmatter && frontmatterMode !== 'hide';
300
+ const headerCollapsed = frontmatterMode === 'collapse';
301
+ // PU2-06: Resolve FrontmatterHeader component (override or default)
302
+ const FrontmatterHeaderComponent = componentOverrides?.FrontmatterHeader ?? DefaultFrontmatterHeader;
303
+ // PU4-02: Loading state — suppress all content, show skeleton
304
+ if (isLoading) {
305
+ return _jsx(ArticleViewerSkeleton, {});
306
+ }
307
+ // PU4-03: Error prop — show accessible error message
308
+ if (error != null) {
309
+ return _jsx(ArticleViewerError, { error: error });
310
+ }
311
+ return (_jsx(ArticleViewerErrorBoundary, { onError: onError, children: _jsxs("div", { className: cn('article-viewer prose prose-sm max-w-none', 'dark:prose-invert', variantClassName, className), "data-format": resolvedFormat, "data-variant": variant, "data-sanitized": resolvedFormat === 'html' ? String(shouldSanitize) : undefined, "data-code-highlight": codeHighlight ? 'true' : undefined, children: [showHeader && (_jsx(FrontmatterHeaderComponent, { frontmatter: frontmatterData, isCollapsed: headerCollapsed })), resolvedFormat === 'html' ? (
312
+ // HTML path (PU3-01): render sanitized (or trusted) HTML string
313
+ // dangerouslySetInnerHTML is acceptable here because:
314
+ // - When sanitize=true: content has been cleaned by rehype-sanitize / DOMPurify
315
+ // - When sanitize=false: caller explicitly asserts content is trusted
316
+ _jsx("div", { dangerouslySetInnerHTML: { __html: htmlContent } })) : (
317
+ // Markdown pipeline — use body with frontmatter stripped
318
+ _jsx(ReactMarkdown, { remarkPlugins: [remarkGfm, remarkDirective, remarkCallouts], rehypePlugins: rehypePlugins.length > 0 ? rehypePlugins : undefined, components: componentMap, children: body }))] }) }));
319
+ }
320
+ export default ArticleViewer;
321
+ //# sourceMappingURL=ArticleViewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ArticleViewer.js","sourceRoot":"","sources":["../../../src/components/content-viewer/ArticleViewer.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * FrontmatterHeader — collapsible YAML frontmatter display (PU2-02)
3
+ *
4
+ * Renders parsed YAML frontmatter as a compact key-value table above
5
+ * article content. Supports 1-level object nesting; deeper values are
6
+ * stringified as JSON. Arrays are rendered as comma-separated values.
7
+ *
8
+ * The collapsed state can be controlled externally via `isCollapsed` /
9
+ * `onToggleCollapse`, or managed internally when those props are omitted.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * <FrontmatterHeader
14
+ * frontmatter={{ title: 'My Doc', tags: ['react', 'ui'] }}
15
+ * isCollapsed={false}
16
+ * onToggleCollapse={(c) => setCollapsed(c)}
17
+ * />
18
+ * ```
19
+ */
20
+ import React from 'react';
21
+ import type { FrontmatterHeaderProps } from './types';
22
+ /**
23
+ * Collapsible header that displays YAML frontmatter above article content.
24
+ *
25
+ * When neither `isCollapsed` nor `onToggleCollapse` are provided, the
26
+ * component manages its own internal open/closed state.
27
+ *
28
+ * If `frontmatter` is empty (`{}`), the component renders nothing.
29
+ */
30
+ export declare function FrontmatterHeader({ frontmatter, isCollapsed, onToggleCollapse, className, }: FrontmatterHeaderProps): React.ReactElement | null;
31
+ export default FrontmatterHeader;
32
+ //# sourceMappingURL=FrontmatterHeader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FrontmatterHeader.d.ts","sourceRoot":"","sources":["../../../src/components/content-viewer/FrontmatterHeader.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAmB,MAAM,OAAO,CAAC;AAGxC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAkEtD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,WAAW,EACX,WAAW,EACX,gBAAgB,EAChB,SAAS,GACV,EAAE,sBAAsB,GAAG,KAAK,CAAC,YAAY,GAAG,IAAI,CAmFpD;AAED,eAAe,iBAAiB,CAAC"}
@@ -0,0 +1,95 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ /**
4
+ * FrontmatterHeader — collapsible YAML frontmatter display (PU2-02)
5
+ *
6
+ * Renders parsed YAML frontmatter as a compact key-value table above
7
+ * article content. Supports 1-level object nesting; deeper values are
8
+ * stringified as JSON. Arrays are rendered as comma-separated values.
9
+ *
10
+ * The collapsed state can be controlled externally via `isCollapsed` /
11
+ * `onToggleCollapse`, or managed internally when those props are omitted.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * <FrontmatterHeader
16
+ * frontmatter={{ title: 'My Doc', tags: ['react', 'ui'] }}
17
+ * isCollapsed={false}
18
+ * onToggleCollapse={(c) => setCollapsed(c)}
19
+ * />
20
+ * ```
21
+ */
22
+ import { useState } from 'react';
23
+ import { ChevronDown, ChevronUp } from 'lucide-react';
24
+ import { cn } from '../../primitives/utils';
25
+ // ============================================================================
26
+ // Value renderer (1-level nesting)
27
+ // ============================================================================
28
+ /**
29
+ * Render a frontmatter value as a React node.
30
+ * - Primitives: rendered inline
31
+ * - Arrays: comma-separated list
32
+ * - Objects (1-level): indented key-value rows
33
+ * - Deeper nesting: JSON stringified
34
+ */
35
+ function renderValue(value) {
36
+ if (value === null || value === undefined) {
37
+ return (_jsx("span", { className: "italic text-muted-foreground", "aria-label": "null value", children: "null" }));
38
+ }
39
+ if (typeof value === 'boolean') {
40
+ return _jsx("span", { className: "text-muted-foreground", children: value ? 'true' : 'false' });
41
+ }
42
+ if (typeof value === 'number') {
43
+ return _jsx("span", { children: value });
44
+ }
45
+ if (typeof value === 'string') {
46
+ return _jsx("span", { children: value });
47
+ }
48
+ if (Array.isArray(value)) {
49
+ const stringValues = value.map((item) => {
50
+ if (typeof item === 'object' && item !== null) {
51
+ return JSON.stringify(item);
52
+ }
53
+ return String(item);
54
+ });
55
+ return _jsx("span", { children: stringValues.join(', ') });
56
+ }
57
+ if (typeof value === 'object') {
58
+ // 1-level nesting: render as indented rows
59
+ return (_jsx("div", { className: "ml-4 mt-1 space-y-0.5", children: Object.entries(value).map(([k, v]) => (_jsxs("div", { className: "text-xs", children: [_jsx("span", { className: "font-medium text-muted-foreground", children: k }), ': ', typeof v === 'object' && v !== null ? JSON.stringify(v) : String(v ?? '')] }, k))) }));
60
+ }
61
+ return _jsx("span", { children: String(value) });
62
+ }
63
+ // ============================================================================
64
+ // FrontmatterHeader component
65
+ // ============================================================================
66
+ /**
67
+ * Collapsible header that displays YAML frontmatter above article content.
68
+ *
69
+ * When neither `isCollapsed` nor `onToggleCollapse` are provided, the
70
+ * component manages its own internal open/closed state.
71
+ *
72
+ * If `frontmatter` is empty (`{}`), the component renders nothing.
73
+ */
74
+ export function FrontmatterHeader({ frontmatter, isCollapsed, onToggleCollapse, className, }) {
75
+ const entries = Object.entries(frontmatter);
76
+ // Internal state — used only when the caller does not control collapsed state
77
+ const [internalCollapsed, setInternalCollapsed] = useState(isCollapsed ?? false);
78
+ // Derive the effective collapsed state
79
+ const isControlled = isCollapsed !== undefined;
80
+ const effectiveCollapsed = isControlled ? isCollapsed : internalCollapsed;
81
+ const handleToggle = () => {
82
+ const next = !effectiveCollapsed;
83
+ if (!isControlled) {
84
+ setInternalCollapsed(next);
85
+ }
86
+ onToggleCollapse?.(next);
87
+ };
88
+ // Nothing to show
89
+ if (entries.length === 0) {
90
+ return null;
91
+ }
92
+ return (_jsxs("div", { className: cn('mb-4 rounded-md border border-border bg-muted/30', className), "data-testid": "frontmatter-header", children: [_jsxs("div", { className: "flex items-center justify-between px-3 py-2", children: [_jsx("h4", { className: "text-xs font-semibold uppercase tracking-wider text-muted-foreground", children: "Frontmatter" }), _jsx("button", { type: "button", onClick: handleToggle, "aria-expanded": !effectiveCollapsed, "aria-controls": "frontmatter-header-content", className: cn('inline-flex items-center gap-1 rounded px-2 py-0.5', 'text-xs text-muted-foreground', 'hover:bg-accent hover:text-accent-foreground', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', 'transition-colors'), children: effectiveCollapsed ? (_jsxs(_Fragment, { children: [_jsx(ChevronDown, { className: "h-3.5 w-3.5", "aria-hidden": "true" }), _jsx("span", { children: "Show" })] })) : (_jsxs(_Fragment, { children: [_jsx(ChevronUp, { className: "h-3.5 w-3.5", "aria-hidden": "true" }), _jsx("span", { children: "Hide" })] })) })] }), !effectiveCollapsed && (_jsx("div", { id: "frontmatter-header-content", className: "max-h-64 overflow-y-auto border-t border-border px-3 py-2", children: _jsx("dl", { className: "space-y-1.5", children: entries.map(([key, value]) => (_jsxs("div", { className: "flex flex-wrap items-baseline gap-x-1.5 text-sm", children: [_jsx("dt", { className: "shrink-0 font-medium text-foreground", children: key }), _jsx("dd", { className: "min-w-0 break-words text-muted-foreground", children: renderValue(value) })] }, key))) }) }))] }));
93
+ }
94
+ export default FrontmatterHeader;
95
+ //# sourceMappingURL=FrontmatterHeader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FrontmatterHeader.js","sourceRoot":"","sources":["../../../src/components/content-viewer/FrontmatterHeader.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAc,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,EAAE,EAAE,MAAM,wBAAwB,CAAC;AAG5C,+EAA+E;AAC/E,mCAAmC;AACnC,+EAA+E;AAE/E;;;;;;GAMG;AACH,SAAS,WAAW,CAAC,KAAc;IACjC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QAC1C,OAAO,CACL,eAAM,SAAS,EAAC,8BAA8B,gBAAY,YAAY,qBAE/D,CACR,CAAC;IACJ,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,eAAM,SAAS,EAAC,uBAAuB,YAAE,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,GAAQ,CAAC;IACnF,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,yBAAO,KAAK,GAAQ,CAAC;IAC9B,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,yBAAO,KAAK,GAAQ,CAAC;IAC9B,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,YAAY,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;YACtC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;gBAC9C,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;YAC9B,CAAC;YACD,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC;QACtB,CAAC,CAAC,CAAC;QACH,OAAO,yBAAO,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,GAAQ,CAAC;IAChD,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,2CAA2C;QAC3C,OAAO,CACL,cAAK,SAAS,EAAC,uBAAuB,YACnC,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAChE,eAAa,SAAS,EAAC,SAAS,aAC9B,eAAM,SAAS,EAAC,mCAAmC,YAAE,CAAC,GAAQ,EAC7D,IAAI,EACJ,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,KAHlE,CAAC,CAIL,CACP,CAAC,GACE,CACP,CAAC;IACJ,CAAC;IAED,OAAO,yBAAO,MAAM,CAAC,KAAK,CAAC,GAAQ,CAAC;AACtC,CAAC;AAED,+EAA+E;AAC/E,8BAA8B;AAC9B,+EAA+E;AAE/E;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,EAChC,WAAW,EACX,WAAW,EACX,gBAAgB,EAChB,SAAS,GACc;IACvB,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAE5C,8EAA8E;IAC9E,MAAM,CAAC,iBAAiB,EAAE,oBAAoB,CAAC,GAAG,QAAQ,CAAC,WAAW,IAAI,KAAK,CAAC,CAAC;IAEjF,uCAAuC;IACvC,MAAM,YAAY,GAAG,WAAW,KAAK,SAAS,CAAC;IAC/C,MAAM,kBAAkB,GAAG,YAAY,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,iBAAiB,CAAC;IAE1E,MAAM,YAAY,GAAG,GAAG,EAAE;QACxB,MAAM,IAAI,GAAG,CAAC,kBAAkB,CAAC;QACjC,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,oBAAoB,CAAC,IAAI,CAAC,CAAC;QAC7B,CAAC;QACD,gBAAgB,EAAE,CAAC,IAAI,CAAC,CAAC;IAC3B,CAAC,CAAC;IAEF,kBAAkB;IAClB,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,CACL,eACE,SAAS,EAAE,EAAE,CACX,kDAAkD,EAClD,SAAS,CACV,iBACW,oBAAoB,aAGhC,eAAK,SAAS,EAAC,6CAA6C,aAC1D,aAAI,SAAS,EAAC,sEAAsE,4BAE/E,EACL,iBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,YAAY,mBACN,CAAC,kBAAkB,mBACpB,4BAA4B,EAC1C,SAAS,EAAE,EAAE,CACX,oDAAoD,EACpD,+BAA+B,EAC/B,8CAA8C,EAC9C,yEAAyE,EACzE,mBAAmB,CACpB,YAEA,kBAAkB,CAAC,CAAC,CAAC,CACpB,8BACE,KAAC,WAAW,IAAC,SAAS,EAAC,aAAa,iBAAa,MAAM,GAAG,EAC1D,kCAAiB,IAChB,CACJ,CAAC,CAAC,CAAC,CACF,8BACE,KAAC,SAAS,IAAC,SAAS,EAAC,aAAa,iBAAa,MAAM,GAAG,EACxD,kCAAiB,IAChB,CACJ,GACM,IACL,EAGL,CAAC,kBAAkB,IAAI,CACtB,cACE,EAAE,EAAC,4BAA4B,EAC/B,SAAS,EAAC,2DAA2D,YAErE,aAAI,SAAS,EAAC,aAAa,YACxB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAC7B,eAAe,SAAS,EAAC,iDAAiD,aACxE,aAAI,SAAS,EAAC,sCAAsC,YAAE,GAAG,GAAM,EAC/D,aAAI,SAAS,EAAC,2CAA2C,YACtD,WAAW,CAAC,KAAK,CAAC,GAChB,KAJG,GAAG,CAKP,CACP,CAAC,GACC,GACD,CACP,IACG,CACP,CAAC;AACJ,CAAC;AAED,eAAe,iBAAiB,CAAC"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Base Callout component and default callout variants.
3
+ *
4
+ * Each variant renders with:
5
+ * - A left accent border (type-specific color via Tailwind + CSS variable hooks)
6
+ * - A type label / icon prefix
7
+ * - Semantic HTML structure
8
+ * - `children` forwarded as-is (ReactMarkdown passes rendered JSX)
9
+ *
10
+ * CSS variable hooks (applied in Phase 2):
11
+ * - `--callout-note-border` / `--callout-note-bg`
12
+ * - `--callout-reference-border` / `--callout-reference-bg`
13
+ * - `--callout-warning-border` / `--callout-warning-bg`
14
+ * - `--callout-info-border` / `--callout-info-bg`
15
+ */
16
+ import type { CalloutProps } from '../types';
17
+ /**
18
+ * Base callout renderer used by all type-specific variants.
19
+ * Accepts an explicit `type` prop for cases where the component is used
20
+ * without a type-specific wrapper.
21
+ */
22
+ export declare function Callout({ type, children, className }: CalloutProps): import("react/jsx-runtime").JSX.Element;
23
+ /**
24
+ * Note callout — informational content that supplements the main text.
25
+ * @example `::: note\n...\n:::`
26
+ */
27
+ export declare function NoteCallout({ children, className }: Omit<CalloutProps, 'type'>): import("react/jsx-runtime").JSX.Element;
28
+ /**
29
+ * Reference callout — citations, sources, or cross-references.
30
+ * @example `::: reference\n...\n:::`
31
+ */
32
+ export declare function ReferenceCallout({ children, className }: Omit<CalloutProps, 'type'>): import("react/jsx-runtime").JSX.Element;
33
+ /**
34
+ * Warning callout — potentially destructive or dangerous information.
35
+ * @example `::: warning\n...\n:::`
36
+ */
37
+ export declare function WarningCallout({ children, className }: Omit<CalloutProps, 'type'>): import("react/jsx-runtime").JSX.Element;
38
+ /**
39
+ * Info callout — supplementary information or tips.
40
+ * @example `::: info\n...\n:::`
41
+ */
42
+ export declare function InfoCallout({ children, className }: Omit<CalloutProps, 'type'>): import("react/jsx-runtime").JSX.Element;
43
+ //# sourceMappingURL=Callout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Callout.d.ts","sourceRoot":"","sources":["../../../../src/components/content-viewer/callouts/Callout.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAe,MAAM,UAAU,CAAC;AAuD1D;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,YAAY,2CAqClE;AAMD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,2CAM9E;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,2CAMnF;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,2CAMjF;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,2CAM9E"}