@zccz14/ux 0.4.0 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +121 -2
  2. package/dist/index.js +224 -195
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -117,7 +117,126 @@ One prop, on purpose. There is no `math`, `mermaid`, `components`, `remarkPlugin
117
117
  ### What it renders
118
118
 
119
119
  - **GitHub Flavored Markdown** — tables, task lists, strikethrough, autolinks.
120
- - **Math through KaTeX** — `$E=mc^2$`, `$$…$$`, `\(…\)`, `\[…\]` and `math`-tagged code fences, because models emit all of them. KaTeX's stylesheet and fonts are imported by the component itself, so a formula is never unstyled. Broken math degrades to one `.katex-error` span carrying the parser message; math that is still streaming — no closing delimiter yet — stays source instead of flickering.
120
+ - **Math through KaTeX** — `$E=mc^2# @zccz14/ux
121
+
122
+ `@zccz14/ux` provides the shared app shell and the shared Markdown renderer for the NTNL app family (Cybion, NormAI, CTX, Linkit, Midas, HIT, 1Exchange, DeepSeek-LB, OpenAI-LB). One implementation of the sidebar, the AppHeader and the Linkit account control keeps the interaction identical across apps; one implementation of `<Markdown>` keeps rendered content — replies, checkpoints, manuals, documents, articles — identical too. Both replace the per-app copies they supersede.
123
+
124
+ ## What the shell owns
125
+
126
+ - **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.
127
+ - **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.
128
+ - **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.
129
+ - **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.
130
+ - **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.
131
+ - **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)).
132
+
133
+ ## Installation
134
+
135
+ ```sh
136
+ npm install @zccz14/ux
137
+ ```
138
+
139
+ Peer dependencies: `react` (18 or 19), `react-dom`, `react-router-dom` 7, `@base-ui/react` ≥ 1.6, `linkit-react-components` ≥ 0.6.
140
+
141
+ ## Usage
142
+
143
+ ```tsx
144
+ import { AuthMiniProvider } from "auth-mini-react-components";
145
+ import { LinkitProvider } from "linkit-react-components";
146
+ import { AppLayout } from "@zccz14/ux";
147
+ import { MessagesSquareIcon, ServerIcon } from "lucide-react";
148
+ import { HashRouter, Navigate, Route, Routes } from "react-router-dom";
149
+
150
+ import "@zccz14/ux/styles.css";
151
+ import "linkit-react-components/styles.css";
152
+ import { CybionMark } from "./cybion-mark";
153
+
154
+ export function App() {
155
+ return (
156
+ <AuthMiniProvider
157
+ authMiniBaseUrl="https://auth.ntnl.io"
158
+ audiences={["cybion.ntnl.io", "linkit.ntnl.io"]}
159
+ autoRedirectToLogin
160
+ >
161
+ <LinkitProvider linkitBaseUrl="https://linkit.ntnl.io" lang="zh-CN">
162
+ <HashRouter>
163
+ <AppLayout
164
+ logo={<CybionMark />}
165
+ title="Cybion"
166
+ nav={[
167
+ {
168
+ label: "工作台",
169
+ items: [
170
+ { to: "/threads", label: "对话", icon: <MessagesSquareIcon /> },
171
+ { to: "/system", label: "系统资源", icon: <ServerIcon /> },
172
+ ],
173
+ },
174
+ ]}
175
+ headerSlot={<RefreshButton />}
176
+ >
177
+ <Routes>
178
+ <Route path="/threads" element={<ThreadsPage />} />
179
+ <Route path="*" element={<Navigate replace to="/threads" />} />
180
+ </Routes>
181
+ </AppLayout>
182
+ </HashRouter>
183
+ </LinkitProvider>
184
+ </AuthMiniProvider>
185
+ );
186
+ }
187
+ ```
188
+
189
+ `AppLayout` must render below `AuthMiniProvider → LinkitProvider` (it renders `LinkitMyInfo` and reads `useLinkit()`), and inside a router (navigation items render router links).
190
+
191
+ ## API
192
+
193
+ ### `AppLayout` props
194
+
195
+ | Prop | Type | Required | Description |
196
+ | --- | --- | --- | --- |
197
+ | `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. |
198
+ | `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. |
199
+ | `title` | `string` | yes | App name beside the logo. Hidden while the sidebar is collapsed. |
200
+ | `nav` | `AppNavGroup[]` | yes | Navigation groups rendered in the sidebar. |
201
+ | `headerSlot` | `ReactNode` | no | Rendered immediately left of `LinkitMyInfo` in the AppHeader. Use it for page-level actions such as refresh. |
202
+ | `pageTitle` | `ReactNode` | no | Overrides the page title derived from the active navigation item. Use it for detail routes with their own title. |
203
+ | `children` | `ReactNode` | yes | Page content, rendered in the scrollable main region. |
204
+
205
+ ### Navigation types
206
+
207
+ ```ts
208
+ type AppNavItem = { to: string; label: string; icon: ReactNode };
209
+ type AppNavGroup = { label: string; items: AppNavItem[] };
210
+ ```
211
+
212
+ `to` is a router path. Each item renders a `NavLink`, so active items receive `aria-current="page"` and the standard active styling. Route matching uses longest-prefix wins, and `/` matches only exactly.
213
+
214
+ ## Behavior
215
+
216
+ - **Page title** — the AppHeader shows the active navigation item's `label`, matched from `location.pathname`. `pageTitle` wins when supplied.
217
+ - **Collapse** — the header trigger or `⌘B` / `Ctrl+B` toggles the sidebar; the choice persists in the `sidebar_state` cookie.
218
+ - **Mobile** — below 768px the sidebar becomes a modal navigation drawer: the trigger opens it, tapping a link or the backdrop closes it.
219
+ - **Collapsed tooltips** — icon-only items reveal their label in a tooltip on hover and focus.
220
+ - **Reduced motion** — `prefers-reduced-motion: reduce` disables the animations.
221
+ - **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.
222
+ - **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.
223
+
224
+ ## Markdown
225
+
226
+ `<Markdown content={...} />` is the single renderer for every piece of Markdown in the family.
227
+
228
+ ```tsx
229
+ import { Markdown } from "@zccz14/ux";
230
+
231
+ <Markdown content={reply} />;
232
+ ```
233
+
234
+ One prop, on purpose. There is no `math`, `mermaid`, `components`, `remarkPlugins` or `urlTransform` switch: a surface either renders Markdown the family way, or it renders through something else. Every capability below is always on, because the point of the component is that a reply in Cybion, a checkpoint, a manual preview and a CTX article look the same.
235
+
236
+ ### What it renders
237
+
238
+ - **GitHub Flavored Markdown** — tables, task lists, strikethrough, autolinks.
239
+ , `$…$`, `\(…\)`, `\[…\]` and `math`-tagged code fences, because models emit all of them. The stylesheet and fonts are imported by the component itself, so a formula is never unstyled; the runtime (~76 KB gzipped) is fetched only by a surface whose content can actually contain a formula, and stays cached for the rest of the session. Until that chunk arrives — and always when rendering on a server — a formula shows as the source it was written as, the same state a still-streaming formula is in. Broken math degrades to one `.katex-error` span carrying the parser message.
121
240
  - **Mermaid diagrams** — a `mermaid`-tagged fence becomes an SVG, loaded on demand: a surface without a diagram never downloads mermaid. The chart follows the current theme and the surrounding text size, and re-renders when `.dark` flips. An unparseable diagram falls back to its own source block.
