@design-edito/tools 0.5.2 → 0.5.4

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 (152) hide show
  1. package/agnostic/html/deep-select/index.js +5 -3
  2. package/agnostic/html/hyper-json/smart-tags/coalesced/index.d.ts +9 -9
  3. package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +9 -9
  4. package/agnostic/html/hyper-json/smart-tags/isolated/index.d.ts +3 -3
  5. package/agnostic/html/hyper-json/smart-tags/isolated/index.js +3 -3
  6. package/agnostic/html/index.d.ts +1 -1
  7. package/agnostic/html/index.js +1 -1
  8. package/agnostic/index.d.ts +2 -2
  9. package/agnostic/index.js +2 -2
  10. package/agnostic/misc/index.d.ts +3 -3
  11. package/agnostic/misc/index.js +3 -3
  12. package/agnostic/misc/logs/index.d.ts +1 -1
  13. package/agnostic/misc/logs/index.js +1 -1
  14. package/agnostic/numbers/index.d.ts +1 -1
  15. package/agnostic/numbers/index.js +1 -1
  16. package/agnostic/optim/index.d.ts +1 -1
  17. package/agnostic/optim/index.js +1 -1
  18. package/agnostic/strings/index.d.ts +2 -2
  19. package/agnostic/strings/index.js +2 -2
  20. package/agnostic/time/dates/format-date/index.d.ts +4 -2
  21. package/agnostic/time/dates/format-date/index.js +43 -94
  22. package/agnostic/time/dates/format-date/index.test.js +154 -0
  23. package/agnostic/time/dates/format-date/parts.d.ts +28 -0
  24. package/agnostic/time/dates/format-date/parts.js +136 -0
  25. package/agnostic/time/dates/format-date/types.d.ts +84 -0
  26. package/agnostic/time/dates/format-date/types.js +1 -0
  27. package/agnostic/time/duration/format-duration/index.d.ts +42 -0
  28. package/agnostic/time/duration/format-duration/index.js +94 -0
  29. package/agnostic/time/duration/format-duration/index.test.d.ts +1 -0
  30. package/agnostic/time/duration/format-duration/index.test.js +148 -0
  31. package/agnostic/time/duration/format-duration/parts.d.ts +32 -0
  32. package/agnostic/time/duration/format-duration/parts.js +107 -0
  33. package/agnostic/time/duration/format-duration/types.d.ts +93 -0
  34. package/agnostic/time/duration/format-duration/types.js +1 -0
  35. package/agnostic/time/duration/index.d.ts +6 -1
  36. package/agnostic/time/duration/index.js +2 -2
  37. package/agnostic/time/duration/index.test.d.ts +1 -0
  38. package/agnostic/time/duration/index.test.js +168 -0
  39. package/agnostic/time/index.d.ts +1 -1
  40. package/agnostic/time/index.js +1 -1
  41. package/components/BeforeAfter/index.controlled.d.ts +25 -25
  42. package/components/BeforeAfter/index.controlled.js +47 -53
  43. package/components/BeforeAfter/index.d.ts +20 -23
  44. package/components/BeforeAfter/index.js +48 -40
  45. package/components/Button/index.js +1 -1
  46. package/components/Clippable/index.d.ts +17 -26
  47. package/components/Clippable/index.js +21 -23
  48. package/components/Disclaimer/index.d.ts +32 -24
  49. package/components/Disclaimer/index.js +29 -24
  50. package/components/Drawer/index.d.ts +45 -25
  51. package/components/Drawer/index.js +61 -52
  52. package/components/EventListener/index.d.ts +4 -5
  53. package/components/Gallery/index.d.ts +14 -16
  54. package/components/Gallery/index.js +25 -30
  55. package/components/Iframe/index.js +2 -2
  56. package/components/Image/index.d.ts +5 -15
  57. package/components/Image/index.js +9 -34
  58. package/components/Input/index.d.ts +23 -19
  59. package/components/Input/index.js +30 -20
  60. package/components/JsonEditor/index.d.ts +177 -9
  61. package/components/JsonEditor/index.js +133 -77
  62. package/components/JsonEditor/types.d.ts +11 -0
  63. package/components/JsonEditor/types.js +1 -0
  64. package/components/JsonEditor/utils.d.ts +8 -0
  65. package/components/JsonEditor/utils.js +23 -0
  66. package/components/ListLoader/index.controlled.d.ts +5 -5
  67. package/components/ListLoader/index.controlled.js +4 -4
  68. package/components/ListLoader/index.d.ts +11 -11
  69. package/components/ListLoader/index.js +12 -12
  70. package/components/Overlayer/index.d.ts +4 -4
  71. package/components/Overlayer/index.js +6 -6
  72. package/components/Paginator/index.d.ts +7 -12
  73. package/components/Paginator/index.js +7 -11
  74. package/components/ResizeObserver/index.d.ts +14 -3
  75. package/components/ResizeObserver/index.js +49 -24
  76. package/components/Scrllgngn/index.d.ts +17 -13
  77. package/components/Scrllgngn/index.js +25 -84
  78. package/components/Scrllgngn/utils.d.ts +52 -0
  79. package/components/Scrllgngn/utils.js +102 -0
  80. package/components/ScrollListener/index.d.ts +43 -28
  81. package/components/ScrollListener/index.js +59 -79
  82. package/components/ScrollListener/utils.d.ts +37 -16
  83. package/components/ScrollListener/utils.js +141 -28
  84. package/components/Select/index.d.ts +24 -20
  85. package/components/Select/index.js +30 -20
  86. package/components/Sequencer/index.controlled.d.ts +16 -26
  87. package/components/Sequencer/index.controlled.js +15 -12
  88. package/components/Sequencer/index.d.ts +25 -30
  89. package/components/Sequencer/index.js +20 -34
  90. package/components/Subtitles/index.d.ts +29 -5
  91. package/components/Subtitles/index.js +90 -12
  92. package/components/Textarea/index.d.ts +26 -20
  93. package/components/Textarea/index.js +40 -21
  94. package/components/Theatre/index.d.ts +35 -39
  95. package/components/Theatre/index.js +46 -58
  96. package/components/UIModule/index.d.ts +25 -29
  97. package/components/UIModule/index.js +81 -110
  98. package/components/Video/index.controlled.d.ts +51 -51
  99. package/components/Video/index.controlled.js +75 -72
  100. package/components/Video/index.d.ts +12 -14
  101. package/components/Video/index.js +41 -86
  102. package/components/Video/utils.d.ts +35 -3
  103. package/components/Video/utils.js +37 -14
  104. package/components/index.d.ts +6 -6
  105. package/components/index.js +6 -6
  106. package/components/utils/index.d.ts +22 -0
  107. package/components/utils/index.js +31 -0
  108. package/index.d.ts +1 -1
  109. package/index.js +1 -1
  110. package/node/@aws-s3/storage/directory/index.d.ts +2 -2
  111. package/node/@aws-s3/storage/directory/index.js +2 -2
  112. package/node/@aws-s3/storage/file/index.d.ts +1 -1
  113. package/node/@aws-s3/storage/file/index.js +1 -1
  114. package/node/@aws-s3/storage/index.d.ts +1 -1
  115. package/node/@aws-s3/storage/index.js +1 -1
  116. package/node/@google-cloud/storage/directory/index.d.ts +2 -2
  117. package/node/@google-cloud/storage/directory/index.js +2 -2
  118. package/node/@google-cloud/storage/file/index.d.ts +2 -2
  119. package/node/@google-cloud/storage/file/index.js +2 -2
  120. package/node/@google-cloud/storage/index.d.ts +1 -1
  121. package/node/@google-cloud/storage/index.js +1 -1
  122. package/node/cloud-storage/operations/index.d.ts +2 -2
  123. package/node/cloud-storage/operations/index.js +2 -2
  124. package/node/ftps/file/index.d.ts +2 -2
  125. package/node/ftps/file/index.js +2 -2
  126. package/node/images/index.d.ts +1 -1
  127. package/node/images/index.js +1 -1
  128. package/node/images/transform/operations/index.d.ts +2 -2
  129. package/node/images/transform/operations/index.js +2 -2
  130. package/node/index.d.ts +1 -1
  131. package/node/index.js +1 -1
  132. package/node/process/index.d.ts +1 -1
  133. package/node/process/index.js +1 -1
  134. package/node/sftp/file/index.d.ts +2 -2
  135. package/node/sftp/file/index.js +2 -2
  136. package/node/sftp/index.d.ts +1 -1
  137. package/node/sftp/index.js +1 -1
  138. package/package.json +20 -5
  139. package/TODO.md +0 -269
  140. package/components/BeforeAfter/utils.d.ts +0 -4
  141. package/components/BeforeAfter/utils.js +0 -9
  142. package/components/Input/index.controlled.d.ts +0 -42
  143. package/components/Input/index.controlled.js +0 -54
  144. package/components/Select/index.controlled.d.ts +0 -43
  145. package/components/Select/index.controlled.js +0 -54
  146. package/components/Subtitles/types.d.ts +0 -24
  147. package/components/Subtitles/utils.d.ts +0 -32
  148. package/components/Subtitles/utils.js +0 -107
  149. package/components/Textarea/index.controlled.d.ts +0 -50
  150. package/components/Textarea/index.controlled.js +0 -75
  151. /package/{components/Subtitles/types.js → agnostic/time/dates/format-date/index.test.d.ts} +0 -0
  152. /package/components/ResizeObserver/{style.module.css → styles.module.css} +0 -0
