Clean up terra color picker reactive props

main
dev 6 months ago
parent 5630dc38bf
commit 20cfd3f50b

@ -23,7 +23,7 @@
let activeFormat = $state<"hex" | "rgb" | "hsl">("hex");
</script>
<div class="page">
<div class="page w-full h-full bg-gray-300">
<header>
<h1>ColorPicker</h1>
<p>Headless, composable, accessible. Part of Bits UI.</p>

@ -20,6 +20,23 @@ import type {
} from "./types";
import { clamp } from '$lib';
type ColorPickerRootStateProps = {
value: Active<ColorValue | undefined>;
defaultValue: Active<ColorValue | undefined>;
onValueChange: Active<((value: ColorValue) => void) | undefined>;
disabled: Active<boolean>;
format: Active<ColorFormat>;
};
type ColorPickerInputStateProps = {
format: Active<ColorFormat>;
};
type ColorPickerSwatchesStateProps = {
colors: Active<string[]>;
onSelect: Active<((color: string) => void) | undefined>;
};
// ─── Root State ───────────────────────────────────────────────────────────────
export class ColorPickerRootState {
@ -37,15 +54,15 @@ export class ColorPickerRootState {
readonly disabled: Active<boolean>;
readonly format: Active<ColorFormat>;
readonly #onValueChange: ((v: ColorValue) => void) | undefined;
readonly #onValueChange: Active<((v: ColorValue) => void) | undefined>;
constructor(props: ColorPickerRootProps) {
const initial = props.defaultValue ?? props.value ?? DEFAULT_COLOR;
constructor(props: ColorPickerRootStateProps) {
const initial = props.defaultValue.current ?? props.value.current ?? DEFAULT_COLOR;
this.#hsv = state<HSVColor>(initial.hsv);
this.#hsv = state<HSVColor>(initial.hsv);
this.#onValueChange = props.onValueChange;
this.disabled = readableActive(() => props.disabled ?? false);
this.format = readableActive(() => props.format ?? "hex");
this.disabled = props.disabled;
this.format = props.format;
this.value = readableActive(() => colorValueFromHsv(this.#hsv.current));
this.hex = readableActive(() => hsvToHex(this.#hsv.current));
@ -58,7 +75,7 @@ export class ColorPickerRootState {
setHsv(hsv: Partial<HSVColor>) {
const next = { ...this.#hsv.current, ...hsv };
this.#hsv.current = next;
this.#onValueChange?.(colorValueFromHsv(next));
this.#onValueChange.current?.(colorValueFromHsv(next));
}
setHue(h: number) {
@ -77,18 +94,18 @@ export class ColorPickerRootState {
const hsv = parseColor(css);
if (!hsv) return false;
this.#hsv.current = hsv;
this.#onValueChange?.(colorValueFromHsv(hsv));
this.#onValueChange.current?.(colorValueFromHsv(hsv));
return true;
}
setFromColorValue(color: ColorValue) {
this.#hsv.current = color.hsv;
this.#onValueChange?.(color);
this.#onValueChange.current?.(color);
}
// ─── Factory ─────────────────────────────────────────────────────────────
static create(props: ColorPickerRootProps) {
static create(props: ColorPickerRootStateProps) {
return ColorPickerRootState.ctx.set(new ColorPickerRootState(props));
}
@ -221,7 +238,7 @@ export class ColorPickerAlphaSliderState {
export class ColorPickerInputState {
readonly #root: ColorPickerRootState;
readonly format: ColorFormat;
readonly format: Active<ColorFormat>;
// Displayed string — formatted according to `format`
readonly displayValue: Active<string>;
@ -231,13 +248,13 @@ export class ColorPickerInputState {
readonly inputValue: Active<string>;
constructor(props: ColorPickerInputProps) {
constructor(props: ColorPickerInputStateProps) {
this.#root = ColorPickerRootState.get();
this.format = props.format;
this.displayValue = readableActive(() => {
const color = this.#root.value.current;
switch (this.format) {
switch (this.format.current) {
case "hex": return color.hex;
case "rgb": {
const { r, g, b, a } = color.rgb;
@ -275,7 +292,7 @@ export class ColorPickerInputState {
if (e.key === "Escape") this.#editBuffer = null;
}
static create(props: ColorPickerInputProps) {
static create(props: ColorPickerInputStateProps) {
return new ColorPickerInputState(props);
}
}
@ -284,10 +301,10 @@ export class ColorPickerInputState {
export class ColorPickerSwatchesState {
readonly #root: ColorPickerRootState;
readonly #onSelect: ((color: string) => void) | undefined;
readonly colors: string[];
readonly #onSelect: Active<((color: string) => void) | undefined>;
readonly colors: Active<string[]>;
constructor(props: ColorPickerSwatchesProps) {
constructor(props: ColorPickerSwatchesStateProps) {
this.#root = ColorPickerRootState.get();
this.colors = props.colors;
this.#onSelect = props.onSelect;
@ -295,7 +312,7 @@ export class ColorPickerSwatchesState {
selectSwatch(color: string) {
const success = this.#root.setFromString(color);
if (success) this.#onSelect?.(color);
if (success) this.#onSelect.current?.(color);
}
isSelected(color: string): boolean {
@ -305,7 +322,7 @@ export class ColorPickerSwatchesState {
return hsvToHex(hsv) === current;
}
static create(props: ColorPickerSwatchesProps) {
static create(props: ColorPickerSwatchesStateProps) {
return new ColorPickerSwatchesState(props);
}
}

@ -1,17 +1,22 @@
<script lang="ts">
import { readableActive } from "../../utils";
import { ColorPickerInputState } from "../color-picker.svelte";
import type { ColorPickerInputProps } from "../types";
let { format, class: className }: ColorPickerInputProps = $props();
const input = ColorPickerInputState.create({ format });
const input = ColorPickerInputState.create({
format: readableActive(() => format),
});
const ariaLabel = $derived(`Color value (${format})`);
</script>
<input
type="text"
class={className}
value={input.inputValue.current}
aria-label="Color value ({format})"
aria-label={ariaLabel}
spellcheck="false"
autocomplete="off"
oninput={(e) => input.handleInput(e.currentTarget.value)}

@ -1,4 +1,5 @@
<script lang="ts">
import { readableActive } from "../../utils";
import { ColorPickerRootState } from "../color-picker.svelte";
import type { ColorPickerRootProps } from "../types";
@ -12,11 +13,11 @@
}: ColorPickerRootProps & { children?: import("svelte").Snippet } = $props();
ColorPickerRootState.create({
value,
defaultValue,
onValueChange,
disabled,
format,
value: readableActive(() => value),
defaultValue: readableActive(() => defaultValue),
onValueChange: readableActive(() => onValueChange),
disabled: readableActive(() => disabled),
format: readableActive(() => format),
});
</script>

@ -1,11 +1,15 @@
<script lang="ts">
import { readableActive } from "../../utils";
import { ColorPickerSwatchesState } from "../color-picker.svelte";
import type { ColorPickerSwatchesProps } from "../types";
import ColorPickerSwatch from "./color-picker-swatch.svelte";
let { colors, onSelect, class: className }: ColorPickerSwatchesProps = $props();
const swatches = ColorPickerSwatchesState.create({ colors, onSelect });
const swatches = ColorPickerSwatchesState.create({
colors: readableActive(() => colors),
onSelect: readableActive(() => onSelect),
});
</script>
<div
@ -13,7 +17,7 @@
role="group"
aria-label="Color swatches"
>
{#each swatches.colors as color (color)}
{#each swatches.colors.current as color (color)}
<ColorPickerSwatch
{color}
selected={swatches.isSelected(color)}

Loading…
Cancel
Save

Powered by TurnKey Linux.