@uniweb/kit 0.19.12 → 0.19.14
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/README.md +2 -2
- package/package.json +4 -4
- package/src/components/Link/Link.jsx +16 -26
- package/src/styled/Render/index.jsx +6 -3
- package/src/styled/Visual/index.jsx +52 -14
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ function Hero({ content }) {
|
|
|
53
53
|
|
|
54
54
|
### Link
|
|
55
55
|
|
|
56
|
-
Smart link component with routing, downloads,
|
|
56
|
+
Smart link component with routing and downloads. A link that leaves the site, downloads a file, writes an email or places a call gets a tooltip saying so; an internal link gets none.
|
|
57
57
|
|
|
58
58
|
```jsx
|
|
59
59
|
import { Link } from '@uniweb/kit'
|
|
@@ -67,7 +67,7 @@ import { Link } from '@uniweb/kit'
|
|
|
67
67
|
| Prop | Type | Description |
|
|
68
68
|
|------|------|-------------|
|
|
69
69
|
| `to` / `href` | `string` | Destination URL |
|
|
70
|
-
| `title` | `string` | Tooltip
|
|
70
|
+
| `title` | `string` | Tooltip. If omitted, one is generated only where the link leaves the site, downloads, writes an email or places a call |
|
|
71
71
|
| `target` | `string` | Link target |
|
|
72
72
|
| `download` | `boolean` | Force download behavior |
|
|
73
73
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/kit",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.14",
|
|
4
4
|
"description": "Standard component library for Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -43,10 +43,10 @@
|
|
|
43
43
|
"shiki": "^3.0.0",
|
|
44
44
|
"tailwind-merge": "^3.6.0",
|
|
45
45
|
"temml": "^0.13.2",
|
|
46
|
-
"@uniweb/
|
|
47
|
-
"@uniweb/
|
|
46
|
+
"@uniweb/schemas": "^0.10.0",
|
|
47
|
+
"@uniweb/semantic-parser": "^1.5.1",
|
|
48
48
|
"@uniweb/scene": "^0.1.3",
|
|
49
|
-
"@uniweb/
|
|
49
|
+
"@uniweb/core": "^0.35.0",
|
|
50
50
|
"@uniweb/projections": "^0.7.11"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* - Internal navigation via React Router
|
|
6
6
|
* - External links with appropriate attributes
|
|
7
7
|
* - File downloads
|
|
8
|
-
* -
|
|
8
|
+
* - A generated tooltip where a link does something its text may not say:
|
|
9
|
+
* leaves the site, downloads a file, writes an email, places a call
|
|
9
10
|
*
|
|
10
11
|
* @module @uniweb/kit/Link
|
|
11
12
|
*/
|
|
@@ -52,10 +53,17 @@ function detectSocialPlatform(url) {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
/**
|
|
55
|
-
*
|
|
56
|
+
* The tooltip a link gets when its caller gives none — only where the link does
|
|
57
|
+
* something its text may not say. An internal link gets none: its text names where
|
|
58
|
+
* it goes.
|
|
59
|
+
*
|
|
60
|
+
* ⛔ Until 2026-09-30 an internal link got `Go to <its path>`, dashes as spaces —
|
|
61
|
+
* `Go to blog/writing for the web` on every card of a blog [Diego: "stop adding it
|
|
62
|
+
* to internal links"].
|
|
63
|
+
*
|
|
56
64
|
* @param {string} href - The link URL
|
|
57
65
|
* @param {Function} localize - Localization function
|
|
58
|
-
* @returns {string}
|
|
66
|
+
* @returns {string} the tooltip, or '' for none
|
|
59
67
|
*/
|
|
60
68
|
function generateTitle(href, localize) {
|
|
61
69
|
if (!href) return ''
|
|
@@ -108,26 +116,6 @@ function generateTitle(href, localize) {
|
|
|
108
116
|
})
|
|
109
117
|
}
|
|
110
118
|
|
|
111
|
-
// Internal links - humanize the path
|
|
112
|
-
try {
|
|
113
|
-
const url = new URL(href, window.location.origin)
|
|
114
|
-
const path = decodeURIComponent(url.pathname)
|
|
115
|
-
.replace(/^\/+/, '')
|
|
116
|
-
.replace(/[-_]/g, ' ')
|
|
117
|
-
.replace(/\.\w+$/, '')
|
|
118
|
-
.trim()
|
|
119
|
-
|
|
120
|
-
if (path) {
|
|
121
|
-
return localize({
|
|
122
|
-
en: `Go to ${path}`,
|
|
123
|
-
fr: `Aller à ${path}`,
|
|
124
|
-
es: `Ir a ${path}`
|
|
125
|
-
})
|
|
126
|
-
}
|
|
127
|
-
} catch {
|
|
128
|
-
// Invalid URL
|
|
129
|
-
}
|
|
130
|
-
|
|
131
119
|
return ''
|
|
132
120
|
}
|
|
133
121
|
|
|
@@ -137,7 +125,9 @@ function generateTitle(href, localize) {
|
|
|
137
125
|
* @param {Object} props
|
|
138
126
|
* @param {string} [props.to] - Destination URL (alias for href)
|
|
139
127
|
* @param {string} [props.href] - Destination URL
|
|
140
|
-
* @param {string} [props.title] - Custom title/tooltip
|
|
128
|
+
* @param {string} [props.title] - Custom title/tooltip. Without one, a link that leaves the
|
|
129
|
+
* site, downloads, writes an email or places a call gets one saying so; an internal
|
|
130
|
+
* link gets none
|
|
141
131
|
* @param {string} [props.target] - Link target (_blank, _self, etc.)
|
|
142
132
|
* @param {string} [props.className] - CSS classes
|
|
143
133
|
* @param {boolean} [props.download] - Force download behavior
|
|
@@ -192,8 +182,8 @@ export function Link({
|
|
|
192
182
|
// Determine if external
|
|
193
183
|
const isExternal = isExternalUrl(linkHref)
|
|
194
184
|
|
|
195
|
-
//
|
|
196
|
-
const linkTitle = title || generateTitle(linkHref, localize)
|
|
185
|
+
// A generated tooltip when none is given — none at all rather than an empty one.
|
|
186
|
+
const linkTitle = title || generateTitle(linkHref, localize) || undefined
|
|
197
187
|
|
|
198
188
|
// Links with reload: render plain <a> for full page navigation
|
|
199
189
|
// Used for locale switches (same-domain or cross-domain)
|
|
@@ -325,12 +325,15 @@ export function SequenceElement({ element, block, components }) {
|
|
|
325
325
|
// 2026-09-29 the parser delivered a document as an image, and this drew
|
|
326
326
|
// `<img src="report.pdf">`.
|
|
327
327
|
case 'document': {
|
|
328
|
-
|
|
329
|
-
|
|
328
|
+
// `file` is the file's own name — the parser reads it from the address, and a build
|
|
329
|
+
// that copies the file under a hashed name stamps the one it had; the address is
|
|
330
|
+
// the fallback for content parsed before documents carried one (2026-09-29).
|
|
331
|
+
const { url, alt, caption, preview, author, description, name: file } = element.attrs || {}
|
|
332
|
+
const name = caption || alt || file || fileNameOf(url)
|
|
330
333
|
return (
|
|
331
334
|
<figure id={authoredId(element)} data-kind="document">
|
|
332
335
|
<Link to={url}>
|
|
333
|
-
{preview ? <Image src={preview} alt="" /> : <FileLogo filename={url} />}
|
|
336
|
+
{preview ? <Image src={preview} alt="" /> : <FileLogo filename={file || url} />}
|
|
334
337
|
<span>{name}</span>
|
|
335
338
|
</Link>
|
|
336
339
|
{(author || description) && (
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Visual Component
|
|
3
3
|
*
|
|
4
|
-
* Renders the first non-empty
|
|
5
|
-
*
|
|
4
|
+
* Renders one visual: a `content.media` item by its kind, or the first non-empty of
|
|
5
|
+
* the candidates it is handed (inset, then video, then image).
|
|
6
6
|
*
|
|
7
7
|
* @module @uniweb/kit/styled/Visual
|
|
8
8
|
*/
|
|
@@ -13,9 +13,19 @@ import { Image } from "../../components/Image/index.js";
|
|
|
13
13
|
import { getChildBlockRenderer } from "../../utils/index.js";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Renders
|
|
16
|
+
* Renders one visual.
|
|
17
|
+
*
|
|
18
|
+
* ⭐ `media` is a `content.media` item — an image, a video or an embedded component,
|
|
19
|
+
* tagged with its `kind` — rendered by that kind. Handed the whole `content.media`,
|
|
20
|
+
* it renders the first item: a slot of one is the first thing the author placed in
|
|
21
|
+
* it, whatever its kind. An embedded component needs `block`, whose insets it is
|
|
22
|
+
* one of.
|
|
23
|
+
*
|
|
24
|
+
* Without `media`, the first non-empty candidate: inset, then video, then image.
|
|
17
25
|
*
|
|
18
26
|
* @param {Object} props
|
|
27
|
+
* @param {Object|Object[]} [props.media] - A `content.media` item, or the list (its first item)
|
|
28
|
+
* @param {Object} [props.block] - The section's block — resolves an embedded component
|
|
19
29
|
* @param {Object} [props.inset] - Inset Block instance (from block.insets or block.getInset())
|
|
20
30
|
* @param {Object} [props.video] - Video object with src property
|
|
21
31
|
* @param {Object} [props.image] - Image object with src and alt properties
|
|
@@ -23,24 +33,25 @@ import { getChildBlockRenderer } from "../../utils/index.js";
|
|
|
23
33
|
* @param {React.ReactNode} [props.fallback] - Fallback when no visual is found
|
|
24
34
|
*
|
|
25
35
|
* @example
|
|
26
|
-
* //
|
|
27
|
-
* <Visual
|
|
36
|
+
* // The slot of one — the author's first image, video or component
|
|
37
|
+
* <Visual media={content.media} block={block} className="rounded-lg" />
|
|
28
38
|
*
|
|
29
39
|
* @example
|
|
30
|
-
* //
|
|
31
|
-
* <Visual
|
|
40
|
+
* // Every item of a gallery, in the author's order
|
|
41
|
+
* {content.media.map((item, i) => <Visual key={i} media={item} block={block} />)}
|
|
32
42
|
*
|
|
33
43
|
* @example
|
|
34
|
-
* //
|
|
35
|
-
* <Visual image={content.images[
|
|
44
|
+
* // Try inset first, fall back to video, then image
|
|
45
|
+
* <Visual inset={block.insets[0]} video={content.videos[0]} image={content.images[0]} />
|
|
36
46
|
*/
|
|
37
|
-
export function Visual({ inset, video, image, className, fallback = null }) {
|
|
38
|
-
if (
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
return className ? <div className={className}>{rendered}</div> : rendered;
|
|
47
|
+
export function Visual({ media, block, inset, video, image, className, fallback = null }) {
|
|
48
|
+
if (media !== undefined) {
|
|
49
|
+
const item = Array.isArray(media) ? media[0] : media;
|
|
50
|
+
return renderItem(item, block, className) ?? fallback;
|
|
42
51
|
}
|
|
43
52
|
|
|
53
|
+
if (inset) return renderInset(inset, className);
|
|
54
|
+
|
|
44
55
|
if (video) {
|
|
45
56
|
return <Media {...video} className={className} />;
|
|
46
57
|
}
|
|
@@ -52,3 +63,30 @@ export function Visual({ inset, video, image, className, fallback = null }) {
|
|
|
52
63
|
|
|
53
64
|
return fallback;
|
|
54
65
|
}
|
|
66
|
+
|
|
67
|
+
/** A `content.media` item, rendered by its kind — or null when it cannot be. */
|
|
68
|
+
function renderItem(item, block, className) {
|
|
69
|
+
if (!item || typeof item !== "object") return null;
|
|
70
|
+
const { kind, ...attrs } = item;
|
|
71
|
+
switch (kind) {
|
|
72
|
+
case "image": {
|
|
73
|
+
const { contentType, viewType, contentId, imgPos, ...imageProps } = attrs;
|
|
74
|
+
return <Image {...imageProps} className={className} />;
|
|
75
|
+
}
|
|
76
|
+
case "video":
|
|
77
|
+
return <Media {...attrs} className={className} />;
|
|
78
|
+
case "inset": {
|
|
79
|
+
const insetBlock = attrs.refId ? block?.getInset?.(attrs.refId) : null;
|
|
80
|
+
return insetBlock ? renderInset(insetBlock, className) : null;
|
|
81
|
+
}
|
|
82
|
+
default:
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function renderInset(inset, className) {
|
|
88
|
+
const Renderer = getChildBlockRenderer();
|
|
89
|
+
if (!Renderer) return null;
|
|
90
|
+
const rendered = <Renderer blocks={[inset]} />;
|
|
91
|
+
return className ? <div className={className}>{rendered}</div> : rendered;
|
|
92
|
+
}
|