@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.
Files changed (182) hide show
  1. package/README.md +219 -0
  2. package/bin/classes/index.d.ts +0 -0
  3. package/bin/classes/index.js +2 -0
  4. package/bin/classes/index.js.map +1 -0
  5. package/bin/classes/logger.d.ts +42 -0
  6. package/bin/classes/logger.js +185 -0
  7. package/bin/classes/logger.js.map +1 -0
  8. package/bin/cli.d.ts +2 -0
  9. package/bin/cli.js +48 -0
  10. package/bin/cli.js.map +1 -0
  11. package/bin/commands/build.d.ts +2 -0
  12. package/bin/commands/build.js +151 -0
  13. package/bin/commands/build.js.map +1 -0
  14. package/bin/commands/create.d.ts +60 -0
  15. package/bin/commands/create.js +368 -0
  16. package/bin/commands/create.js.map +1 -0
  17. package/bin/commands/generate-documentation.d.ts +2 -0
  18. package/bin/commands/generate-documentation.js +249 -0
  19. package/bin/commands/generate-documentation.js.map +1 -0
  20. package/bin/commands/generate.d.ts +2 -0
  21. package/bin/commands/generate.js +79 -0
  22. package/bin/commands/generate.js.map +1 -0
  23. package/bin/commands/index.d.ts +6 -0
  24. package/bin/commands/index.js +16 -0
  25. package/bin/commands/index.js.map +1 -0
  26. package/bin/commands/init.d.ts +2 -0
  27. package/bin/commands/init.js +54 -0
  28. package/bin/commands/init.js.map +1 -0
  29. package/bin/commands/serve.d.ts +2 -0
  30. package/bin/commands/serve.js +124 -0
  31. package/bin/commands/serve.js.map +1 -0
  32. package/bin/commands/types.d.ts +65 -0
  33. package/bin/commands/types.js +9 -0
  34. package/bin/commands/types.js.map +1 -0
  35. package/bin/config/reader.d.ts +17 -0
  36. package/bin/config/reader.js +166 -0
  37. package/bin/config/reader.js.map +1 -0
  38. package/bin/config/types.d.ts +57 -0
  39. package/bin/config/types.js +21 -0
  40. package/bin/config/types.js.map +1 -0
  41. package/bin/data/documentation-folder.d.ts +1 -0
  42. package/bin/data/documentation-folder.js +5 -0
  43. package/bin/data/documentation-folder.js.map +1 -0
  44. package/bin/data/files-generated.d.ts +1 -0
  45. package/bin/data/files-generated.js +10 -0
  46. package/bin/data/files-generated.js.map +1 -0
  47. package/bin/data/index.d.ts +2 -0
  48. package/bin/data/index.js +8 -0
  49. package/bin/data/index.js.map +1 -0
  50. package/bin/pipeline/generate.d.ts +26 -0
  51. package/bin/pipeline/generate.js +269 -0
  52. package/bin/pipeline/generate.js.map +1 -0
  53. package/bin/reporters/data-loading.d.ts +121 -0
  54. package/bin/reporters/data-loading.js +398 -0
  55. package/bin/reporters/data-loading.js.map +1 -0
  56. package/bin/reporters/data-transformation.d.ts +101 -0
  57. package/bin/reporters/data-transformation.js +392 -0
  58. package/bin/reporters/data-transformation.js.map +1 -0
  59. package/bin/reporters/file-writing.d.ts +29 -0
  60. package/bin/reporters/file-writing.js +100 -0
  61. package/bin/reporters/file-writing.js.map +1 -0
  62. package/bin/reporters/generate-domain-documentation.d.ts +17 -0
  63. package/bin/reporters/generate-domain-documentation.js +161 -0
  64. package/bin/reporters/generate-domain-documentation.js.map +1 -0
  65. package/bin/reporters/generate-use-cases-documentation.d.ts +18 -0
  66. package/bin/reporters/generate-use-cases-documentation.js +123 -0
  67. package/bin/reporters/generate-use-cases-documentation.js.map +1 -0
  68. package/bin/reporters/rendering.d.ts +116 -0
  69. package/bin/reporters/rendering.js +385 -0
  70. package/bin/reporters/rendering.js.map +1 -0
  71. package/bin/reporters/templates/README.md +28 -0
  72. package/bin/reporters/templates/card.template.jsx +5 -0
  73. package/bin/reporters/templates/cards-container.template.jsx +5 -0
  74. package/bin/reporters/types.d.ts +190 -0
  75. package/bin/reporters/types.js +8 -0
  76. package/bin/reporters/types.js.map +1 -0
  77. package/bin/scaffold/assembler.d.ts +13 -0
  78. package/bin/scaffold/assembler.js +371 -0
  79. package/bin/scaffold/assembler.js.map +1 -0
  80. package/bin/scaffold/doc-assembler.d.ts +10 -0
  81. package/bin/scaffold/doc-assembler.js +113 -0
  82. package/bin/scaffold/doc-assembler.js.map +1 -0
  83. package/bin/scripts/add-import.d.ts +1 -0
  84. package/bin/scripts/add-import.js +26 -0
  85. package/bin/scripts/add-import.js.map +1 -0
  86. package/bin/scripts/converter.d.ts +6 -0
  87. package/bin/scripts/converter.js +120 -0
  88. package/bin/scripts/converter.js.map +1 -0
  89. package/bin/scripts/copy-files.d.ts +1 -0
  90. package/bin/scripts/copy-files.js +96 -0
  91. package/bin/scripts/copy-files.js.map +1 -0
  92. package/bin/scripts/create-folder.d.ts +1 -0
  93. package/bin/scripts/create-folder.js +23 -0
  94. package/bin/scripts/create-folder.js.map +1 -0
  95. package/bin/scripts/delete-paths.d.ts +1 -0
  96. package/bin/scripts/delete-paths.js +34 -0
  97. package/bin/scripts/delete-paths.js.map +1 -0
  98. package/bin/scripts/exists-file.d.ts +5 -0
  99. package/bin/scripts/exists-file.js +12 -0
  100. package/bin/scripts/exists-file.js.map +1 -0
  101. package/bin/scripts/generate-track-artifacts.d.ts +1 -0
  102. package/bin/scripts/generate-track-artifacts.js +59 -0
  103. package/bin/scripts/generate-track-artifacts.js.map +1 -0
  104. package/bin/scripts/get-artifacts.d.ts +1 -0
  105. package/bin/scripts/get-artifacts.js +38 -0
  106. package/bin/scripts/get-artifacts.js.map +1 -0
  107. package/bin/scripts/get-directories.d.ts +1 -0
  108. package/bin/scripts/get-directories.js +10 -0
  109. package/bin/scripts/get-directories.js.map +1 -0
  110. package/bin/scripts/get-file.d.ts +9 -0
  111. package/bin/scripts/get-file.js +38 -0
  112. package/bin/scripts/get-file.js.map +1 -0
  113. package/bin/scripts/labels.d.ts +35 -0
  114. package/bin/scripts/labels.js +108 -0
  115. package/bin/scripts/labels.js.map +1 -0
  116. package/bin/scripts/markdown.d.ts +34 -0
  117. package/bin/scripts/markdown.js +99 -0
  118. package/bin/scripts/markdown.js.map +1 -0
  119. package/bin/scripts/names.d.ts +18 -0
  120. package/bin/scripts/names.js +64 -0
  121. package/bin/scripts/names.js.map +1 -0
  122. package/bin/scripts/track-artifacts.d.ts +2 -0
  123. package/bin/scripts/track-artifacts.js +101 -0
  124. package/bin/scripts/track-artifacts.js.map +1 -0
  125. package/bin/utils/create-folder.d.ts +1 -0
  126. package/bin/utils/create-folder.js +23 -0
  127. package/bin/utils/create-folder.js.map +1 -0
  128. package/bin/utils/delete-markdown-files.d.ts +1 -0
  129. package/bin/utils/delete-markdown-files.js +46 -0
  130. package/bin/utils/delete-markdown-files.js.map +1 -0
  131. package/bin/utils/delete-paths.d.ts +5 -0
  132. package/bin/utils/delete-paths.js +26 -0
  133. package/bin/utils/delete-paths.js.map +1 -0
  134. package/bin/utils/exists-file.d.ts +6 -0
  135. package/bin/utils/exists-file.js +13 -0
  136. package/bin/utils/exists-file.js.map +1 -0
  137. package/bin/utils/exists-folder.d.ts +6 -0
  138. package/bin/utils/exists-folder.js +13 -0
  139. package/bin/utils/exists-folder.js.map +1 -0
  140. package/bin/utils/get-internal-resource-path.d.ts +1 -0
  141. package/bin/utils/get-internal-resource-path.js +43 -0
  142. package/bin/utils/get-internal-resource-path.js.map +1 -0
  143. package/bin/utils/get-json-file.d.ts +1 -0
  144. package/bin/utils/get-json-file.js +27 -0
  145. package/bin/utils/get-json-file.js.map +1 -0
  146. package/bin/utils/index.d.ts +6 -0
  147. package/bin/utils/index.js +16 -0
  148. package/bin/utils/index.js.map +1 -0
  149. package/bin/utils/run-command.d.ts +5 -0
  150. package/bin/utils/run-command.js +36 -0
  151. package/bin/utils/run-command.js.map +1 -0
  152. package/jsdoc.conf.json +30 -0
  153. package/package.json +75 -0
  154. package/templates/documentation/README.md +41 -0
  155. package/templates/documentation/docs/.gitkeep +0 -0
  156. package/templates/documentation/docusaurus.config.ts +127 -0
  157. package/templates/documentation/package-lock.json +19431 -0
  158. package/templates/documentation/package.json +49 -0
  159. package/templates/documentation/sidebars.ts +33 -0
  160. package/templates/documentation/src/components/ArtifactTable/FilterBar.tsx +185 -0
  161. package/templates/documentation/src/components/ArtifactTable/index.tsx +298 -0
  162. package/templates/documentation/src/components/ArtifactTable/styles.module.css +282 -0
  163. package/templates/documentation/src/components/ArtifactTable/types.ts +31 -0
  164. package/templates/documentation/src/components/HomepageFeatures/index.tsx +77 -0
  165. package/templates/documentation/src/components/HomepageFeatures/styles.module.css +11 -0
  166. package/templates/documentation/src/css/custom.css +30 -0
  167. package/templates/documentation/src/pages/index.module.css +23 -0
  168. package/templates/documentation/src/pages/index.tsx +43 -0
  169. package/templates/documentation/src/pages/markdown-page.md +7 -0
  170. package/templates/documentation/static/.nojekyll +0 -0
  171. package/templates/documentation/static/img/docusaurus-social-card.jpg +0 -0
  172. package/templates/documentation/static/img/docusaurus.png +0 -0
  173. package/templates/documentation/static/img/favicon.ico +0 -0
  174. package/templates/documentation/static/img/logo.svg +1 -0
  175. package/templates/documentation/static/img/logo_imolko_azul.png +0 -0
  176. package/templates/documentation/static/img/undraw_docusaurus_mountain.svg +171 -0
  177. package/templates/documentation/static/img/undraw_docusaurus_react.svg +170 -0
  178. package/templates/documentation/static/img/undraw_docusaurus_tree.svg +40 -0
  179. package/templates/documentation/tsconfig.json +8 -0
  180. package/templates/documentation/ultra-reporter.config.json +55 -0
  181. package/templates/track-artifacts-script.ts +44 -0
  182. 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}>&nbsp;</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
+ }