@ankhorage/zora 20.0.2 → 20.1.1

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 (65) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cli/commands/create.d.ts +14 -0
  3. package/dist/cli/commands/create.d.ts.map +1 -0
  4. package/dist/cli/commands/create.js +61 -0
  5. package/dist/cli/commands/create.js.map +1 -0
  6. package/dist/cli/index.d.ts +18 -0
  7. package/dist/cli/index.d.ts.map +1 -0
  8. package/dist/cli/index.js +33 -0
  9. package/dist/cli/index.js.map +1 -0
  10. package/dist/features/tree-view/adapters/inbound/TreeItem.d.ts +1 -5
  11. package/dist/features/tree-view/adapters/inbound/TreeItem.d.ts.map +1 -1
  12. package/dist/features/tree-view/adapters/inbound/TreeItem.js +16 -9
  13. package/dist/features/tree-view/adapters/inbound/TreeItem.js.map +1 -1
  14. package/dist/features/tree-view/adapters/inbound/TreeView.d.ts +1 -5
  15. package/dist/features/tree-view/adapters/inbound/TreeView.d.ts.map +1 -1
  16. package/dist/features/tree-view/adapters/inbound/TreeView.js +6 -9
  17. package/dist/features/tree-view/adapters/inbound/TreeView.js.map +1 -1
  18. package/dist/features/tree-view/adapters/inbound/web-artifact/TreeItemRow.d.ts +15 -0
  19. package/dist/features/tree-view/adapters/inbound/web-artifact/TreeItemRow.d.ts.map +1 -0
  20. package/dist/features/tree-view/adapters/inbound/web-artifact/TreeItemRow.js +121 -0
  21. package/dist/features/tree-view/adapters/inbound/web-artifact/TreeItemRow.js.map +1 -0
  22. package/dist/features/tree-view/adapters/inbound/web-artifact/TreeView.d.ts +32 -0
  23. package/dist/features/tree-view/adapters/inbound/web-artifact/TreeView.d.ts.map +1 -0
  24. package/dist/features/tree-view/adapters/inbound/web-artifact/TreeView.js +24 -0
  25. package/dist/features/tree-view/adapters/inbound/web-artifact/TreeView.js.map +1 -0
  26. package/dist/features/tree-view/utils/toggleExpandedIds.d.ts +3 -0
  27. package/dist/features/tree-view/utils/toggleExpandedIds.d.ts.map +1 -0
  28. package/dist/features/tree-view/utils/toggleExpandedIds.js +7 -0
  29. package/dist/features/tree-view/utils/toggleExpandedIds.js.map +1 -0
  30. package/dist/features/web-component-artifact/adapters/outbound/node/createNodeWebComponentArtifactFileSystem.d.ts +4 -0
  31. package/dist/features/web-component-artifact/adapters/outbound/node/createNodeWebComponentArtifactFileSystem.d.ts.map +1 -0
  32. package/dist/features/web-component-artifact/adapters/outbound/node/createNodeWebComponentArtifactFileSystem.js +20 -0
  33. package/dist/features/web-component-artifact/adapters/outbound/node/createNodeWebComponentArtifactFileSystem.js.map +1 -0
  34. package/dist/features/web-component-artifact/application/ports/outbound/WebComponentArtifactFileSystemPort.d.ts +7 -0
  35. package/dist/features/web-component-artifact/application/ports/outbound/WebComponentArtifactFileSystemPort.d.ts.map +1 -0
  36. package/dist/features/web-component-artifact/application/ports/outbound/WebComponentArtifactFileSystemPort.js +2 -0
  37. package/dist/features/web-component-artifact/application/ports/outbound/WebComponentArtifactFileSystemPort.js.map +1 -0
  38. package/dist/features/web-component-artifact/application/use-cases/materializeWebComponentArtifactAsync.d.ts +16 -0
  39. package/dist/features/web-component-artifact/application/use-cases/materializeWebComponentArtifactAsync.d.ts.map +1 -0
  40. package/dist/features/web-component-artifact/application/use-cases/materializeWebComponentArtifactAsync.js +42 -0
  41. package/dist/features/web-component-artifact/application/use-cases/materializeWebComponentArtifactAsync.js.map +1 -0
  42. package/dist/features/web-component-artifact/composition/materializeWebComponentArtifactWithNodeAsync.d.ts +4 -0
  43. package/dist/features/web-component-artifact/composition/materializeWebComponentArtifactWithNodeAsync.d.ts.map +1 -0
  44. package/dist/features/web-component-artifact/composition/materializeWebComponentArtifactWithNodeAsync.js +7 -0
  45. package/dist/features/web-component-artifact/composition/materializeWebComponentArtifactWithNodeAsync.js.map +1 -0
  46. package/package.json +18 -5
  47. package/src/cli/commands/create.ts +96 -0
  48. package/src/cli/index.test.ts +18 -0
  49. package/src/cli/index.ts +50 -0
  50. package/src/features/tree-view/adapters/inbound/TreeItem.tsx +33 -27
  51. package/src/features/tree-view/adapters/inbound/TreeView.tsx +6 -9
  52. package/src/features/tree-view/adapters/inbound/web-artifact/TreeItemRow.tsx +197 -0
  53. package/src/features/tree-view/adapters/inbound/web-artifact/TreeView.test.tsx +28 -0
  54. package/src/features/tree-view/adapters/inbound/web-artifact/TreeView.tsx +91 -0
  55. package/src/features/tree-view/utils/toggleExpandedIds.test.ts +12 -0
  56. package/src/features/tree-view/utils/toggleExpandedIds.ts +9 -0
  57. package/src/features/web-component-artifact/adapters/outbound/node/createNodeWebComponentArtifactFileSystem.ts +22 -0
  58. package/src/features/web-component-artifact/application/ports/outbound/WebComponentArtifactFileSystemPort.ts +6 -0
  59. package/src/features/web-component-artifact/application/use-cases/materializeWebComponentArtifactAsync.test.ts +84 -0
  60. package/src/features/web-component-artifact/application/use-cases/materializeWebComponentArtifactAsync.ts +83 -0
  61. package/src/features/web-component-artifact/composition/materializeWebComponentArtifactWithNodeAsync.ts +16 -0
  62. package/src/platformAcceptance.test.ts +1 -1
  63. package/src/structureOwnership.test.ts +2 -2
  64. package/web-dist/tree-view/TreeView.d.ts +32 -0
  65. package/web-dist/tree-view/TreeView.js +219 -0
