@5even7/dlc-ui 0.2.0 → 0.2.2

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/vue3.mjs CHANGED
@@ -184,60 +184,266 @@ function getPreset(kind, ref) {
184
184
  return found;
185
185
  }
186
186
 
187
- const DEFAULTS = {
188
- capsule: {
189
- width: '100%',
190
- height: 160,
187
+ const DEFAULTS = {
188
+ capsule: {
189
+ width: '100%',
190
+ height: 160,
191
191
  quality: 'auto',
192
192
  renderer: 'auto',
193
193
  respectReducedMotion: true,
194
194
  mouseColor: true,
195
- showCopy: true
196
- },
197
- progress: {
198
- width: 454,
199
- height: 104,
200
- min: 0,
195
+ textRatio: 39
196
+ },
197
+ progress: {
198
+ width: 454,
199
+ height: 104,
200
+ min: 0,
201
201
  max: 100,
202
- draggable: true,
203
- keyboard: true,
204
- edgeStyle: 'flow',
205
- quality: 'auto',
206
- renderer: 'auto',
207
- showCopy: true,
208
- respectReducedMotion: true
209
- }
202
+ draggable: true,
203
+ keyboard: true,
204
+ disabled: false,
205
+ valueSuffix: '%',
206
+ edgeStyle: 'flow',
207
+ quality: 'auto',
208
+ renderer: 'auto',
209
+ textRatio: 54,
210
+ showValue: true,
211
+ respectReducedMotion: true
212
+ }
210
213
  };
211
214
 
212
- /**
213
- * All user-facing copy lives here so wording/brand changes never require a
214
- * global search. Templates use {brand} {code} {name} placeholders.
215
- */
216
- const COPY = {
217
- brandName: '画境观屿',
218
- dragLabel: 'DRAG',
219
- valueSuffix: '%',
215
+ /**
216
+ * Internal accessibility labels and small UI text. Since the copy API was
217
+ * removed (slots own the text area), these are no longer user-overridable.
218
+ * Templates use {brand} {code} {name} placeholders.
219
+ */
220
+ const COPY = {
221
+ brandName: '画境观屿',
222
+ valueSuffix: '%',
220
223
  progressAria: '{brand} {code} 加载进度',
221
224
  capsuleAria: '打开 {name} 沉浸预览'
222
225
  };
223
226
 
224
- function hexToRgb01$1(hex) {
225
- const normalized = hex.replace('#', '');
226
- const value = Number.parseInt(normalized, 16);
227
- return [
228
- ((value >> 16) & 255) / 255,
229
- ((value >> 8) & 255) / 255,
230
- (value & 255) / 255
231
- ];
232
- }
233
-
234
- function hexToRgba(hex, alpha = 1) {
235
- const normalized = hex.replace('#', '');
236
- const value = Number.parseInt(normalized, 16);
237
- const red = (value >> 16) & 255;
238
- const green = (value >> 8) & 255;
239
- const blue = value & 255;
240
- return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
227
+ /**
228
+ * Color helpers. `normalizeColor` converts any supported CSS color
229
+ * (hex / named / rgb() / rgba()) into a 6-digit hex string, so renderers can
230
+ * keep working with #rrggbb only. rgba() alpha is intentionally dropped:
231
+ * the WebGL shader has no per-color alpha channel, and the component is
232
+ * opaque by design use component-level `opacity` for transparency.
233
+ */
234
+
235
+ const NAMED_COLORS = {
236
+ aliceblue: '#f0f8ff',
237
+ antiquewhite: '#faebd7',
238
+ aqua: '#00ffff',
239
+ aquamarine: '#7fffd4',
240
+ azure: '#f0ffff',
241
+ beige: '#f5f5dc',
242
+ bisque: '#ffe4c4',
243
+ black: '#000000',
244
+ blanchedalmond: '#ffebcd',
245
+ blue: '#0000ff',
246
+ blueviolet: '#8a2be2',
247
+ brown: '#a52a2a',
248
+ burlywood: '#deb887',
249
+ cadetblue: '#5f9ea0',
250
+ chartreuse: '#7fff00',
251
+ chocolate: '#d2691e',
252
+ coral: '#ff7f50',
253
+ cornflowerblue: '#6495ed',
254
+ cornsilk: '#fff8dc',
255
+ crimson: '#dc143c',
256
+ cyan: '#00ffff',
257
+ darkblue: '#00008b',
258
+ darkcyan: '#008b8b',
259
+ darkgoldenrod: '#b8860b',
260
+ darkgray: '#a9a9a9',
261
+ darkgreen: '#006400',
262
+ darkgrey: '#a9a9a9',
263
+ darkkhaki: '#bdb76b',
264
+ darkmagenta: '#8b008b',
265
+ darkolivegreen: '#556b2f',
266
+ darkorange: '#ff8c00',
267
+ darkorchid: '#9932cc',
268
+ darkred: '#8b0000',
269
+ darksalmon: '#e9967a',
270
+ darkseagreen: '#8fbc8f',
271
+ darkslateblue: '#483d8b',
272
+ darkslategray: '#2f4f4f',
273
+ darkslategrey: '#2f4f4f',
274
+ darkturquoise: '#00ced1',
275
+ darkviolet: '#9400d3',
276
+ deeppink: '#ff1493',
277
+ deepskyblue: '#00bfff',
278
+ dimgray: '#696969',
279
+ dimgrey: '#696969',
280
+ dodgerblue: '#1e90ff',
281
+ firebrick: '#b22222',
282
+ floralwhite: '#fffaf0',
283
+ forestgreen: '#228b22',
284
+ fuchsia: '#ff00ff',
285
+ gainsboro: '#dcdcdc',
286
+ ghostwhite: '#f8f8ff',
287
+ gold: '#ffd700',
288
+ goldenrod: '#daa520',
289
+ gray: '#808080',
290
+ green: '#008000',
291
+ greenyellow: '#adff2f',
292
+ grey: '#808080',
293
+ honeydew: '#f0fff0',
294
+ hotpink: '#ff69b4',
295
+ indianred: '#cd5c5c',
296
+ indigo: '#4b0082',
297
+ ivory: '#fffff0',
298
+ khaki: '#f0e68c',
299
+ lavender: '#e6e6fa',
300
+ lavenderblush: '#fff0f5',
301
+ lawngreen: '#7cfc00',
302
+ lemonchiffon: '#fffacd',
303
+ lightblue: '#add8e6',
304
+ lightcoral: '#f08080',
305
+ lightcyan: '#e0ffff',
306
+ lightgoldenrodyellow: '#fafad2',
307
+ lightgray: '#d3d3d3',
308
+ lightgreen: '#90ee90',
309
+ lightgrey: '#d3d3d3',
310
+ lightpink: '#ffb6c1',
311
+ lightsalmon: '#ffa07a',
312
+ lightseagreen: '#20b2aa',
313
+ lightskyblue: '#87cefa',
314
+ lightslategray: '#778899',
315
+ lightslategrey: '#778899',
316
+ lightsteelblue: '#b0c4de',
317
+ lightyellow: '#ffffe0',
318
+ lime: '#00ff00',
319
+ limegreen: '#32cd32',
320
+ linen: '#faf0e6',
321
+ magenta: '#ff00ff',
322
+ maroon: '#800000',
323
+ mediumaquamarine: '#66cdaa',
324
+ mediumblue: '#0000cd',
325
+ mediumorchid: '#ba55d3',
326
+ mediumpurple: '#9370db',
327
+ mediumseagreen: '#3cb371',
328
+ mediumslateblue: '#7b68ee',
329
+ mediumspringgreen: '#00fa9a',
330
+ mediumturquoise: '#48d1cc',
331
+ mediumvioletred: '#c71585',
332
+ midnightblue: '#191970',
333
+ mintcream: '#f5fffa',
334
+ mistyrose: '#ffe4e1',
335
+ moccasin: '#ffe4b5',
336
+ navajowhite: '#ffdead',
337
+ navy: '#000080',
338
+ oldlace: '#fdf5e6',
339
+ olive: '#808000',
340
+ olivedrab: '#6b8e23',
341
+ orange: '#ffa500',
342
+ orangered: '#ff4500',
343
+ orchid: '#da70d6',
344
+ palegoldenrod: '#eee8aa',
345
+ palegreen: '#98fb98',
346
+ paleturquoise: '#afeeee',
347
+ palevioletred: '#db7093',
348
+ papayawhip: '#ffefd5',
349
+ peachpuff: '#ffdab9',
350
+ peru: '#cd853f',
351
+ pink: '#ffc0cb',
352
+ plum: '#dda0dd',
353
+ powderblue: '#b0e0e6',
354
+ purple: '#800080',
355
+ rebeccapurple: '#663399',
356
+ red: '#ff0000',
357
+ rosybrown: '#bc8f8f',
358
+ royalblue: '#4169e1',
359
+ saddlebrown: '#8b4513',
360
+ salmon: '#fa8072',
361
+ sandybrown: '#f4a460',
362
+ seagreen: '#2e8b57',
363
+ seashell: '#fff5ee',
364
+ sienna: '#a0522d',
365
+ silver: '#c0c0c0',
366
+ skyblue: '#87ceeb',
367
+ slateblue: '#6a5acd',
368
+ slategray: '#708090',
369
+ slategrey: '#708090',
370
+ snow: '#fffafa',
371
+ springgreen: '#00ff7f',
372
+ steelblue: '#4682b4',
373
+ tan: '#d2b48c',
374
+ teal: '#008080',
375
+ thistle: '#d8bfd8',
376
+ tomato: '#ff6347',
377
+ turquoise: '#40e0d0',
378
+ violet: '#ee82ee',
379
+ wheat: '#f5deb3',
380
+ white: '#ffffff',
381
+ whitesmoke: '#f5f5f5',
382
+ yellow: '#ffff00',
383
+ yellowgreen: '#9acd32'
384
+ };
385
+
386
+ function clampChannel(value) {
387
+ return Math.min(255, Math.max(0, Math.round(value)));
388
+ }
389
+
390
+ function normalizeRgbArgs(args) {
391
+ const parts = args.split(/[,\s/]+/).filter(Boolean);
392
+ if (parts.length < 3) return null;
393
+ const to255 = (value) => {
394
+ if (typeof value !== 'string' || !value) return null;
395
+ if (value.endsWith('%')) return clampChannel((parseFloat(value) / 100) * 255);
396
+ const number = Number(value);
397
+ return Number.isFinite(number) ? clampChannel(number) : null;
398
+ };
399
+ const red = to255(parts[0]);
400
+ const green = to255(parts[1]);
401
+ const blue = to255(parts[2]);
402
+ if (red === null || green === null || blue === null) return null;
403
+ return `#${[red, green, blue].map((n) => n.toString(16).padStart(2, '0')).join('')}`;
404
+ }
405
+
406
+ /**
407
+ * Convert any supported CSS color into `#rrggbb`. Returns null when the
408
+ * value cannot be parsed. Supports: #rgb / #rrggbb (case-insensitive),
409
+ * CSS named colors, rgb() / rgba() with comma or modern space syntax,
410
+ * including percentages. Alpha in rgba() is ignored.
411
+ */
412
+ function normalizeColor(value) {
413
+ if (typeof value !== 'string') return null;
414
+ const input = value.trim();
415
+ if (!input) return null;
416
+ const lower = input.toLowerCase();
417
+ if (NAMED_COLORS[lower]) return NAMED_COLORS[lower];
418
+ if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(input)) {
419
+ const hex = input.slice(1).toLowerCase();
420
+ if (hex.length === 3) return `#${hex.split('').map((c) => c + c).join('')}`;
421
+ return `#${hex}`;
422
+ }
423
+ const match = input.match(/^rgba?\((.*)\)$/i);
424
+ if (match) return normalizeRgbArgs(match[1]);
425
+ return null;
426
+ }
427
+
428
+ function hexToRgb01$1(color) {
429
+ const hex = normalizeColor(color);
430
+ if (!hex) return [0, 0, 0];
431
+ const value = Number.parseInt(hex.slice(1), 16);
432
+ return [
433
+ ((value >> 16) & 255) / 255,
434
+ ((value >> 8) & 255) / 255,
435
+ (value & 255) / 255
436
+ ];
437
+ }
438
+
439
+ function hexToRgba(color, alpha = 1) {
440
+ const hex = normalizeColor(color);
441
+ if (!hex) return 'rgba(0, 0, 0, 0)';
442
+ const value = Number.parseInt(hex.slice(1), 16);
443
+ const red = (value >> 16) & 255;
444
+ const green = (value >> 8) & 255;
445
+ const blue = value & 255;
446
+ return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
241
447
  }
242
448
 
243
449
  const VERTEX_SHADER$1 = `#version 300 es
@@ -465,12 +671,31 @@ class CosmicRenderer {
465
671
  this.timeOffset = preset.seed * 0.73;
466
672
  }
467
673
 
468
- setDprCap(cap) {
469
- this.options.dprCap = cap;
470
- this.resize();
471
- }
472
-
473
- randomize() {
674
+ setDprCap(cap) {
675
+ this.options.dprCap = cap;
676
+ this.resize();
677
+ }
678
+
679
+ setMouseColor(enabled) {
680
+ this.options.mouseColor = enabled !== false;
681
+ if (this.options.mouseColor) {
682
+ if (this.onPointerMove) return this;
683
+ this.#bindEvents();
684
+ return this;
685
+ }
686
+ if (this.onPointerMove) {
687
+ const target = this.eventTarget;
688
+ target.removeEventListener('pointermove', this.onPointerMove);
689
+ target.removeEventListener('pointerdown', this.onPointerMove);
690
+ target.removeEventListener('pointerleave', this.onPointerLeave);
691
+ this.onPointerMove = null;
692
+ this.onPointerLeave = null;
693
+ }
694
+ this.motionTarget = 0;
695
+ return this;
696
+ }
697
+
698
+ randomize() {
474
699
  this.preset.seed = Math.random() * 100;
475
700
  this.timeOffset = Math.random() * 40;
476
701
  }
@@ -550,10 +775,12 @@ class FallbackRenderer {
550
775
  }
551
776
  }
552
777
 
553
- drawNebula(time) {
554
- const ctx = this.context;
555
- const { width, height } = this.canvas;
556
- const gradient = ctx.createLinearGradient(0, 0, width, height);
778
+ drawNebula(time) {
779
+ const ctx = this.context;
780
+ const { width, height } = this.canvas;
781
+ const t = time * (this.preset.speed || 1);
782
+ const phase = (this.preset.seed || 0) * 0.7;
783
+ const gradient = ctx.createLinearGradient(0, 0, width, height);
557
784
  gradient.addColorStop(0, this.preset.colors[0]);
558
785
  gradient.addColorStop(0.38, this.preset.colors[1]);
559
786
  gradient.addColorStop(0.72, this.preset.colors[2]);
@@ -561,10 +788,10 @@ class FallbackRenderer {
561
788
  ctx.fillStyle = gradient;
562
789
  ctx.fillRect(0, 0, width, height);
563
790
 
564
- ctx.globalCompositeOperation = 'screen';
565
- for (let index = 0; index < 6; index += 1) {
566
- const x = (0.5 + 0.45 * Math.sin(time * 0.32 + index * 1.7)) * width;
567
- const y = (0.5 + 0.4 * Math.cos(time * 0.25 + index)) * height;
791
+ ctx.globalCompositeOperation = 'screen';
792
+ for (let index = 0; index < 6; index += 1) {
793
+ const x = (0.5 + 0.45 * Math.sin(t * 0.32 + index * 1.7 + phase)) * width;
794
+ const y = (0.5 + 0.4 * Math.cos(t * 0.25 + index + phase)) * height;
568
795
  const radius = Math.max(width, height) * (0.08 + (index % 3) * 0.04);
569
796
  const glow = ctx.createRadialGradient(x, y, 0, x, y, radius);
570
797
  glow.addColorStop(0, rgb(this.preset.colors[(index + 1) % 4], 0.24));
@@ -585,8 +812,11 @@ class FallbackRenderer {
585
812
  this.preset = preset;
586
813
  }
587
814
 
588
- randomize() {}
589
- dispose() {}
815
+ randomize() {
816
+ if (this.preset) this.preset.seed = Math.random() * 100;
817
+ }
818
+ setMouseColor() {}
819
+ dispose() {}
590
820
  }
591
821
 
592
822
  /**
@@ -686,78 +916,105 @@ function createVisibilityGuard(element) {
686
916
  };
687
917
  }
688
918
 
689
- function prefersReducedMotion$1() {
919
+ /**
920
+ * Run a callback asynchronously, right after the current task and before the
921
+ * next paint. Used for mount-time events (ready / error) so listeners that
922
+ * are attached after create() still receive them. Falls back for legacy
923
+ * browsers that lack queueMicrotask / Promise.
924
+ */
925
+ function nextTick(fn) {
926
+ if (typeof queueMicrotask === 'function') {
927
+ queueMicrotask(fn);
928
+ } else if (typeof Promise !== 'undefined' && typeof Promise.resolve === 'function') {
929
+ Promise.resolve().then(fn);
930
+ } else {
931
+ setTimeout(fn, 0);
932
+ }
933
+ }
934
+
935
+ function prefersReducedMotion$2() {
690
936
  return typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;
691
937
  }
692
938
 
693
939
  /**
694
- * Mount a cosmic (nebula) capsule into `container`.
695
- *
696
- * Options: preset (id/code/name or object), width, height (number=px or
697
- * string with px/%/vw/vh/em/rem), colors, seed, speed, quality,
698
- * respectReducedMotion, copy, cssVars.
699
- */
700
- function createCapsule(container, options = {}) {
940
+ * Mount a cosmic (nebula) capsule into `container`.
941
+ *
942
+ * Options: preset (literary name), width, height (number=px or string with
943
+ * px/%/vw/vh/em/rem), colors, seed, speed, textRatio (0-100, text region
944
+ * width in percent), text (HTML string or DOM nodes for the text slot),
945
+ * colorContent (HTML string or DOM nodes for the color slot), quality,
946
+ * renderer, respectReducedMotion, mouseColor, cssVars.
947
+ */
948
+ function createCapsule(container, options = {}) {
701
949
  if (!container || typeof container.appendChild !== 'function') {
702
950
  throw new Error('createCapsule: container element is required');
703
951
  }
704
952
 
705
953
  const preset = { ...getPreset('capsule', options.preset ?? '初光') };
706
- const merged = normalizeOptions(DEFAULTS.capsule, preset, options);
707
- const copy = { ...COPY, ...(merged.copy || {}) };
708
-
709
- const root = document.createElement('div');
710
- root.className = 'hj-capsule-root hj-capsule-cosmic';
711
- root.dataset.group = preset.group;
712
- root.dataset.mode = 'nebula';
713
- root.dataset.theme = preset.theme || 'light';
714
- root.setAttribute('aria-label', copy.capsuleAria.replace('{name}', preset.name));
715
-
716
- const copyEnabled = copy.enabled !== false && merged.showCopy !== false;
717
-
718
- const copyText = (key, fallback) => {
719
- const value = copy[key];
720
- return value === undefined ? fallback : value;
721
- };
722
-
723
- function buildCopyHtml() {
724
- const codeText = copyText('code', preset.code);
725
- const nameText = copyText('name', preset.name);
726
- const subtitleText = copyText('subtitle', preset.subtitle);
727
- const stateText = copyText('state', 'LIVE COSMIC STUDY');
728
- let html = '';
729
- if (codeText) html += `<span class="hj-capsule-code">${codeText}</span>`;
730
- if (nameText) html += `<span class="hj-capsule-name">${nameText}</span>`;
731
- if (subtitleText) html += `<span class="hj-capsule-brand">${subtitleText}</span>`;
732
- if (stateText) html += `<span class="hj-capsule-state">${stateText}</span>`;
733
- return html;
734
- }
735
-
736
- const copyLayer = document.createElement('div');
737
- copyLayer.className = 'hj-capsule-copy';
738
- const renderCopy = () => {
739
- if (copyEnabled) copyLayer.innerHTML = buildCopyHtml();
740
- };
741
- renderCopy();
742
-
743
- const isHexColor = (value) => typeof value === 'string' && /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(value);
744
- if (copy.background) {
745
- root.style.setProperty('--hj-copy-bg', isHexColor(copy.background)
746
- ? `linear-gradient(90deg, ${copy.background} 0%, ${copy.background} 74%, ${hexToRgba(copy.background, 0.97)} 84%, ${hexToRgba(copy.background, 0)} 100%)`
747
- : copy.background);
748
- }
749
- if (copy.color) root.style.setProperty('--hj-copy-color', copy.color);
750
-
751
- const canvas = document.createElement('canvas');
752
- canvas.className = 'hj-capsule-canvas';
753
- canvas.setAttribute('aria-hidden', 'true');
754
-
755
- if (copyEnabled) root.appendChild(copyLayer);
756
- root.appendChild(canvas);
757
- container.appendChild(root);
758
-
759
- const emitter = createEmitter();
760
- let paused = merged.respectReducedMotion && prefersReducedMotion$1();
954
+ const merged = normalizeOptions(DEFAULTS.capsule, preset, options);
955
+ const normalizedColors = merged.colors.map(normalizeColor);
956
+ if (normalizedColors.every(Boolean)) preset.colors = normalizedColors;
957
+ preset.seed = merged.seed;
958
+ preset.speed = merged.speed;
959
+ let customLabel = typeof options.label === 'string' && options.label ? options.label : null;
960
+
961
+ const root = document.createElement('div');
962
+ root.className = 'hj-capsule-root hj-capsule-cosmic';
963
+ root.dataset.group = preset.group;
964
+ root.dataset.mode = 'nebula';
965
+ root.dataset.theme = preset.theme || 'light';
966
+ root.setAttribute('role', 'img');
967
+ const updateAria = () => {
968
+ root.setAttribute('aria-label', customLabel || COPY.capsuleAria.replace('{name}', preset.name));
969
+ };
970
+ updateAria();
971
+
972
+ // Slot containers are always present but transparent by default: without
973
+ // content they are invisible, so "no slot" behaves like the old
974
+ // showCopy=false. They only provide geometry — no typography, padding or
975
+ // background is imposed on user content (see docs: 插槽 CSS 约定).
976
+ const fillContent = (layer, content) => {
977
+ layer.innerHTML = '';
978
+ if (content == null) return;
979
+ if (typeof content === 'string') {
980
+ layer.innerHTML = content;
981
+ return;
982
+ }
983
+ const nodes = Array.isArray(content) ? content : [content];
984
+ for (const node of nodes) {
985
+ if (node && typeof node.nodeType === 'number') layer.appendChild(node);
986
+ }
987
+ };
988
+
989
+ const textLayer = document.createElement('div');
990
+ textLayer.className = 'hj-capsule-text hj-capsule-copy';
991
+
992
+ const visualLayer = document.createElement('div');
993
+ visualLayer.className = 'hj-capsule-visual';
994
+
995
+ fillContent(textLayer, options.text);
996
+ fillContent(visualLayer, options.colorContent);
997
+
998
+ const canvas = document.createElement('canvas');
999
+ canvas.className = 'hj-capsule-canvas';
1000
+ canvas.setAttribute('aria-hidden', 'true');
1001
+
1002
+ root.appendChild(textLayer);
1003
+ root.appendChild(visualLayer);
1004
+ root.appendChild(canvas);
1005
+ container.appendChild(root);
1006
+
1007
+ const emitter = createEmitter();
1008
+ let paused = merged.respectReducedMotion && prefersReducedMotion$2();
1009
+ let disposed = false;
1010
+ const dirty = {
1011
+ preset: false,
1012
+ seed: false,
1013
+ speed: false,
1014
+ colors: false,
1015
+ textRatio: false,
1016
+ cssVars: false
1017
+ };
761
1018
 
762
1019
  let renderer;
763
1020
  const useWebgl = merged.renderer !== 'canvas2d';
@@ -766,19 +1023,25 @@ function createCapsule(container, options = {}) {
766
1023
  renderer = new CosmicRenderer(canvas, merged, { dprCap: dprCapFor(merged.quality) });
767
1024
  } catch (error) {
768
1025
  renderer = new FallbackRenderer(canvas, merged);
769
- emitter.emit('error', { message: String(error && error.message ? error.message : error) });
1026
+ nextTick(() => {
1027
+ if (disposed) return;
1028
+ emitter.emit('error', { message: String(error && error.message ? error.message : error) });
1029
+ });
770
1030
  }
771
1031
  } else {
772
1032
  renderer = new FallbackRenderer(canvas, merged);
773
1033
  }
774
1034
 
775
- const applySize = () => {
776
- root.style.width = parseSize(merged.width);
777
- root.style.height = parseSize(merged.height);
778
- const vars = merged.cssVars || {};
779
- for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
780
- renderer.resize();
781
- };
1035
+ const applySize = () => {
1036
+ root.style.width = parseSize(merged.width);
1037
+ root.style.height = parseSize(merged.height);
1038
+ const vars = merged.cssVars || {};
1039
+ for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
1040
+ if (merged.textRatio !== undefined) {
1041
+ root.style.setProperty('--hj-text-width', `${merged.textRatio}%`);
1042
+ }
1043
+ renderer.resize();
1044
+ };
782
1045
  applySize();
783
1046
 
784
1047
  const resizeObserver = typeof ResizeObserver !== 'undefined'
@@ -807,38 +1070,47 @@ function createCapsule(container, options = {}) {
807
1070
  () => paused
808
1071
  );
809
1072
 
810
- emitter.emit('ready', { preset: { ...preset } });
1073
+ nextTick(() => {
1074
+ if (disposed) return;
1075
+ emitter.emit('ready', { preset: { ...preset } });
1076
+ });
811
1077
 
812
- const syncCopy = () => {
813
- root.dataset.mode = 'nebula';
814
- root.dataset.theme = preset.theme || 'light';
815
- renderCopy();
816
- };
817
-
818
- return {
1078
+ const syncTheme = () => {
1079
+ root.dataset.mode = 'nebula';
1080
+ root.dataset.theme = preset.theme || 'light';
1081
+ updateAria();
1082
+ };
1083
+
1084
+ return {
819
1085
  element: root,
820
1086
  canvas,
821
1087
  preset,
822
1088
  on: emitter.on,
823
1089
  off: emitter.off,
824
- setPreset(ref) {
825
- const next = getPreset('capsule', ref);
826
- Object.assign(preset, next);
827
- renderer.setPreset(next);
828
- syncCopy();
829
- renderer.resize();
830
- emitter.emit('presetchange', { preset: { ...next } });
831
- return this;
1090
+ setPreset(ref) {
1091
+ const next = getPreset('capsule', ref);
1092
+ dirty.preset = true;
1093
+ Object.assign(preset, next);
1094
+ const nextColors = preset.colors.map(normalizeColor);
1095
+ if (nextColors.every(Boolean)) preset.colors = nextColors;
1096
+ renderer.setPreset({ ...preset });
1097
+ syncTheme();
1098
+ renderer.resize();
1099
+ emitter.emit('presetchange', { preset: { ...next } });
1100
+ return this;
832
1101
  },
833
1102
  setColors(colors) {
834
- if (!Array.isArray(colors) || colors.length !== 4) return this;
835
- preset.colors = colors;
836
- renderer.setPreset({ ...preset, colors });
1103
+ const next = Array.isArray(colors) ? colors.map(normalizeColor) : [];
1104
+ if (next.length !== 4 || next.some((color) => !color)) return this;
1105
+ dirty.colors = true;
1106
+ preset.colors = next;
1107
+ renderer.setPreset({ ...preset, colors: next });
837
1108
  return this;
838
1109
  },
839
1110
  setSeed(seed) {
840
1111
  const value = Number(seed);
841
1112
  if (!Number.isFinite(value)) return this;
1113
+ dirty.seed = true;
842
1114
  preset.seed = value;
843
1115
  renderer.setPreset({ ...preset });
844
1116
  return this;
@@ -846,20 +1118,49 @@ function createCapsule(container, options = {}) {
846
1118
  setSpeed(speed) {
847
1119
  const value = Number(speed);
848
1120
  if (!Number.isFinite(value)) return this;
1121
+ dirty.speed = true;
849
1122
  preset.speed = value;
850
1123
  renderer.setPreset({ ...preset });
851
1124
  return this;
852
1125
  },
853
- setSize(width, height) {
1126
+ setText(content) {
1127
+ fillContent(textLayer, content);
1128
+ return this;
1129
+ },
1130
+ setColorContent(content) {
1131
+ fillContent(visualLayer, content);
1132
+ return this;
1133
+ },
1134
+ setTextRatio(ratio) {
1135
+ const value = Number(ratio);
1136
+ if (!Number.isFinite(value) || value < 0 || value > 100) return this;
1137
+ dirty.textRatio = true;
1138
+ merged.textRatio = value;
1139
+ root.style.setProperty('--hj-text-width', `${value}%`);
1140
+ return this;
1141
+ },
1142
+ setCssVars(vars) {
1143
+ if (!vars || typeof vars !== 'object') return this;
1144
+ dirty.cssVars = true;
1145
+ merged.cssVars = { ...(merged.cssVars || {}), ...vars };
1146
+ for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
1147
+ return this;
1148
+ },
1149
+ setLabel(label) {
1150
+ customLabel = typeof label === 'string' && label ? label : null;
1151
+ updateAria();
1152
+ return this;
1153
+ },
1154
+ setSize(width, height) {
854
1155
  if (width !== undefined) merged.width = width;
855
1156
  if (height !== undefined) merged.height = height;
856
1157
  applySize();
857
1158
  return this;
858
1159
  },
859
- randomize() {
860
- renderer.randomize();
861
- return this;
862
- },
1160
+ randomize() {
1161
+ this.setSeed(Math.random() * 100);
1162
+ return this;
1163
+ },
863
1164
  pause() {
864
1165
  paused = true;
865
1166
  return this;
@@ -873,15 +1174,19 @@ function createCapsule(container, options = {}) {
873
1174
  if (typeof renderer.setDprCap === 'function') renderer.setDprCap(dprCapFor(quality));
874
1175
  return this;
875
1176
  },
876
- dispose() {
877
- unsubscribe();
1177
+ dispose() {
1178
+ disposed = true;
1179
+ unsubscribe();
878
1180
  visibility.dispose();
879
1181
  if (resizeObserver) resizeObserver.disconnect();
880
1182
  else window.removeEventListener('resize', renderer.resize);
881
- renderer.dispose();
882
- root.remove();
883
- }
884
- };
1183
+ renderer.dispose();
1184
+ root.remove();
1185
+ },
1186
+ textRatio: merged.textRatio,
1187
+ cssVars: merged.cssVars,
1188
+ dirty
1189
+ };
885
1190
  }
886
1191
 
887
1192
  const PROGRESS_MOTION_WIDTH = 240;
@@ -1506,7 +1811,7 @@ function interpolate(template, values) {
1506
1811
  return template.replace(/\{(\w+)\}/g, (_, key) => (values[key] !== undefined ? values[key] : ''));
1507
1812
  }
1508
1813
 
1509
- function prefersReducedMotion() {
1814
+ function prefersReducedMotion$1() {
1510
1815
  return typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;
1511
1816
  }
1512
1817
 
@@ -1569,14 +1874,16 @@ const FLOW_PROFILES = {
1569
1874
  };
1570
1875
 
1571
1876
  class ProgressCapsuleController {
1572
- constructor({ root, canvas, valueElement, preset, emitter, options, copy }) {
1877
+ constructor({ root, canvas, valueElement, preset, emitter, options, copy, dirty }) {
1573
1878
  this.root = root;
1574
1879
  this.canvas = canvas;
1575
1880
  this.valueElement = valueElement;
1576
1881
  this.preset = preset;
1577
1882
  this.emitter = emitter;
1578
- this.options = options;
1579
- this.copy = copy;
1883
+ this.options = options;
1884
+ this.copy = copy;
1885
+ this.dirty = dirty;
1886
+ this.valueSuffix = options.valueSuffix ?? copy.valueSuffix;
1580
1887
  this.profile = preset.edgeStyle === 'tide'
1581
1888
  ? FLOW_PROFILES.tide
1582
1889
  : (FLOW_PROFILES[preset.id] || FLOW_PROFILES['visual-training']);
@@ -1617,11 +1924,22 @@ class ProgressCapsuleController {
1617
1924
  this.value = clamp(nextValue, this.min, this.max);
1618
1925
  const rounded = Math.round(this.value);
1619
1926
  this.root.style.setProperty('--progress', this.value.toFixed(2));
1620
- this.root.setAttribute('aria-valuenow', String(rounded));
1621
- this.root.setAttribute('aria-valuetext', `${rounded}${this.copy.valueSuffix}`);
1622
- this.valueElement.textContent = `${rounded}${this.copy.valueSuffix}`;
1623
- if (!this.suppressEvents) this.emitter.emit('change', { value: this.value, source });
1624
- }
1927
+ this.root.setAttribute('aria-valuenow', String(rounded));
1928
+ this.root.setAttribute('aria-valuetext', `${rounded}${this.valueSuffix}`);
1929
+ if (this.valueElement) this.valueElement.textContent = `${rounded}${this.valueSuffix}`;
1930
+ if (!this.suppressEvents) {
1931
+ if (this.dirty) this.dirty.value = true;
1932
+ this.emitter.emit('change', { value: this.value, source });
1933
+ }
1934
+ }
1935
+
1936
+ setValueSuffix(suffix) {
1937
+ this.valueSuffix = String(suffix == null ? '' : suffix);
1938
+ const rounded = Math.round(this.value);
1939
+ if (this.valueElement) this.valueElement.textContent = `${rounded}${this.valueSuffix}`;
1940
+ this.root.setAttribute('aria-valuetext', `${rounded}${this.valueSuffix}`);
1941
+ return this;
1942
+ }
1625
1943
 
1626
1944
  resizeCanvas() {
1627
1945
  const bounds = this.root.getBoundingClientRect();
@@ -1794,13 +2112,14 @@ class ProgressCapsuleController {
1794
2112
  if (clamped !== this.value) this.setProgress(clamped, 'prop');
1795
2113
  }
1796
2114
 
1797
- drawReferenceFlow() {
2115
+ drawReferenceFlow() {
1798
2116
  const ctx = this.ctx;
1799
2117
  const width = this.width;
1800
2118
  const height = this.height;
1801
2119
  if (!ctx || width <= 0 || height <= 0) return;
1802
2120
 
1803
- const shoreline = width * (this.value / 100);
2121
+ const range = Math.max(this.max - this.min, 1);
2122
+ const shoreline = width * Math.min(Math.max((this.value - this.min) / range, 0), 1);
1804
2123
  const time = this.flowTime;
1805
2124
  const [dark, accentA, accentB, glow] = this.preset.colors;
1806
2125
 
@@ -1918,32 +2237,49 @@ class ProgressCapsuleController {
1918
2237
  }
1919
2238
  }
1920
2239
 
1921
- updateFromPointer(event) {
1922
- const bounds = this.root.getBoundingClientRect();
1923
- const ratio = bounds.width > 0 ? (event.clientX - bounds.left) / bounds.width : 0;
1924
- this.setProgress(ratio * 100, 'drag');
1925
- }
1926
-
1927
- beginDrag(event) {
1928
- if (event.button !== undefined && event.button !== 0) return;
1929
- event.preventDefault();
1930
- this.dragging = true;
1931
- this.root.classList.add('is-dragging');
1932
- try { this.root.setPointerCapture?.(event.pointerId); } catch {}
1933
- this.emitter.emit('dragstart', { value: this.value });
1934
- this.updateFromPointer(event);
1935
- }
1936
-
1937
- moveDrag(event) {
1938
- if (!this.dragging) return;
1939
- event.preventDefault();
1940
- this.updateFromPointer(event);
1941
- }
1942
-
1943
- endDrag(event) {
1944
- if (!this.dragging) return;
1945
- this.dragging = false;
1946
- this.root.classList.remove('is-dragging');
2240
+ updateFromPointer(event) {
2241
+ const bounds = this.root.getBoundingClientRect();
2242
+ const ratio = bounds.width > 0 ? (event.clientX - bounds.left) / bounds.width : 0;
2243
+ this.setProgress(this.min + ratio * (this.max - this.min), 'drag');
2244
+ }
2245
+
2246
+ beginDrag(event) {
2247
+ if (event.button !== undefined && event.button !== 0) return;
2248
+ event.preventDefault();
2249
+ this.dragging = true;
2250
+ this.pendingPointer = null;
2251
+ this._dragRaf = 0;
2252
+ this.root.classList.add('is-dragging');
2253
+ try { this.root.setPointerCapture?.(event.pointerId); } catch {}
2254
+ this.emitter.emit('dragstart', { value: this.value });
2255
+ this.updateFromPointer(event);
2256
+ }
2257
+
2258
+ moveDrag(event) {
2259
+ if (!this.dragging) return;
2260
+ event.preventDefault();
2261
+ // rAF 合并:一帧最多一次 setProgress/change,视觉仍每帧更新。
2262
+ this.pendingPointer = event;
2263
+ if (this._dragRaf) return;
2264
+ this._dragRaf = requestAnimationFrame(() => {
2265
+ this._dragRaf = 0;
2266
+ const pending = this.pendingPointer;
2267
+ this.pendingPointer = null;
2268
+ if (pending) this.updateFromPointer(pending);
2269
+ });
2270
+ }
2271
+
2272
+ endDrag(event) {
2273
+ if (!this.dragging) return;
2274
+ this.dragging = false;
2275
+ if (this._dragRaf) {
2276
+ cancelAnimationFrame(this._dragRaf);
2277
+ this._dragRaf = 0;
2278
+ }
2279
+ const pending = this.pendingPointer;
2280
+ this.pendingPointer = null;
2281
+ if (pending) this.updateFromPointer(pending);
2282
+ this.root.classList.remove('is-dragging');
1947
2283
  try {
1948
2284
  if (event?.pointerId !== undefined && this.root.hasPointerCapture?.(event.pointerId)) {
1949
2285
  this.root.releasePointerCapture(event.pointerId);
@@ -1965,15 +2301,16 @@ class ProgressCapsuleController {
1965
2301
  }
1966
2302
 
1967
2303
  if (this.options.keyboard !== false) {
1968
- this.handlers.keydown = (event) => {
1969
- const actions = {
1970
- ArrowLeft: -2,
1971
- ArrowDown: -2,
1972
- ArrowRight: 2,
1973
- ArrowUp: 2,
1974
- PageDown: -10,
1975
- PageUp: 10
1976
- };
2304
+ this.handlers.keydown = (event) => {
2305
+ const step = (this.max - this.min) * 0.02;
2306
+ const actions = {
2307
+ ArrowLeft: -step,
2308
+ ArrowDown: -step,
2309
+ ArrowRight: step,
2310
+ ArrowUp: step,
2311
+ PageDown: -step * 5,
2312
+ PageUp: step * 5
2313
+ };
1977
2314
  if (event.key === 'Home') {
1978
2315
  event.preventDefault();
1979
2316
  this.setProgress(this.min, 'keyboard');
@@ -2011,8 +2348,9 @@ class ProgressCapsuleController {
2011
2348
  this.preset = { ...this.preset, colors };
2012
2349
  }
2013
2350
 
2014
- dispose() {
2015
- if (this.resizeObserver) this.resizeObserver.disconnect();
2351
+ dispose() {
2352
+ if (this._dragRaf) cancelAnimationFrame(this._dragRaf);
2353
+ if (this.resizeObserver) this.resizeObserver.disconnect();
2016
2354
  else window.removeEventListener('resize', this.resizeCanvas);
2017
2355
  for (const name of Object.keys(this.handlers)) {
2018
2356
  const handler = this.handlers[name];
@@ -2023,103 +2361,127 @@ class ProgressCapsuleController {
2023
2361
  }
2024
2362
 
2025
2363
  /**
2026
- * Mount a fluid progress capsule into `container`.
2027
- *
2028
- * Options: preset (id/code/name or object), width, height, value, min, max,
2029
- * draggable, keyboard, colors, edgeStyle, quality,
2030
- * respectReducedMotion, copy, cssVars.
2031
- */
2032
- function createProgressCapsule(container, options = {}) {
2364
+ * Mount a fluid progress capsule into `container`.
2365
+ *
2366
+ * Options: preset (id/code/name or object), width, height, value, min, max,
2367
+ * draggable, keyboard, colors, edgeStyle, textRatio (0-100, text region
2368
+ * width in percent), text (HTML string or DOM nodes for the text slot),
2369
+ * colorContent (HTML string or DOM nodes for the color slot),
2370
+ * showValue (show/hide the right-side percentage), quality,
2371
+ * respectReducedMotion, cssVars.
2372
+ */
2373
+ function createProgressCapsule(container, options = {}) {
2033
2374
  if (!container || typeof container.appendChild !== 'function') {
2034
2375
  throw new Error('createProgressCapsule: container element is required');
2035
2376
  }
2036
2377
 
2037
2378
  const preset = { ...getPreset('progress', options.preset ?? '星火') };
2038
- const merged = normalizeOptions(DEFAULTS.progress, preset, options);
2039
- const copy = { ...COPY, ...(merged.copy || {}) };
2040
-
2041
- const root = document.createElement('div');
2042
- root.className = 'hj-capsule-root hj-progress-root';
2043
- root.setAttribute('role', 'slider');
2044
- root.setAttribute('tabindex', '0');
2045
- root.setAttribute('data-draggable', String(merged.draggable !== false));
2379
+ const merged = normalizeOptions(DEFAULTS.progress, preset, options);
2380
+ const normalizedColors = merged.colors.map(normalizeColor);
2381
+ if (normalizedColors.every(Boolean)) preset.colors = normalizedColors;
2382
+ preset.edgeStyle = merged.edgeStyle;
2383
+ const copy = COPY;
2384
+ let customLabel = typeof options.label === 'string' && options.label ? options.label : null;
2385
+ // Vue 的裸布尔属性(<ProgressCapsule disabled />)会传成空字符串,
2386
+ // 这里统一按 true 处理。
2387
+ const disabled = merged.disabled === true || merged.disabled === '' || merged.disabled === 'true';
2388
+ const effectiveDraggable = disabled ? false : merged.draggable !== false;
2389
+ const effectiveKeyboard = disabled ? false : merged.keyboard !== false;
2390
+ const dirty = {
2391
+ preset: false,
2392
+ colors: false,
2393
+ textRatio: false,
2394
+ cssVars: false,
2395
+ edgeStyle: false,
2396
+ value: false
2397
+ };
2398
+
2399
+ const root = document.createElement('div');
2400
+ root.className = 'hj-capsule-root hj-progress-root';
2401
+ root.setAttribute('role', 'slider');
2402
+ root.setAttribute('tabindex', '0');
2403
+ root.setAttribute('data-draggable', String(effectiveDraggable));
2404
+ if (disabled) {
2405
+ root.setAttribute('aria-disabled', 'true');
2406
+ root.classList.add('is-disabled');
2407
+ }
2046
2408
  root.setAttribute('aria-valuemin', String(merged.min ?? 0));
2047
2409
  root.setAttribute('aria-valuemax', String(merged.max ?? 100));
2048
2410
  root.setAttribute('aria-valuenow', String(preset.initialProgress));
2049
- root.setAttribute(
2050
- 'aria-label',
2051
- interpolate(copy.progressAria, { brand: copy.brandName, code: preset.code, name: preset.name })
2052
- );
2053
- if (merged.keyboard === false) root.setAttribute('tabindex', '-1');
2411
+ const updateAria = () => {
2412
+ root.setAttribute(
2413
+ 'aria-label',
2414
+ customLabel || interpolate(copy.progressAria, { brand: copy.brandName, code: preset.code, name: preset.name })
2415
+ );
2416
+ };
2417
+ updateAria();
2418
+ if (!effectiveKeyboard) root.setAttribute('tabindex', '-1');
2054
2419
 
2055
2420
  const canvas = document.createElement('canvas');
2056
2421
  canvas.className = 'hj-progress-canvas';
2057
2422
  canvas.setAttribute('aria-hidden', 'true');
2058
2423
 
2059
- const copyEnabled = copy.enabled !== false && merged.showCopy !== false;
2060
-
2061
- const copyText = (key, fallback) => {
2062
- const value = copy[key];
2063
- return value === undefined ? fallback : value;
2064
- };
2065
-
2066
- function buildCopyHtml() {
2067
- let html = '';
2068
- const brandText = copy.brandName || '';
2069
- const subtitleText = copyText('subtitle', preset.subtitle);
2070
- if (brandText) html += `<span class="hj-progress-name">${brandText}</span>`;
2071
- if (subtitleText) html += `<span class="hj-progress-subtitle">${subtitleText}</span>`;
2072
- return html;
2073
- }
2074
-
2075
- const copyLayer = document.createElement('div');
2076
- copyLayer.className = 'hj-progress-copy';
2077
- const renderCopy = () => {
2078
- if (copyEnabled) copyLayer.innerHTML = buildCopyHtml();
2079
- };
2080
- renderCopy();
2081
-
2082
- if (copy.background) {
2083
- root.style.setProperty('--hj-progress-copy-bg', copy.background);
2084
- root.style.setProperty('--hj-progress-copy-pad', '6px 12px');
2085
- }
2086
- if (copy.color) root.style.setProperty('--hj-progress-copy-color', copy.color);
2087
-
2088
- const valueElement = document.createElement('div');
2089
- valueElement.className = 'hj-progress-value';
2090
- valueElement.setAttribute('aria-hidden', 'true');
2091
-
2092
- const dragLabel = document.createElement('span');
2093
- dragLabel.className = 'hj-progress-drag-label';
2094
- dragLabel.setAttribute('aria-hidden', 'true');
2095
- dragLabel.textContent = copy.dragLabel;
2096
-
2097
- root.appendChild(canvas);
2098
- if (copyEnabled) root.appendChild(copyLayer);
2099
- root.appendChild(valueElement);
2100
- root.appendChild(dragLabel);
2101
- container.appendChild(root);
2102
-
2103
- const emitter = createEmitter();
2104
- let paused = merged.respectReducedMotion && prefersReducedMotion();
2105
-
2106
- const applySize = () => {
2107
- root.style.width = parseSize(merged.width);
2108
- root.style.height = parseSize(merged.height);
2109
- const vars = merged.cssVars || {};
2110
- for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
2111
- };
2424
+ // Slot containers are always present but transparent by default; they only
2425
+ // provide geometry, never typography/padding/background (docs: 插槽 CSS 约定).
2426
+ const fillContent = (layer, content) => {
2427
+ layer.innerHTML = '';
2428
+ if (content == null) return;
2429
+ if (typeof content === 'string') {
2430
+ layer.innerHTML = content;
2431
+ return;
2432
+ }
2433
+ const nodes = Array.isArray(content) ? content : [content];
2434
+ for (const node of nodes) {
2435
+ if (node && typeof node.nodeType === 'number') layer.appendChild(node);
2436
+ }
2437
+ };
2438
+
2439
+ const textLayer = document.createElement('div');
2440
+ textLayer.className = 'hj-progress-text hj-progress-copy';
2441
+
2442
+ const visualLayer = document.createElement('div');
2443
+ visualLayer.className = 'hj-progress-visual';
2444
+
2445
+ fillContent(textLayer, options.text);
2446
+ fillContent(visualLayer, options.colorContent);
2447
+
2448
+ const valueElement = merged.showValue === false ? null : document.createElement('div');
2449
+ if (valueElement) {
2450
+ valueElement.className = 'hj-progress-value';
2451
+ valueElement.setAttribute('aria-hidden', 'true');
2452
+ }
2453
+
2454
+ root.appendChild(canvas);
2455
+ root.appendChild(textLayer);
2456
+ root.appendChild(visualLayer);
2457
+ if (valueElement) root.appendChild(valueElement);
2458
+ container.appendChild(root);
2459
+
2460
+ const emitter = createEmitter();
2461
+ let paused = merged.respectReducedMotion && prefersReducedMotion$1();
2462
+ let disposed = false;
2463
+
2464
+ const applySize = () => {
2465
+ root.style.width = parseSize(merged.width);
2466
+ root.style.height = parseSize(merged.height);
2467
+ const vars = merged.cssVars || {};
2468
+ for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
2469
+ if (merged.textRatio !== undefined) {
2470
+ root.style.setProperty('--hj-text-width', `${merged.textRatio}%`);
2471
+ }
2472
+ };
2112
2473
  applySize();
2113
2474
 
2114
- const controller = new ProgressCapsuleController({
2115
- root,
2116
- canvas,
2117
- valueElement,
2118
- preset,
2119
- emitter,
2120
- options: merged,
2121
- copy
2122
- });
2475
+ const controller = new ProgressCapsuleController({
2476
+ root,
2477
+ canvas,
2478
+ valueElement,
2479
+ preset,
2480
+ emitter,
2481
+ options: { ...merged, draggable: effectiveDraggable, keyboard: effectiveKeyboard },
2482
+ copy,
2483
+ dirty
2484
+ });
2123
2485
 
2124
2486
  let overlay = null;
2125
2487
  if (merged.renderer !== 'canvas2d') {
@@ -2130,7 +2492,13 @@ function createProgressCapsule(container, options = {}) {
2130
2492
  getProgress: () => controller.value
2131
2493
  });
2132
2494
  }
2133
- if (overlay) controller.webglActive = true;
2495
+ if (overlay) controller.webglActive = true;
2496
+ else if (merged.renderer !== 'canvas2d') {
2497
+ nextTick(() => {
2498
+ if (disposed) return;
2499
+ emitter.emit('error', { message: 'WebGL2 unavailable, using Canvas2D fallback' });
2500
+ });
2501
+ }
2134
2502
 
2135
2503
  const visibility = createVisibilityGuard(root);
2136
2504
  let flowTime = 0;
@@ -2146,15 +2514,14 @@ function createProgressCapsule(container, options = {}) {
2146
2514
  () => paused
2147
2515
  );
2148
2516
 
2149
- emitter.emit('ready', { preset: { ...preset } });
2517
+ nextTick(() => {
2518
+ if (disposed) return;
2519
+ emitter.emit('ready', { preset: { ...preset } });
2520
+ });
2150
2521
 
2151
- const syncDom = () => {
2152
- root.setAttribute(
2153
- 'aria-label',
2154
- interpolate(copy.progressAria, { brand: copy.brandName, code: preset.code, name: preset.name })
2155
- );
2156
- renderCopy();
2157
- };
2522
+ const syncDom = () => {
2523
+ updateAria();
2524
+ };
2158
2525
 
2159
2526
  return {
2160
2527
  element: root,
@@ -2175,26 +2542,30 @@ function createProgressCapsule(container, options = {}) {
2175
2542
  },
2176
2543
  setPreset(ref) {
2177
2544
  const next = getPreset('progress', ref);
2545
+ dirty.preset = true;
2178
2546
  Object.assign(preset, next);
2179
- controller.preset = next;
2180
- controller.profile = next.edgeStyle === 'tide'
2181
- ? FLOW_PROFILES.tide
2182
- : (FLOW_PROFILES[next.id] || FLOW_PROFILES['visual-training']);
2183
- controller.flowTime = stringSeed(next.id) * 31;
2184
- controller.seed = stringSeed(`${next.id}-reference`) * Math.PI * 2;
2185
- syncDom();
2186
- if (overlay) {
2187
- overlay.dispose();
2188
- overlay = attachProgressFlowOverlay({ root, canvas, preset: next, getProgress: () => controller.value });
2189
- }
2190
- controller.webglActive = Boolean(overlay);
2191
- controller.resizeCanvas();
2192
- emitter.emit('presetchange', { preset: { ...next } });
2547
+ const nextColors = preset.colors.map(normalizeColor);
2548
+ if (nextColors.every(Boolean)) preset.colors = nextColors;
2549
+ controller.preset = preset;
2550
+ controller.profile = next.edgeStyle === 'tide'
2551
+ ? FLOW_PROFILES.tide
2552
+ : (FLOW_PROFILES[next.id] || FLOW_PROFILES['visual-training']);
2553
+ controller.flowTime = stringSeed(next.id) * 31;
2554
+ controller.seed = stringSeed(`${next.id}-reference`) * Math.PI * 2;
2555
+ syncDom();
2556
+ if (overlay) {
2557
+ overlay.dispose();
2558
+ overlay = attachProgressFlowOverlay({ root, canvas, preset, getProgress: () => controller.value });
2559
+ }
2560
+ controller.webglActive = Boolean(overlay);
2561
+ controller.resizeCanvas();
2562
+ emitter.emit('presetchange', { preset: { ...preset } });
2193
2563
  return this;
2194
2564
  },
2195
2565
  setEdgeStyle(edgeStyle) {
2196
2566
  const value = String(edgeStyle || '').toLowerCase();
2197
2567
  if (value !== 'flow' && value !== 'tide') return this;
2568
+ dirty.edgeStyle = true;
2198
2569
  preset.edgeStyle = value;
2199
2570
  controller.profile = value === 'tide'
2200
2571
  ? FLOW_PROFILES.tide
@@ -2207,12 +2578,46 @@ function createProgressCapsule(container, options = {}) {
2207
2578
  controller.resizeCanvas();
2208
2579
  return this;
2209
2580
  },
2581
+ setText(content) {
2582
+ fillContent(textLayer, content);
2583
+ return this;
2584
+ },
2585
+ setColorContent(content) {
2586
+ fillContent(visualLayer, content);
2587
+ return this;
2588
+ },
2589
+ setTextRatio(ratio) {
2590
+ const value = Number(ratio);
2591
+ if (!Number.isFinite(value) || value < 0 || value > 100) return this;
2592
+ dirty.textRatio = true;
2593
+ merged.textRatio = value;
2594
+ root.style.setProperty('--hj-text-width', `${value}%`);
2595
+ return this;
2596
+ },
2597
+ setCssVars(vars) {
2598
+ if (!vars || typeof vars !== 'object') return this;
2599
+ dirty.cssVars = true;
2600
+ merged.cssVars = { ...(merged.cssVars || {}), ...vars };
2601
+ for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
2602
+ return this;
2603
+ },
2604
+ setLabel(label) {
2605
+ customLabel = typeof label === 'string' && label ? label : null;
2606
+ updateAria();
2607
+ return this;
2608
+ },
2609
+ setValueSuffix(suffix) {
2610
+ controller.setValueSuffix(suffix);
2611
+ return this;
2612
+ },
2210
2613
  setColors(colors) {
2211
- if (!Array.isArray(colors) || colors.length !== 4) return this;
2212
- controller.setColors(colors);
2213
- if (overlay) overlay.setColors(colors);
2214
- controller.resizeCanvas();
2215
- return this;
2614
+ const next = Array.isArray(colors) ? colors.map(normalizeColor) : [];
2615
+ if (next.length !== 4 || next.some((color) => !color)) return this;
2616
+ dirty.colors = true;
2617
+ controller.setColors(next);
2618
+ if (overlay) overlay.setColors(next);
2619
+ controller.resizeCanvas();
2620
+ return this;
2216
2621
  },
2217
2622
  setSize(width, height) {
2218
2623
  if (width !== undefined) merged.width = width;
@@ -2239,14 +2644,240 @@ function createProgressCapsule(container, options = {}) {
2239
2644
  controller.resizeCanvas();
2240
2645
  return this;
2241
2646
  },
2242
- dispose() {
2243
- unsubscribe();
2647
+ dispose() {
2648
+ disposed = true;
2649
+ unsubscribe();
2244
2650
  visibility.dispose();
2245
2651
  if (overlay) overlay.dispose();
2246
- controller.dispose();
2247
- root.remove();
2248
- }
2652
+ controller.dispose();
2653
+ root.remove();
2654
+ },
2655
+ textRatio: merged.textRatio,
2656
+ cssVars: merged.cssVars,
2657
+ dirty
2658
+ };
2659
+ }
2660
+
2661
+ function prefersReducedMotion() {
2662
+ return typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;
2663
+ }
2664
+
2665
+ /**
2666
+ * Mount the cosmic (nebula) material as a background layer inside `host`.
2667
+ *
2668
+ * The layer is absolutely positioned and defaults to `z-index: -1`, so it
2669
+ * paints behind the host's in-flow content: text/buttons/images inside the
2670
+ * host sit on top with zero extra CSS. Host gets `position: relative;
2671
+ * isolation: isolate` automatically so the layer can never escape its box
2672
+ * (or break stacking outside it).
2673
+ *
2674
+ * Options: preset (id/code/name or object), colors, seed, speed, quality,
2675
+ * renderer, mouseColor, respectReducedMotion, opacity (0-1), fallbackColor
2676
+ * (true=preset base color, a hex string, or false to disable).
2677
+ */
2678
+ function createColorBackground(host, options = {}) {
2679
+ if (!host || typeof host.appendChild !== 'function') {
2680
+ throw new Error('createColorBackground: host element is required');
2681
+ }
2682
+
2683
+ const preset = { ...getPreset('capsule', options.preset ?? '初光') };
2684
+ const merged = normalizeOptions(
2685
+ {
2686
+ quality: DEFAULTS.capsule.quality,
2687
+ renderer: DEFAULTS.capsule.renderer,
2688
+ respectReducedMotion: DEFAULTS.capsule.respectReducedMotion,
2689
+ mouseColor: true,
2690
+ opacity: 1,
2691
+ fallbackColor: true
2692
+ },
2693
+ preset,
2694
+ options
2695
+ );
2696
+ const normalizedColors = merged.colors.map(normalizeColor);
2697
+ if (normalizedColors.every(Boolean)) preset.colors = normalizedColors;
2698
+ preset.seed = merged.seed;
2699
+ preset.speed = merged.speed;
2700
+
2701
+ const computed = getComputedStyle(host);
2702
+ if (computed.position === 'static' || computed.position === '') {
2703
+ host.style.position = 'relative';
2704
+ }
2705
+ host.style.isolation = 'isolate';
2706
+
2707
+ const layer = document.createElement('div');
2708
+ layer.className = 'dlc-color-layer';
2709
+
2710
+ const syncLayerVars = () => {
2711
+ layer.style.setProperty('--dlc-color-opacity', String(merged.opacity));
2712
+ const fallback =
2713
+ merged.fallbackColor === false || merged.fallbackColor == null
2714
+ ? 'transparent'
2715
+ : typeof merged.fallbackColor === 'string'
2716
+ ? merged.fallbackColor
2717
+ : preset.colors[0];
2718
+ layer.style.setProperty('--dlc-color-bg', fallback);
2249
2719
  };
2720
+ syncLayerVars();
2721
+
2722
+ const canvas = document.createElement('canvas');
2723
+ canvas.className = 'dlc-color-canvas';
2724
+ canvas.setAttribute('aria-hidden', 'true');
2725
+ layer.appendChild(canvas);
2726
+ host.appendChild(layer);
2727
+
2728
+ const emitter = createEmitter();
2729
+ let paused = merged.respectReducedMotion && prefersReducedMotion();
2730
+ let disposed = false;
2731
+ const dirty = {
2732
+ preset: false,
2733
+ seed: false,
2734
+ speed: false,
2735
+ colors: false,
2736
+ opacity: false,
2737
+ fallbackColor: false,
2738
+ mouseColor: false
2739
+ };
2740
+
2741
+ let renderer;
2742
+ const useWebgl = merged.renderer !== 'canvas2d';
2743
+ if (useWebgl) {
2744
+ try {
2745
+ renderer = new CosmicRenderer(canvas, merged, {
2746
+ dprCap: dprCapFor(merged.quality),
2747
+ mouseColor: merged.mouseColor !== false,
2748
+ eventTarget: host
2749
+ });
2750
+ } catch (error) {
2751
+ renderer = new FallbackRenderer(canvas, merged);
2752
+ nextTick(() => {
2753
+ if (disposed) return;
2754
+ emitter.emit('error', {
2755
+ message: String(error && error.message ? error.message : error)
2756
+ });
2757
+ });
2758
+ }
2759
+ } else {
2760
+ renderer = new FallbackRenderer(canvas, merged);
2761
+ }
2762
+
2763
+ const resizeObserver =
2764
+ typeof ResizeObserver !== 'undefined'
2765
+ ? new ResizeObserver(() => renderer.resize())
2766
+ : null;
2767
+ if (resizeObserver) resizeObserver.observe(host);
2768
+ else window.addEventListener('resize', renderer.resize);
2769
+
2770
+ const visibility = createVisibilityGuard(layer);
2771
+
2772
+ let animationTime = 0;
2773
+ const unsubscribe = subscribeScheduler(
2774
+ (delta) => {
2775
+ animationTime += delta;
2776
+ if (visibility.isVisible()) renderer.draw(animationTime);
2777
+ },
2778
+ () => paused
2779
+ );
2780
+
2781
+ nextTick(() => {
2782
+ if (disposed) return;
2783
+ emitter.emit('ready', { preset: { ...preset } });
2784
+ });
2785
+
2786
+ return {
2787
+ element: host,
2788
+ layer,
2789
+ canvas,
2790
+ preset,
2791
+ on: emitter.on,
2792
+ off: emitter.off,
2793
+ setPreset(ref) {
2794
+ const next = getPreset('capsule', ref);
2795
+ dirty.preset = true;
2796
+ Object.assign(preset, next);
2797
+ const nextColors = preset.colors.map(normalizeColor);
2798
+ if (nextColors.every(Boolean)) preset.colors = nextColors;
2799
+ renderer.setPreset({ ...preset });
2800
+ renderer.resize();
2801
+ syncLayerVars();
2802
+ emitter.emit('presetchange', { preset: { ...preset } });
2803
+ return this;
2804
+ },
2805
+ setColors(colors) {
2806
+ const next = Array.isArray(colors) ? colors.map(normalizeColor) : [];
2807
+ if (next.length !== 4 || next.some((color) => !color)) return this;
2808
+ dirty.colors = true;
2809
+ preset.colors = next;
2810
+ renderer.setPreset({ ...preset, colors: next });
2811
+ syncLayerVars();
2812
+ return this;
2813
+ },
2814
+ setSeed(seed) {
2815
+ const value = Number(seed);
2816
+ if (!Number.isFinite(value)) return this;
2817
+ dirty.seed = true;
2818
+ preset.seed = value;
2819
+ renderer.setPreset({ ...preset });
2820
+ return this;
2821
+ },
2822
+ setSpeed(speed) {
2823
+ const value = Number(speed);
2824
+ if (!Number.isFinite(value)) return this;
2825
+ dirty.speed = true;
2826
+ preset.speed = value;
2827
+ renderer.setPreset({ ...preset });
2828
+ return this;
2829
+ },
2830
+ setFallbackColor(value) {
2831
+ dirty.fallbackColor = true;
2832
+ merged.fallbackColor = value;
2833
+ syncLayerVars();
2834
+ return this;
2835
+ },
2836
+ setMouseColor(value) {
2837
+ dirty.mouseColor = true;
2838
+ merged.mouseColor = value !== false;
2839
+ if (typeof renderer.setMouseColor === 'function') renderer.setMouseColor(merged.mouseColor);
2840
+ return this;
2841
+ },
2842
+ setQuality(quality) {
2843
+ merged.quality = quality;
2844
+ if (typeof renderer.setDprCap === 'function') renderer.setDprCap(dprCapFor(quality));
2845
+ return this;
2846
+ },
2847
+ setOpacity(value) {
2848
+ const opacity = Number(value);
2849
+ if (!Number.isFinite(opacity)) return this;
2850
+ dirty.opacity = true;
2851
+ merged.opacity = Math.min(1, Math.max(0, opacity));
2852
+ layer.style.setProperty('--dlc-color-opacity', String(merged.opacity));
2853
+ return this;
2854
+ },
2855
+ randomize() {
2856
+ this.setSeed(Math.random() * 100);
2857
+ return this;
2858
+ },
2859
+ pause() {
2860
+ paused = true;
2861
+ return this;
2862
+ },
2863
+ resume() {
2864
+ paused = false;
2865
+ return this;
2866
+ },
2867
+ dispose() {
2868
+ disposed = true;
2869
+ unsubscribe();
2870
+ visibility.dispose();
2871
+ if (resizeObserver) resizeObserver.disconnect();
2872
+ else window.removeEventListener('resize', renderer.resize);
2873
+ renderer.dispose();
2874
+ layer.remove();
2875
+ },
2876
+ opacity: merged.opacity,
2877
+ fallbackColor: merged.fallbackColor,
2878
+ mouseColor: merged.mouseColor,
2879
+ dirty
2880
+ };
2250
2881
  }
2251
2882
 
2252
2883
  /**
@@ -2255,11 +2886,12 @@ function createProgressCapsule(container, options = {}) {
2255
2886
  * `h` (see vue2.js / vue3.js). Lifecycle options cover both.
2256
2887
  */
2257
2888
 
2258
- const CAPSULE_EVENTS = [
2259
- 'ready', 'error', 'click', 'pointerdown', 'pointerup', 'dblclick',
2260
- 'pointerenter', 'pointerleave', 'presetchange'
2261
- ];
2262
- const PROGRESS_EVENTS = ['ready', 'error', 'change', 'dragstart', 'dragend', 'presetchange'];
2889
+ const CAPSULE_EVENTS = [
2890
+ 'ready', 'error', 'click', 'pointerdown', 'pointerup', 'dblclick',
2891
+ 'pointerenter', 'pointerleave', 'presetchange'
2892
+ ];
2893
+ const PROGRESS_EVENTS = ['ready', 'error', 'change', 'dragstart', 'dragend', 'presetchange'];
2894
+ const COLOR_EVENTS = ['ready', 'error', 'presetchange'];
2263
2895
 
2264
2896
  function hookEvents(controller, events, emit) {
2265
2897
  for (const event of events) {
@@ -2267,42 +2899,92 @@ function hookEvents(controller, events, emit) {
2267
2899
  }
2268
2900
  }
2269
2901
 
2270
- function makeWrapper({ name, props, events, create, render, methods, watch }) {
2271
- const internalMethods = {
2272
- mountController() {
2273
- this.controller = create(this.$el, this.$props);
2274
- hookEvents(this.controller, events, (event, payload) => this.$emit(event, payload));
2275
- },
2276
- recreate() {
2277
- if (this.controller) this.controller.dispose();
2278
- if (this.$el) this.$el.innerHTML = '';
2279
- this.mountController();
2280
- },
2281
- scheduleRecreate() {
2282
- if (this._recreateQueued) return;
2283
- this._recreateQueued = true;
2902
+ function makeWrapper({ name, props, events, create, render, methods, watch }) {
2903
+ const internalMethods = {
2904
+ mountController(extraState = {}) {
2905
+ this.controller = create(this.$el, { ...this.$props, ...extraState });
2906
+ hookEvents(this.controller, events, (event, payload) => this.$emit(event, payload));
2907
+ this.moveSlots();
2908
+ this.applyAttrs();
2909
+ },
2910
+ recreate() {
2911
+ const extraState = this.runtimeState();
2912
+ if (this.controller) this.controller.dispose();
2913
+ this.mountController(extraState);
2914
+ },
2915
+ scheduleRecreate() {
2916
+ if (this._recreateQueued) return;
2917
+ this._recreateQueued = true;
2284
2918
  this.$nextTick(() => {
2285
2919
  this._recreateQueued = false;
2286
- this.recreate();
2287
- });
2288
- }
2289
- };
2290
-
2291
- return {
2292
- name,
2293
- props,
2294
- emits: events,
2295
- render,
2920
+ this.recreate();
2921
+ });
2922
+ },
2923
+ moveSlots() {
2924
+ if (!this.controller || !this.controller.element || !this.$el) return;
2925
+ const root = this.controller.element;
2926
+ const textLayer = root.querySelector('.hj-capsule-text, .hj-progress-text');
2927
+ const visualLayer = root.querySelector('.hj-capsule-visual, .hj-progress-visual');
2928
+ const move = (wrapper, layer) => {
2929
+ if (!wrapper || !layer) return;
2930
+ for (const node of Array.from(wrapper.childNodes)) layer.appendChild(node);
2931
+ };
2932
+ const refs = this.$refs || {};
2933
+ move(refs.slotText, textLayer);
2934
+ move(refs.slotColor, visualLayer);
2935
+ },
2936
+ applyAttrs() {
2937
+ if (!this.controller || !this.controller.element || !this.$attrs) return;
2938
+ const root = this.controller.element;
2939
+ for (const key of Object.keys(this.$attrs)) {
2940
+ if (key === 'class' || key === 'style') continue;
2941
+ const value = this.$attrs[key];
2942
+ if (value == null || value === false) root.removeAttribute(key);
2943
+ else root.setAttribute(key, String(value));
2944
+ }
2945
+ },
2946
+ runtimeState() {
2947
+ const controller = this.controller;
2948
+ if (!controller || !controller.dirty) return {};
2949
+ const dirty = controller.dirty;
2950
+ const state = {};
2951
+ if (dirty.preset && controller.preset) state.preset = controller.preset.name;
2952
+ if (dirty.seed && controller.preset && controller.preset.seed !== undefined) state.seed = controller.preset.seed;
2953
+ if (dirty.speed && controller.preset && controller.preset.speed !== undefined) state.speed = controller.preset.speed;
2954
+ if (dirty.colors && controller.preset && Array.isArray(controller.preset.colors)) {
2955
+ state.colors = [...controller.preset.colors];
2956
+ }
2957
+ if (dirty.textRatio && controller.textRatio !== undefined) state.textRatio = controller.textRatio;
2958
+ if (dirty.cssVars && controller.cssVars) state.cssVars = { ...controller.cssVars };
2959
+ if (dirty.value && typeof controller.getValue === 'function') state.value = controller.getValue();
2960
+ if (dirty.edgeStyle && controller.preset) state.edgeStyle = controller.preset.edgeStyle;
2961
+ if (dirty.opacity) state.opacity = controller.opacity;
2962
+ if (dirty.fallbackColor) state.fallbackColor = controller.fallbackColor;
2963
+ if (dirty.mouseColor) state.mouseColor = controller.mouseColor;
2964
+ return state;
2965
+ }
2966
+ };
2967
+
2968
+ return {
2969
+ name,
2970
+ props,
2971
+ emits: events,
2972
+ render,
2973
+ inheritAttrs: false,
2296
2974
  mounted() {
2297
2975
  this.mountController();
2298
2976
  },
2299
2977
  beforeUnmount() {
2300
2978
  if (this.controller) this.controller.dispose();
2301
2979
  },
2302
- beforeDestroy() {
2303
- if (this.controller) this.controller.dispose();
2304
- },
2305
- watch: {
2980
+ beforeDestroy() {
2981
+ if (this.controller) this.controller.dispose();
2982
+ },
2983
+ updated() {
2984
+ this.moveSlots();
2985
+ this.applyAttrs();
2986
+ },
2987
+ watch: {
2306
2988
  preset(value) { if (value && this.controller) this.controller.setPreset(value); },
2307
2989
  colors(value) { if (value && this.controller) this.controller.setColors(value); },
2308
2990
  width(value) { if (value !== undefined && this.controller) this.controller.setSize(value); },
@@ -2316,11 +2998,11 @@ function makeWrapper({ name, props, events, create, render, methods, watch }) {
2316
2998
 
2317
2999
  function makeCapsuleWrapper(createCapsule, render) {
2318
3000
  return makeWrapper({
2319
- name: 'CosmicCapsule',
3001
+ name: 'CosmicCapsule',
2320
3002
  props: [
2321
3003
  'preset', 'width', 'height', 'colors', 'seed', 'speed',
2322
- 'mouseColor', 'renderer', 'quality', 'respectReducedMotion', 'copy', 'cssVars'
2323
- ],
3004
+ 'textRatio', 'label', 'mouseColor', 'renderer', 'quality', 'respectReducedMotion', 'cssVars'
3005
+ ],
2324
3006
  events: CAPSULE_EVENTS,
2325
3007
  create: createCapsule,
2326
3008
  render,
@@ -2337,6 +3019,15 @@ function makeCapsuleWrapper(createCapsule, render) {
2337
3019
  this.controller.setSpeed(speed);
2338
3020
  }
2339
3021
  },
3022
+ textRatio(value) {
3023
+ if (value !== undefined && this.controller) this.controller.setTextRatio(Number(value));
3024
+ },
3025
+ cssVars(value) {
3026
+ if (value && this.controller) this.controller.setCssVars(value);
3027
+ },
3028
+ label(value) {
3029
+ if (this.controller) this.controller.setLabel(value);
3030
+ },
2340
3031
  mouseColor() {
2341
3032
  this.scheduleRecreate();
2342
3033
  },
@@ -2348,23 +3039,28 @@ function makeCapsuleWrapper(createCapsule, render) {
2348
3039
  randomize() { return this.controller && this.controller.randomize(); },
2349
3040
  pause() { return this.controller && this.controller.pause(); },
2350
3041
  resume() { return this.controller && this.controller.resume(); },
2351
- setPreset(preset) { return this.controller && this.controller.setPreset(preset); },
2352
- setSize(width, height) { return this.controller && this.controller.setSize(width, height); },
2353
- setColors(colors) { return this.controller && this.controller.setColors(colors); },
2354
- setQuality(quality) { return this.controller && this.controller.setQuality(quality); },
2355
- getController() { return this.controller; }
3042
+ setPreset(preset) { return this.controller && this.controller.setPreset(preset); },
3043
+ setSize(width, height) { return this.controller && this.controller.setSize(width, height); },
3044
+ setColors(colors) { return this.controller && this.controller.setColors(colors); },
3045
+ setText(content) { return this.controller && this.controller.setText(content); },
3046
+ setColorContent(content) { return this.controller && this.controller.setColorContent(content); },
3047
+ setTextRatio(ratio) { return this.controller && this.controller.setTextRatio(ratio); },
3048
+ setCssVars(vars) { return this.controller && this.controller.setCssVars(vars); },
3049
+ setLabel(label) { return this.controller && this.controller.setLabel(label); },
3050
+ setQuality(quality) { return this.controller && this.controller.setQuality(quality); },
3051
+ getController() { return this.controller; }
2356
3052
  }
2357
3053
  });
2358
3054
  }
2359
3055
 
2360
- function makeProgressWrapper(createProgressCapsule, render) {
3056
+ function makeProgressWrapper(createProgressCapsule, render) {
2361
3057
  return makeWrapper({
2362
- name: 'ProgressCapsule',
2363
- props: [
2364
- 'preset', 'width', 'height', 'value', 'min', 'max',
2365
- 'draggable', 'keyboard', 'edgeStyle', 'colors', 'quality', 'renderer',
2366
- 'respectReducedMotion', 'copy', 'cssVars'
2367
- ],
3058
+ name: 'ProgressCapsule',
3059
+ props: [
3060
+ 'preset', 'width', 'height', 'value', 'min', 'max',
3061
+ 'draggable', 'keyboard', 'disabled', 'edgeStyle', 'colors', 'textRatio', 'label', 'showValue', 'valueSuffix',
3062
+ 'quality', 'renderer', 'respectReducedMotion', 'cssVars'
3063
+ ],
2368
3064
  events: PROGRESS_EVENTS,
2369
3065
  create: createProgressCapsule,
2370
3066
  render,
@@ -2381,9 +3077,27 @@ function makeProgressWrapper(createProgressCapsule, render) {
2381
3077
  edgeStyle(value) {
2382
3078
  if (value && this.controller) this.controller.setEdgeStyle(value);
2383
3079
  },
2384
- draggable() {
2385
- this.scheduleRecreate();
2386
- },
3080
+ textRatio(value) {
3081
+ if (value !== undefined && this.controller) this.controller.setTextRatio(Number(value));
3082
+ },
3083
+ cssVars(value) {
3084
+ if (value && this.controller) this.controller.setCssVars(value);
3085
+ },
3086
+ label(value) {
3087
+ if (this.controller) this.controller.setLabel(value);
3088
+ },
3089
+ valueSuffix(value) {
3090
+ if (value !== undefined && this.controller) this.controller.setValueSuffix(value);
3091
+ },
3092
+ showValue() {
3093
+ this.scheduleRecreate();
3094
+ },
3095
+ disabled() {
3096
+ this.scheduleRecreate();
3097
+ },
3098
+ draggable() {
3099
+ this.scheduleRecreate();
3100
+ },
2387
3101
  keyboard() {
2388
3102
  this.scheduleRecreate();
2389
3103
  },
@@ -2398,24 +3112,96 @@ function makeProgressWrapper(createProgressCapsule, render) {
2398
3112
  randomize() { return this.controller && this.controller.randomize(); },
2399
3113
  pause() { return this.controller && this.controller.pause(); },
2400
3114
  resume() { return this.controller && this.controller.resume(); },
2401
- setPreset(preset) { return this.controller && this.controller.setPreset(preset); },
2402
- setSize(width, height) { return this.controller && this.controller.setSize(width, height); },
2403
- setColors(colors) { return this.controller && this.controller.setColors(colors); },
2404
- setQuality(quality) { return this.controller && this.controller.setQuality(quality); },
3115
+ setPreset(preset) { return this.controller && this.controller.setPreset(preset); },
3116
+ setSize(width, height) { return this.controller && this.controller.setSize(width, height); },
3117
+ setColors(colors) { return this.controller && this.controller.setColors(colors); },
3118
+ setText(content) { return this.controller && this.controller.setText(content); },
3119
+ setColorContent(content) { return this.controller && this.controller.setColorContent(content); },
3120
+ setTextRatio(ratio) { return this.controller && this.controller.setTextRatio(ratio); },
3121
+ setCssVars(vars) { return this.controller && this.controller.setCssVars(vars); },
3122
+ setLabel(label) { return this.controller && this.controller.setLabel(label); },
3123
+ setValueSuffix(suffix) { return this.controller && this.controller.setValueSuffix(suffix); },
3124
+ setQuality(quality) { return this.controller && this.controller.setQuality(quality); },
2405
3125
  getController() { return this.controller; }
2406
3126
  }
2407
- });
3127
+ });
2408
3128
  }
2409
3129
 
2410
- // Vue3 does NOT pass `h` to the render option; it must be imported.
2411
- const render = () => h('div', { class: 'hj-vue-host' });
2412
-
2413
- const CosmicCapsule = makeCapsuleWrapper(createCapsule, render);
2414
- const ProgressCapsule = makeProgressWrapper(createProgressCapsule, render);
2415
-
2416
- var vue3 = {
2417
- CosmicCapsule,
2418
- ProgressCapsule
3130
+ function makeColorWrapper(createColorBackground, render) {
3131
+ return makeWrapper({
3132
+ name: 'ColorBackground',
3133
+ props: [
3134
+ 'preset', 'colors', 'seed', 'speed', 'quality', 'renderer',
3135
+ 'mouseColor', 'respectReducedMotion', 'opacity', 'fallbackColor'
3136
+ ],
3137
+ events: COLOR_EVENTS,
3138
+ create: createColorBackground,
3139
+ render,
3140
+ watch: {
3141
+ seed(value) {
3142
+ const seed = Number(value);
3143
+ if (this.controller && Number.isFinite(seed)) this.controller.setSeed(seed);
3144
+ },
3145
+ speed(value) {
3146
+ const speed = Number(value);
3147
+ if (this.controller && Number.isFinite(speed)) this.controller.setSpeed(speed);
3148
+ },
3149
+ opacity(value) {
3150
+ if (value !== undefined && this.controller) this.controller.setOpacity(Number(value));
3151
+ },
3152
+ fallbackColor(value) {
3153
+ if (this.controller) this.controller.setFallbackColor(value);
3154
+ },
3155
+ mouseColor(value) {
3156
+ if (this.controller) this.controller.setMouseColor(value);
3157
+ },
3158
+ renderer() {
3159
+ this.scheduleRecreate();
3160
+ },
3161
+ respectReducedMotion() {
3162
+ this.scheduleRecreate();
3163
+ }
3164
+ },
3165
+ methods: {
3166
+ setPreset(preset) { return this.controller && this.controller.setPreset(preset); },
3167
+ setColors(colors) { return this.controller && this.controller.setColors(colors); },
3168
+ setSeed(seed) { return this.controller && this.controller.setSeed(seed); },
3169
+ setSpeed(speed) { return this.controller && this.controller.setSpeed(speed); },
3170
+ setQuality(quality) { return this.controller && this.controller.setQuality(quality); },
3171
+ setOpacity(value) { return this.controller && this.controller.setOpacity(value); },
3172
+ setFallbackColor(value) { return this.controller && this.controller.setFallbackColor(value); },
3173
+ setMouseColor(value) { return this.controller && this.controller.setMouseColor(value); },
3174
+ randomize() { return this.controller && this.controller.randomize(); },
3175
+ pause() { return this.controller && this.controller.pause(); },
3176
+ resume() { return this.controller && this.controller.resume(); },
3177
+ getController() { return this.controller; }
3178
+ }
3179
+ });
3180
+ }
3181
+
3182
+ // Vue3 does NOT pass `h` to the render option; it must be imported. Slot
3183
+ // content renders into two marker wrappers, then moveSlots moves it into the
3184
+ // component's slot containers.
3185
+ const render = function render() {
3186
+ return h('div', { class: 'hj-vue-host' }, [
3187
+ h('div', { class: 'hj-vue-slot-text', ref: 'slotText' }, this.$slots.default ? this.$slots.default() : []),
3188
+ h('div', { class: 'hj-vue-slot-color', ref: 'slotColor' }, this.$slots.color ? this.$slots.color() : [])
3189
+ ]);
3190
+ };
3191
+
3192
+ // Color 的宿主内容就是组件内容:直接放在 host 里,浮在背景层上方。
3193
+ const renderColor = function renderColor() {
3194
+ return h('div', { class: 'hj-vue-host' }, this.$slots.default ? this.$slots.default() : []);
3195
+ };
3196
+
3197
+ const CosmicCapsule = makeCapsuleWrapper(createCapsule, render);
3198
+ const ProgressCapsule = makeProgressWrapper(createProgressCapsule, render);
3199
+ const ColorBackground = makeColorWrapper(createColorBackground, renderColor);
3200
+
3201
+ var vue3 = {
3202
+ CosmicCapsule,
3203
+ ProgressCapsule,
3204
+ ColorBackground
2419
3205
  };
2420
3206
 
2421
- export { CosmicCapsule, ProgressCapsule, vue3 as default };
3207
+ export { ColorBackground, CosmicCapsule, ProgressCapsule, vue3 as default };