@dxos/react-ui-attention 0.8.4-main.84f28bd → 0.8.4-main.8baae0fced

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 (76) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/browser/chunk-D2BLVJ6Z.mjs +421 -0
  3. package/dist/lib/browser/chunk-D2BLVJ6Z.mjs.map +7 -0
  4. package/dist/lib/browser/chunk-I4VO2RQM.mjs +255 -0
  5. package/dist/lib/browser/chunk-I4VO2RQM.mjs.map +7 -0
  6. package/dist/lib/browser/index.mjs +90 -22
  7. package/dist/lib/browser/index.mjs.map +4 -4
  8. package/dist/lib/browser/meta.json +1 -1
  9. package/dist/lib/browser/testing/index.mjs +22 -7
  10. package/dist/lib/browser/testing/index.mjs.map +3 -3
  11. package/dist/lib/browser/types/index.mjs +29 -0
  12. package/dist/lib/browser/types/index.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-6INLBC62.mjs +423 -0
  14. package/dist/lib/node-esm/chunk-6INLBC62.mjs.map +7 -0
  15. package/dist/lib/node-esm/chunk-ZTUVSH4P.mjs +256 -0
  16. package/dist/lib/node-esm/chunk-ZTUVSH4P.mjs.map +7 -0
  17. package/dist/lib/node-esm/index.mjs +90 -22
  18. package/dist/lib/node-esm/index.mjs.map +4 -4
  19. package/dist/lib/node-esm/meta.json +1 -1
  20. package/dist/lib/node-esm/testing/index.mjs +22 -7
  21. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  22. package/dist/lib/node-esm/types/index.mjs +30 -0
  23. package/dist/lib/node-esm/types/index.mjs.map +7 -0
  24. package/dist/types/src/attention.d.ts +60 -20
  25. package/dist/types/src/attention.d.ts.map +1 -1
  26. package/dist/types/src/components/AttentionGlyph/AttentionGlyph.d.ts.map +1 -0
  27. package/dist/types/src/components/AttentionGlyph/AttentionGlyph.stories.d.ts +16 -0
  28. package/dist/types/src/components/AttentionGlyph/AttentionGlyph.stories.d.ts.map +1 -0
  29. package/dist/types/src/components/AttentionGlyph/index.d.ts +2 -0
  30. package/dist/types/src/components/AttentionGlyph/index.d.ts.map +1 -0
  31. package/dist/types/src/components/AttentionProvider/AttentionProvider.d.ts +34 -0
  32. package/dist/types/src/components/AttentionProvider/AttentionProvider.d.ts.map +1 -0
  33. package/dist/types/src/components/AttentionProvider/AttentionProvider.stories.d.ts +12 -0
  34. package/dist/types/src/components/AttentionProvider/AttentionProvider.stories.d.ts.map +1 -0
  35. package/dist/types/src/components/AttentionProvider/index.d.ts +2 -0
  36. package/dist/types/src/components/AttentionProvider/index.d.ts.map +1 -0
  37. package/dist/types/src/components/{SelectionProvider.d.ts → SelectionProvider/SelectionProvider.d.ts} +7 -6
  38. package/dist/types/src/components/SelectionProvider/SelectionProvider.d.ts.map +1 -0
  39. package/dist/types/src/components/SelectionProvider/index.d.ts +2 -0
  40. package/dist/types/src/components/SelectionProvider/index.d.ts.map +1 -0
  41. package/dist/types/src/selection.d.ts +49 -17
  42. package/dist/types/src/selection.d.ts.map +1 -1
  43. package/dist/types/src/testing/decorators/withAttention.d.ts +5 -1
  44. package/dist/types/src/testing/decorators/withAttention.d.ts.map +1 -1
  45. package/dist/types/src/types/index.d.ts +3 -0
  46. package/dist/types/src/types/index.d.ts.map +1 -0
  47. package/dist/types/tsconfig.tsbuildinfo +1 -1
  48. package/package.json +36 -32
  49. package/src/attention.test.ts +188 -27
  50. package/src/attention.ts +179 -74
  51. package/src/components/{AttentionGlyph.stories.tsx → AttentionGlyph/AttentionGlyph.stories.tsx} +17 -14
  52. package/src/components/{AttentionGlyph.tsx → AttentionGlyph/AttentionGlyph.tsx} +14 -7
  53. package/src/components/AttentionGlyph/index.ts +5 -0
  54. package/src/components/{AttentionProvider.stories.tsx → AttentionProvider/AttentionProvider.stories.tsx} +12 -9
  55. package/src/components/AttentionProvider/AttentionProvider.tsx +176 -0
  56. package/src/components/AttentionProvider/index.ts +5 -0
  57. package/src/components/SelectionProvider/SelectionProvider.tsx +174 -0
  58. package/src/components/SelectionProvider/index.ts +5 -0
  59. package/src/selection.ts +141 -69
  60. package/src/testing/decorators/withAttention.ts +22 -4
  61. package/src/types/index.ts +9 -0
  62. package/dist/lib/browser/chunk-QPEFFJ4E.mjs +0 -616
  63. package/dist/lib/browser/chunk-QPEFFJ4E.mjs.map +0 -7
  64. package/dist/lib/node-esm/chunk-F2JRKDYV.mjs +0 -618
  65. package/dist/lib/node-esm/chunk-F2JRKDYV.mjs.map +0 -7
  66. package/dist/types/src/components/AttentionGlyph.d.ts.map +0 -1
  67. package/dist/types/src/components/AttentionGlyph.stories.d.ts +0 -27
  68. package/dist/types/src/components/AttentionGlyph.stories.d.ts.map +0 -1
  69. package/dist/types/src/components/AttentionProvider.d.ts +0 -38
  70. package/dist/types/src/components/AttentionProvider.d.ts.map +0 -1
  71. package/dist/types/src/components/AttentionProvider.stories.d.ts +0 -10
  72. package/dist/types/src/components/AttentionProvider.stories.d.ts.map +0 -1
  73. package/dist/types/src/components/SelectionProvider.d.ts.map +0 -1
  74. package/src/components/AttentionProvider.tsx +0 -198
  75. package/src/components/SelectionProvider.tsx +0 -120
  76. /package/dist/types/src/components/{AttentionGlyph.d.ts → AttentionGlyph/AttentionGlyph.d.ts} +0 -0
