pf2e-party-tracker 0.1.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/LICENSE +31 -0
- package/README.md +70 -0
- package/bin/cli.mjs +86 -0
- package/data/reference.generated.js +6 -0
- package/dist/icon.svg +5 -0
- package/dist/index.html +1330 -0
- package/dist/manifest.webmanifest +14 -0
- package/dist/sw.js +62 -0
- package/notice.md +81 -0
- package/package.json +47 -0
- package/src/config.js +140 -0
- package/src/engine.js +766 -0
- package/src/icon.svg +5 -0
- package/src/manifest.webmanifest +14 -0
- package/src/styles.css +298 -0
- package/src/sw.js +62 -0
- package/src/template.html +120 -0
- package/tools/build.py +60 -0
- package/tools/build_reference.py +249 -0
- package/tools/fixtures/investigator.json +1 -0
- package/tools/test.mjs +165 -0
package/src/icon.svg
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#6c7a89" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
+
<path d="M12 2.6 4.5 5.3v6.2c0 4.6 3.1 7.9 7.5 9.9 4.4-2 7.5-5.3 7.5-9.9V5.3z"/>
|
|
3
|
+
<circle cx="12" cy="9.3" r="1.7"/>
|
|
4
|
+
<path d="M8.7 15.4a3.4 3.4 0 0 1 6.6 0"/>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "PF2e Party Tracker",
|
|
3
|
+
"short_name": "Party Tracker",
|
|
4
|
+
"description": "A GM's Pathfinder 2e party tracker — passive stats, secret checks, exploration activities, offline.",
|
|
5
|
+
"start_url": ".",
|
|
6
|
+
"scope": ".",
|
|
7
|
+
"display": "standalone",
|
|
8
|
+
"orientation": "landscape",
|
|
9
|
+
"background_color": "#14161b",
|
|
10
|
+
"theme_color": "#14161b",
|
|
11
|
+
"icons": [
|
|
12
|
+
{ "src": "icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any maskable" }
|
|
13
|
+
]
|
|
14
|
+
}
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
THEME TOKENS — base tokens flip with [data-theme]; --accent (+ ink)
|
|
3
|
+
is set by JS (custom palette). Accent shades derive via color-mix().
|
|
4
|
+
============================================================ */
|
|
5
|
+
:root{
|
|
6
|
+
--accent:#6c7a89;
|
|
7
|
+
--accent-ink:#ffffff;
|
|
8
|
+
--accent-dim:color-mix(in srgb, var(--accent) 55%, var(--line));
|
|
9
|
+
--accent-soft:color-mix(in srgb, var(--accent) 14%, transparent);
|
|
10
|
+
--accent-text:var(--accent);
|
|
11
|
+
--radius:14px;
|
|
12
|
+
}
|
|
13
|
+
:root, :root[data-theme="dark"]{
|
|
14
|
+
color-scheme:dark;
|
|
15
|
+
--bg:#14161b; --surface:#1d2027; --surface-2:#262a33;
|
|
16
|
+
--ink:#e8eaf0; --muted:#9aa1b0; --line:#2f3440;
|
|
17
|
+
--heal:#5bbf7e; --harm:#d9637a; --info:#6fa8dc; --warn:#d6a94a;
|
|
18
|
+
--accent-text:var(--accent);
|
|
19
|
+
--shadow:0 1px 2px rgba(0,0,0,.4), 0 4px 14px rgba(0,0,0,.22);
|
|
20
|
+
}
|
|
21
|
+
:root[data-theme="light"]{
|
|
22
|
+
color-scheme:light;
|
|
23
|
+
--bg:#f4f5f8; --surface:#ffffff; --surface-2:#eef0f4;
|
|
24
|
+
--ink:#1b1e24; --muted:#5d6470; --line:#dfe3ea;
|
|
25
|
+
--heal:#2f9e5b; --harm:#c43a57; --info:#2b6cb0; --warn:#9a6b12;
|
|
26
|
+
--accent-text:color-mix(in srgb, var(--accent) 72%, #000);
|
|
27
|
+
--shadow:0 1px 2px rgba(20,30,50,.08), 0 4px 14px rgba(20,30,50,.06);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent;}
|
|
31
|
+
html,body{margin:0;padding:0;background:var(--bg);}
|
|
32
|
+
body{
|
|
33
|
+
color:var(--ink);
|
|
34
|
+
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
|
35
|
+
font-size:16px; line-height:1.5; -webkit-text-size-adjust:100%;
|
|
36
|
+
}
|
|
37
|
+
h1,h2,h3{margin:0 0 .4em;line-height:1.2;letter-spacing:-.01em;}
|
|
38
|
+
h1{font-size:1.25rem;font-weight:800;}
|
|
39
|
+
h2{font-size:1.05rem;font-weight:700;}
|
|
40
|
+
p{margin:.4em 0;}
|
|
41
|
+
.hide{display:none !important;}
|
|
42
|
+
.icn{display:inline-block;width:1em;height:1em;flex:0 0 auto;vertical-align:-.14em;}
|
|
43
|
+
.icn.flip{transform:rotate(180deg);}
|
|
44
|
+
button,.chip,input,select,textarea,.rrow,details.ref{transition:background-color .15s ease,border-color .15s ease,color .15s ease;}
|
|
45
|
+
|
|
46
|
+
/* ---- Header ---- */
|
|
47
|
+
header.top{
|
|
48
|
+
background:linear-gradient(180deg, color-mix(in srgb, var(--accent) 9%, var(--surface)), var(--surface));
|
|
49
|
+
border-bottom:1px solid var(--line);
|
|
50
|
+
padding:12px 20px; position:sticky; top:0; z-index:30;
|
|
51
|
+
}
|
|
52
|
+
.tophead{display:flex;align-items:center;justify-content:space-between;gap:10px;max-width:1200px;margin:0 auto;}
|
|
53
|
+
header.top h1{margin:0;display:flex;align-items:center;gap:9px;}
|
|
54
|
+
.apptitle-ic{display:inline-flex;color:var(--accent-text);width:1.3rem;height:1.3rem;}
|
|
55
|
+
header.top .sub{color:var(--muted);font-size:.82rem;margin:2px auto 0;max-width:1200px;}
|
|
56
|
+
.menubtn{
|
|
57
|
+
flex:0 0 auto;background:var(--surface-2);border:1px solid var(--line);border-radius:11px;
|
|
58
|
+
width:40px;height:40px;color:var(--accent-text);display:inline-flex;align-items:center;justify-content:center;
|
|
59
|
+
}
|
|
60
|
+
.menubtn .icn{width:1.25rem;height:1.25rem;}
|
|
61
|
+
|
|
62
|
+
/* ---- Top tab nav ---- */
|
|
63
|
+
nav.tabs{
|
|
64
|
+
position:sticky;top:0;z-index:20;background:var(--surface);border-bottom:1px solid var(--line);
|
|
65
|
+
display:flex;gap:2px;justify-content:center;padding:0 12px;
|
|
66
|
+
}
|
|
67
|
+
nav.tabs button{
|
|
68
|
+
background:none;border:none;color:var(--muted);font-family:inherit;font-weight:700;font-size:.92rem;
|
|
69
|
+
padding:12px 16px;display:inline-flex;align-items:center;gap:7px;cursor:pointer;
|
|
70
|
+
border-bottom:2px solid transparent;
|
|
71
|
+
}
|
|
72
|
+
nav.tabs button .ic{display:inline-flex;width:1.1rem;height:1.1rem;}
|
|
73
|
+
nav.tabs button:hover{color:var(--ink);}
|
|
74
|
+
nav.tabs button.on{color:var(--accent-text);border-bottom-color:var(--accent);}
|
|
75
|
+
|
|
76
|
+
/* ---- Layout ---- */
|
|
77
|
+
.wrap{max-width:1200px;margin:0 auto;padding:20px 20px 60px;}
|
|
78
|
+
.panel{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:18px;box-shadow:var(--shadow);}
|
|
79
|
+
.meta{font-size:.9rem;color:var(--muted);}
|
|
80
|
+
.meta b{color:var(--ink);font-weight:600;}
|
|
81
|
+
.hint{font-size:.82rem;color:var(--muted);}
|
|
82
|
+
.hint b{color:var(--accent-text);}
|
|
83
|
+
.empty{text-align:center;color:var(--muted);padding:52px 20px;}
|
|
84
|
+
.empty h2{color:var(--ink);}
|
|
85
|
+
|
|
86
|
+
/* ---- Buttons ---- */
|
|
87
|
+
button{font-family:inherit;cursor:pointer;}
|
|
88
|
+
.btn{
|
|
89
|
+
display:block;width:100%;padding:13px;border-radius:11px;border:none;
|
|
90
|
+
background:var(--accent);color:var(--accent-ink);font-weight:800;font-size:1rem;margin:12px 0;box-shadow:var(--shadow);
|
|
91
|
+
display:flex;align-items:center;justify-content:center;gap:8px;
|
|
92
|
+
}
|
|
93
|
+
.btn.secondary{background:var(--surface-2);color:var(--accent-text);border:1px solid var(--accent-dim);box-shadow:none;}
|
|
94
|
+
.btn.sm{width:auto;padding:9px 15px;font-size:.9rem;margin:0;border-radius:9px;}
|
|
95
|
+
.btn:active{transform:translateY(1px);}
|
|
96
|
+
|
|
97
|
+
/* ---- Fields ---- */
|
|
98
|
+
label.field{display:block;margin:12px 0;}
|
|
99
|
+
label.field.inline{margin:0;}
|
|
100
|
+
label.field .name{display:block;font-weight:700;margin-bottom:6px;font-size:.92rem;}
|
|
101
|
+
label.field .hint{font-weight:400;}
|
|
102
|
+
input,select,textarea{
|
|
103
|
+
width:100%;padding:11px;font-size:1rem;border-radius:10px;font-family:inherit;
|
|
104
|
+
background:var(--surface-2);color:var(--ink);border:1px solid var(--line);appearance:none;-webkit-appearance:none;accent-color:var(--accent);
|
|
105
|
+
}
|
|
106
|
+
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--accent);}
|
|
107
|
+
select{background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),linear-gradient(135deg,var(--muted) 50%,transparent 50%);
|
|
108
|
+
background-position:calc(100% - 20px) center,calc(100% - 14px) center;background-size:6px 6px,6px 6px;background-repeat:no-repeat;padding-right:38px;}
|
|
109
|
+
textarea{resize:vertical;line-height:1.4;}
|
|
110
|
+
input[type="color"]{padding:4px;height:42px;cursor:pointer;}
|
|
111
|
+
input[type="number"]{-moz-appearance:textfield;}
|
|
112
|
+
.row{display:flex;gap:12px;}
|
|
113
|
+
.row.tight{gap:8px;align-items:center;}
|
|
114
|
+
.row.tight .btn{margin:0;}
|
|
115
|
+
.row>*{flex:1;}
|
|
116
|
+
|
|
117
|
+
/* ============================================================
|
|
118
|
+
ROSTER TABLE
|
|
119
|
+
============================================================ */
|
|
120
|
+
table.roster{width:100%;border-collapse:collapse;font-variant-numeric:tabular-nums;}
|
|
121
|
+
.roster thead th{
|
|
122
|
+
position:sticky;top:49px;background:var(--surface-2);z-index:5;
|
|
123
|
+
font-size:.72rem;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);font-weight:700;
|
|
124
|
+
padding:9px 8px;text-align:center;border-bottom:1px solid var(--line);
|
|
125
|
+
}
|
|
126
|
+
.roster th.c-name{text-align:left;padding-left:14px;}
|
|
127
|
+
.roster th .passv{font-size:.62rem;color:var(--muted);opacity:.7;margin-left:3px;text-transform:none;}
|
|
128
|
+
.roster td{padding:10px 8px;border-bottom:1px solid var(--line);text-align:center;vertical-align:middle;}
|
|
129
|
+
.roster td.num{font-weight:700;font-size:1rem;}
|
|
130
|
+
.roster td.num.strong{font-size:1.1rem;color:var(--accent-text);}
|
|
131
|
+
.roster td .passv{display:inline-block;margin-left:5px;font-size:.72rem;font-weight:600;color:var(--muted);}
|
|
132
|
+
.roster td.c-name{text-align:left;padding-left:14px;max-width:280px;}
|
|
133
|
+
.roster td.c-exp{width:34px;color:var(--muted);}
|
|
134
|
+
.rrow{cursor:pointer;}
|
|
135
|
+
.rrow:hover{background:var(--accent-soft);}
|
|
136
|
+
.rrow.open{background:var(--accent-soft);}
|
|
137
|
+
.pname{font-weight:700;font-size:1rem;}
|
|
138
|
+
.pmeta{font-size:.78rem;color:var(--muted);margin-top:1px;}
|
|
139
|
+
.rconds{margin-top:4px;display:flex;flex-wrap:wrap;gap:4px;}
|
|
140
|
+
.condchip{font-size:.68rem;font-weight:700;background:var(--surface-2);border:1px solid var(--line);color:var(--muted);border-radius:6px;padding:1px 6px;}
|
|
141
|
+
.condchip.crit{background:color-mix(in srgb,var(--harm) 18%,var(--surface));border-color:color-mix(in srgb,var(--harm) 45%,var(--line));color:var(--harm);}
|
|
142
|
+
|
|
143
|
+
.hpwrap{display:flex;flex-direction:column;align-items:center;gap:3px;min-width:74px;}
|
|
144
|
+
.hpbar{width:70px;height:7px;border-radius:4px;background:var(--surface-2);border:1px solid var(--line);overflow:hidden;}
|
|
145
|
+
.hpfill{height:100%;background:var(--heal);}
|
|
146
|
+
.hpfill.mid{background:var(--warn);}
|
|
147
|
+
.hpfill.low{background:var(--harm);}
|
|
148
|
+
.hpnum{font-size:.8rem;font-weight:700;}
|
|
149
|
+
.hptemp{color:var(--info);margin-left:3px;}
|
|
150
|
+
|
|
151
|
+
/* ---- Expanded detail ---- */
|
|
152
|
+
.rdetail td{padding:0;background:var(--surface-2);}
|
|
153
|
+
.detail{border-left:4px solid var(--accent);padding:16px 18px;display:grid;grid-template-columns:1fr 1fr;gap:18px 26px;}
|
|
154
|
+
.dsec.live,.dsec:first-child{grid-column:1 / -1;}
|
|
155
|
+
.dsec-h{font-size:.72rem;text-transform:uppercase;letter-spacing:.6px;color:var(--accent-text);font-weight:800;margin-bottom:8px;border-bottom:1px solid var(--line);padding-bottom:4px;}
|
|
156
|
+
.dsec-note{float:right;color:var(--muted);font-weight:600;text-transform:none;letter-spacing:0;}
|
|
157
|
+
.abrow{display:flex;gap:8px;flex-wrap:wrap;}
|
|
158
|
+
.abcell{flex:1;min-width:64px;text-align:center;background:var(--surface);border:1px solid var(--line);border-radius:9px;padding:7px 4px;}
|
|
159
|
+
.ablbl{display:block;font-size:.66rem;color:var(--muted);letter-spacing:.5px;}
|
|
160
|
+
.abmod{display:block;font-size:1.15rem;font-weight:800;color:var(--accent-text);}
|
|
161
|
+
.skgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:5px 12px;}
|
|
162
|
+
.skcell{display:flex;align-items:baseline;gap:6px;padding:3px 0;border-bottom:1px dotted var(--line);}
|
|
163
|
+
.skcell.lore .skname{font-style:italic;}
|
|
164
|
+
.skname{flex:1;font-size:.9rem;}
|
|
165
|
+
.skmod{font-weight:800;font-size:.95rem;}
|
|
166
|
+
.skpsv{font-size:.72rem;color:var(--muted);width:22px;text-align:right;}
|
|
167
|
+
.dfacts{display:flex;flex-direction:column;gap:4px;font-size:.9rem;}
|
|
168
|
+
.dfacts b{color:var(--accent-text);font-weight:700;margin-right:4px;}
|
|
169
|
+
.castrow{margin-top:8px;display:flex;flex-wrap:wrap;gap:6px;}
|
|
170
|
+
.castchip{font-size:.8rem;background:var(--surface);border:1px solid var(--accent-dim);color:var(--accent-text);border-radius:7px;padding:3px 8px;font-weight:600;}
|
|
171
|
+
.dactions{grid-column:1 / -1;display:flex;justify-content:flex-end;}
|
|
172
|
+
|
|
173
|
+
/* ---- Live tracking ---- */
|
|
174
|
+
.livewrap{display:flex;flex-direction:column;gap:10px;}
|
|
175
|
+
.liverow{display:flex;flex-wrap:wrap;align-items:center;gap:12px;}
|
|
176
|
+
.hpctl{display:flex;align-items:center;gap:7px;flex-wrap:wrap;}
|
|
177
|
+
.hpin{width:62px;text-align:center;padding:7px;}
|
|
178
|
+
.amtin{width:52px;text-align:center;padding:7px;}
|
|
179
|
+
.stlbl{font-size:.72rem;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);font-weight:700;}
|
|
180
|
+
.stbtn{background:var(--surface);border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:7px 11px;font-weight:800;font-size:.9rem;}
|
|
181
|
+
.stbtn.harm{color:var(--harm);border-color:color-mix(in srgb,var(--harm) 40%,var(--line));}
|
|
182
|
+
.stbtn.heal{color:var(--heal);border-color:color-mix(in srgb,var(--heal) 40%,var(--line));}
|
|
183
|
+
.heroctl{display:flex;align-items:center;gap:6px;}
|
|
184
|
+
.pip{width:20px;height:20px;border-radius:50%;border:2px solid var(--accent);background:transparent;padding:0;}
|
|
185
|
+
.pip.full{background:var(--accent);}
|
|
186
|
+
.stepper{display:inline-flex;align-items:center;gap:7px;background:var(--surface);border:1px solid var(--line);border-radius:9px;padding:4px 8px;}
|
|
187
|
+
.stval{min-width:16px;text-align:center;font-weight:800;color:var(--muted);}
|
|
188
|
+
.stval.on{color:var(--accent-text);}
|
|
189
|
+
.condbtns{display:flex;flex-wrap:wrap;gap:6px;}
|
|
190
|
+
.condbtn{background:var(--surface);border:1px solid var(--line);color:var(--muted);border-radius:8px;padding:5px 10px;font-size:.82rem;font-weight:700;}
|
|
191
|
+
.condbtn.on{background:var(--accent);color:var(--accent-ink);border-color:var(--accent);}
|
|
192
|
+
.activeconds{display:flex;flex-wrap:wrap;gap:8px;}
|
|
193
|
+
.activecond{display:inline-flex;align-items:center;gap:5px;background:color-mix(in srgb,var(--accent) 12%,var(--surface));border:1px solid var(--accent-dim);color:var(--accent-text);border-radius:8px;padding:3px 5px 3px 10px;font-size:.85rem;font-weight:700;}
|
|
194
|
+
.condx{background:none;border:none;color:var(--muted);display:inline-flex;padding:2px;}
|
|
195
|
+
.condx .icn{width:.9rem;height:.9rem;}
|
|
196
|
+
|
|
197
|
+
/* ============================================================
|
|
198
|
+
ROLLER
|
|
199
|
+
============================================================ */
|
|
200
|
+
.rollbar{display:flex;align-items:flex-end;gap:14px;flex-wrap:wrap;}
|
|
201
|
+
.rollbar .field.inline select{min-width:180px;}
|
|
202
|
+
.rollbar .field.inline.dc input{width:90px;}
|
|
203
|
+
.rollhead{font-weight:800;color:var(--accent-text);margin:16px 0 8px;font-size:1.02rem;}
|
|
204
|
+
.rolllist{display:flex;flex-direction:column;gap:6px;margin-bottom:14px;}
|
|
205
|
+
.rollrow{display:flex;align-items:center;gap:12px;padding:9px 12px;border-radius:9px;background:var(--surface-2);border:1px solid var(--line);border-left:4px solid var(--line);}
|
|
206
|
+
.rollrow.csucc,.rollrow.succ{border-left-color:var(--heal);}
|
|
207
|
+
.rollrow.fail{border-left-color:var(--warn);}
|
|
208
|
+
.rollrow.cfail{border-left-color:var(--harm);}
|
|
209
|
+
.rname{flex:1;font-weight:700;}
|
|
210
|
+
.rmath{color:var(--muted);font-size:.85rem;font-variant-numeric:tabular-nums;}
|
|
211
|
+
.nat{font-weight:800;padding:0 4px;border-radius:5px;}
|
|
212
|
+
.nat.nat20{color:var(--heal);background:color-mix(in srgb,var(--heal) 18%,transparent);}
|
|
213
|
+
.nat.nat1{color:var(--harm);background:color-mix(in srgb,var(--harm) 18%,transparent);}
|
|
214
|
+
.natp{font-weight:700;color:var(--ink);}
|
|
215
|
+
.rtotal{font-size:1.25rem;font-weight:800;min-width:34px;text-align:right;}
|
|
216
|
+
.deg{font-size:.72rem;font-weight:800;text-transform:uppercase;letter-spacing:.4px;padding:2px 8px;border-radius:6px;min-width:96px;text-align:center;}
|
|
217
|
+
.deg.csucc{background:color-mix(in srgb,var(--heal) 22%,var(--surface));color:var(--heal);}
|
|
218
|
+
.deg.succ{background:color-mix(in srgb,var(--heal) 12%,var(--surface));color:var(--heal);}
|
|
219
|
+
.deg.fail{background:color-mix(in srgb,var(--warn) 16%,var(--surface));color:var(--warn);}
|
|
220
|
+
.deg.cfail{background:color-mix(in srgb,var(--harm) 18%,var(--surface));color:var(--harm);}
|
|
221
|
+
|
|
222
|
+
/* ============================================================
|
|
223
|
+
EXPLORATION BOARD
|
|
224
|
+
============================================================ */
|
|
225
|
+
.expboard{display:flex;flex-direction:column;}
|
|
226
|
+
.exphead,.exprow{display:grid;grid-template-columns:88px 1.4fr 1.3fr 100px 2fr;gap:12px;align-items:center;}
|
|
227
|
+
.exphead{font-size:.72rem;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);font-weight:700;padding:0 0 8px;border-bottom:1px solid var(--line);}
|
|
228
|
+
.exprow{padding:10px 0;border-bottom:1px solid var(--line);}
|
|
229
|
+
.expord{display:flex;align-items:center;gap:4px;}
|
|
230
|
+
.ordbtn{background:var(--surface-2);border:1px solid var(--line);color:var(--muted);border-radius:7px;padding:4px;display:inline-flex;}
|
|
231
|
+
.ordbtn .icn{width:1rem;height:1rem;}
|
|
232
|
+
.ordnum{font-weight:800;color:var(--accent-text);min-width:16px;text-align:center;}
|
|
233
|
+
.expsel select{padding:9px;}
|
|
234
|
+
.govmod{font-weight:800;color:var(--accent-text);font-size:1rem;}
|
|
235
|
+
.expnote{font-size:.82rem;color:var(--muted);}
|
|
236
|
+
|
|
237
|
+
/* ============================================================
|
|
238
|
+
REFERENCE
|
|
239
|
+
============================================================ */
|
|
240
|
+
.searchbar{display:flex;align-items:center;gap:8px;background:var(--surface-2);border:1px solid var(--line);border-radius:10px;padding:0 12px;margin-bottom:14px;}
|
|
241
|
+
.searchbar .icn{color:var(--muted);width:1.1rem;height:1.1rem;}
|
|
242
|
+
.searchbar input{border:none;background:none;padding:11px 0;}
|
|
243
|
+
.searchbar input:focus{border:none;}
|
|
244
|
+
details.ref{border:1px solid var(--line);border-radius:11px;margin:8px 0;background:var(--surface);overflow:hidden;}
|
|
245
|
+
details.ref summary{padding:12px 14px;cursor:pointer;list-style:none;display:flex;align-items:center;justify-content:space-between;gap:10px;}
|
|
246
|
+
details.ref summary::-webkit-details-marker{display:none;}
|
|
247
|
+
.refname{font-weight:700;}
|
|
248
|
+
.refkind{font-size:.68rem;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);background:var(--surface-2);border:1px solid var(--line);border-radius:6px;padding:1px 7px;}
|
|
249
|
+
.refbody{padding:0 14px 14px;font-size:.92rem;line-height:1.55;color:var(--ink);}
|
|
250
|
+
|
|
251
|
+
/* ============================================================
|
|
252
|
+
MENU
|
|
253
|
+
============================================================ */
|
|
254
|
+
.importbox{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:16px;box-shadow:var(--shadow);}
|
|
255
|
+
.importstatus{font-size:.85rem;margin-top:8px;min-height:1.2em;}
|
|
256
|
+
.importstatus.ok{color:var(--heal);}
|
|
257
|
+
.importstatus.err{color:var(--harm);}
|
|
258
|
+
.charcard{display:flex;align-items:center;gap:12px;background:var(--surface);border:1px solid var(--line);border-radius:11px;padding:12px 14px;margin:8px 0;box-shadow:var(--shadow);}
|
|
259
|
+
.charcard .txt{flex:1;}
|
|
260
|
+
.charcard .nm{font-weight:700;}
|
|
261
|
+
.charcard .tl{font-size:.82rem;color:var(--muted);}
|
|
262
|
+
.rmrow{width:34px;height:34px;flex:0 0 34px;border-radius:9px;background:var(--surface-2);border:1px solid var(--line);color:var(--muted);display:inline-flex;align-items:center;justify-content:center;}
|
|
263
|
+
.rmrow .icn{width:1rem;height:1rem;}
|
|
264
|
+
.seg{display:flex;gap:8px;margin:6px 0 12px;}
|
|
265
|
+
.seg button{flex:1;padding:11px;border-radius:10px;border:1px solid var(--line);background:var(--surface-2);color:var(--ink);font-weight:700;}
|
|
266
|
+
.seg button.on{background:var(--accent);color:var(--accent-ink);border-color:var(--accent);}
|
|
267
|
+
.swatchrow{display:flex;align-items:center;gap:10px;margin:8px 0;}
|
|
268
|
+
.swatchrow label{flex:1;font-weight:600;font-size:.92rem;}
|
|
269
|
+
.swatchrow input[type="color"]{flex:0 0 54px;width:54px;}
|
|
270
|
+
.credit{text-align:center;margin-top:22px;font-size:.78rem;}
|
|
271
|
+
|
|
272
|
+
/* ---- Toast ---- */
|
|
273
|
+
.toast{
|
|
274
|
+
position:fixed;bottom:28px;left:50%;transform:translateX(-50%);
|
|
275
|
+
background:var(--accent);color:var(--accent-ink);font-weight:800;padding:12px 20px;border-radius:30px;
|
|
276
|
+
z-index:50;opacity:0;transition:opacity .25s;pointer-events:none;box-shadow:0 4px 16px rgba(0,0,0,.35);max-width:90vw;text-align:center;
|
|
277
|
+
}
|
|
278
|
+
.toast.show{opacity:1;}
|
|
279
|
+
|
|
280
|
+
/* ============================================================
|
|
281
|
+
RESPONSIVE — collapse the wide table to stacked cards on small screens
|
|
282
|
+
============================================================ */
|
|
283
|
+
@media (max-width:820px){
|
|
284
|
+
.wrap{padding:14px 12px 60px;}
|
|
285
|
+
.roster thead{display:none;}
|
|
286
|
+
table.roster,.roster tbody,.roster tr,.roster td{display:block;width:100%;}
|
|
287
|
+
.roster tr.rrow{border:1px solid var(--line);border-radius:12px;margin:10px 0;padding:6px 4px;background:var(--surface);}
|
|
288
|
+
.roster td{border:none;text-align:left;padding:4px 12px;}
|
|
289
|
+
.roster td.num::before,.roster td.c-hp::before{content:attr(data-lbl);font-size:.7rem;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);font-weight:700;margin-right:8px;}
|
|
290
|
+
.roster td.num{display:inline-block;width:auto;min-width:96px;}
|
|
291
|
+
.roster td.c-exp{display:none;}
|
|
292
|
+
.rdetail td{display:block;}
|
|
293
|
+
.detail{grid-template-columns:1fr;}
|
|
294
|
+
.exphead{display:none;}
|
|
295
|
+
.exprow{grid-template-columns:1fr;gap:6px;}
|
|
296
|
+
.rollbar{flex-direction:column;align-items:stretch;}
|
|
297
|
+
.rollbar .field.inline select,.rollbar .field.inline.dc input{width:100%;}
|
|
298
|
+
}
|
package/src/sw.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/* Service worker — offline support for the hosted (GitHub Pages) PWA.
|
|
2
|
+
Network-first for the app page (fresh when online, cached when offline),
|
|
3
|
+
cache-first for static assets. Cache version is stamped at build time so a
|
|
4
|
+
redeploy refreshes clients automatically. */
|
|
5
|
+
const CACHE = "pf2e-party-tracker-__CACHE_VERSION__";
|
|
6
|
+
/* Every cache this app has ever made starts with PREFIX. The sweep below is
|
|
7
|
+
scoped to it on purpose: the three PF2e tools share an origin when they are
|
|
8
|
+
served from one host (GitHub Pages does it, and so does the toolbox), and an
|
|
9
|
+
unscoped sweep would delete the other two apps' offline caches every time
|
|
10
|
+
this one activated. */
|
|
11
|
+
const PREFIX = "pf2e-party-tracker-";
|
|
12
|
+
const SHELL = ["./", "./index.html", "./manifest.webmanifest", "./icon.svg"];
|
|
13
|
+
|
|
14
|
+
self.addEventListener("install", (e) => {
|
|
15
|
+
e.waitUntil(
|
|
16
|
+
caches.open(CACHE)
|
|
17
|
+
.then((c) => c.addAll(SHELL).catch(() => {}))
|
|
18
|
+
.then(() => self.skipWaiting())
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
self.addEventListener("activate", (e) => {
|
|
23
|
+
e.waitUntil(
|
|
24
|
+
caches.keys()
|
|
25
|
+
.then((keys) => Promise.all(keys.filter((k) => k.startsWith(PREFIX) && k !== CACHE)
|
|
26
|
+
.map((k) => caches.delete(k))))
|
|
27
|
+
.then(() => self.clients.claim())
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
self.addEventListener("fetch", (e) => {
|
|
32
|
+
const req = e.request;
|
|
33
|
+
if (req.method !== "GET") return;
|
|
34
|
+
|
|
35
|
+
// App page: network-first so deploys are picked up, cache fallback when offline.
|
|
36
|
+
if (req.mode === "navigate") {
|
|
37
|
+
e.respondWith(
|
|
38
|
+
fetch(req)
|
|
39
|
+
.then((res) => {
|
|
40
|
+
const copy = res.clone();
|
|
41
|
+
caches.open(CACHE).then((c) => c.put("./index.html", copy));
|
|
42
|
+
return res;
|
|
43
|
+
})
|
|
44
|
+
.catch(() => caches.match("./index.html").then((r) => r || caches.match("./")))
|
|
45
|
+
);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Static assets: cache-first.
|
|
50
|
+
e.respondWith(
|
|
51
|
+
caches.match(req).then((r) =>
|
|
52
|
+
r ||
|
|
53
|
+
fetch(req).then((res) => {
|
|
54
|
+
if (res.ok && new URL(req.url).origin === self.location.origin) {
|
|
55
|
+
const copy = res.clone();
|
|
56
|
+
caches.open(CACHE).then((c) => c.put(req, copy));
|
|
57
|
+
}
|
|
58
|
+
return res;
|
|
59
|
+
}).catch(() => r)
|
|
60
|
+
)
|
|
61
|
+
);
|
|
62
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
6
|
+
<meta name="theme-color" content="#14161b">
|
|
7
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
8
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
9
|
+
<meta name="apple-mobile-web-app-title" content="Party Tracker">
|
|
10
|
+
<meta name="description" content="A GM's Pathfinder 2e party tracker — passive stats, secret checks, and exploration activities.">
|
|
11
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236c7a89' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'><path d='M12 2.6 4.5 5.3v6.2c0 4.6 3.1 7.9 7.5 9.9 4.4-2 7.5-5.3 7.5-9.9V5.3z'/><circle cx='12' cy='9.3' r='1.7'/><path d='M8.7 15.4a3.4 3.4 0 0 1 6.6 0'/></svg>">
|
|
12
|
+
<!-- Set the light/dark theme before first paint (avoids a flash); the engine reconciles fully on load. -->
|
|
13
|
+
<script>
|
|
14
|
+
(function(){try{
|
|
15
|
+
var lib=JSON.parse(localStorage.getItem("pf2ePartyTracker.v1"))||{};
|
|
16
|
+
var s=lib.settings||{}, mode=s.themeMode||"auto";
|
|
17
|
+
if(mode!=="light"&&mode!=="dark") mode=(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches)?"dark":"light";
|
|
18
|
+
var root=document.documentElement; root.dataset.theme=mode;
|
|
19
|
+
var c=s.custom;
|
|
20
|
+
if(c){ if(c.accent)root.style.setProperty("--accent",c.accent); if(c.bg)root.style.setProperty("--bg",c.bg);
|
|
21
|
+
if(c.ink)root.style.setProperty("--ink",c.ink); if(c.surface)root.style.setProperty("--surface",c.surface); }
|
|
22
|
+
}catch(e){}})();
|
|
23
|
+
</script>
|
|
24
|
+
<link rel="manifest" href="manifest.webmanifest">
|
|
25
|
+
<link rel="apple-touch-icon" href="icon.svg">
|
|
26
|
+
<title>PF2e Party Tracker</title>
|
|
27
|
+
<style>
|
|
28
|
+
/*__INJECT_STYLES__*/
|
|
29
|
+
</style>
|
|
30
|
+
</head>
|
|
31
|
+
<body>
|
|
32
|
+
|
|
33
|
+
<header class="top">
|
|
34
|
+
<div class="tophead">
|
|
35
|
+
<h1><span class="apptitle-ic" id="titleIcon"></span> Party Tracker</h1>
|
|
36
|
+
<button class="menubtn" onclick="openMenu()" title="Characters & backup" aria-label="Menu"><svg class="icn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="8" r="3.4"/><path d="M5.5 20a6.5 6.5 0 0 1 13 0"/></svg></button>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="sub" id="partySub">Pathfinder 2e (Remaster)</div>
|
|
39
|
+
</header>
|
|
40
|
+
|
|
41
|
+
<nav class="tabs">
|
|
42
|
+
<button id="nav-roster" class="on" onclick="go('roster')"><span class="ic"></span>Roster</button>
|
|
43
|
+
<button id="nav-roll" onclick="go('roll')"><span class="ic"></span>Roll</button>
|
|
44
|
+
<button id="nav-explore" onclick="go('explore')"><span class="ic"></span>Explore</button>
|
|
45
|
+
<button id="nav-reference" onclick="go('reference')"><span class="ic"></span>Reference</button>
|
|
46
|
+
</nav>
|
|
47
|
+
|
|
48
|
+
<main class="wrap">
|
|
49
|
+
<section id="view-roster"></section>
|
|
50
|
+
<section id="view-roll" class="hide"></section>
|
|
51
|
+
<section id="view-explore" class="hide"></section>
|
|
52
|
+
<section id="view-reference" class="hide"></section>
|
|
53
|
+
|
|
54
|
+
<!-- ================= MENU ================= -->
|
|
55
|
+
<section id="view-menu" class="hide">
|
|
56
|
+
<h2>Import from Pathbuilder</h2>
|
|
57
|
+
<p class="meta">In Pathbuilder 2e: <b>Menu → Export → Export JSON</b>. You'll get an id number and a JSON export.</p>
|
|
58
|
+
<div class="importbox">
|
|
59
|
+
<label class="field"><span class="name">By export id</span>
|
|
60
|
+
<div class="row tight">
|
|
61
|
+
<input type="text" id="importCode" inputmode="numeric" placeholder="e.g. 102154">
|
|
62
|
+
<button class="btn sm" onclick="importFromCode()">Fetch</button>
|
|
63
|
+
</div>
|
|
64
|
+
</label>
|
|
65
|
+
<label class="field"><span class="name">…or paste JSON <span class="hint">always works, offline</span></span>
|
|
66
|
+
<textarea id="importJSON" rows="4" placeholder='Paste the exported JSON here, then tap Import'></textarea>
|
|
67
|
+
</label>
|
|
68
|
+
<button class="btn" onclick="importFromJSON()">Import from JSON</button>
|
|
69
|
+
<div class="importstatus" id="importStatus"></div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<h2 style="margin-top:26px">Characters</h2>
|
|
73
|
+
<div id="menuList"></div>
|
|
74
|
+
|
|
75
|
+
<h2 style="margin-top:26px">Back up & share</h2>
|
|
76
|
+
<p class="meta">Everything is stored <b>on this device only</b>. Export the whole party (or a single character from its detail panel) to a code you can save or send to another GM.</p>
|
|
77
|
+
<div class="row tight">
|
|
78
|
+
<button class="btn secondary" onclick="exportParty()">Export party</button>
|
|
79
|
+
<button class="btn secondary" onclick="importBackup()">Import code</button>
|
|
80
|
+
</div>
|
|
81
|
+
<textarea id="backupIO" rows="3" placeholder="A code appears here on export — or paste one here, then tap Import."></textarea>
|
|
82
|
+
|
|
83
|
+
<h2 style="margin-top:26px">Use offline</h2>
|
|
84
|
+
<div class="row tight">
|
|
85
|
+
<button class="btn secondary" onclick="downloadOffline()">Save offline copy</button>
|
|
86
|
+
<button class="btn secondary" id="installBtn" style="display:none" onclick="installApp()">Install app</button>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<h2 style="margin-top:26px">Appearance</h2>
|
|
90
|
+
<div id="appearancePanel"></div>
|
|
91
|
+
|
|
92
|
+
<button class="btn secondary" style="margin-top:22px" onclick="closeMenu()">← Back to roster</button>
|
|
93
|
+
<p class="meta credit">Condition & action text: Paizo (OGL/ORC) via the Foundry VTT pf2e project · unofficial fan tool<br><span id="dataStamp"></span></p>
|
|
94
|
+
</section>
|
|
95
|
+
</main>
|
|
96
|
+
|
|
97
|
+
<div class="toast" id="toast"></div>
|
|
98
|
+
|
|
99
|
+
<script>
|
|
100
|
+
/*__INJECT_REFERENCE__*/
|
|
101
|
+
</script>
|
|
102
|
+
<script>
|
|
103
|
+
/*__INJECT_CONFIG__*/
|
|
104
|
+
</script>
|
|
105
|
+
<script>
|
|
106
|
+
/*__INJECT_ENGINE__*/
|
|
107
|
+
</script>
|
|
108
|
+
<script>
|
|
109
|
+
(function(){
|
|
110
|
+
// fill in nav + title icons from the registry once config+engine are live
|
|
111
|
+
try{
|
|
112
|
+
document.getElementById("titleIcon").innerHTML = iconSvg("shield");
|
|
113
|
+
[["roster","roster"],["roll","roll"],["explore","explore"],["reference","reference"]].forEach(function(p){
|
|
114
|
+
var b=document.querySelector("#nav-"+p[0]+" .ic"); if(b) b.innerHTML=iconSvg(p[1]);
|
|
115
|
+
});
|
|
116
|
+
}catch(e){}
|
|
117
|
+
})();
|
|
118
|
+
</script>
|
|
119
|
+
</body>
|
|
120
|
+
</html>
|
package/tools/build.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
build.py — assemble the single, self-contained dist/index.html.
|
|
4
|
+
|
|
5
|
+
Inlines styles + Foundry-derived reference data + static config + engine into
|
|
6
|
+
the template so the result is one file you can open offline (file://) or hand to
|
|
7
|
+
another GM. Run from the project root:
|
|
8
|
+
|
|
9
|
+
python3 tools/build.py
|
|
10
|
+
"""
|
|
11
|
+
import os
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def read(rel):
|
|
18
|
+
with open(os.path.join(ROOT, rel), encoding="utf-8") as fh:
|
|
19
|
+
return fh.read()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main():
|
|
23
|
+
html = read("src/template.html")
|
|
24
|
+
parts = {
|
|
25
|
+
"/*__INJECT_STYLES__*/": read("src/styles.css"),
|
|
26
|
+
"/*__INJECT_REFERENCE__*/": read("data/reference.generated.js"),
|
|
27
|
+
"/*__INJECT_CONFIG__*/": read("src/config.js"),
|
|
28
|
+
"/*__INJECT_ENGINE__*/": read("src/engine.js"),
|
|
29
|
+
}
|
|
30
|
+
for marker, content in parts.items():
|
|
31
|
+
if marker not in html:
|
|
32
|
+
raise SystemExit(f"marker not found in template: {marker}")
|
|
33
|
+
# str.replace (not regex) so JS/CSS content is inserted verbatim
|
|
34
|
+
html = html.replace(marker, content)
|
|
35
|
+
|
|
36
|
+
out_dir = os.path.join(ROOT, "dist")
|
|
37
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
38
|
+
out_path = os.path.join(out_dir, "index.html")
|
|
39
|
+
with open(out_path, "w", encoding="utf-8") as fh:
|
|
40
|
+
fh.write(html)
|
|
41
|
+
|
|
42
|
+
# Static PWA files (for the hosted GitHub Pages version). Harmlessly absent
|
|
43
|
+
# when index.html is opened as a downloaded standalone file.
|
|
44
|
+
cache_ver = time.strftime("%Y%m%d%H%M%S")
|
|
45
|
+
for name in ("icon.svg", "manifest.webmanifest", "sw.js"):
|
|
46
|
+
src = os.path.join(ROOT, "src", name)
|
|
47
|
+
if not os.path.exists(src):
|
|
48
|
+
continue
|
|
49
|
+
content = read("src/" + name)
|
|
50
|
+
if name == "sw.js":
|
|
51
|
+
content = content.replace("__CACHE_VERSION__", cache_ver)
|
|
52
|
+
with open(os.path.join(out_dir, name), "w", encoding="utf-8") as fh:
|
|
53
|
+
fh.write(content)
|
|
54
|
+
|
|
55
|
+
kb = os.path.getsize(out_path) / 1024
|
|
56
|
+
print(f"built {out_path} ({kb:.0f} KB) + PWA files (cache {cache_ver})")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
main()
|