@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
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
* a v4 setup produces an unstyled page.
|
|
15
15
|
* - `@custom-variant dark` makes `dark:` respond to a `.dark` class rather
|
|
16
16
|
* than the system preference, so a theme toggle can override the OS.
|
|
17
|
+
* - `tw-animate-css` supplies `animate-in` / `animate-out` and the `fade-*`,
|
|
18
|
+
* `zoom-*` and `slide-*` modifiers — where shadcn gets them too. Imported
|
|
19
|
+
* rather than reimplemented, so the components use its class strings
|
|
20
|
+
* verbatim. nebula previously declared four keyframes of its own and reached
|
|
21
|
+
* them through arbitrary animation values, which Tailwind compiles whether or
|
|
22
|
+
* not the keyframes exist: an app that skipped the stylesheet got overlays
|
|
23
|
+
* that never finished CLOSING rather than overlays that never animated.
|
|
17
24
|
* - `@theme inline` maps nebula's plain custom properties onto Tailwind's
|
|
18
25
|
* colour namespace, which is what turns `--primary` into a `bg-primary`
|
|
19
26
|
* utility.
|
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
* a v4 setup produces an unstyled page.
|
|
15
15
|
* - `@custom-variant dark` makes `dark:` respond to a `.dark` class rather
|
|
16
16
|
* than the system preference, so a theme toggle can override the OS.
|
|
17
|
+
* - `tw-animate-css` supplies `animate-in` / `animate-out` and the `fade-*`,
|
|
18
|
+
* `zoom-*` and `slide-*` modifiers — where shadcn gets them too. Imported
|
|
19
|
+
* rather than reimplemented, so the components use its class strings
|
|
20
|
+
* verbatim. nebula previously declared four keyframes of its own and reached
|
|
21
|
+
* them through arbitrary animation values, which Tailwind compiles whether or
|
|
22
|
+
* not the keyframes exist: an app that skipped the stylesheet got overlays
|
|
23
|
+
* that never finished CLOSING rather than overlays that never animated.
|
|
17
24
|
* - `@theme inline` maps nebula's plain custom properties onto Tailwind's
|
|
18
25
|
* colour namespace, which is what turns `--primary` into a `bg-primary`
|
|
19
26
|
* utility.
|
|
@@ -123,7 +130,10 @@ function relativeFromCss(config) {
|
|
|
123
130
|
export const tailwindAdapter = {
|
|
124
131
|
name: "tailwind",
|
|
125
132
|
summary: "Tailwind CSS v4, configured in CSS. What shadcn/ui itself targets.",
|
|
126
|
-
|
|
133
|
+
// `tw-animate-css` is where the overlay animations come from — the same
|
|
134
|
+
// package shadcn tells you to install, and the reason the components can use
|
|
135
|
+
// its class strings verbatim.
|
|
136
|
+
packages: ["tailwindcss", "@tailwindcss/cli", "tw-animate-css"],
|
|
127
137
|
files(config) {
|
|
128
138
|
return [
|
|
129
139
|
{
|
package/dist/adapters/unocss.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
const CONFIG_PATH = "uno.config.ts";
|
|
17
17
|
function unoConfig(config) {
|
|
18
18
|
return `import { defineConfig, presetWind4 } from 'unocss'
|
|
19
|
+
import presetAnimations from 'unocss-preset-animations'
|
|
19
20
|
import { readFileSync } from 'node:fs'
|
|
20
21
|
import { createRequire } from 'node:module'
|
|
21
22
|
|
|
@@ -40,7 +41,15 @@ const tokens = readFileSync(require.resolve('@c9up/nebula/theme.css'), 'utf8')
|
|
|
40
41
|
const color = (name: string) => \`var(--\${name})\`
|
|
41
42
|
|
|
42
43
|
export default defineConfig({
|
|
43
|
-
|
|
44
|
+
/*
|
|
45
|
+
* \`presetAnimations\` is the UnoCSS port of the same utilities shadcn uses
|
|
46
|
+
* through \`tw-animate-css\`: \`animate-in\` / \`animate-out\` and the
|
|
47
|
+
* \`fade-*\` / \`zoom-*\` / \`slide-*\` modifiers. nebula's components emit
|
|
48
|
+
* shadcn's class strings verbatim, so the utilities have to exist under every
|
|
49
|
+
* engine — and registering them is what makes a missing setup produce no
|
|
50
|
+
* animation rather than an overlay that never finishes closing.
|
|
51
|
+
*/
|
|
52
|
+
presets: [presetWind4(), presetAnimations()],
|
|
44
53
|
content: {
|
|
45
54
|
filesystem: ['${config.paths.components}/**/*.{ts,js}'],
|
|
46
55
|
},
|
|
@@ -110,7 +119,7 @@ body {
|
|
|
110
119
|
export const unocssAdapter = {
|
|
111
120
|
name: "unocss",
|
|
112
121
|
summary: "UnoCSS with presetWind4 — same class syntax, no PostCSS, faster builds.",
|
|
113
|
-
packages: ["unocss", "@unocss/cli"],
|
|
122
|
+
packages: ["unocss", "@unocss/cli", "unocss-preset-animations"],
|
|
114
123
|
files(config) {
|
|
115
124
|
return [
|
|
116
125
|
{ path: CONFIG_PATH, contents: unoConfig(config), skipIfExists: true },
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
import { type ImageFit, type ImageFormat, type ImageLayout, type ImagePosition } from "../lib/image.js";
|
|
26
|
+
import { type Reactive } from "../lib/props.js";
|
|
27
|
+
export interface ImageProps {
|
|
28
|
+
src: Reactive<string>;
|
|
29
|
+
/** Required. `""` for a decorative image, which is what hides it. */
|
|
30
|
+
alt: Reactive<string>;
|
|
31
|
+
/**
|
|
32
|
+
* Displayed width in pixels, and the `width` attribute.
|
|
33
|
+
*
|
|
34
|
+
* Together with `height` it is what reserves the space before the bytes
|
|
35
|
+
* arrive. Optional only because `full-width` does not need it.
|
|
36
|
+
*/
|
|
37
|
+
width?: Reactive<number | undefined>;
|
|
38
|
+
height?: Reactive<number | undefined>;
|
|
39
|
+
/** Defaults to `constrained`. */
|
|
40
|
+
layout?: Reactive<ImageLayout>;
|
|
41
|
+
/** CSS `object-fit`. The image is never cropped server-side. */
|
|
42
|
+
fit?: Reactive<ImageFit | undefined>;
|
|
43
|
+
/** CSS `object-position`, for when `fit` crops. */
|
|
44
|
+
position?: Reactive<ImagePosition | undefined>;
|
|
45
|
+
/** Output format to request. Defaults to `webp`. */
|
|
46
|
+
format?: Reactive<ImageFormat | undefined>;
|
|
47
|
+
/** 1-100. The endpoint decides which values it accepts. */
|
|
48
|
+
quality?: Reactive<number | undefined>;
|
|
49
|
+
/** Override the generated width ladder. Pair it with `sizes`. */
|
|
50
|
+
widths?: Reactive<readonly number[] | undefined>;
|
|
51
|
+
/**
|
|
52
|
+
* Serve by pixel ratio instead of width, e.g. `[1, 2]`.
|
|
53
|
+
*
|
|
54
|
+
* Mutually exclusive with `widths` — a `srcset` mixing `w` and `x`
|
|
55
|
+
* descriptors is invalid, and a browser that sees one ignores the
|
|
56
|
+
* attribute entirely. `widths` wins if both are given.
|
|
57
|
+
*/
|
|
58
|
+
densities?: Reactive<readonly number[] | undefined>;
|
|
59
|
+
/** Override the generated `sizes`. */
|
|
60
|
+
sizes?: Reactive<string | undefined>;
|
|
61
|
+
/** Width ladder to draw from. Defaults to the device-width ladder. */
|
|
62
|
+
breakpoints?: Reactive<readonly number[] | undefined>;
|
|
63
|
+
/** The source's own width, when known. Stops variants past it being offered. */
|
|
64
|
+
originalWidth?: Reactive<number | undefined>;
|
|
65
|
+
/**
|
|
66
|
+
* Above the fold: load it now.
|
|
67
|
+
*
|
|
68
|
+
* Sets `loading="eager"`, `decoding="sync"` and `fetchpriority="high"`
|
|
69
|
+
* together, because setting one without the others is the usual reason a
|
|
70
|
+
* hero image is still not the first thing painted.
|
|
71
|
+
*/
|
|
72
|
+
priority?: Reactive<boolean>;
|
|
73
|
+
loading?: Reactive<"lazy" | "eager" | undefined>;
|
|
74
|
+
decoding?: Reactive<"async" | "sync" | "auto" | undefined>;
|
|
75
|
+
fetchPriority?: Reactive<"high" | "low" | "auto" | undefined>;
|
|
76
|
+
class?: Reactive<string>;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The widths this image offers, honouring an explicit `widths` prop.
|
|
80
|
+
*
|
|
81
|
+
* Exported because `Picture` needs exactly the same ladder for every one of
|
|
82
|
+
* its `<source>` elements — computing it twice is how the AVIF and WebP
|
|
83
|
+
* variants end up offering different sizes.
|
|
84
|
+
*/
|
|
85
|
+
export declare function resolvedWidths(props: ImageProps): number[];
|
|
86
|
+
/** The `srcset` for one format, in whichever descriptor style applies. */
|
|
87
|
+
export declare function resolvedSrcSet(props: ImageProps, format: ImageFormat | undefined): string | undefined;
|
|
88
|
+
/** The `sizes` for this image, or `undefined` when it carries `x` descriptors. */
|
|
89
|
+
export declare function resolvedSizes(props: ImageProps): string | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* The `src` a browser falls back to.
|
|
92
|
+
*
|
|
93
|
+
* The declared width rather than the largest variant: it is what a browser
|
|
94
|
+
* with no `srcset` support (and every crawler that reads the attribute
|
|
95
|
+
* literally) will fetch.
|
|
96
|
+
*/
|
|
97
|
+
export declare function resolvedSrc(props: ImageProps, format: ImageFormat | undefined): string;
|
|
98
|
+
/** The class list for the layout, fit and position of an image. */
|
|
99
|
+
export declare function imageClasses(props: ImageProps): string;
|
|
100
|
+
/** `loading`, `decoding` and `fetchpriority`, with `priority` applied. */
|
|
101
|
+
export declare function loadingAttributes(props: ImageProps): {
|
|
102
|
+
loading: "lazy" | "eager";
|
|
103
|
+
decoding: "async" | "sync" | "auto";
|
|
104
|
+
fetchPriority: "high" | "low" | "auto" | undefined;
|
|
105
|
+
};
|
|
106
|
+
export declare const Image: (props?: ImageProps | undefined) => import("@c9up/aurora").TemplateResult;
|
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
import { component, html } from "@c9up/aurora";
|
|
26
|
+
import { cn } from "../lib/cn.js";
|
|
27
|
+
import { densitySrcSet, fitClass, imageSizes, imageSrcSet, imageUrl, imageWidths, layoutClasses, offeredWidth, positionClass, } from "../lib/image.js";
|
|
28
|
+
import { read, readOr } from "../lib/props.js";
|
|
29
|
+
/**
|
|
30
|
+
* The widths this image offers, honouring an explicit `widths` prop.
|
|
31
|
+
*
|
|
32
|
+
* Exported because `Picture` needs exactly the same ladder for every one of
|
|
33
|
+
* its `<source>` elements — computing it twice is how the AVIF and WebP
|
|
34
|
+
* variants end up offering different sizes.
|
|
35
|
+
*/
|
|
36
|
+
export function resolvedWidths(props) {
|
|
37
|
+
const explicit = read(props.widths);
|
|
38
|
+
if (explicit !== undefined)
|
|
39
|
+
return [...explicit];
|
|
40
|
+
return imageWidths({
|
|
41
|
+
width: read(props.width),
|
|
42
|
+
layout: readOr(props.layout, "constrained"),
|
|
43
|
+
breakpoints: read(props.breakpoints),
|
|
44
|
+
originalWidth: read(props.originalWidth),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** The `srcset` for one format, in whichever descriptor style applies. */
|
|
48
|
+
export function resolvedSrcSet(props, format) {
|
|
49
|
+
const quality = read(props.quality);
|
|
50
|
+
const width = read(props.width);
|
|
51
|
+
const densities = read(props.densities);
|
|
52
|
+
// `widths` wins: an explicit ladder is the more specific instruction, and
|
|
53
|
+
// honouring both would emit the invalid mixed-descriptor set.
|
|
54
|
+
if (densities !== undefined && read(props.widths) === undefined) {
|
|
55
|
+
if (width === undefined)
|
|
56
|
+
return undefined;
|
|
57
|
+
return densitySrcSet({
|
|
58
|
+
src: read(props.src),
|
|
59
|
+
width,
|
|
60
|
+
densities,
|
|
61
|
+
format,
|
|
62
|
+
quality,
|
|
63
|
+
originalWidth: read(props.originalWidth),
|
|
64
|
+
breakpoints: read(props.breakpoints),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return imageSrcSet({
|
|
68
|
+
src: read(props.src),
|
|
69
|
+
widths: resolvedWidths(props),
|
|
70
|
+
format,
|
|
71
|
+
quality,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** The `sizes` for this image, or `undefined` when it carries `x` descriptors. */
|
|
75
|
+
export function resolvedSizes(props) {
|
|
76
|
+
const explicit = read(props.sizes);
|
|
77
|
+
if (explicit !== undefined)
|
|
78
|
+
return explicit;
|
|
79
|
+
// Density srcsets describe themselves; a `sizes` alongside them means
|
|
80
|
+
// nothing and browsers ignore it.
|
|
81
|
+
if (read(props.densities) !== undefined && read(props.widths) === undefined) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
return imageSizes({
|
|
85
|
+
width: read(props.width),
|
|
86
|
+
layout: readOr(props.layout, "constrained"),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The `src` a browser falls back to.
|
|
91
|
+
*
|
|
92
|
+
* The declared width rather than the largest variant: it is what a browser
|
|
93
|
+
* with no `srcset` support (and every crawler that reads the attribute
|
|
94
|
+
* literally) will fetch.
|
|
95
|
+
*/
|
|
96
|
+
export function resolvedSrc(props, format) {
|
|
97
|
+
const src = read(props.src);
|
|
98
|
+
const width = read(props.width);
|
|
99
|
+
if (width === undefined)
|
|
100
|
+
return src;
|
|
101
|
+
return imageUrl(src, {
|
|
102
|
+
// Snapped like every other entry: the declared width is almost never a
|
|
103
|
+
// width the endpoint offers, and this URL is the one a browser without
|
|
104
|
+
// `srcset` support fetches.
|
|
105
|
+
width: offeredWidth(width, read(props.breakpoints)),
|
|
106
|
+
format,
|
|
107
|
+
quality: read(props.quality),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/** The class list for the layout, fit and position of an image. */
|
|
111
|
+
export function imageClasses(props) {
|
|
112
|
+
return cn(layoutClasses(readOr(props.layout, "constrained")), fitClass(read(props.fit)), positionClass(read(props.position)), read(props.class));
|
|
113
|
+
}
|
|
114
|
+
/** `loading`, `decoding` and `fetchpriority`, with `priority` applied. */
|
|
115
|
+
export function loadingAttributes(props) {
|
|
116
|
+
const priority = readOr(props.priority, false);
|
|
117
|
+
return {
|
|
118
|
+
loading: read(props.loading) ?? (priority ? "eager" : "lazy"),
|
|
119
|
+
decoding: read(props.decoding) ?? (priority ? "sync" : "async"),
|
|
120
|
+
fetchPriority: read(props.fetchPriority) ?? (priority ? "high" : undefined),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export const Image = component((props) => {
|
|
124
|
+
const format = () => read(props.format) ?? "webp";
|
|
125
|
+
return html `<img
|
|
126
|
+
data-slot="image"
|
|
127
|
+
data-layout="${() => readOr(props.layout, "constrained")}"
|
|
128
|
+
src="${() => resolvedSrc(props, format())}"
|
|
129
|
+
srcset="${() => resolvedSrcSet(props, format())}"
|
|
130
|
+
sizes="${() => resolvedSizes(props)}"
|
|
131
|
+
alt="${() => read(props.alt)}"
|
|
132
|
+
width="${() => read(props.width)}"
|
|
133
|
+
height="${() => read(props.height)}"
|
|
134
|
+
loading="${() => loadingAttributes(props).loading}"
|
|
135
|
+
decoding="${() => loadingAttributes(props).decoding}"
|
|
136
|
+
fetchpriority="${() => loadingAttributes(props).fetchPriority}"
|
|
137
|
+
class="${() => imageClasses(props)}"
|
|
138
|
+
/>`;
|
|
139
|
+
});
|
|
@@ -44,7 +44,25 @@ export const NativeSelect = component((props) => {
|
|
|
44
44
|
}
|
|
45
45
|
return out;
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* `?selected` on each option, NOT `.value` on the select alone.
|
|
49
|
+
*
|
|
50
|
+
* A property binding sits on the opening tag, so it is assigned before the
|
|
51
|
+
* options exist. `select.value = 'x'` against a childless `<select>` is a
|
|
52
|
+
* no-op the DOM does not report, and nothing re-runs it afterwards: the
|
|
53
|
+
* control then shows its FIRST option while holding the value it was given.
|
|
54
|
+
* Nothing throws and nothing logs, so the value the user sees is not the
|
|
55
|
+
* value the form will submit — in a form that edits money or tax status
|
|
56
|
+
* that is a wrong record written on save.
|
|
57
|
+
*
|
|
58
|
+
* Marking the option is order-independent, and it is also what makes the
|
|
59
|
+
* choice correct in server-rendered HTML before any script runs.
|
|
60
|
+
*/
|
|
61
|
+
const renderOption = (option) => html `<option
|
|
62
|
+
value="${option.value}"
|
|
63
|
+
?disabled="${option.disabled === true}"
|
|
64
|
+
?selected="${() => read(props.value) === option.value}"
|
|
65
|
+
>
|
|
48
66
|
${option.label}
|
|
49
67
|
</option>`;
|
|
50
68
|
return html `<div data-slot="native-select" class="relative inline-flex w-full items-center">
|
|
@@ -61,7 +79,14 @@ export const NativeSelect = component((props) => {
|
|
|
61
79
|
>
|
|
62
80
|
${props.placeholder === undefined
|
|
63
81
|
? null
|
|
64
|
-
: html `<option
|
|
82
|
+
: html `<option
|
|
83
|
+
value=""
|
|
84
|
+
disabled
|
|
85
|
+
?selected="${() => {
|
|
86
|
+
const current = read(props.value);
|
|
87
|
+
return current === undefined || current === "";
|
|
88
|
+
}}"
|
|
89
|
+
>${props.placeholder}</option>`}
|
|
65
90
|
${runs().map((run) => run.group === undefined
|
|
66
91
|
? run.items.map(renderOption)
|
|
67
92
|
: html `<optgroup label="${run.group}">${run.items.map(renderOption)}</optgroup>`)}
|
package/dist/atoms/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export { Avatar, type AvatarProps } from "./Avatar.js";
|
|
|
16
16
|
export { Badge, type BadgeProps, type BadgeVariants, badgeVariants, } from "./Badge.js";
|
|
17
17
|
export { Button, type ButtonProps, type ButtonVariants, buttonVariants, } from "./Button.js";
|
|
18
18
|
export { Checkbox, type CheckboxProps, checkboxClasses } from "./Checkbox.js";
|
|
19
|
+
export { Image, type ImageProps, imageClasses, loadingAttributes, resolvedSizes, resolvedSrc, resolvedSrcSet, resolvedWidths, } from "./Image.js";
|
|
19
20
|
export { Input, type InputProps, inputClasses } from "./Input.js";
|
|
20
21
|
export { Kbd, type KbdProps } from "./Kbd.js";
|
|
21
22
|
export { Label, type LabelProps, labelClasses } from "./Label.js";
|
package/dist/atoms/index.js
CHANGED
|
@@ -16,6 +16,7 @@ export { Avatar } from "./Avatar.js";
|
|
|
16
16
|
export { Badge, badgeVariants, } from "./Badge.js";
|
|
17
17
|
export { Button, buttonVariants, } from "./Button.js";
|
|
18
18
|
export { Checkbox, checkboxClasses } from "./Checkbox.js";
|
|
19
|
+
export { Image, imageClasses, loadingAttributes, resolvedSizes, resolvedSrc, resolvedSrcSet, resolvedWidths, } from "./Image.js";
|
|
19
20
|
export { Input, inputClasses } from "./Input.js";
|
|
20
21
|
export { Kbd } from "./Kbd.js";
|
|
21
22
|
export { Label, labelClasses } from "./Label.js";
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Responsive image plumbing — the arithmetic behind `Image` and `Picture`.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is pure: widths in, strings out. It lives apart from the
|
|
5
|
+
* components because the interesting part of a responsive image is not the
|
|
6
|
+
* markup, it is deciding *which* variants to offer the browser, and that
|
|
7
|
+
* decision is worth testing without a DOM.
|
|
8
|
+
*
|
|
9
|
+
* **Where the bytes come from is not nebula's business.** A component that
|
|
10
|
+
* imported an image processor would drag a native module into a package whose
|
|
11
|
+
* only peer is Aurora, and would pin every app to one way of serving images.
|
|
12
|
+
* So the URL is produced by a resolver the app installs once
|
|
13
|
+
* ({@link setImageResolver}). The default one points at `/__image`, the
|
|
14
|
+
* endpoint `@c9up/prism` registers; pointing it at a CDN instead is a
|
|
15
|
+
* one-line change and no component notices.
|
|
16
|
+
*
|
|
17
|
+
* The resolver asks for a width, a format and a quality — never a height and
|
|
18
|
+
* never a crop. Those three axes are the ones the server can safely allow-list,
|
|
19
|
+
* which is what keeps an open transformation endpoint from becoming a cache
|
|
20
|
+
* bomb. Cropping is `object-fit` on the element, where it costs nothing.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* How an image responds to the width of its container.
|
|
24
|
+
*
|
|
25
|
+
* - `constrained` — scales down to fit, never past its declared width. The
|
|
26
|
+
* default, and what almost every image in a page wants.
|
|
27
|
+
* - `full-width` — always the width of its container. Hero images, banners.
|
|
28
|
+
* - `fixed` — the declared size, whatever the viewport. Logos, avatars,
|
|
29
|
+
* icons. Still offers a 2x variant for dense screens.
|
|
30
|
+
* - `none` — no `srcset`, no `sizes`. An escape hatch for when the markup is
|
|
31
|
+
* being assembled by something else.
|
|
32
|
+
*/
|
|
33
|
+
export type ImageLayout = "constrained" | "full-width" | "fixed" | "none";
|
|
34
|
+
/** Output formats an image endpoint is expected to understand. */
|
|
35
|
+
export type ImageFormat = "avif" | "webp" | "jpeg" | "png";
|
|
36
|
+
/**
|
|
37
|
+
* How the rendered box reconciles a different aspect ratio.
|
|
38
|
+
*
|
|
39
|
+
* These are the CSS `object-fit` values, and they are applied as CSS — the
|
|
40
|
+
* image is never cropped server-side. A crop asked for through a URL is an
|
|
41
|
+
* unbounded axis on a public endpoint, and the same result is one class here.
|
|
42
|
+
*/
|
|
43
|
+
export type ImageFit = "cover" | "contain" | "fill" | "none" | "scale-down";
|
|
44
|
+
/** Where the visible part sits when `fit` crops. CSS `object-position`. */
|
|
45
|
+
export type ImagePosition = "center" | "top" | "bottom" | "left" | "right" | "top left" | "top right" | "bottom left" | "bottom right";
|
|
46
|
+
/** What a resolver is asked to produce one URL for. */
|
|
47
|
+
export interface ImageTransform {
|
|
48
|
+
/** Target width in pixels. The server must not upscale past the source. */
|
|
49
|
+
width: number;
|
|
50
|
+
format?: ImageFormat;
|
|
51
|
+
/** 1-100. The endpoint decides whether a given value is allowed. */
|
|
52
|
+
quality?: number;
|
|
53
|
+
}
|
|
54
|
+
/** Turns a source and a transform into the URL to put in a `srcset`. */
|
|
55
|
+
export type ImageUrlResolver = (src: string, transform: ImageTransform) => string;
|
|
56
|
+
/**
|
|
57
|
+
* Widths for elements that are not the width of a screen.
|
|
58
|
+
*
|
|
59
|
+
* Avatars, icons, thumbnails. Without them a 48px avatar's nearest offer is
|
|
60
|
+
* 640 — thirteen times the pixels it can show — because every other ladder
|
|
61
|
+
* starts at a phone's viewport.
|
|
62
|
+
*/
|
|
63
|
+
export declare const IMAGE_SIZES: readonly number[];
|
|
64
|
+
/**
|
|
65
|
+
* The widths worth generating, from real device pixel counts.
|
|
66
|
+
*
|
|
67
|
+
* Astro's ladder, close to the one Next.js settled on independently — which is
|
|
68
|
+
* the reason to reuse it rather than invent one. They are device widths, not
|
|
69
|
+
* round numbers: 828 is the iPhone XR, 1668 is an iPad. A tidier
|
|
70
|
+
* `[400, 800, 1200, 1600]` misses every one of them and ships each device a
|
|
71
|
+
* slightly-too-large image.
|
|
72
|
+
*
|
|
73
|
+
* Density is what makes the ladder cheap to round up to. The nearest rung
|
|
74
|
+
* above 2400 here is 2560, six percent over; on a sparser ladder it would be
|
|
75
|
+
* 3840.
|
|
76
|
+
*/
|
|
77
|
+
export declare const DEFAULT_RESOLUTIONS: readonly number[];
|
|
78
|
+
/**
|
|
79
|
+
* The ladder without the sizes only a desktop display asks for.
|
|
80
|
+
*
|
|
81
|
+
* Worth choosing deliberately for a full-width image: the top of
|
|
82
|
+
* {@link DEFAULT_RESOLUTIONS} is eleven extra variants to store and warm, for
|
|
83
|
+
* screens most sites see rarely.
|
|
84
|
+
*/
|
|
85
|
+
export declare const LIMITED_RESOLUTIONS: readonly number[];
|
|
86
|
+
/**
|
|
87
|
+
* Every width a component may ask for, ascending.
|
|
88
|
+
*
|
|
89
|
+
* This list and the endpoint's allow-list are the same list, and that is the
|
|
90
|
+
* contract between the two halves: a component that emits a width the endpoint
|
|
91
|
+
* does not serve produces a `srcset` where every entry is a 400, and the page
|
|
92
|
+
* silently falls back to the one `src`.
|
|
93
|
+
*
|
|
94
|
+
* It is also why a declared width is never emitted literally. `width: 1200`
|
|
95
|
+
* asking for 1200 and 2400 would be two widths no allow-list contains, and
|
|
96
|
+
* allow-listing whatever an author happens to type is not an allow-list.
|
|
97
|
+
*/
|
|
98
|
+
export declare function allSizes(breakpoints: readonly number[]): number[];
|
|
99
|
+
/**
|
|
100
|
+
* The width to actually request for a declared one.
|
|
101
|
+
*
|
|
102
|
+
* The `src` attribute and a density entry are single URLs rather than ladders,
|
|
103
|
+
* and they are subject to the same contract: a literal declared width is a
|
|
104
|
+
* width no allow-list contains.
|
|
105
|
+
*/
|
|
106
|
+
export declare function offeredWidth(width: number, breakpoints?: readonly number[]): number;
|
|
107
|
+
export interface WidthsOptions {
|
|
108
|
+
/** The declared display width. Required by every layout but `full-width`. */
|
|
109
|
+
width?: number;
|
|
110
|
+
layout: ImageLayout;
|
|
111
|
+
/** Ladder to draw from. Defaults to {@link DEFAULT_RESOLUTIONS}. */
|
|
112
|
+
breakpoints?: readonly number[];
|
|
113
|
+
/**
|
|
114
|
+
* The source's own width, when it is known.
|
|
115
|
+
*
|
|
116
|
+
* Only a refinement: the endpoint must refuse to upscale anyway, so
|
|
117
|
+
* leaving it out costs a duplicate URL that serves the same bytes, never a
|
|
118
|
+
* blurry enlargement. Pass it and those duplicates disappear.
|
|
119
|
+
*/
|
|
120
|
+
originalWidth?: number;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The widths to offer, ascending.
|
|
124
|
+
*
|
|
125
|
+
* Every value comes off {@link allSizes} — see there for why a declared width
|
|
126
|
+
* is never emitted literally. 1x and 2x are always represented, rounded up, so
|
|
127
|
+
* the common case of a dense screen at the declared width is never served
|
|
128
|
+
* fewer pixels than it can show.
|
|
129
|
+
*/
|
|
130
|
+
export declare function imageWidths(options: WidthsOptions): number[];
|
|
131
|
+
/**
|
|
132
|
+
* The `sizes` attribute for a layout.
|
|
133
|
+
*
|
|
134
|
+
* Without it the browser assumes the image is the full width of the viewport
|
|
135
|
+
* and picks the largest variant on every screen — which is the failure mode
|
|
136
|
+
* where adding a `srcset` makes a page slower, not faster.
|
|
137
|
+
*/
|
|
138
|
+
export declare function imageSizes(options: {
|
|
139
|
+
width?: number;
|
|
140
|
+
layout: ImageLayout;
|
|
141
|
+
}): string | undefined;
|
|
142
|
+
export interface SrcSetOptions {
|
|
143
|
+
src: string;
|
|
144
|
+
widths: readonly number[];
|
|
145
|
+
format?: ImageFormat;
|
|
146
|
+
quality?: number;
|
|
147
|
+
/** Defaults to the installed resolver. */
|
|
148
|
+
resolve?: ImageUrlResolver;
|
|
149
|
+
}
|
|
150
|
+
/** A `srcset` with `w` descriptors, or `undefined` when there is nothing to offer. */
|
|
151
|
+
export declare function imageSrcSet(options: SrcSetOptions): string | undefined;
|
|
152
|
+
export interface DensitySrcSetOptions {
|
|
153
|
+
src: string;
|
|
154
|
+
/** The width at 1x. */
|
|
155
|
+
width: number;
|
|
156
|
+
/** Pixel ratios to serve, e.g. `[1, 2]`. */
|
|
157
|
+
densities: readonly number[];
|
|
158
|
+
format?: ImageFormat;
|
|
159
|
+
quality?: number;
|
|
160
|
+
originalWidth?: number;
|
|
161
|
+
breakpoints?: readonly number[];
|
|
162
|
+
resolve?: ImageUrlResolver;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* A `srcset` with `x` descriptors.
|
|
166
|
+
*
|
|
167
|
+
* The alternative to widths, never a companion to them: a `srcset` mixing `w`
|
|
168
|
+
* and `x` descriptors is invalid, and browsers handle the mixture by ignoring
|
|
169
|
+
* the whole attribute.
|
|
170
|
+
*/
|
|
171
|
+
export declare function densitySrcSet(options: DensitySrcSetOptions): string | undefined;
|
|
172
|
+
/** Point the built-in resolver at a different path. */
|
|
173
|
+
export declare function setImageEndpoint(path: string): void;
|
|
174
|
+
/**
|
|
175
|
+
* The resolver used when an application installs none.
|
|
176
|
+
*
|
|
177
|
+
* Parameter names are one letter because they end up in every `srcset` entry
|
|
178
|
+
* of every image on the page, and a `srcset` with fifteen entries repeats them
|
|
179
|
+
* fifteen times.
|
|
180
|
+
*/
|
|
181
|
+
export declare const defaultImageResolver: ImageUrlResolver;
|
|
182
|
+
/**
|
|
183
|
+
* Install the resolver every image uses.
|
|
184
|
+
*
|
|
185
|
+
* Call it once at startup — from the app's entry on the client, and from the
|
|
186
|
+
* same module on the server so SSR and hydration agree. They must agree: an
|
|
187
|
+
* `src` that differs between the two makes the browser discard the image the
|
|
188
|
+
* server already started fetching and request another.
|
|
189
|
+
*
|
|
190
|
+
* Passing `null` restores {@link defaultImageResolver}.
|
|
191
|
+
*/
|
|
192
|
+
export declare function setImageResolver(next: ImageUrlResolver | null): void;
|
|
193
|
+
/** The resolver currently installed. */
|
|
194
|
+
export declare function getImageResolver(): ImageUrlResolver;
|
|
195
|
+
/** Build one image URL through the installed resolver. */
|
|
196
|
+
export declare function imageUrl(src: string, transform: ImageTransform): string;
|
|
197
|
+
/** Tailwind classes for a layout. Height is always `auto` so the ratio holds. */
|
|
198
|
+
export declare function layoutClasses(layout: ImageLayout): string;
|
|
199
|
+
/** The `object-fit` class for a fit value. */
|
|
200
|
+
export declare function fitClass(fit: ImageFit | undefined): string | undefined;
|
|
201
|
+
/** The `object-position` class for a position value. */
|
|
202
|
+
export declare function positionClass(position: ImagePosition | undefined): string | undefined;
|
|
203
|
+
/** The MIME type a `<source type>` needs for a format. */
|
|
204
|
+
export declare function mimeType(format: ImageFormat): string;
|
|
205
|
+
/**
|
|
206
|
+
* The format a source's file name claims, if any.
|
|
207
|
+
*
|
|
208
|
+
* Used to pick the fallback of a `<picture>`: converting a PNG with
|
|
209
|
+
* transparency to JPEG puts a black box behind it, so the fallback follows the
|
|
210
|
+
* source rather than a fixed default. The extension is a claim, not proof —
|
|
211
|
+
* only the server knows what the bytes really are, and it re-reads them.
|
|
212
|
+
*/
|
|
213
|
+
export declare function formatFromSource(src: string): ImageFormat | undefined;
|