@5even7/dlc-ui 0.2.2 → 0.2.4

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
@@ -177,9 +177,13 @@ function getPreset(kind, ref) {
177
177
  const list = kind === 'capsule' ? CAPSULE_PRESETS : kind === 'progress' ? PROGRESS_PRESETS : null;
178
178
  if (!list) throw new Error(`Unknown preset kind: ${kind} (use "capsule" or "progress")`);
179
179
  const key = String(ref).trim().toLowerCase();
180
- const found = list.find(
181
- (preset) => preset.name.toLowerCase() === key
182
- );
180
+ let found = null;
181
+ for (let index = 0; index < list.length; index += 1) {
182
+ if (list[index].name.toLowerCase() === key) {
183
+ found = list[index];
184
+ break;
185
+ }
186
+ }
183
187
  if (!found) throw new Error(`Unknown ${kind} preset: ${ref}`);
184
188
  return found;
185
189
  }
@@ -198,10 +202,10 @@ const DEFAULTS = {
198
202
  width: 454,
199
203
  height: 104,
200
204
  min: 0,
201
- max: 100,
205
+ max: 100,
202
206
  draggable: true,
203
- keyboard: true,
204
207
  disabled: false,
208
+ readonly: false,
205
209
  valueSuffix: '%',
206
210
  edgeStyle: 'flow',
207
211
  quality: 'auto',
@@ -392,7 +396,7 @@ function normalizeRgbArgs(args) {
392
396
  if (parts.length < 3) return null;
393
397
  const to255 = (value) => {
394
398
  if (typeof value !== 'string' || !value) return null;
395
- if (value.endsWith('%')) return clampChannel((parseFloat(value) / 100) * 255);
399
+ if (value.charAt(value.length - 1) === '%') return clampChannel((parseFloat(value) / 100) * 255);
396
400
  const number = Number(value);
397
401
  return Number.isFinite(number) ? clampChannel(number) : null;
398
402
  };
@@ -400,7 +404,11 @@ function normalizeRgbArgs(args) {
400
404
  const green = to255(parts[1]);
401
405
  const blue = to255(parts[2]);
402
406
  if (red === null || green === null || blue === null) return null;
403
- return `#${[red, green, blue].map((n) => n.toString(16).padStart(2, '0')).join('')}`;
407
+ const hex = (n) => {
408
+ const text = n.toString(16);
409
+ return text.length === 1 ? `0${text}` : text;
410
+ };
411
+ return `#${hex(red)}${hex(green)}${hex(blue)}`;
404
412
  }
405
413
 
406
414
  /**
@@ -757,12 +765,13 @@ function rgb(color, alpha = 1) {
757
765
  }
758
766
 
759
767
  class FallbackRenderer {
760
- constructor(canvas, preset) {
761
- this.canvas = canvas;
762
- this.preset = preset;
763
- this.context = canvas.getContext('2d');
764
- this.visible = true;
765
- }
768
+ constructor(canvas, preset) {
769
+ this.canvas = canvas;
770
+ this.preset = preset;
771
+ this.context = canvas.getContext('2d');
772
+ this.visible = true;
773
+ this.timePhase = 0;
774
+ }
766
775
 
767
776
  resize() {
768
777
  const rect = this.canvas.getBoundingClientRect();
@@ -778,7 +787,7 @@ class FallbackRenderer {
778
787
  drawNebula(time) {
779
788
  const ctx = this.context;
780
789
  const { width, height } = this.canvas;
781
- const t = time * (this.preset.speed || 1);
790
+ const t = (time + this.timePhase) * (this.preset.speed || 1);
782
791
  const phase = (this.preset.seed || 0) * 0.7;
783
792
  const gradient = ctx.createLinearGradient(0, 0, width, height);
784
793
  gradient.addColorStop(0, this.preset.colors[0]);
@@ -813,7 +822,10 @@ class FallbackRenderer {
813
822
  }
814
823
 
815
824
  randomize() {
816
- if (this.preset) this.preset.seed = Math.random() * 100;
825
+ if (this.preset) {
826
+ this.preset.seed = Math.random() * 100;
827
+ this.timePhase = Math.random() * Math.PI * 2;
828
+ }
817
829
  }
818
830
  setMouseColor() {}
819
831
  dispose() {}
@@ -1039,6 +1051,10 @@ function createCapsule(container, options = {}) {
1039
1051
  for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
1040
1052
  if (merged.textRatio !== undefined) {
1041
1053
  root.style.setProperty('--hj-text-width', `${merged.textRatio}%`);
1054
+ const userMinWidth = merged.cssVars && merged.cssVars['--hj-text-min-width'];
1055
+ if (merged.textRatio === 0) root.style.setProperty('--hj-text-min-width', '0');
1056
+ else if (userMinWidth) root.style.setProperty('--hj-text-min-width', userMinWidth);
1057
+ else root.style.removeProperty('--hj-text-min-width');
1042
1058
  }
1043
1059
  renderer.resize();
1044
1060
  };
@@ -1137,6 +1153,10 @@ function createCapsule(container, options = {}) {
1137
1153
  dirty.textRatio = true;
1138
1154
  merged.textRatio = value;
1139
1155
  root.style.setProperty('--hj-text-width', `${value}%`);
1156
+ const userMinWidth = merged.cssVars && merged.cssVars['--hj-text-min-width'];
1157
+ if (value === 0) root.style.setProperty('--hj-text-min-width', '0');
1158
+ else if (userMinWidth) root.style.setProperty('--hj-text-min-width', userMinWidth);
1159
+ else root.style.removeProperty('--hj-text-min-width');
1140
1160
  return this;
1141
1161
  },
1142
1162
  setCssVars(vars) {
@@ -1152,11 +1172,17 @@ function createCapsule(container, options = {}) {
1152
1172
  return this;
1153
1173
  },
1154
1174
  setSize(width, height) {
1155
- if (width !== undefined) merged.width = width;
1156
- if (height !== undefined) merged.height = height;
1157
- applySize();
1158
- return this;
1159
- },
1175
+ if (width !== undefined) {
1176
+ parseSize(width); // 非法尺寸直接抛错前先校验,避免污染内部状态
1177
+ merged.width = width;
1178
+ }
1179
+ if (height !== undefined) {
1180
+ parseSize(height);
1181
+ merged.height = height;
1182
+ }
1183
+ applySize();
1184
+ return this;
1185
+ },
1160
1186
  randomize() {
1161
1187
  this.setSeed(Math.random() * 100);
1162
1188
  return this;
@@ -1183,8 +1209,8 @@ function createCapsule(container, options = {}) {
1183
1209
  renderer.dispose();
1184
1210
  root.remove();
1185
1211
  },
1186
- textRatio: merged.textRatio,
1187
- cssVars: merged.cssVars,
1212
+ get textRatio() { return merged.textRatio; },
1213
+ get cssVars() { return merged.cssVars; },
1188
1214
  dirty
1189
1215
  };
1190
1216
  }
@@ -1669,13 +1695,17 @@ const PALETTES = {
1669
1695
  'tide': ['#0a2239', '#2e9bff', '#7fe3ff', '#eaf9ff']
1670
1696
  };
1671
1697
 
1672
- function drawCloud(context, x, y, radiusX, radiusY, color, alpha) {
1673
- context.save();
1674
- context.translate(x, y);
1675
- context.scale(1, radiusY / radiusX);
1676
- const gradient = context.createRadialGradient(0, 0, 0, 0, 0, radiusX);
1677
- gradient.addColorStop(0, `${color}${Math.round(alpha * 255).toString(16).padStart(2, '0')}`);
1678
- gradient.addColorStop(0.46, `${color}${Math.round(alpha * 0.42 * 255).toString(16).padStart(2, '0')}`);
1698
+ function drawCloud(context, x, y, radiusX, radiusY, color, alpha) {
1699
+ context.save();
1700
+ context.translate(x, y);
1701
+ context.scale(1, radiusY / radiusX);
1702
+ const gradient = context.createRadialGradient(0, 0, 0, 0, 0, radiusX);
1703
+ const alphaHex = (value) => {
1704
+ const text = Math.round(value).toString(16);
1705
+ return text.length === 1 ? `0${text}` : text;
1706
+ };
1707
+ gradient.addColorStop(0, `${color}${alphaHex(alpha * 255)}`);
1708
+ gradient.addColorStop(0.46, `${color}${alphaHex(alpha * 0.42 * 255)}`);
1679
1709
  gradient.addColorStop(1, `${color}00`);
1680
1710
  context.fillStyle = gradient;
1681
1711
  context.fillRect(-radiusX, -radiusX, radiusX * 2, radiusX * 2);
@@ -2105,9 +2135,14 @@ class ProgressCapsuleController {
2105
2135
  ctx.restore();
2106
2136
  }
2107
2137
 
2108
- setRange(min, max) {
2109
- this.min = min;
2110
- this.max = max;
2138
+ setRange(min, max) {
2139
+ if (min > max) {
2140
+ const swap = min;
2141
+ min = max;
2142
+ max = swap;
2143
+ }
2144
+ this.min = min;
2145
+ this.max = max;
2111
2146
  const clamped = clamp(this.value, this.min, this.max);
2112
2147
  if (clamped !== this.value) this.setProgress(clamped, 'prop');
2113
2148
  }
@@ -2246,6 +2281,8 @@ class ProgressCapsuleController {
2246
2281
  beginDrag(event) {
2247
2282
  if (event.button !== undefined && event.button !== 0) return;
2248
2283
  event.preventDefault();
2284
+ // preventDefault 会吞掉点击聚焦,主动聚焦让键盘方向键立即可用。
2285
+ try { this.root.focus({ preventScroll: true }); } catch { this.root.focus(); }
2249
2286
  this.dragging = true;
2250
2287
  this.pendingPointer = null;
2251
2288
  this._dragRaf = 0;
@@ -2300,33 +2337,7 @@ class ProgressCapsuleController {
2300
2337
  this.root.addEventListener('pointercancel', this.handlers.pointercancel);
2301
2338
  }
2302
2339
 
2303
- if (this.options.keyboard !== false) {
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
- };
2314
- if (event.key === 'Home') {
2315
- event.preventDefault();
2316
- this.setProgress(this.min, 'keyboard');
2317
- } else if (event.key === 'End') {
2318
- event.preventDefault();
2319
- this.setProgress(this.max, 'keyboard');
2320
- } else if (actions[event.key]) {
2321
- event.preventDefault();
2322
- this.setProgress(this.value + actions[event.key], 'keyboard');
2323
- } else {
2324
- return;
2325
- }
2326
- };
2327
- this.root.addEventListener('keydown', this.handlers.keydown);
2328
- }
2329
- }
2340
+ }
2330
2341
 
2331
2342
  update(delta, paused) {
2332
2343
  if (!paused) this.flowTime += delta;
@@ -2364,7 +2375,7 @@ class ProgressCapsuleController {
2364
2375
  * Mount a fluid progress capsule into `container`.
2365
2376
  *
2366
2377
  * Options: preset (id/code/name or object), width, height, value, min, max,
2367
- * draggable, keyboard, colors, edgeStyle, textRatio (0-100, text region
2378
+ * draggable, colors, edgeStyle, textRatio (0-100, text region
2368
2379
  * width in percent), text (HTML string or DOM nodes for the text slot),
2369
2380
  * colorContent (HTML string or DOM nodes for the color slot),
2370
2381
  * showValue (show/hide the right-side percentage), quality,
@@ -2377,6 +2388,11 @@ function createProgressCapsule(container, options = {}) {
2377
2388
 
2378
2389
  const preset = { ...getPreset('progress', options.preset ?? '星火') };
2379
2390
  const merged = normalizeOptions(DEFAULTS.progress, preset, options);
2391
+ if (merged.min > merged.max) {
2392
+ const swap = merged.min;
2393
+ merged.min = merged.max;
2394
+ merged.max = swap;
2395
+ }
2380
2396
  const normalizedColors = merged.colors.map(normalizeColor);
2381
2397
  if (normalizedColors.every(Boolean)) preset.colors = normalizedColors;
2382
2398
  preset.edgeStyle = merged.edgeStyle;
@@ -2385,8 +2401,9 @@ function createProgressCapsule(container, options = {}) {
2385
2401
  // Vue 的裸布尔属性(<ProgressCapsule disabled />)会传成空字符串,
2386
2402
  // 这里统一按 true 处理。
2387
2403
  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;
2404
+ const readonly = merged.readonly === true || merged.readonly === '' || merged.readonly === 'true';
2405
+ const locked = disabled || readonly;
2406
+ const effectiveDraggable = locked ? false : merged.draggable !== false;
2390
2407
  const dirty = {
2391
2408
  preset: false,
2392
2409
  colors: false,
@@ -2399,12 +2416,16 @@ function createProgressCapsule(container, options = {}) {
2399
2416
  const root = document.createElement('div');
2400
2417
  root.className = 'hj-capsule-root hj-progress-root';
2401
2418
  root.setAttribute('role', 'slider');
2402
- root.setAttribute('tabindex', '0');
2403
2419
  root.setAttribute('data-draggable', String(effectiveDraggable));
2404
2420
  if (disabled) {
2405
2421
  root.setAttribute('aria-disabled', 'true');
2406
2422
  root.classList.add('is-disabled');
2407
2423
  }
2424
+ if (readonly) {
2425
+ root.setAttribute('aria-readonly', 'true');
2426
+ root.classList.add('is-readonly');
2427
+ }
2428
+ if (locked) root.setAttribute('tabindex', '-1');
2408
2429
  root.setAttribute('aria-valuemin', String(merged.min ?? 0));
2409
2430
  root.setAttribute('aria-valuemax', String(merged.max ?? 100));
2410
2431
  root.setAttribute('aria-valuenow', String(preset.initialProgress));
@@ -2415,8 +2436,6 @@ function createProgressCapsule(container, options = {}) {
2415
2436
  );
2416
2437
  };
2417
2438
  updateAria();
2418
- if (!effectiveKeyboard) root.setAttribute('tabindex', '-1');
2419
-
2420
2439
  const canvas = document.createElement('canvas');
2421
2440
  canvas.className = 'hj-progress-canvas';
2422
2441
  canvas.setAttribute('aria-hidden', 'true');
@@ -2478,7 +2497,7 @@ function createProgressCapsule(container, options = {}) {
2478
2497
  valueElement,
2479
2498
  preset,
2480
2499
  emitter,
2481
- options: { ...merged, draggable: effectiveDraggable, keyboard: effectiveKeyboard },
2500
+ options: { ...merged, draggable: effectiveDraggable },
2482
2501
  copy,
2483
2502
  dirty
2484
2503
  });
@@ -2619,10 +2638,16 @@ function createProgressCapsule(container, options = {}) {
2619
2638
  controller.resizeCanvas();
2620
2639
  return this;
2621
2640
  },
2622
- setSize(width, height) {
2623
- if (width !== undefined) merged.width = width;
2624
- if (height !== undefined) merged.height = height;
2625
- applySize();
2641
+ setSize(width, height) {
2642
+ if (width !== undefined) {
2643
+ parseSize(width);
2644
+ merged.width = width;
2645
+ }
2646
+ if (height !== undefined) {
2647
+ parseSize(height);
2648
+ merged.height = height;
2649
+ }
2650
+ applySize();
2626
2651
  controller.resizeCanvas();
2627
2652
  return this;
2628
2653
  },
@@ -2652,8 +2677,8 @@ function createProgressCapsule(container, options = {}) {
2652
2677
  controller.dispose();
2653
2678
  root.remove();
2654
2679
  },
2655
- textRatio: merged.textRatio,
2656
- cssVars: merged.cssVars,
2680
+ get textRatio() { return merged.textRatio; },
2681
+ get cssVars() { return merged.cssVars; },
2657
2682
  dirty
2658
2683
  };
2659
2684
  }
@@ -2873,9 +2898,9 @@ function createColorBackground(host, options = {}) {
2873
2898
  renderer.dispose();
2874
2899
  layer.remove();
2875
2900
  },
2876
- opacity: merged.opacity,
2877
- fallbackColor: merged.fallbackColor,
2878
- mouseColor: merged.mouseColor,
2901
+ get opacity() { return merged.opacity; },
2902
+ get fallbackColor() { return merged.fallbackColor; },
2903
+ get mouseColor() { return merged.mouseColor; },
2879
2904
  dirty
2880
2905
  };
2881
2906
  }
@@ -2911,6 +2936,9 @@ function makeWrapper({ name, props, events, create, render, methods, watch }) {
2911
2936
  const extraState = this.runtimeState();
2912
2937
  if (this.controller) this.controller.dispose();
2913
2938
  this.mountController(extraState);
2939
+ // 结构属性连续变化时,插槽内容可能在重建后才被 Vue 写入容器,
2940
+ // 补一次搬运确保不丢。
2941
+ this.$nextTick(() => this.moveSlots());
2914
2942
  },
2915
2943
  scheduleRecreate() {
2916
2944
  if (this._recreateQueued) return;
@@ -2925,13 +2953,15 @@ function makeWrapper({ name, props, events, create, render, methods, watch }) {
2925
2953
  const root = this.controller.element;
2926
2954
  const textLayer = root.querySelector('.hj-capsule-text, .hj-progress-text');
2927
2955
  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
2956
  const refs = this.$refs || {};
2933
- move(refs.slotText, textLayer);
2934
- move(refs.slotColor, visualLayer);
2957
+ // 把插槽容器整体搬进对应区域,而不是搬子节点:Vue 重渲染/重建组件时
2958
+ // 内容始终留在区域内,不会丢(子节点搬运会在 recreate 时序下丢失)。
2959
+ if (refs.slotText && textLayer && refs.slotText.parentElement !== textLayer) {
2960
+ textLayer.appendChild(refs.slotText);
2961
+ }
2962
+ if (refs.slotColor && visualLayer && refs.slotColor.parentElement !== visualLayer) {
2963
+ visualLayer.appendChild(refs.slotColor);
2964
+ }
2935
2965
  },
2936
2966
  applyAttrs() {
2937
2967
  if (!this.controller || !this.controller.element || !this.$attrs) return;
@@ -3058,7 +3088,7 @@ function makeProgressWrapper(createProgressCapsule, render) {
3058
3088
  name: 'ProgressCapsule',
3059
3089
  props: [
3060
3090
  'preset', 'width', 'height', 'value', 'min', 'max',
3061
- 'draggable', 'keyboard', 'disabled', 'edgeStyle', 'colors', 'textRatio', 'label', 'showValue', 'valueSuffix',
3091
+ 'draggable', 'disabled', 'readonly', 'edgeStyle', 'colors', 'textRatio', 'label', 'showValue', 'valueSuffix',
3062
3092
  'quality', 'renderer', 'respectReducedMotion', 'cssVars'
3063
3093
  ],
3064
3094
  events: PROGRESS_EVENTS,
@@ -3095,12 +3125,12 @@ function makeProgressWrapper(createProgressCapsule, render) {
3095
3125
  disabled() {
3096
3126
  this.scheduleRecreate();
3097
3127
  },
3128
+ readonly() {
3129
+ this.scheduleRecreate();
3130
+ },
3098
3131
  draggable() {
3099
3132
  this.scheduleRecreate();
3100
3133
  },
3101
- keyboard() {
3102
- this.scheduleRecreate();
3103
- },
3104
3134
  renderer() {
3105
3135
  this.scheduleRecreate();
3106
3136
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@5even7/dlc-ui",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/styles/base.css CHANGED
@@ -35,8 +35,8 @@
35
35
  box-sizing: border-box;
36
36
  }
37
37
 
38
- .hj-capsule-canvas,
39
- .hj-progress-canvas {
38
+ .hj-capsule-canvas,
39
+ .hj-progress-canvas {
40
40
  position: absolute;
41
41
  inset: 0;
42
42
  z-index: 1;
@@ -44,5 +44,13 @@
44
44
  width: 100%;
45
45
  height: 100%;
46
46
  border-radius: inherit;
47
- pointer-events: none;
48
- }
47
+ pointer-events: none;
48
+ }
49
+
50
+ /* Vue wrapper 的插槽容器会被搬到文字区/颜色区里,撑满区域以便内容正常布局。 */
51
+ .hj-vue-slot-text,
52
+ .hj-vue-slot-color {
53
+ display: block;
54
+ width: 100%;
55
+ height: 100%;
56
+ }
@@ -23,6 +23,10 @@
23
23
  outline: none;
24
24
  }
25
25
 
26
+ .hj-progress-root.is-readonly {
27
+ cursor: default;
28
+ }
29
+
26
30
  .hj-progress-root:focus-visible {
27
31
  outline: 2px solid rgba(255, 255, 255, 0.8);
28
32
  outline-offset: 4px;
package/types/index.d.ts CHANGED
@@ -59,8 +59,8 @@ export interface ProgressOptions {
59
59
  min?: number;
60
60
  max?: number;
61
61
  draggable?: boolean;
62
- keyboard?: boolean;
63
62
  disabled?: boolean;
63
+ readonly?: boolean;
64
64
  valueSuffix?: string;
65
65
  edgeStyle?: 'flow' | 'tide';
66
66
  colors?: string[];
@@ -180,7 +180,7 @@ export declare function getPreset(kind: 'capsule' | 'progress', ref: string): Ca
180
180
 
181
181
  export declare const DEFAULTS: {
182
182
  capsule: Required<Pick<CapsuleOptions, 'width' | 'height' | 'quality' | 'respectReducedMotion' | 'mouseColor' | 'renderer' | 'textRatio'>>;
183
- progress: Required<Pick<ProgressOptions, 'width' | 'height' | 'min' | 'max' | 'draggable' | 'keyboard' | 'disabled' | 'valueSuffix' | 'edgeStyle' | 'quality' | 'respectReducedMotion' | 'renderer' | 'textRatio' | 'showValue'>>;
183
+ progress: Required<Pick<ProgressOptions, 'width' | 'height' | 'min' | 'max' | 'draggable' | 'disabled' | 'readonly' | 'valueSuffix' | 'edgeStyle' | 'quality' | 'respectReducedMotion' | 'renderer' | 'textRatio' | 'showValue'>>;
184
184
  };
185
185
 
186
186
  export declare const COPY: Copy;