@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,331 @@
|
|
|
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
|
+
* Widths for elements that are not the width of a screen.
|
|
24
|
+
*
|
|
25
|
+
* Avatars, icons, thumbnails. Without them a 48px avatar's nearest offer is
|
|
26
|
+
* 640 — thirteen times the pixels it can show — because every other ladder
|
|
27
|
+
* starts at a phone's viewport.
|
|
28
|
+
*/
|
|
29
|
+
export const IMAGE_SIZES = [
|
|
30
|
+
16, 32, 48, 64, 96, 128, 256, 384,
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* The widths worth generating, from real device pixel counts.
|
|
34
|
+
*
|
|
35
|
+
* Astro's ladder, close to the one Next.js settled on independently — which is
|
|
36
|
+
* the reason to reuse it rather than invent one. They are device widths, not
|
|
37
|
+
* round numbers: 828 is the iPhone XR, 1668 is an iPad. A tidier
|
|
38
|
+
* `[400, 800, 1200, 1600]` misses every one of them and ships each device a
|
|
39
|
+
* slightly-too-large image.
|
|
40
|
+
*
|
|
41
|
+
* Density is what makes the ladder cheap to round up to. The nearest rung
|
|
42
|
+
* above 2400 here is 2560, six percent over; on a sparser ladder it would be
|
|
43
|
+
* 3840.
|
|
44
|
+
*/
|
|
45
|
+
export const DEFAULT_RESOLUTIONS = [
|
|
46
|
+
640, // older and lower-end phones
|
|
47
|
+
750, // iPhone 6-8
|
|
48
|
+
828, // iPhone XR/11
|
|
49
|
+
960, // older horizontal phones
|
|
50
|
+
1080, // iPhone 6-8 Plus
|
|
51
|
+
1280, // 720p
|
|
52
|
+
1668, // various iPads
|
|
53
|
+
1920, // 1080p
|
|
54
|
+
2048, // QXGA
|
|
55
|
+
2560, // WQXGA
|
|
56
|
+
3200, // QHD+
|
|
57
|
+
3840, // 4K
|
|
58
|
+
4480, // 4.5K
|
|
59
|
+
5120, // 5K
|
|
60
|
+
6016, // 6K
|
|
61
|
+
];
|
|
62
|
+
/**
|
|
63
|
+
* The ladder without the sizes only a desktop display asks for.
|
|
64
|
+
*
|
|
65
|
+
* Worth choosing deliberately for a full-width image: the top of
|
|
66
|
+
* {@link DEFAULT_RESOLUTIONS} is eleven extra variants to store and warm, for
|
|
67
|
+
* screens most sites see rarely.
|
|
68
|
+
*/
|
|
69
|
+
export const LIMITED_RESOLUTIONS = [
|
|
70
|
+
640, 750, 828, 1080, 1280, 1668, 2048, 2560,
|
|
71
|
+
];
|
|
72
|
+
/**
|
|
73
|
+
* Every width a component may ask for, ascending.
|
|
74
|
+
*
|
|
75
|
+
* This list and the endpoint's allow-list are the same list, and that is the
|
|
76
|
+
* contract between the two halves: a component that emits a width the endpoint
|
|
77
|
+
* does not serve produces a `srcset` where every entry is a 400, and the page
|
|
78
|
+
* silently falls back to the one `src`.
|
|
79
|
+
*
|
|
80
|
+
* It is also why a declared width is never emitted literally. `width: 1200`
|
|
81
|
+
* asking for 1200 and 2400 would be two widths no allow-list contains, and
|
|
82
|
+
* allow-listing whatever an author happens to type is not an allow-list.
|
|
83
|
+
*/
|
|
84
|
+
export function allSizes(breakpoints) {
|
|
85
|
+
return [...new Set([...IMAGE_SIZES, ...breakpoints])].sort((a, b) => a - b);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The smallest offered width that still covers `target`.
|
|
89
|
+
*
|
|
90
|
+
* Upwards, never downwards. Rounding down is how a 2x screen is handed an
|
|
91
|
+
* image with fewer pixels than it can show, and the result is soft in a way
|
|
92
|
+
* that is hard to attribute later. Falls back to the largest rung, which the
|
|
93
|
+
* endpoint clamps to the source anyway.
|
|
94
|
+
*/
|
|
95
|
+
function snapUp(target, sizes) {
|
|
96
|
+
return sizes.find((size) => size >= target) ?? sizes[sizes.length - 1] ?? 0;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The width to actually request for a declared one.
|
|
100
|
+
*
|
|
101
|
+
* The `src` attribute and a density entry are single URLs rather than ladders,
|
|
102
|
+
* and they are subject to the same contract: a literal declared width is a
|
|
103
|
+
* width no allow-list contains.
|
|
104
|
+
*/
|
|
105
|
+
export function offeredWidth(width, breakpoints = DEFAULT_RESOLUTIONS) {
|
|
106
|
+
return snapUp(width, allSizes(breakpoints));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The widths to offer, ascending.
|
|
110
|
+
*
|
|
111
|
+
* Every value comes off {@link allSizes} — see there for why a declared width
|
|
112
|
+
* is never emitted literally. 1x and 2x are always represented, rounded up, so
|
|
113
|
+
* the common case of a dense screen at the declared width is never served
|
|
114
|
+
* fewer pixels than it can show.
|
|
115
|
+
*/
|
|
116
|
+
export function imageWidths(options) {
|
|
117
|
+
const { width, layout, breakpoints = DEFAULT_RESOLUTIONS, originalWidth, } = options;
|
|
118
|
+
const every = allSizes(breakpoints);
|
|
119
|
+
/**
|
|
120
|
+
* Drop what the source cannot fill, but never return nothing.
|
|
121
|
+
*
|
|
122
|
+
* A source smaller than every rung still has to be offered at some width,
|
|
123
|
+
* and the endpoint clamps to the source rather than enlarging it.
|
|
124
|
+
*/
|
|
125
|
+
const withinSource = (candidates) => {
|
|
126
|
+
if (originalWidth === undefined)
|
|
127
|
+
return candidates;
|
|
128
|
+
const kept = candidates.filter((candidate) => candidate <= originalWidth);
|
|
129
|
+
if (kept.length > 0)
|
|
130
|
+
return kept;
|
|
131
|
+
const smallest = candidates[0];
|
|
132
|
+
return smallest === undefined ? [] : [smallest];
|
|
133
|
+
};
|
|
134
|
+
if (layout === "full-width") {
|
|
135
|
+
return withinSource([...breakpoints]);
|
|
136
|
+
}
|
|
137
|
+
if (width === undefined || width <= 0)
|
|
138
|
+
return [];
|
|
139
|
+
const oneX = snapUp(width, every);
|
|
140
|
+
const twoX = snapUp(width * 2, every);
|
|
141
|
+
if (layout === "fixed") {
|
|
142
|
+
return withinSource([...new Set([oneX, twoX])].sort((a, b) => a - b));
|
|
143
|
+
}
|
|
144
|
+
if (layout === "constrained") {
|
|
145
|
+
// Below its declared width a constrained image is the width of the
|
|
146
|
+
// viewport, so the viewport rungs under the cap earn their place. The
|
|
147
|
+
// floor is the smallest device width: nothing narrower is a screen,
|
|
148
|
+
// and offering 32w to an image that is at least 640 wide is noise in
|
|
149
|
+
// every `srcset` on the page.
|
|
150
|
+
const floor = breakpoints[0] ?? 0;
|
|
151
|
+
const intermediate = every.filter((size) => size >= floor && size <= twoX);
|
|
152
|
+
return withinSource([...new Set([oneX, twoX, ...intermediate])].sort((a, b) => a - b));
|
|
153
|
+
}
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The `sizes` attribute for a layout.
|
|
158
|
+
*
|
|
159
|
+
* Without it the browser assumes the image is the full width of the viewport
|
|
160
|
+
* and picks the largest variant on every screen — which is the failure mode
|
|
161
|
+
* where adding a `srcset` makes a page slower, not faster.
|
|
162
|
+
*/
|
|
163
|
+
export function imageSizes(options) {
|
|
164
|
+
const { width, layout } = options;
|
|
165
|
+
if (width === undefined || width <= 0) {
|
|
166
|
+
return layout === "full-width" ? "100vw" : undefined;
|
|
167
|
+
}
|
|
168
|
+
switch (layout) {
|
|
169
|
+
case "constrained":
|
|
170
|
+
// Wider screen than the cap: the image stops at the cap. Narrower:
|
|
171
|
+
// it is the screen.
|
|
172
|
+
return `(min-width: ${width}px) ${width}px, 100vw`;
|
|
173
|
+
case "fixed":
|
|
174
|
+
return `${width}px`;
|
|
175
|
+
case "full-width":
|
|
176
|
+
return "100vw";
|
|
177
|
+
default:
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** A `srcset` with `w` descriptors, or `undefined` when there is nothing to offer. */
|
|
182
|
+
export function imageSrcSet(options) {
|
|
183
|
+
const { src, widths, format, quality, resolve = imageUrl } = options;
|
|
184
|
+
if (widths.length === 0)
|
|
185
|
+
return undefined;
|
|
186
|
+
return widths
|
|
187
|
+
.map((width) => `${resolve(src, { width, format, quality })} ${width}w`)
|
|
188
|
+
.join(", ");
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* A `srcset` with `x` descriptors.
|
|
192
|
+
*
|
|
193
|
+
* The alternative to widths, never a companion to them: a `srcset` mixing `w`
|
|
194
|
+
* and `x` descriptors is invalid, and browsers handle the mixture by ignoring
|
|
195
|
+
* the whole attribute.
|
|
196
|
+
*/
|
|
197
|
+
export function densitySrcSet(options) {
|
|
198
|
+
const { src, width, densities, format, quality, originalWidth, breakpoints = DEFAULT_RESOLUTIONS, resolve = imageUrl, } = options;
|
|
199
|
+
if (width <= 0 || densities.length === 0)
|
|
200
|
+
return undefined;
|
|
201
|
+
const seen = new Set();
|
|
202
|
+
const entries = [];
|
|
203
|
+
for (const density of [...densities].sort((a, b) => a - b)) {
|
|
204
|
+
if (density <= 0)
|
|
205
|
+
continue;
|
|
206
|
+
const target = offeredWidth(Math.round(width * density), breakpoints);
|
|
207
|
+
if (originalWidth !== undefined && target > originalWidth)
|
|
208
|
+
continue;
|
|
209
|
+
if (seen.has(target))
|
|
210
|
+
continue;
|
|
211
|
+
seen.add(target);
|
|
212
|
+
entries.push(`${resolve(src, { width: target, format, quality })} ${density}x`);
|
|
213
|
+
}
|
|
214
|
+
return entries.length === 0 ? undefined : entries.join(", ");
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Where the default resolver sends its requests.
|
|
218
|
+
*
|
|
219
|
+
* `__`-prefixed because it is the framework's route rather than the
|
|
220
|
+
* application's, the same convention as `__assets` and `__relay`.
|
|
221
|
+
*/
|
|
222
|
+
let endpoint = "/__image";
|
|
223
|
+
/** Point the built-in resolver at a different path. */
|
|
224
|
+
export function setImageEndpoint(path) {
|
|
225
|
+
endpoint = path;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The resolver used when an application installs none.
|
|
229
|
+
*
|
|
230
|
+
* Parameter names are one letter because they end up in every `srcset` entry
|
|
231
|
+
* of every image on the page, and a `srcset` with fifteen entries repeats them
|
|
232
|
+
* fifteen times.
|
|
233
|
+
*/
|
|
234
|
+
export const defaultImageResolver = (src, transform) => {
|
|
235
|
+
const params = new URLSearchParams({
|
|
236
|
+
src,
|
|
237
|
+
w: String(Math.round(transform.width)),
|
|
238
|
+
});
|
|
239
|
+
if (transform.format !== undefined)
|
|
240
|
+
params.set("f", transform.format);
|
|
241
|
+
if (transform.quality !== undefined) {
|
|
242
|
+
params.set("q", String(Math.round(transform.quality)));
|
|
243
|
+
}
|
|
244
|
+
return `${endpoint}?${params.toString()}`;
|
|
245
|
+
};
|
|
246
|
+
let resolver = defaultImageResolver;
|
|
247
|
+
/**
|
|
248
|
+
* Install the resolver every image uses.
|
|
249
|
+
*
|
|
250
|
+
* Call it once at startup — from the app's entry on the client, and from the
|
|
251
|
+
* same module on the server so SSR and hydration agree. They must agree: an
|
|
252
|
+
* `src` that differs between the two makes the browser discard the image the
|
|
253
|
+
* server already started fetching and request another.
|
|
254
|
+
*
|
|
255
|
+
* Passing `null` restores {@link defaultImageResolver}.
|
|
256
|
+
*/
|
|
257
|
+
export function setImageResolver(next) {
|
|
258
|
+
resolver = next ?? defaultImageResolver;
|
|
259
|
+
}
|
|
260
|
+
/** The resolver currently installed. */
|
|
261
|
+
export function getImageResolver() {
|
|
262
|
+
return resolver;
|
|
263
|
+
}
|
|
264
|
+
/** Build one image URL through the installed resolver. */
|
|
265
|
+
export function imageUrl(src, transform) {
|
|
266
|
+
return resolver(src, transform);
|
|
267
|
+
}
|
|
268
|
+
/** Tailwind classes for a layout. Height is always `auto` so the ratio holds. */
|
|
269
|
+
export function layoutClasses(layout) {
|
|
270
|
+
switch (layout) {
|
|
271
|
+
case "full-width":
|
|
272
|
+
return "h-auto w-full";
|
|
273
|
+
case "constrained":
|
|
274
|
+
return "h-auto max-w-full";
|
|
275
|
+
default:
|
|
276
|
+
return "h-auto";
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const FIT_CLASSES = {
|
|
280
|
+
cover: "object-cover",
|
|
281
|
+
contain: "object-contain",
|
|
282
|
+
fill: "object-fill",
|
|
283
|
+
none: "object-none",
|
|
284
|
+
"scale-down": "object-scale-down",
|
|
285
|
+
};
|
|
286
|
+
const POSITION_CLASSES = {
|
|
287
|
+
center: "object-center",
|
|
288
|
+
top: "object-top",
|
|
289
|
+
bottom: "object-bottom",
|
|
290
|
+
left: "object-left",
|
|
291
|
+
right: "object-right",
|
|
292
|
+
"top left": "object-left-top",
|
|
293
|
+
"top right": "object-right-top",
|
|
294
|
+
"bottom left": "object-left-bottom",
|
|
295
|
+
"bottom right": "object-right-bottom",
|
|
296
|
+
};
|
|
297
|
+
/** The `object-fit` class for a fit value. */
|
|
298
|
+
export function fitClass(fit) {
|
|
299
|
+
return fit === undefined ? undefined : FIT_CLASSES[fit];
|
|
300
|
+
}
|
|
301
|
+
/** The `object-position` class for a position value. */
|
|
302
|
+
export function positionClass(position) {
|
|
303
|
+
return position === undefined ? undefined : POSITION_CLASSES[position];
|
|
304
|
+
}
|
|
305
|
+
/** The MIME type a `<source type>` needs for a format. */
|
|
306
|
+
export function mimeType(format) {
|
|
307
|
+
return `image/${format}`;
|
|
308
|
+
}
|
|
309
|
+
const EXTENSIONS = {
|
|
310
|
+
avif: "avif",
|
|
311
|
+
jpeg: "jpeg",
|
|
312
|
+
jpg: "jpeg",
|
|
313
|
+
png: "png",
|
|
314
|
+
webp: "webp",
|
|
315
|
+
};
|
|
316
|
+
/**
|
|
317
|
+
* The format a source's file name claims, if any.
|
|
318
|
+
*
|
|
319
|
+
* Used to pick the fallback of a `<picture>`: converting a PNG with
|
|
320
|
+
* transparency to JPEG puts a black box behind it, so the fallback follows the
|
|
321
|
+
* source rather than a fixed default. The extension is a claim, not proof —
|
|
322
|
+
* only the server knows what the bytes really are, and it re-reads them.
|
|
323
|
+
*/
|
|
324
|
+
export function formatFromSource(src) {
|
|
325
|
+
// Cut the query and fragment first, or `photo.png?v=2` has no extension.
|
|
326
|
+
const path = src.split(/[?#]/, 1)[0] ?? "";
|
|
327
|
+
const dot = path.lastIndexOf(".");
|
|
328
|
+
if (dot === -1)
|
|
329
|
+
return undefined;
|
|
330
|
+
return EXTENSIONS[path.slice(dot + 1).toLowerCase()];
|
|
331
|
+
}
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -9,4 +9,5 @@
|
|
|
9
9
|
export { type ClassValue, clsx, cn, twMerge } from "./cn.js";
|
|
10
10
|
export { type ClassOverrides, type CompoundVariant, cva, type VariantConfig, type VariantProps, type VariantSelection, type VariantShape, } from "./cva.js";
|
|
11
11
|
export { byId, resetIds, uid } from "./id.js";
|
|
12
|
+
export { allSizes, DEFAULT_RESOLUTIONS, defaultImageResolver, densitySrcSet, fitClass, formatFromSource, getImageResolver, type ImageFit, type ImageFormat, type ImageLayout, type ImagePosition, type ImageTransform, type ImageUrlResolver, imageSizes, imageSrcSet, imageUrl, imageWidths, LIMITED_RESOLUTIONS, layoutClasses, mimeType, positionClass, setImageEndpoint, setImageResolver, } from "./image.js";
|
|
12
13
|
export { accessor, callHandler, type Reactive, read, readOr } from "./props.js";
|
package/dist/lib/index.js
CHANGED
|
@@ -9,4 +9,5 @@
|
|
|
9
9
|
export { clsx, cn, twMerge } from "./cn.js";
|
|
10
10
|
export { cva, } from "./cva.js";
|
|
11
11
|
export { byId, resetIds, uid } from "./id.js";
|
|
12
|
+
export { allSizes, DEFAULT_RESOLUTIONS, defaultImageResolver, densitySrcSet, fitClass, formatFromSource, getImageResolver, imageSizes, imageSrcSet, imageUrl, imageWidths, LIMITED_RESOLUTIONS, layoutClasses, mimeType, positionClass, setImageEndpoint, setImageResolver, } from "./image.js";
|
|
12
13
|
export { accessor, callHandler, read, readOr } from "./props.js";
|
package/dist/lib/motion.d.ts
CHANGED
|
@@ -1,27 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Enter and exit animations for overlays.
|
|
3
3
|
*
|
|
4
|
-
* shadcn
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* plugin for.
|
|
4
|
+
* These are shadcn's own class strings, verbatim, and they come from the same
|
|
5
|
+
* place shadcn's do: `tw-animate-css`, which registers `animate-in` /
|
|
6
|
+
* `animate-out` and the `fade-*` / `zoom-*` / `slide-*` modifiers through
|
|
7
|
+
* `@theme inline` and `@utility`.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* nebula used to declare four bespoke keyframes and reference them through
|
|
10
|
+
* ARBITRARY animation values (`animate-[nebula-zoom-out_120ms_ease-in]`), to
|
|
11
|
+
* avoid the plugin. That is what made a missing stylesheet fatal rather than
|
|
12
|
+
* cosmetic: Tailwind compiles an arbitrary value unconditionally, so the
|
|
13
|
+
* `animation` property was always set while the keyframes behind it might exist
|
|
14
|
+
* nowhere — the browser then never fires `animationend` and every closed
|
|
15
|
+
* overlay stays in the document. With a theme-registered utility the class
|
|
16
|
+
* simply is not emitted when the theme is absent, the element animates not at
|
|
17
|
+
* all, and closing is instant. That is the whole reason upstream registers
|
|
18
|
+
* rather than inlines, and it is why the deviation is gone.
|
|
13
19
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* wait, sees no animation, and removes the node immediately. Reduced motion
|
|
17
|
-
* therefore gets an instant close rather than a delayed one, with no branch in
|
|
18
|
-
* the component.
|
|
20
|
+
* The durations are shadcn's too, which means asymmetric by way of the sheet
|
|
21
|
+
* variants only; everything else takes `tw-animate-css`'s defaults.
|
|
19
22
|
*/
|
|
20
23
|
import type { Side } from "../primitives/floating.js";
|
|
21
|
-
/**
|
|
22
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Popovers, menus, selects — scale up from the anchor.
|
|
26
|
+
*
|
|
27
|
+
* shadcn's dialog and popover string. `motion-reduce:animate-none` is ours and
|
|
28
|
+
* stays: it also makes `onExitFinished` see no animation and remove the node at
|
|
29
|
+
* once, so reduced motion gets an instant close with no branch in the component.
|
|
30
|
+
*/
|
|
31
|
+
export declare const zoomInOut = "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95 motion-reduce:animate-none";
|
|
23
32
|
/** Backdrops and tooltips — no movement, just opacity. */
|
|
24
|
-
export declare const fadeInOut = "data-[state=open]:animate-[
|
|
33
|
+
export declare const fadeInOut = "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 motion-reduce:animate-none";
|
|
25
34
|
/**
|
|
26
35
|
* Panels that slide in from an edge — Sheet, Drawer.
|
|
27
36
|
*
|
package/dist/lib/motion.js
CHANGED
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Enter and exit animations for overlays.
|
|
3
3
|
*
|
|
4
|
-
* shadcn
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* plugin for.
|
|
4
|
+
* These are shadcn's own class strings, verbatim, and they come from the same
|
|
5
|
+
* place shadcn's do: `tw-animate-css`, which registers `animate-in` /
|
|
6
|
+
* `animate-out` and the `fade-*` / `zoom-*` / `slide-*` modifiers through
|
|
7
|
+
* `@theme inline` and `@utility`.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* nebula used to declare four bespoke keyframes and reference them through
|
|
10
|
+
* ARBITRARY animation values (`animate-[nebula-zoom-out_120ms_ease-in]`), to
|
|
11
|
+
* avoid the plugin. That is what made a missing stylesheet fatal rather than
|
|
12
|
+
* cosmetic: Tailwind compiles an arbitrary value unconditionally, so the
|
|
13
|
+
* `animation` property was always set while the keyframes behind it might exist
|
|
14
|
+
* nowhere — the browser then never fires `animationend` and every closed
|
|
15
|
+
* overlay stays in the document. With a theme-registered utility the class
|
|
16
|
+
* simply is not emitted when the theme is absent, the element animates not at
|
|
17
|
+
* all, and closing is instant. That is the whole reason upstream registers
|
|
18
|
+
* rather than inlines, and it is why the deviation is gone.
|
|
13
19
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* wait, sees no animation, and removes the node immediately. Reduced motion
|
|
17
|
-
* therefore gets an instant close rather than a delayed one, with no branch in
|
|
18
|
-
* the component.
|
|
20
|
+
* The durations are shadcn's too, which means asymmetric by way of the sheet
|
|
21
|
+
* variants only; everything else takes `tw-animate-css`'s defaults.
|
|
19
22
|
*/
|
|
20
|
-
/**
|
|
21
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Popovers, menus, selects — scale up from the anchor.
|
|
25
|
+
*
|
|
26
|
+
* shadcn's dialog and popover string. `motion-reduce:animate-none` is ours and
|
|
27
|
+
* stays: it also makes `onExitFinished` see no animation and remove the node at
|
|
28
|
+
* once, so reduced motion gets an instant close with no branch in the component.
|
|
29
|
+
*/
|
|
30
|
+
export const zoomInOut = "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95 motion-reduce:animate-none";
|
|
22
31
|
/** Backdrops and tooltips — no movement, just opacity. */
|
|
23
|
-
export const fadeInOut = "data-[state=open]:animate-[
|
|
32
|
+
export const fadeInOut = "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 motion-reduce:animate-none";
|
|
24
33
|
/**
|
|
25
34
|
* Panels that slide in from an edge — Sheet, Drawer.
|
|
26
35
|
*
|
|
@@ -28,12 +37,9 @@ export const fadeInOut = "data-[state=open]:animate-[nebula-fade-in_150ms_ease-o
|
|
|
28
37
|
* depends on the panel's own size and only `translate-x-full` knows that.
|
|
29
38
|
*/
|
|
30
39
|
export function slideFrom(side) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
? "data-[state=closed]:-translate-y-full"
|
|
37
|
-
: "data-[state=closed]:translate-y-full";
|
|
38
|
-
return `transition-transform duration-300 ease-in-out data-[state=open]:translate-x-0 data-[state=open]:translate-y-0 ${axis} motion-reduce:transition-none`;
|
|
40
|
+
// shadcn's sheet: a slide utility per side, and its asymmetric durations —
|
|
41
|
+
// entering is slower than leaving, because a surface appearing wants to be
|
|
42
|
+
// noticed while one dismissed wants to be out of the way.
|
|
43
|
+
const slide = `data-[state=open]:slide-in-from-${side} data-[state=closed]:slide-out-to-${side}`;
|
|
44
|
+
return `data-[state=open]:animate-in data-[state=closed]:animate-out ${slide} data-[state=open]:duration-500 data-[state=closed]:duration-300 motion-reduce:animate-none`;
|
|
39
45
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Picture — the same image in several formats, browser picks the first it can.
|
|
3
|
+
*
|
|
4
|
+
* `Image` asks for one format and trusts it to be supported. That is fine for
|
|
5
|
+
* WebP, which every current browser reads. It is not fine for AVIF, which is
|
|
6
|
+
* 20-30% smaller again and still not universal — so AVIF can only be offered
|
|
7
|
+
* *alongside* a fallback, which is what `<picture>` is for.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* Picture({ src: "/photos/hero.jpg", alt: "…", width: 1200, height: 800 })
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* renders a `<source type="image/avif">`, a `<source type="image/webp">` and
|
|
14
|
+
* the original as the `<img>` underneath. The browser takes the first `type`
|
|
15
|
+
* it understands; one that understands none takes the `<img>`, which is also
|
|
16
|
+
* what a crawler reads.
|
|
17
|
+
*
|
|
18
|
+
* The `<img>` is `Image` itself, with every prop forwarded. That is what makes
|
|
19
|
+
* this a molecule rather than an atom, and it is deliberate: the `srcset`
|
|
20
|
+
* arithmetic, the `sizes`, the loading attributes and the layout classes have
|
|
21
|
+
* exactly one implementation, so a `<source>` can never end up offering a
|
|
22
|
+
* different ladder than the `<img>` beneath it.
|
|
23
|
+
*/
|
|
24
|
+
import { type ImageProps } from "../atoms/Image.js";
|
|
25
|
+
import { type ImageFormat } from "../lib/image.js";
|
|
26
|
+
import { type Reactive } from "../lib/props.js";
|
|
27
|
+
export interface PictureProps extends ImageProps {
|
|
28
|
+
/**
|
|
29
|
+
* Formats to offer above the fallback, best first.
|
|
30
|
+
*
|
|
31
|
+
* Order is the whole contract: a browser takes the first `type` it
|
|
32
|
+
* supports without comparing sizes, so listing WebP before AVIF means no
|
|
33
|
+
* browser ever receives the smaller file.
|
|
34
|
+
*/
|
|
35
|
+
formats?: Reactive<readonly ImageFormat[]>;
|
|
36
|
+
/**
|
|
37
|
+
* Format of the `<img>` underneath.
|
|
38
|
+
*
|
|
39
|
+
* Defaults to what the source's extension claims, falling back to JPEG.
|
|
40
|
+
* Following the source matters for PNG: a transparent logo flattened to
|
|
41
|
+
* JPEG arrives with a black background, and the fallback is exactly the
|
|
42
|
+
* path taken by the browsers least likely to be checked.
|
|
43
|
+
*/
|
|
44
|
+
fallbackFormat?: Reactive<ImageFormat | undefined>;
|
|
45
|
+
}
|
|
46
|
+
export declare const Picture: (props?: PictureProps | undefined) => import("@c9up/aurora").TemplateResult;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Picture — the same image in several formats, browser picks the first it can.
|
|
3
|
+
*
|
|
4
|
+
* `Image` asks for one format and trusts it to be supported. That is fine for
|
|
5
|
+
* WebP, which every current browser reads. It is not fine for AVIF, which is
|
|
6
|
+
* 20-30% smaller again and still not universal — so AVIF can only be offered
|
|
7
|
+
* *alongside* a fallback, which is what `<picture>` is for.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* Picture({ src: "/photos/hero.jpg", alt: "…", width: 1200, height: 800 })
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* renders a `<source type="image/avif">`, a `<source type="image/webp">` and
|
|
14
|
+
* the original as the `<img>` underneath. The browser takes the first `type`
|
|
15
|
+
* it understands; one that understands none takes the `<img>`, which is also
|
|
16
|
+
* what a crawler reads.
|
|
17
|
+
*
|
|
18
|
+
* The `<img>` is `Image` itself, with every prop forwarded. That is what makes
|
|
19
|
+
* this a molecule rather than an atom, and it is deliberate: the `srcset`
|
|
20
|
+
* arithmetic, the `sizes`, the loading attributes and the layout classes have
|
|
21
|
+
* exactly one implementation, so a `<source>` can never end up offering a
|
|
22
|
+
* different ladder than the `<img>` beneath it.
|
|
23
|
+
*/
|
|
24
|
+
import { component, html } from "@c9up/aurora";
|
|
25
|
+
import { Image, resolvedSizes, resolvedSrcSet, } from "../atoms/Image.js";
|
|
26
|
+
import { formatFromSource, mimeType } from "../lib/image.js";
|
|
27
|
+
import { read, readOr } from "../lib/props.js";
|
|
28
|
+
const DEFAULT_FORMATS = ["avif", "webp"];
|
|
29
|
+
/**
|
|
30
|
+
* The format for the `<img>`, never one of the modern ones.
|
|
31
|
+
*
|
|
32
|
+
* A fallback in a format the `<source>` elements already cover is not a
|
|
33
|
+
* fallback — a browser that cannot read AVIF or WebP would be handed WebP.
|
|
34
|
+
*/
|
|
35
|
+
function fallbackOf(props) {
|
|
36
|
+
const explicit = read(props.fallbackFormat);
|
|
37
|
+
if (explicit !== undefined)
|
|
38
|
+
return explicit;
|
|
39
|
+
const claimed = formatFromSource(read(props.src));
|
|
40
|
+
return claimed === "png" ? "png" : "jpeg";
|
|
41
|
+
}
|
|
42
|
+
export const Picture = component((props) => {
|
|
43
|
+
const formats = () => readOr(props.formats, DEFAULT_FORMATS);
|
|
44
|
+
return html `<picture data-slot="picture">
|
|
45
|
+
${() => formats().map((format) => {
|
|
46
|
+
const srcset = resolvedSrcSet(props, format);
|
|
47
|
+
// A layout that generates no ladder (`none`, or a `fixed`
|
|
48
|
+
// image with no width) leaves nothing for a `<source>` to
|
|
49
|
+
// point at, and an empty `srcset` makes the browser pick that
|
|
50
|
+
// source and render nothing.
|
|
51
|
+
if (srcset === undefined)
|
|
52
|
+
return null;
|
|
53
|
+
return html `<source
|
|
54
|
+
data-slot="picture-source"
|
|
55
|
+
type="${mimeType(format)}"
|
|
56
|
+
srcset="${srcset}"
|
|
57
|
+
sizes="${() => resolvedSizes(props)}"
|
|
58
|
+
/>`;
|
|
59
|
+
})}
|
|
60
|
+
${() => Image({ ...props, format: fallbackOf(props) })}
|
|
61
|
+
</picture>`;
|
|
62
|
+
});
|
|
@@ -29,6 +29,7 @@ export { InputOTP, type InputOTPProps } from "./InputOTP.js";
|
|
|
29
29
|
export { Item, ItemActions, ItemContent, ItemDescription, ItemGroup, ItemMedia, type ItemProps, ItemSeparator, ItemTitle, type ItemVariants, itemVariants, } from "./Item.js";
|
|
30
30
|
export { Message, type MessageProps } from "./Message.js";
|
|
31
31
|
export { type PageSlot, Pagination, type PaginationProps, pageWindow, } from "./Pagination.js";
|
|
32
|
+
export { Picture, type PictureProps } from "./Picture.js";
|
|
32
33
|
export { RadioGroup, type RadioGroupProps, type RadioOption, } from "./RadioGroup.js";
|
|
33
34
|
export { Resizable, type ResizableProps } from "./Resizable.js";
|
|
34
35
|
export { Table, TableBody, TableCaption, TableCell, type TableCellProps, TableFooter, TableHead, TableHeader, type TableProps, TableRow, type TableRowProps, } from "./Table.js";
|
package/dist/molecules/index.js
CHANGED
|
@@ -29,6 +29,7 @@ export { InputOTP } from "./InputOTP.js";
|
|
|
29
29
|
export { Item, ItemActions, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemSeparator, ItemTitle, itemVariants, } from "./Item.js";
|
|
30
30
|
export { Message } from "./Message.js";
|
|
31
31
|
export { Pagination, pageWindow, } from "./Pagination.js";
|
|
32
|
+
export { Picture } from "./Picture.js";
|
|
32
33
|
export { RadioGroup, } from "./RadioGroup.js";
|
|
33
34
|
export { Resizable } from "./Resizable.js";
|
|
34
35
|
export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, } from "./Table.js";
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
* required rather than optional — `srOnlyTitle` is there for designs that show
|
|
17
17
|
* no visible heading.
|
|
18
18
|
*/
|
|
19
|
-
import type { Child } from "../lib/children.js";
|
|
20
19
|
import { type Slot } from "../lib/children.js";
|
|
21
20
|
import { type Reactive } from "../lib/props.js";
|
|
22
21
|
export declare const dialogBackdropClasses = "fixed inset-0 z-50 bg-black/50";
|
|
@@ -24,9 +23,17 @@ export declare const dialogPanelClasses = "bg-background fixed top-1/2 left-1/2
|
|
|
24
23
|
export interface DialogProps {
|
|
25
24
|
/** Rendered inside the trigger button. Omit to drive `open` yourself. */
|
|
26
25
|
trigger?: Slot;
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Announced on open. Hide it visually with `srOnlyTitle`.
|
|
28
|
+
*
|
|
29
|
+
* A `Slot`, so it may be an accessor: one Dialog driven between "create" and
|
|
30
|
+
* "edit" needs a title that follows. It already behaved that way — the
|
|
31
|
+
* renderer binds whatever it is given — while the type said `Child` and
|
|
32
|
+
* refused the function, so the working call did not compile and the type
|
|
33
|
+
* disagreed with `children` beside it for no reason.
|
|
34
|
+
*/
|
|
35
|
+
title: Slot;
|
|
36
|
+
description?: Slot;
|
|
30
37
|
children?: Slot;
|
|
31
38
|
/** Actions, laid out bottom-right. */
|
|
32
39
|
footer?: Slot;
|
package/dist/organisms/Dialog.js
CHANGED
|
@@ -59,14 +59,14 @@ export const Dialog = component((props) => {
|
|
|
59
59
|
id="${titleId}"
|
|
60
60
|
data-slot="dialog-title"
|
|
61
61
|
class="${cn("text-lg leading-none font-semibold", props.srOnlyTitle === true ? "sr-only" : "")}"
|
|
62
|
-
>${props.title}</h2>
|
|
62
|
+
>${slot(props.title)}</h2>
|
|
63
63
|
${props.description === undefined
|
|
64
64
|
? null
|
|
65
65
|
: html `<p
|
|
66
66
|
id="${descriptionId}"
|
|
67
67
|
data-slot="dialog-description"
|
|
68
68
|
class="text-muted-foreground text-sm"
|
|
69
|
-
>${props.description}</p>`}
|
|
69
|
+
>${slot(props.description)}</p>`}
|
|
70
70
|
</div>
|
|
71
71
|
${slot(props.children)}
|
|
72
72
|
${props.footer === undefined
|