@miethe/ui 0.2.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 (251) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +1536 -0
  3. package/dist/bulk-actions/Button.d.ts +28 -0
  4. package/dist/bulk-actions/Button.d.ts.map +1 -0
  5. package/dist/bulk-actions/Button.js +45 -0
  6. package/dist/bulk-actions/Button.js.map +1 -0
  7. package/dist/bulk-actions/bulk-action-bar.d.ts +91 -0
  8. package/dist/bulk-actions/bulk-action-bar.d.ts.map +1 -0
  9. package/dist/bulk-actions/bulk-action-bar.js +94 -0
  10. package/dist/bulk-actions/bulk-action-bar.js.map +1 -0
  11. package/dist/bulk-actions/index.d.ts +5 -0
  12. package/dist/bulk-actions/index.d.ts.map +1 -0
  13. package/dist/bulk-actions/index.js +7 -0
  14. package/dist/bulk-actions/index.js.map +1 -0
  15. package/dist/bulk-actions/utils.d.ts +6 -0
  16. package/dist/bulk-actions/utils.d.ts.map +1 -0
  17. package/dist/bulk-actions/utils.js +9 -0
  18. package/dist/bulk-actions/utils.js.map +1 -0
  19. package/dist/components/ui/alert.d.ts +9 -0
  20. package/dist/components/ui/alert.d.ts.map +1 -0
  21. package/dist/components/ui/alert.js +23 -0
  22. package/dist/components/ui/alert.js.map +1 -0
  23. package/dist/components/ui/button.d.ts +12 -0
  24. package/dist/components/ui/button.d.ts.map +1 -0
  25. package/dist/components/ui/button.js +34 -0
  26. package/dist/components/ui/button.js.map +1 -0
  27. package/dist/components/ui/collapsible.d.ts +6 -0
  28. package/dist/components/ui/collapsible.d.ts.map +1 -0
  29. package/dist/components/ui/collapsible.js +7 -0
  30. package/dist/components/ui/collapsible.js.map +1 -0
  31. package/dist/components/ui/skeleton.d.ts +4 -0
  32. package/dist/components/ui/skeleton.d.ts.map +1 -0
  33. package/dist/components/ui/skeleton.js +7 -0
  34. package/dist/components/ui/skeleton.js.map +1 -0
  35. package/dist/content-viewer/ContentPane.d.ts +107 -0
  36. package/dist/content-viewer/ContentPane.d.ts.map +1 -0
  37. package/dist/content-viewer/ContentPane.js +247 -0
  38. package/dist/content-viewer/ContentPane.js.map +1 -0
  39. package/dist/content-viewer/ContentViewerProvider.d.ts +83 -0
  40. package/dist/content-viewer/ContentViewerProvider.d.ts.map +1 -0
  41. package/dist/content-viewer/ContentViewerProvider.js +92 -0
  42. package/dist/content-viewer/ContentViewerProvider.js.map +1 -0
  43. package/dist/content-viewer/FileTree.d.ts +71 -0
  44. package/dist/content-viewer/FileTree.d.ts.map +1 -0
  45. package/dist/content-viewer/FileTree.js +294 -0
  46. package/dist/content-viewer/FileTree.js.map +1 -0
  47. package/dist/content-viewer/adapters.d.ts +101 -0
  48. package/dist/content-viewer/adapters.d.ts.map +1 -0
  49. package/dist/content-viewer/adapters.js +32 -0
  50. package/dist/content-viewer/adapters.js.map +1 -0
  51. package/dist/content-viewer/index.d.ts +8 -0
  52. package/dist/content-viewer/index.d.ts.map +1 -0
  53. package/dist/content-viewer/index.js +5 -0
  54. package/dist/content-viewer/index.js.map +1 -0
  55. package/dist/diff/DiffViewer.d.ts +112 -0
  56. package/dist/diff/DiffViewer.d.ts.map +1 -0
  57. package/dist/diff/DiffViewer.js +414 -0
  58. package/dist/diff/DiffViewer.js.map +1 -0
  59. package/dist/diff/diff.d.ts +32 -0
  60. package/dist/diff/diff.d.ts.map +1 -0
  61. package/dist/diff/diff.js +8 -0
  62. package/dist/diff/diff.js.map +1 -0
  63. package/dist/diff/index.d.ts +4 -0
  64. package/dist/diff/index.d.ts.map +1 -0
  65. package/dist/diff/index.js +3 -0
  66. package/dist/diff/index.js.map +1 -0
  67. package/dist/display/FilePreviewPane.d.ts +31 -0
  68. package/dist/display/FilePreviewPane.d.ts.map +1 -0
  69. package/dist/display/FilePreviewPane.js +144 -0
  70. package/dist/display/FilePreviewPane.js.map +1 -0
  71. package/dist/display/FrontmatterDisplay.d.ts +33 -0
  72. package/dist/display/FrontmatterDisplay.d.ts.map +1 -0
  73. package/dist/display/FrontmatterDisplay.js +79 -0
  74. package/dist/display/FrontmatterDisplay.js.map +1 -0
  75. package/dist/display/index.d.ts +5 -0
  76. package/dist/display/index.d.ts.map +1 -0
  77. package/dist/display/index.js +4 -0
  78. package/dist/display/index.js.map +1 -0
  79. package/dist/editor/MarkdownEditor.d.ts +28 -0
  80. package/dist/editor/MarkdownEditor.d.ts.map +1 -0
  81. package/dist/editor/MarkdownEditor.js +160 -0
  82. package/dist/editor/MarkdownEditor.js.map +1 -0
  83. package/dist/editor/SplitPreview.d.ts +28 -0
  84. package/dist/editor/SplitPreview.d.ts.map +1 -0
  85. package/dist/editor/SplitPreview.js +34 -0
  86. package/dist/editor/SplitPreview.js.map +1 -0
  87. package/dist/editor/index.d.ts +5 -0
  88. package/dist/editor/index.d.ts.map +1 -0
  89. package/dist/editor/index.js +4 -0
  90. package/dist/editor/index.js.map +1 -0
  91. package/dist/filters/filters-dropdown.d.ts +24 -0
  92. package/dist/filters/filters-dropdown.d.ts.map +1 -0
  93. package/dist/filters/filters-dropdown.js +36 -0
  94. package/dist/filters/filters-dropdown.js.map +1 -0
  95. package/dist/filters/index.d.ts +9 -0
  96. package/dist/filters/index.d.ts.map +1 -0
  97. package/dist/filters/index.js +5 -0
  98. package/dist/filters/index.js.map +1 -0
  99. package/dist/filters/sort-dropdown.d.ts +13 -0
  100. package/dist/filters/sort-dropdown.d.ts.map +1 -0
  101. package/dist/filters/sort-dropdown.js +20 -0
  102. package/dist/filters/sort-dropdown.js.map +1 -0
  103. package/dist/filters/tag-filter-popover.d.ts +39 -0
  104. package/dist/filters/tag-filter-popover.d.ts.map +1 -0
  105. package/dist/filters/tag-filter-popover.js +72 -0
  106. package/dist/filters/tag-filter-popover.js.map +1 -0
  107. package/dist/filters/tool-filter-popover.d.ts +42 -0
  108. package/dist/filters/tool-filter-popover.d.ts.map +1 -0
  109. package/dist/filters/tool-filter-popover.js +67 -0
  110. package/dist/filters/tool-filter-popover.js.map +1 -0
  111. package/dist/hooks/use-debounce.d.ts +9 -0
  112. package/dist/hooks/use-debounce.d.ts.map +1 -0
  113. package/dist/hooks/use-debounce.js +21 -0
  114. package/dist/hooks/use-debounce.js.map +1 -0
  115. package/dist/hooks/use-intersection-observer.d.ts +11 -0
  116. package/dist/hooks/use-intersection-observer.d.ts.map +1 -0
  117. package/dist/hooks/use-intersection-observer.js +25 -0
  118. package/dist/hooks/use-intersection-observer.js.map +1 -0
  119. package/dist/index.d.ts +10 -0
  120. package/dist/index.d.ts.map +1 -0
  121. package/dist/index.js +10 -0
  122. package/dist/index.js.map +1 -0
  123. package/dist/pickers/EntityPickerDialog.d.ts +233 -0
  124. package/dist/pickers/EntityPickerDialog.d.ts.map +1 -0
  125. package/dist/pickers/EntityPickerDialog.js +355 -0
  126. package/dist/pickers/EntityPickerDialog.js.map +1 -0
  127. package/dist/pickers/EntityPickerViewToggle.d.ts +8 -0
  128. package/dist/pickers/EntityPickerViewToggle.d.ts.map +1 -0
  129. package/dist/pickers/EntityPickerViewToggle.js +17 -0
  130. package/dist/pickers/EntityPickerViewToggle.js.map +1 -0
  131. package/dist/pickers/index.d.ts +5 -0
  132. package/dist/pickers/index.d.ts.map +1 -0
  133. package/dist/pickers/index.js +3 -0
  134. package/dist/pickers/index.js.map +1 -0
  135. package/dist/primitives/Badge.d.ts +16 -0
  136. package/dist/primitives/Badge.d.ts.map +1 -0
  137. package/dist/primitives/Badge.js +43 -0
  138. package/dist/primitives/Badge.js.map +1 -0
  139. package/dist/primitives/BaseArtifactModal.d.ts +114 -0
  140. package/dist/primitives/BaseArtifactModal.d.ts.map +1 -0
  141. package/dist/primitives/BaseArtifactModal.js +76 -0
  142. package/dist/primitives/BaseArtifactModal.js.map +1 -0
  143. package/dist/primitives/Dialog.d.ts +20 -0
  144. package/dist/primitives/Dialog.d.ts.map +1 -0
  145. package/dist/primitives/Dialog.js +24 -0
  146. package/dist/primitives/Dialog.js.map +1 -0
  147. package/dist/primitives/DropdownMenu.d.ts +28 -0
  148. package/dist/primitives/DropdownMenu.d.ts.map +1 -0
  149. package/dist/primitives/DropdownMenu.js +34 -0
  150. package/dist/primitives/DropdownMenu.js.map +1 -0
  151. package/dist/primitives/EnterpriseOwnerBadge.d.ts +9 -0
  152. package/dist/primitives/EnterpriseOwnerBadge.d.ts.map +1 -0
  153. package/dist/primitives/EnterpriseOwnerBadge.js +12 -0
  154. package/dist/primitives/EnterpriseOwnerBadge.js.map +1 -0
  155. package/dist/primitives/GroupedSelect.d.ts +30 -0
  156. package/dist/primitives/GroupedSelect.d.ts.map +1 -0
  157. package/dist/primitives/GroupedSelect.js +47 -0
  158. package/dist/primitives/GroupedSelect.js.map +1 -0
  159. package/dist/primitives/Input.d.ts +6 -0
  160. package/dist/primitives/Input.d.ts.map +1 -0
  161. package/dist/primitives/Input.js +9 -0
  162. package/dist/primitives/Input.js.map +1 -0
  163. package/dist/primitives/LockIcon.d.ts +11 -0
  164. package/dist/primitives/LockIcon.d.ts.map +1 -0
  165. package/dist/primitives/LockIcon.js +15 -0
  166. package/dist/primitives/LockIcon.js.map +1 -0
  167. package/dist/primitives/MaskedSecretInput.d.ts +16 -0
  168. package/dist/primitives/MaskedSecretInput.d.ts.map +1 -0
  169. package/dist/primitives/MaskedSecretInput.js +42 -0
  170. package/dist/primitives/MaskedSecretInput.js.map +1 -0
  171. package/dist/primitives/ModalHeader.d.ts +66 -0
  172. package/dist/primitives/ModalHeader.d.ts.map +1 -0
  173. package/dist/primitives/ModalHeader.js +58 -0
  174. package/dist/primitives/ModalHeader.js.map +1 -0
  175. package/dist/primitives/Popover.d.ts +9 -0
  176. package/dist/primitives/Popover.d.ts.map +1 -0
  177. package/dist/primitives/Popover.js +13 -0
  178. package/dist/primitives/Popover.js.map +1 -0
  179. package/dist/primitives/ScrollArea.d.ts +6 -0
  180. package/dist/primitives/ScrollArea.d.ts.map +1 -0
  181. package/dist/primitives/ScrollArea.js +11 -0
  182. package/dist/primitives/ScrollArea.js.map +1 -0
  183. package/dist/primitives/SearchableCombobox.d.ts +30 -0
  184. package/dist/primitives/SearchableCombobox.d.ts.map +1 -0
  185. package/dist/primitives/SearchableCombobox.js +124 -0
  186. package/dist/primitives/SearchableCombobox.js.map +1 -0
  187. package/dist/primitives/SearchablePickerDialog.d.ts +20 -0
  188. package/dist/primitives/SearchablePickerDialog.d.ts.map +1 -0
  189. package/dist/primitives/SearchablePickerDialog.js +78 -0
  190. package/dist/primitives/SearchablePickerDialog.js.map +1 -0
  191. package/dist/primitives/StatusBadge.d.ts +21 -0
  192. package/dist/primitives/StatusBadge.d.ts.map +1 -0
  193. package/dist/primitives/StatusBadge.js +25 -0
  194. package/dist/primitives/StatusBadge.js.map +1 -0
  195. package/dist/primitives/TabNavigation.d.ts +68 -0
  196. package/dist/primitives/TabNavigation.d.ts.map +1 -0
  197. package/dist/primitives/TabNavigation.js +74 -0
  198. package/dist/primitives/TabNavigation.js.map +1 -0
  199. package/dist/primitives/Tabs.d.ts +8 -0
  200. package/dist/primitives/Tabs.d.ts.map +1 -0
  201. package/dist/primitives/Tabs.js +14 -0
  202. package/dist/primitives/Tabs.js.map +1 -0
  203. package/dist/primitives/Tooltip.d.ts +8 -0
  204. package/dist/primitives/Tooltip.d.ts.map +1 -0
  205. package/dist/primitives/Tooltip.js +12 -0
  206. package/dist/primitives/Tooltip.js.map +1 -0
  207. package/dist/primitives/VerticalTabNavigation.d.ts +75 -0
  208. package/dist/primitives/VerticalTabNavigation.d.ts.map +1 -0
  209. package/dist/primitives/VerticalTabNavigation.js +166 -0
  210. package/dist/primitives/VerticalTabNavigation.js.map +1 -0
  211. package/dist/primitives/ViewModeToggle.d.ts +12 -0
  212. package/dist/primitives/ViewModeToggle.d.ts.map +1 -0
  213. package/dist/primitives/ViewModeToggle.js +56 -0
  214. package/dist/primitives/ViewModeToggle.js.map +1 -0
  215. package/dist/primitives/WizardShell.d.ts +81 -0
  216. package/dist/primitives/WizardShell.d.ts.map +1 -0
  217. package/dist/primitives/WizardShell.js +73 -0
  218. package/dist/primitives/WizardShell.js.map +1 -0
  219. package/dist/primitives/index.d.ts +38 -0
  220. package/dist/primitives/index.d.ts.map +1 -0
  221. package/dist/primitives/index.js +24 -0
  222. package/dist/primitives/index.js.map +1 -0
  223. package/dist/primitives/utils.d.ts +6 -0
  224. package/dist/primitives/utils.d.ts.map +1 -0
  225. package/dist/primitives/utils.js +9 -0
  226. package/dist/primitives/utils.js.map +1 -0
  227. package/dist/types/index.d.ts +63 -0
  228. package/dist/types/index.d.ts.map +1 -0
  229. package/dist/types/index.js +9 -0
  230. package/dist/types/index.js.map +1 -0
  231. package/dist/utils/frontmatter.d.ts +63 -0
  232. package/dist/utils/frontmatter.d.ts.map +1 -0
  233. package/dist/utils/frontmatter.js +345 -0
  234. package/dist/utils/frontmatter.js.map +1 -0
  235. package/dist/utils/index.d.ts +6 -0
  236. package/dist/utils/index.d.ts.map +1 -0
  237. package/dist/utils/index.js +6 -0
  238. package/dist/utils/index.js.map +1 -0
  239. package/dist/utils/perf-marks.d.ts +28 -0
  240. package/dist/utils/perf-marks.d.ts.map +1 -0
  241. package/dist/utils/perf-marks.js +45 -0
  242. package/dist/utils/perf-marks.js.map +1 -0
  243. package/dist/utils/readme-utils.d.ts +67 -0
  244. package/dist/utils/readme-utils.d.ts.map +1 -0
  245. package/dist/utils/readme-utils.js +164 -0
  246. package/dist/utils/readme-utils.js.map +1 -0
  247. package/dist/utils/type-colors.d.ts +70 -0
  248. package/dist/utils/type-colors.d.ts.map +1 -0
  249. package/dist/utils/type-colors.js +118 -0
  250. package/dist/utils/type-colors.js.map +1 -0
  251. package/package.json +131 -0
