@zccz14/ux 0.2.1 → 0.3.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,6 +7,7 @@
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
+ - **Favicon** — the same mark becomes the page favicon: the shell serialises the rendered logo into `link[rel="icon"]`, pins the colour the mark currently shows (a favicon has no app stylesheet, so `currentColor` would otherwise fall back to black), and re-renders it whenever the theme changes. Pass `favicon={false}` to keep an app-owned icon.
10
11
  - **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
12
  - **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
13
 
@@ -74,7 +75,8 @@ export function App() {
74
75
 
75
76
  | Prop | Type | Required | Description |
76
77
  | --- | --- | --- | --- |
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
+ | `logo` | `ReactNode` | yes | The logo mark. The shell renders it at its own fixed 1.75rem size and mirrors it into the page favicon; pass a mark that follows the current colour. |
79
+ | `favicon` | `boolean` | no | Defaults to `true`: the logo mark is written to `link[rel="icon"]` and re-rendered on theme changes. Set `false` to leave the app's own icon alone. |
78
80
  | `title` | `string` | yes | App name beside the logo. Hidden while the sidebar is collapsed. |
79
81
  | `nav` | `AppNavGroup[]` | yes | Navigation groups rendered in the sidebar. |
80
82
  | `headerSlot` | `ReactNode` | no | Rendered immediately left of `LinkitMyInfo` in the AppHeader. Use it for page-level actions such as refresh. |
@@ -97,6 +99,7 @@ type AppNavGroup = { label: string; items: AppNavItem[] };
97
99
  - **Mobile** — below 768px the sidebar becomes a modal navigation drawer: the trigger opens it, tapping a link or the backdrop closes it.
98
100
  - **Collapsed tooltips** — icon-only items reveal their label in a tooltip on hover and focus.
99
101
  - **Reduced motion** — `prefers-reduced-motion: reduce` disables the animations.
102
+ - **Favicon** — an `<svg>` logo is serialised to a `data:image/svg+xml` icon with its rendered colour pinned; an `<img>` logo reuses the image URL. The icon is rewritten when `.dark` flips, when `prefers-color-scheme` changes, and when the mark itself is swapped.
100
103
  - **Layout contract** — the shell is viewport-locked (`100svh`); the main region is the page's only scroll container and applies the standard `1rem` / `1.5rem` padding. Pages own their inner layout.
101
104
 
102
105
  ## Styling
@@ -113,6 +116,7 @@ Theme state itself is not the shell's business: whatever toggles `.dark` on `<ht
113
116
  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+).
114
117
  3. Move any sidebar footer item (settings, environment status) into the last navigation group.
115
118
  4. Pass the single logo mark and delete per-call sizes; the shell normalizes every mark to its 1.75rem box.
119
+ 5. Delete the app's own `applyFavicon` / favicon effect: the shell now owns `link[rel="icon"]`. Keep a hand-written favicon only by passing `favicon={false}`.
116
120
 
117
121
  ## Development
118
122
 
@@ -1,2 +1,2 @@
1
1
  import type { AppLayoutProps } from "./types.js";
