@5even7/dlc-ui 0.1.0 → 0.2.1

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/dist/progress.mjs CHANGED
@@ -96,40 +96,40 @@ function normalizeOptions(defaults, preset, user) {
96
96
  }
97
97
 
98
98
  const PROGRESS_PRESETS = [
99
- {
100
- id: 'model-training',
101
- code: 'NC-10',
102
- name: 'MODEL TRAINING',
99
+ {
100
+ id: 'model-training',
101
+ code: 'NC-10',
102
+ name: '星火',
103
103
  subtitle: 'SHA 4.5 + 100 2026 TKN',
104
104
  group: 'progress',
105
105
  initialProgress: 43,
106
106
  edgeStyle: 'flow',
107
107
  colors: ['#2B1025', '#FF3F94', '#FF8A3D', '#FFF06A']
108
108
  },
109
- {
110
- id: 'agent-migration',
111
- code: 'NC-11',
112
- name: 'AGENT MIGRATION',
109
+ {
110
+ id: 'agent-migration',
111
+ code: 'NC-11',
112
+ name: '迁流',
113
113
  subtitle: 'TRANSFERRING PROTOCOL',
114
114
  group: 'progress',
115
115
  initialProgress: 33,
116
116
  edgeStyle: 'flow',
117
117
  colors: ['#101C37', '#245BFF', '#00CFFF', '#5DFFE6']
118
118
  },
119
- {
120
- id: 'visual-training',
121
- code: 'NC-12',
122
- name: 'VISUAL TRAINING',
119
+ {
120
+ id: 'visual-training',
121
+ code: 'NC-12',
122
+ name: '幻境',
123
123
  subtitle: 'GENERATING POWER ++',
124
124
  group: 'progress',
125
125
  initialProgress: 58,
126
126
  edgeStyle: 'flow',
127
127
  colors: ['#21142D', '#7042FF', '#42F58D', '#C4FF8A']
128
128
  },
129
- {
130
- id: 'tide',
131
- code: 'NC-13',
132
- name: 'TIDE',
129
+ {
130
+ id: 'tide',
131
+ code: 'NC-13',
132
+ name: '汐潮',
133
133
  subtitle: 'MOON PULL / COAST',
134
134
  group: 'progress',
135
135
  initialProgress: 30,
@@ -138,74 +138,262 @@ const PROGRESS_PRESETS = [
138
138
  }
139
139
  ];
140
140
 
141
- function validateProgressPreset(preset) {
142
- return Boolean(
143
- preset &&
144
- typeof preset.id === 'string' &&
145
- typeof preset.code === 'string' &&
146
- typeof preset.name === 'string' &&
147
- preset.group === 'progress' &&
148
- Number.isFinite(preset.initialProgress) &&
149
- ['flow', 'tide'].indexOf(preset.edgeStyle || 'flow') !== -1 &&
150
- Array.isArray(preset.colors) &&
151
- preset.colors.length === 4
152
- );
153
- }
154
-
155
- function getPreset(kind, ref) {
156
- const list = PROGRESS_PRESETS ;
157
- if (!list) throw new Error(`Unknown preset kind: ${kind} (use "capsule" or "progress")`);
158
- if (ref && typeof ref === 'object') {
159
- const valid = validateProgressPreset(ref);
160
- if (!valid) throw new Error(`Invalid ${kind} preset object`);
161
- return ref;
162
- }
163
- const key = String(ref).trim().toLowerCase();
164
- const found = list.find(
165
- (preset) =>
166
- preset.id.toLowerCase() === key ||
167
- preset.code.toLowerCase() === key ||
168
- preset.name.toLowerCase() === key
169
- );
170
- if (!found) throw new Error(`Unknown ${kind} preset: ${ref}`);
171
- return found;
141
+ /**
142
+ * Resolve a preset by its literary name (e.g. '沧溟').
143
+ * Matching is case-insensitive and ignores surrounding whitespace,
144
+ * so '沧溟' and ' 沧溟 ' both work.
145
+ */
146
+ function getPreset(kind, ref) {
147
+ const list = PROGRESS_PRESETS ;
148
+ if (!list) throw new Error(`Unknown preset kind: ${kind} (use "capsule" or "progress")`);
149
+ const key = String(ref).trim().toLowerCase();
150
+ const found = list.find(
151
+ (preset) => preset.name.toLowerCase() === key
152
+ );
153
+ if (!found) throw new Error(`Unknown ${kind} preset: ${ref}`);
154
+ return found;
172
155
  }
173
156
 
174
- const DEFAULTS = {
175
- progress: {
176
- width: 454,
177
- height: 104,
178
- min: 0,
157
+ const DEFAULTS = {
158
+ progress: {
159
+ width: 454,
160
+ height: 104,
161
+ min: 0,
179
162
  max: 100,
180
- draggable: true,
181
- keyboard: true,
182
- edgeStyle: 'flow',
183
- quality: 'auto',
184
- renderer: 'auto',
185
- showCopy: true,
186
- respectReducedMotion: true
187
- }
163
+ draggable: true,
164
+ keyboard: true,
165
+ disabled: false,
166
+ valueSuffix: '%',
167
+ edgeStyle: 'flow',
168
+ quality: 'auto',
169
+ renderer: 'auto',
170
+ textRatio: 54,
171
+ showValue: true,
172
+ respectReducedMotion: true
173
+ }
188
174
  };
189
175
 
190
- /**
191
- * All user-facing copy lives here so wording/brand changes never require a
192
- * global search. Templates use {brand} {code} {name} placeholders.
193
- */
194
- const COPY = {
195
- brandName: '画境观屿',
196
- dragLabel: 'DRAG',
197
- valueSuffix: '%',
176
+ /**
177
+ * Internal accessibility labels and small UI text. Since the copy API was
178
+ * removed (slots own the text area), these are no longer user-overridable.
179
+ * Templates use {brand} {code} {name} placeholders.
180
+ */
181
+ const COPY = {
182
+ brandName: '画境观屿',
183
+ valueSuffix: '%',
198
184
  progressAria: '{brand} {code} 加载进度',
199
185
  capsuleAria: '打开 {name} 沉浸预览'
200
186
  };
201
187
 
202
- function hexToRgba(hex, alpha = 1) {
203
- const normalized = hex.replace('#', '');
204
- const value = Number.parseInt(normalized, 16);
205
- const red = (value >> 16) & 255;
206
- const green = (value >> 8) & 255;
207
- const blue = value & 255;
208
- return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
188
+ /**
189
+ * Color helpers. `normalizeColor` converts any supported CSS color
190
+ * (hex / named / rgb() / rgba()) into a 6-digit hex string, so renderers can
191
+ * keep working with #rrggbb only. rgba() alpha is intentionally dropped:
192
+ * the WebGL shader has no per-color alpha channel, and the component is
193
+ * opaque by design — use component-level `opacity` for transparency.
194
+ */
195
+
196
+ const NAMED_COLORS = {
197
+ aliceblue: '#f0f8ff',
198
+ antiquewhite: '#faebd7',
199
+ aqua: '#00ffff',
200
+ aquamarine: '#7fffd4',
201
+ azure: '#f0ffff',
202
+ beige: '#f5f5dc',
203
+ bisque: '#ffe4c4',
204
+ black: '#000000',
205
+ blanchedalmond: '#ffebcd',
206
+ blue: '#0000ff',
207
+ blueviolet: '#8a2be2',
208
+ brown: '#a52a2a',
209
+ burlywood: '#deb887',
210
+ cadetblue: '#5f9ea0',
211
+ chartreuse: '#7fff00',
212
+ chocolate: '#d2691e',
213
+ coral: '#ff7f50',
214
+ cornflowerblue: '#6495ed',
215
+ cornsilk: '#fff8dc',
216
+ crimson: '#dc143c',
217
+ cyan: '#00ffff',
218
+ darkblue: '#00008b',
219
+ darkcyan: '#008b8b',
220
+ darkgoldenrod: '#b8860b',
221
+ darkgray: '#a9a9a9',
222
+ darkgreen: '#006400',
223
+ darkgrey: '#a9a9a9',
224
+ darkkhaki: '#bdb76b',
225
+ darkmagenta: '#8b008b',
226
+ darkolivegreen: '#556b2f',
227
+ darkorange: '#ff8c00',
228
+ darkorchid: '#9932cc',
229
+ darkred: '#8b0000',
230
+ darksalmon: '#e9967a',
231
+ darkseagreen: '#8fbc8f',
232
+ darkslateblue: '#483d8b',
233
+ darkslategray: '#2f4f4f',
234
+ darkslategrey: '#2f4f4f',
235
+ darkturquoise: '#00ced1',
236
+ darkviolet: '#9400d3',
237
+ deeppink: '#ff1493',
238
+ deepskyblue: '#00bfff',
239
+ dimgray: '#696969',
240
+ dimgrey: '#696969',
241
+ dodgerblue: '#1e90ff',
242
+ firebrick: '#b22222',
243
+ floralwhite: '#fffaf0',
244
+ forestgreen: '#228b22',
245
+ fuchsia: '#ff00ff',
246
+ gainsboro: '#dcdcdc',
247
+ ghostwhite: '#f8f8ff',
248
+ gold: '#ffd700',
249
+ goldenrod: '#daa520',
250
+ gray: '#808080',
251
+ green: '#008000',
252
+ greenyellow: '#adff2f',
253
+ grey: '#808080',
254
+ honeydew: '#f0fff0',
255
+ hotpink: '#ff69b4',
256
+ indianred: '#cd5c5c',
257
+ indigo: '#4b0082',
258
+ ivory: '#fffff0',
259
+ khaki: '#f0e68c',
260
+ lavender: '#e6e6fa',
261
+ lavenderblush: '#fff0f5',
262
+ lawngreen: '#7cfc00',
263
+ lemonchiffon: '#fffacd',
264
+ lightblue: '#add8e6',
265
+ lightcoral: '#f08080',
266
+ lightcyan: '#e0ffff',
267
+ lightgoldenrodyellow: '#fafad2',
268
+ lightgray: '#d3d3d3',
269
+ lightgreen: '#90ee90',
270
+ lightgrey: '#d3d3d3',
271
+ lightpink: '#ffb6c1',
272
+ lightsalmon: '#ffa07a',
273
+ lightseagreen: '#20b2aa',
274
+ lightskyblue: '#87cefa',
275
+ lightslategray: '#778899',
276
+ lightslategrey: '#778899',
277
+ lightsteelblue: '#b0c4de',
278
+ lightyellow: '#ffffe0',
279
+ lime: '#00ff00',
280
+ limegreen: '#32cd32',
281
+ linen: '#faf0e6',
282
+ magenta: '#ff00ff',
283
+ maroon: '#800000',
284
+ mediumaquamarine: '#66cdaa',
285
+ mediumblue: '#0000cd',
286
+ mediumorchid: '#ba55d3',
287
+ mediumpurple: '#9370db',
288
+ mediumseagreen: '#3cb371',
289
+ mediumslateblue: '#7b68ee',
290
+ mediumspringgreen: '#00fa9a',
291
+ mediumturquoise: '#48d1cc',
292
+ mediumvioletred: '#c71585',
293
+ midnightblue: '#191970',
294
+ mintcream: '#f5fffa',
295
+ mistyrose: '#ffe4e1',
296
+ moccasin: '#ffe4b5',
297
+ navajowhite: '#ffdead',
298
+ navy: '#000080',
299
+ oldlace: '#fdf5e6',
300
+ olive: '#808000',
301
+ olivedrab: '#6b8e23',
302
+ orange: '#ffa500',
303
+ orangered: '#ff4500',
304
+ orchid: '#da70d6',
305
+ palegoldenrod: '#eee8aa',
306
+ palegreen: '#98fb98',
307
+ paleturquoise: '#afeeee',
308
+ palevioletred: '#db7093',
309
+ papayawhip: '#ffefd5',
310
+ peachpuff: '#ffdab9',
311
+ peru: '#cd853f',
312
+ pink: '#ffc0cb',
313
+ plum: '#dda0dd',
314
+ powderblue: '#b0e0e6',
315
+ purple: '#800080',
316
+ rebeccapurple: '#663399',
317
+ red: '#ff0000',
318
+ rosybrown: '#bc8f8f',
319
+ royalblue: '#4169e1',
320
+ saddlebrown: '#8b4513',
321
+ salmon: '#fa8072',
322
+ sandybrown: '#f4a460',
323
+ seagreen: '#2e8b57',
324
+ seashell: '#fff5ee',
325
+ sienna: '#a0522d',
326
+ silver: '#c0c0c0',
327
+ skyblue: '#87ceeb',
328
+ slateblue: '#6a5acd',
329
+ slategray: '#708090',
330
+ slategrey: '#708090',
331
+ snow: '#fffafa',
332
+ springgreen: '#00ff7f',
333
+ steelblue: '#4682b4',
334
+ tan: '#d2b48c',
335
+ teal: '#008080',
336
+ thistle: '#d8bfd8',
337
+ tomato: '#ff6347',
338
+ turquoise: '#40e0d0',
339
+ violet: '#ee82ee',
340
+ wheat: '#f5deb3',
341
+ white: '#ffffff',
342
+ whitesmoke: '#f5f5f5',
343
+ yellow: '#ffff00',
344
+ yellowgreen: '#9acd32'
345
+ };
346
+
347
+ function clampChannel(value) {
348
+ return Math.min(255, Math.max(0, Math.round(value)));
349
+ }
350
+
351
+ function normalizeRgbArgs(args) {
352
+ const parts = args.split(/[,\s/]+/).filter(Boolean);
353
+ if (parts.length < 3) return null;
354
+ const to255 = (value) => {
355
+ if (typeof value !== 'string' || !value) return null;
356
+ if (value.endsWith('%')) return clampChannel((parseFloat(value) / 100) * 255);
357
+ const number = Number(value);
358
+ return Number.isFinite(number) ? clampChannel(number) : null;
359
+ };
360
+ const red = to255(parts[0]);
361
+ const green = to255(parts[1]);
362
+ const blue = to255(parts[2]);
363
+ if (red === null || green === null || blue === null) return null;
364
+ return `#${[red, green, blue].map((n) => n.toString(16).padStart(2, '0')).join('')}`;
365
+ }
366
+
367
+ /**
368
+ * Convert any supported CSS color into `#rrggbb`. Returns null when the
369
+ * value cannot be parsed. Supports: #rgb / #rrggbb (case-insensitive),
370
+ * CSS named colors, rgb() / rgba() with comma or modern space syntax,
371
+ * including percentages. Alpha in rgba() is ignored.
372
+ */
373
+ function normalizeColor(value) {
374
+ if (typeof value !== 'string') return null;
375
+ const input = value.trim();
376
+ if (!input) return null;
377
+ const lower = input.toLowerCase();
378
+ if (NAMED_COLORS[lower]) return NAMED_COLORS[lower];
379
+ if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(input)) {
380
+ const hex = input.slice(1).toLowerCase();
381
+ if (hex.length === 3) return `#${hex.split('').map((c) => c + c).join('')}`;
382
+ return `#${hex}`;
383
+ }
384
+ const match = input.match(/^rgba?\((.*)\)$/i);
385
+ if (match) return normalizeRgbArgs(match[1]);
386
+ return null;
387
+ }
388
+
389
+ function hexToRgba(color, alpha = 1) {
390
+ const hex = normalizeColor(color);
391
+ if (!hex) return 'rgba(0, 0, 0, 0)';
392
+ const value = Number.parseInt(hex.slice(1), 16);
393
+ const red = (value >> 16) & 255;
394
+ const green = (value >> 8) & 255;
395
+ const blue = value & 255;
396
+ return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
209
397
  }
210
398
 
211
399
  /**
@@ -911,6 +1099,22 @@ function attachProgressFlowOverlay({ root, canvas, preset, getProgress }) {
911
1099
  };
912
1100
  }
913
1101
 
1102
+ /**
1103
+ * Run a callback asynchronously, right after the current task and before the
1104
+ * next paint. Used for mount-time events (ready / error) so listeners that
1105
+ * are attached after create() still receive them. Falls back for legacy
1106
+ * browsers that lack queueMicrotask / Promise.
1107
+ */
1108
+ function nextTick(fn) {
1109
+ if (typeof queueMicrotask === 'function') {
1110
+ queueMicrotask(fn);
1111
+ } else if (typeof Promise !== 'undefined' && typeof Promise.resolve === 'function') {
1112
+ Promise.resolve().then(fn);
1113
+ } else {
1114
+ setTimeout(fn, 0);
1115
+ }
1116
+ }
1117
+
914
1118
  const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
915
1119
  const SUPPORTS_CTX_FILTER = typeof CanvasRenderingContext2D !== 'undefined' && 'filter' in CanvasRenderingContext2D.prototype;
916
1120
 
@@ -990,14 +1194,16 @@ const FLOW_PROFILES = {
990
1194
  };
991
1195
 
992
1196
  class ProgressCapsuleController {
993
- constructor({ root, canvas, valueElement, preset, emitter, options, copy }) {
1197
+ constructor({ root, canvas, valueElement, preset, emitter, options, copy, dirty }) {
994
1198
  this.root = root;
995
1199
  this.canvas = canvas;
996
1200
  this.valueElement = valueElement;
997
1201
  this.preset = preset;
998
1202
  this.emitter = emitter;
999
- this.options = options;
1000
- this.copy = copy;
1203
+ this.options = options;
1204
+ this.copy = copy;
1205
+ this.dirty = dirty;
1206
+ this.valueSuffix = options.valueSuffix ?? copy.valueSuffix;
1001
1207
  this.profile = preset.edgeStyle === 'tide'
1002
1208
  ? FLOW_PROFILES.tide
1003
1209
  : (FLOW_PROFILES[preset.id] || FLOW_PROFILES['visual-training']);
@@ -1038,11 +1244,22 @@ class ProgressCapsuleController {
1038
1244
  this.value = clamp(nextValue, this.min, this.max);
1039
1245
  const rounded = Math.round(this.value);
1040
1246
  this.root.style.setProperty('--progress', this.value.toFixed(2));
1041
- this.root.setAttribute('aria-valuenow', String(rounded));
1042
- this.root.setAttribute('aria-valuetext', `${rounded}${this.copy.valueSuffix}`);
1043
- this.valueElement.textContent = `${rounded}${this.copy.valueSuffix}`;
1044
- if (!this.suppressEvents) this.emitter.emit('change', { value: this.value, source });
1045
- }
1247
+ this.root.setAttribute('aria-valuenow', String(rounded));
1248
+ this.root.setAttribute('aria-valuetext', `${rounded}${this.valueSuffix}`);
1249
+ if (this.valueElement) this.valueElement.textContent = `${rounded}${this.valueSuffix}`;
1250
+ if (!this.suppressEvents) {
1251
+ if (this.dirty) this.dirty.value = true;
1252
+ this.emitter.emit('change', { value: this.value, source });
1253
+ }
1254
+ }
1255
+
1256
+ setValueSuffix(suffix) {
1257
+ this.valueSuffix = String(suffix == null ? '' : suffix);
1258
+ const rounded = Math.round(this.value);
1259
+ if (this.valueElement) this.valueElement.textContent = `${rounded}${this.valueSuffix}`;
1260
+ this.root.setAttribute('aria-valuetext', `${rounded}${this.valueSuffix}`);
1261
+ return this;
1262
+ }
1046
1263
 
1047
1264
  resizeCanvas() {
1048
1265
  const bounds = this.root.getBoundingClientRect();
@@ -1215,13 +1432,14 @@ class ProgressCapsuleController {
1215
1432
  if (clamped !== this.value) this.setProgress(clamped, 'prop');
1216
1433
  }
1217
1434
 
1218
- drawReferenceFlow() {
1435
+ drawReferenceFlow() {
1219
1436
  const ctx = this.ctx;
1220
1437
  const width = this.width;
1221
1438
  const height = this.height;
1222
1439
  if (!ctx || width <= 0 || height <= 0) return;
1223
1440
 
1224
- const shoreline = width * (this.value / 100);
1441
+ const range = Math.max(this.max - this.min, 1);
1442
+ const shoreline = width * Math.min(Math.max((this.value - this.min) / range, 0), 1);
1225
1443
  const time = this.flowTime;
1226
1444
  const [dark, accentA, accentB, glow] = this.preset.colors;
1227
1445
 
@@ -1339,32 +1557,49 @@ class ProgressCapsuleController {
1339
1557
  }
1340
1558
  }
1341
1559
 
1342
- updateFromPointer(event) {
1343
- const bounds = this.root.getBoundingClientRect();
1344
- const ratio = bounds.width > 0 ? (event.clientX - bounds.left) / bounds.width : 0;
1345
- this.setProgress(ratio * 100, 'drag');
1346
- }
1347
-
1348
- beginDrag(event) {
1349
- if (event.button !== undefined && event.button !== 0) return;
1350
- event.preventDefault();
1351
- this.dragging = true;
1352
- this.root.classList.add('is-dragging');
1353
- try { this.root.setPointerCapture?.(event.pointerId); } catch {}
1354
- this.emitter.emit('dragstart', { value: this.value });
1355
- this.updateFromPointer(event);
1356
- }
1357
-
1358
- moveDrag(event) {
1359
- if (!this.dragging) return;
1360
- event.preventDefault();
1361
- this.updateFromPointer(event);
1362
- }
1363
-
1364
- endDrag(event) {
1365
- if (!this.dragging) return;
1366
- this.dragging = false;
1367
- this.root.classList.remove('is-dragging');
1560
+ updateFromPointer(event) {
1561
+ const bounds = this.root.getBoundingClientRect();
1562
+ const ratio = bounds.width > 0 ? (event.clientX - bounds.left) / bounds.width : 0;
1563
+ this.setProgress(this.min + ratio * (this.max - this.min), 'drag');
1564
+ }
1565
+
1566
+ beginDrag(event) {
1567
+ if (event.button !== undefined && event.button !== 0) return;
1568
+ event.preventDefault();
1569
+ this.dragging = true;
1570
+ this.pendingPointer = null;
1571
+ this._dragRaf = 0;
1572
+ this.root.classList.add('is-dragging');
1573
+ try { this.root.setPointerCapture?.(event.pointerId); } catch {}
1574
+ this.emitter.emit('dragstart', { value: this.value });
1575
+ this.updateFromPointer(event);
1576
+ }
1577
+
1578
+ moveDrag(event) {
1579
+ if (!this.dragging) return;
1580
+ event.preventDefault();
1581
+ // rAF 合并:一帧最多一次 setProgress/change,视觉仍每帧更新。
1582
+ this.pendingPointer = event;
1583
+ if (this._dragRaf) return;
1584
+ this._dragRaf = requestAnimationFrame(() => {
1585
+ this._dragRaf = 0;
1586
+ const pending = this.pendingPointer;
1587
+ this.pendingPointer = null;
1588
+ if (pending) this.updateFromPointer(pending);
1589
+ });
1590
+ }
1591
+
1592
+ endDrag(event) {
1593
+ if (!this.dragging) return;
1594
+ this.dragging = false;
1595
+ if (this._dragRaf) {
1596
+ cancelAnimationFrame(this._dragRaf);
1597
+ this._dragRaf = 0;
1598
+ }
1599
+ const pending = this.pendingPointer;
1600
+ this.pendingPointer = null;
1601
+ if (pending) this.updateFromPointer(pending);
1602
+ this.root.classList.remove('is-dragging');
1368
1603
  try {
1369
1604
  if (event?.pointerId !== undefined && this.root.hasPointerCapture?.(event.pointerId)) {
1370
1605
  this.root.releasePointerCapture(event.pointerId);
@@ -1386,15 +1621,16 @@ class ProgressCapsuleController {
1386
1621
  }
1387
1622
 
1388
1623
  if (this.options.keyboard !== false) {
1389
- this.handlers.keydown = (event) => {
1390
- const actions = {
1391
- ArrowLeft: -2,
1392
- ArrowDown: -2,
1393
- ArrowRight: 2,
1394
- ArrowUp: 2,
1395
- PageDown: -10,
1396
- PageUp: 10
1397
- };
1624
+ this.handlers.keydown = (event) => {
1625
+ const step = (this.max - this.min) * 0.02;
1626
+ const actions = {
1627
+ ArrowLeft: -step,
1628
+ ArrowDown: -step,
1629
+ ArrowRight: step,
1630
+ ArrowUp: step,
1631
+ PageDown: -step * 5,
1632
+ PageUp: step * 5
1633
+ };
1398
1634
  if (event.key === 'Home') {
1399
1635
  event.preventDefault();
1400
1636
  this.setProgress(this.min, 'keyboard');
@@ -1432,8 +1668,9 @@ class ProgressCapsuleController {
1432
1668
  this.preset = { ...this.preset, colors };
1433
1669
  }
1434
1670
 
1435
- dispose() {
1436
- if (this.resizeObserver) this.resizeObserver.disconnect();
1671
+ dispose() {
1672
+ if (this._dragRaf) cancelAnimationFrame(this._dragRaf);
1673
+ if (this.resizeObserver) this.resizeObserver.disconnect();
1437
1674
  else window.removeEventListener('resize', this.resizeCanvas);
1438
1675
  for (const name of Object.keys(this.handlers)) {
1439
1676
  const handler = this.handlers[name];
@@ -1444,103 +1681,127 @@ class ProgressCapsuleController {
1444
1681
  }
1445
1682
 
1446
1683
  /**
1447
- * Mount a fluid progress capsule into `container`.
1448
- *
1449
- * Options: preset (id/code/name or object), width, height, value, min, max,
1450
- * draggable, keyboard, colors, edgeStyle, quality,
1451
- * respectReducedMotion, copy, cssVars.
1452
- */
1453
- function createProgressCapsule(container, options = {}) {
1684
+ * Mount a fluid progress capsule into `container`.
1685
+ *
1686
+ * Options: preset (id/code/name or object), width, height, value, min, max,
1687
+ * draggable, keyboard, colors, edgeStyle, textRatio (0-100, text region
1688
+ * width in percent), text (HTML string or DOM nodes for the text slot),
1689
+ * colorContent (HTML string or DOM nodes for the color slot),
1690
+ * showValue (show/hide the right-side percentage), quality,
1691
+ * respectReducedMotion, cssVars.
1692
+ */
1693
+ function createProgressCapsule(container, options = {}) {
1454
1694
  if (!container || typeof container.appendChild !== 'function') {
1455
1695
  throw new Error('createProgressCapsule: container element is required');
1456
1696
  }
1457
1697
 
1458
- const preset = { ...getPreset('progress', options.preset ?? 'NC-10') };
1459
- const merged = normalizeOptions(DEFAULTS.progress, preset, options);
1460
- const copy = { ...COPY, ...(merged.copy || {}) };
1461
-
1462
- const root = document.createElement('div');
1463
- root.className = 'hj-capsule-root hj-progress-root';
1464
- root.setAttribute('role', 'slider');
1465
- root.setAttribute('tabindex', '0');
1466
- root.setAttribute('data-draggable', String(merged.draggable !== false));
1698
+ const preset = { ...getPreset('progress', options.preset ?? '星火') };
1699
+ const merged = normalizeOptions(DEFAULTS.progress, preset, options);
1700
+ const normalizedColors = merged.colors.map(normalizeColor);
1701
+ if (normalizedColors.every(Boolean)) preset.colors = normalizedColors;
1702
+ preset.edgeStyle = merged.edgeStyle;
1703
+ const copy = COPY;
1704
+ const customLabel = typeof options.label === 'string' && options.label ? options.label : null;
1705
+ // Vue 的裸布尔属性(<ProgressCapsule disabled />)会传成空字符串,
1706
+ // 这里统一按 true 处理。
1707
+ const disabled = merged.disabled === true || merged.disabled === '' || merged.disabled === 'true';
1708
+ const effectiveDraggable = disabled ? false : merged.draggable !== false;
1709
+ const effectiveKeyboard = disabled ? false : merged.keyboard !== false;
1710
+ const dirty = {
1711
+ preset: false,
1712
+ colors: false,
1713
+ textRatio: false,
1714
+ cssVars: false,
1715
+ edgeStyle: false,
1716
+ value: false
1717
+ };
1718
+
1719
+ const root = document.createElement('div');
1720
+ root.className = 'hj-capsule-root hj-progress-root';
1721
+ root.setAttribute('role', 'slider');
1722
+ root.setAttribute('tabindex', '0');
1723
+ root.setAttribute('data-draggable', String(effectiveDraggable));
1724
+ if (disabled) {
1725
+ root.setAttribute('aria-disabled', 'true');
1726
+ root.classList.add('is-disabled');
1727
+ }
1467
1728
  root.setAttribute('aria-valuemin', String(merged.min ?? 0));
1468
1729
  root.setAttribute('aria-valuemax', String(merged.max ?? 100));
1469
1730
  root.setAttribute('aria-valuenow', String(preset.initialProgress));
1470
- root.setAttribute(
1471
- 'aria-label',
1472
- interpolate(copy.progressAria, { brand: copy.brandName, code: preset.code, name: preset.name })
1473
- );
1474
- if (merged.keyboard === false) root.setAttribute('tabindex', '-1');
1731
+ const updateAria = () => {
1732
+ root.setAttribute(
1733
+ 'aria-label',
1734
+ customLabel || interpolate(copy.progressAria, { brand: copy.brandName, code: preset.code, name: preset.name })
1735
+ );
1736
+ };
1737
+ updateAria();
1738
+ if (!effectiveKeyboard) root.setAttribute('tabindex', '-1');
1475
1739
 
1476
1740
  const canvas = document.createElement('canvas');
1477
1741
  canvas.className = 'hj-progress-canvas';
1478
1742
  canvas.setAttribute('aria-hidden', 'true');
1479
1743
 
1480
- const copyEnabled = copy.enabled !== false && merged.showCopy !== false;
1481
-
1482
- const copyText = (key, fallback) => {
1483
- const value = copy[key];
1484
- return value === undefined ? fallback : value;
1485
- };
1486
-
1487
- function buildCopyHtml() {
1488
- let html = '';
1489
- const brandText = copy.brandName || '';
1490
- const subtitleText = copyText('subtitle', preset.subtitle);
1491
- if (brandText) html += `<span class="hj-progress-name">${brandText}</span>`;
1492
- if (subtitleText) html += `<span class="hj-progress-subtitle">${subtitleText}</span>`;
1493
- return html;
1494
- }
1495
-
1496
- const copyLayer = document.createElement('div');
1497
- copyLayer.className = 'hj-progress-copy';
1498
- const renderCopy = () => {
1499
- if (copyEnabled) copyLayer.innerHTML = buildCopyHtml();
1500
- };
1501
- renderCopy();
1502
-
1503
- if (copy.background) {
1504
- root.style.setProperty('--hj-progress-copy-bg', copy.background);
1505
- root.style.setProperty('--hj-progress-copy-pad', '6px 12px');
1506
- }
1507
- if (copy.color) root.style.setProperty('--hj-progress-copy-color', copy.color);
1508
-
1509
- const valueElement = document.createElement('div');
1510
- valueElement.className = 'hj-progress-value';
1511
- valueElement.setAttribute('aria-hidden', 'true');
1512
-
1513
- const dragLabel = document.createElement('span');
1514
- dragLabel.className = 'hj-progress-drag-label';
1515
- dragLabel.setAttribute('aria-hidden', 'true');
1516
- dragLabel.textContent = copy.dragLabel;
1517
-
1518
- root.appendChild(canvas);
1519
- if (copyEnabled) root.appendChild(copyLayer);
1520
- root.appendChild(valueElement);
1521
- root.appendChild(dragLabel);
1522
- container.appendChild(root);
1523
-
1524
- const emitter = createEmitter();
1525
- let paused = merged.respectReducedMotion && prefersReducedMotion();
1526
-
1527
- const applySize = () => {
1528
- root.style.width = parseSize(merged.width);
1529
- root.style.height = parseSize(merged.height);
1530
- const vars = merged.cssVars || {};
1531
- for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
1532
- };
1744
+ // Slot containers are always present but transparent by default; they only
1745
+ // provide geometry, never typography/padding/background (docs: 插槽 CSS 约定).
1746
+ const fillContent = (layer, content) => {
1747
+ layer.innerHTML = '';
1748
+ if (content == null) return;
1749
+ if (typeof content === 'string') {
1750
+ layer.innerHTML = content;
1751
+ return;
1752
+ }
1753
+ const nodes = Array.isArray(content) ? content : [content];
1754
+ for (const node of nodes) {
1755
+ if (node && typeof node.nodeType === 'number') layer.appendChild(node);
1756
+ }
1757
+ };
1758
+
1759
+ const textLayer = document.createElement('div');
1760
+ textLayer.className = 'hj-progress-text hj-progress-copy';
1761
+
1762
+ const visualLayer = document.createElement('div');
1763
+ visualLayer.className = 'hj-progress-visual';
1764
+
1765
+ fillContent(textLayer, options.text);
1766
+ fillContent(visualLayer, options.colorContent);
1767
+
1768
+ const valueElement = merged.showValue === false ? null : document.createElement('div');
1769
+ if (valueElement) {
1770
+ valueElement.className = 'hj-progress-value';
1771
+ valueElement.setAttribute('aria-hidden', 'true');
1772
+ }
1773
+
1774
+ root.appendChild(canvas);
1775
+ root.appendChild(textLayer);
1776
+ root.appendChild(visualLayer);
1777
+ if (valueElement) root.appendChild(valueElement);
1778
+ container.appendChild(root);
1779
+
1780
+ const emitter = createEmitter();
1781
+ let paused = merged.respectReducedMotion && prefersReducedMotion();
1782
+ let disposed = false;
1783
+
1784
+ const applySize = () => {
1785
+ root.style.width = parseSize(merged.width);
1786
+ root.style.height = parseSize(merged.height);
1787
+ const vars = merged.cssVars || {};
1788
+ for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
1789
+ if (merged.textRatio !== undefined) {
1790
+ root.style.setProperty('--hj-text-width', `${merged.textRatio}%`);
1791
+ }
1792
+ };
1533
1793
  applySize();
1534
1794
 
1535
- const controller = new ProgressCapsuleController({
1536
- root,
1537
- canvas,
1538
- valueElement,
1539
- preset,
1540
- emitter,
1541
- options: merged,
1542
- copy
1543
- });
1795
+ const controller = new ProgressCapsuleController({
1796
+ root,
1797
+ canvas,
1798
+ valueElement,
1799
+ preset,
1800
+ emitter,
1801
+ options: { ...merged, draggable: effectiveDraggable, keyboard: effectiveKeyboard },
1802
+ copy,
1803
+ dirty
1804
+ });
1544
1805
 
1545
1806
  let overlay = null;
1546
1807
  if (merged.renderer !== 'canvas2d') {
@@ -1551,7 +1812,13 @@ function createProgressCapsule(container, options = {}) {
1551
1812
  getProgress: () => controller.value
1552
1813
  });
1553
1814
  }
1554
- if (overlay) controller.webglActive = true;
1815
+ if (overlay) controller.webglActive = true;
1816
+ else if (merged.renderer !== 'canvas2d') {
1817
+ nextTick(() => {
1818
+ if (disposed) return;
1819
+ emitter.emit('error', { message: 'WebGL2 unavailable, using Canvas2D fallback' });
1820
+ });
1821
+ }
1555
1822
 
1556
1823
  const visibility = createVisibilityGuard(root);
1557
1824
  let flowTime = 0;
@@ -1567,15 +1834,14 @@ function createProgressCapsule(container, options = {}) {
1567
1834
  () => paused
1568
1835
  );
1569
1836
 
1570
- emitter.emit('ready', { preset: { ...preset } });
1837
+ nextTick(() => {
1838
+ if (disposed) return;
1839
+ emitter.emit('ready', { preset: { ...preset } });
1840
+ });
1571
1841
 
1572
- const syncDom = () => {
1573
- root.setAttribute(
1574
- 'aria-label',
1575
- interpolate(copy.progressAria, { brand: copy.brandName, code: preset.code, name: preset.name })
1576
- );
1577
- renderCopy();
1578
- };
1842
+ const syncDom = () => {
1843
+ updateAria();
1844
+ };
1579
1845
 
1580
1846
  return {
1581
1847
  element: root,
@@ -1594,31 +1860,84 @@ function createProgressCapsule(container, options = {}) {
1594
1860
  controller.setRange(min, max);
1595
1861
  return this;
1596
1862
  },
1597
- setPreset(ref) {
1598
- const next = getPreset('progress', ref);
1599
- Object.assign(preset, next);
1600
- controller.preset = next;
1601
- controller.profile = next.edgeStyle === 'tide'
1602
- ? FLOW_PROFILES.tide
1603
- : (FLOW_PROFILES[next.id] || FLOW_PROFILES['visual-training']);
1604
- controller.flowTime = stringSeed(next.id) * 31;
1605
- controller.seed = stringSeed(`${next.id}-reference`) * Math.PI * 2;
1606
- syncDom();
1607
- if (overlay) {
1608
- overlay.dispose();
1609
- overlay = attachProgressFlowOverlay({ root, canvas, preset: next, getProgress: () => controller.value });
1610
- }
1611
- controller.webglActive = Boolean(overlay);
1612
- controller.resizeCanvas();
1613
- emitter.emit('presetchange', { preset: { ...next } });
1614
- return this;
1615
- },
1616
- setColors(colors) {
1617
- if (!Array.isArray(colors) || colors.length !== 4) return this;
1618
- controller.setColors(colors);
1619
- if (overlay) overlay.setColors(colors);
1620
- controller.resizeCanvas();
1621
- return this;
1863
+ setPreset(ref) {
1864
+ const next = getPreset('progress', ref);
1865
+ dirty.preset = true;
1866
+ Object.assign(preset, next);
1867
+ const nextColors = preset.colors.map(normalizeColor);
1868
+ if (nextColors.every(Boolean)) preset.colors = nextColors;
1869
+ controller.preset = preset;
1870
+ controller.profile = next.edgeStyle === 'tide'
1871
+ ? FLOW_PROFILES.tide
1872
+ : (FLOW_PROFILES[next.id] || FLOW_PROFILES['visual-training']);
1873
+ controller.flowTime = stringSeed(next.id) * 31;
1874
+ controller.seed = stringSeed(`${next.id}-reference`) * Math.PI * 2;
1875
+ syncDom();
1876
+ if (overlay) {
1877
+ overlay.dispose();
1878
+ overlay = attachProgressFlowOverlay({ root, canvas, preset, getProgress: () => controller.value });
1879
+ }
1880
+ controller.webglActive = Boolean(overlay);
1881
+ controller.resizeCanvas();
1882
+ emitter.emit('presetchange', { preset: { ...preset } });
1883
+ return this;
1884
+ },
1885
+ setEdgeStyle(edgeStyle) {
1886
+ const value = String(edgeStyle || '').toLowerCase();
1887
+ if (value !== 'flow' && value !== 'tide') return this;
1888
+ dirty.edgeStyle = true;
1889
+ preset.edgeStyle = value;
1890
+ controller.profile = value === 'tide'
1891
+ ? FLOW_PROFILES.tide
1892
+ : (FLOW_PROFILES[preset.id] || FLOW_PROFILES['visual-training']);
1893
+ if (overlay) {
1894
+ overlay.dispose();
1895
+ overlay = attachProgressFlowOverlay({ root, canvas, preset, getProgress: () => controller.value });
1896
+ }
1897
+ controller.webglActive = Boolean(overlay);
1898
+ controller.resizeCanvas();
1899
+ return this;
1900
+ },
1901
+ setText(content) {
1902
+ fillContent(textLayer, content);
1903
+ return this;
1904
+ },
1905
+ setColorContent(content) {
1906
+ fillContent(visualLayer, content);
1907
+ return this;
1908
+ },
1909
+ setTextRatio(ratio) {
1910
+ const value = Number(ratio);
1911
+ if (!Number.isFinite(value) || value < 0 || value > 100) return this;
1912
+ dirty.textRatio = true;
1913
+ merged.textRatio = value;
1914
+ root.style.setProperty('--hj-text-width', `${value}%`);
1915
+ return this;
1916
+ },
1917
+ setCssVars(vars) {
1918
+ if (!vars || typeof vars !== 'object') return this;
1919
+ dirty.cssVars = true;
1920
+ merged.cssVars = { ...(merged.cssVars || {}), ...vars };
1921
+ for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
1922
+ return this;
1923
+ },
1924
+ setLabel(label) {
1925
+ customLabel = typeof label === 'string' && label ? label : null;
1926
+ updateAria();
1927
+ return this;
1928
+ },
1929
+ setValueSuffix(suffix) {
1930
+ controller.setValueSuffix(suffix);
1931
+ return this;
1932
+ },
1933
+ setColors(colors) {
1934
+ const next = Array.isArray(colors) ? colors.map(normalizeColor) : [];
1935
+ if (next.length !== 4 || next.some((color) => !color)) return this;
1936
+ dirty.colors = true;
1937
+ controller.setColors(next);
1938
+ if (overlay) overlay.setColors(next);
1939
+ controller.resizeCanvas();
1940
+ return this;
1622
1941
  },
1623
1942
  setSize(width, height) {
1624
1943
  if (width !== undefined) merged.width = width;
@@ -1645,14 +1964,18 @@ function createProgressCapsule(container, options = {}) {
1645
1964
  controller.resizeCanvas();
1646
1965
  return this;
1647
1966
  },
1648
- dispose() {
1649
- unsubscribe();
1967
+ dispose() {
1968
+ disposed = true;
1969
+ unsubscribe();
1650
1970
  visibility.dispose();
1651
1971
  if (overlay) overlay.dispose();
1652
- controller.dispose();
1653
- root.remove();
1654
- }
1655
- };
1972
+ controller.dispose();
1973
+ root.remove();
1974
+ },
1975
+ textRatio: merged.textRatio,
1976
+ cssVars: merged.cssVars,
1977
+ dirty
1978
+ };
1656
1979
  }
1657
1980
 
1658
1981
  export { ProgressCapsuleController, createProgressCapsule };