@uniweb/kit 0.2.3 → 0.3.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Standard component library for Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -36,18 +36,14 @@
|
|
|
36
36
|
"node": ">=20.19"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
+
"fuse.js": "^7.0.0",
|
|
40
|
+
"shiki": "^3.0.0",
|
|
39
41
|
"tailwind-merge": "^2.6.0",
|
|
40
42
|
"@uniweb/core": "0.2.3"
|
|
41
43
|
},
|
|
42
44
|
"peerDependencies": {
|
|
43
45
|
"react": "^18.0.0 || ^19.0.0",
|
|
44
|
-
"react-dom": "^18.0.0 || ^19.0.0"
|
|
45
|
-
"fuse.js": "^7.0.0"
|
|
46
|
-
},
|
|
47
|
-
"peerDependenciesMeta": {
|
|
48
|
-
"fuse.js": {
|
|
49
|
-
"optional": true
|
|
50
|
-
}
|
|
46
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
51
47
|
},
|
|
52
48
|
"devDependencies": {
|
|
53
49
|
"tailwindcss": "^3.4.0"
|
|
@@ -1,31 +1,173 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Code Block Renderer
|
|
3
3
|
*
|
|
4
|
-
* Renders syntax-highlighted code blocks.
|
|
5
|
-
*
|
|
4
|
+
* Renders syntax-highlighted code blocks using Shiki.
|
|
5
|
+
* Shiki is lazy-loaded only when code blocks are actually used,
|
|
6
|
+
* and CSS variables are injected at runtime from theme.code.
|
|
6
7
|
*
|
|
7
8
|
* @module @uniweb/kit/Section/renderers/Code
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
|
-
import React, { useEffect,
|
|
11
|
+
import React, { useEffect, useState, useMemo } from 'react'
|
|
11
12
|
import { cn } from '../../../utils/index.js'
|
|
13
|
+
import { getUniweb } from '@uniweb/core'
|
|
14
|
+
|
|
15
|
+
// Module-level state to track CSS injection and Shiki loading
|
|
16
|
+
let cssInjected = false
|
|
17
|
+
let shikiInstance = null
|
|
18
|
+
let shikiLoadPromise = null
|
|
12
19
|
|
|
13
20
|
/**
|
|
14
|
-
*
|
|
21
|
+
* Map theme.code keys to Shiki CSS variable names
|
|
15
22
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
const CSS_VAR_MAP = {
|
|
24
|
+
background: '--shiki-background',
|
|
25
|
+
foreground: '--shiki-foreground',
|
|
26
|
+
keyword: '--shiki-token-keyword',
|
|
27
|
+
string: '--shiki-token-string',
|
|
28
|
+
number: '--shiki-token-constant',
|
|
29
|
+
comment: '--shiki-token-comment',
|
|
30
|
+
function: '--shiki-token-function',
|
|
31
|
+
variable: '--shiki-token-variable',
|
|
32
|
+
operator: '--shiki-token-operator',
|
|
33
|
+
punctuation: '--shiki-token-punctuation',
|
|
34
|
+
type: '--shiki-token-type',
|
|
35
|
+
constant: '--shiki-token-constant',
|
|
36
|
+
property: '--shiki-token-property',
|
|
37
|
+
tag: '--shiki-token-tag',
|
|
38
|
+
attribute: '--shiki-token-attribute',
|
|
39
|
+
lineNumber: '--shiki-line-number',
|
|
40
|
+
selection: '--shiki-selection',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Inject CSS variables from theme.code into the document
|
|
45
|
+
*/
|
|
46
|
+
function injectCodeThemeCSS(codeTheme) {
|
|
47
|
+
if (cssInjected || typeof document === 'undefined') return
|
|
48
|
+
|
|
49
|
+
const styleId = 'uniweb-code-theme'
|
|
50
|
+
|
|
51
|
+
// Check if already injected (e.g., by another component instance)
|
|
52
|
+
if (document.getElementById(styleId)) {
|
|
53
|
+
cssInjected = true
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Build CSS variables
|
|
58
|
+
const cssVars = []
|
|
59
|
+
for (const [key, value] of Object.entries(codeTheme || {})) {
|
|
60
|
+
const varName = CSS_VAR_MAP[key]
|
|
61
|
+
if (varName && value) {
|
|
62
|
+
cssVars.push(`${varName}: ${value};`)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Create and inject style element
|
|
67
|
+
const style = document.createElement('style')
|
|
68
|
+
style.id = styleId
|
|
69
|
+
style.textContent = `
|
|
70
|
+
:root {
|
|
71
|
+
${cssVars.join('\n ')}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Code block base styles */
|
|
75
|
+
.shiki {
|
|
76
|
+
background-color: var(--shiki-background, #1e1e2e);
|
|
77
|
+
color: var(--shiki-foreground, #cdd6f4);
|
|
78
|
+
padding: 1rem;
|
|
79
|
+
border-radius: 0.5rem;
|
|
80
|
+
overflow-x: auto;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Ensure proper token colors */
|
|
84
|
+
.shiki span {
|
|
85
|
+
color: var(--shiki-token-foreground, inherit);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Code element inside shiki */
|
|
89
|
+
.shiki code {
|
|
90
|
+
display: block;
|
|
91
|
+
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace;
|
|
92
|
+
}
|
|
93
|
+
`
|
|
94
|
+
document.head.appendChild(style)
|
|
95
|
+
cssInjected = true
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Lazy-load Shiki highlighter
|
|
100
|
+
*/
|
|
101
|
+
async function loadShiki() {
|
|
102
|
+
if (shikiInstance) return shikiInstance
|
|
103
|
+
if (shikiLoadPromise) return shikiLoadPromise
|
|
104
|
+
|
|
105
|
+
shikiLoadPromise = (async () => {
|
|
18
106
|
try {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
107
|
+
const { createHighlighter } = await import('shiki')
|
|
108
|
+
|
|
109
|
+
// Create highlighter with CSS variables theme
|
|
110
|
+
// Only load common languages initially, others load on-demand
|
|
111
|
+
shikiInstance = await createHighlighter({
|
|
112
|
+
themes: ['css-variables'],
|
|
113
|
+
langs: [
|
|
114
|
+
'javascript',
|
|
115
|
+
'typescript',
|
|
116
|
+
'jsx',
|
|
117
|
+
'tsx',
|
|
118
|
+
'json',
|
|
119
|
+
'html',
|
|
120
|
+
'css',
|
|
121
|
+
'markdown',
|
|
122
|
+
'yaml',
|
|
123
|
+
'bash',
|
|
124
|
+
'shell',
|
|
125
|
+
'python',
|
|
126
|
+
],
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
return shikiInstance
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.warn('[Code] Failed to load Shiki:', error)
|
|
132
|
+
shikiLoadPromise = null
|
|
133
|
+
return null
|
|
134
|
+
}
|
|
135
|
+
})()
|
|
136
|
+
|
|
137
|
+
return shikiLoadPromise
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Highlight code using Shiki
|
|
142
|
+
*/
|
|
143
|
+
async function highlightCode(code, language, highlighter) {
|
|
144
|
+
if (!highlighter) return null
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
// Load language if not already loaded
|
|
148
|
+
const loadedLangs = highlighter.getLoadedLanguages()
|
|
149
|
+
const lang = language?.toLowerCase() || 'plaintext'
|
|
150
|
+
|
|
151
|
+
if (!loadedLangs.includes(lang) && lang !== 'plaintext') {
|
|
152
|
+
try {
|
|
153
|
+
await highlighter.loadLanguage(lang)
|
|
154
|
+
} catch {
|
|
155
|
+
// Language not available, fall back to plaintext
|
|
156
|
+
return highlighter.codeToHtml(code, {
|
|
157
|
+
lang: 'plaintext',
|
|
158
|
+
theme: 'css-variables',
|
|
159
|
+
})
|
|
23
160
|
}
|
|
24
|
-
} catch (e) {
|
|
25
|
-
console.warn('[Code] Prism highlighting failed:', e)
|
|
26
161
|
}
|
|
162
|
+
|
|
163
|
+
return highlighter.codeToHtml(code, {
|
|
164
|
+
lang: lang === 'plaintext' ? 'text' : lang,
|
|
165
|
+
theme: 'css-variables',
|
|
166
|
+
})
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.warn('[Code] Highlighting failed:', error)
|
|
169
|
+
return null
|
|
27
170
|
}
|
|
28
|
-
return false
|
|
29
171
|
}
|
|
30
172
|
|
|
31
173
|
/**
|
|
@@ -37,18 +179,77 @@ function highlightCode(code, language, element) {
|
|
|
37
179
|
* @param {string} [props.className] - Additional CSS classes
|
|
38
180
|
*/
|
|
39
181
|
export function Code({ content, language = 'plaintext', className, ...props }) {
|
|
40
|
-
const
|
|
182
|
+
const [highlightedHtml, setHighlightedHtml] = useState(null)
|
|
183
|
+
|
|
184
|
+
// Get theme from website context (getUniweb is a regular function, not a hook)
|
|
185
|
+
const codeTheme = useMemo(() => {
|
|
186
|
+
try {
|
|
187
|
+
const uniweb = getUniweb()
|
|
188
|
+
return uniweb?.activeWebsite?.themeData?.code
|
|
189
|
+
} catch {
|
|
190
|
+
// Not in runtime context (e.g., storybook), use defaults
|
|
191
|
+
return null
|
|
192
|
+
}
|
|
193
|
+
}, [])
|
|
41
194
|
|
|
42
195
|
// Normalize language
|
|
43
|
-
const lang =
|
|
196
|
+
const lang = useMemo(() => {
|
|
197
|
+
const l = language?.toLowerCase() || 'plaintext'
|
|
198
|
+
// Common aliases
|
|
199
|
+
const aliases = {
|
|
200
|
+
js: 'javascript',
|
|
201
|
+
ts: 'typescript',
|
|
202
|
+
sh: 'bash',
|
|
203
|
+
yml: 'yaml',
|
|
204
|
+
md: 'markdown',
|
|
205
|
+
}
|
|
206
|
+
return aliases[l] || l
|
|
207
|
+
}, [language])
|
|
208
|
+
|
|
209
|
+
// Inject CSS on first render (if in browser)
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
if (typeof document !== 'undefined' && codeTheme) {
|
|
212
|
+
injectCodeThemeCSS(codeTheme)
|
|
213
|
+
}
|
|
214
|
+
}, [codeTheme])
|
|
44
215
|
|
|
45
|
-
//
|
|
216
|
+
// Load Shiki and highlight code
|
|
46
217
|
useEffect(() => {
|
|
47
|
-
|
|
48
|
-
|
|
218
|
+
let cancelled = false
|
|
219
|
+
|
|
220
|
+
async function highlight() {
|
|
221
|
+
const highlighter = await loadShiki()
|
|
222
|
+
if (cancelled) return
|
|
223
|
+
|
|
224
|
+
if (highlighter && content) {
|
|
225
|
+
const html = await highlightCode(content, lang, highlighter)
|
|
226
|
+
if (!cancelled) {
|
|
227
|
+
setHighlightedHtml(html)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
highlight()
|
|
233
|
+
|
|
234
|
+
return () => {
|
|
235
|
+
cancelled = true
|
|
49
236
|
}
|
|
50
237
|
}, [content, lang])
|
|
51
238
|
|
|
239
|
+
// Render highlighted code or fallback
|
|
240
|
+
if (highlightedHtml) {
|
|
241
|
+
return (
|
|
242
|
+
<div
|
|
243
|
+
className={cn('overflow-x-auto rounded-lg text-sm', className)}
|
|
244
|
+
dangerouslySetInnerHTML={{ __html: highlightedHtml }}
|
|
245
|
+
{...props}
|
|
246
|
+
/>
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Fallback: plain code block (shown before Shiki loads or if it fails)
|
|
251
|
+
// No loading indicator - the code content is already visible and readable.
|
|
252
|
+
// When Shiki loads, syntax colors will appear smoothly.
|
|
52
253
|
return (
|
|
53
254
|
<pre
|
|
54
255
|
className={cn(
|
|
@@ -57,10 +258,7 @@ export function Code({ content, language = 'plaintext', className, ...props }) {
|
|
|
57
258
|
)}
|
|
58
259
|
{...props}
|
|
59
260
|
>
|
|
60
|
-
<code
|
|
61
|
-
ref={codeRef}
|
|
62
|
-
className={`language-${lang} text-gray-100`}
|
|
63
|
-
>
|
|
261
|
+
<code className={`language-${lang} text-gray-100`}>
|
|
64
262
|
{content}
|
|
65
263
|
</code>
|
|
66
264
|
</pre>
|