@jjlmoya/utils-tabletop 1.5.0 → 1.6.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 (46) hide show
  1. package/package.json +5 -3
  2. package/src/entries.ts +5 -1
  3. package/src/index.ts +2 -0
  4. package/src/tests/locale_completeness.test.ts +2 -1
  5. package/src/tests/tool_validation.test.ts +2 -1
  6. package/src/tool/investigation-board/bibliography.astro +16 -0
  7. package/src/tool/investigation-board/bibliography.ts +12 -0
  8. package/src/tool/investigation-board/boardContextMenu.ts +64 -0
  9. package/src/tool/investigation-board/client.ts +223 -0
  10. package/src/tool/investigation-board/component.astro +23 -0
  11. package/src/tool/investigation-board/components/BoardCanvas.astro +39 -0
  12. package/src/tool/investigation-board/components/CardDrawer.astro +73 -0
  13. package/src/tool/investigation-board/components/ConnectionModal.astro +49 -0
  14. package/src/tool/investigation-board/components/ContextMenu.astro +26 -0
  15. package/src/tool/investigation-board/components/ControlPanel.astro +57 -0
  16. package/src/tool/investigation-board/connectionModal.ts +60 -0
  17. package/src/tool/investigation-board/dom.ts +89 -0
  18. package/src/tool/investigation-board/drawer.ts +110 -0
  19. package/src/tool/investigation-board/entry.ts +59 -0
  20. package/src/tool/investigation-board/i18n/de.ts +126 -0
  21. package/src/tool/investigation-board/i18n/en.ts +127 -0
  22. package/src/tool/investigation-board/i18n/es.ts +126 -0
  23. package/src/tool/investigation-board/i18n/fr.ts +126 -0
  24. package/src/tool/investigation-board/i18n/id.ts +126 -0
  25. package/src/tool/investigation-board/i18n/it.ts +126 -0
  26. package/src/tool/investigation-board/i18n/ja.ts +126 -0
  27. package/src/tool/investigation-board/i18n/ko.ts +126 -0
  28. package/src/tool/investigation-board/i18n/nl.ts +126 -0
  29. package/src/tool/investigation-board/i18n/pl.ts +126 -0
  30. package/src/tool/investigation-board/i18n/pt.ts +126 -0
  31. package/src/tool/investigation-board/i18n/ru.ts +126 -0
  32. package/src/tool/investigation-board/i18n/sv.ts +126 -0
  33. package/src/tool/investigation-board/i18n/tr.ts +126 -0
  34. package/src/tool/investigation-board/i18n/zh.ts +126 -0
  35. package/src/tool/investigation-board/index.ts +11 -0
  36. package/src/tool/investigation-board/interactDrag.ts +58 -0
  37. package/src/tool/investigation-board/investigation-board.css +799 -0
  38. package/src/tool/investigation-board/layout.ts +16 -0
  39. package/src/tool/investigation-board/logic.test.ts +105 -0
  40. package/src/tool/investigation-board/logic.ts +106 -0
  41. package/src/tool/investigation-board/panzoom.ts +24 -0
  42. package/src/tool/investigation-board/renderer.ts +256 -0
  43. package/src/tool/investigation-board/seo.astro +16 -0
  44. package/src/tool/investigation-board/stateManager.ts +126 -0
  45. package/src/tool/investigation-board/types.ts +26 -0
  46. package/src/tools.ts +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-tabletop",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -17,7 +17,7 @@
17
17
  "access": "public"
18
18
  },
19
19
  "scripts": {
20
- "dev": "astro dev",
20
+ "dev": "astro dev --host",
21
21
  "start": "astro dev",
22
22
  "build": "astro build",
23
23
  "preview": "astro preview",
@@ -42,9 +42,11 @@
42
42
  "@iconify-json/mdi": "^1.2.3",
43
43
  "@jjlmoya/prompagate": "^1.1.0",
44
44
  "@jjlmoya/utils-shared": "1.2.0",
45
+ "@panzoom/panzoom": "^4.6.2",
45
46
  "astro": "^6.1.2",
46
47
  "astro-icon": "^1.1.0",
47
- "html2canvas": "^1.4.1"
48
+ "html2canvas": "^1.4.1",
49
+ "interactjs": "^1.10.27"
48
50
  },
