@gtkx/components 1.0.0-rc.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +8 -9
  2. package/dist/adw/combo-row.d.ts +1 -0
  3. package/dist/adw/combo-row.d.ts.map +1 -1
  4. package/dist/adw/combo-row.js.map +1 -1
  5. package/dist/adw/types.d.ts +2 -1
  6. package/dist/adw/types.d.ts.map +1 -1
  7. package/dist/adw/types.js.map +1 -1
  8. package/dist/column-view.d.ts +2 -2
  9. package/dist/column-view.d.ts.map +1 -1
  10. package/dist/column-view.js +15 -13
  11. package/dist/column-view.js.map +1 -1
  12. package/dist/drop-down.d.ts +1 -0
  13. package/dist/drop-down.d.ts.map +1 -1
  14. package/dist/drop-down.js.map +1 -1
  15. package/dist/grid-view.js +2 -2
  16. package/dist/grid-view.js.map +1 -1
  17. package/dist/index.d.ts +1 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +0 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/internal/cells.d.ts +32 -24
  22. package/dist/internal/cells.d.ts.map +1 -1
  23. package/dist/internal/cells.js +168 -77
  24. package/dist/internal/cells.js.map +1 -1
  25. package/dist/internal/collection-index.d.ts +9 -13
  26. package/dist/internal/collection-index.d.ts.map +1 -1
  27. package/dist/internal/collection-index.js +33 -70
  28. package/dist/internal/collection-index.js.map +1 -1
  29. package/dist/internal/collection-model.d.ts +23 -2
  30. package/dist/internal/collection-model.d.ts.map +1 -1
  31. package/dist/internal/collection-model.js +210 -98
  32. package/dist/internal/collection-model.js.map +1 -1
  33. package/dist/internal/collection.d.ts +9 -12
  34. package/dist/internal/collection.d.ts.map +1 -1
  35. package/dist/internal/collection.js +38 -49
  36. package/dist/internal/collection.js.map +1 -1
  37. package/dist/internal/controlled-sync.d.ts +11 -0
  38. package/dist/internal/controlled-sync.d.ts.map +1 -0
  39. package/dist/internal/controlled-sync.js +19 -0
  40. package/dist/internal/controlled-sync.js.map +1 -0
  41. package/dist/internal/drop-down.d.ts.map +1 -1
  42. package/dist/internal/drop-down.js +27 -15
  43. package/dist/internal/drop-down.js.map +1 -1
  44. package/dist/internal/expansion.d.ts +3 -2
  45. package/dist/internal/expansion.d.ts.map +1 -1
  46. package/dist/internal/expansion.js +80 -47
  47. package/dist/internal/expansion.js.map +1 -1
  48. package/dist/internal/keys.d.ts +5 -0
  49. package/dist/internal/keys.d.ts.map +1 -0
  50. package/dist/internal/keys.js +13 -0
  51. package/dist/internal/keys.js.map +1 -0
  52. package/dist/internal/selection.d.ts +3 -2
  53. package/dist/internal/selection.d.ts.map +1 -1
  54. package/dist/internal/selection.js +68 -35
  55. package/dist/internal/selection.js.map +1 -1
  56. package/dist/internal/slots.d.ts +10 -0
  57. package/dist/internal/slots.d.ts.map +1 -0
  58. package/dist/internal/slots.js +43 -0
  59. package/dist/internal/slots.js.map +1 -0
  60. package/dist/internal/tree-expansion.d.ts +20 -0
  61. package/dist/internal/tree-expansion.d.ts.map +1 -0
  62. package/dist/internal/tree-expansion.js +68 -0
  63. package/dist/internal/tree-expansion.js.map +1 -0
  64. package/dist/internal/tree-order.d.ts +27 -0
  65. package/dist/internal/tree-order.d.ts.map +1 -0
  66. package/dist/internal/tree-order.js +71 -0
  67. package/dist/internal/tree-order.js.map +1 -0
  68. package/dist/internal/use-collection.d.ts +5 -5
  69. package/dist/internal/use-collection.d.ts.map +1 -1
  70. package/dist/internal/use-collection.js +6 -6
  71. package/dist/internal/use-collection.js.map +1 -1
  72. package/dist/list-view.d.ts.map +1 -1
  73. package/dist/list-view.js +6 -6
  74. package/dist/list-view.js.map +1 -1
  75. package/dist/types.d.ts +127 -54
  76. package/dist/types.d.ts.map +1 -1
  77. package/dist/types.js.map +1 -1
  78. package/package.json +7 -7
  79. package/src/adw/combo-row.tsx +1 -0
  80. package/src/adw/types.ts +2 -1
  81. package/src/column-view.tsx +38 -35
  82. package/src/drop-down.tsx +1 -0
  83. package/src/grid-view.tsx +2 -2
  84. package/src/index.ts +10 -11
  85. package/src/internal/cells.tsx +345 -123
  86. package/src/internal/collection-index.ts +51 -99
  87. package/src/internal/collection-model.ts +259 -110
  88. package/src/internal/collection.ts +46 -68
  89. package/src/internal/controlled-sync.ts +34 -0
  90. package/src/internal/drop-down.tsx +44 -23
  91. package/src/internal/expansion.ts +104 -67
  92. package/src/internal/keys.ts +18 -0
  93. package/src/internal/selection.tsx +100 -51
  94. package/src/internal/slots.ts +67 -0
  95. package/src/internal/tree-expansion.ts +110 -0
  96. package/src/internal/tree-order.ts +138 -0
  97. package/src/internal/use-collection.tsx +11 -11
  98. package/src/list-view.tsx +14 -10
  99. package/src/types.ts +140 -65
  100. package/dist/size-group.d.ts +0 -12
  101. package/dist/size-group.d.ts.map +0 -1
  102. package/dist/size-group.js +0 -41
  103. package/dist/size-group.js.map +0 -1
  104. package/src/size-group.tsx +0 -77
