@uniweb/kit 0.11.1 → 0.11.3
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 +4 -4
- package/src/components/Icon/Icon.jsx +26 -4
- package/src/styled/Render/index.jsx +49 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/kit",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.3",
|
|
4
4
|
"description": "Standard component library for Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
"shiki": "^3.0.0",
|
|
45
45
|
"tailwind-merge": "^3.6.0",
|
|
46
46
|
"temml": "^0.13.2",
|
|
47
|
-
"@uniweb/core": "0.8.
|
|
48
|
-
"@uniweb/
|
|
49
|
-
"@uniweb/
|
|
47
|
+
"@uniweb/core": "^0.8.5",
|
|
48
|
+
"@uniweb/semantic-parser": "^1.2.2",
|
|
49
|
+
"@uniweb/scene": "^0.1.3"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"react": "^19.0.0",
|
|
@@ -38,6 +38,28 @@ const BUILT_IN_ICONS = {
|
|
|
38
38
|
* @param {string} svgContent - Raw SVG string
|
|
39
39
|
* @returns {Object} { viewBox, content, width, height }
|
|
40
40
|
*/
|
|
41
|
+
/**
|
|
42
|
+
* Coerce a size prop into a valid CSS length.
|
|
43
|
+
*
|
|
44
|
+
* React appends `px` to a NUMERIC style value but passes a string through
|
|
45
|
+
* verbatim — and this component's default is the string `'24'`, so it emitted
|
|
46
|
+
* `style="width:24;height:24"`. Both declarations are invalid CSS and the
|
|
47
|
+
* browser drops them, leaving the <svg> at its default replaced-element size.
|
|
48
|
+
*
|
|
49
|
+
* That stayed invisible for as long as every caller sized the icon with a class
|
|
50
|
+
* (`h-8 w-8`) or placed it where a stray size did not matter. It became visible
|
|
51
|
+
* when icons started rendering INLINE inside prose: with no valid width, the
|
|
52
|
+
* glyph could not share a line and broke the sentence across three lines.
|
|
53
|
+
*
|
|
54
|
+
* Only a bare number is touched, so `'2rem'`, `'1.5em'`, `'100%'` and a real
|
|
55
|
+
* number are all unchanged.
|
|
56
|
+
*/
|
|
57
|
+
function cssLength(value) {
|
|
58
|
+
return typeof value === 'string' && /^-?\d*\.?\d+$/.test(value.trim())
|
|
59
|
+
? `${value.trim()}px`
|
|
60
|
+
: value
|
|
61
|
+
}
|
|
62
|
+
|
|
41
63
|
/**
|
|
42
64
|
* Extract an attribute value from an SVG opening tag string
|
|
43
65
|
*/
|
|
@@ -253,7 +275,7 @@ export function Icon({
|
|
|
253
275
|
loadingComponent || (
|
|
254
276
|
<span
|
|
255
277
|
className={cn('inline-flex items-center justify-center', className)}
|
|
256
|
-
style={{ width: size, height: size }}
|
|
278
|
+
style={{ width: cssLength(size), height: cssLength(size) }}
|
|
257
279
|
role="img"
|
|
258
280
|
aria-hidden="true"
|
|
259
281
|
/>
|
|
@@ -287,7 +309,7 @@ export function Icon({
|
|
|
287
309
|
return (
|
|
288
310
|
<span
|
|
289
311
|
className={cn('inline-flex items-center justify-center', className)}
|
|
290
|
-
style={{ width: size, height: size }}
|
|
312
|
+
style={{ width: cssLength(size), height: cssLength(size) }}
|
|
291
313
|
role="img"
|
|
292
314
|
aria-hidden="true"
|
|
293
315
|
/>
|
|
@@ -298,8 +320,8 @@ export function Icon({
|
|
|
298
320
|
|
|
299
321
|
// Build style
|
|
300
322
|
const style = {
|
|
301
|
-
width: size,
|
|
302
|
-
height: size,
|
|
323
|
+
width: cssLength(size),
|
|
324
|
+
height: cssLength(size),
|
|
303
325
|
...(color && !preserveColors ? { color } : {})
|
|
304
326
|
}
|
|
305
327
|
|
|
@@ -59,6 +59,22 @@ import { Divider } from '../Section/renderers/Divider.jsx'
|
|
|
59
59
|
|
|
60
60
|
const INLINE_INSET_RE = /<uniweb-inset data-ref-id="([^"]+)"><\/uniweb-inset>/g
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Inline atoms the parser leaves as markers in a paragraph's HTML, because each
|
|
64
|
+
* needs a React component rather than markup: an inset resolves to a Block, an
|
|
65
|
+
* icon to <Icon> (which fetches library+name from the icon CDN).
|
|
66
|
+
*
|
|
67
|
+
* One regex for both so a paragraph carrying a mix is split in a single pass and
|
|
68
|
+
* every fragment keeps its authored position.
|
|
69
|
+
*/
|
|
70
|
+
const INLINE_MARKER_RE =
|
|
71
|
+
/<uniweb-inset data-ref-id="([^"]+)"><\/uniweb-inset>|<uniweb-icon data-index="(\d+)"><\/uniweb-icon>/g
|
|
72
|
+
|
|
73
|
+
/** Does this HTML string carry any inline marker needing component substitution? */
|
|
74
|
+
function hasInlineMarkers(html) {
|
|
75
|
+
return typeof html === 'string' && /<uniweb-(?:inset|icon)\b/.test(html)
|
|
76
|
+
}
|
|
77
|
+
|
|
62
78
|
/**
|
|
63
79
|
* Element types this engine knows about and deliberately renders NOTHING for.
|
|
64
80
|
*
|
|
@@ -130,22 +146,37 @@ function toSequence(content, block) {
|
|
|
130
146
|
* positions via the framework's child-block renderer — the same path
|
|
131
147
|
* block-level insets take, scoped to one inset.
|
|
132
148
|
*/
|
|
133
|
-
function renderParagraphWithInsets(html, block) {
|
|
134
|
-
|
|
149
|
+
function renderParagraphWithInsets(html, block, element) {
|
|
150
|
+
// An icon needs no Block, so a paragraph carrying only icons still resolves
|
|
151
|
+
// without one — only insets require `block` for the getInset() lookup.
|
|
152
|
+
const icons = (element?.children || []).filter((c) => c?.type === 'icon')
|
|
153
|
+
if (!block && !icons.length) return <SafeHtml value={html} as="span" />
|
|
154
|
+
|
|
135
155
|
const InsetRenderer = getChildBlockRenderer()
|
|
136
156
|
const parts = []
|
|
137
157
|
let lastIdx = 0
|
|
138
|
-
|
|
158
|
+
INLINE_MARKER_RE.lastIndex = 0
|
|
139
159
|
let match
|
|
140
|
-
while ((match =
|
|
160
|
+
while ((match = INLINE_MARKER_RE.exec(html)) !== null) {
|
|
141
161
|
if (match.index > lastIdx) {
|
|
142
162
|
parts.push(<SafeHtml key={`t${lastIdx}`} value={html.slice(lastIdx, match.index)} as="span" />)
|
|
143
163
|
}
|
|
164
|
+
|
|
144
165
|
const refId = match[1]
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
166
|
+
const iconIndex = match[2]
|
|
167
|
+
|
|
168
|
+
if (refId !== undefined) {
|
|
169
|
+
const insetBlock = block?.getInset?.(refId)
|
|
170
|
+
if (insetBlock && InsetRenderer) {
|
|
171
|
+
parts.push(<InsetRenderer key={`i${refId}`} blocks={[insetBlock]} />)
|
|
172
|
+
}
|
|
173
|
+
} else if (iconIndex !== undefined) {
|
|
174
|
+
// Ordinal into the icons of this element's `children`, in document order
|
|
175
|
+
// — the contract semantic-parser's getTextContent() emits the marker on.
|
|
176
|
+
const icon = icons[Number(iconIndex)]
|
|
177
|
+
if (icon) parts.push(<Icon key={`ic${iconIndex}`} {...icon.attrs} />)
|
|
148
178
|
}
|
|
179
|
+
|
|
149
180
|
lastIdx = match.index + match[0].length
|
|
150
181
|
}
|
|
151
182
|
if (lastIdx < html.length) {
|
|
@@ -187,8 +218,8 @@ function renderCell(cell, block, components) {
|
|
|
187
218
|
|
|
188
219
|
if (only?.type === 'paragraph') {
|
|
189
220
|
if (!only.text) return null
|
|
190
|
-
return
|
|
191
|
-
renderParagraphWithInsets(only.text, block)
|
|
221
|
+
return hasInlineMarkers(only.text) ? (
|
|
222
|
+
renderParagraphWithInsets(only.text, block, only)
|
|
192
223
|
) : (
|
|
193
224
|
<SafeHtml value={only.text} as="span" />
|
|
194
225
|
)
|
|
@@ -229,17 +260,23 @@ export function SequenceElement({ element, block, components }) {
|
|
|
229
260
|
const Tag = `h${level}`
|
|
230
261
|
// The id is the anchor `useHeadings()` and every in-page nav link
|
|
231
262
|
// resolve against, so it is behaviour rather than decoration.
|
|
263
|
+
// headingId() runs stripTags() first, so a marker in the text does not
|
|
264
|
+
// change the anchor — an icon in a heading keeps the id the TOC links to.
|
|
232
265
|
return (
|
|
233
266
|
<Tag id={authoredId(element) || headingId(element.text || '')}>
|
|
234
|
-
|
|
267
|
+
{hasInlineMarkers(element.text) ? (
|
|
268
|
+
renderParagraphWithInsets(element.text, block, element)
|
|
269
|
+
) : (
|
|
270
|
+
<SafeHtml value={element.text} as="span" />
|
|
271
|
+
)}
|
|
235
272
|
</Tag>
|
|
236
273
|
)
|
|
237
274
|
}
|
|
238
275
|
|
|
239
276
|
case 'paragraph': {
|
|
240
277
|
if (!element.text) return null
|
|
241
|
-
if (
|
|
242
|
-
return <p>{renderParagraphWithInsets(element.text, block)}</p>
|
|
278
|
+
if (hasInlineMarkers(element.text)) {
|
|
279
|
+
return <p>{renderParagraphWithInsets(element.text, block, element)}</p>
|
|
243
280
|
}
|
|
244
281
|
return (
|
|
245
282
|
<p>
|