@herongxhr/ui-react 0.1.1 → 0.1.3

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,10 @@
1
+ export interface AccountMenuProps {
2
+ name: string;
3
+ description?: string;
4
+ avatarUrl?: string;
5
+ loggingOut?: boolean;
6
+ logoutDisabled?: boolean;
7
+ onLogout: () => void;
8
+ className?: string;
9
+ }
10
+ export declare function AccountMenu({ name, description, avatarUrl, loggingOut, logoutDisabled, onLogout, className }: AccountMenuProps): import("react").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Menu } from '@base-ui/react/menu';
3
+ import { ChevronDown, LogOut } from 'lucide-react';
4
+ import { Avatar } from './avatar.js';
5
+ export function AccountMenu({ name, description, avatarUrl, loggingOut = false, logoutDisabled = false, onLogout, className }) {
6
+ const displayName = name.trim() || '已登录账号';
7
+ const detail = description?.trim();
8
+ return _jsx("div", { className: ['hrui-account-menu', className].filter(Boolean).join(' '), children: _jsxs(Menu.Root, { modal: false, children: [_jsxs(Menu.Trigger, { className: "hrui-account-trigger", "aria-label": `打开${displayName}的账号菜单`, children: [_jsx(Avatar, { src: avatarUrl, alt: "", size: "small" }), _jsx("span", { className: "hrui-account-trigger-name", children: displayName }), _jsx(ChevronDown, { className: "hrui-account-chevron", "aria-hidden": "true" })] }), _jsx(Menu.Portal, { children: _jsx(Menu.Positioner, { className: "hrui-account-positioner", side: "bottom", align: "end", sideOffset: 8, children: _jsxs(Menu.Popup, { className: "hrui-account-popup", "aria-label": `${displayName}的账号菜单`, children: [_jsxs("div", { className: "hrui-account-summary", children: [_jsx(Avatar, { src: avatarUrl, alt: "" }), _jsxs("div", { className: "hrui-account-identity", children: [_jsx("strong", { children: displayName }), detail && _jsx("span", { children: detail })] })] }), _jsx(Menu.Separator, { className: "hrui-account-separator" }), _jsxs(Menu.Item, { className: "hrui-account-logout", closeOnClick: false, disabled: loggingOut || logoutDisabled, "aria-busy": loggingOut || undefined, label: "\u9000\u51FA\u767B\u5F55", onClick: () => { if (!loggingOut && !logoutDisabled)
9
+ onLogout(); }, children: [_jsx(LogOut, { "aria-hidden": "true" }), _jsx("span", { children: loggingOut ? '正在退出…' : '退出登录' })] })] }) }) })] }) });
10
+ }
@@ -0,0 +1,7 @@
1
+ export interface AvatarProps {
2
+ src?: string;
3
+ alt?: string;
4
+ size?: 'small' | 'medium';
5
+ className?: string;
6
+ }
7
+ export declare function Avatar({ src, alt, size, className }: AvatarProps): import("react").JSX.Element;
package/dist/avatar.js ADDED
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Avatar as BaseAvatar } from '@base-ui/react/avatar';
3
+ import { UserRound } from 'lucide-react';
4
+ export function Avatar({ src, alt = '用户头像', size = 'medium', className }) {
5
+ return _jsxs(BaseAvatar.Root, { className: ['hrui-avatar', className].filter(Boolean).join(' '), "data-size": size, "aria-hidden": alt === '' || undefined, children: [src && _jsx(BaseAvatar.Image, { className: "hrui-avatar-image", src: src, alt: alt, referrerPolicy: "no-referrer" }), _jsx(BaseAvatar.Fallback, { className: "hrui-avatar-fallback", role: alt ? 'img' : undefined, "aria-label": alt || undefined, children: _jsx(UserRound, { "aria-hidden": "true" }) })] }, src || '');
6
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  export * from './select.js';
2
2
  export * from './dialog.js';
3
3
  export * from './theme-select.js';
4
+ export * from './theme-toggle.js';
5
+ export * from './avatar.js';
6
+ export * from './account-menu.js';
4
7
  export { CSPProvider } from '@base-ui/react/csp-provider';
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  export * from './select.js';
2
2
  export * from './dialog.js';
3
3
  export * from './theme-select.js';
4
+ export * from './theme-toggle.js';
5
+ export * from './avatar.js';
6
+ export * from './account-menu.js';
4
7
  export { CSPProvider } from '@base-ui/react/csp-provider';
package/dist/styles.css CHANGED
@@ -123,6 +123,68 @@
123
123
  color: var(--hr-ui-color-warning);
124
124
  font-size: var(--hr-ui-font-caption);
125
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-avatar { position: relative; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: var(--hr-ui-space-10); height: var(--hr-ui-space-10); overflow: hidden; border-radius: var(--hr-ui-radius-round); background: var(--hr-ui-color-surface-raised); color: var(--hr-ui-color-muted); }
162
+ .hrui-avatar[data-size="small"] { width: var(--hr-ui-space-8); height: var(--hr-ui-space-8); }
163
+ .hrui-avatar-image { position: absolute; inset: 0; display: block; width: 100%; height: 100%; object-fit: cover; }
164
+ .hrui-avatar-fallback { display: inline-flex; align-items: center; justify-content: center; width: 100%; height: 100%; }
165
+ .hrui-avatar-fallback svg { width: var(--hr-ui-icon-medium); height: var(--hr-ui-icon-medium); stroke-width: var(--hr-ui-icon-stroke); }
166
+ .hrui-account-menu { display: inline-flex; align-items: center; min-width: 0; }
167
+ .hrui-account-trigger { appearance: none; box-sizing: border-box; display: inline-flex; align-items: center; gap: var(--hr-ui-space-2); min-height: var(--hr-ui-control-height); min-width: var(--hr-ui-control-height); max-width: 100%; padding: var(--hr-ui-space-1) var(--hr-ui-space-2); border: 0; border-radius: var(--hr-ui-radius-medium); background: transparent; color: var(--hr-ui-color-text); font: var(--hr-ui-font-small)/var(--hr-ui-leading-heading) var(--hr-ui-font-sans); cursor: pointer; outline: none; -webkit-tap-highlight-color: transparent; transition: background-color var(--hr-ui-transition-fast), color var(--hr-ui-transition-fast); }
168
+ .hrui-account-trigger:hover,.hrui-account-trigger[data-popup-open] { background: var(--hr-ui-color-surface-raised); }
169
+ .hrui-account-trigger:focus-visible { outline: var(--hr-ui-focus-width-subtle) solid var(--hr-ui-color-focus); outline-offset: var(--hr-ui-focus-offset); }
170
+ .hrui-account-trigger-name { min-width: 0; max-width: var(--hr-ui-popover-width-small); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: var(--hr-ui-weight-medium); }
171
+ .hrui-account-chevron { width: var(--hr-ui-icon-small); height: var(--hr-ui-icon-small); flex-shrink: 0; color: var(--hr-ui-color-subtle); stroke-width: var(--hr-ui-icon-stroke); transition: transform var(--hr-ui-transition-fast) var(--hr-ui-motion-easing-standard); }
172
+ .hrui-account-trigger[data-popup-open] .hrui-account-chevron { transform: rotate(180deg); }
173
+ .hrui-account-positioner { z-index: var(--hr-ui-layer-popover); outline: none; }
174
+ .hrui-account-popup { box-sizing: border-box; width: calc(var(--hr-ui-popover-width-small) + var(--hr-ui-space-8)); max-width: var(--available-width); max-height: var(--available-height); overflow-y: auto; overscroll-behavior: contain; padding: var(--hr-ui-space-1); border: var(--hr-ui-border-width) solid var(--hr-ui-color-border); border-radius: var(--hr-ui-radius-medium); background: var(--hr-ui-color-surface); color: var(--hr-ui-color-text); box-shadow: var(--hr-ui-shadow-popover); font: var(--hr-ui-font-small)/var(--hr-ui-leading-body) var(--hr-ui-font-sans); outline: none; transform-origin: var(--transform-origin); opacity: 1; transform: translateY(0) scale(1); 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); }
175
+ .hrui-account-popup[data-starting-style],.hrui-account-popup[data-ending-style] { opacity: 0; transform: translateY(calc(-1 * var(--hr-ui-space-1))) scale(var(--hr-ui-motion-scale-enter)); }
176
+ .hrui-account-popup[data-ending-style] { transition-duration: var(--hr-ui-transition-fast); }
177
+ .hrui-account-summary { display: flex; align-items: center; gap: var(--hr-ui-space-3); padding: var(--hr-ui-space-3); }
178
+ .hrui-account-identity { display: flex; flex-direction: column; min-width: 0; gap: var(--hr-ui-space-1); overflow-wrap: anywhere; }
179
+ .hrui-account-identity strong { font-size: var(--hr-ui-font-small); font-weight: var(--hr-ui-weight-semibold); line-height: var(--hr-ui-leading-heading); }
180
+ .hrui-account-identity>span { color: var(--hr-ui-color-subtle); font-size: var(--hr-ui-font-caption); }
181
+ .hrui-account-separator { height: var(--hr-ui-border-width); margin: var(--hr-ui-space-1) 0; background: var(--hr-ui-color-border); }
182
+ .hrui-account-logout { box-sizing: border-box; display: flex; align-items: center; gap: var(--hr-ui-space-3); min-height: var(--hr-ui-control-height); padding: var(--hr-ui-space-3); border-radius: var(--hr-ui-radius-small); outline: none; cursor: pointer; user-select: none; color: var(--hr-ui-color-text); transition: color var(--hr-ui-transition-fast), background-color var(--hr-ui-transition-fast); }
183
+ .hrui-account-logout[data-highlighted] { color: var(--hr-ui-color-highlight-text); background: var(--hr-ui-color-highlight); }
184
+ .hrui-account-logout:focus-visible { outline: var(--hr-ui-focus-width-subtle) solid var(--hr-ui-color-focus); outline-offset: calc(-1 * var(--hr-ui-focus-width-subtle)); }
185
+ .hrui-account-logout[data-disabled] { opacity: var(--hr-ui-opacity-disabled); cursor: not-allowed; }
186
+ .hrui-account-logout svg { width: var(--hr-ui-icon-medium); height: var(--hr-ui-icon-medium); stroke-width: var(--hr-ui-icon-stroke); flex-shrink: 0; }
187
+ @media (max-width: 760px) { .hrui-account-trigger-name { display: none; } }
126
188
  .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); }
127
189
  .hrui-dialog-backdrop[data-starting-style],
128
190
  .hrui-dialog-backdrop[data-ending-style] { opacity: 0; }
@@ -135,5 +197,6 @@
135
197
  @media (max-width: 760px) { .hrui-dialog-viewport { padding: var(--hr-ui-space-2); } }
136
198
  @media (prefers-reduced-motion: reduce) {
137
199
  .hrui-select-trigger, .hrui-select-icon, .hrui-select-popup, .hrui-select-item,
138
- .hrui-dialog-backdrop, .hrui-dialog-popup { transition: none; }
200
+ .hrui-dialog-backdrop, .hrui-dialog-popup, .hrui-theme-toggle-button, .hrui-theme-toggle-icon,
201
+ .hrui-account-trigger, .hrui-account-chevron, .hrui-account-popup, .hrui-account-logout { transition: none; }
139
202
  }
@@ -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,6 @@
1
1
  {
2
2
  "name": "@herongxhr/ui-react",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "license": "UNLICENSED",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/",