@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.
- package/LICENSE +201 -0
- package/README.md +7 -0
- package/package.json +52 -0
- package/src/index.d.ts +17 -0
- package/src/index.js +18 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/color-picker-tokens.d.ts +135 -0
- package/src/lib/components/color-picker-tokens.js +431 -0
- package/src/lib/components/color-picker-tokens.js.map +1 -0
- package/src/lib/components/color-picker.component.d.ts +64 -0
- package/src/lib/components/color-picker.component.js +322 -0
- package/src/lib/components/color-picker.component.js.map +1 -0
- package/src/lib/components/create-artifact-drawer.component.d.ts +100 -0
- package/src/lib/components/create-artifact-drawer.component.js +184 -0
- package/src/lib/components/create-artifact-drawer.component.js.map +1 -0
- package/src/lib/components/grid-form-template.component.d.ts +21 -0
- package/src/lib/components/grid-form-template.component.js +81 -0
- package/src/lib/components/grid-form-template.component.js.map +1 -0
- package/src/lib/components/icon-select.component.d.ts +52 -0
- package/src/lib/components/icon-select.component.js +453 -0
- package/src/lib/components/icon-select.component.js.map +1 -0
- package/src/lib/components/select.component.d.ts +58 -0
- package/src/lib/components/select.component.js +107 -0
- package/src/lib/components/select.component.js.map +1 -0
- package/src/lib/components/switch.component.d.ts +38 -0
- package/src/lib/components/switch.component.js +76 -0
- package/src/lib/components/switch.component.js.map +1 -0
- package/src/lib/components/text-field.component.d.ts +45 -0
- package/src/lib/components/text-field.component.js +77 -0
- package/src/lib/components/text-field.component.js.map +1 -0
- package/src/lib/components/textarea.component.d.ts +45 -0
- package/src/lib/components/textarea.component.js +61 -0
- package/src/lib/components/textarea.component.js.map +1 -0
- package/src/lib/components/toggle-button.component.d.ts +27 -0
- package/src/lib/components/toggle-button.component.js +116 -0
- package/src/lib/components/toggle-button.component.js.map +1 -0
- package/src/lib/constants/component-mappers.d.ts +42 -0
- package/src/lib/constants/component-mappers.js +70 -0
- package/src/lib/constants/component-mappers.js.map +1 -0
- package/src/lib/constants/dynamic-fields.d.ts +45 -0
- package/src/lib/constants/dynamic-fields.js +65 -0
- package/src/lib/constants/dynamic-fields.js.map +1 -0
- package/src/lib/constants/field-configurations.d.ts +53 -0
- package/src/lib/constants/field-configurations.js +121 -0
- package/src/lib/constants/field-configurations.js.map +1 -0
- package/src/lib/constants/flags.d.ts +72 -0
- package/src/lib/constants/flags.js +69 -0
- package/src/lib/constants/flags.js.map +1 -0
- package/src/lib/hocs/with-grid-item.d.ts +25 -0
- package/src/lib/hocs/with-grid-item.js +48 -0
- package/src/lib/hocs/with-grid-item.js.map +1 -0
- package/src/lib/jsx-forms.d.ts +41 -0
- package/src/lib/jsx-forms.js +42 -0
- package/src/lib/jsx-forms.js.map +1 -0
- package/src/lib/mapper/breakpoint-span.d.ts +73 -0
- package/src/lib/mapper/breakpoint-span.js +346 -0
- package/src/lib/mapper/breakpoint-span.js.map +1 -0
- package/src/lib/mapper/checkbox.d.ts +43 -0
- package/src/lib/mapper/checkbox.js +111 -0
- package/src/lib/mapper/checkbox.js.map +1 -0
- package/src/lib/mapper/css-border.d.ts +113 -0
- package/src/lib/mapper/css-border.js +410 -0
- package/src/lib/mapper/css-border.js.map +1 -0
- package/src/lib/mapper/css-dimension.d.ts +89 -0
- package/src/lib/mapper/css-dimension.js +327 -0
- package/src/lib/mapper/css-dimension.js.map +1 -0
- package/src/lib/mapper/css-gradient.d.ts +110 -0
- package/src/lib/mapper/css-gradient.js +720 -0
- package/src/lib/mapper/css-gradient.js.map +1 -0
- package/src/lib/mapper/data-table.d.ts +23 -0
- package/src/lib/mapper/data-table.js +296 -0
- package/src/lib/mapper/data-table.js.map +1 -0
- package/src/lib/mapper/date-picker.d.ts +26 -0
- package/src/lib/mapper/date-picker.js +69 -0
- package/src/lib/mapper/date-picker.js.map +1 -0
- package/src/lib/mapper/dual-list-select.d.ts +115 -0
- package/src/lib/mapper/dual-list-select.js +353 -0
- package/src/lib/mapper/dual-list-select.js.map +1 -0
- package/src/lib/mapper/field-array.d.ts +71 -0
- package/src/lib/mapper/field-array.js +326 -0
- package/src/lib/mapper/field-array.js.map +1 -0
- package/src/lib/mapper/form-field-grid.d.ts +155 -0
- package/src/lib/mapper/form-field-grid.js +194 -0
- package/src/lib/mapper/form-field-grid.js.map +1 -0
- package/src/lib/mapper/index.d.ts +50 -0
- package/src/lib/mapper/index.js +49 -0
- package/src/lib/mapper/index.js.map +1 -0
- package/src/lib/mapper/multiple-choice-list.d.ts +32 -0
- package/src/lib/mapper/multiple-choice-list.js +105 -0
- package/src/lib/mapper/multiple-choice-list.js.map +1 -0
- package/src/lib/mapper/plain-text.d.ts +31 -0
- package/src/lib/mapper/plain-text.js +48 -0
- package/src/lib/mapper/plain-text.js.map +1 -0
- package/src/lib/mapper/preset-choice.d.ts +106 -0
- package/src/lib/mapper/preset-choice.js +267 -0
- package/src/lib/mapper/preset-choice.js.map +1 -0
- package/src/lib/mapper/radio.d.ts +40 -0
- package/src/lib/mapper/radio.js +122 -0
- package/src/lib/mapper/radio.js.map +1 -0
- package/src/lib/mapper/select.d.ts +60 -0
- package/src/lib/mapper/select.js +203 -0
- package/src/lib/mapper/select.js.map +1 -0
- package/src/lib/mapper/slider.d.ts +45 -0
- package/src/lib/mapper/slider.js +119 -0
- package/src/lib/mapper/slider.js.map +1 -0
- package/src/lib/mapper/stored-field-value.d.ts +23 -0
- package/src/lib/mapper/stored-field-value.js +29 -0
- package/src/lib/mapper/stored-field-value.js.map +1 -0
- package/src/lib/mapper/sub-form.d.ts +39 -0
- package/src/lib/mapper/sub-form.js +108 -0
- package/src/lib/mapper/sub-form.js.map +1 -0
- package/src/lib/mapper/switch.d.ts +47 -0
- package/src/lib/mapper/switch.js +93 -0
- package/src/lib/mapper/switch.js.map +1 -0
- package/src/lib/mapper/tabs.d.ts +34 -0
- package/src/lib/mapper/tabs.js +64 -0
- package/src/lib/mapper/tabs.js.map +1 -0
- package/src/lib/mapper/text-field.d.ts +33 -0
- package/src/lib/mapper/text-field.js +88 -0
- package/src/lib/mapper/text-field.js.map +1 -0
- package/src/lib/mapper/textarea.d.ts +31 -0
- package/src/lib/mapper/textarea.js +68 -0
- package/src/lib/mapper/textarea.js.map +1 -0
- package/src/lib/mapper/theme-scale.d.ts +79 -0
- package/src/lib/mapper/theme-scale.js +162 -0
- package/src/lib/mapper/theme-scale.js.map +1 -0
- package/src/lib/mapper/time-picker.d.ts +26 -0
- package/src/lib/mapper/time-picker.js +69 -0
- package/src/lib/mapper/time-picker.js.map +1 -0
- package/src/lib/mapper/types.d.ts +43 -0
- package/src/lib/mapper/types.js +22 -0
- package/src/lib/mapper/types.js.map +1 -0
- package/src/lib/mapper/validation-error.d.ts +24 -0
- package/src/lib/mapper/validation-error.js +24 -0
- package/src/lib/mapper/validation-error.js.map +1 -0
- package/src/lib/mapper/wizard.d.ts +21 -0
- package/src/lib/mapper/wizard.js +203 -0
- package/src/lib/mapper/wizard.js.map +1 -0
- package/src/lib/types.d.ts +19 -0
- package/src/lib/types.js +18 -0
- package/src/lib/types.js.map +1 -0
- package/src/lib/utils/option-is-equal-to-value.d.ts +20 -0
- package/src/lib/utils/option-is-equal-to-value.js +21 -0
- package/src/lib/utils/option-is-equal-to-value.js.map +1 -0
- package/src/lib/utils/validation-message.d.ts +22 -0
- package/src/lib/utils/validation-message.js +24 -0
- package/src/lib/utils/validation-message.js.map +1 -0
- package/src/lib/vendor/data-driven-forms.d.ts +33 -0
- package/src/lib/vendor/data-driven-forms.js +29 -0
- 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":";;;AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,iBAAiB,EACjBC,SAAS,EACTC,OAAO,EACPC,YAAY,EACZC,iBAAiB,QACZ,2BAA0B;AACjC,SACEC,cAAc,EACdC,QAAQ,EACRC,MAAM,EACNC,aAAaC,YAAY,QACpB,gBAAe;AACtB,SAASC,WAAW,EAAEC,SAAS,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAO;AAEhE,SAASC,WAAW,QAAQ,iCAA6B;AACzD,OAAOC,iBACLC,eAAe,QAEV,uBAAmB;AAG1B,SAAiCC,eAAe,QAAQ,wBAAoB;AA+C5E;;;CAGC,GACD,OAAO,MAAMC,sBAAsB,CACjCC,OACAC,WAA8B,IAAI;IAElC,IAAI,OAAOD,UAAU,UAAU,OAAOA;IACtC,IAAI,OAAOA,UAAU,YAAY,CAACE,OAAOC,QAAQ,CAACH,QAAQ,OAAO;IACjE,IAAIC,aAAa,gBAAgBD,SAAS,KAAKA,UAAU,GAAG;QAC1D,mEAAmE;QACnE,mEAAmE;QACnE,OAAO,GAAGE,OAAO,AAACF,CAAAA,QAAQ,GAAE,EAAGI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C;IACA,OAAO,GAAGJ,MAAM,EAAE,CAAC;AACrB,EAAC;AAED,OAAO,MAAMK,qBAAqB,CAChCL,OACAC,WAA8B,IAAI;QAQ1BK;IANR,MAAMA,SAASrB,kBAAkBc,oBAAoBC,OAAOC;IAC5D,IAAIK,OAAOC,GAAG,KAAKC,WAAW;QAC5B,OAAO;YAAEC,MAAMH,OAAOC,GAAG;YAAEG,MAAM;YAAIC,QAAQ;QAAK;IACpD;IACA,OAAO;QACLF,MAAMH,OAAON,KAAK,KAAKQ,YAAY,KAAK,GAAGF,OAAON,KAAK,EAAE;QACzDU,IAAI,GAAEJ,eAAAA,OAAOI,IAAI,YAAXJ,eAAe;QACrBK,QAAQ;IACV;AACF,EAAC;AAED,OAAO,MAAMC,0BAA0B,CAACC;IACtC,IAAIA,MAAMF,MAAM,EAAE,OAAOE,MAAMJ,IAAI;IACnC,MAAMK,UAAUD,MAAMJ,IAAI,CAACM,IAAI;IAC/B,OAAOlC,kBAAkB;QACvBmB,OAAOc,YAAY,KAAKN,YAAYN,OAAOY;QAC3CJ,MAAMG,MAAMH,IAAI,IAAIF;IACtB;AACF,EAAC;AAqBD,OAAO,MAAMQ,oBAAoB,CAACC;IAChC,MAuBItB,eAAAA,YAAYsB,QAvBV,EACJC,KAAK,EACLC,UAAU,EACVC,UAAU,EACVC,UAAU,EACVC,KAAK,EACLC,WAAW,EACXC,UAAU,EACVC,WAAW,EACXC,eAAe,EACfC,IAAI,EACJC,IAAI,EACJC,QAAQ/C,SAAS,EACjBmB,WAAW,IAAI,EACf6B,eAAe,EAAE,EACjBC,SAAS,EACTC,qBAAqB,CAAC,CAAC,EACvB,sEAAsE;IACtE,iEAAiE;IACjEC,YAAYC,WAAW,EACvBC,YAAYC,WAAW,EACvBC,WAAWC,UAAU,EAEtB,GAAG3C,cADC4C,wCACD5C;;;;;;;;;;;;;;;;;;;;;IACJ,MAAM6C,UAAU1C,gBAAgB6B,MAA2BD;IAC3D,yEAAyE;IACzE,0EAA0E;IAC1E,uEAAuE;IACvE,2EAA2E;IAC3E,4EAA4E;IAC5E,MAAM1B,QAAQD,oBAAoBmB,MAAMlB,KAAK,EAAEC;IAE/C,MAAM,CAACY,OAAO4B,SAAS,GAAG/C,SAAyB,IACjDW,mBAAmBL;IAErB,sEAAsE;IACtE,qEAAqE;IACrE,mEAAmE;IACnE,MAAM0C,aAAajD,OAAOO;IAC1BR,UAAU;QACR,IAAIQ,UAAU0C,WAAWC,OAAO,EAAE;QAClCD,WAAWC,OAAO,GAAG3C;QACrByC,SAASpC,mBAAmBL;IAC9B,GAAG;QAACA;KAAM;IAEV,MAAM4C,SAASrD,YACb,CAACsD;QACC,MAAMC,aAAalC,wBAAwBiC;QAC3CH,WAAWC,OAAO,GAAGG;QACrB,oEAAoE;QACpE,4CAA4C;QAC5C,MAAMC,WAAW9D,kBAAkB6D;QACnCL,SACEI,KAAKlC,MAAM,IAAImC,WAAW/B,IAAI,OAAO,MAAMgC,SAASxC,GAAG,KAAKC,YACxDH,mBAAmByC,cACnBD;QAEN3B,MAAM8B,QAAQ,CAACF;IACjB,GACA;QAAC5B;KAAM;IAGT,MAAM+B,cAAc,CAACpC,MAAMF,MAAM,IAAI,CAAC,CAACE,MAAMH,IAAI,IAAI1B,aAAa6B,MAAMH,IAAI;IAE5E,sEAAsE;IACtE,yEAAyE;IACzE,0EAA0E;IAC1E,SAAS;IACT,MAAMwC,QAAQrD,gBAAgB;QAC5BkC;QACAT;QACA6B,UAAUnD,UAAU;QACpBoD,QAAQC,QAAQjC,cAAcD;QAC9BmC,SAAS,IAAMV,OAAO;gBAAEnC,MAAM;gBAAIC,MAAM;gBAAIC,QAAQ;YAAM;IAC5D;IAEA,MAAM4C,mBAAmBhE,YACvB,CAACiE;QACC,MAAM/C,OAAO+C,MAAMC,MAAM,CAACzD,KAAK;QAC/B,iEAAiE;QACjE,gEAAgE;QAChE,oCAAoC;QACpC,MAAMU,OACJ,CAACG,MAAMF,MAAM,IAAI,CAACE,MAAMH,IAAI,IAAID,KAAKM,IAAI,OAAO,KAC5ChC,QAAQ2E,MAAM,GACd7C,MAAMH,IAAI;QAChBkC,OAAO,aAAK/B;YAAOJ;YAAMC;;IAC3B,GACA;QAACkC;QAAQ/B;KAAM;IAGjB,MAAM8C,mBAAmBpE,YACvB,CAACiE;QACC,MAAM9C,OAAQ8C,MAAMC,MAAM,CAACzD,KAAK,IAAI;QACpC,mEAAmE;QACnE,kEAAkE;QAClE4C,OAAO,aAAK/B;YAAOH;YAAMD,MAAMC,QAAQ1B,aAAa0B,QAAQ,KAAKG,MAAMJ,IAAI;;IAC7E,GACA;QAACmC;QAAQ/B;KAAM;IAGjB,yEAAyE;IACzE,yEAAyE;IACzE,0EAA0E;IAC1E,yEAAyE;IACzE,uEAAuE;IACvE,0EAA0E;IAC1E,4DAA4D;IAC5D,MAAM+C,oBAAoB9B,aAAa+B,IAAI,CACzC,CAACC,SAAWA,OAAO9D,KAAK,KAAKA;IAE/B,MAAM+D,cAAcjC,aAAakC,MAAM,iBACrC,KAAC9E;QAAe+E,UAAS;QAAMC,IAAI;YAAEC,IAAI;YAAGC,IAAI;QAAI;kBAClD,cAAA,MAAChF;YACCY,OAAO4D,oBAAoBA,kBAAkB5D,KAAK,GAAG;YACrDgD,UAAU,CAACQ;oBACSA;gBAAlB,MAAMa,SAAS,IAAGb,sBAAAA,MAAMC,MAAM,CAACzD,KAAK,YAAlBwD,sBAAsB,IAAI;gBAC5C,gEAAgE;gBAChE,8DAA8D;gBAC9DZ,OACEyB,SACI;oBAAE5D,MAAM4D;oBAAQ3D,MAAM;oBAAIC,QAAQ;gBAAK,IACvC;oBAAEF,MAAM;oBAAIC,MAAM;oBAAIC,QAAQ;gBAAM;YAE5C;YACA2D,UAAUlD,cAAcD;YACxBoD,SAAQ;YACRC,gBAAgB;YAChBC,YAAY;YACZC,aAAa,IACXd,oBAAoBA,kBAAkBtC,KAAK,GAAG;YAEhDW,YAAY;gBAAE,cAAc;YAAc;YAC1CiC,IAAI;gBACF,uBAAuB;oBACrBS,IAAI;oBACJC,IAAI;oBACJ,2DAA2D;oBAC3D,6CAA6C;oBAC7CC,cAAc;gBAChB;gBACAC,UAAU;gBACVC,OAAO;gBACPC,UAAU;gBACVC,UAAU;YACZ;;8BAEA,KAAC9F;oBAASa,OAAM;8BACd,cAAA,KAACkF;kCAAI;;;gBAENpD,aAAaqD,GAAG,CAAC,CAACrB,uBACjB,KAAC3E;wBAA4Ba,OAAO8D,OAAO9D,KAAK;kCAC7C8D,OAAOsB,IAAI,GAAG,GAAGtB,OAAOxC,KAAK,CAAC,GAAG,EAAEwC,OAAOsB,IAAI,EAAE,GAAGtB,OAAOxC,KAAK;uBADnDwC,OAAO9D,KAAK;;;SAM/B;IAEJ,MAAMqF,2BACJ,KAACnG;QAAe+E,UAAS;QAAMC,IAAI;YAAEC,IAAI;QAAE;kBACzC,cAAA,MAAC/E;YACCY,OAAOa,MAAMF,MAAM,GAAG,KAAKE,MAAMH,IAAI;YACrCsC,UAAUW;YACVW,UAAUlD,cAAcD,cAAcN,MAAMF,MAAM;YAClD4D,SAAQ;YACRC,gBAAgB;YAChB,mEAAmE;YACnE,uDAAuD;YACvDC,YAAY;YACZC,aAAa,CAACY,WACZzE,MAAMF,MAAM,GAAG,WAAW,AAAC2E,YAAuB;YAEpDrD,YAAY;gBAAE,cAAc;YAAO;YACnCiC,IAAI;gBACF,uBAAuB;oBAAES,IAAI;oBAAmBC,IAAI;gBAAE;gBACtDE,UAAU;gBACVC,OAAO;gBACPC,UAAU;gBACVO,YAAY;YACd;;8BAEA,KAACpG;oBAASa,OAAM;8BACd,cAAA,KAACkF;kCAAI;;;gBAENrD,MAAMsD,GAAG,CAAC,CAACzE,qBACV,KAACvB;wBAAoBa,OAAOU;kCACzBA;uBADYA;;;;IAQvB,qBACE,KAACd;QAAcgC,MAAMA;QAAMsB,OAAOA;OAAWlB;kBAC3C,cAAA,KAAC1C,2BAGKiD;YACJiD,MAAMtE,MAAMsE,IAAI;YAChBxF,OAAOiD,cAAc,KAAKpC,MAAMJ,IAAI;YACpCuC,UAAUO;YACVkC,QAAQvE,MAAMuE,MAAM;YACpBC,SAASxE,MAAMwE,OAAO;YACtBC,SAAS;YACTC,OAAO,CAAC,CAACpD;YACThB,YACEgB,WACC,AAACb,CAAAA,KAAKkE,OAAO,IAAInE,eAAc,KAAMC,KAAKmE,OAAO,IAClDtE,cACAC;YAEF6C,UAAUlD;YACVE,OAAOA;YACPC,aAAa0B,cAAc,GAAGpC,MAAMH,IAAI,EAAE,GAAGa;YAC7CwE,UAAU1E;YACV,kEAAkE;YAClE,6DAA6D;YAC7D2E,MAAMnF,MAAMF,MAAM,IAAIsC,cAAc,SAAS;YAC7CgD,WAAW;gBACT,6DAA6D;gBAC7D,4DAA4D;gBAC5D,+DAA+D;gBAC/D,gEAAgE;gBAChE,4DAA4D;gBAC5D,qDAAqD;gBACrDC,YAAYjD,cAAc;oBAAEkD,QAAQ;gBAAK,IAAI3F;gBAC7CU,OAAO;oBACLkF,UAAUjF,cAAc8B;oBACxBoD,4BACE;;4BACGtC;4BACAsB;;;gBAGP;gBACA;;;;;;;;;;WAUC,GACDiB,WAAW;oBACTC,WAAW;oBACXC,OAAO;wBAAExB,UAAU;oBAAQ;gBAC7B;YACF;;;AAIR,EAAC;AAED,eAAehE,kBAAiB"}
|
|
@@ -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;
|