@godxjp/ui 18.12.15 → 18.12.17
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/dist/components/data-display/descriptions.d.ts +10 -1
- package/dist/components/data-display/descriptions.js +20 -4
- package/dist/components/data-entry/form-field.d.ts +1 -1
- package/dist/components/data-entry/form-field.js +12 -12
- package/dist/props/components/data-entry.prop.d.ts +33 -1
- package/dist/styles/form-layout.css +19 -1
- package/dist/tokens/components/descriptions.css +5 -0
- package/dist/tokens/components/form.css +12 -0
- package/package.json +2 -2
|
@@ -5,10 +5,19 @@ export interface DescriptionsProps {
|
|
|
5
5
|
columns?: 1 | 2 | 3;
|
|
6
6
|
/** Label placement within each item. Default `vertical` (label over value). */
|
|
7
7
|
layout?: DescriptionsLayoutProp;
|
|
8
|
+
/**
|
|
9
|
+
* Text alignment of each item's label within its label column — the SAME contract `Form`
|
|
10
|
+
* exposes (gh#294), so a `Descriptions` composed beside a `Form`/`FormField` (a read-only
|
|
11
|
+
* name/email block above an editable role field, for example) can be told to match it. Applies
|
|
12
|
+
* only in `layout="horizontal"` — a vertical label sits above the value and end-aligning it
|
|
13
|
+
* there would read as a mistake, exactly like `Form`'s own contract. Default `"start"`, matching
|
|
14
|
+
* this component's historical unconditional left-align — no existing consumer's render changes.
|
|
15
|
+
*/
|
|
16
|
+
labelAlign?: "start" | "end";
|
|
8
17
|
className?: string;
|
|
9
18
|
children: React.ReactNode;
|
|
10
19
|
}
|
|
11
|
-
export declare function Descriptions({ columns, layout, className, children, }: DescriptionsProps): React.JSX.Element;
|
|
20
|
+
export declare function Descriptions({ columns, layout, labelAlign, className, children, }: DescriptionsProps): React.JSX.Element;
|
|
12
21
|
export declare namespace Descriptions {
|
|
13
22
|
var Item: ({ label, mono, span, className, children, }: DescriptionsItemProps) => React.JSX.Element;
|
|
14
23
|
}
|
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { cn } from "../../lib/utils.js";
|
|
5
|
-
const DescriptionsLayoutContext = React.createContext(
|
|
5
|
+
const DescriptionsLayoutContext = React.createContext({
|
|
6
|
+
layout: "vertical",
|
|
7
|
+
labelAlign: "start"
|
|
8
|
+
});
|
|
6
9
|
function Descriptions({
|
|
7
10
|
columns = 2,
|
|
8
11
|
layout = "vertical",
|
|
12
|
+
labelAlign = "start",
|
|
9
13
|
className,
|
|
10
14
|
children
|
|
11
15
|
}) {
|
|
12
16
|
const colsClass = columns === 1 ? "grid-cols-1" : columns === 3 ? "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3" : "grid-cols-1 sm:grid-cols-2";
|
|
13
|
-
|
|
17
|
+
const context = React.useMemo(() => ({ layout, labelAlign }), [layout, labelAlign]);
|
|
18
|
+
return /* @__PURE__ */ jsx(DescriptionsLayoutContext.Provider, { value: context, children: /* @__PURE__ */ jsx("dl", { className: cn("grid gap-x-6 gap-y-[var(--descriptions-row-gap)]", colsClass, className), children }) });
|
|
14
19
|
}
|
|
15
20
|
Descriptions.Item = function DescriptionsItem({
|
|
16
21
|
label,
|
|
@@ -19,7 +24,7 @@ Descriptions.Item = function DescriptionsItem({
|
|
|
19
24
|
className,
|
|
20
25
|
children
|
|
21
26
|
}) {
|
|
22
|
-
const layout = React.useContext(DescriptionsLayoutContext);
|
|
27
|
+
const { layout, labelAlign } = React.useContext(DescriptionsLayoutContext);
|
|
23
28
|
const spanClass = span === 2 ? "sm:col-span-2" : span === 3 ? "sm:col-span-2 lg:col-span-3" : "";
|
|
24
29
|
return /* @__PURE__ */ jsxs(
|
|
25
30
|
"div",
|
|
@@ -38,7 +43,18 @@ Descriptions.Item = function DescriptionsItem({
|
|
|
38
43
|
className
|
|
39
44
|
),
|
|
40
45
|
children: [
|
|
41
|
-
/* @__PURE__ */ jsx(
|
|
46
|
+
/* @__PURE__ */ jsx(
|
|
47
|
+
"dt",
|
|
48
|
+
{
|
|
49
|
+
className: cn(
|
|
50
|
+
"text-muted-foreground text-xs",
|
|
51
|
+
// `end`-align only ever applies in horizontal layout — same guard `Form` uses, so a
|
|
52
|
+
// vertical label (already above its value) never mistakenly right-aligns (gh#294).
|
|
53
|
+
layout === "horizontal" && labelAlign === "end" && "text-end"
|
|
54
|
+
),
|
|
55
|
+
children: label
|
|
56
|
+
}
|
|
57
|
+
),
|
|
42
58
|
/* @__PURE__ */ jsx("dd", { className: cn("text-sm break-all", mono && "font-mono"), children })
|
|
43
59
|
]
|
|
44
60
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import type { FormFieldProp } from "../../props/components/data-entry.prop.js";
|
|
3
3
|
export type { FormFieldProp, FormFieldProp as FormFieldProps, } from "../../props/components/data-entry.prop.js";
|
|
4
|
-
export declare function FormField({ id, label, required, helper, error, labelAddon, layout: layoutProp, labelWidth: labelWidthProp, controlWidth: controlWidthProp, colSpan, className, children, }: FormFieldProp): React.JSX.Element;
|
|
4
|
+
export declare function FormField({ id, label, required, helper, error, labelAddon, layout: layoutProp, labelWidth: labelWidthProp, controlWidth: controlWidthProp, colSpan, className, children, staticText, }: FormFieldProp): React.JSX.Element;
|
|
@@ -19,7 +19,8 @@ function FormField({
|
|
|
19
19
|
controlWidth: controlWidthProp,
|
|
20
20
|
colSpan,
|
|
21
21
|
className,
|
|
22
|
-
children
|
|
22
|
+
children,
|
|
23
|
+
staticText
|
|
23
24
|
}) {
|
|
24
25
|
const form = useFormLayout();
|
|
25
26
|
const layout = layoutProp ?? form?.layout ?? "vertical";
|
|
@@ -32,14 +33,19 @@ function FormField({
|
|
|
32
33
|
const labelId = `${resolvedId}-label`;
|
|
33
34
|
const helperId = helper ? `${resolvedId}-helper` : void 0;
|
|
34
35
|
const errorId = error ? `${resolvedId}-error` : void 0;
|
|
35
|
-
|
|
36
|
+
const isStatic = staticText !== void 0;
|
|
37
|
+
if (!isStatic && typeof process !== "undefined" && process.env?.NODE_ENV !== "production" && !React.isValidElement(children)) {
|
|
36
38
|
console.warn(
|
|
37
|
-
"FormField expects a single React element child to receive aria-describedby/aria-errormessage; the helper text and error message will not be associated with the control."
|
|
39
|
+
"FormField expects a single React element child to receive aria-describedby/aria-errormessage; the helper text and error message will not be associated with the control. Pass plain text via `staticText` instead of `children` for a read-only value row."
|
|
38
40
|
);
|
|
39
41
|
}
|
|
40
42
|
const childProps = React.isValidElement(children) ? children.props : void 0;
|
|
41
43
|
const mergeIds = mergeAriaIds;
|
|
42
|
-
const childWithA11y =
|
|
44
|
+
const childWithA11y = isStatic ? (
|
|
45
|
+
// Byte-for-byte the same value typography as `Descriptions.Item`'s `dd` (gh#294), so a
|
|
46
|
+
// read-only FormField row and a Descriptions value are indistinguishable when mixed.
|
|
47
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm break-all", children: staticText })
|
|
48
|
+
) : React.isValidElement(children) ? React.cloneElement(children, {
|
|
43
49
|
// The label is associated via aria-labelledby (not <label for>): composite
|
|
44
50
|
// controls (Radio.Group, checkbox lists, range pairs) have no labelable root,
|
|
45
51
|
// and a dangling `for` triggers Chrome's "Incorrect use of <label>" issue.
|
|
@@ -52,14 +58,8 @@ function FormField({
|
|
|
52
58
|
"aria-label": childProps?.["aria-label"] ?? (typeof label === "string" ? label : void 0),
|
|
53
59
|
// Helper and error can coexist: helper stays on aria-describedby, the error on
|
|
54
60
|
// aria-errormessage (surfaced when aria-invalid is true).
|
|
55
|
-
"aria-describedby": mergeIds(
|
|
56
|
-
|
|
57
|
-
helperId
|
|
58
|
-
),
|
|
59
|
-
"aria-errormessage": mergeIds(
|
|
60
|
-
childProps?.["aria-errormessage"],
|
|
61
|
-
errorId
|
|
62
|
-
),
|
|
61
|
+
"aria-describedby": mergeIds(childProps?.["aria-describedby"], helperId),
|
|
62
|
+
"aria-errormessage": mergeIds(childProps?.["aria-errormessage"], errorId),
|
|
63
63
|
"aria-required": required ? true : childProps?.["aria-required"],
|
|
64
64
|
"aria-invalid": error ? true : childProps?.["aria-invalid"]
|
|
65
65
|
}) : children;
|
|
@@ -90,7 +90,17 @@ export type FormProp = React.FormHTMLAttributes<HTMLFormElement> & {
|
|
|
90
90
|
density?: DensityProp;
|
|
91
91
|
className?: ClassNameProp;
|
|
92
92
|
};
|
|
93
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* @see FormField — exactly one of `children` (an interactive control) or `staticText` (gh#294):
|
|
95
|
+
* a read-only VALUE row inside the same Form, styled to match `Descriptions.Item`'s value
|
|
96
|
+
* typography (`text-sm break-all`) byte-for-byte. This is the "mixed read-only + editable fields
|
|
97
|
+
* on one form" case (an immutable name/email row above an editable role Select, for example) —
|
|
98
|
+
* putting the read-only rows through FormField itself, not a separate `Descriptions` composed
|
|
99
|
+
* alongside it, gets perfect layout/labelAlign/row-gap sync FOR FREE because it IS the same
|
|
100
|
+
* component reading the same Form context, rather than two components whose contracts need
|
|
101
|
+
* reconciling. `staticText` skips FormField's control a11y wiring (id/aria-labelledby/
|
|
102
|
+
* aria-describedby cloning) entirely — there is no real control to label, so none of that applies.
|
|
103
|
+
*/
|
|
94
104
|
export type FormFieldProp = {
|
|
95
105
|
/** Optional — auto-generated and injected into the child control when omitted. */
|
|
96
106
|
id?: IdProp;
|
|
@@ -110,6 +120,28 @@ export type FormFieldProp = {
|
|
|
110
120
|
colSpan?: number;
|
|
111
121
|
className?: ClassNameProp;
|
|
112
122
|
children: React.ReactNode;
|
|
123
|
+
staticText?: never;
|
|
124
|
+
} | {
|
|
125
|
+
/** Optional — auto-generated and injected into the child control when omitted. */
|
|
126
|
+
id?: IdProp;
|
|
127
|
+
label: LabelProp;
|
|
128
|
+
required?: RequiredProp;
|
|
129
|
+
helper?: HelperProp;
|
|
130
|
+
error?: ErrorProp;
|
|
131
|
+
/** Optional control rendered inline after the label (e.g. a help button). */
|
|
132
|
+
labelAddon?: React.ReactNode;
|
|
133
|
+
/** Override the Form's layout for this field only. */
|
|
134
|
+
layout?: FormLayoutProp;
|
|
135
|
+
/** Override the Form's label width for this field (horizontal layout). */
|
|
136
|
+
labelWidth?: WidthProp;
|
|
137
|
+
/** Override the Form's control width for this field. */
|
|
138
|
+
controlWidth?: WidthProp;
|
|
139
|
+
/** Span N columns when inside a `columns` Form grid. */
|
|
140
|
+
colSpan?: number;
|
|
141
|
+
className?: ClassNameProp;
|
|
142
|
+
children?: never;
|
|
143
|
+
/** Read-only value — renders as `Descriptions.Item`-matched text instead of a control. */
|
|
144
|
+
staticText: React.ReactNode;
|
|
113
145
|
};
|
|
114
146
|
/** @see SearchInput */
|
|
115
147
|
export type SearchInputProp = FieldA11yProps & {
|
|
@@ -12,7 +12,25 @@
|
|
|
12
12
|
.ui-form {
|
|
13
13
|
display: flex;
|
|
14
14
|
flex-direction: column;
|
|
15
|
-
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* Row rhythm between a Form's own top-level blocks (gh#295) — e.g. the field group's
|
|
18
|
+
* CardContent and its CardFooter action row. MARGIN, not flex `gap`: `gap` only reaches DIRECT
|
|
19
|
+
* children, which is dead the moment a consumer needs a CardFooter Save button to actually
|
|
20
|
+
* submit (it must stay a `<form>` descendant, so Form has to wrap CardContent — the FormFields
|
|
21
|
+
* become grandchildren, past flex `gap`'s reach). Margin-based sibling spacing works at any
|
|
22
|
+
* depth as long as the elements are literal siblings. */
|
|
23
|
+
.ui-form > * + * {
|
|
24
|
+
margin-block-start: var(--form-block-gap);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* Field-to-field row rhythm (gh#295) — MORE SPECIFIC than the rule above (two class selectors
|
|
28
|
+
* beat one), so it wins rather than stacking with it whenever two FormFields are adjacent
|
|
29
|
+
* siblings: Form's own direct children (a plain field list, no Card framing) or nested one
|
|
30
|
+
* level through CardContent (the common Card-framed, Save-button-capable case). This is what
|
|
31
|
+
* makes the rhythm hold regardless of DOM depth — the actual fix for gh#295. */
|
|
32
|
+
.ui-form-field + .ui-form-field {
|
|
33
|
+
margin-block-start: var(--form-field-row-gap);
|
|
16
34
|
}
|
|
17
35
|
|
|
18
36
|
/* Vertical (default + collapsed state): label stacked above the control column, LEFT-aligned.
|
|
@@ -6,4 +6,9 @@
|
|
|
6
6
|
* A rem value gives a fixed aligned column; set `max-content` to size each label to its text.
|
|
7
7
|
* (rule #44/#45 — a service theme tunes it here instead of forking CSS.) */
|
|
8
8
|
--descriptions-label-width: 8rem;
|
|
9
|
+
/* Row-to-row gap (gh#294). Default = the historical hardcoded `gap-y-3`, so nothing changes
|
|
10
|
+
* visually by default. A consumer composing Descriptions beside a Form/FormField (a read-only
|
|
11
|
+
* block above an editable field on the same card) retunes this to `var(--space-4)` to match
|
|
12
|
+
* Form's own field-to-field rhythm instead of the two blocks reading as visually unrelated. */
|
|
13
|
+
--descriptions-row-gap: var(--space-3);
|
|
9
14
|
}
|
|
@@ -12,4 +12,16 @@
|
|
|
12
12
|
|
|
13
13
|
/* Column gap between the label and its control in horizontal/inline layout. */
|
|
14
14
|
--form-label-gap: var(--space-4); /* 16px */
|
|
15
|
+
|
|
16
|
+
/* Row rhythm between a Form's own top-level blocks (gh#295) — e.g. the field group's
|
|
17
|
+
* CardContent and its CardFooter action row, or two FormFields that happen to be Form's OWN
|
|
18
|
+
* direct children (no Card framing). Unchanged from the historical value. */
|
|
19
|
+
--form-block-gap: var(--space-4); /* 16px */
|
|
20
|
+
|
|
21
|
+
/* Field-to-field row rhythm (gh#295) — mirrors --descriptions-row-gap so a read-only value
|
|
22
|
+
* mixed in via `FormField.staticText` (gh#294) and a real Descriptions block share ONE canonical
|
|
23
|
+
* rhythm. Applied via margin-based sibling spacing on FormField itself (not Form's flex `gap`,
|
|
24
|
+
* which only reaches DIRECT children — dead in the common `Form > CardContent > FormField*`
|
|
25
|
+
* composition every real Save-button form needs), so it holds at any DOM depth relative to Form. */
|
|
26
|
+
--form-field-row-gap: var(--space-3); /* 12px */
|
|
15
27
|
}
|