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

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

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

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

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

Loading…
Cancel
Save

Powered by TurnKey Linux.