package/src/selection.ts CHANGED
@@ -2,27 +2,42 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { untracked } from '@preact/signals-core';
6
- import { Match } from 'effect';
5
+ import { Atom, type Registry } from '@effect-atom/atom-react';
6
+ import * as Match from 'effect/Match';
7
+ import * as Schema from 'effect/Schema';
7
8
 
8
9
  import { invariant } from '@dxos/invariant';
9
- import { live, type Live } from '@dxos/live-object';
10
-
11
- // TODO(burdon): Reconcile with @dxos/graph.
12
10
 
13
11
  export type SelectionMode = 'single' | 'multi' | 'range' | 'multi-range';
14
12
 
15
- export type Selection =
16
- | { mode: 'single'; id?: string }
17
- | { mode: 'multi'; ids: string[] }
18
- | { mode: 'range'; from?: string; to?: string }
19
- | { mode: 'multi-range'; ranges: { from: string; to: string }[] };
13
+ export const SelectionSchema = Schema.Union(
14
+ Schema.Struct({
15
+ mode: Schema.Literal('single'),
16
+ id: Schema.optional(Schema.String),
17
+ }).pipe(Schema.mutable),
18
+ Schema.Struct({
19
+ mode: Schema.Literal('multi'),
20
+ ids: Schema.Array(Schema.String).pipe(Schema.mutable),
21
+ }).pipe(Schema.mutable),
22
+ Schema.Struct({
23
+ mode: Schema.Literal('range'),
24
+ from: Schema.optional(Schema.String),
25
+ to: Schema.optional(Schema.String),
26
+ }).pipe(Schema.mutable),
27
+ Schema.Struct({
28
+ mode: Schema.Literal('multi-range'),
29
+ ranges: Schema.Array(Schema.Struct({ from: Schema.String, to: Schema.String })).pipe(Schema.mutable),
30
+ }).pipe(Schema.mutable),
31
+ ).pipe(Schema.mutable);
32
+
33
+ export type Selection = Schema.Schema.Type<typeof SelectionSchema>;
20
34
 