@@ -0,0 +1,67 @@
1
+ import { decodePartAt, encodePart } from "./keys.js";
2
+
3
+ type SlotKey = {
4
+ levelPath: string;
5
+ slot: number;
6
+ };
7
+
8
+ type SlotMap = Map<string, Set<number>>;
9
+
10
+ function getLevelBoundary(path: string): number {
11
+ let offset = 0;
12
+ let boundary = 0;
13
+
14
+ while (offset < path.length) {
15
+ const part = decodePartAt(path, offset);
16
+
17
+ if (part === null) {
18
+ return boundary;
19
+ }
20
+
21
+ boundary = offset;
22
+ offset += encodePart(part).length;
23
+ }
24
+
25
+ return boundary;
26
+ }
27
+
28
+ function getSlotKey(path: string): SlotKey | null {
29
+ const boundary = getLevelBoundary(path);
30
+ const part = decodePartAt(path, boundary);
31
+
32
+ if (part === null) {
33
+ return null;
34
+ }
35
+
36
+ return { levelPath: path.slice(0, boundary), slot: Number(part) };
37
+ }
38
+
39
+ function trackPath(slots: SlotMap, path: string): void {
40
+ const key = getSlotKey(path);
41
+
42
+ if (key === null) {
43
+ return;
44
+ }
45
+
46
+ const level = slots.get(key.levelPath);
47
+
48
+ if (level === undefined) {
49
+ slots.set(key.levelPath, new Set([key.slot]));
50
+
51
+ return;
52
+ }
53
+
54
+ level.add(key.slot);
55
+ }
56
+
57
+ function trackPaths(paths: Iterable<string>): SlotMap {
58
+ const slots: SlotMap = new Map();
59
+
60
+ for (const path of paths) {
61
+ trackPath(slots, path);
62
+ }
63
+
64
+ return slots;
65
+ }
66
+
67
+ export { getSlotKey, trackPath, trackPaths, type SlotMap };
@@ -0,0 +1,110 @@
1
+ import type { CollectionIndex } from "./collection-index.js";
2
+ import type { SlotMap } from "./slots.js";
3
+ import type { VisibleOrder } from "./tree-order.js";
4
+ import { encodePart } from "./keys.js";
5
+ import { getSlotKey, trackPath, trackPaths } from "./slots.js";
6
+ import { buildVisibleOrder } from "./tree-order.js";
7
+
8
+ type TreeExpansion = {
9
+ index: CollectionIndex;
10
+ expanded: Set<string>;
11
+ slots: SlotMap;
12
+ order: VisibleOrder | null;
13
+ isApplying: boolean;
14
+ isSyncing: boolean;
15
+ };
16
+
17
+ function dropSubtree(expansion: TreeExpansion, path: string): void {
18
+ expansion.expanded.delete(path);
19
+ const level = expansion.slots.get(path);
20
+
21
+ if (level === undefined) {
22
+ return;
23
+ }
24
+
25
+ expansion.slots.delete(path);
26
+
27
+ for (const slot of level) {
28
+ dropSubtree(expansion, path + encodePart(String(slot)));
29
+ }
30
+ }
31
+
32
+ function createTreeExpansion(index: CollectionIndex): TreeExpansion {
33
+ return { index, expanded: new Set(), slots: new Map(), order: null, isApplying: false, isSyncing: false };
34
+ }
35
+
36
+ function orderFor(expansion: TreeExpansion): VisibleOrder {
37
+ expansion.order ??= buildVisibleOrder(expansion.index, expansion.slots);
38
+
39
+ return expansion.order;
40
+ }
41
+
42
+ function adoptOrder(expansion: TreeExpansion, order: VisibleOrder): void {
43
+ expansion.expanded = new Set(order.expandedPaths);
44
+ expansion.slots = trackPaths(order.expandedPaths);
45
+ expansion.order = order;
46
+ }
47
+
48
+ function prunePath(expansion: TreeExpansion, path: string): void {
49
+ const key = getSlotKey(path);
50
+
51
+ if (key !== null) {
52
+ expansion.slots.get(key.levelPath)?.delete(key.slot);
53
+ }
54
+
55
+ dropSubtree(expansion, path);
56
+ expansion.order = null;
57
+ }
58
+
59
+ function pruneSlots(expansion: TreeExpansion, levelPath: string, isPruned: (slot: number) => boolean): void {
60
+ const level = expansion.slots.get(levelPath);
61
+
62
+ if (level === undefined) {
63
+ return;
64
+ }
65
+
66
+ for (const slot of level) {
67
+ if (!isPruned(slot)) {
68
+ continue;
69
+ }
70
+
71
+ level.delete(slot);
72
+ dropSubtree(expansion, levelPath + encodePart(String(slot)));
73
+ }
74
+
75
+ expansion.order = null;
76
+ }
77
+
78
+ function markExpanded(expansion: TreeExpansion, path: string, isExpanded: boolean): void {
79
+ if (isExpanded) {
80
+ expansion.expanded.add(path);
81
+ trackPath(expansion.slots, path);
82
+ expansion.order = null;
83
+
84
+ return;
85
+ }
86
+
87
+ prunePath(expansion, path);
88
+ }
89
+
90
+ function resetExpansion(expansion: TreeExpansion): void {
91
+ expansion.expanded = new Set();
92
+ expansion.slots = new Map();
93
+ expansion.order = null;
94
+ }
95
+
96
+ function adoptIndex(expansion: TreeExpansion, index: CollectionIndex): void {
97
+ expansion.index = index;
98
+ expansion.order = null;
99
+ }
100
+
101
+ export {
102
+ adoptIndex,
103
+ adoptOrder,
104
+ createTreeExpansion,
105
+ markExpanded,
106
+ orderFor,
107
+ pruneSlots,
108
+ resetExpansion,
109
+ type TreeExpansion,
110
+ };
@@ -0,0 +1,138 @@
1
+ import type { ListItem } from "../types.js";
2
+ import type { CollectionIndex, Level } from "./collection-index.js";
3
+ import type { SlotMap } from "./slots.js";
4
+ import { encodePart } from "./keys.js";
5
+
6
+ type VisibleRow = {
7
+ level: Level;
8
+ slot: number;
9
+ item: ListItem;
10
+ position: number;
11
+ isOpen: boolean;
12
+ isMarked: boolean;
13
+ };
14
+
15
+ type VisibleOrder = {
16
+ expandedPaths: string[];
17
+ expandedIds: string[];
18
+ };
19
+
20
+ type RowVisitor = (row: VisibleRow) => void;
21
+
22
+ type WalkOptions = {
23
+ index: CollectionIndex;
24
+ slots: SlotMap;
25
+ marks?: SlotMap | undefined;
26
+ visit?: RowVisitor | undefined;
27
+ };
28
+
29
+ type WalkFrame = {
30
+ level: Level;
31
+ cursor: number;
32
+ open: Set<number> | undefined;
33
+ marked: Set<number> | undefined;
34
+ };
35
+
36
+ type WalkState = {
37
+ options: WalkOptions;
38
+ stack: WalkFrame[];
39
+ row: VisibleRow;
40
+ order: VisibleOrder;
41
+ };
42
+
43
+ const NO_LEVEL: Level = { path: "", items: [], expandableFlags: [] };
44
+ const NO_ITEM: ListItem = { id: "", value: undefined };
45
+
46
+ const getRowPath = (row: VisibleRow): string => row.level.path + encodePart(String(row.slot));
47
+
48
+ function frameFor(options: WalkOptions, level: Level): WalkFrame {
49
+ return { level, cursor: 0, open: options.slots.get(level.path), marked: options.marks?.get(level.path) };
50
+ }
51
+
52
+ function descend(state: WalkState, path: string): void {
53
+ const child = state.options.index.children.get(path);
54
+
55
+ if (child === undefined || child.items.length === 0) {
56
+ return;
57
+ }
58
+
59
+ state.order.expandedPaths.push(path);
60
+ state.order.expandedIds.push(state.row.item.id);
61
+ state.stack.push(frameFor(state.options, child));
62
+ }
63
+
64
+ function visitSlot(state: WalkState, frame: WalkFrame, slot: number): void {
65
+ const item = frame.level.items[slot];
66
+
67
+ if (item === undefined) {
68
+ return;
69
+ }
70
+
71
+ const { row } = state;
72
+ row.level = frame.level;
73
+ row.slot = slot;
74
+ row.item = item;
75
+ row.isOpen = frame.open?.has(slot) ?? false;
76
+ row.isMarked = frame.marked?.has(slot) ?? false;
77
+ state.options.visit?.(row);
78
+ row.position += 1;
79
+
80
+ if (row.isOpen) {
81
+ descend(state, getRowPath(row));
82
+ }
83
+ }
84
+
85
+ function advanceFrame(state: WalkState): void {
86
+ const frame = state.stack.at(-1);
87
+
88
+ if (frame === undefined || frame.cursor >= frame.level.items.length) {
89
+ state.stack.pop();
90
+
91
+ return;
92
+ }
93
+
94
+ const slot = frame.cursor;
95
+ frame.cursor += 1;
96
+ visitSlot(state, frame, slot);
97
+ }
98
+
99
+ function walkVisible(options: WalkOptions): VisibleOrder {
100
+ const order: VisibleOrder = { expandedPaths: [], expandedIds: [] };
101
+ const row: VisibleRow = { level: NO_LEVEL, slot: 0, item: NO_ITEM, position: 0, isOpen: false, isMarked: false };
102
+ const frames = options.index.groups.map((level) => frameFor(options, level));
103
+ const state: WalkState = { options, stack: frames.toReversed(), row, order };
104
+
105
+ while (state.stack.length > 0) {
106
+ advanceFrame(state);
107
+ }
108
+
109
+ return order;
110
+ }
111
+
112
+ function buildVisibleOrder(index: CollectionIndex, slots: SlotMap): VisibleOrder {
113
+ return walkVisible({ index, slots });
114
+ }
115
+
116
+ function findPositions(index: CollectionIndex, slots: SlotMap, ids: Set<string>): number[] {
117
+ const found: number[] = [];
118
+
119
+ walkVisible({
120
+ index,
121
+ slots,
122
+ visit: (row) => {
123
+ if (ids.has(row.item.id)) {
124
+ found.push(row.position);
125
+ }
126
+ },
127
+ });
128
+
129
+ return found;
130
+ }
131
+
132
+ export {
133
+ buildVisibleOrder,
134
+ findPositions,
135
+ walkVisible,
136
+ type VisibleOrder,
137
+ type WalkOptions,
138
+ };
@@ -1,6 +1,6 @@
1
1
  import type { ReactElement } from "react";
