@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
package/README.md CHANGED
@@ -71,17 +71,37 @@ Or reference the workspace package directly in `package.json`:
71
71
 
72
72
  ## Subpath Imports
73
73
 
74
- The package is organized into seven submodules with tree-shakeable exports:
74
+ The package is organized into eight submodules with tree-shakeable exports.
75
+
76
+ **IMPORTANT: Server Component Rule**
77
+
78
+ Never import from the root barrel (`@miethe/ui`) in a React Server Component. All re-exports lack a `'use client'` directive and will fail in server-component-only files. Use subpath imports instead:
79
+
80
+ ```typescript
81
+ // ✗ Server component — do not use root barrel
82
+ import { FileTree, ContentPane } from '@miethe/ui';
83
+
84
+ // ✓ Server component — use subpath imports
85
+ import { FileTree, ContentPane } from '@miethe/ui/content-viewer';
86
+ ```
87
+
88
+ Client components can safely use root barrel imports (the `'use client'` directive in submodules propagates to consumers).
89
+
90
+ **Available Subpaths**:
75
91
 
76
92
  ```typescript
77
93
  // Content viewer components
78
- import { FileTree, ContentPane, ContentViewerProvider } from '@miethe/ui/content-viewer';
94
+ import { FileTree, ContentPane, ArticleViewer, ContentViewerProvider } from '@miethe/ui/content-viewer';
79
95
 
80
96
  // Diff viewing components
81
97
  import { DiffViewer, DiffViewerSkeleton } from '@miethe/ui/diff';
82
98
 
99
+ // Discovery card (compact artifact discovery / search contexts)
100
+ import { DiscoveryCard } from '@miethe/ui/discovery';
101
+ import type { DiscoveryCardProps, AgentDiscoveryCandidate } from '@miethe/ui/discovery';
102
+
83
103
  // Editor components
84
- import { MarkdownEditor, SplitPreview } from '@miethe/ui/editor';
104
+ import { MarkdownEditor, SplitPreview, CodeEditor } from '@miethe/ui/editor';
85
105
 
86
106
  // Display components
87
107
  import { FrontmatterDisplay, FilePreviewPane } from '@miethe/ui/display';
@@ -136,6 +156,52 @@ module.exports = {
136
156
 
137
157
  **Important**: Without Tailwind CSS configured, components will render with no styles. Components use semantic Tailwind classes like `text-muted-foreground` and `bg-background` — ensure your Tailwind config includes these classes (standard in shadcn/ui projects).
138
158
 
159
+ ### Required shadcn-Compatible CSS Variables
160
+
161
+ Define these CSS variables at `:root` or an appropriate ancestor element. These variables control the color palette for all UI components:
162
+
163
+ ```css
164
+ :root {
165
+ --background: 0 0% 100%;
166
+ --foreground: 0 0% 3.6%;
167
+ --muted: 0 0% 96.1%;
168
+ --muted-foreground: 0 0% 45.1%;
169
+ --card: 0 0% 100%;
170
+ --card-foreground: 0 0% 3.6%;
171
+ --border: 0 0% 89.8%;
172
+ --ring: 0 0% 3.6%;
173
+ --primary: 0 0% 9%;
174
+ --accent: 0 84.6% 60.2%;
175
+ --accent-foreground: 0 0% 100%;
176
+ --secondary: 0 0% 14.9%;
177
+ --secondary-foreground: 0 0% 100%;
178
+ --destructive: 0 84.3% 60.2%;
179
+ --input: 0 0% 89.8%;
180
+ }
181
+
182
+ @media (prefers-color-scheme: dark) {
183
+ :root {
184
+ --background: 0 0% 3.6%;
185
+ --foreground: 0 0% 98%;
186
+ --muted: 0 0% 14.9%;
187
+ --muted-foreground: 0 0% 63.9%;
188
+ --card: 0 0% 3.6%;
189
+ --card-foreground: 0 0% 98%;
190
+ --border: 0 0% 14.9%;
191
+ --ring: 0 84.6% 60.2%;
192
+ --primary: 0 0% 98%;
193
+ --accent: 0 84.6% 60.2%;
194
+ --accent-foreground: 0 0% 9%;
195
+ --secondary: 0 0% 85.1%;
196
+ --secondary-foreground: 0 0% 9%;
197
+ --destructive: 0 84.3% 60.2%;
198
+ --input: 0 0% 14.9%;
199
+ }
200
+ }
201
+ ```
202
+
203
+ All components degrade gracefully if some variables are missing — the browser will use default colors. Define only the ones you need to customize.
204
+
139
205
  ### Dark Mode
140
206
 
141
207
  Components support dark mode via the Tailwind `dark:` variant. Enable dark mode in your Tailwind config:
@@ -163,6 +229,54 @@ Then add the `dark` class to your root HTML element to activate dark mode styles
163
229
 
164
230
  Or use a theme provider component that toggles the class dynamically based on user preference.
165
231
 
232
+ ### CodeMirror Deduplication
233
+
234
+ CodeMirror 6 requires exactly **one** `@codemirror/state` instance in the dependency tree. If multiple versions are installed, the editor will fail silently or behave unpredictably.
235
+
236
+ **Verify Installation**:
237
+
238
+ ```bash
239
+ npm ls @codemirror/state
240
+ # or
241
+ yarn why @codemirror/state
242
+ # or
243
+ pnpm ls @codemirror/state
244
+ ```
245
+
246
+ If you see multiple versions, force deduplication in `package.json`:
247
+
248
+ ```json
249
+ {
250
+ "pnpm": {
251
+ "overrides": {
252
+ "@codemirror/state": "^6.4.0"
253
+ }
254
+ }
255
+ }
256
+ ```
257
+
258
+ For yarn, use `resolutions`:
259
+
260
+ ```json
261
+ {
262
+ "resolutions": {
263
+ "@codemirror/state": "^6.4.0"
264
+ }
265
+ }
266
+ ```
267
+
268
+ For npm, add an `overrides` field (npm v8.3.0+):
269
+
270
+ ```json
271
+ {
272
+ "overrides": {
273
+ "@codemirror/state": "^6.4.0"
274
+ }
275
+ }
276
+ ```
277
+
278
+ After updating, reinstall and verify the deduplicated list shows only one entry.
279
+
166
280
  ## Quick Start
167
281
 
168
282
  ### 1. Create an Adapter
@@ -283,6 +397,67 @@ export function FilePreviewExample() {
283
397
 
284
398
  ## Components API
285
399
 
400
+ ### DiscoveryCard
401
+
402
+ Compact single-tier artifact card for discovery and search result contexts. Designed for reuse across `/discover` and marketplace search results pages.
403
+
404
+ **Props:**
405
+
406
+ ```typescript
407
+ interface DiscoveryCardProps {
408
+ candidate: AgentDiscoveryCandidate; // The discovery candidate to display
409
+ isSelected: boolean; // Selection state (affects styling)
410
+ onToggleSelect: (candidate) => void; // Called when checkbox is toggled
411
+ onOpenDetail: (candidate) => void; // Called when card is clicked
412
+ onImport: (candidate) => void; // Called when Import button is clicked
413
+ onDeploy: (candidate) => void; // Called when Deploy button is clicked
414
+ isImporting?: boolean; // Show loading state on Import
415
+ isDeploying?: boolean; // Show loading state on Deploy
416
+ importDisabled?: boolean; // Disable Import button
417
+ deployDisabled?: boolean; // Disable Deploy button
418
+ className?: string; // Additional className
419
+ }
420
+ ```
421
+
422
+ **Features:**
423
+
424
+ - Fixed-height card (min 160px) with left-border type color from `typeBarColors`
425
+ - Three zones: header (type icon + name + badges) | content (description + marketplace source + score bar) | actions (checkbox + import/deploy buttons)
426
+ - Source-aware rendering: shows collection/marketplace/web badges and source links
427
+ - Trust tier badge with icon (trusted, candidate, unverified, quarantined)
428
+ - Relevance score bar (0.0–1.0 displayed as 0–100%)
429
+ - Selection state with ring styling
430
+ - No app-hook coupling: all data and handlers passed as props
431
+
432
+ **Design Contract:**
433
+
434
+ - Parent grid must use `auto-rows-fr` for uniform row heights
435
+ - Type colors from `@miethe/ui/utils` (`typeBarColors`, `TYPE_BAR_FALLBACK`)
436
+ - Icon colors from component-local `TYPE_ICON_*` maps (semi-transparent variants)
437
+
438
+ **Example:**
439
+
440
+ ```typescript
441
+ import { DiscoveryCard } from '@miethe/ui/discovery';
442
+
443
+ <div className="grid grid-cols-3 auto-rows-fr gap-4">
444
+ {candidates.map((candidate) => (
445
+ <DiscoveryCard
446
+ key={candidate.name}
447
+ candidate={candidate}
448
+ isSelected={selected.has(candidate.name)}
449
+ onToggleSelect={handleSelect}
450
+ onOpenDetail={handleDetail}
451
+ onImport={handleImport}
452
+ onDeploy={handleDeploy}
453
+ isImporting={importing === candidate.name}
454
+ />
455
+ ))}
456
+ </div>
457
+ ```
458
+
459
+ **Full Documentation:** See `src/discovery/README.md` for complete API reference, zone architecture, accessibility notes, and marketplace reuse guidance.
460
+
286
461
  ### DiffViewer
287
462
 
288
463
  A side-by-side unified diff viewer with file browser sidebar and optional sync conflict resolution actions.
@@ -485,6 +660,252 @@ interface FileTreeProps {
485
660
  />
486
661
  ```