@@ -1,10 +1,7 @@
1
- import { type FunctionComponent } from 'react';
1
+ import { type FunctionComponent, type EventHandler, type ChangeEvent } from 'react';
2
2
  import { type ZodType, type core } from 'zod';
3
3
  import type { WithClassName } from '../utils/types.js';
4
- type JsonPrimitive = string | number | boolean | null;
5
- type JsonValue = JsonPrimitive | JsonValue[] | {
6
- [k: string]: JsonValue;
7
- };
4
+ import type { JsonValue } from './types.js';
8
5
  /**
9
6
  * Props for the {@link JsonEditor} component.
10
7
  *
@@ -14,6 +11,9 @@ type JsonValue = JsonPrimitive | JsonValue[] | {
14
11
  * with the list of Zod issues. Not called when no `schema` is provided.
15
12
  * @property schema - Optional Zod schema used to validate the value after each change.
16
13
  * When provided, validation runs after `onChange` is dispatched.
14
+ * @property preventSameNameKeys - Refuses a record key rename that would collide
15
+ * with a sibling key, instead of letting the renamed entry replace it. Applies to
16
+ * every record in the tree. Defaults to `false`.
17
17
  * @property className - Additional class name(s) applied to the root element.
18
18
  */
19
19
  export type Props = WithClassName<{
@@ -21,6 +21,7 @@ export type Props = WithClassName<{
21
21
  onChange?: (val: JsonValue) => void;
22
22
  onValidationError?: (issues: core.$ZodIssue[]) => void;
23
23
  schema?: ZodType<JsonValue>;
24
+ preventSameNameKeys?: boolean;
24
25
  }>;
25
26
  /**
26
27
  * Interactive JSON editor component.
@@ -38,22 +39,189 @@ export type Props = WithClassName<{
38
39
  * matching the current value type (`string`, `number`, `boolean`, `null`, `array`, `record`).
39
40
  * - `type` — the type selector control.
40
41
  * - `string`, `number`, `boolean`, `null` — primitive value inputs.
41
- * - `record` — record entry list.
42
- * - `array` — array entry list items.
42
+ * - `record` — record entry list; `record-item` — one entry of it.
43
+ * - `array` — array item list; `array-item` — one item of it.
43
44
  * - `prop-name` — key input for record entries.
44
45
  * - `create-prop` — button to append a new entry to a record or array.
45
46
  * - `delete-prop` — button to remove an existing entry.
46
47
  * - `lift-prop` — button to move an array entry up.
47
48
  * - `drop-prop` — button to move an array entry down.
49
+ * - `preview` — the serialized JSON preview.
48
50
  *
49
51
  * @param props - Component properties.
50
52
  * @see {@link Props}
51
53
  * @returns A recursive JSON value editor with a live serialized preview.
54
+ *
55
+ * @remarks
56
+ * **The editor is uncontrolled, at every level of the tree, and there is no
57
+ * controlled variant.** Each node seeds its state from its `defaultValue` on
58
+ * mount and never reads it again, so handing a mounted editor a new
59
+ * `defaultValue` changes nothing on screen — remount it with a different `key`
60
+ * to load another document. A controlled mode would mean rebuilding the state
61
+ * model so the whole tree reads from a single owner, which is why none of these
62
+ * components accepts a `value` prop.
52
63
  */
53
64
  export declare const JsonEditor: FunctionComponent<Props>;
54
- export declare const ValueEditor: FunctionComponent<{
65
+ /**
66
+ * Props for the {@link ValueEditor} component.
67
+ *
68
+ * @property defaultValue - Initial value of this node. Read on mount only.
69
+ * @property onChange - Called after the node's value changed, with the new value.
70
+ * @property path - Keys and indices leading to this node from the document root,
71
+ * exposed on the node as `data-path` and passed down to its children.
72
+ * @property preventSameNameKeys - Forwarded to every record below this node.
73
+ * @property className - Additional class name(s) applied to the root element.
74
+ */
75
+ export type ValueEditorProps = WithClassName<{
55
76
  defaultValue?: JsonValue;
56
77
  onChange?: (newValue: JsonValue) => void;
57
78
  path?: Array<string | number>;
79
+ preventSameNameKeys?: boolean;
80
+ }>;
81
+ /**
82
+ * One node of the editor tree: a type selector, plus the editor matching the
83
+ * value's current type. Switching type replaces the value with an empty one of
84
+ * the new type.
85
+ *
86
+ * @param props - Component properties.
87
+ * @see {@link ValueEditorProps}
88
+ * @returns A span holding the type selector and the type-appropriate editor.
89
+ */
90
+ export declare const ValueEditor: FunctionComponent<ValueEditorProps>;
91
+ /**
92
+ * Props for the {@link StringEditor} component.
93
+ *
94
+ * @property type - Which control to render. Defaults to a single-line `input`.
95
+ * @property defaultValue - Initial text. Read on mount only.
96
+ * @property onChange - Native change handler, forwarded to the control.
97
+ * @property className - Additional class name(s) applied to the control.
98
+ */
99
+ export type StringEditorProps = WithClassName<{
100
+ type?: 'input' | 'textarea';
101
+ defaultValue?: string;
102
+ onChange?: EventHandler<ChangeEvent<HTMLTextAreaElement | HTMLInputElement>>;
103
+ }>;
104
+ /**
105
+ * The editor for a string value.
106
+ *
107
+ * @param props - Component properties.
108
+ * @see {@link StringEditorProps}
109
+ * @returns A textarea or a text input, depending on `type`.
110
+ */
111
+ export declare const StringEditor: FunctionComponent<StringEditorProps>;
112
+ /**
113
+ * Props for the {@link NumberEditor} component.
114
+ *
115
+ * @property defaultValue - Initial number. Read on mount only.
116
+ * @property onChange - Native change handler, forwarded to the input.
117
+ * @property className - Additional class name(s) applied to the input.
118
+ */
119
+ export type NumberEditorProps = WithClassName<{
120
+ defaultValue?: number;
121
+ onChange?: EventHandler<ChangeEvent<HTMLInputElement>>;
122
+ }>;
123
+ /**
124
+ * The editor for a number value.
125
+ *
126
+ * @param props - Component properties.
127
+ * @see {@link NumberEditorProps}
128
+ * @returns A number input.
129
+ */
130
+ export declare const NumberEditor: FunctionComponent<NumberEditorProps>;
131
+ /**
132
+ * Props for the {@link BooleanEditor} component.
133
+ *
134
+ * @property defaultValue - Initial checked state. Read on mount only.
135
+ * @property onChange - Native change handler, forwarded to the input.
136
+ * @property className - Additional class name(s) applied to the input.
137
+ */
138
+ export type BooleanEditorProps = WithClassName<{
139
+ defaultValue?: boolean;
140
+ onChange?: EventHandler<ChangeEvent<HTMLInputElement>>;
58
141
  }>;
59
- export {};
142
+ /**
143
+ * The editor for a boolean value.
144
+ *
145
+ * @param props - Component properties.
146
+ * @see {@link BooleanEditorProps}
147
+ * @returns A checkbox.
148
+ */
149
+ export declare const BooleanEditor: FunctionComponent<BooleanEditorProps>;
150
+ /**
151
+ * Props for the {@link NullEditor} component.
152
+ *
153
+ * @property className - Additional class name(s) applied to the root element.
154
+ */
155
+ export type NullEditorProps = WithClassName<Record<string, never>>;
156
+ /**
157
+ * The editor for a null value — there is nothing to edit, so it only states the
158
+ * type.
159
+ *
160
+ * @param props - Component properties.
161
+ * @see {@link NullEditorProps}
162
+ * @returns A span reading `null`.
163
+ */
164
+ export declare const NullEditor: FunctionComponent<NullEditorProps>;
165
+ /**
166
+ * Props for the {@link RecordEditor} component.
167
+ *
168
+ * @property defaultValue - Initial entries. Read on mount only.
169
+ * @property onChange - Called after any entry is added, removed, renamed or
170
+ * edited, with the rebuilt record.
171
+ * @property path - Keys and indices leading to this record from the document
172
+ * root, extended with each entry's key before being passed down.
173
+ * @property preventSameNameKeys - Refuses a rename that would collide with a
174
+ * sibling key, instead of letting the renamed entry replace it. Defaults to
175
+ * `false`. Forwarded to every record nested below.
176
+ * @property className - Additional class name(s) applied to the root element.
177
+ */
178
+ export type RecordEditorProps = WithClassName<{
179
+ defaultValue?: Record<string, JsonValue>;
180
+ onChange?: (newValue: Record<string, JsonValue>) => void;
181
+ path?: Array<string | number>;
182
+ preventSameNameKeys?: boolean;
183
+ }>;
184
+ /**
185
+ * The editor for a record: one renamable, removable entry per key, each holding
186
+ * a nested {@link ValueEditor}.
187
+ *
188
+ * @param props - Component properties.
189
+ * @see {@link RecordEditorProps}
190
+ * @returns A list of entries, followed by the button that appends one.
191
+ *
192
+ * @remarks
193
+ * Entries are held in a `Map` keyed by property name, each carrying a stable id
194
+ * used as the React key — so renaming a property doesn't remount its editor and
195
+ * lose the subtree's state.
196
+ */
197
+ export declare const RecordEditor: FunctionComponent<RecordEditorProps>;
198
+ /**
199
+ * Props for the {@link ArrayEditor} component.
200
+ *
201
+ * @property defaultValue - Initial items. Read on mount only.
202
+ * @property onChange - Called after any item is added, removed, moved or edited,
203
+ * with the rebuilt array.
204
+ * @property path - Keys and indices leading to this array from the document
205
+ * root, extended with each item's index before being passed down.
206
+ * @property preventSameNameKeys - Forwarded to every record nested below.
207
+ * @property className - Additional class name(s) applied to the root element.
208
+ */
209
+ export type ArrayEditorProps = WithClassName<{
210
+ defaultValue?: JsonValue[];
211
+ onChange?: (newValue: JsonValue[]) => void;
212
+ path?: Array<string | number>;
213
+ preventSameNameKeys?: boolean;
214
+ }>;
215
+ /**
216
+ * The editor for an array: one removable, movable item per index, each holding a
217
+ * nested {@link ValueEditor}.
218
+ *
219
+ * @param props - Component properties.
220
+ * @see {@link ArrayEditorProps}
221
+ * @returns An ordered list of items, followed by the button that appends one.
222
+ *
223
+ * @remarks
224
+ * Items are held in a `Map` keyed by a stable id used as the React key, so
225
+ * reordering moves the editors rather than remounting them.
226
+ */
227
+ export declare const ArrayEditor: FunctionComponent<ArrayEditorProps>;
@@ -1,17 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { ZodError } from 'zod';
4
- import { clss } from '../../agnostic/css/clss/index.js';
5
- import { isNonNullObject } from '../../agnostic/objects/is-object/index.js';
6
4
  import { randomHash } from '../../agnostic/random/uuid/index.js';
5
+ import { isNonNullObject } from '../../agnostic/objects/is-object/index.js';
7
6
  import { Input } from '../Input/index.js';
8
7
  import { Select } from '../Select/index.js';
9
8
  import { Textarea } from '../Textarea/index.js';
10
9
  import { mergeClassNames } from '../utils/index.js';
11
- import { jsonEditor as publicClassName } from '../public-classnames.js';
12
- import cssModule from './styles.module.css';
13
- /* Shared across all sub components */
14
- const c = clss(publicClassName, { cssModule });
10
+ import { c, getValueType } from './utils.js';
15
11
  /**
16
12
  * Interactive JSON editor component.
17
13
  *
@@ -28,19 +24,29 @@ const c = clss(publicClassName, { cssModule });
28
24
  * matching the current value type (`string`, `number`, `boolean`, `null`, `array`, `record`).
29
25
  * - `type` — the type selector control.
30
26
  * - `string`, `number`, `boolean`, `null` — primitive value inputs.
31
- * - `record` — record entry list.
32
- * - `array` — array entry list items.
27
+ * - `record` — record entry list; `record-item` — one entry of it.
28
+ * - `array` — array item list; `array-item` — one item of it.
33
29
  * - `prop-name` — key input for record entries.
34
30
  * - `create-prop` — button to append a new entry to a record or array.
35
31
  * - `delete-prop` — button to remove an existing entry.
36
32
  * - `lift-prop` — button to move an array entry up.
37
33
  * - `drop-prop` — button to move an array entry down.
34
+ * - `preview` — the serialized JSON preview.
38
35
  *
39
36
  * @param props - Component properties.
40
37
  * @see {@link Props}
41
38
  * @returns A recursive JSON value editor with a live serialized preview.
39
+ *
40
+ * @remarks
41
+ * **The editor is uncontrolled, at every level of the tree, and there is no
42
+ * controlled variant.** Each node seeds its state from its `defaultValue` on
43
+ * mount and never reads it again, so handing a mounted editor a new
44
+ * `defaultValue` changes nothing on screen — remount it with a different `key`
45
+ * to load another document. A controlled mode would mean rebuilding the state
46
+ * model so the whole tree reads from a single owner, which is why none of these
47
+ * components accepts a `value` prop.
42
48
  */
43
- export const JsonEditor = ({ defaultValue = null, onChange, onValidationError, schema, className }) => {
49
+ export const JsonEditor = ({ defaultValue = null, onChange, onValidationError, schema, preventSameNameKeys, className }) => {
44
50
  const [value, setValue] = useState(defaultValue);
45
51
  const setValueAndDispatch = (val) => {
46
52
  setValue(val);
@@ -57,23 +63,19 @@ export const JsonEditor = ({ defaultValue = null, onChange, onValidationError, s
57
63
  throw err;
58
64
  }
59
65
  };
60
- const rootClss = mergeClassNames(c(null, {}), className);
61
- return _jsxs("div", { className: rootClss, children: [_jsx(ValueEditor, { defaultValue: defaultValue, onChange: val => setValueAndDispatch(val), path: [] }), _jsx("pre", { children: JSON.stringify(value, null, 2) })] });
66
+ const rootClss = mergeClassNames(c(), className);
67
+ return _jsxs("div", { className: rootClss, children: [_jsx(ValueEditor, { defaultValue: defaultValue, onChange: val => setValueAndDispatch(val), preventSameNameKeys: preventSameNameKeys, path: [] }), _jsx("pre", { className: c('preview'), children: JSON.stringify(value, null, 2) })] });
62
68
  };
63
- function getValueType(value) {
64
- if (value === null)
65
- return 'null';
66
- if (Array.isArray(value))
67
- return 'array';
68
- if (isNonNullObject(value))
69
- return 'record';
70
- if (typeof value === 'string')
71
- return 'string';
72
- if (typeof value === 'number')
73
- return 'number';
74
- return 'boolean';
75
- }
76
- export const ValueEditor = ({ defaultValue = {}, onChange, path = [] }) => {
69
+ /**
70
+ * One node of the editor tree: a type selector, plus the editor matching the
71
+ * value's current type. Switching type replaces the value with an empty one of
72
+ * the new type.
73
+ *
74
+ * @param props - Component properties.
75
+ * @see {@link ValueEditorProps}
76
+ * @returns A span holding the type selector and the type-appropriate editor.
77
+ */
78
+ export const ValueEditor = ({ defaultValue = null, onChange, path = [], preventSameNameKeys, className }) => {
77
79
  // State
78
80
  const [value, setValue] = useState(defaultValue);
79
81
  const setValueAndDispatch = (newValue) => {
@@ -92,7 +94,7 @@ export const ValueEditor = ({ defaultValue = {}, onChange, path = [] }) => {
92
94
  if (newType === 'null')
93
95
  return setValueAndDispatch(null);
94
96
  if (newType === 'record')
95
- return setValueAndDispatch({ a: 0, b: false });
97
+ return setValueAndDispatch({});
96
98
  if (newType === 'array')
97
99
  return setValueAndDispatch([]);
98
100
  };
@@ -103,48 +105,66 @@ export const ValueEditor = ({ defaultValue = {}, onChange, path = [] }) => {
103
105
  const handleArrayValueChange = e => setValueAndDispatch(e);
104
106
  // Rendering
105
107
  const valueType = getValueType(value);
106
- const valueEditorClss = c('value', valueType);
108
+ const valueEditorClss = mergeClassNames(c('value', valueType), className);
107
109
  const pathStringDataAttr = path.map(e => e.toString()).join('.');
108
- return _jsxs("span", { className: valueEditorClss, "data-path": pathStringDataAttr, children: [_jsxs(Select, { className: c('type'), value: valueType, onChange: handleTypeSelectChange, children: [_jsx("option", { value: 'string', children: "string" }), _jsx("option", { value: 'number', children: "number" }), _jsx("option", { value: 'boolean', children: "boolean" }), _jsx("option", { value: 'null', children: "null" }), _jsx("option", { value: 'record', children: "record" }), _jsx("option", { value: 'array', children: "array" })] }), typeof value === 'string' && _jsx(StringEditor, { type: 'textarea', defaultValue: value, onChange: handleStringValueChange }), typeof value === 'number' && _jsx(NumberEditor, { defaultValue: value, onChange: handleNumberValueChange }), typeof value === 'boolean' && _jsx(BooleanEditor, { defaultValue: value, onChange: handleBooleanValueChange }), value === null && _jsx(NullEditor, { defaultValue: value }), isNonNullObject(value) && !Array.isArray(value) && _jsx(RecordEditor, { defaultValue: value, onChange: handleRecordValueChange, path: path }), Array.isArray(value) && _jsx(ArrayEditor, { defaultValue: value, onChange: handleArrayValueChange, path: path })] });
110
+ return _jsxs("span", { className: valueEditorClss, "data-path": pathStringDataAttr, children: [_jsxs(Select, { className: c('type'), value: valueType, onChange: handleTypeSelectChange, children: [_jsx("option", { value: 'string', children: "string" }), _jsx("option", { value: 'number', children: "number" }), _jsx("option", { value: 'boolean', children: "boolean" }), _jsx("option", { value: 'null', children: "null" }), _jsx("option", { value: 'record', children: "record" }), _jsx("option", { value: 'array', children: "array" })] }), typeof value === 'string' && _jsx(StringEditor, { type: 'textarea', defaultValue: value, onChange: handleStringValueChange }), typeof value === 'number' && _jsx(NumberEditor, { defaultValue: value, onChange: handleNumberValueChange }), typeof value === 'boolean' && _jsx(BooleanEditor, { defaultValue: value, onChange: handleBooleanValueChange }), value === null && _jsx(NullEditor, {}), isNonNullObject(value) && !Array.isArray(value) && _jsx(RecordEditor, { defaultValue: value, onChange: handleRecordValueChange, preventSameNameKeys: preventSameNameKeys, path: path }), Array.isArray(value) && _jsx(ArrayEditor, { defaultValue: value, onChange: handleArrayValueChange, preventSameNameKeys: preventSameNameKeys, path: path })] });
109
111
  };
110
- /* * * * * * * * * * * * * * * * *
111
- *
112
- * String
113
- *
114
- * * * * * * * * * * * * * * * * */
115
- const StringEditor = (props) => props.type === 'textarea'
116
- ? _jsx(Textarea, { className: c('string'), defaultValue: props.defaultValue ?? '', onChange: props.onChange })
117
- : _jsx(Input, { type: 'text', className: c('string'), defaultValue: props.defaultValue ?? '', onChange: props.onChange });
118
- /* * * * * * * * * * * * * * * * *
119
- *
120
- * Number
121
- *
122
- * * * * * * * * * * * * * * * * */
123
- const NumberEditor = (props) => _jsx(Input, { className: c('number'), type: 'number', defaultValue: props.defaultValue ?? 0, onChange: props.onChange });
124
- /* * * * * * * * * * * * * * * * *
112
+ /**
113
+ * The editor for a string value.
125
114
  *
126
- * Boolean
115
+ * @param props - Component properties.
116
+ * @see {@link StringEditorProps}
117
+ * @returns A textarea or a text input, depending on `type`.
118
+ */
119
+ export const StringEditor = ({ type, defaultValue, onChange, className }) => type === 'textarea'
120
+ ? _jsx(Textarea, { className: mergeClassNames(c('string'), className), defaultValue: defaultValue ?? '', onChange: onChange })
121
+ : _jsx(Input, { type: 'text', className: mergeClassNames(c('string'), className), defaultValue: defaultValue ?? '', onChange: onChange });
122
+ /**
123
+ * The editor for a number value.
127
124
  *
128
- * * * * * * * * * * * * * * * * */
129
- const BooleanEditor = (props) => _jsx(Input, { className: c('boolean'), type: 'checkbox', defaultChecked: props.defaultValue ?? false, onChange: props.onChange });
130
- /* * * * * * * * * * * * * * * * *
125
+ * @param props - Component properties.
126
+ * @see {@link NumberEditorProps}
127
+ * @returns A number input.
128
+ */
129
+ export const NumberEditor = ({ defaultValue, onChange, className }) => _jsx(Input, { className: mergeClassNames(c('number'), className), type: 'number', defaultValue: defaultValue ?? 0, onChange: onChange });
130
+ /**
131
+ * The editor for a boolean value.
131
132
  *
132
- * Null
133
+ * @param props - Component properties.
134
+ * @see {@link BooleanEditorProps}
135
+ * @returns A checkbox.
136
+ */
137
+ export const BooleanEditor = ({ defaultValue, onChange, className }) => _jsx(Input, { className: mergeClassNames(c('boolean'), className), type: 'checkbox', defaultChecked: defaultValue ?? false, onChange: onChange });
138
+ /**
139
+ * The editor for a null value — there is nothing to edit, so it only states the
140
+ * type.
133
141
  *
134
- * * * * * * * * * * * * * * * * */
135
- const NullEditor = () => _jsx("span", { className: c('null'), children: "null" });
136
- /* * * * * * * * * * * * * * * * *
142
+ * @param props - Component properties.
143
+ * @see {@link NullEditorProps}
144
+ * @returns A span reading `null`.
145
+ */
146
+ export const NullEditor = ({ className }) => _jsx("span", { className: mergeClassNames(c('null'), className), children: "null" });
147
+ /**
148
+ * The editor for a record: one renamable, removable entry per key, each holding
149
+ * a nested {@link ValueEditor}.
137
150
  *
138
- * Record
151
+ * @param props - Component properties.
152
+ * @see {@link RecordEditorProps}
153
+ * @returns A list of entries, followed by the button that appends one.
139
154
  *
140
- * * * * * * * * * * * * * * * * */
141
- const RecordEditor = ({ defaultValue = {}, onChange, path }) => {
155
+ * @remarks
156
+ * Entries are held in a `Map` keyed by property name, each carrying a stable id
157
+ * used as the React key — so renaming a property doesn't remount its editor and
158
+ * lose the subtree's state.
159
+ */
160
+ export const RecordEditor = ({ defaultValue = {}, onChange, path, preventSameNameKeys = false, className }) => {
142
161
  const [value, setValue] = useState(new Map(Object
143
162
  .entries(defaultValue)
144
163
  .map(([key, val]) => [key, {
145
164
  id: randomHash(8),
146
165
  val
147
166
  }])));
167
+ const [refusedRenames, setRefusedRenames] = useState(0);
148
168
  const setValueAndDispatch = (newValue) => {
149
169
  setValue(newValue);
150
170
  onChange?.(Array.from(newValue).reduce((acc, [key, { val }]) => ({
@@ -158,7 +178,7 @@ const RecordEditor = ({ defaultValue = {}, onChange, path }) => {
158
178
  setValueAndDispatch(newValue);
159
179
  };
160
180
  const handleCreateProp = () => {
161
- let propName = '';
181
+ let propName = randomHash(4);
162
182
  while (value.get(propName) !== undefined) {
163
183
  propName = randomHash(4);
164
184
  }
@@ -168,12 +188,24 @@ const RecordEditor = ({ defaultValue = {}, onChange, path }) => {
168
188
  };
169
189
  const handleRenameProp = (oldName) => e => {
170
190
  const newName = e.target.value;
191
+ if (newName === oldName)
192
+ return;
193
+ if (preventSameNameKeys && value.has(newName)) {
194
+ // Refusing leaves the state untouched, and the name input is uncontrolled —
195
+ // it would go on showing what was typed. Bumping this counter is what puts
196
+ // the field back: it feeds the input's key, so the input remounts and reads
197
+ // its `defaultValue` again.
198
+ setRefusedRenames(count => count + 1);
199
+ return;
200
+ }
201
+ // Dropping the colliding entry first is what makes the outcome predictable:
202
+ // the renamed entry wins, and keeps its own position in the record. Rebuilding
203
+ // the Map without this leaves two entries under the same key, and `new Map`
204
+ // silently keeps the last one — losing the value being renamed.
171
205
  const newValue = new Map(Array
172
206
  .from(value)
173
- .map(([key, { id, val }]) => [
174
- key === oldName ? newName : key,
175
- { id, val }
176
- ]));
207
+ .filter(([key]) => key !== newName)
208
+ .map(([key, entry]) => [key === oldName ? newName : key, entry]));
177
209
  setValueAndDispatch(newValue);
178
210
  };
179
211
  const handleChangeProp = (changedKey) => (newValue) => {
@@ -183,14 +215,21 @@ const RecordEditor = ({ defaultValue = {}, onChange, path }) => {
183
215
  }]));
184
216
  setValueAndDispatch(newVal);
185
217
  };
186
- return _jsxs("ul", { className: c('record'), children: [Array.from(value).map(([key, { id, val }]) => _jsxs("li", { "data-key": key, "data-path": [...(path ?? []), key].join('.'), children: [_jsx("button", { className: c('delete-prop'), onClick: () => handleDeleteProp(key), children: "x" }), _jsx("span", { className: c('prop-name'), children: _jsx(StringEditor, { type: 'input', defaultValue: key, onChange: handleRenameProp(key) }) }), _jsx(ValueEditor, { defaultValue: val, onChange: handleChangeProp(key), path: [...(path ?? []), key] })] }, id)), _jsx("button", { className: c('create-prop'), onClick: handleCreateProp, children: "+" })] });
218
+ return _jsxs("ul", { className: mergeClassNames(c('record'), className), children: [Array.from(value).map(([key, { id, val }]) => _jsxs("li", { className: c('record-item'), "data-key": key, "data-path": [...(path ?? []), key].join('.'), children: [_jsx("button", { className: c('delete-prop'), onClick: () => handleDeleteProp(key), children: "x" }), _jsx("span", { className: c('prop-name'), children: _jsx(StringEditor, { type: 'input', defaultValue: key, onChange: handleRenameProp(key) }, `${id}-${refusedRenames}`) }), _jsx(ValueEditor, { defaultValue: val, onChange: handleChangeProp(key), preventSameNameKeys: preventSameNameKeys, path: [...(path ?? []), key] })] }, id)), _jsx("button", { className: c('create-prop'), onClick: handleCreateProp, children: "+" })] });
187
219
  };
188
- /* * * * * * * * * * * * * * * * *
220
+ /**
221
+ * The editor for an array: one removable, movable item per index, each holding a
222
+ * nested {@link ValueEditor}.
189
223
  *
190
- * Array
224
+ * @param props - Component properties.
225
+ * @see {@link ArrayEditorProps}
226
+ * @returns An ordered list of items, followed by the button that appends one.
191
227
  *
192
- * * * * * * * * * * * * * * * * */
193
- const ArrayEditor = ({ defaultValue = [], onChange, path }) => {
228
+ * @remarks
229
+ * Items are held in a `Map` keyed by a stable id used as the React key, so
230
+ * reordering moves the editors rather than remounting them.
231
+ */
232
+ export const ArrayEditor = ({ defaultValue = [], onChange, path, preventSameNameKeys, className }) => {
194
233
  const [value, setValue] = useState(new Map(defaultValue.map(val => [randomHash(8), val])));
195
234
  const setValueAndDispatch = (newValue) => {
196
235
  setValue(newValue);
@@ -209,17 +248,34 @@ const ArrayEditor = ({ defaultValue = [], onChange, path }) => {
209
248
  [Array.from(value)[pos]?.[0] ?? randomHash(8), newValue],
210
249
  ...Array.from(value).slice(pos + 1)
211
250
  ]));
212
- const handleLiftProp = (pos) => setValueAndDispatch(new Map([
213
- ...Array.from(value).slice(0, pos - 1),
214
- Array.from(value)[pos],
215
- Array.from(value)[pos - 1],
216
- ...Array.from(value).slice(pos + 1)
217
- ].filter(e => e !== undefined)));
218
- const handleDropProp = (pos) => setValueAndDispatch(new Map([
219
- ...Array.from(value).slice(0, pos),
220
- Array.from(value)[pos + 1],
221
- Array.from(value)[pos],
222
- ...Array.from(value).slice(pos + 2)
223
- ].filter(e => e !== undefined)));
224
- return _jsxs("ol", { children: [Array.from(value).map(([id, val], pos) => _jsxs("li", { className: c('array'), "data-path": [...(path ?? []), pos].join('.'), children: [_jsx("button", { className: c('delete-prop'), onClick: () => handleDeleteProp(pos), children: "x" }), _jsx("button", { className: c('lift-prop'), disabled: pos === 0, onClick: () => handleLiftProp(pos), children: "\u2191" }), _jsx("button", { className: c('drop-prop'), disabled: pos === value.size - 1, onClick: () => handleDropProp(pos), children: "\u2193" }), _jsx(ValueEditor, { defaultValue: val, onChange: handleChangeProp(pos), path: [...(path ?? []), pos] })] }, id)), _jsx("button", { className: c('create-prop'), onClick: handleCreateProp, children: "+" })] });
251
+ // Swapping with the entry before, so position 0 has nothing to swap with. The
252
+ // guard is what keeps `pos - 1` from reaching back into the array as `-1`.
253
+ const handleLiftProp = (pos) => {
254
+ if (pos <= 0)
255
+ return;
256
+ const entries = Array.from(value);
257
+ setValueAndDispatch(new Map([
258
+ ...entries.slice(0, pos - 1),
259
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
260
+ entries[pos],
261
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
262
+ entries[pos - 1],
263
+ ...entries.slice(pos + 1)
264
+ ]));
265
+ };
266
+ // Same, mirrored: the last entry has nothing to swap with.
267
+ const handleDropProp = (pos) => {
268
+ const entries = Array.from(value);
269
+ if (pos < 0 || pos >= entries.length - 1)
270
+ return;
271
+ setValueAndDispatch(new Map([
272
+ ...entries.slice(0, pos),
273
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
274
+ entries[pos + 1],
275
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
276
+ entries[pos],
277
+ ...entries.slice(pos + 2)
278
+ ]));
279
+ };
280
+ return _jsxs("ol", { className: mergeClassNames(c('array'), className), children: [Array.from(value).map(([id, val], pos) => _jsxs("li", { className: c('array-item'), "data-path": [...(path ?? []), pos].join('.'), children: [_jsx("button", { className: c('delete-prop'), onClick: () => handleDeleteProp(pos), children: "x" }), _jsx("button", { className: c('lift-prop'), disabled: pos === 0, onClick: () => handleLiftProp(pos), children: "\u2191" }), _jsx("button", { className: c('drop-prop'), disabled: pos === value.size - 1, onClick: () => handleDropProp(pos), children: "\u2193" }), _jsx(ValueEditor, { defaultValue: val, onChange: handleChangeProp(pos), preventSameNameKeys: preventSameNameKeys, path: [...(path ?? []), pos] })] }, id)), _jsx("button", { className: c('create-prop'), onClick: handleCreateProp, children: "+" })] });
225
281
  };
@@ -0,0 +1,11 @@
1
+ /** A JSON value that contains no other value. */
2
+ export type JsonPrimitive = string | number | boolean | null;
3
+ /** Any value `JSON.parse` can produce, and the type the editor reads and emits. */
4
+ export type JsonValue = JsonPrimitive | JsonValue[] | {
5
+ [k: string]: JsonValue;
6
+ };
7
+ /**
8
+ * The editor's name for a value's kind. It is what the type selector offers, and
9
+ * what modifies the `value` class name on each node.
10
+ */
11
+ export type ValueType = 'string' | 'number' | 'boolean' | 'null' | 'array' | 'record';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import type { JsonValue, ValueType } from './types.js';
2
+ /**
3
+ * Shared by every editor in the tree — they all emit class names under the same
4
+ * public prefix, so a single instance is what keeps them consistent.
5
+ */
6
+ export declare const c: import("../../agnostic/css/clss/index.js").ClssMaker;
7
+ /** Which {@link ValueType} a value belongs to. */
8
+ export declare function getValueType(value: JsonValue): ValueType;
@@ -0,0 +1,23 @@
1
+ import { clss } from '../../agnostic/css/clss/index.js';
2
+ import { isNonNullObject } from '../../agnostic/objects/is-object/index.js';
3
+ import { jsonEditor as publicClassName } from '../public-classnames.js';
4
+ import cssModule from './styles.module.css';
5
+ /**
6
+ * Shared by every editor in the tree — they all emit class names under the same
7
+ * public prefix, so a single instance is what keeps them consistent.
8
+ */
9
+ export const c = clss(publicClassName, { cssModule });
10
+ /** Which {@link ValueType} a value belongs to. */
11
+ export function getValueType(value) {
12
+ if (value === null)
13
+ return 'null';
14
+ if (Array.isArray(value))
15
+ return 'array';
16
+ if (isNonNullObject(value))
17
+ return 'record';
18
+ if (typeof value === 'string')
19
+ return 'string';
20
+ if (typeof value === 'number')
21
+ return 'number';
22
+ return 'boolean';
23
+ }
@@ -1,7 +1,7 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { WithClassName } from '../utils/types.js';
3
3
  /**
4
- * Props for the {@link ListLoaderControlled} component.
4
+ * Props for the {@link ControlledListLoader} component.
5
5
  *
6
6
  * @template T - The item type carried by each page.
7
7
  *
@@ -21,10 +21,10 @@ import type { WithClassName } from '../utils/types.js';
21
21
  * back to a positional key.
22
22
  * @property loadingPages - Page positions currently being fetched. Exposed on the
23
23
  * root element as `data-loading-pages`.
24
- * @property onLoadPageClick - Called with the page position a load button targets,
24
+ * @property onLoadPageClicked - Called with the page position a load button targets,
25
25
  * either on click or on viewport entry when auto-loading is enabled.
26
26
  * @property autoLoadPrevWhenVisible - Wraps the leading load button in an
27
- * {@link IntersectionObserverComponent} and fires `onLoadPageClick` when it enters the viewport.
27
+ * {@link IntersectionObserverComponent} and fires `onLoadPageClicked` when it enters the viewport.
28
28
  * @property autoLoadNextWhenVisible - Same, for the trailing load button.
29
29
  * @property className - Additional class name(s) applied to the root element.
30
30
  */
@@ -37,7 +37,7 @@ export type Props<T> = WithClassName<{
37
37
  display: (item: T) => ReactNode;
38
38
  getIdentifier?: (item: T) => string | undefined;
39
39
  loadingPages?: number[];
40
- onLoadPageClick?: (pagePos: number) => void;
40
+ onLoadPageClicked?: (pagePos: number) => void;
41
41
  autoLoadPrevWhenVisible?: boolean;
42
42
  autoLoadNextWhenVisible?: boolean;
43
43
  }>;
@@ -75,4 +75,4 @@ export type Props<T> = WithClassName<{
75
75
  * Deduplication runs after `filter`, so an item hidden by `filter` never evicts
76
76
  * an earlier duplicate.
77
77
  */
78
- export declare const ListLoaderControlled: <T>({ className, pages, firstPagePos, lastPagePos, itemsPages, filter, display, getIdentifier, loadingPages, onLoadPageClick, autoLoadPrevWhenVisible, autoLoadNextWhenVisible }: Props<T>) => ReactNode;
78
+ export declare const ControlledListLoader: <T>({ className, pages, firstPagePos, lastPagePos, itemsPages, filter, display, getIdentifier, loadingPages, onLoadPageClicked, autoLoadPrevWhenVisible, autoLoadNextWhenVisible }: Props<T>) => ReactNode;
@@ -39,7 +39,7 @@ import cssModule from './styles.module.css';
39
39
  * Deduplication runs after `filter`, so an item hidden by `filter` never evicts
40
40
  * an earlier duplicate.
41
41
  */
42
- export const ListLoaderControlled = ({ className, pages, firstPagePos, lastPagePos, itemsPages, filter, display, getIdentifier, loadingPages, onLoadPageClick, autoLoadPrevWhenVisible, autoLoadNextWhenVisible }) => {
42
+ export const ControlledListLoader = ({ className, pages, firstPagePos, lastPagePos, itemsPages, filter, display, getIdentifier, loadingPages, onLoadPageClicked, autoLoadPrevWhenVisible, autoLoadNextWhenVisible }) => {
43
43
  const inBoundsPages = pages.filter(pagePos => pagePos >= firstPagePos && pagePos <= lastPagePos);
44
44
  const filteredItems = Array.from(itemsPages)
45
45
  .filter(([pagePos]) => inBoundsPages.includes(pagePos))
@@ -78,9 +78,9 @@ export const ListLoaderControlled = ({ className, pages, firstPagePos, lastPageP
78
78
  .filter(pagePos => inBoundsPages.includes(pagePos))
79
79
  .sort((a, b) => a - b);
80
80
  const c = clss(publicClassName, { cssModule });
81
- const rootClss = mergeClassNames(c(null), className);
81
+ const rootClss = mergeClassNames(c(), className);
82
82
  const renderLoadButton = (pagePos, kind) => {
83
- const button = _jsx("button", { className: c('load', kind), "data-page": pagePos, onClick: () => onLoadPageClick?.(pagePos) }, `load/${pagePos}`);
83
+ const button = _jsx("button", { className: c('load', kind), "data-page": pagePos, onClick: () => onLoadPageClicked?.(pagePos) }, `load/${pagePos}`);
84
84
  const autoLoads = (kind === 'prev' && autoLoadPrevWhenVisible === true)
85
85
  || (kind === 'next' && autoLoadNextWhenVisible === true);
86
86
  if (!autoLoads)
@@ -88,7 +88,7 @@ export const ListLoaderControlled = ({ className, pages, firstPagePos, lastPageP
88
88
  return _jsx(IntersectionObserverComponent, { className: c('load-observer', kind), onIntersected: ({ ioEntry }) => {
89
89
  if (ioEntry?.isIntersecting !== true)
90
90
  return;
91
- onLoadPageClick?.(pagePos);
91
+ onLoadPageClicked?.(pagePos);
92
92
  }, children: button }, `load/${pagePos}`);
93
93
  };
94
94
  return _jsx("div", { className: rootClss, "data-loading-pages": sortedLoadingPages.length === 0 ? undefined : sortedLoadingPages.join(','), children: slots.map(({ pagePos, kind }) => kind === 'items'