vanilla-jui 1.4.5 → 1.4.7

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.
Files changed (103) hide show
  1. package/README.md +6 -0
  2. package/dist/index.d.ts +1273 -1896
  3. package/dist/index.js +1 -1
  4. package/dist/index.umd.js +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +21 -2
  7. package/src/components/accordion.ts +297 -216
  8. package/src/components/flow.ts +222 -258
  9. package/src/components/form.ts +44 -102
  10. package/src/components/menu.ts +268 -356
  11. package/src/components/modal.ts +181 -641
  12. package/src/components/offcanvas.ts +225 -106
  13. package/src/components/pagination.ts +296 -326
  14. package/src/components/sticky.ts +55 -54
  15. package/src/components/swiper.ts +237 -247
  16. package/src/components/tabs.ts +278 -271
  17. package/src/components/toc.ts +42 -46
  18. package/src/core/Component.ts +69 -47
  19. package/src/css/accordion.css +2 -2
  20. package/src/css/animation.css +1 -5
  21. package/src/css/avatar.css +1 -1
  22. package/src/css/badge.css +1 -1
  23. package/src/css/breadcrumb.css +9 -10
  24. package/src/css/button.css +1 -1
  25. package/src/css/card.css +2 -2
  26. package/src/css/color.css +974 -1002
  27. package/src/css/color_p3.css +974 -1004
  28. package/src/css/components.css +1 -1
  29. package/src/css/content.css +72 -63
  30. package/src/css/drop.css +1 -1
  31. package/src/css/editor.css +14 -14
  32. package/src/css/flow.css +4 -14
  33. package/src/css/font.css +5 -4
  34. package/src/css/form.css +76 -52
  35. package/src/css/gradient.css +299 -222
  36. package/src/css/icon.css +57 -57
  37. package/src/css/index.css +1 -1
  38. package/src/css/layout.css +13 -10
  39. package/src/css/loading.css +60 -12
  40. package/src/css/menu.css +262 -188
  41. package/src/css/modal.css +1 -1
  42. package/src/css/offcanvas.css +9 -13
  43. package/src/css/pagination.css +5 -1
  44. package/src/css/popup.css +1 -1
  45. package/src/css/skeleton.css +3 -2
  46. package/src/css/status.css +2 -3
  47. package/src/css/swiper.css +1 -1
  48. package/src/css/table.css +1 -1
  49. package/src/css/tabs.css +12 -11
  50. package/src/css/tag.css +1 -1
  51. package/src/css/theme-palette.css +19 -5
  52. package/src/css/themes/default/accordion.css +61 -61
  53. package/src/css/themes/default/avatar.css +54 -54
  54. package/src/css/themes/default/badge.css +62 -62
  55. package/src/css/themes/default/breadcrumb.css +23 -23
  56. package/src/css/themes/default/button.css +203 -203
  57. package/src/css/themes/default/content.css +53 -53
  58. package/src/css/themes/default/drop.css +10 -10
  59. package/src/css/themes/default/editor.css +9 -9
  60. package/src/css/themes/default/index.css +1 -1
  61. package/src/css/themes/default/loading.css +26 -20
  62. package/src/css/themes/default/menu.css +75 -11
  63. package/src/css/themes/default/modal.css +32 -36
  64. package/src/css/themes/default/pagination.css +7 -7
  65. package/src/css/themes/default/popup.css +6 -6
  66. package/src/css/themes/default/swiper.css +32 -32
  67. package/src/css/themes/default/table.css +16 -16
  68. package/src/css/themes/default/tabs.css +30 -30
  69. package/src/css/themes/default/tag.css +71 -71
  70. package/src/css/themes/default/theme-palette.css +11 -11
  71. package/src/css/themes/default/tip.css +42 -42
  72. package/src/css/themes/default/toast.css +60 -60
  73. package/src/css/themes/default/toolbar.css +23 -29
  74. package/src/css/themes/default/tooltip.css +43 -13
  75. package/src/css/themes/index.css +1 -1
  76. package/src/css/tip.css +2 -2
  77. package/src/css/toast.css +3 -3
  78. package/src/css/tokens.css +12 -17
  79. package/src/css/toolbar.css +9 -10
  80. package/src/css/tooltip.css +3 -2
  81. package/src/index.ts +22 -19
  82. package/src/{components → primitives}/drop.ts +58 -49
  83. package/src/{components → primitives}/icons.ts +1 -1
  84. package/src/primitives/loading.ts +23 -0
  85. package/src/primitives/parabola.ts +388 -0
  86. package/src/primitives/popup.ts +61 -0
  87. package/src/{components → primitives}/theme.ts +89 -57
  88. package/src/{components → primitives}/toast.ts +18 -25
  89. package/src/{components → primitives}/tooltip.ts +100 -25
  90. package/src/utilities/browser.ts +23 -49
  91. package/src/utilities/class-name.ts +5 -0
  92. package/src/utilities/dom.ts +14 -32
  93. package/src/utilities/function.ts +4 -0
  94. package/src/utilities/http.ts +7 -7
  95. package/src/utilities/id.ts +57 -0
  96. package/src/utilities/object.ts +5 -0
  97. package/src/utilities/timer.ts +38 -0
  98. package/src/utilities/types.ts +420 -0
  99. package/src/{components → validation}/validator.ts +64 -53
  100. package/src/components/parabola.ts +0 -335
  101. package/src/utilities/cache.ts +0 -57
  102. package/src/utilities/core.ts +0 -614
  103. package/src/utilities/storage.ts +0 -46
@@ -7,30 +7,21 @@ import {
7
7
  render,
8
8
  } from 'vanilla-signal';
9
9
 
10
+ import { type RenderableContent } from '../utilities/dom.ts';
11
+ import { randomId } from '../utilities/id.ts';
12
+ import { isPlainObject } from '../utilities/object.ts';
10
13
  import {
11
14
  type ResolveSchema,
12
- isPlainObject,
13
- randomId,
14
15
  resolveProps,
15
16
  validateParam,
16
- } from '../utilities/core.ts';
17
- import {
18
- type DOMReference,
19
- type RenderableContent,
20
- isRenderableContent,
21
- requireContainer,
22
- } from '../utilities/dom.ts';
17
+ } from '../utilities/types.ts';
23
18
 
