fix: update persisted state workflow
This commit is contained in:
@@ -31,7 +31,8 @@
|
||||
{width}
|
||||
{height}
|
||||
fontSize={textConfigState.fontSize}
|
||||
fill={textConfigState.color}
|
||||
stroke={textConfigState.color}
|
||||
fill="transparent"
|
||||
fontFamily={textConfigState.fontFamily}
|
||||
align={textConfigState.align}
|
||||
wrap="none"
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
<SettingsRow label="Смещение">
|
||||
<RangeSlider
|
||||
bind:value={textConfigState.offsetY}
|
||||
min={TYPOGRAPHY.VERTICAL_OFFSET_MIN}
|
||||
max={TYPOGRAPHY.VERTICAL_OFFSET_MAX}
|
||||
min={TYPOGRAPHY.OFFSET_Y_MIN}
|
||||
max={TYPOGRAPHY.OFFSET_Y_MAX}
|
||||
step={1}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
@@ -35,8 +35,9 @@ export const TYPOGRAPHY = {
|
||||
PADDING_X_MAX: 100,
|
||||
|
||||
// Vertical offset for range inputs
|
||||
VERTICAL_OFFSET_MAX: 100,
|
||||
VERTICAL_OFFSET_MIN: -100,
|
||||
OFFSET_Y_MAX: 100,
|
||||
OFFSET_Y_MIN: -100,
|
||||
OFFSET_Y_DEFAULT: 0,
|
||||
|
||||
// Colors
|
||||
TEXT_COLOR_DEFAULT: "#ffffff",
|
||||
@@ -82,7 +83,7 @@ export const TextAlign = {
|
||||
|
||||
export type TextAlignType = (typeof TextAlign)[keyof typeof TextAlign];
|
||||
|
||||
export const DEFAULT_TEXT_ALIGN: TextAlignType = TextAlign.CENTER;
|
||||
export const TEXT_ALIGN_DEFAULT: TextAlignType = TextAlign.CENTER;
|
||||
|
||||
export const Theme = {
|
||||
DARK: "dark",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
export const STATE_DATA = Symbol("state-data");
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
export const DEBOUNCE_DURATION = import.meta.env.MODE === "test" ? 0 : 500;
|
||||
|
||||
export interface Persistable<D> {
|
||||
[STATE_DATA]: D;
|
||||
toSnapshot(): D;
|
||||
fromSnapshot(data: Partial<D>): void;
|
||||
}
|
||||
|
||||
export function withPersistence<D extends object, T extends Persistable<D>>(
|
||||
export function withPersistence<D, T extends Persistable<D>>(
|
||||
key: string,
|
||||
state: T,
|
||||
debounceMs = DEBOUNCE_DURATION,
|
||||
@@ -21,7 +21,7 @@ export function withPersistence<D extends object, T extends Persistable<D>>(
|
||||
const parsed = JSON.parse(saved);
|
||||
|
||||
if (parsed) {
|
||||
state[STATE_DATA] = parsed;
|
||||
state.fromSnapshot(parsed);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error repairing state for ${key}`, e);
|
||||
@@ -30,7 +30,7 @@ export function withPersistence<D extends object, T extends Persistable<D>>(
|
||||
|
||||
$effect.root(() => {
|
||||
$effect(() => {
|
||||
const data = JSON.stringify($state.snapshot(state[STATE_DATA]));
|
||||
const data = JSON.stringify($state.snapshot(state.toSnapshot()));
|
||||
|
||||
if (debounceMs > 0) {
|
||||
const timeout = setTimeout(() => localStorage.setItem(key, data), debounceMs);
|
||||
|
||||
@@ -1,83 +1,55 @@
|
||||
import { type TextAlignType } from "$lib/constants";
|
||||
import { TEXT_ALIGN_DEFAULT, TYPOGRAPHY, type TextAlignType } from "$lib/constants";
|
||||
import type { HexColor } from "$lib/types";
|
||||
import { STATE_DATA, withPersistence } from "./persisted.svelte";
|
||||
import { withPersistence, type Persistable } from "./persisted.svelte";
|
||||
|
||||
export type TextConfig = {
|
||||
export type TextConfigDTO = {
|
||||
fontSize: number;
|
||||
fontFamily: string;
|
||||
color: HexColor;
|
||||
align: TextAlignType;
|
||||
paddingX: number;
|
||||
offsetY: number;
|
||||
outlined: boolean;
|
||||
};
|
||||
|
||||
function createState() {
|
||||
const defaults: TextConfig = {
|
||||
fontSize: 24,
|
||||
fontFamily: "Arial",
|
||||
color: "#ffffff",
|
||||
align: "center",
|
||||
paddingX: 10,
|
||||
offsetY: 0,
|
||||
};
|
||||
const state: TextConfig = $state({ ...defaults });
|
||||
const textConfigKeys: (keyof TextConfigDTO)[] = [
|
||||
"fontSize",
|
||||
"fontFamily",
|
||||
"color",
|
||||
"align",
|
||||
"paddingX",
|
||||
"offsetY",
|
||||
"outlined",
|
||||
];
|
||||
|
||||
return {
|
||||
get fontSize() {
|
||||
return state.fontSize;
|
||||
},
|
||||
set fontSize(value: number) {
|
||||
state.fontSize = value;
|
||||
},
|
||||
get fontFamily() {
|
||||
return state.fontFamily;
|
||||
},
|
||||
set fontFamily(fontFamily: string) {
|
||||
state.fontFamily = fontFamily;
|
||||
},
|
||||
get color() {
|
||||
return state.color;
|
||||
},
|
||||
set color(color: HexColor) {
|
||||
state.color = color;
|
||||
},
|
||||
get align() {
|
||||
return state.align;
|
||||
},
|
||||
set align(align: TextAlignType) {
|
||||
state.align = align;
|
||||
},
|
||||
get paddingX() {
|
||||
return state.paddingX;
|
||||
},
|
||||
set paddingX(paddingX: number) {
|
||||
state.paddingX = paddingX;
|
||||
},
|
||||
get offsetY() {
|
||||
return state.offsetY;
|
||||
},
|
||||
set offsetY(offsetY: number) {
|
||||
state.offsetY = offsetY;
|
||||
},
|
||||
get [STATE_DATA]() {
|
||||
return {
|
||||
fontSize: state.fontSize,
|
||||
fontFamily: state.fontFamily,
|
||||
color: state.color,
|
||||
align: state.align,
|
||||
paddingX: state.paddingX,
|
||||
offsetY: state.offsetY,
|
||||
};
|
||||
},
|
||||
set [STATE_DATA](newConfig: TextConfig) {
|
||||
state.fontSize = newConfig.fontSize;
|
||||
state.fontFamily = newConfig.fontFamily;
|
||||
state.color = newConfig.color;
|
||||
state.align = newConfig.align;
|
||||
state.paddingX = newConfig.paddingX;
|
||||
state.offsetY = newConfig.offsetY;
|
||||
},
|
||||
};
|
||||
export class TextConfigState implements Persistable<TextConfigDTO> {
|
||||
fontSize: number = $state(TYPOGRAPHY.FONT_SIZE_DEFAULT);
|
||||
fontFamily: string = $state(TYPOGRAPHY.FONT_FAMILY_DEFAULT);
|
||||
color: HexColor = $state(TYPOGRAPHY.TEXT_COLOR_DEFAULT);
|
||||
align: TextAlignType = $state(TEXT_ALIGN_DEFAULT);
|
||||
paddingX: number = $state(TYPOGRAPHY.PADDING_X_DEFAULT);
|
||||
offsetY: number = $state(TYPOGRAPHY.OFFSET_Y_DEFAULT);
|
||||
outlined: boolean = $state(false);
|
||||
|
||||
toSnapshot(): TextConfigDTO {
|
||||
return {
|
||||
fontSize: this.fontSize,
|
||||
fontFamily: this.fontFamily,
|
||||
color: this.color,
|
||||
align: this.align,
|
||||
paddingX: this.paddingX,
|
||||
offsetY: this.offsetY,
|
||||
outlined: this.outlined,
|
||||
};
|
||||
}
|
||||
|
||||
fromSnapshot(data: Partial<TextConfigDTO>): void {
|
||||
for (const key of textConfigKeys) {
|
||||
if (key in data && data[key] !== undefined) {
|
||||
(this as TextConfigDTO)[key] = data[key] as never;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const textConfigState = withPersistence("text-config", createState());
|
||||
export const textConfigState = withPersistence("text-config", new TextConfigState());
|
||||
|
||||
+40
-33
@@ -1,43 +1,50 @@
|
||||
import { STATE_DATA, withPersistence } from "./persisted.svelte";
|
||||
import { withPersistence, type Persistable } from "./persisted.svelte";
|
||||
|
||||
export interface TextItem {
|
||||
text: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
const defaultTexts: Array<TextItem> = ["About me", "Links", "Projects"].map((text, idx) => ({
|
||||
text,
|
||||
id: idx,
|
||||
}));
|
||||
export type TextState = Array<TextItem>;
|
||||
export type TextStateDTO = Array<string>;
|
||||
|
||||
export function createState() {
|
||||
let texts: Array<TextItem> = $state(defaultTexts);
|
||||
let nextId = $state(defaultTexts.length);
|
||||
const defaultTexts: TextStateDTO = ["About me", "Links", "Projects"];
|
||||
|
||||
return {
|
||||
get texts() {
|
||||
return texts;
|
||||
},
|
||||
addText(text: string) {
|
||||
if (text.trim().length === 0) return;
|
||||
texts.push({ text, id: nextId });
|
||||
nextId++;
|
||||
},
|
||||
removeText(id: number) {
|
||||
texts = texts.filter((textItem) => textItem.id !== id);
|
||||
},
|
||||
clear() {
|
||||
texts = [];
|
||||
nextId = 0;
|
||||
},
|
||||
get [STATE_DATA]() {
|
||||
return texts.map(({ text }) => text);
|
||||
},
|
||||
set [STATE_DATA](newTexts: Array<string>) {
|
||||
texts = newTexts.map((text, idx) => ({ text, id: idx }));
|
||||
nextId = newTexts.length;
|
||||
},
|
||||
};
|
||||
export class TextsState implements Persistable<TextStateDTO> {
|
||||
#texts: Array<TextItem> = $state(this.fromDTO(defaultTexts));
|
||||
#nextId = $state(defaultTexts.length);
|
||||
|
||||
get texts() {
|
||||
return this.#texts;
|
||||
}
|
||||
|
||||
addText(text: string) {
|
||||
if (text.trim().length === 0) return;
|
||||
this.#texts.push({ text, id: this.#nextId });
|
||||
this.#nextId++;
|
||||
}
|
||||
|
||||
removeText(id: number) {
|
||||
this.#texts = this.#texts.filter((textItem) => textItem.id !== id);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.#texts = [];
|
||||
this.#nextId = 0;
|
||||
}
|
||||
|
||||
toSnapshot(): TextStateDTO {
|
||||
return this.#texts.map(({ text }) => text);
|
||||
}
|
||||
|
||||
fromSnapshot(data: TextStateDTO) {
|
||||
this.#texts = this.fromDTO(data);
|
||||
this.#nextId = data.length;
|
||||
}
|
||||
|
||||
fromDTO(data: TextStateDTO): TextState {
|
||||
return data.map((text, idx) => ({ text, id: idx }));
|
||||
}
|
||||
}
|
||||
|
||||
export const textsState = withPersistence("texts", createState());
|
||||
export const textsState = withPersistence("texts", new TextsState());
|
||||
|
||||
+24
-24
@@ -1,28 +1,28 @@
|
||||
import { Theme, type ThemeType } from "$lib/constants";
|
||||
import { STATE_DATA, withPersistence } from "./persisted.svelte";
|
||||
import { withPersistence, type Persistable } from "./persisted.svelte";
|
||||
|
||||
function createState() {
|
||||
let current: ThemeType = $state(Theme.LIGHT);
|
||||
|
||||
return {
|
||||
get current() {
|
||||
return current;
|
||||
},
|
||||
set current(value: ThemeType) {
|
||||
current = value;
|
||||
},
|
||||
toggle() {
|
||||
current = current === Theme.DARK ? Theme.LIGHT : Theme.DARK;
|
||||
},
|
||||
get [STATE_DATA]() {
|
||||
return {
|
||||
current,
|
||||
};
|
||||
},
|
||||
set [STATE_DATA](data: { current: ThemeType }) {
|
||||
current = data.current;
|
||||
},
|
||||
};
|
||||
export interface Theme {
|
||||
current: ThemeType;
|
||||
}
|
||||
|
||||
export const themeState = withPersistence("theme", createState());
|
||||
export class ThemeState implements Persistable<Theme> {
|
||||
current: ThemeType = $state(Theme.LIGHT);
|
||||
|
||||
toggle() {
|
||||
this.current = this.current === Theme.DARK ? Theme.LIGHT : Theme.DARK;
|
||||
}
|
||||
|
||||
toSnapshot(): Theme {
|
||||
return {
|
||||
current: this.current,
|
||||
};
|
||||
}
|
||||
|
||||
fromSnapshot(data: Partial<Theme>): void {
|
||||
if (data.current !== undefined) {
|
||||
this.current = data.current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const themeState = withPersistence("theme", new ThemeState());
|
||||
|
||||
Reference in New Issue
Block a user