@clidey/dory 1.0.5 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/bin/dist/dory.js +36 -2
  2. package/bin/dist/dory.js.map +1 -1
  3. package/index.html +0 -1
  4. package/package.json +16 -22
  5. package/src/components/hooks.tsx +2 -1
  6. package/src/components/store.ts +5 -4
  7. package/src/config/mdx.js +8 -3
  8. package/src/config/mdx.ts +8 -3
  9. package/src/embed/widget-iframe.ts +10 -3
  10. package/src/entry-server.tsx +11 -7
  11. package/src/main.tsx +5 -1
  12. package/src/mdx/accordion.tsx +11 -8
  13. package/src/mdx/api.tsx +2 -352
  14. package/src/mdx/async-api.tsx +124 -295
  15. package/src/mdx/callout.tsx +22 -24
  16. package/src/mdx/card.tsx +20 -22
  17. package/src/mdx/chip.tsx +14 -0
  18. package/src/mdx/code.tsx +133 -110
  19. package/src/mdx/expandable.tsx +8 -8
  20. package/src/mdx/fence-meta.test.ts +39 -0
  21. package/src/mdx/fence-meta.ts +61 -0
  22. package/src/mdx/fence.tsx +155 -44
  23. package/src/mdx/icon-grid.tsx +29 -0
  24. package/src/mdx/icon.tsx +2 -2
  25. package/src/mdx/latex.tsx +14 -7
  26. package/src/mdx/list.tsx +3 -3
  27. package/src/mdx/mdx.tsx +7 -1
  28. package/src/mdx/mermaid.tsx +81 -11
  29. package/src/mdx/open-api.tsx +396 -239
  30. package/src/mdx/source.tsx +148 -40
  31. package/src/mdx/specimen.tsx +69 -0
  32. package/src/mdx/step.tsx +22 -30
  33. package/src/mdx/table.tsx +4 -9
  34. package/src/mdx/tabs.tsx +39 -0
  35. package/src/mdx/tag.tsx +13 -21
  36. package/src/mdx/text.tsx +17 -9
  37. package/src/mdx/type-scale.tsx +28 -0
  38. package/src/mdx/websocket-playground.tsx +221 -704
  39. package/src/plugins/code-meta.ts +23 -0
  40. package/src/plugins/frontmatter-dev-server.ts +6 -2
  41. package/src/plugins/html-files-middleware.ts +9 -2
  42. package/src/plugins/html-metadata-injector.ts +4 -3
  43. package/src/plugins/llm-txt-dev-server.ts +5 -2
  44. package/src/plugins/llm-txt-generator.ts +2 -1
  45. package/src/plugins/prerender.ts +7 -4
  46. package/src/plugins/relative-assets.test.ts +26 -0
  47. package/src/plugins/relative-assets.ts +80 -0
  48. package/src/plugins/robots-generator.ts +6 -4
  49. package/src/plugins/sanitize.test.ts +5 -0
  50. package/src/plugins/sanitize.ts +2 -0
  51. package/src/plugins/shared.ts +17 -0
  52. package/src/plugins/sitemap-generator.ts +2 -1
  53. package/src/theme.scss +282 -3
  54. package/src/types/config.ts +3 -0
  55. package/src/ui/header.tsx +4 -3
  56. package/src/ui/layout.tsx +22 -4
  57. package/src/ui/page-meta.tsx +37 -0
  58. package/src/ui/prev-next-link.tsx +4 -3
  59. package/src/ui/table-of-content.tsx +11 -3
  60. package/src/utils/base-path.test.ts +46 -0
  61. package/src/utils/base-path.ts +41 -0
  62. package/vite.config.ssr.ts +3 -0
  63. package/vite.config.ts +3 -0
