@zccz14/ux 0.1.6 → 0.2.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/README.md CHANGED
@@ -7,8 +7,8 @@
7
7
  - **Sidebar** — one animated icon-collapse sidebar: 16rem expanded, 3rem collapsed, 200 ms width transition. Midas, 1Exchange and 1earn-style shells without animation are replaced by this behavior.
8
8
  - **Navigation groups only** — the sidebar has no separate bottom section. Settings and status destinations live in the navigation groups like every other destination, so the bottom edge of every app's sidebar looks the same.
9
9
  - **AppHeader** — sidebar trigger, current page title, your `headerSlot`, and the Linkit account control (`LinkitMyInfo`), in that order. Role badges such as 超级管理员 stay out of the shell: administrator affordances live in the administrator navigation group and pages.
10
- - **Logo** — you pass the light and dark logo marks; the shell renders the active variant inside a fixed 1.75rem box and controls the display size itself, so the logo size is identical everywhere. The title beside the logo disappears while collapsed.
11
- - **Linkit integration** — the signed-in viewer's `resolvedTheme` picks the logo variant, the viewer's language selects the shell copy (中文/English), and theme, language and account controls all live in the `LinkitMyInfo` dialog.
10
+ - **Logo** — you pass one mark; the shell renders it inside a fixed 1.75rem box and controls the display size itself, so the logo size is identical everywhere. Pass a mark that follows the current colour (an icon using `currentColor`, or an SVG with mode-aware fills) so it keeps working in both themes. The title beside the logo disappears while collapsed.
11
+ - **Linkit integration** — the viewer's language selects the shell copy (中文/English), and theme, language and account controls all live in the `LinkitMyInfo` dialog. The shell never reads the theme itself: it renders whatever colour scheme its own CSS resolves (see [Styling](#styling)).
12
12
 
13
13
  ## Installation
14
14
 
@@ -16,7 +16,7 @@
16
16
  npm install @zccz14/ux
