mnfst 0.5.163 → 0.5.165
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/lib/manifest.appwrite.auth.js +107 -199
- package/lib/manifest.appwrite.data.js +1 -3
- package/lib/manifest.appwrite.presences.js +292 -0
- package/lib/manifest.charts.js +34 -79
- package/lib/manifest.chat.js +515 -0
- package/lib/manifest.code.js +110 -340
- package/lib/manifest.colorpicker.js +252 -551
- package/lib/manifest.combobox.js +73 -91
- package/lib/manifest.components.js +34 -95
- package/lib/manifest.css +0 -132
- package/lib/manifest.data.js +189 -562
- package/lib/manifest.datepicker.js +43 -128
- package/lib/manifest.dropdowns.js +18 -39
- package/lib/manifest.export.js +29 -147
- package/lib/manifest.icons.js +9 -24
- package/lib/manifest.integrity.json +24 -23
- package/lib/manifest.js +65 -129
- package/lib/manifest.localization.js +67 -200
- package/lib/manifest.markdown.js +65 -174
- package/lib/manifest.min.css +1 -1
- package/lib/manifest.payments.js +40 -139
- package/lib/manifest.router.js +24 -73
- package/lib/manifest.status.js +20 -56
- package/lib/manifest.svg.js +12 -30
- package/lib/manifest.tooltips.js +28 -65
- package/lib/manifest.utilities.js +40 -107
- package/lib/manifest.virtual.js +15 -53
- package/package.json +1 -1
- package/lib/manifest.appwrite.presence.js +0 -1650
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/* Manifest Appwrite Presences ($presence)
|
|
2
|
+
/* By Andrew Matlock under MIT license
|
|
3
|
+
/* https://github.com/andrewmatlock/Manifest
|
|
4
|
+
/*
|
|
5
|
+
/* Reactive user status/roster over the native Appwrite Presences API
|
|
6
|
+
/* Requires Alpine JS (alpinejs.dev) to operate
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
(function () {
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
/* State */
|
|
13
|
+
|
|
14
|
+
const ctx = {
|
|
15
|
+
client: null,
|
|
16
|
+
presences: null,
|
|
17
|
+
realtime: null,
|
|
18
|
+
sub: null,
|
|
19
|
+
heartbeat: null,
|
|
20
|
+
started: false,
|
|
21
|
+
status: 'online',
|
|
22
|
+
metadata: null,
|
|
23
|
+
auto: true, // focus/blur auto-flip
|
|
24
|
+
heartbeatMs: 25000, // TTL refresh cadence
|
|
25
|
+
onFocus: null,
|
|
26
|
+
onBlur: null
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/* Config */
|
|
30
|
+
|
|
31
|
+
// Resolve ${VAR} against window.env
|
|
32
|
+
function resolveEnv(v) {
|
|
33
|
+
if (typeof v !== 'string') return v;
|
|
34
|
+
return v.replace(/\$\{([^}]+)\}/g, (m, name) => {
|
|
35
|
+
if (typeof window !== 'undefined' && window.env && window.env[name] != null) return window.env[name];
|
|
36
|
+
return null;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function getConfig() {
|
|
41
|
+
try {
|
|
42
|
+
const manifest = await window.ManifestDataConfig?.ensureManifest?.();
|
|
43
|
+
const aw = manifest?.appwrite;
|
|
44
|
+
if (!aw) return null;
|
|
45
|
+
const endpoint = resolveEnv(aw.endpoint);
|
|
46
|
+
const projectId = resolveEnv(aw.projectId);
|
|
47
|
+
if (!endpoint || !projectId) return null;
|
|
48
|
+
let devKey = aw.devKey ? resolveEnv(aw.devKey) : null;
|
|
49
|
+
if (devKey && /\$\{/.test(devKey)) devKey = null;
|
|
50
|
+
return { endpoint, projectId, devKey };
|
|
51
|
+
} catch (e) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function store() {
|
|
57
|
+
return (typeof Alpine !== 'undefined') ? Alpine.store('presence') : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function authUser() {
|
|
61
|
+
const s = (typeof Alpine !== 'undefined') ? Alpine.store('auth') : null;
|
|
62
|
+
return s && s.user ? s.user : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Active team id (read-scoping)
|
|
66
|
+
function currentTeamId() {
|
|
67
|
+
const s = (typeof Alpine !== 'undefined') ? Alpine.store('auth') : null;
|
|
68
|
+
if (!s) return null;
|
|
69
|
+
if (s.currentTeam) return typeof s.currentTeam === 'string' ? s.currentTeam : (s.currentTeam.$id || s.currentTeam.id || null);
|
|
70
|
+
if (Array.isArray(s.teams) && s.teams.length) return s.teams[0].$id || s.teams[0].id || null;
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Consistent per-user color
|
|
75
|
+
function userColor(id) {
|
|
76
|
+
if (!id) return '#666';
|
|
77
|
+
let hash = 0;
|
|
78
|
+
for (let i = 0; i < id.length; i++) hash = id.charCodeAt(i) + ((hash << 5) - hash);
|
|
79
|
+
return `hsl(${Math.abs(hash) % 360}, 70%, 50%)`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Appwrite client + services — authed via the session cookie, not the dev key (which is the guests principal for user ops)
|
|
83
|
+
async function ensureServices() {
|
|
84
|
+
if (ctx.presences && ctx.realtime) return true;
|
|
85
|
+
const A = window.Appwrite;
|
|
86
|
+
if (!A || !A.Presences || !A.Realtime) return false;
|
|
87
|
+
const cfg = await getConfig();
|
|
88
|
+
if (!cfg) return false;
|
|
89
|
+
const client = new A.Client().setEndpoint(cfg.endpoint).setProject(cfg.projectId);
|
|
90
|
+
ctx.client = client;
|
|
91
|
+
ctx.presences = new A.Presences(client);
|
|
92
|
+
ctx.realtime = new A.Realtime(client);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Records */
|
|
97
|
+
|
|
98
|
+
function normalize(rec) {
|
|
99
|
+
if (!rec) return null;
|
|
100
|
+
const userId = rec.userId || rec.$userId || rec.presenceId || rec.$id;
|
|
101
|
+
if (!userId) return null;
|
|
102
|
+
let metadata = rec.metadata;
|
|
103
|
+
if (typeof metadata === 'string') { try { metadata = JSON.parse(metadata); } catch (e) { } }
|
|
104
|
+
const updatedAt = rec.$updatedAt || rec.$createdAt || null;
|
|
105
|
+
return {
|
|
106
|
+
userId,
|
|
107
|
+
status: rec.status || null,
|
|
108
|
+
metadata: metadata || null,
|
|
109
|
+
lastSeen: updatedAt ? Date.parse(updatedAt) : Date.now(),
|
|
110
|
+
expiresAt: rec.expiresAt || rec.$expiresAt || null,
|
|
111
|
+
color: userColor(userId)
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Reactive map updates (reassign for Alpine tracking)
|
|
116
|
+
function putRecord(rec) {
|
|
117
|
+
const s = store(); if (!s || !rec) return;
|
|
118
|
+
s.records = { ...s.records, [rec.userId]: rec };
|
|
119
|
+
}
|
|
120
|
+
function removeRecord(userId) {
|
|
121
|
+
const s = store(); if (!s || !userId) return;
|
|
122
|
+
if (!s.records[userId]) return;
|
|
123
|
+
const next = { ...s.records }; delete next[userId]; s.records = next;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function hydrate() {
|
|
127
|
+
const s = store();
|
|
128
|
+
try {
|
|
129
|
+
const res = await ctx.presences.list();
|
|
130
|
+
const rows = res?.presences || res?.rows || res?.documents || [];
|
|
131
|
+
const map = {};
|
|
132
|
+
rows.forEach(r => { const n = normalize(r); if (n) map[n.userId] = n; });
|
|
133
|
+
if (s) s.records = map;
|
|
134
|
+
} catch (e) {
|
|
135
|
+
if (s) s.error = e?.message || String(e);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function subscribe() {
|
|
140
|
+
const A = window.Appwrite;
|
|
141
|
+
try {
|
|
142
|
+
const handle = ctx.realtime.subscribe(A.Channel.presences(), (response) => {
|
|
143
|
+
if (!response) return;
|
|
144
|
+
const events = Array.isArray(response.events) ? response.events : (response.events ? [response.events] : []);
|
|
145
|
+
const rec = normalize(response.payload || response);
|
|
146
|
+
if (!rec) return;
|
|
147
|
+
// Ignore echoes of our own presence (tracked locally; delayed echoes would clobber newer status)
|
|
148
|
+
const meId = authUser()?.$id;
|
|
149
|
+
if (meId && rec.userId === meId) return;
|
|
150
|
+
if (events.some(e => typeof e === 'string' && e.includes('delete'))) removeRecord(rec.userId);
|
|
151
|
+
else putRecord(rec);
|
|
152
|
+
});
|
|
153
|
+
// Normalise the unsubscribe handle
|
|
154
|
+
if (handle && typeof handle.then === 'function') {
|
|
155
|
+
handle.then(r => { ctx.sub = typeof r === 'function' ? r : (r && r.close ? () => r.close() : null); });
|
|
156
|
+
ctx.sub = () => { handle.then(r => { if (typeof r === 'function') r(); else if (r && r.close) r.close(); }); };
|
|
157
|
+
} else if (typeof handle === 'function') {
|
|
158
|
+
ctx.sub = handle;
|
|
159
|
+
} else if (handle && typeof handle.close === 'function') {
|
|
160
|
+
ctx.sub = () => handle.close();
|
|
161
|
+
}
|
|
162
|
+
} catch (e) {
|
|
163
|
+
const s = store(); if (s) s.error = e?.message || String(e);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* Operations */
|
|
168
|
+
|
|
169
|
+
async function set(status, metadata) {
|
|
170
|
+
const A = window.Appwrite;
|
|
171
|
+
const user = authUser();
|
|
172
|
+
const s = store();
|
|
173
|
+
if (!user) { if (s) s.error = 'sign in required'; return false; }
|
|
174
|
+
if (!(await ensureServices())) { if (s) s.error = 'Appwrite Presences unavailable'; return false; }
|
|
175
|
+
ctx.status = status != null ? status : ctx.status;
|
|
176
|
+
ctx.metadata = metadata !== undefined ? metadata : ctx.metadata;
|
|
177
|
+
|
|
178
|
+
// Read-scope + owner write (owner update/delete required, else updates 401)
|
|
179
|
+
const teamId = currentTeamId();
|
|
180
|
+
const permissions = [
|
|
181
|
+
teamId ? A.Permission.read(A.Role.team(teamId)) : A.Permission.read(A.Role.users()),
|
|
182
|
+
A.Permission.update(A.Role.user(user.$id)),
|
|
183
|
+
A.Permission.delete(A.Role.user(user.$id))
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
// name/color in metadata for rosters (name null when unknown — display label is the author's)
|
|
187
|
+
const meta = Object.assign(
|
|
188
|
+
{ name: user.name || user.email || null, color: userColor(user.$id) },
|
|
189
|
+
ctx.metadata || {}
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Optimistic local update
|
|
193
|
+
putRecord(normalize({ userId: user.$id, status: ctx.status, metadata: meta, $updatedAt: new Date().toISOString() }));
|
|
194
|
+
if (s) s.me = ctx.status;
|
|
195
|
+
try {
|
|
196
|
+
await ctx.presences.upsert({ presenceId: user.$id, status: ctx.status, metadata: meta, permissions });
|
|
197
|
+
if (s) s.error = null;
|
|
198
|
+
return true;
|
|
199
|
+
} catch (e) {
|
|
200
|
+
if (s) s.error = e?.message || String(e);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function clear() {
|
|
206
|
+
const user = authUser();
|
|
207
|
+
if (!user || !ctx.presences) return;
|
|
208
|
+
try { await ctx.presences.delete({ presenceId: user.$id }); } catch (e) { }
|
|
209
|
+
removeRecord(user.$id);
|
|
210
|
+
const s = store(); if (s) s.me = null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function of(userId) {
|
|
214
|
+
const s = store();
|
|
215
|
+
return (s && s.records ? s.records[userId] : null) || null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function all() {
|
|
219
|
+
const s = store();
|
|
220
|
+
return s && s.records ? Object.values(s.records) : [];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function start(options) {
|
|
224
|
+
const opts = options || {};
|
|
225
|
+
if (typeof opts.auto === 'boolean') ctx.auto = opts.auto;
|
|
226
|
+
if (typeof opts.heartbeatMs === 'number') ctx.heartbeatMs = opts.heartbeatMs;
|
|
227
|
+
if (typeof opts.status === 'string') ctx.status = opts.status;
|
|
228
|
+
|
|
229
|
+
const s = store();
|
|
230
|
+
if (!authUser()) { if (s) s.error = 'sign in required'; return false; }
|
|
231
|
+
if (!(await ensureServices())) { if (s) s.error = 'Appwrite Presences unavailable'; return false; }
|
|
232
|
+
if (ctx.started) return true;
|
|
233
|
+
ctx.started = true;
|
|
234
|
+
|
|
235
|
+
await hydrate();
|
|
236
|
+
subscribe();
|
|
237
|
+
await set(ctx.status, ctx.metadata);
|
|
238
|
+
|
|
239
|
+
// Heartbeat (refresh TTL)
|
|
240
|
+
ctx.heartbeat = setInterval(() => { if (authUser()) set(ctx.status, ctx.metadata); }, ctx.heartbeatMs);
|
|
241
|
+
|
|
242
|
+
// Auto focus/blur flip
|
|
243
|
+
if (ctx.auto) {
|
|
244
|
+
ctx.onBlur = () => set('away');
|
|
245
|
+
ctx.onFocus = () => set('online');
|
|
246
|
+
window.addEventListener('blur', ctx.onBlur);
|
|
247
|
+
window.addEventListener('focus', ctx.onFocus);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (s) { s.ready = true; s.error = null; }
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function stop() {
|
|
255
|
+
if (ctx.heartbeat) { clearInterval(ctx.heartbeat); ctx.heartbeat = null; }
|
|
256
|
+
if (ctx.onBlur) { window.removeEventListener('blur', ctx.onBlur); ctx.onBlur = null; }
|
|
257
|
+
if (ctx.onFocus) { window.removeEventListener('focus', ctx.onFocus); ctx.onFocus = null; }
|
|
258
|
+
if (typeof ctx.sub === 'function') { try { ctx.sub(); } catch (e) { } }
|
|
259
|
+
ctx.sub = null;
|
|
260
|
+
ctx.started = false;
|
|
261
|
+
const s = store(); if (s) s.ready = false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* Registration */
|
|
265
|
+
|
|
266
|
+
document.addEventListener('alpine:init', () => {
|
|
267
|
+
Alpine.store('presence', { records: {}, ready: false, error: null, me: null });
|
|
268
|
+
|
|
269
|
+
// Clear on sign-out
|
|
270
|
+
try {
|
|
271
|
+
Alpine.effect(() => {
|
|
272
|
+
if (!authUser() && ctx.started) { clear(); stop(); }
|
|
273
|
+
});
|
|
274
|
+
} catch (e) { }
|
|
275
|
+
|
|
276
|
+
const api = {
|
|
277
|
+
start, stop, set, clear, of, all,
|
|
278
|
+
get records() { return store()?.records || {}; },
|
|
279
|
+
get list() { return all(); },
|
|
280
|
+
get ready() { return !!store()?.ready; },
|
|
281
|
+
get error() { return store()?.error || null; },
|
|
282
|
+
get me() { return store()?.me || null; }
|
|
283
|
+
};
|
|
284
|
+
Alpine.magic('presence', () => api);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Clear on tab close
|
|
288
|
+
window.addEventListener('beforeunload', () => { try { if (ctx.started) clear(); } catch (e) { } });
|
|
289
|
+
|
|
290
|
+
// Non-Alpine handle
|
|
291
|
+
window.ManifestPresence = { start, stop, set, clear, of, all };
|
|
292
|
+
})();
|
package/lib/manifest.charts.js
CHANGED
|
@@ -1,33 +1,18 @@
|
|
|
1
|
-
/* Manifest Charts
|
|
1
|
+
/* Manifest Charts — in-house SVG chart renderer (themeable, prerender-safe, a11y).
|
|
2
2
|
/* By Andrew Matlock under MIT license
|
|
3
3
|
/* https://manifestx.dev
|
|
4
|
-
/*
|
|
5
|
-
/* An in-house SVG chart renderer. SVG (not canvas) so charts inherit theme
|
|
6
|
-
/* colors via CSS variables, are restylable with the same selector
|
|
7
|
-
/* conventions as every other element, survive static prerendering as real
|
|
8
|
-
/* DOM, and expose accessible <title>/<desc>. The only dependencies are the
|
|
9
|
-
/* d3-scale / d3-shape / d3-array micro-modules (ISC, ~22KB), lazy-loaded
|
|
10
|
-
/* from esm.run on first scroll-into-view — the same posture as the code
|
|
11
|
-
/* plugin loading highlight.js on demand.
|
|
4
|
+
/* d3-scale/shape/array micro-modules (ISC) lazy-loaded from esm.run on scroll-in.
|
|
12
5
|
*/
|
|
13
6
|
|
|
14
7
|
(function () {
|
|
15
8
|
'use strict';
|
|
16
9
|
|
|
17
|
-
/*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* loaded. `_ui` is a reserved, self-identifying key: any loaded data source
|
|
21
|
-
* may carry a top-level `_ui` object, namespaced per element (`_ui.charts`,
|
|
22
|
-
* `_ui.colorpicker`, …); no manifest flag — overrides piggyback on the normal
|
|
23
|
-
* local-data/localization model. resolve() deep-merges every loaded source's
|
|
24
|
-
* `_ui[component]` onto the plugin's English fallbacks. Kept byte-identical
|
|
25
|
-
* across the date picker / color picker copies.
|
|
26
|
-
* ------------------------------------------------------------------ */
|
|
10
|
+
/* Shared localized-UI resolver: deep-merges every loaded source's `_ui[component]`
|
|
11
|
+
* onto the plugin's English fallbacks. Guarded; byte-identical across the picker
|
|
12
|
+
* copies (first plugin to load defines it). */
|
|
27
13
|
if (!window.ManifestUI) {
|
|
28
14
|
window.ManifestUI = {
|
|
29
|
-
/*
|
|
30
|
-
* sources only — never force-loads others just to scan them for `_ui`. */
|
|
15
|
+
/* Loaded data sources (current locale); never force-loads others. */
|
|
31
16
|
_loadedSourceNames() {
|
|
32
17
|
try {
|
|
33
18
|
const store = window.ManifestDataStore && window.ManifestDataStore.rawDataStore;
|
|
@@ -35,8 +20,7 @@
|
|
|
35
20
|
} catch (_) { }
|
|
36
21
|
return [];
|
|
37
22
|
},
|
|
38
|
-
/*
|
|
39
|
-
* Reads inside the caller's Alpine effect (if any) so $x/$locale make it reactive. */
|
|
23
|
+
/* Reads inside the caller's Alpine effect (if any) so $x/$locale stay reactive. */
|
|
40
24
|
resolve(component, fallbacks) {
|
|
41
25
|
const merged = JSON.parse(JSON.stringify(fallbacks || {}));
|
|
42
26
|
try {
|
|
@@ -256,19 +240,15 @@
|
|
|
256
240
|
t.appendChild(document.createTextNode(str == null ? '' : String(str))); // untrusted-safe
|
|
257
241
|
return t;
|
|
258
242
|
}
|
|
259
|
-
// Entry animations run only on
|
|
260
|
-
// (a bound value
|
|
261
|
-
// — otherwise dragging a slider replays the reveal every frame and the
|
|
262
|
-
// chart flickers. drawChart sets this before dispatching.
|
|
243
|
+
// Entry animations run only on first draw; redraws paint final state directly
|
|
244
|
+
// (else a bound value change replays the reveal every frame). Set by drawChart.
|
|
263
245
|
let _suppressAnim = false;
|
|
264
246
|
function animate(el, keyframes, opts) {
|
|
265
247
|
if (_suppressAnim || prefersReducedMotion() || typeof el.animate !== 'function') return;
|
|
266
248
|
try { el.animate(keyframes, Object.assign({ duration: 600, easing: 'cubic-bezier(0.22,1,0.36,1)', fill: 'backwards' }, opts)); } catch (_) { }
|
|
267
249
|
}
|
|
268
|
-
// Cursor-following tooltip
|
|
269
|
-
//
|
|
270
|
-
// box) — so charts use their own tip, themed to match, following the
|
|
271
|
-
// pointer (better UX for dense charts). aria-label carries AT semantics.
|
|
250
|
+
// Cursor-following tooltip: x-tooltip's CSS anchor positioning can't target SVG
|
|
251
|
+
// children (no layout box), so charts use their own themed tip. aria-label for AT.
|
|
272
252
|
function applyTip(seg, tip, cfg) {
|
|
273
253
|
seg.setAttribute('aria-label', tip);
|
|
274
254
|
if (!cfg.tooltip) return;
|
|
@@ -337,12 +317,8 @@
|
|
|
337
317
|
if (self.el.id) _registry[self.el.id] = self.api;
|
|
338
318
|
},
|
|
339
319
|
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
// tabs), and does NOT reset a pending timer on re-entry — a
|
|
343
|
-
// high-frequency reactive trigger (e.g. a plugin bumping the data-store
|
|
344
|
-
// version every tick) would otherwise perpetually reschedule and starve
|
|
345
|
-
// the draw.
|
|
320
|
+
// One draw per tick. setTimeout (not rAF) so backgrounded tabs still draw;
|
|
321
|
+
// doesn't reset a pending timer, so a high-frequency trigger can't starve it.
|
|
346
322
|
schedule() { if (this._t) return; this._t = setTimeout(() => { this._t = null; this.draw(); }, 0); },
|
|
347
323
|
|
|
348
324
|
renderError(msg) { this.el.innerHTML = ''; const d = document.createElement('small'); d.textContent = msg; this.el.appendChild(d); },
|
|
@@ -407,10 +383,8 @@
|
|
|
407
383
|
return 4 + n * cfg.rowHeight + 22;
|
|
408
384
|
}
|
|
409
385
|
|
|
410
|
-
// Palette size
|
|
411
|
-
//
|
|
412
|
-
// segment colours cycle through however many exist. Re-probed per draw
|
|
413
|
-
// so per-scope overrides apply.
|
|
386
|
+
// Palette size = count of consecutive --color-chart-N tokens (themes may extend
|
|
387
|
+
// past 8); colours cycle through them. Re-probed per draw for per-scope overrides.
|
|
414
388
|
let _paletteN = 8;
|
|
415
389
|
function probePalette(el) {
|
|
416
390
|
try {
|
|
@@ -649,13 +623,8 @@
|
|
|
649
623
|
if (cfg.legend) drawLegend(state, labels);
|
|
650
624
|
}
|
|
651
625
|
|
|
652
|
-
//
|
|
653
|
-
//
|
|
654
|
-
// bands (e.g. positive/warning/negative ranges). `unit` suffixes the
|
|
655
|
-
// centered readout; `min`/`max` default 0–100.
|
|
656
|
-
// Widest rendered label in px — exact via getComputedTextLength (the root
|
|
657
|
-
// SVG is already in the document); char-estimate fallback pre-layout. Used
|
|
658
|
-
// to size axis gutters to their content so charts sit balanced, not lopsided.
|
|
626
|
+
// Widest rendered label in px (exact via getComputedTextLength, char-estimate
|
|
627
|
+
// fallback). Sizes axis gutters to their content so charts sit balanced.
|
|
659
628
|
function labelWidth(root, items, cls) {
|
|
660
629
|
if (!items || !items.length) return 0;
|
|
661
630
|
const probe = svg('text', cls ? { class: cls, x: -9999, y: -9999 } : { x: -9999, y: -9999 }, root);
|
|
@@ -694,15 +663,12 @@
|
|
|
694
663
|
svg('path', { class: 'gauge-track', d: arc({ startAngle: START, endAngle: END }) }, g);
|
|
695
664
|
}
|
|
696
665
|
|
|
697
|
-
// Value arc — final geometry
|
|
698
|
-
//
|
|
699
|
-
// the sweep is a CSS reveal, never the source of the end state.
|
|
666
|
+
// Value arc — final geometry set unconditionally, so a background tab that
|
|
667
|
+
// skips the fade reveal still renders correctly.
|
|
700
668
|
const color = (cfg.series[0] && cfg.series[0].color) || 'var(--color-chart-1)';
|
|
701
669
|
const valueAngle = scale(value);
|
|
702
670
|
const vArc = svg('path', { class: 'gauge-value', d: arc({ startAngle: START, endAngle: valueAngle }), style: `--color-chart-color:${color}` }, g);
|
|
703
671
|
applyTip(vArc, (cfg.title ? cfg.title + ': ' : '') + value + unit, cfg);
|
|
704
|
-
// Reveal via fade (WAAPI, origin-independent) — the final geometry above
|
|
705
|
-
// is unconditional, so the gauge is correct even if this never runs.
|
|
706
672
|
animate(vArc, [{ opacity: 0 }, { opacity: 1 }], { duration: 500 });
|
|
707
673
|
|
|
708
674
|
// Centered readout + range end labels.
|
|
@@ -714,10 +680,8 @@
|
|
|
714
680
|
}
|
|
715
681
|
}
|
|
716
682
|
|
|
717
|
-
// Heatmap —
|
|
718
|
-
//
|
|
719
|
-
// --color-chart-heat-* tokens (no JS colour-interpolation dep), driven
|
|
720
|
-
// by the per-cell `--heat` percentage.
|
|
683
|
+
// Heatmap — matrix of cells (row per series, column per datum). Cell colour is a
|
|
684
|
+
// CSS color-mix of the --color-chart-heat-* tokens via a per-cell `--heat` %.
|
|
721
685
|
function drawHeatmap(state, root, width, height) {
|
|
722
686
|
const cfg = state.config, d3 = state.d3;
|
|
723
687
|
const rows = cfg.series;
|
|
@@ -747,9 +711,8 @@
|
|
|
747
711
|
|
|
748
712
|
const plot = svg('g', { transform: `translate(${m.left},${m.top})` }, root);
|
|
749
713
|
|
|
750
|
-
// Round only the grid's outer corners: square tiles clipped to
|
|
751
|
-
//
|
|
752
|
-
// last tile's inner edge, so the radius isn't clipping empty gutter).
|
|
714
|
+
// Round only the grid's outer corners: square tiles clipped to one rounded
|
|
715
|
+
// rect hugging the cell extent (so the radius doesn't clip empty gutter).
|
|
753
716
|
const clipId = 'mnfst-heat-' + (++_uid);
|
|
754
717
|
const cp = svg('clipPath', { id: clipId }, svg('defs', null, root));
|
|
755
718
|
svg('rect', { x: 0, y: 0, width: Math.max(0, iw - gap), height: Math.max(0, ih - gap), rx: cssRadius(state.el) }, cp);
|
|
@@ -790,12 +753,9 @@
|
|
|
790
753
|
state.el.appendChild(footer);
|
|
791
754
|
}
|
|
792
755
|
|
|
793
|
-
// Gantt — each series is a track
|
|
794
|
-
//
|
|
795
|
-
//
|
|
796
|
-
// date-ish values → time, other strings → category (ordinal stages). A
|
|
797
|
-
// segment with no `to` is a point marker. Single track = a status strip;
|
|
798
|
-
// many tracks = swimlanes / Gantt.
|
|
756
|
+
// Gantt — each series is a track of `{ from, to, status?, label?, color? }`
|
|
757
|
+
// segments on a shared X axis, adapting numbers→linear, dates→time, else
|
|
758
|
+
// category. A `to`-less segment is a point marker.
|
|
799
759
|
function drawGantt(state, root, width, height) {
|
|
800
760
|
const cfg = state.config, d3 = state.d3;
|
|
801
761
|
const tracks = cfg.series;
|
|
@@ -967,9 +927,8 @@
|
|
|
967
927
|
const valueById = {};
|
|
968
928
|
entries.forEach(([key, val]) => resolveRegion(key, geo).ids.forEach(id => { valueById[id] = val; }));
|
|
969
929
|
|
|
970
|
-
// Display
|
|
971
|
-
//
|
|
972
|
-
// redraw when they arrive.
|
|
930
|
+
// Display name: `_ui.map.regions` override → localized pack ($locale) →
|
|
931
|
+
// English atlas name. Async packs trigger a redraw on arrival.
|
|
973
932
|
const num2a2 = (geo.meta && geo.meta.num2a2) || {};
|
|
974
933
|
const locale = chartLocale();
|
|
975
934
|
const ui = (window.ManifestUI && window.ManifestUI.resolve) ? window.ManifestUI.resolve('map', {}) : {};
|
|
@@ -986,9 +945,8 @@
|
|
|
986
945
|
return eng;
|
|
987
946
|
};
|
|
988
947
|
|
|
989
|
-
// Author-supplied place gazetteer (config `places` or `_ui.map.cities`)
|
|
990
|
-
//
|
|
991
|
-
// source it from a third-party library in their own project.
|
|
948
|
+
// Author-supplied place gazetteer (config `places` or `_ui.map.cities`) for
|
|
949
|
+
// resolving point names → coords. No city data is bundled.
|
|
992
950
|
const placesSrc = cfg.places || ui.cities || null;
|
|
993
951
|
let places = null;
|
|
994
952
|
if (placesSrc) { places = {}; Object.keys(placesSrc).forEach(k => { places[k.toLowerCase()] = placesSrc[k]; }); }
|
|
@@ -1066,9 +1024,8 @@
|
|
|
1066
1024
|
state.el.appendChild(footer);
|
|
1067
1025
|
}
|
|
1068
1026
|
|
|
1069
|
-
// Legend is a <footer> sibling below the SVG
|
|
1070
|
-
//
|
|
1071
|
-
// a <span> with an <i> swatch carrying the series colour.
|
|
1027
|
+
// Legend is a <footer> sibling below the SVG, not an overlay, so it never
|
|
1028
|
+
// collides with axis labels. Each item: a <span> with an <i> colour swatch.
|
|
1072
1029
|
function drawLegend(state, labelsOverride) {
|
|
1073
1030
|
const cfg = state.config;
|
|
1074
1031
|
const items = labelsOverride || cfg.series.map(s => s.name).filter(Boolean);
|
|
@@ -1091,10 +1048,8 @@
|
|
|
1091
1048
|
const state = createState(el, expression, modifiers);
|
|
1092
1049
|
el._chartState = state;
|
|
1093
1050
|
el.classList.add('chart');
|
|
1094
|
-
// Reserve
|
|
1095
|
-
//
|
|
1096
|
-
// reliably trigger the lazy-load IntersectionObserver, and reserving it
|
|
1097
|
-
// also prevents a layout jump when the SVG renders.
|
|
1051
|
+
// Reserve height up front (config, default 240): a 0-height box doesn't
|
|
1052
|
+
// reliably trigger the lazy-load observer, and reserving avoids a layout jump.
|
|
1098
1053
|
const hm = expression && /height\s*:\s*(\d+)/.exec(expression);
|
|
1099
1054
|
el.style.minHeight = (hm ? +hm[1] : 240) + 'px';
|
|
1100
1055
|
observer().observe(el);
|