49
51
  "devDependencies": {
50
52
  "@astrojs/check": "^0.9.8",
package/src/entries.ts CHANGED
@@ -10,6 +10,8 @@ export { initiativeTracker } from './tool/rpg-initiative-tracker/entry';
10
10
  export type { InitiativeTrackerUI, InitiativeTrackerLocaleContent } from './tool/rpg-initiative-tracker/entry';
11
11
  export { fantasyRunesTranslator } from './tool/fantasy-runes-translator/entry';
12
12
  export type { FantasyRunesTranslatorUI, FantasyRunesTranslatorLocaleContent } from './tool/fantasy-runes-translator/entry';
13
+ export { investigationBoard } from './tool/investigation-board/entry';
14
+ export type { InvestigationBoardUI, InvestigationBoardLocaleContent } from './tool/investigation-board/entry';
13
15
  export { tabletopCategory } from './category';
14
16
 
15
17
  import { diceRollerSimulator } from './tool/dice-roller-simulator/entry';
@@ -19,5 +21,7 @@ import { scoreTracker } from './tool/score-tracker/entry';
19
21
  import { initiativeTracker } from './tool/rpg-initiative-tracker/entry';
20
22
  import { fantasyRunesTranslator } from './tool/fantasy-runes-translator/entry';
21
23
  import { decisionWheel } from './tool/decision-wheel/entry';
24
+ import { investigationBoard } from './tool/investigation-board/entry';
25
+
26
+ export const ALL_ENTRIES = [diceRollerSimulator, boardGameTimer, firstPlayerSelector, scoreTracker, initiativeTracker, fantasyRunesTranslator, decisionWheel, investigationBoard];
22
27
 
23
- export const ALL_ENTRIES = [diceRollerSimulator, boardGameTimer, firstPlayerSelector, scoreTracker, initiativeTracker, fantasyRunesTranslator, decisionWheel];
package/src/index.ts CHANGED
@@ -4,6 +4,8 @@ export const tabletopCategorySEO = () => import('./category/TabletopCategorySEO.
4
4
  export { diceRollerSimulator, DICE_ROLLER_SIMULATOR_TOOL } from './tool/dice-roller-simulator';
5
5
  export { boardGameTimer, BOARD_GAME_TIMER_TOOL } from './tool/board-game-timer';
6
6
  export { fantasyRunesTranslator, FANTASY_RUNES_TRANSLATOR_TOOL } from './tool/fantasy-runes-translator';
7
+ export { investigationBoard, INVESTIGATION_BOARD_TOOL } from './tool/investigation-board';
8
+
7
9
 
8
10
  export type {
9
11
  KnownLocale,
@@ -22,8 +22,9 @@ describe('Locale Completeness Validation', () => {
22
22
  });
23
23
 
24
24
  it('all tools registered', () => {
25
- expect(ALL_TOOLS.length).toBe(7);
25
+ expect(ALL_TOOLS.length).toBe(8);
26
26
  });
27
27
 
28
+
28
29
  });
29
30
 
@@ -5,9 +5,10 @@ import { tabletopCategory } from '../data';
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
7
  it('should have tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(7);
8
+ expect(ALL_TOOLS.length).toBe(8);
9
9
  });
10
10
 
