@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.
- package/CHANGELOG.md +77 -0
- package/README.md +1536 -0
- package/dist/bulk-actions/Button.d.ts +28 -0
- package/dist/bulk-actions/Button.d.ts.map +1 -0
- package/dist/bulk-actions/Button.js +45 -0
- package/dist/bulk-actions/Button.js.map +1 -0
- package/dist/bulk-actions/bulk-action-bar.d.ts +91 -0
- package/dist/bulk-actions/bulk-action-bar.d.ts.map +1 -0
- package/dist/bulk-actions/bulk-action-bar.js +94 -0
- package/dist/bulk-actions/bulk-action-bar.js.map +1 -0
- package/dist/bulk-actions/index.d.ts +5 -0
- package/dist/bulk-actions/index.d.ts.map +1 -0
- package/dist/bulk-actions/index.js +7 -0
- package/dist/bulk-actions/index.js.map +1 -0
- package/dist/bulk-actions/utils.d.ts +6 -0
- package/dist/bulk-actions/utils.d.ts.map +1 -0
- package/dist/bulk-actions/utils.js +9 -0
- package/dist/bulk-actions/utils.js.map +1 -0
- package/dist/components/ui/alert.d.ts +9 -0
- package/dist/components/ui/alert.d.ts.map +1 -0
- package/dist/components/ui/alert.js +23 -0
- package/dist/components/ui/alert.js.map +1 -0
- package/dist/components/ui/button.d.ts +12 -0
- package/dist/components/ui/button.d.ts.map +1 -0
- package/dist/components/ui/button.js +34 -0
- package/dist/components/ui/button.js.map +1 -0
- package/dist/components/ui/collapsible.d.ts +6 -0
- package/dist/components/ui/collapsible.d.ts.map +1 -0
- package/dist/components/ui/collapsible.js +7 -0
- package/dist/components/ui/collapsible.js.map +1 -0
- package/dist/components/ui/skeleton.d.ts +4 -0
- package/dist/components/ui/skeleton.d.ts.map +1 -0
- package/dist/components/ui/skeleton.js +7 -0
- package/dist/components/ui/skeleton.js.map +1 -0
- package/dist/content-viewer/ContentPane.d.ts +107 -0
- package/dist/content-viewer/ContentPane.d.ts.map +1 -0
- package/dist/content-viewer/ContentPane.js +247 -0
- package/dist/content-viewer/ContentPane.js.map +1 -0
- package/dist/content-viewer/ContentViewerProvider.d.ts +83 -0
- package/dist/content-viewer/ContentViewerProvider.d.ts.map +1 -0
- package/dist/content-viewer/ContentViewerProvider.js +92 -0
- package/dist/content-viewer/ContentViewerProvider.js.map +1 -0
- package/dist/content-viewer/FileTree.d.ts +71 -0
- package/dist/content-viewer/FileTree.d.ts.map +1 -0
- package/dist/content-viewer/FileTree.js +294 -0
- package/dist/content-viewer/FileTree.js.map +1 -0
- package/dist/content-viewer/adapters.d.ts +101 -0
- package/dist/content-viewer/adapters.d.ts.map +1 -0
- package/dist/content-viewer/adapters.js +32 -0
- package/dist/content-viewer/adapters.js.map +1 -0
- package/dist/content-viewer/index.d.ts +8 -0
- package/dist/content-viewer/index.d.ts.map +1 -0
- package/dist/content-viewer/index.js +5 -0
- package/dist/content-viewer/index.js.map +1 -0
- package/dist/diff/DiffViewer.d.ts +112 -0
- package/dist/diff/DiffViewer.d.ts.map +1 -0
- package/dist/diff/DiffViewer.js +414 -0
- package/dist/diff/DiffViewer.js.map +1 -0
- package/dist/diff/diff.d.ts +32 -0
- package/dist/diff/diff.d.ts.map +1 -0
- package/dist/diff/diff.js +8 -0
- package/dist/diff/diff.js.map +1 -0
- package/dist/diff/index.d.ts +4 -0
- package/dist/diff/index.d.ts.map +1 -0
- package/dist/diff/index.js +3 -0
- package/dist/diff/index.js.map +1 -0
- package/dist/display/FilePreviewPane.d.ts +31 -0
- package/dist/display/FilePreviewPane.d.ts.map +1 -0
- package/dist/display/FilePreviewPane.js +144 -0
- package/dist/display/FilePreviewPane.js.map +1 -0
- package/dist/display/FrontmatterDisplay.d.ts +33 -0
- package/dist/display/FrontmatterDisplay.d.ts.map +1 -0
- package/dist/display/FrontmatterDisplay.js +79 -0
- package/dist/display/FrontmatterDisplay.js.map +1 -0
- package/dist/display/index.d.ts +5 -0
- package/dist/display/index.d.ts.map +1 -0
- package/dist/display/index.js +4 -0
- package/dist/display/index.js.map +1 -0
- package/dist/editor/MarkdownEditor.d.ts +28 -0
- package/dist/editor/MarkdownEditor.d.ts.map +1 -0
- package/dist/editor/MarkdownEditor.js +160 -0
- package/dist/editor/MarkdownEditor.js.map +1 -0
- package/dist/editor/SplitPreview.d.ts +28 -0
- package/dist/editor/SplitPreview.d.ts.map +1 -0
- package/dist/editor/SplitPreview.js +34 -0
- package/dist/editor/SplitPreview.js.map +1 -0
- package/dist/editor/index.d.ts +5 -0
- package/dist/editor/index.d.ts.map +1 -0
- package/dist/editor/index.js +4 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/filters/filters-dropdown.d.ts +24 -0
- package/dist/filters/filters-dropdown.d.ts.map +1 -0
- package/dist/filters/filters-dropdown.js +36 -0
- package/dist/filters/filters-dropdown.js.map +1 -0
- package/dist/filters/index.d.ts +9 -0
- package/dist/filters/index.d.ts.map +1 -0
- package/dist/filters/index.js +5 -0
- package/dist/filters/index.js.map +1 -0
- package/dist/filters/sort-dropdown.d.ts +13 -0
- package/dist/filters/sort-dropdown.d.ts.map +1 -0
- package/dist/filters/sort-dropdown.js +20 -0
- package/dist/filters/sort-dropdown.js.map +1 -0
- package/dist/filters/tag-filter-popover.d.ts +39 -0
- package/dist/filters/tag-filter-popover.d.ts.map +1 -0
- package/dist/filters/tag-filter-popover.js +72 -0
- package/dist/filters/tag-filter-popover.js.map +1 -0
- package/dist/filters/tool-filter-popover.d.ts +42 -0
- package/dist/filters/tool-filter-popover.d.ts.map +1 -0
- package/dist/filters/tool-filter-popover.js +67 -0
- package/dist/filters/tool-filter-popover.js.map +1 -0
- package/dist/hooks/use-debounce.d.ts +9 -0
- package/dist/hooks/use-debounce.d.ts.map +1 -0
- package/dist/hooks/use-debounce.js +21 -0
- package/dist/hooks/use-debounce.js.map +1 -0
- package/dist/hooks/use-intersection-observer.d.ts +11 -0
- package/dist/hooks/use-intersection-observer.d.ts.map +1 -0
- package/dist/hooks/use-intersection-observer.js +25 -0
- package/dist/hooks/use-intersection-observer.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/pickers/EntityPickerDialog.d.ts +233 -0
- package/dist/pickers/EntityPickerDialog.d.ts.map +1 -0
- package/dist/pickers/EntityPickerDialog.js +355 -0
- package/dist/pickers/EntityPickerDialog.js.map +1 -0
- package/dist/pickers/EntityPickerViewToggle.d.ts +8 -0
- package/dist/pickers/EntityPickerViewToggle.d.ts.map +1 -0
- package/dist/pickers/EntityPickerViewToggle.js +17 -0
- package/dist/pickers/EntityPickerViewToggle.js.map +1 -0
- package/dist/pickers/index.d.ts +5 -0
- package/dist/pickers/index.d.ts.map +1 -0
- package/dist/pickers/index.js +3 -0
- package/dist/pickers/index.js.map +1 -0
- package/dist/primitives/Badge.d.ts +16 -0
- package/dist/primitives/Badge.d.ts.map +1 -0
- package/dist/primitives/Badge.js +43 -0
- package/dist/primitives/Badge.js.map +1 -0
- package/dist/primitives/BaseArtifactModal.d.ts +114 -0
- package/dist/primitives/BaseArtifactModal.d.ts.map +1 -0
- package/dist/primitives/BaseArtifactModal.js +76 -0
- package/dist/primitives/BaseArtifactModal.js.map +1 -0
- package/dist/primitives/Dialog.d.ts +20 -0
- package/dist/primitives/Dialog.d.ts.map +1 -0
- package/dist/primitives/Dialog.js +24 -0
- package/dist/primitives/Dialog.js.map +1 -0
- package/dist/primitives/DropdownMenu.d.ts +28 -0
- package/dist/primitives/DropdownMenu.d.ts.map +1 -0
- package/dist/primitives/DropdownMenu.js +34 -0
- package/dist/primitives/DropdownMenu.js.map +1 -0
- package/dist/primitives/EnterpriseOwnerBadge.d.ts +9 -0
- package/dist/primitives/EnterpriseOwnerBadge.d.ts.map +1 -0
- package/dist/primitives/EnterpriseOwnerBadge.js +12 -0
- package/dist/primitives/EnterpriseOwnerBadge.js.map +1 -0
- package/dist/primitives/GroupedSelect.d.ts +30 -0
- package/dist/primitives/GroupedSelect.d.ts.map +1 -0
- package/dist/primitives/GroupedSelect.js +47 -0
- package/dist/primitives/GroupedSelect.js.map +1 -0
- package/dist/primitives/Input.d.ts +6 -0
- package/dist/primitives/Input.d.ts.map +1 -0
- package/dist/primitives/Input.js +9 -0
- package/dist/primitives/Input.js.map +1 -0
- package/dist/primitives/LockIcon.d.ts +11 -0
- package/dist/primitives/LockIcon.d.ts.map +1 -0
- package/dist/primitives/LockIcon.js +15 -0
- package/dist/primitives/LockIcon.js.map +1 -0
- package/dist/primitives/MaskedSecretInput.d.ts +16 -0
- package/dist/primitives/MaskedSecretInput.d.ts.map +1 -0
- package/dist/primitives/MaskedSecretInput.js +42 -0
- package/dist/primitives/MaskedSecretInput.js.map +1 -0
- package/dist/primitives/ModalHeader.d.ts +66 -0
- package/dist/primitives/ModalHeader.d.ts.map +1 -0
- package/dist/primitives/ModalHeader.js +58 -0
- package/dist/primitives/ModalHeader.js.map +1 -0
- package/dist/primitives/Popover.d.ts +9 -0
- package/dist/primitives/Popover.d.ts.map +1 -0
- package/dist/primitives/Popover.js +13 -0
- package/dist/primitives/Popover.js.map +1 -0
- package/dist/primitives/ScrollArea.d.ts +6 -0
- package/dist/primitives/ScrollArea.d.ts.map +1 -0
- package/dist/primitives/ScrollArea.js +11 -0
- package/dist/primitives/ScrollArea.js.map +1 -0
- package/dist/primitives/SearchableCombobox.d.ts +30 -0
- package/dist/primitives/SearchableCombobox.d.ts.map +1 -0
- package/dist/primitives/SearchableCombobox.js +124 -0
- package/dist/primitives/SearchableCombobox.js.map +1 -0
- package/dist/primitives/SearchablePickerDialog.d.ts +20 -0
- package/dist/primitives/SearchablePickerDialog.d.ts.map +1 -0
- package/dist/primitives/SearchablePickerDialog.js +78 -0
- package/dist/primitives/SearchablePickerDialog.js.map +1 -0
- package/dist/primitives/StatusBadge.d.ts +21 -0
- package/dist/primitives/StatusBadge.d.ts.map +1 -0
- package/dist/primitives/StatusBadge.js +25 -0
- package/dist/primitives/StatusBadge.js.map +1 -0
- package/dist/primitives/TabNavigation.d.ts +68 -0
- package/dist/primitives/TabNavigation.d.ts.map +1 -0
- package/dist/primitives/TabNavigation.js +74 -0
- package/dist/primitives/TabNavigation.js.map +1 -0
- package/dist/primitives/Tabs.d.ts +8 -0
- package/dist/primitives/Tabs.d.ts.map +1 -0
- package/dist/primitives/Tabs.js +14 -0
- package/dist/primitives/Tabs.js.map +1 -0
- package/dist/primitives/Tooltip.d.ts +8 -0
- package/dist/primitives/Tooltip.d.ts.map +1 -0
- package/dist/primitives/Tooltip.js +12 -0
- package/dist/primitives/Tooltip.js.map +1 -0
- package/dist/primitives/VerticalTabNavigation.d.ts +75 -0
- package/dist/primitives/VerticalTabNavigation.d.ts.map +1 -0
- package/dist/primitives/VerticalTabNavigation.js +166 -0
- package/dist/primitives/VerticalTabNavigation.js.map +1 -0
- package/dist/primitives/ViewModeToggle.d.ts +12 -0
- package/dist/primitives/ViewModeToggle.d.ts.map +1 -0
- package/dist/primitives/ViewModeToggle.js +56 -0
- package/dist/primitives/ViewModeToggle.js.map +1 -0
- package/dist/primitives/WizardShell.d.ts +81 -0
- package/dist/primitives/WizardShell.d.ts.map +1 -0
- package/dist/primitives/WizardShell.js +73 -0
- package/dist/primitives/WizardShell.js.map +1 -0
- package/dist/primitives/index.d.ts +38 -0
- package/dist/primitives/index.d.ts.map +1 -0
- package/dist/primitives/index.js +24 -0
- package/dist/primitives/index.js.map +1 -0
- package/dist/primitives/utils.d.ts +6 -0
- package/dist/primitives/utils.d.ts.map +1 -0
- package/dist/primitives/utils.js +9 -0
- package/dist/primitives/utils.js.map +1 -0
- package/dist/types/index.d.ts +63 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +9 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/frontmatter.d.ts +63 -0
- package/dist/utils/frontmatter.d.ts.map +1 -0
- package/dist/utils/frontmatter.js +345 -0
- package/dist/utils/frontmatter.js.map +1 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/perf-marks.d.ts +28 -0
- package/dist/utils/perf-marks.d.ts.map +1 -0
- package/dist/utils/perf-marks.js +45 -0
- package/dist/utils/perf-marks.js.map +1 -0
- package/dist/utils/readme-utils.d.ts +67 -0
- package/dist/utils/readme-utils.d.ts.map +1 -0
- package/dist/utils/readme-utils.js +164 -0
- package/dist/utils/readme-utils.js.map +1 -0
- package/dist/utils/type-colors.d.ts +70 -0
- package/dist/utils/type-colors.d.ts.map +1 -0
- package/dist/utils/type-colors.js +118 -0
- package/dist/utils/type-colors.js.map +1 -0
- 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.
|