@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.
Files changed (58) hide show
  1. package/dist/lib/browser/chunk-4SEURHOL.mjs +721 -0
  2. package/dist/lib/browser/chunk-4SEURHOL.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-GMKHOVJ5.mjs +1 -0
  4. package/dist/lib/browser/chunk-GMKHOVJ5.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +87 -13
  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 +20 -7
  9. package/dist/lib/browser/testing/index.mjs.map +3 -3
  10. package/dist/lib/browser/types.mjs +2 -0
  11. package/dist/lib/browser/types.mjs.map +7 -0
  12. package/dist/lib/node-esm/chunk-6PQ4P7AL.mjs +723 -0
  13. package/dist/lib/node-esm/chunk-6PQ4P7AL.mjs.map +7 -0
  14. package/dist/lib/node-esm/chunk-UHVMYYIB.mjs +2 -0
  15. package/dist/lib/node-esm/chunk-UHVMYYIB.mjs.map +7 -0
  16. package/dist/lib/node-esm/index.mjs +87 -13
  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 +20 -7
  20. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  21. package/dist/lib/node-esm/types.mjs +3 -0
  22. package/dist/lib/node-esm/types.mjs.map +7 -0
  23. package/dist/types/src/attention.d.ts +56 -20
  24. package/dist/types/src/attention.d.ts.map +1 -1
  25. package/dist/types/src/components/AttentionGlyph.d.ts.map +1 -1
  26. package/dist/types/src/components/AttentionGlyph.stories.d.ts +9 -20
  27. package/dist/types/src/components/AttentionGlyph.stories.d.ts.map +1 -1
  28. package/dist/types/src/components/AttentionProvider.d.ts +15 -16
  29. package/dist/types/src/components/AttentionProvider.d.ts.map +1 -1
  30. package/dist/types/src/components/AttentionProvider.stories.d.ts +6 -4
  31. package/dist/types/src/components/AttentionProvider.stories.d.ts.map +1 -1
  32. package/dist/types/src/components/SelectionProvider.d.ts +6 -5
  33. package/dist/types/src/components/SelectionProvider.d.ts.map +1 -1
  34. package/dist/types/src/index.d.ts +1 -0
  35. package/dist/types/src/index.d.ts.map +1 -1
  36. package/dist/types/src/selection.d.ts +49 -17
  37. package/dist/types/src/selection.d.ts.map +1 -1
  38. package/dist/types/src/testing/decorators/withAttention.d.ts +5 -1
  39. package/dist/types/src/testing/decorators/withAttention.d.ts.map +1 -1
  40. package/dist/types/src/types.d.ts +9 -0
  41. package/dist/types/src/types.d.ts.map +1 -0
  42. package/dist/types/tsconfig.tsbuildinfo +1 -1
  43. package/package.json +38 -24
  44. package/src/attention.test.ts +188 -27
  45. package/src/attention.ts +175 -74
  46. package/src/components/AttentionGlyph.stories.tsx +17 -14
  47. package/src/components/AttentionGlyph.tsx +14 -7
  48. package/src/components/AttentionProvider.stories.tsx +12 -9
  49. package/src/components/AttentionProvider.tsx +80 -97
  50. package/src/components/SelectionProvider.tsx +66 -19
  51. package/src/index.ts +1 -0
  52. package/src/selection.ts +135 -65
  53. package/src/testing/decorators/withAttention.ts +21 -4
  54. package/src/types.ts +13 -0
  55. package/dist/lib/browser/chunk-QPEFFJ4E.mjs +0 -616
  56. package/dist/lib/browser/chunk-QPEFFJ4E.mjs.map +0 -7
  57. package/dist/lib/node-esm/chunk-F2JRKDYV.mjs +0 -618
  58. package/dist/lib/node-esm/chunk-F2JRKDYV.mjs.map +0 -7