@@ -0,0 +1,197 @@
1
+ import React from 'react';
2
+
3
+ import type { TreeItemNode, TreeItemRenderProps } from './TreeView';
4
+
5
+ /*** Render one recursive browser tree row while keeping selection and expansion independent. */
6
+ export function TreeItemRow<TId extends string>(props: TreeItemRowProps<TId>) {
7
+ const { node } = props;
8
+ const hasChildren = node.children !== undefined && node.children.length > 0;
9
+ const expanded = props.expandedIds.includes(node.id);
10
+ const selected = props.selectedId === node.id;
11
+ const content = props.renderItem?.({
12
+ node,
13
+ depth: props.depth,
14
+ selected,
15
+ expanded,
16
+ hasChildren,
17
+ }) ?? <TreeItemDefaultContent node={node} />;
18
+
19
+ return (
20
+ <>
21
+ <div
22
+ aria-disabled={node.disabled === true ? true : undefined}
23
+ aria-expanded={hasChildren ? expanded : undefined}
24
+ aria-selected={selected}
25
+ onClick={() => selectNode(props)}
26
+ onKeyDown={(event) => selectNodeFromKeyboard(event, props)}
27
+ role="treeitem"
28
+ tabIndex={node.disabled || props.onSelect === undefined ? undefined : 0}
29
+ style={createRowStyle(props.depth, selected, node.disabled, props.onSelect !== undefined)}
30
+ >
31
+ {content}
32
+ <TreeItemActions actions={node.actions} />
33
+ <TreeItemExpander
34
+ expanded={expanded}
35
+ hasChildren={hasChildren}
36
+ nodeId={node.id}
37
+ onToggleExpand={props.onToggleExpand}
38
+ />
39
+ </div>
40
+ {hasChildren && expanded ? <TreeItemChildren {...props} /> : null}
41
+ </>
42
+ );
43
+ }
44
+
45
+ interface TreeItemRowProps<TId extends string> {
46
+ readonly node: TreeItemNode<TId>;
47
+ readonly depth: number;
48
+ readonly selectedId?: TId;
49
+ readonly expandedIds: readonly TId[];
50
+ readonly onSelect?: (id: TId) => void;
51
+ readonly onToggleExpand: (id: TId) => void;
52
+ readonly renderItem?: (props: TreeItemRenderProps<TId>) => React.ReactNode;
53
+ }
54
+
55
+ /*** Render the default browser tree-row label, icon, and metadata. */
56
+ function TreeItemDefaultContent<TId extends string>({
57
+ node,
58
+ }: {
59
+ readonly node: TreeItemNode<TId>;
60
+ }) {
61
+ return (
62
+ <>
63
+ {node.icon === undefined ? null : (
64
+ <span aria-hidden="true" style={ICON_STYLE}>
65
+ {node.icon}
66
+ </span>
67
+ )}
68
+ <span style={LABEL_STYLE}>{node.label}</span>
69
+ {node.meta === undefined ? null : <span style={META_STYLE}>{node.meta}</span>}
70
+ </>
71
+ );
72
+ }
73
+
74
+ /*** Isolate optional row actions so their pointer events do not select the tree item. */
75
+ function TreeItemActions({ actions }: { readonly actions?: React.ReactNode }) {
76
+ if (actions === undefined) return null;
77
+ return (
78
+ <span
79
+ onClick={(event) => event.stopPropagation()}
80
+ onKeyDown={(event) => event.stopPropagation()}
81
+ style={INLINE_STYLE}
82
+ >
83
+ {actions}
84
+ </span>
85
+ );
86
+ }
87
+
88
+ /*** Render the independent expansion control for a tree row with children. */
89
+ function TreeItemExpander<TId extends string>(props: {
90
+ readonly expanded: boolean;
91
+ readonly hasChildren: boolean;
92
+ readonly nodeId: TId;
93
+ readonly onToggleExpand: (id: TId) => void;
94
+ }) {
95
+ if (!props.hasChildren) return null;
96
+ return (
97
+ <button
98
+ aria-label={props.expanded ? 'Collapse' : 'Expand'}
99
+ onClick={(event) => {
100
+ event.stopPropagation();
101
+ props.onToggleExpand(props.nodeId);
102
+ }}
103
+ style={EXPANDER_STYLE}
104
+ type="button"
105
+ >
106
+ {props.expanded ? '⌄' : '›'}
107
+ </button>
108
+ );
109
+ }
110
+
111
+ /*** Render expanded descendants recursively. */
112
+ function TreeItemChildren<TId extends string>(props: TreeItemRowProps<TId>) {
113
+ return (
114
+ <div role="group">
115
+ {props.node.children?.map((child) => (
116
+ <TreeItemRow {...props} key={child.id} depth={props.depth + 1} node={child} />
117
+ ))}
118
+ </div>
119
+ );
120
+ }
121
+
122
+ /*** Select a row from pointer activation when it is interactive. */
123
+ function selectNode<TId extends string>(props: TreeItemRowProps<TId>) {
124
+ if (!props.node.disabled) props.onSelect?.(props.node.id);
125
+ }
126
+
127
+ /*** Select a row from keyboard activation while preserving native navigation keys. */
128
+ function selectNodeFromKeyboard<TId extends string>(
129
+ event: React.KeyboardEvent<HTMLDivElement>,
130
+ props: TreeItemRowProps<TId>,
131
+ ) {
132
+ if (props.node.disabled || (event.key !== 'Enter' && event.key !== ' ')) return;
133
+ event.preventDefault();
134
+ props.onSelect?.(props.node.id);
135
+ }
136
+
137
+ /*** Resolve browser row presentation from the interaction state. */
138
+ function createRowStyle(
139
+ depth: number,
140
+ selected: boolean,
141
+ disabled: boolean | undefined,
142
+ selectable: boolean,
143
+ ): React.CSSProperties {
144
+ return {
145
+ ...ROW_STYLE,
146
+ background: selected ? 'color-mix(in srgb, currentColor 10%, transparent)' : 'transparent',
147
+ cursor: disabled ? 'default' : selectable ? 'pointer' : 'inherit',
148
+ opacity: disabled ? 0.5 : 1,
149
+ paddingLeft: depth * 16 + 6,
150
+ };
151
+ }
152
+
153
+ const ROW_STYLE = {
154
+ alignItems: 'center',
155
+ borderRadius: 6,
156
+ display: 'flex',
157
+ gap: 6,
158
+ minHeight: 28,
159
+ paddingRight: 4,
160
+ userSelect: 'none',
161
+ } as const satisfies React.CSSProperties;
162
+
163
+ const INLINE_STYLE = {
164
+ display: 'inline-flex',
165
+ flexShrink: 0,
166
+ } as const satisfies React.CSSProperties;
167
+
168
+ const ICON_STYLE = INLINE_STYLE;
169
+
170
+ const LABEL_STYLE = {
171
+ flex: 1,
172
+ minWidth: 0,
173
+ overflow: 'hidden',
174
+ textOverflow: 'ellipsis',
175
+ whiteSpace: 'nowrap',
176
+ } as const satisfies React.CSSProperties;
177
+
178
+ const META_STYLE = {
179
+ flexShrink: 0,
180
+ opacity: 0.65,
181
+ } as const satisfies React.CSSProperties;
182
+
183
+ const EXPANDER_STYLE = {
184
+ alignItems: 'center',
185
+ appearance: 'none',
186
+ background: 'transparent',
187
+ border: 0,
188
+ color: 'inherit',
189
+ cursor: 'pointer',
190
+ display: 'inline-flex',
191
+ flexShrink: 0,
192
+ font: 'inherit',
193
+ height: 24,
194
+ justifyContent: 'center',
195
+ padding: 0,
196
+ width: 24,
197
+ } as const satisfies React.CSSProperties;
@@ -0,0 +1,28 @@
1
+ import { expect, test } from 'bun:test';
2
+ import React from 'react';
3
+ import { renderToStaticMarkup } from 'react-dom/server';
4
+
5
+ import { TreeView } from './TreeView';
6
+
7
+ test('renders a standalone web tree without a ZORA or React Native provider', () => {
8
+ const markup = renderToStaticMarkup(
9
+ <TreeView
10
+ defaultExpandedIds={['src']}
11
+ nodes={[
12
+ {
13
+ id: 'src',
14
+ label: 'src',
15
+ icon: <span>folder</span>,
16
+ children: [{ id: 'index', label: 'index.ts' }],
17
+ },
18
+ ]}
19
+ selectedId="index"
20
+ />,
21
+ );
22
+
23
+ expect(markup).toContain('role="tree"');
24
+ expect(markup).toContain('role="treeitem"');
25
+ expect(markup).toContain('folder');
26
+ expect(markup).toContain('index.ts');
27
+ expect(markup).toContain('aria-selected="true"');
28
+ });
@@ -0,0 +1,91 @@
1
+ import React from 'react';
2
+
3
+ import { toggleExpandedIds } from '../../../utils/toggleExpandedIds';
4
+ import { TreeItemRow } from './TreeItemRow';
5
+
6
+ export interface TreeItemNode<TId extends string = string> {
7
+ readonly id: TId;
8
+ readonly label: React.ReactNode;
9
+ readonly icon?: React.ReactNode;
10
+ readonly children?: readonly TreeItemNode<TId>[];
11
+ readonly disabled?: boolean;
12
+ readonly meta?: React.ReactNode;
13
+ readonly actions?: React.ReactNode;
14
+ }
15
+
16
+ export interface TreeItemRenderProps<TId extends string = string> {
17
+ readonly node: TreeItemNode<TId>;
18
+ readonly depth: number;
19
+ readonly selected: boolean;
20
+ readonly expanded: boolean;
21
+ readonly hasChildren: boolean;
22
+ }
23
+
24
+ export interface TreeViewProps<TId extends string = string> {
25
+ readonly nodes: readonly TreeItemNode<TId>[];
26
+ readonly selectedId?: TId;
27
+ readonly expandedIds?: readonly TId[];
28
+ readonly defaultExpandedIds?: readonly TId[];
29
+ readonly onSelect?: (id: TId) => void;
30
+ readonly onExpandedChange?: (ids: readonly TId[]) => void;
31
+ readonly renderItem?: (props: TreeItemRenderProps<TId>) => React.ReactNode;
32
+ readonly className?: string;
33
+ readonly style?: React.CSSProperties;
34
+ readonly ariaLabel?: string;
35
+ }
36
+
37
+ /*** Render the standalone browser TreeView artifact without React Native runtime dependencies. */
38
+ export function TreeView<TId extends string = string>({
39
+ nodes,
40
+ selectedId,
41
+ expandedIds: controlledExpandedIds,
42
+ defaultExpandedIds,
43
+ onSelect,
44
+ onExpandedChange,
45
+ renderItem,
46
+ className,
47
+ style,
48
+ ariaLabel = 'Tree',
49
+ }: TreeViewProps<TId>) {
50
+ const [internalExpandedIds, setInternalExpandedIds] = React.useState<readonly TId[]>(
51
+ defaultExpandedIds ?? [],
52
+ );
53
+ const isControlled = controlledExpandedIds !== undefined;
54
+ const expandedIds = isControlled ? controlledExpandedIds : internalExpandedIds;
55
+ const handleToggleExpand = React.useCallback(
56
+ (id: TId) => {
57
+ const nextExpandedIds = toggleExpandedIds(expandedIds, id);
58
+ if (!isControlled) setInternalExpandedIds(nextExpandedIds);
59
+ onExpandedChange?.(nextExpandedIds);
60
+ },
61
+ [expandedIds, isControlled, onExpandedChange],
62
+ );
63
+
64
+ return (
65
+ <div
66
+ aria-label={ariaLabel}
67
+ className={className}
68
+ role="tree"
69
+ style={{ ...TREE_STYLE, ...style }}
70
+ >
71
+ {nodes.map((node) => (
72
+ <TreeItemRow
73
+ key={node.id}
74
+ depth={0}
75
+ expandedIds={expandedIds}
76
+ node={node}
77
+ onSelect={onSelect}
78
+ onToggleExpand={handleToggleExpand}
79
+ renderItem={renderItem}
80
+ selectedId={selectedId}
81
+ />
82
+ ))}
83
+ </div>
84
+ );
85
+ }
86
+
87
+ const TREE_STYLE = {
88
+ display: 'flex',
89
+ flexDirection: 'column',
90
+ minWidth: 0,
91
+ } as const satisfies React.CSSProperties;
@@ -0,0 +1,12 @@
1
+ import { expect, test } from 'bun:test';
2
+
3
+ import { toggleExpandedIds } from './toggleExpandedIds';
4
+
5
+ test('adds and removes expanded tree ids immutably', () => {
6
+ const initial = ['src'] as const;
7
+ const expanded = toggleExpandedIds(initial, 'test');
8
+
9
+ expect(expanded).toEqual(['src', 'test']);
10
+ expect(initial).toEqual(['src']);
11
+ expect(toggleExpandedIds(expanded, 'src')).toEqual(['test']);
12
+ });
@@ -0,0 +1,9 @@
1
+ /*** Toggle one tree node id while preserving immutable expansion state. */
2
+ export function toggleExpandedIds<TId extends string>(
3
+ expandedIds: readonly TId[],
4
+ id: TId,
5
+ ): readonly TId[] {
6
+ return expandedIds.includes(id)
7
+ ? expandedIds.filter((expandedId) => expandedId !== id)
8
+ : [...expandedIds, id];
9
+ }
@@ -0,0 +1,22 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+
4
+ import type { WebComponentArtifactFileSystemPort } from '../../../application/ports/outbound/WebComponentArtifactFileSystemPort';
5
+
6
+ /*** Create the Node filesystem adapter used by ZORA web-component materialization. */
7
+ export function createNodeWebComponentArtifactFileSystem(): WebComponentArtifactFileSystemPort {
8
+ return {
9
+ joinPath(...parts) {
10
+ return join(...parts);
11
+ },
12
+ async ensureDirectoryAsync(path) {
13
+ await mkdir(path, { recursive: true });
14
+ },
15
+ async readTextFileAsync(path) {
16
+ return await readFile(path, 'utf8');
17
+ },
18
+ async writeTextFileAsync(path, content) {
19
+ await writeFile(path, content, 'utf8');
20
+ },
21
+ };
22
+ }
@@ -0,0 +1,6 @@
1
+ export interface WebComponentArtifactFileSystemPort {
2
+ joinPath(...parts: readonly string[]): string;
3
+ ensureDirectoryAsync(path: string): Promise<void>;
4
+ readTextFileAsync(path: string): Promise<string>;
5
+ writeTextFileAsync(path: string, content: string): Promise<void>;
6
+ }
@@ -0,0 +1,84 @@
1
+ import { expect, test } from 'bun:test';
2
+
3
+ import type { WebComponentArtifactFileSystemPort } from '../ports/outbound/WebComponentArtifactFileSystemPort';
4
+ import { materializeWebComponentArtifactAsync } from './materializeWebComponentArtifactAsync';
5
+
6
+ test('materializes the packaged tree-view web artifact without source ownership transfer', async () => {
7
+ const packageRoot = '/package';
8
+ const outputDirectory = '/consumer/src/generated/zora/tree-view';
9
+ const writes = new Map<string, string>();
10
+ const sources = new Map<string, string>([
11
+ ['/package/web-dist/tree-view/TreeView.js', 'export const TreeView = 1;\n'],
12
+ ['/package/web-dist/tree-view/TreeView.d.ts', 'export declare const TreeView: unknown;\n'],
13
+ ]);
14
+ const fileSystem = createFakeFileSystem(sources, writes);
15
+
16
+ const result = await materializeWebComponentArtifactAsync(
17
+ {
18
+ component: 'tree-view',
19
+ outputDirectory,
20
+ packageRoot,
21
+ packageVersion: '1.2.3',
22
+ },
23
+ fileSystem,
24
+ );
25
+
26
+ expect(result.platform).toBe('web');
27
+ expect(result.createdFiles).toEqual([
28
+ `${outputDirectory}/TreeView.js`,
29
+ `${outputDirectory}/TreeView.d.ts`,
30
+ `${outputDirectory}/zora-artifact.json`,
31
+ ]);
32
+ expect(writes.get(`${outputDirectory}/TreeView.js`)).toStartWith(
33
+ '// Generated by @ankhorage/zora v1.2.3. Do not edit manually.',
34
+ );
35
+ expect(writes.get(`${outputDirectory}/zora-artifact.json`)).toContain(
36
+ '"owner": "@ankhorage/zora"',
37
+ );
38
+ });
39
+
40
+ test('rejects unsupported web components explicitly', async () => {
41
+ const fileSystem = createFakeFileSystem(new Map(), new Map());
42
+ let caught: unknown;
43
+
44
+ try {
45
+ await materializeWebComponentArtifactAsync(
46
+ {
47
+ component: 'unknown',
48
+ outputDirectory: '/consumer',
49
+ packageRoot: '/package',
50
+ packageVersion: '1.2.3',
51
+ },
52
+ fileSystem,
53
+ );
54
+ } catch (error) {
55
+ caught = error;
56
+ }
57
+
58
+ expect(caught).toBeInstanceOf(Error);
59
+ expect((caught as Error).message).toBe('Unsupported ZORA web component: unknown');
60
+ });
61
+
62
+ /*** Create a portable fake for the artifact filesystem port. */
63
+ function createFakeFileSystem(
64
+ sources: ReadonlyMap<string, string>,
65
+ writes: Map<string, string>,
66
+ ): WebComponentArtifactFileSystemPort {
67
+ return {
68
+ joinPath(...parts) {
69
+ return parts.join('/').replaceAll('//', '/');
70
+ },
71
+ ensureDirectoryAsync() {
72
+ return Promise.resolve();
73
+ },
74
+ readTextFileAsync(path) {
75
+ const source = sources.get(path);
76
+ if (source === undefined) return Promise.reject(new Error(`Missing test source: ${path}`));
77
+ return Promise.resolve(source);
78
+ },
79
+ writeTextFileAsync(path, content) {
80
+ writes.set(path, content);
81
+ return Promise.resolve();
82
+ },
83
+ };
84
+ }
@@ -0,0 +1,83 @@
1
+ import type { WebComponentArtifactFileSystemPort } from '../ports/outbound/WebComponentArtifactFileSystemPort';
2
+
3
+ export interface MaterializeWebComponentArtifactInput {
4
+ readonly component: string;
5
+ readonly outputDirectory: string;
6
+ readonly packageRoot: string;
7
+ readonly packageVersion: string;
8
+ }
9
+
10
+ export interface MaterializeWebComponentArtifactResult {
11
+ readonly component: string;
12
+ readonly createdFiles: readonly string[];
13
+ readonly outputDirectory: string;
14
+ readonly platform: 'web';
15
+ }
16
+
17
+ /*** Materialize one replaceable web-only component artifact from the published ZORA build. */
18
+ export async function materializeWebComponentArtifactAsync(
19
+ input: MaterializeWebComponentArtifactInput,
20
+ fileSystem: WebComponentArtifactFileSystemPort,
21
+ ): Promise<MaterializeWebComponentArtifactResult> {
22
+ const artifact = resolveWebArtifact(input.component);
23
+ const sourceDirectory = fileSystem.joinPath(input.packageRoot, 'web-dist', artifact.directory);
24
+ await fileSystem.ensureDirectoryAsync(input.outputDirectory);
25
+
26
+ const createdFiles = await Promise.all(
27
+ artifact.files.map(async (fileName) => {
28
+ const sourcePath = fileSystem.joinPath(sourceDirectory, fileName);
29
+ const targetPath = fileSystem.joinPath(input.outputDirectory, fileName);
30
+ const source = await fileSystem.readTextFileAsync(sourcePath);
31
+ await fileSystem.writeTextFileAsync(
32
+ targetPath,
33
+ createGeneratedSource(input.packageVersion, source),
34
+ );
35
+ return targetPath;
36
+ }),
37
+ );
38
+
39
+ const metadataPath = fileSystem.joinPath(input.outputDirectory, 'zora-artifact.json');
40
+ await fileSystem.writeTextFileAsync(
41
+ metadataPath,
42
+ `${JSON.stringify(
43
+ {
44
+ generated: true,
45
+ owner: '@ankhorage/zora',
46
+ version: input.packageVersion,
47
+ component: input.component,
48
+ platform: 'web',
49
+ },
50
+ null,
51
+ 2,
52
+ )}\n`,
53
+ );
54
+
55
+ return {
56
+ component: input.component,
57
+ createdFiles: [...createdFiles, metadataPath],
58
+ outputDirectory: input.outputDirectory,
59
+ platform: 'web',
60
+ };
61
+ }
62
+
63
+ interface WebArtifactDefinition {
64
+ readonly directory: string;
65
+ readonly files: readonly string[];
66
+ }
67
+
68
+ /*** Resolve the canonical packaged web artifact for a requested component. */
69
+ function resolveWebArtifact(component: string): WebArtifactDefinition {
70
+ if (component === 'tree-view') {
71
+ return {
72
+ directory: 'tree-view',
73
+ files: ['TreeView.js', 'TreeView.d.ts'],
74
+ };
75
+ }
76
+
77
+ throw new Error(`Unsupported ZORA web component: ${component}`);
78
+ }
79
+
80
+ /*** Add a generated-file marker without changing the compiled artifact semantics. */
81
+ function createGeneratedSource(packageVersion: string, source: string): string {
82
+ return `// Generated by @ankhorage/zora v${packageVersion}. Do not edit manually.\n${source}`;
83
+ }
@@ -0,0 +1,16 @@
1
+ import { createNodeWebComponentArtifactFileSystem } from '../adapters/outbound/node/createNodeWebComponentArtifactFileSystem';
2
+ import {
3
+ materializeWebComponentArtifactAsync,
4
+ type MaterializeWebComponentArtifactInput,
5
+ type MaterializeWebComponentArtifactResult,
6
+ } from '../application/use-cases/materializeWebComponentArtifactAsync';
7
+
8
+ /*** Compose the Node filesystem adapter with the web-component materialization use case. */
9
+ export async function materializeWebComponentArtifactWithNodeAsync(
10
+ input: MaterializeWebComponentArtifactInput,
11
+ ): Promise<MaterializeWebComponentArtifactResult> {
12
+ return await materializeWebComponentArtifactAsync(
13
+ input,
14
+ createNodeWebComponentArtifactFileSystem(),
15
+ );
16
+ }
@@ -9,6 +9,6 @@ test('passes the isolated Surface and RN Web 0.21 acceptance suite', () => {
9
9
  const output = `${subprocess.stdout.toString()}\n${subprocess.stderr.toString()}`;
10
10
 
11
11
  expect(subprocess.exitCode, output).toBe(0);
12
- expect(output).toContain('10 pass');
12
+ expect(output).toContain('11 pass');
13
13
  expect(output).toContain('0 fail');
14
14
  });
