@jjlmoya/utils-tabletop 1.26.0 → 1.27.0

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.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -0
  3. package/src/entries.ts +2 -0
  4. package/src/index.ts +1 -0
  5. package/src/tests/locale_completeness.test.ts +1 -1
  6. package/src/tests/tool_validation.test.ts +1 -1
  7. package/src/tool/token-stamp-studio/bibliography.astro +16 -0
  8. package/src/tool/token-stamp-studio/bibliography.ts +12 -0
  9. package/src/tool/token-stamp-studio/component.astro +91 -0
  10. package/src/tool/token-stamp-studio/controller.ts +179 -0
  11. package/src/tool/token-stamp-studio/dom-views.ts +266 -0
  12. package/src/tool/token-stamp-studio/entry.ts +27 -0
  13. package/src/tool/token-stamp-studio/evaluator.ts +6 -0
  14. package/src/tool/token-stamp-studio/i18n/de.ts +132 -0
  15. package/src/tool/token-stamp-studio/i18n/en.ts +132 -0
  16. package/src/tool/token-stamp-studio/i18n/es.ts +132 -0
  17. package/src/tool/token-stamp-studio/i18n/fr.ts +132 -0
  18. package/src/tool/token-stamp-studio/i18n/id.ts +132 -0
  19. package/src/tool/token-stamp-studio/i18n/it.ts +132 -0
  20. package/src/tool/token-stamp-studio/i18n/ja.ts +132 -0
  21. package/src/tool/token-stamp-studio/i18n/ko.ts +132 -0
  22. package/src/tool/token-stamp-studio/i18n/nl.ts +132 -0
  23. package/src/tool/token-stamp-studio/i18n/pl.ts +132 -0
  24. package/src/tool/token-stamp-studio/i18n/pt.ts +132 -0
  25. package/src/tool/token-stamp-studio/i18n/ru.ts +132 -0
  26. package/src/tool/token-stamp-studio/i18n/sv.ts +132 -0
  27. package/src/tool/token-stamp-studio/i18n/tr.ts +132 -0
  28. package/src/tool/token-stamp-studio/i18n/zh.ts +132 -0
  29. package/src/tool/token-stamp-studio/index.ts +11 -0
  30. package/src/tool/token-stamp-studio/logic.test.ts +36 -0
  31. package/src/tool/token-stamp-studio/logic.ts +127 -0
  32. package/src/tool/token-stamp-studio/seo.astro +16 -0
  33. package/src/tool/token-stamp-studio/storage.ts +69 -0
  34. package/src/tool/token-stamp-studio/store.ts +139 -0
  35. package/src/tool/token-stamp-studio/token-stamp-studio.css +646 -0
  36. package/src/tool/token-stamp-studio/types.ts +65 -0
  37. package/src/tool/token-stamp-studio/ui.ts +50 -0
  38. package/src/tool/token-stamp-studio/workspace-media.ts +36 -0
  39. package/src/tool/token-stamp-studio/workspace.ts +83 -0
  40. package/src/tools.ts +2 -0
