@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.
- package/README.md +121 -2
- package/dist/index.js +224 -195
- 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
|
|
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
|
|
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
|
|
2
|
-
import { useEffect as
|
|
3
|
-
import { Dialog as
|
|
4
|
-
import { Tooltip as
|
|
5
|
-
import { useLinkit as
|
|
6
|
-
import { useLocation as
|
|
7
|
-
import
|
|
8
|
-
import
|
|
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
|
-
},
|
|
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
|
|
27
|
+
function D(e) {
|
|
30
28
|
if (e instanceof SVGSVGElement)
|
|
31
29
|
return {
|
|
32
|
-
href: `data:image/svg+xml,${encodeURIComponent(
|
|
30
|
+
href: `data:image/svg+xml,${encodeURIComponent(q(e))}`,
|
|
33
31
|
type: "image/svg+xml"
|
|
34
32
|
};
|
|
35
|
-
const { currentSrc: t, src:
|
|
36
|
-
return /\.svg([?#]|$)/i.test(
|
|
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
|
|
39
|
-
const e = document.querySelector(
|
|
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
|
|
46
|
-
|
|
43
|
+
function G(e, t) {
|
|
44
|
+
g(() => {
|
|
47
45
|
if (!t)
|
|
48
46
|
return;
|
|
49
|
-
const
|
|
47
|
+
const r = () => {
|
|
50
48
|
var m;
|
|
51
|
-
const
|
|
52
|
-
if (!
|
|
49
|
+
const o = (m = e.current) == null ? void 0 : m.querySelector("svg, img");
|
|
50
|
+
if (!o)
|
|
53
51
|
return;
|
|
54
|
-
const { href:
|
|
55
|
-
s ?
|
|
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
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
return
|
|
62
|
-
|
|
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__ */
|
|
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
|
|
85
|
-
return /* @__PURE__ */
|
|
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
|
|
103
|
-
return /* @__PURE__ */
|
|
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
|
|
123
|
-
function
|
|
124
|
-
const e = document.cookie.split("; ").find((t) => t.startsWith(`${
|
|
125
|
-
return e ? e.slice(
|
|
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
|
|
128
|
-
document.cookie = `${
|
|
125
|
+
function J(e) {
|
|
126
|
+
document.cookie = `${k}=${e}; path=/; max-age=${Q}`;
|
|
129
127
|
}
|
|
130
|
-
function
|
|
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
|
|
135
|
-
return
|
|
136
|
-
|
|
137
|
-
() => window.matchMedia(
|
|
132
|
+
function Z() {
|
|
133
|
+
return L(
|
|
134
|
+
Y,
|
|
135
|
+
() => window.matchMedia(P).matches,
|
|
138
136
|
() => !1
|
|
139
137
|
);
|
|
140
138
|
}
|
|
141
|
-
function
|
|
142
|
-
const e =
|
|
143
|
-
|
|
144
|
-
}, []),
|
|
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
|
-
|
|
144
|
+
i((s) => !s);
|
|
147
145
|
return;
|
|
148
146
|
}
|
|
149
|
-
|
|
150
|
-
}, [e, t,
|
|
151
|
-
return
|
|
152
|
-
function s(
|
|
153
|
-
(
|
|
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
|
-
}, [
|
|
154
|
+
}, [u]), { isMobile: e, open: t, openMobile: a, setOpenMobile: i, toggleSidebar: u };
|
|
157
155
|
}
|
|
158
|
-
function
|
|
159
|
-
let
|
|
160
|
-
for (const
|
|
161
|
-
for (const
|
|
162
|
-
(
|
|
163
|
-
return
|
|
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
|
|
163
|
+
function te({
|
|
166
164
|
collapsed: e,
|
|
167
165
|
end: t,
|
|
168
|
-
item:
|
|
169
|
-
onNavigate:
|
|
166
|
+
item: r,
|
|
167
|
+
onNavigate: a
|
|
170
168
|
}) {
|
|
171
|
-
return /* @__PURE__ */
|
|
172
|
-
/* @__PURE__ */
|
|
173
|
-
|
|
169
|
+
return /* @__PURE__ */ d(h.Root, { children: [
|
|
170
|
+
/* @__PURE__ */ d(
|
|
171
|
+
h.Trigger,
|
|
174
172
|
{
|
|
175
173
|
render: /* @__PURE__ */ n(
|
|
176
|
-
|
|
174
|
+
$,
|
|
177
175
|
{
|
|
178
176
|
className: "ux-nav-item",
|
|
179
177
|
end: t,
|
|
180
|
-
onClick:
|
|
181
|
-
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:
|
|
186
|
-
/* @__PURE__ */ n("span", { className: "ux-nav-item__text", children:
|
|
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(
|
|
191
|
-
|
|
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(
|
|
194
|
+
children: /* @__PURE__ */ n(h.Popup, { className: "ux-tooltip", hidden: !e, children: r.label })
|
|
197
195
|
}
|
|
198
196
|
) })
|
|
199
197
|
] });
|
|
200
198
|
}
|
|
201
|
-
function
|
|
199
|
+
function M({
|
|
202
200
|
copy: e,
|
|
203
201
|
logo: t,
|
|
204
|
-
logoRef:
|
|
205
|
-
nav:
|
|
206
|
-
onNavigate:
|
|
207
|
-
state:
|
|
208
|
-
title:
|
|
202
|
+
logoRef: r,
|
|
203
|
+
nav: a,
|
|
204
|
+
onNavigate: i,
|
|
205
|
+
state: o,
|
|
206
|
+
title: u
|
|
209
207
|
}) {
|
|
210
|
-
const { pathname: s } =
|
|
211
|
-
return /* @__PURE__ */
|
|
212
|
-
/* @__PURE__ */
|
|
213
|
-
/* @__PURE__ */ n("span", { className: "ux-sidebar__logo", ref:
|
|
214
|
-
/* @__PURE__ */ n("span", { className: "ux-sidebar__title", children:
|
|
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:
|
|
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((
|
|
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
|
-
|
|
221
|
+
te,
|
|
224
222
|
{
|
|
225
|
-
collapsed:
|
|
226
|
-
end:
|
|
227
|
-
item:
|
|
228
|
-
onNavigate:
|
|
223
|
+
collapsed: o === "collapsed",
|
|
224
|
+
end: l !== c || l.to === s,
|
|
225
|
+
item: l,
|
|
226
|
+
onNavigate: i
|
|
229
227
|
},
|
|
230
|
-
|
|
228
|
+
l.to
|
|
231
229
|
)
|
|
232
230
|
))
|
|
233
231
|
] }, m.label)) })
|
|
234
232
|
] });
|
|
235
233
|
}
|
|
236
|
-
function
|
|
234
|
+
function Ce({
|
|
237
235
|
children: e,
|
|
238
236
|
favicon: t = !0,
|
|
239
|
-
headerSlot:
|
|
240
|
-
logo:
|
|
241
|
-
nav:
|
|
242
|
-
pageTitle:
|
|
243
|
-
title:
|
|
237
|
+
headerSlot: r,
|
|
238
|
+
logo: a,
|
|
239
|
+
nav: i,
|
|
240
|
+
pageTitle: o,
|
|
241
|
+
title: u
|
|
244
242
|
}) {
|
|
245
|
-
var
|
|
246
|
-
const { pathname: s } =
|
|
247
|
-
return
|
|
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":
|
|
249
|
+
"data-state": l.open ? "expanded" : "collapsed",
|
|
252
250
|
children: [
|
|
253
251
|
/* @__PURE__ */ n("aside", { className: "ux-sidebar", children: /* @__PURE__ */ n(
|
|
254
|
-
|
|
252
|
+
M,
|
|
255
253
|
{
|
|
256
254
|
copy: m,
|
|
257
|
-
logo:
|
|
258
|
-
logoRef:
|
|
259
|
-
nav:
|
|
260
|
-
state:
|
|
261
|
-
title:
|
|
255
|
+
logo: a,
|
|
256
|
+
logoRef: N,
|
|
257
|
+
nav: i,
|
|
258
|
+
state: l.open ? "expanded" : "collapsed",
|
|
259
|
+
title: u
|
|
262
260
|
}
|
|
263
261
|
) }),
|
|
264
|
-
|
|
265
|
-
|
|
262
|
+
l.isMobile ? /* @__PURE__ */ n(
|
|
263
|
+
f.Root,
|
|
266
264
|
{
|
|
267
|
-
open:
|
|
268
|
-
onOpenChange:
|
|
269
|
-
children: /* @__PURE__ */
|
|
270
|
-
/* @__PURE__ */ n(
|
|
271
|
-
/* @__PURE__ */
|
|
272
|
-
/* @__PURE__ */ n(
|
|
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: () =>
|
|
277
|
+
onClick: () => l.setOpenMobile(!1),
|
|
280
278
|
children: /* @__PURE__ */ n(K, {})
|
|
281
279
|
}
|
|
282
280
|
),
|
|
283
281
|
/* @__PURE__ */ n(
|
|
284
|
-
|
|
282
|
+
M,
|
|
285
283
|
{
|
|
286
284
|
copy: m,
|
|
287
|
-
logo:
|
|
288
|
-
nav:
|
|
289
|
-
onNavigate: () =>
|
|
285
|
+
logo: a,
|
|
286
|
+
nav: i,
|
|
287
|
+
onNavigate: () => l.setOpenMobile(!1),
|
|
290
288
|
state: "expanded",
|
|
291
|
-
title:
|
|
289
|
+
title: u
|
|
292
290
|
}
|
|
293
291
|
)
|
|
294
292
|
] })
|
|
295
293
|
] })
|
|
296
294
|
}
|
|
297
295
|
) : null,
|
|
298
|
-
/* @__PURE__ */
|
|
299
|
-
/* @__PURE__ */
|
|
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":
|
|
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:
|
|
308
|
-
children: /* @__PURE__ */ n(
|
|
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:
|
|
313
|
-
/* @__PURE__ */ n("div", { className: "ux-app-header__slot", children:
|
|
314
|
-
/* @__PURE__ */ n(
|
|
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
|
|
323
|
-
function
|
|
320
|
+
let ne = 0;
|
|
321
|
+
function re() {
|
|
324
322
|
return document.documentElement.classList.contains("dark") ? "dark" : "neutral";
|
|
325
323
|
}
|
|
326
|
-
function
|
|
324
|
+
function ae() {
|
|
327
325
|
const e = getComputedStyle(document.documentElement).fontFamily.trim();
|
|
328
326
|
return e === "" ? "sans-serif" : e;
|
|
329
327
|
}
|
|
330
|
-
function
|
|
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
|
|
332
|
+
async function oe(e, t) {
|
|
335
333
|
try {
|
|
336
|
-
const { default:
|
|
337
|
-
|
|
334
|
+
const { default: r } = await import("mermaid");
|
|
335
|
+
r.initialize({
|
|
338
336
|
startOnLoad: !1,
|
|
339
337
|
securityLevel: "strict",
|
|
340
|
-
theme:
|
|
341
|
-
themeVariables: { fontFamily:
|
|
338
|
+
theme: re(),
|
|
339
|
+
themeVariables: { fontFamily: ae(), fontSize: `${ie(t)}px` }
|
|
342
340
|
});
|
|
343
|
-
const { svg:
|
|
344
|
-
return
|
|
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
|
|
350
|
-
const [t,
|
|
351
|
-
return
|
|
352
|
-
const
|
|
353
|
-
const s = ++
|
|
354
|
-
|
|
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
|
-
|
|
357
|
-
const
|
|
358
|
-
return
|
|
359
|
-
|
|
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: (
|
|
367
|
-
|
|
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: (
|
|
379
|
-
|
|
376
|
+
ref: (o) => {
|
|
377
|
+
a.current = o;
|
|
380
378
|
},
|
|
381
379
|
role: "img"
|
|
382
380
|
}
|
|
383
381
|
);
|
|
384
382
|
}
|
|
385
|
-
|
|
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
|
|
390
|
-
if (!Array.isArray(
|
|
415
|
+
const r = t.properties.className;
|
|
416
|
+
if (!Array.isArray(r) || !r.includes("language-mermaid"))
|
|
391
417
|
return null;
|
|
392
|
-
const
|
|
393
|
-
return (
|
|
418
|
+
const a = t.children[0];
|
|
419
|
+
return (a == null ? void 0 : a.type) === "text" ? a.value : null;
|
|
394
420
|
}
|
|
395
|
-
const
|
|
396
|
-
a: ({ children: e, href: t, node:
|
|
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(
|
|
399
|
-
] }) : /* @__PURE__ */ n("a", { ...
|
|
400
|
-
pre: ({ children: e, node: t, ...
|
|
401
|
-
const
|
|
402
|
-
return
|
|
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, ...
|
|
407
|
-
},
|
|
408
|
-
function
|
|
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
|
|
412
|
-
const t = e.trim(),
|
|
413
|
-
if (
|
|
437
|
+
function be(e) {
|
|
438
|
+
const t = e.trim(), r = t.indexOf(":");
|
|
439
|
+
if (r === -1)
|
|
414
440
|
return t;
|
|
415
|
-
const
|
|
416
|
-
return /^[a-z][a-z0-9+.-]*$/.test(
|
|
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
|
|
419
|
-
function
|
|
420
|
-
|
|
421
|
-
|
|
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:
|
|
424
|
-
rehypePlugins:
|
|
425
|
-
remarkPlugins:
|
|
426
|
-
urlTransform:
|
|
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
|
-
|
|
433
|
-
|
|
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.
|
|
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": {
|