@herongxhr/ui-react 0.0.0-stage → 0.1.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.
@@ -0,0 +1 @@
1
+ export declare function withClass<State>(base: string, className?: string | ((state: State) => string | undefined)): string | ((state: State) => string);
@@ -0,0 +1,5 @@
1
+ export function withClass(base, className) {
2
+ return typeof className === 'function'
3
+ ? (state) => [base, className(state)].filter(Boolean).join(' ')
4
+ : [base, className].filter(Boolean).join(' ');
5
+ }
@@ -0,0 +1,22 @@
1
+ import type { ComponentProps } from 'react';
2
+ import { Dialog as BaseDialog } from '@base-ui/react/dialog';
3
+ export declare const DialogRoot: <Payload>(props: BaseDialog.Root.Props<Payload>) => import("react").JSX.Element;
4
+ export declare const DialogTrigger: BaseDialog.Trigger;
5
+ export declare const DialogPortal: import("react").ForwardRefExoticComponent<Omit<import("@base-ui/react/dialog").DialogPortalProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
6
+ export declare const DialogClose: import("react").ForwardRefExoticComponent<Omit<import("@base-ui/react/dialog").DialogCloseProps, "ref"> & import("react").RefAttributes<HTMLButtonElement>>;
7
+ export declare const DialogTitle: import("react").ForwardRefExoticComponent<Omit<import("@base-ui/react/dialog").DialogTitleProps, "ref"> & import("react").RefAttributes<HTMLHeadingElement>>;
8
+ export declare const DialogDescription: import("react").ForwardRefExoticComponent<Omit<import("@base-ui/react/dialog").DialogDescriptionProps, "ref"> & import("react").RefAttributes<HTMLParagraphElement>>;
9
+ export declare function DialogBackdrop({ className, ...props }: ComponentProps<typeof BaseDialog.Backdrop>): import("react").JSX.Element;
10
+ export declare function DialogViewport({ className, ...props }: ComponentProps<typeof BaseDialog.Viewport>): import("react").JSX.Element;
11
+ export declare function DialogPopup({ className, ...props }: ComponentProps<typeof BaseDialog.Popup>): import("react").JSX.Element;
12
+ export declare const Dialog: {
13
+ Root: <Payload>(props: BaseDialog.Root.Props<Payload>) => import("react").JSX.Element;
14
+ Trigger: BaseDialog.Trigger;
15
+ Portal: import("react").ForwardRefExoticComponent<Omit<import("@base-ui/react/dialog").DialogPortalProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
16
+ Close: import("react").ForwardRefExoticComponent<Omit<import("@base-ui/react/dialog").DialogCloseProps, "ref"> & import("react").RefAttributes<HTMLButtonElement>>;
17
+ Title: import("react").ForwardRefExoticComponent<Omit<import("@base-ui/react/dialog").DialogTitleProps, "ref"> & import("react").RefAttributes<HTMLHeadingElement>>;
18
+ Description: import("react").ForwardRefExoticComponent<Omit<import("@base-ui/react/dialog").DialogDescriptionProps, "ref"> & import("react").RefAttributes<HTMLParagraphElement>>;
19
+ Backdrop: typeof DialogBackdrop;
20
+ Viewport: typeof DialogViewport;
21
+ Popup: typeof DialogPopup;
22
+ };
package/dist/dialog.js ADDED
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Dialog as BaseDialog } from '@base-ui/react/dialog';
3
+ import { withClass } from './classes.js';
4
+ export const DialogRoot = BaseDialog.Root;
5
+ export const DialogTrigger = BaseDialog.Trigger;
6
+ export const DialogPortal = BaseDialog.Portal;
7
+ export const DialogClose = BaseDialog.Close;
8
+ export const DialogTitle = BaseDialog.Title;
9
+ export const DialogDescription = BaseDialog.Description;
10
+ export function DialogBackdrop({ className, ...props }) {
11
+ return _jsx(BaseDialog.Backdrop, { ...props, className: withClass('hrui-dialog-backdrop', className) });
12
+ }
13
+ export function DialogViewport({ className, ...props }) {
14
+ return _jsx(BaseDialog.Viewport, { ...props, className: withClass('hrui-dialog-viewport', className) });
15
+ }
16
+ export function DialogPopup({ className, ...props }) {
17
+ return _jsx(BaseDialog.Popup, { ...props, className: withClass('hrui-dialog-popup', className) });
18
+ }
19
+ export const Dialog = {
20
+ Root: DialogRoot, Trigger: DialogTrigger, Portal: DialogPortal, Close: DialogClose,
21
+ Title: DialogTitle, Description: DialogDescription, Backdrop: DialogBackdrop,
22
+ Viewport: DialogViewport, Popup: DialogPopup,
23
+ };
@@ -0,0 +1,5 @@
1
+ export * from './select.js';
2
+ export * from './dialog.js';
3
+ export * from './theme-select.js';
4
+ export * from './theme-toggle.js';
5
+ export { CSPProvider } from '@base-ui/react/csp-provider';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from './select.js';
2
+ export * from './dialog.js';
3
+ export * from './theme-select.js';
4
+ export * from './theme-toggle.js';
5
+ export { CSPProvider } from '@base-ui/react/csp-provider';
@@ -0,0 +1,46 @@
1
+ import { type ComponentProps } from 'react';
2
+ import { Select as BaseSelect } from '@base-ui/react/select';
3
+ export declare const SelectRoot: typeof BaseSelect.Root;
4
+ export declare const SelectValue: import("react").ForwardRefExoticComponent<Omit<import("@base-ui/react/select").SelectValueProps, "ref"> & import("react").RefAttributes<HTMLSpanElement>>;
5
+ export declare const SelectPortal: import("react").ForwardRefExoticComponent<Omit<import("@base-ui/react/select").SelectPortalProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
6
+ export declare function SelectTrigger({ className, variant, ...props }: ComponentProps<typeof BaseSelect.Trigger> & {
7
+ variant?: 'field' | 'ghost';
8
+ }): import("react").JSX.Element;
9
+ export declare function SelectIcon({ className, ...props }: ComponentProps<typeof BaseSelect.Icon>): import("react").JSX.Element;
10
+ export declare function SelectPositioner({ className, ...props }: ComponentProps<typeof BaseSelect.Positioner>): import("react").JSX.Element;
11
+ export declare function SelectPopup({ className, ...props }: ComponentProps<typeof BaseSelect.Popup>): import("react").JSX.Element;
12
+ export declare function SelectList({ className, ...props }: ComponentProps<typeof BaseSelect.List>): import("react").JSX.Element;
13
+ export declare function SelectItem({ className, ...props }: ComponentProps<typeof BaseSelect.Item>): import("react").JSX.Element;
14
+ export declare function SelectItemText({ className, ...props }: ComponentProps<typeof BaseSelect.ItemText>): import("react").JSX.Element;
15
+ export declare function SelectItemIndicator({ className, ...props }: ComponentProps<typeof BaseSelect.ItemIndicator>): import("react").JSX.Element;
16
+ export declare const Select: {
17
+ Root: typeof BaseSelect.Root;
18
+ Trigger: typeof SelectTrigger;
19
+ Value: import("react").ForwardRefExoticComponent<Omit<import("@base-ui/react/select").SelectValueProps, "ref"> & import("react").RefAttributes<HTMLSpanElement>>;
20
+ Icon: typeof SelectIcon;
21
+ Portal: import("react").ForwardRefExoticComponent<Omit<import("@base-ui/react/select").SelectPortalProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
22
+ Positioner: typeof SelectPositioner;
23
+ Popup: typeof SelectPopup;
24
+ List: typeof SelectList;
25
+ Item: typeof SelectItem;
26
+ ItemText: typeof SelectItemText;
27
+ ItemIndicator: typeof SelectItemIndicator;
28
+ };
29
+ export interface Option {
30
+ value: string;
31
+ label: string;
32
+ disabled?: boolean;
33
+ }
34
+ export interface SelectControlProps {
35
+ label: string;
36
+ name?: string;
37
+ options: Option[];
38
+ value?: string;
39
+ defaultValue?: string;
40
+ onValueChange?: (value: string) => void;
41
+ required?: boolean;
42
+ disabled?: boolean;
43
+ className?: string;
44
+ labelledBy?: string;
45
+ }
46
+ export declare function SelectControl({ label, name, options, value, defaultValue, onValueChange, required, disabled, className, labelledBy }: SelectControlProps): import("react").JSX.Element;
package/dist/select.js ADDED
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Select as BaseSelect } from '@base-ui/react/select';
4
+ import { Check, ChevronDown } from 'lucide-react';
5
+ import { withClass } from './classes.js';
6
+ export const SelectRoot = BaseSelect.Root;
7
+ export const SelectValue = BaseSelect.Value;
8
+ export const SelectPortal = BaseSelect.Portal;
9
+ export function SelectTrigger({ className, variant = 'field', ...props }) {
10
+ return _jsx(BaseSelect.Trigger, { ...props, "data-variant": variant, className: withClass('hrui-select-trigger', className) });
11
+ }
12
+ export function SelectIcon({ className, ...props }) {
13
+ return _jsx(BaseSelect.Icon, { ...props, className: withClass('hrui-select-icon', className) });
14
+ }
15
+ export function SelectPositioner({ className, ...props }) {
16
+ return _jsx(BaseSelect.Positioner, { alignItemWithTrigger: false, side: "bottom", align: "start", sideOffset: 4, ...props, className: withClass('hrui-select-positioner', className) });
17
+ }
18
+ export function SelectPopup({ className, ...props }) {
19
+ return _jsx(BaseSelect.Popup, { ...props, className: withClass('hrui-select-popup', className) });
20
+ }
21
+ export function SelectList({ className, ...props }) {
22
+ return _jsx(BaseSelect.List, { ...props, className: withClass('hrui-select-list', className) });
23
+ }
24
+ export function SelectItem({ className, ...props }) {
25
+ return _jsx(BaseSelect.Item, { ...props, className: withClass('hrui-select-item', className) });
26
+ }
27
+ export function SelectItemText({ className, ...props }) {
28
+ return _jsx(BaseSelect.ItemText, { ...props, className: withClass('hrui-select-item-text', className) });
29
+ }
30
+ export function SelectItemIndicator({ className, ...props }) {
31
+ return _jsx(BaseSelect.ItemIndicator, { ...props, className: withClass('hrui-select-item-indicator', className) });
32
+ }
33
+ export const Select = {
34
+ Root: SelectRoot, Trigger: SelectTrigger, Value: SelectValue, Icon: SelectIcon,
35
+ Portal: SelectPortal, Positioner: SelectPositioner, Popup: SelectPopup,
36
+ List: SelectList, Item: SelectItem, ItemText: SelectItemText, ItemIndicator: SelectItemIndicator,
37
+ };
38
+ export function SelectControl({ label, name, options, value, defaultValue = '', onValueChange, required, disabled, className, labelledBy }) {
39
+ const [ownValue, setOwnValue] = useState(defaultValue);
40
+ return _jsxs(Select.Root, { items: options, name: name, value: value === undefined ? ownValue : value, required: required, disabled: disabled, onValueChange: (next) => { const nextValue = next ?? ''; setOwnValue(nextValue); onValueChange?.(nextValue); }, children: [_jsxs(Select.Trigger, { className: className, "aria-label": labelledBy ? undefined : label, "aria-labelledby": labelledBy, children: [_jsx(Select.Value, {}), _jsx(Select.Icon, { children: _jsx(ChevronDown, { "aria-hidden": "true" }) })] }), _jsx(Select.Portal, { children: _jsx(Select.Positioner, { children: _jsx(Select.Popup, { children: _jsx(Select.List, { children: options.map((option) => _jsxs(Select.Item, { value: option.value, label: option.label, disabled: option.disabled, children: [_jsx(Select.ItemText, { children: option.label }), _jsx(Select.ItemIndicator, { children: _jsx(Check, { "aria-hidden": "true" }) })] }, option.value)) }) }) }) })] });
41
+ }
@@ -0,0 +1,174 @@
1
+ /* Import @herongxhr/design-tokens/tokens.css before this file and the app brand adapter. */
2
+ .hrui-select-trigger,
3
+ .hrui-select-popup,
4
+ .hrui-select-list,
5
+ .hrui-select-item,
6
+ .hrui-dialog-viewport,
7
+ .hrui-dialog-popup { box-sizing: border-box; }
8
+ .hrui-select-trigger {
9
+ appearance: none;
10
+ display: inline-flex;
11
+ align-items: center;
12
+ justify-content: space-between;
13
+ gap: var(--hr-ui-space-3);
14
+ min-height: var(--hr-ui-control-height);
15
+ min-width: 0;
16
+ max-width: 100%;
17
+ padding: var(--hr-ui-space-2) var(--hr-ui-space-3);
18
+ border: var(--hr-ui-border-width) solid var(--hr-ui-color-border-strong);
19
+ border-radius: var(--hr-ui-radius-small);
20
+ background: var(--hr-ui-color-surface);
21
+ color: var(--hr-ui-color-text);
22
+ font: inherit;
23
+ text-align: left;
24
+ cursor: pointer;
25
+ outline: none;
26
+ -webkit-tap-highlight-color: transparent;
27
+ transition: background-color var(--hr-ui-transition-fast), color var(--hr-ui-transition-fast), border-color var(--hr-ui-transition-fast);
28
+ }
29
+ .hrui-select-trigger:hover:not([data-disabled]),
30
+ .hrui-select-trigger[data-popup-open] { background: var(--hr-ui-color-surface-raised); border-color: var(--hr-ui-color-border-hover); }
31
+ .hrui-select-trigger:focus-visible { outline: var(--hr-ui-focus-width-subtle) solid var(--hr-ui-color-focus); outline-offset: var(--hr-ui-focus-offset); }
32
+ .hrui-select-trigger[data-disabled] { opacity: var(--hr-ui-opacity-disabled); cursor: not-allowed; }
33
+ .hrui-select-trigger>span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
34
+ .hrui-select-trigger[data-variant="ghost"] {
35
+ gap: var(--hr-ui-space-2);
36
+ border-color: transparent;
37
+ border-radius: var(--hr-ui-radius-medium);
38
+ background: transparent;
39
+ color: var(--hr-ui-color-muted);
40
+ font-size: var(--hr-ui-font-small);
41
+ line-height: var(--hr-ui-leading-heading);
42
+ white-space: nowrap;
43
+ }
44
+ .hrui-select-trigger[data-variant="ghost"]:hover:not([data-disabled]),
45
+ .hrui-select-trigger[data-variant="ghost"][data-popup-open] { color: var(--hr-ui-color-text); background: var(--hr-ui-color-surface-raised); }
46
+ .hrui-select-trigger[data-variant="ghost"]:hover:not([data-disabled]):not([data-popup-open]) { border-color: transparent; }
47
+ .hrui-select-trigger[data-variant="ghost"][data-popup-open] { border-color: var(--hr-ui-color-border); }
48
+ .hrui-select-trigger>svg { width: var(--hr-ui-icon-small); height: var(--hr-ui-icon-small); flex-shrink: 0; }
49
+ .hrui-select-icon { display: inline-flex; flex-shrink: 0; margin-left: auto; color: var(--hr-ui-color-subtle); transition: transform var(--hr-ui-transition-fast) var(--hr-ui-motion-easing-standard); }
50
+ .hrui-select-icon[data-popup-open] { transform: rotate(180deg); }
51
+ .hrui-select-icon svg,
52
+ .hrui-select-item-indicator svg { width: var(--hr-ui-icon-medium); height: var(--hr-ui-icon-medium); }
53
+ .hrui-select-trigger svg,
54
+ .hrui-select-item svg { stroke-width: var(--hr-ui-icon-stroke); }
55
+ .hrui-select-trigger[data-variant="ghost"] .hrui-select-icon svg { width: var(--hr-ui-icon-small); height: var(--hr-ui-icon-small); }
56
+ .hrui-select-positioner { z-index: var(--hr-ui-layer-popover); outline: none; }
57
+ .hrui-select-popup {
58
+ min-width: min(var(--anchor-width), var(--available-width));
59
+ width: max-content;
60
+ max-width: min(var(--available-width), var(--hr-ui-select-max-width));
61
+ max-height: var(--available-height);
62
+ padding: var(--hr-ui-space-1);
63
+ border: var(--hr-ui-border-width) solid var(--hr-ui-color-border);
64
+ border-radius: var(--hr-ui-radius-small);
65
+ background: var(--hr-ui-color-surface);
66
+ color: var(--hr-ui-color-text);
67
+ box-shadow: var(--hr-ui-shadow-popover);
68
+ font: var(--hr-ui-font-body)/var(--hr-ui-leading-body) var(--hr-ui-font-sans);
69
+ overflow: hidden;
70
+ outline: none;
71
+ transform-origin: var(--transform-origin);
72
+ opacity: 1;
73
+ transform: translateY(0) scale(1);
74
+ transition: opacity var(--hr-ui-transition-fast) var(--hr-ui-motion-easing-standard), transform var(--hr-ui-transition-normal) var(--hr-ui-motion-easing-standard);
75
+ }
76
+ .hrui-select-popup[data-starting-style],
77
+ .hrui-select-popup[data-ending-style] { opacity: 0; transform: translateY(calc(-1 * var(--hr-ui-space-1))) scale(var(--hr-ui-motion-scale-enter)); }
78
+ .hrui-select-popup[data-ending-style] { transition-duration: var(--hr-ui-transition-fast); }
79
+ .hrui-select-list {
80
+ max-height: max(0px, min(var(--hr-ui-select-max-height), calc(var(--available-height) - var(--hr-ui-space-1) * 2 - var(--hr-ui-border-width) * 2)));
81
+ overflow-y: auto;
82
+ overscroll-behavior: contain;
83
+ padding: 0;
84
+ margin: 0;
85
+ outline: none;
86
+ }
87
+ .hrui-select-item {
88
+ display: flex;
89
+ align-items: center;
90
+ gap: var(--hr-ui-space-3);
91
+ min-height: var(--hr-ui-control-height);
92
+ padding: var(--hr-ui-space-2) var(--hr-ui-space-3);
93
+ border-radius: var(--hr-ui-radius-small);
94
+ color: var(--hr-ui-color-text);
95
+ cursor: default;
96
+ user-select: none;
97
+ outline: none;
98
+ transition: background-color var(--hr-ui-transition-fast), color var(--hr-ui-transition-fast);
99
+ }
100
+ .hrui-select-item[data-highlighted] { color: var(--hr-ui-color-highlight-text); background: var(--hr-ui-color-highlight); }
101
+ .hrui-select-item[data-disabled] { color: var(--hr-ui-color-muted); opacity: var(--hr-ui-opacity-disabled); }
102
+ .hrui-select-item>svg { width: var(--hr-ui-icon-medium); height: var(--hr-ui-icon-medium); flex-shrink: 0; }
103
+ .hrui-select-item-text { flex: 1; min-width: 0; overflow-wrap: anywhere; }
104
+ .hrui-select-item-indicator { display: inline-flex; margin-left: auto; color: var(--hr-ui-color-accent); }
105
+ .hrui-theme-control { position: relative; }
106
+ .hrui-theme-control .hrui-select-trigger { width: var(--hr-ui-control-width-theme); }
107
+ .hrui-theme-popup { width: var(--hr-ui-popover-width-small); border-radius: var(--hr-ui-radius-medium); font-size: var(--hr-ui-font-small); line-height: var(--hr-ui-leading-heading); }
108
+ .hrui-theme-popup .hrui-select-item { color: var(--hr-ui-color-muted); }
109
+ .hrui-theme-popup .hrui-select-item[data-highlighted],
110
+ .hrui-theme-popup .hrui-select-item[data-selected] { color: var(--hr-ui-color-text); }
111
+ .hrui-theme-popup .hrui-select-item-indicator svg { width: var(--hr-ui-icon-small); height: var(--hr-ui-icon-small); }
112
+ .hrui-theme-status:not([data-warning]) { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
113
+ .hrui-theme-status[data-warning] {
114
+ position: absolute;
115
+ top: 100%;
116
+ right: 0;
117
+ z-index: var(--hr-ui-layer-popover);
118
+ width: max-content;
119
+ max-width: min(var(--hr-ui-warning-max-width), calc(100vw - var(--hr-ui-space-6) * 2));
120
+ padding: var(--hr-ui-space-2) var(--hr-ui-space-3);
121
+ border-radius: var(--hr-ui-radius-small);
122
+ background: var(--hr-ui-color-warning-bg);
123
+ color: var(--hr-ui-color-warning);
124
+ font-size: var(--hr-ui-font-caption);
125
+ }
126
+ .hrui-theme-toggle { position: relative; display: inline-flex; align-items: center; gap: var(--hr-ui-space-1); }
127
+ .hrui-theme-toggle-button {
128
+ appearance: none;
129
+ display: inline-grid;
130
+ place-items: center;
131
+ flex-shrink: 0;
132
+ width: var(--hr-ui-control-height);
133
+ height: var(--hr-ui-control-height);
134
+ min-width: var(--hr-ui-control-height);
135
+ min-height: var(--hr-ui-control-height);
136
+ padding: 0;
137
+ border: 0;
138
+ border-radius: var(--hr-ui-radius-medium);
139
+ background: transparent;
140
+ color: var(--hr-ui-color-muted);
141
+ cursor: pointer;
142
+ outline: none;
143
+ -webkit-tap-highlight-color: transparent;
144
+ transition: color var(--hr-ui-transition-fast), background-color var(--hr-ui-transition-fast);
145
+ }
146
+ .hrui-theme-toggle-button:hover:not(:disabled) { color: var(--hr-ui-color-text); background: var(--hr-ui-color-surface-raised); }
147
+ .hrui-theme-toggle-button:focus-visible { outline: var(--hr-ui-focus-width-subtle) solid var(--hr-ui-color-focus); outline-offset: var(--hr-ui-focus-offset); }
148
+ .hrui-theme-toggle-button:disabled { cursor: default; }
149
+ .hrui-theme-toggle-icons { position: relative; display: block; width: var(--hr-ui-icon-medium); height: var(--hr-ui-icon-medium); }
150
+ .hrui-theme-toggle-icon { position: absolute; inset: 0; width: 100%; height: 100%; stroke-width: var(--hr-ui-icon-stroke); transition: opacity var(--hr-ui-transition-fast) var(--hr-ui-motion-easing-standard), transform var(--hr-ui-transition-normal) var(--hr-ui-motion-easing-standard); }
151
+ .hrui-theme-toggle-sun { opacity: 0; transform: translateY(var(--hr-ui-space-1)) scale(var(--hr-ui-motion-scale-enter)); }
152
+ .hrui-theme-toggle-moon { opacity: 1; transform: translateY(0) scale(1); }
153
+ :root[data-theme="dark"] .hrui-theme-toggle-sun { opacity: 1; transform: translateY(0) scale(1); }
154
+ :root[data-theme="dark"] .hrui-theme-toggle-moon { opacity: 0; transform: translateY(calc(-1 * var(--hr-ui-space-1))) scale(var(--hr-ui-motion-scale-enter)); }
155
+ @media (prefers-color-scheme: dark) {
156
+ :root:not([data-theme="light"]):not([data-theme="dark"]) .hrui-theme-toggle-sun { opacity: 1; transform: translateY(0) scale(1); }
157
+ :root:not([data-theme="light"]):not([data-theme="dark"]) .hrui-theme-toggle-moon { opacity: 0; transform: translateY(calc(-1 * var(--hr-ui-space-1))) scale(var(--hr-ui-motion-scale-enter)); }
158
+ }
159
+ .hrui-theme-toggle-system>svg { width: var(--hr-ui-icon-medium); height: var(--hr-ui-icon-medium); stroke-width: var(--hr-ui-icon-stroke); }
160
+ .hrui-theme-toggle-system[aria-pressed="true"] { color: var(--hr-ui-color-accent); background: var(--hr-ui-color-accent-soft); }
161
+ .hrui-dialog-backdrop { position: fixed; inset: 0; z-index: var(--hr-ui-layer-backdrop); background: var(--hr-ui-color-scrim); transition: opacity var(--hr-ui-transition-fast) var(--hr-ui-motion-easing-standard); }
162
+ .hrui-dialog-backdrop[data-starting-style],
163
+ .hrui-dialog-backdrop[data-ending-style] { opacity: 0; }
164
+ .hrui-dialog-viewport { position: fixed; inset: 0; z-index: var(--hr-ui-layer-dialog); display: flex; align-items: center; justify-content: center; padding: var(--hr-ui-space-4); overflow: auto; overscroll-behavior: contain; }
165
+ .hrui-dialog-popup { display: flex; flex-direction: column; flex-shrink: 0; margin: 0; overflow: hidden; outline: none; transition: opacity var(--hr-ui-transition-fast) var(--hr-ui-motion-easing-standard), transform var(--hr-ui-transition-normal) var(--hr-ui-motion-easing-standard); }
166
+ .hrui-dialog-popup[data-starting-style],
167
+ .hrui-dialog-popup[data-ending-style] { opacity: 0; transform: translateY(var(--hr-ui-space-2)) scale(var(--hr-ui-motion-scale-enter)); }
168
+ .hrui-dialog-popup[data-ending-style] { transition-duration: var(--hr-ui-transition-fast); }
169
+ /* This viewport breakpoint matches the existing compact admin layout. */
170
+ @media (max-width: 760px) { .hrui-dialog-viewport { padding: var(--hr-ui-space-2); } }
171
+ @media (prefers-reduced-motion: reduce) {
172
+ .hrui-select-trigger, .hrui-select-icon, .hrui-select-popup, .hrui-select-item,
173
+ .hrui-dialog-backdrop, .hrui-dialog-popup, .hrui-theme-toggle-button, .hrui-theme-toggle-icon { transition: none; }
174
+ }
@@ -0,0 +1,7 @@
1
+ export type ThemeChoice = 'system' | 'light' | 'dark';
2
+ export interface ThemeSelectProps {
3
+ storageKey?: string;
4
+ className?: string;
5
+ defaultTheme?: ThemeChoice;
6
+ }
7
+ export declare function ThemeSelect({ storageKey, className, defaultTheme }: ThemeSelectProps): import("react").JSX.Element;
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useId, useState } from 'react';
3
+ import { Check, ChevronDown, Monitor, Moon, Sun } from 'lucide-react';
4
+ import { Select } from './select.js';
5
+ const themeItems = [
6
+ { value: 'system', label: '跟随系统', icon: Monitor },
7
+ { value: 'light', label: '浅色', icon: Sun },
8
+ { value: 'dark', label: '深色', icon: Moon },
9
+ ];
10
+ function isTheme(value) { return value === 'system' || value === 'light' || value === 'dark'; }
11
+ export function ThemeSelect({ storageKey = 'herong-theme', className, defaultTheme = 'system' }) {
12
+ // Stable initial markup keeps server rendering and the hydration render equal.
13
+ // Each application's pre-paint theme adapter remains the source of truth.
14
+ const [theme, setTheme] = useState(defaultTheme);
15
+ const [status, setStatus] = useState({ message: '', warning: false });
16
+ const statusId = useId();
17
+ const CurrentIcon = themeItems.find((item) => item.value === theme).icon;
18
+ useEffect(() => {
19
+ const initial = document.documentElement.dataset.theme;
20
+ setTheme(isTheme(initial) ? initial : defaultTheme);
21
+ let storage;
22
+ try {
23
+ storage = window.localStorage;
24
+ }
25
+ catch { /* Storage can be unavailable while theme switching still works. */ }
26
+ const onStorage = (event) => {
27
+ if ((storage && event.storageArea !== storage) || (event.key !== storageKey && event.key !== null))
28
+ return;
29
+ const next = isTheme(event.newValue) ? event.newValue : defaultTheme;
30
+ document.documentElement.dataset.theme = next;
31
+ setTheme(next);
32
+ };
33
+ window.addEventListener('storage', onStorage);
34
+ return () => window.removeEventListener('storage', onStorage);
35
+ }, [storageKey, defaultTheme]);
36
+ const changeTheme = (value) => {
37
+ if (!isTheme(value))
38
+ return;
39
+ document.documentElement.dataset.theme = value;
40
+ setTheme(value);
41
+ try {
42
+ localStorage.setItem(storageKey, value);
43
+ setStatus({ message: '外观已保存', warning: false });
44
+ }
45
+ catch {
46
+ setStatus({ message: '外观已切换,但当前浏览器无法保存偏好。', warning: true });
47
+ }
48
+ };
49
+ return _jsxs("div", { className: ['hrui-theme-control', className].filter(Boolean).join(' '), "data-theme-choice": theme, children: [_jsxs(Select.Root, { items: themeItems, value: theme, onValueChange: changeTheme, children: [_jsxs(Select.Trigger, { variant: "ghost", "aria-label": "\u9875\u9762\u5916\u89C2", "aria-describedby": statusId, children: [_jsx(CurrentIcon, { "aria-hidden": "true" }), _jsx(Select.Value, {}), _jsx(Select.Icon, { children: _jsx(ChevronDown, { "aria-hidden": "true" }) })] }), _jsx(Select.Portal, { children: _jsx(Select.Positioner, { align: "end", sideOffset: 8, children: _jsx(Select.Popup, { className: "hrui-theme-popup", children: _jsx(Select.List, { children: themeItems.map(({ value, label, icon: Icon }) => _jsxs(Select.Item, { value: value, children: [_jsx(Icon, { "aria-hidden": "true" }), _jsx(Select.ItemText, { children: label }), _jsx(Select.ItemIndicator, { children: _jsx(Check, { "aria-hidden": "true" }) })] }, value)) }) }) }) })] }), _jsx("span", { id: statusId, className: "hrui-theme-status", "data-warning": status.warning || undefined, role: "status", "aria-live": "polite", children: status.message })] });
50
+ }
@@ -0,0 +1,11 @@
1
+ import type { ThemeChoice } from './theme-select.js';
2
+ export type ResolvedTheme = 'light' | 'dark';
3
+ export interface ThemeToggleProps {
4
+ storageKey?: string;
5
+ defaultTheme?: ThemeChoice;
6
+ className?: string;
7
+ showSystemControl?: boolean;
8
+ /** Runs only after a user action. A boolean result reports adapter persistence. */
9
+ onThemeChange?: (choice: ThemeChoice, resolved: ResolvedTheme) => boolean | void;
10
+ }
11
+ export declare function ThemeToggle({ storageKey, defaultTheme, className, showSystemControl, onThemeChange }: ThemeToggleProps): import("react").JSX.Element;
@@ -0,0 +1,115 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useId, useRef, useState } from 'react';
3
+ import { Monitor, Moon, Sun } from 'lucide-react';
4
+ const CHANGE_EVENT = 'herong:ui-theme-change';
5
+ function choice(value) {
6
+ return value === 'auto' ? 'system' : value === 'light' || value === 'dark' || value === 'system' ? value : null;
7
+ }
8
+ export function ThemeToggle({ storageKey = 'herong-theme', defaultTheme = 'system', className, showSystemControl = false, onThemeChange }) {
9
+ const [mounted, setMounted] = useState(false);
10
+ const [preference, setPreference] = useState(defaultTheme);
11
+ const [resolved, setResolved] = useState('light');
12
+ const [status, setStatus] = useState({ message: '', warning: false });
13
+ const preferenceRef = useRef(defaultTheme);
14
+ const callbackRef = useRef(onThemeChange);
15
+ const changeRef = useRef(null);
16
+ const statusId = useId();
17
+ callbackRef.current = onThemeChange;
18
+ useEffect(() => {
19
+ const root = document.documentElement;
20
+ const media = window.matchMedia('(prefers-color-scheme: dark)');
21
+ let storage;
22
+ let saved = null;
23
+ try {
24
+ storage = window.localStorage;
25
+ saved = choice(storage.getItem(storageKey));
26
+ }
27
+ catch { /* The current page can still change its theme. */ }
28
+ const resolveChoice = (next) => next === 'system' ? media.matches ? 'dark' : 'light' : next;
29
+ const rootPreference = () => choice(root.dataset.hrThemePreference) || choice(root.dataset.themePreference);
30
+ const rootResolved = () => {
31
+ const current = choice(root.dataset.theme);
32
+ return current === 'light' || current === 'dark' ? current : resolveChoice(preferenceRef.current);
33
+ };
34
+ function apply(next) {
35
+ preferenceRef.current = next;
36
+ setPreference(next);
37
+ const actual = resolveChoice(next);
38
+ root.dataset.themePreference = next;
39
+ // Keep the established Herong pre-paint preference meaningful; its adapter
40
+ // synchronizes the legacy controller and storage keys after user actions.
41
+ if (root.dataset.hrThemePreference !== undefined)
42
+ root.dataset.hrThemePreference = next === 'system' ? 'auto' : next;
43
+ root.dataset.theme = actual;
44
+ setResolved(actual);
45
+ return actual;
46
+ }
47
+ apply(rootPreference() || saved || (root.dataset.theme === 'system' ? 'system' : defaultTheme));
48
+ function onSystemChange() { if (preferenceRef.current === 'system')
49
+ apply('system'); }
50
+ function onStorage(event) {
51
+ if ((storage && event.storageArea !== storage) || (event.key !== storageKey && event.key !== null))
52
+ return;
53
+ apply(choice(event.newValue) || defaultTheme);
54
+ setStatus({ message: '', warning: false });
55
+ }
56
+ function onSharedChange(event) {
57
+ const detail = event.detail;
58
+ const next = choice(detail?.choice);
59
+ if (detail?.storageKey !== storageKey || !next)
60
+ return;
61
+ apply(next);
62
+ if (detail.saved)
63
+ setStatus({ message: '', warning: false });
64
+ }
65
+ const observer = new MutationObserver(() => {
66
+ // A resolved dark/light attribute changing does not turn a system
67
+ // preference into an explicit choice. Observe preference separately.
68
+ const next = rootPreference();
69
+ if (next) {
70
+ preferenceRef.current = next;
71
+ setPreference(next);
72
+ }
73
+ setResolved(rootResolved());
74
+ });
75
+ observer.observe(root, { attributes: true, attributeFilter: ['data-theme', 'data-theme-preference', 'data-hr-theme-preference'] });
76
+ media.addEventListener('change', onSystemChange);
77
+ window.addEventListener('storage', onStorage);
78
+ window.addEventListener(CHANGE_EVENT, onSharedChange);
79
+ changeRef.current = (requested) => {
80
+ const next = requested === 'toggle' ? rootResolved() === 'dark' ? 'light' : 'dark' : requested;
81
+ const actual = apply(next);
82
+ let persisted = true;
83
+ try {
84
+ window.localStorage.setItem(storageKey, next);
85
+ }
86
+ catch {
87
+ persisted = false;
88
+ }
89
+ try {
90
+ const adapted = callbackRef.current?.(next, actual);
91
+ if (typeof adapted === 'boolean')
92
+ persisted = adapted;
93
+ }
94
+ catch {
95
+ persisted = false;
96
+ }
97
+ // Notify other controls in this document without synthesizing native
98
+ // storage events or rereading stale storage after a failed save.
99
+ window.dispatchEvent(new CustomEvent(CHANGE_EVENT, { detail: { storageKey, choice: next, saved: persisted } }));
100
+ setStatus(persisted
101
+ ? { message: next === 'system' ? '已跟随系统主题' : next === 'dark' ? '已切换为深色模式' : '已切换为浅色模式', warning: false }
102
+ : { message: '外观已切换,但当前浏览器无法保存偏好。', warning: true });
103
+ };
104
+ setMounted(true);
105
+ return () => {
106
+ changeRef.current = null;
107
+ observer.disconnect();
108
+ media.removeEventListener('change', onSystemChange);
109
+ window.removeEventListener('storage', onStorage);
110
+ window.removeEventListener(CHANGE_EVENT, onSharedChange);
111
+ };
112
+ }, [storageKey, defaultTheme]);
113
+ const action = mounted ? resolved === 'dark' ? '切换为浅色模式' : '切换为深色模式' : '切换外观';
114
+ return _jsxs("div", { className: ['hrui-theme-toggle', className].filter(Boolean).join(' '), "data-theme-choice": preference, children: [_jsx("button", { type: "button", className: "hrui-theme-toggle-button", disabled: !mounted, "aria-label": action, title: action, "aria-describedby": statusId, onClick: () => changeRef.current?.('toggle'), children: _jsxs("span", { className: "hrui-theme-toggle-icons", "aria-hidden": "true", children: [_jsx(Sun, { className: "hrui-theme-toggle-icon hrui-theme-toggle-sun" }), _jsx(Moon, { className: "hrui-theme-toggle-icon hrui-theme-toggle-moon" })] }) }), showSystemControl && _jsx("button", { type: "button", className: "hrui-theme-toggle-button hrui-theme-toggle-system", disabled: !mounted, "aria-label": "\u8DDF\u968F\u7CFB\u7EDF\u4E3B\u9898", title: "\u8DDF\u968F\u7CFB\u7EDF\u4E3B\u9898", "aria-pressed": mounted && preference === 'system', "aria-describedby": statusId, onClick: () => changeRef.current?.('system'), children: _jsx(Monitor, { "aria-hidden": "true" }) }), _jsx("span", { id: statusId, className: "hrui-theme-status", "data-warning": status.warning || undefined, role: "status", "aria-live": "polite", "aria-atomic": "true", children: status.message })] });
115
+ }
package/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
2
  "name": "@herongxhr/ui-react",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.2",
4
+ "license": "UNLICENSED",
5
+ "publishConfig": {
6
+ "registry": "https://registry.npmjs.org/",
7
+ "access": "public"
8
+ },
9
+ "description": "Shared accessible Base UI components for Herong applications.",
10
+ "type": "module",
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js"
19
+ },
20
+ "./styles.css": "./dist/styles.css"
21
+ },
22
+ "sideEffects": [
23
+ "**/*.css"
24
+ ],
25
+ "dependencies": {
26
+ "@base-ui/react": "1.9.0",
27
+ "lucide-react": "1.72.0"
28
+ },
29
+ "peerDependencies": {
30
+ "@herongxhr/design-tokens": "0.1.0",
31
+ "react": "^19.3.0",
32
+ "react-dom": "^19.3.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/react": "19.3.0",
36
+ "@types/react-dom": "19.3.0",
37
+ "react": "19.3.1",
38
+ "react-dom": "19.3.1",
39
+ "typescript": "6.0.3",
40
+ "@herongxhr/design-tokens": "0.1.0"
41
+ },
42
+ "scripts": {
43
+ "build": "tsc -p tsconfig.json && node scripts/copy-styles.mjs",
44
+ "check": "tsc -p tsconfig.json --noEmit"
45
+ }
6
46
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.