@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.
Files changed (42) hide show
  1. package/dist/adapters/tailwind.d.ts +7 -0
  2. package/dist/adapters/tailwind.js +11 -1
  3. package/dist/adapters/unocss.js +11 -2
  4. package/dist/atoms/Image.d.ts +106 -0
  5. package/dist/atoms/Image.js +139 -0
  6. package/dist/atoms/NativeSelect.js +27 -2
  7. package/dist/atoms/index.d.ts +1 -0
  8. package/dist/atoms/index.js +1 -0
  9. package/dist/lib/image.d.ts +213 -0
  10. package/dist/lib/image.js +331 -0
  11. package/dist/lib/index.d.ts +1 -0
  12. package/dist/lib/index.js +1 -0
  13. package/dist/lib/motion.d.ts +25 -16
  14. package/dist/lib/motion.js +30 -24
  15. package/dist/molecules/Picture.d.ts +46 -0
  16. package/dist/molecules/Picture.js +62 -0
  17. package/dist/molecules/index.d.ts +1 -0
  18. package/dist/molecules/index.js +1 -0
  19. package/dist/organisms/Dialog.d.ts +11 -4
  20. package/dist/organisms/Dialog.js +2 -2
  21. package/dist/organisms/Sidebar.d.ts +9 -0
  22. package/dist/organisms/Sidebar.js +5 -2
  23. package/dist/primitives/floatingSurface.js +17 -8
  24. package/dist/primitives/presence.js +401 -5
  25. package/nebula.css +1 -1
  26. package/package.json +7 -6
  27. package/registry.json +25 -0
  28. package/src/adapters/tailwind.ts +11 -1
  29. package/src/adapters/unocss.ts +11 -2
  30. package/src/atoms/Image.ts +226 -0
  31. package/src/atoms/NativeSelect.ts +27 -2
  32. package/src/atoms/index.ts +10 -0
  33. package/src/lib/image.ts +470 -0
  34. package/src/lib/index.ts +25 -0
  35. package/src/lib/motion.ts +30 -26
  36. package/src/molecules/Picture.ts +92 -0
  37. package/src/molecules/index.ts +1 -0
  38. package/src/organisms/Dialog.ts +13 -6
  39. package/src/organisms/Sidebar.ts +15 -2
  40. package/src/primitives/floatingSurface.ts +18 -9
  41. package/src/primitives/presence.ts +428 -5
  42. 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
- packages: ["tailwindcss", "@tailwindcss/cli"],
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
  {
@@ -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
- presets: [presetWind4()],
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
- const renderOption = (option) => html `<option value="${option.value}" ?disabled="${option.disabled === true}">
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 value="" disabled selected>${props.placeholder}</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>`)}
@@ -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";
@@ -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;