@c9up/nebula 0.1.5 → 0.1.7
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/dist/adapters/tailwind.d.ts +7 -0
- package/dist/adapters/tailwind.js +11 -1
- package/dist/adapters/unocss.js +11 -2
- package/dist/atoms/Image.d.ts +106 -0
- package/dist/atoms/Image.js +139 -0
- package/dist/atoms/NativeSelect.js +27 -2
- package/dist/atoms/index.d.ts +1 -0
- package/dist/atoms/index.js +1 -0
- package/dist/lib/image.d.ts +213 -0
- package/dist/lib/image.js +331 -0
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +1 -0
- package/dist/lib/motion.d.ts +25 -16
- package/dist/lib/motion.js +30 -24
- package/dist/molecules/Picture.d.ts +46 -0
- package/dist/molecules/Picture.js +62 -0
- package/dist/molecules/index.d.ts +1 -0
- package/dist/molecules/index.js +1 -0
- package/dist/organisms/Dialog.d.ts +11 -4
- package/dist/organisms/Dialog.js +2 -2
- package/dist/organisms/Sidebar.d.ts +9 -0
- package/dist/organisms/Sidebar.js +5 -2
- package/dist/primitives/floatingSurface.js +17 -8
- package/dist/primitives/presence.js +401 -5
- package/nebula.css +1 -1
- package/package.json +7 -6
- package/registry.json +25 -0
- package/src/adapters/tailwind.ts +11 -1
- package/src/adapters/unocss.ts +11 -2
- package/src/atoms/Image.ts +226 -0
- package/src/atoms/NativeSelect.ts +27 -2
- package/src/atoms/index.ts +10 -0
- package/src/lib/image.ts +470 -0
- package/src/lib/index.ts +25 -0
- package/src/lib/motion.ts +30 -26
- package/src/molecules/Picture.ts +92 -0
- package/src/molecules/index.ts +1 -0
- package/src/organisms/Dialog.ts +13 -6
- package/src/organisms/Sidebar.ts +15 -2
- package/src/primitives/floatingSurface.ts +18 -9
- package/src/primitives/presence.ts +428 -5
- package/theme.css +0 -35
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image — one `<img>`, with the attributes that make it fast already filled in.
|
|
3
|
+
*
|
|
4
|
+
* A bare `<img src>` costs a page twice: it ships desktop pixels to a phone,
|
|
5
|
+
* and it reserves no space until it arrives, so everything under it jumps.
|
|
6
|
+
* Both are solved by attributes nobody writes by hand — a `srcset` of a dozen
|
|
7
|
+
* widths, a matching `sizes`, `width`/`height` for the ratio, `loading` and
|
|
8
|
+
* `decoding`. This component writes them.
|
|
9
|
+
*
|
|
10
|
+
* The bytes come from whatever {@link setImageResolver} is pointed at; the
|
|
11
|
+
* component only decides which variants to ask for. See `lib/image.ts` for
|
|
12
|
+
* why the split is there and what the resolver is allowed to be asked.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* Image({ src: "/photos/hero.jpg", alt: "…", width: 1200, height: 800 })
|
|
16
|
+
* Image({ src: "/logo.png", alt: "…", width: 48, height: 48, layout: "fixed" })
|
|
17
|
+
* Image({ src: "/banner.jpg", alt: "…", layout: "full-width", priority: true })
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* `alt` is required, and not as a gesture: the attribute is what a screen
|
|
21
|
+
* reader announces, and an `<img>` without one is read out as its file name.
|
|
22
|
+
* Decorative images pass `alt: ""`, which is the markup that says "skip me"
|
|
23
|
+
* — a deliberate empty string, not a forgotten prop.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { component, html } from "@c9up/aurora";
|
|
27
|
+
import { cn } from "../lib/cn.js";
|
|
28
|
+
import {
|
|
29
|
+
densitySrcSet,
|
|
30
|
+
fitClass,
|
|
31
|
+
type ImageFit,
|
|
32
|
+
type ImageFormat,
|
|
33
|
+
type ImageLayout,
|
|
34
|
+
type ImagePosition,
|
|
35
|
+
imageSizes,
|
|
36
|
+
imageSrcSet,
|
|
37
|
+
imageUrl,
|
|
38
|
+
imageWidths,
|
|
39
|
+
layoutClasses,
|
|
40
|
+
offeredWidth,
|
|
41
|
+
positionClass,
|
|
42
|
+
} from "../lib/image.js";
|
|
43
|
+
import { type Reactive, read, readOr } from "../lib/props.js";
|
|
44
|
+
|
|
45
|
+
export interface ImageProps {
|
|
46
|
+
src: Reactive<string>;
|
|
47
|
+
/** Required. `""` for a decorative image, which is what hides it. */
|
|
48
|
+
alt: Reactive<string>;
|
|
49
|
+
/**
|
|
50
|
+
* Displayed width in pixels, and the `width` attribute.
|
|
51
|
+
*
|
|
52
|
+
* Together with `height` it is what reserves the space before the bytes
|
|
53
|
+
* arrive. Optional only because `full-width` does not need it.
|
|
54
|
+
*/
|
|
55
|
+
width?: Reactive<number | undefined>;
|
|
56
|
+
height?: Reactive<number | undefined>;
|
|
57
|
+
/** Defaults to `constrained`. */
|
|
58
|
+
layout?: Reactive<ImageLayout>;
|
|
59
|
+
/** CSS `object-fit`. The image is never cropped server-side. */
|
|
60
|
+
fit?: Reactive<ImageFit | undefined>;
|
|
61
|
+
/** CSS `object-position`, for when `fit` crops. */
|
|
62
|
+
position?: Reactive<ImagePosition | undefined>;
|
|
63
|
+
/** Output format to request. Defaults to `webp`. */
|
|
64
|
+
format?: Reactive<ImageFormat | undefined>;
|
|
65
|
+
/** 1-100. The endpoint decides which values it accepts. */
|
|
66
|
+
quality?: Reactive<number | undefined>;
|
|
67
|
+
/** Override the generated width ladder. Pair it with `sizes`. */
|
|
68
|
+
widths?: Reactive<readonly number[] | undefined>;
|
|
69
|
+
/**
|
|
70
|
+
* Serve by pixel ratio instead of width, e.g. `[1, 2]`.
|
|
71
|
+
*
|
|
72
|
+
* Mutually exclusive with `widths` — a `srcset` mixing `w` and `x`
|
|
73
|
+
* descriptors is invalid, and a browser that sees one ignores the
|
|
74
|
+
* attribute entirely. `widths` wins if both are given.
|
|
75
|
+
*/
|
|
76
|
+
densities?: Reactive<readonly number[] | undefined>;
|
|
77
|
+
/** Override the generated `sizes`. */
|
|
78
|
+
sizes?: Reactive<string | undefined>;
|
|
79
|
+
/** Width ladder to draw from. Defaults to the device-width ladder. */
|
|
80
|
+
breakpoints?: Reactive<readonly number[] | undefined>;
|
|
81
|
+
/** The source's own width, when known. Stops variants past it being offered. */
|
|
82
|
+
originalWidth?: Reactive<number | undefined>;
|
|
83
|
+
/**
|
|
84
|
+
* Above the fold: load it now.
|
|
85
|
+
*
|
|
86
|
+
* Sets `loading="eager"`, `decoding="sync"` and `fetchpriority="high"`
|
|
87
|
+
* together, because setting one without the others is the usual reason a
|
|
88
|
+
* hero image is still not the first thing painted.
|
|
89
|
+
*/
|
|
90
|
+
priority?: Reactive<boolean>;
|
|
91
|
+
loading?: Reactive<"lazy" | "eager" | undefined>;
|
|
92
|
+
decoding?: Reactive<"async" | "sync" | "auto" | undefined>;
|
|
93
|
+
fetchPriority?: Reactive<"high" | "low" | "auto" | undefined>;
|
|
94
|
+
class?: Reactive<string>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The widths this image offers, honouring an explicit `widths` prop.
|
|
99
|
+
*
|
|
100
|
+
* Exported because `Picture` needs exactly the same ladder for every one of
|
|
101
|
+
* its `<source>` elements — computing it twice is how the AVIF and WebP
|
|
102
|
+
* variants end up offering different sizes.
|
|
103
|
+
*/
|
|
104
|
+
export function resolvedWidths(props: ImageProps): number[] {
|
|
105
|
+
const explicit = read(props.widths);
|
|
106
|
+
if (explicit !== undefined) return [...explicit];
|
|
107
|
+
return imageWidths({
|
|
108
|
+
width: read(props.width),
|
|
109
|
+
layout: readOr(props.layout, "constrained"),
|
|
110
|
+
breakpoints: read(props.breakpoints),
|
|
111
|
+
originalWidth: read(props.originalWidth),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The `srcset` for one format, in whichever descriptor style applies. */
|
|
116
|
+
export function resolvedSrcSet(
|
|
117
|
+
props: ImageProps,
|
|
118
|
+
format: ImageFormat | undefined,
|
|
119
|
+
): string | undefined {
|
|
120
|
+
const quality = read(props.quality);
|
|
121
|
+
const width = read(props.width);
|
|
122
|
+
const densities = read(props.densities);
|
|
123
|
+
|
|
124
|
+
// `widths` wins: an explicit ladder is the more specific instruction, and
|
|
125
|
+
// honouring both would emit the invalid mixed-descriptor set.
|
|
126
|
+
if (densities !== undefined && read(props.widths) === undefined) {
|
|
127
|
+
if (width === undefined) return undefined;
|
|
128
|
+
return densitySrcSet({
|
|
129
|
+
src: read(props.src),
|
|
130
|
+
width,
|
|
131
|
+
densities,
|
|
132
|
+
format,
|
|
133
|
+
quality,
|
|
134
|
+
originalWidth: read(props.originalWidth),
|
|
135
|
+
breakpoints: read(props.breakpoints),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return imageSrcSet({
|
|
139
|
+
src: read(props.src),
|
|
140
|
+
widths: resolvedWidths(props),
|
|
141
|
+
format,
|
|
142
|
+
quality,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The `sizes` for this image, or `undefined` when it carries `x` descriptors. */
|
|
147
|
+
export function resolvedSizes(props: ImageProps): string | undefined {
|
|
148
|
+
const explicit = read(props.sizes);
|
|
149
|
+
if (explicit !== undefined) return explicit;
|
|
150
|
+
// Density srcsets describe themselves; a `sizes` alongside them means
|
|
151
|
+
// nothing and browsers ignore it.
|
|
152
|
+
if (read(props.densities) !== undefined && read(props.widths) === undefined) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
return imageSizes({
|
|
156
|
+
width: read(props.width),
|
|
157
|
+
layout: readOr(props.layout, "constrained"),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The `src` a browser falls back to.
|
|
163
|
+
*
|
|
164
|
+
* The declared width rather than the largest variant: it is what a browser
|
|
165
|
+
* with no `srcset` support (and every crawler that reads the attribute
|
|
166
|
+
* literally) will fetch.
|
|
167
|
+
*/
|
|
168
|
+
export function resolvedSrc(
|
|
169
|
+
props: ImageProps,
|
|
170
|
+
format: ImageFormat | undefined,
|
|
171
|
+
): string {
|
|
172
|
+
const src = read(props.src);
|
|
173
|
+
const width = read(props.width);
|
|
174
|
+
if (width === undefined) return src;
|
|
175
|
+
return imageUrl(src, {
|
|
176
|
+
// Snapped like every other entry: the declared width is almost never a
|
|
177
|
+
// width the endpoint offers, and this URL is the one a browser without
|
|
178
|
+
// `srcset` support fetches.
|
|
179
|
+
width: offeredWidth(width, read(props.breakpoints)),
|
|
180
|
+
format,
|
|
181
|
+
quality: read(props.quality),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The class list for the layout, fit and position of an image. */
|
|
186
|
+
export function imageClasses(props: ImageProps): string {
|
|
187
|
+
return cn(
|
|
188
|
+
layoutClasses(readOr(props.layout, "constrained")),
|
|
189
|
+
fitClass(read(props.fit)),
|
|
190
|
+
positionClass(read(props.position)),
|
|
191
|
+
read(props.class),
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** `loading`, `decoding` and `fetchpriority`, with `priority` applied. */
|
|
196
|
+
export function loadingAttributes(props: ImageProps): {
|
|
197
|
+
loading: "lazy" | "eager";
|
|
198
|
+
decoding: "async" | "sync" | "auto";
|
|
199
|
+
fetchPriority: "high" | "low" | "auto" | undefined;
|
|
200
|
+
} {
|
|
201
|
+
const priority = readOr(props.priority, false);
|
|
202
|
+
return {
|
|
203
|
+
loading: read(props.loading) ?? (priority ? "eager" : "lazy"),
|
|
204
|
+
decoding: read(props.decoding) ?? (priority ? "sync" : "async"),
|
|
205
|
+
fetchPriority: read(props.fetchPriority) ?? (priority ? "high" : undefined),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export const Image = component<ImageProps>((props) => {
|
|
210
|
+
const format = (): ImageFormat | undefined => read(props.format) ?? "webp";
|
|
211
|
+
|
|
212
|
+
return html`<img
|
|
213
|
+
data-slot="image"
|
|
214
|
+
data-layout="${() => readOr(props.layout, "constrained")}"
|
|
215
|
+
src="${() => resolvedSrc(props, format())}"
|
|
216
|
+
srcset="${() => resolvedSrcSet(props, format())}"
|
|
217
|
+
sizes="${() => resolvedSizes(props)}"
|
|
218
|
+
alt="${() => read(props.alt)}"
|
|
219
|
+
width="${() => read(props.width)}"
|
|
220
|
+
height="${() => read(props.height)}"
|
|
221
|
+
loading="${() => loadingAttributes(props).loading}"
|
|
222
|
+
decoding="${() => loadingAttributes(props).decoding}"
|
|
223
|
+
fetchpriority="${() => loadingAttributes(props).fetchPriority}"
|
|
224
|
+
class="${() => imageClasses(props)}"
|
|
225
|
+
/>`;
|
|
226
|
+
});
|
|
@@ -72,8 +72,26 @@ export const NativeSelect = component<NativeSelectProps>((props) => {
|
|
|
72
72
|
return out;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* `?selected` on each option, NOT `.value` on the select alone.
|
|
77
|
+
*
|
|
78
|
+
* A property binding sits on the opening tag, so it is assigned before the
|
|
79
|
+
* options exist. `select.value = 'x'` against a childless `<select>` is a
|
|
80
|
+
* no-op the DOM does not report, and nothing re-runs it afterwards: the
|
|
81
|
+
* control then shows its FIRST option while holding the value it was given.
|
|
82
|
+
* Nothing throws and nothing logs, so the value the user sees is not the
|
|
83
|
+
* value the form will submit — in a form that edits money or tax status
|
|
84
|
+
* that is a wrong record written on save.
|
|
85
|
+
*
|
|
86
|
+
* Marking the option is order-independent, and it is also what makes the
|
|
87
|
+
* choice correct in server-rendered HTML before any script runs.
|
|
88
|
+
*/
|
|
75
89
|
const renderOption = (option: NativeSelectOption) =>
|
|
76
|
-
html`<option
|
|
90
|
+
html`<option
|
|
91
|
+
value="${option.value}"
|
|
92
|
+
?disabled="${option.disabled === true}"
|
|
93
|
+
?selected="${() => read(props.value) === option.value}"
|
|
94
|
+
>
|
|
77
95
|
${option.label}
|
|
78
96
|
</option>`;
|
|
79
97
|
|
|
@@ -92,7 +110,14 @@ export const NativeSelect = component<NativeSelectProps>((props) => {
|
|
|
92
110
|
${
|
|
93
111
|
props.placeholder === undefined
|
|
94
112
|
? null
|
|
95
|
-
: html`<option
|
|
113
|
+
: html`<option
|
|
114
|
+
value=""
|
|
115
|
+
disabled
|
|
116
|
+
?selected="${() => {
|
|
117
|
+
const current = read(props.value);
|
|
118
|
+
return current === undefined || current === "";
|
|
119
|
+
}}"
|
|
120
|
+
>${props.placeholder}</option>`
|
|
96
121
|
}
|
|
97
122
|
${runs().map((run) =>
|
|
98
123
|
run.group === undefined
|
package/src/atoms/index.ts
CHANGED
|
@@ -27,6 +27,16 @@ export {
|
|
|
27
27
|
buttonVariants,
|
|
28
28
|
} from "./Button.js";
|
|
29
29
|
export { Checkbox, type CheckboxProps, checkboxClasses } from "./Checkbox.js";
|
|
30
|
+
export {
|
|
31
|
+
Image,
|
|
32
|
+
type ImageProps,
|
|
33
|
+
imageClasses,
|
|
34
|
+
loadingAttributes,
|
|
35
|
+
resolvedSizes,
|
|
36
|
+
resolvedSrc,
|
|
37
|
+
resolvedSrcSet,
|
|
38
|
+
resolvedWidths,
|
|
39
|
+
} from "./Image.js";
|
|
30
40
|
export { Input, type InputProps, inputClasses } from "./Input.js";
|
|
31
41
|
export { Kbd, type KbdProps } from "./Kbd.js";
|
|
32
42
|
export { Label, type LabelProps, labelClasses } from "./Label.js";
|