487
662
 
663
+ ### ArticleViewer
664
+
665
+ Read-only markdown and HTML renderer with frontmatter support, callouts, and typography variants.
666
+
667
+ **Props:**
668
+
669
+ ```typescript
670
+ interface ArticleViewerProps {
671
+ /**
672
+ * The raw content string to render.
673
+ * For markdown, the full remark pipeline (GFM + directives) is applied.
674
+ * YAML frontmatter (if present) is extracted and not rendered as content.
675
+ */
676
+ content: string;
677
+
678
+ /**
679
+ * Format of the content. Defaults to `"auto"` which detects markdown
680
+ * by looking for common markdown patterns.
681
+ * @default "auto"
682
+ */
683
+ format?: 'markdown' | 'html' | 'auto';
684
+
685
+ /**
686
+ * Typography variant. Applies a CSS class (`cv-variant-{name}`) to the root
687
+ * element. Each variant relies on CSS custom properties at document root.
688
+ * See the CSS-Variable Contract below for the full list of expected variables.
689
+ * @default undefined (no variant class applied)
690
+ */
691
+ variant?: 'editorial' | 'compact' | 'technical';
692
+
693
+ /**
694
+ * Controls visibility of the YAML frontmatter header above article content.
695
+ * - `"show"` — header rendered, expanded by default
696
+ * - `"collapse"` — header rendered, collapsed by default
697
+ * - `"hide"` — header omitted entirely
698
+ * Has no effect if the content has no frontmatter.
699
+ * @default "hide"
700
+ */
701
+ frontmatter?: 'show' | 'collapse' | 'hide';
702
+
703
+ /**
704
+ * Override map for sub-components rendered by ArticleViewer.
705
+ * - Callout keys (`note`, `reference`, `warning`, `info`): override default callout renderers
706
+ * - `FrontmatterHeader`: override the default frontmatter header component
707
+ *
708
+ * Any key not provided falls back to the default implementation.
709
+ */
710
+ components?: {
711
+ note?: React.ComponentType<CalloutProps>;
712
+ reference?: React.ComponentType<CalloutProps>;
713
+ warning?: React.ComponentType<CalloutProps>;
714
+ info?: React.ComponentType<CalloutProps>;
715
+ FrontmatterHeader?: React.ComponentType<FrontmatterHeaderProps>;
716
+ };
717
+
718
+ /**
719
+ * Whether to sanitize HTML content (applies to `format="html"` and auto-detected HTML).
720
+ * When `true` (default for HTML input), `rehype-sanitize@6` strips XSS vectors:
721
+ * `<script>`, event handlers (`onclick`, `onerror`, …), `javascript:` and unsafe
722
+ * `data:` URLs, `<iframe>`, `<object>`, `<embed>`, and `<svg>` containing scripts.
723
+ *
724
+ * Set to `false` **only** when the HTML source is fully trusted (e.g., content
725
+ * compiled by the MeatyWiki engine through Portal's controlled pipeline).
726
+ * Never set `false` for user-supplied or third-party content.
727
+ *
728
+ * @default true (for format="html" / auto-detected HTML), false (for format="markdown")
729
+ */
730
+ sanitize?: boolean;
731
+
732
+ /**
733
+ * Enable opt-in syntax highlighting for fenced code blocks via lowlight.
734
+ * When `false` (default), code blocks render as styled monospace plain text.
735
+ * When `true`, dynamically imports lowlight (~15KB gzip) and applies highlighting.
736
+ *
737
+ * @default false
738
+ */
739
+ codeHighlight?: boolean;
740
+
741
+ /**
742
+ * Automatically generate `id` attributes on heading elements (h1–h6)
743
+ * using a GitHub-compatible slug algorithm.
744
+ * @default true
745
+ */
746
+ generateHeadingIds?: boolean;
747
+
748
+ /**
749
+ * When `true`, suppress all content rendering and show an accessible
750
+ * skeleton/placeholder instead.
751
+ * @default false
752
+ */
753
+ isLoading?: boolean;
754
+
755
+ /**
756
+ * Render an accessible error message instead of content.
757
+ * Accepts either a `string` message or an `Error` object.
758
+ * @default undefined
759
+ */
760
+ error?: string | Error | null;
761
+
762
+ /**
763
+ * Callback invoked when a rendering error occurs.
764
+ */
765
+ onError?: (error: Error) => void;
766
+
767
+ /**
768
+ * Additional CSS class names applied to the root wrapper element.
769
+ */
770
+ className?: string;
771
+ }
772
+ ```
773
+
774
+ **Features:**
775
+
776
+ - Full CommonMark + GitHub-Flavored Markdown (tables, task lists, strikethrough, blockquotes, code blocks, links, lists)
777
+ - Callout directives: `::: note`, `::: reference`, `::: warning`, `::: info` with customizable components
778
+ - YAML frontmatter parsing and optional display via collapsible header
779
+ - HTML input support with XSS sanitization (default ON; opt-out for trusted sources)
780
+ - Typography variants (CSS-variable-driven): editorial, compact, technical
781
+ - Syntax highlighting (opt-in, lowlight-based)
782
+ - Heading anchor links (auto-generated IDs)
783
+ - Loading and error states
784
+ - Fully typed TypeScript exports
785
+ - Zero React Markdown coupling (pure remark pipeline)
786
+
787
+ **CSS-Variable Contract:**
788
+
789
+ When using `variant="editorial"`, define these variables at `:root` or a suitable ancestor:
790
+
791
+ ```css
792
+ :root {
793
+ /* Typography */
794
+ --cv-editorial-h1-font: Fraunces, Georgia, serif;
795
+ --cv-editorial-h1-size: 2.25rem;
796
+ --cv-editorial-h2-font: Fraunces, Georgia, serif;
797
+ --cv-editorial-h2-size: 1.875rem;
798
+ --cv-editorial-body-font: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
799
+ --cv-editorial-body-size: 1rem;
800
+ --cv-editorial-body-line-height: 1.75;
801
+
802
+ /* Quotes & blockquotes */
803
+ --cv-editorial-quote-color: #64748b;
804
+ --cv-editorial-quote-font-style: italic;
805
+
806
+ /* Callout colors */
807
+ --cv-callout-note-accent: #0ea5e9;
808
+ --cv-callout-note-bg: #f0f9ff;
809
+ --cv-callout-reference-accent: #64748b;
810
+ --cv-callout-reference-bg: #f1f5f9;
811
+ --cv-callout-warning-accent: #f59e0b;
812
+ --cv-callout-warning-bg: #fffbeb;
813
+ --cv-callout-info-accent: #0ea5e9;
814
+ --cv-callout-info-bg: #f0f9ff;
815
+ }
816
+ ```
817
+
818
+ Analogous `--cv-compact-*` and `--cv-technical-*` sets follow the same pattern. Missing variables are silently ignored — browser defaults apply.
819
+
820
+ **Examples:**
821
+
822
+ ```typescript
823
+ import { ArticleViewer } from '@miethe/ui/content-viewer';
824
+
825
+ // Basic markdown rendering
826
+ <ArticleViewer
827
+ content="# Hello\n\nThis is **bold** and *italic*."
828
+ format="markdown"
829
+ />
830
+
831
+ // With frontmatter display
832
+ <ArticleViewer
833
+ content={`---
834
+ title: My Article
835
+ date: 2026-04-24
836
+ tags: [markdown, ui]
837
+ ---
838
+
839
+ # Article title
840
+
841
+ Content here...`}
842
+ frontmatter="show"
843
+ variant="editorial"
844
+ />
845
+
846
+ // With callouts
847
+ <ArticleViewer
848
+ content={`# Document
849
+
850
+ ::: note
851
+ This is a note callout.
852
+ :::
853
+
854
+ ::: warning
855
+ This is a warning.
856
+ :::`}
857
+ format="markdown"
858
+ />
859
+
860
+ // With callout override
861
+ <ArticleViewer
862
+ content={markdown}
863
+ components={{
864
+ note: CustomNoteCallout,
865
+ warning: CustomWarningCallout,
866
+ }}
867
+ />
868
+
869
+ // HTML input with sanitization (default ON)
870
+ <ArticleViewer
871
+ content="<p>This <script>alert('xss')</script> is <strong>safe</strong>.</p>"
872
+ format="html"
873
+ sanitize={true}
874
+ />
875
+
876
+ // Trusted engine-compiled HTML (sanitization OFF)
877
+ <ArticleViewer
878
+ content={engineOutput}
879
+ format="html"
880
+ sanitize={false}
881
+ />
882
+
883
+ // With syntax highlighting
884
+ <ArticleViewer
885
+ content={`\`\`\`typescript
886
+ function hello(name: string) {
887
+ console.log('Hello', name);
888
+ }
889
+ \`\`\``}
890
+ codeHighlight={true}
891
+ />
892
+
893
+ // Typography variant with custom CSS variables
894
+ <ArticleViewer
895
+ content={markdown}
896
+ variant="editorial"
897
+ />
898
+ {/* In your globals.css: */}
899
+ {/* :root { --cv-editorial-body-font: 'Libre Baskerville', serif; ... } */}
900
+ ```
901
+
902
+ **Sanitization Notes:**
903
+
904
+ - By default, HTML input is sanitized via `rehype-sanitize@6`, which strips script tags, event handlers, unsafe URLs, and dangerous elements
905
+ - Markdown input (via `react-markdown`) is inherently safe — the sanitization prop has no effect on markdown
906
+ - For pre-compiled engine output (trusted source), set `sanitize={false}` to skip sanitization overhead
907
+ - For user-supplied or third-party HTML, always use `sanitize={true}` (the default)
908
+
488
909
  ### ContentPane
489
910
 
490
911
  Display and edit file content with syntax highlighting, markdown preview, and optional editing.
@@ -499,6 +920,8 @@ interface ContentPaneProps {
499
920
  error?: string | null; // Error message to display
500
921
  readOnly?: boolean; // Hide edit/save buttons (default: false)
501
922
  truncationInfo?: TruncationInfo; // Info about truncated files
923
+ codeHighlight?: boolean; // Enable syntax highlighting (opt-in, requires lowlight)
924
+ renderBinaryPreview?: (ext: string, content: string | Uint8Array) => ReactNode | null; // Custom binary preview
502
925
  // Lifted edit state
503
926
  isEditing?: boolean; // True when in edit mode
504
927
  editedContent?: string; // Content being edited
@@ -513,12 +936,66 @@ interface ContentPaneProps {
513
936
  **Features:**
514
937
 
515
938
  - Breadcrumb navigation for file paths
516
- - Syntax highlighting for code files
939
+ - Syntax highlighting for code files (opt-in via `codeHighlight` prop)
517
940
  - Markdown split-preview (editor + preview) for `.md` files
518
941
  - Optional frontmatter display
519
- - Edit mode for supported file types
942
+ - Edit mode for supported file types (.md, .ts, .tsx, .js, .jsx, .py, .json, .css)
520
943
  - Truncation warning for large files
521
944
  - Lazy-loaded CodeMirror editor (bundle cost only on demand)
945
+ - Custom binary/image preview via `renderBinaryPreview` render-prop
946
+
947
+ **Code Highlighting (Optional)**:
948
+
949
+ Enable syntax highlighting for non-markdown code blocks:
950
+
951
+ ```typescript
952
+ import { ContentPane } from '@miethe/ui/content-viewer';
953
+
954
+ <ContentPane
955
+ path="src/utils.ts"
956
+ content={tsCode}
957
+ codeHighlight={true}
958
+ />
959
+ ```
960
+
961
+ Requires `lowlight` (v2 or v3) installed:
962
+
963
+ ```bash
964
+ npm install lowlight
965
+ ```
966
+
967
+ Import a highlight.js theme CSS:
968
+
969
+ ```typescript
970
+ // In your app layout or CSS file
971
+ import 'highlight.js/styles/github.css'; // Light mode
972
+ // or
973
+ import 'highlight.js/styles/github-dark.css'; // Dark mode
974
+ ```
975
+
976
+ Without lowlight installed, code renders as plain text (no error).
977
+
978
+ **Custom Binary Previews**:
979
+
980
+ Provide a render-prop for custom binary/image/PDF previews:
981
+
982
+ ```typescript
983
+ <ContentPane
984
+ path={path}
985
+ content={content}
986
+ renderBinaryPreview={(ext, content) => {
987
+ if (ext === 'pdf') {
988
+ return <PdfViewer content={content} />; // Your custom viewer
989
+ }
990
+ if (ext === 'png' || ext === 'jpg') {
991
+ return <img src={`data:image/${ext};base64,${content}`} />;
992
+ }
993
+ return null; // Fall through to text view
994
+ }}
995
+ />
996
+ ```
997
+
998
+ Returning `null` from the render-prop falls through to the default text display.
522
999
 
523
1000
  **Example:**
524
1001
 
@@ -599,7 +1076,7 @@ interface SplitPreviewProps {
599
1076
 
600
1077
  ### MarkdownEditor
601
1078
 
602
- CodeMirror-based markdown editor for editing `.md` files. Also lazy-loaded.
1079
+ CodeMirror-based markdown editor for editing `.md` files. Also lazy-loaded with reactive dark-mode support.
603
1080
 
604
1081
  **Props:**
605
1082
 
@@ -611,6 +1088,78 @@ interface MarkdownEditorProps {
611
1088
  }
612
1089
  ```
