atom.io 0.3.0 → 0.4.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.
Files changed (55) hide show
  1. package/README.md +14 -8
  2. package/dist/index.d.ts +117 -62
  3. package/dist/index.js +577 -287
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +574 -285
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +16 -6
  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 +1579 -0
  17. package/react-devtools/dist/index.js.map +1 -0
  18. package/react-devtools/dist/index.mjs +1551 -0
  19. package/react-devtools/dist/index.mjs.map +1 -0
  20. package/react-devtools/package.json +15 -0
  21. package/src/index.ts +14 -8
  22. package/src/internal/atom-internal.ts +10 -5
  23. package/src/internal/families-internal.ts +7 -7
  24. package/src/internal/get.ts +9 -9
  25. package/src/internal/index.ts +2 -1
  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 +14 -3
  32. package/src/internal/selector-internal.ts +37 -15
  33. package/src/internal/store.ts +35 -6
  34. package/src/internal/time-travel-internal.ts +89 -0
  35. package/src/internal/timeline-internal.ts +110 -93
  36. package/src/internal/transaction-internal.ts +14 -5
  37. package/src/{internal/logger.ts → logger.ts} +2 -2
  38. package/src/react/index.ts +28 -46
  39. package/src/react-devtools/AtomIODevtools.tsx +107 -0
  40. package/src/react-devtools/StateEditor.tsx +73 -0
  41. package/src/react-devtools/TokenList.tsx +49 -0
  42. package/src/react-devtools/devtools.scss +130 -0
  43. package/src/react-devtools/index.ts +1 -0
  44. package/src/react-explorer/AtomIOExplorer.tsx +208 -0
  45. package/src/react-explorer/explorer-effects.ts +20 -0
  46. package/src/react-explorer/explorer-states.ts +224 -0
  47. package/src/react-explorer/index.ts +23 -0
  48. package/src/react-explorer/space-states.ts +73 -0
  49. package/src/react-explorer/view-states.ts +43 -0
  50. package/src/selector.ts +11 -11
  51. package/src/subscribe.ts +3 -3
  52. package/src/timeline.ts +3 -12
  53. package/src/transaction.ts +9 -4
  54. package/src/web-effects/index.ts +1 -0
  55. package/src/web-effects/storage.ts +30 -0
@@ -1,8 +1,7 @@
1
1
  import HAMT from "hamt_plus"
2
2
 
3
3
  import type { KeyedStateUpdate, TransactionUpdate, Store } from "."
4
- import { IMPLICIT, withdraw } from "."
5
- import { setState } from ".."
4
+ import { target, IMPLICIT, withdraw } from "."
6
5
  import type { AtomToken, TimelineOptions, TimelineToken, ƒn } from ".."
7
6
 
