@karimsa/mdxserve 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +233 -2
- package/client/App.tsx +7 -0
- package/client/CodeBlock.tsx +395 -0
- package/client/CrossFade.tsx +72 -0
- package/client/DocContext.ts +14 -0
- package/client/DocView.tsx +107 -0
- package/client/ErrorBox.tsx +23 -0
- package/client/Heading.tsx +31 -0
- package/client/HomeEmptyState.tsx +101 -0
- package/client/HomeView.tsx +78 -0
- package/client/ListingView.tsx +663 -0
- package/client/MdSection.tsx +234 -0
- package/client/MdSectionEditor.tsx +233 -0
- package/client/Mermaid.tsx +435 -0
- package/client/RenderErrorBoundary.tsx +40 -0
- package/client/Table.tsx +14 -0
- package/client/TaskCheckbox.tsx +38 -0
- package/client/api.ts +138 -0
- package/client/app.css +372 -0
- package/client/builtins/Badge.tsx +109 -0
- package/client/builtins/Button.tsx +111 -0
- package/client/builtins/Callout.tsx +97 -0
- package/client/builtins/Card.tsx +111 -0
- package/client/builtins/Chart.tsx +875 -0
- package/client/builtins/Diff.tsx +722 -0
- package/client/builtins/Dropdown.tsx +417 -0
- package/client/builtins/FileTree.tsx +87 -0
- package/client/builtins/Kbd.tsx +18 -0
- package/client/builtins/Screenshot.tsx +209 -0
- package/client/builtins/Sparkline.tsx +63 -0
- package/client/builtins/Tabs.tsx +169 -0
- package/client/builtins/Tooltip.tsx +52 -0
- package/client/builtins/chart-data.ts +133 -0
- package/client/builtins/index.ts +167 -0
- package/client/design/base/editor.css +151 -0
- package/client/design/base/prose.css +143 -0
- package/client/design/base/reset.css +79 -0
- package/client/design/tokens/colors.css +188 -0
- package/client/design/tokens/elevation.css +42 -0
- package/client/design/tokens/fonts.css +6 -0
- package/client/design/tokens/motion.css +76 -0
- package/client/design/tokens/spacing.css +34 -0
- package/client/design/tokens/typography.css +56 -0
- package/client/doc-module-cache.ts +17 -0
- package/client/editor-link.ts +27 -0
- package/client/entry.tsx +51 -0
- package/client/export-doc.ts +80 -0
- package/client/export-save.ts +96 -0
- package/client/favicon.svg +1 -0
- package/client/file-system-access.d.ts +29 -0
- package/client/format.ts +17 -0
- package/client/hooks.ts +34 -0
- package/client/lucide-icons.d.ts +9 -0
- package/client/mdx-components-base.ts +32 -0
- package/client/mdx-components.ts +18 -0
- package/client/mermaid-chart.ts +109 -0
- package/client/mermaid-direction.ts +73 -0
- package/client/motion.ts +104 -0
- package/client/platform.ts +16 -0
- package/client/route-path.ts +15 -0
- package/client/router.ts +452 -0
- package/client/shell/AppShell.tsx +401 -0
- package/client/shell/Footer.tsx +33 -0
- package/client/shell/NotFoundView.tsx +22 -0
- package/client/shell/Sidebar.tsx +169 -0
- package/client/shell/StandaloneShell.tsx +65 -0
- package/client/shell/TocRail.tsx +53 -0
- package/client/shell/TopBar.tsx +117 -0
- package/client/shell/use-doc-width.ts +61 -0
- package/client/shell/useToc.ts +77 -0
- package/client/ssr-entry.tsx +22 -0
- package/client/standalone-entry.tsx +51 -0
- package/client/state.ts +90 -0
- package/client/theme.ts +65 -0
- package/client/ui/Breadcrumb.tsx +49 -0
- package/client/ui/ConfirmDeleteDialog.tsx +113 -0
- package/client/ui/ExpandModal.tsx +342 -0
- package/client/ui/Icon.tsx +114 -0
- package/client/ui/IconButton.tsx +63 -0
- package/client/ui/Kbd.tsx +17 -0
- package/client/ui/PageNav.tsx +77 -0
- package/client/ui/ResizeHandle.tsx +201 -0
- package/client/ui/SearchDialog.tsx +187 -0
- package/client/ui/Tag.tsx +44 -0
- package/client/ui/Toast.tsx +189 -0
- package/client/ui/TocList.tsx +71 -0
- package/client/ui/icon-set.ts +102 -0
- package/client/ui/toast-count.ts +28 -0
- package/dist/cli.js +5091 -0
- package/dist/registry.json +703 -0
- package/dist/render-worker.js +145 -0
- package/package.json +113 -5
- package/skills/mdxserve/SKILL.md +178 -0
package/client/app.css
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/* mdxserve global stylesheet.
|
|
2
|
+
*
|
|
3
|
+
* Order matters:
|
|
4
|
+
* 1. @imports — tippy's base css (see the Tooltip note below), then the design
|
|
5
|
+
* system's tokens and base styles (client/design/, ported verbatim from the
|
|
6
|
+
* "mdxserve Design System" Claude Design project).
|
|
7
|
+
* 2. @custom-variant dark — the `dark:` utility variant keys on the
|
|
8
|
+
* `data-theme` attribute that src/http/html.ts sets on <html>, not on the OS
|
|
9
|
+
* media query, so the in-app toggle works.
|
|
10
|
+
* 3. @theme inline — exposes the design tokens to Tailwind so utilities like
|
|
11
|
+
* `bg-surface-card` / `text-text-muted` / `shadow-sm` point straight at
|
|
12
|
+
* the CSS variables (no duplicated values, dark mode comes for free).
|
|
13
|
+
* 4. App-specific rules that are not part of the design system proper.
|
|
14
|
+
*
|
|
15
|
+
* The design tokens are deliberately left *unlayered*: `@theme inline` entries
|
|
16
|
+
* whose name matches a token (`--font-mono`, `--radius-md`) emit a
|
|
17
|
+
* self-reference inside `@layer theme`, and the unlayered `:root` declaration
|
|
18
|
+
* is what wins the cascade. Wrapping client/design in @layer would break that.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/* Base tippy.js styles for the builtin Tooltip component (client/builtins/Tooltip.tsx).
|
|
22
|
+
Imported here (rather than from the component) so it flows through Vite's normal
|
|
23
|
+
CSS pipeline and doesn't break scripts/build-registry.ts, which imports that
|
|
24
|
+
component under plain Node to read its zod schema. */
|
|
25
|
+
@import "tippy.js/dist/tippy.css";
|
|
26
|
+
|
|
27
|
+
@import "./design/tokens/fonts.css";
|
|
28
|
+
@import "./design/tokens/colors.css";
|
|
29
|
+
@import "./design/tokens/typography.css";
|
|
30
|
+
@import "./design/tokens/spacing.css";
|
|
31
|
+
@import "./design/tokens/elevation.css";
|
|
32
|
+
@import "./design/tokens/motion.css";
|
|
33
|
+
@import "./design/base/reset.css";
|
|
34
|
+
@import "./design/base/prose.css";
|
|
35
|
+
@import "./design/base/editor.css";
|
|
36
|
+
|
|
37
|
+
/* `:root[...]` (not bare `[data-theme]`) because rehype-pretty-code stamps
|
|
38
|
+
data-theme="<shiki theme name>" on every <pre>/<code> it renders. */
|
|
39
|
+
@custom-variant dark (&:where(:root[data-theme="dark"], :root[data-theme="dark"] *));
|
|
40
|
+
|
|
41
|
+
@theme inline {
|
|
42
|
+
/* fonts */
|
|
43
|
+
--font-sans: var(--font-core);
|
|
44
|
+
--font-mono: var(--font-mono);
|
|
45
|
+
|
|
46
|
+
/* surfaces */
|
|
47
|
+
--color-surface-page: var(--surface-page);
|
|
48
|
+
--color-surface-card: var(--surface-card);
|
|
49
|
+
--color-surface-sunken: var(--surface-sunken);
|
|
50
|
+
--color-surface-raised: var(--surface-raised);
|
|
51
|
+
--color-surface-inverse: var(--surface-inverse);
|
|
52
|
+
--color-surface-hover: var(--surface-hover);
|
|
53
|
+
--color-surface-active: var(--surface-active);
|
|
54
|
+
--color-surface-accent-soft: var(--surface-accent-soft);
|
|
55
|
+
--color-surface-accent: var(--surface-accent);
|
|
56
|
+
|
|
57
|
+
/* text */
|
|
58
|
+
--color-text-body: var(--text-body);
|
|
59
|
+
--color-text-heading: var(--text-heading);
|
|
60
|
+
--color-text-muted: var(--text-muted);
|
|
61
|
+
--color-text-subtle: var(--text-subtle);
|
|
62
|
+
--color-text-inverse: var(--text-inverse);
|
|
63
|
+
--color-text-accent: var(--text-accent);
|
|
64
|
+
--color-text-link: var(--text-link);
|
|
65
|
+
--color-text-link-hover: var(--text-link-hover);
|
|
66
|
+
|
|
67
|
+
/* borders */
|
|
68
|
+
--color-border-subtle: var(--border-subtle);
|
|
69
|
+
--color-border-default: var(--border-default);
|
|
70
|
+
--color-border-strong: var(--border-strong);
|
|
71
|
+
--color-border-accent: var(--border-accent);
|
|
72
|
+
--color-focus-ring: var(--focus-ring);
|
|
73
|
+
|
|
74
|
+
/* status tints (callouts, badges, toasts) */
|
|
75
|
+
--color-status-info-bg: var(--status-info-bg);
|
|
76
|
+
--color-status-info-fg: var(--status-info-fg);
|
|
77
|
+
--color-status-ok-bg: var(--status-ok-bg);
|
|
78
|
+
--color-status-ok-fg: var(--status-ok-fg);
|
|
79
|
+
--color-status-warn-bg: var(--status-warn-bg);
|
|
80
|
+
--color-status-warn-fg: var(--status-warn-fg);
|
|
81
|
+
--color-status-danger-bg: var(--status-danger-bg);
|
|
82
|
+
--color-status-danger-fg: var(--status-danger-fg);
|
|
83
|
+
--color-status-note-bg: var(--status-note-bg);
|
|
84
|
+
--color-status-note-fg: var(--status-note-fg);
|
|
85
|
+
|
|
86
|
+
/* code surfaces */
|
|
87
|
+
--color-code-bg: var(--code-bg);
|
|
88
|
+
--color-code-bg-inline: var(--code-bg-inline);
|
|
89
|
+
--color-code-fg: var(--code-fg);
|
|
90
|
+
--color-code-border: var(--code-border);
|
|
91
|
+
--color-code-gutter: var(--code-gutter);
|
|
92
|
+
--color-code-highlight: var(--code-highlight);
|
|
93
|
+
|
|
94
|
+
/* raw ramps, for the few places the design names them directly */
|
|
95
|
+
--color-teal-50: var(--teal-50);
|
|
96
|
+
--color-teal-100: var(--teal-100);
|
|
97
|
+
--color-teal-200: var(--teal-200);
|
|
98
|
+
--color-teal-300: var(--teal-300);
|
|
99
|
+
--color-teal-400: var(--teal-400);
|
|
100
|
+
--color-teal-500: var(--teal-500);
|
|
101
|
+
--color-teal-550: var(--teal-550);
|
|
102
|
+
--color-teal-600: var(--teal-600);
|
|
103
|
+
--color-teal-700: var(--teal-700);
|
|
104
|
+
--color-teal-800: var(--teal-800);
|
|
105
|
+
--color-teal-900: var(--teal-900);
|
|
106
|
+
--color-n-0: var(--n-0);
|
|
107
|
+
--color-n-25: var(--n-25);
|
|
108
|
+
--color-n-50: var(--n-50);
|
|
109
|
+
--color-n-100: var(--n-100);
|
|
110
|
+
--color-n-200: var(--n-200);
|
|
111
|
+
--color-n-300: var(--n-300);
|
|
112
|
+
--color-n-400: var(--n-400);
|
|
113
|
+
--color-n-500: var(--n-500);
|
|
114
|
+
--color-n-600: var(--n-600);
|
|
115
|
+
--color-n-700: var(--n-700);
|
|
116
|
+
--color-n-800: var(--n-800);
|
|
117
|
+
--color-n-900: var(--n-900);
|
|
118
|
+
--color-n-950: var(--n-950);
|
|
119
|
+
|
|
120
|
+
/* shadows — the one namespace Tailwind would otherwise inline its own
|
|
121
|
+
literal values for, so these mappings are required, not cosmetic */
|
|
122
|
+
--shadow-xs: var(--shadow-xs);
|
|
123
|
+
--shadow-sm: var(--shadow-sm);
|
|
124
|
+
--shadow-md: var(--shadow-md);
|
|
125
|
+
--shadow-lg: var(--shadow-lg);
|
|
126
|
+
--shadow-hairline: var(--shadow-hairline);
|
|
127
|
+
--shadow-focus: var(--shadow-focus);
|
|
128
|
+
|
|
129
|
+
/* radii — token names already match Tailwind's; listed for the extras */
|
|
130
|
+
--radius-xs: var(--radius-xs);
|
|
131
|
+
--radius-sm: var(--radius-sm);
|
|
132
|
+
--radius-md: var(--radius-md);
|
|
133
|
+
--radius-lg: var(--radius-lg);
|
|
134
|
+
--radius-xl: var(--radius-xl);
|
|
135
|
+
--radius-pill: var(--radius-pill);
|
|
136
|
+
|
|
137
|
+
/* layout */
|
|
138
|
+
--spacing-sidebar: var(--sidebar-width);
|
|
139
|
+
--spacing-toc: var(--toc-width);
|
|
140
|
+
--spacing-topbar: var(--topbar-height);
|
|
141
|
+
--container-prose: var(--width-prose);
|
|
142
|
+
--container-content: var(--width-content);
|
|
143
|
+
|
|
144
|
+
/* motion */
|
|
145
|
+
--ease-standard: var(--ease-standard);
|
|
146
|
+
--ease-out: var(--ease-out);
|
|
147
|
+
--ease-in: var(--ease-in);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* Opt out of the global focus ring for inputs whose container already shows
|
|
151
|
+
focus (the search palette's text field). */
|
|
152
|
+
[data-bare-focus]:focus-visible {
|
|
153
|
+
box-shadow: none;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* Anchor jumps (TOC, `.mdx-anchor`, in-page links) clear the sticky topbar and
|
|
157
|
+
glide rather than snap. Programmatic scrolls that should not animate (route
|
|
158
|
+
change reset, first-load hash) pass `behavior: "instant"` explicitly. */
|
|
159
|
+
html {
|
|
160
|
+
scroll-padding-top: calc(var(--topbar-height) + var(--space-2));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
164
|
+
html {
|
|
165
|
+
scroll-behavior: smooth;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.mdx-prose h1,
|
|
170
|
+
.mdx-prose h2,
|
|
171
|
+
.mdx-prose h3,
|
|
172
|
+
.mdx-prose h4 {
|
|
173
|
+
scroll-margin-top: calc(var(--topbar-height) + var(--space-2));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* Tailwind preflight resets list-style; the design's prose.css assumes markers. */
|
|
177
|
+
.mdx-prose ul:not(:where(.not-prose, .not-prose *)) {
|
|
178
|
+
list-style: disc;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.mdx-prose ol:not(:where(.not-prose, .not-prose *)) {
|
|
182
|
+
list-style: decimal;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.mdx-prose ul ul:not(:where(.not-prose, .not-prose *)) {
|
|
186
|
+
list-style: circle;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* GFM task list items: the checkbox is the marker. */
|
|
190
|
+
.mdx-prose li:has(> input[type="checkbox"]) {
|
|
191
|
+
list-style: none;
|
|
192
|
+
margin-left: -1.35em;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.mdx-prose li > input[type="checkbox"] {
|
|
196
|
+
margin: 0 0.5em 0 0;
|
|
197
|
+
accent-color: var(--teal-550);
|
|
198
|
+
vertical-align: -0.1em;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* `not-prose` is a marker class (it used to come from @tailwindcss/typography).
|
|
202
|
+
Every element rule in client/design/base/prose.css carries a
|
|
203
|
+
`:not(:where(.not-prose, .not-prose *))` guard, so a builtin that sets it on
|
|
204
|
+
its root keeps its own typography. No rule is needed for the class itself. */
|
|
205
|
+
|
|
206
|
+
/* Code blocks: hand off entirely to CodeFrame/Pre (client/CodeBlock.tsx) — prose
|
|
207
|
+
must not impose padding, background, or figcaption styling on them. Scoped to
|
|
208
|
+
rehype-pretty-code's figure so component <figure>s (Chart, Diagram) keep their
|
|
209
|
+
own radius and the prose block gap. The `.mdx-prose code` inline chip rule must
|
|
210
|
+
not hit code inside a <pre> either. */
|
|
211
|
+
.mdx-prose pre,
|
|
212
|
+
.mdx-prose pre code,
|
|
213
|
+
.mdx-prose figure[data-rehype-pretty-code-figure] {
|
|
214
|
+
margin: 0;
|
|
215
|
+
padding: 0;
|
|
216
|
+
background: transparent;
|
|
217
|
+
font: var(--type-code);
|
|
218
|
+
color: var(--code-fg);
|
|
219
|
+
border-radius: 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* Same `:is()` widening as prose.css's direct-child rhythm rules — a code
|
|
223
|
+
block can sit inside a `.mdx-section` wrapper or a section's own edit-mode
|
|
224
|
+
`.ProseMirror` surface, not just bare under `.mdx-prose`. */
|
|
225
|
+
:is(.mdx-prose, .mdx-section, .mdx-prose .ProseMirror) > figure[data-rehype-pretty-code-figure] + *,
|
|
226
|
+
:is(.mdx-prose, .mdx-section, .mdx-prose .ProseMirror)
|
|
227
|
+
> *
|
|
228
|
+
+ figure[data-rehype-pretty-code-figure] {
|
|
229
|
+
margin-top: var(--prose-gap);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* Padding lives here (not in utility classes) so the unlayered reset above
|
|
233
|
+
can't strip it; CodeFrame's <pre> relies on it. */
|
|
234
|
+
.mdx-prose pre,
|
|
235
|
+
.not-prose pre {
|
|
236
|
+
padding: var(--space-4) 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/*
|
|
240
|
+
* rehype-pretty-code line rendering (client/CodeBlock.tsx owns the surrounding
|
|
241
|
+
* card; this handles the per-line grid rows it emits):
|
|
242
|
+
* - every line is a `<span data-line>` inside a `<code style="display:grid">`,
|
|
243
|
+
* so it stretches to the full (possibly scrolled) width by default.
|
|
244
|
+
* - `showLineNumbers` meta sets `data-line-numbers` on `<code>` and
|
|
245
|
+
* `data-line-numbers-max-digits` to the gutter's digit count.
|
|
246
|
+
* - `{1,3-4}`-style meta sets `data-highlighted-line` on the matching lines.
|
|
247
|
+
*/
|
|
248
|
+
[data-line] {
|
|
249
|
+
padding-inline: var(--space-4);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
[data-highlighted-line] {
|
|
253
|
+
background-color: var(--code-highlight);
|
|
254
|
+
box-shadow: inset 2px 0 0 var(--teal-400);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
code[data-line-numbers] {
|
|
258
|
+
counter-reset: line;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
code[data-line-numbers] [data-line]::before {
|
|
262
|
+
counter-increment: line;
|
|
263
|
+
content: counter(line);
|
|
264
|
+
display: inline-block;
|
|
265
|
+
width: 1ch;
|
|
266
|
+
margin-right: var(--space-4);
|
|
267
|
+
text-align: right;
|
|
268
|
+
color: var(--code-gutter);
|
|
269
|
+
user-select: none;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
code[data-line-numbers-max-digits="2"] [data-line]::before {
|
|
273
|
+
width: 2ch;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
code[data-line-numbers-max-digits="3"] [data-line]::before {
|
|
277
|
+
width: 3ch;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
code[data-line-numbers-max-digits="4"] [data-line]::before {
|
|
281
|
+
width: 4ch;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/* Tippy theme for the builtin Tooltip component (client/builtins/Tooltip.tsx).
|
|
285
|
+
The arrow's border-*-color is `initial` (== currentcolor) per placement in
|
|
286
|
+
tippy.js/dist/tippy.css, picking up whatever `color` is set on the arrow
|
|
287
|
+
element itself — so setting `color` on `::before` (not just `.tippy-arrow`)
|
|
288
|
+
is what actually tints it. Inverse surface in both themes. */
|
|
289
|
+
.tippy-box[data-theme~="mdxserve"] {
|
|
290
|
+
background: var(--surface-inverse);
|
|
291
|
+
color: var(--text-inverse);
|
|
292
|
+
font: var(--type-ui-sm);
|
|
293
|
+
font-size: var(--size-xs);
|
|
294
|
+
border-radius: var(--radius-md);
|
|
295
|
+
padding: var(--space-0-5) var(--space-1);
|
|
296
|
+
box-shadow: var(--shadow-md);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.tippy-box[data-theme~="mdxserve"] > .tippy-arrow::before {
|
|
300
|
+
color: var(--surface-inverse);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* Folder listing titles are rendered from the doc's first h1 as inline HTML. */
|
|
304
|
+
/* Keep this in lockstep with the `.mdx-prose code` chip in design/base/prose.css. */
|
|
305
|
+
.listing-title code {
|
|
306
|
+
font: var(--type-code-inline);
|
|
307
|
+
background: var(--code-bg-inline);
|
|
308
|
+
color: var(--text-accent);
|
|
309
|
+
padding: 0.12em 0.34em;
|
|
310
|
+
border-radius: var(--radius-xs);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/* Wide tables scroll within the prose column instead of overflowing it. The
|
|
314
|
+
wrapper takes the table's block margin so the prose gap is unchanged. */
|
|
315
|
+
.mdx-table-scroll {
|
|
316
|
+
max-width: 100%;
|
|
317
|
+
overflow-x: auto;
|
|
318
|
+
}
|
|
319
|
+
.mdx-table-scroll > table {
|
|
320
|
+
margin-bottom: 0;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/* client/ui/Toast.tsx's attention bounce for a repeat push. Two keyframe sets
|
|
324
|
+
with identical frames: CSS only restarts an animation when its
|
|
325
|
+
animation-name changes, so a repeat push flips the wrapper between the two
|
|
326
|
+
names to replay the bounce. */
|
|
327
|
+
@keyframes toast-bump-a {
|
|
328
|
+
0% {
|
|
329
|
+
transform: none;
|
|
330
|
+
}
|
|
331
|
+
40% {
|
|
332
|
+
transform: translateY(-6px) scale(1.03);
|
|
333
|
+
}
|
|
334
|
+
100% {
|
|
335
|
+
transform: none;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
@keyframes toast-bump-b {
|
|
339
|
+
0% {
|
|
340
|
+
transform: none;
|
|
341
|
+
}
|
|
342
|
+
40% {
|
|
343
|
+
transform: translateY(-6px) scale(1.03);
|
|
344
|
+
}
|
|
345
|
+
100% {
|
|
346
|
+
transform: none;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
.toast-bump-a {
|
|
350
|
+
animation: toast-bump-a 380ms cubic-bezier(0.2, 0, 0.2, 1);
|
|
351
|
+
}
|
|
352
|
+
.toast-bump-b {
|
|
353
|
+
animation: toast-bump-b 380ms cubic-bezier(0.2, 0, 0.2, 1);
|
|
354
|
+
}
|
|
355
|
+
/* The ×N chip already says a repeat landed; the bounce is decoration. */
|
|
356
|
+
@media (prefers-reduced-motion: reduce) {
|
|
357
|
+
.toast-bump-a,
|
|
358
|
+
.toast-bump-b {
|
|
359
|
+
animation: none;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* Print: the reader gets the prose, nothing else. */
|
|
364
|
+
@media print {
|
|
365
|
+
[data-print-hide] {
|
|
366
|
+
display: none !important;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.mdx-anchor {
|
|
370
|
+
display: none;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const badgeProps = z.object({
|
|
5
|
+
tone: z
|
|
6
|
+
.enum(["neutral", "teal", "info", "ok", "warn", "danger", "note"])
|
|
7
|
+
.default("neutral")
|
|
8
|
+
.describe("Tone of the badge."),
|
|
9
|
+
color: z
|
|
10
|
+
.enum(["gray", "sky", "green", "amber", "red", "violet"])
|
|
11
|
+
.optional()
|
|
12
|
+
.describe(
|
|
13
|
+
"Deprecated alias for `tone` (gray→neutral, sky→info, green→ok, amber→warn, red→danger, violet→note). Prefer `tone`.",
|
|
14
|
+
),
|
|
15
|
+
variant: z.enum(["soft", "solid", "outline"]).default("soft").describe("Fill style."),
|
|
16
|
+
dot: z.boolean().default(false).describe("Show a leading status dot."),
|
|
17
|
+
children: z.custom<ReactNode>().describe("Badge label."),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export type BadgeProps = z.infer<typeof badgeProps>;
|
|
21
|
+
|
|
22
|
+
type Tone = NonNullable<BadgeProps["tone"]>;
|
|
23
|
+
type Variant = NonNullable<BadgeProps["variant"]>;
|
|
24
|
+
|
|
25
|
+
const COLOR_TO_TONE: Record<NonNullable<BadgeProps["color"]>, Tone> = {
|
|
26
|
+
gray: "neutral",
|
|
27
|
+
sky: "info",
|
|
28
|
+
green: "ok",
|
|
29
|
+
amber: "warn",
|
|
30
|
+
red: "danger",
|
|
31
|
+
violet: "note",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function resolveTone({ tone, color }: Pick<BadgeProps, "tone" | "color">): Tone {
|
|
35
|
+
if (tone) return tone;
|
|
36
|
+
if (color) return COLOR_TO_TONE[color];
|
|
37
|
+
return "neutral";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const TONE_STYLES: Record<Tone, Record<Variant, string>> = {
|
|
41
|
+
neutral: {
|
|
42
|
+
soft: "bg-surface-sunken text-text-muted border-border-default",
|
|
43
|
+
solid: "bg-n-600 text-n-0 border-transparent",
|
|
44
|
+
outline: "bg-transparent text-text-muted border-border-default",
|
|
45
|
+
},
|
|
46
|
+
teal: {
|
|
47
|
+
soft: "bg-surface-accent-soft text-text-accent border-teal-200",
|
|
48
|
+
solid: "bg-teal-550 text-n-0 border-transparent",
|
|
49
|
+
outline: "bg-transparent text-text-accent border-teal-300",
|
|
50
|
+
},
|
|
51
|
+
info: {
|
|
52
|
+
soft: "bg-status-info-bg text-status-info-fg border-transparent",
|
|
53
|
+
solid: "bg-status-info-fg text-text-inverse border-transparent",
|
|
54
|
+
outline: "bg-transparent text-status-info-fg border-current",
|
|
55
|
+
},
|
|
56
|
+
ok: {
|
|
57
|
+
soft: "bg-status-ok-bg text-status-ok-fg border-transparent",
|
|
58
|
+
solid: "bg-status-ok-fg text-text-inverse border-transparent",
|
|
59
|
+
outline: "bg-transparent text-status-ok-fg border-current",
|
|
60
|
+
},
|
|
61
|
+
warn: {
|
|
62
|
+
soft: "bg-status-warn-bg text-status-warn-fg border-transparent",
|
|
63
|
+
solid: "bg-status-warn-fg text-text-inverse border-transparent",
|
|
64
|
+
outline: "bg-transparent text-status-warn-fg border-current",
|
|
65
|
+
},
|
|
66
|
+
danger: {
|
|
67
|
+
soft: "bg-status-danger-bg text-status-danger-fg border-transparent",
|
|
68
|
+
solid: "bg-status-danger-fg text-text-inverse border-transparent",
|
|
69
|
+
outline: "bg-transparent text-status-danger-fg border-current",
|
|
70
|
+
},
|
|
71
|
+
note: {
|
|
72
|
+
soft: "bg-status-note-bg text-status-note-fg border-transparent",
|
|
73
|
+
solid: "bg-status-note-fg text-text-inverse border-transparent",
|
|
74
|
+
outline: "bg-transparent text-status-note-fg border-current",
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const DOT_STYLES: Record<Tone, string> = {
|
|
79
|
+
neutral: "bg-n-500",
|
|
80
|
+
teal: "bg-teal-500",
|
|
81
|
+
info: "bg-status-info-fg",
|
|
82
|
+
ok: "bg-status-ok-fg",
|
|
83
|
+
warn: "bg-status-warn-fg",
|
|
84
|
+
danger: "bg-status-danger-fg",
|
|
85
|
+
note: "bg-status-note-fg",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export default function Badge({
|
|
89
|
+
tone,
|
|
90
|
+
color,
|
|
91
|
+
variant = "soft",
|
|
92
|
+
dot = false,
|
|
93
|
+
children,
|
|
94
|
+
}: BadgeProps) {
|
|
95
|
+
const resolved = resolveTone({ tone, color });
|
|
96
|
+
return (
|
|
97
|
+
<span
|
|
98
|
+
className={`not-prose inline-flex items-center gap-1.5 h-5 rounded-sm border px-1.5 font-sans leading-normal text-[length:var(--size-xs)] font-semibold ${TONE_STYLES[resolved][variant]}`}
|
|
99
|
+
>
|
|
100
|
+
{dot ? (
|
|
101
|
+
<span
|
|
102
|
+
className={`h-1.5 w-1.5 shrink-0 rounded-full ${DOT_STYLES[resolved]}`}
|
|
103
|
+
aria-hidden="true"
|
|
104
|
+
/>
|
|
105
|
+
) : null}
|
|
106
|
+
{children}
|
|
107
|
+
</span>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { motion } from "framer-motion";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { TRANSITIONS } from "../motion";
|
|
5
|
+
import { Icon } from "../ui/Icon";
|
|
6
|
+
|
|
7
|
+
export const buttonProps = z.object({
|
|
8
|
+
variant: z
|
|
9
|
+
.enum(["primary", "secondary", "ghost", "danger"])
|
|
10
|
+
.default("primary")
|
|
11
|
+
.describe(
|
|
12
|
+
"Visual style: primary (filled teal), secondary (outlined), ghost (text-only), or danger (outlined, danger tone).",
|
|
13
|
+
),
|
|
14
|
+
size: z.enum(["sm", "md", "lg"]).default("md").describe("Height/padding/text size."),
|
|
15
|
+
icon: z.string().optional().describe("Lucide icon name rendered before the label."),
|
|
16
|
+
iconRight: z.string().optional().describe("Lucide icon name rendered after the label."),
|
|
17
|
+
href: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe("If set, renders as a link (`<a>`) instead of a `<button>`."),
|
|
21
|
+
onClick: z.custom<() => void>().optional().describe("Click handler (MDX/JSX only)."),
|
|
22
|
+
disabled: z.boolean().default(false).describe("Disables the button and dims it."),
|
|
23
|
+
autoFocus: z.boolean().optional().describe("Focus the button on mount."),
|
|
24
|
+
children: z.custom<ReactNode>().describe("Button label/content."),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// z.input, not z.infer: fields with zod defaults stay optional for TSX callers
|
|
28
|
+
// (the component destructures the same defaults itself).
|
|
29
|
+
export type ButtonProps = z.input<typeof buttonProps>;
|
|
30
|
+
|
|
31
|
+
type Variant = NonNullable<ButtonProps["variant"]>;
|
|
32
|
+
type Size = NonNullable<ButtonProps["size"]>;
|
|
33
|
+
|
|
34
|
+
const VARIANT_STYLES: Record<Variant, string> = {
|
|
35
|
+
primary: "bg-teal-550 text-n-0 border border-teal-600 shadow-xs hover:bg-teal-600",
|
|
36
|
+
secondary:
|
|
37
|
+
"bg-surface-card text-text-body border border-border-default shadow-xs hover:bg-surface-hover",
|
|
38
|
+
ghost: "bg-transparent text-text-muted border border-transparent hover:bg-surface-hover",
|
|
39
|
+
danger:
|
|
40
|
+
"bg-surface-card text-status-danger-fg border border-border-default hover:bg-status-danger-bg",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const SIZE_STYLES: Record<Size, string> = {
|
|
44
|
+
sm: "h-7 px-2.5 text-[length:var(--size-sm)]",
|
|
45
|
+
md: "h-[34px] px-3.5 text-[length:var(--size-md)]",
|
|
46
|
+
lg: "h-10 px-4.5 text-[length:var(--size-md)]",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const ICON_SIZE: Record<Size, "sm" | "md"> = { sm: "sm", md: "md", lg: "md" };
|
|
50
|
+
|
|
51
|
+
export default function Button({
|
|
52
|
+
variant = "primary",
|
|
53
|
+
size = "md",
|
|
54
|
+
icon,
|
|
55
|
+
iconRight,
|
|
56
|
+
href,
|
|
57
|
+
onClick,
|
|
58
|
+
disabled = false,
|
|
59
|
+
autoFocus,
|
|
60
|
+
children,
|
|
61
|
+
}: ButtonProps) {
|
|
62
|
+
const className = [
|
|
63
|
+
"not-prose inline-flex cursor-pointer items-center justify-center gap-2 rounded-md no-underline",
|
|
64
|
+
"font-sans leading-normal text-[length:var(--size-md)] font-semibold tracking-[var(--tracking-snug)] whitespace-nowrap",
|
|
65
|
+
"transition-colors duration-150",
|
|
66
|
+
"focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus-ring)]",
|
|
67
|
+
"disabled:cursor-not-allowed disabled:opacity-45",
|
|
68
|
+
VARIANT_STYLES[variant],
|
|
69
|
+
SIZE_STYLES[size],
|
|
70
|
+
].join(" ");
|
|
71
|
+
|
|
72
|
+
const press = disabled ? undefined : { y: 0.5, scale: 0.988 };
|
|
73
|
+
const iconSize = ICON_SIZE[size];
|
|
74
|
+
|
|
75
|
+
const content = (
|
|
76
|
+
<>
|
|
77
|
+
{icon ? <Icon name={icon} size={iconSize} /> : null}
|
|
78
|
+
{children}
|
|
79
|
+
{iconRight ? <Icon name={iconRight} size={iconSize} /> : null}
|
|
80
|
+
</>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (href) {
|
|
84
|
+
return (
|
|
85
|
+
<motion.a
|
|
86
|
+
href={disabled ? undefined : href}
|
|
87
|
+
aria-disabled={disabled || undefined}
|
|
88
|
+
whileTap={press}
|
|
89
|
+
transition={TRANSITIONS.snap}
|
|
90
|
+
onClick={onClick}
|
|
91
|
+
className={className}
|
|
92
|
+
>
|
|
93
|
+
{content}
|
|
94
|
+
</motion.a>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<motion.button
|
|
100
|
+
type="button"
|
|
101
|
+
disabled={disabled}
|
|
102
|
+
autoFocus={autoFocus}
|
|
103
|
+
whileTap={press}
|
|
104
|
+
transition={TRANSITIONS.snap}
|
|
105
|
+
onClick={onClick}
|
|
106
|
+
className={className}
|
|
107
|
+
>
|
|
108
|
+
{content}
|
|
109
|
+
</motion.button>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { motion } from "framer-motion";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { fadeRise } from "../motion";
|
|
5
|
+
import { Icon } from "../ui/Icon";
|
|
6
|
+
|
|
7
|
+
export const calloutProps = z.object({
|
|
8
|
+
tone: z
|
|
9
|
+
.enum(["note", "tip", "ok", "warn", "danger"])
|
|
10
|
+
.default("note")
|
|
11
|
+
.describe(
|
|
12
|
+
"Visual intent and icon: note (info), tip (lightbulb), ok (success), warn (caution), or danger (breaking).",
|
|
13
|
+
),
|
|
14
|
+
type: z
|
|
15
|
+
.enum(["info", "warn"])
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Deprecated alias for `tone` (info→note, warn→warn). Prefer `tone`."),
|
|
18
|
+
title: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Optional bold heading shown above the body; defaults to the tone's label."),
|
|
22
|
+
children: z.custom<ReactNode>().describe("Callout body; any MDX/Markdown content."),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export type CalloutProps = z.infer<typeof calloutProps>;
|
|
26
|
+
|
|
27
|
+
type Tone = NonNullable<CalloutProps["tone"]>;
|
|
28
|
+
|
|
29
|
+
const TONES: Record<Tone, { icon: string; bg: string; fg: string; fgVar: string; label: string }> =
|
|
30
|
+
{
|
|
31
|
+
note: {
|
|
32
|
+
icon: "info",
|
|
33
|
+
bg: "bg-status-info-bg",
|
|
34
|
+
fg: "text-status-info-fg",
|
|
35
|
+
fgVar: "var(--status-info-fg)",
|
|
36
|
+
label: "Note",
|
|
37
|
+
},
|
|
38
|
+
tip: {
|
|
39
|
+
icon: "lightbulb",
|
|
40
|
+
bg: "bg-surface-accent-soft",
|
|
41
|
+
fg: "text-text-accent",
|
|
42
|
+
fgVar: "var(--text-accent)",
|
|
43
|
+
label: "Tip",
|
|
44
|
+
},
|
|
45
|
+
ok: {
|
|
46
|
+
icon: "circle-check",
|
|
47
|
+
bg: "bg-status-ok-bg",
|
|
48
|
+
fg: "text-status-ok-fg",
|
|
49
|
+
fgVar: "var(--status-ok-fg)",
|
|
50
|
+
label: "Works",
|
|
51
|
+
},
|
|
52
|
+
warn: {
|
|
53
|
+
icon: "triangle-alert",
|
|
54
|
+
bg: "bg-status-warn-bg",
|
|
55
|
+
fg: "text-status-warn-fg",
|
|
56
|
+
fgVar: "var(--status-warn-fg)",
|
|
57
|
+
label: "Careful",
|
|
58
|
+
},
|
|
59
|
+
danger: {
|
|
60
|
+
icon: "octagon-alert",
|
|
61
|
+
bg: "bg-status-danger-bg",
|
|
62
|
+
fg: "text-status-danger-fg",
|
|
63
|
+
fgVar: "var(--status-danger-fg)",
|
|
64
|
+
label: "Breaking",
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** `type` is a deprecated alias kept so existing docs written before `tone` keep working. */
|
|
69
|
+
function resolveTone({ tone, type }: Pick<CalloutProps, "tone" | "type">): Tone {
|
|
70
|
+
if (tone) return tone;
|
|
71
|
+
if (type === "warn") return "warn";
|
|
72
|
+
if (type === "info") return "note";
|
|
73
|
+
return "note";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default function Callout({ tone, type, title, children }: CalloutProps) {
|
|
77
|
+
const resolved = resolveTone({ tone, type });
|
|
78
|
+
const style = TONES[resolved];
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<motion.div
|
|
82
|
+
variants={fadeRise}
|
|
83
|
+
initial="initial"
|
|
84
|
+
animate="enter"
|
|
85
|
+
className={`not-prose flex items-start gap-3 rounded-lg p-4 border border-transparent font-sans font-medium leading-normal text-[length:var(--size-md)] ${style.bg}`}
|
|
86
|
+
style={{ borderColor: `color-mix(in oklab, ${style.fgVar} 22%, transparent)` }}
|
|
87
|
+
>
|
|
88
|
+
<Icon name={style.icon} size="md" className={`mt-0.5 shrink-0 ${style.fg}`} />
|
|
89
|
+
<div className="min-w-0 flex-1">
|
|
90
|
+
<p className={`font-bold tracking-[var(--tracking-snug)] ${style.fg}`}>
|
|
91
|
+
{title || style.label}
|
|
92
|
+
</p>
|
|
93
|
+
<div className="mt-1 leading-6 text-text-body">{children}</div>
|
|
94
|
+
</div>
|
|
95
|
+
</motion.div>
|
|
96
|
+
);
|
|
97
|
+
}
|