@godxjp/ui 19.4.1 → 19.4.2
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/list-row.d.ts +2 -0
- package/dist/components/data-display/list-row.js +16 -12
- package/dist/components/data-display/timeline-grid.js +5 -3
- package/dist/components/data-entry/checkbox.js +1 -1
- package/dist/components/data-entry/form-field.js +5 -5
- package/dist/components/data-entry/input.js +1 -1
- package/dist/components/data-entry/radio.js +1 -1
- package/dist/components/data-entry/search-input.d.ts +2 -18
- package/dist/components/data-entry/search-input.js +3 -0
- package/dist/components/data-entry/select.js +1 -1
- package/dist/components/general/button.d.ts +2 -0
- package/dist/components/general/button.js +5 -1
- package/dist/components/general/inert-background.js +2 -1
- package/dist/components/layout/app-shell.js +1 -1
- package/dist/components/layout/master-detail.d.ts +1 -1
- package/dist/components/layout/master-detail.js +47 -3
- package/dist/components/layout/responsive-grid.js +7 -4
- package/dist/components/navigation/app-setting-picker.js +10 -1
- package/dist/email/tokens.generated.d.ts +1 -1
- package/dist/email/tokens.generated.js +1 -1
- package/dist/props/components/data-entry.prop.d.ts +10 -5
- package/dist/props/components/general.prop.d.ts +4 -0
- package/dist/props/components/layout.prop.d.ts +6 -0
- package/dist/props/registry.d.ts +2 -2
- package/dist/props/registry.js +2 -1
- package/dist/styles/control.css +29 -0
- package/dist/styles/data-display-layout.css +22 -1
- package/dist/styles/focus-ring.css +9 -0
- package/dist/styles/form-layout.css +6 -0
- package/dist/styles/layout.css +36 -1
- package/dist/styles/navigation-layout.css +2 -1
- package/dist/tokens/components/data-display.css +2 -1
- package/dist/tokens/foundation.css +2 -1
- package/docs/data-display/list-row.tsx +21 -0
- package/docs/data-entry/search-input.tsx +1 -0
- package/docs/general/button/index.md +4 -0
- package/docs/layout/master-detail.tsx +3 -0
- package/docs/layout/responsive-grid.tsx +19 -2
- package/package.json +2 -2
|
@@ -15,6 +15,8 @@ export type ListRowDensity = ListRowDensityProp;
|
|
|
15
15
|
export interface ListRowProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
|
|
16
16
|
/** Render element — `div` (default) or `li` when the parent is a `<ul>`/`<ol>`. */
|
|
17
17
|
as?: "div" | "li";
|
|
18
|
+
/** Use the supplied link as the entire row. Do not nest interactive trailing controls. */
|
|
19
|
+
asChild?: boolean;
|
|
18
20
|
/** Leading slot — a decorative icon or an Avatar. Mark a purely decorative icon `aria-hidden`. */
|
|
19
21
|
leading?: React.ReactNode;
|
|
20
22
|
/** Primary line — rendered in medium weight. */
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
4
5
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
5
6
|
import { cn } from "../../lib/utils.js";
|
|
6
7
|
import { Text } from "../general/typography.js";
|
|
7
8
|
const ListRow = React.forwardRef(
|
|
8
9
|
({
|
|
9
10
|
as = "div",
|
|
11
|
+
asChild = false,
|
|
12
|
+
children,
|
|
10
13
|
leading,
|
|
11
14
|
title,
|
|
12
15
|
description,
|
|
@@ -19,9 +22,18 @@ const ListRow = React.forwardRef(
|
|
|
19
22
|
...props
|
|
20
23
|
}, ref) => {
|
|
21
24
|
const { t } = useTranslation();
|
|
22
|
-
const Comp = as;
|
|
25
|
+
const Comp = asChild ? Slot : as;
|
|
23
26
|
const truncate = overflow !== "wrap";
|
|
24
|
-
|
|
27
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
28
|
+
unread !== void 0 ? /* @__PURE__ */ jsx("span", { "data-slot": "list-row-indicator", children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: unread ? t("dataDisplay.listRow.unread") : t("dataDisplay.listRow.read") }) }) : null,
|
|
29
|
+
leading != null ? /* @__PURE__ */ jsx("span", { "data-slot": "list-row-leading", children: leading }) : null,
|
|
30
|
+
/* @__PURE__ */ jsxs("span", { "data-slot": "list-row-body", children: [
|
|
31
|
+
/* @__PURE__ */ jsx(Text, { weight: "medium", truncate, children: title }),
|
|
32
|
+
description != null ? /* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", truncate, children: description }) : null
|
|
33
|
+
] }),
|
|
34
|
+
trailing != null ? /* @__PURE__ */ jsx("span", { "data-slot": "list-row-trailing", children: trailing }) : null
|
|
35
|
+
] });
|
|
36
|
+
return /* @__PURE__ */ jsx(
|
|
25
37
|
Comp,
|
|
26
38
|
{
|
|
27
39
|
ref,
|
|
@@ -32,15 +44,7 @@ const ListRow = React.forwardRef(
|
|
|
32
44
|
"data-unread": unread === true ? "" : void 0,
|
|
33
45
|
className: cn("ui-list-row", className),
|
|
34
46
|
...props,
|
|
35
|
-
children:
|
|
36
|
-
unread !== void 0 ? /* @__PURE__ */ jsx("span", { "data-slot": "list-row-indicator", children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: unread ? t("dataDisplay.listRow.unread") : t("dataDisplay.listRow.read") }) }) : null,
|
|
37
|
-
leading != null ? /* @__PURE__ */ jsx("span", { "data-slot": "list-row-leading", children: leading }) : null,
|
|
38
|
-
/* @__PURE__ */ jsxs("span", { "data-slot": "list-row-body", children: [
|
|
39
|
-
/* @__PURE__ */ jsx(Text, { weight: "medium", truncate, children: title }),
|
|
40
|
-
description != null ? /* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", truncate, children: description }) : null
|
|
41
|
-
] }),
|
|
42
|
-
trailing != null ? /* @__PURE__ */ jsx("span", { "data-slot": "list-row-trailing", children: trailing }) : null
|
|
43
|
-
]
|
|
47
|
+
children: asChild && React.isValidElement(children) ? React.cloneElement(children, void 0, content) : content
|
|
44
48
|
}
|
|
45
49
|
);
|
|
46
50
|
}
|
|
@@ -25,7 +25,9 @@ function resolveEvent(event) {
|
|
|
25
25
|
if (startMin === null || endMin === null) return null;
|
|
26
26
|
return { event, startMin, endMin: endMin > startMin ? endMin : endMin + MINUTES_PER_DAY };
|
|
27
27
|
}
|
|
28
|
+
const EVENT_MIN_MINUTES = 36;
|
|
28
29
|
function placeColumn(events) {
|
|
30
|
+
const drawnEnd = (e) => Math.max(e.endMin, e.startMin + EVENT_MIN_MINUTES);
|
|
29
31
|
const sorted = [...events].sort((a, b) => a.startMin - b.startMin || a.endMin - b.endMin);
|
|
30
32
|
const placed = [];
|
|
31
33
|
let cluster = [];
|
|
@@ -42,8 +44,8 @@ function placeColumn(events) {
|
|
|
42
44
|
if (entry.startMin >= clusterEnd) closeCluster();
|
|
43
45
|
let lane = laneEnds.findIndex((end) => end <= entry.startMin);
|
|
44
46
|
if (lane === -1) lane = laneEnds.length;
|
|
45
|
-
laneEnds[lane] = entry
|
|
46
|
-
clusterEnd = Math.max(clusterEnd, entry
|
|
47
|
+
laneEnds[lane] = drawnEnd(entry);
|
|
48
|
+
clusterEnd = Math.max(clusterEnd, drawnEnd(entry));
|
|
47
49
|
cluster.push({ ...entry, lane, lanes: 1 });
|
|
48
50
|
}
|
|
49
51
|
closeCluster();
|
|
@@ -93,7 +95,7 @@ const TimelineGrid = React.forwardRef(
|
|
|
93
95
|
const axis = React.useMemo(() => resolveWindow(start, end, resolved), [start, end, resolved]);
|
|
94
96
|
const span = axis.endMin - axis.startMin;
|
|
95
97
|
const hours = span / 60;
|
|
96
|
-
const step = Number.isFinite(interval) && interval > 0 ? interval : 1;
|
|
98
|
+
const step = Number.isFinite(interval) && interval > 0 ? Math.max(interval, 1 / 60) : 1;
|
|
97
99
|
const byColumn = React.useMemo(() => {
|
|
98
100
|
const buckets = columns.map(() => []);
|
|
99
101
|
for (const entry of resolved) {
|
|
@@ -26,7 +26,7 @@ const CheckboxRoot = React.forwardRef(({ className, ...props }, ref) => {
|
|
|
26
26
|
// service overriding --checkbox-checked-background got no fill change at all. Radix sets
|
|
27
27
|
// `data-state`/`data-disabled` on the root, so the CSS rules match. Rendering is unchanged:
|
|
28
28
|
// both knobs default to exactly the values these utilities hard-coded.
|
|
29
|
-
"peer ui-checkbox aria-invalid:border-destructive
|
|
29
|
+
"peer ui-checkbox aria-invalid:border-destructive data-[state=checked]:border-primary data-[state=checked]:text-primary-foreground shrink-0 shadow-xs transition-shadow outline-none",
|
|
30
30
|
className
|
|
31
31
|
),
|
|
32
32
|
...props,
|
|
@@ -114,7 +114,7 @@ function FormField({
|
|
|
114
114
|
asChild: true,
|
|
115
115
|
id: labelId,
|
|
116
116
|
className: "ui-inline-xs text-[length:var(--form-label-font-size)]",
|
|
117
|
-
children: /* @__PURE__ */
|
|
117
|
+
children: /* @__PURE__ */ jsx(
|
|
118
118
|
"span",
|
|
119
119
|
{
|
|
120
120
|
onClick: () => {
|
|
@@ -123,10 +123,10 @@ function FormField({
|
|
|
123
123
|
const focusable = el.matches(FOCUSABLE_SELECTOR) ? el : el.querySelector(FOCUSABLE_SELECTOR);
|
|
124
124
|
(focusable ?? el).focus();
|
|
125
125
|
},
|
|
126
|
-
children: [
|
|
127
|
-
|
|
128
|
-
required && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "
|
|
129
|
-
]
|
|
126
|
+
children: /* @__PURE__ */ jsxs("span", { children: [
|
|
127
|
+
label,
|
|
128
|
+
required && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "ui-form-field-required", children: "*" })
|
|
129
|
+
] })
|
|
130
130
|
}
|
|
131
131
|
)
|
|
132
132
|
}
|
|
@@ -9,7 +9,7 @@ const inputBaseClass = [
|
|
|
9
9
|
"ui-control ui-input border-input bg-background w-full rounded-[var(--control-radius)] transition-[color,box-shadow] outline-none",
|
|
10
10
|
"selection:bg-primary selection:text-primary-foreground",
|
|
11
11
|
"placeholder:text-muted-foreground",
|
|
12
|
-
"aria-invalid:border-destructive
|
|
12
|
+
"aria-invalid:border-destructive"
|
|
13
13
|
];
|
|
14
14
|
const Input = React.forwardRef(
|
|
15
15
|
({
|
|
@@ -27,7 +27,7 @@ const RadioItem = React.forwardRef(({ className, ...props }, ref) => /* @__PURE_
|
|
|
27
27
|
// and reads --disabled-opacity. The utility was layered after components, so it silently
|
|
28
28
|
// outranked that token — a service theme's --disabled-opacity never reached a radio.
|
|
29
29
|
// Byte-identical: --disabled-opacity defaults to 0.5.
|
|
30
|
-
"ui-radio aria-invalid:border-destructive
|
|
30
|
+
"ui-radio aria-invalid:border-destructive shrink-0 shadow-xs transition-shadow outline-none",
|
|
31
31
|
className
|
|
32
32
|
),
|
|
33
33
|
...props,
|
|
@@ -1,20 +1,4 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
value?: string;
|
|
5
|
-
defaultValue?: string;
|
|
6
|
-
placeholder?: string;
|
|
7
|
-
debounce?: number;
|
|
8
|
-
/** Fires on EVERY keystroke (immediate) — required to keep a controlled `value` responsive. */
|
|
9
|
-
onValueChange?: (q: string) => void;
|
|
10
|
-
/** Fires with the DEBOUNCED term. Optional — omit it when you drive filtering off `onValueChange`. */
|
|
11
|
-
onSearch?: (q: string) => void;
|
|
12
|
-
label?: React.ReactNode;
|
|
13
|
-
ariaLabel?: string;
|
|
14
|
-
className?: string;
|
|
15
|
-
inputClassName?: string;
|
|
16
|
-
id?: string;
|
|
17
|
-
disabled?: boolean;
|
|
18
|
-
}
|
|
2
|
+
import type { SearchInputProp } from "../../props/components/data-entry.prop.js";
|
|
3
|
+
export type SearchInputProps = SearchInputProp;
|
|
19
4
|
export declare function SearchInput({ value: controlledValue, defaultValue, placeholder, debounce, onValueChange, onSearch, label, ariaLabel, className, inputClassName, id, disabled, ...ariaProps }: SearchInputProps): React.JSX.Element;
|
|
20
|
-
export {};
|
|
@@ -37,7 +37,10 @@ function SearchInput({
|
|
|
37
37
|
React.useEffect(() => {
|
|
38
38
|
onSearchRef.current = onSearch;
|
|
39
39
|
});
|
|
40
|
+
const lastSearch = React.useRef(debounced);
|
|
40
41
|
React.useEffect(() => {
|
|
42
|
+
if (lastSearch.current === debounced) return;
|
|
43
|
+
lastSearch.current = debounced;
|
|
41
44
|
onSearchRef.current?.(debounced);
|
|
42
45
|
}, [debounced]);
|
|
43
46
|
const setValue = (v) => {
|
|
@@ -99,7 +99,7 @@ const SelectTrigger = React.forwardRef(({ className, children, size = "md", widt
|
|
|
99
99
|
className: cn(
|
|
100
100
|
controlTriggerClass,
|
|
101
101
|
width === "auto" ? "w-auto" : "w-full",
|
|
102
|
-
"aria-invalid:border-destructive
|
|
102
|
+
"aria-invalid:border-destructive data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground whitespace-nowrap transition-[color,box-shadow] outline-none *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center",
|
|
103
103
|
className
|
|
104
104
|
),
|
|
105
105
|
...props,
|
|
@@ -10,6 +10,8 @@ export declare const Button: React.ForwardRefExoticComponent<React.ButtonHTMLAtt
|
|
|
10
10
|
size?: import("../../props/index.js").ButtonSizeProp;
|
|
11
11
|
shape?: import("../../props/index.js").ShapeProp;
|
|
12
12
|
fullWidth?: boolean;
|
|
13
|
+
wrap?: boolean;
|
|
14
|
+
align?: import("../../props/index.js").TextAlignProp;
|
|
13
15
|
asChild?: import("../../props/index.js").AsChildProp;
|
|
14
16
|
onClick?: import("../../props/index.js").OnClickProp;
|
|
15
17
|
disabled?: import("../../props/index.js").DisabledProp;
|
|
@@ -47,6 +47,8 @@ const Button = React.forwardRef(
|
|
|
47
47
|
size,
|
|
48
48
|
shape,
|
|
49
49
|
fullWidth = false,
|
|
50
|
+
wrap = false,
|
|
51
|
+
align = "center",
|
|
50
52
|
asChild = false,
|
|
51
53
|
loading = false,
|
|
52
54
|
loadingText,
|
|
@@ -76,13 +78,15 @@ const Button = React.forwardRef(
|
|
|
76
78
|
"data-size": size ?? "default",
|
|
77
79
|
"data-shape": shape ?? "default",
|
|
78
80
|
"data-full-width": fullWidth ? "" : void 0,
|
|
81
|
+
"data-wrap": wrap ? "" : void 0,
|
|
82
|
+
"data-align": align === "center" ? void 0 : align,
|
|
79
83
|
"data-loading": isLoading ? "" : void 0,
|
|
80
84
|
"aria-busy": isLoading || void 0,
|
|
81
85
|
disabled: isLoading || disabled,
|
|
82
86
|
type: asChild ? void 0 : type ?? "button",
|
|
83
87
|
className: cn(
|
|
84
88
|
fullWidth && "w-full",
|
|
85
|
-
"aria-invalid:border-destructive
|
|
89
|
+
"aria-invalid:border-destructive",
|
|
86
90
|
"[&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
87
91
|
buttonVariants({ variant, size, shape, className })
|
|
88
92
|
),
|
|
@@ -7,7 +7,8 @@ let mounted = 0;
|
|
|
7
7
|
function sync() {
|
|
8
8
|
for (const el of document.querySelectorAll("[data-aria-hidden]")) {
|
|
9
9
|
if (owned.has(el) || el.hasAttribute("inert")) continue;
|
|
10
|
-
if (el.
|
|
10
|
+
if (el.matches("[data-radix-focus-guard]")) continue;
|
|
11
|
+
if (el.matches(TABBABLE) || el.querySelector(TABBABLE)) {
|
|
11
12
|
el.setAttribute("inert", "");
|
|
12
13
|
owned.add(el);
|
|
13
14
|
}
|
|
@@ -51,7 +51,7 @@ function AppShell({
|
|
|
51
51
|
type: "button",
|
|
52
52
|
variant: "ghost",
|
|
53
53
|
size: "sm",
|
|
54
|
-
className: "app-mobile-nav-trigger
|
|
54
|
+
className: "app-mobile-nav-trigger",
|
|
55
55
|
"aria-label": t("layout.appShell.openNav"),
|
|
56
56
|
"aria-haspopup": "dialog",
|
|
57
57
|
children: /* @__PURE__ */ jsx(Menu, { className: "size-[var(--app-shell-mobile-nav-icon-size)]", "aria-hidden": "true" })
|
|
@@ -5,4 +5,4 @@ export type MasterDetailProps = MasterDetailProp;
|
|
|
5
5
|
* drives. Either way `master` comes first in DOM order, so below the threshold the regions stack
|
|
6
6
|
* list-then-detail.
|
|
7
7
|
*/
|
|
8
|
-
export declare function MasterDetail({ master, children, rail, railWidth, masterViewport, collapseBelow, masterLabel, detailLabel, detailId, }: MasterDetailProps): import("react").JSX.Element;
|
|
8
|
+
export declare function MasterDetail({ master, mobilePane, detailBack, children, rail, railWidth, masterViewport, collapseBelow, masterLabel, detailLabel, detailId, }: MasterDetailProps): import("react").JSX.Element;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
2
4
|
function MasterDetail({
|
|
3
5
|
master,
|
|
6
|
+
mobilePane,
|
|
7
|
+
detailBack,
|
|
4
8
|
children,
|
|
5
9
|
rail = "detail",
|
|
6
10
|
railWidth = "standard",
|
|
@@ -10,11 +14,45 @@ function MasterDetail({
|
|
|
10
14
|
detailLabel,
|
|
11
15
|
detailId
|
|
12
16
|
}) {
|
|
17
|
+
const rootRef = useRef(null);
|
|
18
|
+
const thresholdRef = useRef(null);
|
|
19
|
+
const [compact, setCompact] = useState(false);
|
|
20
|
+
const navigable = mobilePane !== void 0;
|
|
21
|
+
useLayoutEffect(() => {
|
|
22
|
+
if (!navigable || !rootRef.current || !thresholdRef.current) return;
|
|
23
|
+
const root = rootRef.current;
|
|
24
|
+
const threshold = thresholdRef.current;
|
|
25
|
+
const measure = () => setCompact(root.getBoundingClientRect().width < threshold.getBoundingClientRect().width);
|
|
26
|
+
measure();
|
|
27
|
+
const observer = new ResizeObserver(measure);
|
|
28
|
+
observer.observe(root);
|
|
29
|
+
observer.observe(threshold);
|
|
30
|
+
return () => observer.disconnect();
|
|
31
|
+
}, [navigable, collapseBelow]);
|
|
32
|
+
const masterRef = useRef(null);
|
|
33
|
+
const detailRef = useRef(null);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!mobilePane) return;
|
|
36
|
+
if (mobilePane === "detail" && masterRef.current?.getClientRects().length === 0) {
|
|
37
|
+
const heading = detailRef.current?.querySelector("h1, h2, h3");
|
|
38
|
+
if (heading) {
|
|
39
|
+
heading.tabIndex = -1;
|
|
40
|
+
heading.focus();
|
|
41
|
+
} else {
|
|
42
|
+
detailRef.current?.focus();
|
|
43
|
+
}
|
|
44
|
+
} else if (mobilePane === "master" && detailRef.current?.getClientRects().length === 0) {
|
|
45
|
+
masterRef.current?.querySelector("a[aria-current], a[href]")?.focus({ preventScroll: true });
|
|
46
|
+
}
|
|
47
|
+
}, [mobilePane, detailLabel, compact]);
|
|
13
48
|
const bounded = masterViewport !== "auto";
|
|
14
49
|
return /* @__PURE__ */ jsxs(
|
|
15
50
|
"div",
|
|
16
51
|
{
|
|
52
|
+
ref: rootRef,
|
|
17
53
|
className: "ui-master-detail",
|
|
54
|
+
"data-mobile-compact": navigable && compact ? "" : void 0,
|
|
55
|
+
"data-mobile-pane": mobilePane,
|
|
18
56
|
"data-rail": rail,
|
|
19
57
|
"data-rail-width": railWidth,
|
|
20
58
|
"data-master-viewport": masterViewport,
|
|
@@ -23,22 +61,28 @@ function MasterDetail({
|
|
|
23
61
|
/* @__PURE__ */ jsx(
|
|
24
62
|
"section",
|
|
25
63
|
{
|
|
64
|
+
ref: masterRef,
|
|
26
65
|
className: "ui-master-detail-master",
|
|
27
66
|
"aria-label": masterLabel,
|
|
28
67
|
tabIndex: bounded ? 0 : void 0,
|
|
29
68
|
children: master
|
|
30
69
|
}
|
|
31
70
|
),
|
|
32
|
-
/* @__PURE__ */
|
|
71
|
+
/* @__PURE__ */ jsxs(
|
|
33
72
|
"section",
|
|
34
73
|
{
|
|
74
|
+
ref: detailRef,
|
|
35
75
|
className: "ui-master-detail-detail",
|
|
36
76
|
id: detailId,
|
|
37
77
|
"aria-label": detailLabel,
|
|
38
78
|
tabIndex: -1,
|
|
39
|
-
children
|
|
79
|
+
children: [
|
|
80
|
+
mobilePane && detailBack ? /* @__PURE__ */ jsx("div", { className: "ui-master-detail-back", children: detailBack }) : null,
|
|
81
|
+
children
|
|
82
|
+
]
|
|
40
83
|
}
|
|
41
|
-
)
|
|
84
|
+
),
|
|
85
|
+
navigable ? /* @__PURE__ */ jsx("span", { className: "ui-master-detail-measure", "aria-hidden": "true", children: /* @__PURE__ */ jsx("span", { ref: thresholdRef }) }) : null
|
|
42
86
|
]
|
|
43
87
|
}
|
|
44
88
|
);
|
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from "../../lib/utils.js";
|
|
3
3
|
const RESPONSIVE_GRID_PRESET_COLUMNS = {
|
|
4
|
-
"pricing-plans": { sm: 3, md: 3, lg: 3 }
|
|
4
|
+
"pricing-plans": { base: 1, sm: 3, md: 3, lg: 3 }
|
|
5
5
|
};
|
|
6
6
|
function resolveColumns(columns) {
|
|
7
7
|
if (typeof columns === "number") {
|
|
8
8
|
return {
|
|
9
|
+
base: 1,
|
|
9
10
|
sm: Math.min(columns, 2),
|
|
10
11
|
md: Math.min(columns, 3),
|
|
11
12
|
lg: columns
|
|
12
13
|
};
|
|
13
14
|
}
|
|
14
15
|
return {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
base: columns.base ?? 1,
|
|
17
|
+
sm: columns.sm ?? columns.base ?? 1,
|
|
18
|
+
md: columns.md ?? columns.sm ?? columns.base ?? 1,
|
|
19
|
+
lg: columns.lg ?? columns.md ?? columns.sm ?? columns.base ?? 1
|
|
18
20
|
};
|
|
19
21
|
}
|
|
20
22
|
function toStyle(resolved) {
|
|
21
23
|
return {
|
|
24
|
+
"--responsive-grid-base": resolved.base,
|
|
22
25
|
"--responsive-grid-sm": resolved.sm,
|
|
23
26
|
"--responsive-grid-md": resolved.md,
|
|
24
27
|
"--responsive-grid-lg": resolved.lg
|
|
@@ -139,7 +139,16 @@ const AppSettingPicker = React.forwardRef(
|
|
|
139
139
|
// (`data-[state=open]:border-ring`, from controlTriggerClass) and the
|
|
140
140
|
// focus-visible ring are untouched, so keyboard and "is this open" affordance
|
|
141
141
|
// still hold.
|
|
142
|
-
|
|
142
|
+
// Bề ngang phải là UTILITY, không phải luật class. SelectTrigger phát `w-full`,
|
|
143
|
+
// mà utility nằm sau components trong thứ tự layer nên `inline-size` khai trong
|
|
144
|
+
// .ui-app-setting-picker-icon luôn thua. Trong một khe co theo nội dung của
|
|
145
|
+
// topbar, `width: 100%` co lại bằng chính nội dung, tức 18px thay vì ô vuông
|
|
146
|
+
// --control-height. Nhánh có nhãn ngay dưới đã phải tự vệ đúng như vậy bằng
|
|
147
|
+
// `w-auto`. Giá trị vẫn đọc token chứ không phải một con số.
|
|
148
|
+
cn(
|
|
149
|
+
"ui-app-setting-picker-icon hover:bg-accent hover:text-accent-foreground",
|
|
150
|
+
"w-[length:var(--control-height)]"
|
|
151
|
+
)
|
|
143
152
|
) : (
|
|
144
153
|
// Labeled: sized to a per-kind width from `sm` up; below `sm` it hugs its content and
|
|
145
154
|
// A form field that wants a full-width control passes
|
|
@@ -8,7 +8,7 @@ import type { DateRange } from "react-day-picker";
|
|
|
8
8
|
import type * as React from "react";
|
|
9
9
|
import type { UploadFileItem } from "../../components/data-entry/upload-types.js";
|
|
10
10
|
import type { FieldA11yProps } from "../../lib/field-a11y.js";
|
|
11
|
-
import type { ClassNameProp, DisabledProp, EmptyMessageProp, ErrorBagProp, ErrorProp, HelperProp, IdProp, LabelProp, NameProp,
|
|
11
|
+
import type { ClassNameProp, DisabledProp, EmptyMessageProp, ErrorBagProp, ErrorProp, HelperProp, IdProp, LabelProp, NameProp, OnValueChangeProp, OnSearchChangeProp, OpenProp, OnOpenChangeProp, PlaceholderProp, RequiredProp, ValueProp, DefaultValueProp, FormLayoutProp, WidthProp, BreakpointProp, DensityProp, SizeProp, TitleProp } from "../vocabulary/index.js";
|
|
12
12
|
import type { ResponsiveGridColumnsProp } from "./layout.prop.js";
|
|
13
13
|
/** One-outline-per-group appearance for the compound InputOTP control. */
|
|
14
14
|
export type InputOTPGroupAppearanceProp = "slots" | "grouped";
|
|
@@ -214,12 +214,17 @@ export type FormErrorsProviderProp = {
|
|
|
214
214
|
export type SearchInputProp = FieldA11yProps & {
|
|
215
215
|
id?: IdProp;
|
|
216
216
|
label?: LabelProp;
|
|
217
|
+
ariaLabel?: string;
|
|
217
218
|
placeholder?: PlaceholderProp;
|
|
218
|
-
value?:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
219
|
+
value?: string;
|
|
220
|
+
defaultValue?: string;
|
|
221
|
+
onValueChange?: (query: string) => void;
|
|
222
|
+
/** Emits changed, debounced queries; does not run for the initial value. */
|
|
223
|
+
onSearch?: (query: string) => void;
|
|
224
|
+
debounce?: number;
|
|
225
|
+
disabled?: DisabledProp;
|
|
222
226
|
className?: ClassNameProp;
|
|
227
|
+
inputClassName?: ClassNameProp;
|
|
223
228
|
};
|
|
224
229
|
/** @see Checkbox — extends Radix checkbox root props. */
|
|
225
230
|
export type CheckboxProp = React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>;
|
|
@@ -43,6 +43,10 @@ export type ButtonProp = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
|
43
43
|
/** Corner shape — `default` (control radius) · `pill` (fully rounded) · `sharp` (square). */
|
|
44
44
|
shape?: ShapeProp;
|
|
45
45
|
fullWidth?: boolean;
|
|
46
|
+
/** Allow a text button to grow vertically for multi-line labels. */
|
|
47
|
+
wrap?: boolean;
|
|
48
|
+
/** Logical content alignment, especially for full-width collection actions. */
|
|
49
|
+
align?: TextAlignProp;
|
|
46
50
|
asChild?: AsChildProp;
|
|
47
51
|
onClick?: OnClickProp;
|
|
48
52
|
disabled?: DisabledProp;
|
|
@@ -127,7 +127,9 @@ export type FlexProp = React.HTMLAttributes<HTMLDivElement> & {
|
|
|
127
127
|
*/
|
|
128
128
|
hideFrom?: BreakpointProp;
|
|
129
129
|
};
|
|
130
|
+
/** Container column counts; omitted steps inherit from the previous step. Base defaults to 1. */
|
|
130
131
|
export type ResponsiveGridColumnsProp = number | {
|
|
132
|
+
base?: number;
|
|
131
133
|
sm?: number;
|
|
132
134
|
md?: number;
|
|
133
135
|
lg?: number;
|
|
@@ -147,6 +149,10 @@ export type MasterDetailRailProp = "master" | "detail";
|
|
|
147
149
|
export type MasterDetailMasterViewportProp = "auto" | "compact" | "standard";
|
|
148
150
|
/** @see MasterDetail */
|
|
149
151
|
export type MasterDetailProp = {
|
|
152
|
+
/** Opt into one-pane navigation on mobile (below the token-owned collapse threshold); desktop retains both panes. */
|
|
153
|
+
mobilePane?: "master" | "detail";
|
|
154
|
+
/** Back link shown above detail only in mobile navigation mode. */
|
|
155
|
+
detailBack?: ReactNode;
|
|
150
156
|
/** Selectable collection; always first in DOM order, so the stacked order stays list-then-detail. */
|
|
151
157
|
master: ReactNode;
|
|
152
158
|
/** Detail surface for the current selection. */
|
package/dist/props/registry.d.ts
CHANGED
|
@@ -930,7 +930,7 @@ export declare const COMPONENT_PROP_REGISTRY: {
|
|
|
930
930
|
readonly ButtonProp: {
|
|
931
931
|
readonly group: "general";
|
|
932
932
|
readonly file: "components/general.prop.ts";
|
|
933
|
-
readonly vocabulary: readonly ["ButtonVariantProp", "SizeProp", "ShapeProp", "AsChildProp", "DisabledProp", "OnClickProp", "PendingProp"];
|
|
933
|
+
readonly vocabulary: readonly ["TextAlignProp", "ButtonVariantProp", "SizeProp", "ShapeProp", "AsChildProp", "DisabledProp", "OnClickProp", "PendingProp"];
|
|
934
934
|
};
|
|
935
935
|
readonly RevealProp: {
|
|
936
936
|
readonly group: "general";
|
|
@@ -998,7 +998,7 @@ export declare const COMPONENT_PROP_REGISTRY: {
|
|
|
998
998
|
readonly SearchInputProp: {
|
|
999
999
|
readonly group: "data-entry";
|
|
1000
1000
|
readonly file: "components/data-entry.prop.ts";
|
|
1001
|
-
readonly vocabulary: readonly ["LabelProp", "PlaceholderProp", "
|
|
1001
|
+
readonly vocabulary: readonly ["LabelProp", "PlaceholderProp", "DisabledProp", "ClassNameProp", "IdProp"];
|
|
1002
1002
|
};
|
|
1003
1003
|
readonly CheckboxProp: {
|
|
1004
1004
|
readonly group: "data-entry";
|
package/dist/props/registry.js
CHANGED
|
@@ -1024,6 +1024,7 @@ const COMPONENT_PROP_REGISTRY = {
|
|
|
1024
1024
|
group: "general",
|
|
1025
1025
|
file: "components/general.prop.ts",
|
|
1026
1026
|
vocabulary: [
|
|
1027
|
+
"TextAlignProp",
|
|
1027
1028
|
"ButtonVariantProp",
|
|
1028
1029
|
"SizeProp",
|
|
1029
1030
|
"ShapeProp",
|
|
@@ -1147,7 +1148,7 @@ const COMPONENT_PROP_REGISTRY = {
|
|
|
1147
1148
|
SearchInputProp: {
|
|
1148
1149
|
group: "data-entry",
|
|
1149
1150
|
file: "components/data-entry.prop.ts",
|
|
1150
|
-
vocabulary: ["LabelProp", "PlaceholderProp", "
|
|
1151
|
+
vocabulary: ["LabelProp", "PlaceholderProp", "DisabledProp", "ClassNameProp", "IdProp"]
|
|
1151
1152
|
},
|
|
1152
1153
|
CheckboxProp: {
|
|
1153
1154
|
group: "data-entry",
|
package/dist/styles/control.css
CHANGED
|
@@ -2063,3 +2063,32 @@
|
|
|
2063
2063
|
color: var(--button-count-secondary-color, hsl(var(--secondary)));
|
|
2064
2064
|
}
|
|
2065
2065
|
}
|
|
2066
|
+
|
|
2067
|
+
@layer components {
|
|
2068
|
+
.ui-button[data-wrap] {
|
|
2069
|
+
--button-wrap-min-height: var(--control-height);
|
|
2070
|
+
height: auto;
|
|
2071
|
+
min-height: var(--button-wrap-min-height);
|
|
2072
|
+
max-inline-size: 100%;
|
|
2073
|
+
padding-block: var(--space-1);
|
|
2074
|
+
white-space: normal;
|
|
2075
|
+
overflow-wrap: anywhere;
|
|
2076
|
+
}
|
|
2077
|
+
.ui-button[data-wrap][data-size="xs"] {
|
|
2078
|
+
--button-wrap-min-height: var(--control-height-xs);
|
|
2079
|
+
}
|
|
2080
|
+
.ui-button[data-wrap][data-size="sm"] {
|
|
2081
|
+
--button-wrap-min-height: var(--control-height-sm);
|
|
2082
|
+
}
|
|
2083
|
+
.ui-button[data-wrap][data-size="lg"] {
|
|
2084
|
+
--button-wrap-min-height: var(--control-height-lg);
|
|
2085
|
+
}
|
|
2086
|
+
.ui-button[data-align="start"] {
|
|
2087
|
+
justify-content: flex-start;
|
|
2088
|
+
text-align: start;
|
|
2089
|
+
}
|
|
2090
|
+
.ui-button[data-align="end"] {
|
|
2091
|
+
justify-content: flex-end;
|
|
2092
|
+
text-align: end;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
@@ -983,7 +983,9 @@
|
|
|
983
983
|
box-sizing: border-box;
|
|
984
984
|
inline-size: calc(100% / var(--timeline-grid-event-lanes, 1));
|
|
985
985
|
block-size: calc(var(--timeline-grid-hour-height) * var(--timeline-grid-event-span, 0));
|
|
986
|
-
min-block-size:
|
|
986
|
+
min-block-size: calc(
|
|
987
|
+
var(--timeline-grid-hour-height) * var(--timeline-grid-event-min-height-minutes) / 60
|
|
988
|
+
);
|
|
987
989
|
padding-inline-end: var(--timeline-grid-event-gap);
|
|
988
990
|
padding-block-end: var(--timeline-grid-event-gap);
|
|
989
991
|
}
|
|
@@ -1064,3 +1066,22 @@
|
|
|
1064
1066
|
pointer-events: none;
|
|
1065
1067
|
}
|
|
1066
1068
|
}
|
|
1069
|
+
|
|
1070
|
+
@layer components {
|
|
1071
|
+
a.ui-list-row {
|
|
1072
|
+
color: inherit;
|
|
1073
|
+
text-decoration: none;
|
|
1074
|
+
cursor: pointer;
|
|
1075
|
+
}
|
|
1076
|
+
a.ui-list-row:hover {
|
|
1077
|
+
background: hsl(var(--muted));
|
|
1078
|
+
}
|
|
1079
|
+
a.ui-list-row[aria-current="page"] {
|
|
1080
|
+
|
|
1081
|
+
background: hsl(var(--accent));
|
|
1082
|
+
box-shadow: inset var(--space-1) 0 0 hsl(var(--primary));
|
|
1083
|
+
}
|
|
1084
|
+
[dir="rtl"] a.ui-list-row[aria-current="page"] {
|
|
1085
|
+
box-shadow: inset calc(-1 * var(--space-1)) 0 0 hsl(var(--primary));
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
:is(
|
|
36
36
|
.ui-focus-ring-outline,
|
|
37
|
+
a.ui-list-row,
|
|
37
38
|
.ui-accordion-trigger,
|
|
38
39
|
.ui-carousel-dot,
|
|
39
40
|
.ui-rating-star,
|
|
@@ -57,6 +58,10 @@
|
|
|
57
58
|
--focus-ring-opacity: var(--topbar-icon-focus-ring-alpha);
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
a.ui-list-row {
|
|
62
|
+
--focus-ring-offset: calc(-1 * var(--focus-ring-width));
|
|
63
|
+
}
|
|
64
|
+
|
|
60
65
|
.ui-accordion-trigger {
|
|
61
66
|
--focus-ring-offset: var(--accordion-focus-ring-offset);
|
|
62
67
|
}
|
|
@@ -78,4 +83,8 @@
|
|
|
78
83
|
outline: none;
|
|
79
84
|
box-shadow: none;
|
|
80
85
|
}
|
|
86
|
+
|
|
87
|
+
[aria-invalid="true"] {
|
|
88
|
+
--focus-ring-color: var(--destructive);
|
|
89
|
+
}
|
|
81
90
|
}
|
package/dist/styles/layout.css
CHANGED
|
@@ -239,7 +239,7 @@
|
|
|
239
239
|
.ui-responsive-grid {
|
|
240
240
|
display: grid;
|
|
241
241
|
gap: var(--space-stack-md);
|
|
242
|
-
grid-template-columns: minmax(0, 1fr);
|
|
242
|
+
grid-template-columns: repeat(var(--responsive-grid-base, 1), minmax(0, 1fr));
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
.ui-responsive-grid[data-gap="none"] {
|
|
@@ -1348,3 +1348,38 @@
|
|
|
1348
1348
|
}
|
|
1349
1349
|
}
|
|
1350
1350
|
}
|
|
1351
|
+
|
|
1352
|
+
@layer components {
|
|
1353
|
+
.ui-master-detail-back {
|
|
1354
|
+
display: none;
|
|
1355
|
+
}
|
|
1356
|
+
.ui-master-detail[data-mobile-pane] {
|
|
1357
|
+
position: relative;
|
|
1358
|
+
}
|
|
1359
|
+
.ui-master-detail-measure {
|
|
1360
|
+
position: absolute;
|
|
1361
|
+
inset-inline: 0;
|
|
1362
|
+
inset-block-start: 0;
|
|
1363
|
+
block-size: 0;
|
|
1364
|
+
overflow: hidden;
|
|
1365
|
+
visibility: hidden;
|
|
1366
|
+
pointer-events: none;
|
|
1367
|
+
}
|
|
1368
|
+
.ui-master-detail-measure > span {
|
|
1369
|
+
display: block;
|
|
1370
|
+
inline-size: var(--master-detail-collapse-below);
|
|
1371
|
+
block-size: 0;
|
|
1372
|
+
}
|
|
1373
|
+
.ui-master-detail[data-mobile-compact][data-mobile-pane="master"] > .ui-master-detail-detail,
|
|
1374
|
+
.ui-master-detail[data-mobile-compact][data-mobile-pane="detail"] > .ui-master-detail-master {
|
|
1375
|
+
display: none;
|
|
1376
|
+
}
|
|
1377
|
+
.ui-master-detail[data-mobile-compact] > .ui-master-detail-master {
|
|
1378
|
+
max-block-size: none;
|
|
1379
|
+
overflow: visible;
|
|
1380
|
+
}
|
|
1381
|
+
.ui-master-detail[data-mobile-compact] .ui-master-detail-back {
|
|
1382
|
+
display: block;
|
|
1383
|
+
margin-block-end: var(--space-3);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
@@ -86,7 +86,8 @@
|
|
|
86
86
|
--timeline-grid-hour-height: 2.5rem;
|
|
87
87
|
--timeline-grid-axis-width: 3.25rem;
|
|
88
88
|
--timeline-grid-column-min-width: 6rem;
|
|
89
|
-
|
|
89
|
+
|
|
90
|
+
--timeline-grid-event-min-height-minutes: 36;
|
|
90
91
|
--timeline-grid-axis-font-size: var(--font-size-2xs);
|
|
91
92
|
--timeline-grid-head-font-size: var(--font-size-xs);
|
|
92
93
|
--timeline-grid-event-font-size: var(--font-size-2xs);
|
|
@@ -26,6 +26,27 @@ export default function Demo() {
|
|
|
26
26
|
subtitle="leading · title/description · trailing action · short entity lists inside a Card"
|
|
27
27
|
>
|
|
28
28
|
<Flex direction="col" gap="lg">
|
|
29
|
+
<Card>
|
|
30
|
+
<CardHeader>
|
|
31
|
+
<CardTitle level={2}>Navigation rows</CardTitle>
|
|
32
|
+
<CardDescription>
|
|
33
|
+
The whole row is one link, including its metadata. Current location uses aria-current.
|
|
34
|
+
</CardDescription>
|
|
35
|
+
</CardHeader>
|
|
36
|
+
<CardContent flush>
|
|
37
|
+
<ListRow
|
|
38
|
+
asChild
|
|
39
|
+
title="Roles and permissions"
|
|
40
|
+
description="Inspect organization access"
|
|
41
|
+
overflow="wrap"
|
|
42
|
+
>
|
|
43
|
+
<a href="?section=roles" aria-current="page" />
|
|
44
|
+
</ListRow>
|
|
45
|
+
<ListRow asChild title="Members" description="Assign roles to organization members">
|
|
46
|
+
<a href="?section=members" />
|
|
47
|
+
</ListRow>
|
|
48
|
+
</CardContent>
|
|
49
|
+
</Card>
|
|
29
50
|
<Card>
|
|
30
51
|
<CardHeader>
|
|
31
52
|
<CardTitle level={2}>アクティブなセッション Active sessions</CardTitle>
|
|
@@ -7,6 +7,7 @@ import { Flex, PageContainer } from "@godxjp/ui/layout";
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* SearchInput — debounced search box with a built-in clear button.
|
|
10
|
+
* onSearch runs after a changed query settles, never on mount.
|
|
10
11
|
* Always listen to onSearch (NOT onChange). Uncontrolled for local filters,
|
|
11
12
|
* controlled when search state lives in a URL param. Never a raw <input>.
|
|
12
13
|
* Composed only from real @godxjp/ui components.
|
|
@@ -10,3 +10,7 @@ For filter tabs / segmented toggles that show a per-option total, pass the `coun
|
|
|
10
10
|
(`<Button variant="outline" count={18}>Chờ bay</Button>`) instead of nesting a `Badge` —
|
|
11
11
|
it renders a borderless counter pill, formatted in the active locale and toned to the
|
|
12
12
|
button variant, so an outline button never ends up with a doubled border.
|
|
13
|
+
|
|
14
|
+
For long collection labels, use `wrap` with `align="start"` and `fullWidth`. The button
|
|
15
|
+
grows vertically while retaining the size preset's minimum target height. Keep badges
|
|
16
|
+
and other row metadata outside the button. `align="end"` follows the document direction.
|
|
@@ -15,6 +15,9 @@ import { Button, Text } from "@godxjp/ui/general";
|
|
|
15
15
|
import { Flex, MasterDetail, PageContainer } from "@godxjp/ui/layout";
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
+
* Pass mobilePane="master" or "detail" for one-pane navigation below the token-owned collapse threshold.
|
|
19
|
+
* Supply detailBack with a native back link; own selection in URL/router history.
|
|
20
|
+
* Omit mobilePane to retain the original stacked composition.
|
|
18
21
|
* MasterDetail · チーム画面 (SCR-110) の正準構成 — 流動的な一覧 + 固定幅の詳細レール。
|
|
19
22
|
*
|
|
20
23
|
* Shows: rail="detail" (既定 · 1fr / 320px) と rail="master" (先頭ナビゲーションレール)、
|
|
@@ -16,7 +16,7 @@ import { Flex, PageContainer, ResponsiveGrid, SplitPane } from "@godxjp/ui/layou
|
|
|
16
16
|
* with no external container-type declaration. Direct children are typically
|
|
17
17
|
* StatCard (self-contained bordered card · never wrap in Card/CardContent) or
|
|
18
18
|
* Card+CardContent for richer tile bodies. columns accepts a number OR breakpoint
|
|
19
|
-
* object { sm?, md?, lg? } — or use the named `preset` prop (e.g. "pricing-plans") for a
|
|
19
|
+
* object { base?, sm?, md?, lg? } — or use the named `preset` prop (e.g. "pricing-plans") for a
|
|
20
20
|
* recognised collection shape instead of hand-rolling the breakpoint map. Composed only from
|
|
21
21
|
* real @godxjp/ui components.
|
|
22
22
|
*/
|
|
@@ -27,6 +27,23 @@ export default function Demo() {
|
|
|
27
27
|
subtitle="等幅タイルグリッド · KPI・カード比較・ダッシュボード行"
|
|
28
28
|
>
|
|
29
29
|
<Flex direction="col" gap="lg">
|
|
30
|
+
<Card>
|
|
31
|
+
<CardHeader>
|
|
32
|
+
<CardTitle level={2}>Compact mobile metrics</CardTitle>
|
|
33
|
+
<CardDescription>
|
|
34
|
+
columns={{ base: 2, sm: 4 }} keeps two columns below the first
|
|
35
|
+
container breakpoint and four above it.
|
|
36
|
+
</CardDescription>
|
|
37
|
+
</CardHeader>
|
|
38
|
+
<CardContent>
|
|
39
|
+
<ResponsiveGrid columns={{ base: 2, sm: 4 }}>
|
|
40
|
+
<StatCard label="Services" value="3" />
|
|
41
|
+
<StatCard label="Members" value="14" />
|
|
42
|
+
<StatCard label="Invitations" value="0" />
|
|
43
|
+
<StatCard label="Invoices" value="2" />
|
|
44
|
+
</ResponsiveGrid>
|
|
45
|
+
</CardContent>
|
|
46
|
+
</Card>
|
|
30
47
|
<Card>
|
|
31
48
|
<CardHeader>
|
|
32
49
|
<CardTitle level={2}>コンテナクエリで折り返し(自己所有のクエリコンテナ)</CardTitle>
|
|
@@ -36,7 +53,7 @@ export default function Demo() {
|
|
|
36
53
|
折り返しの基準がコンテナ幅であることが分かる。ResponsiveGrid は自身のクエリコンテナ
|
|
37
54
|
(container-type: inline-size)を持つため、外側に @container を用意しなくても正しく
|
|
38
55
|
折り返す。閾値はコンテナ幅 40 / 48 / 64rem。 列間の gap は var(--space-stack-md)
|
|
39
|
-
|
|
56
|
+
が既定で、gap prop で変更できます。
|
|
40
57
|
</CardDescription>
|
|
41
58
|
</CardHeader>
|
|
42
59
|
<CardContent>
|