@symbo.ls/default-config 3.8.9 → 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.
Files changed (54) hide show
  1. package/README.md +4 -1
  2. package/__tests__/assetMedia.test.js +242 -0
  3. package/__tests__/componentsManifest.test.js +41 -0
  4. package/blank/font.js +1 -1
  5. package/blank/index.js +8 -8
  6. package/components/Atoms/AssetPicture.js +34 -0
  7. package/components/Atoms/Audio.js +38 -0
  8. package/components/Atoms/Block.js +29 -0
  9. package/components/Atoms/Box.js +37 -0
  10. package/components/Atoms/Form.js +5 -0
  11. package/components/Atoms/Grid.js +37 -0
  12. package/components/Atoms/Hgroup.js +75 -0
  13. package/components/Atoms/Iframe.js +8 -0
  14. package/components/Atoms/Img.js +33 -0
  15. package/components/Atoms/InteractiveComponent.js +65 -0
  16. package/components/Atoms/Picture.js +34 -0
  17. package/components/Atoms/Shape/index.js +3 -0
  18. package/components/Atoms/Shape.js +249 -0
  19. package/components/Atoms/Svg.js +78 -0
  20. package/components/Atoms/Text.js +45 -0
  21. package/components/Atoms/Theme.js +20 -0
  22. package/components/Atoms/Video.js +45 -0
  23. package/components/Avatar.js +10 -0
  24. package/components/Button.js +72 -0
  25. package/components/Dialog.js +69 -0
  26. package/components/Dropdown.js +153 -0
  27. package/components/Icon/index.js +236 -0
  28. package/components/Icon/style.js +8 -0
  29. package/components/Input/Checkbox.js +54 -0
  30. package/components/Input/Input.js +37 -0
  31. package/components/Input/NumberInput.js +25 -0
  32. package/components/Input/Radio.js +31 -0
  33. package/components/Input/Textarea.js +47 -0
  34. package/components/Input/Toggle.js +36 -0
  35. package/components/Link.js +166 -0
  36. package/components/Notification.js +43 -0
  37. package/components/Range.js +106 -0
  38. package/components/Select.js +33 -0
  39. package/components/Tooltip/index.js +133 -0
  40. package/components/Tooltip/style.js +12 -0
  41. package/components/index.js +91 -0
  42. package/default/font.js +1 -1
  43. package/default/index.js +2 -2
  44. package/designSystem/color.js +70 -0
  45. package/designSystem/font.js +21 -0
  46. package/designSystem/icons.js +69 -0
  47. package/designSystem/index.js +45 -0
  48. package/designSystem/media.js +31 -0
  49. package/designSystem/spacing.js +6 -0
  50. package/designSystem/theme.js +247 -0
  51. package/designSystem/timing.js +8 -0
  52. package/designSystem/typography.js +9 -0
  53. package/index.js +15 -0
  54. package/package.json +4 -1
package/README.md CHANGED
@@ -1,2 +1,5 @@
1
1
  # Default Config
2
- This is a default repository for Symbols Design Systems. It shows how it works and provides some default propoerties. Check out the [docs page](http://symbols.app/developersintro#configuration) to learn more.
2
+
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
+
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
+ })
package/blank/font.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  export const font = {}
4
4
 
