@dxos/react-ui-attention 0.8.4-main.ead640a → 0.8.4-main.f466a3d56e
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/LICENSE +102 -5
- package/dist/lib/browser/chunk-D2BLVJ6Z.mjs +421 -0
- package/dist/lib/browser/chunk-D2BLVJ6Z.mjs.map +7 -0
- package/dist/lib/browser/chunk-I4VO2RQM.mjs +255 -0
- package/dist/lib/browser/chunk-I4VO2RQM.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +89 -23
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +22 -5
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/browser/types/index.mjs +29 -0
- package/dist/lib/browser/types/index.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-6INLBC62.mjs +423 -0
- package/dist/lib/node-esm/chunk-6INLBC62.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-ZTUVSH4P.mjs +256 -0
- package/dist/lib/node-esm/chunk-ZTUVSH4P.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +89 -23
- 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 +22 -5
- package/dist/lib/node-esm/testing/index.mjs.map +3 -3
- package/dist/lib/node-esm/types/index.mjs +30 -0
- package/dist/lib/node-esm/types/index.mjs.map +7 -0
- package/dist/types/src/attention.d.ts +60 -20
- package/dist/types/src/attention.d.ts.map +1 -1
- package/dist/types/src/components/AttentionGlyph/AttentionGlyph.d.ts.map +1 -0
- package/dist/types/src/components/AttentionGlyph/AttentionGlyph.stories.d.ts.map +1 -0
- package/dist/types/src/components/AttentionGlyph/index.d.ts +2 -0
- package/dist/types/src/components/AttentionGlyph/index.d.ts.map +1 -0
- package/dist/types/src/components/AttentionProvider/AttentionProvider.d.ts +34 -0
- package/dist/types/src/components/AttentionProvider/AttentionProvider.d.ts.map +1 -0
- package/dist/types/src/components/AttentionProvider/AttentionProvider.stories.d.ts.map +1 -0
- package/dist/types/src/components/AttentionProvider/index.d.ts +2 -0
- package/dist/types/src/components/AttentionProvider/index.d.ts.map +1 -0
- package/dist/types/src/components/{SelectionProvider.d.ts → SelectionProvider/SelectionProvider.d.ts} +7 -6
- package/dist/types/src/components/SelectionProvider/SelectionProvider.d.ts.map +1 -0
- package/dist/types/src/components/SelectionProvider/index.d.ts +2 -0
- package/dist/types/src/components/SelectionProvider/index.d.ts.map +1 -0
- package/dist/types/src/selection.d.ts +23 -3
- 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/index.d.ts +3 -0
- package/dist/types/src/types/index.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +34 -32
- package/src/attention.test.ts +188 -27
- package/src/attention.ts +179 -75
- package/src/components/{AttentionGlyph.stories.tsx → AttentionGlyph/AttentionGlyph.stories.tsx} +2 -2
- package/src/components/{AttentionGlyph.tsx → AttentionGlyph/AttentionGlyph.tsx} +13 -6
- package/src/components/AttentionGlyph/index.ts +5 -0
- package/src/components/{AttentionProvider.stories.tsx → AttentionProvider/AttentionProvider.stories.tsx} +3 -4
- package/src/components/AttentionProvider/AttentionProvider.tsx +176 -0
- package/src/components/AttentionProvider/index.ts +5 -0
- package/src/components/SelectionProvider/SelectionProvider.tsx +174 -0
- package/src/components/SelectionProvider/index.ts +5 -0
- package/src/selection.ts +121 -79
- package/src/testing/decorators/withAttention.ts +22 -4
- package/src/types/index.ts +9 -0
- package/dist/lib/browser/chunk-HELLA3BH.mjs +0 -675
- package/dist/lib/browser/chunk-HELLA3BH.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-FBZV7QG7.mjs +0 -677
- package/dist/lib/node-esm/chunk-FBZV7QG7.mjs.map +0 -7
- package/dist/types/src/components/AttentionGlyph.d.ts.map +0 -1
- package/dist/types/src/components/AttentionGlyph.stories.d.ts.map +0 -1
- package/dist/types/src/components/AttentionProvider.d.ts +0 -38
- package/dist/types/src/components/AttentionProvider.d.ts.map +0 -1
- package/dist/types/src/components/AttentionProvider.stories.d.ts.map +0 -1
- package/dist/types/src/components/SelectionProvider.d.ts.map +0 -1
- package/src/components/AttentionProvider.tsx +0 -201
- package/src/components/SelectionProvider.tsx +0 -120
- /package/dist/types/src/components/{AttentionGlyph.d.ts → AttentionGlyph/AttentionGlyph.d.ts} +0 -0
- /package/dist/types/src/components/{AttentionGlyph.stories.d.ts → AttentionGlyph/AttentionGlyph.stories.d.ts} +0 -0
- /package/dist/types/src/components/{AttentionProvider.stories.d.ts → AttentionProvider/AttentionProvider.stories.d.ts} +0 -0
package/src/selection.ts
CHANGED
|
@@ -2,22 +2,23 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
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
|
-
|
|
12
|
-
// TODO(burdon): Reconcile with @dxos/graph.
|
|
13
10
|
|
|
14
11
|
export type SelectionMode = 'single' | 'multi' | 'range' | 'multi-range';
|
|
15
12
|
|
|
16
13
|
export const SelectionSchema = Schema.Union(
|
|
17
|
-
Schema.Struct({
|
|
18
|
-
|
|
19
|
-
Schema.
|
|
20
|
-
),
|
|
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),
|
|
21
22
|
Schema.Struct({
|
|
22
23
|
mode: Schema.Literal('range'),
|
|
23
24
|
from: Schema.optional(Schema.String),
|
|
@@ -50,7 +51,6 @@ export type SelectionResult<T extends SelectionMode> = T extends 'single'
|
|
|
50
51
|
? { from: string; to: string }[]
|
|
51
52
|
: never;
|
|
52
53
|
|
|
53
|
-
// TODO(burdon): Refactor.
|
|
54
54
|
export const getSelectionSet = (selectionManager: SelectionManager, contextId?: string) => {
|
|
55
55
|
const ids = new Set<string>(contextId ? [contextId] : []);
|
|
56
56
|
for (const context of selectionManager.getSelectionContexts()) {
|
|
@@ -70,117 +70,159 @@ export const getSelectionSet = (selectionManager: SelectionManager, contextId?:
|
|
|
70
70
|
* Each context maintains its own selection mode and state.
|
|
71
71
|
*/
|
|
72
72
|
export class SelectionManager {
|
|
73
|
-
private readonly _state
|
|
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
|
+
}
|
|
74
83
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
+
});
|
|
81
106
|
}
|
|
82
107
|
|
|
83
108
|
getSelectionContexts(): string[] {
|
|
84
|
-
return Object.keys(this._state.selections);
|
|
109
|
+
return Object.keys(this._registry.get(this._state).selections);
|
|
85
110
|
}
|
|
86
111
|
|
|
112
|
+
getSelection(contextId: string): Selection | undefined;
|
|
113
|
+
getSelection<T extends SelectionMode>(contextId: string, mode: T): Selection;
|
|
87
114
|
getSelection<T extends SelectionMode | undefined>(
|
|
88
115
|
contextId: string,
|
|
89
116
|
mode: T = undefined as T,
|
|
90
|
-
):
|
|
91
|
-
const
|
|
117
|
+
): Selection | undefined {
|
|
118
|
+
const state = this._registry.get(this._state);
|
|
119
|
+
const selection = state.selections[contextId];
|
|
92
120
|
if (!mode || selection) {
|
|
93
121
|
return selection;
|
|
94
122
|
}
|
|
95
123
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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;
|
|
100
131
|
}
|
|
101
132
|
|
|
102
|
-
// TODO(burdon): Disambiguate with getSelection?
|
|
103
133
|
getSelected<T extends SelectionMode>(contextId: string, mode: T = 'multi' as T): SelectionResult<T> {
|
|
104
134
|
const selection = this.getSelection(contextId, mode);
|
|
105
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.
|
|
106
138
|
return Match.type<Selection>().pipe(
|
|
107
139
|
Match.when({ mode: 'single' }, (s) => s.id),
|
|
108
140
|
Match.when({ mode: 'multi' }, (s) => s.ids),
|
|
109
141
|
Match.when({ mode: 'range' }, (s) => (s.from && s.to ? { from: s.from, to: s.to } : undefined)),
|
|
110
142
|
Match.when({ mode: 'multi-range' }, (s) => s.ranges),
|
|
111
143
|
Match.exhaustive,
|
|
112
|
-
)(selection) as
|
|
144
|
+
)(selection) as SelectionResult<T>;
|
|
113
145
|
}
|
|
114
146
|
|
|
115
147
|
updateSingle(contextId: string, id: string): void {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
+
}));
|
|
121
156
|
}
|
|
122
157
|
|
|
123
158
|
updateMulti(contextId: string, ids: string[]) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
+
}));
|
|
129
167
|
}
|
|
130
168
|
|
|
131
169
|
updateRange(contextId: string, from: string, to: string) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
+
}));
|
|
138
178
|
}
|
|
139
179
|
|
|
140
180
|
updateMultiRange(contextId: string, ranges: { from: string; to: string }[]) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
+
}));
|
|
146
189
|
}
|
|
147
190
|
|
|
148
191
|
clearSelection(contextId: string): void {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
Match.exhaustive,
|
|
169
|
-
)(selection);
|
|
170
|
-
});
|
|
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
|
+
}));
|
|
171
211
|
}
|
|
172
212
|
|
|
173
213
|
toggleSelection(contextId: string, id: string): void {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
+
}));
|
|
185
227
|
}
|
|
186
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 {
|
|
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
|
+
|
|
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';
|