@godxjp/ui 19.4.2 → 19.6.0
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/app/app-provider.js +10 -1
- package/dist/app/theme-axes.d.ts +18 -3
- package/dist/app/theme-axes.js +15 -3
- package/dist/components/data-display/badge.js +4 -1
- package/dist/components/data-display/data-table.js +9 -2
- package/dist/components/data-entry/checkbox.d.ts +12 -0
- package/dist/components/data-entry/checkbox.js +34 -3
- package/dist/components/data-entry/index.d.ts +2 -0
- package/dist/components/data-entry/index.js +2 -0
- package/dist/components/data-entry/segmented.d.ts +2 -0
- package/dist/components/data-entry/segmented.js +5 -0
- package/dist/components/data-entry/select.d.ts +2 -0
- package/dist/components/data-entry/select.js +8 -3
- package/dist/components/feedback/sheet.js +1 -1
- package/dist/components/feedback/sonner.d.ts +7 -1
- package/dist/components/feedback/sonner.js +27 -7
- package/dist/components/general/button.js +6 -1
- package/dist/components/general/inert-background.d.ts +8 -3
- package/dist/components/general/inert-background.js +26 -2
- package/dist/components/general/logo.d.ts +53 -0
- package/dist/components/general/logo.js +57 -3
- package/dist/components/general/typography.d.ts +7 -1
- package/dist/components/general/typography.js +20 -5
- package/dist/components/layout/index.d.ts +4 -0
- package/dist/components/layout/index.js +4 -0
- package/dist/components/layout/nav-list.d.ts +26 -0
- package/dist/components/layout/nav-list.js +21 -0
- package/dist/components/layout/topbar-item.d.ts +17 -0
- package/dist/components/layout/topbar-item.js +21 -0
- package/dist/components/navigation/app-setting-picker.js +22 -5
- package/dist/components/navigation/context-menu.js +2 -2
- package/dist/components/navigation/dropdown-menu.js +2 -2
- package/dist/components/navigation/tabs.js +42 -10
- package/dist/components/ui/segmented.d.ts +48 -0
- package/dist/components/ui/segmented.js +48 -0
- package/dist/email/tokens.generated.d.ts +3 -3
- package/dist/email/tokens.generated.js +3 -3
- package/dist/i18n/messages/en.json +2 -1
- package/dist/i18n/messages/ja.json +2 -1
- package/dist/i18n/messages/vi.json +2 -1
- package/dist/props/components/app.prop.d.ts +9 -2
- package/dist/props/components/general.prop.d.ts +31 -1
- package/dist/props/components/layout.prop.d.ts +48 -2
- package/dist/props/registry.d.ts +19 -0
- package/dist/props/registry.js +27 -0
- package/dist/props/vocabulary/shared.prop.d.ts +11 -2
- package/dist/styles/alert-layout.css +4 -0
- package/dist/styles/base.css +2 -0
- package/dist/styles/control.css +94 -26
- package/dist/styles/data-display-layout.css +2 -1
- package/dist/styles/dialog-layout.css +2 -1
- package/dist/styles/focus-ring.css +85 -24
- package/dist/styles/layout.css +2 -1
- package/dist/styles/logo-layout.css +56 -0
- package/dist/styles/navigation-layout.css +15 -0
- package/dist/styles/shell-layout.css +64 -10
- package/dist/styles/text-layout.css +11 -0
- package/dist/tokens/antd.generated.css +40 -0
- package/dist/tokens/axes.css +6 -0
- package/dist/tokens/base.css +4 -0
- package/dist/tokens/components/control.css +5 -1
- package/dist/tokens/components/feedback.css +2 -0
- package/dist/tokens/components/legal-document.css +0 -2
- package/dist/tokens/components/logo.css +13 -0
- package/dist/tokens/components/navigation.css +2 -0
- package/dist/tokens/components/segmented.css +27 -0
- package/dist/tokens/components/shell.css +15 -2
- package/dist/tokens/components/text.css +7 -0
- package/dist/tokens/foundation.css +19 -12
- package/docs/DESIGN-AUTHORITY.md +346 -0
- package/docs/FRAME-COVERAGE-REPORT.md +5 -2
- package/docs/README.md +14 -13
- package/docs/data-entry/segmented.tsx +124 -0
- package/docs/general/typography.tsx +84 -1
- package/docs/layout/nav-list.tsx +63 -0
- package/docs/layout/topbar-item.tsx +157 -0
- package/package.json +8 -2
|
@@ -3,16 +3,21 @@ import { cn } from "../../lib/utils.js";
|
|
|
3
3
|
const Text = React.forwardRef(
|
|
4
4
|
({
|
|
5
5
|
as = "span",
|
|
6
|
+
asChild = false,
|
|
6
7
|
size = "sm",
|
|
7
|
-
|
|
8
|
+
// `link` is an affordance, not a colour: it only moves the DEFAULT tone, so
|
|
9
|
+
// `link tone="destructive"` is a destructive link rather than an argument between two rules.
|
|
10
|
+
tone,
|
|
8
11
|
weight = "regular",
|
|
9
12
|
align,
|
|
10
13
|
truncate,
|
|
11
14
|
clamp,
|
|
12
15
|
tabular,
|
|
13
16
|
mono,
|
|
17
|
+
link,
|
|
14
18
|
className,
|
|
15
19
|
style,
|
|
20
|
+
children,
|
|
16
21
|
...props
|
|
17
22
|
}, ref) => {
|
|
18
23
|
const clampLines = typeof clamp === "number" && Number.isFinite(clamp) && clamp >= 1 ? Math.floor(clamp) : void 0;
|
|
@@ -28,11 +33,11 @@ const Text = React.forwardRef(
|
|
|
28
33
|
);
|
|
29
34
|
}
|
|
30
35
|
}
|
|
31
|
-
|
|
32
|
-
ref,
|
|
36
|
+
const typography = {
|
|
33
37
|
"data-slot": "text",
|
|
34
38
|
"data-size": size,
|
|
35
|
-
"data-tone": tone,
|
|
39
|
+
"data-tone": tone ?? (link ? "primary" : "default"),
|
|
40
|
+
"data-link": link ? "" : void 0,
|
|
36
41
|
"data-weight": weight,
|
|
37
42
|
"data-align": align,
|
|
38
43
|
"data-truncate": truncate && clampLines === void 0 ? "" : void 0,
|
|
@@ -42,7 +47,17 @@ const Text = React.forwardRef(
|
|
|
42
47
|
"data-mono": mono ? "" : void 0,
|
|
43
48
|
className: cn("ui-text", className),
|
|
44
49
|
...props
|
|
45
|
-
}
|
|
50
|
+
};
|
|
51
|
+
if (asChild) {
|
|
52
|
+
const child = React.Children.only(children);
|
|
53
|
+
return React.cloneElement(child, {
|
|
54
|
+
...typography,
|
|
55
|
+
...child.props,
|
|
56
|
+
ref,
|
|
57
|
+
className: cn(typography.className, child.props.className)
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return React.createElement(as, { ref, ...typography }, children);
|
|
46
61
|
}
|
|
47
62
|
);
|
|
48
63
|
Text.displayName = "Text";
|
|
@@ -3,6 +3,8 @@ export type { PageContainerProp, PageContainerProps, BreadcrumbItem, BreadcrumbI
|
|
|
3
3
|
export { Flex } from "./flex.js";
|
|
4
4
|
export type { FlexAlignProp, FlexDirectionProp, FlexJustifyProp, FlexProp, FlexProps, } from "./flex.js";
|
|
5
5
|
export { ResizablePanel, ResizablePanelGroup, ResizableHandle } from "./resizable.js";
|
|
6
|
+
export { NavList } from "./nav-list.js";
|
|
7
|
+
export type { NavListProp, NavListProps } from "./nav-list.js";
|
|
6
8
|
export { AppShell } from "./app-shell.js";
|
|
7
9
|
export type { AppShellProps } from "./app-shell.js";
|
|
8
10
|
export { OrgSwitcher } from "./org-switcher.js";
|
|
@@ -37,6 +39,8 @@ export { createSidebarLink, Sidebar, SidebarHeader, SidebarItem, SidebarSection
|
|
|
37
39
|
export type { SidebarItemData, SidebarLinkComponentProp, SidebarLinkProp, SidebarProductProp as SidebarProduct, SidebarProp, SidebarProp as SidebarProps, SidebarRenderItemProp, SidebarSectionProp, } from "../../props/components/layout.prop.js";
|
|
38
40
|
export { Topbar } from "./topbar.js";
|
|
39
41
|
export type { TopbarProp, TopbarProps } from "./topbar.js";
|
|
42
|
+
export { TopbarItem } from "./topbar-item.js";
|
|
43
|
+
export type { TopbarItemProp, TopbarItemProps } from "./topbar-item.js";
|
|
40
44
|
export { ResponsiveGrid } from "./responsive-grid.js";
|
|
41
45
|
export type { ResponsiveGridProps } from "./responsive-grid.js";
|
|
42
46
|
export { MasterDetail } from "./master-detail.js";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PageContainer } from "./page-container.js";
|
|
2
2
|
import { Flex } from "./flex.js";
|
|
3
3
|
import { ResizablePanel, ResizablePanelGroup, ResizableHandle } from "./resizable.js";
|
|
4
|
+
import { NavList } from "./nav-list.js";
|
|
4
5
|
import { AppShell } from "./app-shell.js";
|
|
5
6
|
import { OrgSwitcher } from "./org-switcher.js";
|
|
6
7
|
import { AuthShell } from "./auth-shell.js";
|
|
@@ -16,6 +17,7 @@ import { ErrorSurface } from "./error-surface.js";
|
|
|
16
17
|
import { Breadcrumb } from "./breadcrumb.js";
|
|
17
18
|
import { createSidebarLink, Sidebar, SidebarHeader, SidebarItem, SidebarSection } from "./sidebar.js";
|
|
18
19
|
import { Topbar } from "./topbar.js";
|
|
20
|
+
import { TopbarItem } from "./topbar-item.js";
|
|
19
21
|
import { ResponsiveGrid } from "./responsive-grid.js";
|
|
20
22
|
import { MasterDetail } from "./master-detail.js";
|
|
21
23
|
import { SplitPane } from "./split-pane.js";
|
|
@@ -40,6 +42,7 @@ export {
|
|
|
40
42
|
LegalDocumentShell,
|
|
41
43
|
MasterDetail,
|
|
42
44
|
MobileShell,
|
|
45
|
+
NavList,
|
|
43
46
|
OrgSwitcher,
|
|
44
47
|
PageContainer,
|
|
45
48
|
ResizableHandle,
|
|
@@ -54,5 +57,6 @@ export {
|
|
|
54
57
|
SidebarSection,
|
|
55
58
|
SplitPane,
|
|
56
59
|
Topbar,
|
|
60
|
+
TopbarItem,
|
|
57
61
|
createSidebarLink
|
|
58
62
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { NavListProp } from "../../props/components/layout.prop.js";
|
|
3
|
+
export type { NavListProp };
|
|
4
|
+
export type NavListProps = NavListProp;
|
|
5
|
+
/**
|
|
6
|
+
* NavList — a vertical route navigation that is NOT shell furniture (gh#374).
|
|
7
|
+
*
|
|
8
|
+
* `Sidebar` owns the app's primary rail and renders into `AppShell`'s grid, so it cannot be nested
|
|
9
|
+
* in a page. The settings-nav shape — a column of links beside the pane they drive, one of them
|
|
10
|
+
* current — had no primitive, which left every consumer hand-rolling rows out of `Button`s with the
|
|
11
|
+
* current one encoded as a variant swap: no `aria-current`, no icon column, a different invention
|
|
12
|
+
* per app.
|
|
13
|
+
*
|
|
14
|
+
* This is the SAME row as the rail, deliberately: it composes `SidebarItem`, so icon slot, label,
|
|
15
|
+
* badge, active tokens and `aria-current="page"` are one implementation shared by both, and a
|
|
16
|
+
* consumer that already learnt `SidebarItemProp` / `linkComponent` for the rail knows this too.
|
|
17
|
+
* Only the container differs — a `<nav>` landmark instead of the shell's grid area, and no
|
|
18
|
+
* collapsed rail (a page-level nav has no rail to collapse into).
|
|
19
|
+
*/
|
|
20
|
+
export declare const NavList: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLElement>, "onSelect"> & {
|
|
21
|
+
items: import("../../props/index.js").SidebarItemProp[];
|
|
22
|
+
activeId?: string;
|
|
23
|
+
label: string;
|
|
24
|
+
linkComponent?: import("./sidebar.js").SidebarLinkComponentProp;
|
|
25
|
+
onSelect?: (id: string) => void;
|
|
26
|
+
} & React.RefAttributes<HTMLElement>>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../../lib/utils.js";
|
|
5
|
+
import { SidebarItem } from "./sidebar.js";
|
|
6
|
+
const NavList = React.forwardRef(
|
|
7
|
+
({ items, activeId, label, linkComponent, onSelect, className, ...props }, ref) => /* @__PURE__ */ jsx("nav", { ref, "aria-label": label, className: cn("ui-nav-list", className), ...props, children: items.map((item) => /* @__PURE__ */ jsx(
|
|
8
|
+
SidebarItem,
|
|
9
|
+
{
|
|
10
|
+
item,
|
|
11
|
+
active: item.id === activeId,
|
|
12
|
+
linkComponent,
|
|
13
|
+
onActivate: onSelect
|
|
14
|
+
},
|
|
15
|
+
item.id
|
|
16
|
+
)) })
|
|
17
|
+
);
|
|
18
|
+
NavList.displayName = "NavList";
|
|
19
|
+
export {
|
|
20
|
+
NavList
|
|
21
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type { TopbarItemProp, TopbarItemProp as TopbarItemProps, } from "../../props/components/layout.prop.js";
|
|
3
|
+
/**
|
|
4
|
+
* TopbarItem — one interactive cell of a {@link Topbar} slot, shaped like part of the bar.
|
|
5
|
+
*
|
|
6
|
+
* Full bar height, the bar's own hover surface, and the focus mark hosted INSIDE the cell (a
|
|
7
|
+
* full-bleed cell has nothing outside itself to ring, and the bar clips its own overflow). All of
|
|
8
|
+
* that lives in `.ui-topbar-item`; this component only picks the element and the classes.
|
|
9
|
+
*
|
|
10
|
+
* `ui-focus-ring` is the ONE focus source (styles/focus-ring.css) — the cell hosts whatever that
|
|
11
|
+
* file resolves, including nothing at all while the `--focus-outline` switch ships off.
|
|
12
|
+
*/
|
|
13
|
+
export declare const TopbarItem: React.ForwardRefExoticComponent<Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> & {
|
|
14
|
+
asChild?: boolean;
|
|
15
|
+
children?: React.ReactNode;
|
|
16
|
+
className?: import("../../props/index.js").ClassNameProp;
|
|
17
|
+
} & React.RefAttributes<HTMLButtonElement>>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
5
|
+
import { cn } from "../../lib/utils.js";
|
|
6
|
+
const TopbarItem = React.forwardRef(function TopbarItem2({ asChild = false, className, type, ...props }, ref) {
|
|
7
|
+
const Comp = asChild ? Slot : "button";
|
|
8
|
+
return /* @__PURE__ */ jsx(
|
|
9
|
+
Comp,
|
|
10
|
+
{
|
|
11
|
+
ref,
|
|
12
|
+
"data-slot": "topbar-item",
|
|
13
|
+
className: cn("ui-topbar-item ui-focus-ring", className),
|
|
14
|
+
type: asChild ? void 0 : type ?? "button",
|
|
15
|
+
...props
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
export {
|
|
20
|
+
TopbarItem
|
|
21
|
+
};
|
|
@@ -135,19 +135,36 @@ const AppSettingPicker = React.forwardRef(
|
|
|
135
135
|
// sits among ghost icon buttons (the topbar's sidebar-toggle/notifications/account
|
|
136
136
|
// triggers), so it drops controlTriggerClass's form-input chrome (border/bg/shadow)
|
|
137
137
|
// at rest and adopts the same ghost hover — a resting border here read as visually
|
|
138
|
-
// inconsistent next to its borderless topbar siblings. The open-state
|
|
139
|
-
//
|
|
140
|
-
// focus-
|
|
141
|
-
//
|
|
138
|
+
// inconsistent next to its borderless topbar siblings. The open-state and
|
|
139
|
+
// focus-visible rings are untouched, so keyboard and "is this open" affordance
|
|
140
|
+
// still hold: both come from styles/focus-ring.css, which also excludes this
|
|
141
|
+
// variant from the bordered-field rebind precisely BECAUSE it has no resting
|
|
142
|
+
// border to recolour, so it keeps the opaque ring form.
|
|
142
143
|
// Bề ngang phải là UTILITY, không phải luật class. SelectTrigger phát `w-full`,
|
|
143
144
|
// mà utility nằm sau components trong thứ tự layer nên `inline-size` khai trong
|
|
144
145
|
// .ui-app-setting-picker-icon luôn thua. Trong một khe co theo nội dung của
|
|
145
146
|
// topbar, `width: 100%` co lại bằng chính nội dung, tức 18px thay vì ô vuông
|
|
146
147
|
// --control-height. Nhánh có nhãn ngay dưới đã phải tự vệ đúng như vậy bằng
|
|
147
148
|
// `w-auto`. Giá trị vẫn đọc token chứ không phải một con số.
|
|
149
|
+
// `justify-content` đi cùng lý do đó: SelectTrigger phát `justify-between`,
|
|
150
|
+
// với một con duy nhất thì nó dồn glyph sát viền trái (khe 1px/15px trong ô
|
|
151
|
+
// 32px). `justify-content: center` khai trong .ui-app-setting-picker-icon
|
|
152
|
+
// cũng nằm ở @layer components nên cũng thua. Cả hai trục của cái hộp này
|
|
153
|
+
// phải do lớp utility sở hữu, không được để sót nửa nào lại trong luật class.
|
|
154
|
+
// `shrink-0` là NỬA CÒN LẠI của trục ngang, không phải trang trí. `w-*` chỉ khai
|
|
155
|
+
// kích thước MONG MUỐN; trong một flex row chật, `flex-shrink: 1` mặc định vẫn
|
|
156
|
+
// ép nó nhỏ hơn. Đo thật trong Chromium ở chính frame tài liệu của component:
|
|
157
|
+
// `.ui-topbar-end` rộng 82px, gap 8px, hai trigger cùng co → ô vuông ra
|
|
158
|
+
// 21,28 × 32px thay vì 32 × 32. Đây không phải gh#366 quay lại: `inline-size`
|
|
159
|
+
// vẫn giải đúng --control-height; cái hỏng là co giãn. Một tap target vuông
|
|
160
|
+
// biến thành hình chữ nhật 21px mỗi khi topbar chật là lỗi thật, và nó kéo
|
|
161
|
+
// theo cả vùng chạm. Nhánh CÓ NHÃN cố tình KHÔNG nhận `shrink-0`: nó rộng theo
|
|
162
|
+
// nội dung và mang sẵn `max-w-full` để được phép thu lại.
|
|
163
|
+
// Vẫn phải là utility vì cùng lý do với `w-*`: luật trong @layer components
|
|
164
|
+
// thua lớp utilities mà SelectTrigger phát ra.
|
|
148
165
|
cn(
|
|
149
166
|
"ui-app-setting-picker-icon hover:bg-accent hover:text-accent-foreground",
|
|
150
|
-
"w-[length:var(--control-height)]"
|
|
167
|
+
"w-[length:var(--control-height)] shrink-0 justify-center"
|
|
151
168
|
)
|
|
152
169
|
) : (
|
|
153
170
|
// Labeled: sized to a per-kind width from `sm` up; below `sm` it hugs its content and
|
|
@@ -11,11 +11,11 @@ function ContextMenuTrigger(props) {
|
|
|
11
11
|
}
|
|
12
12
|
const ContextMenuPortal = ContextMenuPrimitive.Portal;
|
|
13
13
|
const ContextMenuContent = React.forwardRef(({ className, ...props }, ref) => {
|
|
14
|
-
useInertHiddenBackground();
|
|
14
|
+
const contentRef = useInertHiddenBackground(ref);
|
|
15
15
|
return /* @__PURE__ */ jsx(ContextMenuPrimitive.Portal, { children: /* @__PURE__ */ jsx(
|
|
16
16
|
ContextMenuPrimitive.Content,
|
|
17
17
|
{
|
|
18
|
-
ref,
|
|
18
|
+
ref: contentRef,
|
|
19
19
|
"data-slot": "context-menu-content",
|
|
20
20
|
className: cn("ui-context-menu-content", className),
|
|
21
21
|
...props
|
|
@@ -24,11 +24,11 @@ function DropdownMenuSub(props) {
|
|
|
24
24
|
return /* @__PURE__ */ jsx(DropdownMenuPrimitive.Sub, { "data-slot": "dropdown-menu-sub", ...props });
|
|
25
25
|
}
|
|
26
26
|
const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => {
|
|
27
|
-
useInertHiddenBackground();
|
|
27
|
+
const contentRef = useInertHiddenBackground(ref);
|
|
28
28
|
return /* @__PURE__ */ jsx(DropdownMenuPortal, { children: /* @__PURE__ */ jsx(
|
|
29
29
|
DropdownMenuPrimitive.Content,
|
|
30
30
|
{
|
|
31
|
-
ref,
|
|
31
|
+
ref: contentRef,
|
|
32
32
|
"data-slot": "dropdown-menu-content",
|
|
33
33
|
sideOffset,
|
|
34
34
|
className: cn(
|
|
@@ -47,9 +47,28 @@ function Tabs({
|
|
|
47
47
|
"data-slot": "tabs-list",
|
|
48
48
|
variant: variant === "line" ? "line" : "default",
|
|
49
49
|
className: cn(
|
|
50
|
-
// The
|
|
51
|
-
// stay a UTILITY so tailwind-merge still drops the strip's own padding step
|
|
52
|
-
|
|
50
|
+
// The inset override is an arbitrary value reading the knob (never `p-0`): it must
|
|
51
|
+
// stay a UTILITY so tailwind-merge still drops the strip's own padding step.
|
|
52
|
+
//
|
|
53
|
+
// IT IS SPLIT INTO `px-`/`py-` FOR THE FOCUS RING (gh#376). The strip clips its
|
|
54
|
+
// block axis (`overflow-y: hidden` beside `overflow-x: auto`), and this variant's
|
|
55
|
+
// inset is 0px, so a focused trigger had NO block headroom at all — measured in
|
|
56
|
+
// Chromium, 0px top and bottom, i.e. the entire ring was shaved. The headroom has to
|
|
57
|
+
// come from layout: `overflow-y: visible` next to `overflow-x: auto` computes back
|
|
58
|
+
// to `auto` per spec (a second scroll container, not a ring), and Chromium honours
|
|
59
|
+
// `overflow-clip-margin` only when BOTH axes are `clip`. So the block padding carries
|
|
60
|
+
// the ring's outer reach and an equal negative block margin hands it straight back —
|
|
61
|
+
// the ring paints inside the scrollport and the strip's OUTER box is unchanged
|
|
62
|
+
// (measured: 38px before and after). Both values read the same token the ring is
|
|
63
|
+
// sized from, so the two can never drift apart.
|
|
64
|
+
//
|
|
65
|
+
// The cost, recorded rather than hidden: the rail hairline (`border-b`, drawn at the
|
|
66
|
+
// border box) now sits the ring's reach below the triggers instead of flush. That is
|
|
67
|
+
// where a hand-composed <TabsList variant="line"> already put it — its `p-1` gives
|
|
68
|
+
// the same block inset — so the two construction paths now agree. A service that
|
|
69
|
+
// wants the active bar parked back on the hairline raises `--tabs-indicator-offset`,
|
|
70
|
+
// which exists for exactly that.
|
|
71
|
+
variant === "line" && "my-[calc(-1_*_var(--tabs-list-focus-ring-space-inset,calc(var(--focus-ring-width)_+_var(--focus-ring-glow-width))))] h-auto w-full justify-start border-b px-[var(--tabs-list-line-space-inset)] py-[calc(var(--tabs-list-line-space-inset)_+_var(--tabs-list-focus-ring-space-inset,calc(var(--focus-ring-width)_+_var(--focus-ring-glow-width))))]",
|
|
53
72
|
variant === "card" && "w-full justify-start",
|
|
54
73
|
listClassName
|
|
55
74
|
),
|
|
@@ -134,12 +153,25 @@ const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => /* @__PUR
|
|
|
134
153
|
ref,
|
|
135
154
|
"data-slot": "tabs-trigger",
|
|
136
155
|
className: cn(
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
|
|
156
|
+
// SELECTED IS A BORDER TINT, NOT A RING — and that is a WCAG fix, not a style preference.
|
|
157
|
+
// It used to be `ring-1 ring-primary/25`, which writes `--tw-ring-shadow` in the UTILITIES
|
|
158
|
+
// layer and therefore overwrote the focus ring that focus-ring.css feeds from `components`.
|
|
159
|
+
// Measured: a focused ACTIVE tab painted `oklab(… / 0.25) 0 0 0 1px` and nothing else — a
|
|
160
|
+
// 1px indicator at 25% alpha, failing both clauses of SC 2.4.13 (2px perimeter, ≥3:1),
|
|
161
|
+
// while every other control in the library carried a full 2px ring. The trigger already
|
|
162
|
+
// owns `border border-transparent`, so tinting that border reproduces the selected hairline
|
|
163
|
+
// at the same colour and width with no layout change, and leaves `--tw-ring-shadow` free for
|
|
164
|
+
// the focus ring. `shadow-sm` stays: its composite READS `--tw-ring-shadow`, so the lift and
|
|
165
|
+
// the ring coexist.
|
|
166
|
+
//
|
|
167
|
+
// No `focus-visible:outline-1` either. It was the fallback that survived the clobber — a 1px
|
|
168
|
+
// currentColor line squeezed between the border and the ring once both paint. The ring is
|
|
169
|
+
// the indicator now; two marks for one state is what this pass exists to remove.
|
|
170
|
+
//
|
|
171
|
+
// The line indicator lives in src/styles/navigation-layout.css so it reads --tabs-indicator-*.
|
|
172
|
+
// Selected and focused stay visually distinct (WCAG 2.4.7): selected is a 1px hairline in the
|
|
173
|
+
// border, focused is the 2px ring plus its halo outside it.
|
|
174
|
+
"text-muted-foreground ring-offset-background hover:text-foreground ui-focus-ring data-[state=active]:bg-background data-[state=active]:text-foreground group-data-[variant=default]/tabs-list:data-[state=active]:border-primary/25 relative inline-flex flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-3 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start group-data-[variant=line]/tabs-list:border-e-0 group-data-[variant=line]/tabs-list:border-b-0 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none",
|
|
143
175
|
className
|
|
144
176
|
),
|
|
145
177
|
...props
|
|
@@ -151,7 +183,7 @@ const TabsContent = React.forwardRef(({ className, ...props }, ref) => /* @__PUR
|
|
|
151
183
|
{
|
|
152
184
|
ref,
|
|
153
185
|
"data-slot": "tabs-content",
|
|
154
|
-
className: cn("focus-
|
|
186
|
+
className: cn("ui-focus-ring flex-1 outline-none", className),
|
|
155
187
|
...props
|
|
156
188
|
}
|
|
157
189
|
));
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/** One choice in a {@link Segmented}. */
|
|
3
|
+
export type SegmentedOption = {
|
|
4
|
+
/** Wire value — what `onValueChange` reports and what a form submits. */
|
|
5
|
+
value: string;
|
|
6
|
+
/** Visible label. It is also the item's accessible name, so it is required. */
|
|
7
|
+
label: React.ReactNode;
|
|
8
|
+
/** Optional leading glyph, rendered `aria-hidden` beside the label. */
|
|
9
|
+
icon?: React.ReactNode;
|
|
10
|
+
/** Disable this one choice; the rest of the group stays operable. */
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type SegmentedProp = {
|
|
14
|
+
/** The closed set of choices, in reading order. */
|
|
15
|
+
options: readonly SegmentedOption[];
|
|
16
|
+
/** Controlled selection. */
|
|
17
|
+
value?: string;
|
|
18
|
+
/** Uncontrolled initial selection. */
|
|
19
|
+
defaultValue?: string;
|
|
20
|
+
onValueChange?: (value: string) => void;
|
|
21
|
+
/** Disable the whole group. */
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
/** Form field name — submits the selected value with the form. */
|
|
24
|
+
name?: string;
|
|
25
|
+
id?: string;
|
|
26
|
+
className?: string;
|
|
27
|
+
/** Accessible name of the group. Required unless a visible label points at `id`. */
|
|
28
|
+
"aria-label"?: string;
|
|
29
|
+
"aria-labelledby"?: string;
|
|
30
|
+
};
|
|
31
|
+
export type SegmentedProps = SegmentedProp;
|
|
32
|
+
/**
|
|
33
|
+
* Segmented — one-of-N from a small, closed, always-visible set. antd's `Segmented`
|
|
34
|
+
* (docs/DESIGN-AUTHORITY.md names Ant Design the taxonomy authority) drawn on Radix's RadioGroup,
|
|
35
|
+
* which is this repo's authority for behaviour primitives.
|
|
36
|
+
*
|
|
37
|
+
* WHY NOT `ToggleGroup`. A ToggleGroup is a row of PRESSED buttons: `aria-pressed`, independently
|
|
38
|
+
* togglable, and — even at `type="single"` — deselectable, so "no theme at all" is a state the
|
|
39
|
+
* markup permits. A segmented control is a radio group: exactly one member is always chosen, the
|
|
40
|
+
* arrow keys move between members rather than Tab, and a screen reader must say "Light, radio
|
|
41
|
+
* button, 1 of 3, selected" and not "Light, toggle button, pressed". WAI-ARIA APG owns that
|
|
42
|
+
* distinction and it is not a skin.
|
|
43
|
+
*
|
|
44
|
+
* The primitive gives roving tabindex, arrow-key traversal (RTL-aware), the radiogroup/radio roles
|
|
45
|
+
* and the hidden input a native form submit needs. This file adds the antd geometry and nothing
|
|
46
|
+
* else — the focus mark comes from `ui-focus-ring`, the ONE source in styles/focus-ring.css.
|
|
47
|
+
*/
|
|
48
|
+
export declare const Segmented: React.ForwardRefExoticComponent<SegmentedProp & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
|
5
|
+
import { cn } from "../../lib/utils.js";
|
|
6
|
+
const Segmented = React.forwardRef(function Segmented2({ options, value, defaultValue, onValueChange, disabled, name, id, className, ...props }, ref) {
|
|
7
|
+
return /* @__PURE__ */ jsx(
|
|
8
|
+
RadioGroupPrimitive.Root,
|
|
9
|
+
{
|
|
10
|
+
ref,
|
|
11
|
+
id,
|
|
12
|
+
"data-slot": "segmented",
|
|
13
|
+
className: cn("ui-segmented", className),
|
|
14
|
+
orientation: "horizontal",
|
|
15
|
+
value,
|
|
16
|
+
defaultValue,
|
|
17
|
+
onValueChange,
|
|
18
|
+
disabled,
|
|
19
|
+
name,
|
|
20
|
+
...props,
|
|
21
|
+
children: options.map((option) => /* @__PURE__ */ jsxs(
|
|
22
|
+
RadioGroupPrimitive.Item,
|
|
23
|
+
{
|
|
24
|
+
value: option.value,
|
|
25
|
+
disabled: option.disabled,
|
|
26
|
+
"data-slot": "segmented-item",
|
|
27
|
+
className: "ui-segmented-item ui-focus-ring",
|
|
28
|
+
children: [
|
|
29
|
+
option.icon == null ? null : /* @__PURE__ */ jsx(
|
|
30
|
+
"span",
|
|
31
|
+
{
|
|
32
|
+
"data-slot": "segmented-item-icon",
|
|
33
|
+
className: "ui-segmented-item-icon",
|
|
34
|
+
"aria-hidden": "true",
|
|
35
|
+
children: option.icon
|
|
36
|
+
}
|
|
37
|
+
),
|
|
38
|
+
/* @__PURE__ */ jsx("span", { "data-slot": "segmented-item-label", className: "ui-segmented-item-label", children: option.label })
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
option.value
|
|
42
|
+
))
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
export {
|
|
47
|
+
Segmented
|
|
48
|
+
};
|
|
@@ -44,8 +44,8 @@ export declare const EMAIL_COLOR_SOURCE: {
|
|
|
44
44
|
readonly hsl: "60 33% 99%";
|
|
45
45
|
};
|
|
46
46
|
readonly focus: {
|
|
47
|
-
readonly cssVar: "--
|
|
48
|
-
readonly hsl: "204 100%
|
|
47
|
+
readonly cssVar: "--primary";
|
|
48
|
+
readonly hsl: "204 100% 37%";
|
|
49
49
|
};
|
|
50
50
|
readonly brand: {
|
|
51
51
|
readonly cssVar: "--brand";
|
|
@@ -103,7 +103,7 @@ export declare const EMAIL_COLOR_SOURCE_DARK: {
|
|
|
103
103
|
readonly hsl: "48 9% 9%";
|
|
104
104
|
};
|
|
105
105
|
readonly focus: {
|
|
106
|
-
readonly cssVar: "--
|
|
106
|
+
readonly cssVar: "--primary";
|
|
107
107
|
readonly hsl: "204 90% 60%";
|
|
108
108
|
};
|
|
109
109
|
readonly brand: {
|
|
@@ -36,8 +36,8 @@ const EMAIL_COLOR_SOURCE = {
|
|
|
36
36
|
"hsl": "60 33% 99%"
|
|
37
37
|
},
|
|
38
38
|
"focus": {
|
|
39
|
-
"cssVar": "--
|
|
40
|
-
"hsl": "204 100%
|
|
39
|
+
"cssVar": "--primary",
|
|
40
|
+
"hsl": "204 100% 37%"
|
|
41
41
|
},
|
|
42
42
|
"brand": {
|
|
43
43
|
"cssVar": "--brand",
|
|
@@ -94,7 +94,7 @@ const EMAIL_COLOR_SOURCE_DARK = {
|
|
|
94
94
|
"hsl": "48 9% 9%"
|
|
95
95
|
},
|
|
96
96
|
"focus": {
|
|
97
|
-
"cssVar": "--
|
|
97
|
+
"cssVar": "--primary",
|
|
98
98
|
"hsl": "204 90% 60%"
|
|
99
99
|
},
|
|
100
100
|
"brand": {
|
|
@@ -28,7 +28,11 @@ export type AppProviderProp = {
|
|
|
28
28
|
storageKey?: string;
|
|
29
29
|
/** Persist user choices. Default: true. */
|
|
30
30
|
persist?: boolean;
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Initial theme choice. `"light"` / `"dark"` are written straight to `<html data-theme>`;
|
|
33
|
+
* `"system"` defers to `prefers-color-scheme` and is re-resolved whenever the OS changes.
|
|
34
|
+
* Default: `"light"`.
|
|
35
|
+
*/
|
|
32
36
|
theme?: AppTheme;
|
|
33
37
|
/**
|
|
34
38
|
* Initial brand palette preset — written to `<html data-brand>`. OPT-IN: omit
|
|
@@ -101,7 +105,10 @@ export type AppContextValue = {
|
|
|
101
105
|
requestHeaders: AppRequestHeaders;
|
|
102
106
|
/** Configured timezone list; `undefined` → full IANA in the timezone-picker recipe. */
|
|
103
107
|
timezoneOptions?: readonly AppTimezone[];
|
|
104
|
-
/**
|
|
108
|
+
/**
|
|
109
|
+
* Current theme axes (mirror `<html data-*>` / inline `--scaling`). `theme` is the user's
|
|
110
|
+
* CHOICE — it can be `"system"`, which `<html data-theme>` never is.
|
|
111
|
+
*/
|
|
105
112
|
theme: AppTheme;
|
|
106
113
|
brand: AppBrand | null;
|
|
107
114
|
density: AppDensity;
|
|
@@ -4,7 +4,25 @@ import type { ActivityAnnounceProp, ActivityVariantProp, AsChildProp, ButtonSize
|
|
|
4
4
|
/** @see Text — typographic primitive; replaces hand-rolled `<span className="text-[13px] …">`. */
|
|
5
5
|
export type TextProp = Omit<React.HTMLAttributes<HTMLElement>, "color"> & {
|
|
6
6
|
/** Render element. Default `span`. */
|
|
7
|
-
as?: "span" | "p" | "div" | "label" | "strong" | "em" | "small" | "code" | "kbd" | "dt" | "dd" | "caption" | "abbr";
|
|
7
|
+
as?: "span" | "p" | "div" | "a" | "label" | "strong" | "em" | "small" | "code" | "kbd" | "dt" | "dd" | "caption" | "abbr";
|
|
8
|
+
/**
|
|
9
|
+
* Render the typography onto the child element instead of emitting one — for a router link
|
|
10
|
+
* (`<Text asChild link><Link href=…>…</Link></Text>`). The child owns the element and its
|
|
11
|
+
* navigation; Text owns the type step, tone, weight and truncation.
|
|
12
|
+
*/
|
|
13
|
+
asChild?: AsChildProp;
|
|
14
|
+
/**
|
|
15
|
+
* This text IS a link: underline on hover and on keyboard focus, at the token underline offset,
|
|
16
|
+
* and the focus mark every other interactive element draws.
|
|
17
|
+
*
|
|
18
|
+
* It is an AFFORDANCE, not a colour — `tone` still owns the colour and simply defaults to
|
|
19
|
+
* `primary` here, so a destructive link (`link tone="destructive"`) reads destructive and still
|
|
20
|
+
* underlines. Use this INSTEAD of `className="text-primary hover:underline"`, and instead of
|
|
21
|
+
* `Button variant="link"` whenever the link sits in running content: `.ui-button` is a control
|
|
22
|
+
* box (`white-space: nowrap`, `flex-shrink: 0`, a `--control-height` tier and inline padding),
|
|
23
|
+
* so in a table cell it cannot wrap and cannot share the cell's line height.
|
|
24
|
+
*/
|
|
25
|
+
link?: boolean;
|
|
8
26
|
/** Size from the type scale — never an arbitrary px. Default `sm` (base). */
|
|
9
27
|
size?: TextSizeProp;
|
|
10
28
|
/** Semantic colour intent. Default `default` (foreground). */
|
|
@@ -24,6 +42,18 @@ export type TextProp = Omit<React.HTMLAttributes<HTMLElement>, "color"> & {
|
|
|
24
42
|
/** Monospace family (codes, ids). */
|
|
25
43
|
mono?: boolean;
|
|
26
44
|
htmlFor?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Anchor attributes, for `as="a"` (and for the `<a>` a router link supplies under `asChild`).
|
|
47
|
+
*
|
|
48
|
+
* Declared explicitly rather than by widening the base to `AnchorHTMLAttributes`, and for the
|
|
49
|
+
* same reason `htmlFor` is declared explicitly for `as="label"`: the element union is the
|
|
50
|
+
* contract, so each polymorphic branch names the attributes it actually accepts instead of every
|
|
51
|
+
* span silently offering an `href` it will never render.
|
|
52
|
+
*/
|
|
53
|
+
href?: string;
|
|
54
|
+
target?: React.HTMLAttributeAnchorTarget;
|
|
55
|
+
rel?: string;
|
|
56
|
+
download?: React.AnchorHTMLAttributes<HTMLAnchorElement>["download"];
|
|
27
57
|
};
|
|
28
58
|
/** @see Heading — h1..h4 sized from the `--heading-h*` tokens. */
|
|
29
59
|
export type HeadingProp = Omit<React.HTMLAttributes<HTMLHeadingElement>, "color"> & {
|