atom.io 0.28.1 → 0.29.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 (68) hide show
  1. package/dist/{chunk-6WL4RQMQ.js → chunk-XPYU2HY2.js} +22 -57
  2. package/eslint-plugin/dist/index.js +0 -1
  3. package/eslint-plugin/src/walk.ts +0 -1
  4. package/internal/dist/index.d.ts +4 -4
  5. package/internal/dist/index.js +1 -1
  6. package/internal/src/atom/dispose-atom.ts +0 -11
  7. package/internal/src/ingest-updates/ingest-creation-disposal.ts +16 -25
  8. package/internal/src/operation.ts +7 -7
  9. package/internal/src/selector/create-writable-selector.ts +1 -1
  10. package/internal/src/selector/dispose-selector.ts +0 -13
  11. package/internal/src/set-state/become.ts +1 -3
  12. package/internal/src/set-state/evict-downstream.ts +2 -2
  13. package/internal/src/set-state/set-atom.ts +1 -1
  14. package/internal/src/set-state/set-into-store.ts +1 -1
  15. package/internal/src/set-state/stow-update.ts +2 -2
  16. package/internal/src/store/store.ts +1 -1
  17. package/introspection/dist/index.d.ts +15 -6
  18. package/introspection/dist/index.js +620 -1
  19. package/introspection/src/attach-atom-index.ts +5 -6
  20. package/introspection/src/attach-introspection-states.ts +5 -6
  21. package/introspection/src/attach-selector-index.ts +6 -7
  22. package/introspection/src/attach-timeline-family.ts +3 -4
  23. package/introspection/src/attach-timeline-index.ts +4 -8
  24. package/introspection/src/attach-transaction-index.ts +4 -8
  25. package/introspection/src/attach-transaction-logs.ts +4 -8
  26. package/introspection/src/attach-type-selectors.ts +13 -6
  27. package/introspection/src/differ.ts +1 -1
  28. package/introspection/src/index.ts +1 -0
  29. package/introspection/src/refinery.ts +9 -7
  30. package/introspection/src/sprawl.ts +42 -0
  31. package/json/dist/index.d.ts +12 -1
  32. package/json/dist/index.js +111 -2
  33. package/json/src/index.ts +29 -0
  34. package/package.json +12 -12
  35. package/react-devtools/dist/index.d.ts +159 -2
  36. package/react-devtools/dist/index.js +260 -663
  37. package/react-devtools/src/AtomIODevtools.tsx +24 -13
  38. package/react-devtools/src/StateEditor.tsx +5 -47
  39. package/react-devtools/src/StateIndex.tsx +15 -9
  40. package/react-devtools/src/TimelineIndex.tsx +9 -6
  41. package/react-devtools/src/TransactionIndex.tsx +9 -11
  42. package/react-devtools/src/elastic-input/ElasticInput.tsx +86 -0
  43. package/react-devtools/src/elastic-input/NumberInput.tsx +199 -0
  44. package/react-devtools/src/elastic-input/TextInput.tsx +47 -0
  45. package/react-devtools/src/elastic-input/index.ts +3 -0
  46. package/react-devtools/src/error-boundary/DefaultFallback.tsx +49 -0
  47. package/react-devtools/src/error-boundary/ReactErrorBoundary.tsx +56 -0
  48. package/react-devtools/src/error-boundary/index.ts +2 -0
  49. package/react-devtools/src/index.ts +3 -0
  50. package/react-devtools/src/json-editor/assets/Untitled-1.ai +1436 -2
  51. package/react-devtools/src/json-editor/assets/data-vis.ai +1548 -1
  52. package/react-devtools/src/json-editor/comp/json-editor-sketches.ai +1451 -3
  53. package/react-devtools/src/json-editor/default-components.tsx +101 -0
  54. package/react-devtools/src/json-editor/developer-interface.tsx +81 -0
  55. package/react-devtools/src/json-editor/editors-by-type/array-editor.tsx +38 -0
  56. package/react-devtools/src/json-editor/editors-by-type/non-json.tsx +23 -0
  57. package/react-devtools/src/json-editor/editors-by-type/object-editor.tsx +128 -0
  58. package/react-devtools/src/json-editor/editors-by-type/primitive-editors.tsx +73 -0
  59. package/react-devtools/src/json-editor/editors-by-type/utilities/array-elements.ts +16 -0
  60. package/react-devtools/src/json-editor/editors-by-type/utilities/cast-json.ts +57 -0
  61. package/react-devtools/src/json-editor/editors-by-type/utilities/cast-to-json.ts +156 -0
  62. package/react-devtools/src/json-editor/editors-by-type/utilities/object-properties.ts +106 -0
  63. package/react-devtools/src/json-editor/index.ts +32 -0
  64. package/react-devtools/src/json-editor/json-editor-internal.tsx +128 -0
  65. package/react-devtools/src/json-editor/todo.md +7 -0
  66. package/react-devtools/src/store.ts +70 -46
  67. package/dist/chunk-D52JNVER.js +0 -721
  68. 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 = (): JSX.Element => {
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
- ) : devtoolsView === `timelines` ? (
99
+ ) : (
89
100
  <TimelineIndex />
90
- ) : null}
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 { ElasticInput } from "~/packages/hamr/react-elastic-input/src"
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 isJson(data) ? (
23
- <JsonEditor data={data} set={set} schema={true} />
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 isJson(data) ? (
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 { findState } from "atom.io/ephemeral"
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 { FC } from "react"
11
+ import { type FC, useContext } from "react"
12
12
 
13
13
  import { button } from "./Button"
14
14
  import { StoreEditor } from "./StateEditor"
15
- import { typeSelectors, viewIsOpenAtoms } from "./store"
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
- findState(viewIsOpenAtoms, key)
70
- findState(typeSelectors, childNode.key)
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={findState(viewIsOpenAtoms, childNode.key)}
91
- typeState={findState(typeSelectors, childNode.key)}
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={findState(viewIsOpenAtoms, node.key)}
134
- typeState={findState(typeSelectors, node.key)}
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 { findState } from "atom.io/ephemeral"
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 { timelineIndex, timelineSelectors, viewIsOpenAtoms } from "./store"
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={findState(viewIsOpenAtoms, token.key)}
100
- timelineState={findState(timelineSelectors, token.key)}
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 { findState } from "atom.io/ephemeral"
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 { FC } from "react"
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={findState(viewIsOpenAtoms, token.key)}
72
- logState={findState(transactionLogSelectors, token.key)}
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,3 @@
1
+ export * from "./ElasticInput"
2
+ export * from "./NumberInput"
3
+ export * from "./TextInput"
@@ -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
+ }