21
35
  export const defaultSelection = Match.type<SelectionMode>().pipe(
22
- Match.when('single', () => ({ mode: 'single' }) as Selection),
23
- Match.when('multi', () => ({ mode: 'multi', ids: [] }) as Selection),
24
- Match.when('range', () => ({ mode: 'range' }) as Selection),
25
- Match.when('multi-range', () => ({ mode: 'multi-range', ranges: [] }) as Selection),
36
+ Match.withReturnType<Selection>(),
37
+ Match.when('single', () => ({ mode: 'single' })),
38
+ Match.when('multi', () => ({ mode: 'multi', ids: [] })),
39
+ Match.when('range', () => ({ mode: 'range' })),
40
+ Match.when('multi-range', () => ({ mode: 'multi-range', ranges: [] })),
26
41
  Match.exhaustive,
27
42
  );
28
43
 
@@ -36,7 +51,6 @@ export type SelectionResult<T extends SelectionMode> = T extends 'single'
36
51
  ? { from: string; to: string }[]
37
52
  : never;
38
53
 
39
- // TODO(burdon): Refactor.
40
54
  export const getSelectionSet = (selectionManager: SelectionManager, contextId?: string) => {
41
55
  const ids = new Set<string>(contextId ? [contextId] : []);
42
56
  for (const context of selectionManager.getSelectionContexts()) {
@@ -56,101 +70,159 @@ export const getSelectionSet = (selectionManager: SelectionManager, contextId?:
56
70
  * Each context maintains its own selection mode and state.
57
71
  */
58
72
  export class SelectionManager {
59
- private readonly _state = live<{ selections: Record<string, Selection> }>({ selections: {} });
73
+ private readonly _state: Atom.Writable<{ selections: Record<string, Selection> }>;
74
+
75
+ constructor(
76
+ private readonly _registry: Registry.Registry,
77
+ initial: Record<string, Selection> = {},
78
+ ) {
79
+ this._state = Atom.make<{ selections: Record<string, Selection> }>({
80
+ selections: { ...initial },
81
+ });
82
+ }
60
83
 
61
- constructor(initial: Record<string, Selection> = {}) {
62
- if (Object.keys(initial).length > 0) {
63
- untracked(() => {
64
- this._state.selections = initial;
65
- });
66
- }
84
+ /**
85
+ * Get the state atom for reactive access in the graph system.
86
+ */
87
+ get state(): Atom.Atom<{ selections: Record<string, Selection> }> {
88
+ return this._state;
89
+ }
90
+
91
+ /**
92
+ * Gets the current state.
93
+ */
94
+ getState(): { selections: Record<string, Selection> } {
95
+ return this._registry.get(this._state);
96
+ }
97
+
98
+ /**
99
+ * Subscribe to changes in the selection state.
100
+ */
101
+ subscribe(cb: (state: { selections: Record<string, Selection> }) => void): () => void {
102
+ this._registry.get(this._state);
103
+ return this._registry.subscribe(this._state, () => {
104
+ cb(this._registry.get(this._state));
105
+ });
67
106
  }
68
107
 
69
108
  getSelectionContexts(): string[] {
70
- return Object.keys(this._state.selections);
109
+ return Object.keys(this._registry.get(this._state).selections);
71
110
  }
72
111
 
112
+ getSelection(contextId: string): Selection | undefined;
113
+ getSelection<T extends SelectionMode>(contextId: string, mode: T): Selection;
73
114
  getSelection<T extends SelectionMode | undefined>(
74
115
  contextId: string,
75
116
  mode: T = undefined as T,
76
- ): T extends undefined ? Live<Selection> | undefined : Live<Selection> {
77
- const selection = untracked(() => this._state.selections[contextId]);
117
+ ): Selection | undefined {
118
+ const state = this._registry.get(this._state);
119
+ const selection = state.selections[contextId];
78
120
  if (!mode || selection) {
79
121
  return selection;
80
122
  }
81
123
 
82
- return untracked(() => {
83
- this._state.selections[contextId] = defaultSelection(mode);
84
- return this._state.selections[contextId];
85
- });
124
+ // Create new selection for the context.
125
+ const newSelection = defaultSelection(mode);
126
+ this._registry.update(this._state, (state) => ({
127
+ selections: { ...state.selections, [contextId]: newSelection },
128
+ }));
129
+
130
+ return newSelection;
86
131
  }
87
132
 
88
- // TODO(burdon): Disambiguate with getSelection?
89
133
  getSelected<T extends SelectionMode>(contextId: string, mode: T = 'multi' as T): SelectionResult<T> {
90
134
  const selection = this.getSelection(contextId, mode);
91
135
  invariant(selection?.mode === mode, 'Selection mode mismatch');
136
+
137
+ // Cast required because TypeScript can't infer the relationship between T and the matched result.
92
138
  return Match.type<Selection>().pipe(
93
139
  Match.when({ mode: 'single' }, (s) => s.id),
94
140
  Match.when({ mode: 'multi' }, (s) => s.ids),
95
141
  Match.when({ mode: 'range' }, (s) => (s.from && s.to ? { from: s.from, to: s.to } : undefined)),
96
142
  Match.when({ mode: 'multi-range' }, (s) => s.ranges),
97
143
  Match.exhaustive,
98
- )(selection) as any;
144
+ )(selection) as SelectionResult<T>;
99
145
  }
100
146
 
101
147
  updateSingle(contextId: string, id: string): void {
102
- untracked(() => {
103
- const selection = this.getSelection(contextId, 'single');
104
- invariant(selection?.mode === 'single', 'Selection mode is not single');
105
- selection.id = id;
106
- });
148
+ const selection = this.getSelection(contextId, 'single');
149
+ invariant(selection?.mode === 'single', 'Selection mode is not single');
150
+ this._registry.update(this._state, (state) => ({
151
+ selections: {
152
+ ...state.selections,
153
+ [contextId]: { ...selection, id },
154
+ },
155
+ }));
107
156
  }
108
157
 
109
158
  updateMulti(contextId: string, ids: string[]) {
110
- untracked(() => {
111
- const selection = this.getSelection(contextId, 'multi');
112
- invariant(selection?.mode === 'multi', 'Selection mode is not multi');
113
- selection.ids = ids;
114
- });
159
+ const selection = this.getSelection(contextId, 'multi');
160
+ invariant(selection?.mode === 'multi', 'Selection mode is not multi');
161
+ this._registry.update(this._state, (state) => ({
162
+ selections: {
163
+ ...state.selections,
164
+ [contextId]: { ...selection, ids: [...ids] },
165
+ },
166
+ }));
115
167
  }
116
168
 
117
169
  updateRange(contextId: string, from: string, to: string) {
118
- untracked(() => {
119
- const selection = this.getSelection(contextId, 'range');
120
- invariant(selection?.mode === 'range', 'Selection mode is not range');
121
- selection.from = from;
122
- selection.to = to;
123
- });
170
+ const selection = this.getSelection(contextId, 'range');
171
+ invariant(selection?.mode === 'range', 'Selection mode is not range');
172
+ this._registry.update(this._state, (state) => ({
173
+ selections: {
174
+ ...state.selections,
175
+ [contextId]: { ...selection, from, to },
176
+ },
177
+ }));
124
178
  }
125
179
 
126
180
  updateMultiRange(contextId: string, ranges: { from: string; to: string }[]) {
127
- untracked(() => {
128
- const selection = this.getSelection(contextId, 'multi-range');
129
- invariant(selection?.mode === 'multi-range', 'Selection mode is not multi-range');
130
- selection.ranges = ranges;
131
- });
181
+ const selection = this.getSelection(contextId, 'multi-range');
182
+ invariant(selection?.mode === 'multi-range', 'Selection mode is not multi-range');
183
+ this._registry.update(this._state, (state) => ({
184
+ selections: {
185
+ ...state.selections,
186
+ [contextId]: { ...selection, ranges: [...ranges] },
187
+ },
188
+ }));
132
189
  }
133
190
 
134
191
  clearSelection(contextId: string): void {
135
- untracked(() => {
136
- const selection = this.getSelection(contextId);
137
- if (selection) {
138
- this._state.selections[contextId] = defaultSelection(selection.mode);
139
- }
140
- });
192
+ const selection = this.getSelection(contextId);
193
+ if (!selection) {
194
+ return;
195
+ }
196
+
197
+ const clearedSelection = Match.type<Selection>().pipe(
198
+ Match.when({ mode: 'single' }, (s) => ({ ...s, id: undefined })),
199
+ Match.when({ mode: 'multi' }, (s) => ({ ...s, ids: [] })),
200
+ Match.when({ mode: 'range' }, (s) => ({ ...s, from: undefined, to: undefined })),
201
+ Match.when({ mode: 'multi-range' }, (s) => ({ ...s, ranges: [] })),
202
+ Match.exhaustive,
203
+ )(selection);
204
+
205
+ this._registry.update(this._state, (state) => ({
206
+ selections: {
207
+ ...state.selections,
208
+ [contextId]: clearedSelection,
209
+ },
210
+ }));
141
211
  }
142
212
 
143
213
  toggleSelection(contextId: string, id: string): void {
144
- untracked(() => {
145
- const selection = this.getSelection(contextId, 'multi');
146
- invariant(selection?.mode === 'multi', 'Selection mode is not multi');
147
-
148
- if (selection.ids.includes(id)) {
149
- const index = selection.ids.indexOf(id);
150
- selection.ids.splice(index, 1);
151
- } else {
152
- selection.ids.push(id);
153
- }
154
- });
214
+ const selection = this.getSelection(contextId, 'multi');
215
+ invariant(selection?.mode === 'multi', 'Selection mode is not multi');
216
+
217
+ const newIds = selection.ids.includes(id)
218
+ ? selection.ids.filter((existingId) => existingId !== id)
219
+ : [...selection.ids, id];
220
+
221
+ this._registry.update(this._state, (state) => ({
222
+ selections: {
223
+ ...state.selections,
224
+ [contextId]: { ...selection, ids: newIds },
225
+ },
226
+ }));
155
227
  }
156
228
  }
@@ -2,11 +2,29 @@
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
 
8
- import { RootAttentionProvider } from '../../components';
9
+ import { AttentionManager } from '../../attention';
10
+ import { RootAttentionProvider, SelectionProvider } from '../../components';
9
11
 
10
- export const withAttention: Decorator = (Story) => {
11
- return createElement(RootAttentionProvider, { children: 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
+
24
+ return createElement(
25
+ RegistryContext.Provider,
26
+ { value: registry },
27
+ createElement(RootAttentionProvider, { attention }, createElement(SelectionProvider, {}, createElement(Story))),
28
+ );
29
+ };
12
30
  };
@@ -0,0 +1,9 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ // UI-free re-exports of @dxos/react-ui-attention. Importing from here does not
6
+ // pull in React components, so it is safe to use from operations, schemas, and
7
+ // other non-DOM code that only needs the data model and pure helpers.
8
+ export * from '../attention';
9
+ export * from '../selection';