@dxos/react-ui-attention 0.8.4-main.e8ec1fe → 0.8.4-main.ef1bc66f44

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 (53) hide show
  1. package/dist/lib/browser/chunk-PAZBZOO2.mjs +7 -0
  2. package/dist/lib/browser/chunk-PAZBZOO2.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-W5HPVX5Q.mjs +705 -0
  4. package/dist/lib/browser/chunk-W5HPVX5Q.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +73 -6
  6. package/dist/lib/browser/index.mjs.map +4 -4
  7. package/dist/lib/browser/meta.json +1 -1
  8. package/dist/lib/browser/testing/index.mjs +19 -4
  9. package/dist/lib/browser/testing/index.mjs.map +3 -3
  10. package/dist/lib/browser/types.mjs +7 -0
  11. package/dist/lib/browser/types.mjs.map +7 -0
  12. package/dist/lib/node-esm/chunk-NFJ45DDP.mjs +706 -0
  13. package/dist/lib/node-esm/chunk-NFJ45DDP.mjs.map +7 -0
  14. package/dist/lib/node-esm/chunk-PMGNPL4M.mjs +9 -0
  15. package/dist/lib/node-esm/chunk-PMGNPL4M.mjs.map +7 -0
  16. package/dist/lib/node-esm/index.mjs +73 -6
  17. package/dist/lib/node-esm/index.mjs.map +4 -4
  18. package/dist/lib/node-esm/meta.json +1 -1
  19. package/dist/lib/node-esm/testing/index.mjs +19 -4
  20. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  21. package/dist/lib/node-esm/types.mjs +8 -0
  22. package/dist/lib/node-esm/types.mjs.map +7 -0
  23. package/dist/types/src/attention.d.ts +22 -17
  24. package/dist/types/src/attention.d.ts.map +1 -1
  25. package/dist/types/src/components/AttentionProvider.d.ts +3 -2
  26. package/dist/types/src/components/AttentionProvider.d.ts.map +1 -1
  27. package/dist/types/src/components/SelectionProvider.d.ts +5 -4
  28. package/dist/types/src/components/SelectionProvider.d.ts.map +1 -1
  29. package/dist/types/src/index.d.ts +1 -0
  30. package/dist/types/src/index.d.ts.map +1 -1
  31. package/dist/types/src/selection.d.ts +23 -3
  32. package/dist/types/src/selection.d.ts.map +1 -1
  33. package/dist/types/src/testing/decorators/withAttention.d.ts +5 -1
  34. package/dist/types/src/testing/decorators/withAttention.d.ts.map +1 -1
  35. package/dist/types/src/types.d.ts +10 -0
  36. package/dist/types/src/types.d.ts.map +1 -0
  37. package/dist/types/tsconfig.tsbuildinfo +1 -1
  38. package/package.json +35 -23
  39. package/src/attention.test.ts +12 -7
  40. package/src/attention.ts +64 -42
  41. package/src/components/AttentionGlyph.stories.tsx +1 -1
  42. package/src/components/AttentionGlyph.tsx +1 -1
  43. package/src/components/AttentionProvider.stories.tsx +1 -1
  44. package/src/components/AttentionProvider.tsx +64 -55
  45. package/src/components/SelectionProvider.tsx +62 -15
  46. package/src/index.ts +1 -0
  47. package/src/selection.ts +111 -71
  48. package/src/testing/decorators/withAttention.ts +20 -3
  49. package/src/types.ts +16 -0
  50. package/dist/lib/browser/chunk-MVIMDGR2.mjs +0 -662
  51. package/dist/lib/browser/chunk-MVIMDGR2.mjs.map +0 -7
  52. package/dist/lib/node-esm/chunk-NPW2V7JQ.mjs +0 -664
  53. package/dist/lib/node-esm/chunk-NPW2V7JQ.mjs.map +0 -7
package/src/index.ts CHANGED
@@ -5,3 +5,4 @@
5
5
  export * from './attention';
6
6
  export * from './components';
7
7
  export * from './selection';