2
- export declare function AppLayout({ children, headerSlot, logo, nav, pageTitle, title, }: AppLayoutProps): import("react").JSX.Element;
2
+ export declare function AppLayout({ children, favicon, headerSlot, logo, nav, pageTitle, title, }: AppLayoutProps): import("react").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { type RefObject } from "react";
2
+ /**
3
+ * Mirrors the rendered logo into the page favicon: the tab shows the same mark
4
+ * as the sidebar, including its current colour, and follows theme changes.
5
+ */
6
+ export declare function useLogoFavicon(logo: RefObject<HTMLElement | null>, enabled: boolean): void;
package/dist/index.js CHANGED
@@ -1,23 +1,65 @@
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";
1
+ import { jsxs as d, jsx as n, Fragment as L } from "react/jsx-runtime";
2
+ import { useEffect as k, useRef as M, useState as v, useCallback as x, useSyncExternalStore as C } from "react";
3
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
- const O = {
4
+ import { Tooltip as m } from "@base-ui/react/tooltip";
5
+ import { useLinkit as E, LinkitMyInfo as O } from "linkit-react-components";
6
+ import { useLocation as y, NavLink as I } from "react-router-dom";
7
+ const P = {
8
8
  closeNavigation: "Close navigation",
9
9
  navigation: "Navigation",
10
10
  toggleSidebar: "Toggle sidebar"
11
- }, P = {
11
+ }, T = {
12
12
  closeNavigation: "关闭导航",
13
13
  navigation: "导航",
14
14
  toggleSidebar: "切换侧边栏"
15
15
  };
16
- function I(a) {
17
- return a.toLowerCase().startsWith("zh") ? P : O;
16
+ function A(e) {
17
+ return e.toLowerCase().startsWith("zh") ? T : P;
18
18
  }
19
- function E() {
20
- return /* @__PURE__ */ s(
19
+ const $ = 'link[rel~="icon"]';
20
+ function R(e) {
21
+ const t = e.cloneNode(!0);
22
+ return t.setAttribute("xmlns", "http://www.w3.org/2000/svg"), t.style.color = getComputedStyle(e).color, new XMLSerializer().serializeToString(t);
23
+ }
24
+ function W(e) {
25
+ if (e instanceof SVGSVGElement)
26
+ return {
27
+ href: `data:image/svg+xml,${encodeURIComponent(R(e))}`,
28
+ type: "image/svg+xml"
29
+ };
30
+ const { currentSrc: t, src: o } = e, i = t || o;
31
+ return /\.svg([?#]|$)/i.test(i) ? { href: i, type: "image/svg+xml" } : { href: i };
32
+ }
33
+ function j() {
34
+ const e = document.querySelector($);
35
+ if (e)
36
+ return e;
37
+ const t = document.createElement("link");
38
+ return t.rel = "icon", document.head.append(t), t;
39
+ }
40
+ function z(e, t) {
41
+ k(() => {
42
+ if (!t)
43
+ return;
44
+ const o = () => {
45
+ var l;
46
+ const p = (l = e.current) == null ? void 0 : l.querySelector("svg, img");
47
+ if (!p)
48
+ return;
49
+ const { href: u, type: s } = W(p), c = j();
50
+ s ? c.type = s : c.removeAttribute("type"), c.getAttribute("href") !== u && c.setAttribute("href", u);
51
+ };
52
+ o();
53
+ const i = new MutationObserver(o);
54
+ i.observe(document.documentElement, { attributes: !0, attributeFilter: ["class", "style"] }), e.current && i.observe(e.current, { childList: !0, subtree: !0 });
55
+ const r = window.matchMedia("(prefers-color-scheme: dark)");
56
+ return r.addEventListener("change", o), () => {
57
+ i.disconnect(), r.removeEventListener("change", o);
58
+ };
59
+ }, [t, e]);
60
+ }
61
+ function B() {
62
+ return /* @__PURE__ */ d(
21
63
  "svg",
22
64
  {
23
65
  "aria-hidden": "true",
@@ -28,14 +70,14 @@ function E() {
28
70
  strokeWidth: "2",
29
71
  viewBox: "0 0 24 24",
30
72
  children: [
31
- /* @__PURE__ */ e("path", { d: "M18 6 6 18" }),
32
- /* @__PURE__ */ e("path", { d: "m6 6 12 12" })
73
+ /* @__PURE__ */ n("path", { d: "M18 6 6 18" }),
74
+ /* @__PURE__ */ n("path", { d: "m6 6 12 12" })
33
75
  ]
34
76
  }
35
77
  );
36
78
  }
37
- function T() {
38
- return /* @__PURE__ */ s(
79
+ function F() {
80
+ return /* @__PURE__ */ d(
39
81
  "svg",
40
82
  {
41
83
  "aria-hidden": "true",
@@ -46,209 +88,212 @@ function T() {
46
88
  strokeWidth: "2",
47
89
  viewBox: "0 0 24 24",
48
90
  children: [
49
- /* @__PURE__ */ e("rect", { height: "18", rx: "2", width: "18", x: "3", y: "3" }),
50
- /* @__PURE__ */ e("path", { d: "M9 3v18" })
91
+ /* @__PURE__ */ n("rect", { height: "18", rx: "2", width: "18", x: "3", y: "3" }),
92
+ /* @__PURE__ */ n("path", { d: "M9 3v18" })
51
93
  ]
52
94
  }
53
95
  );
54
96
  }
55
- const m = "sidebar_state", W = 3600 * 24 * 7, N = "(max-width: 767px)";
56
- function $() {
57
- const a = document.cookie.split("; ").find((n) => n.startsWith(`${m}=`));
58
- return a ? a.slice(m.length + 1) !== "false" : !0;
97
+ const f = "sidebar_state", q = 3600 * 24 * 7, w = "(max-width: 767px)";
98
+ function D() {
99
+ const e = document.cookie.split("; ").find((t) => t.startsWith(`${f}=`));
100
+ return e ? e.slice(f.length + 1) !== "false" : !0;
59
101
  }
60
- function j(a) {
61
- document.cookie = `${m}=${a}; path=/; max-age=${W}`;
102
+ function K(e) {
103
+ document.cookie = `${f}=${e}; path=/; max-age=${q}`;
62
104
  }
63
- function B(a) {
64
- const n = window.matchMedia(N);
65
- return n.addEventListener("change", a), () => n.removeEventListener("change", a);
105
+ function G(e) {
106
+ const t = window.matchMedia(w);
107
+ return t.addEventListener("change", e), () => t.removeEventListener("change", e);
66
108
  }
67
- function A() {
68
- return M(
69
- B,
70
- () => window.matchMedia(N).matches,
109
+ function V() {
110
+ return C(
111
+ G,
112
+ () => window.matchMedia(w).matches,
71
113
  () => !1
72
114
  );
73
115
  }
74
- function D() {
75
- const a = A(), [n, t] = f($), [l, r] = f(!1), d = g((c) => {
76
- t(c), j(c);
77
- }, []), u = g(() => {
78
- if (a) {
79
- r((c) => !c);
116
+ function Q() {
117
+ const e = V(), [t, o] = v(D), [i, r] = v(!1), p = x((s) => {
118
+ o(s), K(s);
119
+ }, []), u = x(() => {
120
+ if (e) {
121
+ r((s) => !s);
80
122
  return;
81
123
  }
82
- d(!n);
83
- }, [a, n, d]);
84
- return w(() => {
85
- function c(i) {
86
- (i.metaKey || i.ctrlKey) && i.key === "b" && (i.preventDefault(), u());
124
+ p(!t);
125
+ }, [e, t, p]);
126
+ return k(() => {
127
+ function s(c) {
128
+ (c.metaKey || c.ctrlKey) && c.key === "b" && (c.preventDefault(), u());
87
129
  }
88
- return window.addEventListener("keydown", c), () => window.removeEventListener("keydown", c);
89
- }, [u]), { isMobile: a, open: n, openMobile: l, setOpenMobile: r, toggleSidebar: u };
90
- }
91
- function k(a, n) {
92
- let t;
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
- return t;
97
- }
98
- function K({
99
- collapsed: a,
100
- end: n,
101
- item: t,
102
- onNavigate: l
130
+ return window.addEventListener("keydown", s), () => window.removeEventListener("keydown", s);
131
+ }, [u]), { isMobile: e, open: t, openMobile: i, setOpenMobile: r, toggleSidebar: u };
132
+ }
133
+ function _(e, t) {
134
+ let o;
135
+ for (const i of e)
136
+ for (const r of i.items)
137
+ (r.to === "/" ? t === "/" : t === r.to || t.startsWith(`${r.to}/`)) && (!o || r.to.length > o.to.length) && (o = r);
138
+ return o;
139
+ }
140
+ function U({
141
+ collapsed: e,
142
+ end: t,
143
+ item: o,
144
+ onNavigate: i
103
145
  }) {
104
- return /* @__PURE__ */ s(p.Root, { children: [
105
- /* @__PURE__ */ s(
106
- p.Trigger,
146
+ return /* @__PURE__ */ d(m.Root, { children: [
147
+ /* @__PURE__ */ d(
148
+ m.Trigger,
107
149
  {
108
- render: /* @__PURE__ */ e(
109
- C,
150
+ render: /* @__PURE__ */ n(
151
+ I,
110
152
  {
111
153
  className: "ux-nav-item",
112
- end: n,
113
- onClick: l,
114
- to: t.to
154
+ end: t,
155
+ onClick: i,
156
+ to: o.to
115
157
  }
116
158
  ),
117
159
  children: [
118
- /* @__PURE__ */ e("span", { className: "ux-nav-item__icon", children: t.icon }),
119
- /* @__PURE__ */ e("span", { className: "ux-nav-item__text", children: t.label })
160
+ /* @__PURE__ */ n("span", { className: "ux-nav-item__icon", children: o.icon }),
161
+ /* @__PURE__ */ n("span", { className: "ux-nav-item__text", children: o.label })
120
162
  ]
121
163
  }
122
164
  ),
123
- /* @__PURE__ */ e(p.Portal, { children: /* @__PURE__ */ e(
124
- p.Positioner,
165
+ /* @__PURE__ */ n(m.Portal, { children: /* @__PURE__ */ n(
166
+ m.Positioner,
125
167
  {
126
168
  className: "ux-tooltip-positioner",
127
169
  side: "right",
128
170
  sideOffset: 6,
129
- children: /* @__PURE__ */ e(p.Popup, { className: "ux-tooltip", hidden: !a, children: t.label })
171
+ children: /* @__PURE__ */ n(m.Popup, { className: "ux-tooltip", hidden: !e, children: o.label })
130
172
  }
131
173
  ) })
132
174
  ] });
133
175
  }
134
- function x({
135
- copy: a,
136
- logo: n,
137
- nav: t,
138
- onNavigate: l,
139
- state: r,
140
- title: d
176
+ function N({
177
+ copy: e,
178
+ logo: t,
179
+ logoRef: o,
180
+ nav: i,
181
+ onNavigate: r,
182
+ state: p,
183
+ title: u
141
184
  }) {
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
- /* @__PURE__ */ e("span", { className: "ux-sidebar__title", children: d })
185
+ const { pathname: s } = y(), c = _(i, s);
186
+ return /* @__PURE__ */ d(L, { children: [
187
+ /* @__PURE__ */ d("div", { className: "ux-sidebar__header", children: [
188
+ /* @__PURE__ */ n("span", { className: "ux-sidebar__logo", ref: o, children: t }),
189
+ /* @__PURE__ */ n("span", { className: "ux-sidebar__title", children: u })
147
190
  ] }),
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) => (
191
+ /* @__PURE__ */ n("nav", { "aria-label": e.navigation, className: "ux-sidebar__nav", children: i.map((l) => /* @__PURE__ */ d("div", { className: "ux-nav-group", children: [
192
+ /* @__PURE__ */ n("div", { className: "ux-nav-group__label", children: l.label }),
193
+ l.items.map((a) => (
151
194
  // NavLink would mark a parent section current on its children's
152
195
  // routes; follow matchActiveItem instead so only the deepest
153
196
  // match (or the exact path) stays marked.
154
- /* @__PURE__ */ e(
155
- K,
197
+ /* @__PURE__ */ n(
198
+ U,
156
199
  {
157
- collapsed: r === "collapsed",
158
- end: o !== c || o.to === u,
159
- item: o,
160
- onNavigate: l
200
+ collapsed: p === "collapsed",
201
+ end: a !== c || a.to === s,
202
+ item: a,
203
+ onNavigate: r
161
204
  },
162
- o.to
205
+ a.to
163
206
  )
164
207
  ))
165
- ] }, i.label)) })
208
+ ] }, l.label)) })
166
209
  ] });
167
210
  }
168
- function H({
169
- children: a,
170
- headerSlot: n,
171
- logo: t,
172
- nav: l,
173
- pageTitle: r,
174
- title: d
211
+ function te({
212
+ children: e,
213
+ favicon: t = !0,
214
+ headerSlot: o,
215
+ logo: i,
216
+ nav: r,
217
+ pageTitle: p,
218
+ title: u
175
219
  }) {
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(
220
+ var g;
221
+ const { pathname: s } = y(), { lang: c } = E(), l = A(c), a = Q(), b = M(null), S = p ?? ((g = _(r, s)) == null ? void 0 : g.label);
222
+ return z(b, t), /* @__PURE__ */ n(m.Provider, { delay: 0, children: /* @__PURE__ */ d(
179
223
  "div",
180
224
  {
181
225
  className: "ux-app-layout",
182
- "data-state": o.open ? "expanded" : "collapsed",
226
+ "data-state": a.open ? "expanded" : "collapsed",
183
227
  children: [
184
- /* @__PURE__ */ e("aside", { className: "ux-sidebar", children: /* @__PURE__ */ e(
185
- x,
228
+ /* @__PURE__ */ n("aside", { className: "ux-sidebar", children: /* @__PURE__ */ n(
229
+ N,
186
230
  {
187
- copy: i,
188
- logo: t,
189
- nav: l,
190
- state: o.open ? "expanded" : "collapsed",
191
- title: d
231
+ copy: l,
232
+ logo: i,
233
+ logoRef: b,
234
+ nav: r,
235
+ state: a.open ? "expanded" : "collapsed",
236
+ title: u
192
237
  }
193
238
  ) }),
194
- o.isMobile ? /* @__PURE__ */ e(
239
+ a.isMobile ? /* @__PURE__ */ n(
195
240
  h.Root,
196
241
  {
197
- open: o.openMobile,
198
- onOpenChange: o.setOpenMobile,
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
- /* @__PURE__ */ e(
242
+ open: a.openMobile,
243
+ onOpenChange: a.setOpenMobile,
244
+ children: /* @__PURE__ */ d(h.Portal, { children: [
245
+ /* @__PURE__ */ n(h.Backdrop, { className: "ux-drawer-backdrop" }),
246
+ /* @__PURE__ */ d(h.Popup, { className: "ux-drawer", children: [
247
+ /* @__PURE__ */ n(h.Title, { className: "ux-visually-hidden", children: l.navigation }),
248
+ /* @__PURE__ */ n(
204
249
  "button",
205
250
  {
206
- "aria-label": i.closeNavigation,
251
+ "aria-label": l.closeNavigation,
207
252
  className: "ux-drawer__close",
208
253
  type: "button",
209
- onClick: () => o.setOpenMobile(!1),
210
- children: /* @__PURE__ */ e(E, {})
254
+ onClick: () => a.setOpenMobile(!1),
255
+ children: /* @__PURE__ */ n(B, {})
211
256
  }
212
257
  ),
213
- /* @__PURE__ */ e(
214
- x,
258
+ /* @__PURE__ */ n(
259
+ N,
215
260
  {
216
- copy: i,
217
- logo: t,
218
- nav: l,
219
- onNavigate: () => o.setOpenMobile(!1),
261
+ copy: l,
262
+ logo: i,
263
+ nav: r,
264
+ onNavigate: () => a.setOpenMobile(!1),
220
265
  state: "expanded",
221
- title: d
266
+ title: u
222
267
  }
223
268
  )
224
269
  ] })
225
270
  ] })
226
271
  }
227
272
  ) : null,
228
- /* @__PURE__ */ s("div", { className: "ux-shell", children: [
229
- /* @__PURE__ */ s("header", { className: "ux-app-header", children: [
230
- /* @__PURE__ */ e(
273
+ /* @__PURE__ */ d("div", { className: "ux-shell", children: [
274
+ /* @__PURE__ */ d("header", { className: "ux-app-header", children: [
275
+ /* @__PURE__ */ n(
231
276
  "button",
232
277
  {
233
- "aria-expanded": o.isMobile ? o.openMobile : o.open,
234
- "aria-label": i.toggleSidebar,
278
+ "aria-expanded": a.isMobile ? a.openMobile : a.open,
279
+ "aria-label": l.toggleSidebar,
235
280
  className: "ux-app-header__trigger",
236
281
  type: "button",
237
- onClick: o.toggleSidebar,
238
- children: /* @__PURE__ */ e(T, {})
282
+ onClick: a.toggleSidebar,
283
+ children: /* @__PURE__ */ n(F, {})
239
284
  }
240
285
  ),
241
- /* @__PURE__ */ e("span", { "aria-hidden": "true", className: "ux-app-header__separator" }),
242
- /* @__PURE__ */ e("p", { className: "ux-app-header__title", children: _ }),
243
- /* @__PURE__ */ e("div", { className: "ux-app-header__slot", children: n }),
244
- /* @__PURE__ */ e(S, {})
286
+ /* @__PURE__ */ n("span", { "aria-hidden": "true", className: "ux-app-header__separator" }),
287
+ /* @__PURE__ */ n("p", { className: "ux-app-header__title", children: S }),
288
+ /* @__PURE__ */ n("div", { className: "ux-app-header__slot", children: o }),
289
+ /* @__PURE__ */ n(O, {})
245
290
  ] }),
246
- /* @__PURE__ */ e("main", { className: "ux-main", children: a })
291
+ /* @__PURE__ */ n("main", { className: "ux-main", children: e })
247
292
  ] })
248
293
  ]
249
294
  }
250
295
  ) });
251
296
  }
252
297
  export {
253
- H as AppLayout
298
+ te as AppLayout
254
299
  };
package/dist/types.d.ts CHANGED
@@ -9,6 +9,7 @@ export type AppNavGroup = {
9
9
  items: AppNavItem[];
10
10
  };
11
11
  export type AppLayoutProps = {
12
+ favicon?: boolean;
12
13
  logo: ReactNode;
13
14
  title: string;
14
15
  nav: AppNavGroup[];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zccz14/ux",
3
- "version": "0.2.1",
4
- "description": "Unified AppLayout shell for the NTNL app family: one animated icon sidebar, a fixed-size logo box, and the Linkit-integrated AppHeader.",
3
+ "version": "0.3.0",
4
+ "description": "Unified AppLayout shell for the NTNL app family: one animated icon sidebar, a fixed-size logo that doubles as the page favicon, and the Linkit-integrated AppHeader.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",