@imolko/create-ultra-reporter 2.1.23-beta
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/README.md +219 -0
- package/bin/classes/index.d.ts +0 -0
- package/bin/classes/index.js +2 -0
- package/bin/classes/index.js.map +1 -0
- package/bin/classes/logger.d.ts +42 -0
- package/bin/classes/logger.js +185 -0
- package/bin/classes/logger.js.map +1 -0
- package/bin/cli.d.ts +2 -0
- package/bin/cli.js +48 -0
- package/bin/cli.js.map +1 -0
- package/bin/commands/build.d.ts +2 -0
- package/bin/commands/build.js +151 -0
- package/bin/commands/build.js.map +1 -0
- package/bin/commands/create.d.ts +60 -0
- package/bin/commands/create.js +368 -0
- package/bin/commands/create.js.map +1 -0
- package/bin/commands/generate-documentation.d.ts +2 -0
- package/bin/commands/generate-documentation.js +249 -0
- package/bin/commands/generate-documentation.js.map +1 -0
- package/bin/commands/generate.d.ts +2 -0
- package/bin/commands/generate.js +79 -0
- package/bin/commands/generate.js.map +1 -0
- package/bin/commands/index.d.ts +6 -0
- package/bin/commands/index.js +16 -0
- package/bin/commands/index.js.map +1 -0
- package/bin/commands/init.d.ts +2 -0
- package/bin/commands/init.js +54 -0
- package/bin/commands/init.js.map +1 -0
- package/bin/commands/serve.d.ts +2 -0
- package/bin/commands/serve.js +124 -0
- package/bin/commands/serve.js.map +1 -0
- package/bin/commands/types.d.ts +65 -0
- package/bin/commands/types.js +9 -0
- package/bin/commands/types.js.map +1 -0
- package/bin/config/reader.d.ts +17 -0
- package/bin/config/reader.js +166 -0
- package/bin/config/reader.js.map +1 -0
- package/bin/config/types.d.ts +57 -0
- package/bin/config/types.js +21 -0
- package/bin/config/types.js.map +1 -0
- package/bin/data/documentation-folder.d.ts +1 -0
- package/bin/data/documentation-folder.js +5 -0
- package/bin/data/documentation-folder.js.map +1 -0
- package/bin/data/files-generated.d.ts +1 -0
- package/bin/data/files-generated.js +10 -0
- package/bin/data/files-generated.js.map +1 -0
- package/bin/data/index.d.ts +2 -0
- package/bin/data/index.js +8 -0
- package/bin/data/index.js.map +1 -0
- package/bin/pipeline/generate.d.ts +26 -0
- package/bin/pipeline/generate.js +269 -0
- package/bin/pipeline/generate.js.map +1 -0
- package/bin/reporters/data-loading.d.ts +121 -0
- package/bin/reporters/data-loading.js +398 -0
- package/bin/reporters/data-loading.js.map +1 -0
- package/bin/reporters/data-transformation.d.ts +101 -0
- package/bin/reporters/data-transformation.js +392 -0
- package/bin/reporters/data-transformation.js.map +1 -0
- package/bin/reporters/file-writing.d.ts +29 -0
- package/bin/reporters/file-writing.js +100 -0
- package/bin/reporters/file-writing.js.map +1 -0
- package/bin/reporters/generate-domain-documentation.d.ts +17 -0
- package/bin/reporters/generate-domain-documentation.js +161 -0
- package/bin/reporters/generate-domain-documentation.js.map +1 -0
- package/bin/reporters/generate-use-cases-documentation.d.ts +18 -0
- package/bin/reporters/generate-use-cases-documentation.js +123 -0
- package/bin/reporters/generate-use-cases-documentation.js.map +1 -0
- package/bin/reporters/rendering.d.ts +116 -0
- package/bin/reporters/rendering.js +385 -0
- package/bin/reporters/rendering.js.map +1 -0
- package/bin/reporters/templates/README.md +28 -0
- package/bin/reporters/templates/card.template.jsx +5 -0
- package/bin/reporters/templates/cards-container.template.jsx +5 -0
- package/bin/reporters/types.d.ts +190 -0
- package/bin/reporters/types.js +8 -0
- package/bin/reporters/types.js.map +1 -0
- package/bin/scaffold/assembler.d.ts +13 -0
- package/bin/scaffold/assembler.js +371 -0
- package/bin/scaffold/assembler.js.map +1 -0
- package/bin/scaffold/doc-assembler.d.ts +10 -0
- package/bin/scaffold/doc-assembler.js +113 -0
- package/bin/scaffold/doc-assembler.js.map +1 -0
- package/bin/scripts/add-import.d.ts +1 -0
- package/bin/scripts/add-import.js +26 -0
- package/bin/scripts/add-import.js.map +1 -0
- package/bin/scripts/converter.d.ts +6 -0
- package/bin/scripts/converter.js +120 -0
- package/bin/scripts/converter.js.map +1 -0
- package/bin/scripts/copy-files.d.ts +1 -0
- package/bin/scripts/copy-files.js +96 -0
- package/bin/scripts/copy-files.js.map +1 -0
- package/bin/scripts/create-folder.d.ts +1 -0
- package/bin/scripts/create-folder.js +23 -0
- package/bin/scripts/create-folder.js.map +1 -0
- package/bin/scripts/delete-paths.d.ts +1 -0
- package/bin/scripts/delete-paths.js +34 -0
- package/bin/scripts/delete-paths.js.map +1 -0
- package/bin/scripts/exists-file.d.ts +5 -0
- package/bin/scripts/exists-file.js +12 -0
- package/bin/scripts/exists-file.js.map +1 -0
- package/bin/scripts/generate-track-artifacts.d.ts +1 -0
- package/bin/scripts/generate-track-artifacts.js +59 -0
- package/bin/scripts/generate-track-artifacts.js.map +1 -0
- package/bin/scripts/get-artifacts.d.ts +1 -0
- package/bin/scripts/get-artifacts.js +38 -0
- package/bin/scripts/get-artifacts.js.map +1 -0
- package/bin/scripts/get-directories.d.ts +1 -0
- package/bin/scripts/get-directories.js +10 -0
- package/bin/scripts/get-directories.js.map +1 -0
- package/bin/scripts/get-file.d.ts +9 -0
- package/bin/scripts/get-file.js +38 -0
- package/bin/scripts/get-file.js.map +1 -0
- package/bin/scripts/labels.d.ts +35 -0
- package/bin/scripts/labels.js +108 -0
- package/bin/scripts/labels.js.map +1 -0
- package/bin/scripts/markdown.d.ts +34 -0
- package/bin/scripts/markdown.js +99 -0
- package/bin/scripts/markdown.js.map +1 -0
- package/bin/scripts/names.d.ts +18 -0
- package/bin/scripts/names.js +64 -0
- package/bin/scripts/names.js.map +1 -0
- package/bin/scripts/track-artifacts.d.ts +2 -0
- package/bin/scripts/track-artifacts.js +101 -0
- package/bin/scripts/track-artifacts.js.map +1 -0
- package/bin/utils/create-folder.d.ts +1 -0
- package/bin/utils/create-folder.js +23 -0
- package/bin/utils/create-folder.js.map +1 -0
- package/bin/utils/delete-markdown-files.d.ts +1 -0
- package/bin/utils/delete-markdown-files.js +46 -0
- package/bin/utils/delete-markdown-files.js.map +1 -0
- package/bin/utils/delete-paths.d.ts +5 -0
- package/bin/utils/delete-paths.js +26 -0
- package/bin/utils/delete-paths.js.map +1 -0
- package/bin/utils/exists-file.d.ts +6 -0
- package/bin/utils/exists-file.js +13 -0
- package/bin/utils/exists-file.js.map +1 -0
- package/bin/utils/exists-folder.d.ts +6 -0
- package/bin/utils/exists-folder.js +13 -0
- package/bin/utils/exists-folder.js.map +1 -0
- package/bin/utils/get-internal-resource-path.d.ts +1 -0
- package/bin/utils/get-internal-resource-path.js +43 -0
- package/bin/utils/get-internal-resource-path.js.map +1 -0
- package/bin/utils/get-json-file.d.ts +1 -0
- package/bin/utils/get-json-file.js +27 -0
- package/bin/utils/get-json-file.js.map +1 -0
- package/bin/utils/index.d.ts +6 -0
- package/bin/utils/index.js +16 -0
- package/bin/utils/index.js.map +1 -0
- package/bin/utils/run-command.d.ts +5 -0
- package/bin/utils/run-command.js +36 -0
- package/bin/utils/run-command.js.map +1 -0
- package/jsdoc.conf.json +30 -0
- package/package.json +75 -0
- package/templates/documentation/README.md +41 -0
- package/templates/documentation/docs/.gitkeep +0 -0
- package/templates/documentation/docusaurus.config.ts +127 -0
- package/templates/documentation/package-lock.json +19431 -0
- package/templates/documentation/package.json +49 -0
- package/templates/documentation/sidebars.ts +33 -0
- package/templates/documentation/src/components/ArtifactTable/FilterBar.tsx +185 -0
- package/templates/documentation/src/components/ArtifactTable/index.tsx +298 -0
- package/templates/documentation/src/components/ArtifactTable/styles.module.css +282 -0
- package/templates/documentation/src/components/ArtifactTable/types.ts +31 -0
- package/templates/documentation/src/components/HomepageFeatures/index.tsx +77 -0
- package/templates/documentation/src/components/HomepageFeatures/styles.module.css +11 -0
- package/templates/documentation/src/css/custom.css +30 -0
- package/templates/documentation/src/pages/index.module.css +23 -0
- package/templates/documentation/src/pages/index.tsx +43 -0
- package/templates/documentation/src/pages/markdown-page.md +7 -0
- package/templates/documentation/static/.nojekyll +0 -0
- package/templates/documentation/static/img/docusaurus-social-card.jpg +0 -0
- package/templates/documentation/static/img/docusaurus.png +0 -0
- package/templates/documentation/static/img/favicon.ico +0 -0
- package/templates/documentation/static/img/logo.svg +1 -0
- package/templates/documentation/static/img/logo_imolko_azul.png +0 -0
- package/templates/documentation/static/img/undraw_docusaurus_mountain.svg +171 -0
- package/templates/documentation/static/img/undraw_docusaurus_react.svg +170 -0
- package/templates/documentation/static/img/undraw_docusaurus_tree.svg +40 -0
- package/templates/documentation/tsconfig.json +8 -0
- package/templates/documentation/ultra-reporter.config.json +55 -0
- package/templates/track-artifacts-script.ts +44 -0
- package/tsconfig.build-track.json +39 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "documentation",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"docusaurus": "docusaurus",
|
|
7
|
+
"start": "docusaurus start",
|
|
8
|
+
"build": "docusaurus build",
|
|
9
|
+
"swizzle": "docusaurus swizzle",
|
|
10
|
+
"deploy": "docusaurus deploy",
|
|
11
|
+
"clear": "docusaurus clear",
|
|
12
|
+
"serve": "docusaurus serve",
|
|
13
|
+
"write-translations": "docusaurus write-translations",
|
|
14
|
+
"write-heading-ids": "docusaurus write-heading-ids",
|
|
15
|
+
"typecheck": "tsc"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@docusaurus/core": "3.9.2",
|
|
19
|
+
"@docusaurus/preset-classic": "3.9.2",
|
|
20
|
+
"@docusaurus/theme-mermaid": "3.9.2",
|
|
21
|
+
"@mdx-js/react": "^3.0.0",
|
|
22
|
+
"clsx": "^2.0.0",
|
|
23
|
+
"mermaid": "11.12.1",
|
|
24
|
+
"prism-react-renderer": "^2.3.0",
|
|
25
|
+
"react": "^19.0.0",
|
|
26
|
+
"react-dom": "^19.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@docusaurus/module-type-aliases": "3.9.2",
|
|
30
|
+
"@docusaurus/tsconfig": "3.9.2",
|
|
31
|
+
"@docusaurus/types": "3.9.2",
|
|
32
|
+
"typescript": "~5.9.3"
|
|
33
|
+
},
|
|
34
|
+
"browserslist": {
|
|
35
|
+
"production": [
|
|
36
|
+
">0.5%",
|
|
37
|
+
"not dead",
|
|
38
|
+
"not op_mini all"
|
|
39
|
+
],
|
|
40
|
+
"development": [
|
|
41
|
+
"last 3 chrome version",
|
|
42
|
+
"last 3 firefox version",
|
|
43
|
+
"last 5 safari version"
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=20.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';
|
|
2
|
+
|
|
3
|
+
// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creating a sidebar enables you to:
|
|
7
|
+
- create an ordered group of docs
|
|
8
|
+
- render a sidebar for each doc of that group
|
|
9
|
+
- provide next/previous navigation
|
|
10
|
+
|
|
11
|
+
The sidebars can be generated from the filesystem, or explicitly defined here.
|
|
12
|
+
|
|
13
|
+
Create as many sidebars as you want.
|
|
14
|
+
*/
|
|
15
|
+
const sidebars: SidebarsConfig = {
|
|
16
|
+
// By default, Docusaurus generates a sidebar from the docs folder structure
|
|
17
|
+
tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
|
|
18
|
+
|
|
19
|
+
// But you can create a sidebar manually
|
|
20
|
+
/*
|
|
21
|
+
tutorialSidebar: [
|
|
22
|
+
'intro',
|
|
23
|
+
'hello',
|
|
24
|
+
{
|
|
25
|
+
type: 'category',
|
|
26
|
+
label: 'Tutorial',
|
|
27
|
+
items: ['tutorial-basics/create-a-document'],
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
*/
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default sidebars;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
|
+
import type { FilterState } from './types';
|
|
3
|
+
import styles from './styles.module.css';
|
|
4
|
+
|
|
5
|
+
interface FilterBarProps {
|
|
6
|
+
filters: FilterState;
|
|
7
|
+
availableTypes: string[];
|
|
8
|
+
availableContexts: string[];
|
|
9
|
+
onFilterChange: (updates: Partial<FilterState>) => void;
|
|
10
|
+
onClearFilters: () => void;
|
|
11
|
+
totalResults: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Debounce hook — delays updating a value until `delay` ms of inactivity.
|
|
16
|
+
*/
|
|
17
|
+
function useDebouncedValue<T>(value: T, delay: number): T {
|
|
18
|
+
const [debounced, setDebounced] = useState(value);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const timer = setTimeout(() => setDebounced(value), delay);
|
|
22
|
+
return () => clearTimeout(timer);
|
|
23
|
+
}, [value, delay]);
|
|
24
|
+
|
|
25
|
+
return debounced;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Filter bar with text inputs, a multi-select type dropdown, and a
|
|
30
|
+
* single-select contextName dropdown.
|
|
31
|
+
*/
|
|
32
|
+
export default function FilterBar({
|
|
33
|
+
filters,
|
|
34
|
+
availableTypes,
|
|
35
|
+
availableContexts,
|
|
36
|
+
onFilterChange,
|
|
37
|
+
onClearFilters,
|
|
38
|
+
totalResults,
|
|
39
|
+
}: FilterBarProps): JSX.Element {
|
|
40
|
+
// Local state for text inputs (debounced before propagating)
|
|
41
|
+
const [localName, setLocalName] = useState(filters.nameQuery);
|
|
42
|
+
const [localTags, setLocalTags] = useState(filters.tagsQuery);
|
|
43
|
+
|
|
44
|
+
const debouncedName = useDebouncedValue(localName, 250);
|
|
45
|
+
const debouncedTags = useDebouncedValue(localTags, 250);
|
|
46
|
+
|
|
47
|
+
// Sync debounced values upward
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
onFilterChange({ nameQuery: debouncedName });
|
|
50
|
+
}, [debouncedName]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
onFilterChange({ tagsQuery: debouncedTags });
|
|
54
|
+
}, [debouncedTags]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
55
|
+
|
|
56
|
+
// Sync from parent (e.g. URL restore)
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
setLocalName(filters.nameQuery);
|
|
59
|
+
setLocalTags(filters.tagsQuery);
|
|
60
|
+
}, [filters.nameQuery, filters.tagsQuery]);
|
|
61
|
+
|
|
62
|
+
// Multi-select dropdown state
|
|
63
|
+
const [typeDropdownOpen, setTypeDropdownOpen] = useState(false);
|
|
64
|
+
const typeDropdownRef = useRef<HTMLDivElement>(null);
|
|
65
|
+
|
|
66
|
+
// Close dropdown on outside click
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
function handleClickOutside(event: MouseEvent) {
|
|
69
|
+
if (
|
|
70
|
+
typeDropdownRef.current &&
|
|
71
|
+
!typeDropdownRef.current.contains(event.target as Node)
|
|
72
|
+
) {
|
|
73
|
+
setTypeDropdownOpen(false);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
77
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
const toggleType = useCallback(
|
|
81
|
+
(type: string) => {
|
|
82
|
+
const next = filters.selectedTypes.includes(type)
|
|
83
|
+
? filters.selectedTypes.filter((t) => t !== type)
|
|
84
|
+
: [...filters.selectedTypes, type];
|
|
85
|
+
onFilterChange({ selectedTypes: next });
|
|
86
|
+
},
|
|
87
|
+
[filters.selectedTypes, onFilterChange],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const hasActiveFilters =
|
|
91
|
+
filters.nameQuery !== '' ||
|
|
92
|
+
filters.tagsQuery !== '' ||
|
|
93
|
+
filters.selectedTypes.length > 0 ||
|
|
94
|
+
filters.selectedContext !== '';
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className={styles.filterBar}>
|
|
98
|
+
<div className={styles.filterGroup}>
|
|
99
|
+
<input
|
|
100
|
+
type="text"
|
|
101
|
+
className={styles.filterInput}
|
|
102
|
+
placeholder="Search by name..."
|
|
103
|
+
value={localName}
|
|
104
|
+
onChange={(e) => setLocalName(e.target.value)}
|
|
105
|
+
aria-label="Filter by artifact name"
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div className={styles.filterGroup}>
|
|
110
|
+
<input
|
|
111
|
+
type="text"
|
|
112
|
+
className={styles.filterInput}
|
|
113
|
+
placeholder="Search by tags..."
|
|
114
|
+
value={localTags}
|
|
115
|
+
onChange={(e) => setLocalTags(e.target.value)}
|
|
116
|
+
aria-label="Filter by tags"
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div className={styles.filterGroup} ref={typeDropdownRef}>
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
className={styles.multiSelectToggle}
|
|
124
|
+
onClick={() => setTypeDropdownOpen(!typeDropdownOpen)}
|
|
125
|
+
aria-expanded={typeDropdownOpen}
|
|
126
|
+
aria-haspopup="listbox"
|
|
127
|
+
>
|
|
128
|
+
Type
|
|
129
|
+
{filters.selectedTypes.length > 0 && (
|
|
130
|
+
<span className={styles.multiSelectBadge}>
|
|
131
|
+
{filters.selectedTypes.length}
|
|
132
|
+
</span>
|
|
133
|
+
)}
|
|
134
|
+
<span className={styles.dropdownArrow}>▾</span>
|
|
135
|
+
</button>
|
|
136
|
+
{typeDropdownOpen && (
|
|
137
|
+
<div className={styles.multiSelectDropdown} role="listbox" aria-multiselectable="true">
|
|
138
|
+
{availableTypes.map((type) => (
|
|
139
|
+
<label key={type} className={styles.multiSelectOption}>
|
|
140
|
+
<input
|
|
141
|
+
type="checkbox"
|
|
142
|
+
checked={filters.selectedTypes.includes(type)}
|
|
143
|
+
onChange={() => toggleType(type)}
|
|
144
|
+
/>
|
|
145
|
+
<span>{type}</span>
|
|
146
|
+
</label>
|
|
147
|
+
))}
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div className={styles.filterGroup}>
|
|
153
|
+
<select
|
|
154
|
+
className={styles.filterSelect}
|
|
155
|
+
value={filters.selectedContext}
|
|
156
|
+
onChange={(e) =>
|
|
157
|
+
onFilterChange({ selectedContext: e.target.value })
|
|
158
|
+
}
|
|
159
|
+
aria-label="Filter by context"
|
|
160
|
+
>
|
|
161
|
+
<option value="">All contexts</option>
|
|
162
|
+
{availableContexts.map((ctx) => (
|
|
163
|
+
<option key={ctx} value={ctx}>
|
|
164
|
+
{ctx}
|
|
165
|
+
</option>
|
|
166
|
+
))}
|
|
167
|
+
</select>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{hasActiveFilters && (
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
className={styles.clearFiltersButton}
|
|
174
|
+
onClick={onClearFilters}
|
|
175
|
+
>
|
|
176
|
+
Clear filters
|
|
177
|
+
</button>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
<span className={styles.resultCount}>
|
|
181
|
+
{totalResults} artifact{totalResults !== 1 ? 's' : ''}
|
|
182
|
+
</span>
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
|
2
|
+
import FilterBar from './FilterBar';
|
|
3
|
+
import type { ArtifactEntry, FilterState, SortColumn, SortDirection } from './types';
|
|
4
|
+
import styles from './styles.module.css';
|
|
5
|
+
|
|
6
|
+
interface ArtifactTableProps {
|
|
7
|
+
category: 'domain-artifacts' | 'aggregates';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** All available columns sorted by their position in the table. */
|
|
11
|
+
const COLUMNS: { key: SortColumn | 'contextName' | 'tags' | 'description'; label: string }[] = [
|
|
12
|
+
{ key: 'name', label: 'Name' },
|
|
13
|
+
{ key: 'type', label: 'Type' },
|
|
14
|
+
{ key: 'contextName', label: 'Context' },
|
|
15
|
+
{ key: 'tags', label: 'Tags' },
|
|
16
|
+
{ key: 'description', label: 'Description' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const MANIFEST_URL = '/data/artifacts-manifest.json';
|
|
20
|
+
const DESCRIPTION_MAX_LENGTH = 120;
|
|
21
|
+
|
|
22
|
+
function readFiltersFromURL(): Partial<FilterState> {
|
|
23
|
+
if (typeof window === 'undefined') return {};
|
|
24
|
+
const params = new URLSearchParams(window.location.search);
|
|
25
|
+
const result: Partial<FilterState> = {};
|
|
26
|
+
const name = params.get('name');
|
|
27
|
+
const tags = params.get('tags');
|
|
28
|
+
const type = params.get('type');
|
|
29
|
+
const context = params.get('context');
|
|
30
|
+
if (name) result.nameQuery = name;
|
|
31
|
+
if (tags) result.tagsQuery = tags;
|
|
32
|
+
if (type) result.selectedTypes = type.split(',');
|
|
33
|
+
if (context) result.selectedContext = context;
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writeFiltersToURL(filters: FilterState): void {
|
|
38
|
+
if (typeof window === 'undefined') return;
|
|
39
|
+
const params = new URLSearchParams();
|
|
40
|
+
if (filters.nameQuery) params.set('name', filters.nameQuery);
|
|
41
|
+
if (filters.tagsQuery) params.set('tags', filters.tagsQuery);
|
|
42
|
+
if (filters.selectedTypes.length > 0) params.set('type', filters.selectedTypes.join(','));
|
|
43
|
+
if (filters.selectedContext) params.set('context', filters.selectedContext);
|
|
44
|
+
const qs = params.toString();
|
|
45
|
+
const url = qs ? `${window.location.pathname}?${qs}` : window.location.pathname;
|
|
46
|
+
window.history.replaceState(null, '', url);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type ViewState = 'loading' | 'ready' | 'error';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Main component: fetches the artifact manifest, filters by category,
|
|
53
|
+
* and renders a sortable, filterable table.
|
|
54
|
+
*/
|
|
55
|
+
export default function ArtifactTable({ category }: ArtifactTableProps): JSX.Element {
|
|
56
|
+
const [artifacts, setArtifacts] = useState<ArtifactEntry[]>([]);
|
|
57
|
+
const [viewState, setViewState] = useState<ViewState>('loading');
|
|
58
|
+
|
|
59
|
+
// ---- Fetch manifest ----
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
let cancelled = false;
|
|
62
|
+
async function fetchManifest() {
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch(MANIFEST_URL);
|
|
65
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
66
|
+
const data: ArtifactEntry[] = await res.json();
|
|
67
|
+
if (!cancelled) {
|
|
68
|
+
setArtifacts(data);
|
|
69
|
+
setViewState('ready');
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
if (!cancelled) setViewState('error');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
fetchManifest();
|
|
76
|
+
return () => { cancelled = true; };
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
// ---- Derived data ----
|
|
80
|
+
const categoryArtifacts = useMemo(
|
|
81
|
+
() => artifacts.filter((a) => a.categories.includes(category)),
|
|
82
|
+
[artifacts, category],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const availableTypes = useMemo(
|
|
86
|
+
() => [...new Set(categoryArtifacts.map((a) => a.type))].sort(),
|
|
87
|
+
[categoryArtifacts],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const availableContexts = useMemo(
|
|
91
|
+
() => [...new Set(categoryArtifacts.map((a) => a.contextName))].sort(),
|
|
92
|
+
[categoryArtifacts],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// ---- Filter state (initialised from URL) ----
|
|
96
|
+
const [filters, setFilters] = useState<FilterState>(() => ({
|
|
97
|
+
nameQuery: '',
|
|
98
|
+
tagsQuery: '',
|
|
99
|
+
selectedTypes: [],
|
|
100
|
+
selectedContext: '',
|
|
101
|
+
...readFiltersFromURL(),
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
// Sync filters to URL
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
writeFiltersToURL(filters);
|
|
107
|
+
}, [filters]);
|
|
108
|
+
|
|
109
|
+
const updateFilters = useCallback((updates: Partial<FilterState>) => {
|
|
110
|
+
setFilters((prev) => ({ ...prev, ...updates }));
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
const clearFilters = useCallback(() => {
|
|
114
|
+
setFilters({ nameQuery: '', tagsQuery: '', selectedTypes: [], selectedContext: '' });
|
|
115
|
+
}, []);
|
|
116
|
+
|
|
117
|
+
// ---- Filtered data ----
|
|
118
|
+
const filtered = useMemo(() => {
|
|
119
|
+
return categoryArtifacts.filter((a) => {
|
|
120
|
+
if (filters.nameQuery && !a.name.toLowerCase().includes(filters.nameQuery.toLowerCase())) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
if (filters.tagsQuery && !a.tags.some((t) => t.toLowerCase().includes(filters.tagsQuery.toLowerCase()))) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
if (filters.selectedTypes.length > 0 && !filters.selectedTypes.includes(a.type)) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
if (filters.selectedContext && a.contextName !== filters.selectedContext) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
});
|
|
134
|
+
}, [categoryArtifacts, filters]);
|
|
135
|
+
|
|
136
|
+
// ---- Sorting ----
|
|
137
|
+
const [sort, setSort] = useState<{ column: SortColumn; direction: SortDirection }>({
|
|
138
|
+
column: 'name',
|
|
139
|
+
direction: 'asc',
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const sorted = useMemo(() => {
|
|
143
|
+
const dir = sort.direction === 'asc' ? 1 : -1;
|
|
144
|
+
return [...filtered].sort((a, b) => {
|
|
145
|
+
const aVal = a[sort.column].toLowerCase();
|
|
146
|
+
const bVal = b[sort.column].toLowerCase();
|
|
147
|
+
if (aVal < bVal) return -1 * dir;
|
|
148
|
+
if (aVal > bVal) return 1 * dir;
|
|
149
|
+
return 0;
|
|
150
|
+
});
|
|
151
|
+
}, [filtered, sort]);
|
|
152
|
+
|
|
153
|
+
const toggleSort = useCallback((column: SortColumn) => {
|
|
154
|
+
setSort((prev) => ({
|
|
155
|
+
column,
|
|
156
|
+
direction: prev.column === column && prev.direction === 'asc' ? 'desc' : 'asc',
|
|
157
|
+
}));
|
|
158
|
+
}, []);
|
|
159
|
+
|
|
160
|
+
// ---- Render helpers ----
|
|
161
|
+
function renderSortIndicator(col: SortColumn): string {
|
|
162
|
+
if (col !== sort.column) return '';
|
|
163
|
+
return sort.direction === 'asc' ? ' ▲' : ' ▼';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function renderCell(artifact: ArtifactEntry, col: (typeof COLUMNS)[number]): React.ReactNode {
|
|
167
|
+
switch (col.key) {
|
|
168
|
+
case 'name':
|
|
169
|
+
return <a href={`./${artifact.idMarkdown}`}>{artifact.name}</a>;
|
|
170
|
+
case 'type':
|
|
171
|
+
return artifact.type;
|
|
172
|
+
case 'contextName':
|
|
173
|
+
return artifact.contextName;
|
|
174
|
+
case 'tags':
|
|
175
|
+
return (
|
|
176
|
+
<span className={styles.tagList}>
|
|
177
|
+
{artifact.tags.length > 0
|
|
178
|
+
? artifact.tags.map((tag) => (
|
|
179
|
+
<span key={tag} className={styles.tagChip}>{tag}</span>
|
|
180
|
+
))
|
|
181
|
+
: '—'}
|
|
182
|
+
</span>
|
|
183
|
+
);
|
|
184
|
+
case 'description':
|
|
185
|
+
return (
|
|
186
|
+
<span className={styles.descriptionCell} title={artifact.description || undefined}>
|
|
187
|
+
{artifact.description
|
|
188
|
+
? artifact.description.length > DESCRIPTION_MAX_LENGTH
|
|
189
|
+
? artifact.description.slice(0, DESCRIPTION_MAX_LENGTH) + '…'
|
|
190
|
+
: artifact.description
|
|
191
|
+
: '—'}
|
|
192
|
+
</span>
|
|
193
|
+
);
|
|
194
|
+
default:
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---- Error state ----
|
|
200
|
+
if (viewState === 'error') {
|
|
201
|
+
return (
|
|
202
|
+
<div className={styles.errorState}>
|
|
203
|
+
<p>Could not load artifact data. Please try refreshing the page.</p>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ---- Loading state ----
|
|
209
|
+
if (viewState === 'loading') {
|
|
210
|
+
return (
|
|
211
|
+
<div className={styles.tableContainer}>
|
|
212
|
+
<table className={`table ${styles.table}`}>
|
|
213
|
+
<thead>
|
|
214
|
+
<tr>
|
|
215
|
+
{COLUMNS.map((col) => (
|
|
216
|
+
<th key={col.key}>{col.label}</th>
|
|
217
|
+
))}
|
|
218
|
+
</tr>
|
|
219
|
+
</thead>
|
|
220
|
+
<tbody>
|
|
221
|
+
{[1, 2, 3, 4].map((i) => (
|
|
222
|
+
<tr key={i}>
|
|
223
|
+
{COLUMNS.map((col) => (
|
|
224
|
+
<td key={col.key}>
|
|
225
|
+
<div className={styles.skeleton}> </div>
|
|
226
|
+
</td>
|
|
227
|
+
))}
|
|
228
|
+
</tr>
|
|
229
|
+
))}
|
|
230
|
+
</tbody>
|
|
231
|
+
</table>
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ---- Ready state ----
|
|
237
|
+
return (
|
|
238
|
+
<div>
|
|
239
|
+
<FilterBar
|
|
240
|
+
filters={filters}
|
|
241
|
+
availableTypes={availableTypes}
|
|
242
|
+
availableContexts={availableContexts}
|
|
243
|
+
onFilterChange={updateFilters}
|
|
244
|
+
onClearFilters={clearFilters}
|
|
245
|
+
totalResults={sorted.length}
|
|
246
|
+
/>
|
|
247
|
+
|
|
248
|
+
{sorted.length === 0 && filtered.length === 0 && categoryArtifacts.length > 0 ? (
|
|
249
|
+
<div className={styles.emptyState}>
|
|
250
|
+
<p>No artifacts match your filters.</p>
|
|
251
|
+
<button type="button" className={styles.clearFiltersButton} onClick={clearFilters}>
|
|
252
|
+
Clear filters
|
|
253
|
+
</button>
|
|
254
|
+
</div>
|
|
255
|
+
) : sorted.length === 0 ? (
|
|
256
|
+
<div className={styles.emptyState}>
|
|
257
|
+
<p>No artifacts found for this category.</p>
|
|
258
|
+
</div>
|
|
259
|
+
) : (
|
|
260
|
+
<div className={styles.tableContainer}>
|
|
261
|
+
<table className={`table ${styles.table}`}>
|
|
262
|
+
<thead>
|
|
263
|
+
<tr>
|
|
264
|
+
{COLUMNS.map((col) => (
|
|
265
|
+
<th key={col.key}>
|
|
266
|
+
{col.key === 'name' || col.key === 'type' ? (
|
|
267
|
+
<button
|
|
268
|
+
type="button"
|
|
269
|
+
className={styles.sortableHeader}
|
|
270
|
+
onClick={() => toggleSort(col.key as SortColumn)}
|
|
271
|
+
>
|
|
272
|
+
{col.label}
|
|
273
|
+
<span className={styles.sortIndicator}>
|
|
274
|
+
{renderSortIndicator(col.key as SortColumn)}
|
|
275
|
+
</span>
|
|
276
|
+
</button>
|
|
277
|
+
) : (
|
|
278
|
+
col.label
|
|
279
|
+
)}
|
|
280
|
+
</th>
|
|
281
|
+
))}
|
|
282
|
+
</tr>
|
|
283
|
+
</thead>
|
|
284
|
+
<tbody>
|
|
285
|
+
{sorted.map((artifact) => (
|
|
286
|
+
<tr key={`${artifact.contextName}-${artifact.idMarkdown}`}>
|
|
287
|
+
{COLUMNS.map((col) => (
|
|
288
|
+
<td key={col.key}>{renderCell(artifact, col)}</td>
|
|
289
|
+
))}
|
|
290
|
+
</tr>
|
|
291
|
+
))}
|
|
292
|
+
</tbody>
|
|
293
|
+
</table>
|
|
294
|
+
</div>
|
|
295
|
+
)}
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
}
|