@jjlmoya/utils-tabletop 1.4.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.
- package/package.json +5 -3
- package/src/entries.ts +15 -1
- package/src/index.ts +3 -0
- package/src/tests/locale_completeness.test.ts +2 -1
- package/src/tests/tool_validation.test.ts +2 -1
- package/src/tool/decision-wheel/audio.ts +78 -0
- package/src/tool/decision-wheel/bibliography.astro +16 -0
- package/src/tool/decision-wheel/bibliography.ts +8 -0
- package/src/tool/decision-wheel/client.ts +226 -0
- package/src/tool/decision-wheel/component.astro +48 -0
- package/src/tool/decision-wheel/components/SegmentEditor.astro +22 -0
- package/src/tool/decision-wheel/components/SpinHistory.astro +17 -0
- package/src/tool/decision-wheel/components/SpinResult.astro +13 -0
- package/src/tool/decision-wheel/components/WheelCanvas.astro +23 -0
- package/src/tool/decision-wheel/decision-wheel.css +632 -0
- package/src/tool/decision-wheel/drawing.ts +144 -0
- package/src/tool/decision-wheel/entry.ts +48 -0
- package/src/tool/decision-wheel/i18n/de.ts +97 -0
- package/src/tool/decision-wheel/i18n/en.ts +251 -0
- package/src/tool/decision-wheel/i18n/es.ts +93 -0
- package/src/tool/decision-wheel/i18n/fr.ts +93 -0
- package/src/tool/decision-wheel/i18n/id.ts +93 -0
- package/src/tool/decision-wheel/i18n/it.ts +93 -0
- package/src/tool/decision-wheel/i18n/ja.ts +93 -0
- package/src/tool/decision-wheel/i18n/ko.ts +93 -0
- package/src/tool/decision-wheel/i18n/nl.ts +93 -0
- package/src/tool/decision-wheel/i18n/pl.ts +93 -0
- package/src/tool/decision-wheel/i18n/pt.ts +93 -0
- package/src/tool/decision-wheel/i18n/ru.ts +75 -0
- package/src/tool/decision-wheel/i18n/sv.ts +93 -0
- package/src/tool/decision-wheel/i18n/tr.ts +93 -0
- package/src/tool/decision-wheel/i18n/zh.ts +93 -0
- package/src/tool/decision-wheel/index.ts +9 -0
- package/src/tool/decision-wheel/logic.test.ts +163 -0
- package/src/tool/decision-wheel/logic.ts +145 -0
- package/src/tool/decision-wheel/seo.astro +16 -0
- package/src/tool/decision-wheel/types.ts +14 -0
- package/src/tool/decision-wheel/ui.ts +121 -0
- package/src/tool/fantasy-runes-translator/bibliography.astro +16 -0
- package/src/tool/fantasy-runes-translator/bibliography.ts +16 -0
- package/src/tool/fantasy-runes-translator/client.ts +205 -0
- package/src/tool/fantasy-runes-translator/component.astro +95 -0
- package/src/tool/fantasy-runes-translator/entry.ts +56 -0
- package/src/tool/fantasy-runes-translator/fantasy-runes-translator.css +712 -0
- package/src/tool/fantasy-runes-translator/i18n/de.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/en.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/es.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/fr.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/id.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/it.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/ja.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/ko.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/nl.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/pl.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/pt.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/ru.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/sv.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/tr.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/zh.ts +139 -0
- package/src/tool/fantasy-runes-translator/index.ts +9 -0
- package/src/tool/fantasy-runes-translator/logic.ts +174 -0
- package/src/tool/fantasy-runes-translator/seo.astro +16 -0
- package/src/tool/fantasy-runes-translator/types.ts +26 -0
- package/src/tool/investigation-board/bibliography.astro +16 -0
- package/src/tool/investigation-board/bibliography.ts +12 -0
- package/src/tool/investigation-board/boardContextMenu.ts +64 -0
- package/src/tool/investigation-board/client.ts +223 -0
- package/src/tool/investigation-board/component.astro +23 -0
- package/src/tool/investigation-board/components/BoardCanvas.astro +39 -0
- package/src/tool/investigation-board/components/CardDrawer.astro +73 -0
- package/src/tool/investigation-board/components/ConnectionModal.astro +49 -0
- package/src/tool/investigation-board/components/ContextMenu.astro +26 -0
- package/src/tool/investigation-board/components/ControlPanel.astro +57 -0
- package/src/tool/investigation-board/connectionModal.ts +60 -0
- package/src/tool/investigation-board/dom.ts +89 -0
- package/src/tool/investigation-board/drawer.ts +110 -0
- package/src/tool/investigation-board/entry.ts +59 -0
- package/src/tool/investigation-board/i18n/de.ts +126 -0
- package/src/tool/investigation-board/i18n/en.ts +127 -0
- package/src/tool/investigation-board/i18n/es.ts +126 -0
- package/src/tool/investigation-board/i18n/fr.ts +126 -0
- package/src/tool/investigation-board/i18n/id.ts +126 -0
- package/src/tool/investigation-board/i18n/it.ts +126 -0
- package/src/tool/investigation-board/i18n/ja.ts +126 -0
- package/src/tool/investigation-board/i18n/ko.ts +126 -0
- package/src/tool/investigation-board/i18n/nl.ts +126 -0
- package/src/tool/investigation-board/i18n/pl.ts +126 -0
- package/src/tool/investigation-board/i18n/pt.ts +126 -0
- package/src/tool/investigation-board/i18n/ru.ts +126 -0
- package/src/tool/investigation-board/i18n/sv.ts +126 -0
- package/src/tool/investigation-board/i18n/tr.ts +126 -0
- package/src/tool/investigation-board/i18n/zh.ts +126 -0
- package/src/tool/investigation-board/index.ts +11 -0
- package/src/tool/investigation-board/interactDrag.ts +58 -0
- package/src/tool/investigation-board/investigation-board.css +799 -0
- package/src/tool/investigation-board/layout.ts +16 -0
- package/src/tool/investigation-board/logic.test.ts +105 -0
- package/src/tool/investigation-board/logic.ts +106 -0
- package/src/tool/investigation-board/panzoom.ts +24 -0
- package/src/tool/investigation-board/renderer.ts +256 -0
- package/src/tool/investigation-board/seo.astro +16 -0
- package/src/tool/investigation-board/stateManager.ts +126 -0
- package/src/tool/investigation-board/types.ts +26 -0
- package/src/tool/rpg-initiative-tracker/bibliography.astro +16 -0
- package/src/tool/rpg-initiative-tracker/bibliography.ts +12 -0
- package/src/tool/rpg-initiative-tracker/client.ts +201 -0
- package/src/tool/rpg-initiative-tracker/component.astro +23 -0
- package/src/tool/rpg-initiative-tracker/components/AddForm.astro +49 -0
- package/src/tool/rpg-initiative-tracker/components/CombatantList.astro +16 -0
- package/src/tool/rpg-initiative-tracker/components/TurnControls.astro +28 -0
- package/src/tool/rpg-initiative-tracker/entry.ts +50 -0
- package/src/tool/rpg-initiative-tracker/i18n/de.ts +266 -0
- package/src/tool/rpg-initiative-tracker/i18n/en.ts +263 -0
- package/src/tool/rpg-initiative-tracker/i18n/es.ts +264 -0
- package/src/tool/rpg-initiative-tracker/i18n/fr.ts +263 -0
- package/src/tool/rpg-initiative-tracker/i18n/id.ts +263 -0
- package/src/tool/rpg-initiative-tracker/i18n/it.ts +263 -0
- package/src/tool/rpg-initiative-tracker/i18n/ja.ts +263 -0
- package/src/tool/rpg-initiative-tracker/i18n/ko.ts +263 -0
- package/src/tool/rpg-initiative-tracker/i18n/nl.ts +263 -0
- package/src/tool/rpg-initiative-tracker/i18n/pl.ts +264 -0
- package/src/tool/rpg-initiative-tracker/i18n/pt.ts +263 -0
- package/src/tool/rpg-initiative-tracker/i18n/ru.ts +264 -0
- package/src/tool/rpg-initiative-tracker/i18n/sv.ts +263 -0
- package/src/tool/rpg-initiative-tracker/i18n/tr.ts +263 -0
- package/src/tool/rpg-initiative-tracker/i18n/zh.ts +263 -0
- package/src/tool/rpg-initiative-tracker/index.ts +9 -0
- package/src/tool/rpg-initiative-tracker/logic.test.ts +121 -0
- package/src/tool/rpg-initiative-tracker/logic.ts +122 -0
- package/src/tool/rpg-initiative-tracker/rpg-initiative-tracker.css +617 -0
- package/src/tool/rpg-initiative-tracker/seo.astro +16 -0
- package/src/tool/rpg-initiative-tracker/types.ts +15 -0
- package/src/tool/score-tracker/bibliography.astro +16 -0
- package/src/tool/score-tracker/bibliography.ts +12 -0
- package/src/tool/score-tracker/client.ts +248 -0
- package/src/tool/score-tracker/component.astro +34 -0
- package/src/tool/score-tracker/components/Leaderboard.astro +19 -0
- package/src/tool/score-tracker/components/PlayerSetup.astro +32 -0
- package/src/tool/score-tracker/components/RoundScoring.astro +22 -0
- package/src/tool/score-tracker/components/ScoreHistory.astro +25 -0
- package/src/tool/score-tracker/entry.ts +57 -0
- package/src/tool/score-tracker/i18n/de.ts +278 -0
- package/src/tool/score-tracker/i18n/en.ts +278 -0
- package/src/tool/score-tracker/i18n/es.ts +251 -0
- package/src/tool/score-tracker/i18n/fr.ts +278 -0
- package/src/tool/score-tracker/i18n/id.ts +278 -0
- package/src/tool/score-tracker/i18n/it.ts +278 -0
- package/src/tool/score-tracker/i18n/ja.ts +278 -0
- package/src/tool/score-tracker/i18n/ko.ts +278 -0
- package/src/tool/score-tracker/i18n/nl.ts +278 -0
- package/src/tool/score-tracker/i18n/pl.ts +278 -0
- package/src/tool/score-tracker/i18n/pt.ts +278 -0
- package/src/tool/score-tracker/i18n/ru.ts +278 -0
- package/src/tool/score-tracker/i18n/sv.ts +278 -0
- package/src/tool/score-tracker/i18n/tr.ts +278 -0
- package/src/tool/score-tracker/i18n/zh.ts +278 -0
- package/src/tool/score-tracker/index.ts +9 -0
- package/src/tool/score-tracker/logic.test.ts +83 -0
- package/src/tool/score-tracker/logic.ts +122 -0
- package/src/tool/score-tracker/score-tracker.css +1029 -0
- package/src/tool/score-tracker/seo.astro +16 -0
- package/src/tool/score-tracker/types.ts +17 -0
- package/src/tools.ts +11 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface RuneCharacter {
|
|
2
|
+
symbol: string;
|
|
3
|
+
name: string;
|
|
4
|
+
meaning: string;
|
|
5
|
+
phonetic: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type RuneAlphabetKey = 'elder-futhark' | 'anglo-saxon' | 'fantasy' | 'theban' | 'enochian' | 'daemonic' | 'ogham';
|
|
9
|
+
|
|
10
|
+
export interface RuneAlphabet {
|
|
11
|
+
key: RuneAlphabetKey;
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
mapping: Record<string, RuneCharacter>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RuneCharacterResult {
|
|
18
|
+
input: string;
|
|
19
|
+
char: RuneCharacter | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RuneTranslationResult {
|
|
23
|
+
alphabetKey: RuneAlphabetKey;
|
|
24
|
+
characters: RuneCharacterResult[];
|
|
25
|
+
raw: string;
|
|
26
|
+
}
|
|
@@ -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
|
+
×
|
|
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
|
+
×
|
|
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>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
state,
|
|
3
|
+
connOverlay,
|
|
4
|
+
connSourceSelect,
|
|
5
|
+
connTargetSelect,
|
|
6
|
+
connTextInput,
|
|
7
|
+
connColorInput,
|
|
8
|
+
saveConnBtn,
|
|
9
|
+
cancelConnBtn,
|
|
10
|
+
closeConnBtn,
|
|
11
|
+
addConnectionBtn,
|
|
12
|
+
} from './dom';
|
|
13
|
+
import { createConnection } from './logic';
|
|
14
|
+
import { saveState } from './stateManager';
|
|
15
|
+
import { renderBoard } from './renderer';
|
|
16
|
+
|
|
17
|
+
export function populateSelects(sourceId?: string, targetId?: string): void {
|
|
18
|
+
connSourceSelect.innerHTML = '';
|
|
19
|
+
connTargetSelect.innerHTML = '';
|
|
20
|
+
state.nodes.forEach((n) => {
|
|
21
|
+
const opt1 = document.createElement('option');
|
|
22
|
+
opt1.value = n.id;
|
|
23
|
+
opt1.textContent = n.name;
|
|
24
|
+
opt1.selected = !!(sourceId && n.id === sourceId);
|
|
25
|
+
connSourceSelect.appendChild(opt1);
|
|
26
|
+
|
|
27
|
+
const opt2 = document.createElement('option');
|
|
28
|
+
opt2.value = n.id;
|
|
29
|
+
opt2.textContent = n.name;
|
|
30
|
+
opt2.selected = !!(targetId && n.id === targetId);
|
|
31
|
+
connTargetSelect.appendChild(opt2);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function openConnModal(sourceId?: string, targetId?: string): void {
|
|
36
|
+
populateSelects(sourceId, targetId);
|
|
37
|
+
connTextInput.value = '';
|
|
38
|
+
connOverlay.classList.add('active');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function closeConnModal(): void {
|
|
42
|
+
connOverlay.classList.remove('active');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
saveConnBtn.addEventListener('click', () => {
|
|
46
|
+
const fromId = connSourceSelect.value;
|
|
47
|
+
const toId = connTargetSelect.value;
|
|
48
|
+
const label = connTextInput.value;
|
|
49
|
+
const color = connColorInput.value;
|
|
50
|
+
if (!fromId || !toId || fromId === toId) return;
|
|
51
|
+
const conn = createConnection(fromId, toId, label, color);
|
|
52
|
+
state.connections.push(conn);
|
|
53
|
+
saveState();
|
|
54
|
+
closeConnModal();
|
|
55
|
+
renderBoard();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
cancelConnBtn.addEventListener('click', closeConnModal);
|
|
59
|
+
closeConnBtn.addEventListener('click', closeConnModal);
|
|
60
|
+
addConnectionBtn.addEventListener('click', () => openConnModal());
|