5
- export const font_family = {
5
+ export const fontFamily = {
6
6
  system: {
7
7
  value: ['"Helvetica Neue"', 'Helvetica', 'Arial'],
8
8
  type: 'sans-serif'
package/blank/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  'use strict'
2
2
 
3
- import { color, gradient } from './color'
4
- import { theme } from './theme'
5
- import { typography } from './typography'
6
- import { spacing } from './spacing'
7
- import { font, font_family } from './font'
8
- import { media } from './media'
9
- import { timing } from './timing'
3
+ import { color, gradient } from './color.js'
4
+ import { theme } from './theme.js'
5
+ import { typography } from './typography.js'
6
+ import { spacing } from './spacing.js'
7
+ import { font, fontFamily } from './font.js'
8
+ import { media } from './media.js'
9
+ import { timing } from './timing.js'
10
10
 
11
11
  export const DEFAULT_CONFIG = {
12
12
  version: '0.0.1',
@@ -16,7 +16,7 @@ export const DEFAULT_CONFIG = {
16
16
  typography,
17
17
  spacing,
18
18
  font,
19
- font_family,
19
+ fontFamily,
20
20
  timing,
21
21
  media,
22
22
  reset: {
@@ -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
+ }
@@ -0,0 +1,29 @@
1
+ 'use strict'
2
+
3
+ export const Block = {
4
+ display: 'block'
5
+ }
6
+
7
+ export const Inline = {
8
+ display: 'inline'
9
+ }
10
+
11
+ export const Flex = {
12
+ display: 'flex'
13
+ }
14
+
15
+ export const InlineFlex = {
16
+ display: 'inline-flex'
17
+ }
18
+
19
+ export const Grid = {
20
+ display: 'grid'
21
+ }
22
+
23
+ export const InlineGrid = {
24
+ display: 'inline-grid'
25
+ }
26
+
27
+ export const Gutter = {
28
+ boxSize: 'C1'
29
+ }
@@ -0,0 +1,37 @@
1
+ 'use strict'
2
+
3
+ import { useCssInProps } from 'css-in-props'
4
+
5
+ // Main class assignment handler
6
+ const onBeforeClassAssign = (element) => {
7
+ if (!element.context) return
8
+ const { props, __ref: ref } = element
9
+ const result = useCssInProps(props, element, { unpack: false })
10
+ ref.__class = result
11
+ }
12
+
13
+ export const Box = {
14
+ extends: [
15
+ 'Shape',
16
+ 'Theme'
17
+ ],
18
+
19
+ boxSizing: 'border-box',
20
+
21
+ onBeforeClassAssign
22
+ }
23
+
24
+ export const Hr = {
25
+ tag: 'hr',
26
+ margin: 'C1 0'
27
+ }
28
+ export const Br = { tag: 'br' }
29
+ export const Li = { tag: 'li' }
30
+ export const Ul = {
31
+ tag: 'ul',
32
+ childExtends: { extends: 'Li' }
33
+ }
34
+ export const Ol = {
35
+ tag: 'ol',
36
+ childExtends: { extends: 'Li' }
37
+ }
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ export const Form = {
4
+ tag: 'form',
5
+ }
@@ -0,0 +1,37 @@
1
+ 'use strict'
2
+
3
+ export const Grid = {
4
+ display: 'grid',
5
+
6
+ class: {
7
+ area: (el) => (el.area ? { gridArea: el.area } : null),
8
+ template: (el) =>
9
+ el.template ? { gridTemplate: el.template } : null,
10
+ templateAreas: (el) =>
11
+ el.templateAreas ? { gridTemplateAreas: el.templateAreas } : null,
12
+
13
+ column: (el) => (el.column ? { gridColumn: el.column } : null),
14
+ columns: (el) =>
15
+ el.columns ? { gridTemplateColumns: el.columns } : null,
16
+ templateColumns: (el) =>
17
+ el.templateColumns
18
+ ? { gridTemplateColumns: el.templateColumns }
19
+ : null,
20
+ autoColumns: (el) =>
21
+ el.autoColumns ? { gridAutoColumns: el.autoColumns } : null,
22
+ columnStart: (el) =>
23
+ el.columnStart ? { gridColumnStart: el.columnStart } : null,
24
+
25
+ row: (el) => (el.row ? { gridRow: el.row } : null),
26
+ rows: (el) => (el.rows ? { gridTemplateRows: el.rows } : null),
27
+ templateRows: (el) =>
28
+ el.templateRows ? { gridTemplateRows: el.templateRows } : null,
29
+ autoRows: (el) =>
30
+ el.autoRows ? { gridAutoRows: el.autoRows } : null,
31
+ rowStart: (el) =>
32
+ el.rowStart ? { gridRowStart: el.rowStart } : null,
33
+
34
+ autoFlow: (el) =>
35
+ el.autoFlow ? { gridAutoFlow: el.autoFlow } : null
36
+ }
37
+ }
@@ -0,0 +1,75 @@
1
+ 'use strict'
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
+
19
+ export const Hgroup = {
20
+ display: 'flex',
21
+ tag: 'hgroup',
22
+
23
+ flow: 'y',
24
+ gap: 'Y2',
25
+
26
+ H: {
27
+ if: hasSlotContent,
28
+ color: 'title',
29
+ tag: 'h3',
30
+ lineHeight: '1em',
31
+ margin: '0'
32
+ },
33
+ P: {
34
+ if: hasSlotContent,
35
+ margin: '0',
36
+ color: 'paragraph'
37
+ }
38
+ }
39
+
40
+ export const HgroupRows = {
41
+ extends: 'Hgroup',
42
+
43
+ H: {
44
+ display: 'flex',
45
+ color: 'title',
46
+ align: 'center space-between'
47
+ },
48
+
49
+ P: {
50
+ color: 'paragraph',
51
+ align: 'center space-between'
52
+ }
53
+ }
54
+
55
+ export const HgroupButton = {
56
+ extends: 'HgroupRows',
57
+
58
+ H: {
59
+ justifyContent: 'space-between',
60
+
61
+ Span: {},
62
+
63
+ Button: {
64
+ background: 'transparent',
65
+ color: 'currentColor',
66
+ padding: '0',
67
+ Icon: {
68
+ name: 'x',
69
+ fontSize: 'C'
70
+ }
71
+ }
72
+ },
73
+
74
+ P: {}
75
+ }
@@ -0,0 +1,8 @@
1
+ 'use strict'
2
+
3
+ export const Iframe = {
4
+ tag: 'iframe',
5
+ position: 'relative',
6
+ minWidth: 'H',
7
+ minHeight: 'H'
8
+ }
@@ -0,0 +1,33 @@
1
+ 'use strict'
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
+ */
18
+ export const Img = {
19
+ tag: 'img',
20
+
21
+ attr: {
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
+ }
32
+ }
33
+ }