package/README.md ADDED
@@ -0,0 +1,1536 @@
1
+ # @miethe/ui
2
+
3
+ A collection of reusable React components for viewing, editing, and navigating file content. Provides an adapter abstraction pattern that decouples components from any specific backend API, allowing flexible integration with custom data sources.
4
+
5
+ ## Overview
6
+
7
+ This package (formerly `@skillmeat/content-viewer`) was extracted from the SkillMeat web application during a UI refactoring effort. It provides production-ready components for:
8
+
9
+ ### Content Viewing & Editing
10
+ - **File tree browser** — Hierarchical navigation with keyboard support
11
+ - **File content viewer** — Display files with markdown editing and split preview
12
+ - **Diff viewer** — Side-by-side unified diff display with conflict resolution
13
+ - **File preview pane** — Quick file preview with markdown rendering and tier badges
14
+ - **Frontmatter display** — Collapsible YAML frontmatter viewer
15
+ - **Markdown editor** — CodeMirror-based editor with live preview
16
+
17
+ ### Consolidated Modal System (Phase 3)
18
+ - **Tab registry** — Declarative tab configuration with entity type / edition / lens / feature-flag gating
19
+ - **Metadata grid** — Key-value metadata display with collapsible sections
20
+ - **Timeline view** — Ordered history or event timeline rendering
21
+
22
+ ### Artifact Type Visualization (Tiered Card System)
23
+ - **Type-aware badges** — ColoredBadge and TypeIndicator with color mapping
24
+ - **Tag color provider** — Context-based type-to-color mapping
25
+ - **Type-color utilities** — Artifact/entity type to Tailwind color class resolution
26
+
27
+ ### Filtering & Operations
28
+ - **Filter components** — Tag/Tool filter popovers, Filters dropdown with AND/OR toggle, Sort dropdown
29
+ - **Utilities** — Frontmatter parsing, README extraction, type-color resolution, and more
30
+
31
+ The package uses an **adapter pattern** to remain backend-agnostic. You implement a simple `ContentViewerAdapter` interface and connect your own data-fetching hooks, making the components reusable across different APIs and applications.
32
+
33
+ ## Installation
34
+
35
+ ### From GitHub Packages (published package)
36
+
37
+ `@miethe/ui` is published to GitHub Packages at `npm.pkg.github.com`. Consuming it requires a GitHub token with `read:packages` scope.
38
+
39
+ **Step 1 — Configure the registry** in `.npmrc` at your project root:
40
+
41
+ ```
42
+ @miethe:registry=https://npm.pkg.github.com
43
+ //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
44
+ ```
45
+
46
+ Set the `GITHUB_TOKEN` environment variable to a [personal access token](https://github.com/settings/tokens) with `read:packages` scope. Do not hard-code the token in `.npmrc` — use the env var reference as shown.
47
+
48
+ **Step 2 — Install:**
49
+
50
+ ```bash
51
+ npm install @miethe/ui@0.1.0
52
+ # or
53
+ pnpm add @miethe/ui@0.1.0
54
+ ```
55
+
56
+ ### Within the SkillMeat monorepo (workspace)
57
+
58
+ ```bash
59
+ pnpm add @miethe/ui --filter your-package
60
+ ```
61
+
62
+ Or reference the workspace package directly in `package.json`:
63
+
64
+ ```json
65
+ {
66
+ "dependencies": {
67
+ "@miethe/ui": "workspace:*"
68
+ }
69
+ }
70
+ ```
71
+
72
+ ## Subpath Imports
73
+
74
+ The package is organized into seven submodules with tree-shakeable exports:
75
+
76
+ ```typescript
77
+ // Content viewer components
78
+ import { FileTree, ContentPane, ContentViewerProvider } from '@miethe/ui/content-viewer';
79
+
80
+ // Diff viewing components
81
+ import { DiffViewer, DiffViewerSkeleton } from '@miethe/ui/diff';
82
+
83
+ // Editor components
84
+ import { MarkdownEditor, SplitPreview } from '@miethe/ui/editor';
85
+
86
+ // Display components
87
+ import { FrontmatterDisplay, FilePreviewPane } from '@miethe/ui/display';
88
+
89
+ // Filter components
90
+ import {
91
+ TagFilterPopover, TagFilterBar,
92
+ ToolFilterPopover, ToolFilterBar,
93
+ FiltersDropdown,
94
+ SortDropdown,
95
+ } from '@miethe/ui/filters';
96
+
97
+ // Bulk action toolbar
98
+ import { BulkActionBar } from '@miethe/ui/bulk-actions';
99
+ import type { BulkActionBarProps, BulkAction } from '@miethe/ui/bulk-actions';
100
+
101
+ // UI primitives
102
+ import { BaseArtifactModal, ModalHeader, TabNavigation, EnterpriseOwnerBadge, LockIcon } from '@miethe/ui/primitives';
103
+
104
+ // Tab registry system (consolidated modals)
105
+ import { TabRegistry, getTabsForContext, type TabConfig, type TabConditions } from '@miethe/ui/tab-registry';
106
+
107
+ // Metadata & timeline (Phase 3 extractions)
108
+ import { MetadataGrid, TimelineView } from '@miethe/ui/components';
109
+
110
+ // Card system (type-aware badges & indicators)
111
+ import { TagColorProvider, ColoredBadge, TypeIndicator } from '@miethe/ui/card-system';
112
+
113
+ // Utilities
114
+ import { parseFrontmatter, stripFrontmatter, extractFirstParagraph, getTypeBarColor, getCardTint, artifactTypeCardTints } from '@miethe/ui/utils';
115
+ ```
116
+
117
+ ## Prerequisites
118
+
119
+ ### Tailwind CSS
120
+
121
+ Components use Tailwind CSS utility classes for all styling. Your build pipeline must have tailwindcss configured:
122
+
123
+ ```javascript
124
+ // tailwind.config.js
125
+ module.exports = {
126
+ content: [
127
+ './src/**/*.{ts,tsx}',
128
+ './node_modules/@miethe/ui/dist/**/*.js',
129
+ ],
130
+ theme: {
131
+ extend: {},
132
+ },
133
+ plugins: [],
134
+ };
135
+ ```
136
+
137
+ **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
+
139
+ ### Dark Mode
140
+
141
+ Components support dark mode via the Tailwind `dark:` variant. Enable dark mode in your Tailwind config:
142
+
143
+ ```javascript
144
+ // tailwind.config.js
145
+ module.exports = {
146
+ darkMode: 'class', // Enable dark mode with class strategy
147
+ content: [
148
+ './src/**/*.{ts,tsx}',
149
+ './node_modules/@miethe/ui/dist/**/*.js',
150
+ ],
151
+ // ... rest of config
152
+ };
153
+ ```
154
+
155
+ Then add the `dark` class to your root HTML element to activate dark mode styles:
156
+
157
+ ```html
158
+ <!-- Dark mode enabled -->
159
+ <html class="dark">
160
+ <body>...</body>
161
+ </html>
162
+ ```
163
+
164
+ Or use a theme provider component that toggles the class dynamically based on user preference.
165
+
166
+ ## Quick Start
167
+
168
+ ### 1. Create an Adapter
169
+
170
+ Implement the `ContentViewerAdapter` interface by wrapping your application's data-fetching hooks:
171
+
172
+ ```typescript
173
+ // lib/my-content-viewer-adapter.ts
174
+ import type { ContentViewerAdapter, AdapterHookOptions } from '@miethe/ui/content-viewer';
175
+ import { useFetchFileTree, useFetchFileContent } from '@/hooks';
176
+
177
+ export const myAdapter: ContentViewerAdapter = {
178
+ useFileTree(artifactId: string, options?: AdapterHookOptions) {
179
+ // Wrap your hook and normalize the return shape
180
+ const result = useFetchFileTree(artifactId, {
181
+ enabled: options?.enabled !== false,
182
+ });
183
+
184
+ return {
185
+ data: result.data,
186
+ isLoading: result.isLoading,
187
+ error: result.error ?? null,
188
+ };
189
+ },
190
+
191
+ useFileContent(artifactId: string, filePath: string, options?: AdapterHookOptions) {
192
+ const result = useFetchFileContent(artifactId, filePath, {
193
+ enabled: options?.enabled !== false,
194
+ });
195
+
196
+ return {
197
+ data: result.data,
198
+ isLoading: result.isLoading,
199
+ error: result.error ?? null,
200
+ };
201
+ },
202
+ };
203
+ ```
204
+
205
+ ### 2. Provide the Adapter
206
+
207
+ Wrap your component tree with `ContentViewerProvider`:
208
+
209
+ ```typescript
210
+ // app/layout.tsx
211
+ import { ContentViewerProvider } from '@miethe/ui/content-viewer';
212
+ import { myAdapter } from '@/lib/my-content-viewer-adapter';
213
+
214
+ export default function Layout({ children }: { children: React.ReactNode }) {
215
+ return (
216
+ <ContentViewerProvider adapter={myAdapter}>
217
+ {children}
218
+ </ContentViewerProvider>
219
+ );
220
+ }
221
+ ```
222
+
223
+ ### 3. Use Components
224
+
225
+ Now components can fetch data through your adapter:
226
+
227
+ ```typescript
228
+ // components/MyViewer.tsx
229
+ 'use client';
230
+
231
+ import { useState } from 'react';
232
+ import { FileTree, ContentPane } from '@miethe/ui/content-viewer';
233
+ import { DiffViewer, FilePreviewPane } from '@miethe/ui/display';
234
+
235
+ export function MyViewer({ artifactId }: { artifactId: string }) {
236
+ const [selectedPath, setSelectedPath] = useState<string | null>(null);
237
+
238
+ return (
239
+ <div className="flex h-screen gap-4">
240
+ <div className="w-64 border-r">
241
+ <FileTree
242
+ entityId={artifactId}
243
+ files={[]} // Loaded via adapter
244
+ selectedPath={selectedPath}
245
+ onSelect={setSelectedPath}
246
+ />
247
+ </div>
248
+ <div className="flex-1">
249
+ <ContentPane
250
+ path={selectedPath}
251
+ content={null} // Loaded via adapter
252
+ isLoading={false}
253
+ onSave={(content) => console.log('Save:', content)}
254
+ />
255
+ </div>
256
+ </div>
257
+ );
258
+ }
259
+
260
+ // Example: Diff Viewer for comparing versions
261
+ export function DiffExample() {
262
+ return (
263
+ <DiffViewer
264
+ files={[]}
265
+ leftLabel="Collection"
266
+ rightLabel="Project"
267
+ />
268
+ );
269
+ }
270
+
271
+ // Example: File preview with tier information
272
+ export function FilePreviewExample() {
273
+ return (
274
+ <FilePreviewPane
275
+ filePath="README.md"
276
+ content={null}
277
+ tier="collection"
278
+ isLoading={false}
279
+ />
280
+ );
281
+ }
282
+ ```
283
+
284
+ ## Components API
285
+
286
+ ### DiffViewer
287
+
288
+ A side-by-side unified diff viewer with file browser sidebar and optional sync conflict resolution actions.
289
+
290
+ **Props:**
291
+
292
+ ```typescript
293
+ interface DiffViewerProps {
294
+ files: FileDiff[]; // Array of file diffs to display
295
+ leftLabel?: string; // Label for left (before) panel
296
+ rightLabel?: string; // Label for right (after) panel
297
+ onClose?: () => void; // Callback when user closes the viewer
298
+ showResolutionActions?: boolean; // Show resolution action buttons (for sync conflicts)
299
+ onResolve?: (resolution: ResolutionType) => void; // Callback for resolution selection
300
+ localLabel?: string; // Custom label for local version (default: "Local (Project)")
301
+ remoteLabel?: string; // Custom label for remote version (default: "Remote (Collection)")
302
+ previewMode?: boolean; // Show preview mode UI before applying resolution
303
+ isResolving?: boolean; // Show loading state during resolution
304
+ isLoading?: boolean; // Show skeleton loading state
305
+ }
306
+
307
+ type ResolutionType = 'keep_local' | 'keep_remote' | 'merge';
308
+ ```
309
+
310
+ **Features:**
311
+
312
+ - Side-by-side unified diff display with syntax-colored additions and deletions
313
+ - File list sidebar for navigating multiple diffs
314
+ - Summary badges showing file counts by status (added, modified, deleted, unchanged)
315
+ - Optional conflict resolution actions for sync workflows
316
+ - Large diff handling with lazy-loading (diffs > 50KB or 1000 lines collapsed by default)
317
+ - Full keyboard navigation and accessibility support
318
+
319
+ **Loading State:**
320
+
321
+ Use `DiffViewerSkeleton` to show a loading state while diff data is being fetched:
322
+
323
+ ```typescript
324
+ import { DiffViewer, DiffViewerSkeleton } from '@miethe/ui/diff';
325
+
326
+ {isLoading ? (
327
+ <DiffViewerSkeleton />
328
+ ) : (
329
+ <DiffViewer
330
+ files={diffs}
331
+ leftLabel="Collection"
332
+ rightLabel="Project"
333
+ showResolutionActions={true}
334
+ onResolve={(resolution) => handleResolve(resolution)}
335
+ />
336
+ )}
337
+ ```
338
+
339
+ **Example with Mock Data:**
340
+
341
+ ```typescript
342
+ import { DiffViewer } from '@miethe/ui/diff';
343
+ import type { FileDiff } from '@miethe/ui/diff';
344
+
345
+ const mockDiffs: FileDiff[] = [
346
+ {
347
+ file_path: 'src/index.ts',
348
+ status: 'modified',
349
+ collection_hash: 'abc123',
350
+ project_hash: 'def456',
351
+ unified_diff: `--- a/src/index.ts
352
+ +++ b/src/index.ts
353
+ @@ -1,3 +1,4 @@
354
+ export function hello() {
355
+ - return 'world';
356
+ + return 'world!';
357
+ }`
358
+ },
359
+ {
360
+ file_path: 'README.md',
361
+ status: 'added',
362
+ collection_hash: null,
363
+ project_hash: '789abc',
364
+ unified_diff: `--- /dev/null
365
+ +++ b/README.md
366
+ @@ -0,0 +1 @@
367
+ +# My Project`
368
+ }
369
+ ];
370
+
371
+ export function DiffExample() {
372
+ return (
373
+ <DiffViewer
374
+ files={mockDiffs}
375
+ leftLabel="Collection"
376
+ rightLabel="Project"
377
+ showResolutionActions={true}
378
+ onResolve={(resolution) => console.log('Resolution:', resolution)}
379
+ />
380
+ );
381
+ }
382
+ ```
383
+
384
+ **Resolution Actions:**
385
+
386
+ When `showResolutionActions` is true, users can choose from:
387
+ - `keep_local` - Keep the local (project) version
388
+ - `keep_remote` - Keep the remote (collection) version
389
+ - `merge` - Merge changes from both versions
390
+
391
+ ### FilePreviewPane
392
+
393
+ File content preview with markdown rendering, code display, and plain text support. Includes a tier badge showing the source context (source, collection, or project).
394
+
395
+ **Props:**
396
+
397
+ ```typescript
398
+ interface FilePreviewPaneProps {
399
+ filePath: string | null; // Path of file being previewed
400
+ content: string | null; // File content to display
401
+ tier: 'source' | 'collection' | 'project'; // Context tier for badge display
402
+ isLoading: boolean; // Show loading skeleton
403
+ }
404
+ ```
405
+
406
+ **Features:**
407
+
408
+ - Auto-detects file type (markdown, code, text) based on extension
409
+ - Markdown files rendered with basic HTML conversion (headers, bold, italic, code blocks, links, lists)
410
+ - Code files displayed in monospace with line numbers
411
+ - Tier badge indicating file source (Source, Collection, or Project)
412
+ - Scrollable container for large files
413
+ - Loading skeleton during fetch
414
+
415
+ **Example with Markdown Preview:**
416
+
417
+ ```typescript
418
+ import { FilePreviewPane } from '@miethe/ui/display';
419
+
420
+ export function PreviewExample() {
421
+ const [content, setContent] = useState<string | null>(null);
422
+ const [isLoading, setIsLoading] = useState(false);
423
+
424
+ return (
425
+ <FilePreviewPane
426
+ filePath="README.md"
427
+ content={content}
428
+ tier="collection"
429
+ isLoading={isLoading}
430
+ />
431
+ );
432
+ }
433
+ ```
434
+
435
+ **Tier Badges:**
436
+
437
+ The `tier` prop controls the badge appearance and label:
438
+ - `source` - Outline badge labeled "Source"
439
+ - `collection` - Secondary variant labeled "Collection"
440
+ - `project` - Default/primary variant labeled "Project"
441
+
442
+ ### FileTree
443
+
444
+ A hierarchical file browser with keyboard navigation and selection support.
445
+
446
+ **Props:**
447
+
448
+ ```typescript
449
+ interface FileTreeProps {
450
+ entityId: string; // Unique identifier for the entity (used as adapter key)
451
+ files: FileNode[]; // Array of file tree nodes
452
+ selectedPath: string | null; // Currently selected file path
453
+ onSelect: (path: string) => void; // Called when user selects a file
454
+ onAddFile?: () => void; // Optional: called when user clicks "Add File"
455
+ onDeleteFile?: (path: string) => void; // Optional: called when user deletes a file
456
+ isLoading?: boolean; // Show loading skeleton
457
+ readOnly?: boolean; // Hide create/delete buttons (default: false)
458
+ ariaLabel?: string; // Accessible label (default: "File browser")
459
+ }
460
+ ```
461
+
462
+ **Features:**
463
+
464
+ - Expandable/collapsible directories
465
+ - File type icons (markdown, code, JSON, etc.)
466
+ - Full keyboard navigation (arrows, home/end, enter/space)
467
+ - ARIA tree pattern with roving tabindex
468
+ - Optional file creation and deletion
469
+ - Read-only mode for view-only interfaces
470
+
471
+ **Example:**
472
+
473
+ ```typescript
474
+ <FileTree
475
+ entityId="skill-123"
476
+ files={[
477
+ { name: 'src', type: 'directory', path: 'src', children: [
478
+ { name: 'index.ts', type: 'file', path: 'src/index.ts' }
479
+ ] }
480
+ ]}
481
+ selectedPath="src/index.ts"
482
+ onSelect={(path) => handleSelect(path)}
483
+ onDeleteFile={(path) => handleDelete(path)}
484
+ readOnly={false}
485
+ />
486
+ ```
487
+
488
+ ### ContentPane
489
+
490
+ Display and edit file content with syntax highlighting, markdown preview, and optional editing.
491
+
492
+ **Props:**
493
+
494
+ ```typescript
495
+ interface ContentPaneProps {
496
+ path: string | null; // File path being displayed
497
+ content: string | null; // File content
498
+ isLoading?: boolean; // Show loading skeleton
499
+ error?: string | null; // Error message to display
500
+ readOnly?: boolean; // Hide edit/save buttons (default: false)
501
+ truncationInfo?: TruncationInfo; // Info about truncated files
502
+ // Lifted edit state
503
+ isEditing?: boolean; // True when in edit mode
504
+ editedContent?: string; // Content being edited
505
+ onEditStart?: () => void; // Called when user clicks "Edit"
506
+ onEditChange?: (content: string) => void; // Called on every keystroke
507
+ onSave?: (content: string) => void | Promise<void>; // Called on save
508
+ onCancel?: () => void; // Called on cancel
509
+ ariaLabel?: string; // Accessible label
510
+ }
511
+ ```
512
+
513
+ **Features:**
514
+
515
+ - Breadcrumb navigation for file paths
516
+ - Syntax highlighting for code files
517
+ - Markdown split-preview (editor + preview) for `.md` files
518
+ - Optional frontmatter display
519
+ - Edit mode for supported file types
520
+ - Truncation warning for large files
521
+ - Lazy-loaded CodeMirror editor (bundle cost only on demand)
522
+
523
+ **Example:**
524
+
525
+ ```typescript
526
+ const [isEditing, setIsEditing] = useState(false);
527
+ const [editedContent, setEditedContent] = useState('');
528
+
529
+ <ContentPane
530
+ path="README.md"
531
+ content={fileContent}
532
+ isLoading={isLoading}
533
+ isEditing={isEditing}
534
+ editedContent={editedContent}
535
+ onEditStart={() => {
536
+ setEditedContent(fileContent);
537
+ setIsEditing(true);
538
+ }}
539
+ onEditChange={setEditedContent}
540
+ onSave={async (content) => {
541
+ await saveFile(content);
542
+ setIsEditing(false);
543
+ }}
544
+ onCancel={() => setIsEditing(false)}
545
+ />
546
+ ```
547
+
548
+ ### FrontmatterDisplay
549
+
550
+ Display parsed YAML frontmatter as key-value pairs with collapsible state.
551
+
552
+ **Props:**
553
+
554
+ ```typescript
555
+ interface FrontmatterDisplayProps {
556
+ frontmatter: Record<string, unknown>; // Parsed YAML frontmatter object
557
+ defaultCollapsed?: boolean; // Start collapsed (default: false)
558
+ className?: string; // Additional CSS classes
559
+ }
560
+ ```
561
+
562
+ **Supports:**
563
+
564
+ - Strings, numbers, booleans, null
565
+ - Arrays (rendered as comma-separated values)
566
+ - Nested objects (one level, rendered indented)
567
+
568
+ **Example:**
569
+
570
+ ```typescript
571
+ const frontmatter = {
572
+ title: 'My Document',
573
+ tags: ['react', 'typescript'],
574
+ author: { name: 'John', email: 'john@example.com' }
575
+ };
576
+
577
+ <FrontmatterDisplay
578
+ frontmatter={frontmatter}
579
+ defaultCollapsed={false}
580
+ className="mb-4"
581
+ />
582
+ ```
583
+
584
+ ### SplitPreview
585
+
586
+ CodeMirror-based markdown editor with live preview. Lazy-loaded for performance.
587
+
588
+ **Props:**
589
+
590
+ ```typescript
591
+ interface SplitPreviewProps {
592
+ content: string; // Current content
593
+ onChange: (content: string) => void; // Called on every keystroke
594
+ isEditing: boolean; // Control editor visibility
595
+ }
596
+ ```
597
+
598
+ **Note:** This component is lazy-loaded and only fetched when rendering a markdown file in edit mode. Non-markdown files never trigger the download.
599
+
600
+ ### MarkdownEditor
601
+
602
+ CodeMirror-based markdown editor for editing `.md` files. Also lazy-loaded.
603
+
604
+ **Props:**
605
+
606
+ ```typescript
607
+ interface MarkdownEditorProps {
608
+ content: string; // Current content
609
+ onChange: (content: string) => void; // Called on every keystroke
610
+ readOnly?: boolean; // Disable editing (default: false)
611
+ }
612
+ ```
613
+
614
+ ## Primitives API (`@miethe/ui/primitives`)
615
+
616
+ Reusable UI primitives extracted from SkillMeat components. All primitives are production-ready, fully accessible, and compose with shadcn/ui components.
617
+
618
+ ### BaseArtifactModal
619
+
620
+ Controlled composition-based modal foundation for artifact-focused dialogs. Encapsulates common structure (dialog wrapper, header, tabs, content area) while delegating domain-specific logic to consumers.
621
+
622
+ **Props:**
623
+
624
+ ```typescript
625
+ interface BaseArtifactModalProps {
626
+ artifact: Artifact; // Artifact to display
627
+ open: boolean; // Dialog open state
628
+ onClose: () => void; // Close handler
629
+ activeTab: string; // Controlled tab value
630
+ onTabChange: (tab: string) => void; // Tab change callback
631
+ tabs: Tab[]; // Tab definitions for navigation
632
+ headerActions?: React.ReactNode; // Optional actions in header (right side)
633
+ children: React.ReactNode; // Tab content (TabContentWrapper elements)
634
+ aboveTabsContent?: React.ReactNode; // Content between header and tabs
635
+ returnTo?: string; // Optional return URL
636
+ onReturn?: () => void; // Optional return button handler
637
+ }
638
+
639
+ interface Tab {
640
+ value: string;
641
+ label: string;
642
+ icon?: React.ComponentType<{ className?: string }>;
643
+ }
644
+ ```
645
+
646
+ **Features:**
647
+
648
+ - Automatic artifact icon resolution from ARTIFACT_TYPES config
649
+ - Composable tab content with TabContentWrapper
650
+ - Header action slots for custom controls
651
+ - Return navigation support
652
+ - Full keyboard navigation and accessibility
653
+
654
+ **Example:**
655
+
656
+ ```typescript
657
+ const tabs: Tab[] = [
658
+ { value: 'status', label: 'Status', icon: Activity },
659
+ { value: 'sync', label: 'Sync', icon: RefreshCcw },
660
+ ];
661
+
662
+ <BaseArtifactModal
663
+ artifact={artifact}
664
+ open={isOpen}
665
+ onClose={() => setIsOpen(false)}
666
+ activeTab={activeTab}
667
+ onTabChange={setActiveTab}
668
+ tabs={tabs}
669
+ headerActions={<HealthIndicator artifact={artifact} />}
670
+ >
671
+ <TabContentWrapper value="status">
672
+ <StatusContent artifact={artifact} />
673
+ </TabContentWrapper>
674
+ <TabContentWrapper value="sync">
675
+ <SyncContent artifact={artifact} />
676
+ </TabContentWrapper>
677
+ </BaseArtifactModal>
678
+ ```
679
+
680
+ ### ModalHeader
681
+
682
+ Header component for use within BaseArtifactModal or standalone dialogs. Displays artifact metadata with icon, name, and optional action buttons.
683
+
684
+ **Props:**
685
+
686
+ ```typescript
687
+ interface ModalHeaderProps {
688
+ artifact?: Artifact; // Optional artifact for icon/name display
689
+ title?: string; // Custom title (overrides artifact name)
690
+ icon?: React.ReactNode; // Custom icon
691
+ actions?: React.ReactNode; // Action buttons or controls
692
+ className?: string; // Additional CSS classes
693
+ }
694
+ ```
695
+
696
+ **Features:**
697
+
698
+ - Icon auto-resolution from artifact type
699
+ - Artifact name display
700
+ - Right-aligned action slot
701
+ - Consistent styling with SkillMeat modals
702
+ - Accessible heading semantic
703
+
704
+ ### TabNavigation
705
+
706
+ Horizontal tab list component for BaseArtifactModal and custom tab interfaces. Supports icons and keyboard navigation.
707
+
708
+ **Props:**
709
+
710
+ ```typescript
711
+ interface TabNavigationProps {
712
+ tabs: Tab[]; // Tab definitions
713
+ activeTab: string; // Active tab value
714
+ onChange: (tab: string) => void; // Tab change handler
715
+ className?: string; // Additional CSS classes
716
+ }
717
+
718
+ interface Tab {
719
+ value: string; // Tab identifier
720
+ label: string; // Display label
721
+ icon?: React.ComponentType<{ className?: string }>; // Optional icon component
722
+ disabled?: boolean; // Disable tab (optional)
723
+ }
724
+ ```
725
+
726
+ **Features:**
727
+
728
+ - Icon display (from lucide-react or custom)
729
+ - Full keyboard navigation (ArrowLeft, ArrowRight, Home, End)
730
+ - Accessible ARIA attributes
731
+ - Auto-activates on focus
732
+
733
+ ### EnterpriseOwnerBadge
734
+
735
+ Badge component indicating that an artifact is managed by the enterprise organization. Displays inline on artifact cards to signal enterprise governance.
736
+
737
+ **Props:**
738
+
739
+ ```typescript
740
+ interface EnterpriseOwnerBadgeProps {
741
+ className?: string; // Additional CSS classes
742
+ }
743
+ ```
744
+
745
+ **Features:**
746
+
747
+ - Building2 icon from lucide-react
748
+ - Violet color scheme for enterprise branding
749
+ - "Enterprise Managed" label text
750
+ - Accessible aria-label
751
+ - Compact inline styling
752
+
753
+ **Example:**
754
+
755
+ ```typescript
756
+ import { EnterpriseOwnerBadge } from '@miethe/ui/primitives';
757
+
758
+ <div className="flex items-center gap-2">
759
+ <ArtifactName artifact={artifact} />
760
+ {artifact.owner_type === 'enterprise' && <EnterpriseOwnerBadge />}
761
+ </div>
762
+ ```
763
+
764
+ ### LockIcon
765
+
766
+ Tooltip-wrapped lock indicator for artifacts with enforce_override=True. Renders a small lock icon with an accessible tooltip explaining the enforced state.
767
+
768
+ **Props:**
769
+
770
+ ```typescript
771
+ interface LockIconProps {
772
+ className?: string; // Additional CSS classes
773
+ tooltip?: string; // Custom tooltip text (optional)
774
+ }
775
+ ```
776
+
777
+ **Default tooltip:** "This artifact cannot be modified — enforced by your organization"
778
+
779
+ **Features:**
780
+
781
+ - Lock icon from lucide-react
782
+ - Radix UI Tooltip for accessible popover
783
+ - Keyboard-accessible trigger
784
+ - Customizable tooltip message
785
+ - Accessible ARIA labels on both trigger and icon
786
+
787
+ **Example:**
788
+
789
+ ```typescript
790
+ import { LockIcon } from '@miethe/ui/primitives';
791
+
792
+ <div className="flex items-center gap-2">
793
+ <ArtifactName artifact={artifact} />
794
+ {artifact.enforce_override && (
795
+ <LockIcon tooltip="Custom enforcement message" />
796
+ )}
797
+ </div>
798
+ ```
799
+
800
+ ## Filters API (`@miethe/ui/filters`)
801
+
802
+ Reusable filter components for building toolbar filter bars. All components are pure presentational — consumers provide data via props.
803
+
804
+ ### TagFilterPopover
805
+
806
+ Multi-select tag filter with search, color-coded badges, and artifact counts.
807
+
808
+ ```tsx
809
+ import { TagFilterPopover, TagFilterBar } from '@miethe/ui/filters';
810
+
811
+ <TagFilterPopover
812
+ selectedTags={['design', 'canvas']}
813
+ onChange={(tags) => setTags(tags)}
814
+ availableTags={[
815
+ { name: 'design', artifact_count: 5 },
816
+ { name: 'canvas', artifact_count: 3 },
817
+ ]}
818
+ />
819
+
820
+ {/* Inline chip bar showing selected tags with remove buttons */}
821
+ <TagFilterBar
822
+ selectedTags={selectedTags}
823
+ onChange={setTags}
824
+ availableTags={availableTags}
825
+ />
826
+ ```
827
+
828
+ ### ToolFilterPopover
829
+
830
+ Multi-select tool filter with search and counts.
831
+
832
+ ```tsx
833
+ import { ToolFilterPopover, ToolFilterBar } from '@miethe/ui/filters';
834
+
835
+ <ToolFilterPopover
836
+ selectedTools={['Bash', 'Read']}
837
+ onChange={(tools) => setTools(tools)}
838
+ availableTools={[
839
+ { name: 'Bash', artifact_count: 12 },
840
+ { name: 'Read', artifact_count: 8 },
841
+ ]}
842
+ />
843
+ ```
844
+
845
+ ### FiltersDropdown
846
+
847
+ Dropdown button with multi-select category sub-menus and AND/OR toggle.
848
+
849
+ ```tsx
850
+ import { FiltersDropdown, type FilterCategory } from '@miethe/ui/filters';
851
+
852
+ const categories: FilterCategory[] = [
853
+ {
854
+ id: 'status',
855
+ label: 'Status',
856
+ options: [
857
+ { value: 'active', label: 'Active' },
858
+ { value: 'error', label: 'Error' },
859
+ ],
860
+ selected: selectedStatuses,
861
+ onChange: setStatuses,
862
+ },
863
+ ];
864
+
865
+ <FiltersDropdown
866
+ categories={categories}
867
+ filterMode="and"
868
+ onFilterModeChange={setFilterMode}
869
+ />
870
+ ```
871
+
872
+ **AND/OR mode**: Controls how values *within* each category combine. AND = must match all selected, OR = match any. Across categories is always AND.
873
+
874
+ ### SortDropdown
875
+
876
+ Sort field + order toggle dropdown.
877
+
878
+ ```tsx
879
+ import { SortDropdown } from '@miethe/ui/filters';
880
+
881
+ <SortDropdown
882
+ options={[
883
+ { value: 'name', label: 'Name' },
884
+ { value: 'updatedAt', label: 'Last Updated' },
885
+ ]}
886
+ sortField="name"
887
+ sortOrder="asc"
888
+ onSortChange={(field, order) => { /* ... */ }}
889
+ />
890
+ ```
891
+
892
+ Clicking an already-selected field toggles the order.
893
+
894
+ ## Bulk Actions API (`@miethe/ui/bulk-actions`)
895
+
896
+ Floating toolbar component for multi-select interfaces. Displays a bottom-fixed action bar with selection count and action buttons when items are selected.
897
+
898
+ ### BulkActionBar
899
+
900
+ Generic floating bulk action bar that slides in from the bottom of the viewport. Fully props-driven with no backend dependencies.
901
+
902
+ **Props:**
903
+
904
+ | Prop | Type | Required | Description |
905
+ |------|------|----------|-------------|
906
+ | `selectedCount` | `number` | Yes | Number of currently selected items |
907
+ | `hasSelection` | `boolean` | Yes | Controls visibility (true = visible, false = hidden) |
908
+ | `actions` | `BulkAction[]` | Yes | Array of action button definitions |
909
+ | `onClearSelection` | `() => void` | Yes | Callback when user clicks the clear/X button |
910
+ | `className` | `string` | No | Optional className for custom styling |
911
+
912
+ **BulkAction Interface:**
913
+
914
+ | Field | Type | Required | Description |
915
+ |-------|------|----------|-------------|
916
+ | `id` | `string` | Yes | Unique identifier used as React key and loading state key |
917
+ | `label` | `string` | Yes | Button label text |
918
+ | `icon` | `React.ReactNode` | No | Optional icon rendered left of label |
919
+ | `variant` | `'default' \| 'destructive' \| 'outline' \| 'secondary' \| 'ghost'` | No | Button visual style (default: 'ghost') |
920
+ | `onClick` | `() => void \| Promise<void>` | Yes | Click handler; may return a Promise for async actions |
921
+ | `disabled` | `boolean` | No | When true, button is disabled |
922
+
923
+ **Basic Usage:**
924
+
925
+ ```tsx
926
+ import { BulkActionBar } from '@miethe/ui/bulk-actions';
927
+ import { Trash2, Download } from 'lucide-react';
928
+ import { useState } from 'react';
929
+
930
+ export function MyList() {
931
+ const [selected, setSelected] = useState<Set<string>>(new Set());
932
+
933
+ const actions = [
934
+ {
935
+ id: 'delete',
936
+ label: 'Delete',
937
+ icon: <Trash2 className="h-3.5 w-3.5" />,
938
+ variant: 'destructive',
939
+ onClick: () => handleDelete(Array.from(selected)),
940
+ },
941
+ {
942
+ id: 'download',
943
+ label: 'Download',
944
+ icon: <Download className="h-3.5 w-3.5" />,
945
+ onClick: async () => {
946
+ await downloadItems(Array.from(selected));
947
+ },
948
+ },
949
+ ];
950
+
951
+ return (
952
+ <>
953
+ {/* Your list items with selection checkboxes */}
954
+ <BulkActionBar
955
+ selectedCount={selected.size}
956
+ hasSelection={selected.size > 0}
957
+ actions={actions}
958
+ onClearSelection={() => setSelected(new Set())}
959
+ />
960
+ </>
961
+ );
962
+ }
963
+ ```
964
+
965
+ **Features:**
966
+
967
+ - Smooth slide-up/down transitions
968
+ - Per-action loading spinners for async operations
969
+ - Global disabled state during in-flight requests
970
+ - Full keyboard navigation support
971
+ - ARIA labels and live region announcements for selection count changes
972
+
973
+ ## Adapter Pattern
974
+
975
+ The adapter pattern is the core architectural decision that makes this package reusable. Instead of baking in dependencies on a specific API client or state management library, components call `useContentViewerAdapter()` to access injected hooks.
976
+
977
+ ### The `ContentViewerAdapter` Interface
978
+
979
+ ```typescript
980
+ import type { ContentViewerAdapter, AdapterHookOptions, AdapterQueryResult, FileTreeResponse, FileContentResponse } from '@miethe/ui/content-viewer';
981
+
982
+ interface ContentViewerAdapter {
983
+ useFileTree(
984
+ artifactId: string,
985
+ options?: AdapterHookOptions
986
+ ): AdapterQueryResult<FileTreeResponse>;
987
+
988
+ useFileContent(
989
+ artifactId: string,
990
+ filePath: string,
991
+ options?: AdapterHookOptions
992
+ ): AdapterQueryResult<FileContentResponse>;
993
+ }
994
+ ```
995
+
996
+ ### Implementing an Adapter
997
+
998
+ An adapter wraps your application's hooks and normalizes their return shape:
999
+
1000
+ ```typescript
1001
+ const myAdapter: ContentViewerAdapter = {
1002
+ useFileTree(artifactId, options) {
1003
+ const result = myCustomHook(artifactId, { enabled: options?.enabled });
1004
+ return {
1005
+ data: result.data,
1006
+ isLoading: result.loading,
1007
+ error: result.err ?? null, // Normalize error field
1008
+ };
1009
+ },
1010
+
1011
+ useFileContent(artifactId, filePath, options) {
1012
+ const result = myOtherHook(artifactId, filePath, {
1013
+ enabled: options?.enabled
1014
+ });
1015
+ return {
1016
+ data: result.data,
1017
+ isLoading: result.loading,
1018
+ error: result.err ?? null,
1019
+ };
1020
+ },
1021
+ };
1022
+ ```
1023
+
1024
+ ### Return Shape
1025
+
1026
+ All adapter hooks return `AdapterQueryResult<T>`:
1027
+
1028
+ ```typescript
1029
+ interface AdapterQueryResult<T> {
1030
+ data: T | undefined; // Undefined while loading or on error
1031
+ isLoading: boolean; // True during initial fetch
1032
+ error: Error | null; // Non-null when fetch fails
1033
+ }
1034
+ ```
1035
+
1036
+ ### Error Handling
1037
+
1038
+ The `error` field can contain any Error object. Normalize errors from different data sources before returning:
1039
+
1040
+ **REST API Adapter Example:**
1041
+
1042
+ ```typescript
1043
+ import { useQuery } from '@tanstack/react-query';
1044
+ import type { ContentViewerAdapter } from '@miethe/ui/content-viewer';
1045
+
1046
+ export const restAdapter: ContentViewerAdapter = {
1047
+ useFileTree(artifactId: string, options) {
1048
+ const query = useQuery({
1049
+ queryKey: ['file-tree', artifactId],
1050
+ queryFn: async () => {
1051
+ const res = await fetch(`/api/files/${artifactId}/tree`);
1052
+ if (!res.ok) {
1053
+ throw new Error(`Failed to load file tree: ${res.status} ${res.statusText}`);
1054
+ }
1055
+ return res.json();
1056
+ },
1057
+ enabled: options?.enabled ?? true,
1058
+ });
1059
+
1060
+ return {
1061
+ data: query.data ?? undefined,
1062
+ isLoading: query.isPending,
1063
+ error: query.error ?? null,
1064
+ };
1065
+ },
1066
+
1067
+ useFileContent(artifactId: string, filePath: string, options) {
1068
+ const query = useQuery({
1069
+ queryKey: ['file-content', artifactId, filePath],
1070
+ queryFn: async () => {
1071
+ const res = await fetch(`/api/files/${artifactId}/content?path=${filePath}`);
1072
+ if (!res.ok) {
1073
+ throw new Error(`Failed to load file: ${res.status}`);
1074
+ }
1075
+ return res.json();
1076
+ },
1077
+ enabled: options?.enabled ?? true,
1078
+ });
1079
+
1080
+ return {
1081
+ data: query.data ?? undefined,
1082
+ isLoading: query.isPending,
1083
+ error: query.error ?? null,
1084
+ };
1085
+ },
1086
+ };
1087
+ ```
1088
+
1089
+ **GraphQL Adapter Example:**
1090
+
1091
+ ```typescript
1092
+ import { useQuery } from '@apollo/client';
1093
+ import gql from 'graphql-tag';
1094
+ import type { ContentViewerAdapter } from '@miethe/ui/content-viewer';
1095
+
1096
+ const FILE_TREE_QUERY = gql`
1097
+ query GetFileTree($id: ID!) {
1098
+ fileTree(id: $id) {
1099
+ entries {
1100
+ name
1101
+ path
1102
+ type
1103
+ }
1104
+ }
1105
+ }
1106
+ `;
1107
+
1108
+ const FILE_CONTENT_QUERY = gql`
1109
+ query GetFileContent($id: ID!, $path: String!) {
1110
+ fileContent(id: $id, path: $path) {
1111
+ content
1112
+ encoding
1113
+ size
1114
+ }
1115
+ }
1116
+ `;
1117
+
1118
+ export const graphqlAdapter: ContentViewerAdapter = {
1119
+ useFileTree(artifactId: string, options) {
1120
+ const { data, loading, error } = useQuery(FILE_TREE_QUERY, {
1121
+ variables: { id: artifactId },
1122
+ skip: !(options?.enabled ?? true),
1123
+ });
1124
+
1125
+ return {
1126
+ data: data?.fileTree ?? undefined,
1127
+ isLoading: loading,
1128
+ error: error ?? null,
1129
+ };
1130
+ },
1131
+
1132
+ useFileContent(artifactId: string, filePath: string, options) {
1133
+ const { data, loading, error } = useQuery(FILE_CONTENT_QUERY, {
1134
+ variables: { id: artifactId, path: filePath },
1135
+ skip: !(options?.enabled ?? true),
1136
+ });
1137
+
1138
+ return {
1139
+ data: data?.fileContent ?? undefined,
1140
+ isLoading: loading,
1141
+ error: error ?? null,
1142
+ };
1143
+ },
1144
+ };
1145
+ ```
1146
+
1147
+ **Error Display:**
1148
+
1149
+ The `ContentPane` and `FileTree` components automatically display error states when the `error` field is truthy:
1150
+
1151
+ ```typescript
1152
+ // Components handle error display automatically
1153
+ <ContentPane
1154
+ path={selectedPath}
1155
+ content={content}
1156
+ error={fileError} // Will show error UI if non-null
1157
+ isLoading={isLoading}
1158
+ />
1159
+
1160
+ <FileTree
1161
+ entityId={artifactId}
1162
+ files={files}
1163
+ error={treeError} // Will show error UI if non-null
1164
+ />
1165
+ ```
1166
+
1167
+ **Error Boundary:**
1168
+
1169
+ Wrap components with a React error boundary to catch unexpected render errors:
1170
+
1171
+ ```typescript
1172
+ import { ErrorBoundary } from 'react-error-boundary';
1173
+
1174
+ function ErrorFallback({ error }: { error: Error }) {
1175
+ return (
1176
+ <div className="p-4 text-red-600">
1177
+ <p>Something went wrong:</p>
1178
+ <pre className="text-sm">{error.message}</pre>
1179
+ </div>
1180
+ );
1181
+ }
1182
+
1183
+ export function MyViewer() {
1184
+ return (
1185
+ <ErrorBoundary FallbackComponent={ErrorFallback}>
1186
+ <FileTree entityId={artifactId} files={files} />
1187
+ </ErrorBoundary>
1188
+ );
1189
+ }
1190
+ ```
1191
+
1192
+ ## Utilities
1193
+
1194
+ ### Type-Color Utilities
1195
+
1196
+ Generic color-coding helpers for artifact/entity type indicators. Maps type strings to Tailwind CSS classes for left-border accents and subtle background tints. Type-agnostic and safe to call with arbitrary strings — unknown types receive sensible fallbacks.
1197
+
1198
+ ```typescript
1199
+ import {
1200
+ typeBarColors, // Record mapping types to border-l-{color} classes
1201
+ TYPE_BAR_FALLBACK, // Gray fallback for unknown types
1202
+ getTypeBarColor, // Resolve left-border color
1203
+ artifactTypeCardTints, // Record mapping types to subtle bg tint classes
1204
+ getCardTint, // Resolve background tint
1205
+ } from '@miethe/ui/utils';
1206
+
1207
+ // Get left-border color for a type
1208
+ const borderColor = getTypeBarColor('skill'); // 'border-l-purple-500'
1209
+ const unknown = getTypeBarColor('unknown'); // 'border-l-gray-400' (fallback)
1210
+
1211
+ // Apply with Tailwind class composition
1212
+ <div className={`border-l-4 ${getTypeBarColor(artifact.type)}`}>
1213
+ {artifact.name}
1214
+ </div>
1215
+
1216
+ // Get background tint (for larger display sizes)
1217
+ const tint = getCardTint('skill'); // 'bg-purple-500/[0.02] dark:bg-purple-500/[0.03]'
1218
+ const unknown = getCardTint('unknown'); // '' (returns empty string)
1219
+
1220
+ // Apply tint to card
1221
+ <div className={`p-4 rounded ${getCardTint(artifact.type)}`}>
1222
+ {artifact.content}
1223
+ </div>
1224
+
1225
+ // Customize color maps
1226
+ const customColors = {
1227
+ ...typeBarColors,
1228
+ customType: 'border-l-red-500',
1229
+ };
1230
+ const color = getTypeBarColor('customType', customColors); // 'border-l-red-500'
1231
+ ```
1232
+
1233
+ **Supported Types**:
1234
+ - `skill`, `command`, `agent`, `mcp`, `hook` - Core artifact types
1235
+ - `composite`, `plugin`, `workflow` - Extended types
1236
+ - `context_entity`, `context_module`, `bundle`, `deployment_set` - Context types
1237
+ - Unknown types default to gray (`border-l-gray-400`) or empty string (tints)
1238
+
1239
+ ### Frontmatter Parsing
1240
+
1241
+ ```typescript
1242
+ import {
1243
+ parseFrontmatter, // Parse YAML + content
1244
+ stripFrontmatter, // Remove YAML block
1245
+ detectFrontmatter, // Check if content has YAML
1246
+ } from '@miethe/ui/utils';
1247
+
1248
+ // Parse frontmatter and content separately
1249
+ const { frontmatter, content } = parseFrontmatter(fileContent);
1250
+
1251
+ // Remove frontmatter before displaying
1252
+ const contentWithoutFrontmatter = stripFrontmatter(fileContent);
1253
+
1254
+ // Check if file has frontmatter
1255
+ if (detectFrontmatter(fileContent)) {
1256
+ // Show frontmatter display component
1257
+ }
1258
+ ```
1259
+
1260
+ ### README Utilities
1261
+
1262
+ ```typescript
1263
+ import {
1264
+ extractFirstParagraph, // Get first paragraph from markdown
1265
+ extractFolderReadme, // Find README in folder tree
1266
+ } from '@miethe/ui/utils';
1267
+
1268
+ // Extract first paragraph for preview
1269
+ const description = extractFirstParagraph(content);
1270
+
1271
+ // Find README.md in a folder
1272
+ const readmeEntry = extractFolderReadme(fileTree, 'docs');
1273
+ ```
1274
+
1275
+ ## Types
1276
+
1277
+ The package exports canonical type definitions for all data structures:
1278
+
1279
+ ```typescript
1280
+ import type {
1281
+ FileNode, // A file or directory node
1282
+ FileTreeEntry, // A catalog file tree entry
1283
+ FileTreeResponse, // Catalog file tree API response
1284
+ FileContentResponse, // Catalog file content API response
1285
+ ContentViewerAdapter, // The adapter interface
1286
+ AdapterQueryResult, // Normalized query result shape
1287
+ AdapterHookOptions, // Common adapter hook options
1288
+ } from '@miethe/ui/content-viewer';
1289
+ ```
1290
+
1291
+ **FileNode:**
1292
+
1293
+ ```typescript
1294
+ interface FileNode {
1295
+ name: string;
1296
+ path: string;
1297
+ type: 'file' | 'directory';
1298
+ size?: number; // File size in bytes
1299
+ children?: FileNode[]; // Directory contents
1300
+ }
1301
+ ```
1302
+
1303
+ **FileTreeResponse (from API):**
1304
+
1305
+ ```typescript
1306
+ interface FileTreeResponse {
1307
+ entries: FileTreeEntry[]; // List of files/directories
1308
+ cached: boolean; // Served from cache?
1309
+ cache_age_seconds?: number; // Cache age in seconds
1310
+ }
1311
+ ```
1312
+
1313
+ **FileContentResponse (from API):**
1314
+
1315
+ ```typescript
1316
+ interface FileContentResponse {
1317
+ content: string; // Decoded file content
1318
+ encoding: string; // Encoding (usually "utf-8")
1319
+ size: number; // File size in bytes
1320
+ sha: string; // Git blob SHA
1321
+ truncated?: boolean; // Content was truncated?
1322
+ original_size?: number; // Original size before truncation
1323
+ cached: boolean; // Served from cache?
1324
+ cache_age_seconds?: number; // Cache age in seconds
1325
+ }
1326
+ ```
1327
+
1328
+ ## Examples
1329
+
1330
+ ### Modal Integration
1331
+
1332
+ Display a file viewer inside a modal dialog:
1333
+
1334
+ ```typescript
1335
+ 'use client';
1336
+
1337
+ import { useState } from 'react';
1338
+ import {
1339
+ Dialog,
1340
+ DialogContent,
1341
+ DialogHeader,
1342
+ DialogTitle,
1343
+ } from '@/components/ui/dialog';
1344
+ import { FileTree, ContentPane } from '@miethe/ui/content-viewer';
1345
+
1346
+ export function ViewerModal({ artifactId, open, onClose }: Props) {
1347
+ const [selectedPath, setSelectedPath] = useState<string | null>(null);
1348
+ const [isEditing, setIsEditing] = useState(false);
1349
+ const [editedContent, setEditedContent] = useState('');
1350
+
1351
+ return (
1352
+ <Dialog open={open} onOpenChange={onClose}>
1353
+ <DialogContent className="max-w-4xl h-96">
1354
+ <DialogHeader>
1355
+ <DialogTitle>View Files</DialogTitle>
1356
+ </DialogHeader>
1357
+ <div className="flex gap-4">
1358
+ <div className="w-64 border-r overflow-auto">
1359
+ <FileTree
1360
+ entityId={artifactId}
1361
+ files={[]} // Loaded via adapter
1362
+ selectedPath={selectedPath}
1363
+ onSelect={setSelectedPath}
1364
+ readOnly
1365
+ />
1366
+ </div>
1367
+ <div className="flex-1">
1368
+ <ContentPane
1369
+ path={selectedPath}
1370
+ content={null} // Loaded via adapter
1371
+ isEditing={isEditing}
1372
+ editedContent={editedContent}
1373
+ onEditStart={() => setIsEditing(true)}
1374
+ onEditChange={setEditedContent}
1375
+ onSave={async (content) => {
1376
+ // Handle save
1377
+ setIsEditing(false);
1378
+ }}
1379
+ onCancel={() => setIsEditing(false)}
1380
+ />
1381
+ </div>
1382
+ </div>
1383
+ </DialogContent>
1384
+ </Dialog>
1385
+ );
1386
+ }
1387
+ ```
1388
+
1389
+ ### Standalone Viewer
1390
+
1391
+ Use components without a modal:
1392
+
1393
+ ```typescript
1394
+ 'use client';
1395
+
1396
+ import { useState } from 'react';
1397
+ import { FileTree, ContentPane } from '@miethe/ui/content-viewer';
1398
+
1399
+ export function FileViewer({ artifactId }: { artifactId: string }) {
1400
+ const [selectedPath, setSelectedPath] = useState<string | null>(null);
1401
+
1402
+ return (
1403
+ <div className="grid grid-cols-3 gap-4 h-screen p-4">
1404
+ <div className="col-span-1 border rounded-lg overflow-hidden">
1405
+ <FileTree
1406
+ entityId={artifactId}
1407
+ files={[]}
1408
+ selectedPath={selectedPath}
1409
+ onSelect={setSelectedPath}
1410
+ readOnly
1411
+ />
1412
+ </div>
1413
+ <div className="col-span-2 border rounded-lg overflow-hidden">
1414
+ <ContentPane
1415
+ path={selectedPath}
1416
+ content={null}
1417
+ readOnly
1418
+ />
1419
+ </div>
1420
+ </div>
1421
+ );
1422
+ }
1423
+ ```
1424
+
1425
+ ### Custom Adapter Example (SkillMeat)
1426
+
1427
+ See SkillMeat's concrete implementation for reference:
1428
+
1429
+ ```typescript
1430
+ // In your application
1431
+ import { skillmeatContentViewerAdapter, makeCatalogArtifactId } from '@/lib/content-viewer-adapter';
1432
+ import { ContentViewerProvider } from '@miethe/ui/content-viewer';
1433
+
1434
+ const artifactId = makeCatalogArtifactId(sourceId, artifactPath);
1435
+
1436
+ <ContentViewerProvider adapter={skillmeatContentViewerAdapter}>
1437
+ <FileTree artifactId={artifactId} />
1438
+ </ContentViewerProvider>
1439
+ ```
1440
+
1441
+ The adapter encodes a composite key (sourceId + artifactPath) into a single string for the components to consume, making it easy to bridge between different identity schemes.
1442
+
1443
+ ## Performance Considerations
1444
+
1445
+ ### Lazy-Loaded Editor Bundle
1446
+
1447
+ The CodeMirror editor (used in `SplitPreview` and `MarkdownEditor`) is lazy-loaded and only fetched when needed:
1448
+
1449
+ - **Markdown files in edit mode**: Editor chunk downloaded
1450
+ - **Non-markdown files**: Editor never downloaded
1451
+ - **Read-only mode**: Editor chunk may still load for markdown files (preview uses a lighter markdown renderer)
1452
+
1453
+ 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
+
1455
+ ### Component Structure
1456
+
1457
+ - **`FileTree`** - ~8 KB gzipped (fully bundled, no lazy loading)
1458
+ - **`ContentPane`** - ~5 KB gzipped (fully bundled)
1459
+ - **`FrontmatterDisplay`** - ~2 KB gzipped (fully bundled)
1460
+ - **`SplitPreview` + `MarkdownEditor`** (lazy) - ~50 KB gzipped (on demand)
1461
+
1462
+ ## Accessibility
1463
+
1464
+ All components follow WCAG 2.1 AA standards:
1465
+
1466
+ - **FileTree**: ARIA tree pattern with roving tabindex, keyboard navigation, labels
1467
+ - **ContentPane**: Region landmarks, breadcrumb navigation, semantic HTML
1468
+ - **FrontmatterDisplay**: Semantic structure with strong/emphasis for keys
1469
+ - **Editor**: Full keyboard support and screen reader compatibility via CodeMirror
1470
+
1471
+ Test keyboard navigation with your screen reader before deploying.
1472
+
1473
+ ## TypeScript
1474
+
1475
+ The package is fully typed with TypeScript. All components and utilities have complete type definitions. No `@ts-ignore` should be needed.
1476
+
1477
+ ## Releasing a New Version
1478
+
1479
+ > This section is for maintainers of `@miethe/ui`.
1480
+
1481
+ Publishing is fully automated via GitHub Actions. When a `meaty-ui-v*` tag is pushed, the CI pipeline builds, tests, and publishes to GitHub Packages automatically.
1482
+
1483
+ ### Steps
1484
+
1485
+ **1. Update `CHANGELOG.md`**
1486
+
1487
+ Move entries from `[Unreleased]` into a new version section:
1488
+
1489
+ ```markdown
1490
+ ## [0.2.0] - 2026-04-01
1491
+
1492
+ ### Added
1493
+ - ...
1494
+
1495
+ ### Changed
1496
+ - ...
1497
+ ```
1498
+
1499
+ **2. Bump the version in `package.json`**
1500
+
1501
+ ```bash
1502
+ # From skillmeat/web/packages/ui/
1503
+ npm version minor # 0.1.0 → 0.2.0
1504
+ # or
1505
+ npm version patch # 0.1.0 → 0.1.1
1506
+ ```
1507
+
1508
+ Or edit `package.json` manually and commit.
1509
+
1510
+ **3. Commit**
1511
+
1512
+ ```bash
1513
+ git add skillmeat/web/packages/ui/package.json skillmeat/web/packages/ui/CHANGELOG.md
1514
+ git commit -m "chore(meaty-ui): bump version to v0.2.0"
1515
+ ```
1516
+
1517
+ **4. Tag and push**
1518
+
1519
+ Tags must use the `meaty-ui-v` prefix to avoid colliding with SkillMeat's own `v*` release tags:
1520
+
1521
+ ```bash
1522
+ git tag meaty-ui-v0.2.0 -m "Release @miethe/ui v0.2.0"
1523
+ git push origin main meaty-ui-v0.2.0
1524
+ ```
1525
+
1526
+ The GitHub Actions `publish` job triggers on the tag push, runs CI, then publishes to GitHub Packages. Monitor progress in the **Actions** tab of the repository.
1527
+
1528
+ ### Required GitHub Secret
1529
+
1530
+ The `NPM_TOKEN` secret must be set in the repository's GitHub Actions secrets (Settings → Secrets → Actions). It must be a GitHub Personal Access Token with `write:packages` scope scoped to the `@miethe` namespace on `npm.pkg.github.com`. This is a one-time setup — no action needed per release.
1531
+
1532
+ ---
1533
+
1534
+ ## License
1535
+
1536
+ See LICENSE file in the package root.