atom.io 0.28.1 → 0.28.2
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/{chunk-6WL4RQMQ.js → chunk-D3ZPRYEW.js} +15 -17
- package/eslint-plugin/dist/index.js +0 -1
- package/eslint-plugin/src/walk.ts +0 -1
- package/internal/dist/index.d.ts +3 -3
- package/internal/dist/index.js +1 -1
- package/internal/src/operation.ts +7 -7
- package/internal/src/selector/create-writable-selector.ts +1 -1
- package/internal/src/set-state/become.ts +1 -3
- package/internal/src/set-state/evict-downstream.ts +2 -2
- package/internal/src/set-state/set-atom.ts +1 -1
- package/internal/src/set-state/set-into-store.ts +1 -1
- package/internal/src/set-state/stow-update.ts +2 -2
- package/internal/src/store/store.ts +1 -1
- package/introspection/dist/index.d.ts +15 -6
- package/introspection/dist/index.js +620 -1
- package/introspection/src/attach-atom-index.ts +5 -6
- package/introspection/src/attach-introspection-states.ts +5 -6
- package/introspection/src/attach-selector-index.ts +6 -7
- package/introspection/src/attach-timeline-family.ts +3 -4
- package/introspection/src/attach-timeline-index.ts +4 -8
- package/introspection/src/attach-transaction-index.ts +4 -8
- package/introspection/src/attach-transaction-logs.ts +4 -8
- package/introspection/src/attach-type-selectors.ts +13 -6
- package/introspection/src/differ.ts +1 -1
- package/introspection/src/index.ts +1 -0
- package/introspection/src/refinery.ts +9 -7
- package/introspection/src/sprawl.ts +42 -0
- package/json/dist/index.d.ts +12 -1
- package/json/dist/index.js +111 -2
- package/json/src/index.ts +29 -0
- package/package.json +8 -8
- package/react-devtools/dist/index.d.ts +159 -2
- package/react-devtools/dist/index.js +260 -663
- package/react-devtools/src/AtomIODevtools.tsx +24 -13
- package/react-devtools/src/StateEditor.tsx +5 -47
- package/react-devtools/src/StateIndex.tsx +15 -9
- package/react-devtools/src/TimelineIndex.tsx +9 -6
- package/react-devtools/src/TransactionIndex.tsx +9 -11
- package/react-devtools/src/elastic-input/ElasticInput.tsx +86 -0
- package/react-devtools/src/elastic-input/NumberInput.tsx +199 -0
- package/react-devtools/src/elastic-input/TextInput.tsx +47 -0
- package/react-devtools/src/elastic-input/index.ts +3 -0
- package/react-devtools/src/error-boundary/DefaultFallback.tsx +49 -0
- package/react-devtools/src/error-boundary/ReactErrorBoundary.tsx +56 -0
- package/react-devtools/src/error-boundary/index.ts +2 -0
- package/react-devtools/src/index.ts +3 -0
- package/react-devtools/src/json-editor/assets/Untitled-1.ai +1436 -2
- package/react-devtools/src/json-editor/assets/data-vis.ai +1548 -1
- package/react-devtools/src/json-editor/comp/json-editor-sketches.ai +1451 -3
- package/react-devtools/src/json-editor/default-components.tsx +101 -0
- package/react-devtools/src/json-editor/developer-interface.tsx +81 -0
- package/react-devtools/src/json-editor/editors-by-type/array-editor.tsx +38 -0
- package/react-devtools/src/json-editor/editors-by-type/non-json.tsx +23 -0
- package/react-devtools/src/json-editor/editors-by-type/object-editor.tsx +128 -0
- package/react-devtools/src/json-editor/editors-by-type/primitive-editors.tsx +73 -0
- package/react-devtools/src/json-editor/editors-by-type/utilities/array-elements.ts +16 -0
- package/react-devtools/src/json-editor/editors-by-type/utilities/cast-json.ts +57 -0
- package/react-devtools/src/json-editor/editors-by-type/utilities/cast-to-json.ts +156 -0
- package/react-devtools/src/json-editor/editors-by-type/utilities/object-properties.ts +106 -0
- package/react-devtools/src/json-editor/index.ts +32 -0
- package/react-devtools/src/json-editor/json-editor-internal.tsx +128 -0
- package/react-devtools/src/json-editor/todo.md +7 -0
- package/react-devtools/src/store.ts +70 -46
- package/dist/chunk-D52JNVER.js +0 -721
- package/dist/chunk-YQ46F5O2.js +0 -95
|
@@ -1,23 +1,34 @@
|
|
|
1
1
|
import "./devtools.scss"
|
|
2
2
|
|
|
3
|
-
import { useI, useO } from "atom.io/react"
|
|
3
|
+
import { StoreContext, useI, useO } from "atom.io/react"
|
|
4
4
|
import { LayoutGroup, motion, spring } from "framer-motion"
|
|
5
|
-
import { useRef } from "react"
|
|
5
|
+
import { useContext, useRef } from "react"
|
|
6
6
|
|
|
7
7
|
import { StateIndex } from "./StateIndex"
|
|
8
|
-
import {
|
|
9
|
-
atomIndex,
|
|
10
|
-
devtoolsAreOpenState,
|
|
11
|
-
devtoolsViewOptionsState,
|
|
12
|
-
devtoolsViewSelectionState,
|
|
13
|
-
selectorIndex,
|
|
14
|
-
} from "./store"
|
|
8
|
+
import { attachDevtoolsStates, DevtoolsContext } from "./store"
|
|
15
9
|
import { TimelineIndex } from "./TimelineIndex"
|
|
16
10
|
import { TransactionIndex } from "./TransactionIndex"
|
|
17
11
|
|
|
18
|
-
export const AtomIODevtools = ()
|
|
12
|
+
export const AtomIODevtools: React.FC = () => {
|
|
13
|
+
const store = useContext(StoreContext)
|
|
14
|
+
return (
|
|
15
|
+
<DevtoolsContext.Provider value={attachDevtoolsStates(store)}>
|
|
16
|
+
<AtomIODevtoolsInternal />
|
|
17
|
+
</DevtoolsContext.Provider>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const AtomIODevtoolsInternal = (): JSX.Element => {
|
|
19
22
|
const constraintsRef = useRef(null)
|
|
20
23
|
|
|
24
|
+
const {
|
|
25
|
+
atomIndex,
|
|
26
|
+
selectorIndex,
|
|
27
|
+
devtoolsAreOpenState,
|
|
28
|
+
devtoolsViewSelectionState,
|
|
29
|
+
devtoolsViewOptionsState,
|
|
30
|
+
} = useContext(DevtoolsContext)
|
|
31
|
+
|
|
21
32
|
const setDevtoolsAreOpen = useI(devtoolsAreOpenState)
|
|
22
33
|
const devtoolsAreOpen = useO(devtoolsAreOpenState)
|
|
23
34
|
const setDevtoolsView = useI(devtoolsViewSelectionState)
|
|
@@ -85,9 +96,9 @@ export const AtomIODevtools = (): JSX.Element => {
|
|
|
85
96
|
<StateIndex tokenIndex={selectorIndex} />
|
|
86
97
|
) : devtoolsView === `transactions` ? (
|
|
87
98
|
<TransactionIndex />
|
|
88
|
-
) :
|
|
99
|
+
) : (
|
|
89
100
|
<TimelineIndex />
|
|
90
|
-
)
|
|
101
|
+
)}
|
|
91
102
|
</LayoutGroup>
|
|
92
103
|
</motion.main>
|
|
93
104
|
</>
|
|
@@ -103,7 +114,7 @@ export const AtomIODevtools = (): JSX.Element => {
|
|
|
103
114
|
}
|
|
104
115
|
}}
|
|
105
116
|
>
|
|
106
|
-
|
|
117
|
+
🔍
|
|
107
118
|
</button>
|
|
108
119
|
</footer>
|
|
109
120
|
</motion.main>
|
|
@@ -1,45 +1,16 @@
|
|
|
1
1
|
import type { ReadonlySelectorToken, WritableToken } from "atom.io"
|
|
2
|
-
import { isJson } from "atom.io/json"
|
|
3
2
|
import { useI, useO } from "atom.io/react"
|
|
4
3
|
import type { FC } from "react"
|
|
5
4
|
|
|
6
|
-
import {
|
|
7
|
-
import { JsonEditor } from "~/packages/hamr/react-json-editor/src"
|
|
8
|
-
|
|
9
|
-
export const fallback = <T,>(fn: () => T, fallbackValue: T): T => {
|
|
10
|
-
try {
|
|
11
|
-
return fn()
|
|
12
|
-
} catch (_) {
|
|
13
|
-
return fallbackValue
|
|
14
|
-
}
|
|
15
|
-
}
|
|
5
|
+
import { JsonEditor } from "./json-editor"
|
|
16
6
|
|
|
17
7
|
export const StateEditor: FC<{
|
|
18
8
|
token: WritableToken<unknown>
|
|
19
9
|
}> = ({ token }) => {
|
|
20
10
|
const set = useI(token)
|
|
21
11
|
const data = useO(token)
|
|
22
|
-
return
|
|
23
|
-
<JsonEditor data={data} set={set}
|
|
24
|
-
) : (
|
|
25
|
-
<div className="json_editor">
|
|
26
|
-
<ElasticInput
|
|
27
|
-
value={
|
|
28
|
-
data === undefined || data === null
|
|
29
|
-
? ``
|
|
30
|
-
: typeof data === `object` &&
|
|
31
|
-
`toJson` in data &&
|
|
32
|
-
typeof data.toJson === `function`
|
|
33
|
-
? JSON.stringify(data.toJson())
|
|
34
|
-
: data instanceof Set
|
|
35
|
-
? `Set { ${JSON.stringify([...data]).slice(1, -1)} }`
|
|
36
|
-
: Object.getPrototypeOf(data).constructor.name +
|
|
37
|
-
` ` +
|
|
38
|
-
fallback(() => JSON.stringify(data), `?`)
|
|
39
|
-
}
|
|
40
|
-
disabled={true}
|
|
41
|
-
/>
|
|
42
|
-
</div>
|
|
12
|
+
return (
|
|
13
|
+
<JsonEditor testid={`${token.key}-state-editor`} data={data} set={set} />
|
|
43
14
|
)
|
|
44
15
|
}
|
|
45
16
|
|
|
@@ -47,26 +18,13 @@ export const ReadonlySelectorViewer: FC<{
|
|
|
47
18
|
token: ReadonlySelectorToken<unknown>
|
|
48
19
|
}> = ({ token }) => {
|
|
49
20
|
const data = useO(token)
|
|
50
|
-
return
|
|
21
|
+
return (
|
|
51
22
|
<JsonEditor
|
|
23
|
+
testid={`${token.key}-state-editor`}
|
|
52
24
|
data={data}
|
|
53
25
|
set={() => null}
|
|
54
|
-
schema={true}
|
|
55
26
|
isReadonly={() => true}
|
|
56
27
|
/>
|
|
57
|
-
) : (
|
|
58
|
-
<div className="json_editor">
|
|
59
|
-
<ElasticInput
|
|
60
|
-
value={
|
|
61
|
-
data instanceof Set
|
|
62
|
-
? `Set ` + JSON.stringify([...data])
|
|
63
|
-
: Object.getPrototypeOf(data).constructor.name +
|
|
64
|
-
` ` +
|
|
65
|
-
JSON.stringify(data)
|
|
66
|
-
}
|
|
67
|
-
disabled={true}
|
|
68
|
-
/>
|
|
69
|
-
</div>
|
|
70
28
|
)
|
|
71
29
|
}
|
|
72
30
|
|
|
@@ -4,15 +4,15 @@ import type {
|
|
|
4
4
|
RegularAtomToken,
|
|
5
5
|
} from "atom.io"
|
|
6
6
|
import { getState } from "atom.io"
|
|
7
|
-
import {
|
|
7
|
+
import { findInStore } from "atom.io/internal"
|
|
8
8
|
import type { FamilyNode, WritableTokenIndex } from "atom.io/introspection"
|
|
9
9
|
import { primitiveRefinery } from "atom.io/introspection"
|
|
10
10
|
import { useI, useO } from "atom.io/react"
|
|
11
|
-
import type
|
|
11
|
+
import { type FC, useContext } from "react"
|
|
12
12
|
|
|
13
13
|
import { button } from "./Button"
|
|
14
14
|
import { StoreEditor } from "./StateEditor"
|
|
15
|
-
import {
|
|
15
|
+
import { DevtoolsContext } from "./store"
|
|
16
16
|
|
|
17
17
|
/* eslint-disable no-console */
|
|
18
18
|
|
|
@@ -65,9 +65,12 @@ export const StateIndexTreeNode: FC<{
|
|
|
65
65
|
}> = ({ node, isOpenState }) => {
|
|
66
66
|
const setIsOpen = useI(isOpenState)
|
|
67
67
|
const isOpen = useO(isOpenState)
|
|
68
|
+
|
|
69
|
+
const { typeSelectors, viewIsOpenAtoms, store } = useContext(DevtoolsContext)
|
|
70
|
+
|
|
68
71
|
for (const [key, childNode] of node.familyMembers) {
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
findInStore(store, viewIsOpenAtoms, key)
|
|
73
|
+
findInStore(store, typeSelectors, childNode.key)
|
|
71
74
|
}
|
|
72
75
|
return (
|
|
73
76
|
<>
|
|
@@ -87,8 +90,8 @@ export const StateIndexTreeNode: FC<{
|
|
|
87
90
|
<StateIndexNode
|
|
88
91
|
key={key}
|
|
89
92
|
node={childNode}
|
|
90
|
-
isOpenState={
|
|
91
|
-
typeState={
|
|
93
|
+
isOpenState={findInStore(store, viewIsOpenAtoms, childNode.key)}
|
|
94
|
+
typeState={findInStore(store, typeSelectors, childNode.key)}
|
|
92
95
|
/>
|
|
93
96
|
))
|
|
94
97
|
: null}
|
|
@@ -120,6 +123,9 @@ export const StateIndex: FC<{
|
|
|
120
123
|
tokenIndex: ReadonlySelectorToken<WritableTokenIndex<ReadableToken<unknown>>>
|
|
121
124
|
}> = ({ tokenIndex }) => {
|
|
122
125
|
const tokenIds = useO(tokenIndex)
|
|
126
|
+
|
|
127
|
+
const { typeSelectors, viewIsOpenAtoms, store } = useContext(DevtoolsContext)
|
|
128
|
+
|
|
123
129
|
return (
|
|
124
130
|
<article className="index state_index" data-testid="state-index">
|
|
125
131
|
{[...tokenIds.entries()]
|
|
@@ -130,8 +136,8 @@ export const StateIndex: FC<{
|
|
|
130
136
|
<StateIndexNode
|
|
131
137
|
key={key}
|
|
132
138
|
node={node}
|
|
133
|
-
isOpenState={
|
|
134
|
-
typeState={
|
|
139
|
+
isOpenState={findInStore(store, viewIsOpenAtoms, node.key)}
|
|
140
|
+
typeState={findInStore(store, typeSelectors, node.key)}
|
|
135
141
|
/>
|
|
136
142
|
)
|
|
137
143
|
})}
|
|
@@ -4,13 +4,12 @@ import type {
|
|
|
4
4
|
TimelineToken,
|
|
5
5
|
} from "atom.io"
|
|
6
6
|
import { redo, undo } from "atom.io"
|
|
7
|
-
import {
|
|
8
|
-
import type { Timeline } from "atom.io/internal"
|
|
7
|
+
import { findInStore, type Timeline } from "atom.io/internal"
|
|
9
8
|
import { useI, useO } from "atom.io/react"
|
|
10
|
-
import { type FC, Fragment } from "react"
|
|
9
|
+
import { type FC, Fragment, useContext } from "react"
|
|
11
10
|
|
|
12
11
|
import { button } from "./Button"
|
|
13
|
-
import {
|
|
12
|
+
import { DevtoolsContext } from "./store"
|
|
14
13
|
import { article } from "./Updates"
|
|
15
14
|
|
|
16
15
|
export const YouAreHere: FC = () => {
|
|
@@ -86,7 +85,11 @@ export const TimelineLog: FC<{
|
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
export const TimelineIndex: FC = () => {
|
|
88
|
+
const { timelineIndex, timelineSelectors, viewIsOpenAtoms, store } =
|
|
89
|
+
useContext(DevtoolsContext)
|
|
90
|
+
|
|
89
91
|
const tokenIds = useO(timelineIndex)
|
|
92
|
+
|
|
90
93
|
return (
|
|
91
94
|
<article className="index timeline_index" data-testid="timeline-index">
|
|
92
95
|
{tokenIds
|
|
@@ -96,8 +99,8 @@ export const TimelineIndex: FC = () => {
|
|
|
96
99
|
<TimelineLog
|
|
97
100
|
key={token.key}
|
|
98
101
|
token={token}
|
|
99
|
-
isOpenState={
|
|
100
|
-
timelineState={
|
|
102
|
+
isOpenState={findInStore(store, viewIsOpenAtoms, token.key)}
|
|
103
|
+
timelineState={findInStore(store, timelineSelectors, token.key)}
|
|
101
104
|
/>
|
|
102
105
|
)
|
|
103
106
|
})}
|
|
@@ -4,17 +4,12 @@ import type {
|
|
|
4
4
|
TransactionToken,
|
|
5
5
|
TransactionUpdate,
|
|
6
6
|
} from "atom.io"
|
|
7
|
-
import {
|
|
8
|
-
import type { Func } from "atom.io/internal"
|
|
7
|
+
import { findInStore, type Func } from "atom.io/internal"
|
|
9
8
|
import { useI, useO } from "atom.io/react"
|
|
10
|
-
import type
|
|
9
|
+
import { type FC, useContext } from "react"
|
|
11
10
|
|
|
12
11
|
import { button } from "./Button"
|
|
13
|
-
import {
|
|
14
|
-
transactionIndex,
|
|
15
|
-
transactionLogSelectors,
|
|
16
|
-
viewIsOpenAtoms,
|
|
17
|
-
} from "./store"
|
|
12
|
+
import { DevtoolsContext } from "./store"
|
|
18
13
|
import { article } from "./Updates"
|
|
19
14
|
|
|
20
15
|
export const TransactionLog: FC<{
|
|
@@ -58,18 +53,21 @@ export const TransactionLog: FC<{
|
|
|
58
53
|
}
|
|
59
54
|
|
|
60
55
|
export const TransactionIndex: FC = () => {
|
|
56
|
+
const { transactionIndex, transactionLogSelectors, viewIsOpenAtoms, store } =
|
|
57
|
+
useContext(DevtoolsContext)
|
|
58
|
+
|
|
61
59
|
const tokenIds = useO(transactionIndex)
|
|
62
60
|
return (
|
|
63
61
|
<article className="index transaction_index" data-testid="transaction-index">
|
|
64
62
|
{tokenIds
|
|
65
|
-
.filter((token) => !token.key.startsWith(
|
|
63
|
+
.filter((token) => !token.key.startsWith(`🔍`))
|
|
66
64
|
.map((token) => {
|
|
67
65
|
return (
|
|
68
66
|
<TransactionLog
|
|
69
67
|
key={token.key}
|
|
70
68
|
token={token}
|
|
71
|
-
isOpenState={
|
|
72
|
-
logState={
|
|
69
|
+
isOpenState={findInStore(store, viewIsOpenAtoms, token.key)}
|
|
70
|
+
logState={findInStore(store, transactionLogSelectors, token.key)}
|
|
73
71
|
/>
|
|
74
72
|
)
|
|
75
73
|
})}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DetailedHTMLProps,
|
|
3
|
+
ForwardRefExoticComponent,
|
|
4
|
+
InputHTMLAttributes,
|
|
5
|
+
} from "react"
|
|
6
|
+
import {
|
|
7
|
+
forwardRef,
|
|
8
|
+
useImperativeHandle,
|
|
9
|
+
useLayoutEffect,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
} from "react"
|
|
13
|
+
|
|
14
|
+
export type ElasticInputProps = DetailedHTMLProps<
|
|
15
|
+
InputHTMLAttributes<HTMLInputElement>,
|
|
16
|
+
HTMLInputElement
|
|
17
|
+
> & {
|
|
18
|
+
widthPadding?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const ElasticInput: ForwardRefExoticComponent<
|
|
22
|
+
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> & {
|
|
23
|
+
widthPadding?: number
|
|
24
|
+
}
|
|
25
|
+
> = forwardRef(function ElasticInputFC(props, ref) {
|
|
26
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
27
|
+
const spanRef = useRef<HTMLSpanElement>(null)
|
|
28
|
+
const [inputWidth, setInputWidth] = useState(`auto`)
|
|
29
|
+
|
|
30
|
+
useImperativeHandle<Partial<HTMLInputElement>, Partial<HTMLInputElement>>(
|
|
31
|
+
ref,
|
|
32
|
+
() => ({
|
|
33
|
+
focus: () => {
|
|
34
|
+
inputRef.current?.focus()
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const extraWidth = props.type === `number` ? 15 : 0
|
|
40
|
+
|
|
41
|
+
useLayoutEffect(() => {
|
|
42
|
+
if (spanRef.current) {
|
|
43
|
+
setInputWidth(`${spanRef.current.offsetWidth + extraWidth}px`)
|
|
44
|
+
const interval = setInterval(() => {
|
|
45
|
+
if (spanRef.current) {
|
|
46
|
+
setInputWidth(`${spanRef.current.offsetWidth + extraWidth}px`)
|
|
47
|
+
}
|
|
48
|
+
}, 1000)
|
|
49
|
+
return () => {
|
|
50
|
+
clearInterval(interval)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}, [inputRef.current?.value, props.value])
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div style={{ display: `inline-block`, position: `relative` }}>
|
|
57
|
+
<input
|
|
58
|
+
{...props}
|
|
59
|
+
ref={inputRef}
|
|
60
|
+
style={{
|
|
61
|
+
padding: 0,
|
|
62
|
+
borderRadius: 0,
|
|
63
|
+
border: `none`,
|
|
64
|
+
fontFamily: `inherit`,
|
|
65
|
+
fontSize: `inherit`,
|
|
66
|
+
width: inputWidth,
|
|
67
|
+
...props.style,
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
<span
|
|
71
|
+
ref={spanRef}
|
|
72
|
+
style={{
|
|
73
|
+
padding: props.style?.padding,
|
|
74
|
+
position: `absolute`,
|
|
75
|
+
visibility: `hidden`,
|
|
76
|
+
// color: `red`,
|
|
77
|
+
whiteSpace: `pre`,
|
|
78
|
+
fontFamily: props.style?.fontFamily ?? `inherit`,
|
|
79
|
+
fontSize: props.style?.fontSize ?? `inherit`,
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
{props.value}
|
|
83
|
+
</span>
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
})
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { FC } from "react"
|
|
2
|
+
import { useId, useRef, useState } from "react"
|
|
3
|
+
|
|
4
|
+
import { ElasticInput } from "."
|
|
5
|
+
|
|
6
|
+
export function clampInto(min: number, max: number) {
|
|
7
|
+
return (value: number): number =>
|
|
8
|
+
value < min ? min : value > max ? max : value
|
|
9
|
+
}
|
|
10
|
+
function round(value: number, decimalPlaces?: number): number {
|
|
11
|
+
if (decimalPlaces === undefined) return value
|
|
12
|
+
const factor = 10 ** decimalPlaces
|
|
13
|
+
return Math.round(value * factor) / factor
|
|
14
|
+
}
|
|
15
|
+
function roundAndPad(value: number, decimalPlaces?: number): string {
|
|
16
|
+
const roundedValue = round(value, decimalPlaces)
|
|
17
|
+
const paddedString = roundedValue.toFixed(decimalPlaces)
|
|
18
|
+
return paddedString
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const VALID_NON_NUMBERS = [``, `-`, `.`, `-.`] as const
|
|
22
|
+
export type ValidNonNumber = (typeof VALID_NON_NUMBERS)[number]
|
|
23
|
+
export const isValidNonNumber = (input: string): input is ValidNonNumber =>
|
|
24
|
+
VALID_NON_NUMBERS.includes(input as ValidNonNumber)
|
|
25
|
+
export const VALID_NON_NUMBER_INTERPRETATIONS: Readonly<
|
|
26
|
+
Record<ValidNonNumber, number | null>
|
|
27
|
+
> = {
|
|
28
|
+
"": null,
|
|
29
|
+
"-": 0,
|
|
30
|
+
".": 0,
|
|
31
|
+
"-.": 0,
|
|
32
|
+
} as const
|
|
33
|
+
export type DecimalInProgress = `${number | ``}.${number}`
|
|
34
|
+
export const isDecimalInProgress = (input: string): input is DecimalInProgress =>
|
|
35
|
+
input === `0` || (!Number.isNaN(Number(input)) && input.includes(`.`))
|
|
36
|
+
|
|
37
|
+
const textToValue = (input: string, allowDecimal: boolean): number | null => {
|
|
38
|
+
if (isValidNonNumber(input)) return VALID_NON_NUMBER_INTERPRETATIONS[input]
|
|
39
|
+
return allowDecimal
|
|
40
|
+
? Number.parseFloat(input)
|
|
41
|
+
: Math.round(Number.parseFloat(input))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type NumberConstraints = {
|
|
45
|
+
max: number
|
|
46
|
+
min: number
|
|
47
|
+
decimalPlaces: number
|
|
48
|
+
nullable: boolean
|
|
49
|
+
}
|
|
50
|
+
export const DEFAULT_NUMBER_CONSTRAINTS: NumberConstraints = {
|
|
51
|
+
max: Number.POSITIVE_INFINITY,
|
|
52
|
+
min: Number.NEGATIVE_INFINITY,
|
|
53
|
+
decimalPlaces: 100,
|
|
54
|
+
nullable: true,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const initRefinery =
|
|
58
|
+
<Constraints extends NumberConstraints>(
|
|
59
|
+
constraints: { [K in keyof Constraints]?: Constraints[K] | undefined },
|
|
60
|
+
) =>
|
|
61
|
+
(
|
|
62
|
+
input: number | null,
|
|
63
|
+
): Constraints extends { nullable: true | undefined }
|
|
64
|
+
? number | null
|
|
65
|
+
: number => {
|
|
66
|
+
if (input === null && constraints.nullable === true) {
|
|
67
|
+
return null as Constraints extends { nullable: true }
|
|
68
|
+
? number | null
|
|
69
|
+
: number
|
|
70
|
+
}
|
|
71
|
+
const { max, min, decimalPlaces } = {
|
|
72
|
+
...DEFAULT_NUMBER_CONSTRAINTS,
|
|
73
|
+
...constraints,
|
|
74
|
+
}
|
|
75
|
+
let constrained = clampInto(min, max)(input ?? 0)
|
|
76
|
+
if (decimalPlaces) {
|
|
77
|
+
constrained = round(constrained, decimalPlaces)
|
|
78
|
+
}
|
|
79
|
+
return constrained
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const valueToText = (numericValue: number | null): string => {
|
|
83
|
+
if (numericValue === null || numericValue === undefined) {
|
|
84
|
+
return ``
|
|
85
|
+
}
|
|
86
|
+
return numericValue.toString()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type NumberInputProps = Partial<NumberConstraints> & {
|
|
90
|
+
autoSize?: boolean
|
|
91
|
+
disabled?: boolean
|
|
92
|
+
id?: string
|
|
93
|
+
label?: string
|
|
94
|
+
name?: string
|
|
95
|
+
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
|
|
96
|
+
onClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void
|
|
97
|
+
placeholder?: string
|
|
98
|
+
set?: ((newValue: number | null) => void) | undefined
|
|
99
|
+
testid?: string
|
|
100
|
+
value?: number | null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const NumberInput: FC<NumberInputProps> = ({
|
|
104
|
+
autoSize = false,
|
|
105
|
+
decimalPlaces,
|
|
106
|
+
disabled = false,
|
|
107
|
+
label,
|
|
108
|
+
max,
|
|
109
|
+
min,
|
|
110
|
+
name,
|
|
111
|
+
onChange,
|
|
112
|
+
onClick,
|
|
113
|
+
placeholder = ``,
|
|
114
|
+
set = () => null,
|
|
115
|
+
testid,
|
|
116
|
+
value = null,
|
|
117
|
+
}) => {
|
|
118
|
+
const id = useId()
|
|
119
|
+
const [temporaryEntry, setTemporaryEntry] = useState<
|
|
120
|
+
DecimalInProgress | ValidNonNumber | null
|
|
121
|
+
>(null)
|
|
122
|
+
const userHasMadeDeliberateChange = useRef<boolean>(false)
|
|
123
|
+
|
|
124
|
+
const refine = initRefinery({ max, min, decimalPlaces, nullable: true })
|
|
125
|
+
|
|
126
|
+
const allowDecimal = decimalPlaces === undefined || decimalPlaces > 0
|
|
127
|
+
|
|
128
|
+
const handleBlur = () => {
|
|
129
|
+
if (userHasMadeDeliberateChange.current) {
|
|
130
|
+
set(refine(value ?? null))
|
|
131
|
+
setTemporaryEntry(null)
|
|
132
|
+
}
|
|
133
|
+
userHasMadeDeliberateChange.current = false
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
137
|
+
if (onChange) onChange(event)
|
|
138
|
+
if (set === undefined) return
|
|
139
|
+
userHasMadeDeliberateChange.current = true
|
|
140
|
+
const input = event.target.value
|
|
141
|
+
if (isValidNonNumber(input) || isDecimalInProgress(input)) {
|
|
142
|
+
setTemporaryEntry(input)
|
|
143
|
+
const textInterpretation = isDecimalInProgress(input)
|
|
144
|
+
? input
|
|
145
|
+
: min?.toString() ?? `0`
|
|
146
|
+
const newValue = textToValue(textInterpretation, allowDecimal)
|
|
147
|
+
set(refine(newValue))
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
setTemporaryEntry(null)
|
|
151
|
+
const inputIsNumeric =
|
|
152
|
+
(!Number.isNaN(Number(input)) && !input.includes(` `)) ||
|
|
153
|
+
(allowDecimal && input === `.`) ||
|
|
154
|
+
(allowDecimal && input === `-.`) ||
|
|
155
|
+
input === `` ||
|
|
156
|
+
input === `-`
|
|
157
|
+
const numericValue = textToValue(input, allowDecimal)
|
|
158
|
+
|
|
159
|
+
if (inputIsNumeric) {
|
|
160
|
+
set(refine(numericValue))
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const displayValue =
|
|
165
|
+
temporaryEntry ?? valueToText(value ? refine(value) : value)
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<span>
|
|
169
|
+
{label && <label htmlFor={id}>{label}</label>}
|
|
170
|
+
{autoSize ? (
|
|
171
|
+
<ElasticInput
|
|
172
|
+
type="text"
|
|
173
|
+
value={displayValue}
|
|
174
|
+
placeholder={placeholder ?? `-`}
|
|
175
|
+
onChange={handleChange}
|
|
176
|
+
onBlur={handleBlur}
|
|
177
|
+
disabled={disabled}
|
|
178
|
+
name={name ?? id}
|
|
179
|
+
id={id}
|
|
180
|
+
onClick={onClick}
|
|
181
|
+
data-testid={testid}
|
|
182
|
+
/>
|
|
183
|
+
) : (
|
|
184
|
+
<input
|
|
185
|
+
type="text"
|
|
186
|
+
value={displayValue}
|
|
187
|
+
placeholder={placeholder ?? `-`}
|
|
188
|
+
onChange={handleChange}
|
|
189
|
+
onBlur={handleBlur}
|
|
190
|
+
disabled={disabled}
|
|
191
|
+
name={name ?? id}
|
|
192
|
+
id={id}
|
|
193
|
+
onClick={onClick}
|
|
194
|
+
data-testid={testid}
|
|
195
|
+
/>
|
|
196
|
+
)}
|
|
197
|
+
</span>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { FC } from "react"
|
|
2
|
+
|
|
3
|
+
import { ElasticInput } from "."
|
|
4
|
+
|
|
5
|
+
export type TextInputProps = {
|
|
6
|
+
value: string
|
|
7
|
+
set?: ((value: string) => void) | undefined
|
|
8
|
+
label?: string
|
|
9
|
+
placeholder?: string
|
|
10
|
+
autoSize?: boolean
|
|
11
|
+
readOnly?: boolean
|
|
12
|
+
testid?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const TextInput: FC<TextInputProps> = ({
|
|
16
|
+
value,
|
|
17
|
+
set,
|
|
18
|
+
label,
|
|
19
|
+
placeholder,
|
|
20
|
+
autoSize = false,
|
|
21
|
+
testid,
|
|
22
|
+
}) => {
|
|
23
|
+
return (
|
|
24
|
+
<span>
|
|
25
|
+
<label>{label}</label>
|
|
26
|
+
{autoSize ? (
|
|
27
|
+
<ElasticInput
|
|
28
|
+
type="text"
|
|
29
|
+
value={value}
|
|
30
|
+
onChange={(e) => set?.(e.target.value)}
|
|
31
|
+
disabled={set === undefined}
|
|
32
|
+
placeholder={placeholder}
|
|
33
|
+
data-testid={testid}
|
|
34
|
+
/>
|
|
35
|
+
) : (
|
|
36
|
+
<input
|
|
37
|
+
type="text"
|
|
38
|
+
value={value}
|
|
39
|
+
onChange={(e) => set?.(e.target.value)}
|
|
40
|
+
disabled={set === undefined}
|
|
41
|
+
placeholder={placeholder}
|
|
42
|
+
data-testid={testid}
|
|
43
|
+
/>
|
|
44
|
+
)}
|
|
45
|
+
</span>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ErrorInfo, FC } from "react"
|
|
2
|
+
|
|
3
|
+
export type FallbackProps = {
|
|
4
|
+
error?: Error | string | undefined
|
|
5
|
+
errorInfo?: ErrorInfo | undefined
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const DefaultFallback: FC<FallbackProps> = ({ error, errorInfo }) => {
|
|
9
|
+
const component = errorInfo?.componentStack?.split(` `).filter(Boolean)[2]
|
|
10
|
+
const message =
|
|
11
|
+
error?.toString() ?? errorInfo?.componentStack ?? `Unknown error`
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
data-testid="error-boundary"
|
|
15
|
+
style={{
|
|
16
|
+
flex: `1`,
|
|
17
|
+
background: `black`,
|
|
18
|
+
backgroundImage: `url(./src/assets/kablooey.gif)`,
|
|
19
|
+
backgroundPosition: `center`,
|
|
20
|
+
// backgroundRepeat: `no-repeat`,
|
|
21
|
+
backgroundSize: `overlay`,
|
|
22
|
+
}}
|
|
23
|
+
>
|
|
24
|
+
{/* <img src="./src/assets/kablooey.gif" alt="error" /> */}
|
|
25
|
+
<div
|
|
26
|
+
style={{
|
|
27
|
+
margin: `50px`,
|
|
28
|
+
marginTop: `0`,
|
|
29
|
+
padding: `50px`,
|
|
30
|
+
border: `1px solid dashed`,
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
<span
|
|
34
|
+
style={{
|
|
35
|
+
background: `black`,
|
|
36
|
+
color: `white`,
|
|
37
|
+
padding: 10,
|
|
38
|
+
paddingTop: 5,
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
{`⚠️ `}
|
|
42
|
+
<span style={{ color: `#fc0`, fontWeight: 700 }}>{component}</span>
|
|
43
|
+
{` ⚠️ `}
|
|
44
|
+
{message}
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
}
|