@godxjp/ui 18.12.14 → 18.12.16
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-display/table.d.ts +34 -7
- package/dist/components/data-display/table.js +9 -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/props/vocabulary/data.prop.d.ts +8 -1
- package/dist/styles/shell-layout.css +48 -25
- package/dist/styles/table-layout.css +206 -1
- package/dist/tokens/components/descriptions.css +5 -0
- package/dist/tokens/components/shell.css +13 -0
- package/dist/tokens/components/table.css +12 -0
- package/dist/tokens/foundation.css +5 -2
- 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
|
}
|
|
@@ -32,12 +32,20 @@ export type TableProps = React.HTMLAttributes<HTMLTableElement> & {
|
|
|
32
32
|
* its rem floor (`--table-action-collection-*-width-floor`, ~5 CJK glyphs per line) and the
|
|
33
33
|
* table intentionally scrolls inside this wrapper — SC 1.4.10 permits one-dimensional scrolling
|
|
34
34
|
* of a data table; compressing further would shred CJK headers one character per line.
|
|
35
|
+
*
|
|
36
|
+
* `"stacked-record-collection"` (gh#293 restore — SCR-215) is for a wide, heterogeneous record
|
|
37
|
+
* table that CANNOT be squeezed into any fixed narrow-frame measure — an admin detail row with
|
|
38
|
+
* many disparate fields, not a dense queue. Below `collapseBelow` the `<thead>` hides and every
|
|
39
|
+
* `<tr>` becomes a bordered key-value card: each `TableCell`'s own `label` prop (its column's
|
|
40
|
+
* header text, repeated per cell since `<thead>` is gone) renders inline above the value. Above
|
|
41
|
+
* the step it renders as a byte-for-byte ordinary table — mark `label` on every `TableCell`.
|
|
35
42
|
*/
|
|
36
43
|
preset?: TablePresetProp;
|
|
37
44
|
/**
|
|
38
|
-
* Step at which `preset="action-collection"`
|
|
39
|
-
* the TABLE's own container (not the viewport), so a table
|
|
40
|
-
* page does. Defaults to `"sm"` (40rem). Ignored while
|
|
45
|
+
* Step at which `preset="action-collection"`/`"stacked-record-collection"` switch away from the
|
|
46
|
+
* ordinary table, measured against the TABLE's own container (not the viewport), so a table
|
|
47
|
+
* inside a rail collapses before the page does. Defaults to `"sm"` (40rem). Ignored while
|
|
48
|
+
* `preset` is `"default"`.
|
|
41
49
|
*/
|
|
42
50
|
collapseBelow?: BreakpointProp;
|
|
43
51
|
};
|
|
@@ -73,12 +81,20 @@ export declare const Table: React.ForwardRefExoticComponent<React.HTMLAttributes
|
|
|
73
81
|
* its rem floor (`--table-action-collection-*-width-floor`, ~5 CJK glyphs per line) and the
|
|
74
82
|
* table intentionally scrolls inside this wrapper — SC 1.4.10 permits one-dimensional scrolling
|
|
75
83
|
* of a data table; compressing further would shred CJK headers one character per line.
|
|
84
|
+
*
|
|
85
|
+
* `"stacked-record-collection"` (gh#293 restore — SCR-215) is for a wide, heterogeneous record
|
|
86
|
+
* table that CANNOT be squeezed into any fixed narrow-frame measure — an admin detail row with
|
|
87
|
+
* many disparate fields, not a dense queue. Below `collapseBelow` the `<thead>` hides and every
|
|
88
|
+
* `<tr>` becomes a bordered key-value card: each `TableCell`'s own `label` prop (its column's
|
|
89
|
+
* header text, repeated per cell since `<thead>` is gone) renders inline above the value. Above
|
|
90
|
+
* the step it renders as a byte-for-byte ordinary table — mark `label` on every `TableCell`.
|
|
76
91
|
*/
|
|
77
92
|
preset?: TablePresetProp;
|
|
78
93
|
/**
|
|
79
|
-
* Step at which `preset="action-collection"`
|
|
80
|
-
* the TABLE's own container (not the viewport), so a table
|
|
81
|
-
* page does. Defaults to `"sm"` (40rem). Ignored while
|
|
94
|
+
* Step at which `preset="action-collection"`/`"stacked-record-collection"` switch away from the
|
|
95
|
+
* ordinary table, measured against the TABLE's own container (not the viewport), so a table
|
|
96
|
+
* inside a rail collapses before the page does. Defaults to `"sm"` (40rem). Ignored while
|
|
97
|
+
* `preset` is `"default"`.
|
|
82
98
|
*/
|
|
83
99
|
collapseBelow?: BreakpointProp;
|
|
84
100
|
} & React.RefAttributes<HTMLTableElement>>;
|
|
@@ -94,6 +110,17 @@ export declare const TableRow: React.ForwardRefExoticComponent<React.HTMLAttribu
|
|
|
94
110
|
type TableCellPriority = {
|
|
95
111
|
priority?: TableColumnPriorityProp;
|
|
96
112
|
};
|
|
113
|
+
/**
|
|
114
|
+
* A `TableCell`'s own column label, read only by `Table preset="stacked-record-collection"`
|
|
115
|
+
* below its collapse step — where the real `<thead>` hides and this renders inline above the
|
|
116
|
+
* cell's value inside the stacked card, standing in for the header association that is no longer
|
|
117
|
+
* visually present. Rendered into the DOM unconditionally (so it exists for the preset's CSS to
|
|
118
|
+
* reveal) but visually hidden above the collapse step, where the real `<th>` already carries the
|
|
119
|
+
* label — an ordinary table with no `label` prop supplied gains no extra markup.
|
|
120
|
+
*/
|
|
121
|
+
type TableCellLabel = {
|
|
122
|
+
label?: React.ReactNode;
|
|
123
|
+
};
|
|
97
124
|
export declare const TableHead: React.ForwardRefExoticComponent<React.ThHTMLAttributes<HTMLTableCellElement> & TableCellPriority & React.RefAttributes<HTMLTableCellElement>>;
|
|
98
|
-
export declare const TableCell: React.ForwardRefExoticComponent<React.TdHTMLAttributes<HTMLTableCellElement> & TableCellPriority & React.RefAttributes<HTMLTableCellElement>>;
|
|
125
|
+
export declare const TableCell: React.ForwardRefExoticComponent<React.TdHTMLAttributes<HTMLTableCellElement> & TableCellPriority & TableCellLabel & React.RefAttributes<HTMLTableCellElement>>;
|
|
99
126
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { tableHeadHeightClass } from "../../lib/control-styles.js";
|
|
4
4
|
import { cn } from "../../lib/utils.js";
|
|
@@ -23,7 +23,8 @@ const Table = React.forwardRef(
|
|
|
23
23
|
{
|
|
24
24
|
className: cn(
|
|
25
25
|
scrollable ? "relative w-full overflow-auto" : "relative w-full",
|
|
26
|
-
preset
|
|
26
|
+
preset === "action-collection" && "ui-table-collection",
|
|
27
|
+
preset === "stacked-record-collection" && "ui-table-stacked-collection"
|
|
27
28
|
),
|
|
28
29
|
"data-preset": preset === "default" ? void 0 : preset,
|
|
29
30
|
"data-collapse-below": preset === "default" ? void 0 : collapseBelow,
|
|
@@ -69,14 +70,18 @@ const TableHead = React.forwardRef(({ className, priority, ...props }, ref) => /
|
|
|
69
70
|
}
|
|
70
71
|
));
|
|
71
72
|
TableHead.displayName = "TableHead";
|
|
72
|
-
const TableCell = React.forwardRef(({ className, priority, ...props }, ref) => /* @__PURE__ */
|
|
73
|
+
const TableCell = React.forwardRef(({ className, priority, label, children, ...props }, ref) => /* @__PURE__ */ jsxs(
|
|
73
74
|
"td",
|
|
74
75
|
{
|
|
75
76
|
ref,
|
|
76
77
|
"data-slot": "table-cell",
|
|
77
78
|
"data-priority": priority,
|
|
78
79
|
className: cn(className),
|
|
79
|
-
...props
|
|
80
|
+
...props,
|
|
81
|
+
children: [
|
|
82
|
+
label !== void 0 ? /* @__PURE__ */ jsx("span", { className: "ui-table-stacked-collection-label", "aria-hidden": "true", children: label }) : null,
|
|
83
|
+
children
|
|
84
|
+
]
|
|
80
85
|
}
|
|
81
86
|
));
|
|
82
87
|
TableCell.displayName = "TableCell";
|
|
@@ -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 & {
|
|
@@ -81,8 +81,15 @@ export type StickyProp = boolean;
|
|
|
81
81
|
* the desktop intrinsic column widths are replaced by token-owned column-PRIORITY measures under
|
|
82
82
|
* `table-layout: fixed` and cells wrap, so every column — including the row actions — stays inside
|
|
83
83
|
* the initial narrow frame. The real `<table>` semantics are never rewritten.
|
|
84
|
+
*
|
|
85
|
+
* `"stacked-record-collection"` (gh#293 restore — SCR-215) is the canonical wide-record collection:
|
|
86
|
+
* a table whose column count/content cannot be squeezed into any fixed-measure narrow frame at all
|
|
87
|
+
* (an admin detail row with many heterogeneous fields). Below `collapseBelow` the header row hides
|
|
88
|
+
* and every `<tr>` becomes a bordered key-value CARD stack — each `TableCell`'s own `label` prop
|
|
89
|
+
* renders inline above its value, taking over the accessible-name role the (now hidden) `<th>`
|
|
90
|
+
* association would otherwise carry. Above the step it is a byte-for-byte ordinary table.
|
|
84
91
|
*/
|
|
85
|
-
export type TablePresetProp = "default" | "action-collection";
|
|
92
|
+
export type TablePresetProp = "default" | "action-collection" | "stacked-record-collection";
|
|
86
93
|
/**
|
|
87
94
|
* Relative importance of a table column, used by `preset="action-collection"` to allocate the
|
|
88
95
|
* narrow-frame measure. `"primary"` is the row's subject, `"secondary"` its object/target,
|
|
@@ -32,7 +32,14 @@
|
|
|
32
32
|
min-width: 0;
|
|
33
33
|
min-height: 0;
|
|
34
34
|
flex-direction: column;
|
|
35
|
-
|
|
35
|
+
/* Same ring-headroom contract as the topbar family (gh#291): nav rows are
|
|
36
|
+
* flush with the rail's inline edges, so a plain `hidden` clipped their
|
|
37
|
+
* focus ring on three sides. `clip` + margin (the margin is honoured only
|
|
38
|
+
* when BOTH axes clip) lets the ring paint over the rail border, while the
|
|
39
|
+
* rail still contains its content. Inner scroll regions are unaffected —
|
|
40
|
+
* `clip` creates no scroll container, exactly like `hidden`. */
|
|
41
|
+
overflow: clip;
|
|
42
|
+
overflow-clip-margin: var(--focus-ring-clip-margin);
|
|
36
43
|
border-inline-end: 1px solid hsl(var(--border));
|
|
37
44
|
background: hsl(var(--card));
|
|
38
45
|
/* Opt-in brand-chrome wash — invisible by default (--sidebar-gradient: none). */
|
|
@@ -47,7 +54,15 @@
|
|
|
47
54
|
* set to baseline; --scaling:1 alone is not enough — see foundation.css). */
|
|
48
55
|
display: flex;
|
|
49
56
|
min-width: 0;
|
|
50
|
-
|
|
57
|
+
/* Same both-axes-clip + margin contract as .ui-topbar (gh#291): Chromium
|
|
58
|
+
* honours overflow-clip-margin ONLY when BOTH axes are `clip` — a
|
|
59
|
+
* single-axis clip silently drops the margin, which would cut a flush
|
|
60
|
+
* control's ring whenever a theme sets --app-shell-bar-inset to 0. The 8px
|
|
61
|
+
* margin also covers the vertical ring of coarse-pointer 44px controls,
|
|
62
|
+
* which the 3.5rem coarse bar height already keeps inside the bar. Safari
|
|
63
|
+
* falls back to `visible` with the .ui-topbar family below. */
|
|
64
|
+
overflow: clip;
|
|
65
|
+
overflow-clip-margin: var(--focus-ring-clip-margin);
|
|
51
66
|
align-items: center;
|
|
52
67
|
gap: var(--app-shell-bar-gap);
|
|
53
68
|
padding-inline: var(--app-shell-bar-inset);
|
|
@@ -1271,18 +1286,18 @@
|
|
|
1271
1286
|
align-items: center;
|
|
1272
1287
|
gap: var(--topbar-gap);
|
|
1273
1288
|
padding-inline: var(--topbar-inset);
|
|
1274
|
-
/*
|
|
1275
|
-
*
|
|
1276
|
-
*
|
|
1277
|
-
*
|
|
1278
|
-
*
|
|
1279
|
-
* `auto
|
|
1280
|
-
*
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
*
|
|
1285
|
-
|
|
1289
|
+
/* Ring headroom contract (gh#291), measured empirically:
|
|
1290
|
+
* - Chromium honours overflow-clip-margin ONLY when BOTH axes are `clip`;
|
|
1291
|
+
* a single-axis clip silently ignores the margin, and a flush-edge
|
|
1292
|
+
* control's ring vanishes on the clipped axis.
|
|
1293
|
+
* - `hidden` can never work here (no clip-margin, and pairing it with
|
|
1294
|
+
* `visible` computes to `auto`, which still clips).
|
|
1295
|
+
* - The margin must be a bare var() — Chromium drops calc() at parse time.
|
|
1296
|
+
* So: clip BOTH axes and let the margin carry the ring on every side. The
|
|
1297
|
+
* gh#226 shrink/truncation contract is intact (content clips 8px outside
|
|
1298
|
+
* the box). Safari (clip yes, clip-margin no) falls back to `visible`
|
|
1299
|
+
* below. */
|
|
1300
|
+
overflow: clip;
|
|
1286
1301
|
overflow-clip-margin: var(--focus-ring-clip-margin);
|
|
1287
1302
|
}
|
|
1288
1303
|
.ui-topbar-start,
|
|
@@ -1292,19 +1307,27 @@
|
|
|
1292
1307
|
min-width: 0;
|
|
1293
1308
|
align-items: center;
|
|
1294
1309
|
gap: var(--topbar-gap);
|
|
1295
|
-
/*
|
|
1296
|
-
*
|
|
1297
|
-
*
|
|
1298
|
-
|
|
1299
|
-
* ring headroom where supported. */
|
|
1300
|
-
overflow-x: clip;
|
|
1301
|
-
overflow-y: visible;
|
|
1302
|
-
/* 4px headroom, NOT the 2px ring token: focus/open rings paint up to 3px,
|
|
1303
|
-
* so 1x shaved a flush-edge control's ring (the locale picker is the first
|
|
1304
|
-
* child of `end`). Bare var() only — Chromium drops any calc() in
|
|
1305
|
-
* overflow-clip-margin at parse time, degrading the margin to 0. */
|
|
1310
|
+
/* Same both-axes-clip + margin contract as .ui-topbar above (gh#291): the
|
|
1311
|
+
* 8px margin covers the 3px ring on every side of a flush-edge control;
|
|
1312
|
+
* single-axis clip would silently drop the margin in Chromium. */
|
|
1313
|
+
overflow: clip;
|
|
1306
1314
|
overflow-clip-margin: var(--focus-ring-clip-margin);
|
|
1307
1315
|
}
|
|
1316
|
+
/* Safari implements `overflow: clip` but NOT overflow-clip-margin, so the
|
|
1317
|
+
* both-axes clip above would eat every flush-edge focus ring there with no
|
|
1318
|
+
* margin to save it. Trade slot containment for ring visibility on that
|
|
1319
|
+
* engine only — the .app-topbar horizontal clip still prevents bar blowout
|
|
1320
|
+
* and document scroll. */
|
|
1321
|
+
@supports not (overflow-clip-margin: 1px) {
|
|
1322
|
+
.app-sidebar,
|
|
1323
|
+
.app-topbar,
|
|
1324
|
+
.ui-topbar,
|
|
1325
|
+
.ui-topbar-start,
|
|
1326
|
+
.ui-topbar-center,
|
|
1327
|
+
.ui-topbar-end {
|
|
1328
|
+
overflow: visible;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1308
1331
|
.ui-topbar-start {
|
|
1309
1332
|
flex: 0 1 auto;
|
|
1310
1333
|
}
|
|
@@ -119,7 +119,14 @@
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
.ui-data-table-surface {
|
|
122
|
-
|
|
122
|
+
/* `clip`, NOT `hidden` (gh#291 family): both clip the table to the rounded
|
|
123
|
+
* border, but `hidden` makes this box a SCROLL CONTAINER, which silently
|
|
124
|
+
* captured the sticky context of `stickyHeader` (default true) and of the
|
|
125
|
+
* pinned columns — they anchored to a box that never scrolls, so the header
|
|
126
|
+
* scrolled away with the body inside `.ui-data-table-scroll`. `clip` is not
|
|
127
|
+
* a scroll container, so sticky binds to the real scroll region and works.
|
|
128
|
+
* Measured: hidden → thead moved 300px per 300px scrolled; clip → 1px. */
|
|
129
|
+
overflow: clip;
|
|
123
130
|
/* Narrow-viewport legibility floor — a TOKEN, not the old `min-w-[640px]` literal (gh#253,
|
|
124
131
|
* rule #45). Cleared at the `sm` step below, and opted out of entirely by the collection
|
|
125
132
|
* preset, which owns its width through the column-priority measures. */
|
|
@@ -363,6 +370,29 @@
|
|
|
363
370
|
}
|
|
364
371
|
}
|
|
365
372
|
|
|
373
|
+
@layer components {
|
|
374
|
+
/* ── Table · `preset="stacked-record-collection"` (gh#293 restore — SCR-215) ─────────────────
|
|
375
|
+
* The canonical WIDE, heterogeneous record collection: a table whose column count/content
|
|
376
|
+
* cannot be squeezed into any fixed narrow-frame measure at all — an admin detail row with many
|
|
377
|
+
* disparate, free-text fields of unpredictable length, not a dense queue (contrast
|
|
378
|
+
* `action-collection` above, which keeps the real table and only re-sizes columns). Below
|
|
379
|
+
* `collapseBelow` the `<thead>` hides and every `<tr>` becomes a bordered key-value CARD: each
|
|
380
|
+
* `TableCell`'s own `label` (its column header text, carried per-cell since the `<th>`
|
|
381
|
+
* association is now hidden) renders inline above the value, taking over the accessible-name
|
|
382
|
+
* role. Above the step it is a byte-for-byte ordinary table. Measured against the TABLE'S OWN
|
|
383
|
+
* container, not the viewport, same as `action-collection`. */
|
|
384
|
+
.ui-table-stacked-collection {
|
|
385
|
+
container: ui-table-stacked-collection / inline-size;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/* Rendered into the DOM unconditionally so it exists for the container query below to reveal
|
|
389
|
+
* (see TableCell in table.tsx) but hidden above the collapse step, where the real `<th>` already
|
|
390
|
+
* carries the label — an ordinary table gains no visible extra markup. */
|
|
391
|
+
.ui-table-stacked-collection-label {
|
|
392
|
+
display: none;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
366
396
|
/* The compact tier lives in `godxjp-ui-responsive` — the LAST layer, declared after Tailwind in
|
|
367
397
|
* styles/base.css — not in `@layer components`. `<table>` carries `text-sm`, a Tailwind utility,
|
|
368
398
|
* and `utilities` outranks `components` by LAYER ORDER, so a `font-size:` re-point written here as
|
|
@@ -536,4 +566,179 @@
|
|
|
536
566
|
--table-action-collection-flex-width: var(--table-action-collection-flex-width-floor);
|
|
537
567
|
}
|
|
538
568
|
}
|
|
569
|
+
|
|
570
|
+
/* ── `preset="stacked-record-collection"` (gh#293 restore — SCR-215) ────────────────────────
|
|
571
|
+
* Below the step the `<thead>` hides and each `<tr>` becomes a bordered key-value card. This is
|
|
572
|
+
* a genuine layout-mode swap (`display: block`) rather than a measure re-point, because the
|
|
573
|
+
* whole point of this preset is fields too heterogeneous for ANY fixed column measure to hold —
|
|
574
|
+
* unlike `action-collection`'s fixed-layout column budget above. Above the step, none of this
|
|
575
|
+
* matches and the table renders exactly as an ordinary `<table>`. One block per canonical step,
|
|
576
|
+
* same sm/md/lg/xl scale as `action-collection`. */
|
|
577
|
+
@container ui-table-stacked-collection (width < 40rem) {
|
|
578
|
+
[data-collapse-below="sm"] [data-slot="table"] {
|
|
579
|
+
display: block;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
[data-collapse-below="sm"] [data-slot="table"] > thead {
|
|
583
|
+
display: none;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
[data-collapse-below="sm"] [data-slot="table"] > tbody {
|
|
587
|
+
display: block;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
[data-collapse-below="sm"] .ui-table-row {
|
|
591
|
+
display: block;
|
|
592
|
+
border: 1px solid hsl(var(--border));
|
|
593
|
+
border-radius: var(--radius-md);
|
|
594
|
+
padding: var(--table-stacked-collection-card-padding-y)
|
|
595
|
+
var(--table-stacked-collection-card-padding-x);
|
|
596
|
+
margin-block-end: var(--table-stacked-collection-card-gap);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
[data-collapse-below="sm"] .ui-table-row:last-child {
|
|
600
|
+
margin-block-end: 0;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
[data-collapse-below="sm"] [data-slot="table-cell"] {
|
|
604
|
+
display: block;
|
|
605
|
+
padding-block: var(--table-stacked-collection-cell-padding-y);
|
|
606
|
+
white-space: normal;
|
|
607
|
+
overflow-wrap: anywhere;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
[data-collapse-below="sm"] .ui-table-stacked-collection-label {
|
|
611
|
+
display: block;
|
|
612
|
+
font-size: var(--table-stacked-collection-label-font-size);
|
|
613
|
+
font-weight: var(--font-weight-medium);
|
|
614
|
+
color: hsl(var(--muted-foreground));
|
|
615
|
+
margin-block-end: var(--space-stack-xs);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
@container ui-table-stacked-collection (width < 48rem) {
|
|
620
|
+
[data-collapse-below="md"] [data-slot="table"] {
|
|
621
|
+
display: block;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
[data-collapse-below="md"] [data-slot="table"] > thead {
|
|
625
|
+
display: none;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
[data-collapse-below="md"] [data-slot="table"] > tbody {
|
|
629
|
+
display: block;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
[data-collapse-below="md"] .ui-table-row {
|
|
633
|
+
display: block;
|
|
634
|
+
border: 1px solid hsl(var(--border));
|
|
635
|
+
border-radius: var(--radius-md);
|
|
636
|
+
padding: var(--table-stacked-collection-card-padding-y)
|
|
637
|
+
var(--table-stacked-collection-card-padding-x);
|
|
638
|
+
margin-block-end: var(--table-stacked-collection-card-gap);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
[data-collapse-below="md"] .ui-table-row:last-child {
|
|
642
|
+
margin-block-end: 0;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
[data-collapse-below="md"] [data-slot="table-cell"] {
|
|
646
|
+
display: block;
|
|
647
|
+
padding-block: var(--table-stacked-collection-cell-padding-y);
|
|
648
|
+
white-space: normal;
|
|
649
|
+
overflow-wrap: anywhere;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
[data-collapse-below="md"] .ui-table-stacked-collection-label {
|
|
653
|
+
display: block;
|
|
654
|
+
font-size: var(--table-stacked-collection-label-font-size);
|
|
655
|
+
font-weight: var(--font-weight-medium);
|
|
656
|
+
color: hsl(var(--muted-foreground));
|
|
657
|
+
margin-block-end: var(--space-stack-xs);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
@container ui-table-stacked-collection (width < 64rem) {
|
|
662
|
+
[data-collapse-below="lg"] [data-slot="table"] {
|
|
663
|
+
display: block;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
[data-collapse-below="lg"] [data-slot="table"] > thead {
|
|
667
|
+
display: none;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
[data-collapse-below="lg"] [data-slot="table"] > tbody {
|
|
671
|
+
display: block;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
[data-collapse-below="lg"] .ui-table-row {
|
|
675
|
+
display: block;
|
|
676
|
+
border: 1px solid hsl(var(--border));
|
|
677
|
+
border-radius: var(--radius-md);
|
|
678
|
+
padding: var(--table-stacked-collection-card-padding-y)
|
|
679
|
+
var(--table-stacked-collection-card-padding-x);
|
|
680
|
+
margin-block-end: var(--table-stacked-collection-card-gap);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
[data-collapse-below="lg"] .ui-table-row:last-child {
|
|
684
|
+
margin-block-end: 0;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
[data-collapse-below="lg"] [data-slot="table-cell"] {
|
|
688
|
+
display: block;
|
|
689
|
+
padding-block: var(--table-stacked-collection-cell-padding-y);
|
|
690
|
+
white-space: normal;
|
|
691
|
+
overflow-wrap: anywhere;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
[data-collapse-below="lg"] .ui-table-stacked-collection-label {
|
|
695
|
+
display: block;
|
|
696
|
+
font-size: var(--table-stacked-collection-label-font-size);
|
|
697
|
+
font-weight: var(--font-weight-medium);
|
|
698
|
+
color: hsl(var(--muted-foreground));
|
|
699
|
+
margin-block-end: var(--space-stack-xs);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
@container ui-table-stacked-collection (width < 80rem) {
|
|
704
|
+
[data-collapse-below="xl"] [data-slot="table"] {
|
|
705
|
+
display: block;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
[data-collapse-below="xl"] [data-slot="table"] > thead {
|
|
709
|
+
display: none;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
[data-collapse-below="xl"] [data-slot="table"] > tbody {
|
|
713
|
+
display: block;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
[data-collapse-below="xl"] .ui-table-row {
|
|
717
|
+
display: block;
|
|
718
|
+
border: 1px solid hsl(var(--border));
|
|
719
|
+
border-radius: var(--radius-md);
|
|
720
|
+
padding: var(--table-stacked-collection-card-padding-y)
|
|
721
|
+
var(--table-stacked-collection-card-padding-x);
|
|
722
|
+
margin-block-end: var(--table-stacked-collection-card-gap);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
[data-collapse-below="xl"] .ui-table-row:last-child {
|
|
726
|
+
margin-block-end: 0;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
[data-collapse-below="xl"] [data-slot="table-cell"] {
|
|
730
|
+
display: block;
|
|
731
|
+
padding-block: var(--table-stacked-collection-cell-padding-y);
|
|
732
|
+
white-space: normal;
|
|
733
|
+
overflow-wrap: anywhere;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
[data-collapse-below="xl"] .ui-table-stacked-collection-label {
|
|
737
|
+
display: block;
|
|
738
|
+
font-size: var(--table-stacked-collection-label-font-size);
|
|
739
|
+
font-weight: var(--font-weight-medium);
|
|
740
|
+
color: hsl(var(--muted-foreground));
|
|
741
|
+
margin-block-end: var(--space-stack-xs);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
539
744
|
}
|
|
@@ -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
|
}
|
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
/* DXS application-shell geometry. These defaults mirror the checked-in
|
|
25
25
|
* Admin/Console hi-fi source while remaining themeable by consumers. */
|
|
26
26
|
--app-shell-bar-height: 3rem;
|
|
27
|
+
/* (the coarse-pointer override lives at the end of this file — Rule #24 grows
|
|
28
|
+
* controls to 44px, and a 48px bar cannot hold a 44px control plus its 3px
|
|
29
|
+
* focus ring at the very top of the viewport, where overflow cannot help.) */
|
|
27
30
|
/* Inline inset and slot gap of the AppShell top bar. `-compact` applies below the shell
|
|
28
31
|
* breakpoint (the docked sidebar is gone, so the bar tightens against the viewport edge). */
|
|
29
32
|
--app-shell-bar-inset: var(--space-4);
|
|
@@ -346,3 +349,13 @@
|
|
|
346
349
|
* page is the plain page canvas, not the app's `--muted` chrome. */
|
|
347
350
|
--centered-shell-landing-background: initial;
|
|
348
351
|
}
|
|
352
|
+
|
|
353
|
+
/* Rule #24 companion (gh#291): on coarse pointers --control-height grows to 2.75rem
|
|
354
|
+
* (44px tap floor), and the shell bar sits flush with the viewport top — a 3rem bar
|
|
355
|
+
* leaves the control's 3px focus ring painting ABOVE y=0, off-screen, which no
|
|
356
|
+
* overflow setting can recover. 3.5rem gives 6px of breathing per side. */
|
|
357
|
+
@media (pointer: coarse) {
|
|
358
|
+
:root {
|
|
359
|
+
--app-shell-bar-height: 3.5rem;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
@@ -136,3 +136,15 @@
|
|
|
136
136
|
* Japanese at the compact type tier) and the table scrolls instead of crushing. */
|
|
137
137
|
--table-action-collection-min-inline-size-compact: 0;
|
|
138
138
|
}
|
|
139
|
+
|
|
140
|
+
/* Table · stacked-record-collection preset (gh#293 restore — SCR-215) — the canonical WIDE,
|
|
141
|
+
* heterogeneous record collection. Below the collapse step every `<tr>` becomes a bordered
|
|
142
|
+
* key-value card; these tokens own the card's own geometry (unlike action-collection, there is no
|
|
143
|
+
* column-priority measure to retune — the whole row becomes one block). */
|
|
144
|
+
:root {
|
|
145
|
+
--table-stacked-collection-card-padding-y: var(--space-stack-sm);
|
|
146
|
+
--table-stacked-collection-card-padding-x: var(--space-inline-md);
|
|
147
|
+
--table-stacked-collection-card-gap: var(--space-stack-sm);
|
|
148
|
+
--table-stacked-collection-cell-padding-y: var(--space-stack-xs);
|
|
149
|
+
--table-stacked-collection-label-font-size: var(--font-size-xs);
|
|
150
|
+
}
|
|
@@ -156,8 +156,11 @@
|
|
|
156
156
|
/* Ring headroom for clipped chrome (`overflow-clip-margin` on the topbar family, gh#291).
|
|
157
157
|
* MUST stay a plain length consumed via a bare var(): Chromium rejects any calc() in
|
|
158
158
|
* overflow-clip-margin at parse time (even calc(2 * 2px)), silently dropping the
|
|
159
|
-
* declaration to 0. Value = max ring paint (3px open/focus) +
|
|
160
|
-
|
|
159
|
+
* declaration to 0. Value = max ring paint (3px open/focus) + rounding immunity:
|
|
160
|
+
* at 4px a flush-edge ring had only 1px of slack, which device-pixel rounding on
|
|
161
|
+
* non-retina (dpr 1) displays or fractional browser zoom could consume, shaving the
|
|
162
|
+
* ring's outer edge. 8px keeps ≥5px of slack under any rounding regime. */
|
|
163
|
+
--focus-ring-clip-margin: 8px;
|
|
161
164
|
/* REGION focus ring — the keyboard-scroll affordance on large scroll REGIONS (`.app-main`,
|
|
162
165
|
* gh#271/gh#276), separate from the control ring above. A control-strength 2px brand frame
|
|
163
166
|
* around the entire content area reads as a glitch to mouse-first users (`:focus-visible`
|