atom.io 0.3.1 → 0.4.1

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 (54) hide show
  1. package/README.md +11 -3
  2. package/dist/index.d.ts +84 -30
  3. package/dist/index.js +427 -230
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +427 -230
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +15 -5
  8. package/react/dist/index.d.ts +12 -17
  9. package/react/dist/index.js +25 -34
  10. package/react/dist/index.js.map +1 -1
  11. package/react/dist/index.mjs +21 -34
  12. package/react/dist/index.mjs.map +1 -1
  13. package/react-devtools/dist/index.css +26 -0
  14. package/react-devtools/dist/index.css.map +1 -0
  15. package/react-devtools/dist/index.d.ts +15 -0
  16. package/react-devtools/dist/index.js +1582 -0
  17. package/react-devtools/dist/index.js.map +1 -0
  18. package/react-devtools/dist/index.mjs +1554 -0
  19. package/react-devtools/dist/index.mjs.map +1 -0
  20. package/react-devtools/package.json +15 -0
  21. package/src/index.ts +3 -3
  22. package/src/internal/atom-internal.ts +10 -5
  23. package/src/internal/families-internal.ts +4 -4
  24. package/src/internal/get.ts +8 -8
  25. package/src/internal/index.ts +2 -0
  26. package/src/internal/meta/attach-meta.ts +17 -0
  27. package/src/internal/meta/index.ts +4 -0
  28. package/src/internal/meta/meta-state.ts +135 -0
  29. package/src/internal/meta/meta-timelines.ts +1 -0
  30. package/src/internal/meta/meta-transactions.ts +1 -0
  31. package/src/internal/operation.ts +0 -1
  32. package/src/internal/selector-internal.ts +34 -13
  33. package/src/internal/store.ts +35 -5
  34. package/src/internal/time-travel-internal.ts +89 -0
  35. package/src/internal/timeline-internal.ts +23 -103
  36. package/src/internal/transaction-internal.ts +14 -5
  37. package/src/react/index.ts +28 -46
  38. package/src/react-devtools/AtomIODevtools.tsx +107 -0
  39. package/src/react-devtools/StateEditor.tsx +73 -0
  40. package/src/react-devtools/TokenList.tsx +57 -0
  41. package/src/react-devtools/devtools.scss +130 -0
  42. package/src/react-devtools/index.ts +1 -0
  43. package/src/react-explorer/AtomIOExplorer.tsx +208 -0
  44. package/src/react-explorer/explorer-effects.ts +20 -0
  45. package/src/react-explorer/explorer-states.ts +224 -0
  46. package/src/react-explorer/index.ts +23 -0
  47. package/src/react-explorer/space-states.ts +73 -0
  48. package/src/react-explorer/view-states.ts +43 -0
  49. package/src/selector.ts +6 -6
  50. package/src/subscribe.ts +2 -2
  51. package/src/timeline.ts +1 -5
  52. package/src/transaction.ts +4 -2
  53. package/src/web-effects/index.ts +1 -0
  54. package/src/web-effects/storage.ts +30 -0
@@ -1,64 +1,46 @@
1
- import type Preact from "preact/hooks"
1
+ import { useSyncExternalStore } from "react"
2
2
 
3
- import type React from "react"
4
-
5
- import { subscribe, setState, __INTERNAL__ } from "atom.io"
6
- import type { ReadonlyValueToken, StateToken } from "atom.io"
3
+ import { subscribe, setState, __INTERNAL__, getState } from "atom.io"
4
+ import type { ReadonlySelectorToken, StateToken } from "atom.io"
7
5
 
8
6
  import type { Modifier } from "~/packages/anvl/src/function"
9
7
 
