INIT
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m11s

This commit is contained in:
eewing
2026-02-18 15:17:47 -06:00
parent e74c106f85
commit ec317eb17c
11532 changed files with 1631690 additions and 1 deletions
+65
View File
@@ -0,0 +1,65 @@
import { DOMContext, type ReadableBox, type WritableBox, type ReadableBoxedValues } from "svelte-toolbelt";
import type { HTMLImgAttributes } from "svelte/elements";
import type { AvatarImageLoadingStatus } from "./types.js";
import type { RefAttachment, WithRefOpts } from "../../internal/types.js";
type CrossOrigin = HTMLImgAttributes["crossorigin"];
type ReferrerPolicy = HTMLImgAttributes["referrerpolicy"];
type AvatarImageSrc = string | null | undefined;
interface AvatarRootStateOpts extends WithRefOpts {
delayMs: ReadableBox<number>;
loadingStatus: WritableBox<AvatarImageLoadingStatus>;
}
export declare class AvatarRootState {
static create(opts: AvatarRootStateOpts): AvatarRootState;
readonly opts: AvatarRootStateOpts;
readonly domContext: DOMContext;
readonly attachment: RefAttachment;
constructor(opts: AvatarRootStateOpts);
loadImage(src: string, crossorigin?: CrossOrigin, referrerPolicy?: ReferrerPolicy): (() => void) | undefined;
props: {
readonly id: string;
readonly "data-status": AvatarImageLoadingStatus;
};
}
interface AvatarImageStateOpts extends WithRefOpts, ReadableBoxedValues<{
src: AvatarImageSrc;
crossOrigin: CrossOrigin;
referrerPolicy: ReferrerPolicy;
}> {
}
export declare class AvatarImageState {
static create(opts: AvatarImageStateOpts): AvatarImageState;
readonly opts: AvatarImageStateOpts;
readonly root: AvatarRootState;
readonly attachment: RefAttachment;
constructor(opts: AvatarImageStateOpts, root: AvatarRootState);
readonly props: {
readonly id: string;
readonly style: {
readonly display: "none" | "block";
};
readonly "data-status": AvatarImageLoadingStatus;
readonly src: AvatarImageSrc;
readonly crossorigin: "" | "anonymous" | "use-credentials" | null | undefined;
readonly referrerpolicy: globalThis.ReferrerPolicy | null | undefined;
};
}
interface AvatarFallbackStateOpts extends WithRefOpts {
}
export declare class AvatarFallbackState {
static create(opts: AvatarFallbackStateOpts): AvatarFallbackState;
readonly opts: AvatarFallbackStateOpts;
readonly root: AvatarRootState;
readonly attachment: RefAttachment;
constructor(opts: AvatarFallbackStateOpts, root: AvatarRootState);
readonly style: {
display: string;
} | undefined;
readonly props: {
readonly style: {
display: string;
} | undefined;
readonly "data-status": AvatarImageLoadingStatus;
};
}
export {};
+105
View File
@@ -0,0 +1,105 @@
import { DOMContext, attachRef, } from "svelte-toolbelt";
import { Context, watch } from "runed";
import { createBitsAttrs } from "../../internal/attrs.js";
const avatarAttrs = createBitsAttrs({
component: "avatar",
parts: ["root", "image", "fallback"],
});
const AvatarRootContext = new Context("Avatar.Root");
export class AvatarRootState {
static create(opts) {
return AvatarRootContext.set(new AvatarRootState(opts));
}
opts;
domContext;
attachment;
constructor(opts) {
this.opts = opts;
this.domContext = new DOMContext(this.opts.ref);
this.loadImage = this.loadImage.bind(this);
this.attachment = attachRef(this.opts.ref);
}
loadImage(src, crossorigin, referrerPolicy) {
if (this.opts.loadingStatus.current === "loaded")
return;
let imageTimerId;
const image = new Image();
image.src = src;
if (crossorigin !== undefined)
image.crossOrigin = crossorigin;
if (referrerPolicy)
image.referrerPolicy = referrerPolicy;
this.opts.loadingStatus.current = "loading";
image.onload = () => {
imageTimerId = this.domContext.setTimeout(() => {
this.opts.loadingStatus.current = "loaded";
}, this.opts.delayMs.current);
};
image.onerror = () => {
this.opts.loadingStatus.current = "error";
};
return () => {
if (!imageTimerId)
return;
this.domContext.clearTimeout(imageTimerId);
};
}
props = $derived.by(() => ({
id: this.opts.id.current,
[avatarAttrs.root]: "",
"data-status": this.opts.loadingStatus.current,
...this.attachment,
}));
}
export class AvatarImageState {
static create(opts) {
return new AvatarImageState(opts, AvatarRootContext.get());
}
opts;
root;
attachment;
constructor(opts, root) {
this.opts = opts;
this.root = root;
this.attachment = attachRef(this.opts.ref);
watch.pre([() => this.opts.src.current, () => this.opts.crossOrigin.current], ([src, crossOrigin]) => {
if (!src) {
this.root.opts.loadingStatus.current = "error";
return;
}
this.root.loadImage(src, crossOrigin, this.opts.referrerPolicy.current);
});
}
props = $derived.by(() => ({
id: this.opts.id.current,
style: {
display: this.root.opts.loadingStatus.current === "loaded" ? "block" : "none",
},
"data-status": this.root.opts.loadingStatus.current,
[avatarAttrs.image]: "",
src: this.opts.src.current,
crossorigin: this.opts.crossOrigin.current,
referrerpolicy: this.opts.referrerPolicy.current,
...this.attachment,
}));
}
export class AvatarFallbackState {
static create(opts) {
return new AvatarFallbackState(opts, AvatarRootContext.get());
}
opts;
root;
attachment;
constructor(opts, root) {
this.opts = opts;
this.root = root;
this.attachment = attachRef(this.opts.ref);
}
style = $derived.by(() => this.root.opts.loadingStatus.current === "loaded" ? { display: "none" } : undefined);
props = $derived.by(() => ({
style: this.style,
"data-status": this.root.opts.loadingStatus.current,
[avatarAttrs.fallback]: "",
...this.attachment,
}));
}
@@ -0,0 +1,34 @@
<script lang="ts">
import { boxWith, mergeProps } from "svelte-toolbelt";
import type { AvatarFallbackProps } from "../types.js";
import { AvatarFallbackState } from "../avatar.svelte.js";
import { createId } from "../../../internal/create-id.js";
const uid = $props.id();
let {
children,
child,
id = createId(uid),
ref = $bindable(null),
...restProps
}: AvatarFallbackProps = $props();
const fallbackState = AvatarFallbackState.create({
id: boxWith(() => id),
ref: boxWith(
() => ref,
(v) => (ref = v)
),
});
const mergedProps = $derived(mergeProps(restProps, fallbackState.props));
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<span {...mergedProps}>
{@render children?.()}
</span>
{/if}
@@ -0,0 +1,4 @@
import type { AvatarFallbackProps } from "../types.js";
declare const AvatarFallback: import("svelte").Component<AvatarFallbackProps, {}, "ref">;
type AvatarFallback = ReturnType<typeof AvatarFallback>;
export default AvatarFallback;
+37
View File
@@ -0,0 +1,37 @@
<script lang="ts">
import { boxWith, mergeProps } from "svelte-toolbelt";
import type { AvatarImageProps } from "../types.js";
import { AvatarImageState } from "../avatar.svelte.js";
import { createId } from "../../../internal/create-id.js";
const uid = $props.id();
let {
src,
child,
id = createId(uid),
ref = $bindable(null),
crossorigin = undefined,
referrerpolicy = undefined,
...restProps
}: AvatarImageProps = $props();
const imageState = AvatarImageState.create({
src: boxWith(() => src),
id: boxWith(() => id),
ref: boxWith(
() => ref,
(v) => (ref = v)
),
crossOrigin: boxWith(() => crossorigin),
referrerPolicy: boxWith(() => referrerpolicy),
});
const mergedProps = $derived(mergeProps(restProps, imageState.props));
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<img {...mergedProps} {src} />
{/if}
@@ -0,0 +1,4 @@
import type { AvatarImageProps } from "../types.js";
declare const AvatarImage: import("svelte").Component<AvatarImageProps, {}, "ref">;
type AvatarImage = ReturnType<typeof AvatarImage>;
export default AvatarImage;
+47
View File
@@ -0,0 +1,47 @@
<script lang="ts">
import { boxWith, mergeProps } from "svelte-toolbelt";
import type { AvatarRootProps } from "../types.js";
import { AvatarRootState } from "../avatar.svelte.js";
import { createId } from "../../../internal/create-id.js";
const uid = $props.id();
let {
delayMs = 0,
loadingStatus = $bindable("loading"),
onLoadingStatusChange,
child,
children,
id = createId(uid),
ref = $bindable(null),
...restProps
}: AvatarRootProps = $props();
const rootState = AvatarRootState.create({
delayMs: boxWith(() => delayMs),
loadingStatus: boxWith(
() => loadingStatus,
(v) => {
if (loadingStatus !== v) {
loadingStatus = v;
onLoadingStatusChange?.(v);
}
}
),
id: boxWith(() => id),
ref: boxWith(
() => ref,
(v) => (ref = v)
),
});
const mergedProps = $derived(mergeProps(restProps, rootState.props));
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<div {...mergedProps}>
{@render children?.()}
</div>
{/if}
+4
View File
@@ -0,0 +1,4 @@
import type { AvatarRootProps } from "../types.js";
declare const Avatar: import("svelte").Component<AvatarRootProps, {}, "ref" | "loadingStatus">;
type Avatar = ReturnType<typeof Avatar>;
export default Avatar;
+4
View File
@@ -0,0 +1,4 @@
export { default as Root } from "./components/avatar.svelte";
export { default as Image } from "./components/avatar-image.svelte";
export { default as Fallback } from "./components/avatar-fallback.svelte";
export type { AvatarRootProps as RootProps, AvatarImageProps as ImageProps, AvatarFallbackProps as FallbackProps, } from "./types.js";
+3
View File
@@ -0,0 +1,3 @@
export { default as Root } from "./components/avatar.svelte";
export { default as Image } from "./components/avatar-image.svelte";
export { default as Fallback } from "./components/avatar-fallback.svelte";
+1
View File
@@ -0,0 +1 @@
export * as Avatar from "./exports.js";
+1
View File
@@ -0,0 +1 @@
export * as Avatar from "./exports.js";
+32
View File
@@ -0,0 +1,32 @@
import type { OnChangeFn, WithChild, Without } from "../../internal/types.js";
import type { BitsPrimitiveDivAttributes, BitsPrimitiveImgAttributes, BitsPrimitiveSpanAttributes } from "../../shared/attributes.js";
export type AvatarImageLoadingStatus = "loading" | "loaded" | "error";
export type AvatarRootPropsWithoutHTML = WithChild<{
/**
* The delay in milliseconds to wait before showing the avatar once
* the image has loaded. This can be used to prevent sudden flickering
* of the image if it loads quickly.
*
* @default 0
*/
delayMs?: number;
/**
* The loading status of the image.
*
* If you are confident that the image exists and will load successfully, you can
* set this to `"loaded"` to skip the loading process (which shows the fallback)
* and immediately show the image.
*
* @default "loading"
*/
loadingStatus?: AvatarImageLoadingStatus;
/**
* A callback invoked when the loading status of the image changes.
*/
onLoadingStatusChange?: OnChangeFn<AvatarImageLoadingStatus>;
}>;
export type AvatarRootProps = AvatarRootPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, AvatarRootPropsWithoutHTML>;
export type AvatarImagePropsWithoutHTML = WithChild;
export type AvatarImageProps = AvatarImagePropsWithoutHTML & Without<BitsPrimitiveImgAttributes, AvatarImagePropsWithoutHTML>;
export type AvatarFallbackPropsWithoutHTML = WithChild;
export type AvatarFallbackProps = AvatarFallbackPropsWithoutHTML & Without<BitsPrimitiveSpanAttributes, AvatarFallbackPropsWithoutHTML>;
+1
View File
@@ -0,0 +1 @@
export {};