613
1090
 
1091
+ **Features:**
1092
+
1093
+ - Live theme switching (tracks OS `prefers-color-scheme` changes at runtime)
1094
+ - Full markdown syntax support
1095
+ - Lazy-loaded for optimal bundle size
1096
+
1097
+ ### CodeEditor
1098
+
1099
+ CodeMirror 6 editor for non-markdown code files (.ts, .tsx, .js, .jsx, .py, .json, .css). Exported from `@miethe/ui/editor`.
1100
+
1101
+ **Import:**
1102
+
1103
+ ```typescript
1104
+ import { CodeEditor } from '@miethe/ui/editor';
1105
+ ```
1106
+
1107
+ **Props:**
1108
+
1109
+ ```typescript
1110
+ interface CodeEditorProps {
1111
+ initialContent: string; // Initial code content
1112
+ onChange: (content: string) => void; // Called on every keystroke
1113
+ readOnly?: boolean; // Disable editing (default: false)
1114
+ language?: LanguageSupport; // CodeMirror language extension (optional)
1115
+ className?: string; // Additional CSS classes
1116
+ }
1117
+ ```
1118
+
1119
+ **Features:**
1120
+
1121
+ - Language detection by file extension
1122
+ - Syntax highlighting (via language extensions)
1123
+ - Reactive dark-mode support
1124
+ - Full keyboard navigation
1125
+ - Lazy-loaded via dynamic import
1126
+
1127
+ **Example:**
1128
+
1129
+ ```typescript
1130
+ import { CodeEditor } from '@miethe/ui/editor';
1131
+ import { javascript } from '@codemirror/lang-javascript';
1132
+ import { python } from '@codemirror/lang-python';
1133
+
1134
+ // With auto-detected language
1135
+ <CodeEditor
1136
+ initialContent="const x = 42;"
1137
+ onChange={(content) => console.log(content)}
1138
+ />
1139
+
1140
+ // With explicit language
1141
+ <CodeEditor
1142
+ initialContent="def hello(): pass"
1143
+ onChange={setCode}
1144
+ language={python()}
1145
+ />
1146
+ ```
1147
+
1148
+ **ContentPane Integration:**
1149
+
1150
+ ContentPane automatically routes non-markdown editable files to CodeEditor:
1151
+
1152
+ ```typescript
1153
+ // ContentPane detects .ts/.tsx/.js/.jsx/.py/.json/.css files
1154
+ // and uses CodeEditor instead of MarkdownEditor when editing
1155
+ <ContentPane
1156
+ path="src/utils.ts"
1157
+ content={tsCode}
1158
+ isEditing={true}
1159
+ onEditChange={setCode}
1160
+ />
1161
+ ```
1162
+
614
1163
  ## Primitives API (`@miethe/ui/primitives`)
