@uniweb/kit 0.9.35 → 0.9.36
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/package.json +3 -3
- package/src/components/Asset/Asset.jsx +1 -0
- package/src/components/FileLogo/FileLogo.jsx +1 -0
- package/src/components/MediaIcon/MediaIcon.jsx +1 -0
- package/src/hooks/index.js +0 -1
- package/src/hooks/useScrolled.js +1 -1
- package/src/index.js +0 -4
- package/src/styled/Asset/Asset.jsx +5 -4
- package/src/styled/Disclaimer/Disclaimer.jsx +10 -9
- package/src/styled/Media/Media.jsx +4 -0
- package/src/styled/Section/Render.jsx +9 -9
- package/src/styled/Section/Section.jsx +1 -1
- package/src/styled/Section/renderers/Alert.jsx +10 -10
- package/src/styled/Section/renderers/Code.jsx +93 -13
- package/src/styled/Section/renderers/Details.jsx +5 -5
- package/src/styled/Section/renderers/Divider.jsx +4 -4
- package/src/styled/Section/renderers/Table.jsx +5 -5
- package/src/hooks/useTheme.js +0 -205
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/kit",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.36",
|
|
4
4
|
"description": "Standard component library for Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"fuse.js": "^7.0.0",
|
|
41
41
|
"shiki": "^3.0.0",
|
|
42
42
|
"tailwind-merge": "^3.6.0",
|
|
43
|
-
"@uniweb/
|
|
44
|
-
"@uniweb/
|
|
43
|
+
"@uniweb/core": "0.7.28",
|
|
44
|
+
"@uniweb/scene": "0.1.2"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"react": "^19.0.0",
|
|
@@ -26,6 +26,7 @@ import { useWebsite } from '../../hooks/useWebsite.js'
|
|
|
26
26
|
* @param {React.ReactNode} [props.children] - Custom content (overrides default filename display)
|
|
27
27
|
*
|
|
28
28
|
* @example
|
|
29
|
+
* kit-palette-ok: a caller-supplied class in an example, not kit painting
|
|
29
30
|
* <Asset value="document.pdf" className="text-blue-600 hover:underline" />
|
|
30
31
|
*
|
|
31
32
|
* @example
|
|
@@ -124,6 +124,7 @@ function getExtension(filename) {
|
|
|
124
124
|
* <FileLogo filename="report.pdf" size="32" />
|
|
125
125
|
*
|
|
126
126
|
* @example
|
|
127
|
+
* kit-palette-ok: a caller-supplied class in an example, not kit painting
|
|
127
128
|
* <FileLogo filename="data.xlsx" className="text-green-600" />
|
|
128
129
|
*/
|
|
129
130
|
export function FileLogo({ filename, size = '24', className, ...props }) {
|
|
@@ -67,6 +67,7 @@ const SOCIAL_ICONS = {
|
|
|
67
67
|
* @param {string} [props.className] - Additional CSS classes
|
|
68
68
|
*
|
|
69
69
|
* @example
|
|
70
|
+
* kit-palette-ok: a caller-supplied class in an example, not kit painting
|
|
70
71
|
* <MediaIcon type="twitter" size="32" className="text-blue-400" />
|
|
71
72
|
*
|
|
72
73
|
* @example
|
package/src/hooks/index.js
CHANGED
|
@@ -11,7 +11,6 @@ export { useMobileMenu } from './useMobileMenu.js'
|
|
|
11
11
|
export { useAccordion } from './useAccordion.js'
|
|
12
12
|
export { useHeadings } from './useHeadings.js'
|
|
13
13
|
export { useGridLayout, getGridClasses } from './useGridLayout.js'
|
|
14
|
-
export { useTheme, getThemeClasses, THEMES, THEME_NAMES } from './useTheme.js'
|
|
15
14
|
export { useInView, useIsInView } from './useInView.js'
|
|
16
15
|
export { usePageState } from './usePageState.js'
|
|
17
16
|
export { useWebsiteState } from './useWebsiteState.js'
|
package/src/hooks/useScrolled.js
CHANGED
package/src/index.js
CHANGED
|
@@ -108,7 +108,7 @@ export const Asset = forwardRef(function Asset(
|
|
|
108
108
|
return (
|
|
109
109
|
<div
|
|
110
110
|
className={cn(
|
|
111
|
-
'relative inline-block rounded-lg overflow-hidden border border-
|
|
111
|
+
'relative inline-block rounded-lg overflow-hidden border border-border',
|
|
112
112
|
'transition-shadow hover:shadow-md',
|
|
113
113
|
className
|
|
114
114
|
)}
|
|
@@ -117,7 +117,7 @@ export const Asset = forwardRef(function Asset(
|
|
|
117
117
|
{...props}
|
|
118
118
|
>
|
|
119
119
|
{/* Preview */}
|
|
120
|
-
<div className="w-32 h-32 flex items-center justify-center bg-
|
|
120
|
+
<div className="w-32 h-32 flex items-center justify-center bg-muted">
|
|
121
121
|
{isImage && !imageError ? (
|
|
122
122
|
<Image
|
|
123
123
|
src={src}
|
|
@@ -126,12 +126,12 @@ export const Asset = forwardRef(function Asset(
|
|
|
126
126
|
onError={handleImageError}
|
|
127
127
|
/>
|
|
128
128
|
) : (
|
|
129
|
-
<FileLogo filename={filename} size="48" className="text-
|
|
129
|
+
<FileLogo filename={filename} size="48" className="text-subtle" />
|
|
130
130
|
)}
|
|
131
131
|
</div>
|
|
132
132
|
|
|
133
133
|
{/* Filename */}
|
|
134
|
-
<div className="px-2 py-1 text-xs text-
|
|
134
|
+
<div className="px-2 py-1 text-xs text-body truncate max-w-[128px]" title={filename}>
|
|
135
135
|
{filename}
|
|
136
136
|
</div>
|
|
137
137
|
|
|
@@ -141,6 +141,7 @@ export const Asset = forwardRef(function Asset(
|
|
|
141
141
|
onClick={handleDownload}
|
|
142
142
|
className={cn(
|
|
143
143
|
'absolute inset-0 flex items-center justify-center',
|
|
144
|
+
// kit-palette-ok: a scrim over a thumbnail, not a themed surface
|
|
144
145
|
'bg-black/50 text-white transition-opacity',
|
|
145
146
|
isHovered ? 'opacity-100' : 'opacity-0'
|
|
146
147
|
)}
|
|
@@ -38,6 +38,7 @@ function DisclaimerModal({ isOpen, onClose, title, content, className }) {
|
|
|
38
38
|
<div className="fixed inset-0 z-50 overflow-y-auto">
|
|
39
39
|
{/* Backdrop */}
|
|
40
40
|
<div
|
|
41
|
+
// kit-palette-ok: a modal scrim is the same black in either scheme
|
|
41
42
|
className="fixed inset-0 bg-black/50 transition-opacity"
|
|
42
43
|
onClick={onClose}
|
|
43
44
|
aria-hidden="true"
|
|
@@ -48,7 +49,7 @@ function DisclaimerModal({ isOpen, onClose, title, content, className }) {
|
|
|
48
49
|
<div
|
|
49
50
|
className={cn(
|
|
50
51
|
'relative w-full max-w-lg transform overflow-hidden rounded-lg',
|
|
51
|
-
'bg-
|
|
52
|
+
'bg-card shadow-xl transition-all',
|
|
52
53
|
className
|
|
53
54
|
)}
|
|
54
55
|
role="dialog"
|
|
@@ -56,13 +57,13 @@ function DisclaimerModal({ isOpen, onClose, title, content, className }) {
|
|
|
56
57
|
aria-labelledby="disclaimer-title"
|
|
57
58
|
>
|
|
58
59
|
{/* Header */}
|
|
59
|
-
<div className="flex items-center justify-between border-b border-
|
|
60
|
-
<h3 id="disclaimer-title" className="text-lg font-medium text-
|
|
60
|
+
<div className="flex items-center justify-between border-b border-border px-4 py-3">
|
|
61
|
+
<h3 id="disclaimer-title" className="text-lg font-medium text-heading">
|
|
61
62
|
{title}
|
|
62
63
|
</h3>
|
|
63
64
|
<button
|
|
64
65
|
onClick={onClose}
|
|
65
|
-
className="rounded-md p-1 text-
|
|
66
|
+
className="rounded-md p-1 text-subtle hover:text-heading focus:outline-none focus:ring-2 focus:ring-ring"
|
|
66
67
|
aria-label="Close"
|
|
67
68
|
>
|
|
68
69
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
@@ -81,10 +82,10 @@ function DisclaimerModal({ isOpen, onClose, title, content, className }) {
|
|
|
81
82
|
</div>
|
|
82
83
|
|
|
83
84
|
{/* Footer */}
|
|
84
|
-
<div className="border-t border-
|
|
85
|
+
<div className="border-t border-border px-4 py-3 flex justify-end">
|
|
85
86
|
<button
|
|
86
87
|
onClick={onClose}
|
|
87
|
-
className="px-4 py-2 text-sm font-medium text-
|
|
88
|
+
className="px-4 py-2 text-sm font-medium text-primary-foreground bg-primary rounded-md hover:bg-primary-hover focus:outline-none focus:ring-2 focus:ring-ring"
|
|
88
89
|
>
|
|
89
90
|
Close
|
|
90
91
|
</button>
|
|
@@ -161,8 +162,8 @@ export function Disclaimer({
|
|
|
161
162
|
onClick={handleOpen}
|
|
162
163
|
className={cn(
|
|
163
164
|
'inline-flex items-center px-3 py-1.5 text-sm font-medium',
|
|
164
|
-
'text-
|
|
165
|
-
'border border-
|
|
165
|
+
'text-primary hover:text-primary-hover',
|
|
166
|
+
'border border-primary rounded-md hover:bg-primary/10',
|
|
166
167
|
className
|
|
167
168
|
)}
|
|
168
169
|
{...props}
|
|
@@ -173,7 +174,7 @@ export function Disclaimer({
|
|
|
173
174
|
<button
|
|
174
175
|
onClick={handleOpen}
|
|
175
176
|
className={cn(
|
|
176
|
-
'text-
|
|
177
|
+
'text-primary hover:text-primary-hover underline text-sm',
|
|
177
178
|
className
|
|
178
179
|
)}
|
|
179
180
|
{...props}
|
|
@@ -152,12 +152,15 @@ function PlayButton({ onClick, className }) {
|
|
|
152
152
|
onClick={onClick}
|
|
153
153
|
className={cn(
|
|
154
154
|
'absolute inset-0 flex items-center justify-center',
|
|
155
|
+
// kit-palette-ok: a play-button scrim, the same in any theme
|
|
155
156
|
'bg-black/30 hover:bg-black/40 transition-colors',
|
|
156
157
|
'group cursor-pointer',
|
|
157
158
|
className
|
|
158
159
|
)}
|
|
159
160
|
aria-label="Play video"
|
|
160
161
|
>
|
|
162
|
+
{/* kit-palette-ok: a play control is a white disc with a dark glyph in
|
|
163
|
+
every player ever built — not a themed surface */}
|
|
161
164
|
<div className="w-16 h-16 rounded-full bg-white/90 group-hover:bg-white flex items-center justify-center transition-colors">
|
|
162
165
|
<svg className="w-8 h-8 text-gray-900 ml-1" fill="currentColor" viewBox="0 0 24 24">
|
|
163
166
|
<path d="M8 5v14l11-7z" />
|
|
@@ -310,6 +313,7 @@ export function Media({
|
|
|
310
313
|
|
|
311
314
|
return (
|
|
312
315
|
<div
|
|
316
|
+
// kit-palette-ok: letterboxing behind a video is black, in any theme
|
|
313
317
|
className={cn('relative overflow-hidden bg-black', className)}
|
|
314
318
|
style={{ aspectRatio }}
|
|
315
319
|
{...props}
|
|
@@ -123,16 +123,16 @@ function RenderNode({ node, block, ...props }) {
|
|
|
123
123
|
className="w-full"
|
|
124
124
|
/>
|
|
125
125
|
) : (
|
|
126
|
-
<div className="flex items-center gap-3 p-4 bg-
|
|
127
|
-
<Icon name="download" size="24" className="text-
|
|
128
|
-
<span className="text-
|
|
126
|
+
<div className="flex items-center gap-3 p-4 bg-muted">
|
|
127
|
+
<Icon name="download" size="24" className="text-subtle" />
|
|
128
|
+
<span className="text-link group-hover:underline">
|
|
129
129
|
{filename}
|
|
130
130
|
</span>
|
|
131
131
|
</div>
|
|
132
132
|
)}
|
|
133
133
|
</Link>
|
|
134
134
|
{caption && (
|
|
135
|
-
<figcaption className="mt-2 text-sm text-
|
|
135
|
+
<figcaption className="mt-2 text-sm text-subtle text-center">
|
|
136
136
|
{caption}
|
|
137
137
|
</figcaption>
|
|
138
138
|
)}
|
|
@@ -164,7 +164,7 @@ function RenderNode({ node, block, ...props }) {
|
|
|
164
164
|
<figure className="my-4">
|
|
165
165
|
<Image src={src} alt={alt} className="rounded-lg" />
|
|
166
166
|
{caption && (
|
|
167
|
-
<figcaption className="mt-2 text-sm text-
|
|
167
|
+
<figcaption className="mt-2 text-sm text-subtle text-center">
|
|
168
168
|
{caption}
|
|
169
169
|
</figcaption>
|
|
170
170
|
)}
|
|
@@ -192,7 +192,7 @@ function RenderNode({ node, block, ...props }) {
|
|
|
192
192
|
|
|
193
193
|
case 'blockquote': {
|
|
194
194
|
return (
|
|
195
|
-
<blockquote className="border-l-4 border-
|
|
195
|
+
<blockquote className="border-l-4 border-border pl-4 italic text-subtle my-4">
|
|
196
196
|
{content?.map((child, i) => (
|
|
197
197
|
<RenderNode key={i} node={child} block={block} />
|
|
198
198
|
))}
|
|
@@ -247,7 +247,7 @@ function RenderNode({ node, block, ...props }) {
|
|
|
247
247
|
return (
|
|
248
248
|
<Link
|
|
249
249
|
to={href}
|
|
250
|
-
className="inline-block px-4 py-2 bg-
|
|
250
|
+
className="inline-block px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary-hover transition-colors my-2"
|
|
251
251
|
>
|
|
252
252
|
{label}
|
|
253
253
|
</Link>
|
|
@@ -270,10 +270,10 @@ function RenderNode({ node, block, ...props }) {
|
|
|
270
270
|
text = `<em>${text}</em>`
|
|
271
271
|
break
|
|
272
272
|
case 'code':
|
|
273
|
-
text = `<code class="px-1 py-0.5 bg-
|
|
273
|
+
text = `<code class="px-1 py-0.5 bg-muted rounded text-sm">${text}</code>`
|
|
274
274
|
break
|
|
275
275
|
case 'link':
|
|
276
|
-
text = `<a href="${mark.attrs?.href || '#'}" class="text-
|
|
276
|
+
text = `<a href="${mark.attrs?.href || '#'}" class="text-link hover:underline">${text}</a>`
|
|
277
277
|
break
|
|
278
278
|
}
|
|
279
279
|
})
|
|
@@ -62,7 +62,7 @@ const PADDING_CLASSES = {
|
|
|
62
62
|
* <Section content={blockContent} width="lg" padding="md" />
|
|
63
63
|
*
|
|
64
64
|
* @example
|
|
65
|
-
* <Section width="xl" columns="2" className="bg-
|
|
65
|
+
* <Section width="xl" columns="2" className="bg-muted">
|
|
66
66
|
* <div>Column 1</div>
|
|
67
67
|
* <div>Column 2</div>
|
|
68
68
|
* </Section>
|
|
@@ -15,39 +15,39 @@ import { SafeHtml } from '../../../components/SafeHtml/index.js'
|
|
|
15
15
|
*/
|
|
16
16
|
const ALERT_STYLES = {
|
|
17
17
|
info: {
|
|
18
|
-
container: 'bg-
|
|
18
|
+
container: 'bg-info-subtle border-info text-info',
|
|
19
19
|
icon: (
|
|
20
20
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
21
21
|
),
|
|
22
|
-
iconColor: 'text-
|
|
22
|
+
iconColor: 'text-info'
|
|
23
23
|
},
|
|
24
24
|
success: {
|
|
25
|
-
container: 'bg-
|
|
25
|
+
container: 'bg-success-subtle border-success text-success',
|
|
26
26
|
icon: (
|
|
27
27
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
28
28
|
),
|
|
29
|
-
iconColor: 'text-
|
|
29
|
+
iconColor: 'text-success'
|
|
30
30
|
},
|
|
31
31
|
warning: {
|
|
32
|
-
container: 'bg-
|
|
32
|
+
container: 'bg-warning-subtle border-warning text-warning',
|
|
33
33
|
icon: (
|
|
34
34
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
35
35
|
),
|
|
36
|
-
iconColor: 'text-
|
|
36
|
+
iconColor: 'text-warning'
|
|
37
37
|
},
|
|
38
38
|
danger: {
|
|
39
|
-
container: 'bg-
|
|
39
|
+
container: 'bg-error-subtle border-error text-error',
|
|
40
40
|
icon: (
|
|
41
41
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
42
42
|
),
|
|
43
|
-
iconColor: 'text-
|
|
43
|
+
iconColor: 'text-error'
|
|
44
44
|
},
|
|
45
45
|
error: {
|
|
46
|
-
container: 'bg-
|
|
46
|
+
container: 'bg-error-subtle border-error text-error',
|
|
47
47
|
icon: (
|
|
48
48
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
49
49
|
),
|
|
50
|
-
iconColor: 'text-
|
|
50
|
+
iconColor: 'text-error'
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -17,7 +17,7 @@ import { getUniweb } from '@uniweb/core'
|
|
|
17
17
|
let cssInjected = false
|
|
18
18
|
let shikiInstance = null
|
|
19
19
|
let shikiLoadPromise = null
|
|
20
|
-
|
|
20
|
+
const loadedDerived = new Set()
|
|
21
21
|
|
|
22
22
|
// What a site gets when it declares no `code:` block of its own, and the base
|
|
23
23
|
// every declaration layers over.
|
|
@@ -121,6 +121,24 @@ function injectCodeLayoutCSS() {
|
|
|
121
121
|
display: block;
|
|
122
122
|
font-family: var(--font-code, ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace);
|
|
123
123
|
}
|
|
124
|
+
|
|
125
|
+
/* A site that named a light/dark pair gets CSS variables per token instead of
|
|
126
|
+
inline colour, and these are what read them. Scoped to .shiki-themes, the
|
|
127
|
+
class Shiki adds only in that mode, so a single-theme listing is untouched. */
|
|
128
|
+
.shiki-themes {
|
|
129
|
+
background-color: var(--shiki-light-bg);
|
|
130
|
+
}
|
|
131
|
+
.shiki-themes,
|
|
132
|
+
.shiki-themes span {
|
|
133
|
+
color: var(--shiki-light);
|
|
134
|
+
}
|
|
135
|
+
.scheme-dark .shiki-themes {
|
|
136
|
+
background-color: var(--shiki-dark-bg);
|
|
137
|
+
}
|
|
138
|
+
.scheme-dark .shiki-themes,
|
|
139
|
+
.scheme-dark .shiki-themes span {
|
|
140
|
+
color: var(--shiki-dark);
|
|
141
|
+
}
|
|
124
142
|
`
|
|
125
143
|
document.head.appendChild(style)
|
|
126
144
|
cssInjected = true
|
|
@@ -170,20 +188,82 @@ async function loadShiki() {
|
|
|
170
188
|
}
|
|
171
189
|
|
|
172
190
|
/**
|
|
173
|
-
*
|
|
174
|
-
*
|
|
191
|
+
* Normalize `theme.yml`'s `code:` into the three forms a site can write.
|
|
192
|
+
*
|
|
193
|
+
* code: dracula → one bundled theme
|
|
194
|
+
* code: { light: …, dark: … } → a pair, following the visitor's scheme
|
|
195
|
+
* code: { theme?: …, <colour>: … } → a base theme with colours overridden
|
|
196
|
+
*
|
|
197
|
+
* The third is the escape hatch. Naming a theme is what most sites want — 65
|
|
198
|
+
* ship inside Shiki — and hand-picking a dozen syntax colours is a job few
|
|
199
|
+
* people can do well. Overrides exist because "that theme, on my background"
|
|
200
|
+
* is a reasonable and common ask.
|
|
201
|
+
*
|
|
202
|
+
* @param {string|Object} [code] - the `code:` declaration
|
|
203
|
+
* @returns {{ light: Side, dark?: Side }} where Side is { base, overrides }
|
|
204
|
+
*/
|
|
205
|
+
export function parseCodeConfig(code) {
|
|
206
|
+
const side = (value) => {
|
|
207
|
+
if (typeof value === 'string') return { base: value, overrides: {} }
|
|
208
|
+
const { theme, ...overrides } = value || {}
|
|
209
|
+
return { base: theme || DEFAULT_THEME, overrides }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!code) return { light: { base: DEFAULT_THEME, overrides: {} } }
|
|
213
|
+
if (typeof code === 'string') return { light: side(code) }
|
|
214
|
+
|
|
215
|
+
if (code.light || code.dark) {
|
|
216
|
+
return {
|
|
217
|
+
light: side(code.light ?? code.dark),
|
|
218
|
+
dark: side(code.dark ?? code.light),
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { light: side(code) }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Make sure a side's theme is registered, and answer the name to highlight by.
|
|
227
|
+
* A side with no overrides is just the bundled theme; one with overrides gets a
|
|
228
|
+
* derived theme built from it.
|
|
229
|
+
*/
|
|
230
|
+
async function registerSide(highlighter, side, name) {
|
|
231
|
+
if (!highlighter.getLoadedThemes().includes(side.base)) {
|
|
232
|
+
await highlighter.loadTheme(side.base)
|
|
233
|
+
}
|
|
234
|
+
if (Object.keys(side.overrides).length === 0) return side.base
|
|
235
|
+
|
|
236
|
+
if (!loadedDerived.has(name)) {
|
|
237
|
+
await highlighter.loadTheme({
|
|
238
|
+
...buildCodeTheme(highlighter.getTheme(side.base), side.overrides),
|
|
239
|
+
name,
|
|
240
|
+
})
|
|
241
|
+
loadedDerived.add(name)
|
|
242
|
+
}
|
|
243
|
+
return name
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Resolve the `code:` declaration into codeToHtml options.
|
|
248
|
+
*
|
|
249
|
+
* One theme highlights the same in both schemes — right for a site whose code
|
|
250
|
+
* blocks are a terminal. A pair makes Shiki emit CSS variables per token
|
|
251
|
+
* (`defaultColor: false`) instead of inline colour, so one rendered block can
|
|
252
|
+
* follow the visitor's choice; injectCodeLayoutCSS supplies the rules that read
|
|
253
|
+
* them.
|
|
175
254
|
*/
|
|
176
|
-
async function
|
|
177
|
-
|
|
178
|
-
if (siteThemeLoaded) return SITE_CODE_THEME
|
|
255
|
+
async function resolveThemeOptions(highlighter, code) {
|
|
256
|
+
const config = parseCodeConfig(code)
|
|
179
257
|
|
|
180
258
|
try {
|
|
181
|
-
await
|
|
182
|
-
|
|
183
|
-
|
|
259
|
+
const light = await registerSide(highlighter, config.light, `${SITE_CODE_THEME}-light`)
|
|
260
|
+
if (!config.dark) return { theme: light }
|
|
261
|
+
|
|
262
|
+
const dark = await registerSide(highlighter, config.dark, `${SITE_CODE_THEME}-dark`)
|
|
263
|
+
return { themes: { light, dark }, defaultColor: false }
|
|
184
264
|
} catch (error) {
|
|
185
265
|
console.warn('[Code] Could not apply theme.code, using the default:', error)
|
|
186
|
-
return DEFAULT_THEME
|
|
266
|
+
return { theme: DEFAULT_THEME }
|
|
187
267
|
}
|
|
188
268
|
}
|
|
189
269
|
|
|
@@ -194,7 +274,7 @@ async function highlightCode(code, language, highlighter, codeTheme) {
|
|
|
194
274
|
if (!highlighter) return null
|
|
195
275
|
|
|
196
276
|
try {
|
|
197
|
-
const
|
|
277
|
+
const themeOptions = await resolveThemeOptions(highlighter, codeTheme)
|
|
198
278
|
|
|
199
279
|
// Load language if not already loaded
|
|
200
280
|
const loadedLangs = highlighter.getLoadedLanguages()
|
|
@@ -205,13 +285,13 @@ async function highlightCode(code, language, highlighter, codeTheme) {
|
|
|
205
285
|
await highlighter.loadLanguage(lang)
|
|
206
286
|
} catch {
|
|
207
287
|
// Language not available, fall back to plaintext
|
|
208
|
-
return highlighter.codeToHtml(code, { lang: 'plaintext',
|
|
288
|
+
return highlighter.codeToHtml(code, { lang: 'plaintext', ...themeOptions })
|
|
209
289
|
}
|
|
210
290
|
}
|
|
211
291
|
|
|
212
292
|
return highlighter.codeToHtml(code, {
|
|
213
293
|
lang: lang === 'plaintext' ? 'text' : lang,
|
|
214
|
-
|
|
294
|
+
...themeOptions,
|
|
215
295
|
})
|
|
216
296
|
} catch (error) {
|
|
217
297
|
console.warn('[Code] Highlighting failed:', error)
|
|
@@ -31,7 +31,7 @@ export function Details({
|
|
|
31
31
|
return (
|
|
32
32
|
<div
|
|
33
33
|
className={cn(
|
|
34
|
-
'border border-
|
|
34
|
+
'border border-border rounded-lg overflow-hidden',
|
|
35
35
|
className
|
|
36
36
|
)}
|
|
37
37
|
{...props}
|
|
@@ -40,8 +40,8 @@ export function Details({
|
|
|
40
40
|
<button
|
|
41
41
|
className={cn(
|
|
42
42
|
'w-full flex items-center justify-between px-4 py-3',
|
|
43
|
-
'text-left font-medium text-
|
|
44
|
-
'hover:bg-
|
|
43
|
+
'text-left font-medium text-heading bg-muted',
|
|
44
|
+
'hover:bg-card transition-colors'
|
|
45
45
|
)}
|
|
46
46
|
onClick={() => setIsOpen(!isOpen)}
|
|
47
47
|
aria-expanded={isOpen}
|
|
@@ -49,7 +49,7 @@ export function Details({
|
|
|
49
49
|
<span>{summary}</span>
|
|
50
50
|
<svg
|
|
51
51
|
className={cn(
|
|
52
|
-
'w-5 h-5 text-
|
|
52
|
+
'w-5 h-5 text-subtle transition-transform',
|
|
53
53
|
isOpen && 'rotate-180'
|
|
54
54
|
)}
|
|
55
55
|
fill="none"
|
|
@@ -62,7 +62,7 @@ export function Details({
|
|
|
62
62
|
|
|
63
63
|
{/* Content */}
|
|
64
64
|
{isOpen && (
|
|
65
|
-
<div className="px-4 py-3 border-t border-
|
|
65
|
+
<div className="px-4 py-3 border-t border-border">
|
|
66
66
|
{typeof content === 'string' ? (
|
|
67
67
|
<SafeHtml value={content} className="prose prose-sm" />
|
|
68
68
|
) : (
|
|
@@ -24,16 +24,16 @@ export function Divider({ type = 'hr', className, ...props }) {
|
|
|
24
24
|
role="separator"
|
|
25
25
|
{...props}
|
|
26
26
|
>
|
|
27
|
-
<span className="w-2 h-2 bg-
|
|
28
|
-
<span className="w-2 h-2 bg-
|
|
29
|
-
<span className="w-2 h-2 bg-
|
|
27
|
+
<span className="w-2 h-2 bg-border rounded-full" />
|
|
28
|
+
<span className="w-2 h-2 bg-border rounded-full" />
|
|
29
|
+
<span className="w-2 h-2 bg-border rounded-full" />
|
|
30
30
|
</div>
|
|
31
31
|
)
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
return (
|
|
35
35
|
<hr
|
|
36
|
-
className={cn('border-
|
|
36
|
+
className={cn('border-border my-6', className)}
|
|
37
37
|
{...props}
|
|
38
38
|
/>
|
|
39
39
|
)
|
|
@@ -22,10 +22,10 @@ export function Table({ content, className, ...props }) {
|
|
|
22
22
|
|
|
23
23
|
return (
|
|
24
24
|
<div className={cn('overflow-x-auto', className)} {...props}>
|
|
25
|
-
<table className="min-w-full divide-y divide-
|
|
26
|
-
<tbody className="divide-y divide-
|
|
25
|
+
<table className="min-w-full divide-y divide-border border border-border rounded-lg">
|
|
26
|
+
<tbody className="divide-y divide-border bg-section">
|
|
27
27
|
{content.map((row, rowIndex) => (
|
|
28
|
-
<tr key={rowIndex} className={rowIndex % 2 === 0 ? 'bg-
|
|
28
|
+
<tr key={rowIndex} className={rowIndex % 2 === 0 ? 'bg-section' : 'bg-muted'}>
|
|
29
29
|
{row.content?.map((cell, cellIndex) => {
|
|
30
30
|
const CellTag = cell.type === 'tableHeader' ? 'th' : 'td'
|
|
31
31
|
const cellContent = cell.content?.[0]?.content?.[0]?.text || ''
|
|
@@ -36,8 +36,8 @@ export function Table({ content, className, ...props }) {
|
|
|
36
36
|
className={cn(
|
|
37
37
|
'px-4 py-2 text-sm',
|
|
38
38
|
cell.type === 'tableHeader'
|
|
39
|
-
? 'font-medium text-
|
|
40
|
-
: 'text-
|
|
39
|
+
? 'font-medium text-heading bg-muted'
|
|
40
|
+
: 'text-body'
|
|
41
41
|
)}
|
|
42
42
|
>
|
|
43
43
|
<SafeHtml value={cellContent} as="span" />
|
package/src/hooks/useTheme.js
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useTheme Hook
|
|
3
|
-
*
|
|
4
|
-
* Provides standardized theme classes for components.
|
|
5
|
-
* Eliminates duplicated theme objects across 25+ components.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* function Hero({ params }) {
|
|
9
|
-
* const t = useTheme(params.theme)
|
|
10
|
-
*
|
|
11
|
-
* return (
|
|
12
|
-
* <section className={t.section}>
|
|
13
|
-
* <h1 className={t.title}>...</h1>
|
|
14
|
-
* <p className={t.body}>...</p>
|
|
15
|
-
* </section>
|
|
16
|
-
* )
|
|
17
|
-
* }
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* // With custom overrides
|
|
21
|
-
* const t = useTheme('dark', {
|
|
22
|
-
* card: 'bg-gray-800 rounded-xl',
|
|
23
|
-
* cardHover: 'hover:bg-gray-700'
|
|
24
|
-
* })
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Standard theme definitions.
|
|
29
|
-
* Keys are standardized across all components.
|
|
30
|
-
*/
|
|
31
|
-
const THEMES = {
|
|
32
|
-
light: {
|
|
33
|
-
// Section backgrounds
|
|
34
|
-
section: 'bg-white',
|
|
35
|
-
sectionAlt: 'bg-gray-50',
|
|
36
|
-
|
|
37
|
-
// Typography
|
|
38
|
-
title: 'text-gray-900',
|
|
39
|
-
subtitle: 'text-gray-700',
|
|
40
|
-
pretitle: 'text-primary',
|
|
41
|
-
body: 'text-gray-600',
|
|
42
|
-
muted: 'text-gray-500',
|
|
43
|
-
|
|
44
|
-
// Cards
|
|
45
|
-
card: 'bg-gray-50',
|
|
46
|
-
cardHover: 'hover:shadow-lg',
|
|
47
|
-
cardBorder: 'border border-gray-200',
|
|
48
|
-
|
|
49
|
-
// Interactive
|
|
50
|
-
link: 'text-primary hover:text-primary-dark',
|
|
51
|
-
linkMuted: 'text-gray-600 hover:text-gray-900',
|
|
52
|
-
|
|
53
|
-
// Borders & Dividers
|
|
54
|
-
border: 'border-gray-200',
|
|
55
|
-
divider: 'bg-gray-200',
|
|
56
|
-
},
|
|
57
|
-
|
|
58
|
-
gray: {
|
|
59
|
-
section: 'bg-gray-50',
|
|
60
|
-
sectionAlt: 'bg-gray-100',
|
|
61
|
-
|
|
62
|
-
title: 'text-gray-900',
|
|
63
|
-
subtitle: 'text-gray-700',
|
|
64
|
-
pretitle: 'text-primary',
|
|
65
|
-
body: 'text-gray-600',
|
|
66
|
-
muted: 'text-gray-500',
|
|
67
|
-
|
|
68
|
-
card: 'bg-white shadow-sm',
|
|
69
|
-
cardHover: 'hover:shadow-lg',
|
|
70
|
-
cardBorder: 'border border-gray-200',
|
|
71
|
-
|
|
72
|
-
link: 'text-primary hover:text-primary-dark',
|
|
73
|
-
linkMuted: 'text-gray-600 hover:text-gray-900',
|
|
74
|
-
|
|
75
|
-
border: 'border-gray-200',
|
|
76
|
-
divider: 'bg-gray-300',
|
|
77
|
-
},
|
|
78
|
-
|
|
79
|
-
dark: {
|
|
80
|
-
section: 'bg-gray-900',
|
|
81
|
-
sectionAlt: 'bg-gray-800',
|
|
82
|
-
|
|
83
|
-
title: 'text-white',
|
|
84
|
-
subtitle: 'text-gray-300',
|
|
85
|
-
pretitle: 'text-primary',
|
|
86
|
-
body: 'text-gray-400',
|
|
87
|
-
muted: 'text-gray-500',
|
|
88
|
-
|
|
89
|
-
card: 'bg-gray-800',
|
|
90
|
-
cardHover: 'hover:bg-gray-750',
|
|
91
|
-
cardBorder: 'border border-gray-700',
|
|
92
|
-
|
|
93
|
-
link: 'text-primary hover:text-primary-light',
|
|
94
|
-
linkMuted: 'text-gray-400 hover:text-white',
|
|
95
|
-
|
|
96
|
-
border: 'border-gray-700',
|
|
97
|
-
divider: 'bg-gray-700',
|
|
98
|
-
},
|
|
99
|
-
|
|
100
|
-
primary: {
|
|
101
|
-
section: 'bg-primary',
|
|
102
|
-
sectionAlt: 'bg-primary-dark',
|
|
103
|
-
|
|
104
|
-
title: 'text-white',
|
|
105
|
-
subtitle: 'text-white/90',
|
|
106
|
-
pretitle: 'text-white/80',
|
|
107
|
-
body: 'text-white/80',
|
|
108
|
-
muted: 'text-white/60',
|
|
109
|
-
|
|
110
|
-
card: 'bg-white/10',
|
|
111
|
-
cardHover: 'hover:bg-white/20',
|
|
112
|
-
cardBorder: 'border border-white/20',
|
|
113
|
-
|
|
114
|
-
link: 'text-white hover:text-white/80',
|
|
115
|
-
linkMuted: 'text-white/70 hover:text-white',
|
|
116
|
-
|
|
117
|
-
border: 'border-white/20',
|
|
118
|
-
divider: 'bg-white/20',
|
|
119
|
-
},
|
|
120
|
-
|
|
121
|
-
gradient: {
|
|
122
|
-
section: 'bg-gradient-to-br from-primary to-indigo-600',
|
|
123
|
-
sectionAlt: 'bg-gradient-to-br from-primary-dark to-indigo-700',
|
|
124
|
-
|
|
125
|
-
title: 'text-white',
|
|
126
|
-
subtitle: 'text-white/90',
|
|
127
|
-
pretitle: 'text-white/80',
|
|
128
|
-
body: 'text-white/80',
|
|
129
|
-
muted: 'text-white/60',
|
|
130
|
-
|
|
131
|
-
card: 'bg-white/10 backdrop-blur-sm',
|
|
132
|
-
cardHover: 'hover:bg-white/20',
|
|
133
|
-
cardBorder: 'border border-white/20',
|
|
134
|
-
|
|
135
|
-
link: 'text-white hover:text-white/80',
|
|
136
|
-
linkMuted: 'text-white/70 hover:text-white',
|
|
137
|
-
|
|
138
|
-
border: 'border-white/20',
|
|
139
|
-
divider: 'bg-white/20',
|
|
140
|
-
},
|
|
141
|
-
|
|
142
|
-
glass: {
|
|
143
|
-
section: 'bg-white/10 backdrop-blur-lg',
|
|
144
|
-
sectionAlt: 'bg-white/5 backdrop-blur-lg',
|
|
145
|
-
|
|
146
|
-
title: 'text-white',
|
|
147
|
-
subtitle: 'text-white/90',
|
|
148
|
-
pretitle: 'text-primary',
|
|
149
|
-
body: 'text-white/80',
|
|
150
|
-
muted: 'text-white/60',
|
|
151
|
-
|
|
152
|
-
card: 'bg-white/10 backdrop-blur-sm',
|
|
153
|
-
cardHover: 'hover:bg-white/20',
|
|
154
|
-
cardBorder: 'border border-white/20',
|
|
155
|
-
|
|
156
|
-
link: 'text-white hover:text-primary',
|
|
157
|
-
linkMuted: 'text-white/70 hover:text-white',
|
|
158
|
-
|
|
159
|
-
border: 'border-white/20',
|
|
160
|
-
divider: 'bg-white/20',
|
|
161
|
-
},
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Hook to get theme classes by name.
|
|
166
|
-
*
|
|
167
|
-
* @param {string} themeName - Theme name (light, gray, dark, primary, gradient, glass)
|
|
168
|
-
* @param {Object} overrides - Custom class overrides
|
|
169
|
-
* @returns {Object} Theme classes object
|
|
170
|
-
*/
|
|
171
|
-
export function useTheme(themeName = 'light', overrides = {}) {
|
|
172
|
-
const baseTheme = THEMES[themeName] || THEMES.light
|
|
173
|
-
|
|
174
|
-
if (Object.keys(overrides).length === 0) {
|
|
175
|
-
return baseTheme
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
...baseTheme,
|
|
180
|
-
...overrides,
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Get theme classes without the hook (for non-React contexts)
|
|
186
|
-
* @param {string} themeName
|
|
187
|
-
* @param {Object} overrides
|
|
188
|
-
* @returns {Object}
|
|
189
|
-
*/
|
|
190
|
-
export function getThemeClasses(themeName = 'light', overrides = {}) {
|
|
191
|
-
const baseTheme = THEMES[themeName] || THEMES.light
|
|
192
|
-
return { ...baseTheme, ...overrides }
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* All available theme names
|
|
197
|
-
*/
|
|
198
|
-
export const THEME_NAMES = Object.keys(THEMES)
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Export theme definitions for customization
|
|
202
|
-
*/
|
|
203
|
-
export { THEMES }
|
|
204
|
-
|
|
205
|
-
export default useTheme
|