@aglyn/shared-ui-jsx-forms 1.0.0-beta.143

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 (150) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +7 -0
  3. package/package.json +52 -0
  4. package/src/index.d.ts +17 -0
  5. package/src/index.js +18 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/color-picker-tokens.d.ts +135 -0
  8. package/src/lib/components/color-picker-tokens.js +431 -0
  9. package/src/lib/components/color-picker-tokens.js.map +1 -0
  10. package/src/lib/components/color-picker.component.d.ts +64 -0
  11. package/src/lib/components/color-picker.component.js +322 -0
  12. package/src/lib/components/color-picker.component.js.map +1 -0
  13. package/src/lib/components/create-artifact-drawer.component.d.ts +100 -0
  14. package/src/lib/components/create-artifact-drawer.component.js +184 -0
  15. package/src/lib/components/create-artifact-drawer.component.js.map +1 -0
  16. package/src/lib/components/grid-form-template.component.d.ts +21 -0
  17. package/src/lib/components/grid-form-template.component.js +81 -0
  18. package/src/lib/components/grid-form-template.component.js.map +1 -0
  19. package/src/lib/components/icon-select.component.d.ts +52 -0
  20. package/src/lib/components/icon-select.component.js +453 -0
  21. package/src/lib/components/icon-select.component.js.map +1 -0
  22. package/src/lib/components/select.component.d.ts +58 -0
  23. package/src/lib/components/select.component.js +107 -0
  24. package/src/lib/components/select.component.js.map +1 -0
  25. package/src/lib/components/switch.component.d.ts +38 -0
  26. package/src/lib/components/switch.component.js +76 -0
  27. package/src/lib/components/switch.component.js.map +1 -0
  28. package/src/lib/components/text-field.component.d.ts +45 -0
  29. package/src/lib/components/text-field.component.js +77 -0
  30. package/src/lib/components/text-field.component.js.map +1 -0
  31. package/src/lib/components/textarea.component.d.ts +45 -0
  32. package/src/lib/components/textarea.component.js +61 -0
  33. package/src/lib/components/textarea.component.js.map +1 -0
  34. package/src/lib/components/toggle-button.component.d.ts +27 -0
  35. package/src/lib/components/toggle-button.component.js +116 -0
  36. package/src/lib/components/toggle-button.component.js.map +1 -0
  37. package/src/lib/constants/component-mappers.d.ts +42 -0
  38. package/src/lib/constants/component-mappers.js +70 -0
  39. package/src/lib/constants/component-mappers.js.map +1 -0
  40. package/src/lib/constants/dynamic-fields.d.ts +45 -0
  41. package/src/lib/constants/dynamic-fields.js +65 -0
  42. package/src/lib/constants/dynamic-fields.js.map +1 -0
  43. package/src/lib/constants/field-configurations.d.ts +53 -0
  44. package/src/lib/constants/field-configurations.js +121 -0
  45. package/src/lib/constants/field-configurations.js.map +1 -0
  46. package/src/lib/constants/flags.d.ts +72 -0
  47. package/src/lib/constants/flags.js +69 -0
  48. package/src/lib/constants/flags.js.map +1 -0
  49. package/src/lib/hocs/with-grid-item.d.ts +25 -0
  50. package/src/lib/hocs/with-grid-item.js +48 -0
  51. package/src/lib/hocs/with-grid-item.js.map +1 -0
  52. package/src/lib/jsx-forms.d.ts +41 -0
  53. package/src/lib/jsx-forms.js +42 -0
  54. package/src/lib/jsx-forms.js.map +1 -0
  55. package/src/lib/mapper/breakpoint-span.d.ts +73 -0
  56. package/src/lib/mapper/breakpoint-span.js +346 -0
  57. package/src/lib/mapper/breakpoint-span.js.map +1 -0
  58. package/src/lib/mapper/checkbox.d.ts +43 -0
  59. package/src/lib/mapper/checkbox.js +111 -0
  60. package/src/lib/mapper/checkbox.js.map +1 -0
  61. package/src/lib/mapper/css-border.d.ts +113 -0
  62. package/src/lib/mapper/css-border.js +410 -0
  63. package/src/lib/mapper/css-border.js.map +1 -0
  64. package/src/lib/mapper/css-dimension.d.ts +89 -0
  65. package/src/lib/mapper/css-dimension.js +327 -0
  66. package/src/lib/mapper/css-dimension.js.map +1 -0
  67. package/src/lib/mapper/css-gradient.d.ts +110 -0
  68. package/src/lib/mapper/css-gradient.js +720 -0
  69. package/src/lib/mapper/css-gradient.js.map +1 -0
  70. package/src/lib/mapper/data-table.d.ts +23 -0
  71. package/src/lib/mapper/data-table.js +296 -0
  72. package/src/lib/mapper/data-table.js.map +1 -0
  73. package/src/lib/mapper/date-picker.d.ts +26 -0
  74. package/src/lib/mapper/date-picker.js +69 -0
  75. package/src/lib/mapper/date-picker.js.map +1 -0
  76. package/src/lib/mapper/dual-list-select.d.ts +115 -0
  77. package/src/lib/mapper/dual-list-select.js +353 -0
  78. package/src/lib/mapper/dual-list-select.js.map +1 -0
  79. package/src/lib/mapper/field-array.d.ts +71 -0
  80. package/src/lib/mapper/field-array.js +326 -0
  81. package/src/lib/mapper/field-array.js.map +1 -0
  82. package/src/lib/mapper/form-field-grid.d.ts +155 -0
  83. package/src/lib/mapper/form-field-grid.js +194 -0
  84. package/src/lib/mapper/form-field-grid.js.map +1 -0
  85. package/src/lib/mapper/index.d.ts +50 -0
  86. package/src/lib/mapper/index.js +49 -0
  87. package/src/lib/mapper/index.js.map +1 -0
  88. package/src/lib/mapper/multiple-choice-list.d.ts +32 -0
  89. package/src/lib/mapper/multiple-choice-list.js +105 -0
  90. package/src/lib/mapper/multiple-choice-list.js.map +1 -0
  91. package/src/lib/mapper/plain-text.d.ts +31 -0
  92. package/src/lib/mapper/plain-text.js +48 -0
  93. package/src/lib/mapper/plain-text.js.map +1 -0
  94. package/src/lib/mapper/preset-choice.d.ts +106 -0
  95. package/src/lib/mapper/preset-choice.js +267 -0
  96. package/src/lib/mapper/preset-choice.js.map +1 -0
  97. package/src/lib/mapper/radio.d.ts +40 -0
  98. package/src/lib/mapper/radio.js +122 -0
  99. package/src/lib/mapper/radio.js.map +1 -0
  100. package/src/lib/mapper/select.d.ts +60 -0
  101. package/src/lib/mapper/select.js +203 -0
  102. package/src/lib/mapper/select.js.map +1 -0
  103. package/src/lib/mapper/slider.d.ts +45 -0
  104. package/src/lib/mapper/slider.js +119 -0
  105. package/src/lib/mapper/slider.js.map +1 -0
  106. package/src/lib/mapper/stored-field-value.d.ts +23 -0
  107. package/src/lib/mapper/stored-field-value.js +29 -0
  108. package/src/lib/mapper/stored-field-value.js.map +1 -0
  109. package/src/lib/mapper/sub-form.d.ts +39 -0
  110. package/src/lib/mapper/sub-form.js +108 -0
  111. package/src/lib/mapper/sub-form.js.map +1 -0
  112. package/src/lib/mapper/switch.d.ts +47 -0
  113. package/src/lib/mapper/switch.js +93 -0
  114. package/src/lib/mapper/switch.js.map +1 -0
  115. package/src/lib/mapper/tabs.d.ts +34 -0
  116. package/src/lib/mapper/tabs.js +64 -0
  117. package/src/lib/mapper/tabs.js.map +1 -0
  118. package/src/lib/mapper/text-field.d.ts +33 -0
  119. package/src/lib/mapper/text-field.js +88 -0
  120. package/src/lib/mapper/text-field.js.map +1 -0
  121. package/src/lib/mapper/textarea.d.ts +31 -0
  122. package/src/lib/mapper/textarea.js +68 -0
  123. package/src/lib/mapper/textarea.js.map +1 -0
  124. package/src/lib/mapper/theme-scale.d.ts +79 -0
  125. package/src/lib/mapper/theme-scale.js +162 -0
  126. package/src/lib/mapper/theme-scale.js.map +1 -0
  127. package/src/lib/mapper/time-picker.d.ts +26 -0
  128. package/src/lib/mapper/time-picker.js +69 -0
  129. package/src/lib/mapper/time-picker.js.map +1 -0
  130. package/src/lib/mapper/types.d.ts +43 -0
  131. package/src/lib/mapper/types.js +22 -0
  132. package/src/lib/mapper/types.js.map +1 -0
  133. package/src/lib/mapper/validation-error.d.ts +24 -0
  134. package/src/lib/mapper/validation-error.js +24 -0
  135. package/src/lib/mapper/validation-error.js.map +1 -0
  136. package/src/lib/mapper/wizard.d.ts +21 -0
  137. package/src/lib/mapper/wizard.js +203 -0
  138. package/src/lib/mapper/wizard.js.map +1 -0
  139. package/src/lib/types.d.ts +19 -0
  140. package/src/lib/types.js +18 -0
  141. package/src/lib/types.js.map +1 -0
  142. package/src/lib/utils/option-is-equal-to-value.d.ts +20 -0
  143. package/src/lib/utils/option-is-equal-to-value.js +21 -0
  144. package/src/lib/utils/option-is-equal-to-value.js.map +1 -0
  145. package/src/lib/utils/validation-message.d.ts +22 -0
  146. package/src/lib/utils/validation-message.js +24 -0
  147. package/src/lib/utils/validation-message.js.map +1 -0
  148. package/src/lib/vendor/data-driven-forms.d.ts +33 -0
  149. package/src/lib/vendor/data-driven-forms.js +29 -0
  150. package/src/lib/vendor/data-driven-forms.js.map +1 -0
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { CssUnit } from '@aglyn/shared-data-enums';
18
+ import { type FormFieldGridProps } from './form-field-grid';
19
+ import type { ThemeScaleOption } from './theme-scale';
20
+ import type { BaseFieldProps } from './types';
21
+ /**
22
+ * CssDimension (AGL-1219): a number box plus a unit picker for anything
23
+ * holding a CSS length — an image's Width, a drawer's Width, a video
24
+ * block's Height, and every length in the styles panel's field groups.
25
+ * Authors used to type the whole string (`920px`) into a free-text field
26
+ * and were expected to remember the unit, while the box stylers right
27
+ * above them had a number+unit pair all along.
28
+ *
29
+ * The PERSISTED value is unchanged: still one CSS string (`"920px"`,
30
+ * `"100%"`, `"auto"`, `""`). This is purely an input affordance, so nothing
31
+ * downstream — renderers, published documents, existing nodes — has to know
32
+ * about it. The unit list is {@link CSS_UNITS} (the box styler's list), and
33
+ * parse/serialize is the shared pair in `@aglyn/shared-data-enums` so the
34
+ * two surfaces cannot drift.
35
+ *
36
+ * Values that are not `<number><unit>` — `calc(100% - 2rem)`, `min-content`,
37
+ * a `{{token}}` binding — must NOT be clobbered by an editor that cannot
38
+ * model them, so the field falls back to a plain text box holding the raw
39
+ * string. It flips back to number+unit the moment the text becomes a plain
40
+ * dimension again, which keeps the mode derived from the value rather than
41
+ * remembered in a flag that can go stale.
42
+ */
43
+ /** Draft state: what the two controls show, before serialization. */
44
+ interface DimensionDraft {
45
+ /** Numeric text (or the whole raw string in custom mode). */
46
+ text: string;
47
+ /** Selected unit, `''` when none is chosen. */
48
+ unit: CssUnit | '';
49
+ /** The value is not `<number><unit>` — the text box holds it verbatim. */
50
+ custom: boolean;
51
+ }
52
+ /**
53
+ * How a value stored as a bare NUMBER is to be read (AGL-1219). A node prop
54
+ * is plain CSS, so a number is pixels — the default.
55
+ *
56
+ * `'mui-sizing'` is for the styles panel's `sx` sizing keys, where a number
57
+ * is NOT pixels: MUI's `sizingTransform` renders any number in (0, 1] as a
58
+ * fraction of the parent, so `width: 0.5` is 50%. Read as pixels it would
59
+ * show "0.5" with no unit and the first nudge would turn a half-width
60
+ * element into a 0.6px one.
61
+ */
62
+ export type DimensionNumberAs = 'px' | 'mui-sizing';
63
+ /**
64
+ * The CSS string a stored value stands for — the only place a bare number
65
+ * is given its meaning. Mirrors MUI's `sizingTransform` for `'mui-sizing'`.
66
+ */
67
+ export declare const dimensionValueToCss: (value: unknown, numberAs?: DimensionNumberAs) => string;
68
+ export declare const seedDimensionDraft: (value: unknown, numberAs?: DimensionNumberAs) => DimensionDraft;
69
+ export declare const serializeDimensionDraft: (draft: DimensionDraft) => string;
70
+ export interface CssDimensionProps extends BaseFieldProps {
71
+ placeholder?: string;
72
+ /** Units offered; defaults to the full shared list. */
73
+ units?: CssUnit[];
74
+ /** How a bare number is read. See {@link DimensionNumberAs}. */
75
+ numberAs?: DimensionNumberAs;
76
+ /**
77
+ * Theme scale offered alongside the raw length (AGL-2486). Each option's
78
+ * `value` is a token path MUI's sx system resolves itself — `h4.fontSize`
79
+ * against `theme.typography` — so picking one keeps the element following
80
+ * the theme instead of freezing the number it had when it was styled. An
81
+ * empty list renders no scale menu at all.
82
+ */
83
+ scaleOptions?: ThemeScaleOption[];
84
+ /** Offer the reset-to-unset affordance (AGL-2486). */
85
+ clearable?: boolean;
86
+ FormFieldGridProps?: FormFieldGridProps;
87
+ }
88
+ export declare const CssDimensionField: (props: CssDimensionProps) => import("react").JSX.Element;
89
+ export default CssDimensionField;
@@ -0,0 +1,327 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
3
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
4
+ /**
5
+ * @license
6
+ * Copyright 2026 Aglyn LLC
7
+ *
8
+ * Licensed under the Apache License, Version 2.0 (the "License");
9
+ * you may not use this file except in compliance with the License.
10
+ * You may obtain a copy of the License at
11
+ *
12
+ * http://www.apache.org/licenses/LICENSE-2.0
13
+ *
14
+ * Unless required by applicable law or agreed to in writing, software
15
+ * distributed under the License is distributed on an "AS IS" BASIS,
16
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ * See the License for the specific language governing permissions and
18
+ * limitations under the License.
19
+ */ import { buildCssDimension, CSS_UNITS, CssUnit, isGlobalUnit, parseCssDimension } from "@aglyn/shared-data-enums";
20
+ import { InputAdornment, MenuItem, Select, TextField as MuiTextField } from "@mui/material";
21
+ import { useCallback, useEffect, useRef, useState } from "react";
22
+ import { useFieldApi } from "../vendor/data-driven-forms.js";
23
+ import FormFieldGrid, { buildFieldClear } from "./form-field-grid.js";
24
+ import { validationError } from "./validation-error.js";
25
+ /**
26
+ * The CSS string a stored value stands for — the only place a bare number
27
+ * is given its meaning. Mirrors MUI's `sizingTransform` for `'mui-sizing'`.
28
+ */ export const dimensionValueToCss = (value, numberAs = 'px')=>{
29
+ if (typeof value === 'string') return value;
30
+ if (typeof value !== 'number' || !Number.isFinite(value)) return '';
31
+ if (numberAs === 'mui-sizing' && value <= 1 && value !== 0) {
32
+ // `0.3 * 100` is 30.000000000000004 in binary floating point — MUI
33
+ // emits that verbatim; showing it in a number box would be absurd.
34
+ return `${Number((value * 100).toFixed(4))}%`;
35
+ }
36
+ return `${value}px`;
37
+ };
38
+ export const seedDimensionDraft = (value, numberAs = 'px')=>{
39
+ var _parsed_unit;
40
+ const parsed = parseCssDimension(dimensionValueToCss(value, numberAs));
41
+ if (parsed.raw !== undefined) {
42
+ return {
43
+ text: parsed.raw,
44
+ unit: '',
45
+ custom: true
46
+ };
47
+ }
48
+ return {
49
+ text: parsed.value === undefined ? '' : `${parsed.value}`,
50
+ unit: (_parsed_unit = parsed.unit) != null ? _parsed_unit : '',
51
+ custom: false
52
+ };
53
+ };
54
+ export const serializeDimensionDraft = (draft)=>{
55
+ if (draft.custom) return draft.text;
56
+ const trimmed = draft.text.trim();
57
+ return buildCssDimension({
58
+ value: trimmed === '' ? undefined : Number(trimmed),
59
+ unit: draft.unit || undefined
60
+ });
61
+ };
62
+ export const CssDimensionField = (props)=>{
63
+ const _useFieldApi = useFieldApi(props), { input, isReadOnly, isDisabled, isRequired, label, placeholder, helperText, description, validateOnMount, meta, help, units = CSS_UNITS, numberAs = 'px', scaleOptions = [], clearable, FormFieldGridProps = {}, // Free-text leftovers from the attribute schema that must never reach
64
+ // the DOM (the attributes it was authored with as a TEXT_FIELD).
65
+ inputProps: _inputProps, InputProps: _InputProps, multiline: _multiline } = _useFieldApi, rest = _object_without_properties_loose(_useFieldApi, [
66
+ "input",
67
+ "isReadOnly",
68
+ "isDisabled",
69
+ "isRequired",
70
+ "label",
71
+ "placeholder",
72
+ "helperText",
73
+ "description",
74
+ "validateOnMount",
75
+ "meta",
76
+ "help",
77
+ "units",
78
+ "numberAs",
79
+ "scaleOptions",
80
+ "clearable",
81
+ "FormFieldGridProps",
82
+ "inputProps",
83
+ "InputProps",
84
+ "multiline"
85
+ ]);
86
+ const invalid = validationError(meta, validateOnMount);
87
+ // A value authored as a bare number (`width: 320`, or `width: 0.5` in an
88
+ // sx) is still a value the author has to see — reading only strings would
89
+ // blank the field and then overwrite the number on the first edit. The
90
+ // number is resolved to the CSS it stands for HERE, not in the form value,
91
+ // so an untouched field still emits nothing and the stored number survives.
92
+ const value = dimensionValueToCss(input.value, numberAs);
93
+ const [draft, setDraft] = useState(()=>seedDimensionDraft(value));
94
+ // Re-seed only when the value changed OUTSIDE this field (a different
95
+ // node selected, an undo). Re-seeding from our own emits would round
96
+ // half-typed decimals ("1." -> 1 -> "1") out from under the caret.
97
+ const emittedRef = useRef(value);
98
+ useEffect(()=>{
99
+ if (value === emittedRef.current) return;
100
+ emittedRef.current = value;
101
+ setDraft(seedDimensionDraft(value));
102
+ }, [
103
+ value
104
+ ]);
105
+ const commit = useCallback((next)=>{
106
+ const serialized = serializeDimensionDraft(next);
107
+ emittedRef.current = serialized;
108
+ // A custom value the author edited back into a plain dimension gets
109
+ // the structured controls back immediately.
110
+ const reparsed = parseCssDimension(serialized);
111
+ setDraft(next.custom && serialized.trim() !== '' && reparsed.raw === undefined ? seedDimensionDraft(serialized) : next);
112
+ input.onChange(serialized);
113
+ }, [
114
+ input
115
+ ]);
116
+ const keywordUnit = !draft.custom && !!draft.unit && isGlobalUnit(draft.unit);
117
+ // Clearing a length has to drop the UNIT too (AGL-2486). Emptying the
118
+ // number box alone leaves `px` selected, and the next keystroke silently
119
+ // re-adopts it — which is why "delete the number" was never a way back to
120
+ // unset.
121
+ const clear = buildFieldClear({
122
+ clearable,
123
+ label,
124
+ hasValue: value !== '',
125
+ locked: Boolean(isDisabled || isReadOnly),
126
+ onClear: ()=>commit({
127
+ text: '',
128
+ unit: '',
129
+ custom: false
130
+ })
131
+ });
132
+ const handleTextChange = useCallback((event)=>{
133
+ const text = event.target.value;
134
+ // Typing a number into a field with no unit yet means pixels — a
135
+ // bare `320` is not a valid CSS length and silently did nothing
136
+ // when these were free-text fields.
137
+ const unit = !draft.custom && !draft.unit && text.trim() !== '' ? CssUnit.PIXELS : draft.unit;
138
+ commit(_extends({}, draft, {
139
+ text,
140
+ unit
141
+ }));
142
+ }, [
143
+ commit,
144
+ draft
145
+ ]);
146
+ const handleUnitChange = useCallback((event)=>{
147
+ const unit = event.target.value || '';
148
+ // Keyword units (`auto`) ARE the whole value, so the quantity goes
149
+ // away with them; it comes back when a real unit is picked again.
150
+ commit(_extends({}, draft, {
151
+ unit,
152
+ text: unit && isGlobalUnit(unit) ? '' : draft.text
153
+ }));
154
+ }, [
155
+ commit,
156
+ draft
157
+ ]);
158
+ // Theme scale alongside the raw length (AGL-2486, item 12). A token like
159
+ // `h4.fontSize` is not a `<number><unit>`, so the field already holds it
160
+ // fine — in custom mode, as raw text — and all that was missing was a way
161
+ // to PICK one. Rendered as its own menu rather than folded into the unit
162
+ // list because it does not choose a unit, it replaces the whole value:
163
+ // MUI's sx resolves `fontSize: 'h4.fontSize'` against `theme.typography`,
164
+ // which is what keeps the element following the type scale.
165
+ const activeScaleOption = scaleOptions.find((option)=>option.value === value);
166
+ const scaleSelect = scaleOptions.length ? /*#__PURE__*/ _jsx(InputAdornment, {
167
+ position: "end",
168
+ sx: {
169
+ ml: 0,
170
+ mr: 0.5
171
+ },
172
+ children: /*#__PURE__*/ _jsxs(Select, {
173
+ value: activeScaleOption ? activeScaleOption.value : '',
174
+ onChange: (event)=>{
175
+ var _event_target_value;
176
+ const picked = `${(_event_target_value = event.target.value) != null ? _event_target_value : ''}`;
177
+ // Empty means "back to a raw length"; the number box takes over
178
+ // again and the author is not stranded on a token they undid.
179
+ commit(picked ? {
180
+ text: picked,
181
+ unit: '',
182
+ custom: true
183
+ } : {
184
+ text: '',
185
+ unit: '',
186
+ custom: false
187
+ });
188
+ },
189
+ disabled: isDisabled || isReadOnly,
190
+ variant: "standard",
191
+ disableUnderline: true,
192
+ displayEmpty: true,
193
+ renderValue: ()=>activeScaleOption ? activeScaleOption.label : 'theme',
194
+ inputProps: {
195
+ 'aria-label': 'Theme scale'
196
+ },
197
+ sx: {
198
+ '& .MuiSelect-select': {
199
+ pr: '20px !important',
200
+ py: 0,
201
+ // The token label is the part that may be clipped when the
202
+ // row is tight — never the value (AGL-2486).
203
+ textOverflow: 'ellipsis'
204
+ },
205
+ fontSize: '0.8125rem',
206
+ color: 'text.secondary',
207
+ minWidth: 0,
208
+ maxWidth: 92
209
+ },
210
+ children: [
211
+ /*#__PURE__*/ _jsx(MenuItem, {
212
+ value: "",
213
+ children: /*#__PURE__*/ _jsx("em", {
214
+ children: 'raw value'
215
+ })
216
+ }),
217
+ scaleOptions.map((option)=>/*#__PURE__*/ _jsx(MenuItem, {
218
+ value: option.value,
219
+ children: option.hint ? `${option.label} — ${option.hint}` : option.label
220
+ }, option.value))
221
+ ]
222
+ })
223
+ }) : null;
224
+ const unitSelect = /*#__PURE__*/ _jsx(InputAdornment, {
225
+ position: "end",
226
+ sx: {
227
+ ml: 0
228
+ },
229
+ children: /*#__PURE__*/ _jsxs(Select, {
230
+ value: draft.custom ? '' : draft.unit,
231
+ onChange: handleUnitChange,
232
+ disabled: isDisabled || isReadOnly || draft.custom,
233
+ variant: "standard",
234
+ disableUnderline: true,
235
+ // Without displayEmpty an unset unit renders nothing at all, which
236
+ // reads as a broken control rather than "no unit yet".
237
+ displayEmpty: true,
238
+ renderValue: (selected)=>draft.custom ? 'custom' : selected || '—',
239
+ inputProps: {
240
+ 'aria-label': 'Unit'
241
+ },
242
+ sx: {
243
+ '& .MuiSelect-select': {
244
+ pr: '20px !important',
245
+ py: 0
246
+ },
247
+ fontSize: '0.8125rem',
248
+ color: 'text.secondary',
249
+ minWidth: 44,
250
+ flexShrink: 0
251
+ },
252
+ children: [
253
+ /*#__PURE__*/ _jsx(MenuItem, {
254
+ value: "",
255
+ children: /*#__PURE__*/ _jsx("em", {
256
+ children: '—'
257
+ })
258
+ }),
259
+ units.map((unit)=>/*#__PURE__*/ _jsx(MenuItem, {
260
+ value: unit,
261
+ children: unit
262
+ }, unit))
263
+ ]
264
+ })
265
+ });
266
+ return /*#__PURE__*/ _jsx(FormFieldGrid, _extends({
267
+ help: help,
268
+ clear: clear
269
+ }, FormFieldGridProps, {
270
+ children: /*#__PURE__*/ _jsx(MuiTextField, _extends({}, rest, {
271
+ name: input.name,
272
+ value: keywordUnit ? '' : draft.text,
273
+ onChange: handleTextChange,
274
+ onBlur: input.onBlur,
275
+ onFocus: input.onFocus,
276
+ fullWidth: true,
277
+ error: !!invalid,
278
+ helperText: invalid || (meta.touched || validateOnMount) && meta.warning || helperText || description,
279
+ disabled: isDisabled,
280
+ label: label,
281
+ placeholder: keywordUnit ? `${draft.unit}` : placeholder,
282
+ required: isRequired,
283
+ // A keyword unit has no quantity to type, and a custom expression
284
+ // is free text — only the plain number+unit case is numeric.
285
+ type: draft.custom || keywordUnit ? 'text' : 'number',
286
+ slotProps: {
287
+ // A keyword value (`auto`) is shown through the PLACEHOLDER,
288
+ // and MUI hides a placeholder behind CSS while the label is
289
+ // un-shrunk — so the field printed its label over an empty box
290
+ // on a node that demonstrably had a value (AGL-2486). Only this
291
+ // case: an ordinary empty length has nothing to reveal, and
292
+ // shrinking it would be a restyle rather than a fix.
293
+ inputLabel: keywordUnit ? {
294
+ shrink: true
295
+ } : undefined,
296
+ input: {
297
+ readOnly: isReadOnly || keywordUnit,
298
+ endAdornment: /*#__PURE__*/ _jsxs(_Fragment, {
299
+ children: [
300
+ scaleSelect,
301
+ unitSelect
302
+ ]
303
+ })
304
+ },
305
+ /**
306
+ * The VALUE is never the control that gets squeezed (AGL-2486).
307
+ *
308
+ * The number box is the only part of this row with no width of
309
+ * its own — the two pickers are `endAdornment` siblings with
310
+ * fixed widths, so the input is whatever is left, and in a
311
+ * narrow docked panel that is a couple of characters: Font
312
+ * Size renders as `2.:`. A floor here makes the PICKERS give way
313
+ * instead, which is the right order — a clipped unit label is
314
+ * a nuisance, a clipped value is unreadable.
315
+ */ htmlInput: {
316
+ inputMode: 'decimal',
317
+ style: {
318
+ minWidth: '4.5ch'
319
+ }
320
+ }
321
+ }
322
+ }))
323
+ }));
324
+ };
325
+ export default CssDimensionField;
326
+
327
+ //# sourceMappingURL=css-dimension.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx-forms/src/lib/mapper/css-dimension.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n buildCssDimension,\n CSS_UNITS,\n CssUnit,\n isGlobalUnit,\n parseCssDimension,\n} from '@aglyn/shared-data-enums'\nimport {\n InputAdornment,\n MenuItem,\n Select,\n TextField as MuiTextField,\n} from '@mui/material'\nimport { useCallback, useEffect, useRef, useState } from 'react'\n\nimport { useFieldApi } from '../vendor/data-driven-forms'\nimport FormFieldGrid, {\n buildFieldClear,\n type FormFieldGridProps,\n} from './form-field-grid'\nimport type { ThemeScaleOption } from './theme-scale'\nimport type { BaseFieldProps } from './types'\nimport { type ExtendedFieldMeta, validationError } from './validation-error'\n\n/**\n * CssDimension (AGL-1219): a number box plus a unit picker for anything\n * holding a CSS length — an image's Width, a drawer's Width, a video\n * block's Height, and every length in the styles panel's field groups.\n * Authors used to type the whole string (`920px`) into a free-text field\n * and were expected to remember the unit, while the box stylers right\n * above them had a number+unit pair all along.\n *\n * The PERSISTED value is unchanged: still one CSS string (`\"920px\"`,\n * `\"100%\"`, `\"auto\"`, `\"\"`). This is purely an input affordance, so nothing\n * downstream — renderers, published documents, existing nodes — has to know\n * about it. The unit list is {@link CSS_UNITS} (the box styler's list), and\n * parse/serialize is the shared pair in `@aglyn/shared-data-enums` so the\n * two surfaces cannot drift.\n *\n * Values that are not `<number><unit>` — `calc(100% - 2rem)`, `min-content`,\n * a `{{token}}` binding — must NOT be clobbered by an editor that cannot\n * model them, so the field falls back to a plain text box holding the raw\n * string. It flips back to number+unit the moment the text becomes a plain\n * dimension again, which keeps the mode derived from the value rather than\n * remembered in a flag that can go stale.\n */\n\n/** Draft state: what the two controls show, before serialization. */\ninterface DimensionDraft {\n /** Numeric text (or the whole raw string in custom mode). */\n text: string\n /** Selected unit, `''` when none is chosen. */\n unit: CssUnit | ''\n /** The value is not `<number><unit>` — the text box holds it verbatim. */\n custom: boolean\n}\n\n/**\n * How a value stored as a bare NUMBER is to be read (AGL-1219). A node prop\n * is plain CSS, so a number is pixels — the default.\n *\n * `'mui-sizing'` is for the styles panel's `sx` sizing keys, where a number\n * is NOT pixels: MUI's `sizingTransform` renders any number in (0, 1] as a\n * fraction of the parent, so `width: 0.5` is 50%. Read as pixels it would\n * show \"0.5\" with no unit and the first nudge would turn a half-width\n * element into a 0.6px one.\n */\nexport type DimensionNumberAs = 'px' | 'mui-sizing'\n\n/**\n * The CSS string a stored value stands for — the only place a bare number\n * is given its meaning. Mirrors MUI's `sizingTransform` for `'mui-sizing'`.\n */\nexport const dimensionValueToCss = (\n value: unknown,\n numberAs: DimensionNumberAs = 'px',\n): string => {\n if (typeof value === 'string') return value\n if (typeof value !== 'number' || !Number.isFinite(value)) return ''\n if (numberAs === 'mui-sizing' && value <= 1 && value !== 0) {\n // `0.3 * 100` is 30.000000000000004 in binary floating point — MUI\n // emits that verbatim; showing it in a number box would be absurd.\n return `${Number((value * 100).toFixed(4))}%`\n }\n return `${value}px`\n}\n\nexport const seedDimensionDraft = (\n value: unknown,\n numberAs: DimensionNumberAs = 'px',\n): DimensionDraft => {\n const parsed = parseCssDimension(dimensionValueToCss(value, numberAs))\n if (parsed.raw !== undefined) {\n return { text: parsed.raw, unit: '', custom: true }\n }\n return {\n text: parsed.value === undefined ? '' : `${parsed.value}`,\n unit: parsed.unit ?? '',\n custom: false,\n }\n}\n\nexport const serializeDimensionDraft = (draft: DimensionDraft): string => {\n if (draft.custom) return draft.text\n const trimmed = draft.text.trim()\n return buildCssDimension({\n value: trimmed === '' ? undefined : Number(trimmed),\n unit: draft.unit || undefined,\n })\n}\n\nexport interface CssDimensionProps extends BaseFieldProps {\n placeholder?: string\n /** Units offered; defaults to the full shared list. */\n units?: CssUnit[]\n /** How a bare number is read. See {@link DimensionNumberAs}. */\n numberAs?: DimensionNumberAs\n /**\n * Theme scale offered alongside the raw length (AGL-2486). Each option's\n * `value` is a token path MUI's sx system resolves itself — `h4.fontSize`\n * against `theme.typography` — so picking one keeps the element following\n * the theme instead of freezing the number it had when it was styled. An\n * empty list renders no scale menu at all.\n */\n scaleOptions?: ThemeScaleOption[]\n /** Offer the reset-to-unset affordance (AGL-2486). */\n clearable?: boolean\n FormFieldGridProps?: FormFieldGridProps\n}\n\nexport const CssDimensionField = (props: CssDimensionProps) => {\n const {\n input,\n isReadOnly,\n isDisabled,\n isRequired,\n label,\n placeholder,\n helperText,\n description,\n validateOnMount,\n meta,\n help,\n units = CSS_UNITS,\n numberAs = 'px',\n scaleOptions = [],\n clearable,\n FormFieldGridProps = {},\n // Free-text leftovers from the attribute schema that must never reach\n // the DOM (the attributes it was authored with as a TEXT_FIELD).\n inputProps: _inputProps,\n InputProps: _InputProps,\n multiline: _multiline,\n ...rest\n } = useFieldApi(props)\n const invalid = validationError(meta as ExtendedFieldMeta, validateOnMount)\n // A value authored as a bare number (`width: 320`, or `width: 0.5` in an\n // sx) is still a value the author has to see — reading only strings would\n // blank the field and then overwrite the number on the first edit. The\n // number is resolved to the CSS it stands for HERE, not in the form value,\n // so an untouched field still emits nothing and the stored number survives.\n const value = dimensionValueToCss(input.value, numberAs)\n\n const [draft, setDraft] = useState<DimensionDraft>(() =>\n seedDimensionDraft(value),\n )\n // Re-seed only when the value changed OUTSIDE this field (a different\n // node selected, an undo). Re-seeding from our own emits would round\n // half-typed decimals (\"1.\" -> 1 -> \"1\") out from under the caret.\n const emittedRef = useRef(value)\n useEffect(() => {\n if (value === emittedRef.current) return\n emittedRef.current = value\n setDraft(seedDimensionDraft(value))\n }, [value])\n\n const commit = useCallback(\n (next: DimensionDraft) => {\n const serialized = serializeDimensionDraft(next)\n emittedRef.current = serialized\n // A custom value the author edited back into a plain dimension gets\n // the structured controls back immediately.\n const reparsed = parseCssDimension(serialized)\n setDraft(\n next.custom && serialized.trim() !== '' && reparsed.raw === undefined\n ? seedDimensionDraft(serialized)\n : next,\n )\n input.onChange(serialized)\n },\n [input],\n )\n\n const keywordUnit = !draft.custom && !!draft.unit && isGlobalUnit(draft.unit)\n\n // Clearing a length has to drop the UNIT too (AGL-2486). Emptying the\n // number box alone leaves `px` selected, and the next keystroke silently\n // re-adopts it — which is why \"delete the number\" was never a way back to\n // unset.\n const clear = buildFieldClear({\n clearable,\n label,\n hasValue: value !== '',\n locked: Boolean(isDisabled || isReadOnly),\n onClear: () => commit({ text: '', unit: '', custom: false }),\n })\n\n const handleTextChange = useCallback(\n (event: { target: { value: string } }) => {\n const text = event.target.value\n // Typing a number into a field with no unit yet means pixels — a\n // bare `320` is not a valid CSS length and silently did nothing\n // when these were free-text fields.\n const unit =\n !draft.custom && !draft.unit && text.trim() !== ''\n ? CssUnit.PIXELS\n : draft.unit\n commit({ ...draft, text, unit })\n },\n [commit, draft],\n )\n\n const handleUnitChange = useCallback(\n (event: { target: { value: unknown } }) => {\n const unit = (event.target.value || '') as CssUnit | ''\n // Keyword units (`auto`) ARE the whole value, so the quantity goes\n // away with them; it comes back when a real unit is picked again.\n commit({ ...draft, unit, text: unit && isGlobalUnit(unit) ? '' : draft.text })\n },\n [commit, draft],\n )\n\n // Theme scale alongside the raw length (AGL-2486, item 12). A token like\n // `h4.fontSize` is not a `<number><unit>`, so the field already holds it\n // fine — in custom mode, as raw text — and all that was missing was a way\n // to PICK one. Rendered as its own menu rather than folded into the unit\n // list because it does not choose a unit, it replaces the whole value:\n // MUI's sx resolves `fontSize: 'h4.fontSize'` against `theme.typography`,\n // which is what keeps the element following the type scale.\n const activeScaleOption = scaleOptions.find(\n (option) => option.value === value,\n )\n const scaleSelect = scaleOptions.length ? (\n <InputAdornment position=\"end\" sx={{ ml: 0, mr: 0.5 }}>\n <Select\n value={activeScaleOption ? activeScaleOption.value : ''}\n onChange={(event) => {\n const picked = `${event.target.value ?? ''}`\n // Empty means \"back to a raw length\"; the number box takes over\n // again and the author is not stranded on a token they undid.\n commit(\n picked\n ? { text: picked, unit: '', custom: true }\n : { text: '', unit: '', custom: false },\n )\n }}\n disabled={isDisabled || isReadOnly}\n variant=\"standard\"\n disableUnderline\n displayEmpty\n renderValue={() =>\n activeScaleOption ? activeScaleOption.label : 'theme'\n }\n inputProps={{ 'aria-label': 'Theme scale' }}\n sx={{\n '& .MuiSelect-select': {\n pr: '20px !important',\n py: 0,\n // The token label is the part that may be clipped when the\n // row is tight — never the value (AGL-2486).\n textOverflow: 'ellipsis',\n },\n fontSize: '0.8125rem',\n color: 'text.secondary',\n minWidth: 0,\n maxWidth: 92,\n }}\n >\n <MenuItem value=\"\">\n <em>{'raw value'}</em>\n </MenuItem>\n {scaleOptions.map((option) => (\n <MenuItem key={option.value} value={option.value}>\n {option.hint ? `${option.label} — ${option.hint}` : option.label}\n </MenuItem>\n ))}\n </Select>\n </InputAdornment>\n ) : null\n\n const unitSelect = (\n <InputAdornment position=\"end\" sx={{ ml: 0 }}>\n <Select\n value={draft.custom ? '' : draft.unit}\n onChange={handleUnitChange}\n disabled={isDisabled || isReadOnly || draft.custom}\n variant=\"standard\"\n disableUnderline\n // Without displayEmpty an unset unit renders nothing at all, which\n // reads as a broken control rather than \"no unit yet\".\n displayEmpty\n renderValue={(selected) =>\n draft.custom ? 'custom' : (selected as string) || '—'\n }\n inputProps={{ 'aria-label': 'Unit' }}\n sx={{\n '& .MuiSelect-select': { pr: '20px !important', py: 0 },\n fontSize: '0.8125rem',\n color: 'text.secondary',\n minWidth: 44,\n flexShrink: 0,\n }}\n >\n <MenuItem value=\"\">\n <em>{'—'}</em>\n </MenuItem>\n {units.map((unit) => (\n <MenuItem key={unit} value={unit}>\n {unit}\n </MenuItem>\n ))}\n </Select>\n </InputAdornment>\n )\n\n return (\n <FormFieldGrid help={help} clear={clear} {...FormFieldGridProps}>\n <MuiTextField\n // Schema leftovers first: the controlled value/handlers below must\n // win over anything the attribute was authored with.\n {...rest}\n name={input.name}\n value={keywordUnit ? '' : draft.text}\n onChange={handleTextChange}\n onBlur={input.onBlur}\n onFocus={input.onFocus}\n fullWidth\n error={!!invalid}\n helperText={\n invalid ||\n ((meta.touched || validateOnMount) && meta.warning) ||\n helperText ||\n description\n }\n disabled={isDisabled}\n label={label}\n placeholder={keywordUnit ? `${draft.unit}` : placeholder}\n required={isRequired}\n // A keyword unit has no quantity to type, and a custom expression\n // is free text — only the plain number+unit case is numeric.\n type={draft.custom || keywordUnit ? 'text' : 'number'}\n slotProps={{\n // A keyword value (`auto`) is shown through the PLACEHOLDER,\n // and MUI hides a placeholder behind CSS while the label is\n // un-shrunk — so the field printed its label over an empty box\n // on a node that demonstrably had a value (AGL-2486). Only this\n // case: an ordinary empty length has nothing to reveal, and\n // shrinking it would be a restyle rather than a fix.\n inputLabel: keywordUnit ? { shrink: true } : undefined,\n input: {\n readOnly: isReadOnly || keywordUnit,\n endAdornment: (\n <>\n {scaleSelect}\n {unitSelect}\n </>\n ),\n },\n /**\n * The VALUE is never the control that gets squeezed (AGL-2486).\n *\n * The number box is the only part of this row with no width of\n * its own — the two pickers are `endAdornment` siblings with\n * fixed widths, so the input is whatever is left, and in a\n * narrow docked panel that is a couple of characters: Font\n * Size renders as `2.:`. A floor here makes the PICKERS give way\n * instead, which is the right order — a clipped unit label is\n * a nuisance, a clipped value is unreadable.\n */\n htmlInput: {\n inputMode: 'decimal',\n style: { minWidth: '4.5ch' },\n },\n }}\n />\n </FormFieldGrid>\n )\n}\n\nexport default CssDimensionField\n"],"names":["buildCssDimension","CSS_UNITS","CssUnit","isGlobalUnit","parseCssDimension","InputAdornment","MenuItem","Select","TextField","MuiTextField","useCallback","useEffect","useRef","useState","useFieldApi","FormFieldGrid","buildFieldClear","validationError","dimensionValueToCss","value","numberAs","Number","isFinite","toFixed","seedDimensionDraft","parsed","raw","undefined","text","unit","custom","serializeDimensionDraft","draft","trimmed","trim","CssDimensionField","props","input","isReadOnly","isDisabled","isRequired","label","placeholder","helperText","description","validateOnMount","meta","help","units","scaleOptions","clearable","FormFieldGridProps","inputProps","_inputProps","InputProps","_InputProps","multiline","_multiline","rest","invalid","setDraft","emittedRef","current","commit","next","serialized","reparsed","onChange","keywordUnit","clear","hasValue","locked","Boolean","onClear","handleTextChange","event","target","PIXELS","handleUnitChange","activeScaleOption","find","option","scaleSelect","length","position","sx","ml","mr","picked","disabled","variant","disableUnderline","displayEmpty","renderValue","pr","py","textOverflow","fontSize","color","minWidth","maxWidth","em","map","hint","unitSelect","selected","flexShrink","name","onBlur","onFocus","fullWidth","error","touched","warning","required","type","slotProps","inputLabel","shrink","readOnly","endAdornment","htmlInput","inputMode","style"],"mappings":"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"}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type ColorPickerProps as AglynColorPickerProps } from '@aglyn/shared-ui-color-picker';
18
+ import { type CssGradientStop, type CssGradientType } from '@aglyn/shared-data-enums';
19
+ import { type FormFieldGridProps } from './form-field-grid';
20
+ import type { BaseFieldProps } from './types';
21
+ /**
22
+ * CssGradient (AGL-1331): the Background Fill editor.
23
+ *
24
+ * The Styles panel's Colors group offered *Text Color* and *Background
25
+ * Color* and nothing else, so a gradient could not be authored at all —
26
+ * and typing one into Background Color was accepted, stored as
27
+ * `background-color: linear-gradient(…)`, and dropped by the CSS parser
28
+ * with no error anywhere. This is the control that expresses the value,
29
+ * and `describeCssColorProblem` is the guard that stops the silent store;
30
+ * the two ship together on purpose.
31
+ *
32
+ * The PERSISTED value is ONE CSS string under `backgroundImage` — the same
33
+ * contract as `CssDimensionField`, so nothing downstream has to know
34
+ * this editor exists. A **Solid** fill writes the explicit keyword
35
+ * `none`, which paints no image and leaves the neighbouring Background
36
+ * Color field to do the job; the two never fight over one property.
37
+ * **Default** writes `''`, which clears the property entirely.
38
+ *
39
+ * Solid and Default are two states, not one (AGL-1338). Solid shipped as
40
+ * "write nothing" — and on a reusable-component INSTANCE, where the panel
41
+ * edits an override slice, writing nothing IS "no override": the field
42
+ * could not be used to take a gradient off one placement, because the
43
+ * absence it produced was indistinguishable from never having touched it.
44
+ * `none` is a value, so it merges over the component's gradient and wins.
45
+ *
46
+ * Stops bind to palette TOKENS or to literals, and a single gradient can
47
+ * mix them: the marketing CTA's endpoints are `primary.main` /
48
+ * `secondary.main` while its mid stop `#7A5CF0` has no token at all. A
49
+ * token stop persists as `var(--mui-palette-primary-main, #00B0FF)` and is
50
+ * substituted against the site theme by the node renderer
51
+ * (`resolvePaletteVarsSx`) — see that module for why the reference is not
52
+ * left to the browser.
53
+ *
54
+ * Anything the editor cannot model — `conic-gradient`, `to bottom right`,
55
+ * a comma-stacked image list — falls back to a free-text box holding the
56
+ * raw string, so no existing value is destroyed by an editor that cannot
57
+ * show it. The stacked case reaches that box only because `parseCssGradient`
58
+ * splits top-level layers first (AGL-1336); until it did, a tint over a
59
+ * photo opened in the STOP editor and the author's first edit deleted the
60
+ * photo.
61
+ */
62
+ /**
63
+ * The CSS an explicit **Solid** fill persists — `background-image`'s own
64
+ * initial value, so it renders identically to no declaration at all while
65
+ * remaining a VALUE that can override an inherited one (AGL-1338).
66
+ */
67
+ export declare const SOLID_FILL_VALUE = "none";
68
+ /**
69
+ * What the fill-type switch offers. Three states, because "no gradient"
70
+ * and "no opinion" are different answers on a component instance:
71
+ *
72
+ * - `''` — unset: the property is not written at all. On a plain node
73
+ * that is the browser default; in an instance's override slice it is
74
+ * how "this instance does not override the fill" is stored, so the
75
+ * component's own fill keeps painting.
76
+ * - `solid` — an explicit {@link SOLID_FILL_VALUE}: paint no image and
77
+ * let Background Color show. A real value, so it beats a gradient the
78
+ * component set.
79
+ * - `linear` / `radial` — the gradient the stop editor builds.
80
+ */
81
+ type GradientFillType = '' | 'solid' | CssGradientType;
82
+ interface GradientDraft {
83
+ fill: GradientFillType;
84
+ angle: string;
85
+ /** Radial centre, percent, as typed. Empty means CSS's default (centre). */
86
+ positionX: string;
87
+ positionY: string;
88
+ stops: CssGradientStop[];
89
+ /** The value is not a gradient this editor can model — held verbatim. */
90
+ raw?: string;
91
+ }
92
+ export declare const seedGradientDraft: (value: unknown) => GradientDraft;
93
+ export declare const serializeGradientDraft: (draft: GradientDraft) => string;
94
+ export interface CssGradientProps extends BaseFieldProps {
95
+ /** Swatches offered on the custom stage, same as the colour field. */
96
+ presetColors?: string[];
97
+ /**
98
+ * What the unset choice is called (AGL-1338). Defaults to the panel's
99
+ * usual `Default`; a caller editing a component instance's override
100
+ * slice says so, because there "unset" means the component's own fill
101
+ * keeps painting rather than nothing painting.
102
+ */
103
+ unsetLabel?: string;
104
+ /** Offer the reset-to-unset affordance (AGL-2486). */
105
+ clearable?: boolean;
106
+ ColorPickerProps?: Partial<AglynColorPickerProps>;
107
+ FormFieldGridProps?: FormFieldGridProps;
108
+ }
109
+ export declare const CssGradientField: (props: CssGradientProps) => import("react").JSX.Element;
110
+ export default CssGradientField;