@@ -0,0 +1,723 @@
1
+ import { createRequire } from 'node:module';const require = createRequire(import.meta.url);
2
+
3
+ // src/attention.ts
4
+ import { Atom } from "@effect-atom/atom-react";
5
+ var AttentionManager = class {
6
+ _registry;
7
+ _map = /* @__PURE__ */ new Map();
8
+ _currentAtom;
9
+ constructor(_registry, initial = []) {
10
+ this._registry = _registry;
11
+ this._currentAtom = Atom.make([]).pipe(Atom.keepAlive);
12
+ if (initial.length > 0) {
13
+ this.update(initial);
14
+ }
15
+ }
16
+ /**
17
+ * Atom for the currently attended element IDs.
18
+ */
19
+ get current() {
20
+ return this._currentAtom;
21
+ }
22
+ /**
23
+ * Gets the currently attended element IDs.
24
+ */
25
+ getCurrent() {
26
+ return this._registry.get(this._currentAtom);
27
+ }
28
+ /**
29
+ * Subscribe to changes in the current attention IDs.
30
+ */
31
+ subscribeCurrent(cb) {
32
+ this._registry.get(this._currentAtom);
33
+ return this._registry.subscribe(this._currentAtom, () => {
34
+ cb(this._registry.get(this._currentAtom));
35
+ });
36
+ }
37
+ /**
38
+ * All tracked qualified IDs.
39
+ */
40
+ keys() {
41
+ return Array.from(this._map.keys());
42
+ }
43
+ /**
44
+ * Get the attention state for a given qualified ID.
45
+ */
46
+ get(id) {
47
+ const atom = this._getAtom(id);
48
+ return this._registry.get(atom);
49
+ }
50
+ /**
51
+ * Subscribe to changes in the attention state for a given qualified ID.
52
+ */
53
+ subscribe(id, cb) {
54
+ const atom = this._getAtom(id);
55
+ this._registry.get(atom);
56
+ return this._registry.subscribe(atom, () => {
57
+ cb(this._registry.get(atom));
58
+ });
59
+ }
60
+ _getAtom(id) {
61
+ const existing = this._map.get(id);
62
+ if (existing) {
63
+ return existing;
64
+ }
65
+ const newAtom = Atom.make({
66
+ hasAttention: false,
67
+ isAncestor: false,
68
+ isRelated: false
69
+ }).pipe(Atom.keepAlive);
70
+ this._map.set(id, newAtom);
71
+ return newAtom;
72
+ }
73
+ /**
74
+ * Update the currently attended element.
75
+ * Takes the array of qualified IDs collected from the DOM; the first element is the primary attended item.
76
+ * Ancestry is derived from the progressive prefixes of the primary ID.
77
+ * Relatedness is derived from the segment ID: any tracked key whose last `/` segment matches the
78
+ * attended ID's segment ID is marked `isRelated`. Additionally, if the primary ID is a linked segment
79
+ * (starts with `~`), its immediate parent gets `isRelated` alongside `isAncestor`.
80
+ *
81
+ * @internal
82
+ */
83
+ update(nextIds) {
84
+ const primaryId = nextIds[0];
85
+ if (!primaryId) {
86
+ return;
87
+ }
88
+ const currentIds = this.getCurrent();
89
+ const prevPrimaryId = currentIds[0];
90
+ if (prevPrimaryId) {
91
+ const prevPrefixes = expandAttendableId(prevPrimaryId);
92
+ for (const prefix of prevPrefixes) {
93
+ this._set(prefix, {});
94
+ }
95
+ const prevSegmentId = getSegmentId(prevPrimaryId);
96
+ for (const key of this.keys()) {
97
+ if (getSegmentId(key) === prevSegmentId) {
98
+ this._set(key, {});
99
+ }
100
+ }
101
+ }
102
+ this._registry.set(this._currentAtom, nextIds);
103
+ const prefixes = expandAttendableId(primaryId);
104
+ const prefixSet = new Set(prefixes);
105
+ const linkedParent = isLinkedSegment(primaryId) ? getParentId(primaryId) : void 0;
106
+ for (const prefix of prefixes) {
107
+ if (prefix === primaryId) {
108
+ this._set(prefix, {
109
+ hasAttention: true
110
+ });
111
+ } else if (prefix === linkedParent) {
112
+ this._set(prefix, {
113
+ isAncestor: true,
114
+ isRelated: true
115
+ });
116
+ } else {
117
+ this._set(prefix, {
118
+ isAncestor: true
119
+ });
120
+ }
121
+ }
122
+ const segmentId = getSegmentId(primaryId);
123
+ for (const key of this.keys()) {
124
+ if (!prefixSet.has(key) && getSegmentId(key) === segmentId) {
125
+ this._set(key, {
126
+ isRelated: true
127
+ });
128
+ }
129
+ }
130
+ }
131
+ _set(id, attention) {
132
+ const atom = this._getAtom(id);
133
+ this._registry.set(atom, {
134
+ hasAttention: attention.hasAttention ?? false,
135
+ isAncestor: attention.isAncestor ?? false,
136
+ isRelated: attention.isRelated ?? false
137
+ });
138
+ }
139
+ };
140
+ var getAttendables = (selector, cursor, acc = []) => {
141
+ const closestAttendable = cursor.closest(selector);
142
+ if (closestAttendable) {
143
+ const attendableId = closestAttendable.getAttribute("data-attendable-id");
144
+ if (!attendableId) {
145
+ const trigger = document.querySelector(`[aria-controls="${closestAttendable.getAttribute("id")}"]`);
146
+ if (!trigger) {
147
+ return acc;
148
+ } else {
149
+ return getAttendables(selector, trigger, acc);
150
+ }
151
+ } else {
152
+ acc.push(attendableId);
153
+ return !closestAttendable.parentElement ? acc : getAttendables(selector, closestAttendable.parentElement, acc);
154
+ }
155
+ }
156
+ return [
157
+ ...new Set(acc)
158
+ ];
159
+ };
160
+ var LINKED_PREFIX = "~";
161
+ var expandAttendableId = (qualifiedId) => {
162
+ const segments = qualifiedId.split("/");
163
+ return segments.map((_, index) => segments.slice(0, index + 1).join("/"));
164
+ };
165
+ var isLinkedSegment = (qualifiedId) => {
166
+ const lastSegment = qualifiedId.split("/").pop() ?? "";
167
+ return lastSegment.startsWith(LINKED_PREFIX);
168
+ };
169
+ var linkedSegment = (variant) => `${LINKED_PREFIX}${variant}`;
170
+ var getLinkedVariant = (qualifiedId) => {
171
+ const lastSegment = qualifiedId.split("/").pop() ?? "";
172
+ return lastSegment.startsWith(LINKED_PREFIX) ? lastSegment.slice(LINKED_PREFIX.length) : lastSegment;
173
+ };
174
+ var getParentId = (qualifiedId) => {
175
+ if (!qualifiedId) {
176
+ return void 0;
177
+ }
178
+ const lastSlash = qualifiedId.lastIndexOf("/");
179
+ return lastSlash > 0 ? qualifiedId.slice(0, lastSlash) : void 0;
180
+ };
181
+ var getSegmentId = (qualifiedId) => {
182
+ return qualifiedId.split("/").pop() ?? qualifiedId;
183
+ };
184
+
185
+ // src/components/AttentionProvider.tsx
186
+ import { RegistryContext } from "@effect-atom/atom-react";
187
+ import { createContext } from "@radix-ui/react-context";
188
+ import { Primitive } from "@radix-ui/react-primitive";
189
+ import { Slot } from "@radix-ui/react-slot";
190
+ import React, { forwardRef, useContext, useEffect, useMemo, useState } from "react";
191
+ import { log } from "@dxos/log";
192
+ import { useDefaultValue } from "@dxos/react-hooks";
193
+ import { mx } from "@dxos/ui-theme";
194
+ var __dxlog_file = "/__w/dxos/dxos/packages/ui/react-ui-attention/src/components/AttentionProvider.tsx";
195
+ var ATTENTION_NAME = "Attention";
196
+ var ATTENDABLE_ATTRIBUTE = "data-attendable-id";
197
+ var ATTENTION_SOURCE_ATTRIBUTE = "data-w-attention-source";
198
+ var [AttentionContextProvider, useAttentionContext] = createContext(ATTENTION_NAME, {
199
+ attention: void 0
200
+ });
201
+ var UNKNOWN_ATTENDABLE = {
202
+ hasAttention: false,
203
+ isAncestor: false,
204
+ isRelated: false
205
+ };
206
+ var useAttention = (attendableId) => {
207
+ const { attention } = useAttentionContext(ATTENTION_NAME);
208
+ const [state, setState] = useState(UNKNOWN_ATTENDABLE);
209
+ useEffect(() => {
210
+ if (!attendableId || !attention) {
211
+ setState(UNKNOWN_ATTENDABLE);
212
+ return;
213
+ }
214
+ const currentState = attention.get(attendableId);
215
+ setState(currentState);
216
+ return attention.subscribe(attendableId, (newState) => {
217
+ setState(newState);
218
+ });
219
+ }, [
220
+ attention,
221
+ attendableId
222
+ ]);
223
+ return state;
224
+ };
225
+ var useAttended = () => {
226
+ const { attention } = useAttentionContext(ATTENTION_NAME);
227
+ const [current, setCurrent] = useState([]);
228
+ useEffect(() => {
229
+ if (!attention) {
230
+ return;
231
+ }
232
+ setCurrent(attention.getCurrent());
233
+ return attention.subscribeCurrent((newCurrent) => {
234
+ setCurrent(newCurrent);
235
+ });
236
+ }, [
237
+ attention
238
+ ]);
239
+ return current;
240
+ };
241
+ var useAttentionAttributes = (attendableId) => {
242
+ const { hasAttention } = useAttention(attendableId);
243
+ return useMemo(() => {
244
+ const attributes = {
245
+ [ATTENDABLE_ATTRIBUTE]: attendableId
246
+ };
247
+ if (hasAttention) {
248
+ attributes[ATTENTION_SOURCE_ATTRIBUTE] = "true";
249
+ }
250
+ return attributes;
251
+ }, [
252
+ attendableId,
253
+ hasAttention
254
+ ]);
255
+ };
256
+ var RootAttentionProvider = ({ children, attention: propsAttention, onChange }) => {
257
+ const registry = useContext(RegistryContext);
258
+ const attention = useDefaultValue(propsAttention, () => new AttentionManager(registry));
259
+ const handleFocus = (event) => {
260
+ log("focus", {
261
+ related: event.relatedTarget,
262
+ target: event.target
263
+ }, {
264
+ F: __dxlog_file,
265
+ L: 115,
266
+ S: void 0,
267
+ C: (f, a) => f(...a)
268
+ });
269
+ const selector = [
270
+ "[data-attendable-id]",
271
+ ...Array.from(document.querySelectorAll("[aria-controls]")).map((el) => `[id="${el.getAttribute("aria-controls")}"]`)
272
+ ].join(",");
273
+ const prev = attention.getCurrent();
274
+ const next = getAttendables(selector, event.target);
275
+ if (next.length > 0 && (prev.length !== next.length || !!prev.find((id, index) => next[index] !== id))) {
276
+ attention.update(next);
277
+ onChange?.(next);
278
+ }
279
+ };
280
+ return /* @__PURE__ */ React.createElement(AttentionContextProvider, {
281
+ attention
282
+ }, /* @__PURE__ */ React.createElement("div", {
283
+ role: "none",
284
+ className: "contents",
285
+ onFocusCapture: handleFocus
286
+ }, children));
287
+ };
288
+ var AttendableContainer = /* @__PURE__ */ forwardRef(({ id, classNames, children, asChild, ...props }, forwardedRef) => {
289
+ const attentionAttrs = useAttentionAttributes(id);
290
+ const Comp = asChild ? Slot : Primitive.div;
291
+ return /* @__PURE__ */ React.createElement(Comp, {
292
+ role: "none",
293
+ ...props,
294
+ ...attentionAttrs,
295
+ className: mx("dx-attention-surface", props.tabIndex === 0 && "dx-focus-ring-inset-over-all", classNames),
296
+ ref: forwardedRef
297
+ }, children);
298
+ });
299
+
300
+ // src/selection.ts
301
+ import { Atom as Atom2 } from "@effect-atom/atom-react";
302
+ import * as Match from "effect/Match";
303
+ import * as Schema from "effect/Schema";
304
+ import { invariant } from "@dxos/invariant";
305
+ var __dxlog_file2 = "/__w/dxos/dxos/packages/ui/react-ui-attention/src/selection.ts";
306
+ var SelectionSchema = Schema.Union(Schema.Struct({
307
+ mode: Schema.Literal("single"),
308
+ id: Schema.optional(Schema.String)
309
+ }).pipe(Schema.mutable), Schema.Struct({
310
+ mode: Schema.Literal("multi"),
311
+ ids: Schema.Array(Schema.String).pipe(Schema.mutable)
312
+ }).pipe(Schema.mutable), Schema.Struct({
313
+ mode: Schema.Literal("range"),
314
+ from: Schema.optional(Schema.String),
315
+ to: Schema.optional(Schema.String)
316
+ }).pipe(Schema.mutable), Schema.Struct({
317
+ mode: Schema.Literal("multi-range"),
318
+ ranges: Schema.Array(Schema.Struct({
319
+ from: Schema.String,
320
+ to: Schema.String
321
+ })).pipe(Schema.mutable)
322
+ }).pipe(Schema.mutable)).pipe(Schema.mutable);
323
+ var defaultSelection = Match.type().pipe(Match.withReturnType(), Match.when("single", () => ({
324
+ mode: "single"
325
+ })), Match.when("multi", () => ({
326
+ mode: "multi",
327
+ ids: []
328
+ })), Match.when("range", () => ({
329
+ mode: "range"
330
+ })), Match.when("multi-range", () => ({
331
+ mode: "multi-range",
332
+ ranges: []
333
+ })), Match.exhaustive);
334
+ var getSelectionSet = (selectionManager, contextId) => {
335
+ const ids = new Set(contextId ? [
336
+ contextId
337
+ ] : []);
338
+ for (const context of selectionManager.getSelectionContexts()) {
339
+ const selection = selectionManager.getSelection(context);
340
+ if (selection?.mode === "multi") {
341
+ for (const id of selection.ids) {
342
+ ids.add(id);
343
+ }
344
+ }
345
+ }
346
+ return ids;
347
+ };
348
+ var SelectionManager = class {
349
+ _registry;
350
+ _state;
351
+ constructor(_registry, initial = {}) {
352
+ this._registry = _registry;
353
+ this._state = Atom2.make({
354
+ selections: {
355
+ ...initial
356
+ }
357
+ });
358
+ }
359
+ /**
360
+ * Get the state atom for reactive access in the graph system.
361
+ */
362
+ get state() {
363
+ return this._state;
364
+ }
365
+ /**
366
+ * Gets the current state.
367
+ */
368
+ getState() {
369
+ return this._registry.get(this._state);
370
+ }
371
+ /**
372
+ * Subscribe to changes in the selection state.
373
+ */
374
+ subscribe(cb) {
375
+ this._registry.get(this._state);
376
+ return this._registry.subscribe(this._state, () => {
377
+ cb(this._registry.get(this._state));
378
+ });
379
+ }
380
+ getSelectionContexts() {
381
+ return Object.keys(this._registry.get(this._state).selections);
382
+ }
383
+ getSelection(contextId, mode = void 0) {
384
+ const state = this._registry.get(this._state);
385
+ const selection = state.selections[contextId];
386
+ if (!mode || selection) {
387
+ return selection;
388
+ }
389
+ const newSelection = defaultSelection(mode);
390
+ this._registry.update(this._state, (state2) => ({
391
+ selections: {
392
+ ...state2.selections,
393
+ [contextId]: newSelection
394
+ }
395
+ }));
396
+ return newSelection;
397
+ }
398
+ // TODO(burdon): Disambiguate with getSelection?
399
+ getSelected(contextId, mode = "multi") {
400
+ const selection = this.getSelection(contextId, mode);
401
+ invariant(selection?.mode === mode, "Selection mode mismatch", {
402
+ F: __dxlog_file2,
403
+ L: 134,
404
+ S: this,
405
+ A: [
406
+ "selection?.mode === mode",
407
+ "'Selection mode mismatch'"
408
+ ]
409
+ });
410
+ return Match.type().pipe(Match.when({
411
+ mode: "single"
412
+ }, (s) => s.id), Match.when({
413
+ mode: "multi"
414
+ }, (s) => s.ids), Match.when({
415
+ mode: "range"
416
+ }, (s) => s.from && s.to ? {
417
+ from: s.from,
418
+ to: s.to
419
+ } : void 0), Match.when({
420
+ mode: "multi-range"
421
+ }, (s) => s.ranges), Match.exhaustive)(selection);
422
+ }
423
+ updateSingle(contextId, id) {
424
+ const selection = this.getSelection(contextId, "single");
425
+ invariant(selection?.mode === "single", "Selection mode is not single", {
426
+ F: __dxlog_file2,
427
+ L: 147,
428
+ S: this,
429
+ A: [
430
+ "selection?.mode === 'single'",
431
+ "'Selection mode is not single'"
432
+ ]
433
+ });
434
+ this._registry.update(this._state, (state) => ({
435
+ selections: {
436
+ ...state.selections,
437
+ [contextId]: {
438
+ ...selection,
439
+ id
440
+ }
441
+ }
442
+ }));
443
+ }
444
+ updateMulti(contextId, ids) {
445
+ const selection = this.getSelection(contextId, "multi");
446
+ invariant(selection?.mode === "multi", "Selection mode is not multi", {
447
+ F: __dxlog_file2,
448
+ L: 158,
449
+ S: this,
450
+ A: [
451
+ "selection?.mode === 'multi'",
452
+ "'Selection mode is not multi'"
453
+ ]
454
+ });
455
+ this._registry.update(this._state, (state) => ({
456
+ selections: {
457
+ ...state.selections,
458
+ [contextId]: {
459
+ ...selection,
460
+ ids: [
461
+ ...ids
462
+ ]
463
+ }
464
+ }
465
+ }));
466
+ }
467
+ updateRange(contextId, from, to) {
468
+ const selection = this.getSelection(contextId, "range");
469
+ invariant(selection?.mode === "range", "Selection mode is not range", {
470
+ F: __dxlog_file2,
471
+ L: 169,
472
+ S: this,
473
+ A: [
474
+ "selection?.mode === 'range'",
475
+ "'Selection mode is not range'"
476
+ ]
477
+ });
478
+ this._registry.update(this._state, (state) => ({
479
+ selections: {
480
+ ...state.selections,
481
+ [contextId]: {
482
+ ...selection,
483
+ from,
484
+ to
485
+ }
486
+ }
487
+ }));
488
+ }
489
+ updateMultiRange(contextId, ranges) {
490
+ const selection = this.getSelection(contextId, "multi-range");
491
+ invariant(selection?.mode === "multi-range", "Selection mode is not multi-range", {
492
+ F: __dxlog_file2,
493
+ L: 180,
494
+ S: this,
495
+ A: [
496
+ "selection?.mode === 'multi-range'",
497
+ "'Selection mode is not multi-range'"
498
+ ]
499
+ });
500
+ this._registry.update(this._state, (state) => ({
501
+ selections: {
502
+ ...state.selections,
503
+ [contextId]: {
504
+ ...selection,
505
+ ranges: [
506
+ ...ranges
507
+ ]
508
+ }
509
+ }
510
+ }));
511
+ }
512
+ clearSelection(contextId) {
513
+ const selection = this.getSelection(contextId);
514
+ if (!selection) {
515
+ return;
516
+ }
517
+ const clearedSelection = Match.type().pipe(Match.when({
518
+ mode: "single"
519
+ }, (s) => ({
520
+ ...s,
521
+ id: void 0
522
+ })), Match.when({
523
+ mode: "multi"
524
+ }, (s) => ({
525
+ ...s,
526
+ ids: []
527
+ })), Match.when({
528
+ mode: "range"
529
+ }, (s) => ({
530
+ ...s,
531
+ from: void 0,
532
+ to: void 0
533
+ })), Match.when({
534
+ mode: "multi-range"
535
+ }, (s) => ({
536
+ ...s,
537
+ ranges: []
538
+ })), Match.exhaustive)(selection);
539
+ this._registry.update(this._state, (state) => ({
540
+ selections: {
541
+ ...state.selections,
542
+ [contextId]: clearedSelection
543
+ }
544
+ }));
545
+ }
546
+ toggleSelection(contextId, id) {
547
+ const selection = this.getSelection(contextId, "multi");
548
+ invariant(selection?.mode === "multi", "Selection mode is not multi", {
549
+ F: __dxlog_file2,
550
+ L: 213,
551
+ S: this,
552
+ A: [
553
+ "selection?.mode === 'multi'",
554
+ "'Selection mode is not multi'"
555
+ ]
556
+ });
557
+ const newIds = selection.ids.includes(id) ? selection.ids.filter((existingId) => existingId !== id) : [
558
+ ...selection.ids,
559
+ id
560
+ ];
561
+ this._registry.update(this._state, (state) => ({
562
+ selections: {
563
+ ...state.selections,
564
+ [contextId]: {
565
+ ...selection,
566
+ ids: newIds
567
+ }
568
+ }
569
+ }));
570
+ }
571
+ };
572
+
573
+ // src/components/SelectionProvider.tsx
574
+ import { RegistryContext as RegistryContext2 } from "@effect-atom/atom-react";
575
+ import { createContext as createContext2 } from "@radix-ui/react-context";
576
+ import * as Match2 from "effect/Match";
577
+ import React2, { useCallback, useContext as useContext2, useEffect as useEffect2, useMemo as useMemo2, useState as useState2 } from "react";
578
+ import { useDefaultValue as useDefaultValue2 } from "@dxos/react-hooks";
579
+ var SELECTION_NAME = "Selection";
580
+ var [SelectionContextProvider, useSelectionContext] = createContext2(SELECTION_NAME, {
581
+ selection: void 0
582
+ });
583
+ var SelectionProvider = ({ children, selection: propsSelection }) => {
584
+ const registry = useContext2(RegistryContext2);
585
+ const selection = useDefaultValue2(propsSelection, () => new SelectionManager(registry));
586
+ return /* @__PURE__ */ React2.createElement(SelectionContextProvider, {
587
+ selection
588
+ }, children);
589
+ };
590
+ var useSelectionManager = () => {
591
+ const { selection } = useSelectionContext(SELECTION_NAME);
592
+ return selection;
593
+ };
594
+ var getDefaultResult = (mode) => {
595
+ return Match2.type().pipe(Match2.when({
596
+ mode: "single"
597
+ }, (selection) => selection.id), Match2.when({
598
+ mode: "multi"
599
+ }, (selection) => selection.ids), Match2.when({
600
+ mode: "range"
601
+ }, (selection) => selection.from && selection.to ? {
602
+ from: selection.from,
603
+ to: selection.to
604
+ } : void 0), Match2.when({
605
+ mode: "multi-range"
606
+ }, (selection) => selection.ranges), Match2.exhaustive)(defaultSelection(mode));
607
+ };
608
+ var useSelected = (contextId, mode = "multi") => {
609
+ const { selection } = useSelectionContext(SELECTION_NAME);
610
+ const [state, setState] = useState2(() => contextId && selection ? selection.getSelected(contextId, mode) : getDefaultResult(mode));
611
+ useEffect2(() => {
612
+ if (!contextId || !selection) {
613
+ setState(getDefaultResult(mode));
614
+ return;
615
+ }
616
+ setState(selection.getSelected(contextId, mode));
617
+ return selection.subscribe(() => {
618
+ setState(selection.getSelected(contextId, mode));
619
+ });
620
+ }, [
621
+ selection,
622
+ contextId,
623
+ mode
624
+ ]);
625
+ return state;
626
+ };
627
+ var useSelectionActions = (contextIds, mode = "multi") => {
628
+ const stableContextIds = useMemo2(() => contextIds, [
629
+ JSON.stringify(contextIds)
630
+ ]);
631
+ const { selection } = useSelectionContext(SELECTION_NAME);
632
+ const singleSelect = useCallback((id) => {
633
+ if (!selection) {
634
+ return;
635
+ }
636
+ for (const contextId of stableContextIds) {
637
+ selection.updateSingle(contextId, id);
638
+ }
639
+ }, [
640
+ selection,
641
+ stableContextIds
642
+ ]);
643
+ const multiSelect = useCallback((ids) => {
644
+ if (!selection) {
645
+ return;
646
+ }
647
+ for (const contextId of stableContextIds) {
648
+ selection.updateMulti(contextId, ids);
649
+ }
650
+ }, [
651
+ selection,
652
+ stableContextIds
653
+ ]);
654
+ const rangeSelect = useCallback((from, to) => {
655
+ if (!selection) {
656
+ return;
657
+ }
658
+ for (const contextId of stableContextIds) {
659
+ selection.updateRange(contextId, from, to);
660
+ }
661
+ }, [
662
+ selection,
663
+ stableContextIds
664
+ ]);
665
+ const toggle = useCallback((id) => {
666
+ if (!selection) {
667
+ return;
668
+ }
669
+ for (const contextId of stableContextIds) {
670
+ selection.toggleSelection(contextId, id);
671
+ }
672
+ }, [
673
+ selection,
674
+ stableContextIds
675
+ ]);
676
+ const clear = useCallback(() => {
677
+ if (!selection) {
678
+ return;
679
+ }
680
+ for (const contextId of stableContextIds) {
681
+ selection.clearSelection(contextId);
682
+ }
683
+ }, [
684
+ selection,
685
+ stableContextIds
686
+ ]);
687
+ return {
688
+ singleSelect,
689
+ multiSelect,
690
+ rangeSelect,
691
+ toggle,
692
+ clear
693
+ };
694
+ };
695
+
696
+ export {
697
+ AttentionManager,
698
+ getAttendables,
699
+ expandAttendableId,
700
+ isLinkedSegment,
701
+ linkedSegment,
702
+ getLinkedVariant,
703
+ getParentId,
704
+ getSegmentId,
705
+ ATTENTION_NAME,
706
+ ATTENDABLE_ATTRIBUTE,
707
+ ATTENTION_SOURCE_ATTRIBUTE,
708
+ useAttentionContext,
709
+ useAttention,
710
+ useAttended,
711
+ useAttentionAttributes,
712
+ RootAttentionProvider,
713
+ AttendableContainer,
714
+ SelectionSchema,
715
+ defaultSelection,
716
+ getSelectionSet,
717
+ SelectionManager,
718
+ SelectionProvider,
719
+ useSelectionManager,
720
+ useSelected,
721
+ useSelectionActions
722
+ };
723
+ //# sourceMappingURL=chunk-6PQ4P7AL.mjs.map