@@ -1,3 +1,8 @@
1
+ import classNames from 'classnames'
2
+ import { ExternalLink, File } from 'lucide-react'
3
+ import { useEffect, useState } from 'preact/hooks'
4
+ import { Fence } from './fence'
5
+
1
6
  type PathRange = {
2
7
  path: string;
3
8
  range?: string;
@@ -9,50 +14,153 @@ type SourceProps = {
9
14
  paths: PathRange[];
10
15
  };
11
16
 
17
+ type Host = 'github' | 'gitlab' | 'bitbucket' | 'other'
18
+
19
+ function hostOf(url: string): Host {
20
+ if (url.includes('github.com')) return 'github'
21
+ if (url.includes('gitlab.com')) return 'gitlab'
22
+ if (url.includes('bitbucket.org')) return 'bitbucket'
23
+ return 'other'
24
+ }
25
+
26
+ const hostNames: Record<Host, string> = { github: 'GitHub', gitlab: 'GitLab', bitbucket: 'Bitbucket', other: 'source' }
27
+
28
+ /** Browser URL for a file, with the host's line-range anchor when a range is given. */
29
+ export function sourceUrl(url: string, branch: string, path: string, range?: string): string {
30
+ const host = hostOf(url)
31
+ const base = host === 'github' ? `${url}/blob/${branch}/${path}`
32
+ : host === 'gitlab' ? `${url}/-/blob/${branch}/${path}`
33
+ : host === 'bitbucket' ? `${url}/src/${branch}/${path}`
34
+ : url
35
+ if (!range || host === 'other') return base
36
+ const [start, end] = range.split('-')
37
+ if (host === 'bitbucket') return `${base}#lines-${start}${end ? `:${end}` : ''}`
38
+ return `${base}#L${start}${end ? `-L${end}` : ''}`
39
+ }
40
+
41
+ /** Raw file URL used to fetch the excerpt, or null when the host has no raw endpoint. */
42
+ export function rawUrl(url: string, branch: string, path: string): string | null {
43
+ const host = hostOf(url)
44
+ const repo = url.replace(/\/+$/, '').split('/').slice(-2).join('/')
45
+ if (host === 'github') return `https://raw.githubusercontent.com/${repo}/${branch}/${path}`
46
+ if (host === 'gitlab') return `${url}/-/raw/${branch}/${path}`
47
+ if (host === 'bitbucket') return `${url}/raw/${branch}/${path}`
48
+ return null
49
+ }
50
+
51
+ const languages: Record<string, string> = {
52
+ ts: 'typescript', tsx: 'tsx', js: 'javascript', jsx: 'jsx', mjs: 'javascript', json: 'json', go: 'go', py: 'python',
53
+ rs: 'rust', java: 'java', rb: 'ruby', php: 'php', sh: 'bash', yml: 'yaml', yaml: 'yaml', toml: 'toml', md: 'markdown',
54
+ mdx: 'markdown', css: 'css', scss: 'css', html: 'html', sql: 'sql', graphql: 'graphql',
55
+ }
56
+
57
+ function languageOf(path: string): string {
58
+ const ext = path.split('.').pop()?.toLowerCase() ?? ''
59
+ return languages[ext] ?? 'markup'
60
+ }
61
+
62
+ const previewLines = 30
63
+
64
+ /** Lines for the given range, or the first 30 lines (with the total) when no range is set. */
65
+ function sliceRange(text: string, range?: string): { code: string; startLine: number; total: number } {
66
+ const lines = text.replace(/\r\n/g, '\n').replace(/\n$/, '').split('\n')
67
+ if (!range) return { code: lines.slice(0, previewLines).join('\n'), startLine: 1, total: lines.length }
68
+ const [start, end] = range.split('-').map((n) => parseInt(n, 10))
69
+ const from = Math.max(1, start || 1)
70
+ const to = end || from
71
+ return { code: lines.slice(from - 1, to).join('\n'), startLine: from, total: lines.length }
72
+ }
73
+
74
+ /**
75
+ * Links to files in a repository and previews the referenced lines inline.
76
+ * Each path is a chip; the selected chip's excerpt is fetched from the host's
77
+ * raw endpoint and shown with line numbers. When the fetch fails, the frame
78
+ * keeps the link to open the file on the host.
79
+ */
12
80
  export const Source = ({ url, branch = "main", paths }: SourceProps) => {
13
- const createSourceUrl = (path: string, range?: string) => {
14
- if (!range) {
15
- if (url.includes('github.com')) {
16
- return `${url}/blob/${branch}/${path}`;
17
- } else if (url.includes('bitbucket.org')) {
18
- return `${url}/src/${branch}/${path}`;
19
- } else if (url.includes('gitlab.com')) {
20
- return `${url}/-/blob/${branch}/${path}`;
21
- }
22
- return url;
23
- }
24
-
25
- const [start, end] = range.split('-');
26
-
27
- if (url.includes('github.com')) {
28
- return `${url}/blob/${branch}/${path}#L${start}${end ? `-L${end}` : ''}`;
29
- } else if (url.includes('bitbucket.org')) {
30
- return `${url}/src/${branch}/${path}#lines-${start}${end ? `:${end}` : ''}`;
31
- } else if (url.includes('gitlab.com')) {
32
- return `${url}/-/blob/${branch}/${path}#L${range.replace('-', '-')}`;
33
- }
34
-
35
- return url;
36
- };
81
+ const [selected, setSelected] = useState(0)
82
+ const [excerpt, setExcerpt] = useState<{ key: string; code: string; startLine: number; total: number } | null>(null)
83
+ const [failed, setFailed] = useState(false)
84
+
85
+ const host = hostOf(url)
86
+ const repo = url.replace(/\/+$/, '').split('/').slice(-2).join('/')
87
+ const current = paths[selected] ?? paths[0]
88
+ const key = `${current?.path}@${current?.range ?? ''}`
89
+
90
+ useEffect(() => {
91
+ if (!current) return
92
+ const raw = rawUrl(url, branch, current.path)
93
+ setFailed(false)
94
+ if (!raw) { setFailed(true); return }
95
+ let cancelled = false
96
+ fetch(raw)
97
+ .then((response) => (response.ok ? response.text() : Promise.reject(new Error(String(response.status)))))
98
+ .then((text) => { if (!cancelled) setExcerpt({ key, ...sliceRange(text, current.range) }) })
99
+ .catch(() => { if (!cancelled) setFailed(true) })
100
+ return () => { cancelled = true }
101
+ }, [url, branch, key])
102
+
103
+ if (!current) return null
104
+ const openLabel = `Open on ${hostNames[host]}`
105
+ const ready = excerpt?.key === key
37
106
 
38
107
  return (
39
- <div>
40
- <p className="text-sm text-foreground dark:text-white">Sources:</p>
41
- <ul className="list-none pl-0 flex flex-wrap gap-2 mt-2">
42
- {paths.map(({ path, range }) => (
43
- <li key={path + (range || '')}>
44
- <a
45
- href={createSourceUrl(path, range)}
46
- target="_blank"
47
- rel="noopener noreferrer"
48
- className="rounded px-3 py-1 font-mono text-sm text-foreground/80 hover:opacity-80 transition-colors border border-border"
49
- >
50
- {path}
51
- {range && <span className="ml-2 opacity-70">{range}</span>}
52
- </a>
53
- </li>
108
+ <div className="dory-source not-prose my-5">
109
+ <p className="mb-2.5 font-mono text-[11px] uppercase tracking-[0.16em] text-muted-foreground">Sources</p>
110
+ <div className="flex flex-wrap gap-2">
111
+ {paths.map(({ path, range }, index) => (
112
+ <button
113
+ key={path + (range || '')}
114
+ type="button"
115
+ onClick={() => setSelected(index)}
116
+ aria-pressed={index === selected}
117
+ className={classNames(
118
+ 'inline-flex items-center gap-2 rounded-md border px-3 py-1.5 font-mono text-xs transition',
119
+ index === selected
120
+ ? 'border-[var(--primary)] bg-accent text-foreground'
121
+ : 'border-[var(--border)] bg-muted text-foreground hover:border-[var(--primary)]',
122
+ )}
123
+ >
124
+ <File aria-hidden="true" className="h-3.5 w-3.5 text-muted-foreground" />
125
+ {path}
126
+ {range && <span className="text-muted-foreground">{range}</span>}
127
+ </button>
54
128
  ))}
55
- </ul>
129
+ </div>
130
+ <div className="dory-code mt-3 overflow-hidden rounded-lg border border-[var(--border)] bg-card">
131
+ <div className="flex min-h-10 items-center gap-2 border-b border-[var(--border)] bg-muted px-4">
132
+ <File aria-hidden="true" className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
133
+ <p className="mr-auto truncate font-mono text-xs text-foreground">
134
+ {repo} · {current.path}
135
+ {current.range && <span className="ml-2 text-muted-foreground">{current.range}</span>}
136
+ </p>
137
+ <a
138
+ href={sourceUrl(url, branch, current.path, current.range)}
139
+ target="_blank"
140
+ rel="noopener noreferrer"
141
+ className="inline-flex shrink-0 items-center gap-1 text-xs font-medium text-[var(--primary)] hover:underline"
142
+ >
143
+ {openLabel}
144
+ <ExternalLink aria-hidden="true" className="h-3 w-3" />
145
+ </a>
146
+ </div>
147
+ {ready ? (
148
+ <>
149
+ <div className="overflow-x-auto py-3 pl-4 pr-12 font-mono text-[12.5px] leading-[1.7]">
150
+ <Fence language={languageOf(current.path)} startLine={excerpt.startLine}>{excerpt.code}</Fence>
151
+ </div>
152
+ {!current.range && excerpt.total > previewLines && (
153
+ <p className="border-t border-[var(--border)] px-4 py-2 font-mono text-[11px] text-muted-foreground">
154
+ Showing lines 1–{previewLines} of {excerpt.total}
155
+ </p>
156
+ )}
157
+ </>
158
+ ) : (
159
+ <p className="px-4 py-6 text-center text-sm text-muted-foreground">
160
+ {failed ? `Preview unavailable. ${openLabel} to read the file.` : 'Loading excerpt…'}
161
+ </p>
162
+ )}
163
+ </div>
56
164
  </div>
57
165
  );
58
166
  };
@@ -0,0 +1,69 @@
1
+ import classNames from 'classnames'
2
+ import { Children, isValidElement } from 'preact/compat'
3
+ import { useState } from 'preact/hooks'
4
+ import { fencePropsOf } from './code'
5
+ import { Pre } from './fence'
6
+
7
+ /** Marks the props table inside a Specimen; its children render in the Props tab. */
8
+ export function SpecimenProps({ children }: { children: preact.ComponentChildren }) {
9
+ return <>{children}</>
10
+ }
11
+
12
+ type SpecimenTab = 'Preview' | 'Code' | 'Props'
13
+
14
+ function isMdxSource(child: preact.ComponentChild): boolean {
15
+ return isValidElement(child) && (child as preact.VNode).type === Pre && fencePropsOf(child as preact.JSX.Element).language === 'mdx'
16
+ }
17
+
18
+ function isPropsBlock(child: preact.ComponentChild): boolean {
19
+ return isValidElement(child) && (child as preact.VNode).type === SpecimenProps
20
+ }
21
+
22
+ /**
23
+ * Framed component example with Preview / Code / Props tabs.
24
+ * An ```mdx fence becomes the Code tab, a <SpecimenProps> block becomes the
25
+ * Props tab, and everything else is the rendered preview.
26
+ */
27
+ export function Specimen({ label, children }: { label?: string; children: preact.ComponentChildren }) {
28
+ const items = Children.toArray(children)
29
+ const code = items.filter(isMdxSource)
30
+ const props = items.filter(isPropsBlock)
31
+ const preview = items.filter((child) => !isMdxSource(child) && !isPropsBlock(child))
32
+
33
+ const tabs: SpecimenTab[] = ['Preview']
34
+ if (code.length > 0) tabs.push('Code')
35
+ if (props.length > 0) tabs.push('Props')
36
+
37
+ const [active, setActive] = useState<SpecimenTab>('Preview')
38
+ const panels: Record<SpecimenTab, preact.ComponentChildren[]> = { Preview: preview, Code: code, Props: props }
39
+
40
+ return (
41
+ <div className="dory-specimen not-prose my-5 overflow-hidden rounded-lg border border-[var(--border)] bg-card">
42
+ <div className="flex min-h-10 items-center justify-between gap-3 border-b border-[var(--border)] py-1.5 pl-4 pr-1.5">
43
+ <p className="truncate font-mono text-xs text-[var(--primary)]">{label}</p>
44
+ {tabs.length > 1 && (
45
+ <div role="tablist" className="flex shrink-0 rounded-md bg-muted p-0.5">
46
+ {tabs.map((tab) => (
47
+ <button
48
+ key={tab}
49
+ type="button"
50
+ role="tab"
51
+ aria-selected={active === tab}
52
+ onClick={() => setActive(tab)}
53
+ className={classNames('rounded-[5px] px-2.5 py-1 text-xs font-medium transition', {
54
+ 'bg-card text-foreground shadow-[0_1px_2px_rgb(14_34_64/0.08)] dark:bg-[var(--border)]': active === tab,
55
+ 'text-muted-foreground hover:text-foreground': active !== tab,
56
+ })}
57
+ >
58
+ {tab}
59
+ </button>
60
+ ))}
61
+ </div>
62
+ )}
63
+ </div>
64
+ <div role="tabpanel" className="p-5 [&>:first-child]:mt-0 [&>:last-child]:mb-0">
65
+ {panels[active]}
66
+ </div>
67
+ </div>
68
+ )
69
+ }
package/src/mdx/step.tsx CHANGED
@@ -4,38 +4,30 @@ import { Children } from "preact/compat"
4
4
  export function Steps({ children }: { children: preact.ComponentChildren }) {
5
5
  const steps = Children.toArray(children) as preact.ComponentChildren[];
6
6
  return (
7
- <nav aria-label="Progress" className="py-16">
8
- <div className="overflow-hidden">
9
- {steps.map((step, stepIdx) => {
10
- const stepElement = Children.toArray(step)[0] as preact.JSX.Element;
11
- return (
12
- <div key={stepIdx} className={classNames(
13
- {
14
- "pb-10": stepIdx !== steps.length - 1,
15
- "pb-2": stepIdx === steps.length - 1,
16
- },
17
- 'relative'
7
+ <nav aria-label="Progress" className="my-6">
8
+ {steps.map((step, stepIdx) => {
9
+ const stepElement = Children.toArray(step)[0] as preact.JSX.Element;
10
+ const isLast = stepIdx === steps.length - 1;
11
+ return (
12
+ <div key={stepIdx} className={classNames('relative flex gap-3.5', { 'pb-6': !isLast })}>
13
+ {!isLast && <div aria-hidden="true" className="absolute left-[13.5px] top-9 bottom-1 w-px bg-[var(--border)]" />}
14
+ <span className={classNames(
15
+ 'relative z-10 flex size-7 shrink-0 items-center justify-center rounded-md border text-xs font-semibold',
16
+ stepIdx === 0
17
+ ? 'border-transparent bg-[var(--primary)] text-white'
18
+ : 'border-[var(--border)] bg-muted text-foreground',
18
19
  )}>
19
- <div aria-hidden="true" className="absolute top-10 left-4 h-full w-[0.5px]" style={{ top: '40px', height: 'calc(100% - 44px)', backgroundColor: 'color-mix(in oklch, var(--brand-foreground) 30%, transparent)' }} />
20
- <div className="group relative flex items-start">
21
- <span className="flex h-9 items-center">
22
- <span className="relative z-10 flex size-8 items-center justify-center rounded-full border" style={{ borderColor: 'var(--brand-foreground)', backgroundColor: 'color-mix(in oklch, var(--brand-foreground) 12%, transparent)' }}>
23
- <span className="text-sm font-medium" style={{ color: 'var(--brand-foreground)' }}>
24
- {stepIdx + 1}
25
- </span>
26
- </span>
27
- </span>
28
- <span className="ml-4 flex min-w-0 flex-col">
29
- <p className="text-lg font-medium text-inherit">{stepElement.props.title}</p>
30
- </span>
31
- </div>
32
- <div className="ml-12 not-prose">
20
+ {stepIdx + 1}
21
+ </span>
22
+ <div className="min-w-0 flex-1 pt-0.5">
23
+ <p className="font-heading text-[15px] font-semibold tracking-tight text-foreground">{stepElement.props.title}</p>
24
+ <div className="not-prose mt-0.5 text-muted-foreground">
33
25
  {stepElement.props.children}
34
26
  </div>
35
27
  </div>
36
- )
37
- })}
38
- </div>
28
+ </div>
29
+ )
30
+ })}
39
31
  </nav>
40
32
  )
41
33
  }
@@ -47,5 +39,5 @@ export function Step({
47
39
  title?: string;
48
40
  children: preact.ComponentChildren
49
41
  }) {
50
- return <p className='text-sm'>{children}</p>
51
- }
42
+ return <p className='leading-relaxed'>{children}</p>
43
+ }
package/src/mdx/table.tsx CHANGED
@@ -1,20 +1,15 @@
1
1
  import type { JSX } from 'preact/jsx-runtime';
2
- import {
3
- Table as UXTable,
4
- TableHead,
5
- TableCell
6
- } from '@clidey/ux'
7
2
 
8
3
  export const Table = (props: JSX.IntrinsicElements['table']) => (
9
- <div className="overflow-x-auto rounded-lg my-8">
10
- <UXTable {...props} />
4
+ <div className="dory-table my-5 overflow-x-auto rounded-lg border border-[var(--border)] bg-card">
5
+ <table className="w-full border-collapse text-left" {...props} />
11
6
  </div>
12
7
  );
13
8
 
14
9
  export const Th = (props: JSX.IntrinsicElements['th']) => (
15
- <TableHead {...props} />
10
+ <th className="bg-muted px-3.5 py-2 font-mono text-[11px] font-normal uppercase tracking-[0.16em] text-muted-foreground" {...props} />
16
11
  );
17
12
 
18
13
  export const Td = (props: JSX.IntrinsicElements['td']) => (
19
- <TableCell {...props} />
14
+ <td className="border-t border-[var(--border)] px-3.5 py-2.5 align-top text-[13px] text-muted-foreground first:font-semibold first:text-foreground" {...props} />
20
15
  );
@@ -0,0 +1,39 @@
1
+ import classNames from 'classnames'
2
+ import { Children, isValidElement } from 'preact/compat'
3
+ import { useState } from 'preact/hooks'
4
+
5
+ /** One tab panel inside <Tabs>. Renders its children when active. */
6
+ export function Tab({ children }: { title: string; children: preact.ComponentChildren }) {
7
+ return <>{children}</>
8
+ }
9
+
10
+ /** Underlined tab strip for any MDX content, e.g. several diagrams of one thing. */
11
+ export function Tabs({ children }: { children: preact.ComponentChildren }) {
12
+ const panels = Children.toArray(children).filter(isValidElement) as preact.VNode<{ title: string }>[]
13
+ const [active, setActive] = useState(0)
14
+
15
+ return (
16
+ <div className="dory-tabs not-prose my-5">
17
+ <div role="tablist" className="flex gap-5 border-b border-[var(--border)]">
18
+ {panels.map((panel, index) => (
19
+ <button
20
+ key={index}
21
+ type="button"
22
+ role="tab"
23
+ aria-selected={active === index}
24
+ onClick={() => setActive(index)}
25
+ className={classNames('-mb-px border-b-2 pb-2.5 pt-1 text-sm font-medium transition', {
26
+ 'border-[var(--primary)] text-foreground': active === index,
27
+ 'border-transparent text-muted-foreground hover:text-foreground': active !== index,
28
+ })}
29
+ >
30
+ {panel.props.title}
31
+ </button>
32
+ ))}
33
+ </div>
34
+ <div role="tabpanel" className="pt-4 [&>:first-child]:mt-0 [&>:last-child]:mb-0">
35
+ {panels[active]}
36
+ </div>
37
+ </div>
38
+ )
39
+ }
package/src/mdx/tag.tsx CHANGED
@@ -1,25 +1,19 @@
1
- import { Badge } from '@clidey/ux'
2
1
  import classNames from 'classnames'
3
2
 
4
- const valueColorMap = {
5
- GET: 'default',
6
- POST: 'secondary',
7
- PUT: 'outline',
8
- DELETE: 'destructive',
9
- } as const
10
-
11
3
  const customColorClasses = {
12
- emerald: 'bg-emerald-400/10 text-emerald-500 dark:text-emerald-400 border-emerald-300 dark:border-emerald-400/30',
13
- sky: 'bg-sky-400/10 text-sky-500 dark:text-sky-400 border-sky-300 dark:border-sky-400/30',
14
- amber: 'bg-amber-400/10 text-amber-500 dark:text-amber-400 border-amber-300 dark:border-amber-400/30',
15
- rose: 'bg-rose-50 text-red-500 dark:bg-rose-400/10 dark:text-rose-400 border-rose-200 dark:border-rose-500/20',
16
- zinc: 'bg-zinc-50 text-zinc-500 dark:bg-zinc-400/10 dark:text-zinc-400 border-zinc-200 dark:border-zinc-500/20',
4
+ emerald: 'bg-[var(--success-muted)] text-foreground border-transparent',
5
+ sky: 'bg-accent text-[var(--primary)] border-transparent',
6
+ amber: 'bg-transparent text-[var(--brand-readable,#B65401)] border-current',
7
+ rose: 'bg-transparent text-red-700 dark:text-red-400 border-current',
8
+ zinc: 'bg-transparent text-muted-foreground border-border',
9
+ yellow: 'bg-[var(--highlight)] text-[var(--highlight-foreground)] border-transparent',
17
10
  }
18
11
 
19
12
  const methodColors: Record<string, keyof typeof customColorClasses> = {
20
- GET: 'emerald',
21
- POST: 'sky',
13
+ GET: 'sky',
14
+ POST: 'emerald',
22
15
  PUT: 'amber',
16
+ PATCH: 'amber',
23
17
  DELETE: 'rose',
24
18
  }
25
19
 
@@ -35,19 +29,17 @@ export function Tag({
35
29
  className?: string
36
30
  }) {
37
31
  const colorKey = color || methodColors[children] || 'zinc'
38
- const badgeVariant = valueColorMap[children as keyof typeof valueColorMap] || 'outline'
39
32
 
40
33
  return (
41
- <Badge
42
- variant={badgeVariant}
34
+ <span
43
35
  className={classNames(
44
- 'font-mono text-[0.625rem]/6 font-semibold',
36
+ 'mr-1.5 inline-flex w-fit items-center rounded-[5px] border font-mono font-semibold align-middle',
37
+ variant === 'small' ? 'px-1.5 py-0 text-[0.625rem]/5' : 'px-2 py-px text-[11px]/5',
45
38
  customColorClasses[colorKey],
46
- variant === 'small' && 'px-1 py-0',
47
39
  className,
48
40
  )}
49
41
  >
50
42
  {children}
51
- </Badge>
43
+ </span>
52
44
  )
53
45
  }
package/src/mdx/text.tsx CHANGED
@@ -1,22 +1,28 @@
1
1
  import { cn } from "@clidey/ux"
2
+ import { withBase } from '../utils/base-path';
2
3
  import classNames from "classnames"
3
4
  import { useState } from "preact/hooks"
4
5
 
5
6
 
6
7
  export const h1 = function H1(props: preact.JSX.HTMLAttributes<HTMLHeadingElement>) {
7
- return <h1 className="text-4xl font-bold mb-4 mt-12" {...props} />
8
+ return <h1 className="dory-h1 font-heading mb-3 mt-10" {...props} />
8
9
  }
9
10
 
10
- export const h2 = function H2(props: preact.JSX.HTMLAttributes<HTMLHeadingElement>) {
11
- return <h2 className="text-3xl font-bold mb-3 mt-10" {...props} />
11
+ export const h2 = function H2({ children, ...props }: preact.JSX.HTMLAttributes<HTMLHeadingElement>) {
12
+ return (
13
+ <h2 className="dory-h2 font-heading mb-2 mt-10" {...props}>
14
+ <span>{children}</span>
15
+ {props.id && <a href={`#${props.id}`} className="dory-anchor" aria-label="Link to this section">#</a>}
16
+ </h2>
17
+ )
12
18
  }
13
19
 
14
20
  export const h3 = function H3(props: preact.JSX.HTMLAttributes<HTMLHeadingElement>) {
15
- return <h3 className="text-2xl font-bold mb-2 mt-8" {...props} />
21
+ return <h3 className="dory-h3 font-heading mb-2 mt-7" {...props} />
16
22
  }
17
23
 
18
24
  export const h4 = function H4(props: preact.JSX.HTMLAttributes<HTMLHeadingElement>) {
19
- return <h4 className="text-xl font-bold mb-2 mt-6" {...props} />
25
+ return <h4 className="dory-h4 font-heading mb-1.5 mt-5" {...props} />
20
26
  }
21
27
 
22
28
  export const h5 = function H5(props: preact.JSX.HTMLAttributes<HTMLHeadingElement>) {
@@ -68,27 +74,29 @@ export function Col({
68
74
 
69
75
 
70
76
  export function Anchor({ children, href }: { children: preact.ComponentChildren, href: string }) {
71
- return <a href={href} className="text-blue-500 hover:text-blue-600">{children}</a>
77
+ return <a href={withBase(href)} className="text-[var(--primary)] underline decoration-[color-mix(in_oklch,var(--primary)_40%,transparent)] underline-offset-4 hover:decoration-[var(--primary)]">{children}</a>
72
78
  }
73
79
 
74
80
  export function Image({ src, alt }: { src: string, alt: string }) {
75
81
  const [isExpanded, setIsExpanded] = useState(false);
82
+ const resolvedSrc = withBase(src);
76
83
 
77
84
  return (
78
85
  <>
79
86
  <img
80
- src={src}
87
+ src={resolvedSrc}
81
88
  alt={alt}
82
- className="w-full h-auto cursor-pointer"
89
+ className="w-full h-auto cursor-zoom-in rounded-lg border border-[var(--border)]"
83
90
  onClick={() => setIsExpanded(true)}
84
91
  />
92
+ {alt && <span className="mt-1.5 block text-xs text-muted-foreground">{alt}</span>}
85
93
  {isExpanded && (
86
94
  <div
87
95
  className="fixed inset-0 bg-black bg-opacity-90 z-50 flex items-center justify-center"
88
96
  onClick={() => setIsExpanded(false)}
89
97
  >
90
98
  <img
91
- src={src}
99
+ src={resolvedSrc}
92
100
  alt={alt}
93
101
  className="max-h-[90vh] max-w-[90vw] object-contain"
94
102
  />
@@ -0,0 +1,28 @@
1
+ // Mirrors the heading scale in theme.scss (.dory-h1 to .dory-h4). Keep both in sync.
2
+ const scale = [
3
+ { level: 'H1', size: 26, tracking: -0.025, weight: 700 },
4
+ { level: 'H2', size: 21, tracking: -0.02, weight: 700 },
5
+ { level: 'H3', size: 17, tracking: -0.01, weight: 700 },
6
+ { level: 'H4', size: 15, tracking: -0.005, weight: 600 },
7
+ ]
8
+
9
+ /** Shows each heading level with its size and letter-spacing, without emitting real headings. */
10
+ export function TypeScale() {
11
+ return (
12
+ <div className="not-prose my-2">
13
+ {scale.map(({ level, size, tracking, weight }) => (
14
+ <div key={level} className="flex items-baseline gap-6 border-b border-[var(--border)] py-4 last:border-b-0">
15
+ <span className="w-36 shrink-0 font-mono text-[11px] tracking-[0.12em] text-muted-foreground">
16
+ {level} · {size} / {tracking}
17
+ </span>
18
+ <span
19
+ className="font-heading leading-tight text-foreground"
20
+ style={{ fontSize: `${size}px`, letterSpacing: `${tracking}em`, fontWeight: weight }}
21
+ >
22
+ Heading {level.slice(1)}
23
+ </span>
24
+ </div>
25
+ ))}
26
+ </div>
27
+ )
28
+ }