17
17
  ```
18
18
 
19
- Peer dependencies: `react` (18 or 19), `react-dom`, `react-router-dom` 7, `@base-ui/react` ≥ 1.6, `linkit-react-components` ≥ 0.5.1 (older versions do not publish `resolvedTheme`).
19
+ Peer dependencies: `react` (18 or 19), `react-dom`, `react-router-dom` 7, `@base-ui/react` ≥ 1.6, `linkit-react-components` ≥ 0.6.
20
20
 
21
21
  ## Usage
22
22
 
@@ -41,7 +41,7 @@ export function App() {
41
41
  <LinkitProvider linkitBaseUrl="https://linkit.ntnl.io" lang="zh-CN">
42
42
  <HashRouter>
43
43
  <AppLayout
44
- logo={{ light: <CybionMark />, dark: <CybionMark /> }}
44
+ logo={<CybionMark />}
45
45
  title="Cybion"
46
46
  nav={[
47
47
  {
@@ -74,7 +74,7 @@ export function App() {
74
74
 
75
75
  | Prop | Type | Required | Description |
76
76
  | --- | --- | --- | --- |
77
- | `logo` | `{ light: ReactNode; dark: ReactNode }` | yes | The two logo variants. The shell renders the one that matches `useLinkit().resolvedTheme` at its own fixed size. |
77
+ | `logo` | `ReactNode` | yes | The logo mark. The shell renders it at its own fixed 1.75rem size; pass a mark that follows the current colour. |
78
78
  | `title` | `string` | yes | App name beside the logo. Hidden while the sidebar is collapsed. |
79
79
  | `nav` | `AppNavGroup[]` | yes | Navigation groups rendered in the sidebar. |
80
80
  | `headerSlot` | `ReactNode` | no | Rendered immediately left of `LinkitMyInfo` in the AppHeader. Use it for page-level actions such as refresh. |
@@ -101,14 +101,18 @@ type AppNavGroup = { label: string; items: AppNavItem[] };
101
101
 
102
102
  ## Styling
103
103
 
104
- Import `@zccz14/ux/styles.css` once. It consumes the app's shadcn theme tokens with built-in fallbacks: `--background`, `--foreground`, `--border`, `--accent`, `--muted`, `--ring`, `--sidebar`, `--sidebar-foreground`, `--sidebar-accent`, `--sidebar-accent-foreground`, `--sidebar-border`, `--sidebar-ring`.
104
+ Import `@zccz14/ux/styles.css` once. It consumes the app's shadcn theme tokens and falls back to its own values, so the shell stays readable even when a token is missing: `--background`, `--foreground`, `--border`, `--accent`, `--muted`, `--ring`, `--sidebar`, `--sidebar-foreground`, `--sidebar-accent`, `--sidebar-accent-foreground`, `--sidebar-border`, `--sidebar-ring`.
105
+
106
+ Those tokens are read through a `--ux-*` layer declared twice — a light set on `:root` and a dark set on `.dark` — so every fallback has a dark counterpart. A missing `--sidebar` therefore renders dark in dark mode instead of a light sidebar. The shell also sets `color-scheme` per mode, so native scrollbars and controls follow the theme without help from a theme provider.
107
+
108
+ Theme state itself is not the shell's business: whatever toggles `.dark` on `<html>` (in this family, `LinkitProvider`) decides the mode, and the shell follows.
105
109
 
106
110
  ## Migrating an app from its local shell
107
111
 
108
112
  1. Replace the local `SidebarProvider` / `Sidebar` / `AppHeader` copy with one `AppLayout` at the top of `App.tsx`.
109
- 2. Move legacy header buttons (refresh, settings, language, theme) into `headerSlot`. Role badges are dropped: administrator affordances belong to the administrator navigation group and pages, and theme/language preferences live in the `LinkitMyInfo` profile dialog (`linkit-react-components` 0.5+).
113
+ 2. Move legacy header buttons (refresh, settings, language, theme) into `headerSlot`. Role badges are dropped: administrator affordances belong to the administrator navigation group and pages, and theme/language preferences live in the `LinkitMyInfo` profile dialog (`linkit-react-components` 0.6+).
110
114
  3. Move any sidebar footer item (settings, environment status) into the last navigation group.
111
- 4. Pass the logo pair and delete per-call sizes; the shell normalizes every mark to its 1.75rem box.
115
+ 4. Pass the single logo mark and delete per-call sizes; the shell normalizes every mark to its 1.75rem box.
112
116
 
113
117
  ## Development
114
118
 
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export { AppLayout } from "./app-layout.js";
2
- export type { AppLayoutProps, AppLogo, AppNavGroup, AppNavItem } from "./types.js";
2
+ export type { AppLayoutProps, AppNavGroup, AppNavItem } from "./types.js";
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { jsxs as l, jsx as e, Fragment as w } from "react/jsx-runtime";
2
- import { useState as g, useCallback as x, useEffect as M, useSyncExternalStore as L } from "react";
3
- import { Dialog as m } from "@base-ui/react/dialog";
4
- import { Tooltip as h } from "@base-ui/react/tooltip";
5
- import { useLinkit as N, LinkitMyInfo as S } from "linkit-react-components";
6
- import { useLocation as k, NavLink as C } from "react-router-dom";
1
+ import { jsxs as s, jsx as e, Fragment as y } from "react/jsx-runtime";
2
+ import { useState as f, useCallback as g, useEffect as w, useSyncExternalStore as M } from "react";
3
+ import { Dialog as h } from "@base-ui/react/dialog";
4
+ import { Tooltip as p } from "@base-ui/react/tooltip";
5
+ import { useLinkit as L, LinkitMyInfo as S } from "linkit-react-components";
6
+ import { useLocation as v, NavLink as C } from "react-router-dom";
7
7
  const O = {
8
8
  closeNavigation: "Close navigation",
9
9
  navigation: "Navigation",
@@ -16,8 +16,8 @@ const O = {
16
16
  function I(a) {
17
17
  return a.toLowerCase().startsWith("zh") ? P : O;
18
18
  }
19
- function T() {
20
- return /* @__PURE__ */ l(
19
+ function E() {
20
+ return /* @__PURE__ */ s(
21
21
  "svg",
22
22
  {
23
23
  "aria-hidden": "true",
@@ -34,8 +34,8 @@ function T() {
34
34
  }
35
35
  );
36
36
  }
37
- function E() {
38
- return /* @__PURE__ */ l(
37
+ function T() {
38
+ return /* @__PURE__ */ s(
39
39
  "svg",
40
40
  {
41
41
  "aria-hidden": "true",
@@ -52,65 +52,65 @@ function E() {
52
52
  }
53
53
  );
54
54
  }
55
- const b = "sidebar_state", W = 3600 * 24 * 7, _ = "(max-width: 767px)";
55
+ const m = "sidebar_state", W = 3600 * 24 * 7, N = "(max-width: 767px)";
56
56
  function $() {
57
- const a = document.cookie.split("; ").find((n) => n.startsWith(`${b}=`));
58
- return a ? a.slice(b.length + 1) !== "false" : !0;
57
+ const a = document.cookie.split("; ").find((n) => n.startsWith(`${m}=`));
58
+ return a ? a.slice(m.length + 1) !== "false" : !0;
59
59
  }
60
60
  function j(a) {
61
- document.cookie = `${b}=${a}; path=/; max-age=${W}`;
61
+ document.cookie = `${m}=${a}; path=/; max-age=${W}`;
62
62
  }
63
63
  function B(a) {
64
- const n = window.matchMedia(_);
64
+ const n = window.matchMedia(N);
65
65
  return n.addEventListener("change", a), () => n.removeEventListener("change", a);
66
66
  }
67
67
  function A() {
68
- return L(
68
+ return M(
69
69
  B,
70
- () => window.matchMedia(_).matches,
70
+ () => window.matchMedia(N).matches,
71
71
  () => !1
72
72
  );
73
73
  }
74
74
  function D() {
75
- const a = A(), [n, t] = g($), [c, i] = g(!1), d = x((r) => {
76
- t(r), j(r);
77
- }, []), u = x(() => {
75
+ const a = A(), [n, t] = f($), [l, r] = f(!1), d = g((c) => {
76
+ t(c), j(c);
77
+ }, []), u = g(() => {
78
78
  if (a) {
79
- i((r) => !r);
79
+ r((c) => !c);
80
80
  return;
81
81
  }
82
82
  d(!n);
83
83
  }, [a, n, d]);
84
- return M(() => {
85
- function r(s) {
86
- (s.metaKey || s.ctrlKey) && s.key === "b" && (s.preventDefault(), u());
84
+ return w(() => {
85
+ function c(i) {
86
+ (i.metaKey || i.ctrlKey) && i.key === "b" && (i.preventDefault(), u());
87
87
  }
88
- return window.addEventListener("keydown", r), () => window.removeEventListener("keydown", r);
89
- }, [u]), { isMobile: a, open: n, openMobile: c, setOpenMobile: i, toggleSidebar: u };
88
+ return window.addEventListener("keydown", c), () => window.removeEventListener("keydown", c);
89
+ }, [u]), { isMobile: a, open: n, openMobile: l, setOpenMobile: r, toggleSidebar: u };
90
90
  }
91
- function y(a, n) {
91
+ function k(a, n) {
92
92
  let t;
93
- for (const c of a)
94
- for (const i of c.items)
95
- (i.to === "/" ? n === "/" : n === i.to || n.startsWith(`${i.to}/`)) && (!t || i.to.length > t.to.length) && (t = i);
93
+ for (const l of a)
94
+ for (const r of l.items)
95
+ (r.to === "/" ? n === "/" : n === r.to || n.startsWith(`${r.to}/`)) && (!t || r.to.length > t.to.length) && (t = r);
96
96
  return t;
97
97
  }
98
98
  function K({
99
99
  collapsed: a,
100
100
  end: n,
101
101
  item: t,
102
- onNavigate: c
102
+ onNavigate: l
103
103
  }) {
104
- return /* @__PURE__ */ l(h.Root, { children: [
105
- /* @__PURE__ */ l(
106
- h.Trigger,
104
+ return /* @__PURE__ */ s(p.Root, { children: [
105
+ /* @__PURE__ */ s(
106
+ p.Trigger,
107
107
  {
108
108
  render: /* @__PURE__ */ e(
109
109
  C,
110
110
  {
111
111
  className: "ux-nav-item",
112
112
  end: n,
113
- onClick: c,
113
+ onClick: l,
114
114
  to: t.to
115
115
  }
116
116
  ),
@@ -120,102 +120,102 @@ function K({
120
120
  ]
121
121
  }
122
122
  ),
123
- /* @__PURE__ */ e(h.Portal, { children: /* @__PURE__ */ e(
124
- h.Positioner,
123
+ /* @__PURE__ */ e(p.Portal, { children: /* @__PURE__ */ e(
124
+ p.Positioner,
125
125
  {
126
126
  className: "ux-tooltip-positioner",
127
127
  side: "right",
128
128
  sideOffset: 6,
129
- children: /* @__PURE__ */ e(h.Popup, { className: "ux-tooltip", hidden: !a, children: t.label })
129
+ children: /* @__PURE__ */ e(p.Popup, { className: "ux-tooltip", hidden: !a, children: t.label })
130
130
  }
131
131
  ) })
132
132
  ] });
133
133
  }
134
- function v({
134
+ function x({
135
135
  copy: a,
136
136
  logo: n,
137
137
  nav: t,
138
- onNavigate: c,
139
- state: i,
138
+ onNavigate: l,
139
+ state: r,
140
140
  title: d
141
141
  }) {
142
- const { resolvedTheme: u } = N(), { pathname: r } = k(), s = y(t, r);
143
- return /* @__PURE__ */ l(w, { children: [
144
- /* @__PURE__ */ l("div", { className: "ux-sidebar__header", children: [
145
- /* @__PURE__ */ e("span", { className: "ux-sidebar__logo", children: u === "dark" ? n.dark : n.light }),
142
+ const { pathname: u } = v(), c = k(t, u);
143
+ return /* @__PURE__ */ s(y, { children: [
144
+ /* @__PURE__ */ s("div", { className: "ux-sidebar__header", children: [
145
+ /* @__PURE__ */ e("span", { className: "ux-sidebar__logo", children: n }),
146
146
  /* @__PURE__ */ e("span", { className: "ux-sidebar__title", children: d })
147
147
  ] }),
148
- /* @__PURE__ */ e("nav", { "aria-label": a.navigation, className: "ux-sidebar__nav", children: t.map((o) => /* @__PURE__ */ l("div", { className: "ux-nav-group", children: [
149
- /* @__PURE__ */ e("div", { className: "ux-nav-group__label", children: o.label }),
150
- o.items.map((p) => (
148
+ /* @__PURE__ */ e("nav", { "aria-label": a.navigation, className: "ux-sidebar__nav", children: t.map((i) => /* @__PURE__ */ s("div", { className: "ux-nav-group", children: [
149
+ /* @__PURE__ */ e("div", { className: "ux-nav-group__label", children: i.label }),
150
+ i.items.map((o) => (
151
151
  // NavLink would mark a parent section current on its children's
152
152
  // routes; follow matchActiveItem instead so only the deepest
153
153
  // match (or the exact path) stays marked.
154
154
  /* @__PURE__ */ e(
155
155
  K,
156
156
  {
157
- collapsed: i === "collapsed",
158
- end: p !== s || p.to === r,
159
- item: p,
160
- onNavigate: c
157
+ collapsed: r === "collapsed",
158
+ end: o !== c || o.to === u,
159
+ item: o,
160
+ onNavigate: l
161
161
  },
162
- p.to
162
+ o.to
163
163
  )
164
164
  ))
165
- ] }, o.label)) })
165
+ ] }, i.label)) })
166
166
  ] });
167
167
  }
168
168
  function H({
169
169
  children: a,
170
170
  headerSlot: n,
171
171
  logo: t,
172
- nav: c,
173
- pageTitle: i,
172
+ nav: l,
173
+ pageTitle: r,
174
174
  title: d
175
175
  }) {
176
- var f;
177
- const { pathname: u } = k(), { lang: r } = N(), s = I(r), o = D(), p = i ?? ((f = y(c, u)) == null ? void 0 : f.label);
178
- return /* @__PURE__ */ e(h.Provider, { delay: 0, children: /* @__PURE__ */ l(
176
+ var b;
177
+ const { pathname: u } = v(), { lang: c } = L(), i = I(c), o = D(), _ = r ?? ((b = k(l, u)) == null ? void 0 : b.label);
178
+ return /* @__PURE__ */ e(p.Provider, { delay: 0, children: /* @__PURE__ */ s(
179
179
  "div",
180
180
  {
181
181
  className: "ux-app-layout",
182
182
  "data-state": o.open ? "expanded" : "collapsed",
183
183
  children: [
184
184
  /* @__PURE__ */ e("aside", { className: "ux-sidebar", children: /* @__PURE__ */ e(
185
- v,
185
+ x,
186
186
  {
187
- copy: s,
187
+ copy: i,
188
188
  logo: t,
189
- nav: c,
189
+ nav: l,
190
190
  state: o.open ? "expanded" : "collapsed",
191
191
  title: d
192
192
  }
193
193
  ) }),
194
194
  o.isMobile ? /* @__PURE__ */ e(
195
- m.Root,
195
+ h.Root,
196
196
  {
197
197
  open: o.openMobile,
198
198
  onOpenChange: o.setOpenMobile,
199
- children: /* @__PURE__ */ l(m.Portal, { children: [
200
- /* @__PURE__ */ e(m.Backdrop, { className: "ux-drawer-backdrop" }),
201
- /* @__PURE__ */ l(m.Popup, { className: "ux-drawer", children: [
202
- /* @__PURE__ */ e(m.Title, { className: "ux-visually-hidden", children: s.navigation }),
199
+ children: /* @__PURE__ */ s(h.Portal, { children: [
200
+ /* @__PURE__ */ e(h.Backdrop, { className: "ux-drawer-backdrop" }),
201
+ /* @__PURE__ */ s(h.Popup, { className: "ux-drawer", children: [
202
+ /* @__PURE__ */ e(h.Title, { className: "ux-visually-hidden", children: i.navigation }),
203
203
  /* @__PURE__ */ e(
204
204
  "button",
205
205
  {
206
- "aria-label": s.closeNavigation,
206
+ "aria-label": i.closeNavigation,
207
207
  className: "ux-drawer__close",
208
208
  type: "button",
209
209
  onClick: () => o.setOpenMobile(!1),
210
- children: /* @__PURE__ */ e(T, {})
210
+ children: /* @__PURE__ */ e(E, {})
211
211
  }
212
212
  ),
213
213
  /* @__PURE__ */ e(
214
- v,
214
+ x,
215
215
  {
216
- copy: s,
216
+ copy: i,
217
217
  logo: t,
218
- nav: c,
218
+ nav: l,
219
219
  onNavigate: () => o.setOpenMobile(!1),
220
220
  state: "expanded",
221
221
  title: d
@@ -225,21 +225,21 @@ function H({
225
225
  ] })
226
226
  }
227
227
  ) : null,
228
- /* @__PURE__ */ l("div", { className: "ux-shell", children: [
229
- /* @__PURE__ */ l("header", { className: "ux-app-header", children: [
228
+ /* @__PURE__ */ s("div", { className: "ux-shell", children: [
229
+ /* @__PURE__ */ s("header", { className: "ux-app-header", children: [
230
230
  /* @__PURE__ */ e(
231
231
  "button",
232
232
  {
233
233
  "aria-expanded": o.isMobile ? o.openMobile : o.open,
234
- "aria-label": s.toggleSidebar,
234
+ "aria-label": i.toggleSidebar,
235
235
  className: "ux-app-header__trigger",
236
236
  type: "button",
237
237
  onClick: o.toggleSidebar,
238
- children: /* @__PURE__ */ e(E, {})
238
+ children: /* @__PURE__ */ e(T, {})
239
239
  }
240
240
  ),
241
241
  /* @__PURE__ */ e("span", { "aria-hidden": "true", className: "ux-app-header__separator" }),
242
- /* @__PURE__ */ e("p", { className: "ux-app-header__title", children: p }),
242
+ /* @__PURE__ */ e("p", { className: "ux-app-header__title", children: _ }),
243
243
  /* @__PURE__ */ e("div", { className: "ux-app-header__slot", children: n }),
244
244
  /* @__PURE__ */ e(S, {})
245
245
  ] }),
package/dist/styles.css CHANGED
@@ -1,11 +1,52 @@
1
1
  /* @zccz14/ux — unified AppLayout shell. Include this file once from the consuming application. */
2
2
 
3
+ /*
4
+ * Colour layer. Every shell colour resolves through a --ux-* token, and every
5
+ * --ux-* token prefers the host app's shadcn token over a built-in fallback:
6
+ * light values on :root, dark values on .dark. Both sets exist because the
7
+ * fallback is the only thing left when an app omits part of the shadcn token
8
+ * set — without a dark set, a missing --sidebar would keep the sidebar light
9
+ * in dark mode.
10
+ */
11
+ :root {
12
+ --ux-background: var(--background, #fff);
13
+ --ux-foreground: var(--foreground, #0f172a);
14
+ --ux-border: var(--border, #e2e8f0);
15
+ --ux-accent: var(--accent, var(--muted, #f1f5f9));
16
+ --ux-ring: var(--ring, #2563eb);
17
+ --ux-sidebar: var(--sidebar, #fafafa);
18
+ --ux-sidebar-foreground: var(--sidebar-foreground, var(--foreground, #0f172a));
19
+ --ux-sidebar-accent: var(--sidebar-accent, var(--accent, #f1f5f9));
20
+ --ux-sidebar-accent-foreground: var(--sidebar-accent-foreground, var(--foreground, #0f172a));
21
+ --ux-sidebar-border: var(--sidebar-border, var(--border, #e2e8f0));
22
+ --ux-sidebar-ring: var(--sidebar-ring, var(--ring, #2563eb));
23
+ }
24
+
25
+ .dark {
26
+ --ux-background: var(--background, #121212);
27
+ --ux-foreground: var(--foreground, #f2f2f2);
28
+ --ux-border: var(--border, #383838);
29
+ --ux-accent: var(--accent, var(--muted, #303030));
30
+ --ux-ring: var(--ring, #d4d4d4);
31
+ --ux-sidebar: var(--sidebar, #161616);
32
+ --ux-sidebar-foreground: var(--sidebar-foreground, var(--foreground, #f2f2f2));
33
+ --ux-sidebar-accent: var(--sidebar-accent, var(--accent, #303030));
34
+ --ux-sidebar-accent-foreground: var(--sidebar-accent-foreground, var(--foreground, #f2f2f2));
35
+ --ux-sidebar-border: var(--sidebar-border, var(--border, #383838));
36
+ --ux-sidebar-ring: var(--sidebar-ring, var(--ring, #d4d4d4));
37
+ }
38
+
3
39
  .ux-app-layout {
4
40
  display: flex;
5
41
  height: 100svh;
6
42
  overflow: hidden;
7
- color: var(--foreground, #0f172a);
8
- background: var(--background, #fff);
43
+ color: var(--ux-foreground);
44
+ color-scheme: light;
45
+ background: var(--ux-background);
46
+ }
47
+
48
+ .dark .ux-app-layout {
49
+ color-scheme: dark;
9
50
  }
10
51
 
11
52
  /* --- Sidebar --- */
@@ -16,9 +57,9 @@
16
57
  flex-direction: column;
17
58
  width: 16rem;
18
59
  overflow: hidden;
19
- color: var(--sidebar-foreground, var(--foreground, #0f172a));
20
- background: var(--sidebar, #fafafa);
21
- border-right: 1px solid var(--sidebar-border, #e2e8f0);
60
+ color: var(--ux-sidebar-foreground);
61
+ background: var(--ux-sidebar);
62
+ border-right: 1px solid var(--ux-sidebar-border);
22
63
  transition: width 200ms linear;
23
64
  }
24
65
 
@@ -46,7 +87,7 @@
46
87
  padding-right: 3rem;
47
88
  }
48
89
 
49
- /* The framework owns the logo display size: both logo variants render inside a fixed 1.75rem box. */
90
+ /* The framework owns the logo display size: the mark renders inside a fixed 1.75rem box. */
50
91
  .ux-sidebar__logo {
51
92
  display: flex;
52
93
  flex: none;
@@ -105,11 +146,12 @@
105
146
  font-weight: 500;
106
147
  white-space: nowrap;
107
148
  opacity: 0.7;
108
- transition: opacity 200ms linear;
149
+ transition: margin-top 200ms linear, opacity 200ms linear;
109
150
  }
110
151
 
111
152
  .ux-app-layout[data-state="collapsed"] .ux-nav-group__label {
112
- display: none;
153
+ margin-top: -2rem;
154
+ opacity: 0;
113
155
  }
114
156
 
115
157
  .ux-nav-item {
@@ -134,19 +176,19 @@
134
176
  }
135
177
 
136
178
  .ux-nav-item:hover {
137
- color: var(--sidebar-accent-foreground, #0f172a);
138
- background: var(--sidebar-accent, #f1f5f9);
179
+ color: var(--ux-sidebar-accent-foreground);
180
+ background: var(--ux-sidebar-accent);
139
181
  }
140
182
 
141
183
  .ux-nav-item:focus-visible {
142
- outline: 2px solid var(--sidebar-ring, #2563eb);
184
+ outline: 2px solid var(--ux-sidebar-ring);
143
185
  outline-offset: -2px;
144
186
  }
145
187
 
146
188
  .ux-nav-item[aria-current="page"] {
147
- color: var(--sidebar-accent-foreground, #0f172a);
189
+ color: var(--ux-sidebar-accent-foreground);
148
190
  font-weight: 500;
149
- background: var(--sidebar-accent, #f1f5f9);
191
+ background: var(--ux-sidebar-accent);
150
192
  }
151
193
 
152
194
  .ux-app-layout[data-state="collapsed"] .ux-nav-item {
@@ -204,8 +246,8 @@
204
246
  gap: 0.75rem;
205
247
  height: 3.5rem;
206
248
  padding: 0 1rem;
207
- background: var(--background, #fff);
208
- border-bottom: 1px solid var(--border, #e2e8f0);
249
+ background: var(--ux-background);
250
+ border-bottom: 1px solid var(--ux-border);
209
251
  }
210
252
 
211
253
  .ux-app-header__trigger {
@@ -227,11 +269,11 @@
227
269
  }
228
270
 
229
271
  .ux-app-header__trigger:hover {
230
- background: var(--accent, var(--muted, #f1f5f9));
272
+ background: var(--ux-accent);
231
273
  }
232
274
 
233
275
  .ux-app-header__trigger:focus-visible {
234
- outline: 2px solid var(--ring, #2563eb);
276
+ outline: 2px solid var(--ux-ring);
235
277
  outline-offset: 2px;
236
278
  }
237
279
 
@@ -244,7 +286,7 @@
244
286
  flex: none;
245
287
  width: 1px;
246
288
  height: 1.25rem;
247
- background: var(--border, #e2e8f0);
289
+ background: var(--ux-border);
248
290
  }
249
291
 
250
292
  .ux-app-header__title {
@@ -302,13 +344,18 @@
302
344
  width: 18rem;
303
345
  max-width: calc(100vw - 3rem);
304
346
  overflow: hidden;
305
- color: var(--sidebar-foreground, var(--foreground, #0f172a));
306
- background: var(--sidebar, #fafafa);
307
- border-right: 1px solid var(--sidebar-border, #e2e8f0);
347
+ color: var(--ux-sidebar-foreground);
348
+ color-scheme: light;
349
+ background: var(--ux-sidebar);
350
+ border-right: 1px solid var(--ux-sidebar-border);
308
351
  outline: none;
309
352
  transition: transform 200ms ease-out, opacity 200ms ease-out;
310
353
  }
311
354
 
355
+ .dark .ux-drawer {
356
+ color-scheme: dark;
357
+ }
358
+
312
359
  .ux-drawer[data-starting-style],
313
360
  .ux-drawer[data-ending-style] {
314
361
  opacity: 0;
@@ -335,11 +382,11 @@
335
382
  }
336
383
 
337
384
  .ux-drawer__close:hover {
338
- background: var(--sidebar-accent, #f1f5f9);
385
+ background: var(--ux-sidebar-accent);
339
386
  }
340
387
 
341
388
  .ux-drawer__close:focus-visible {
342
- outline: 2px solid var(--sidebar-ring, #2563eb);
389
+ outline: 2px solid var(--ux-sidebar-ring);
343
390
  outline-offset: 2px;
344
391
  }
345
392
 
@@ -360,12 +407,12 @@
360
407
  align-items: center;
361
408
  max-width: 16rem;
362
409
  padding: 0.375rem 0.75rem;
363
- color: var(--background, #fff);
410
+ color: var(--ux-background);
364
411
  font-size: 0.75rem;
365
412
  font-weight: 500;
366
413
  line-height: 1.25;
367
414
  white-space: nowrap;
368
- background: var(--foreground, #0f172a);
415
+ background: var(--ux-foreground);
369
416
  border-radius: 0.375rem;
370
417
  box-shadow: 0 4px 12px rgb(0 0 0 / 0.12);
371
418
  transition: opacity 120ms ease-out, transform 120ms ease-out;
package/dist/types.d.ts CHANGED
@@ -1,8 +1,4 @@
1
1
  import type { ReactNode } from "react";
2
- export type AppLogo = {
3
- light: ReactNode;
4
- dark: ReactNode;
5
- };
6
2
  export type AppNavItem = {
7
3
  to: string;
8
4
  label: string;
@@ -13,7 +9,7 @@ export type AppNavGroup = {
13
9
  items: AppNavItem[];
14
10
  };
15
11
  export type AppLayoutProps = {
16
- logo: AppLogo;
12
+ logo: ReactNode;
17
13
  title: string;
18
14
  nav: AppNavGroup[];
19
15
  headerSlot?: ReactNode;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zccz14/ux",
3
- "version": "0.1.6",
4
- "description": "Unified AppLayout shell for the NTNL app family: one animated icon sidebar, a fixed-size dark/light logo, and the Linkit-integrated AppHeader.",
3
+ "version": "0.2.0",
4
+ "description": "Unified AppLayout shell for the NTNL app family: one animated icon sidebar, a fixed-size logo box, and the Linkit-integrated AppHeader.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",