8
7
  export type Timeline = {
@@ -12,8 +11,13 @@ export type Timeline = {
12
11
  prev: () => void
13
12
  }
14
13
 
15
- export type TimelineStateUpdate = KeyedStateUpdate<unknown> & {
16
- type: `state_update`
14
+ export type TimelineAtomUpdate = KeyedStateUpdate<unknown> & {
15
+ type: `atom_update`
16
+ }
17
+ export type TimelineSelectorUpdate = {
18
+ key: string
19
+ type: `selector_update`
20
+ atomUpdates: TimelineAtomUpdate[]
17
21
  }
18
22
  export type TimelineTransactionUpdate = TransactionUpdate<ƒn> & {
19
23
  type: `transaction_update`
@@ -22,23 +26,39 @@ export type TimelineTransactionUpdate = TransactionUpdate<ƒn> & {
22
26
  export type TimelineData = {
23
27
  at: number
24
28
  timeTraveling: boolean
25
- history: (TimelineStateUpdate | TimelineTransactionUpdate)[]
29
+ history: (
30
+ | TimelineAtomUpdate
31
+ | TimelineSelectorUpdate
32
+ | TimelineTransactionUpdate
33
+ )[]
34
+ selectorTime: number | null
35
+ transactionKey: string | null
26
36
  }
27
37
 
28
38
  export function timeline__INTERNAL(
29
39
  options: TimelineOptions,
30
40
  store: Store = IMPLICIT.STORE
31
41
  ): TimelineToken {
32
- let incompleteTransactionKey: string | null = null
33
42
  const timelineData: TimelineData = {
34
43
  at: 0,
35
44
  timeTraveling: false,
36
45
  history: [],
46
+ selectorTime: null,
47
+ transactionKey: null,
37
48
  }
38
49
 
39
50
  const subscribeToAtom = (token: AtomToken<any>) => {
40
51
  const state = withdraw(token, store)
41
52
  state.subject.subscribe((update) => {
53
+ const storeCurrentSelectorKey =
54
+ store.operation.open && store.operation.token.type === `selector`
55
+ ? store.operation.token.key
56
+ : null
57
+ const storeCurrentSelectorTime =
58
+ store.operation.open && store.operation.token.type === `selector`
59
+ ? store.operation.time
60
+ : null
61
+
42
62
  const storeCurrentTransactionKey =
43
63
  store.transactionStatus.phase === `applying`
44
64
  ? store.transactionStatus.key
@@ -49,8 +69,10 @@ export function timeline__INTERNAL(
49
69
  `->`,
50
70
  update.newValue,
51
71
  storeCurrentTransactionKey
52
- ? `) in "${storeCurrentTransactionKey}"`
53
- : `) independently`
72
+ ? `) in transaction "${storeCurrentTransactionKey}"`
73
+ : storeCurrentSelectorKey
74
+ ? `) in selector "${storeCurrentSelectorKey}"`
75
+ : `)`
54
76
  )
55
77
 
56
78
  if (
@@ -61,15 +83,18 @@ export function timeline__INTERNAL(
61
83
  { key: storeCurrentTransactionKey, type: `transaction` },
62
84
  store
63
85
  )
64
- if (incompleteTransactionKey !== storeCurrentTransactionKey) {
65
- if (incompleteTransactionKey) {
86
+ if (timelineData.transactionKey !== storeCurrentTransactionKey) {
87
+ if (timelineData.transactionKey) {
66
88
  store.config.logger?.error(
67
- `Timeline "${options.key}" was unable to resolve transaction "${incompleteTransactionKey}. This is probably a bug.`
89
+ `Timeline "${options.key}" was unable to resolve transaction "${timelineData.transactionKey}. This is probably a bug.`
68
90
  )
69
91
  }
70
- incompleteTransactionKey = storeCurrentTransactionKey
92
+ timelineData.transactionKey = storeCurrentTransactionKey
71
93
  const subscription = currentTransaction.subject.subscribe((update) => {
72
94
  if (timelineData.timeTraveling === false) {
95
+ if (timelineData.at !== timelineData.history.length) {
96
+ timelineData.history.splice(timelineData.at)
97
+ }
73
98
  timelineData.history.push({
74
99
  type: `transaction_update`,
75
100
  ...update,
@@ -80,117 +105,109 @@ export function timeline__INTERNAL(
80
105
  }
81
106
  timelineData.at = timelineData.history.length
82
107
  subscription.unsubscribe()
83
- incompleteTransactionKey = null
108
+ timelineData.transactionKey = null
84
109
  store.config.logger?.info(
85
- `⌛ timeline "${options.key}" pushed a transaction_update from "${update.key}"`
110
+ `⌛ timeline "${options.key}" got a transaction_update "${update.key}"`
86
111
  )
87
112
  })
88
113
  }
114
+ } else if (storeCurrentSelectorKey) {
115
+ if (timelineData.timeTraveling === false) {
116
+ if (storeCurrentSelectorTime !== timelineData.selectorTime) {
117
+ const newSelectorUpdate: TimelineSelectorUpdate = {
118
+ type: `selector_update`,
119
+ key: storeCurrentSelectorKey,
120
+ atomUpdates: [],
121
+ }
122
+ newSelectorUpdate.atomUpdates.push({
123
+ key: token.key,
124
+ type: `atom_update`,
125
+ ...update,
126
+ })
127
+ if (timelineData.at !== timelineData.history.length) {
128
+ timelineData.history.splice(timelineData.at)
129
+ }
130
+ timelineData.history.push(newSelectorUpdate)
131
+
132
+ store.config.logger?.info(
133
+ `⌛ timeline "${options.key}" got a selector_update "${storeCurrentSelectorKey}" with`,
134
+ newSelectorUpdate.atomUpdates.map((atomUpdate) => atomUpdate.key)
135
+ )
136
+ timelineData.at = timelineData.history.length
137
+ timelineData.selectorTime = storeCurrentSelectorTime
138
+ } else {
139
+ const latestUpdate = timelineData.history.at(-1)
140
+ if (latestUpdate?.type === `selector_update`) {
141
+ latestUpdate.atomUpdates.push({
142
+ key: token.key,
143
+ type: `atom_update`,
144
+ ...update,
145
+ })
146
+ store.config.logger?.info(
147
+ ` ⌛ timeline "${options.key}" set selector_update "${storeCurrentSelectorKey}" to`,
148
+ latestUpdate?.atomUpdates.map((atomUpdate) => atomUpdate.key)
149
+ )
150
+ }
151
+ }
152
+ }
89
153
  } else {
90
154
  if (timelineData.timeTraveling === false) {
155
+ timelineData.selectorTime = null
156
+ if (timelineData.at !== timelineData.history.length) {
157
+ timelineData.history.splice(timelineData.at)
158
+ }
91
159
  timelineData.history.push({
92
- type: `state_update`,
160
+ type: `atom_update`,
93
161
  key: token.key,
94
162
  oldValue: update.oldValue,
95
163
  newValue: update.newValue,
96
164
  })
97
165
  store.config.logger?.info(
98
- `⌛ timeline "${options.key}" pushed a state_update to "${token.key}"`
166
+ `⌛ timeline "${options.key}" got a state_update to "${token.key}"`
99
167
  )
100
168
  timelineData.at = timelineData.history.length
101
169
  }
102
170
  }
103
171
  })
104
172
  }
105
-
173
+ const core = target(store)
106
174
  for (const tokenOrFamily of options.atoms) {
175
+ const timelineKey = core.timelineAtoms.getRelatedId(tokenOrFamily.key)
176
+ if (timelineKey) {
177
+ store.config.logger?.error(
178
+ `❌ Failed to add atom "${tokenOrFamily.key}" to timeline "${options.key}" because it belongs to timeline "${timelineKey}"`
179
+ )
180
+ continue
181
+ }
107
182
  if (tokenOrFamily.type === `atom_family`) {
108
183
  const family = tokenOrFamily
109
184
  family.subject.subscribe((token) => subscribeToAtom(token))
110
185
  } else {
111
186
  const token = tokenOrFamily
187
+ if (`family` in token && token.family) {
188
+ const familyTimelineKey = core.timelineAtoms.getRelatedId(
189
+ token.family.key
190
+ )
191
+ if (familyTimelineKey) {
192
+ store.config.logger?.error(
193
+ `❌ Failed to add atom "${token.key}" to timeline "${options.key}" because its family "${token.family.key}" belongs to timeline "${familyTimelineKey}"`
194
+ )
195
+ continue
196
+ }
197
+ }
112
198
  subscribeToAtom(token)
113
199
  }
200
+ core.timelineAtoms = core.timelineAtoms.set({
201
+ atomKey: tokenOrFamily.key,
202
+ timelineKey: options.key,
203
+ })
114
204
  }
115
205
 
116
206
  store.timelineStore = HAMT.set(options.key, timelineData, store.timelineStore)
117
-
118
- return {
207
+ const token: TimelineToken = {
119
208
  key: options.key,
120
209
  type: `timeline`,
121
210
  }
122
- }
123
-
124
- export const redo__INTERNAL = (
125
- token: TimelineToken,
126
- store: Store = IMPLICIT.STORE
127
- ): void => {
128
- const timelineData = store.timelineStore.get(token.key)
129
- if (!timelineData) {
130
- store.config.logger?.error(
131
- `Tried to redo on timeline "${token.key}" has not been initialized.`
132
- )
133
- return
134
- }
135
- if (timelineData.at === timelineData.history.length) {
136
- store.config.logger?.warn(
137
- `Tried to redo on timeline "${token.key}" but there is nothing to redo.`
138
- )
139
- return
140
- }
141
- timelineData.timeTraveling = true
142
- const update = timelineData.history[timelineData.at]
143
- switch (update.type) {
144
- case `state_update`: {
145
- const { key, newValue } = update
146
- setState({ key, type: `atom` }, newValue)
147
- break
148
- }
149
- case `transaction_update`: {
150
- for (const atomUpdate of update.atomUpdates) {
151
- const { key, newValue } = atomUpdate
152
- setState({ key, type: `atom` }, newValue)
153
- }
154
- break
155
- }
156
- }
157
- ++timelineData.at
158
- timelineData.timeTraveling = false
159
- }
160
-
161
- export const undo__INTERNAL = (
162
- token: TimelineToken,
163
- store: Store = IMPLICIT.STORE
164
- ): void => {
165
- const timelineData = store.timelineStore.get(token.key)
166
- if (!timelineData) {
167
- store.config.logger?.error(
168
- `Tried to undo on timeline "${token.key}" has not been initialized.`
169
- )
170
- return
171
- }
172
- if (timelineData.at === 0) {
173
- store.config.logger?.warn(
174
- `Tried to undo on timeline "${token.key}" but there is nothing to undo.`
175
- )
176
- return
177
- }
178
- timelineData.timeTraveling = true
179
- --timelineData.at
180
- const update = timelineData.history[timelineData.at]
181
- switch (update.type) {
182
- case `state_update`: {
183
- const { key, oldValue } = update
184
- setState({ key, type: `atom` }, oldValue)
185
- break
186
- }
187
- case `transaction_update`: {
188
- for (const atomUpdate of update.atomUpdates) {
189
- const { key, oldValue } = atomUpdate
190
- setState({ key, type: `atom` }, oldValue)
191
- }
192
- break
193
- }
194
- }
195
- timelineData.timeTraveling = false
211
+ store.subject.timelineCreation.next(token)
212
+ return token
196
213
  }
@@ -2,7 +2,7 @@ import HAMT from "hamt_plus"
2
2
  import * as Rx from "rxjs"
3
3
 
4
4
  import type { Store, StoreCore } from "."
5
- import { deposit, withdraw, IMPLICIT } from "."
5
+ import { cacheValue, deposit, withdraw, IMPLICIT } from "."
6
6
  import { getState, setState } from ".."
7
7
  import type {
8
8
  AtomToken,
@@ -75,14 +75,22 @@ export const applyTransaction = <ƒ extends ƒn>(
75
75
  return
76
76
  }
77
77
  store.config.logger?.info(
78
- ` ▶️ apply transaction "${store.transactionStatus.key}" (init)`
78
+ `🛃 apply transaction "${store.transactionStatus.key}"`
79
79
  )
80
80
  store.transactionStatus.phase = `applying`
81
81
  store.transactionStatus.output = output
82
82
  const { atomUpdates } = store.transactionStatus
83
- for (const { key, oldValue, newValue } of atomUpdates) {
83
+
84
+ for (const { key, newValue } of atomUpdates) {
84
85
  const token: AtomToken<unknown> = { key, type: `atom` }
86
+ if (!HAMT.has(token.key, store.valueMap)) {
87
+ const atom = HAMT.get(token.key, store.transactionStatus.core.atoms)
88
+ store.atoms = HAMT.set(atom.key, atom, store.atoms)
89
+ store.valueMap = HAMT.set(atom.key, atom.default, store.valueMap)
90
+ store.config.logger?.info(`🔧`, `add atom "${atom.key}"`)
91
+ }
85
92
  const state = withdraw(token, store)
93
+
86
94
  setState(state, newValue, store)
87
95
  }
88
96
  const myTransaction = withdraw<ƒ>(
@@ -103,7 +111,7 @@ export const undoTransactionUpdate = <ƒ extends ƒn>(
103
111
  store: Store
104
112
  ): void => {
105
113
  store.config.logger?.info(` ⏮ undo transaction "${update.key}" (undo)`)
106
- for (const { key, oldValue, newValue } of update.atomUpdates) {
114
+ for (const { key, oldValue } of update.atomUpdates) {
107
115
  const token: AtomToken<unknown> = { key, type: `atom` }
108
116
  const state = withdraw(token, store)
109
117
  setState(state, oldValue, store)
@@ -114,7 +122,7 @@ export const redoTransactionUpdate = <ƒ extends ƒn>(
114
122
  store: Store
115
123
  ): void => {
116
124
  store.config.logger?.info(` ⏭ redo transaction "${update.key}" (redo)`)
117
- for (const { key, oldValue, newValue } of update.atomUpdates) {
125
+ for (const { key, newValue } of update.atomUpdates) {
118
126
  const token: AtomToken<unknown> = { key, type: `atom` }
119
127
  const state = withdraw(token, store)
120
128
  setState(state, newValue, store)
@@ -166,6 +174,7 @@ export function transaction__INTERNAL<ƒ extends ƒn>(
166
174
  core.transactions
167
175
  )
168
176
  const token = deposit(newTransaction)
177
+ store.subject.transactionCreation.next(token)
169
178
  return token
170
179
  }
171
180
 
@@ -1,7 +1,7 @@
1
1
  import { doNothing } from "~/packages/anvl/src/function"
2
2
 
3
- import type { Store } from "./store"
4
- import { IMPLICIT } from "./store"
3
+ import type { Store } from "./internal/store"
4
+ import { IMPLICIT } from "./internal/store"
5
5
 
6
6
  export type Logger = Pick<Console, `error` | `info` | `warn`>
7
7
  export const LOG_LEVELS: ReadonlyArray<keyof Logger> = [
@@ -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,49 @@
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 type { StoreHooks } from "atom.io/react"
11
+
12
+ import { recordToEntries } from "~/packages/anvl/src/object"
13
+
14
+ import { StoreEditor } from "./StateEditor"
15
+
16
+ export const TokenList: FC<{
17
+ storeHooks: StoreHooks
18
+ tokenIndex: ReadonlySelectorToken<
19
+ __INTERNAL__.META.StateTokenIndex<
20
+ | AtomToken<unknown>
21
+ | ReadonlySelectorToken<unknown>
22
+ | SelectorToken<unknown>
23
+ >
24
+ >
25
+ }> = ({ storeHooks, tokenIndex }) => {
26
+ const tokenIds = storeHooks.useO(tokenIndex)
27
+ return (
28
+ <>
29
+ {Object.entries(tokenIds).map(([key, token]) => (
30
+ <Fragment key={key}>
31
+ {key.startsWith(`👁‍🗨_`) ? null : (
32
+ <div className="node">
33
+ {key}:
34
+ {`type` in token ? (
35
+ <StoreEditor storeHooks={storeHooks} token={token} />
36
+ ) : (
37
+ recordToEntries(token.familyMembers).map(([key, token]) => (
38
+ <div key={key} className="node">
39
+ {key}:<StoreEditor storeHooks={storeHooks} token={token} />
40
+ </div>
41
+ ))
42
+ )}
43
+ </div>
44
+ )}
45
+ </Fragment>
46
+ ))}
47
+ </>
48
+ )
49
+ }