2
2
  import { useLayoutEffect, useMemo, useState } from "react";
3
- import type { ExpansionProps, Item, Section, SelectionProps } from "../types.js";
3
+ import type { ExpansionProps, ListItem, ListSection, SelectionProps } from "../types.js";
4
4
  import type { Collection } from "./collection.js";
5
5
  import { createCollectionIndex } from "./collection-index.js";
6
6
  import { createCollectionModel } from "./collection-model.js";
@@ -9,9 +9,9 @@ import { useExpansion } from "./expansion.js";
9
9
  import { useSelection } from "./selection.js";
10
10
 
11
11
  type CollectionDataOptions = {
12
- items?: Item[] | undefined;
13
- sections?: Section[] | undefined;
14
- flat?: boolean | undefined;
12
+ items?: ListItem[] | undefined;
13
+ sections?: ListSection[] | undefined;
14
+ isFlat?: boolean | undefined;
15
15
  };
16
16
 
17
17
  type CollectionOptions = CollectionDataOptions & SelectionProps & ExpansionProps;
@@ -22,14 +22,14 @@ type CollectionResult = {
22
22
  };
23
23
 
24
24
  function useCollectionData(options: CollectionDataOptions): Collection {
25
- const { items, sections, flat } = options;
26
- const [gtk] = useState(createCollectionModel);
27
- const index = useMemo(() => createCollectionIndex(items, sections, flat === true), [items, sections, flat]);
28
- const collection = useMemo(() => createCollection(gtk, index), [gtk, index]);
25
+ const { items, sections, isFlat } = options;
26
+ const [collectionModel] = useState(createCollectionModel);
27
+ const index = useMemo(() => createCollectionIndex(items, sections, isFlat === true), [items, sections, isFlat]);
28
+ const collection = useMemo(() => createCollection(collectionModel, index), [collectionModel, index]);
29
29
 
30
30
  useLayoutEffect(() => {
31
- gtk.sync(index);
32
- }, [gtk, index]);
31
+ collectionModel.sync(index);
32
+ }, [collectionModel, index]);
33
33
 
