@uniweb/kit 0.9.35 → 0.9.37

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/kit",
3
- "version": "0.9.35",
3
+ "version": "0.9.37",
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/scene": "0.1.2",
44
- "@uniweb/core": "0.7.27"
43
+ "@uniweb/core": "0.7.29",
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
@@ -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'
@@ -8,7 +8,7 @@
8
8
  * function Header() {
9
9
  * const scrolled = useScrolled()
10
10
  * return (
11
- * <header className={scrolled ? 'bg-white shadow' : 'bg-transparent'}>
11
+ * <header className={scrolled ? 'bg-section shadow' : 'bg-transparent'}>
12
12
  * ...
13
13
  * </header>
14
14
  * )
package/src/index.js CHANGED
@@ -65,10 +65,6 @@ export {
65
65
  useHeadings,
66
66
  useGridLayout,
67
67
  getGridClasses,
68
- useTheme,
69
- getThemeClasses,
70
- THEMES,
71
- THEME_NAMES,
72
68
  // Viewport detection
73
69
  useInView,
74
70
  useIsInView,
@@ -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-gray-200',
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-gray-50">
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-gray-400" />
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-gray-600 truncate max-w-[128px]" title={filename}>
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-white shadow-xl transition-all',
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-gray-200 px-4 py-3">
60
- <h3 id="disclaimer-title" className="text-lg font-medium text-gray-900">
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-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
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-gray-200 px-4 py-3 flex justify-end">
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-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
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-blue-600 hover:text-blue-700',
165
- 'border border-blue-600 rounded-md hover:bg-blue-50',
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-blue-600 hover:text-blue-700 underline text-sm',
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-gray-50">
127
- <Icon name="download" size="24" className="text-gray-500" />
128
- <span className="text-blue-600 group-hover:underline">
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-gray-500 text-center">
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-gray-500 text-center">
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-gray-300 pl-4 italic text-gray-600 my-4">
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-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors my-2"
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-gray-100 rounded text-sm">${text}</code>`
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-blue-600 hover:underline">${text}</a>`
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-gray-50">
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-blue-50 border-blue-200 text-blue-800',
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-blue-500'
22
+ iconColor: 'text-info'
23
23
  },
24
24
  success: {
25
- container: 'bg-green-50 border-green-200 text-green-800',
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-green-500'
29
+ iconColor: 'text-success'
30
30
  },
31
31
  warning: {
32
- container: 'bg-yellow-50 border-yellow-200 text-yellow-800',
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-yellow-500'
36
+ iconColor: 'text-warning'
37
37
  },
38
38
  danger: {
39
- container: 'bg-red-50 border-red-200 text-red-800',
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-red-500'
43
+ iconColor: 'text-error'
44
44
  },
45
45
  error: {
46
- container: 'bg-red-50 border-red-200 text-red-800',
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-red-500'
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
- let siteThemeLoaded = false
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
- * Register the site's `code:` declaration as a theme, once, and answer with the
174
- * theme name to highlight against. Sites that declare nothing get the default.
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 resolveThemeName(highlighter, codeTheme) {
177
- if (!codeTheme || Object.keys(codeTheme).length === 0) return DEFAULT_THEME
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 highlighter.loadTheme(buildCodeTheme(highlighter.getTheme(DEFAULT_THEME), codeTheme))
182
- siteThemeLoaded = true
183
- return SITE_CODE_THEME
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 theme = await resolveThemeName(highlighter, codeTheme)
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', theme })
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
- theme,
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-gray-200 rounded-lg overflow-hidden',
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-gray-900 bg-gray-50',
44
- 'hover:bg-gray-100 transition-colors'
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-gray-500 transition-transform',
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-gray-200">
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-gray-300 rounded-full" />
28
- <span className="w-2 h-2 bg-gray-300 rounded-full" />
29
- <span className="w-2 h-2 bg-gray-300 rounded-full" />
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-gray-200 my-6', className)}
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-gray-200 border border-gray-200 rounded-lg">
26
- <tbody className="divide-y divide-gray-200 bg-white">
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-white' : 'bg-gray-50'}>
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-gray-900 bg-gray-100'
40
- : 'text-gray-600'
39
+ ? 'font-medium text-heading bg-muted'
40
+ : 'text-body'
41
41
  )}
42
42
  >
43
43
  <SafeHtml value={cellContent} as="span" />
@@ -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