10
- export type AtomStoreReactConfig = {
11
- useState: typeof Preact.useState | typeof React.useState
12
- useEffect: typeof Preact.useEffect | typeof React.useEffect
13
- store?: __INTERNAL__.Store
8
+ export type StoreHooks = {
9
+ useI: <T>(token: StateToken<T>) => (next: Modifier<T> | T) => void
10
+ useO: <T>(token: ReadonlySelectorToken<T> | StateToken<T>) => T
11
+ useIO: <T>(token: StateToken<T>) => [T, (next: Modifier<T> | T) => void]
14
12
  }
15
13
 
16
- /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
17
- export const composeStoreHooks = ({
18
- useState,
19
- useEffect,
20
- store = __INTERNAL__.IMPLICIT.STORE,
21
- }: AtomStoreReactConfig) => {
14
+ export const composeStoreHooks = (
15
+ store: __INTERNAL__.Store = __INTERNAL__.IMPLICIT.STORE
16
+ ): StoreHooks => {
22
17
  function useI<T>(token: StateToken<T>): (next: Modifier<T> | T) => void {
23
18
  const updateState = (next: Modifier<T> | T) => setState(token, next, store)
24
19
  return updateState
25
20
  }
26
21
 
27
- function useO<T>(token: ReadonlyValueToken<T> | StateToken<T>): T {
28
- const state = __INTERNAL__.withdraw(token, store)
29
- const initialValue = __INTERNAL__.getState__INTERNAL(state, store)
30
- const [current, dispatch] = useState(initialValue)
31
- useEffect(() => {
32
- const unsubscribe = subscribe(
33
- token,
34
- ({ newValue, oldValue }) => {
35
- if (oldValue !== newValue) {
36
- dispatch(newValue)
37
- }
38
- },
39
- store
40
- )
41
- return unsubscribe
42
- }, [])
43
-
44
- return current
22
+ function useO<T>(token: ReadonlySelectorToken<T> | StateToken<T>): T {
23
+ return useSyncExternalStore<T>(
24
+ (observe) => subscribe(token, observe, store),
25
+ () => getState(token, store)
26
+ )
45
27
  }
46
28
 
47
29
  function useIO<T>(token: StateToken<T>): [T, (next: Modifier<T> | T) => void] {
48
30
  return [useO(token), useI(token)]
49
31
  }
50
32
 
51
- function useStore<T>(
52
- token: StateToken<T>
53
- ): [T, (next: Modifier<T> | T) => void]
54
- function useStore<T>(token: ReadonlyValueToken<T>): T
55
- function useStore<T>(
56
- token: ReadonlyValueToken<T> | StateToken<T>
57
- ): T | [T, (next: Modifier<T> | T) => void] {
58
- if (token.type === `readonly_selector`) {
59
- return useO(token)
60
- }
61
- return useIO(token)
62
- }
63
- return { useI, useO, useIO, useStore }
33
+ return { useI, useO, useIO }
34
+ }
35
+
36
+ export const { useI, useO, useIO } = composeStoreHooks()
37
+
38
+ export function useStore<T>(
39
+ token: StateToken<T>
40
+ ): [T, (next: Modifier<T> | T) => void]
41
+ export function useStore<T>(token: ReadonlySelectorToken<T>): T
42
+ export function useStore<T>(
43
+ token: ReadonlySelectorToken<T> | StateToken<T>
44
+ ): T | [T, (next: Modifier<T> | T) => void] {
45
+ return token.type === `readonly_selector` ? useO(token) : useIO(token)
64
46
  }
@@ -0,0 +1,107 @@
1
+ import type { FC } from "react"
2
+ import { useRef } from "react"
3
+
4
+ import { atom, __INTERNAL__ } from "atom.io"
5
+ import { useI, useO, useIO } from "atom.io/react"
6
+ import type { StoreHooks } from "atom.io/react"
7
+ import { LayoutGroup, motion, spring } from "framer-motion"
8
+
9
+ import { TokenList } from "./TokenList"
10
+ import { lazyLocalStorageEffect } from "../web-effects"
11
+
12
+ import "./devtools.scss"
13
+
14
+ const { atomTokenIndexState, selectorTokenIndexState } =
15
+ __INTERNAL__.META.attachMetaState()
16
+
17
+ const devtoolsAreOpenState = atom<boolean>({
18
+ key: `👁‍🗨_devtools_are_open`,
19
+ default: true,
20
+ effects: [lazyLocalStorageEffect(`👁‍🗨_devtools_are_open`)],
21
+ })
22
+
23
+ export const composeDevtools = (storeHooks: StoreHooks): FC => {
24
+ const Devtools: FC = () => {
25
+ const constraintsRef = useRef(null)
26
+
27
+ const [devtoolsAreOpen, setDevtoolsAreOpen] =
28
+ storeHooks.useIO(devtoolsAreOpenState)
29
+
30
+ const mouseHasMoved = useRef(false)
31
+
32
+ return (
33
+ <>
34
+ <motion.span
35
+ ref={constraintsRef}
36
+ className="atom_io_devtools_zone"
37
+ style={{
38
+ position: `fixed`,
39
+ top: 0,
40
+ left: 0,
41
+ right: 0,
42
+ bottom: 0,
43
+ pointerEvents: `none`,
44
+ }}
45
+ />
46
+ <motion.main
47
+ drag
48
+ dragConstraints={constraintsRef}
49
+ className="atom_io_devtools"
50
+ transition={spring}
51
+ style={
52
+ devtoolsAreOpen
53
+ ? {}
54
+ : {
55
+ backgroundColor: `#0000`,
56
+ borderColor: `#0000`,
57
+ maxHeight: 28,
58
+ maxWidth: 33,
59
+ }
60
+ }
61
+ >
62
+ {devtoolsAreOpen ? (
63
+ <>
64
+ <motion.header>
65
+ <h1>atom.io</h1>
66
+ </motion.header>
67
+ <motion.main>
68
+ <LayoutGroup>
69
+ <section>
70
+ <h2>atoms</h2>
71
+ <TokenList
72
+ storeHooks={storeHooks}
73
+ tokenIndex={atomTokenIndexState}
74
+ />
75
+ </section>
76
+ <section>
77
+ <h2>selectors</h2>
78
+ <TokenList
79
+ storeHooks={storeHooks}
80
+ tokenIndex={selectorTokenIndexState}
81
+ />
82
+ </section>
83
+ </LayoutGroup>
84
+ </motion.main>
85
+ </>
86
+ ) : null}
87
+ <footer>
88
+ <button
89
+ onMouseDown={() => (mouseHasMoved.current = false)}
90
+ onMouseMove={() => (mouseHasMoved.current = true)}
91
+ onMouseUp={() => {
92
+ if (!mouseHasMoved.current) {
93
+ setDevtoolsAreOpen((open) => !open)
94
+ }
95
+ }}
96
+ >
97
+ 👁‍🗨
98
+ </button>
99
+ </footer>
100
+ </motion.main>
101
+ </>
102
+ )
103
+ }
104
+ return Devtools
105
+ }
106
+
107
+ export const AtomIODevtools = composeDevtools({ useI, useO, useIO })
@@ -0,0 +1,73 @@
1
+ import type { FC } from "react"
2
+
3
+ import type { ReadonlySelectorToken, StateToken } from "atom.io"
4
+ import type { StoreHooks } from "atom.io/react"
5
+
6
+ import { isPlainJson } from "~/packages/anvl/src/json"
7
+ import { ElasticInput } from "~/packages/hamr/src/react-elastic-input"
8
+ import { JsonEditor } from "~/packages/hamr/src/react-json-editor"
9
+
10
+ export const StateEditor: FC<{
11
+ storeHooks: StoreHooks
12
+ token: StateToken<unknown>
13
+ }> = ({ storeHooks, token }) => {
14
+ const [data, set] = storeHooks.useIO(token)
15
+ return isPlainJson(data) ? (
16
+ <JsonEditor data={data} set={set} schema={true} />
17
+ ) : (
18
+ <div className="json_editor">
19
+ <ElasticInput
20
+ value={
21
+ data instanceof Set
22
+ ? `Set { ${JSON.stringify([...data]).slice(1, -1)} }`
23
+ : data instanceof Map
24
+ ? `Map ` + JSON.stringify([...data])
25
+ : Object.getPrototypeOf(data).constructor.name +
26
+ ` ` +
27
+ JSON.stringify(data)
28
+ }
29
+ disabled={true}
30
+ />
31
+ </div>
32
+ )
33
+ }
34
+
35
+ export const ReadonlySelectorEditor: FC<{
36
+ storeHooks: StoreHooks
37
+ token: ReadonlySelectorToken<unknown>
38
+ }> = ({ storeHooks, token }) => {
39
+ const data = storeHooks.useO(token)
40
+ return isPlainJson(data) ? (
41
+ <JsonEditor
42
+ data={data}
43
+ set={() => null}
44
+ schema={true}
45
+ isReadonly={() => true}
46
+ />
47
+ ) : (
48
+ <div className="json_editor">
49
+ <ElasticInput
50
+ value={
51
+ data instanceof Set
52
+ ? `Set ` + JSON.stringify([...data])
53
+ : data instanceof Map
54
+ ? `Map ` + JSON.stringify([...data])
55
+ : Object.getPrototypeOf(data).constructor.name +
56
+ ` ` +
57
+ JSON.stringify(data)
58
+ }
59
+ disabled={true}
60
+ />
61
+ </div>
62
+ )
63
+ }
64
+
65
+ export const StoreEditor: FC<{
66
+ storeHooks: StoreHooks
67
+ token: ReadonlySelectorToken<unknown> | StateToken<unknown>
68
+ }> = ({ storeHooks, token }) => {
69
+ if (token.type === `readonly_selector`) {
70
+ return <ReadonlySelectorEditor storeHooks={storeHooks} token={token} />
71
+ }
72
+ return <StateEditor storeHooks={storeHooks} token={token} />
73
+ }
@@ -0,0 +1,57 @@
1
+ import type { FC } from "react"
2
+ import { Fragment } from "react"
3
+
4
+ import type {
5
+ AtomToken,
6
+ ReadonlySelectorToken,
7
+ SelectorToken,
8
+ __INTERNAL__,
9
+ } from "atom.io"
10
+ import { getState } from "atom.io"
11
+ import type { StoreHooks } from "atom.io/react"
12
+
13
+ import { recordToEntries } from "~/packages/anvl/src/object"
14
+
15
+ import { StoreEditor } from "./StateEditor"
16
+
17
+ export const TokenList: FC<{
18
+ storeHooks: StoreHooks
19
+ tokenIndex: ReadonlySelectorToken<
20
+ __INTERNAL__.META.StateTokenIndex<
21
+ | AtomToken<unknown>
22
+ | ReadonlySelectorToken<unknown>
23
+ | SelectorToken<unknown>
24
+ >
25
+ >
26
+ }> = ({ storeHooks, tokenIndex }) => {
27
+ const tokenIds = storeHooks.useO(tokenIndex)
28
+ return (
29
+ <>
30
+ {Object.entries(tokenIds).map(([key, token]) => (
31
+ <Fragment key={key}>
32
+ {key.startsWith(`👁‍🗨_`) ? null : (
33
+ <div className="node">
34
+ {`type` in token ? (
35
+ <>
36
+ <label onClick={() => console.log(token, getState(token))}>
37
+ {key}
38
+ </label>
39
+ <StoreEditor storeHooks={storeHooks} token={token} />
40
+ </>
41
+ ) : (
42
+ recordToEntries(token.familyMembers).map(([key, token]) => (
43
+ <>
44
+ <label>{key}</label>
45
+ <div key={key} className="node">
46
+ {key}:<StoreEditor storeHooks={storeHooks} token={token} />
47
+ </div>
48
+ </>
49
+ ))
50
+ )}
51
+ </div>
52
+ )}
53
+ </Fragment>
54
+ ))}
55
+ </>
56
+ )
57
+ }
@@ -0,0 +1,130 @@
1
+ main.atom_io_devtools {
2
+ --fg-color: #eee;
3
+ --bg-color: #111;
4
+ @media (prefers-color-scheme: light) {
5
+ --fg-color: #222;
6
+ --bg-color: #ccc;
7
+ }
8
+ box-sizing: border-box;
9
+ color: var(--fg-color);
10
+ background-color: var(--bg-color);
11
+ border: 2px solid var(--fg-color);
12
+ position: fixed;
13
+ right: 0;
14
+ bottom: 0;
15
+ height: 100%;
16
+ display: flex;
17
+ flex-flow: column;
18
+ max-height: 800px;
19
+ width: 100%;
20
+ max-width: 460px;
21
+ overflow-y: scroll;
22
+ padding: 5px;
23
+ header {
24
+ display: flex;
25
+ justify-content: space-between;
26
+ h1 {
27
+ font-size: inherit;
28
+ margin: 0;
29
+ }
30
+ }
31
+ main {
32
+ overflow-y: scroll;
33
+ flex-grow: 1;
34
+ section {
35
+ margin-top: 30px;
36
+ h2 {
37
+ font-size: inherit;
38
+ margin: 0;
39
+ }
40
+ .node {
41
+ border: 1px solid var(--fg-color);
42
+ padding: 5px;
43
+ margin: 5px;
44
+ overflow-x: scroll;
45
+ }
46
+ }
47
+ }
48
+ footer {
49
+ display: flex;
50
+ justify-content: flex-end;
51
+ button {
52
+ cursor: pointer;
53
+ background: none;
54
+ border: none;
55
+ padding: none;
56
+ position: absolute;
57
+ right: 0;
58
+ bottom: 0;
59
+ }
60
+ }
61
+
62
+ .json_editor {
63
+ input {
64
+ font-size: 20px;
65
+ font-family: theia;
66
+ border: none;
67
+ border-bottom: 1px solid;
68
+ background: none;
69
+ &:disabled {
70
+ border: none;
71
+ }
72
+ }
73
+ button {
74
+ background: none;
75
+ margin-left: auto;
76
+ color: #777;
77
+ border: none;
78
+ font-family: theia;
79
+ font-size: 14px;
80
+ margin: none;
81
+ padding: 4px;
82
+ padding-bottom: 6px;
83
+ cursor: pointer;
84
+ &:hover {
85
+ color: #333;
86
+ background-color: #aaa;
87
+ }
88
+ }
89
+ select {
90
+ font-family: theia;
91
+ font-size: 14px;
92
+ background: none;
93
+ border: none;
94
+ color: #777;
95
+ @media (prefers-color-scheme: light) {
96
+ color: #999;
97
+ }
98
+ }
99
+ .json_editor_unofficial {
100
+ background-color: #777;
101
+ button {
102
+ color: #333;
103
+ }
104
+ }
105
+ .json_editor_missing {
106
+ background-color: #f055;
107
+ }
108
+ .json_editor_key {
109
+ input {
110
+ color: #999;
111
+ @media (prefers-color-scheme: light) {
112
+ color: #777;
113
+ }
114
+ }
115
+ }
116
+ .json_editor_object {
117
+ border-left: 2px solid #333;
118
+ padding-left: 20px;
119
+ @media (prefers-color-scheme: light) {
120
+ border-color: #ccc;
121
+ }
122
+ .json_editor_properties {
123
+ > * {
124
+ border-bottom: 2px solid #333;
125
+ margin-bottom: 2px;
126
+ }
127
+ }
128
+ }
129
+ }
130
+ }
@@ -0,0 +1 @@
1
+ export * from "./AtomIODevtools"
@@ -0,0 +1,208 @@
1
+ import type { FC, ReactNode } from "react"
2
+ import { useEffect } from "react"
3
+
4
+ import { Link, MemoryRouter, useLocation } from "react-router-dom"
5
+
6
+ import type { composeStoreHooks } from "~/packages/atom.io/src/react"
7
+ import { ErrorBoundary } from "~/packages/hamr/src/react-error-boundary"
8
+ import type { WC } from "~/packages/hamr/src/react-json-editor"
9
+
10
+ import { attachExplorerState } from "./explorer-states"
11
+ import { setState } from ".."
12
+ import { runTransaction } from "../transaction"
13
+
14
+ export type ExplorerOptions = {
15
+ key: string
16
+ Components?: {
17
+ SpaceWrapper: WC
18
+ CloseSpaceButton: FC<{ onClick: () => void }>
19
+ }
20
+ storeHooks: ReturnType<typeof composeStoreHooks>
21
+ }
22
+
23
+ const DEFAULT_COMPONENTS: ExplorerOptions[`Components`] = {
24
+ SpaceWrapper: ({ children }) => <div>{children}</div>,
25
+ CloseSpaceButton: ({ onClick }) => <button onClick={onClick}>X</button>,
26
+ }
27
+
28
+ export const composeExplorer = ({
29
+ key,
30
+ Components,
31
+ storeHooks: { useO, useIO },
32
+ }: ExplorerOptions): ReturnType<typeof attachExplorerState> & {
33
+ Explorer: FC<{ children: ReactNode }>
34
+ useSetTitle: (viewId: string) => void
35
+ } => {
36
+ const { SpaceWrapper, CloseSpaceButton } = {
37
+ ...DEFAULT_COMPONENTS,
38
+ ...Components,
39
+ }
40
+
41
+ const state = attachExplorerState(key)
42
+
43
+ const {
44
+ addSpace,
45
+ addView,
46
+ allViewsState,
47
+ findSpaceFocusedViewState,
48
+ findSpaceLayoutNode,
49
+ findSpaceViewsState,
50
+ findViewFocusedState,
51
+ findViewState,
52
+ removeSpace,
53
+ removeView,
54
+ spaceLayoutState,
55
+ viewIndexState,
56
+ } = state
57
+
58
+ const View: FC<{
59
+ children: ReactNode
60
+ viewId: string
61
+ }> = ({ children, viewId }) => {
62
+ const location = useLocation()
63
+ const viewState = findViewState(viewId)
64
+ const [view, setView] = useIO(viewState)
65
+ useEffect(() => {
66
+ setView((view) => ({ ...view, location }))
67
+ }, [location.key])
68
+ return (
69
+ <div className="view">
70
+ <header>
71
+ <h1>{view.title}</h1>
72
+ <CloseSpaceButton onClick={() => runTransaction(removeView)(viewId)} />
73
+ </header>
74
+ <main>{children}</main>
75
+ <footer>
76
+ <nav>
77
+ {location.pathname.split(`/`).map((pathPiece, idx, array) =>
78
+ pathPiece === `` && idx === 1 ? null : (
79
+ <Link
80
+ to={array.slice(0, idx + 1).join(`/`)}
81
+ key={`${pathPiece}_${viewId}`}
82
+ >
83
+ {idx === 0 ? `home` : pathPiece}/
84
+ </Link>
85
+ )
86
+ )}
87
+ </nav>
88
+ </footer>
89
+ </div>
90
+ )
91
+ }
92
+
93
+ const Tab: FC<{ viewId: string; spaceId: string }> = ({ viewId, spaceId }) => {
94
+ const view = useO(findViewState(viewId))
95
+ const [spaceFocusedView, setSpaceFocusedView] = useIO(
96
+ findSpaceFocusedViewState(spaceId)
97
+ )
98
+ return (
99
+ <div
100
+ className={`tab ${spaceFocusedView === viewId ? `focused` : ``}`}
101
+ onClick={() => setSpaceFocusedView(viewId)}
102
+ >
103
+ {view.title}
104
+ </div>
105
+ )
106
+ }
107
+
108
+ const TabBar: FC<{
109
+ spaceId: string
110
+ viewIds: string[]
111
+ }> = ({ spaceId, viewIds }) => {
112
+ return (
113
+ <nav className="tab-bar">
114
+ {viewIds.map((viewId) => (
115
+ <Tab key={viewId} viewId={viewId} spaceId={spaceId} />
116
+ ))}
117
+ </nav>
118
+ )
119
+ }
120
+
121
+ const Space: FC<{
122
+ children: ReactNode
123
+ focusedViewId: string
124
+ spaceId: string
125
+ viewIds: string[]
126
+ }> = ({ children, focusedViewId, spaceId, viewIds }) => {
127
+ const view = useO(findViewState(focusedViewId))
128
+ return (
129
+ <div className="space">
130
+ <ErrorBoundary>
131
+ <MemoryRouter
132
+ initialEntries={view.location ? [view.location.pathname] : []}
133
+ >
134
+ <TabBar spaceId={spaceId} viewIds={viewIds} />
135
+ <View viewId={focusedViewId}>{children}</View>
136
+ </MemoryRouter>
137
+ </ErrorBoundary>
138
+ </div>
139
+ )
140
+ }
141
+
142
+ const Spaces: FC<{ children: ReactNode; spaceId?: string }> = ({
143
+ children,
144
+ spaceId = `root`,
145
+ }) => {
146
+ const spaceLayout = useO(findSpaceLayoutNode(spaceId))
147
+ const viewIds = useO(findSpaceViewsState(spaceId))
148
+ const focusedViewId = useO(findSpaceFocusedViewState(spaceId))
149
+ console.log({ spaceLayout, viewIds, focusedViewId })
150
+ return (
151
+ <div className="spaces">
152
+ {spaceLayout.childSpaceIds.length === 0 ? (
153
+ focusedViewId ? (
154
+ <Space
155
+ focusedViewId={focusedViewId}
156
+ spaceId={spaceId}
157
+ viewIds={viewIds}
158
+ >
159
+ {children}
160
+ </Space>
161
+ ) : (
162
+ `no view`
163
+ )
164
+ ) : (
165
+ spaceLayout.childSpaceIds.map((childSpaceId) => (
166
+ <Spaces key={childSpaceId} spaceId={childSpaceId}>
167
+ {children}
168
+ </Spaces>
169
+ ))
170
+ )}
171
+ <button onClick={() => runTransaction(addView)({ spaceId })}>
172
+ + View
173
+ </button>
174
+ <button onClick={() => runTransaction(addSpace)({ parentId: spaceId })}>
175
+ + Space
176
+ </button>
177
+ </div>
178
+ )
179
+ }
180
+
181
+ const Explorer: FC<{ children: ReactNode }> = ({ children }) => {
182
+ return <Spaces>{children}</Spaces>
183
+ }
184
+
185
+ const useSetTitle = (title: string): void => {
186
+ let location: ReturnType<typeof useLocation>
187
+ try {
188
+ location = useLocation()
189
+ } catch (thrown) {
190
+ console.warn(
191
+ `Failed to set title to "${title}"; useSetTitle must be called within the children of Explorer`
192
+ )
193
+ return
194
+ }
195
+ const views = useO(allViewsState)
196
+ const locationView = views.find(
197
+ ([, view]) => view.location.key === location.key
198
+ )
199
+ const viewId = locationView?.[0] ?? null
200
+ useEffect(() => {
201
+ if (viewId) {
202
+ setState(findViewState(viewId), (v) => ({ ...v, title }))
203
+ }
204
+ }, [viewId])
205
+ }
206
+
207
+ return { Explorer, useSetTitle, ...state }
208
+ }
@@ -0,0 +1,20 @@
1
+ import { isString } from "fp-ts/lib/string"
2
+
3
+ import { isArray } from "~/packages/anvl/src/array"
4
+ import { parseJson, stringifyJson } from "~/packages/anvl/src/json"
5
+
6
+ import { persistAtom } from "../web-effects"
7
+
8
+ export const persistStringSetAtom = persistAtom<Set<string>>(localStorage)({
9
+ stringify: (set) => stringifyJson([...set]),
10
+ parse: (string) => {
11
+ try {
12
+ const json = parseJson(string)
13
+ const array = isArray(isString)(json) ? json : []
14
+ return new Set(array)
15
+ } catch (thrown) {
16
+ console.error(`Error parsing spaceIndexState from localStorage`)
17
+ return new Set()
18
+ }
19
+ },
20
+ })