8
+ export * from './types';
package/src/selection.ts CHANGED
@@ -2,12 +2,11 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { untracked } from '@preact/signals-core';
5
+ import { Atom, type Registry } from '@effect-atom/atom-react';
6
6
  import * as Match from 'effect/Match';
7
7
  import * as Schema from 'effect/Schema';
8
8
 
9
9
  import { invariant } from '@dxos/invariant';
10
- import { type Live, live } from '@dxos/live-object';
11
10
 
12
11
  // TODO(burdon): Reconcile with @dxos/graph.
13
12
 
@@ -70,117 +69,158 @@ export const getSelectionSet = (selectionManager: SelectionManager, contextId?:
70
69
  * Each context maintains its own selection mode and state.
71
70
  */
72
71
  export class SelectionManager {
73
- private readonly _state = live<{ selections: Record<string, Selection> }>({ selections: {} });
72
+ private readonly _state: Atom.Writable<{ selections: Record<string, Selection> }>;
73
+
74
+ constructor(
75
+ private readonly _registry: Registry.Registry,
76
+ initial: Record<string, Selection> = {},
77
+ ) {
78
+ this._state = Atom.make<{ selections: Record<string, Selection> }>({
79
+ selections: { ...initial },
80
+ });
81
+ }
74
82
 
75
- constructor(initial: Record<string, Selection> = {}) {
76
- if (Object.keys(initial).length > 0) {
77
- untracked(() => {
78
- this._state.selections = initial;
79
- });
80
- }
83
+ /**
84
+ * Get the state atom for reactive access in the graph system.
85
+ */
86
+ get state(): Atom.Atom<{ selections: Record<string, Selection> }> {
87
+ return this._state;
88
+ }
89
+
90
+ /**
91
+ * Gets the current state.
92
+ */
93
+ getState(): { selections: Record<string, Selection> } {
94
+ return this._registry.get(this._state);
95
+ }
96
+
97
+ /**
98
+ * Subscribe to changes in the selection state.
99
+ */
100
+ subscribe(cb: (state: { selections: Record<string, Selection> }) => void): () => void {
101
+ this._registry.get(this._state);
102
+ return this._registry.subscribe(this._state, () => {
103
+ cb(this._registry.get(this._state));
104
+ });
81
105
  }
82
106
 
83
107
  getSelectionContexts(): string[] {
84
- return Object.keys(this._state.selections);
108
+ return Object.keys(this._registry.get(this._state).selections);
85
109
  }
86
110
 
111
+ getSelection(contextId: string): Selection | undefined;
112
+ getSelection<T extends SelectionMode>(contextId: string, mode: T): Selection;
87
113
  getSelection<T extends SelectionMode | undefined>(
88
114
  contextId: string,
89
115
  mode: T = undefined as T,
90
- ): T extends undefined ? Live<Selection> | undefined : Live<Selection> {
91
- const selection = untracked(() => this._state.selections[contextId]);
116
+ ): Selection | undefined {
117
+ const state = this._registry.get(this._state);
118
+ const selection = state.selections[contextId];
92
119
  if (!mode || selection) {
93
120
  return selection;
94
121
  }
95
122
 
96
- return untracked(() => {
97
- this._state.selections[contextId] = defaultSelection(mode);
98
- return this._state.selections[contextId];
99
- });
123
+ // Create new selection for the context.
124
+ const newSelection = defaultSelection(mode);
125
+ this._registry.update(this._state, (state) => ({
126
+ selections: { ...state.selections, [contextId]: newSelection },
127
+ }));
128
+ return newSelection;
100
129
  }
101
130
 
102
131
  // TODO(burdon): Disambiguate with getSelection?
103
132
  getSelected<T extends SelectionMode>(contextId: string, mode: T = 'multi' as T): SelectionResult<T> {
104
133
  const selection = this.getSelection(contextId, mode);
105
134
  invariant(selection?.mode === mode, 'Selection mode mismatch');
135
+ // Cast required because TypeScript can't infer the relationship between T and the matched result.
106
136
  return Match.type<Selection>().pipe(
107
137
  Match.when({ mode: 'single' }, (s) => s.id),
108
138
  Match.when({ mode: 'multi' }, (s) => s.ids),
109
139
  Match.when({ mode: 'range' }, (s) => (s.from && s.to ? { from: s.from, to: s.to } : undefined)),
110
140
  Match.when({ mode: 'multi-range' }, (s) => s.ranges),
111
141
  Match.exhaustive,
112
- )(selection) as any;
142
+ )(selection) as SelectionResult<T>;
113
143
  }
114
144
 
115
145
  updateSingle(contextId: string, id: string): void {
116
- untracked(() => {
117
- const selection = this.getSelection(contextId, 'single');
118
- invariant(selection?.mode === 'single', 'Selection mode is not single');
119
- selection.id = id;
120
- });
146
+ const selection = this.getSelection(contextId, 'single');
147
+ invariant(selection?.mode === 'single', 'Selection mode is not single');
148
+ this._registry.update(this._state, (state) => ({
149
+ selections: {
150
+ ...state.selections,
151
+ [contextId]: { ...selection, id },
152
+ },
153
+ }));
121
154
  }
122
155
 
123
156
  updateMulti(contextId: string, ids: string[]) {
124
- untracked(() => {
125
- const selection = this.getSelection(contextId, 'multi');
126
- invariant(selection?.mode === 'multi', 'Selection mode is not multi');
127
- selection.ids.splice(0, selection.ids.length, ...ids);
128
- });
157
+ const selection = this.getSelection(contextId, 'multi');
158
+ invariant(selection?.mode === 'multi', 'Selection mode is not multi');
159
+ this._registry.update(this._state, (state) => ({
160
+ selections: {
161
+ ...state.selections,
162
+ [contextId]: { ...selection, ids: [...ids] },
163
+ },
164
+ }));
129
165
  }
130
166
 
131
167
  updateRange(contextId: string, from: string, to: string) {
132
- untracked(() => {
133
- const selection = this.getSelection(contextId, 'range');
134
- invariant(selection?.mode === 'range', 'Selection mode is not range');
135
- selection.from = from;
136
- selection.to = to;
137
- });
168
+ const selection = this.getSelection(contextId, 'range');
169
+ invariant(selection?.mode === 'range', 'Selection mode is not range');
170
+ this._registry.update(this._state, (state) => ({
171
+ selections: {
172
+ ...state.selections,
173
+ [contextId]: { ...selection, from, to },
174
+ },
175
+ }));
138
176
  }
139
177
 
140
178
  updateMultiRange(contextId: string, ranges: { from: string; to: string }[]) {
141
- untracked(() => {
142
- const selection = this.getSelection(contextId, 'multi-range');
143
- invariant(selection?.mode === 'multi-range', 'Selection mode is not multi-range');
144
- selection.ranges.splice(0, selection.ranges.length, ...ranges);
145
- });
179
+ const selection = this.getSelection(contextId, 'multi-range');
180
+ invariant(selection?.mode === 'multi-range', 'Selection mode is not multi-range');
181
+ this._registry.update(this._state, (state) => ({
182
+ selections: {
183
+ ...state.selections,
184
+ [contextId]: { ...selection, ranges: [...ranges] },
185
+ },
186
+ }));
146
187
  }
147
188
 
148
189
  clearSelection(contextId: string): void {
149
- untracked(() => {
150
- const selection = this.getSelection(contextId);
151
- Match.type<Selection | undefined>().pipe(
152
- Match.when(undefined, () => {
153
- // No-op.
154
- }),
155
- Match.when({ mode: 'single' }, (s) => {
156
- s.id = undefined;
157
- }),
158
- Match.when({ mode: 'multi' }, (s) => {
159
- s.ids.splice(0, s.ids.length);
160
- }),
161
- Match.when({ mode: 'range' }, (s) => {
162
- s.from = undefined;
163
- s.to = undefined;
164
- }),
165
- Match.when({ mode: 'multi-range' }, (s) => {
166
- s.ranges.splice(0, s.ranges.length);
167
- }),
168
- Match.exhaustive,
169
- )(selection);
170
- });
190
+ const selection = this.getSelection(contextId);
191
+ if (!selection) {
192
+ return;
193
+ }
194
+
195
+ const clearedSelection = Match.type<Selection>().pipe(
196
+ Match.when({ mode: 'single' }, (s) => ({ ...s, id: undefined })),
197
+ Match.when({ mode: 'multi' }, (s) => ({ ...s, ids: [] })),
198
+ Match.when({ mode: 'range' }, (s) => ({ ...s, from: undefined, to: undefined })),
199
+ Match.when({ mode: 'multi-range' }, (s) => ({ ...s, ranges: [] })),
200
+ Match.exhaustive,
201
+ )(selection);
202
+
203
+ this._registry.update(this._state, (state) => ({
204
+ selections: {
205
+ ...state.selections,
206
+ [contextId]: clearedSelection,
207
+ },
208
+ }));
171
209
  }
172
210
 
173
211
  toggleSelection(contextId: string, id: string): void {
174
- untracked(() => {
175
- const selection = this.getSelection(contextId, 'multi');
176
- invariant(selection?.mode === 'multi', 'Selection mode is not multi');
177
-
178
- if (selection.ids.includes(id)) {
179
- const index = selection.ids.indexOf(id);
180
- selection.ids.splice(index, 1);
181
- } else {
182
- selection.ids.push(id);
183
- }
184
- });
212
+ const selection = this.getSelection(contextId, 'multi');
213
+ invariant(selection?.mode === 'multi', 'Selection mode is not multi');
214
+
215
+ const newIds = selection.ids.includes(id)
216
+ ? selection.ids.filter((existingId) => existingId !== id)
217
+ : [...selection.ids, id];
218
+
219
+ this._registry.update(this._state, (state) => ({
220
+ selections: {
221
+ ...state.selections,
222
+ [contextId]: { ...selection, ids: newIds },
223
+ },
224
+ }));
185
225
  }
186
226
  }
@@ -2,11 +2,28 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { Registry, RegistryContext } from '@effect-atom/atom-react';
5
6
  import { type Decorator } from '@storybook/react';
6
- import { createElement } from 'react';
7
+ import { createElement, useMemo } from 'react';
7
8
 
9
+ import { AttentionManager } from '../../attention';
8
10
  import { RootAttentionProvider, SelectionProvider } from '../../components';
9
11
 
10
- export const withAttention: Decorator = (Story) => {
11
- return createElement(RootAttentionProvider, {}, createElement(SelectionProvider, {}, createElement(Story)));
12
+ /**
13
+ * Storybook decorator that provides attention context.
14
+ * @param initialAttendedId Optional ID to set as initially attended.
15
+ */
16
+ export const withAttention = (initialAttendedId?: string): Decorator => {
17
+ return (Story) => {
18
+ const registry = useMemo(() => Registry.make(), []);
19
+ const attention = useMemo(
20
+ () => (initialAttendedId ? new AttentionManager(registry, [initialAttendedId]) : undefined),
21
+ [registry],
22
+ );
23
+ return createElement(
24
+ RegistryContext.Provider,
25
+ { value: registry },
26
+ createElement(RootAttentionProvider, { attention }, createElement(SelectionProvider, {}, createElement(Story))),
27
+ );
28
+ };
12
29
  };
package/src/types.ts ADDED
@@ -0,0 +1,16 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ // NOTE: Chosen from RFC 1738's `safe` characters: http://www.faqs.org/rfcs/rfc1738.html
6
+ export const ATTENDABLE_PATH_SEPARATOR = '~';
7
+
8
+ export type Attention = {
9
+ hasAttention: boolean;
10
+ isAncestor: boolean;
11
+ isRelated: boolean;
12
+ };
13
+
14
+ export type CurrentState = {
15
+ current: string[];
16
+ };