122
241
  - **Links** — absolute URLs open in a new tab with `rel="noopener noreferrer"` and an external marker; in-page anchors and relative paths stay in the app. Only `http(s)` and `mailto` survive: `javascript:`, `data:` and friends degrade to plain text.
123
242
  - **Tables and code** — a wide table or a long code block scrolls inside its own box, so the transcript or article around it never grows a horizontal scrollbar.
@@ -129,7 +248,7 @@ The component wraps its content in `.ux-markdown`, and that block is the family'
129
248
 
130
249
  ### Why the pipeline is a runtime dependency
131
250
 
132
- `react-markdown`, `remark-gfm`, `remark-math-extended`, `rehype-katex`, `katex` and `mermaid` are `dependencies`, not peers, so an app wires nothing. An app that never renders Markdown tree-shakes the whole pipeline away, with one deliberate exception: `katex/dist/katex.min.css` is a module-level side effect of the renderer (about 3.6 KB gzipped, plus fonts the browser fetches only when a formula appears). `scripts/check-package.mjs` fails the build if the manifest ever claims `sideEffects: false`, which would let a bundler drop that stylesheet and render every formula as bare LaTeX with no error to notice.
251
+ `react-markdown`, `remark-gfm`, `remark-math-extended`, `rehype-katex`, `katex` and `mermaid` are `dependencies`, not peers, so an app wires nothing. An app that never renders Markdown tree-shakes the whole chain away, and two pieces are deliberately kept out of the main bundle even for apps that do: Mermaid (loaded by the first diagram, so a surface without one never downloads it) and the math runtime (KaTeX's ~76 KB gzipped JavaScript, loaded by the first surface whose content may contain a formula). What stays static is KaTeX's stylesheet — about 3.6 KB gzipped, with fonts the browser only fetches once a formula is on screen — because a formula must never render unstyled. `scripts/check-package.mjs` fails the build if the manifest ever claims `sideEffects: false`, which would let a bundler drop that stylesheet and render every formula as bare LaTeX with no error to notice.
133
252
 
134
253
  ## Migrating an app from its local Markdown renderer
135
254
 
package/dist/index.js CHANGED
@@ -1,70 +1,68 @@
1
- import { jsxs as u, jsx as n, Fragment as L } from "react/jsx-runtime";
2
- import { useEffect as v, useRef as f, useState as g, useCallback as y, useSyncExternalStore as C } 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 E, LinkitMyInfo as O } from "linkit-react-components";
6
- import { useLocation as w, NavLink as z } from "react-router-dom";
7
- import T from "react-markdown";
8
- import P from "rehype-katex";
9
- import F from "remark-gfm";
10
- import I from "remark-math-extended";
1
+ import { jsxs as d, jsx as n, Fragment as T } from "react/jsx-runtime";
2
+ import { useEffect as g, useRef as v, useState as x, useCallback as _, useSyncExternalStore as L } from "react";
3
+ import { Dialog as f } from "@base-ui/react/dialog";
4
+ import { Tooltip as h } from "@base-ui/react/tooltip";
5
+ import { useLinkit as z, LinkitMyInfo as F } from "linkit-react-components";
6
+ import { useLocation as C, NavLink as $ } from "react-router-dom";
7
+ import I from "react-markdown";
8
+ import A from "remark-gfm";
11
9
  import "katex/dist/katex.min.css";
