@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,113 @@
|
|
|
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 FormFieldGridProps } from './form-field-grid';
|
|
18
|
+
import type { BaseFieldProps } from './types';
|
|
19
|
+
/**
|
|
20
|
+
* CssBorder (AGL-2486): a thickness box plus a line-style picker for anything
|
|
21
|
+
* holding a CSS border shorthand.
|
|
22
|
+
*
|
|
23
|
+
* The alternative is a raw shorthand text box — `Border` and the four per-side
|
|
24
|
+
* fields each wanting `1px solid`, `Outline` wanting `2px solid`. A border is
|
|
25
|
+
* two obvious choices (how thick, what kind of line) plus a colour, and a text
|
|
26
|
+
* box turns them into a grammar exercise for an author who does not write CSS.
|
|
27
|
+
* A mom-and-pop shop owner is one of Aglyn's three stated ICPs.
|
|
28
|
+
*
|
|
29
|
+
* **The persisted value is unchanged: still one CSS shorthand string**
|
|
30
|
+
* (`"1px solid"`, `"2px dashed"`, `"none"`, `""`). This is purely an input
|
|
31
|
+
* affordance, so renderers, published documents and existing nodes stay
|
|
32
|
+
* untouched, and it composes with the responsive/scheme pipeline exactly as
|
|
33
|
+
* the free-text box did.
|
|
34
|
+
*
|
|
35
|
+
* **Colour deliberately stays its own field.** MUI declares `border` with
|
|
36
|
+
* `borderTransform`, which only turns a bare NUMBER into `${n}px solid` and
|
|
37
|
+
* otherwise passes the string through verbatim — so a palette token inside
|
|
38
|
+
* the shorthand would not resolve, and the panel's per-scheme colour scoping
|
|
39
|
+
* (`SCHEME_SCOPED_STYLE_FIELDS`) is keyed on `borderColor`. Folding the
|
|
40
|
+
* colour in here would have cost both. The group instead places Border
|
|
41
|
+
* Color immediately beside this control so the three choices read as one
|
|
42
|
+
* row.
|
|
43
|
+
*
|
|
44
|
+
* **The escape hatch is the same one `CssDimension` uses.** A value this
|
|
45
|
+
* editor cannot model — `thin solid`, `1px solid #f00`, `calc()` in a
|
|
46
|
+
* width, a `{{token}}` binding — falls back to a plain text box holding the
|
|
47
|
+
* raw string rather than being clobbered, and flips back to the structured
|
|
48
|
+
* controls the moment the text becomes a plain `<width> <style>` again. The
|
|
49
|
+
* mode is DERIVED from the value, never remembered in a flag that can go
|
|
50
|
+
* stale.
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* The line styles offered.
|
|
54
|
+
*
|
|
55
|
+
* `none` is on the list and is a real value, not the absence of one: it
|
|
56
|
+
* removes a border a component or the theme is drawing, which is different
|
|
57
|
+
* from clearing the field (that hands the decision back to the theme). The
|
|
58
|
+
* long tail CSS also has — `groove`, `ridge`, `inset`, `outset` — is
|
|
59
|
+
* deliberately absent: they are bevel effects nobody has asked for, and an
|
|
60
|
+
* author who wants one still has the raw text box.
|
|
61
|
+
*/
|
|
62
|
+
export declare const CSS_BORDER_STYLES: readonly ["solid", "dashed", "dotted", "double", "none"];
|
|
63
|
+
export type CssBorderStyle = (typeof CSS_BORDER_STYLES)[number];
|
|
64
|
+
/** Plain-English name for each line style — no CSS vocabulary required. */
|
|
65
|
+
export declare const CSS_BORDER_STYLE_LABELS: Record<CssBorderStyle, string>;
|
|
66
|
+
/**
|
|
67
|
+
* What the CLOSED picker shows, which is not what the menu shows.
|
|
68
|
+
*
|
|
69
|
+
* The picker is an `endAdornment` inside a half-width field, so its width
|
|
70
|
+
* comes straight out of the box the author types the thickness into — and
|
|
71
|
+
* out of the room the field's own label needs. "Dashed line" collides with
|
|
72
|
+
* `Border Bottom`; "Dashed" does not. The menu, which has the whole popover
|
|
73
|
+
* to itself, keeps the sentence.
|
|
74
|
+
*/
|
|
75
|
+
export declare const CSS_BORDER_STYLE_SHORT_LABELS: Record<CssBorderStyle, string>;
|
|
76
|
+
/** Draft state: what the two controls show, before serialization. */
|
|
77
|
+
export interface BorderDraft {
|
|
78
|
+
/** Thickness in pixels as typed (or the whole raw string in custom mode). */
|
|
79
|
+
width: string;
|
|
80
|
+
/** Selected line style, `''` when none is chosen. */
|
|
81
|
+
style: CssBorderStyle | '';
|
|
82
|
+
/** The value is not `<width>px <style>` — the text box holds it verbatim. */
|
|
83
|
+
custom: boolean;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Reads a stored shorthand into the two controls.
|
|
87
|
+
*
|
|
88
|
+
* Accepts exactly the shapes this editor can put back together — a pixel
|
|
89
|
+
* width, a known style, or both in either CSS order — and marks everything
|
|
90
|
+
* else `custom` so the raw text survives untouched. `0` is a legitimate
|
|
91
|
+
* width (it is how an author removes a border while keeping its box), and
|
|
92
|
+
* `strictNullChecks` is off repo-wide, so emptiness is spelled out rather
|
|
93
|
+
* than left to a falsy test.
|
|
94
|
+
*/
|
|
95
|
+
export declare const seedBorderDraft: (value: unknown) => BorderDraft;
|
|
96
|
+
/**
|
|
97
|
+
* Serializes the two controls back into one shorthand.
|
|
98
|
+
*
|
|
99
|
+
* `none` is the whole value — a width in front of it is meaningless CSS and
|
|
100
|
+
* would read as a thickness the author cannot see the effect of.
|
|
101
|
+
*/
|
|
102
|
+
export declare const serializeBorderDraft: (draft: BorderDraft) => string;
|
|
103
|
+
export interface CssBorderProps extends BaseFieldProps {
|
|
104
|
+
placeholder?: string;
|
|
105
|
+
/** Offer the reset-to-unset affordance (AGL-2486). */
|
|
106
|
+
clearable?: boolean;
|
|
107
|
+
FormFieldGridProps?: FormFieldGridProps;
|
|
108
|
+
}
|
|
109
|
+
export declare const CssBorderField: {
|
|
110
|
+
(props: CssBorderProps): import("react").JSX.Element;
|
|
111
|
+
displayName: string;
|
|
112
|
+
};
|
|
113
|
+
export default CssBorderField;
|
|
@@ -0,0 +1,410 @@
|
|
|
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 { Box, InputAdornment, MenuItem, Select, TextField as MuiTextField, Typography } from "@mui/material";
|
|
20
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
21
|
+
import { useFieldApi } from "../vendor/data-driven-forms.js";
|
|
22
|
+
import FormFieldGrid, { buildFieldClear } from "./form-field-grid.js";
|
|
23
|
+
import { validationError } from "./validation-error.js";
|
|
24
|
+
/**
|
|
25
|
+
* CssBorder (AGL-2486): a thickness box plus a line-style picker for anything
|
|
26
|
+
* holding a CSS border shorthand.
|
|
27
|
+
*
|
|
28
|
+
* The alternative is a raw shorthand text box — `Border` and the four per-side
|
|
29
|
+
* fields each wanting `1px solid`, `Outline` wanting `2px solid`. A border is
|
|
30
|
+
* two obvious choices (how thick, what kind of line) plus a colour, and a text
|
|
31
|
+
* box turns them into a grammar exercise for an author who does not write CSS.
|
|
32
|
+
* A mom-and-pop shop owner is one of Aglyn's three stated ICPs.
|
|
33
|
+
*
|
|
34
|
+
* **The persisted value is unchanged: still one CSS shorthand string**
|
|
35
|
+
* (`"1px solid"`, `"2px dashed"`, `"none"`, `""`). This is purely an input
|
|
36
|
+
* affordance, so renderers, published documents and existing nodes stay
|
|
37
|
+
* untouched, and it composes with the responsive/scheme pipeline exactly as
|
|
38
|
+
* the free-text box did.
|
|
39
|
+
*
|
|
40
|
+
* **Colour deliberately stays its own field.** MUI declares `border` with
|
|
41
|
+
* `borderTransform`, which only turns a bare NUMBER into `${n}px solid` and
|
|
42
|
+
* otherwise passes the string through verbatim — so a palette token inside
|
|
43
|
+
* the shorthand would not resolve, and the panel's per-scheme colour scoping
|
|
44
|
+
* (`SCHEME_SCOPED_STYLE_FIELDS`) is keyed on `borderColor`. Folding the
|
|
45
|
+
* colour in here would have cost both. The group instead places Border
|
|
46
|
+
* Color immediately beside this control so the three choices read as one
|
|
47
|
+
* row.
|
|
48
|
+
*
|
|
49
|
+
* **The escape hatch is the same one `CssDimension` uses.** A value this
|
|
50
|
+
* editor cannot model — `thin solid`, `1px solid #f00`, `calc()` in a
|
|
51
|
+
* width, a `{{token}}` binding — falls back to a plain text box holding the
|
|
52
|
+
* raw string rather than being clobbered, and flips back to the structured
|
|
53
|
+
* controls the moment the text becomes a plain `<width> <style>` again. The
|
|
54
|
+
* mode is DERIVED from the value, never remembered in a flag that can go
|
|
55
|
+
* stale.
|
|
56
|
+
*/ /**
|
|
57
|
+
* The line styles offered.
|
|
58
|
+
*
|
|
59
|
+
* `none` is on the list and is a real value, not the absence of one: it
|
|
60
|
+
* removes a border a component or the theme is drawing, which is different
|
|
61
|
+
* from clearing the field (that hands the decision back to the theme). The
|
|
62
|
+
* long tail CSS also has — `groove`, `ridge`, `inset`, `outset` — is
|
|
63
|
+
* deliberately absent: they are bevel effects nobody has asked for, and an
|
|
64
|
+
* author who wants one still has the raw text box.
|
|
65
|
+
*/ export const CSS_BORDER_STYLES = [
|
|
66
|
+
'solid',
|
|
67
|
+
'dashed',
|
|
68
|
+
'dotted',
|
|
69
|
+
'double',
|
|
70
|
+
'none'
|
|
71
|
+
];
|
|
72
|
+
/** Plain-English name for each line style — no CSS vocabulary required. */ export const CSS_BORDER_STYLE_LABELS = {
|
|
73
|
+
solid: 'Solid line',
|
|
74
|
+
dashed: 'Dashed line',
|
|
75
|
+
dotted: 'Dotted line',
|
|
76
|
+
double: 'Double line',
|
|
77
|
+
none: 'No line'
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* What the CLOSED picker shows, which is not what the menu shows.
|
|
81
|
+
*
|
|
82
|
+
* The picker is an `endAdornment` inside a half-width field, so its width
|
|
83
|
+
* comes straight out of the box the author types the thickness into — and
|
|
84
|
+
* out of the room the field's own label needs. "Dashed line" collides with
|
|
85
|
+
* `Border Bottom`; "Dashed" does not. The menu, which has the whole popover
|
|
86
|
+
* to itself, keeps the sentence.
|
|
87
|
+
*/ export const CSS_BORDER_STYLE_SHORT_LABELS = {
|
|
88
|
+
solid: 'Solid',
|
|
89
|
+
dashed: 'Dashed',
|
|
90
|
+
dotted: 'Dotted',
|
|
91
|
+
double: 'Double',
|
|
92
|
+
none: 'None'
|
|
93
|
+
};
|
|
94
|
+
/** A bare pixel thickness: `1`, `1px`, `0.5px`. Units other than px are raw. */ const BORDER_WIDTH_PATTERN = /^([+-]?(?:\d+(?:\.\d+)?|\.\d+))(?:px)?$/i;
|
|
95
|
+
/**
|
|
96
|
+
* Reads a stored shorthand into the two controls.
|
|
97
|
+
*
|
|
98
|
+
* Accepts exactly the shapes this editor can put back together — a pixel
|
|
99
|
+
* width, a known style, or both in either CSS order — and marks everything
|
|
100
|
+
* else `custom` so the raw text survives untouched. `0` is a legitimate
|
|
101
|
+
* width (it is how an author removes a border while keeping its box), and
|
|
102
|
+
* `strictNullChecks` is off repo-wide, so emptiness is spelled out rather
|
|
103
|
+
* than left to a falsy test.
|
|
104
|
+
*/ export const seedBorderDraft = (value)=>{
|
|
105
|
+
// A bare NUMBER is what MUI's own `borderTransform` reads as `${n}px
|
|
106
|
+
// solid`, so the controls must show exactly that rather than blanking.
|
|
107
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
108
|
+
return {
|
|
109
|
+
width: `${value}`,
|
|
110
|
+
style: 'solid',
|
|
111
|
+
custom: false
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const text = typeof value === 'string' ? value.trim() : '';
|
|
115
|
+
if (text === '') return {
|
|
116
|
+
width: '',
|
|
117
|
+
style: '',
|
|
118
|
+
custom: false
|
|
119
|
+
};
|
|
120
|
+
const parts = text.split(/\s+/);
|
|
121
|
+
if (parts.length > 2) return {
|
|
122
|
+
width: text,
|
|
123
|
+
style: '',
|
|
124
|
+
custom: true
|
|
125
|
+
};
|
|
126
|
+
let width = '';
|
|
127
|
+
let style = '';
|
|
128
|
+
for (const part of parts){
|
|
129
|
+
const lower = part.toLowerCase();
|
|
130
|
+
if (!style && CSS_BORDER_STYLES.includes(lower)) {
|
|
131
|
+
style = lower;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const matched = BORDER_WIDTH_PATTERN.exec(part);
|
|
135
|
+
if (width === '' && matched) {
|
|
136
|
+
width = matched[1];
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
// Anything the pair cannot account for (a colour, `thin`, `2rem`)
|
|
140
|
+
// makes the whole value raw — a partial parse would silently drop it.
|
|
141
|
+
return {
|
|
142
|
+
width: text,
|
|
143
|
+
style: '',
|
|
144
|
+
custom: true
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
width,
|
|
149
|
+
style,
|
|
150
|
+
custom: false
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Serializes the two controls back into one shorthand.
|
|
155
|
+
*
|
|
156
|
+
* `none` is the whole value — a width in front of it is meaningless CSS and
|
|
157
|
+
* would read as a thickness the author cannot see the effect of.
|
|
158
|
+
*/ export const serializeBorderDraft = (draft)=>{
|
|
159
|
+
if (draft.custom) return draft.width;
|
|
160
|
+
if (draft.style === 'none') return 'none';
|
|
161
|
+
const width = draft.width.trim();
|
|
162
|
+
const parts = [];
|
|
163
|
+
if (width !== '') parts.push(`${width}px`);
|
|
164
|
+
if (draft.style) parts.push(draft.style);
|
|
165
|
+
return parts.join(' ');
|
|
166
|
+
};
|
|
167
|
+
export const CssBorderField = (props)=>{
|
|
168
|
+
var _input_value;
|
|
169
|
+
const _useFieldApi = useFieldApi(props), { input, isReadOnly, isDisabled, isRequired, label, placeholder, helperText, description, validateOnMount, meta, help, clearable, FormFieldGridProps = {}, // Free-text leftovers from the schema these fields were authored with
|
|
170
|
+
// as TEXT_FIELDs; they must never reach the DOM.
|
|
171
|
+
inputProps: _inputProps, InputProps: _InputProps, multiline: _multiline, type: _type, options: _options } = _useFieldApi, rest = _object_without_properties_loose(_useFieldApi, [
|
|
172
|
+
"input",
|
|
173
|
+
"isReadOnly",
|
|
174
|
+
"isDisabled",
|
|
175
|
+
"isRequired",
|
|
176
|
+
"label",
|
|
177
|
+
"placeholder",
|
|
178
|
+
"helperText",
|
|
179
|
+
"description",
|
|
180
|
+
"validateOnMount",
|
|
181
|
+
"meta",
|
|
182
|
+
"help",
|
|
183
|
+
"clearable",
|
|
184
|
+
"FormFieldGridProps",
|
|
185
|
+
"inputProps",
|
|
186
|
+
"InputProps",
|
|
187
|
+
"multiline",
|
|
188
|
+
"type",
|
|
189
|
+
"options"
|
|
190
|
+
]);
|
|
191
|
+
const invalid = validationError(meta, validateOnMount);
|
|
192
|
+
const value = typeof input.value === 'number' ? `${input.value}` : `${(_input_value = input.value) != null ? _input_value : ''}`;
|
|
193
|
+
const [draft, setDraft] = useState(()=>seedBorderDraft(input.value));
|
|
194
|
+
// Re-seed only when the value changed OUTSIDE this field (a different
|
|
195
|
+
// node selected, an undo, a breakpoint switch). Re-seeding from our own
|
|
196
|
+
// emits would round a half-typed decimal out from under the caret — the
|
|
197
|
+
// same hazard `CssDimension` documents.
|
|
198
|
+
const emittedRef = useRef(serializeBorderDraft(seedBorderDraft(input.value)));
|
|
199
|
+
useEffect(()=>{
|
|
200
|
+
var _input_value;
|
|
201
|
+
const incoming = `${(_input_value = input.value) != null ? _input_value : ''}`;
|
|
202
|
+
if (incoming === emittedRef.current) return;
|
|
203
|
+
emittedRef.current = incoming;
|
|
204
|
+
setDraft(seedBorderDraft(input.value));
|
|
205
|
+
}, [
|
|
206
|
+
input.value
|
|
207
|
+
]);
|
|
208
|
+
const commit = useCallback((next)=>{
|
|
209
|
+
const serialized = serializeBorderDraft(next);
|
|
210
|
+
emittedRef.current = serialized;
|
|
211
|
+
// A raw value the author edited back into a plain `<width> <style>`
|
|
212
|
+
// gets the structured controls back immediately, so the escape hatch
|
|
213
|
+
// is never a one-way door.
|
|
214
|
+
const reseeded = seedBorderDraft(serialized);
|
|
215
|
+
setDraft(next.custom && !reseeded.custom ? reseeded : next);
|
|
216
|
+
input.onChange(serialized);
|
|
217
|
+
}, [
|
|
218
|
+
input
|
|
219
|
+
]);
|
|
220
|
+
const clear = buildFieldClear({
|
|
221
|
+
clearable,
|
|
222
|
+
label,
|
|
223
|
+
hasValue: value !== '',
|
|
224
|
+
locked: Boolean(isDisabled || isReadOnly),
|
|
225
|
+
// Clearing drops the STYLE as well as the thickness. Emptying the
|
|
226
|
+
// number alone would leave `solid` selected and the next keystroke
|
|
227
|
+
// would silently re-adopt it, which is the trap AGL-2486 fixed for
|
|
228
|
+
// lengths and units.
|
|
229
|
+
onClear: ()=>commit({
|
|
230
|
+
width: '',
|
|
231
|
+
style: '',
|
|
232
|
+
custom: false
|
|
233
|
+
})
|
|
234
|
+
});
|
|
235
|
+
const handleWidthChange = useCallback((event)=>{
|
|
236
|
+
const width = event.target.value;
|
|
237
|
+
// Typing a thickness with no line style yet means a solid line —
|
|
238
|
+
// a bare `1px` draws nothing at all, which is the silent no-op that
|
|
239
|
+
// made the free-text version feel broken.
|
|
240
|
+
const style = !draft.custom && !draft.style && width.trim() !== '' ? 'solid' : draft.style;
|
|
241
|
+
commit(_extends({}, draft, {
|
|
242
|
+
width,
|
|
243
|
+
style
|
|
244
|
+
}));
|
|
245
|
+
}, [
|
|
246
|
+
commit,
|
|
247
|
+
draft
|
|
248
|
+
]);
|
|
249
|
+
const handleStyleChange = useCallback((event)=>{
|
|
250
|
+
const style = event.target.value || '';
|
|
251
|
+
// "No line" IS the whole value, so the thickness goes away with it
|
|
252
|
+
// and comes back when a drawn style is picked again.
|
|
253
|
+
commit(_extends({}, draft, {
|
|
254
|
+
style,
|
|
255
|
+
width: style === 'none' ? '' : draft.width
|
|
256
|
+
}));
|
|
257
|
+
}, [
|
|
258
|
+
commit,
|
|
259
|
+
draft
|
|
260
|
+
]);
|
|
261
|
+
const noLine = !draft.custom && draft.style === 'none';
|
|
262
|
+
const styleSelect = /*#__PURE__*/ _jsx(InputAdornment, {
|
|
263
|
+
position: "end",
|
|
264
|
+
sx: {
|
|
265
|
+
ml: 0
|
|
266
|
+
},
|
|
267
|
+
children: /*#__PURE__*/ _jsxs(Select, {
|
|
268
|
+
value: draft.custom ? '' : draft.style,
|
|
269
|
+
onChange: handleStyleChange,
|
|
270
|
+
disabled: isDisabled || isReadOnly || draft.custom,
|
|
271
|
+
variant: "standard",
|
|
272
|
+
disableUnderline: true,
|
|
273
|
+
// Without displayEmpty an unset style renders nothing at all, which
|
|
274
|
+
// reads as a broken control rather than "no line chosen yet".
|
|
275
|
+
displayEmpty: true,
|
|
276
|
+
renderValue: (selected)=>draft.custom ? 'custom' : selected ? CSS_BORDER_STYLE_SHORT_LABELS[selected] : // Picker on a length field. The words "line style" here read
|
|
277
|
+
// as help but behave as layout: they push the adornment over
|
|
278
|
+
// the field's own label on every per-side border.
|
|
279
|
+
'—',
|
|
280
|
+
inputProps: {
|
|
281
|
+
'aria-label': 'Line style'
|
|
282
|
+
},
|
|
283
|
+
sx: {
|
|
284
|
+
'& .MuiSelect-select': {
|
|
285
|
+
pr: '20px !important',
|
|
286
|
+
py: 0,
|
|
287
|
+
textOverflow: 'ellipsis'
|
|
288
|
+
},
|
|
289
|
+
fontSize: '0.8125rem',
|
|
290
|
+
color: 'text.secondary',
|
|
291
|
+
minWidth: 0,
|
|
292
|
+
maxWidth: 96
|
|
293
|
+
},
|
|
294
|
+
children: [
|
|
295
|
+
/*#__PURE__*/ _jsx(MenuItem, {
|
|
296
|
+
value: "",
|
|
297
|
+
children: /*#__PURE__*/ _jsx("em", {
|
|
298
|
+
children: 'not set'
|
|
299
|
+
})
|
|
300
|
+
}),
|
|
301
|
+
CSS_BORDER_STYLES.map((style)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
302
|
+
value: style,
|
|
303
|
+
children: /*#__PURE__*/ _jsxs(Box, {
|
|
304
|
+
sx: {
|
|
305
|
+
display: 'flex',
|
|
306
|
+
alignItems: 'center',
|
|
307
|
+
gap: 1,
|
|
308
|
+
width: '100%'
|
|
309
|
+
},
|
|
310
|
+
children: [
|
|
311
|
+
/*#__PURE__*/ _jsx(Box, {
|
|
312
|
+
"aria-hidden": true,
|
|
313
|
+
sx: {
|
|
314
|
+
width: 34,
|
|
315
|
+
flexShrink: 0,
|
|
316
|
+
borderTopWidth: style === 'double' ? 3 : 2,
|
|
317
|
+
borderTopStyle: style === 'none' ? 'solid' : style,
|
|
318
|
+
borderTopColor: style === 'none' ? 'transparent' : 'text.primary'
|
|
319
|
+
}
|
|
320
|
+
}),
|
|
321
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
322
|
+
variant: "body2",
|
|
323
|
+
noWrap: true,
|
|
324
|
+
children: CSS_BORDER_STYLE_LABELS[style]
|
|
325
|
+
})
|
|
326
|
+
]
|
|
327
|
+
})
|
|
328
|
+
}, style))
|
|
329
|
+
]
|
|
330
|
+
})
|
|
331
|
+
});
|
|
332
|
+
return /*#__PURE__*/ _jsx(FormFieldGrid, _extends({
|
|
333
|
+
help: help,
|
|
334
|
+
clear: clear
|
|
335
|
+
}, FormFieldGridProps, {
|
|
336
|
+
children: /*#__PURE__*/ _jsx(MuiTextField, _extends({}, rest, {
|
|
337
|
+
name: input.name,
|
|
338
|
+
value: noLine ? '' : draft.width,
|
|
339
|
+
onChange: handleWidthChange,
|
|
340
|
+
onBlur: input.onBlur,
|
|
341
|
+
onFocus: input.onFocus,
|
|
342
|
+
fullWidth: true,
|
|
343
|
+
error: !!invalid,
|
|
344
|
+
helperText: invalid || (meta.touched || validateOnMount) && meta.warning || helperText || description,
|
|
345
|
+
disabled: isDisabled,
|
|
346
|
+
label: label,
|
|
347
|
+
placeholder: noLine ? 'no line' : placeholder,
|
|
348
|
+
required: isRequired,
|
|
349
|
+
type: draft.custom || noLine ? 'text' : 'number',
|
|
350
|
+
slotProps: {
|
|
351
|
+
/**
|
|
352
|
+
* The label is ALWAYS pinned above the box, never floating
|
|
353
|
+
* inside it (AGL-2486).
|
|
354
|
+
*
|
|
355
|
+
* Two reasons, and they point the same way. `Border Bottom` is
|
|
356
|
+
* thirteen characters and the box is a half column with a
|
|
357
|
+
* picker parked in its right-hand end, so an un-shrunk label
|
|
358
|
+
* runs straight into the adornment and the two print on top of
|
|
359
|
+
* each other. And "No line" is shown through the PLACEHOLDER,
|
|
360
|
+
* which MUI hides
|
|
361
|
+
* behind CSS while the label is un-shrunk, so the field would
|
|
362
|
+
* print its label over an empty box on a node that demonstrably
|
|
363
|
+
* has a value.
|
|
364
|
+
*
|
|
365
|
+
* It also settles the group's rhythm: Border Color, Corner
|
|
366
|
+
* Radius and Shadow all sit under a pinned label, so the six
|
|
367
|
+
* border editors match the group they are in.
|
|
368
|
+
*/ inputLabel: {
|
|
369
|
+
shrink: true
|
|
370
|
+
},
|
|
371
|
+
input: {
|
|
372
|
+
readOnly: isReadOnly || noLine,
|
|
373
|
+
endAdornment: /*#__PURE__*/ _jsxs(_Fragment, {
|
|
374
|
+
children: [
|
|
375
|
+
draft.custom || noLine || draft.width.trim() === '' ? null : /*#__PURE__*/ _jsx(InputAdornment, {
|
|
376
|
+
position: "end",
|
|
377
|
+
sx: {
|
|
378
|
+
ml: 0,
|
|
379
|
+
mr: 0.5,
|
|
380
|
+
'& .MuiTypography-root': {
|
|
381
|
+
fontSize: '0.8125rem',
|
|
382
|
+
color: 'text.secondary'
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
children: 'px'
|
|
386
|
+
}),
|
|
387
|
+
styleSelect
|
|
388
|
+
]
|
|
389
|
+
})
|
|
390
|
+
},
|
|
391
|
+
htmlInput: {
|
|
392
|
+
inputMode: 'decimal',
|
|
393
|
+
min: 0,
|
|
394
|
+
step: 1,
|
|
395
|
+
// The VALUE is never the control that gets squeezed: the style
|
|
396
|
+
// picker is a fixed-width adornment sibling, so without a floor
|
|
397
|
+
// the number box is whatever is left in a narrow docked panel.
|
|
398
|
+
style: {
|
|
399
|
+
minWidth: '4.5ch'
|
|
400
|
+
},
|
|
401
|
+
'aria-label': typeof label === 'string' ? `${label} thickness` : undefined
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}))
|
|
405
|
+
}));
|
|
406
|
+
};
|
|
407
|
+
CssBorderField.displayName = 'AglynCssBorderField';
|
|
408
|
+
export default CssBorderField;
|
|
409
|
+
|
|
410
|
+
//# sourceMappingURL=css-border.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx-forms/src/lib/mapper/css-border.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 Box,\n InputAdornment,\n MenuItem,\n Select,\n TextField as MuiTextField,\n Typography,\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 { BaseFieldProps } from './types'\nimport { type ExtendedFieldMeta, validationError } from './validation-error'\n\n/**\n * CssBorder (AGL-2486): a thickness box plus a line-style picker for anything\n * holding a CSS border shorthand.\n *\n * The alternative is a raw shorthand text box — `Border` and the four per-side\n * fields each wanting `1px solid`, `Outline` wanting `2px solid`. A border is\n * two obvious choices (how thick, what kind of line) plus a colour, and a text\n * box turns them into a grammar exercise for an author who does not write CSS.\n * A mom-and-pop shop owner is one of Aglyn's three stated ICPs.\n *\n * **The persisted value is unchanged: still one CSS shorthand string**\n * (`\"1px solid\"`, `\"2px dashed\"`, `\"none\"`, `\"\"`). This is purely an input\n * affordance, so renderers, published documents and existing nodes stay\n * untouched, and it composes with the responsive/scheme pipeline exactly as\n * the free-text box did.\n *\n * **Colour deliberately stays its own field.** MUI declares `border` with\n * `borderTransform`, which only turns a bare NUMBER into `${n}px solid` and\n * otherwise passes the string through verbatim — so a palette token inside\n * the shorthand would not resolve, and the panel's per-scheme colour scoping\n * (`SCHEME_SCOPED_STYLE_FIELDS`) is keyed on `borderColor`. Folding the\n * colour in here would have cost both. The group instead places Border\n * Color immediately beside this control so the three choices read as one\n * row.\n *\n * **The escape hatch is the same one `CssDimension` uses.** A value this\n * editor cannot model — `thin solid`, `1px solid #f00`, `calc()` in a\n * width, a `{{token}}` binding — falls back to a plain text box holding the\n * raw string rather than being clobbered, and flips back to the structured\n * controls the moment the text becomes a plain `<width> <style>` again. The\n * mode is DERIVED from the value, never remembered in a flag that can go\n * stale.\n */\n\n/**\n * The line styles offered.\n *\n * `none` is on the list and is a real value, not the absence of one: it\n * removes a border a component or the theme is drawing, which is different\n * from clearing the field (that hands the decision back to the theme). The\n * long tail CSS also has — `groove`, `ridge`, `inset`, `outset` — is\n * deliberately absent: they are bevel effects nobody has asked for, and an\n * author who wants one still has the raw text box.\n */\nexport const CSS_BORDER_STYLES = [\n 'solid',\n 'dashed',\n 'dotted',\n 'double',\n 'none',\n] as const\n\nexport type CssBorderStyle = (typeof CSS_BORDER_STYLES)[number]\n\n/** Plain-English name for each line style — no CSS vocabulary required. */\nexport const CSS_BORDER_STYLE_LABELS: Record<CssBorderStyle, string> = {\n solid: 'Solid line',\n dashed: 'Dashed line',\n dotted: 'Dotted line',\n double: 'Double line',\n none: 'No line',\n}\n\n/**\n * What the CLOSED picker shows, which is not what the menu shows.\n *\n * The picker is an `endAdornment` inside a half-width field, so its width\n * comes straight out of the box the author types the thickness into — and\n * out of the room the field's own label needs. \"Dashed line\" collides with\n * `Border Bottom`; \"Dashed\" does not. The menu, which has the whole popover\n * to itself, keeps the sentence.\n */\nexport const CSS_BORDER_STYLE_SHORT_LABELS: Record<CssBorderStyle, string> = {\n solid: 'Solid',\n dashed: 'Dashed',\n dotted: 'Dotted',\n double: 'Double',\n none: 'None',\n}\n\n/** Draft state: what the two controls show, before serialization. */\nexport interface BorderDraft {\n /** Thickness in pixels as typed (or the whole raw string in custom mode). */\n width: string\n /** Selected line style, `''` when none is chosen. */\n style: CssBorderStyle | ''\n /** The value is not `<width>px <style>` — the text box holds it verbatim. */\n custom: boolean\n}\n\n/** A bare pixel thickness: `1`, `1px`, `0.5px`. Units other than px are raw. */\nconst BORDER_WIDTH_PATTERN = /^([+-]?(?:\\d+(?:\\.\\d+)?|\\.\\d+))(?:px)?$/i\n\n/**\n * Reads a stored shorthand into the two controls.\n *\n * Accepts exactly the shapes this editor can put back together — a pixel\n * width, a known style, or both in either CSS order — and marks everything\n * else `custom` so the raw text survives untouched. `0` is a legitimate\n * width (it is how an author removes a border while keeping its box), and\n * `strictNullChecks` is off repo-wide, so emptiness is spelled out rather\n * than left to a falsy test.\n */\nexport const seedBorderDraft = (value: unknown): BorderDraft => {\n // A bare NUMBER is what MUI's own `borderTransform` reads as `${n}px\n // solid`, so the controls must show exactly that rather than blanking.\n if (typeof value === 'number' && Number.isFinite(value)) {\n return { width: `${value}`, style: 'solid', custom: false }\n }\n const text = typeof value === 'string' ? value.trim() : ''\n if (text === '') return { width: '', style: '', custom: false }\n\n const parts = text.split(/\\s+/)\n if (parts.length > 2) return { width: text, style: '', custom: true }\n\n let width = ''\n let style: CssBorderStyle | '' = ''\n for (const part of parts) {\n const lower = part.toLowerCase()\n if (!style && (CSS_BORDER_STYLES as readonly string[]).includes(lower)) {\n style = lower as CssBorderStyle\n continue\n }\n const matched = BORDER_WIDTH_PATTERN.exec(part)\n if (width === '' && matched) {\n width = matched[1]\n continue\n }\n // Anything the pair cannot account for (a colour, `thin`, `2rem`)\n // makes the whole value raw — a partial parse would silently drop it.\n return { width: text, style: '', custom: true }\n }\n return { width, style, custom: false }\n}\n\n/**\n * Serializes the two controls back into one shorthand.\n *\n * `none` is the whole value — a width in front of it is meaningless CSS and\n * would read as a thickness the author cannot see the effect of.\n */\nexport const serializeBorderDraft = (draft: BorderDraft): string => {\n if (draft.custom) return draft.width\n if (draft.style === 'none') return 'none'\n const width = draft.width.trim()\n const parts: string[] = []\n if (width !== '') parts.push(`${width}px`)\n if (draft.style) parts.push(draft.style)\n return parts.join(' ')\n}\n\nexport interface CssBorderProps extends BaseFieldProps {\n placeholder?: string\n /** Offer the reset-to-unset affordance (AGL-2486). */\n clearable?: boolean\n FormFieldGridProps?: FormFieldGridProps\n}\n\nexport const CssBorderField = (props: CssBorderProps) => {\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 clearable,\n FormFieldGridProps = {},\n // Free-text leftovers from the schema these fields were authored with\n // as TEXT_FIELDs; they must never reach the DOM.\n inputProps: _inputProps,\n InputProps: _InputProps,\n multiline: _multiline,\n type: _type,\n options: _options,\n ...rest\n } = useFieldApi(props)\n\n const invalid = validationError(meta as ExtendedFieldMeta, validateOnMount)\n const value =\n typeof input.value === 'number' ? `${input.value}` : `${input.value ?? ''}`\n\n const [draft, setDraft] = useState<BorderDraft>(() =>\n seedBorderDraft(input.value),\n )\n // Re-seed only when the value changed OUTSIDE this field (a different\n // node selected, an undo, a breakpoint switch). Re-seeding from our own\n // emits would round a half-typed decimal out from under the caret — the\n // same hazard `CssDimension` documents.\n const emittedRef = useRef(serializeBorderDraft(seedBorderDraft(input.value)))\n useEffect(() => {\n const incoming = `${input.value ?? ''}`\n if (incoming === emittedRef.current) return\n emittedRef.current = incoming\n setDraft(seedBorderDraft(input.value))\n }, [input.value])\n\n const commit = useCallback(\n (next: BorderDraft) => {\n const serialized = serializeBorderDraft(next)\n emittedRef.current = serialized\n // A raw value the author edited back into a plain `<width> <style>`\n // gets the structured controls back immediately, so the escape hatch\n // is never a one-way door.\n const reseeded = seedBorderDraft(serialized)\n setDraft(next.custom && !reseeded.custom ? reseeded : next)\n input.onChange(serialized)\n },\n [input],\n )\n\n const clear = buildFieldClear({\n clearable,\n label,\n hasValue: value !== '',\n locked: Boolean(isDisabled || isReadOnly),\n // Clearing drops the STYLE as well as the thickness. Emptying the\n // number alone would leave `solid` selected and the next keystroke\n // would silently re-adopt it, which is the trap AGL-2486 fixed for\n // lengths and units.\n onClear: () => commit({ width: '', style: '', custom: false }),\n })\n\n const handleWidthChange = useCallback(\n (event: { target: { value: string } }) => {\n const width = event.target.value\n // Typing a thickness with no line style yet means a solid line —\n // a bare `1px` draws nothing at all, which is the silent no-op that\n // made the free-text version feel broken.\n const style =\n !draft.custom && !draft.style && width.trim() !== ''\n ? ('solid' as CssBorderStyle)\n : draft.style\n commit({ ...draft, width, style })\n },\n [commit, draft],\n )\n\n const handleStyleChange = useCallback(\n (event: { target: { value: unknown } }) => {\n const style = (event.target.value || '') as CssBorderStyle | ''\n // \"No line\" IS the whole value, so the thickness goes away with it\n // and comes back when a drawn style is picked again.\n commit({\n ...draft,\n style,\n width: style === 'none' ? '' : draft.width,\n })\n },\n [commit, draft],\n )\n\n const noLine = !draft.custom && draft.style === 'none'\n\n const styleSelect = (\n <InputAdornment position=\"end\" sx={{ ml: 0 }}>\n <Select\n value={draft.custom ? '' : draft.style}\n onChange={handleStyleChange}\n disabled={isDisabled || isReadOnly || draft.custom}\n variant=\"standard\"\n disableUnderline\n // Without displayEmpty an unset style renders nothing at all, which\n // reads as a broken control rather than \"no line chosen yet\".\n displayEmpty\n renderValue={(selected) =>\n draft.custom\n ? 'custom'\n : selected\n ? CSS_BORDER_STYLE_SHORT_LABELS[selected as CssBorderStyle]\n : // An unset picker is one em-dash wide, exactly like the unit\n // Picker on a length field. The words \"line style\" here read\n // as help but behave as layout: they push the adornment over\n // the field's own label on every per-side border.\n '—'\n }\n inputProps={{ 'aria-label': 'Line style' }}\n sx={{\n '& .MuiSelect-select': {\n pr: '20px !important',\n py: 0,\n textOverflow: 'ellipsis',\n },\n fontSize: '0.8125rem',\n color: 'text.secondary',\n minWidth: 0,\n maxWidth: 96,\n }}\n >\n <MenuItem value=\"\">\n <em>{'not set'}</em>\n </MenuItem>\n {CSS_BORDER_STYLES.map((style) => (\n <MenuItem key={style} value={style}>\n <Box\n sx={{\n display: 'flex',\n alignItems: 'center',\n gap: 1,\n width: '100%',\n }}\n >\n {/* The line itself, drawn in the style it names — the whole\n point of retiring the shorthand is that the choice can be\n made by looking. */}\n <Box\n aria-hidden\n sx={{\n width: 34,\n flexShrink: 0,\n borderTopWidth: style === 'double' ? 3 : 2,\n borderTopStyle: style === 'none' ? 'solid' : style,\n borderTopColor:\n style === 'none' ? 'transparent' : 'text.primary',\n }}\n />\n <Typography variant=\"body2\" noWrap>\n {CSS_BORDER_STYLE_LABELS[style]}\n </Typography>\n </Box>\n </MenuItem>\n ))}\n </Select>\n </InputAdornment>\n )\n\n return (\n <FormFieldGrid help={help} clear={clear} {...FormFieldGridProps}>\n <MuiTextField\n {...rest}\n name={input.name}\n value={noLine ? '' : draft.width}\n onChange={handleWidthChange}\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={noLine ? 'no line' : placeholder}\n required={isRequired}\n type={draft.custom || noLine ? 'text' : 'number'}\n slotProps={{\n /**\n * The label is ALWAYS pinned above the box, never floating\n * inside it (AGL-2486).\n *\n * Two reasons, and they point the same way. `Border Bottom` is\n * thirteen characters and the box is a half column with a\n * picker parked in its right-hand end, so an un-shrunk label\n * runs straight into the adornment and the two print on top of\n * each other. And \"No line\" is shown through the PLACEHOLDER,\n * which MUI hides\n * behind CSS while the label is un-shrunk, so the field would\n * print its label over an empty box on a node that demonstrably\n * has a value.\n *\n * It also settles the group's rhythm: Border Color, Corner\n * Radius and Shadow all sit under a pinned label, so the six\n * border editors match the group they are in.\n */\n inputLabel: { shrink: true },\n input: {\n readOnly: isReadOnly || noLine,\n endAdornment: (\n <>\n {/* The unit is STATED, not asked for — that is the whole\n point of retiring the shorthand. It is hidden in raw\n mode, where the text is not a pixel thickness, and on\n \"no line\", where there is no thickness at all. */}\n {/* The unit is only stated once there is a number to put it\n after. On an empty field it is pure width, and width is\n what pushes the label into the adornments. */}\n {draft.custom || noLine || draft.width.trim() === '' ? null : (\n <InputAdornment\n position=\"end\"\n sx={{\n ml: 0,\n mr: 0.5,\n '& .MuiTypography-root': {\n fontSize: '0.8125rem',\n color: 'text.secondary',\n },\n }}\n >\n {'px'}\n </InputAdornment>\n )}\n {styleSelect}\n </>\n ),\n },\n htmlInput: {\n inputMode: 'decimal',\n min: 0,\n step: 1,\n // The VALUE is never the control that gets squeezed: the style\n // picker is a fixed-width adornment sibling, so without a floor\n // the number box is whatever is left in a narrow docked panel.\n style: { minWidth: '4.5ch' },\n 'aria-label':\n typeof label === 'string' ? `${label} thickness` : undefined,\n },\n }}\n />\n </FormFieldGrid>\n )\n}\nCssBorderField.displayName = 'AglynCssBorderField'\n\nexport default CssBorderField\n"],"names":["Box","InputAdornment","MenuItem","Select","TextField","MuiTextField","Typography","useCallback","useEffect","useRef","useState","useFieldApi","FormFieldGrid","buildFieldClear","validationError","CSS_BORDER_STYLES","CSS_BORDER_STYLE_LABELS","solid","dashed","dotted","double","none","CSS_BORDER_STYLE_SHORT_LABELS","BORDER_WIDTH_PATTERN","seedBorderDraft","value","Number","isFinite","width","style","custom","text","trim","parts","split","length","part","lower","toLowerCase","includes","matched","exec","serializeBorderDraft","draft","push","join","CssBorderField","props","input","isReadOnly","isDisabled","isRequired","label","placeholder","helperText","description","validateOnMount","meta","help","clearable","FormFieldGridProps","inputProps","_inputProps","InputProps","_InputProps","multiline","_multiline","type","_type","options","_options","rest","invalid","setDraft","emittedRef","incoming","current","commit","next","serialized","reseeded","onChange","clear","hasValue","locked","Boolean","onClear","handleWidthChange","event","target","handleStyleChange","noLine","styleSelect","position","sx","ml","disabled","variant","disableUnderline","displayEmpty","renderValue","selected","pr","py","textOverflow","fontSize","color","minWidth","maxWidth","em","map","display","alignItems","gap","aria-hidden","flexShrink","borderTopWidth","borderTopStyle","borderTopColor","noWrap","name","onBlur","onFocus","fullWidth","error","touched","warning","required","slotProps","inputLabel","shrink","readOnly","endAdornment","mr","htmlInput","inputMode","min","step","undefined","displayName"],"mappings":"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"}
|