11
+
11
12
  it('tabletopCategory should be defined', () => {
12
13
  expect(tabletopCategory).toBeDefined();
13
14
  expect(tabletopCategory.i18n).toBeDefined();
@@ -0,0 +1,16 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { investigationBoard } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props as Props;
11
+ const loader = investigationBoard.i18n[locale] || investigationBoard.i18n.en;
12
+ const content = await loader?.();
13
+ if (!content) return null;
14
+ ---
15
+
16
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,12 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Conspiracy Board Corkboard Visualizations',
6
+ url: 'https://en.wikipedia.org/wiki/Conspiracy_board',
7
+ },
8
+ {
9
+ name: 'Concept Mapping in Detective Narratives',
10
+ url: 'https://en.wikipedia.org/wiki/Concept_map',
11
+ },
12
+ ];
@@ -0,0 +1,64 @@
1
+ import { ctxMenu, viewportEl, state } from './dom';
2
+ import { panzoomInstance } from './panzoom';
3
+
4
+ export interface ContextMenuOptions {
5
+ x: number;
6
+ y: number;
7
+ nodeId: string | null;
8
+ canvasX: number;
9
+ canvasY: number;
10
+ }
11
+
12
+ export function showContextMenu(opts: ContextMenuOptions): void {
13
+ state.longPressTargetNodeId = opts.nodeId;
14
+ state.longPressCanvasX = opts.canvasX;
15
+ state.longPressCanvasY = opts.canvasY;
16
+
17
+ ctxMenu.style.left = `${opts.x}px`;
18
+ ctxMenu.style.top = `${opts.y}px`;
19
+ ctxMenu.classList.add('active');
20
+
21
+ const displayVal = opts.nodeId ? 'flex' : 'none';
22
+ ctxMenu.querySelectorAll('.card-only').forEach((el) => {
23
+ (el as HTMLElement).style.display = displayVal;
24
+ });
25
+ }
26
+
27
+ export function hideContextMenu(): void {
28
+ ctxMenu.classList.remove('active');
29
+ state.longPressTargetNodeId = null;
30
+ }
31
+
32
+ export function startLongPress(e: PointerEvent, nodeId: string | null): void {
33
+ hideContextMenu();
34
+ const clientX = e.clientX;
35
+ const clientY = e.clientY;
36
+ state.startX = clientX;
37
+ state.startY = clientY;
38
+
39
+ const viewRect = viewportEl.getBoundingClientRect();
40
+ const localX = clientX - viewRect.left;
41
+ const localY = clientY - viewRect.top;
42
+ const pan = panzoomInstance.getPan();
43
+ const scale = panzoomInstance.getScale();
44
+ const canvasX = (localX - pan.x) / scale;
45
+ const canvasY = (localY - pan.y) / scale;
46
+
47
+ state.longPressTimer = window.setTimeout(() => {
48
+ showContextMenu({ x: clientX, y: clientY, nodeId, canvasX, canvasY });
49
+ }, 600);
50
+ }
51
+
52
+ export function cancelLongPress(): void {
53
+ if (state.longPressTimer) {
54
+ clearTimeout(state.longPressTimer);
55
+ state.longPressTimer = null;
56
+ }
57
+ }
58
+
59
+ export function checkLongPressMove(clientX: number, clientY: number): void {
60
+ const dist = Math.sqrt((clientX - state.startX) ** 2 + (clientY - state.startY) ** 2);
61
+ if (dist > 8) {
62
+ cancelLongPress();
63
+ }
64
+ }
@@ -0,0 +1,223 @@
1
+ import {
2
+ state,
3
+ mainCard,
4
+ viewportEl,
5
+ areaEl,
6
+ svgEl,
7
+ searchInput,
8
+ categoryBtns,
9
+ addCardBtn,
10
+ clearBoardBtn,
11
+ boardSelect,
12
+ saveBoardAsBtn,
13
+ newBoardBtn,
14
+ connBanner,
15
+ zoomInBtn,
16
+ zoomOutBtn,
17
+ zoomResetBtn,
18
+ fullscreenToggleBtn,
19
+ ctxMenu,
20
+ ctxAddCard,
21
+ ctxEditCard,
22
+ ctxLinkCard,
23
+ ctxDeleteCard,
24
+ CURRENT_ID_KEY,
25
+ } from './dom';
26
+ import { panzoomInstance } from './panzoom';
27
+ import {
28
+ startLongPress,
29
+ cancelLongPress,
30
+ checkLongPressMove,
31
+ hideContextMenu,
32
+ } from './boardContextMenu';
33
+ import { adjustBoardSize } from './layout';
34
+ import {
35
+ saveState,
36
+ saveBoardsList,
37
+ updateBoardSelect,
38
+ loadBoardState,
39
+ loadBoardsList,
40
+ } from './stateManager';
41
+ import { renderBoard } from './renderer';
42
+ import { openDrawer } from './drawer';
43
+ import { initializeDrag } from './interactDrag';
44
+ import { createNode, deleteNode } from './logic';
45
+
46
+ zoomInBtn.addEventListener('click', () => panzoomInstance.zoomIn());
47
+ zoomOutBtn.addEventListener('click', () => panzoomInstance.zoomOut());
48
+ zoomResetBtn.addEventListener('click', () => panzoomInstance.reset());
49
+
50
+ fullscreenToggleBtn.addEventListener('click', () => {
51
+ mainCard.classList.toggle('immersive');
52
+ setTimeout(() => {
53
+ adjustBoardSize();
54
+ }, 100);
55
+ });
56
+
57
+ addCardBtn.addEventListener('click', () => {
58
+ const card = createNode({
59
+ name: 'New Card',
60
+ category: 'clue',
61
+ x: 100 + Math.random() * 100,
62
+ y: 100 + Math.random() * 100,
63
+ color: '#ef4444',
64
+ });
65
+ state.nodes.push(card);
66
+ saveState();
67
+ renderBoard();
68
+ openDrawer(card);
69
+ });
70
+
71
+ clearBoardBtn.addEventListener('click', () => {
72
+ if (confirm('Clear the entire board?')) {
73
+ state.nodes = [];
74
+ state.connections = [];
75
+ saveState();
76
+ renderBoard();
77
+ }
78
+ });
79
+
80
+ searchInput.addEventListener('input', () => {
81
+ state.currentSearchQuery = searchInput.value;
82
+ renderBoard();
83
+ });
84
+
85
+ categoryBtns.forEach((btn) => {
86
+ btn.addEventListener('click', () => {
87
+ categoryBtns.forEach((b) => b.classList.remove('active'));
88
+ btn.classList.add('active');
89
+ state.currentCategoryFilter = btn.getAttribute('data-category') || 'all';
90
+ renderBoard();
91
+ });
92
+ });
93
+
94
+ viewportEl.addEventListener('pointerdown', (e) => {
95
+ const target = e.target as HTMLElement;
96
+ if (
97
+ target.closest('.board-node') ||
98
+ target.closest('.zoom-controls') ||
99
+ target.closest('.control-panel') ||
100
+ target.closest('.card-drawer') ||
101
+ target.closest('.modal-overlay') ||
102
+ target.closest('.context-menu')
103
+ ) {
104
+ return;
105
+ }
106
+ if (state.quickLinkSourceId) {
107
+ state.quickLinkSourceId = null;
108
+ connBanner.classList.remove('active');
109
+ }
110
+ startLongPress(e, null);
111
+ });
112
+
113
+ viewportEl.addEventListener('pointermove', (e) => {
114
+ checkLongPressMove(e.clientX, e.clientY);
115
+ });
116
+
117
+ viewportEl.addEventListener('pointerup', () => {
118
+ cancelLongPress();
119
+ });
120
+
121
+ viewportEl.addEventListener('pointercancel', () => {
122
+ cancelLongPress();
123
+ });
124
+
125
+ viewportEl.addEventListener('click', (e) => {
126
+ if (e.target === viewportEl || e.target === areaEl || e.target === svgEl) {
127
+ state.highlightedNodeId = null;
128
+ renderBoard();
129
+ }
130
+ });
131
+
132
+ document.addEventListener('click', (e) => {
133
+ if (!ctxMenu.contains(e.target as Node)) {
134
+ hideContextMenu();
135
+ }
136
+ });
137
+
138
+ ctxAddCard.addEventListener('click', () => {
139
+ hideContextMenu();
140
+ const card = createNode({
141
+ name: 'New Card',
142
+ category: 'clue',
143
+ x: state.longPressCanvasX,
144
+ y: state.longPressCanvasY,
145
+ color: '#ef4444',
146
+ });
147
+ state.nodes.push(card);
148
+ saveState();
149
+ renderBoard();
150
+ openDrawer(card);
151
+ });
152
+
153
+ ctxEditCard.addEventListener('click', () => {
154
+ const nodeId = state.longPressTargetNodeId;
155
+ hideContextMenu();
156
+ if (nodeId) {
157
+ const node = state.nodes.find((n) => n.id === nodeId);
158
+ if (node) openDrawer(node);
159
+ }
160
+ });
161
+
162
+ ctxLinkCard.addEventListener('click', () => {
163
+ const nodeId = state.longPressTargetNodeId;
164
+ hideContextMenu();
165
+ if (nodeId) {
166
+ state.quickLinkSourceId = nodeId;
167
+ connBanner.classList.add('active');
168
+ }
169
+ });
170
+
171
+ ctxDeleteCard.addEventListener('click', () => {
172
+ const nodeId = state.longPressTargetNodeId;
173
+ hideContextMenu();
174
+ if (nodeId) {
175
+ if (confirm('Delete this card?')) {
176
+ const result = deleteNode(state.nodes, state.connections, nodeId);
177
+ state.nodes = result.nodes;
178
+ state.connections = result.connections;
179
+ saveState();
180
+ renderBoard();
181
+ }
182
+ }
183
+ });
184
+
185
+ boardSelect.addEventListener('change', () => {
186
+ loadBoardState(boardSelect.value);
187
+ renderBoard();
188
+ });
189
+
190
+ saveBoardAsBtn.addEventListener('click', () => {
191
+ const name = prompt('Enter a name for this board:');
192
+ if (!name || name.trim().length === 0) return;
193
+ const newId = 'board_' + Math.random().toString(36).substring(2, 9);
194
+ state.boardsList.push({ id: newId, name: name.trim() });
195
+ saveBoardsList();
196
+ state.currentBoardId = newId;
197
+ saveState();
198
+ updateBoardSelect();
199
+ loadBoardState(newId);
200
+ renderBoard();
201
+ });
202
+
203
+ newBoardBtn.addEventListener('click', () => {
204
+ const name = prompt('Enter a name for the new board:');
205
+ if (!name || name.trim().length === 0) return;
206
+ const newId = 'board_' + Math.random().toString(36).substring(2, 9);
207
+ state.boardsList.push({ id: newId, name: name.trim() });
208
+ saveBoardsList();
209
+ state.currentBoardId = newId;
210
+ state.nodes = [];
211
+ state.connections = [];
212
+ saveState();
213
+ updateBoardSelect();
214
+ loadBoardState(newId);
215
+ renderBoard();
216
+ });
217
+
218
+ const savedActiveId = localStorage.getItem(CURRENT_ID_KEY) || 'default';
219
+ loadBoardsList();
220
+ loadBoardState(savedActiveId);
221
+ updateBoardSelect();
222
+ initializeDrag();
223
+ renderBoard();
@@ -0,0 +1,23 @@
1
+ ---
2
+ import ControlPanel from './components/ControlPanel.astro';
3
+ import BoardCanvas from './components/BoardCanvas.astro';
4
+ import CardDrawer from './components/CardDrawer.astro';
5
+ import ConnectionModal from './components/ConnectionModal.astro';
6
+ import ContextMenu from './components/ContextMenu.astro';
7
+
8
+ interface Props {
9
+ ui: Record<string, string>;
10
+ }
11
+
12
+ const { ui } = Astro.props;
13
+ ---
14
+
15
+ <div class="tool-main-card investigation-board-main" data-ui={JSON.stringify(ui)}>
16
+ <ControlPanel labels={ui} />
17
+ <BoardCanvas labels={ui} />
18
+ <CardDrawer labels={ui} />
19
+ <ConnectionModal labels={ui} />
20
+ <ContextMenu labels={ui} />
21
+ </div>
22
+
23
+ <script src="./client.ts"></script>
@@ -0,0 +1,39 @@
1
+ ---
2
+ ---
3
+
4
+ <div class="board-canvas-wrapper">
5
+ <div class="board-viewport" id="board-viewport">
6
+ <div class="connection-banner" id="connection-banner">
7
+ Click another card to link, or click background to cancel.
8
+ </div>
9
+ <div class="board-area" id="board-area">
10
+ <svg class="board-svg" id="board-connections-svg">
11
+
12
+ <defs>
13
+ <marker
14
+ id="arrowhead"
15
+ viewBox="0 0 10 10"
16
+ refX="6"
17
+ refY="5"
18
+ markerWidth="6"
19
+ markerHeight="6"
20
+ orient="auto-start-reverse"
21
+ >
22
+ <path d="M 0 1 L 10 5 L 0 9 z" fill="var(--text-muted)"></path>
23
+ </marker>
24
+ </defs>
25
+ </svg>
26
+ <div class="board-nodes" id="board-nodes-layer"></div>
27
+ </div>
28
+ </div>
29
+ <div class="zoom-controls">
30
+ <button type="button" id="zoom-out-btn" class="zoom-btn" aria-label="Zoom Out">-</button>
31
+ <span id="zoom-value" class="zoom-value">100%</span>
32
+ <button type="button" id="zoom-in-btn" class="zoom-btn" aria-label="Zoom In">+</button>
33
+ <button type="button" id="zoom-reset-btn" class="zoom-btn" aria-label="Reset Zoom">1:1</button>
34
+ <button type="button" id="fullscreen-toggle-btn" class="zoom-btn" aria-label="Toggle Fullscreen">
35
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="display: block;"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>
36
+ </button>
37
+ </div>
38
+ </div>
39
+
@@ -0,0 +1,73 @@
1
+ ---
2
+ interface Props {
3
+ labels: Record<string, string>;
4
+ }
5
+
6
+ const { labels } = Astro.props;
7
+ ---
8
+
9
+ <div class="card-drawer-overlay" id="card-drawer-overlay">
10
+ <div class="card-drawer" role="dialog" aria-modal="true" aria-labelledby="drawer-title">
11
+ <div class="drawer-header">
12
+ <h2 id="drawer-title" class="drawer-heading">{labels.cardName}</h2>
13
+ <button type="button" class="btn-close-drawer" id="close-drawer-btn" aria-label={labels.close}>
14
+ &times;
15
+ </button>
16
+ </div>
17
+
18
+ <form class="drawer-form" id="drawer-form" onSubmit="event.preventDefault();">
19
+ <input type="hidden" id="edit-node-id" />
20
+
21
+ <div class="form-group">
22
+ <label for="edit-node-name">{labels.cardName}</label>
23
+ <input type="text" id="edit-node-name" class="form-control" required />
24
+ </div>
25
+
26
+ <div class="form-group">
27
+ <label for="edit-node-category">{labels.cardCategory}</label>
28
+ <select id="edit-node-category" class="form-control">
29
+ <option value="character">{labels.character}</option>
30
+ <option value="clue">{labels.clue}</option>
31
+ <option value="location">{labels.location}</option>
32
+ <option value="item">{labels.item}</option>
33
+ <option value="custom">{labels.custom}</option>
34
+ </select>
35
+ </div>
36
+
37
+ <div class="form-group">
38
+ <label for="edit-node-description">{labels.cardDescription}</label>
39
+ <textarea id="edit-node-description" class="form-control" rows="2"></textarea>
40
+ </div>
41
+
42
+ <div class="form-group">
43
+ <label>{labels.cardNotes}</label>
44
+ <div id="edit-notes-list" class="notes-input-list"></div>
45
+ <button type="button" id="add-note-row-btn" class="btn btn-secondary btn-sm">+ Add Point</button>
46
+ </div>
47
+
48
+
49
+
50
+ <div class="form-group">
51
+ <label for="edit-node-tags">{labels.cardTags}</label>
52
+ <input type="text" id="edit-node-tags" class="form-control" />
53
+ </div>
54
+
55
+ <div class="form-group" id="color-picker-group">
56
+ <label for="edit-node-color">{labels.cardColor}</label>
57
+ <input type="color" id="edit-node-color" class="form-control color-picker" />
58
+ </div>
59
+
60
+ <div class="drawer-actions">
61
+ <button type="button" id="save-node-btn" class="btn btn-primary">
62
+ {labels.save}
63
+ </button>
64
+ <button type="button" id="delete-node-btn" class="btn btn-danger">
65
+ {labels.delete}
66
+ </button>
67
+ <button type="button" id="cancel-node-btn" class="btn btn-secondary">
68
+ {labels.cancel}
69
+ </button>
70
+ </div>
71
+ </form>
72
+ </div>
73
+ </div>
@@ -0,0 +1,49 @@
1
+ ---
2
+ interface Props {
3
+ labels: Record<string, string>;
4
+ }
5
+
6
+ const { labels } = Astro.props;
7
+ ---
8
+
9
+ <div class="modal-overlay" id="connection-modal-overlay">
10
+ <div class="modal-dialog" role="dialog" aria-modal="true" aria-labelledby="modal-title">
11
+ <div class="modal-header">
12
+ <h3 id="modal-title" class="modal-heading">{labels.addConnection}</h3>
13
+ <button type="button" class="btn-close-modal" id="close-modal-btn" aria-label={labels.close}>
14
+ &times;
15
+ </button>
16
+ </div>
17
+
18
+ <form class="modal-form" id="connection-form" onSubmit="event.preventDefault();">
19
+ <div class="form-group">
20
+ <label for="connection-source">{labels.sourceCard}</label>
21
+ <select id="connection-source" class="form-control" required></select>
22
+ </div>
23
+
24
+ <div class="form-group">
25
+ <label for="connection-target">{labels.targetCard}</label>
26
+ <select id="connection-target" class="form-control" required></select>
27
+ </div>
28
+
29
+ <div class="form-group">
30
+ <label for="connection-text">{labels.connectionLabel}</label>
31
+ <input type="text" id="connection-text" class="form-control" required />
32
+ </div>
33
+
34
+ <div class="form-group">
35
+ <label for="connection-color-picker">{labels.connectionColor}</label>
36
+ <input type="color" id="connection-color-picker" class="form-control color-picker" value="#f43f5e" />
37
+ </div>
38
+
39
+ <div class="modal-actions">
40
+ <button type="button" id="save-connection-btn" class="btn btn-primary">
41
+ {labels.save}
42
+ </button>
43
+ <button type="button" id="cancel-connection-btn" class="btn btn-secondary">
44
+ {labels.cancel}
45
+ </button>
46
+ </div>
47
+ </form>
48
+ </div>
49
+ </div>
@@ -0,0 +1,26 @@
1
+ ---
2
+ interface Props {
3
+ labels: Record<string, string>;
4
+ }
5
+
6
+ const { labels } = Astro.props;
7
+ ---
8
+
9
+ <div id="board-context-menu" class="context-menu">
10
+ <button type="button" id="ctx-add-card" class="context-menu-item">
11
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
12
+ <span>{labels.addCard || 'Add Card'}</span>
13
+ </button>
14
+ <button type="button" id="ctx-edit-card" class="context-menu-item card-only">
15
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path></svg>
16
+ <span>Edit</span>
17
+ </button>
18
+ <button type="button" id="ctx-link-card" class="context-menu-item card-only">
19
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3 M9 17H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3 M8 12h8"></path></svg>
20
+ <span>Link</span>
21
+ </button>
22
+ <button type="button" id="ctx-delete-card" class="context-menu-item card-only danger">
23
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
24
+ <span>Delete</span>
25
+ </button>
26
+ </div>
@@ -0,0 +1,57 @@
1
+ ---
2
+ interface Props {
3
+ labels: Record<string, string>;
4
+ }
5
+
6
+ const { labels } = Astro.props;
7
+ ---
8
+
9
+ <div class="control-panel">
10
+ <div class="search-filter-row">
11
+ <div class="search-box">
12
+ <input
13
+ type="text"
14
+ id="board-search"
15
+ placeholder={labels.searchPlaceholder}
16
+ class="form-control search-input"
17
+ aria-label="Search board elements"
18
+ />
19
+ </div>
20
+ <div class="filter-group">
21
+ <button type="button" class="btn btn-filter active" data-category="all">
22
+ {labels.filterAll}
23
+ </button>
24
+ <button type="button" class="btn btn-filter" data-category="character">
25
+ {labels.character}
26
+ </button>
27
+ <button type="button" class="btn btn-filter" data-category="clue">
28
+ {labels.clue}
29
+ </button>
30
+ <button type="button" class="btn btn-filter" data-category="location">
31
+ {labels.location}
32
+ </button>
33
+ <button type="button" class="btn btn-filter" data-category="item">
34
+ {labels.item}
35
+ </button>
36
+ </div>
37
+ </div>
38
+
39
+ <div class="board-management-row">
40
+ <select id="board-select" class="form-control"></select>
41
+ <button type="button" id="save-board-as-btn" class="btn btn-secondary">Save As</button>
42
+ <button type="button" id="new-board-btn" class="btn btn-secondary">+ New Board</button>
43
+ </div>
44
+
45
+ <div class="action-buttons-row">
46
+
47
+ <button type="button" id="add-card-btn" class="btn btn-primary btn-icon">
48
+ <span class="btn-text">{labels.addCard}</span>
49
+ </button>
50
+ <button type="button" id="add-connection-btn" class="btn btn-secondary btn-icon">
51
+ <span class="btn-text">{labels.addConnection}</span>
52
+ </button>
53
+ <button type="button" id="clear-board-btn" class="btn btn-danger btn-icon">
54
+ <span class="btn-text">{labels.clearBoard}</span>
55
+ </button>
56
+ </div>
57
+ </div>