@dxos/react-ui-attention 0.8.4-main.f9ba587 → 0.8.4-main.fcfe5033a5
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/dist/lib/browser/chunk-4SEURHOL.mjs +721 -0
- package/dist/lib/browser/chunk-4SEURHOL.mjs.map +7 -0
- package/dist/lib/browser/chunk-GMKHOVJ5.mjs +1 -0
- package/dist/lib/browser/chunk-GMKHOVJ5.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +87 -13
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +20 -7
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/browser/types.mjs +2 -0
- package/dist/lib/browser/types.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-6PQ4P7AL.mjs +723 -0
- package/dist/lib/node-esm/chunk-6PQ4P7AL.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-UHVMYYIB.mjs +2 -0
- package/dist/lib/node-esm/chunk-UHVMYYIB.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +87 -13
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +20 -7
- package/dist/lib/node-esm/testing/index.mjs.map +3 -3
- package/dist/lib/node-esm/types.mjs +3 -0
- package/dist/lib/node-esm/types.mjs.map +7 -0
- package/dist/types/src/attention.d.ts +56 -20
- package/dist/types/src/attention.d.ts.map +1 -1
- package/dist/types/src/components/AttentionGlyph.d.ts.map +1 -1
- package/dist/types/src/components/AttentionGlyph.stories.d.ts +9 -20
- package/dist/types/src/components/AttentionGlyph.stories.d.ts.map +1 -1
- package/dist/types/src/components/AttentionProvider.d.ts +15 -16
- package/dist/types/src/components/AttentionProvider.d.ts.map +1 -1
- package/dist/types/src/components/AttentionProvider.stories.d.ts +6 -4
- package/dist/types/src/components/AttentionProvider.stories.d.ts.map +1 -1
- package/dist/types/src/components/SelectionProvider.d.ts +6 -5
- package/dist/types/src/components/SelectionProvider.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/selection.d.ts +49 -17
- package/dist/types/src/selection.d.ts.map +1 -1
- package/dist/types/src/testing/decorators/withAttention.d.ts +5 -1
- package/dist/types/src/testing/decorators/withAttention.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +9 -0
- package/dist/types/src/types.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +38 -24
- package/src/attention.test.ts +188 -27
- package/src/attention.ts +175 -74
- package/src/components/AttentionGlyph.stories.tsx +17 -14
- package/src/components/AttentionGlyph.tsx +14 -7
- package/src/components/AttentionProvider.stories.tsx +12 -9
- package/src/components/AttentionProvider.tsx +80 -97
- package/src/components/SelectionProvider.tsx +66 -19
- package/src/index.ts +1 -0
- package/src/selection.ts +135 -65
- package/src/testing/decorators/withAttention.ts +21 -4
- package/src/types.ts +13 -0
- package/dist/lib/browser/chunk-QPEFFJ4E.mjs +0 -616
- package/dist/lib/browser/chunk-QPEFFJ4E.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-F2JRKDYV.mjs +0 -618
- package/dist/lib/node-esm/chunk-F2JRKDYV.mjs.map +0 -7
package/src/selection.ts
CHANGED
|
@@ -2,27 +2,40 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
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
10
|
|
|
11
11
|
// TODO(burdon): Reconcile with @dxos/graph.
|
|
12
12
|
|
|
13
13
|
export type SelectionMode = 'single' | 'multi' | 'range' | 'multi-range';
|
|
14
14
|
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
export const SelectionSchema = Schema.Union(
|
|
16
|
+
Schema.Struct({ mode: Schema.Literal('single'), id: Schema.optional(Schema.String) }).pipe(Schema.mutable),
|
|
17
|
+
Schema.Struct({ mode: Schema.Literal('multi'), ids: Schema.Array(Schema.String).pipe(Schema.mutable) }).pipe(
|
|
18
|
+
Schema.mutable,
|
|
19
|
+
),
|
|
20
|
+
Schema.Struct({
|
|
21
|
+
mode: Schema.Literal('range'),
|
|
22
|
+
from: Schema.optional(Schema.String),
|
|
23
|
+
to: Schema.optional(Schema.String),
|
|
24
|
+
}).pipe(Schema.mutable),
|
|
25
|
+
Schema.Struct({
|
|
26
|
+
mode: Schema.Literal('multi-range'),
|
|
27
|
+
ranges: Schema.Array(Schema.Struct({ from: Schema.String, to: Schema.String })).pipe(Schema.mutable),
|
|
28
|
+
}).pipe(Schema.mutable),
|
|
29
|
+
).pipe(Schema.mutable);
|
|
30
|
+
|
|
31
|
+
export type Selection = Schema.Schema.Type<typeof SelectionSchema>;
|
|
20
32
|
|
|
21
33
|
export const defaultSelection = Match.type<SelectionMode>().pipe(
|
|
22
|
-
Match.
|
|
23
|
-
Match.when('
|
|
24
|
-
Match.when('
|
|
25
|
-
Match.when('
|
|
34
|
+
Match.withReturnType<Selection>(),
|
|
35
|
+
Match.when('single', () => ({ mode: 'single' })),
|
|
36
|
+
Match.when('multi', () => ({ mode: 'multi', ids: [] })),
|
|
37
|
+
Match.when('range', () => ({ mode: 'range' })),
|
|
38
|
+
Match.when('multi-range', () => ({ mode: 'multi-range', ranges: [] })),
|
|
26
39
|
Match.exhaustive,
|
|
27
40
|
);
|
|
28
41
|
|
|
@@ -56,101 +69,158 @@ export const getSelectionSet = (selectionManager: SelectionManager, contextId?:
|
|
|
56
69
|
* Each context maintains its own selection mode and state.
|
|
57
70
|
*/
|
|
58
71
|
export class SelectionManager {
|
|
59
|
-
private readonly _state
|
|
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
|
+
}
|
|
60
82
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
});
|
|
67
105
|
}
|
|
68
106
|
|
|
69
107
|
getSelectionContexts(): string[] {
|
|
70
|
-
return Object.keys(this._state.selections);
|
|
108
|
+
return Object.keys(this._registry.get(this._state).selections);
|
|
71
109
|
}
|
|
72
110
|
|
|
111
|
+
getSelection(contextId: string): Selection | undefined;
|
|
112
|
+
getSelection<T extends SelectionMode>(contextId: string, mode: T): Selection;
|
|
73
113
|
getSelection<T extends SelectionMode | undefined>(
|
|
74
114
|
contextId: string,
|
|
75
115
|
mode: T = undefined as T,
|
|
76
|
-
):
|
|
77
|
-
const
|
|
116
|
+
): Selection | undefined {
|
|
117
|
+
const state = this._registry.get(this._state);
|
|
118
|
+
const selection = state.selections[contextId];
|
|
78
119
|
if (!mode || selection) {
|
|
79
120
|
return selection;
|
|
80
121
|
}
|
|
81
122
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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;
|
|
86
129
|
}
|
|
87
130
|
|
|
88
131
|
// TODO(burdon): Disambiguate with getSelection?
|
|
89
132
|
getSelected<T extends SelectionMode>(contextId: string, mode: T = 'multi' as T): SelectionResult<T> {
|
|
90
133
|
const selection = this.getSelection(contextId, mode);
|
|
91
134
|
invariant(selection?.mode === mode, 'Selection mode mismatch');
|
|
135
|
+
// Cast required because TypeScript can't infer the relationship between T and the matched result.
|
|
92
136
|
return Match.type<Selection>().pipe(
|
|
93
137
|
Match.when({ mode: 'single' }, (s) => s.id),
|
|
94
138
|
Match.when({ mode: 'multi' }, (s) => s.ids),
|
|
95
139
|
Match.when({ mode: 'range' }, (s) => (s.from && s.to ? { from: s.from, to: s.to } : undefined)),
|
|
96
140
|
Match.when({ mode: 'multi-range' }, (s) => s.ranges),
|
|
97
141
|
Match.exhaustive,
|
|
98
|
-
)(selection) as
|
|
142
|
+
)(selection) as SelectionResult<T>;
|
|
99
143
|
}
|
|
100
144
|
|
|
101
145
|
updateSingle(contextId: string, id: string): void {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
+
}));
|
|
107
154
|
}
|
|
108
155
|
|
|
109
156
|
updateMulti(contextId: string, ids: string[]) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
}));
|
|
115
165
|
}
|
|
116
166
|
|
|
117
167
|
updateRange(contextId: string, from: string, to: string) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
+
}));
|
|
124
176
|
}
|
|
125
177
|
|
|
126
178
|
updateMultiRange(contextId: string, ranges: { from: string; to: string }[]) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
+
}));
|
|
132
187
|
}
|
|
133
188
|
|
|
134
189
|
clearSelection(contextId: string): void {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
+
}));
|
|
141
209
|
}
|
|
142
210
|
|
|
143
211
|
toggleSelection(contextId: string, id: string): void {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
+
}));
|
|
155
225
|
}
|
|
156
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
|
|
|
8
|
-
import {
|
|
9
|
+
import { AttentionManager } from '../../attention';
|
|
10
|
+
import { RootAttentionProvider, SelectionProvider } from '../../components';
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
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
|
};
|