34
34
  return collection;
35
35
  }
@@ -54,4 +54,4 @@ function useCollection(options: CollectionOptions): CollectionResult {
54
54
  return { collection, selection };
55
55
  }
56
56
 
57
- export { useCollection, useCollectionData, type CollectionDataOptions, type CollectionOptions };
57
+ export { useCollection, useCollectionData };
package/src/list-view.tsx CHANGED
@@ -2,7 +2,7 @@ import type { ReactNode } from "react";
2
2
  import { GtkListView, GtkSignalListItemFactory } from "@gtkx/jsx/gtk";
3
3
  import { omit } from "@gtkx/utils";
4
4
  import type { ListViewProps } from "./types.js";
5
- import { HeaderPortals, ItemPortals, useHeaderCells, useItemCells } from "./internal/cells.js";
5
+ import { ItemPortals, useItemCells, useSectionHeader } from "./internal/cells.js";
6
6
  import { useCollection } from "./internal/use-collection.js";
7
7
 
8
8
  const LIST_VIEW_PROPS = [
@@ -15,6 +15,7 @@ const LIST_VIEW_PROPS = [
15
15
  "selectionMode",
16
16
  "expandedIds",
17
17
  "onExpandedChange",
18
+ "expanderDescriptions",
18
19
  "estimatedItemHeight",
19
20
  "estimatedItemWidth",
20
21
  ] as const satisfies (keyof ListViewProps)[];
@@ -24,27 +25,30 @@ const LIST_VIEW_PROPS = [
24
25
  * controlled selection, controlled tree expansion, and estimated item sizing.
25
26
  */
26
27
  function ListView<T = unknown, S = unknown>(props: ListViewProps<T, S>): ReactNode {
27
- const { renderItem, renderHeader, expandedIds, estimatedItemHeight, estimatedItemWidth } = props;
28
+ const { renderItem, renderHeader, expandedIds, expanderDescriptions } = props;
29
+ const { estimatedItemHeight, estimatedItemWidth } = props;
28
30
  const rest = omit(props, LIST_VIEW_PROPS);
29
31
  const size = { width: estimatedItemWidth ?? -1, height: estimatedItemHeight ?? -1 };
30
32
  const { collection, selection } = useCollection(props);
31
33
  const itemCells = useItemCells(size);
32
- const headerCells = useHeaderCells(size);
34
+ const header = useSectionHeader(renderHeader, collection, size);
33
35
 
34
36
  return (
35
37
  <>
36
38
  <GtkListView
37
39
  model={selection}
38
40
  factory={<GtkSignalListItemFactory {...itemCells.handlers} />}
39
- {...(renderHeader != null && {
40
- headerFactory: <GtkSignalListItemFactory {...headerCells.handlers} />,
41
- })}
41
+ {...header.factoryProps}
42
42
  {...rest}
43
43
  />
44
- <ItemPortals store={itemCells} render={renderItem} collection={collection} expandedIds={expandedIds} />
45
- {renderHeader != null && (
46
- <HeaderPortals store={headerCells} render={renderHeader} collection={collection} />
47
- )}
44
+ <ItemPortals
45
+ registry={itemCells}
46
+ render={renderItem}
47
+ collection={collection}
48
+ expandedIds={expandedIds}
49
+ expanderDescriptions={expanderDescriptions}
50
+ />
51
+ {header.portals}
48
52
  </>
49
53
  );
50
54
  }