615
1164
 
616
1165
  Reusable UI primitives extracted from SkillMeat components. All primitives are production-ready, fully accessible, and compose with shadcn/ui components.
@@ -701,6 +1250,125 @@ interface ModalHeaderProps {
701
1250
  - Consistent styling with SkillMeat modals
702
1251
  - Accessible heading semantic
703
1252
 
1253
+ ### CreateEntityDialog
1254
+
1255
+ Schema-driven creation dialog for entities. Dynamically renders form fields from an `EntityFormSchema` definition using react-hook-form for state management and per-field validation.
1256
+
1257
+ **Import:**
1258
+ ```typescript
1259
+ import { CreateEntityDialog } from '@miethe/ui/primitives';
1260
+ import type { EntityFormSchema, FieldDef } from '@miethe/ui/primitives';
1261
+ ```
1262
+
1263
+ **Modes:**
1264
+
1265
+ 1. **Simple** — Flat list of fields
1266
+ 2. **Tabs** — Fields grouped into tabs
1267
+ 3. **Composite** — Tabs + member picker + review step
1268
+
1269
+ **Simple Mode Example:**
1270
+
1271
+ ```typescript
1272
+ <CreateEntityDialog
1273
+ open={isOpen}
1274
+ onOpenChange={setIsOpen}
1275
+ title="Create Skill"
1276
+ description="Add a new skill to your collection"
1277
+ schema={{
1278
+ mode: 'simple',
1279
+ fields: [
1280
+ { name: 'name', label: 'Name', type: 'text', required: true },
1281
+ { name: 'description', label: 'Description', type: 'textarea' },
1282
+ { name: 'tags', label: 'Tags', type: 'tags', options: [
1283
+ { label: 'Important', value: 'important' },
1284
+ ]},
1285
+ ],
1286
+ collection: { enabled: true, required: true, collapsible: true },
1287
+ }}
1288
+ collections={[
1289
+ { id: 'col-1', name: 'My Collection' },
1290
+ ]}
1291
+ onSubmit={async (values) => {
1292
+ await api.createSkill(values);
1293
+ }}
1294
+ submitLabel="Create"
1295
+ />
1296
+ ```
1297
+
1298
+ **Tabs Mode Example:**
1299
+
1300
+ ```typescript
1301
+ <CreateEntityDialog
1302
+ open={isOpen}
1303
+ onOpenChange={setIsOpen}
1304
+ title="Create Project"
1305
+ schema={{
1306
+ mode: 'tabs',
1307
+ tabs: [
1308
+ {
1309
+ id: 'basic',
1310
+ label: 'Basic Info',
1311
+ fields: [
1312
+ { name: 'name', label: 'Name', type: 'text', required: true },
1313
+ ],
1314
+ },
1315
+ {
1316
+ id: 'details',
1317
+ label: 'Details',
1318
+ fields: [
1319
+ { name: 'description', label: 'Description', type: 'textarea' },
1320
+ ],
1321
+ },
1322
+ ],
1323
+ collection: { enabled: true },
1324
+ }}
1325
+ collections={collections}
1326
+ onSubmit={async (values) => {
1327
+ await api.createProject(values);
1328
+ }}
1329
+ />
1330
+ ```
1331
+
1332
+ **Composite Mode:**
1333
+
1334
+ Composite mode adds a `members` picker and optional `renderReviewStep`. Selected member IDs are passed to `onMemberIdsChange`.
1335
+
1336
+ ```typescript
1337
+ schema={{
1338
+ mode: 'composite',
1339
+ tabs: [...],
1340
+ members: {
1341
+ entityType: 'skill',
1342
+ label: 'Skills',
1343
+ validateMin: 1,
1344
+ validateMax: 10,
1345
+ },
1346
+ renderReviewStep: ({ values, memberIds }) => (
1347
+ <div>Review: {values.name} with {memberIds.length} skills</div>
1348
+ ),
1349
+ }}
1350
+ memberIds={selectedIds}
1351
+ onMemberIdsChange={setSelectedIds}
1352
+ ```
1353
+
1354
+ **CollectionPicker Sub-component:**
1355
+
1356
+ The `CollectionPicker` component is embedded in simple/tabs/composite modes when `collection.enabled: true`.
1357
+
1358
+ **Props:**
1359
+ ```typescript
1360
+ interface CollectionPickerConfig {
1361
+ enabled: boolean; // Show picker
1362
+ required?: boolean; // Mandatory before submit
1363
+ collapsible?: boolean; // User can collapse/expand
1364
+ defaultCollectionId?: string; // Pre-select a collection
1365
+ }
1366
+ ```
1367
+
1368
+ **Entity Type Registry:**
1369
+
1370
+ Some entity types are disabled in the internal type registry (see `src/primitives/CreateEntityDialog.tsx`). Check runtime behavior or the source before creating forms for edge-case entity types.
1371
+
704
1372
  ### TabNavigation
705
1373
 
706
1374
  Horizontal tab list component for BaseArtifactModal and custom tab interfaces. Supports icons and keyboard navigation.
@@ -797,6 +1465,93 @@ import { LockIcon } from '@miethe/ui/primitives';
797
1465
  </div>
798
1466
  ```
799
1467
 
1468
+ ## Planning Primitives (`@miethe/ui/primitives`)
1469
+
1470
+ Five status and metadata display primitives extracted from the CCDash Planning Control Plane (PCP-709). These are generic enough to be reused across any planning-adjacent feature.
1471
+
1472
+ ### StatusChip
1473
+
1474
+ Five-variant status chip (neutral / ok / warn / error / info) as an inline-flex badge. Accepts an optional `tooltip` rendered as a `title` attribute.
1475
+
1476
+ ```typescript
1477
+ import { StatusChip } from '@miethe/ui/primitives';
1478
+
1479
+ <StatusChip label="pending" variant="warn" tooltip="Waiting on upstream" />
1480
+ ```
1481
+
1482
+ **Variants:** `neutral` (slate) | `ok` (emerald) | `warn` (amber) | `error` (rose) | `info` (blue)
1483
+
1484
+ ### EffectiveStatusChips
1485
+
1486
+ Renders a raw status chip plus an optional effective status chip when the two differ. The raw chip gains a hover tooltip showing provenance source and reason when `provenance` is supplied.
1487
+
1488
+ ```typescript
1489
+ import { EffectiveStatusChips } from '@miethe/ui/primitives';
1490
+
1491
+ <EffectiveStatusChips
1492
+ rawStatus="pending"
1493
+ effectiveStatus="blocked"
1494
+ isMismatch
1495
+ provenance={{ source: 'derived', reason: 'Blocked by upstream task', evidence: [] }}
1496
+ />
1497
+ ```
1498
+
1499
+ ### MismatchBadge
1500
+
1501
+ Amber mismatch indicator in two modes: compact inline chip or full banner with title, reason, and evidence label chips.
1502
+
1503
+ ```typescript
1504
+ import { MismatchBadge } from '@miethe/ui/primitives';
1505
+
1506
+ // Compact inline chip (for card/list contexts)
1507
+ <MismatchBadge state="stale" reason="Status diverged from progress" compact />
1508
+
1509
+ // Full banner (for detail headers)
1510
+ <MismatchBadge
1511
+ state="mismatched"
1512
+ reason="Progress says done but PRD is pending"
1513
+ evidenceLabels={['PRD-outdated', 'progress-diverged']}
1514
+ />
1515
+ ```
1516
+
1517
+ ### BatchReadinessPill
1518
+
1519
+ Wraps `StatusChip` to show batch readiness state (`ready` / `blocked` / `waiting` / `unknown`) with optional blocking node/task IDs displayed below the chip.
1520
+
1521
+ ```typescript
1522
+ import { BatchReadinessPill } from '@miethe/ui/primitives';
1523
+
1524
+ <BatchReadinessPill
1525
+ readinessState="blocked"
1526
+ blockingNodeIds={['prd-auth', 'prd-onboarding']}
1527
+ blockingTaskIds={['TASK-2.1']}
1528
+ />
1529
+ ```
1530
+
1531
+ ### PlanningNodeTypeIcon
1532
+
1533
+ Maps a `PlanningNodeType` string to a lucide-react icon. Supports 7 node types: `design_spec`, `prd`, `implementation_plan`, `progress`, `context`, `tracker`, `report`. Accepts `size` (default 13) and `className` props.
1534
+
1535
+ ```typescript
1536
+ import { PlanningNodeTypeIcon } from '@miethe/ui/primitives';
1537
+ import type { PlanningNodeType } from '@miethe/ui/primitives';
1538
+
1539
+ <PlanningNodeTypeIcon type="prd" size={16} className="text-blue-400" />
1540
+ ```
1541
+
1542
+ ### Variant Helpers
1543
+
1544
+ ```typescript
1545
+ import { statusVariant, readinessVariant } from '@miethe/ui/primitives';
1546
+ import type { StatusChipVariant, ReadinessVariant } from '@miethe/ui/primitives';
1547
+
1548
+ // Map any status string to a StatusChip variant
1549
+ const variant = statusVariant('in_progress'); // → 'ok'
1550
+ const readiness = readinessVariant('blocked'); // → 'error'
1551
+ ```
1552
+
1553
+ ---
1554
+
800
1555
  ## Filters API (`@miethe/ui/filters`)
801
1556
 
802
1557
  Reusable filter components for building toolbar filter bars. All components are pure presentational — consumers provide data via props.
@@ -891,6 +1646,86 @@ import { SortDropdown } from '@miethe/ui/filters';
891
1646
 
892
1647
  Clicking an already-selected field toggles the order.
893
1648
 
1649
+ ### FilterBar & Filter Slot System
1650
+
1651
+ Reusable filter bar with search, sort, view toggle, and pluggable slot registry. Renders a horizontal toolbar with four zones (left → right): search input, conditional filter slots, sort dropdown, and view toggle.
1652
+
1653
+ **Basic Usage:**
1654
+
1655
+ ```tsx
1656
+ import { FilterBar } from '@miethe/ui/filters';
1657
+
1658
+ <FilterBar
1659
+ searchValue={searchValue}
1660
+ onSearchChange={setSearchValue}
1661
+ sortField="name"
1662
+ sortOrder="asc"
1663
+ onSortChange={(field, order) => { /* ... */ }}
1664
+ view="grid"
1665
+ onViewChange={(mode) => { /* ... */ }}
1666
+ filterSlots={slots}
1667
+ conditionContext={{ pageId: 'artifacts' }}
1668
+ />
1669
+ ```
1670
+
1671
+ **Slot Registration Example:**
1672
+
1673
+ ```tsx
1674
+ import type { FilterSlotConfig } from '@miethe/ui/filters';
1675
+
1676
+ const filterSlots: FilterSlotConfig[] = [
1677
+ {
1678
+ id: 'trust-filter',
1679
+ label: 'Trust Level',
1680
+ component: <TrustLevelSelect selected={trust} onChange={setTrust} />,
1681
+ condition: (ctx) => ctx.pageId === 'marketplace-sources',
1682
+ },
1683
+ {
1684
+ id: 'type-filter',
1685
+ label: 'Type',
1686
+ component: <TypeSelect selected={types} onChange={setTypes} />,
1687
+ },
1688
+ ];
1689
+ ```
1690
+
1691
+ **Interface Definitions:**
1692
+
1693
+ ```typescript
1694
+ export interface FilterSlotConditionContext {
1695
+ pageId: string; // Stable identifier (e.g. "artifacts", "marketplace")
1696
+ edition?: string; // Backend edition: "local" or "enterprise"
1697
+ }
1698
+
1699
+ export interface FilterSlotConfig {
1700
+ id: string; // Unique slot identifier
1701
+ label: string; // Human-readable label for accessibility
1702
+ component: React.ReactNode; // React node to render when slot is active
1703
+ condition?: (ctx: FilterSlotConditionContext) => boolean; // Optional visibility predicate
1704
+ }
1705
+
1706
+ export interface FilterBarProps {
1707
+ searchValue: string;
1708
+ onSearchChange: (value: string) => void;
1709
+ searchPlaceholder?: string;
1710
+ searchAriaLabel?: string;
1711
+ filterSlots?: FilterSlotConfig[];
1712
+ conditionContext?: FilterSlotConditionContext;
1713
+ sort?: FilterBarSortProps;
1714
+ viewToggle?: {
1715
+ value: 'grid' | 'list';
1716
+ onChange: (mode: 'grid' | 'list') => void;
1717
+ enabled?: boolean;
1718
+ };
1719
+ className?: string;
1720
+ }
1721
+ ```
1722
+
1723
+ **Accessibility:**
1724
+
1725
+ The FilterBar renders a `role="search"` landmark region with a configurable `aria-label`. Slot authors should include `aria-label` on interactive elements (selects, popovers) to support screen readers. Condition predicates are evaluated synchronously at render time without side effects.
1726
+
1727
+ **Source:** `skillmeat/web/packages/ui/src/filters/`
1728
+
894
1729
  ## Bulk Actions API (`@miethe/ui/bulk-actions`)
895
1730
 
896
1731
  Floating toolbar component for multi-select interfaces. Displays a bottom-fixed action bar with selection count and action buttons when items are selected.
@@ -1444,20 +2279,39 @@ The adapter encodes a composite key (sourceId + artifactPath) into a single stri
1444
2279
 
1445
2280
  ### Lazy-Loaded Editor Bundle
1446
2281
 
1447
- The CodeMirror editor (used in `SplitPreview` and `MarkdownEditor`) is lazy-loaded and only fetched when needed:
2282
+ The CodeMirror editor (used in `SplitPreview`, `MarkdownEditor`, and `CodeEditor`) is lazy-loaded and only fetched when needed:
1448
2283
 
1449
- - **Markdown files in edit mode**: Editor chunk downloaded
1450
- - **Non-markdown files**: Editor never downloaded
2284
+ - **Markdown files in edit mode**: MarkdownEditor chunk downloaded
2285
+ - **Non-markdown files in edit mode** (.ts, .tsx, .js, .jsx, .py, .json, .css): CodeEditor chunk downloaded
1451
2286
  - **Read-only mode**: Editor chunk may still load for markdown files (preview uses a lighter markdown renderer)
1452
2287
 
1453
2288
  This significantly reduces the initial bundle size for consumers. If you're only using `FileTree` and `ContentPane` for viewing, you may never download the editor.
1454
2289
 
2290
+ ### Optional Syntax Highlighting
2291
+
2292
+ Syntax highlighting via `lowlight` is completely optional:
2293
+
2294
+ - **ArticleViewer** `codeHighlight` prop: Defaults to `false`; dynamically imports lowlight (~15KB gzip) when enabled
2295
+ - **ContentPane** `codeHighlight` prop: Defaults to `false`; gracefully degrades to plain text if lowlight is not installed
2296
+
2297
+ Both use **lowlight** (the same optional peer dependency) for a single, consistent highlighting engine across the package.
2298
+
2299
+ Install lowlight only if you want to enable code highlighting:
2300
+
2301
+ ```bash
2302
+ npm install lowlight
2303
+ ```
2304
+
2305
+ Without lowlight, code renders as plain text with no error or warning.
2306
+
1455
2307
  ### Component Structure
1456
2308
 
1457
2309
  - **`FileTree`** - ~8 KB gzipped (fully bundled, no lazy loading)
1458
2310
  - **`ContentPane`** - ~5 KB gzipped (fully bundled)
1459
2311
  - **`FrontmatterDisplay`** - ~2 KB gzipped (fully bundled)
1460
2312
  - **`SplitPreview` + `MarkdownEditor`** (lazy) - ~50 KB gzipped (on demand)
2313
+ - **`CodeEditor`** (lazy) - ~40 KB gzipped (on demand, only for editing non-markdown files)
2314
+ - **`lowlight`** (optional) - ~15 KB gzipped (only when syntax highlighting is enabled)
1461
2315
 
1462
2316
  ## Accessibility
1463
2317