@zccz14/ux 0.2.1 → 0.4.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 +48 -8
- package/dist/app-layout.d.ts +1 -1
- package/dist/favicon.d.ts +6 -0
- package/dist/icons.d.ts +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +314 -134
- package/dist/markdown.d.ts +3 -0
- package/dist/mermaid.d.ts +3 -0
- package/dist/styles.css +241 -0
- package/dist/types.d.ts +4 -0
- package/package.json +12 -3
package/README.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# @zccz14/ux
|
|
2
2
|
|
|
3
|
-
`@zccz14/ux` provides the shared
|
|
3
|
+
`@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.
|
|
4
4
|
|
|
5
5
|
## What the shell owns
|
|
6
6
|
|
|
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,11 +99,48 @@ 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
|
|
|
105
|
+
## Markdown
|
|
106
|
+
|
|
107
|
+
`<Markdown content={...} />` is the single renderer for every piece of Markdown in the family.
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
import { Markdown } from "@zccz14/ux";
|
|
111
|
+
|
|
112
|
+
<Markdown content={reply} />;
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
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.
|
|
116
|
+
|
|
117
|
+
### What it renders
|
|
118
|
+
|
|
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.
|
|
121
|
+
- **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
|
+
- **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
|
+
- **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.
|
|
124
|
+
- **Raw HTML** — never rendered. The pipeline has no `rehype-raw`, so HTML in the source is shown as text.
|
|
125
|
+
|
|
126
|
+
### Typography is owned here
|
|
127
|
+
|
|
128
|
+
The component wraps its content in `.ux-markdown`, and that block is the family's typographic contract: every size is expressed in `em` off the inherited font size, and no measure is imposed. The same content therefore keeps identical proportions in a 14px chat bubble and a 16px article page. The host decides two things and only two — the font size of the container the content is dropped into, and the width of that container. Do not wrap the component in `prose` classes: the stylesheet is the single source of the look, and `scripts/check-styles.mjs` enforces it.
|
|
129
|
+
|
|
130
|
+
### Why the pipeline is a runtime dependency
|
|
131
|
+
|
|
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.
|
|
133
|
+
|
|
134
|
+
## Migrating an app from its local Markdown renderer
|
|
135
|
+
|
|
136
|
+
1. Delete the app's own renderer — the `react-markdown` wrapper, its plugin wiring, its `components` map, its `Mermaid`/`MarkdownPre` pair, and any `mermaid`/`rehype-katex`/`remark-*`/`katex` dependency entries that only served it.
|
|
137
|
+
2. Replace each call site with `<Markdown content={markdown} />` and drop the surrounding `prose …` classes: the typography lives in `.ux-markdown`, and the container only owes the content a font size and a width.
|
|
138
|
+
3. Keep app-specific node mapping out of the way — if a surface really needs different output (inline mention tokens, a clamped preview), it belongs in the library, not in a local fork of the plugin chain.
|
|
139
|
+
4. Keep the KaTeX stylesheet import out of the app: the component owns it.
|
|
140
|
+
|
|
102
141
|
## Styling
|
|
103
142
|
|
|
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`.
|
|
143
|
+
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`. The Markdown layer reads `--muted`, `--muted-foreground`, `--primary` and `--destructive` through the same `--ux-*` layer (`--ux-muted`, `--ux-muted-foreground`, `--ux-primary`, `--ux-destructive`).
|
|
105
144
|
|
|
106
145
|
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
146
|
|
|
@@ -113,13 +152,14 @@ Theme state itself is not the shell's business: whatever toggles `.dark` on `<ht
|
|
|
113
152
|
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
153
|
3. Move any sidebar footer item (settings, environment status) into the last navigation group.
|
|
115
154
|
4. Pass the single logo mark and delete per-call sizes; the shell normalizes every mark to its 1.75rem box.
|
|
155
|
+
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
156
|
|
|
117
157
|
## Development
|
|
118
158
|
|
|
119
159
|
```sh
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
160
|
+
pnpm install
|
|
161
|
+
pnpm run typecheck
|
|
162
|
+
pnpm test
|
|
163
|
+
pnpm run build
|
|
164
|
+
pnpm run pack:check
|
|
125
165
|
```
|
package/dist/app-layout.d.ts
CHANGED
|
@@ -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/icons.d.ts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,23 +1,70 @@
|
|
|
1
|
-
import { jsxs as
|
|
2
|
-
import {
|
|
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
3
|
import { Dialog as h } from "@base-ui/react/dialog";
|
|
4
4
|
import { Tooltip as p } from "@base-ui/react/tooltip";
|
|
5
|
-
import { useLinkit as
|
|
6
|
-
import { useLocation as
|
|
7
|
-
|
|
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";
|
|
11
|
+
import "katex/dist/katex.min.css";
|
|
12
|
+
const $ = {
|
|
8
13
|
closeNavigation: "Close navigation",
|
|
9
14
|
navigation: "Navigation",
|
|
10
15
|
toggleSidebar: "Toggle sidebar"
|
|
11
|
-
},
|
|
16
|
+
}, A = {
|
|
12
17
|
closeNavigation: "关闭导航",
|
|
13
18
|
navigation: "导航",
|
|
14
19
|
toggleSidebar: "切换侧边栏"
|
|
15
20
|
};
|
|
16
|
-
function
|
|
17
|
-
return
|
|
21
|
+
function W(e) {
|
|
22
|
+
return e.toLowerCase().startsWith("zh") ? A : $;
|
|
18
23
|
}
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
const R = 'link[rel~="icon"]';
|
|
25
|
+
function j(e) {
|
|
26
|
+
const t = e.cloneNode(!0);
|
|
27
|
+
return t.setAttribute("xmlns", "http://www.w3.org/2000/svg"), t.style.color = getComputedStyle(e).color, new XMLSerializer().serializeToString(t);
|
|
28
|
+
}
|
|
29
|
+
function B(e) {
|
|
30
|
+
if (e instanceof SVGSVGElement)
|
|
31
|
+
return {
|
|
32
|
+
href: `data:image/svg+xml,${encodeURIComponent(j(e))}`,
|
|
33
|
+
type: "image/svg+xml"
|
|
34
|
+
};
|
|
35
|
+
const { currentSrc: t, src: a } = e, r = t || a;
|
|
36
|
+
return /\.svg([?#]|$)/i.test(r) ? { href: r, type: "image/svg+xml" } : { href: r };
|
|
37
|
+
}
|
|
38
|
+
function q() {
|
|
39
|
+
const e = document.querySelector(R);
|
|
40
|
+
if (e)
|
|
41
|
+
return e;
|
|
42
|
+
const t = document.createElement("link");
|
|
43
|
+
return t.rel = "icon", document.head.append(t), t;
|
|
44
|
+
}
|
|
45
|
+
function D(e, t) {
|
|
46
|
+
v(() => {
|
|
47
|
+
if (!t)
|
|
48
|
+
return;
|
|
49
|
+
const a = () => {
|
|
50
|
+
var m;
|
|
51
|
+
const i = (m = e.current) == null ? void 0 : m.querySelector("svg, img");
|
|
52
|
+
if (!i)
|
|
53
|
+
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);
|
|
56
|
+
};
|
|
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);
|
|
63
|
+
};
|
|
64
|
+
}, [t, e]);
|
|
65
|
+
}
|
|
66
|
+
function K() {
|
|
67
|
+
return /* @__PURE__ */ u(
|
|
21
68
|
"svg",
|
|
22
69
|
{
|
|
23
70
|
"aria-hidden": "true",
|
|
@@ -28,14 +75,14 @@ function E() {
|
|
|
28
75
|
strokeWidth: "2",
|
|
29
76
|
viewBox: "0 0 24 24",
|
|
30
77
|
children: [
|
|
31
|
-
/* @__PURE__ */
|
|
32
|
-
/* @__PURE__ */
|
|
78
|
+
/* @__PURE__ */ n("path", { d: "M18 6 6 18" }),
|
|
79
|
+
/* @__PURE__ */ n("path", { d: "m6 6 12 12" })
|
|
33
80
|
]
|
|
34
81
|
}
|
|
35
82
|
);
|
|
36
83
|
}
|
|
37
|
-
function
|
|
38
|
-
return /* @__PURE__ */
|
|
84
|
+
function V() {
|
|
85
|
+
return /* @__PURE__ */ u(
|
|
39
86
|
"svg",
|
|
40
87
|
{
|
|
41
88
|
"aria-hidden": "true",
|
|
@@ -46,177 +93,200 @@ function T() {
|
|
|
46
93
|
strokeWidth: "2",
|
|
47
94
|
viewBox: "0 0 24 24",
|
|
48
95
|
children: [
|
|
49
|
-
/* @__PURE__ */
|
|
50
|
-
/* @__PURE__ */
|
|
96
|
+
/* @__PURE__ */ n("rect", { height: "18", rx: "2", width: "18", x: "3", y: "3" }),
|
|
97
|
+
/* @__PURE__ */ n("path", { d: "M9 3v18" })
|
|
51
98
|
]
|
|
52
99
|
}
|
|
53
100
|
);
|
|
54
101
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
102
|
+
function G() {
|
|
103
|
+
return /* @__PURE__ */ u(
|
|
104
|
+
"svg",
|
|
105
|
+
{
|
|
106
|
+
"aria-hidden": "true",
|
|
107
|
+
className: "ux-markdown__external-link",
|
|
108
|
+
fill: "none",
|
|
109
|
+
stroke: "currentColor",
|
|
110
|
+
strokeLinecap: "round",
|
|
111
|
+
strokeLinejoin: "round",
|
|
112
|
+
strokeWidth: "2",
|
|
113
|
+
viewBox: "0 0 24 24",
|
|
114
|
+
children: [
|
|
115
|
+
/* @__PURE__ */ n("path", { d: "M15 3h6v6" }),
|
|
116
|
+
/* @__PURE__ */ n("path", { d: "M10 14 21 3" }),
|
|
117
|
+
/* @__PURE__ */ n("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" })
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
}
|
|
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;
|
|
59
126
|
}
|
|
60
|
-
function
|
|
61
|
-
document.cookie = `${
|
|
127
|
+
function Q(e) {
|
|
128
|
+
document.cookie = `${b}=${e}; path=/; max-age=${H}`;
|
|
62
129
|
}
|
|
63
|
-
function
|
|
64
|
-
const
|
|
65
|
-
return
|
|
130
|
+
function X(e) {
|
|
131
|
+
const t = window.matchMedia(_);
|
|
132
|
+
return t.addEventListener("change", e), () => t.removeEventListener("change", e);
|
|
66
133
|
}
|
|
67
|
-
function
|
|
68
|
-
return
|
|
69
|
-
|
|
70
|
-
() => window.matchMedia(
|
|
134
|
+
function J() {
|
|
135
|
+
return C(
|
|
136
|
+
X,
|
|
137
|
+
() => window.matchMedia(_).matches,
|
|
71
138
|
() => !1
|
|
72
139
|
);
|
|
73
140
|
}
|
|
74
|
-
function
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
}, []),
|
|
78
|
-
if (
|
|
79
|
-
|
|
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(() => {
|
|
145
|
+
if (e) {
|
|
146
|
+
o((s) => !s);
|
|
80
147
|
return;
|
|
81
148
|
}
|
|
82
|
-
|
|
83
|
-
}, [
|
|
84
|
-
return
|
|
85
|
-
function
|
|
86
|
-
(
|
|
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());
|
|
87
154
|
}
|
|
88
|
-
return window.addEventListener("keydown",
|
|
89
|
-
}, [
|
|
90
|
-
}
|
|
91
|
-
function
|
|
92
|
-
let
|
|
93
|
-
for (const
|
|
94
|
-
for (const
|
|
95
|
-
(
|
|
96
|
-
return
|
|
97
|
-
}
|
|
98
|
-
function
|
|
99
|
-
collapsed:
|
|
100
|
-
end:
|
|
101
|
-
item:
|
|
102
|
-
onNavigate:
|
|
155
|
+
return window.addEventListener("keydown", s), () => window.removeEventListener("keydown", s);
|
|
156
|
+
}, [d]), { isMobile: e, open: t, openMobile: r, setOpenMobile: o, toggleSidebar: d };
|
|
157
|
+
}
|
|
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;
|
|
164
|
+
}
|
|
165
|
+
function Z({
|
|
166
|
+
collapsed: e,
|
|
167
|
+
end: t,
|
|
168
|
+
item: a,
|
|
169
|
+
onNavigate: r
|
|
103
170
|
}) {
|
|
104
|
-
return /* @__PURE__ */
|
|
105
|
-
/* @__PURE__ */
|
|
171
|
+
return /* @__PURE__ */ u(p.Root, { children: [
|
|
172
|
+
/* @__PURE__ */ u(
|
|
106
173
|
p.Trigger,
|
|
107
174
|
{
|
|
108
|
-
render: /* @__PURE__ */
|
|
109
|
-
|
|
175
|
+
render: /* @__PURE__ */ n(
|
|
176
|
+
z,
|
|
110
177
|
{
|
|
111
178
|
className: "ux-nav-item",
|
|
112
|
-
end:
|
|
113
|
-
onClick:
|
|
114
|
-
to:
|
|
179
|
+
end: t,
|
|
180
|
+
onClick: r,
|
|
181
|
+
to: a.to
|
|
115
182
|
}
|
|
116
183
|
),
|
|
117
184
|
children: [
|
|
118
|
-
/* @__PURE__ */
|
|
119
|
-
/* @__PURE__ */
|
|
185
|
+
/* @__PURE__ */ n("span", { className: "ux-nav-item__icon", children: a.icon }),
|
|
186
|
+
/* @__PURE__ */ n("span", { className: "ux-nav-item__text", children: a.label })
|
|
120
187
|
]
|
|
121
188
|
}
|
|
122
189
|
),
|
|
123
|
-
/* @__PURE__ */
|
|
190
|
+
/* @__PURE__ */ n(p.Portal, { children: /* @__PURE__ */ n(
|
|
124
191
|
p.Positioner,
|
|
125
192
|
{
|
|
126
193
|
className: "ux-tooltip-positioner",
|
|
127
194
|
side: "right",
|
|
128
195
|
sideOffset: 6,
|
|
129
|
-
children: /* @__PURE__ */
|
|
196
|
+
children: /* @__PURE__ */ n(p.Popup, { className: "ux-tooltip", hidden: !e, children: a.label })
|
|
130
197
|
}
|
|
131
198
|
) })
|
|
132
199
|
] });
|
|
133
200
|
}
|
|
134
|
-
function
|
|
135
|
-
copy:
|
|
136
|
-
logo:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
201
|
+
function N({
|
|
202
|
+
copy: e,
|
|
203
|
+
logo: t,
|
|
204
|
+
logoRef: a,
|
|
205
|
+
nav: r,
|
|
206
|
+
onNavigate: o,
|
|
207
|
+
state: i,
|
|
140
208
|
title: d
|
|
141
209
|
}) {
|
|
142
|
-
const { pathname:
|
|
143
|
-
return /* @__PURE__ */
|
|
144
|
-
/* @__PURE__ */
|
|
145
|
-
/* @__PURE__ */
|
|
146
|
-
/* @__PURE__ */
|
|
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 })
|
|
147
215
|
] }),
|
|
148
|
-
/* @__PURE__ */
|
|
149
|
-
/* @__PURE__ */
|
|
150
|
-
|
|
216
|
+
/* @__PURE__ */ n("nav", { "aria-label": e.navigation, className: "ux-sidebar__nav", children: r.map((m) => /* @__PURE__ */ u("div", { className: "ux-nav-group", children: [
|
|
217
|
+
/* @__PURE__ */ n("div", { className: "ux-nav-group__label", children: m.label }),
|
|
218
|
+
m.items.map((c) => (
|
|
151
219
|
// NavLink would mark a parent section current on its children's
|
|
152
220
|
// routes; follow matchActiveItem instead so only the deepest
|
|
153
221
|
// match (or the exact path) stays marked.
|
|
154
|
-
/* @__PURE__ */
|
|
155
|
-
|
|
222
|
+
/* @__PURE__ */ n(
|
|
223
|
+
Z,
|
|
156
224
|
{
|
|
157
|
-
collapsed:
|
|
158
|
-
end:
|
|
159
|
-
item:
|
|
160
|
-
onNavigate:
|
|
225
|
+
collapsed: i === "collapsed",
|
|
226
|
+
end: c !== l || c.to === s,
|
|
227
|
+
item: c,
|
|
228
|
+
onNavigate: o
|
|
161
229
|
},
|
|
162
|
-
|
|
230
|
+
c.to
|
|
163
231
|
)
|
|
164
232
|
))
|
|
165
|
-
] },
|
|
233
|
+
] }, m.label)) })
|
|
166
234
|
] });
|
|
167
235
|
}
|
|
168
|
-
function
|
|
169
|
-
children:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
236
|
+
function _e({
|
|
237
|
+
children: e,
|
|
238
|
+
favicon: t = !0,
|
|
239
|
+
headerSlot: a,
|
|
240
|
+
logo: r,
|
|
241
|
+
nav: o,
|
|
242
|
+
pageTitle: i,
|
|
174
243
|
title: d
|
|
175
244
|
}) {
|
|
176
|
-
var
|
|
177
|
-
const { pathname:
|
|
178
|
-
return /* @__PURE__ */
|
|
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(
|
|
179
248
|
"div",
|
|
180
249
|
{
|
|
181
250
|
className: "ux-app-layout",
|
|
182
|
-
"data-state":
|
|
251
|
+
"data-state": c.open ? "expanded" : "collapsed",
|
|
183
252
|
children: [
|
|
184
|
-
/* @__PURE__ */
|
|
185
|
-
|
|
253
|
+
/* @__PURE__ */ n("aside", { className: "ux-sidebar", children: /* @__PURE__ */ n(
|
|
254
|
+
N,
|
|
186
255
|
{
|
|
187
|
-
copy:
|
|
188
|
-
logo:
|
|
189
|
-
|
|
190
|
-
|
|
256
|
+
copy: m,
|
|
257
|
+
logo: r,
|
|
258
|
+
logoRef: x,
|
|
259
|
+
nav: o,
|
|
260
|
+
state: c.open ? "expanded" : "collapsed",
|
|
191
261
|
title: d
|
|
192
262
|
}
|
|
193
263
|
) }),
|
|
194
|
-
|
|
264
|
+
c.isMobile ? /* @__PURE__ */ n(
|
|
195
265
|
h.Root,
|
|
196
266
|
{
|
|
197
|
-
open:
|
|
198
|
-
onOpenChange:
|
|
199
|
-
children: /* @__PURE__ */
|
|
200
|
-
/* @__PURE__ */
|
|
201
|
-
/* @__PURE__ */
|
|
202
|
-
/* @__PURE__ */
|
|
203
|
-
/* @__PURE__ */
|
|
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 }),
|
|
273
|
+
/* @__PURE__ */ n(
|
|
204
274
|
"button",
|
|
205
275
|
{
|
|
206
|
-
"aria-label":
|
|
276
|
+
"aria-label": m.closeNavigation,
|
|
207
277
|
className: "ux-drawer__close",
|
|
208
278
|
type: "button",
|
|
209
|
-
onClick: () =>
|
|
210
|
-
children: /* @__PURE__ */
|
|
279
|
+
onClick: () => c.setOpenMobile(!1),
|
|
280
|
+
children: /* @__PURE__ */ n(K, {})
|
|
211
281
|
}
|
|
212
282
|
),
|
|
213
|
-
/* @__PURE__ */
|
|
214
|
-
|
|
283
|
+
/* @__PURE__ */ n(
|
|
284
|
+
N,
|
|
215
285
|
{
|
|
216
|
-
copy:
|
|
217
|
-
logo:
|
|
218
|
-
nav:
|
|
219
|
-
onNavigate: () =>
|
|
286
|
+
copy: m,
|
|
287
|
+
logo: r,
|
|
288
|
+
nav: o,
|
|
289
|
+
onNavigate: () => c.setOpenMobile(!1),
|
|
220
290
|
state: "expanded",
|
|
221
291
|
title: d
|
|
222
292
|
}
|
|
@@ -225,30 +295,140 @@ function H({
|
|
|
225
295
|
] })
|
|
226
296
|
}
|
|
227
297
|
) : null,
|
|
228
|
-
/* @__PURE__ */
|
|
229
|
-
/* @__PURE__ */
|
|
230
|
-
/* @__PURE__ */
|
|
298
|
+
/* @__PURE__ */ u("div", { className: "ux-shell", children: [
|
|
299
|
+
/* @__PURE__ */ u("header", { className: "ux-app-header", children: [
|
|
300
|
+
/* @__PURE__ */ n(
|
|
231
301
|
"button",
|
|
232
302
|
{
|
|
233
|
-
"aria-expanded":
|
|
234
|
-
"aria-label":
|
|
303
|
+
"aria-expanded": c.isMobile ? c.openMobile : c.open,
|
|
304
|
+
"aria-label": m.toggleSidebar,
|
|
235
305
|
className: "ux-app-header__trigger",
|
|
236
306
|
type: "button",
|
|
237
|
-
onClick:
|
|
238
|
-
children: /* @__PURE__ */
|
|
307
|
+
onClick: c.toggleSidebar,
|
|
308
|
+
children: /* @__PURE__ */ n(V, {})
|
|
239
309
|
}
|
|
240
310
|
),
|
|
241
|
-
/* @__PURE__ */
|
|
242
|
-
/* @__PURE__ */
|
|
243
|
-
/* @__PURE__ */
|
|
244
|
-
/* @__PURE__ */
|
|
311
|
+
/* @__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, {})
|
|
245
315
|
] }),
|
|
246
|
-
/* @__PURE__ */
|
|
316
|
+
/* @__PURE__ */ n("main", { className: "ux-main", children: e })
|
|
247
317
|
] })
|
|
248
318
|
]
|
|
249
319
|
}
|
|
250
320
|
) });
|
|
251
321
|
}
|
|
322
|
+
let ee = 0;
|
|
323
|
+
function te() {
|
|
324
|
+
return document.documentElement.classList.contains("dark") ? "dark" : "neutral";
|
|
325
|
+
}
|
|
326
|
+
function ne() {
|
|
327
|
+
const e = getComputedStyle(document.documentElement).fontFamily.trim();
|
|
328
|
+
return e === "" ? "sans-serif" : e;
|
|
329
|
+
}
|
|
330
|
+
function re(e) {
|
|
331
|
+
const t = e ? Number.parseFloat(getComputedStyle(e).fontSize) : Number.NaN;
|
|
332
|
+
return !Number.isFinite(t) || t <= 0 ? 16 : Math.min(Math.max(t, 10), 20);
|
|
333
|
+
}
|
|
334
|
+
async function ae(e, t) {
|
|
335
|
+
try {
|
|
336
|
+
const { default: a } = await import("mermaid");
|
|
337
|
+
a.initialize({
|
|
338
|
+
startOnLoad: !1,
|
|
339
|
+
securityLevel: "strict",
|
|
340
|
+
theme: te(),
|
|
341
|
+
themeVariables: { fontFamily: ne(), fontSize: `${re(t)}px` }
|
|
342
|
+
});
|
|
343
|
+
const { svg: r } = await a.render(`ux-mermaid-${++ee}`, e);
|
|
344
|
+
return r;
|
|
345
|
+
} catch {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
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);
|
|
355
|
+
};
|
|
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();
|
|
360
|
+
};
|
|
361
|
+
}, [e]), t === null ? /* @__PURE__ */ n(
|
|
362
|
+
"pre",
|
|
363
|
+
{
|
|
364
|
+
className: "ux-markdown__mermaid-source",
|
|
365
|
+
"data-mermaid": "source",
|
|
366
|
+
ref: (i) => {
|
|
367
|
+
r.current = i;
|
|
368
|
+
},
|
|
369
|
+
children: /* @__PURE__ */ n("code", { className: "language-mermaid", children: e })
|
|
370
|
+
}
|
|
371
|
+
) : /* @__PURE__ */ n(
|
|
372
|
+
"div",
|
|
373
|
+
{
|
|
374
|
+
"aria-label": "Mermaid diagram",
|
|
375
|
+
className: "ux-markdown__mermaid",
|
|
376
|
+
"data-mermaid": "diagram",
|
|
377
|
+
dangerouslySetInnerHTML: { __html: t },
|
|
378
|
+
ref: (i) => {
|
|
379
|
+
r.current = i;
|
|
380
|
+
},
|
|
381
|
+
role: "img"
|
|
382
|
+
}
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
function ie(e) {
|
|
386
|
+
const t = e == null ? void 0 : e.children[0];
|
|
387
|
+
if ((t == null ? void 0 : t.type) !== "element" || t.tagName !== "code")
|
|
388
|
+
return null;
|
|
389
|
+
const a = t.properties.className;
|
|
390
|
+
if (!Array.isArray(a) || !a.includes("language-mermaid"))
|
|
391
|
+
return null;
|
|
392
|
+
const r = t.children[0];
|
|
393
|
+
return (r == null ? void 0 : r.type) === "text" ? r.value : null;
|
|
394
|
+
}
|
|
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: [
|
|
397
|
+
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 });
|
|
403
|
+
},
|
|
404
|
+
// A wide table scrolls on its own; the transcript or article around it never grows a horizontal
|
|
405
|
+
// 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) {
|
|
409
|
+
return /^[a-z][a-z0-9+.-]*:/i.test(e) || e.startsWith("//");
|
|
410
|
+
}
|
|
411
|
+
function de(e) {
|
|
412
|
+
const t = e.trim(), a = t.indexOf(":");
|
|
413
|
+
if (a === -1)
|
|
414
|
+
return t;
|
|
415
|
+
const r = t.slice(0, a).toLowerCase();
|
|
416
|
+
return /^[a-z][a-z0-9+.-]*$/.test(r) ? ce.has(r) ? t : "" : t;
|
|
417
|
+
}
|
|
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,
|
|
422
|
+
{
|
|
423
|
+
components: se,
|
|
424
|
+
rehypePlugins: me,
|
|
425
|
+
remarkPlugins: ue,
|
|
426
|
+
urlTransform: de,
|
|
427
|
+
children: e
|
|
428
|
+
}
|
|
429
|
+
) });
|
|
430
|
+
}
|
|
252
431
|
export {
|
|
253
|
-
|
|
432
|
+
_e as AppLayout,
|
|
433
|
+
Me as Markdown
|
|
254
434
|
};
|
package/dist/styles.css
CHANGED
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
--ux-foreground: var(--foreground, #0f172a);
|
|
14
14
|
--ux-border: var(--border, #e2e8f0);
|
|
15
15
|
--ux-accent: var(--accent, var(--muted, #f1f5f9));
|
|
16
|
+
--ux-muted: var(--muted, #f1f5f9);
|
|
17
|
+
--ux-muted-foreground: var(--muted-foreground, #64748b);
|
|
18
|
+
--ux-primary: var(--primary, #2563eb);
|
|
19
|
+
--ux-destructive: var(--destructive, #dc2626);
|
|
16
20
|
--ux-ring: var(--ring, #2563eb);
|
|
17
21
|
--ux-sidebar: var(--sidebar, #fafafa);
|
|
18
22
|
--ux-sidebar-foreground: var(--sidebar-foreground, var(--foreground, #0f172a));
|
|
@@ -27,6 +31,10 @@
|
|
|
27
31
|
--ux-foreground: var(--foreground, #f2f2f2);
|
|
28
32
|
--ux-border: var(--border, #383838);
|
|
29
33
|
--ux-accent: var(--accent, var(--muted, #303030));
|
|
34
|
+
--ux-muted: var(--muted, #262626);
|
|
35
|
+
--ux-muted-foreground: var(--muted-foreground, #a3a3a3);
|
|
36
|
+
--ux-primary: var(--primary, #60a5fa);
|
|
37
|
+
--ux-destructive: var(--destructive, #f87171);
|
|
30
38
|
--ux-ring: var(--ring, #d4d4d4);
|
|
31
39
|
--ux-sidebar: var(--sidebar, #161616);
|
|
32
40
|
--ux-sidebar-foreground: var(--sidebar-foreground, var(--foreground, #f2f2f2));
|
|
@@ -462,3 +470,236 @@
|
|
|
462
470
|
transition: none;
|
|
463
471
|
}
|
|
464
472
|
}
|
|
473
|
+
|
|
474
|
+
/* --- Markdown --- */
|
|
475
|
+
|
|
476
|
+
/*
|
|
477
|
+
* One typographic scale for every Markdown surface in the family: replies, checkpoints, manuals,
|
|
478
|
+
* documents and articles all render through <Markdown>. Every size here is relative to the
|
|
479
|
+
* inherited font size and no measure is imposed, so the same content keeps identical proportions
|
|
480
|
+
* inside a 14px chat bubble and a 16px article page — the host container decides the column, this
|
|
481
|
+
* block decides the rhythm.
|
|
482
|
+
*/
|
|
483
|
+
|
|
484
|
+
.ux-markdown {
|
|
485
|
+
min-width: 0;
|
|
486
|
+
line-height: 1.7;
|
|
487
|
+
overflow-wrap: break-word;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.ux-markdown > :first-child {
|
|
491
|
+
margin-top: 0;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.ux-markdown > :last-child {
|
|
495
|
+
margin-bottom: 0;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.ux-markdown p,
|
|
499
|
+
.ux-markdown ul,
|
|
500
|
+
.ux-markdown ol,
|
|
501
|
+
.ux-markdown blockquote,
|
|
502
|
+
.ux-markdown pre,
|
|
503
|
+
.ux-markdown .ux-markdown__table,
|
|
504
|
+
.ux-markdown .ux-markdown__mermaid {
|
|
505
|
+
margin: 0.75em 0;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.ux-markdown h1,
|
|
509
|
+
.ux-markdown h2,
|
|
510
|
+
.ux-markdown h3,
|
|
511
|
+
.ux-markdown h4,
|
|
512
|
+
.ux-markdown h5,
|
|
513
|
+
.ux-markdown h6 {
|
|
514
|
+
margin: 1.5em 0 0.5em;
|
|
515
|
+
font-weight: 600;
|
|
516
|
+
line-height: 1.3;
|
|
517
|
+
letter-spacing: -0.01em;
|
|
518
|
+
text-wrap: balance;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.ux-markdown h1 {
|
|
522
|
+
font-size: 1.5em;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.ux-markdown h2 {
|
|
526
|
+
font-size: 1.25em;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.ux-markdown h3 {
|
|
530
|
+
font-size: 1.125em;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
.ux-markdown h4,
|
|
534
|
+
.ux-markdown h5,
|
|
535
|
+
.ux-markdown h6 {
|
|
536
|
+
font-size: 1em;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.ux-markdown ul,
|
|
540
|
+
.ux-markdown ol {
|
|
541
|
+
padding-left: 1.5em;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.ux-markdown ul {
|
|
545
|
+
list-style: disc;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.ux-markdown ol {
|
|
549
|
+
list-style: decimal;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
.ux-markdown li {
|
|
553
|
+
margin: 0.25em 0;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.ux-markdown li > ul,
|
|
557
|
+
.ux-markdown li > ol {
|
|
558
|
+
margin: 0.25em 0;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/* GFM task lists: the marker column disappears and the checkbox takes its place. */
|
|
562
|
+
.ux-markdown ul.contains-task-list {
|
|
563
|
+
padding-left: 0.25em;
|
|
564
|
+
list-style: none;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.ux-markdown li.task-list-item {
|
|
568
|
+
list-style: none;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.ux-markdown li.task-list-item > input[type="checkbox"] {
|
|
572
|
+
margin: 0 0.4em 0 0;
|
|
573
|
+
vertical-align: 0.05em;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.ux-markdown blockquote {
|
|
577
|
+
padding-left: 1em;
|
|
578
|
+
color: var(--ux-muted-foreground);
|
|
579
|
+
border-left: 2px solid var(--ux-border);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.ux-markdown blockquote > :first-child {
|
|
583
|
+
margin-top: 0;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.ux-markdown blockquote > :last-child {
|
|
587
|
+
margin-bottom: 0;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.ux-markdown hr {
|
|
591
|
+
margin: 2em 0;
|
|
592
|
+
border: 0;
|
|
593
|
+
border-top: 1px solid var(--ux-border);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.ux-markdown a {
|
|
597
|
+
color: var(--ux-primary);
|
|
598
|
+
text-decoration: underline;
|
|
599
|
+
text-underline-offset: 0.2em;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.ux-markdown a:hover {
|
|
603
|
+
text-decoration-thickness: 2px;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/* Sized in `em` so the marker tracks the text it follows instead of sitting at a fixed 12px. */
|
|
607
|
+
.ux-markdown .ux-markdown__external-link {
|
|
608
|
+
display: inline-block;
|
|
609
|
+
width: 0.75em;
|
|
610
|
+
height: 0.75em;
|
|
611
|
+
margin-left: 0.2em;
|
|
612
|
+
vertical-align: -0.05em;
|
|
613
|
+
color: var(--ux-muted-foreground);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.ux-markdown code {
|
|
617
|
+
padding: 0.15em 0.35em;
|
|
618
|
+
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
619
|
+
font-size: 0.875em;
|
|
620
|
+
background: var(--ux-muted);
|
|
621
|
+
border-radius: 0.25rem;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.ux-markdown pre {
|
|
625
|
+
padding: 0.875em 1em;
|
|
626
|
+
overflow-x: auto;
|
|
627
|
+
overscroll-behavior-x: contain;
|
|
628
|
+
line-height: 1.55;
|
|
629
|
+
background: var(--ux-muted);
|
|
630
|
+
border-radius: 0.5rem;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.ux-markdown pre code {
|
|
634
|
+
padding: 0;
|
|
635
|
+
font-size: 0.8125em;
|
|
636
|
+
background: none;
|
|
637
|
+
border-radius: 0;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.ux-markdown .ux-markdown__table {
|
|
641
|
+
max-width: 100%;
|
|
642
|
+
overflow-x: auto;
|
|
643
|
+
overscroll-behavior-x: contain;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/* Wide tables scroll; short ones still fill the column rather than shrinking to their content. */
|
|
647
|
+
.ux-markdown table {
|
|
648
|
+
width: 100%;
|
|
649
|
+
min-width: max-content;
|
|
650
|
+
font-size: 0.9375em;
|
|
651
|
+
text-align: left;
|
|
652
|
+
border-collapse: collapse;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
.ux-markdown th,
|
|
656
|
+
.ux-markdown td {
|
|
657
|
+
padding: 0.4em 0.75em;
|
|
658
|
+
vertical-align: top;
|
|
659
|
+
border-bottom: 1px solid var(--ux-border);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.ux-markdown th {
|
|
663
|
+
font-weight: 600;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.ux-markdown tr:last-child td {
|
|
667
|
+
border-bottom: 0;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
.ux-markdown img {
|
|
671
|
+
max-width: 100%;
|
|
672
|
+
height: auto;
|
|
673
|
+
border-radius: 0.375rem;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
.ux-markdown del {
|
|
677
|
+
color: var(--ux-muted-foreground);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
.ux-markdown strong {
|
|
681
|
+
font-weight: 600;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/* Display math scrolls inside its own line instead of widening the surface around it. */
|
|
685
|
+
.ux-markdown .katex-display {
|
|
686
|
+
padding: 0.25em 0;
|
|
687
|
+
overflow-x: auto;
|
|
688
|
+
overflow-y: hidden;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
.ux-markdown .katex-error {
|
|
692
|
+
color: var(--ux-destructive);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.ux-markdown .ux-markdown__mermaid {
|
|
696
|
+
overflow-x: auto;
|
|
697
|
+
overscroll-behavior-x: contain;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
.ux-markdown .ux-markdown__mermaid svg {
|
|
701
|
+
display: block;
|
|
702
|
+
max-width: none;
|
|
703
|
+
height: auto;
|
|
704
|
+
margin: 0 auto;
|
|
705
|
+
}
|
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[];
|
|
@@ -16,3 +17,6 @@ export type AppLayoutProps = {
|
|
|
16
17
|
pageTitle?: ReactNode;
|
|
17
18
|
children: ReactNode;
|
|
18
19
|
};
|
|
20
|
+
export type MarkdownProps = {
|
|
21
|
+
content: string;
|
|
22
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zccz14/ux",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Unified
|
|
3
|
+
"version": "0.4.0",
|
|
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": {
|
|
7
7
|
"type": "git",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"build": "vite build && tsc -p tsconfig.build.json && cp src/styles.css dist/styles.css",
|
|
36
|
-
"test": "vitest run && node scripts/check-styles.mjs",
|
|
36
|
+
"test": "vitest run && node scripts/check-styles.mjs && node scripts/check-package.mjs",
|
|
37
37
|
"typecheck": "tsc --noEmit",
|
|
38
38
|
"pack:check": "npm pack --dry-run"
|
|
39
39
|
},
|
|
@@ -44,10 +44,19 @@
|
|
|
44
44
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
45
45
|
"react-router-dom": "^7.0.0"
|
|
46
46
|
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"katex": "^0.16.47",
|
|
49
|
+
"mermaid": "^11.17.2",
|
|
50
|
+
"react-markdown": "^10.1.0",
|
|
51
|
+
"rehype-katex": "^7.0.1",
|
|
52
|
+
"remark-gfm": "^4.0.1",
|
|
53
|
+
"remark-math-extended": "^6.1.1"
|
|
54
|
+
},
|
|
47
55
|
"devDependencies": {
|
|
48
56
|
"@base-ui/react": "^1.6.0",
|
|
49
57
|
"@testing-library/jest-dom": "^6.9.1",
|
|
50
58
|
"@testing-library/react": "^16.3.0",
|
|
59
|
+
"@types/hast": "^3.0.5",
|
|
51
60
|
"@types/react": "^19.1.16",
|
|
52
61
|
"@types/react-dom": "^19.1.9",
|
|
53
62
|
"auth-mini-react-components": "^0.6.10",
|