12
- const $ = {
10
+ const R = {
13
11
  closeNavigation: "Close navigation",
14
12
  navigation: "Navigation",
15
13
  toggleSidebar: "Toggle sidebar"
16
- }, A = {
14
+ }, W = {
17
15
  closeNavigation: "关闭导航",
18
16
  navigation: "导航",
19
17
  toggleSidebar: "切换侧边栏"
20
18
  };
21
- function W(e) {
22
- return e.toLowerCase().startsWith("zh") ? A : $;
23
- }
24
- const R = 'link[rel~="icon"]';
25
19
  function j(e) {
20
+ return e.toLowerCase().startsWith("zh") ? W : R;
21
+ }
22
+ const B = 'link[rel~="icon"]';
23
+ function q(e) {
26
24
  const t = e.cloneNode(!0);
27
25
  return t.setAttribute("xmlns", "http://www.w3.org/2000/svg"), t.style.color = getComputedStyle(e).color, new XMLSerializer().serializeToString(t);
28
26
  }
29
- function B(e) {
27
+ function D(e) {
30
28
  if (e instanceof SVGSVGElement)
31
29
  return {
32
- href: `data:image/svg+xml,${encodeURIComponent(j(e))}`,
30
+ href: `data:image/svg+xml,${encodeURIComponent(q(e))}`,
33
31
  type: "image/svg+xml"
34
32
  };
35
- const { currentSrc: t, src: a } = e, r = t || a;
36
- return /\.svg([?#]|$)/i.test(r) ? { href: r, type: "image/svg+xml" } : { href: r };
33
+ const { currentSrc: t, src: r } = e, a = t || r;
34
+ return /\.svg([?#]|$)/i.test(a) ? { href: a, type: "image/svg+xml" } : { href: a };
37
35
  }
38
- function q() {
39
- const e = document.querySelector(R);
36
+ function V() {
37
+ const e = document.querySelector(B);
40
38
  if (e)
41
39
  return e;
42
40
  const t = document.createElement("link");
43
41
  return t.rel = "icon", document.head.append(t), t;
44
42
  }
45
- function D(e, t) {
46
- v(() => {
43
+ function G(e, t) {
44
+ g(() => {
47
45
  if (!t)
48
46
  return;
49
- const a = () => {
47
+ const r = () => {
50
48
  var m;
51
- const i = (m = e.current) == null ? void 0 : m.querySelector("svg, img");
52
- if (!i)
49
+ const o = (m = e.current) == null ? void 0 : m.querySelector("svg, img");
50
+ if (!o)
53
51
  return;
54
- const { href: d, type: s } = B(i), l = q();
55
- s ? l.type = s : l.removeAttribute("type"), l.getAttribute("href") !== d && l.setAttribute("href", d);
52
+ const { href: u, type: s } = D(o), c = V();
53
+ s ? c.type = s : c.removeAttribute("type"), c.getAttribute("href") !== u && c.setAttribute("href", u);
56
54
  };
57
- a();
58
- const r = new MutationObserver(a);
59
- r.observe(document.documentElement, { attributes: !0, attributeFilter: ["class", "style"] }), e.current && r.observe(e.current, { childList: !0, subtree: !0 });
60
- const o = window.matchMedia("(prefers-color-scheme: dark)");
61
- return o.addEventListener("change", a), () => {
62
- r.disconnect(), o.removeEventListener("change", a);
55
+ r();
56
+ const a = new MutationObserver(r);
57
+ a.observe(document.documentElement, { attributes: !0, attributeFilter: ["class", "style"] }), e.current && a.observe(e.current, { childList: !0, subtree: !0 });
58
+ const i = window.matchMedia("(prefers-color-scheme: dark)");
59
+ return i.addEventListener("change", r), () => {
60
+ a.disconnect(), i.removeEventListener("change", r);
63
61
  };
64
62
  }, [t, e]);
65
63
  }
66
64
  function K() {
67
- return /* @__PURE__ */ u(
65
+ return /* @__PURE__ */ d(
68
66
  "svg",
69
67
  {
70
68
  "aria-hidden": "true",
@@ -81,8 +79,8 @@ function K() {
81
79
  }
82
80
  );
83
81
  }
84
- function V() {
85
- return /* @__PURE__ */ u(
82
+ function H() {
83
+ return /* @__PURE__ */ d(
86
84
  "svg",
87
85
  {
88
86
  "aria-hidden": "true",
@@ -99,8 +97,8 @@ function V() {
99
97
  }
100
98
  );
101
99
  }
102
- function G() {
103
- return /* @__PURE__ */ u(
100
+ function U() {
101
+ return /* @__PURE__ */ d(
104
102
  "svg",
105
103
  {
106
104
  "aria-hidden": "true",
@@ -119,199 +117,199 @@ function G() {
119
117
  }
120
118
  );
121
119
  }
122
- const b = "sidebar_state", H = 3600 * 24 * 7, _ = "(max-width: 767px)";
123
- function U() {
124
- const e = document.cookie.split("; ").find((t) => t.startsWith(`${b}=`));
125
- return e ? e.slice(b.length + 1) !== "false" : !0;
120
+ const k = "sidebar_state", Q = 3600 * 24 * 7, P = "(max-width: 767px)";
121
+ function X() {
122
+ const e = document.cookie.split("; ").find((t) => t.startsWith(`${k}=`));
123
+ return e ? e.slice(k.length + 1) !== "false" : !0;
126
124
  }
127
- function Q(e) {
128
- document.cookie = `${b}=${e}; path=/; max-age=${H}`;
125
+ function J(e) {
126
+ document.cookie = `${k}=${e}; path=/; max-age=${Q}`;
129
127
  }
130
- function X(e) {
131
- const t = window.matchMedia(_);
128
+ function Y(e) {
129
+ const t = window.matchMedia(P);
132
130
  return t.addEventListener("change", e), () => t.removeEventListener("change", e);
133
131
  }
134
- function J() {
135
- return C(
136
- X,
137
- () => window.matchMedia(_).matches,
132
+ function Z() {
133
+ return L(
134
+ Y,
135
+ () => window.matchMedia(P).matches,
138
136
  () => !1
139
137
  );
140
138
  }
141
- function Y() {
142
- const e = J(), [t, a] = g(U), [r, o] = g(!1), i = y((s) => {
143
- a(s), Q(s);
144
- }, []), d = y(() => {
139
+ function ee() {
140
+ const e = Z(), [t, r] = x(X), [a, i] = x(!1), o = _((s) => {
141
+ r(s), J(s);
142
+ }, []), u = _(() => {
145
143
  if (e) {
146
- o((s) => !s);
144
+ i((s) => !s);
147
145
  return;
148
146
  }
149
- i(!t);
150
- }, [e, t, i]);
151
- return v(() => {
152
- function s(l) {
153
- (l.metaKey || l.ctrlKey) && l.key === "b" && (l.preventDefault(), d());
147
+ o(!t);
148
+ }, [e, t, o]);
149
+ return g(() => {
150
+ function s(c) {
151
+ (c.metaKey || c.ctrlKey) && c.key === "b" && (c.preventDefault(), u());
154
152
  }
155
153
  return window.addEventListener("keydown", s), () => window.removeEventListener("keydown", s);
156
- }, [d]), { isMobile: e, open: t, openMobile: r, setOpenMobile: o, toggleSidebar: d };
154
+ }, [u]), { isMobile: e, open: t, openMobile: a, setOpenMobile: i, toggleSidebar: u };
157
155
  }
158
- function M(e, t) {
159
- let a;
160
- for (const r of e)
161
- for (const o of r.items)
162
- (o.to === "/" ? t === "/" : t === o.to || t.startsWith(`${o.to}/`)) && (!a || o.to.length > a.to.length) && (a = o);
163
- return a;
156
+ function E(e, t) {
157
+ let r;
158
+ for (const a of e)
159
+ for (const i of a.items)
160
+ (i.to === "/" ? t === "/" : t === i.to || t.startsWith(`${i.to}/`)) && (!r || i.to.length > r.to.length) && (r = i);
161
+ return r;
164
162
  }
165
- function Z({
163
+ function te({
166
164
  collapsed: e,
167
165
  end: t,
168
- item: a,
169
- onNavigate: r
166
+ item: r,
167
+ onNavigate: a
170
168
  }) {
171
- return /* @__PURE__ */ u(p.Root, { children: [
172
- /* @__PURE__ */ u(
173
- p.Trigger,
169
+ return /* @__PURE__ */ d(h.Root, { children: [
170
+ /* @__PURE__ */ d(
171
+ h.Trigger,
174
172
  {
175
173
  render: /* @__PURE__ */ n(
176
- z,
174
+ $,
177
175
  {
178
176
  className: "ux-nav-item",
179
177
  end: t,
180
- onClick: r,
181
- to: a.to
178
+ onClick: a,
179
+ to: r.to
182
180
  }
183
181
  ),
184
182
  children: [
185
- /* @__PURE__ */ n("span", { className: "ux-nav-item__icon", children: a.icon }),
186
- /* @__PURE__ */ n("span", { className: "ux-nav-item__text", children: a.label })
183
+ /* @__PURE__ */ n("span", { className: "ux-nav-item__icon", children: r.icon }),
184
+ /* @__PURE__ */ n("span", { className: "ux-nav-item__text", children: r.label })
187
185
  ]
188
186
  }
189
187
  ),
190
- /* @__PURE__ */ n(p.Portal, { children: /* @__PURE__ */ n(
191
- p.Positioner,
188
+ /* @__PURE__ */ n(h.Portal, { children: /* @__PURE__ */ n(
189
+ h.Positioner,
192
190
  {
193
191
  className: "ux-tooltip-positioner",
194
192
  side: "right",
195
193
  sideOffset: 6,
196
- children: /* @__PURE__ */ n(p.Popup, { className: "ux-tooltip", hidden: !e, children: a.label })
194
+ children: /* @__PURE__ */ n(h.Popup, { className: "ux-tooltip", hidden: !e, children: r.label })
197
195
  }
198
196
  ) })
199
197
  ] });
200
198
  }
201
- function N({
199
+ function M({
202
200
  copy: e,
203
201
  logo: t,
204
- logoRef: a,
205
- nav: r,
206
- onNavigate: o,
207
- state: i,
208
- title: d
202
+ logoRef: r,
203
+ nav: a,
204
+ onNavigate: i,
205
+ state: o,
206
+ title: u
209
207
  }) {
210
- const { pathname: s } = w(), l = M(r, s);
211
- return /* @__PURE__ */ u(L, { children: [
212
- /* @__PURE__ */ u("div", { className: "ux-sidebar__header", children: [
213
- /* @__PURE__ */ n("span", { className: "ux-sidebar__logo", ref: a, children: t }),
214
- /* @__PURE__ */ n("span", { className: "ux-sidebar__title", children: d })
208
+ const { pathname: s } = C(), c = E(a, s);
209
+ return /* @__PURE__ */ d(T, { children: [
210
+ /* @__PURE__ */ d("div", { className: "ux-sidebar__header", children: [
211
+ /* @__PURE__ */ n("span", { className: "ux-sidebar__logo", ref: r, children: t }),
212
+ /* @__PURE__ */ n("span", { className: "ux-sidebar__title", children: u })
215
213
  ] }),
216
- /* @__PURE__ */ n("nav", { "aria-label": e.navigation, className: "ux-sidebar__nav", children: r.map((m) => /* @__PURE__ */ u("div", { className: "ux-nav-group", children: [
214
+ /* @__PURE__ */ n("nav", { "aria-label": e.navigation, className: "ux-sidebar__nav", children: a.map((m) => /* @__PURE__ */ d("div", { className: "ux-nav-group", children: [
217
215
  /* @__PURE__ */ n("div", { className: "ux-nav-group__label", children: m.label }),
218
- m.items.map((c) => (
216
+ m.items.map((l) => (
219
217
  // NavLink would mark a parent section current on its children's
220
218
  // routes; follow matchActiveItem instead so only the deepest
221
219
  // match (or the exact path) stays marked.
222
220
  /* @__PURE__ */ n(
223
- Z,
221
+ te,
224
222
  {
225
- collapsed: i === "collapsed",
226
- end: c !== l || c.to === s,
227
- item: c,
228
- onNavigate: o
223
+ collapsed: o === "collapsed",
224
+ end: l !== c || l.to === s,
225
+ item: l,
226
+ onNavigate: i
229
227
  },
230
- c.to
228
+ l.to
231
229
  )
232
230
  ))
233
231
  ] }, m.label)) })
234
232
  ] });
235
233
  }
236
- function _e({
234
+ function Ce({
237
235
  children: e,
238
236
  favicon: t = !0,
239
- headerSlot: a,
240
- logo: r,
241
- nav: o,
242
- pageTitle: i,
243
- title: d
237
+ headerSlot: r,
238
+ logo: a,
239
+ nav: i,
240
+ pageTitle: o,
241
+ title: u
244
242
  }) {
245
- var k;
246
- const { pathname: s } = w(), { lang: l } = E(), m = W(l), c = Y(), x = f(null), S = i ?? ((k = M(o, s)) == null ? void 0 : k.label);
247
- return D(x, t), /* @__PURE__ */ n(p.Provider, { delay: 0, children: /* @__PURE__ */ u(
243
+ var w;
244
+ const { pathname: s } = C(), { lang: c } = z(), m = j(c), l = ee(), N = v(null), O = o ?? ((w = E(i, s)) == null ? void 0 : w.label);
245
+ return G(N, t), /* @__PURE__ */ n(h.Provider, { delay: 0, children: /* @__PURE__ */ d(
248
246
  "div",
249
247
  {
250
248
  className: "ux-app-layout",
251
- "data-state": c.open ? "expanded" : "collapsed",
249
+ "data-state": l.open ? "expanded" : "collapsed",
252
250
  children: [
253
251
  /* @__PURE__ */ n("aside", { className: "ux-sidebar", children: /* @__PURE__ */ n(
254
- N,
252
+ M,
255
253
  {
256
254
  copy: m,
257
- logo: r,
258
- logoRef: x,
259
- nav: o,
260
- state: c.open ? "expanded" : "collapsed",
261
- title: d
255
+ logo: a,
256
+ logoRef: N,
257
+ nav: i,
258
+ state: l.open ? "expanded" : "collapsed",
259
+ title: u
262
260
  }
263
261
  ) }),
264
- c.isMobile ? /* @__PURE__ */ n(
265
- h.Root,
262
+ l.isMobile ? /* @__PURE__ */ n(
263
+ f.Root,
266
264
  {
267
- open: c.openMobile,
268
- onOpenChange: c.setOpenMobile,
269
- children: /* @__PURE__ */ u(h.Portal, { children: [
270
- /* @__PURE__ */ n(h.Backdrop, { className: "ux-drawer-backdrop" }),
271
- /* @__PURE__ */ u(h.Popup, { className: "ux-drawer", children: [
272
- /* @__PURE__ */ n(h.Title, { className: "ux-visually-hidden", children: m.navigation }),
265
+ open: l.openMobile,
266
+ onOpenChange: l.setOpenMobile,
267
+ children: /* @__PURE__ */ d(f.Portal, { children: [
268
+ /* @__PURE__ */ n(f.Backdrop, { className: "ux-drawer-backdrop" }),
269
+ /* @__PURE__ */ d(f.Popup, { className: "ux-drawer", children: [
270
+ /* @__PURE__ */ n(f.Title, { className: "ux-visually-hidden", children: m.navigation }),
273
271
  /* @__PURE__ */ n(
274
272
  "button",
275
273
  {
276
274
  "aria-label": m.closeNavigation,
277
275
  className: "ux-drawer__close",
278
276
  type: "button",
279
- onClick: () => c.setOpenMobile(!1),
277
+ onClick: () => l.setOpenMobile(!1),
280
278
  children: /* @__PURE__ */ n(K, {})
281
279
  }
282
280
  ),
283
281
  /* @__PURE__ */ n(
284
- N,
282
+ M,
285
283
  {
286
284
  copy: m,
287
- logo: r,
288
- nav: o,
289
- onNavigate: () => c.setOpenMobile(!1),
285
+ logo: a,
286
+ nav: i,
287
+ onNavigate: () => l.setOpenMobile(!1),
290
288
  state: "expanded",
291
- title: d
289
+ title: u
292
290
  }
293
291
  )
294
292
  ] })
295
293
  ] })
296
294
  }
297
295
  ) : null,
298
- /* @__PURE__ */ u("div", { className: "ux-shell", children: [
299
- /* @__PURE__ */ u("header", { className: "ux-app-header", children: [
296
+ /* @__PURE__ */ d("div", { className: "ux-shell", children: [
297
+ /* @__PURE__ */ d("header", { className: "ux-app-header", children: [
300
298
  /* @__PURE__ */ n(
301
299
  "button",
302
300
  {
303
- "aria-expanded": c.isMobile ? c.openMobile : c.open,
301
+ "aria-expanded": l.isMobile ? l.openMobile : l.open,
304
302
  "aria-label": m.toggleSidebar,
305
303
  className: "ux-app-header__trigger",
306
304
  type: "button",
307
- onClick: c.toggleSidebar,
308
- children: /* @__PURE__ */ n(V, {})
305
+ onClick: l.toggleSidebar,
306
+ children: /* @__PURE__ */ n(H, {})
309
307
  }
310
308
  ),
311
309
  /* @__PURE__ */ n("span", { "aria-hidden": "true", className: "ux-app-header__separator" }),
312
- /* @__PURE__ */ n("p", { className: "ux-app-header__title", children: S }),
313
- /* @__PURE__ */ n("div", { className: "ux-app-header__slot", children: a }),
314
- /* @__PURE__ */ n(O, {})
310
+ /* @__PURE__ */ n("p", { className: "ux-app-header__title", children: O }),
311
+ /* @__PURE__ */ n("div", { className: "ux-app-header__slot", children: r }),
312
+ /* @__PURE__ */ n(F, {})
315
313
  ] }),
316
314
  /* @__PURE__ */ n("main", { className: "ux-main", children: e })
317
315
  ] })
@@ -319,52 +317,52 @@ function _e({
319
317
  }
320
318
  ) });
321
319
  }
322
- let ee = 0;
323
- function te() {
320
+ let ne = 0;
321
+ function re() {
324
322
  return document.documentElement.classList.contains("dark") ? "dark" : "neutral";
325
323
  }
326
- function ne() {
324
+ function ae() {
327
325
  const e = getComputedStyle(document.documentElement).fontFamily.trim();
328
326
  return e === "" ? "sans-serif" : e;
329
327
  }
330
- function re(e) {
328
+ function ie(e) {
331
329
  const t = e ? Number.parseFloat(getComputedStyle(e).fontSize) : Number.NaN;
332
330
  return !Number.isFinite(t) || t <= 0 ? 16 : Math.min(Math.max(t, 10), 20);
333
331
  }
334
- async function ae(e, t) {
332
+ async function oe(e, t) {
335
333
  try {
336
- const { default: a } = await import("mermaid");
337
- a.initialize({
334
+ const { default: r } = await import("mermaid");
335
+ r.initialize({
338
336
  startOnLoad: !1,
339
337
  securityLevel: "strict",
340
- theme: te(),
341
- themeVariables: { fontFamily: ne(), fontSize: `${re(t)}px` }
338
+ theme: re(),
339
+ themeVariables: { fontFamily: ae(), fontSize: `${ie(t)}px` }
342
340
  });
343
- const { svg: r } = await a.render(`ux-mermaid-${++ee}`, e);
344
- return r;
341
+ const { svg: a } = await r.render(`ux-mermaid-${++ne}`, e);
342
+ return a;
345
343
  } catch {
346
344
  return null;
347
345
  }
348
346
  }
349
- function oe({ source: e }) {
350
- const [t, a] = g(null), r = f(null), o = f(0);
351
- return v(() => {
352
- const i = async () => {
353
- const s = ++o.current, l = await ae(e, r.current);
354
- o.current === s && a(l);
347
+ function se({ source: e }) {
348
+ const [t, r] = x(null), a = v(null), i = v(0);
349
+ return g(() => {
350
+ const o = async () => {
351
+ const s = ++i.current, c = await oe(e, a.current);
352
+ i.current === s && r(c);
355
353
  };
356
- i();
357
- const d = new MutationObserver(() => void i());
358
- return d.observe(document.documentElement, { attributes: !0, attributeFilter: ["class"] }), () => {
359
- o.current += 1, d.disconnect();
354
+ o();
355
+ const u = new MutationObserver(() => void o());
356
+ return u.observe(document.documentElement, { attributes: !0, attributeFilter: ["class"] }), () => {
357
+ i.current += 1, u.disconnect();
360
358
  };
361
359
  }, [e]), t === null ? /* @__PURE__ */ n(
362
360
  "pre",
363
361
  {
364
362
  className: "ux-markdown__mermaid-source",
365
363
  "data-mermaid": "source",
366
- ref: (i) => {
367
- r.current = i;
364
+ ref: (o) => {
365
+ a.current = o;
368
366
  },
369
367
  children: /* @__PURE__ */ n("code", { className: "language-mermaid", children: e })
370
368
  }
@@ -375,60 +373,91 @@ function oe({ source: e }) {
375
373
  className: "ux-markdown__mermaid",
376
374
  "data-mermaid": "diagram",
377
375
  dangerouslySetInnerHTML: { __html: t },
378
- ref: (i) => {
379
- r.current = i;
376
+ ref: (o) => {
377
+ a.current = o;
380
378
  },
381
379
  role: "img"
382
380
  }
383
381
  );
384
382
  }
385
- function ie(e) {
383
+ let p = null, b = null;
384
+ const y = /* @__PURE__ */ new Set();
385
+ function le(e) {
386
+ return y.add(e), () => {
387
+ y.delete(e);
388
+ };
389
+ }
390
+ function ce() {
391
+ return p !== null;
392
+ }
393
+ const ue = /^ {0,3}(?:```|~~~)\s*math\b/m;
394
+ function de(e) {
395
+ return e.includes("$") || e.includes("\\(") || e.includes("\\[") || ue.test(e);
396
+ }
397
+ function me() {
398
+ return b ?? (b = Promise.all([import("remark-math-extended"), import("rehype-katex")]).then(([e, t]) => {
399
+ p = {
400
+ remarkPlugins: [e.default],
401
+ rehypePlugins: [
402
+ [t.default, { strict: "ignore", errorColor: "var(--ux-destructive)" }]
403
+ ]
404
+ };
405
+ for (const r of y)
406
+ r();
407
+ }).catch(() => {
408
+ b = null;
409
+ })), b;
410
+ }
411
+ function he(e) {
386
412
  const t = e == null ? void 0 : e.children[0];
387
413
  if ((t == null ? void 0 : t.type) !== "element" || t.tagName !== "code")
388
414
  return null;
389
- const a = t.properties.className;
390
- if (!Array.isArray(a) || !a.includes("language-mermaid"))
415
+ const r = t.properties.className;
416
+ if (!Array.isArray(r) || !r.includes("language-mermaid"))
391
417
  return null;
392
- const r = t.children[0];
393
- return (r == null ? void 0 : r.type) === "text" ? r.value : null;
418
+ const a = t.children[0];
419
+ return (a == null ? void 0 : a.type) === "text" ? a.value : null;
394
420
  }
395
- const se = {
396
- a: ({ children: e, href: t, node: a, ...r }) => t ? le(t) ? /* @__PURE__ */ u("a", { ...r, href: t, rel: "noopener noreferrer", target: "_blank", children: [
421
+ const pe = {
422
+ a: ({ children: e, href: t, node: r, ...a }) => t ? ge(t) ? /* @__PURE__ */ d("a", { ...a, href: t, rel: "noopener noreferrer", target: "_blank", children: [
397
423
  e,
398
- /* @__PURE__ */ n(G, {})
399
- ] }) : /* @__PURE__ */ n("a", { ...r, href: t, children: e }) : /* @__PURE__ */ n("span", { ...r, children: e }),
400
- pre: ({ children: e, node: t, ...a }) => {
401
- const r = ie(t);
402
- return r === null ? /* @__PURE__ */ n("pre", { ...a, children: e }) : /* @__PURE__ */ n(oe, { source: r });
424
+ /* @__PURE__ */ n(U, {})
425
+ ] }) : /* @__PURE__ */ n("a", { ...a, href: t, children: e }) : /* @__PURE__ */ n("span", { ...a, children: e }),
426
+ pre: ({ children: e, node: t, ...r }) => {
427
+ const a = he(t);
428
+ return a === null ? /* @__PURE__ */ n("pre", { ...r, children: e }) : /* @__PURE__ */ n(se, { source: a });
403
429
  },
404
430
  // A wide table scrolls on its own; the transcript or article around it never grows a horizontal
405
431
  // scrollbar, so the columns past the fold stay reachable.
406
- table: ({ children: e, node: t, ...a }) => /* @__PURE__ */ n("div", { className: "ux-markdown__table", children: /* @__PURE__ */ n("table", { ...a, children: e }) })
407
- }, ce = /* @__PURE__ */ new Set(["http", "https", "mailto"]);
408
- function le(e) {
432
+ table: ({ children: e, node: t, ...r }) => /* @__PURE__ */ n("div", { className: "ux-markdown__table", children: /* @__PURE__ */ n("table", { ...r, children: e }) })
433
+ }, fe = /* @__PURE__ */ new Set(["http", "https", "mailto"]);
434
+ function ge(e) {
409
435
  return /^[a-z][a-z0-9+.-]*:/i.test(e) || e.startsWith("//");
410
436
  }
411
- function de(e) {
412
- const t = e.trim(), a = t.indexOf(":");
413
- if (a === -1)
437
+ function be(e) {
438
+ const t = e.trim(), r = t.indexOf(":");
439
+ if (r === -1)
414
440
  return t;
415
- const r = t.slice(0, a).toLowerCase();
416
- return /^[a-z][a-z0-9+.-]*$/.test(r) ? ce.has(r) ? t : "" : t;
441
+ const a = t.slice(0, r).toLowerCase();
442
+ return /^[a-z][a-z0-9+.-]*$/.test(a) ? fe.has(a) ? t : "" : t;
417
443
  }
418
- const ue = [F, I], me = [[P, { strict: "ignore", errorColor: "var(--ux-destructive)" }]];
419
- function Me({ content: e }) {
420
- return /* @__PURE__ */ n("div", { className: "ux-markdown", children: /* @__PURE__ */ n(
421
- T,
444
+ const S = [A], ve = [];
445
+ function Pe({ content: e }) {
446
+ const t = L(le, ce, () => !1);
447
+ return g(() => {
448
+ !t && de(e) && me();
449
+ }, [e, t]), /* @__PURE__ */ n("div", { className: "ux-markdown", children: /* @__PURE__ */ n(
450
+ I,
422
451
  {
423
- components: se,
424
- rehypePlugins: me,
425
- remarkPlugins: ue,
426
- urlTransform: de,
452
+ components: pe,
453
+ rehypePlugins: t && p ? p.rehypePlugins : ve,
454
+ remarkPlugins: t && p ? [...S, ...p.remarkPlugins] : S,
455
+ urlTransform: be,
427
456
  children: e
428
457
  }
429
458
  ) });
430
459
  }
431
460
  export {
432
- _e as AppLayout,
433
- Me as Markdown
461
+ Ce as AppLayout,
462
+ Pe as Markdown
434
463
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zccz14/ux",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Unified app shell and Markdown renderer for the NTNL app family: one animated icon sidebar, a fixed-size logo that doubles as the page favicon, the Linkit-integrated AppHeader, and the single Markdown surface every app renders through.",
5
5
  "license": "MIT",
6
6
  "repository": {