@ankhorage/zora 22.0.80 → 22.0.82

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 22.0.82
4
+
5
+ ### Patch Changes
6
+
7
+ - 37b8d04: Reveal controlled TreeView selections in standalone web artifacts without changing expansion ownership.
8
+
9
+ ## 22.0.81
10
+
11
+ ### Patch Changes
12
+
13
+ - d3d3741: Preserve public GraphView type exports in standalone web materialization.
14
+
3
15
  ## 22.0.80
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  # @ankhorage/zora
5
5
 
6
- ![license: MIT](././paradox/badges/license.svg) ![npm: v22.0.80](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![paradox: warnings](././paradox/badges/docs.svg)
6
+ ![license: MIT](././paradox/badges/license.svg) ![npm: v22.0.82](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![paradox: warnings](././paradox/badges/docs.svg)
7
7
 
8
8
  Opinionated React Native and React Native Web UI kit built on @ankhorage/surface.
9
9
 
@@ -1 +1 @@
1
- {"version":3,"file":"TreeView.d.ts","sourceRoot":"","sources":["../../../../../../src/features/tree-view/adapters/inbound/web-artifact/TreeView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAK1B,MAAM,WAAW,YAAY,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM;IACvD,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAC;IACjB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAChC,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;IACjD,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CACpC;AAED,MAAM,WAAW,mBAAmB,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM;IAC9D,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM;IACxD,QAAQ,CAAC,KAAK,EAAE,SAAS,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;IAC7C,QAAQ,CAAC,UAAU,CAAC,EAAE,GAAG,CAAC;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,GAAG,EAAE,CAAC;IACtC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,GAAG,EAAE,CAAC;IAC7C,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,IAAI,CAAC;IACtC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,SAAS,GAAG,EAAE,KAAK,IAAI,CAAC;IAC1D,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,SAAS,CAAC;IAC3E,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IACnD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IACrC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM,EAAE,EACpD,KAAK,EACL,UAAU,EACV,WAAW,EAAE,qBAAqB,EAClC,kBAAkB,EAClB,QAAQ,EACR,gBAAgB,EAChB,UAAU,EACV,kBAA6B,EAC7B,SAAS,EACT,KAAK,EACL,SAAkB,GACnB,EAAE,aAAa,CAAC,GAAG,CAAC,qBAqCpB"}
1
+ {"version":3,"file":"TreeView.d.ts","sourceRoot":"","sources":["../../../../../../src/features/tree-view/adapters/inbound/web-artifact/TreeView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAK1B,MAAM,WAAW,YAAY,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM;IACvD,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAC;IACjB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAChC,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;IACjD,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CACpC;AAED,MAAM,WAAW,mBAAmB,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM;IAC9D,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM;IACxD,QAAQ,CAAC,KAAK,EAAE,SAAS,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;IAC7C,QAAQ,CAAC,UAAU,CAAC,EAAE,GAAG,CAAC;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,GAAG,EAAE,CAAC;IACtC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,GAAG,EAAE,CAAC;IAC7C,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,IAAI,CAAC;IACtC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,SAAS,GAAG,EAAE,KAAK,IAAI,CAAC;IAC1D,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,SAAS,CAAC;IAC3E,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IACnD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IACrC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM,EAAE,EACpD,KAAK,EACL,UAAU,EACV,WAAW,EAAE,qBAAqB,EAClC,kBAAkB,EAClB,QAAQ,EACR,gBAAgB,EAChB,UAAU,EACV,kBAA6B,EAC7B,SAAS,EACT,KAAK,EACL,SAAkB,GACnB,EAAE,aAAa,CAAC,GAAG,CAAC,qBAwCpB"}
@@ -8,6 +8,7 @@ import { TreeItemRow } from './TreeItemRow';
8
8
  */
9
9
  export function TreeView({ nodes, selectedId, expandedIds: controlledExpandedIds, defaultExpandedIds, onSelect, onExpandedChange, renderItem, expansionIndicator = 'folder', className, style, ariaLabel = 'Tree', }) {
10
10
  const [internalExpandedIds, setInternalExpandedIds] = React.useState(defaultExpandedIds ?? []);
11
+ const rootRef = React.useRef(null);
11
12
  const isControlled = controlledExpandedIds !== undefined;
12
13
  const expandedIds = isControlled ? controlledExpandedIds : internalExpandedIds;
13
14
  const handleToggleExpand = React.useCallback((id) => {
@@ -16,10 +17,22 @@ export function TreeView({ nodes, selectedId, expandedIds: controlledExpandedIds
16
17
  setInternalExpandedIds(nextExpandedIds);
17
18
  onExpandedChange?.(nextExpandedIds);
18
19
  }, [expandedIds, isControlled, onExpandedChange]);
19
- return (<div aria-label={ariaLabel} className={className} role="tree" style={{ ...TREE_STYLE, ...style }}>
20
+ useSelectedTreeItemReveal(rootRef, selectedId, expandedIds);
21
+ return (<div ref={rootRef} aria-label={ariaLabel} className={className} role="tree" style={{ ...TREE_STYLE, ...style }}>
20
22
  {nodes.map((node) => (<TreeItemRow key={node.id} depth={0} expandedIds={expandedIds} expansionIndicator={expansionIndicator} node={node} onSelect={onSelect} onToggleExpand={handleToggleExpand} renderItem={renderItem} selectedId={selectedId}/>))}
21
23
  </div>);
22
24
  }
25
+ /*** Reveal a controlled selection after it is rendered without changing expansion ownership. */
26
+ function useSelectedTreeItemReveal(rootRef, selectedId, expandedIds) {
27
+ React.useEffect(() => {
28
+ if (selectedId === undefined)
29
+ return;
30
+ const root = rootRef.current;
31
+ root
32
+ ?.querySelector('[role="treeitem"][aria-selected="true"]')
33
+ ?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
34
+ }, [expandedIds, rootRef, selectedId]);
35
+ }
23
36
  const TREE_STYLE = {
24
37
  display: 'flex',
25
38
  flexDirection: 'column',
@@ -1 +1 @@
1
- {"version":3,"file":"TreeView.js","sourceRoot":"","sources":["../../../../../../src/features/tree-view/adapters/inbound/web-artifact/TreeView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAkC5C;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CAA8B,EACpD,KAAK,EACL,UAAU,EACV,WAAW,EAAE,qBAAqB,EAClC,kBAAkB,EAClB,QAAQ,EACR,gBAAgB,EAChB,UAAU,EACV,kBAAkB,GAAG,QAAQ,EAC7B,SAAS,EACT,KAAK,EACL,SAAS,GAAG,MAAM,GACC;IACnB,MAAM,CAAC,mBAAmB,EAAE,sBAAsB,CAAC,GAAG,KAAK,CAAC,QAAQ,CAClE,kBAAkB,IAAI,EAAE,CACzB,CAAC;IACF,MAAM,YAAY,GAAG,qBAAqB,KAAK,SAAS,CAAC;IACzD,MAAM,WAAW,GAAG,YAAY,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,mBAAmB,CAAC;IAC/E,MAAM,kBAAkB,GAAG,KAAK,CAAC,WAAW,CAC1C,CAAC,EAAO,EAAE,EAAE;QACV,MAAM,eAAe,GAAG,iBAAiB,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QAC3D,IAAI,CAAC,YAAY;YAAE,sBAAsB,CAAC,eAAe,CAAC,CAAC;QAC3D,gBAAgB,EAAE,CAAC,eAAe,CAAC,CAAC;IACtC,CAAC,EACD,CAAC,WAAW,EAAE,YAAY,EAAE,gBAAgB,CAAC,CAC9C,CAAC;IAEF,OAAO,CACL,CAAC,GAAG,CACF,UAAU,CAAC,CAAC,SAAS,CAAC,CACtB,SAAS,CAAC,CAAC,SAAS,CAAC,CACrB,IAAI,CAAC,MAAM,CACX,KAAK,CAAC,CAAC,EAAE,GAAG,UAAU,EAAE,GAAG,KAAK,EAAE,CAAC,CAEnC;MAAA,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CACnB,CAAC,WAAW,CACV,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CACb,KAAK,CAAC,CAAC,CAAC,CAAC,CACT,WAAW,CAAC,CAAC,WAAW,CAAC,CACzB,kBAAkB,CAAC,CAAC,kBAAkB,CAAC,CACvC,IAAI,CAAC,CAAC,IAAI,CAAC,CACX,QAAQ,CAAC,CAAC,QAAQ,CAAC,CACnB,cAAc,CAAC,CAAC,kBAAkB,CAAC,CACnC,UAAU,CAAC,CAAC,UAAU,CAAC,CACvB,UAAU,CAAC,CAAC,UAAU,CAAC,EACvB,CACH,CAAC,CACJ;IAAA,EAAE,GAAG,CAAC,CACP,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,GAAG;IACjB,OAAO,EAAE,MAAM;IACf,aAAa,EAAE,QAAQ;IACvB,QAAQ,EAAE,CAAC;CAC2B,CAAC","sourcesContent":["import React from 'react';\n\nimport { toggleExpandedIds } from '../../../utils/toggleExpandedIds';\nimport { TreeItemRow } from './TreeItemRow';\n\nexport interface TreeItemNode<TId extends string = string> {\n readonly id: TId;\n readonly label: React.ReactNode;\n readonly icon?: React.ReactNode;\n readonly children?: readonly TreeItemNode<TId>[];\n readonly disabled?: boolean;\n readonly meta?: React.ReactNode;\n readonly actions?: React.ReactNode;\n}\n\nexport interface TreeItemRenderProps<TId extends string = string> {\n readonly node: TreeItemNode<TId>;\n readonly depth: number;\n readonly selected: boolean;\n readonly expanded: boolean;\n readonly hasChildren: boolean;\n}\n\nexport interface TreeViewProps<TId extends string = string> {\n readonly nodes: readonly TreeItemNode<TId>[];\n readonly selectedId?: TId;\n readonly expandedIds?: readonly TId[];\n readonly defaultExpandedIds?: readonly TId[];\n readonly onSelect?: (id: TId) => void;\n readonly onExpandedChange?: (ids: readonly TId[]) => void;\n readonly renderItem?: (props: TreeItemRenderProps<TId>) => React.ReactNode;\n readonly expansionIndicator?: 'chevron' | 'folder';\n readonly className?: string;\n readonly style?: React.CSSProperties;\n readonly ariaLabel?: string;\n}\n\n/***\n * Render the standalone browser TreeView artifact without React Native runtime dependencies.\n * expansionIndicator Use `chevron` beside custom node icons, or `folder` (default) for\n * built-in folder/file indicators. Expansion is leading and independent from row selection.\n */\nexport function TreeView<TId extends string = string>({\n nodes,\n selectedId,\n expandedIds: controlledExpandedIds,\n defaultExpandedIds,\n onSelect,\n onExpandedChange,\n renderItem,\n expansionIndicator = 'folder',\n className,\n style,\n ariaLabel = 'Tree',\n}: TreeViewProps<TId>) {\n const [internalExpandedIds, setInternalExpandedIds] = React.useState<readonly TId[]>(\n defaultExpandedIds ?? [],\n );\n const isControlled = controlledExpandedIds !== undefined;\n const expandedIds = isControlled ? controlledExpandedIds : internalExpandedIds;\n const handleToggleExpand = React.useCallback(\n (id: TId) => {\n const nextExpandedIds = toggleExpandedIds(expandedIds, id);\n if (!isControlled) setInternalExpandedIds(nextExpandedIds);\n onExpandedChange?.(nextExpandedIds);\n },\n [expandedIds, isControlled, onExpandedChange],\n );\n\n return (\n <div\n aria-label={ariaLabel}\n className={className}\n role=\"tree\"\n style={{ ...TREE_STYLE, ...style }}\n >\n {nodes.map((node) => (\n <TreeItemRow\n key={node.id}\n depth={0}\n expandedIds={expandedIds}\n expansionIndicator={expansionIndicator}\n node={node}\n onSelect={onSelect}\n onToggleExpand={handleToggleExpand}\n renderItem={renderItem}\n selectedId={selectedId}\n />\n ))}\n </div>\n );\n}\n\nconst TREE_STYLE = {\n display: 'flex',\n flexDirection: 'column',\n minWidth: 0,\n} as const satisfies React.CSSProperties;\n"]}
1
+ {"version":3,"file":"TreeView.js","sourceRoot":"","sources":["../../../../../../src/features/tree-view/adapters/inbound/web-artifact/TreeView.tsx"],"names":[],"mappings":"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","sourcesContent":["import React from 'react';\n\nimport { toggleExpandedIds } from '../../../utils/toggleExpandedIds';\nimport { TreeItemRow } from './TreeItemRow';\n\nexport interface TreeItemNode<TId extends string = string> {\n readonly id: TId;\n readonly label: React.ReactNode;\n readonly icon?: React.ReactNode;\n readonly children?: readonly TreeItemNode<TId>[];\n readonly disabled?: boolean;\n readonly meta?: React.ReactNode;\n readonly actions?: React.ReactNode;\n}\n\nexport interface TreeItemRenderProps<TId extends string = string> {\n readonly node: TreeItemNode<TId>;\n readonly depth: number;\n readonly selected: boolean;\n readonly expanded: boolean;\n readonly hasChildren: boolean;\n}\n\nexport interface TreeViewProps<TId extends string = string> {\n readonly nodes: readonly TreeItemNode<TId>[];\n readonly selectedId?: TId;\n readonly expandedIds?: readonly TId[];\n readonly defaultExpandedIds?: readonly TId[];\n readonly onSelect?: (id: TId) => void;\n readonly onExpandedChange?: (ids: readonly TId[]) => void;\n readonly renderItem?: (props: TreeItemRenderProps<TId>) => React.ReactNode;\n readonly expansionIndicator?: 'chevron' | 'folder';\n readonly className?: string;\n readonly style?: React.CSSProperties;\n readonly ariaLabel?: string;\n}\n\n/***\n * Render the standalone browser TreeView artifact without React Native runtime dependencies.\n * expansionIndicator Use `chevron` beside custom node icons, or `folder` (default) for\n * built-in folder/file indicators. Expansion is leading and independent from row selection.\n */\nexport function TreeView<TId extends string = string>({\n nodes,\n selectedId,\n expandedIds: controlledExpandedIds,\n defaultExpandedIds,\n onSelect,\n onExpandedChange,\n renderItem,\n expansionIndicator = 'folder',\n className,\n style,\n ariaLabel = 'Tree',\n}: TreeViewProps<TId>) {\n const [internalExpandedIds, setInternalExpandedIds] = React.useState<readonly TId[]>(\n defaultExpandedIds ?? [],\n );\n const rootRef = React.useRef<HTMLDivElement | null>(null);\n const isControlled = controlledExpandedIds !== undefined;\n const expandedIds = isControlled ? controlledExpandedIds : internalExpandedIds;\n const handleToggleExpand = React.useCallback(\n (id: TId) => {\n const nextExpandedIds = toggleExpandedIds(expandedIds, id);\n if (!isControlled) setInternalExpandedIds(nextExpandedIds);\n onExpandedChange?.(nextExpandedIds);\n },\n [expandedIds, isControlled, onExpandedChange],\n );\n useSelectedTreeItemReveal(rootRef, selectedId, expandedIds);\n\n return (\n <div\n ref={rootRef}\n aria-label={ariaLabel}\n className={className}\n role=\"tree\"\n style={{ ...TREE_STYLE, ...style }}\n >\n {nodes.map((node) => (\n <TreeItemRow\n key={node.id}\n depth={0}\n expandedIds={expandedIds}\n expansionIndicator={expansionIndicator}\n node={node}\n onSelect={onSelect}\n onToggleExpand={handleToggleExpand}\n renderItem={renderItem}\n selectedId={selectedId}\n />\n ))}\n </div>\n );\n}\n\n/*** Reveal a controlled selection after it is rendered without changing expansion ownership. */\nfunction useSelectedTreeItemReveal<TId extends string>(\n rootRef: React.RefObject<HTMLDivElement | null>,\n selectedId: TId | undefined,\n expandedIds: readonly TId[],\n) {\n React.useEffect(() => {\n if (selectedId === undefined) return;\n const root = rootRef.current as unknown as TreeViewQueryableElement | null;\n root\n ?.querySelector('[role=\"treeitem\"][aria-selected=\"true\"]')\n ?.scrollIntoView({ block: 'nearest', inline: 'nearest' });\n }, [expandedIds, rootRef, selectedId]);\n}\n\nconst TREE_STYLE = {\n display: 'flex',\n flexDirection: 'column',\n minWidth: 0,\n} as const satisfies React.CSSProperties;\n\ninterface TreeViewQueryableElement {\n querySelector(selector: string): TreeViewRevealTarget | null;\n}\n\ninterface TreeViewRevealTarget {\n scrollIntoView(options: { readonly block: 'nearest'; readonly inline: 'nearest' }): void;\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ankhorage/zora",
3
3
  "type": "module",
4
- "version": "22.0.80",
4
+ "version": "22.0.82",
5
5
  "description": "Opinionated React Native and React Native Web UI kit built on @ankhorage/surface.",
6
6
  "homepage": "https://github.com/ankhorage/zora#readme",
7
7
  "bugs": {
@@ -124,7 +124,7 @@
124
124
  "version-packages": "ankhorage-changeset version"
125
125
  },
126
126
  "devDependencies": {
127
- "@ankhorage/devtools": "^2.1.2",
127
+ "@ankhorage/devtools": "^2.1.6",
128
128
  "@ankhorage/paradox": "^0.2.27",
129
129
  "@react-native-vector-icons/fontawesome": "^13.1.4",
130
130
  "@react-native-vector-icons/fontawesome5": "^13.1.4",
@@ -66,6 +66,46 @@ test('expands from the leading control without selecting the row, including keyb
66
66
  }
67
67
  });
68
68
 
69
+ test('reveals a controlled selection when expansion makes the row renderable', async () => {
70
+ const browserWindow = new Window();
71
+ const keys = ['window', 'document', 'Node', 'navigator', 'IS_REACT_ACT_ENVIRONMENT'] as const;
72
+ const previous = keys.map(
73
+ (key) => [key, Object.getOwnPropertyDescriptor(globalThis, key)] as const,
74
+ );
75
+ Object.assign(globalThis, {
76
+ IS_REACT_ACT_ENVIRONMENT: true,
77
+ window: browserWindow,
78
+ document: browserWindow.document,
79
+ Node: browserWindow.Node,
80
+ navigator: browserWindow.navigator,
81
+ });
82
+ const reveals: unknown[] = [];
83
+ browserWindow.HTMLElement.prototype.scrollIntoView = (options) => reveals.push(options);
84
+ const host = document.createElement('div');
85
+ document.body.appendChild(host);
86
+ const root = createRoot(host);
87
+ const interactAsync = (action: () => void) => act(() => Promise.resolve().then(action));
88
+ const nodes = [{ id: 'src', label: 'source', children: [{ id: 'leaf', label: 'leaf' }] }];
89
+ try {
90
+ await interactAsync(() =>
91
+ root.render(<TreeView expandedIds={[]} nodes={nodes} selectedId="leaf" />),
92
+ );
93
+ expect(reveals).toEqual([]);
94
+
95
+ await interactAsync(() =>
96
+ root.render(<TreeView expandedIds={['src']} nodes={nodes} selectedId="leaf" />),
97
+ );
98
+ expect(reveals).toEqual([{ block: 'nearest', inline: 'nearest' }]);
99
+ } finally {
100
+ await interactAsync(() => root.unmount());
101
+ browserWindow.close();
102
+ for (const [key, descriptor] of previous) {
103
+ if (descriptor) Object.defineProperty(globalThis, key, descriptor);
104
+ else Reflect.deleteProperty(globalThis, key);
105
+ }
106
+ }
107
+ });
108
+
69
109
  test('renders a standalone web tree without a ZORA or React Native provider', () => {
70
110
  const markup = renderToStaticMarkup(
71
111
  <TreeView
@@ -56,6 +56,7 @@ export function TreeView<TId extends string = string>({
56
56
  const [internalExpandedIds, setInternalExpandedIds] = React.useState<readonly TId[]>(
57
57
  defaultExpandedIds ?? [],
58
58
  );
59
+ const rootRef = React.useRef<HTMLDivElement | null>(null);
59
60
  const isControlled = controlledExpandedIds !== undefined;
60
61
  const expandedIds = isControlled ? controlledExpandedIds : internalExpandedIds;
61
62
  const handleToggleExpand = React.useCallback(
@@ -66,9 +67,11 @@ export function TreeView<TId extends string = string>({
66
67
  },
67
68
  [expandedIds, isControlled, onExpandedChange],
68
69
  );
70
+ useSelectedTreeItemReveal(rootRef, selectedId, expandedIds);
69
71
 
70
72
  return (
71
73
  <div
74
+ ref={rootRef}
72
75
  aria-label={ariaLabel}
73
76
  className={className}
74
77
  role="tree"
@@ -91,8 +94,31 @@ export function TreeView<TId extends string = string>({
91
94
  );
92
95
  }
93
96
 
97
+ /*** Reveal a controlled selection after it is rendered without changing expansion ownership. */
98
+ function useSelectedTreeItemReveal<TId extends string>(
99
+ rootRef: React.RefObject<HTMLDivElement | null>,
100
+ selectedId: TId | undefined,
101
+ expandedIds: readonly TId[],
102
+ ) {
103
+ React.useEffect(() => {
104
+ if (selectedId === undefined) return;
105
+ const root = rootRef.current as unknown as TreeViewQueryableElement | null;
106
+ root
107
+ ?.querySelector('[role="treeitem"][aria-selected="true"]')
108
+ ?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
109
+ }, [expandedIds, rootRef, selectedId]);
110
+ }
111
+
94
112
  const TREE_STYLE = {
95
113
  display: 'flex',
96
114
  flexDirection: 'column',
97
115
  minWidth: 0,
98
116
  } as const satisfies React.CSSProperties;
117
+
118
+ interface TreeViewQueryableElement {
119
+ querySelector(selector: string): TreeViewRevealTarget | null;
120
+ }
121
+
122
+ interface TreeViewRevealTarget {
123
+ scrollIntoView(options: { readonly block: 'nearest'; readonly inline: 'nearest' }): void;
124
+ }
@@ -1,5 +1,7 @@
1
1
  import React from 'react';
2
- import type { GraphViewProps } from '../../../../../types/graph-view';
2
+
3
+ import type { GraphViewProps } from './GraphView.types-0';
4
+
3
5
  /***
4
6
  * Render a browser graph while one runtime owns Cytoscape layout, viewport, and disposal.
5
7
  * zoomMode Use `fit-relative` to make controller zoom, events and min/max limits relative
@@ -14,4 +16,4 @@ import type { GraphViewProps } from '../../../../../types/graph-view';
14
16
  * cramped layouts but cannot repair non-uniform overlaps.
15
17
  */
16
18
  export declare function GraphView(props: GraphViewProps): React.JSX.Element;
17
- //# sourceMappingURL=GraphView.d.ts.map
19
+ export type { GraphViewCallbacks, GraphViewController, GraphViewEdge, GraphViewElementEvent, GraphViewElementEventType, GraphViewFitOptions, GraphViewLayoutName, GraphViewNode, GraphViewNodeRenderContext, GraphViewPoint, GraphViewProps, GraphViewRenderedNode, GraphViewSize, GraphViewStyleRule, GraphViewViewport } from './GraphView.types-0';
@@ -0,0 +1,98 @@
1
+ import type React from 'react';
2
+ export type GraphViewLayoutName = 'breadthfirst' | 'circle' | 'concentric' | 'elk' | 'grid';
3
+ export type GraphViewElementEventType = 'press' | 'double-press' | 'select' | 'unselect' | 'pointer-enter' | 'pointer-leave';
4
+ export interface GraphViewNode {
5
+ readonly id: string;
6
+ readonly label?: string;
7
+ readonly parentId?: string;
8
+ readonly classes?: string;
9
+ readonly data?: Readonly<Record<string, unknown>>;
10
+ }
11
+ export interface GraphViewEdge {
12
+ readonly id?: string;
13
+ readonly source: string;
14
+ readonly target: string;
15
+ readonly classes?: string;
16
+ readonly data?: Readonly<Record<string, unknown>>;
17
+ }
18
+ export interface GraphViewStyleRule {
19
+ readonly selector: string;
20
+ readonly style: Readonly<Record<string, unknown>>;
21
+ }
22
+ export interface GraphViewPoint {
23
+ readonly x: number;
24
+ readonly y: number;
25
+ }
26
+ export interface GraphViewSize {
27
+ readonly width: number;
28
+ readonly height: number;
29
+ }
30
+ export interface GraphViewNodeRenderContext {
31
+ readonly hovered: boolean;
32
+ readonly node: GraphViewNode;
33
+ readonly selected: boolean;
34
+ readonly zoom: number;
35
+ }
36
+ export interface GraphViewRenderedNode {
37
+ readonly hovered: boolean;
38
+ readonly id: string;
39
+ readonly position: GraphViewPoint;
40
+ readonly selected: boolean;
41
+ readonly zoom: number;
42
+ }
43
+ export interface GraphViewViewport {
44
+ readonly zoom: number;
45
+ readonly pan: GraphViewPoint;
46
+ }
47
+ export interface GraphViewFitOptions {
48
+ /** Optimize existing layout spacing without rerunning its algorithm; whole-graph fits only. */
49
+ readonly optimizeSpacing?: boolean;
50
+ readonly nodeIds?: readonly string[];
51
+ readonly padding?: number;
52
+ }
53
+ export interface GraphViewElementEvent {
54
+ readonly id: string;
55
+ readonly type: GraphViewElementEventType;
56
+ }
57
+ export interface GraphViewController {
58
+ fit(options?: GraphViewFitOptions): void;
59
+ getViewport(): GraphViewViewport;
60
+ getZoomRange(): {
61
+ readonly min: number;
62
+ readonly max: number;
63
+ };
64
+ setPan(pan: GraphViewPoint): void;
65
+ setZoom(zoom: number): void;
66
+ zoomBy(factor: number): void;
67
+ }
68
+ export interface GraphViewProps {
69
+ readonly nodes: readonly GraphViewNode[];
70
+ readonly edges: readonly GraphViewEdge[];
71
+ readonly layout?: GraphViewLayoutName;
72
+ readonly layoutOptions?: Readonly<Record<string, unknown>>;
73
+ readonly spacingFactor?: number;
74
+ readonly styleRules?: readonly GraphViewStyleRule[];
75
+ readonly selectedNodeIds?: readonly string[];
76
+ readonly renderNode?: (context: GraphViewNodeRenderContext) => React.ReactNode;
77
+ readonly getNodeSize?: (node: GraphViewNode) => GraphViewSize | undefined;
78
+ readonly fitPadding?: number;
79
+ readonly minZoom?: number;
80
+ readonly maxZoom?: number;
81
+ readonly minReadableLabelSize?: number;
82
+ readonly maxFitLabelSize?: number;
83
+ /** Zoom values and bounds use the full-node fit as 1 when fit-relative is selected. */
84
+ readonly zoomMode?: 'absolute' | 'fit-relative';
85
+ /** Measure plain labels before layout instead of estimating width from character count. */
86
+ readonly sizeNodesToLabels?: boolean;
87
+ readonly className?: string;
88
+ readonly style?: React.CSSProperties;
89
+ readonly ariaLabel?: string;
90
+ readonly onReady?: (controller: GraphViewController) => void;
91
+ readonly onLayoutComplete?: (controller: GraphViewController) => void;
92
+ readonly onNodeEvent?: (event: GraphViewElementEvent) => void;
93
+ readonly onEdgeEvent?: (event: GraphViewElementEvent) => void;
94
+ readonly onViewportChange?: (viewport: GraphViewViewport) => void;
95
+ readonly onSpacingFactorChange?: (spacingFactor: number) => void;
96
+ }
97
+ export type GraphViewCallbacks = Pick<GraphViewProps, 'onEdgeEvent' | 'onLayoutComplete' | 'onNodeEvent' | 'onReady' | 'onViewportChange' | 'onSpacingFactorChange'>;
98
+ //# sourceMappingURL=graph-view.d.ts.map
@@ -229,6 +229,7 @@ function TreeView({
229
229
  ariaLabel = "Tree"
230
230
  }) {
231
231
  const [internalExpandedIds, setInternalExpandedIds] = React2.useState(defaultExpandedIds ?? []);
232
+ const rootRef = React2.useRef(null);
232
233
  const isControlled = controlledExpandedIds !== undefined;
233
234
  const expandedIds = isControlled ? controlledExpandedIds : internalExpandedIds;
234
235
  const handleToggleExpand = React2.useCallback((id) => {
@@ -237,7 +238,9 @@ function TreeView({
237
238
  setInternalExpandedIds(nextExpandedIds);
238
239
  onExpandedChange?.(nextExpandedIds);
239
240
  }, [expandedIds, isControlled, onExpandedChange]);
241
+ useSelectedTreeItemReveal(rootRef, selectedId, expandedIds);
240
242
  return /* @__PURE__ */ jsx3("div", {
243
+ ref: rootRef,
241
244
  "aria-label": ariaLabel,
242
245
  className,
243
246
  role: "tree",
@@ -254,6 +257,14 @@ function TreeView({
254
257
  }, node.id))
255
258
  });
256
259
  }
260
+ function useSelectedTreeItemReveal(rootRef, selectedId, expandedIds) {
261
+ React2.useEffect(() => {
262
+ if (selectedId === undefined)
263
+ return;
264
+ const root = rootRef.current;
265
+ root?.querySelector('[role="treeitem"][aria-selected="true"]')?.scrollIntoView({ block: "nearest", inline: "nearest" });
266
+ }, [expandedIds, rootRef, selectedId]);
267
+ }
257
268
  var TREE_STYLE = {
258
269
  display: "flex",
259
270
  flexDirection: "column",
@@ -1475,6 +1475,7 @@
1475
1475
  "files": [
1476
1476
  "components/graph-view/GraphView.js",
1477
1477
  "components/graph-view/GraphView.d.ts",
1478
+ "components/graph-view/GraphView.types-0.d.ts",
1478
1479
  "components/graph-view/index.js",
1479
1480
  "components/graph-view/index.d.ts",
1480
1481
  "chunks/Accordion-59pa0677.js",