24
19
  export type FlowData = Record<string, unknown>;
25
20
  export type FlowPayload = FlowData | null;
26
21
  export type FlowAction = 'next' | 'back' | 'goTo' | 'finish';
27
22
  export type FlowBusyStrategy = 'ignore' | 'throw';
28
23
  export type FlowDirection = string;
29
- export type FlowSlotName =
30
- | 'renderHeader'
31
- | 'renderSteps'
32
- | 'renderBody'
33
- | 'renderFooter';
24
+ export type FlowSlotName = 'renderHeader' | 'renderBody' | 'renderFooter';
34
25
  export type FlowCleanup = () => void;
35
26
  export type FlowStepResult =
36
27
  | string
@@ -42,8 +33,6 @@ export type FlowStepResult =
42
33
  export interface FlowClassNames {
43
34
  root: string;
44
35
  header: string;
45
- title: string;
46
- description: string;
47
36
  steps: string;
48
37
  step: string;
49
38
  active: string;
@@ -54,8 +43,6 @@ export interface FlowClassNames {
54
43
  body: string;
55
44
  footer: string;
56
45
  button: string;
57
- buttonGhost: string;
58
- buttonPrimary: string;
59
46
  reset: string;
60
47
  back: string;
61
48
  next: string;
@@ -66,11 +53,9 @@ export type FlowClassNameConfig = Partial<FlowClassNames>;
66
53
  export interface FlowStep {
67
54
  id: string;
68
55
  title?: string;
69
- description?: string;
70
56
  content?: RenderableContent<FlowContext>;
71
57
  data?: FlowData;
72
58
  modal?: FlowData | ((context: FlowContext) => FlowData | null) | null;
73
- view?: FlowData;
74
59
  onEnter?: FlowLifecycleHook;
75
60
  onLeave?: FlowLifecycleHook;
76
61
  onNext?: FlowMoveHook;
@@ -199,7 +184,7 @@ export interface FlowText {
199
184
  [key: string]: string;
200
185
  }
201
186
 
202
- export interface FlowOptions extends Record<string, unknown> {
187
+ export interface FlowProps extends Record<string, unknown> {
203
188
  id?: string | null;
204
189
  steps?: FlowStep[];
205
190
  initial?: string | number | null;
@@ -208,16 +193,12 @@ export interface FlowOptions extends Record<string, unknown> {
208
193
  render?: boolean;
209
194
  rollbackOnError?: boolean;
210
195
  busyStrategy?: FlowBusyStrategy;
211
- showHeader?: boolean;
212
- showFooter?: boolean;
213
- showSteps?: boolean;
214
196
  showBack?: boolean;
215
197
  showNext?: boolean;
216
198
  showReset?: boolean;
217
199
  text?: Partial<FlowText>;
218
200
  className?: FlowClassNameConfig | string;
219
201
  renderHeader?: FlowSlot;
220
- renderSteps?: FlowSlot;
221
202
  renderBody?: FlowSlot;
222
203
  renderFooter?: FlowSlot;
223
204
  onChange?: FlowChangeHook | null;
@@ -228,7 +209,7 @@ export interface FlowOptions extends Record<string, unknown> {
228
209
  onBusy?: FlowBusyHook | null;
229
210
  }
230
211
 
231
- interface ResolvedFlowOptions extends Record<string, unknown> {
212
+ interface ResolvedFlowProps extends Record<string, unknown> {
232
213
  id: string;
233
214
  steps: FlowStep[];
234
215
  initial: string | number | null;
@@ -237,16 +218,12 @@ interface ResolvedFlowOptions extends Record<string, unknown> {
237
218
  render: boolean;
238
219
  rollbackOnError: boolean;
239
220
  busyStrategy: FlowBusyStrategy;
240
- showHeader: boolean;
241
- showFooter: boolean;
242
- showSteps: boolean;
243
221
  showBack: boolean;
244
222
  showNext: boolean;
245
223
  showReset: boolean;
246
224
  text: FlowText;
247
225
  className: FlowClassNames;
248
226
  renderHeader: FlowSlot;
249
- renderSteps: FlowSlot;
250
227
  renderBody: FlowSlot;
251
228
  renderFooter: FlowSlot;
252
229
  onChange: FlowChangeHook | null;
@@ -278,8 +255,18 @@ interface InitialFlowData {
278
255
  global: FlowData;
279
256
  }
280
257
 
281
- interface FlowNodes {
282
- root?: HTMLElement;
258
+ interface FlowDOM {
259
+ root: HTMLElement | null;
260
+ header: HTMLElement | null;
261
+ body: HTMLElement | null;
262
+ footer: HTMLElement | null;
263
+ }
264
+
265
+ interface FlowRuntime {
266
+ built: boolean;
267
+ destroyed: boolean;
268
+ activeAction: FlowAction | null;
269
+ actionController: AbortController | null;
283
270
  }
284
271
 
285
272
  interface FlowGoToOptions {
@@ -327,8 +314,6 @@ interface FlowError extends Error {
327
314
  const DEFAULT_CLASS_NAMES: FlowClassNames = {
328
315
  root: 'j-flow',
329
316
  header: 'flow-header',
330
- title: 'flow-title',
331
- description: 'flow-description',
332
317
  steps: 'flow-steps',
333
318
  step: 'flow-step',
334
319
  active: 'is-active',
@@ -339,27 +324,17 @@ const DEFAULT_CLASS_NAMES: FlowClassNames = {
339
324
  body: 'flow-body',
340
325
  footer: 'flow-footer',
341
326
  button: 'j-button',
342
- buttonGhost: 'is-ghost',
343
- buttonPrimary: 'is-primary',
344
- reset: 'flow-reset',
345
- back: 'flow-back',
346
- next: 'flow-next',
327
+ reset: 'is-ghost flow-reset',
328
+ back: 'is-ghost flow-back',
329
+ next: 'is-primary flow-next',
347
330
  };
348
331
 
349
- function isFlowStep(step: unknown): step is FlowStep {
350
- return (
351
- !!step &&
352
- typeof step === 'object' &&
353
- typeof (step as FlowStep).id === 'string'
354
- );
355
- }
356
-
357
332
  function isFlowRenderSlot(slot: unknown): slot is FlowSlot {
358
333
  return slot == null || slot === false || typeof slot === 'function';
359
334
  }
360
335
 
361
336
  function clonePlainObject(value: unknown): FlowData {
362
- return isPlainObject(value) ? { ...value } : {};
337
+ return isPlainObject(value) ? { ...(value as FlowData) } : {};
363
338
  }
364
339
 
365
340
  function cloneArray<T>(value: unknown): T[] {
@@ -369,7 +344,10 @@ function cloneArray<T>(value: unknown): T[] {
369
344
  function cloneSteps(steps: unknown): FlowStep[] {
370
345
  if (!Array.isArray(steps)) return [];
371
346
  return steps.map((step) => {
372
- const source = step as FlowStep;
347
+ const source = { ...(step as FlowStep & Record<string, unknown>) };
348
+ delete source.description;
349
+ delete source.view;
350
+
373
351
  return {
374
352
  ...source,
375
353
  data: clonePlainObject(source.data),
@@ -377,7 +355,6 @@ function cloneSteps(steps: unknown): FlowStep[] {
377
355
  typeof source.modal === 'function' || source.modal == null
378
356
  ? source.modal
379
357
  : clonePlainObject(source.modal),
380
- view: clonePlainObject(source.view),
381
358
  };
382
359
  });
383
360
  }
@@ -387,13 +364,17 @@ function normalizeStepResult(
387
364
  fallbackId: string
388
365
  ): { id: string; data?: FlowPayload } {
389
366
  if (typeof result === 'string') return { id: result };
390
- if (isPlainObject(result) && typeof result.id === 'string') {
367
+ if (isPlainObject(result)) {
368
+ const source = result as { id?: unknown; data?: unknown };
369
+ if (typeof source.id !== 'string') return { id: fallbackId };
370
+ const data: FlowPayload | undefined = isPlainObject(source.data)
371
+ ? (source.data as FlowData)
372
+ : source.data == null
373
+ ? source.data
374
+ : undefined;
391
375
  return {
392
- id: result.id,
393
- data:
394
- isPlainObject(result.data) || result.data == null
395
- ? result.data
396
- : undefined,
376
+ id: source.id,
377
+ data,
397
378
  };
398
379
  }
399
380
  return { id: fallbackId };
@@ -412,7 +393,7 @@ function normalizeClassNames(value: unknown): FlowClassNames {
412
393
 
413
394
  return {
414
395
  ...DEFAULT_CLASS_NAMES,
415
- ...(isPlainObject(value) ? value : {}),
396
+ ...(isPlainObject(value) ? (value as Partial<FlowClassNames>) : {}),
416
397
  };
417
398
  }
418
399
 
@@ -423,23 +404,23 @@ function joinClasses(
423
404
  }
424
405
 
425
406
  const FLOW_STEP_RULE = {
426
- validate: isFlowStep,
427
- message: 'expects a step object with a string id.',
407
+ type: 'plainObject',
408
+ shape: {
409
+ id: { type: 'string', nonEmpty: true },
410
+ },
428
411
  };
429
412
 
430
413
  const FLOW_CONTENT_RULE = {
431
- validate: isRenderableContent,
432
- message: 'expects string, Node, array, function or null.',
414
+ type: 'renderable',
433
415
  };
434
416
 
435
417
  const FLOW_STEPS_RULE = {
436
- validate: (value: unknown) => Array.isArray(value) && value.length > 0,
437
- message: 'expects a non-empty steps array.',
418
+ type: 'array',
419
+ nonEmpty: true,
438
420
  };
439
421
 
440
422
  const FLOW_PAYLOAD_RULE = {
441
- validate: (value: unknown) => value == null || isPlainObject(value),
442
- message: 'expects an object or null.',
423
+ types: ['plainObject', 'null', 'undefined'],
443
424
  };
444
425
 
445
426
  const FLOW_RENDER_SLOT_RULE = {
@@ -449,10 +430,10 @@ const FLOW_RENDER_SLOT_RULE = {
449
430
 
450
431
  const FLOW_TEXT_RULE = {
451
432
  default: {},
452
- validate: isPlainObject,
453
- message: 'expects an object with text fields.',
433
+ type: 'plainObject',
454
434
  normalize: (value: unknown): FlowText => {
455
- const text = isPlainObject(value) ? value : {};
435
+ const text = (isPlainObject(value) ? value : {}) as Partial<FlowText> &
436
+ Record<string, unknown>;
456
437
  return {
457
438
  ...Object.fromEntries(
458
439
  Object.entries(text).filter(([, item]) => typeof item === 'string')
@@ -465,7 +446,7 @@ const FLOW_TEXT_RULE = {
465
446
  },
466
447
  };
467
448
 
468
- const FLOW_OPTIONS_SCHEMA = {
449
+ const FLOW_PROPS_SCHEMA = {
469
450
  id: {
470
451
  default: null,
471
452
  types: ['string', 'null'],
@@ -489,9 +470,6 @@ const FLOW_OPTIONS_SCHEMA = {
489
470
  type: 'string',
490
471
  enum: ['ignore', 'throw'],
491
472
  },
492
- showHeader: { default: true, type: 'boolean' },
493
- showFooter: { default: true, type: 'boolean' },
494
- showSteps: { default: true, type: 'boolean' },
495
473
  showBack: { default: true, type: 'boolean' },
496
474
  showNext: { default: true, type: 'boolean' },
497
475
  showReset: { default: false, type: 'boolean' },
@@ -502,7 +480,6 @@ const FLOW_OPTIONS_SCHEMA = {
502
480
  normalize: normalizeClassNames,
503
481
  },
504
482
  renderHeader: { default: null, ...FLOW_RENDER_SLOT_RULE },
505
- renderSteps: { default: null, ...FLOW_RENDER_SLOT_RULE },
506
483
  renderBody: { default: null, ...FLOW_RENDER_SLOT_RULE },
507
484
  renderFooter: { default: null, ...FLOW_RENDER_SLOT_RULE },
508
485
  onChange: { default: null, types: ['function', 'null'] },
@@ -511,37 +488,33 @@ const FLOW_OPTIONS_SCHEMA = {
511
488
  onFinish: { default: null, types: ['function', 'null'] },
512
489
  onError: { default: null, types: ['function', 'null'] },
513
490
  onBusy: { default: null, types: ['function', 'null'] },
514
- } satisfies ResolveSchema<FlowOptions>;
491
+ } satisfies ResolveSchema<FlowProps>;
515
492
 
516
- function normalizeOptions(input: FlowOptions): ResolvedFlowOptions {
517
- const options = resolveProps(input, FLOW_OPTIONS_SCHEMA, 'Flow.options');
493
+ function normalizeProps(input: FlowProps): ResolvedFlowProps {
494
+ const props = resolveProps(input, FLOW_PROPS_SCHEMA, 'Flow.props');
518
495
  return {
519
- id: options.id as string,
520
- steps: cloneSteps(options.steps),
521
- initial: options.initial as ResolvedFlowOptions['initial'],
522
- cache: options.cache as boolean,
523
- linear: options.linear as boolean,
524
- render: options.render as boolean,
525
- rollbackOnError: options.rollbackOnError as boolean,
526
- busyStrategy: options.busyStrategy as FlowBusyStrategy,
527
- showHeader: options.showHeader as boolean,
528
- showFooter: options.showFooter as boolean,
529
- showSteps: options.showSteps as boolean,
530
- showBack: options.showBack as boolean,
531
- showNext: options.showNext as boolean,
532
- showReset: options.showReset as boolean,
533
- text: options.text as FlowText,
534
- className: options.className as FlowClassNames,
535
- renderHeader: options.renderHeader as FlowSlot,
536
- renderSteps: options.renderSteps as FlowSlot,
537
- renderBody: options.renderBody as FlowSlot,
538
- renderFooter: options.renderFooter as FlowSlot,
539
- onChange: options.onChange as FlowChangeHook | null,
540
- onNext: options.onNext as FlowMoveHook | null,
541
- onBack: options.onBack as FlowMoveHook | null,
542
- onFinish: options.onFinish as FlowFinishHook | null,
543
- onError: options.onError as FlowErrorHook | null,
544
- onBusy: options.onBusy as FlowBusyHook | null,
496
+ id: props.id as string,
497
+ steps: cloneSteps(props.steps),
498
+ initial: props.initial as ResolvedFlowProps['initial'],
499
+ cache: props.cache as boolean,
500
+ linear: props.linear as boolean,
501
+ render: props.render as boolean,
502
+ rollbackOnError: props.rollbackOnError as boolean,
503
+ busyStrategy: props.busyStrategy as FlowBusyStrategy,
504
+ showBack: props.showBack as boolean,
505
+ showNext: props.showNext as boolean,
506
+ showReset: props.showReset as boolean,
507
+ text: props.text as FlowText,
508
+ className: props.className as FlowClassNames,
509
+ renderHeader: props.renderHeader as FlowSlot,
510
+ renderBody: props.renderBody as FlowSlot,
511
+ renderFooter: props.renderFooter as FlowSlot,
512
+ onChange: props.onChange as FlowChangeHook | null,
513
+ onNext: props.onNext as FlowMoveHook | null,
514
+ onBack: props.onBack as FlowMoveHook | null,
515
+ onFinish: props.onFinish as FlowFinishHook | null,
516
+ onError: props.onError as FlowErrorHook | null,
517
+ onBusy: props.onBusy as FlowBusyHook | null,
545
518
  };
546
519
  }
547
520
 
@@ -550,44 +523,48 @@ function normalizeOptions(input: FlowOptions): ResolvedFlowOptions {
550
523
  *
551
524
  * 适合在 Modal、Offcanvas、页面表单或任意业务组件中复用 next/back/goTo、步骤缓存和生命周期。
552
525
  */
553
- export class Flow {
554
- options: ResolvedFlowOptions;
526
+ class Flow {
527
+ props: ResolvedFlowProps;
555
528
  steps: FlowStep[];
556
529
  state: FlowState;
557
- root: HTMLElement | null;
530
+ dom: FlowDOM;
531
+ runtime: FlowRuntime;
558
532
  private stepMap: Map<string, number>;
559
533
  private initialStepId: string;
560
534
  private initialData: InitialFlowData;
561
535
  private subscribers: Set<FlowSubscriber>;
562
536
  private renderDispose: FlowCleanup | null;
563
537
  private cleanupTasks: Set<FlowCleanup>;
564
- private nodes: FlowNodes;
565
- private destroyed: boolean;
566
- private activeAction: FlowAction | null;
567
- private actionController: AbortController | null;
568
538
 
569
539
  /**
570
540
  * 创建 Flow 实例。
571
- * @param {FlowOptions} [options={}] Flow 配置。
541
+ * @param {FlowProps} [props={}] Flow 配置。
572
542
  */
573
- constructor(options: FlowOptions = {}) {
574
- this.options = normalizeOptions(options);
575
- this.steps = this.options.steps;
543
+ constructor(props: FlowProps = {}) {
544
+ this.props = normalizeProps(props);
545
+ this.steps = this.props.steps;
576
546
  this.validateSteps(this.steps);
577
547
  this.stepMap = new Map(this.steps.map((step, index) => [step.id, index]));
578
- this.initialStepId = this.resolveInitialStepId(this.options.initial);
548
+ this.initialStepId = this.resolveInitialStepId(this.props.initial);
579
549
  this.initialData = this.createInitialStepData();
580
550
  this.subscribers = new Set();
581
551
  this.renderDispose = null;
582
552
  this.cleanupTasks = new Set();
583
- this.nodes = {};
584
- this.destroyed = false;
585
- this.activeAction = null;
586
- this.actionController = null;
587
- this.root = null;
553
+ this.dom = {
554
+ root: null,
555
+ header: null,
556
+ body: null,
557
+ footer: null,
558
+ };
559
+ this.runtime = {
560
+ built: false,
561
+ destroyed: false,
562
+ activeAction: null,
563
+ actionController: null,
564
+ };
588
565
 
589
566
  this.state = createDeepStore({
590
- id: this.options.id,
567
+ id: this.props.id,
591
568
  currentId: this.initialStepId,
592
569
  currentIndex: this.stepMap.get(this.initialStepId) ?? 0,
593
570
  previousId: null,
@@ -694,38 +671,30 @@ export class Flow {
694
671
  }
695
672
 
696
673
  /**
697
- * 挂载默认 Flow UI。
698
- * @param {Element|Node|string|Array} container DOM 容器、选择器或 JSX/h 返回节点。
674
+ * 构建默认 Flow UI。
699
675
  * @returns {Flow}
700
676
  */
701
- mount(container: DOMReference): this {
702
- this.assertActive('mount');
703
- if (!this.options.render) return this;
704
-
705
- const target = requireContainer(
706
- container,
707
- 'Flow.mount.container',
708
- 'element'
709
- );
677
+ build(): this {
678
+ this.assertActive('build');
679
+ if (this.runtime.built) return this;
680
+
681
+ this.runtime.built = true;
682
+ if (!this.props.render) return this;
710
683
 
711
- this.unmount();
712
- this.root = this.buildRoot();
713
- target.appendChild(this.root);
684
+ this.dom.root = this.buildRoot();
714
685
  this.mountView();
715
686
  return this;
716
687
  }
717
688
 
718
- /**
719
- * 卸载默认 UI。
720
- * @returns {Flow}
721
- */
722
- unmount(): this {
689
+ private teardownView(): void {
723
690
  this.renderDispose?.();
724
691
  this.renderDispose = null;
725
- if (this.root?.parentNode) this.root.parentNode.removeChild(this.root);
726
- this.root = null;
727
- this.nodes = {};
728
- return this;
692
+ this.dom.root?.remove();
693
+ this.dom.root = null;
694
+ this.dom.header = null;
695
+ this.dom.body = null;
696
+ this.dom.footer = null;
697
+ this.runtime.built = false;
729
698
  }
730
699
 
731
700
  /**
@@ -868,7 +837,7 @@ export class Flow {
868
837
  ...clonePlainObject(this.state.stepData[stepId]),
869
838
  ...data,
870
839
  };
871
- if (this.options.cache) Object.assign(this.state.data, data);
840
+ if (this.props.cache) Object.assign(this.state.data, data);
872
841
  this.state.version += 1;
873
842
  });
874
843
  if (!options.silent) this.emitChange();
@@ -936,7 +905,7 @@ export class Flow {
936
905
  'finish',
937
906
  async () => {
938
907
  if (payload) this.setStepData(this.state.currentId, payload);
939
- await this.callHook(this.options.onFinish, [this.snapshot(), this]);
908
+ await this.callHook(this.props.onFinish, [this.snapshot(), this]);
940
909
  return this.snapshot();
941
910
  },
942
911
  { internal: options.internal }
@@ -948,10 +917,10 @@ export class Flow {
948
917
  * @returns {void}
949
918
  */
950
919
  destroy(): void {
951
- if (this.destroyed) return;
952
- this.destroyed = true;
920
+ if (this.runtime.destroyed) return;
921
+ this.runtime.destroyed = true;
953
922
  this.abortActiveAction();
954
- this.unmount();
923
+ this.teardownView();
955
924
  for (const cleanup of Array.from(this.cleanupTasks)) {
956
925
  cleanup();
957
926
  }
@@ -959,21 +928,19 @@ export class Flow {
959
928
  this.subscribers.clear();
960
929
  this.steps = [];
961
930
  this.stepMap.clear();
962
- this.activeAction = null;
963
- this.actionController = null;
931
+ this.runtime.activeAction = null;
932
+ this.runtime.actionController = null;
964
933
  }
965
934
 
966
935
  private resolveInitialStepId(initial: string | number | null): string {
967
936
  if (typeof initial === 'number') {
968
937
  const step = this.steps[initial];
969
- if (!step) throw new Error('Flow.options.initial index is out of range.');
938
+ if (!step) throw new Error('Flow.props.initial index is out of range.');
970
939
  return step.id;
971
940
  }
972
941
  if (typeof initial === 'string') {
973
942
  if (!this.stepMap.has(initial)) {
974
- throw new Error(
975
- `Flow.options.initial step "${initial}" does not exist.`
976
- );
943
+ throw new Error(`Flow.props.initial step "${initial}" does not exist.`);
977
944
  }
978
945
  return initial;
979
946
  }
@@ -983,15 +950,15 @@ export class Flow {
983
950
  private validateSteps(steps: FlowStep[]): void {
984
951
  const ids = new Set<string>();
985
952
  for (const [index, step] of steps.entries()) {
986
- validateParam(String(index), step, FLOW_STEP_RULE, 'Flow.options.steps');
953
+ validateParam(String(index), step, FLOW_STEP_RULE, 'Flow.props.steps');
987
954
  validateParam(
988
955
  'content',
989
956
  step.content ?? null,
990
957
  FLOW_CONTENT_RULE,
991
- `Flow.options.steps.${index}`
958
+ `Flow.props.steps.${index}`
992
959
  );
993
960
  if (ids.has(step.id)) {
994
- throw new Error(`Flow.options.steps: duplicated step id "${step.id}".`);
961
+ throw new Error(`Flow.props.steps: duplicated step id "${step.id}".`);
995
962
  }
996
963
  ids.add(step.id);
997
964
  }
@@ -1029,8 +996,7 @@ export class Flow {
1029
996
  payload: FlowPayload,
1030
997
  fallbackId: string
1031
998
  ): Promise<FlowStepResult | FlowPayload | void> {
1032
- const globalHook =
1033
- type === 'back' ? this.options.onBack : this.options.onNext;
999
+ const globalHook = type === 'back' ? this.props.onBack : this.props.onNext;
1034
1000
  const stepHook = type === 'back' ? step.onBack : step.onNext;
1035
1001
  const context = this.createContext({ payload, targetId: fallbackId });
1036
1002
 
@@ -1098,8 +1064,8 @@ export class Flow {
1098
1064
 
1099
1065
  this.emitChange(fromSnapshot);
1100
1066
  } catch (error) {
1101
- if (this.destroyed) throw error;
1102
- if (this.options.rollbackOnError) {
1067
+ if (this.runtime.destroyed) throw error;
1068
+ if (this.props.rollbackOnError) {
1103
1069
  this.restoreState(rollbackState, { keepLoading: true });
1104
1070
  }
1105
1071
  this.handleError(error, fromSnapshot);
@@ -1143,7 +1109,7 @@ export class Flow {
1143
1109
  flow: this,
1144
1110
  step,
1145
1111
  state: this.state,
1146
- signal: this.actionController?.signal || null,
1112
+ signal: this.runtime.actionController?.signal || null,
1147
1113
  snapshot,
1148
1114
  data: clonePlainObject(this.state.data),
1149
1115
  currentData: this.getStepData(step.id),
@@ -1164,23 +1130,23 @@ export class Flow {
1164
1130
  ): Promise<FlowSnapshot | null> {
1165
1131
  const isOuterAction = !options.internal && !this.state.loading;
1166
1132
  if (isOuterAction) {
1167
- this.actionController =
1133
+ this.runtime.actionController =
1168
1134
  typeof AbortController !== 'undefined' ? new AbortController() : null;
1169
- this.activeAction = action;
1135
+ this.runtime.activeAction = action;
1170
1136
  this.setLoading(true, action);
1171
1137
  }
1172
1138
 
1173
1139
  try {
1174
1140
  return await task();
1175
1141
  } catch (error) {
1176
- if (this.destroyed) return null;
1142
+ if (this.runtime.destroyed) return null;
1177
1143
  if (this.state.error !== error) this.handleError(error);
1178
1144
  throw error;
1179
1145
  } finally {
1180
- if (isOuterAction && !this.destroyed) {
1146
+ if (isOuterAction && !this.runtime.destroyed) {
1181
1147
  this.setLoading(false, null);
1182
- this.activeAction = null;
1183
- this.actionController = null;
1148
+ this.runtime.activeAction = null;
1149
+ this.runtime.actionController = null;
1184
1150
  }
1185
1151
  }
1186
1152
  }
@@ -1193,19 +1159,19 @@ export class Flow {
1193
1159
  ) as FlowError;
1194
1160
  error.code = 'FLOW_BUSY';
1195
1161
 
1196
- if (typeof this.options.onBusy === 'function') {
1197
- this.options.onBusy(action, this.snapshot(), this);
1162
+ if (typeof this.props.onBusy === 'function') {
1163
+ this.props.onBusy(action, this.snapshot(), this);
1198
1164
  }
1199
1165
 
1200
- if (this.options.busyStrategy === 'throw') throw error;
1166
+ if (this.props.busyStrategy === 'throw') throw error;
1201
1167
 
1202
1168
  return this.snapshot();
1203
1169
  }
1204
1170
 
1205
1171
  private abortActiveAction(): void {
1206
- this.actionController?.abort();
1207
- this.actionController = null;
1208
- this.activeAction = null;
1172
+ this.runtime.actionController?.abort();
1173
+ this.runtime.actionController = null;
1174
+ this.runtime.activeAction = null;
1209
1175
  }
1210
1176
 
1211
1177
  private async callHook<TResult>(
@@ -1216,7 +1182,7 @@ export class Flow {
1216
1182
  const result = await (
1217
1183
  hook as (...args: unknown[]) => TResult | Promise<TResult>
1218
1184
  )(...args);
1219
- if (this.destroyed) throw this.createAbortError();
1185
+ if (this.runtime.destroyed) throw this.createAbortError();
1220
1186
  return result;
1221
1187
  }
1222
1188
 
@@ -1230,10 +1196,17 @@ export class Flow {
1230
1196
  value: boolean,
1231
1197
  action: FlowAction | null = this.state.busyAction
1232
1198
  ): void {
1199
+ const previous = this.snapshot();
1200
+ const busyAction = value ? action : null;
1201
+ if (this.state.loading === value && this.state.busyAction === busyAction)
1202
+ return;
1203
+
1233
1204
  flushSync(() => {
1234
1205
  this.state.loading = value;
1235
- this.state.busyAction = value ? action : null;
1206
+ this.state.busyAction = busyAction;
1207
+ this.state.version += 1;
1236
1208
  });
1209
+ this.emitChange(previous);
1237
1210
  }
1238
1211
 
1239
1212
  private handleError(
@@ -1244,8 +1217,8 @@ export class Flow {
1244
1217
  this.state.error = error;
1245
1218
  this.state.version += 1;
1246
1219
  });
1247
- if (typeof this.options.onError === 'function') {
1248
- this.options.onError(error, this.snapshot(), this, previous);
1220
+ if (typeof this.props.onError === 'function') {
1221
+ this.props.onError(error, this.snapshot(), this, previous);
1249
1222
  }
1250
1223
  }
1251
1224
 
@@ -1254,8 +1227,8 @@ export class Flow {
1254
1227
  for (const handler of Array.from(this.subscribers)) {
1255
1228
  handler(next, this, previous);
1256
1229
  }
1257
- if (typeof this.options.onChange === 'function') {
1258
- this.options.onChange(next, this, previous);
1230
+ if (typeof this.props.onChange === 'function') {
1231
+ this.props.onChange(next, this, previous);
1259
1232
  }
1260
1233
  }
1261
1234
 
@@ -1312,7 +1285,7 @@ export class Flow {
1312
1285
  if (typeof cleanup !== 'function') {
1313
1286
  throw new Error('Flow.addCleanup: cleanup expects a function.');
1314
1287
  }
1315
- if (this.destroyed) {
1288
+ if (this.runtime.destroyed) {
1316
1289
  cleanup();
1317
1290
  return () => {};
1318
1291
  }
@@ -1321,7 +1294,7 @@ export class Flow {
1321
1294
  }
1322
1295
 
1323
1296
  private assertActive(method: string): void {
1324
- if (this.destroyed) {
1297
+ if (this.runtime.destroyed) {
1325
1298
  throw new Error(`Flow.${method}: instance has been destroyed.`);
1326
1299
  }
1327
1300
  }
@@ -1342,22 +1315,21 @@ export class Flow {
1342
1315
 
1343
1316
  private buildRoot(): HTMLElement {
1344
1317
  return jsx('div', {
1345
- className: this.options.className.root,
1346
- id: this.options.id,
1318
+ className: this.props.className.root,
1319
+ id: this.props.id,
1347
1320
  role: 'group',
1348
1321
  'data-flow': 'root',
1349
1322
  'aria-busy': () => (this.state.loading ? 'true' : 'false'),
1350
- 'aria-labelledby': `${this.options.id}-title`,
1351
- ref: (element: HTMLElement) => {
1352
- this.nodes.root = element;
1353
- },
1354
1323
  }) as HTMLElement;
1355
1324
  }
1356
1325
 
1357
1326
  private mountView(): void {
1358
- if (this.renderDispose || !this.root) return;
1327
+ if (this.renderDispose || !this.dom.root) return;
1359
1328
  this.renderDispose = createRoot((dispose: FlowCleanup) => {
1360
- const viewDispose = render(() => this.view(), this.root as HTMLElement);
1329
+ const viewDispose = render(
1330
+ () => this.view(),
1331
+ this.dom.root as HTMLElement
1332
+ );
1361
1333
  onCleanup(viewDispose);
1362
1334
  return dispose;
1363
1335
  });
@@ -1366,16 +1338,9 @@ export class Flow {
1366
1338
  private view(): RenderableContent<FlowRenderContext> {
1367
1339
  const snapshot = this.snapshot();
1368
1340
  return [
1369
- this.renderSlot('renderHeader', snapshot, () =>
1370
- this.options.showHeader ? this.headerView(snapshot) : null
1371
- ),
1372
- this.renderSlot('renderSteps', snapshot, () =>
1373
- this.options.showSteps ? this.stepsView(snapshot) : null
1374
- ),
1375
- this.renderSlot('renderBody', snapshot, () => this.bodyView(snapshot)),
1376
- this.renderSlot('renderFooter', snapshot, () =>
1377
- this.options.showFooter ? this.footerView(snapshot) : null
1378
- ),
1341
+ this.headerView(snapshot),
1342
+ this.bodyView(snapshot),
1343
+ this.footerView(snapshot),
1379
1344
  ];
1380
1345
  }
1381
1346
 
@@ -1384,7 +1349,7 @@ export class Flow {
1384
1349
  snapshot: FlowSnapshot,
1385
1350
  fallback: () => RenderableContent<FlowRenderContext>
1386
1351
  ): RenderableContent<FlowRenderContext> {
1387
- const slot = this.options[name];
1352
+ const slot = this.props[name];
1388
1353
  if (slot === false) return null;
1389
1354
  if (typeof slot === 'function') {
1390
1355
  return slot(this.createRenderContext(snapshot, fallback));
@@ -1414,30 +1379,20 @@ export class Flow {
1414
1379
 
1415
1380
  private headerView(snapshot: FlowSnapshot): HTMLElement {
1416
1381
  return jsx('div', {
1417
- className: this.options.className.header,
1382
+ className: this.props.className.header,
1418
1383
  'data-flow-header': '',
1419
- children: [
1420
- jsx('div', {
1421
- id: `${this.options.id}-title`,
1422
- className: this.options.className.title,
1423
- 'data-flow-title': '',
1424
- children:
1425
- snapshot.currentStep?.title || snapshot.currentStep?.id || '',
1426
- }),
1427
- snapshot.currentStep?.description
1428
- ? jsx('div', {
1429
- className: this.options.className.description,
1430
- 'data-flow-description': '',
1431
- children: snapshot.currentStep.description,
1432
- })
1433
- : null,
1434
- ],
1384
+ ref: (element: HTMLElement) => {
1385
+ this.dom.header = element;
1386
+ },
1387
+ children: this.renderSlot('renderHeader', snapshot, () =>
1388
+ this.stepsView(snapshot)
1389
+ ),
1435
1390
  }) as HTMLElement;
1436
1391
  }
1437
1392
 
1438
1393
  private stepsView(snapshot: FlowSnapshot): HTMLOListElement {
1439
1394
  return jsx('ol', {
1440
- className: this.options.className.steps,
1395
+ className: this.props.className.steps,
1441
1396
  'data-flow-steps': '',
1442
1397
  role: 'list',
1443
1398
  'aria-label': 'Flow steps',
@@ -1450,12 +1405,12 @@ export class Flow {
1450
1405
  children: [
1451
1406
  jsx('button', {
1452
1407
  type: 'button',
1453
- className: this.options.className.stepButton,
1454
- disabled: this.options.linear && index > snapshot.currentIndex,
1408
+ className: this.props.className.stepButton,
1409
+ disabled: this.props.linear && index > snapshot.currentIndex,
1455
1410
  'aria-current':
1456
1411
  index === snapshot.currentIndex ? 'step' : undefined,
1457
1412
  'aria-disabled':
1458
- this.options.linear && index > snapshot.currentIndex
1413
+ this.props.linear && index > snapshot.currentIndex
1459
1414
  ? 'true'
1460
1415
  : 'false',
1461
1416
  'aria-label': `${index + 1}. ${step.title || step.id}`,
@@ -1466,12 +1421,12 @@ export class Flow {
1466
1421
  },
1467
1422
  children: [
1468
1423
  jsx('span', {
1469
- className: this.options.className.stepIndex,
1424
+ className: this.props.className.stepIndex,
1470
1425
  'data-flow-step-number': '',
1471
1426
  children: index + 1,
1472
1427
  }),
1473
1428
  jsx('span', {
1474
- className: this.options.className.stepTitle,
1429
+ className: this.props.className.stepTitle,
1475
1430
  'data-flow-step-title': '',
1476
1431
  children: step.title || step.id,
1477
1432
  }),
@@ -1485,76 +1440,85 @@ export class Flow {
1485
1440
 
1486
1441
  private bodyView(snapshot: FlowSnapshot): HTMLElement {
1487
1442
  return jsx('div', {
1488
- className: this.options.className.body,
1443
+ className: this.props.className.body,
1489
1444
  'data-flow-body': '',
1490
1445
  role: 'region',
1491
1446
  'aria-live': 'polite',
1492
1447
  'aria-busy': snapshot.loading ? 'true' : 'false',
1493
- children: this.contentView(this.currentStep.content),
1448
+ ref: (element: HTMLElement) => {
1449
+ this.dom.body = element;
1450
+ },
1451
+ children: this.renderSlot('renderBody', snapshot, () =>
1452
+ this.contentView(this.currentStep.content)
1453
+ ),
1494
1454
  }) as HTMLElement;
1495
1455
  }
1496
1456
 
1497
1457
  private footerView(snapshot: FlowSnapshot): HTMLElement {
1498
- const ghostButton = joinClasses(
1499
- this.options.className.button,
1500
- this.options.className.buttonGhost
1501
- );
1502
- const primaryButton = joinClasses(
1503
- this.options.className.button,
1504
- this.options.className.buttonPrimary
1505
- );
1506
-
1507
1458
  return jsx('div', {
1508
- className: this.options.className.footer,
1459
+ className: this.props.className.footer,
1509
1460
  'data-flow-footer': '',
1510
- children: [
1511
- this.options.showReset
1461
+ ref: (element: HTMLElement) => {
1462
+ this.dom.footer = element;
1463
+ },
1464
+ children: this.renderSlot('renderFooter', snapshot, () => [
1465
+ this.props.showReset
1512
1466
  ? jsx('button', {
1513
1467
  type: 'button',
1514
- className: joinClasses(ghostButton, this.options.className.reset),
1468
+ className: joinClasses(
1469
+ this.props.className.button,
1470
+ this.props.className.reset
1471
+ ),
1515
1472
  'data-action': 'reset',
1516
1473
  onClick: () => this.reset(),
1517
1474
  disabled: snapshot.loading,
1518
1475
  'aria-disabled': snapshot.loading ? 'true' : 'false',
1519
- children: this.options.text.reset,
1476
+ children: this.props.text.reset,
1520
1477
  })
1521
1478
  : null,
1522
- this.options.showBack
1479
+ this.props.showBack
1523
1480
  ? jsx('button', {
1524
1481
  type: 'button',
1525
- className: joinClasses(ghostButton, this.options.className.back),
1482
+ className: joinClasses(
1483
+ this.props.className.button,
1484
+ this.props.className.back
1485
+ ),
1526
1486
  'data-action': 'back',
1527
1487
  onClick: () => void this.back(),
1528
1488
  disabled: !snapshot.canBack,
1529
1489
  'aria-disabled': !snapshot.canBack ? 'true' : 'false',
1530
- children: this.options.text.back,
1490
+ children: this.props.text.back,
1531
1491
  })
1532
1492
  : null,
1533
- this.options.showNext
1493
+ this.props.showNext
1534
1494
  ? jsx('button', {
1535
1495
  type: 'button',
1536
1496
  className: joinClasses(
1537
- primaryButton,
1538
- this.options.className.next
1497
+ this.props.className.button,
1498
+ this.props.className.next
1539
1499
  ),
1540
1500
  'data-action': 'next',
1541
1501
  onClick: () => void this.next(),
1542
1502
  disabled: snapshot.loading,
1543
1503
  'aria-disabled': snapshot.loading ? 'true' : 'false',
1544
1504
  children: snapshot.isLast
1545
- ? this.options.text.finish
1546
- : this.options.text.next,
1505
+ ? this.props.text.finish
1506
+ : this.props.text.next,
1547
1507
  })
1548
1508
  : null,
1549
- ],
1509
+ ]),
1550
1510
  }) as HTMLElement;
1551
1511
  }
1552
1512
 
1553
1513
  private contentView(
1554
1514
  content: FlowStep['content']
1555
- ): RenderableContent<FlowContext> {
1556
- if (typeof content === 'function') return content(this.createContext());
1557
- return content ?? '';
1515
+ ): RenderableContent<FlowRenderContext> {
1516
+ if (typeof content === 'function') {
1517
+ return content(
1518
+ this.createContext()
1519
+ ) as RenderableContent<FlowRenderContext>;
1520
+ }
1521
+ return (content ?? '') as RenderableContent<FlowRenderContext>;
1558
1522
  }
1559
1523
 
1560
1524
  private stepClass(
@@ -1562,18 +1526,18 @@ export class Flow {
1562
1526
  currentIndex = this.state.currentIndex
1563
1527
  ): string {
1564
1528
  return joinClasses(
1565
- this.options.className.step,
1566
- index === currentIndex && this.options.className.active,
1567
- index < currentIndex && this.options.className.complete
1529
+ this.props.className.step,
1530
+ index === currentIndex && this.props.className.active,
1531
+ index < currentIndex && this.props.className.complete
1568
1532
  );
1569
1533
  }
1570
1534
  }
1571
1535
 
1572
1536
  /**
1573
1537
  * 创建 Flow 实例。
1574
- * @param {FlowOptions} options Flow 配置。
1538
+ * @param {FlowProps} props Flow 配置。
1575
1539
  * @returns {Flow}
1576
1540
  */
1577
- export function createFlow(options: FlowOptions = {}): Flow {
1578
- return new Flow(options);
1541
+ export function createFlow(props: FlowProps = {}): Flow {
1542
+ return new Flow(props);
1579
1543
  }