@@ -3,13 +3,13 @@ import { existsSync, readdirSync } from 'node:fs';
3
3
  import { describe, expect, test } from 'bun:test';
4
4
 
5
5
  describe('src ownership', () => {
6
- test('keeps only feature and type owner directories at the package source root', () => {
6
+ test('keeps the required CLI, feature, and type owner directories at the package source root', () => {
7
7
  const directories = readdirSync('src', { withFileTypes: true })
8
8
  .filter((entry) => entry.isDirectory())
9
9
  .map((entry) => entry.name)
10
10
  .sort();
11
11
 
12
- expect(directories).toEqual(['features', 'types']);
12
+ expect(directories).toEqual(['cli', 'features', 'types']);
13
13
  });
14
14
 
15
15
  test('does not reintroduce technical ownership roots', () => {
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ export interface TreeItemNode<TId extends string = string> {
3
+ readonly id: TId;
4
+ readonly label: React.ReactNode;
5
+ readonly icon?: React.ReactNode;
6
+ readonly children?: readonly TreeItemNode<TId>[];
7
+ readonly disabled?: boolean;
8
+ readonly meta?: React.ReactNode;
9
+ readonly actions?: React.ReactNode;
10
+ }
11
+ export interface TreeItemRenderProps<TId extends string = string> {
12
+ readonly node: TreeItemNode<TId>;
13
+ readonly depth: number;
14
+ readonly selected: boolean;
15
+ readonly expanded: boolean;
16
+ readonly hasChildren: boolean;
17
+ }
18
+ export interface TreeViewProps<TId extends string = string> {
19
+ readonly nodes: readonly TreeItemNode<TId>[];
20
+ readonly selectedId?: TId;
21
+ readonly expandedIds?: readonly TId[];
22
+ readonly defaultExpandedIds?: readonly TId[];
23
+ readonly onSelect?: (id: TId) => void;
24
+ readonly onExpandedChange?: (ids: readonly TId[]) => void;
25
+ readonly renderItem?: (props: TreeItemRenderProps<TId>) => React.ReactNode;
26
+ readonly className?: string;
27
+ readonly style?: React.CSSProperties;
28
+ readonly ariaLabel?: string;
29
+ }
30
+ /*** Render the standalone browser TreeView artifact without React Native runtime dependencies. */
31
+ export declare function TreeView<TId extends string = string>({ nodes, selectedId, expandedIds: controlledExpandedIds, defaultExpandedIds, onSelect, onExpandedChange, renderItem, className, style, ariaLabel, }: TreeViewProps<TId>): React.JSX.Element;
32
+ //# sourceMappingURL=TreeView.d.ts.map