@symbo.ls/default-config 3.14.0 → 3.14.1
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 +1 -1
- package/__tests__/assetMedia.test.js +242 -0
- package/__tests__/componentsManifest.test.js +41 -0
- package/components/Atoms/AssetPicture.js +34 -0
- package/components/Atoms/Audio.js +38 -0
- package/components/Atoms/Hgroup.js +18 -0
- package/components/Atoms/Img.js +25 -1
- package/components/Atoms/Svg.js +41 -8
- package/components/Atoms/Text.js +2 -0
- package/components/Atoms/Video.js +34 -0
- package/components/Tooltip/index.js +7 -2
- package/components/index.js +57 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Default Config
|
|
2
2
|
|
|
3
|
-
Default design system configuration for Symbols
|
|
3
|
+
Default design system configuration for Symbols v3.14. Provides baseline color, typography, spacing, media query, and theme definitions used when no custom config is supplied. The atomic CSS engine (`@symbo.ls/css`) and `@symbo.ls/scratch` consume this config to generate design tokens.
|
|
4
4
|
|
|
5
5
|
Check out the [docs page](http://symbols.app/developersintro#configuration) to learn more.
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset-aware media components: Img, Video, Audio, AssetPicture. The
|
|
3
|
+
* components themselves are plain DOMQL config objects — DOMQL evaluates
|
|
4
|
+
* functions against an element at runtime. These tests bypass DOMQL and
|
|
5
|
+
* call the functions directly with a mock `el` shape, asserting the
|
|
6
|
+
* src/srcset/children outputs computed from `el.context.assets`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect } from '@jest/globals'
|
|
10
|
+
import { Img } from '../components/Atoms/Img.js'
|
|
11
|
+
import { Video } from '../components/Atoms/Video.js'
|
|
12
|
+
import { Audio } from '../components/Atoms/Audio.js'
|
|
13
|
+
import { AssetPicture } from '../components/Atoms/AssetPicture.js'
|
|
14
|
+
|
|
15
|
+
const HERO_ASSET = {
|
|
16
|
+
src: '/assets/hero.jpg',
|
|
17
|
+
type: 'image/jpeg',
|
|
18
|
+
category: 'image',
|
|
19
|
+
variants: [
|
|
20
|
+
{ src: '/assets/hero.jpg', format: 'image/jpeg', scale: 1 },
|
|
21
|
+
{ src: '/assets/hero@2x.jpg', format: 'image/jpeg', scale: 2 },
|
|
22
|
+
{ src: '/assets/hero.webp', format: 'image/webp', scale: 1 },
|
|
23
|
+
{ src: '/assets/hero@2x.webp', format: 'image/webp', scale: 2 },
|
|
24
|
+
{ src: '/assets/hero.avif', format: 'image/avif', scale: 1 }
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const HERO_WIDTHS_ASSET = {
|
|
29
|
+
src: '/assets/cover.jpg',
|
|
30
|
+
type: 'image/jpeg',
|
|
31
|
+
category: 'image',
|
|
32
|
+
variants: [
|
|
33
|
+
{ src: '/assets/cover.jpg', format: 'image/jpeg', scale: 1 },
|
|
34
|
+
{ src: '/assets/cover-640w.jpg', format: 'image/jpeg', scale: 1, width: 640 },
|
|
35
|
+
{ src: '/assets/cover-1280w.jpg', format: 'image/jpeg', scale: 1, width: 1280 }
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const INTRO_VIDEO_ASSET = {
|
|
40
|
+
src: '/assets/intro.mp4',
|
|
41
|
+
type: 'video/mp4',
|
|
42
|
+
category: 'video',
|
|
43
|
+
variants: [
|
|
44
|
+
{ src: '/assets/intro.mp4', format: 'video/mp4', scale: 1 },
|
|
45
|
+
{ src: '/assets/intro.webm', format: 'video/webm', scale: 1 }
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const VOICE_AUDIO_ASSET = {
|
|
50
|
+
src: '/assets/voice.mp3',
|
|
51
|
+
type: 'audio/mpeg',
|
|
52
|
+
category: 'audio',
|
|
53
|
+
variants: [
|
|
54
|
+
{ src: '/assets/voice.mp3', format: 'audio/mpeg', scale: 1 },
|
|
55
|
+
{ src: '/assets/voice.ogg', format: 'audio/ogg', scale: 1 }
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const ctxWith = (assets) => ({ context: { assets } })
|
|
60
|
+
|
|
61
|
+
const elFor = (overrides = {}) => ({
|
|
62
|
+
...overrides,
|
|
63
|
+
context: { assets: { 'hero.jpg': HERO_ASSET } }
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('Img — asset-manifest awareness via src prop', () => {
|
|
67
|
+
it('falls through to literal src when no manifest exists', () => {
|
|
68
|
+
const el = { src: '/manual.jpg', alt: 'a' }
|
|
69
|
+
expect(Img.attr.src(el)).toBe('/manual.jpg')
|
|
70
|
+
expect(Img.attr.srcset(el)).toBeUndefined()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('uses asset.src when src matches a manifest key', () => {
|
|
74
|
+
const el = { src: 'hero.jpg', ...ctxWith({ 'hero.jpg': HERO_ASSET }) }
|
|
75
|
+
expect(Img.attr.src(el)).toBe('/assets/hero.jpg')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('emits scale-based srcset for same-format variants only', () => {
|
|
79
|
+
const el = { src: 'hero.jpg', ...ctxWith({ 'hero.jpg': HERO_ASSET }) }
|
|
80
|
+
const srcset = Img.attr.srcset(el)
|
|
81
|
+
// Same-format only — webp/avif must not appear in srcset (those need <picture>).
|
|
82
|
+
expect(srcset).toBe('/assets/hero@2x.jpg 2x')
|
|
83
|
+
expect(srcset).not.toMatch(/webp|avif/)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('emits width-based srcset when variants carry .width', () => {
|
|
87
|
+
const el = { src: 'cover.jpg', ...ctxWith({ 'cover.jpg': HERO_WIDTHS_ASSET }) }
|
|
88
|
+
expect(Img.attr.srcset(el)).toBe('/assets/cover-640w.jpg 640w, /assets/cover-1280w.jpg 1280w')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('treats src as a literal URL when not in the manifest', () => {
|
|
92
|
+
const el = { src: '/manual.jpg', alt: 'a', ...ctxWith({}) }
|
|
93
|
+
expect(Img.attr.src(el)).toBe('/manual.jpg')
|
|
94
|
+
expect(Img.attr.srcset(el)).toBeUndefined()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('preserves the title-from-alt fallback', () => {
|
|
98
|
+
expect(Img.attr.title({ title: 't' })).toBe('t')
|
|
99
|
+
expect(Img.attr.title({ alt: 'a' })).toBe('a')
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe('Video — asset-manifest awareness', () => {
|
|
104
|
+
it('emits a <source> child per format variant', () => {
|
|
105
|
+
const el = { src: 'intro.mp4', ...ctxWith({ 'intro.mp4': INTRO_VIDEO_ASSET }) }
|
|
106
|
+
const children = Video.children(el)
|
|
107
|
+
expect(children).toEqual([
|
|
108
|
+
{ tag: 'source', attr: { src: '/assets/intro.mp4', type: 'video/mp4' } },
|
|
109
|
+
{ tag: 'source', attr: { src: '/assets/intro.webm', type: 'video/webm' } }
|
|
110
|
+
])
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('sets src to the primary variant URL', () => {
|
|
114
|
+
const el = { src: 'intro.mp4', ...ctxWith({ 'intro.mp4': INTRO_VIDEO_ASSET }) }
|
|
115
|
+
expect(Video.attr.src(el)).toBe('/assets/intro.mp4')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('returns undefined when no asset resolves (lets DOMQL fall through)', () => {
|
|
119
|
+
expect(Video.children({ src: '/manual.mp4' })).toBeUndefined()
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe('Audio — asset-manifest awareness', () => {
|
|
124
|
+
it('emits a <source> child per format variant', () => {
|
|
125
|
+
const el = { src: 'voice.mp3', ...ctxWith({ 'voice.mp3': VOICE_AUDIO_ASSET }) }
|
|
126
|
+
expect(Audio.children(el)).toEqual([
|
|
127
|
+
{ tag: 'source', attr: { src: '/assets/voice.mp3', type: 'audio/mpeg' } },
|
|
128
|
+
{ tag: 'source', attr: { src: '/assets/voice.ogg', type: 'audio/ogg' } }
|
|
129
|
+
])
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('sets src to the primary variant URL', () => {
|
|
133
|
+
const el = { src: 'voice.mp3', ...ctxWith({ 'voice.mp3': VOICE_AUDIO_ASSET }) }
|
|
134
|
+
expect(Audio.attr.src(el)).toBe('/assets/voice.mp3')
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
describe('Smart resolution: cloud strings + context.files fallback', () => {
|
|
139
|
+
// Cloud / legacy entries arrive as bare URL strings, not manifest
|
|
140
|
+
// objects. Components must use the URL verbatim and skip variant
|
|
141
|
+
// emission entirely (no <source> chain, no srcset).
|
|
142
|
+
|
|
143
|
+
it('Img: cloud-string URL in context.assets resolves to plain src, no srcset', () => {
|
|
144
|
+
const el = { src: 'logo', ...ctxWith({ logo: 'https://cdn.example.com/logo.svg' }) }
|
|
145
|
+
expect(Img.attr.src(el)).toBe('https://cdn.example.com/logo.svg')
|
|
146
|
+
expect(Img.attr.srcset(el)).toBeUndefined()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('Img: falls back to context.files when key missing in context.assets', () => {
|
|
150
|
+
const el = {
|
|
151
|
+
src: 'doc.pdf-thumb.jpg',
|
|
152
|
+
context: { files: { 'doc.pdf-thumb.jpg': '/cdn/doc-thumb.jpg' } }
|
|
153
|
+
}
|
|
154
|
+
expect(Img.attr.src(el)).toBe('/cdn/doc-thumb.jpg')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('Video: cloud-string URL skips <source> children', () => {
|
|
158
|
+
const el = { src: 'reel', ...ctxWith({ reel: 'https://cdn.example.com/reel.mp4' }) }
|
|
159
|
+
expect(Video.attr.src(el)).toBe('https://cdn.example.com/reel.mp4')
|
|
160
|
+
// Video.children falls through to el.children; with none defined that's undefined.
|
|
161
|
+
expect(Video.children(el)).toBeUndefined()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('Audio: cloud-string URL skips <source> children', () => {
|
|
165
|
+
const el = { src: 'theme', ...ctxWith({ theme: 'https://cdn.example.com/theme.mp3' }) }
|
|
166
|
+
expect(Audio.attr.src(el)).toBe('https://cdn.example.com/theme.mp3')
|
|
167
|
+
expect(Audio.children(el)).toBeUndefined()
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('AssetPicture: cloud-string URL emits a single <img> (no <source> chain)', () => {
|
|
171
|
+
const el = { src: 'banner', alt: 'Banner', ...ctxWith({ banner: 'https://cdn.example.com/banner.jpg' }) }
|
|
172
|
+
expect(AssetPicture.children(el)).toEqual([
|
|
173
|
+
{ tag: 'img', attr: { src: 'https://cdn.example.com/banner.jpg', alt: 'Banner' } }
|
|
174
|
+
])
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('Img: variant manifest still wins when both shapes present in different stores', () => {
|
|
178
|
+
// Same key in both context.assets (cloud string) and context.files (manifest).
|
|
179
|
+
// assets is checked first per resolveAsset's lookup order.
|
|
180
|
+
const el = {
|
|
181
|
+
src: 'hero.jpg',
|
|
182
|
+
context: {
|
|
183
|
+
assets: { 'hero.jpg': '/cloud/hero.jpg' },
|
|
184
|
+
files: { 'hero.jpg': HERO_ASSET }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
expect(Img.attr.src(el)).toBe('/cloud/hero.jpg')
|
|
188
|
+
expect(Img.attr.srcset(el)).toBeUndefined() // cloud string → no variants
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('AssetPicture: object-shape entry without variants array emits just <img>', () => {
|
|
192
|
+
// Real-world cloud shape — provider returns { src, type } but no variant chain.
|
|
193
|
+
const minimalManifest = { src: 'https://cdn.example.com/hero.jpg', type: 'image/jpeg' }
|
|
194
|
+
const el = { src: 'hero.jpg', alt: 'Hero', ...ctxWith({ 'hero.jpg': minimalManifest }) }
|
|
195
|
+
expect(AssetPicture.children(el)).toEqual([
|
|
196
|
+
{ tag: 'img', attr: { src: 'https://cdn.example.com/hero.jpg', alt: 'Hero' } }
|
|
197
|
+
])
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
describe('AssetPicture — format-negotiated <picture>', () => {
|
|
202
|
+
it('emits <source type=…> for each non-primary format + trailing <img>', () => {
|
|
203
|
+
const el = { src: 'hero.jpg', alt: 'Hero', ...ctxWith({ 'hero.jpg': HERO_ASSET }) }
|
|
204
|
+
const children = AssetPicture.children(el)
|
|
205
|
+
|
|
206
|
+
// Non-primary formats become <source>: webp and avif (jpg is the primary).
|
|
207
|
+
const sources = children.filter((c) => c.tag === 'source')
|
|
208
|
+
const types = sources.map((c) => c.attr.type).sort()
|
|
209
|
+
expect(types).toEqual(['image/avif', 'image/webp'])
|
|
210
|
+
|
|
211
|
+
// Trailing <img> uses the primary fallback URL + same-format srcset.
|
|
212
|
+
const img = children.find((c) => c.tag === 'img')
|
|
213
|
+
expect(img.attr.src).toBe('/assets/hero.jpg')
|
|
214
|
+
expect(img.attr.alt).toBe('Hero')
|
|
215
|
+
expect(img.attr.srcset).toBe('/assets/hero@2x.jpg 2x')
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('webp <source> includes scale variants in its srcset', () => {
|
|
219
|
+
const el = { src: 'hero.jpg', ...ctxWith({ 'hero.jpg': HERO_ASSET }) }
|
|
220
|
+
const children = AssetPicture.children(el)
|
|
221
|
+
const webp = children.find((c) => c.tag === 'source' && c.attr.type === 'image/webp')
|
|
222
|
+
expect(webp.attr.srcset).toBe('/assets/hero.webp, /assets/hero@2x.webp 2x')
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('avif <source> with single variant emits no scale descriptor', () => {
|
|
226
|
+
const el = { src: 'hero.jpg', ...ctxWith({ 'hero.jpg': HERO_ASSET }) }
|
|
227
|
+
const avif = AssetPicture.children(el).find((c) => c.tag === 'source' && c.attr.type === 'image/avif')
|
|
228
|
+
expect(avif.attr.srcset).toBe('/assets/hero.avif')
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('returns undefined when no asset resolves (lets DOMQL fall through)', () => {
|
|
232
|
+
expect(AssetPicture.children({ src: '/manual.jpg' })).toBeUndefined()
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('emits just <img> when asset has no variants array', () => {
|
|
236
|
+
const minimalAsset = { src: '/assets/x.jpg', type: 'image/jpeg' }
|
|
237
|
+
const el = { src: 'x.jpg', alt: 'X', ...ctxWith({ 'x.jpg': minimalAsset }) }
|
|
238
|
+
expect(AssetPicture.children(el)).toEqual([
|
|
239
|
+
{ tag: 'img', attr: { src: '/assets/x.jpg', alt: 'X' } }
|
|
240
|
+
])
|
|
241
|
+
})
|
|
242
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FT-FRAMEWORK-1: the explicit COMPONENTS manifest must surface every
|
|
3
|
+
* uppercase-keyed component published by the default-config atoms /
|
|
4
|
+
* input / component modules. Parcel under smbls's `sideEffects: false`
|
|
5
|
+
* was pruning entries off the namespace import that prepare.js used to
|
|
6
|
+
* iterate, and consumer projects ended up missing atoms from
|
|
7
|
+
* `context.components`. This test pins the manifest contract so a future
|
|
8
|
+
* build-time prune can't silently strip entries again.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from '@jest/globals'
|
|
12
|
+
import { COMPONENTS } from '../components/index.js'
|
|
13
|
+
|
|
14
|
+
const REQUIRED = [
|
|
15
|
+
// Atoms
|
|
16
|
+
'Block', 'Box', 'Flex', 'Grid', 'Img', 'Form', 'Iframe',
|
|
17
|
+
'Picture', 'Svg', 'Audio', 'Video', 'Hgroup',
|
|
18
|
+
// Text atoms (from Atoms/Text.js)
|
|
19
|
+
'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
|
|
20
|
+
'P', 'Span', 'Strong',
|
|
21
|
+
// Input
|
|
22
|
+
'Input', 'NumberInput', 'Checkbox', 'Radio', 'Toggle', 'Textarea',
|
|
23
|
+
// Components
|
|
24
|
+
'Icon', 'Link', 'Select', 'Button', 'Dialog', 'Tooltip',
|
|
25
|
+
'Avatar', 'Range', 'Dropdown', 'Notification'
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
describe('default-config COMPONENTS manifest', () => {
|
|
29
|
+
it('exposes every required atom + component as a uppercase-keyed entry', () => {
|
|
30
|
+
const missing = REQUIRED.filter(name => !COMPONENTS[name])
|
|
31
|
+
expect(missing).toEqual([])
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('every entry is an object (not a function or primitive)', () => {
|
|
35
|
+
for (const name of REQUIRED) {
|
|
36
|
+
const entry = COMPONENTS[name]
|
|
37
|
+
expect(entry).toBeTruthy()
|
|
38
|
+
expect(typeof entry).toBe('object')
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
import { resolveAsset, buildSourcesAndImg } from '@symbo.ls/utils'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format-negotiated `<picture>`: emits one `<source type="…" srcset="…">`
|
|
7
|
+
* per non-primary format from the asset variant manifest, ending in a
|
|
8
|
+
* fallback `<img src="…">` for the primary (legacy-compatible) format.
|
|
9
|
+
*
|
|
10
|
+
* { extends: 'AssetPicture', src: 'hero.jpg', alt: 'Hero photo' }
|
|
11
|
+
*
|
|
12
|
+
* Asset resolution and source-emission are shared with `Img`/`Video`/
|
|
13
|
+
* `Audio` (see `@symbo.ls/utils#resolveAsset` and `buildSourcesAndImg`):
|
|
14
|
+
* cloud-string URLs, runner-discovered manifests, and literal URLs are
|
|
15
|
+
* all handled transparently. Asset entries without a variants array
|
|
16
|
+
* collapse to a single `<img>` instead of an empty `<source>` chain.
|
|
17
|
+
*
|
|
18
|
+
* Distinct from the existing `Picture` component, which uses
|
|
19
|
+
* `<source media="…">` for theme-based switching. Pick `AssetPicture`
|
|
20
|
+
* when you want format negotiation; pick `Picture` when you want
|
|
21
|
+
* theme/media-query switching.
|
|
22
|
+
*/
|
|
23
|
+
export const AssetPicture = {
|
|
24
|
+
tag: 'picture',
|
|
25
|
+
|
|
26
|
+
// Returning `el.children` inside a `children: (el) => …` function would
|
|
27
|
+
// recursively re-enter it. Return undefined so DOMQL falls through to
|
|
28
|
+
// user-provided children (or none) when no asset resolves.
|
|
29
|
+
children: (el) => {
|
|
30
|
+
const asset = resolveAsset(el)
|
|
31
|
+
if (!asset) return undefined
|
|
32
|
+
return buildSourcesAndImg(asset, el.alt)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
import { resolveAsset } from '@symbo.ls/utils'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `<audio>` mirroring `Video`'s shape — same `src`-prop convention,
|
|
7
|
+
* same smart resolution. See `resolveAsset` in `@symbo.ls/utils`.
|
|
8
|
+
*/
|
|
9
|
+
export const Audio = {
|
|
10
|
+
tag: 'audio',
|
|
11
|
+
|
|
12
|
+
controls: true,
|
|
13
|
+
|
|
14
|
+
attr: {
|
|
15
|
+
src: (el) => {
|
|
16
|
+
const asset = resolveAsset(el)
|
|
17
|
+
return (asset && asset.src) || el.src
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
// Reading `el.children` inside a `children: (el) => …` function would
|
|
22
|
+
// recursively re-enter it. Return undefined to let DOMQL's extends
|
|
23
|
+
// chain pick up user-provided children or fall to childExtends.
|
|
24
|
+
children: (el) => {
|
|
25
|
+
const asset = resolveAsset(el)
|
|
26
|
+
if (!asset || !Array.isArray(asset.variants) || !asset.variants.length) {
|
|
27
|
+
return undefined
|
|
28
|
+
}
|
|
29
|
+
return asset.variants.map((v) => ({
|
|
30
|
+
tag: 'source',
|
|
31
|
+
attr: { src: v.src, type: v.format }
|
|
32
|
+
}))
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
childExtends: {
|
|
36
|
+
tag: 'source'
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
// FRAMEWORK-4: Hgroup's `H` and `P` slots used to render unconditionally,
|
|
4
|
+
// so consumers who wrote `Hgroup: { Name, Submeta }` got phantom empty
|
|
5
|
+
// `<h3></h3><p></p>` rows above their actual content. The `if:` predicate
|
|
6
|
+
// suppresses each slot when the consumer hasn't supplied text, html, or
|
|
7
|
+
// any uppercase-keyed child for it. Self-contained (no closures) so the
|
|
8
|
+
// predicate survives JSON round-trip via deepStringifyFunctions.
|
|
9
|
+
const hasSlotContent = (el) => {
|
|
10
|
+
if (el.text != null) return true
|
|
11
|
+
if (el.html != null) return true
|
|
12
|
+
for (const k in el) {
|
|
13
|
+
const c = k.charCodeAt(0)
|
|
14
|
+
if (c >= 65 && c <= 90) return true
|
|
15
|
+
}
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
18
|
+
|
|
3
19
|
export const Hgroup = {
|
|
4
20
|
display: 'flex',
|
|
5
21
|
tag: 'hgroup',
|
|
@@ -8,12 +24,14 @@ export const Hgroup = {
|
|
|
8
24
|
gap: 'Y2',
|
|
9
25
|
|
|
10
26
|
H: {
|
|
27
|
+
if: hasSlotContent,
|
|
11
28
|
color: 'title',
|
|
12
29
|
tag: 'h3',
|
|
13
30
|
lineHeight: '1em',
|
|
14
31
|
margin: '0'
|
|
15
32
|
},
|
|
16
33
|
P: {
|
|
34
|
+
if: hasSlotContent,
|
|
17
35
|
margin: '0',
|
|
18
36
|
color: 'paragraph'
|
|
19
37
|
}
|
package/components/Atoms/Img.js
CHANGED
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
import { resolveAsset, buildImgSrcset } from '@symbo.ls/utils'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `<img>` with smart asset resolution.
|
|
7
|
+
*
|
|
8
|
+
* `src` is the only prop you set. Resolution order (transparent — see
|
|
9
|
+
* `resolveAsset` in `@symbo.ls/utils`):
|
|
10
|
+
* 1. Cloud-uploaded entry in `ctx.assets`/`ctx.files` → use that URL.
|
|
11
|
+
* 2. Runner-discovered manifest in `ctx.assets`/`ctx.files` → use the
|
|
12
|
+
* primary URL + auto-emit `srcset` from same-format variants.
|
|
13
|
+
* 3. Not found → treat `src` as a literal URL (drop-in `<img>`).
|
|
14
|
+
*
|
|
15
|
+
* Format-negotiated variants (webp/avif fallback chain) need `<picture>`
|
|
16
|
+
* markup — use `AssetPicture` for that.
|
|
17
|
+
*/
|
|
3
18
|
export const Img = {
|
|
4
19
|
tag: 'img',
|
|
5
20
|
|
|
6
21
|
attr: {
|
|
7
|
-
title: (el) => el.title || el.alt
|
|
22
|
+
title: (el) => el.title || el.alt,
|
|
23
|
+
src: (el) => {
|
|
24
|
+
const asset = resolveAsset(el)
|
|
25
|
+
return (asset && asset.src) || el.src
|
|
26
|
+
},
|
|
27
|
+
srcset: (el) => {
|
|
28
|
+
const asset = resolveAsset(el)
|
|
29
|
+
const fromAsset = asset ? buildImgSrcset(asset) : ''
|
|
30
|
+
return fromAsset || el.srcset
|
|
31
|
+
}
|
|
8
32
|
}
|
|
9
33
|
}
|
package/components/Atoms/Svg.js
CHANGED
|
@@ -30,15 +30,48 @@ export const Svg = {
|
|
|
30
30
|
let SVGKey = SVG[symbolId]
|
|
31
31
|
if (SVGKey && SVG[SVGKey]) return useSVGSymbol(SVGKey)
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
// FA-L3: when `el.src` is a sprite symbol NAME (string) but the symbol
|
|
34
|
+
// isn't registered in the merged sprite, the previous code allocated
|
|
35
|
+
// `Math.random()` as the href, producing `<use href="#0.7878391…">`
|
|
36
|
+
// which never resolves and silently renders nothing. That made every
|
|
37
|
+
// missing icon a needle-in-haystack visual bug.
|
|
38
|
+
//
|
|
39
|
+
// New behavior:
|
|
40
|
+
// - If `src` is missing entirely: emit an empty `<use>` (no href).
|
|
41
|
+
// - If `src` is a string (sprite-symbol lookup) but unknown: warn
|
|
42
|
+
// in dev and render no `<use>`. The console-warn surfaces the
|
|
43
|
+
// bad icon name to the consumer.
|
|
44
|
+
// - If `src` is an actual SVG body string (registered raw via
|
|
45
|
+
// `utils.init({svg:{…}})` — the original Math.random path): keep
|
|
46
|
+
// a stable hash-derived key instead of Math.random so the same
|
|
47
|
+
// SVG body always resolves to the same symbol id (and so SSR /
|
|
48
|
+
// hydration produce identical output).
|
|
49
|
+
if (!src) return useSVGSymbol('')
|
|
50
|
+
|
|
51
|
+
if (typeof src === 'string' && !src.includes('<')) {
|
|
52
|
+
// Bare symbol name — not in sprite. Warn loudly in dev so the
|
|
53
|
+
// consumer fixes the icon name; render nothing rather than a
|
|
54
|
+
// broken random href.
|
|
55
|
+
if (typeof console !== 'undefined' && console.warn) {
|
|
56
|
+
console.warn(`[Svg] Unknown sprite symbol "${src}" — not registered in design-system sprite. Set the icon name to one that exists in designSystem.icons / context.svg, or register the SVG body via utils.init({ svg: { '${src}': '<svg>...</svg>' } }).`)
|
|
57
|
+
}
|
|
58
|
+
return useSVGSymbol('')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Inline SVG body — derive a stable sha-like key from the content so
|
|
62
|
+
// identical bodies share a sprite symbol across renders. Avoids
|
|
63
|
+
// Math.random churn in SSR diffs.
|
|
64
|
+
let hash = 0
|
|
65
|
+
for (let i = 0; i < src.length; i++) {
|
|
66
|
+
hash = ((hash << 5) - hash + src.charCodeAt(i)) | 0
|
|
41
67
|
}
|
|
68
|
+
SVGKey = SVG[symbolId] = `svg-${(hash >>> 0).toString(36)}`
|
|
69
|
+
utils.init({
|
|
70
|
+
svg: { [SVGKey]: src }
|
|
71
|
+
}, {
|
|
72
|
+
document: context.document,
|
|
73
|
+
emotion: context.emotion
|
|
74
|
+
})
|
|
42
75
|
|
|
43
76
|
return useSVGSymbol(SVGKey)
|
|
44
77
|
}
|
package/components/Atoms/Text.js
CHANGED
|
@@ -1,10 +1,44 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
import { resolveAsset } from '@symbo.ls/utils'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `<video>` with smart asset resolution. Set `src` to either a manifest
|
|
7
|
+
* key (local-discovered or cloud-uploaded) or a literal URL — the
|
|
8
|
+
* component figures out which one and emits `<source>` children only
|
|
9
|
+
* when the manifest carries multiple variants.
|
|
10
|
+
*
|
|
11
|
+
* Same `src`-prop convention as `<img>` and `<audio>`. See
|
|
12
|
+
* `resolveAsset` in `@symbo.ls/utils` for the normalization rules.
|
|
13
|
+
*/
|
|
3
14
|
export const Video = {
|
|
4
15
|
tag: 'video',
|
|
5
16
|
|
|
6
17
|
controls: true,
|
|
7
18
|
|
|
19
|
+
attr: {
|
|
20
|
+
src: (el) => {
|
|
21
|
+
const asset = resolveAsset(el)
|
|
22
|
+
return (asset && asset.src) || el.src
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
// Auto-populate <source> children only when the resolved asset carries
|
|
27
|
+
// a variants array. Returns undefined otherwise so DOMQL's extends
|
|
28
|
+
// chain falls through to user-provided `children: [...]` when present
|
|
29
|
+
// (override semantics) or to the empty list. Reading `el.children`
|
|
30
|
+
// inside this function would recursively re-enter it — never do that.
|
|
31
|
+
children: (el) => {
|
|
32
|
+
const asset = resolveAsset(el)
|
|
33
|
+
if (!asset || !Array.isArray(asset.variants) || !asset.variants.length) {
|
|
34
|
+
return undefined
|
|
35
|
+
}
|
|
36
|
+
return asset.variants.map((v) => ({
|
|
37
|
+
tag: 'source',
|
|
38
|
+
attr: { src: v.src, type: v.format }
|
|
39
|
+
}))
|
|
40
|
+
},
|
|
41
|
+
|
|
8
42
|
childExtends: {
|
|
9
43
|
tag: 'source'
|
|
10
44
|
}
|
|
@@ -20,11 +20,16 @@ export const Tooltip = {
|
|
|
20
20
|
attr: { tooltip: true },
|
|
21
21
|
|
|
22
22
|
Title: {
|
|
23
|
-
|
|
23
|
+
// FA106: canonical signature is `(el, s)` — destructure-from-object is
|
|
24
|
+
// banned because frank's audit flags it and it's inconsistent with
|
|
25
|
+
// every other reactive prop in the codebase. The `({ parent }) =>`
|
|
26
|
+
// shape happens to work at runtime (the destructure pulls `parent`
|
|
27
|
+
// out of `el`) but breaks on any project that runs frank-audit.
|
|
28
|
+
if: (el) => isDefined(el.parent.title) || el.parent.title,
|
|
24
29
|
width: 'fit-content',
|
|
25
30
|
fontWeight: 500,
|
|
26
31
|
color: 'gray12',
|
|
27
|
-
text: (
|
|
32
|
+
text: (el) => el.parent.title
|
|
28
33
|
},
|
|
29
34
|
|
|
30
35
|
P: {
|
package/components/index.js
CHANGED
|
@@ -1,21 +1,60 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
// Atoms
|
|
4
|
+
import * as _Block from './Atoms/Block.js'
|
|
5
|
+
import * as _Img from './Atoms/Img.js'
|
|
6
|
+
import * as _Form from './Atoms/Form.js'
|
|
7
|
+
import * as _Iframe from './Atoms/Iframe.js'
|
|
8
|
+
import * as _InteractiveComponent from './Atoms/InteractiveComponent.js'
|
|
9
|
+
import * as _Picture from './Atoms/Picture.js'
|
|
10
|
+
import * as _AssetPicture from './Atoms/AssetPicture.js'
|
|
11
|
+
import * as _Svg from './Atoms/Svg.js'
|
|
12
|
+
import * as _Shape from './Atoms/Shape.js'
|
|
13
|
+
import * as _Video from './Atoms/Video.js'
|
|
14
|
+
import * as _Audio from './Atoms/Audio.js'
|
|
15
|
+
import * as _Theme from './Atoms/Theme.js'
|
|
16
|
+
import * as _Text from './Atoms/Text.js'
|
|
17
|
+
import * as _Box from './Atoms/Box.js'
|
|
18
|
+
import * as _Hgroup from './Atoms/Hgroup.js'
|
|
19
|
+
|
|
20
|
+
// Input
|
|
21
|
+
import * as _Input from './Input/Input.js'
|
|
22
|
+
import * as _NumberInput from './Input/NumberInput.js'
|
|
23
|
+
import * as _Checkbox from './Input/Checkbox.js'
|
|
24
|
+
import * as _Radio from './Input/Radio.js'
|
|
25
|
+
import * as _Toggle from './Input/Toggle.js'
|
|
26
|
+
import * as _Textarea from './Input/Textarea.js'
|
|
27
|
+
|
|
28
|
+
// Components
|
|
29
|
+
import * as _Icon from './Icon/index.js'
|
|
30
|
+
import * as _Link from './Link.js'
|
|
31
|
+
import * as _Select from './Select.js'
|
|
32
|
+
import * as _Button from './Button.js'
|
|
33
|
+
import * as _Dialog from './Dialog.js'
|
|
34
|
+
import * as _Tooltip from './Tooltip/index.js'
|
|
35
|
+
import * as _Avatar from './Avatar.js'
|
|
36
|
+
import * as _Range from './Range.js'
|
|
37
|
+
import * as _Dropdown from './Dropdown.js'
|
|
38
|
+
import * as _Notification from './Notification.js'
|
|
39
|
+
|
|
40
|
+
// Re-export named exports for direct consumers (e.g.
|
|
41
|
+
// `import { Box } from '@symbo.ls/default-config/components'`).
|
|
4
42
|
export * from './Atoms/Block.js'
|
|
5
43
|
export * from './Atoms/Img.js'
|
|
6
44
|
export * from './Atoms/Form.js'
|
|
7
45
|
export * from './Atoms/Iframe.js'
|
|
8
46
|
export * from './Atoms/InteractiveComponent.js'
|
|
9
47
|
export * from './Atoms/Picture.js'
|
|
48
|
+
export * from './Atoms/AssetPicture.js'
|
|
10
49
|
export * from './Atoms/Svg.js'
|
|
11
50
|
export * from './Atoms/Shape.js'
|
|
12
51
|
export * from './Atoms/Video.js'
|
|
52
|
+
export * from './Atoms/Audio.js'
|
|
13
53
|
export * from './Atoms/Theme.js'
|
|
14
54
|
export * from './Atoms/Text.js'
|
|
15
55
|
export * from './Atoms/Box.js'
|
|
16
56
|
export * from './Atoms/Hgroup.js'
|
|
17
57
|
|
|
18
|
-
// Input
|
|
19
58
|
export * from './Input/Input.js'
|
|
20
59
|
export * from './Input/NumberInput.js'
|
|
21
60
|
export * from './Input/Checkbox.js'
|
|
@@ -23,7 +62,6 @@ export * from './Input/Radio.js'
|
|
|
23
62
|
export * from './Input/Toggle.js'
|
|
24
63
|
export * from './Input/Textarea.js'
|
|
25
64
|
|
|
26
|
-
// Components
|
|
27
65
|
export * from './Icon/index.js'
|
|
28
66
|
export * from './Link.js'
|
|
29
67
|
export * from './Select.js'
|
|
@@ -34,3 +72,20 @@ export * from './Avatar.js'
|
|
|
34
72
|
export * from './Range.js'
|
|
35
73
|
export * from './Dropdown.js'
|
|
36
74
|
export * from './Notification.js'
|
|
75
|
+
|
|
76
|
+
// FT-FRAMEWORK-1: explicit components manifest. Built by spreading every
|
|
77
|
+
// atom/component module's named exports into one plain object. Parcel
|
|
78
|
+
// (under smbls's `sideEffects: false`) sometimes pruned exports off the
|
|
79
|
+
// `import * as uikit from '...'` namespace that prepare.js iterated, so
|
|
80
|
+
// consumer projects saw atoms missing from `context.components`. This
|
|
81
|
+
// dict is bundler-agnostic — it's a regular object literal whose
|
|
82
|
+
// references are statically locked into the module by `Object.assign`.
|
|
83
|
+
export const COMPONENTS = Object.assign(
|
|
84
|
+
{},
|
|
85
|
+
_Block, _Img, _Form, _Iframe, _InteractiveComponent,
|
|
86
|
+
_Picture, _AssetPicture, _Svg, _Shape, _Video, _Audio,
|
|
87
|
+
_Theme, _Text, _Box, _Hgroup,
|
|
88
|
+
_Input, _NumberInput, _Checkbox, _Radio, _Toggle, _Textarea,
|
|
89
|
+
_Icon, _Link, _Select, _Button, _Dialog, _Tooltip,
|
|
90
|
+
_Avatar, _Range, _Dropdown, _Notification
|
|
91
|
+
)
|