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