@dxos/react-ui-attention 0.9.1-staging.ee54ba693a → 0.11.0
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/chunk-ViewStateProvider.mjs +308 -0
- package/dist/lib/chunk-ViewStateProvider.mjs.map +1 -0
- package/dist/lib/chunk-types.mjs +351 -0
- package/dist/lib/chunk-types.mjs.map +1 -0
- package/dist/lib/index.mjs +149 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/testing.mjs +20 -0
- package/dist/lib/testing.mjs.map +1 -0
- package/dist/lib/types.mjs +2 -0
- package/dist/types/src/components/AttentionProvider/AttentionProvider.d.ts +2 -15
- package/dist/types/src/components/AttentionProvider/AttentionProvider.d.ts.map +1 -1
- package/dist/types/src/components/AttentionProvider/attention-context.d.ts +20 -0
- package/dist/types/src/components/AttentionProvider/attention-context.d.ts.map +1 -0
- package/dist/types/src/components/AttentionProvider/index.d.ts +1 -0
- package/dist/types/src/components/AttentionProvider/index.d.ts.map +1 -1
- package/dist/types/src/components/ViewStateProvider/ViewStateProvider.d.ts +2 -26
- package/dist/types/src/components/ViewStateProvider/ViewStateProvider.d.ts.map +1 -1
- package/dist/types/src/components/ViewStateProvider/index.d.ts +1 -0
- package/dist/types/src/components/ViewStateProvider/index.d.ts.map +1 -1
- package/dist/types/src/components/ViewStateProvider/view-state-hooks.d.ts +32 -0
- package/dist/types/src/components/ViewStateProvider/view-state-hooks.d.ts.map +1 -0
- package/dist/types/src/core/backends.d.ts +24 -0
- package/dist/types/src/core/backends.d.ts.map +1 -0
- package/dist/types/src/core/backends.test.d.ts.map +1 -0
- package/dist/types/src/core/index.d.ts +2 -0
- package/dist/types/src/core/index.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +2 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -0
- package/dist/types/src/hooks/useArticleKeyboardNavigation.d.ts.map +1 -0
- package/dist/types/src/hooks/useArticleKeyboardNavigation.test.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +3 -4
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/{attention.d.ts → types/Attention.d.ts} +13 -2
- package/dist/types/src/types/Attention.d.ts.map +1 -0
- package/dist/types/src/types/Attention.test.d.ts +2 -0
- package/dist/types/src/types/Attention.test.d.ts.map +1 -0
- package/dist/types/src/{selection.d.ts → types/Selection.d.ts} +12 -10
- package/dist/types/src/types/Selection.d.ts.map +1 -0
- package/dist/types/src/types/Selection.test.d.ts +2 -0
- package/dist/types/src/types/Selection.test.d.ts.map +1 -0
- package/dist/types/src/types/ViewState.d.ts +66 -0
- package/dist/types/src/types/ViewState.d.ts.map +1 -0
- package/dist/types/src/types/ViewState.test.d.ts +2 -0
- package/dist/types/src/types/ViewState.test.d.ts.map +1 -0
- package/dist/types/src/types/index.d.ts +3 -3
- package/dist/types/src/types/index.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +18 -20
- package/src/components/AttentionProvider/AttentionProvider.stories.tsx +1 -1
- package/src/components/AttentionProvider/AttentionProvider.tsx +4 -86
- package/src/components/AttentionProvider/attention-context.ts +81 -0
- package/src/components/AttentionProvider/index.ts +2 -0
- package/src/components/ViewStateProvider/ViewStateProvider.test.tsx +7 -5
- package/src/components/ViewStateProvider/ViewStateProvider.tsx +6 -110
- package/src/components/ViewStateProvider/index.ts +9 -0
- package/src/components/ViewStateProvider/view-state-hooks.ts +115 -0
- package/src/{view-state → core}/backends.test.ts +5 -5
- package/src/{view-state → core}/backends.ts +24 -17
- package/src/{view-state → core}/index.ts +0 -1
- package/src/hooks/index.ts +5 -0
- package/src/{useArticleKeyboardNavigation.ts → hooks/useArticleKeyboardNavigation.ts} +1 -1
- package/src/index.ts +3 -4
- package/src/testing/decorators/withAttention.ts +2 -2
- package/src/{attention.test.ts → types/Attention.test.ts} +2 -2
- package/src/{attention.ts → types/Attention.ts} +17 -2
- package/src/types/Selection.test.ts +100 -0
- package/src/{selection.ts → types/Selection.ts} +27 -17
- package/src/{view-state/view-state.test.ts → types/ViewState.test.ts} +6 -6
- package/src/types/ViewState.ts +101 -0
- package/src/types/index.ts +8 -7
- package/dist/lib/browser/chunk-4HYAJ4IO.mjs +0 -434
- package/dist/lib/browser/chunk-4HYAJ4IO.mjs.map +0 -7
- package/dist/lib/browser/chunk-I4FDY4AZ.mjs +0 -236
- package/dist/lib/browser/chunk-I4FDY4AZ.mjs.map +0 -7
- package/dist/lib/browser/index.mjs +0 -218
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/testing/index.mjs +0 -30
- package/dist/lib/browser/testing/index.mjs.map +0 -7
- package/dist/lib/browser/types/index.mjs +0 -43
- package/dist/lib/browser/types/index.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-DLBZMHST.mjs +0 -237
- package/dist/lib/node-esm/chunk-DLBZMHST.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-HWN7KJDX.mjs +0 -436
- package/dist/lib/node-esm/chunk-HWN7KJDX.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -219
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/testing/index.mjs +0 -31
- package/dist/lib/node-esm/testing/index.mjs.map +0 -7
- package/dist/lib/node-esm/types/index.mjs +0 -44
- package/dist/lib/node-esm/types/index.mjs.map +0 -7
- package/dist/types/src/attention.d.ts.map +0 -1
- package/dist/types/src/attention.test.d.ts +0 -2
- package/dist/types/src/attention.test.d.ts.map +0 -1
- package/dist/types/src/selection.d.ts.map +0 -1
- package/dist/types/src/selection.test.d.ts +0 -2
- package/dist/types/src/selection.test.d.ts.map +0 -1
- package/dist/types/src/useArticleKeyboardNavigation.d.ts.map +0 -1
- package/dist/types/src/useArticleKeyboardNavigation.test.d.ts.map +0 -1
- package/dist/types/src/view-state/backends.d.ts +0 -24
- package/dist/types/src/view-state/backends.d.ts.map +0 -1
- package/dist/types/src/view-state/backends.test.d.ts.map +0 -1
- package/dist/types/src/view-state/index.d.ts +0 -3
- package/dist/types/src/view-state/index.d.ts.map +0 -1
- package/dist/types/src/view-state/view-state.d.ts +0 -54
- package/dist/types/src/view-state/view-state.d.ts.map +0 -1
- package/dist/types/src/view-state/view-state.test.d.ts +0 -2
- package/dist/types/src/view-state/view-state.test.d.ts.map +0 -1
- package/src/selection.test.ts +0 -67
- package/src/view-state/view-state.ts +0 -91
- /package/dist/types/src/{view-state → core}/backends.test.d.ts +0 -0
- /package/dist/types/src/{useArticleKeyboardNavigation.d.ts → hooks/useArticleKeyboardNavigation.d.ts} +0 -0
- /package/dist/types/src/{useArticleKeyboardNavigation.test.d.ts → hooks/useArticleKeyboardNavigation.test.d.ts} +0 -0
- /package/src/{useArticleKeyboardNavigation.test.ts → hooks/useArticleKeyboardNavigation.test.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-attention",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Signifier components and providers for attention system.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -16,20 +16,17 @@
|
|
|
16
16
|
".": {
|
|
17
17
|
"source": "./src/index.ts",
|
|
18
18
|
"types": "./dist/types/src/index.d.ts",
|
|
19
|
-
"
|
|
20
|
-
"node": "./dist/lib/node-esm/index.mjs"
|
|
19
|
+
"import": "./dist/lib/index.mjs"
|
|
21
20
|
},
|
|
22
21
|
"./testing": {
|
|
23
22
|
"source": "./src/testing/index.ts",
|
|
24
23
|
"types": "./dist/types/src/testing/index.d.ts",
|
|
25
|
-
"
|
|
26
|
-
"node": "./dist/lib/node-esm/testing/index.mjs"
|
|
24
|
+
"import": "./dist/lib/testing.mjs"
|
|
27
25
|
},
|
|
28
26
|
"./types": {
|
|
29
27
|
"source": "./src/types/index.ts",
|
|
30
28
|
"types": "./dist/types/src/types/index.d.ts",
|
|
31
|
-
"
|
|
32
|
-
"node": "./dist/lib/node-esm/types/index.mjs"
|
|
29
|
+
"import": "./dist/lib/types.mjs"
|
|
33
30
|
}
|
|
34
31
|
},
|
|
35
32
|
"types": "dist/types/src/index.d.ts",
|
|
@@ -38,6 +35,7 @@
|
|
|
38
35
|
"src"
|
|
39
36
|
],
|
|
40
37
|
"dependencies": {
|
|
38
|
+
"@effect-atom/atom": "^0.5.3",
|
|
41
39
|
"@effect-atom/atom-react": "^0.5.0",
|
|
42
40
|
"@radix-ui/primitive": "1.1.1",
|
|
43
41
|
"@radix-ui/react-compose-refs": "1.1.1",
|
|
@@ -47,12 +45,12 @@
|
|
|
47
45
|
"@radix-ui/react-slot": "1.1.2",
|
|
48
46
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
|
49
47
|
"effect": "3.21.4",
|
|
50
|
-
"@dxos/
|
|
51
|
-
"@dxos/
|
|
52
|
-
"@dxos/
|
|
53
|
-
"@dxos/
|
|
54
|
-
"@dxos/
|
|
55
|
-
"@dxos/
|
|
48
|
+
"@dxos/invariant": "0.11.0",
|
|
49
|
+
"@dxos/keyboard": "0.11.0",
|
|
50
|
+
"@dxos/log": "0.11.0",
|
|
51
|
+
"@dxos/react-hooks": "0.11.0",
|
|
52
|
+
"@dxos/util": "0.11.0",
|
|
53
|
+
"@dxos/echo": "0.11.0"
|
|
56
54
|
},
|
|
57
55
|
"devDependencies": {
|
|
58
56
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -61,18 +59,18 @@
|
|
|
61
59
|
"@types/react-dom": "~19.2.3",
|
|
62
60
|
"react": "~19.2.7",
|
|
63
61
|
"react-dom": "~19.2.7",
|
|
64
|
-
"vite": "^8.
|
|
65
|
-
"@dxos/random": "0.
|
|
66
|
-
"@dxos/react-ui": "0.
|
|
67
|
-
"@dxos/storybook-utils": "0.
|
|
68
|
-
"@dxos/ui-theme": "0.
|
|
62
|
+
"vite": "^8.1.4",
|
|
63
|
+
"@dxos/random": "0.11.0",
|
|
64
|
+
"@dxos/react-ui": "0.11.0",
|
|
65
|
+
"@dxos/storybook-utils": "0.11.0",
|
|
66
|
+
"@dxos/ui-theme": "0.11.0"
|
|
69
67
|
},
|
|
70
68
|
"peerDependencies": {
|
|
71
69
|
"effect": "3.21.4",
|
|
72
70
|
"react": "~19.2.7",
|
|
73
71
|
"react-dom": "~19.2.7",
|
|
74
|
-
"@dxos/react-ui": "0.
|
|
75
|
-
"@dxos/ui-theme": "0.
|
|
72
|
+
"@dxos/react-ui": "^0.11.0",
|
|
73
|
+
"@dxos/ui-theme": "^0.11.0"
|
|
76
74
|
},
|
|
77
75
|
"publishConfig": {
|
|
78
76
|
"access": "public"
|
|
@@ -8,7 +8,7 @@ import React from 'react';
|
|
|
8
8
|
import { withTheme } from '@dxos/react-ui/testing';
|
|
9
9
|
|
|
10
10
|
import { withAttention } from '../../testing';
|
|
11
|
-
import { useAttentionAttributes } from './
|
|
11
|
+
import { useAttentionAttributes } from './attention-context';
|
|
12
12
|
|
|
13
13
|
const Attendable = ({ id }: { id: string }) => {
|
|
14
14
|
const attentionAttrs = useAttentionAttributes(id);
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { RegistryContext } from '@effect-atom/atom-react';
|
|
6
|
-
import { createContext } from '@radix-ui/react-context';
|
|
7
6
|
import { Primitive } from '@radix-ui/react-primitive';
|
|
8
7
|
import { Slot } from '@radix-ui/react-slot';
|
|
9
8
|
import React, {
|
|
@@ -13,9 +12,6 @@ import React, {
|
|
|
13
12
|
forwardRef,
|
|
14
13
|
useCallback,
|
|
15
14
|
useContext,
|
|
16
|
-
useEffect,
|
|
17
|
-
useMemo,
|
|
18
|
-
useState,
|
|
19
15
|
} from 'react';
|
|
20
16
|
|
|
21
17
|
import { log } from '@dxos/log';
|
|
@@ -23,78 +19,8 @@ import { useDefaultValue } from '@dxos/react-hooks';
|
|
|
23
19
|
import { type ThemedClassName } from '@dxos/react-ui';
|
|
24
20
|
import { mx } from '@dxos/ui-theme';
|
|
25
21
|
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
const ATTENTION_NAME = 'Attention';
|
|
29
|
-
const ATTENDABLE_ATTRIBUTE = 'data-attendable-id';
|
|
30
|
-
const ATTENTION_SOURCE_ATTRIBUTE = 'data-w-attention-source';
|
|
31
|
-
|
|
32
|
-
type AttentionContextValue = {
|
|
33
|
-
attention: AttentionManager;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const [AttentionContextProvider, useAttentionContext] = createContext<AttentionContextValue>(ATTENTION_NAME, {
|
|
37
|
-
attention: undefined as unknown as AttentionManager,
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const UNKNOWN_ATTENDABLE = { hasAttention: false, isAncestor: false, isRelated: false } as Attention;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Subscribe to the attention state for a qualified graph ID.
|
|
44
|
-
*/
|
|
45
|
-
// TODO(burdon): Unify with seleciton and change to contextId?
|
|
46
|
-
const useAttention = (attendableId?: string): Attention => {
|
|
47
|
-
const { attention } = useAttentionContext(ATTENTION_NAME);
|
|
48
|
-
const [state, setState] = useState<Attention>(UNKNOWN_ATTENDABLE);
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
if (!attendableId || !attention) {
|
|
51
|
-
setState(UNKNOWN_ATTENDABLE);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const currentState = attention.get(attendableId);
|
|
56
|
-
setState(currentState);
|
|
57
|
-
|
|
58
|
-
return attention.subscribe(attendableId, (newState) => {
|
|
59
|
-
setState(newState);
|
|
60
|
-
});
|
|
61
|
-
}, [attention, attendableId]);
|
|
62
|
-
|
|
63
|
-
return state;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const useAttended = () => {
|
|
67
|
-
const { attention } = useAttentionContext(ATTENTION_NAME);
|
|
68
|
-
const [current, setCurrent] = useState<readonly string[]>([]);
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
if (!attention) {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
setCurrent(attention.getCurrent());
|
|
75
|
-
|
|
76
|
-
return attention.subscribeCurrent((newCurrent) => {
|
|
77
|
-
setCurrent(newCurrent);
|
|
78
|
-
});
|
|
79
|
-
}, [attention]);
|
|
80
|
-
|
|
81
|
-
return current;
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Computes HTML element attributes to apply so the attention system can detect changes.
|
|
86
|
-
*/
|
|
87
|
-
const useAttentionAttributes = (attendableId?: string) => {
|
|
88
|
-
const { hasAttention } = useAttention(attendableId);
|
|
89
|
-
return useMemo(() => {
|
|
90
|
-
const attributes: Record<string, string | undefined> = { [ATTENDABLE_ATTRIBUTE]: attendableId };
|
|
91
|
-
if (hasAttention) {
|
|
92
|
-
attributes[ATTENTION_SOURCE_ATTRIBUTE] = 'true';
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return attributes;
|
|
96
|
-
}, [attendableId, hasAttention]);
|
|
97
|
-
};
|
|
22
|
+
import { ATTENDABLE_SELECTOR, AttentionManager, getAttendables } from '../../types/Attention';
|
|
23
|
+
import { AttentionContextProvider, useAttentionAttributes } from './attention-context';
|
|
98
24
|
|
|
99
25
|
type RootAttentionProviderProps = PropsWithChildren<{
|
|
100
26
|
attention?: AttentionManager;
|
|
@@ -111,7 +37,7 @@ const RootAttentionProvider = ({ children, attention: propsAttention, onChange }
|
|
|
111
37
|
log('focus', { related: event.relatedTarget, target: event.target });
|
|
112
38
|
|
|
113
39
|
const selector = [
|
|
114
|
-
|
|
40
|
+
ATTENDABLE_SELECTOR,
|
|
115
41
|
...Array.from(document.querySelectorAll('[aria-controls]')).map(
|
|
116
42
|
(el) => `[id="${el.getAttribute('aria-controls')}"]`,
|
|
117
43
|
),
|
|
@@ -154,7 +80,6 @@ const AttendableContainer = forwardRef<HTMLDivElement, AttendableContainerProps>
|
|
|
154
80
|
const Comp = asChild ? Slot : Primitive.div;
|
|
155
81
|
return (
|
|
156
82
|
<Comp
|
|
157
|
-
role='none'
|
|
158
83
|
{...props}
|
|
159
84
|
{...attentionAttrs}
|
|
160
85
|
className={mx('dx-attention-surface', props.tabIndex === 0 && 'dx-focus-ring-inset-over-all', classNames)}
|
|
@@ -166,11 +91,4 @@ const AttendableContainer = forwardRef<HTMLDivElement, AttendableContainerProps>
|
|
|
166
91
|
},
|
|
167
92
|
);
|
|
168
93
|
|
|
169
|
-
export {
|
|
170
|
-
AttendableContainer,
|
|
171
|
-
RootAttentionProvider,
|
|
172
|
-
useAttended,
|
|
173
|
-
useAttention,
|
|
174
|
-
useAttentionAttributes,
|
|
175
|
-
useAttentionContext,
|
|
176
|
-
};
|
|
94
|
+
export { AttendableContainer, RootAttentionProvider };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { createContext } from '@radix-ui/react-context';
|
|
6
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { ATTENDABLE_ATTRIBUTE, type Attention, AttentionManager } from '../../types/Attention';
|
|
9
|
+
|
|
10
|
+
// Kept out of `AttentionProvider.tsx`: react-refresh only fast-refreshes a module whose exports are
|
|
11
|
+
// all components, so the context and hooks exported beside them force a full page reload on every edit.
|
|
12
|
+
|
|
13
|
+
export const ATTENTION_NAME = 'Attention';
|
|
14
|
+
export const ATTENTION_SOURCE_ATTRIBUTE = 'data-w-attention-source';
|
|
15
|
+
|
|
16
|
+
export type AttentionContextValue = {
|
|
17
|
+
attention: AttentionManager;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const [AttentionContextProvider, useAttentionContext] = createContext<AttentionContextValue>(ATTENTION_NAME, {
|
|
21
|
+
attention: undefined as unknown as AttentionManager,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const UNKNOWN_ATTENDABLE = { hasAttention: false, isAncestor: false, isRelated: false } as Attention;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Subscribe to the attention state for a qualified graph ID.
|
|
28
|
+
*/
|
|
29
|
+
// TODO(burdon): Unify with selection state and change to contextId?
|
|
30
|
+
export const useAttention = (attendableId?: string): Attention => {
|
|
31
|
+
const { attention } = useAttentionContext(ATTENTION_NAME);
|
|
32
|
+
const [state, setState] = useState<Attention>(UNKNOWN_ATTENDABLE);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!attendableId || !attention) {
|
|
35
|
+
setState(UNKNOWN_ATTENDABLE);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const currentState = attention.get(attendableId);
|
|
40
|
+
setState(currentState);
|
|
41
|
+
|
|
42
|
+
return attention.subscribe(attendableId, (newState) => {
|
|
43
|
+
setState(newState);
|
|
44
|
+
});
|
|
45
|
+
}, [attention, attendableId]);
|
|
46
|
+
|
|
47
|
+
return state;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const useAttended = () => {
|
|
51
|
+
const { attention } = useAttentionContext(ATTENTION_NAME);
|
|
52
|
+
const [current, setCurrent] = useState<readonly string[]>([]);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!attention) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setCurrent(attention.getCurrent());
|
|
59
|
+
|
|
60
|
+
return attention.subscribeCurrent((newCurrent) => {
|
|
61
|
+
setCurrent(newCurrent);
|
|
62
|
+
});
|
|
63
|
+
}, [attention]);
|
|
64
|
+
|
|
65
|
+
return current;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Computes HTML element attributes to apply so the attention system can detect changes.
|
|
70
|
+
*/
|
|
71
|
+
export const useAttentionAttributes = (attendableId?: string) => {
|
|
72
|
+
const { hasAttention } = useAttention(attendableId);
|
|
73
|
+
return useMemo(() => {
|
|
74
|
+
const attributes: Record<string, string | undefined> = { [ATTENDABLE_ATTRIBUTE]: attendableId };
|
|
75
|
+
if (hasAttention) {
|
|
76
|
+
attributes[ATTENTION_SOURCE_ATTRIBUTE] = 'true';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return attributes;
|
|
80
|
+
}, [attendableId, hasAttention]);
|
|
81
|
+
};
|
|
@@ -7,13 +7,15 @@ import { act, renderHook } from '@testing-library/react';
|
|
|
7
7
|
import React, { type PropsWithChildren } from 'react';
|
|
8
8
|
import { describe, test } from 'vitest';
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { createDefaultBackends } from '../../core';
|
|
11
|
+
import { ViewState } from '../../types';
|
|
12
|
+
import { useSelection, useSelectionActions } from './view-state-hooks';
|
|
13
|
+
import { ViewStateProvider } from './ViewStateProvider';
|
|
12
14
|
|
|
13
15
|
describe('useSelection / useSelectionActions', () => {
|
|
14
16
|
test('single select updates the resolved value', ({ expect }) => {
|
|
15
17
|
const registry = Registry.make();
|
|
16
|
-
const manager = new
|
|
18
|
+
const manager = new ViewState.Manager({ registry, backends: createDefaultBackends(registry) });
|
|
17
19
|
const Wrapper = wrapper(manager, registry);
|
|
18
20
|
const { result: value } = renderHook(() => useSelection('ctx', 'single'), { wrapper: Wrapper });
|
|
19
21
|
const { result: actions } = renderHook(() => useSelectionActions('ctx'), { wrapper: Wrapper });
|
|
@@ -24,7 +26,7 @@ describe('useSelection / useSelectionActions', () => {
|
|
|
24
26
|
|
|
25
27
|
test('toggle within multi selection', ({ expect }) => {
|
|
26
28
|
const registry = Registry.make();
|
|
27
|
-
const manager = new
|
|
29
|
+
const manager = new ViewState.Manager({ registry, backends: createDefaultBackends(registry) });
|
|
28
30
|
const Wrapper = wrapper(manager, registry);
|
|
29
31
|
const { result: value } = renderHook(() => useSelection('ctx', 'multi'), { wrapper: Wrapper });
|
|
30
32
|
const { result: actions } = renderHook(() => useSelectionActions('ctx'), { wrapper: Wrapper });
|
|
@@ -37,7 +39,7 @@ describe('useSelection / useSelectionActions', () => {
|
|
|
37
39
|
});
|
|
38
40
|
|
|
39
41
|
const wrapper =
|
|
40
|
-
(manager:
|
|
42
|
+
(manager: ViewState.Manager, registry: Registry.Registry) =>
|
|
41
43
|
({ children }: PropsWithChildren) => (
|
|
42
44
|
<RegistryContext.Provider value={registry}>
|
|
43
45
|
<ViewStateProvider manager={manager}>{children}</ViewStateProvider>
|
|
@@ -3,127 +3,23 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { RegistryContext } from '@effect-atom/atom-react';
|
|
6
|
-
import {
|
|
7
|
-
import React, { type PropsWithChildren, useContext, useEffect, useMemo, useState } from 'react';
|
|
6
|
+
import React, { type PropsWithChildren, useContext } from 'react';
|
|
8
7
|
|
|
9
|
-
import { invariant } from '@dxos/invariant';
|
|
10
8
|
import { useDefaultValue } from '@dxos/react-hooks';
|
|
11
9
|
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
resolveSelection,
|
|
16
|
-
selectionAspect,
|
|
17
|
-
toggleSelection,
|
|
18
|
-
} from '../../selection';
|
|
19
|
-
import { type AspectDef, ViewStateManager, createDefaultBackends } from '../../view-state';
|
|
20
|
-
|
|
21
|
-
const VIEW_STATE_NAME = 'ViewState';
|
|
22
|
-
|
|
23
|
-
type ViewStateContextValue = { manager?: ViewStateManager };
|
|
24
|
-
|
|
25
|
-
// Default value lets consumers render outside a provider (isolated stories/tests) without throwing;
|
|
26
|
-
// `manager` reads as `undefined` and hooks fall back to aspect defaults / no-op actions.
|
|
27
|
-
const [ViewStateContextProvider, useViewStateContext] = createContext<ViewStateContextValue>(VIEW_STATE_NAME, {
|
|
28
|
-
manager: undefined,
|
|
29
|
-
});
|
|
10
|
+
import { createDefaultBackends } from '../../core';
|
|
11
|
+
import { ViewState } from '../../types';
|
|
12
|
+
import { ViewStateContextProvider } from './view-state-hooks';
|
|
30
13
|
|
|
31
14
|
/** Provides the per-context UI state manager. Replaces the former `SelectionProvider`. */
|
|
32
15
|
export const ViewStateProvider = ({
|
|
33
16
|
children,
|
|
34
17
|
manager: managerProp,
|
|
35
|
-
}: PropsWithChildren<{ manager?:
|
|
18
|
+
}: PropsWithChildren<{ manager?: ViewState.Manager }>) => {
|
|
36
19
|
const registry = useContext(RegistryContext);
|
|
37
20
|
const manager = useDefaultValue(
|
|
38
21
|
managerProp,
|
|
39
|
-
() => new
|
|
22
|
+
() => new ViewState.Manager({ registry, backends: createDefaultBackends(registry) }),
|
|
40
23
|
);
|
|
41
24
|
return <ViewStateContextProvider manager={manager}>{children}</ViewStateContextProvider>;
|
|
42
25
|
};
|
|
43
|
-
|
|
44
|
-
/** Access the underlying ViewStateManager from context. Throws when used outside a `ViewStateProvider`. */
|
|
45
|
-
export const useViewStateManager = (): ViewStateManager => {
|
|
46
|
-
const { manager } = useViewStateContext(VIEW_STATE_NAME);
|
|
47
|
-
invariant(manager, 'useViewStateManager() requires a ViewStateProvider ancestor.');
|
|
48
|
-
return manager;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
/** Access the ViewStateManager if a provider is present; `undefined` otherwise (e.g. isolated stories/tests). */
|
|
52
|
-
export const useViewStateManagerOptional = (): ViewStateManager | undefined => {
|
|
53
|
-
const { manager } = useViewStateContext(VIEW_STATE_NAME);
|
|
54
|
-
return manager;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/** Reactive read of an aspect value for a context; yields the aspect default when unset or unprovided. */
|
|
58
|
-
export const useViewState = <T,>(aspect: AspectDef<T>, contextId?: string): T => {
|
|
59
|
-
const { manager } = useViewStateContext(VIEW_STATE_NAME);
|
|
60
|
-
const [value, setValue] = useState<T>(() =>
|
|
61
|
-
contextId && manager ? manager.get(aspect, contextId) : aspect.defaultValue(),
|
|
62
|
-
);
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
if (!contextId || !manager) {
|
|
65
|
-
setValue(aspect.defaultValue());
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
setValue(manager.get(aspect, contextId));
|
|
69
|
-
return manager.subscribe(aspect, contextId, setValue);
|
|
70
|
-
}, [manager, aspect, contextId]);
|
|
71
|
-
return value;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
export type UseViewStateActions<T> = {
|
|
75
|
-
set: (value: T) => void;
|
|
76
|
-
update: (fn: (prev: T) => T) => void;
|
|
77
|
-
clear: () => void;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export const useViewStateActions = <T,>(aspect: AspectDef<T>, contextId?: string): UseViewStateActions<T> => {
|
|
81
|
-
const { manager } = useViewStateContext(VIEW_STATE_NAME);
|
|
82
|
-
return useMemo<UseViewStateActions<T>>(
|
|
83
|
-
() => ({
|
|
84
|
-
set: (value) => {
|
|
85
|
-
if (contextId) {
|
|
86
|
-
manager?.set(aspect, contextId, value);
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
update: (fn) => {
|
|
90
|
-
if (contextId) {
|
|
91
|
-
manager?.update(aspect, contextId, fn);
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
clear: () => {
|
|
95
|
-
if (contextId) {
|
|
96
|
-
manager?.set(aspect, contextId, aspect.defaultValue());
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
}),
|
|
100
|
-
[manager, aspect, contextId],
|
|
101
|
-
);
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
/** Resolved selection value for `contextId` in the requested `mode` (default `multi`). */
|
|
105
|
-
export const useSelection = <T extends SelectionMode>(contextId?: string, mode: T = 'multi' as T): SelectionResult<T> =>
|
|
106
|
-
resolveSelection(useViewState(selectionAspect, contextId), mode);
|
|
107
|
-
|
|
108
|
-
export type UseSelectionActions = {
|
|
109
|
-
single: (id: string) => void;
|
|
110
|
-
multi: (ids: string[]) => void;
|
|
111
|
-
range: (from: string, to: string) => void;
|
|
112
|
-
toggle: (id: string) => void;
|
|
113
|
-
clear: () => void;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
/** Selection mutators for a single context, built on the generic ViewState actions. */
|
|
117
|
-
export const useSelectionActions = (contextId?: string): UseSelectionActions => {
|
|
118
|
-
const { update, clear } = useViewStateActions(selectionAspect, contextId);
|
|
119
|
-
return useMemo<UseSelectionActions>(
|
|
120
|
-
() => ({
|
|
121
|
-
single: (id) => update(() => ({ mode: 'single', id })),
|
|
122
|
-
multi: (ids) => update(() => ({ mode: 'multi', ids: [...ids] })),
|
|
123
|
-
range: (from, to) => update(() => ({ mode: 'range', from, to })),
|
|
124
|
-
toggle: (id) => update((prev) => toggleSelection(prev, id)),
|
|
125
|
-
clear,
|
|
126
|
-
}),
|
|
127
|
-
[update, clear],
|
|
128
|
-
);
|
|
129
|
-
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { createContext } from '@radix-ui/react-context';
|
|
6
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { invariant } from '@dxos/invariant';
|
|
9
|
+
|
|
10
|
+
import { Selection, ViewState } from '../../types';
|
|
11
|
+
|
|
12
|
+
// Kept out of `ViewStateProvider.tsx`: react-refresh only fast-refreshes a module whose exports are
|
|
13
|
+
// all components, so the context and hooks exported beside it force a full page reload on every edit.
|
|
14
|
+
|
|
15
|
+
export const VIEW_STATE_NAME = 'ViewState';
|
|
16
|
+
|
|
17
|
+
export type ViewStateContextValue = { manager?: ViewState.Manager };
|
|
18
|
+
|
|
19
|
+
// Default value lets consumers render outside a provider (isolated stories/tests) without throwing;
|
|
20
|
+
// `manager` reads as `undefined` and hooks fall back to aspect defaults / no-op actions.
|
|
21
|
+
export const [ViewStateContextProvider, useViewStateContext] = createContext<ViewStateContextValue>(VIEW_STATE_NAME, {
|
|
22
|
+
manager: undefined,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/** Access the underlying Manager from context. Throws when used outside a `ViewStateProvider`. */
|
|
26
|
+
export const useManager = (): ViewState.Manager => {
|
|
27
|
+
const { manager } = useViewStateContext(VIEW_STATE_NAME);
|
|
28
|
+
invariant(manager, 'useManager() requires a ViewStateProvider ancestor.');
|
|
29
|
+
return manager;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Access the Manager if a provider is present; `undefined` otherwise (e.g. isolated stories/tests). */
|
|
33
|
+
export const useManagerOptional = (): ViewState.Manager | undefined => {
|
|
34
|
+
const { manager } = useViewStateContext(VIEW_STATE_NAME);
|
|
35
|
+
return manager;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Reactive read of an aspect value for a context; yields the aspect default when unset or unprovided. */
|
|
39
|
+
export const useViewState = <T, Encoded = T>(aspect: ViewState.Aspect<T, Encoded>, contextId?: string): T => {
|
|
40
|
+
const { manager } = useViewStateContext(VIEW_STATE_NAME);
|
|
41
|
+
const [value, setValue] = useState<T>(() =>
|
|
42
|
+
contextId && manager ? manager.get(aspect, contextId) : aspect.defaultValue(),
|
|
43
|
+
);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!contextId || !manager) {
|
|
46
|
+
setValue(aspect.defaultValue());
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
setValue(manager.get(aspect, contextId));
|
|
50
|
+
return manager.subscribe(aspect, contextId, setValue);
|
|
51
|
+
}, [manager, aspect, contextId]);
|
|
52
|
+
return value;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type UseViewStateActions<T> = {
|
|
56
|
+
set: (value: T) => void;
|
|
57
|
+
update: (fn: (prev: T) => T) => void;
|
|
58
|
+
clear: () => void;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const useViewStateActions = <T, Encoded = T>(
|
|
62
|
+
aspect: ViewState.Aspect<T, Encoded>,
|
|
63
|
+
contextId?: string,
|
|
64
|
+
): UseViewStateActions<T> => {
|
|
65
|
+
const { manager } = useViewStateContext(VIEW_STATE_NAME);
|
|
66
|
+
return useMemo<UseViewStateActions<T>>(
|
|
67
|
+
() => ({
|
|
68
|
+
set: (value) => {
|
|
69
|
+
if (contextId) {
|
|
70
|
+
manager?.set(aspect, contextId, value);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
update: (fn) => {
|
|
74
|
+
if (contextId) {
|
|
75
|
+
manager?.update(aspect, contextId, fn);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
clear: () => {
|
|
79
|
+
if (contextId) {
|
|
80
|
+
manager?.set(aspect, contextId, aspect.defaultValue());
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
[manager, aspect, contextId],
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** Resolved selection value for `contextId` in the requested `mode` (default `multi`). */
|
|
89
|
+
export const useSelection = <T extends Selection.SelectionMode>(
|
|
90
|
+
contextId?: string,
|
|
91
|
+
mode: T = 'multi' as T,
|
|
92
|
+
): Selection.Result<T> => Selection.resolve(useViewState(Selection.aspect, contextId), mode);
|
|
93
|
+
|
|
94
|
+
export type UseSelectionActions = {
|
|
95
|
+
single: (id: string) => void;
|
|
96
|
+
multi: (ids: string[]) => void;
|
|
97
|
+
range: (from: string, to: string) => void;
|
|
98
|
+
toggle: (id: string) => void;
|
|
99
|
+
clear: () => void;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** Selection mutators for a single context, built on the generic ViewState actions. */
|
|
103
|
+
export const useSelectionActions = (contextId?: string): UseSelectionActions => {
|
|
104
|
+
const { update, clear } = useViewStateActions(Selection.aspect, contextId);
|
|
105
|
+
return useMemo<UseSelectionActions>(
|
|
106
|
+
() => ({
|
|
107
|
+
single: (id) => update(() => ({ mode: 'single', id })),
|
|
108
|
+
multi: (ids) => update(() => ({ mode: 'multi', ids: [...ids] })),
|
|
109
|
+
range: (from, to) => update(() => ({ mode: 'range', from, to })),
|
|
110
|
+
toggle: (id) => update((prev) => Selection.toggle(prev, id)),
|
|
111
|
+
clear,
|
|
112
|
+
}),
|
|
113
|
+
[update, clear],
|
|
114
|
+
);
|
|
115
|
+
};
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
// Copyright 2026 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { Registry } from '@effect-atom/atom
|
|
5
|
+
import { Registry } from '@effect-atom/atom';
|
|
6
6
|
import * as Schema from 'effect/Schema';
|
|
7
7
|
import { afterEach, describe, test } from 'vitest';
|
|
8
8
|
|
|
9
|
+
import { ViewState } from '../types';
|
|
9
10
|
import { LocalBackend } from './backends';
|
|
10
|
-
import { ViewStateManager, defineViewState } from './view-state';
|
|
11
11
|
|
|
12
|
-
const Editor =
|
|
12
|
+
const Editor = ViewState.define({
|
|
13
13
|
key: 'editor',
|
|
14
14
|
backend: 'local',
|
|
15
15
|
schema: Schema.Struct({ scrollTo: Schema.optional(Schema.Number) }).pipe(Schema.mutable),
|
|
@@ -43,7 +43,7 @@ describe('LocalBackend', () => {
|
|
|
43
43
|
const storage = fakeStorage();
|
|
44
44
|
const local = new LocalBackend({ registry, storage });
|
|
45
45
|
disposables.push(local);
|
|
46
|
-
const manager = new
|
|
46
|
+
const manager = new ViewState.Manager({ registry, backends: { memory: local, local } });
|
|
47
47
|
return { manager, storage };
|
|
48
48
|
};
|
|
49
49
|
|
|
@@ -87,7 +87,7 @@ describe('LocalBackend', () => {
|
|
|
87
87
|
const registry = Registry.make();
|
|
88
88
|
const storage = fakeStorage();
|
|
89
89
|
const local = new LocalBackend({ registry, storage });
|
|
90
|
-
const manager = new
|
|
90
|
+
const manager = new ViewState.Manager({ registry, backends: { memory: local, local } });
|
|
91
91
|
|
|
92
92
|
// Touch the context so its atom is registered and observed by the storage listener.
|
|
93
93
|
const key = 'dxos:view-state:editor:doc-4';
|