@@ -0,0 +1,69 @@
1
+ import type { SavedMarker, TokenStampState } from './types';
2
+
3
+ const STORAGE_KEY = 'jjlmoya-token-stamp-studio';
4
+ const LIBRARY_KEY = 'jjlmoya-token-stamp-library';
5
+ const ACTIVE_MARKER_KEY = 'jjlmoya-token-stamp-active-marker';
6
+
7
+ export function saveTokenState(state: TokenStampState): boolean {
8
+ try {
9
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
10
+ return true;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ export function loadTokenState(): TokenStampState | null {
17
+ try {
18
+ const raw = localStorage.getItem(STORAGE_KEY);
19
+ return raw ? JSON.parse(raw) as TokenStampState : null;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ export function clearTokenState(): void {
26
+ try {
27
+ localStorage.removeItem(STORAGE_KEY);
28
+ } catch {}
29
+ }
30
+
31
+ export function listSavedMarkers(): SavedMarker[] {
32
+ try {
33
+ const raw = localStorage.getItem(LIBRARY_KEY);
34
+ const markers = raw ? JSON.parse(raw) as SavedMarker[] : [];
35
+ return Array.isArray(markers) ? markers : [];
36
+ } catch {
37
+ return [];
38
+ }
39
+ }
40
+
41
+ export function saveMarker(marker: SavedMarker): SavedMarker[] {
42
+ const markers = [marker, ...listSavedMarkers().filter((item) => item.id !== marker.id)].slice(0, 24);
43
+ try {
44
+ localStorage.setItem(LIBRARY_KEY, JSON.stringify(markers));
45
+ } catch {}
46
+ return markers;
47
+ }
48
+
49
+ export function deleteMarker(id: string): SavedMarker[] {
50
+ const markers = listSavedMarkers().filter((marker) => marker.id !== id);
51
+ try {
52
+ localStorage.setItem(LIBRARY_KEY, JSON.stringify(markers));
53
+ } catch {}
54
+ return markers;
55
+ }
56
+
57
+ export function saveActiveMarkerId(id: string): void {
58
+ try {
59
+ localStorage.setItem(ACTIVE_MARKER_KEY, id);
60
+ } catch {}
61
+ }
62
+
63
+ export function loadActiveMarkerId(): string | null {
64
+ try {
65
+ return localStorage.getItem(ACTIVE_MARKER_KEY);
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
@@ -0,0 +1,139 @@
1
+ import { createDefaultState, FRAME_PRESETS } from './logic';
2
+ import { drawTokenCanvas, renderFrameGallery, renderMarkerLibrary, renderTextLayers } from './dom-views';
3
+ import { deleteMarker, listSavedMarkers, loadActiveMarkerId, loadTokenState, saveActiveMarkerId, saveMarker, saveTokenState } from './storage';
4
+ import { loadImage } from './workspace-media';
5
+ import type { StudioElements } from './controller';
6
+ import type { TokenStampUI } from './ui';
7
+ import type { SavedMarker, TokenStampState } from './types';
8
+
9
+ function markerName(state: TokenStampState): string {
10
+ return state.markerName?.trim() || state.texts.find((text) => text.text.trim())?.text.trim() || state.imageName.replace(/\.[^/.]+$/, '') || 'Unnamed marker';
11
+ }
12
+
13
+ function createMarkerId(): string {
14
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
15
+ }
16
+
17
+ function getActiveMarkerId(markers: SavedMarker[]): string {
18
+ const stored = loadActiveMarkerId();
19
+ if (stored) return stored;
20
+ if (markers[0]) return markers[0].id;
21
+ return createMarkerId();
22
+ }
23
+
24
+ function normalizeActiveMarkerId(markers: SavedMarker[], id: string): string {
25
+ return markers.some((marker) => marker.id === id) ? id : markers[0]!.id;
26
+ }
27
+
28
+ function createInitialMarkerSession(): { state: TokenStampState; markers: SavedMarker[]; activeMarkerId: string } {
29
+ const storedState = loadTokenState() || createDefaultState();
30
+ let markers = listSavedMarkers();
31
+ let activeMarkerId = getActiveMarkerId(markers);
32
+ if (markers.length === 0) markers = saveMarker({ id: activeMarkerId, name: markerName(storedState), updatedAt: Date.now(), state: storedState });
33
+ activeMarkerId = normalizeActiveMarkerId(markers, activeMarkerId);
34
+ const active = markers.find((marker) => marker.id === activeMarkerId);
35
+ return { markers, activeMarkerId, state: active?.state || storedState };
36
+ }
37
+
38
+ export class StudioStore {
39
+ state: TokenStampState;
40
+ markers: SavedMarker[];
41
+ activeMarkerId: string;
42
+ image: HTMLImageElement | null = null;
43
+
44
+ constructor(private readonly elements: StudioElements, private readonly ui: TokenStampUI) {
45
+ const session = createInitialMarkerSession();
46
+ this.markers = session.markers;
47
+ this.activeMarkerId = session.activeMarkerId;
48
+ this.state = session.state;
49
+ saveActiveMarkerId(this.activeMarkerId);
50
+ }
51
+
52
+ async restore(): Promise<void> {
53
+ if (!this.state.imageSrc) return;
54
+ try { this.image = await loadImage(this.state.imageSrc); } catch { this.state = { ...this.state, imageSrc: null, imageName: '' }; }
55
+ }
56
+
57
+ getState = (): TokenStampState => this.state;
58
+ getMarkers = (): SavedMarker[] => this.markers;
59
+ getImage = (): HTMLImageElement | null => this.image;
60
+
61
+ setState = (next: TokenStampState): void => {
62
+ this.state = next;
63
+ saveTokenState(next);
64
+ const active = this.markers.find((marker) => marker.id === this.activeMarkerId);
65
+ if (active) this.markers = saveMarker({ ...active, name: markerName(next), updatedAt: Date.now(), state: next });
66
+ this.render();
67
+ };
68
+
69
+ setImage = (nextImage: HTMLImageElement | null): void => { this.image = nextImage; this.render(); };
70
+
71
+ selectMarker = (marker: SavedMarker): void => {
72
+ this.activeMarkerId = marker.id;
73
+ this.state = marker.state;
74
+ saveActiveMarkerId(marker.id);
75
+ if (marker.state.imageSrc) void loadImage(marker.state.imageSrc).then(this.setImage).catch(() => this.setImage(null));
76
+ else this.setImage(null);
77
+ this.renderLibrary();
78
+ };
79
+
80
+ addMarker = (): void => {
81
+ const state = createDefaultState();
82
+ const marker: SavedMarker = { id: createMarkerId(), name: markerName(state), updatedAt: Date.now(), state };
83
+ this.markers = saveMarker(marker);
84
+ this.activeMarkerId = marker.id;
85
+ this.image = null;
86
+ saveActiveMarkerId(marker.id);
87
+ this.setState(state);
88
+ };
89
+
90
+ removeMarker = (id: string): void => {
91
+ this.markers = deleteMarker(id);
92
+ if (id !== this.activeMarkerId) return this.renderLibrary();
93
+ const next = this.markers[0];
94
+ if (next) this.selectMarker(next);
95
+ else this.addMarker();
96
+ };
97
+
98
+ render(): void {
99
+ drawTokenCanvas(this.elements.canvas, this.state, this.image);
100
+ renderFrameGallery(this.elements.frames, FRAME_PRESETS, this.state.frameId);
101
+ renderTextLayers(this.elements.textLayers, this.state.texts, this.state.selectedTextId, this.ui.noText);
102
+ this.elements.currentImage.textContent = this.state.imageName || this.ui.noImage;
103
+ this.elements.markerName.value = this.state.markerName || '';
104
+ this.setInputValues();
105
+ this.renderLibrary();
106
+ }
107
+
108
+ setInputValues(): void {
109
+ const text = this.state.texts.find((item) => item.id === this.state.selectedTextId);
110
+ this.elements.textInput.value = text?.text || '';
111
+ this.elements.textSize.value = String(text?.size || 54);
112
+ this.elements.imageZoom.value = String(this.state.imageZoom);
113
+ this.elements.scale.value = String(this.state.scale);
114
+ this.elements.borderWidth.value = String(this.state.borderWidth);
115
+ this.elements.borderOpacity.value = String(this.state.borderOpacity);
116
+ this.elements.overlayOpacity.value = String(this.state.overlayOpacity);
117
+ this.setOutputValues();
118
+ }
119
+
120
+ setOutputValues(): void {
121
+ const root = this.elements.root;
122
+ const output = (selector: string, value: string) => { const element = root.querySelector<HTMLOutputElement>(selector); if (element) element.value = value; };
123
+ output('[data-border-width-output]', String(this.state.borderWidth));
124
+ output('[data-border-opacity-output]', `${Math.round(this.state.borderOpacity * 100)}%`);
125
+ output('[data-overlay-opacity-output]', `${Math.round(this.state.overlayOpacity * 100)}%`);
126
+ output('[data-image-zoom-output]', `${Math.round(this.state.imageZoom * 100)}%`);
127
+ output('[data-scale-output]', `${Math.round(this.state.scale * 100)}%`);
128
+ output('[data-text-size-output]', String(this.state.texts.find((item) => item.id === this.state.selectedTextId)?.size || 54));
129
+ ['background', 'border', 'text', 'overlay'].forEach((name) => {
130
+ const element = root.querySelector<HTMLInputElement>(`[data-color="${name}"]`);
131
+ const key = name === 'text' ? 'textColor' : name;
132
+ if (element) element.value = this.state[key as keyof TokenStampState] as string;
133
+ });
134
+ }
135
+
136
+ renderLibrary(): void {
137
+ renderMarkerLibrary(this.elements.library, this.markers, { emptyText: this.ui.noSavedMarkers, reuseText: this.ui.reuseMarker, deleteText: this.ui.deleteMarker, newText: this.ui.newMarker, activeId: this.activeMarkerId });
138
+ }
139
+ }