@fulate/components 1.0.0

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 (34) hide show
  1. package/README.md +37 -0
  2. package/dist/components/button/button.d.ts +81 -0
  3. package/dist/components/button/style.d.ts +18 -0
  4. package/dist/components/button/types.d.ts +1 -0
  5. package/dist/components/checkbox/checkbox.d.ts +106 -0
  6. package/dist/components/dialog/dialog.d.ts +64 -0
  7. package/dist/components/field/border.d.ts +9 -0
  8. package/dist/components/field/field.d.ts +149 -0
  9. package/dist/components/field/style.d.ts +18 -0
  10. package/dist/components/input/input.d.ts +80 -0
  11. package/dist/components/message/message.d.ts +44 -0
  12. package/dist/components/message/types.d.ts +19 -0
  13. package/dist/components/popover/popover.d.ts +114 -0
  14. package/dist/components/popover/types.d.ts +1 -0
  15. package/dist/components/radio/radio.d.ts +105 -0
  16. package/dist/components/select/select.d.ts +91 -0
  17. package/dist/components/select/types.d.ts +4 -0
  18. package/dist/components/switch/switch.d.ts +52 -0
  19. package/dist/components/tooltip/tooltip.d.ts +58 -0
  20. package/dist/composables/context.d.ts +9 -0
  21. package/dist/composables/index.d.ts +3 -0
  22. package/dist/composables/outside-click.d.ts +6 -0
  23. package/dist/composables/overlay-position.d.ts +10 -0
  24. package/dist/index.d.ts +19 -0
  25. package/dist/index.js +2263 -0
  26. package/dist/internal/control-size.d.ts +14 -0
  27. package/dist/internal/interaction.d.ts +14 -0
  28. package/dist/internal/ripple.d.ts +5 -0
  29. package/dist/internal/state-layer.d.ts +9 -0
  30. package/dist/theme/index.d.ts +1 -0
  31. package/dist/theme/md3.d.ts +25 -0
  32. package/dist/types/control.d.ts +1 -0
  33. package/dist/types/floating.d.ts +4 -0
  34. package/package.json +31 -0
package/dist/index.js ADDED
@@ -0,0 +1,2263 @@
1
+ import { Align, Display, FlexDirection, Justify, Overflow } from "@fulate/yoga";
2
+ import { Teleport, cloneVNode, computed, defineComponent, inject, nextTick, onBeforeUnmount, onMounted, onUnmounted, provide, ref, shallowRef, toValue, unref, watch } from "@vue/runtime-core";
3
+ import { FTransition, FULATE_OVERLAY_KEY, FULATE_ROOT_KEY, VUE_SHAPE_SIZE_KEY } from "@fulate/vue";
4
+ import { computePosition, flip, measureTextWidth, offset, shift } from "@fulate/ui";
5
+ import { createVNode, isVNode, mergeProps, resolveComponent } from "vue";
6
+ import { isString, isUndefined } from "lodash-es";
7
+ import { blendColor, colorWithAlpha } from "@fulate/share";
8
+ import { Element } from "@fulate/core";
9
+ //#region packages/components/src/composables/context.ts
10
+ function useOverlay() {
11
+ return {
12
+ overlay: inject(FULATE_OVERLAY_KEY, null),
13
+ fulateRoot: inject(FULATE_ROOT_KEY, null)
14
+ };
15
+ }
16
+ function useVueShapeSize() {
17
+ return inject(VUE_SHAPE_SIZE_KEY, {
18
+ width: 0,
19
+ height: 0
20
+ });
21
+ }
22
+ //#endregion
23
+ //#region packages/components/src/composables/outside-click.ts
24
+ function isDescendantOf(node, ancestor) {
25
+ if (!node || !ancestor) return false;
26
+ let current = node;
27
+ while (current) {
28
+ if (current === ancestor) return true;
29
+ current = current.parent;
30
+ }
31
+ return false;
32
+ }
33
+ function useOutsideClick(open, excludeRefs, onClose) {
34
+ const { fulateRoot } = useOverlay();
35
+ let removeListener = null;
36
+ function onRootPointerDown(event) {
37
+ if (!toValue(open)) return;
38
+ const target = event.detail?.target;
39
+ if (!target) return;
40
+ if (excludeRefs.some((ref) => isDescendantOf(target, ref.value))) return;
41
+ onClose();
42
+ }
43
+ onMounted(() => {
44
+ removeListener = fulateRoot?.addEventListener("pointerdown", onRootPointerDown) ?? null;
45
+ });
46
+ onUnmounted(() => {
47
+ removeListener?.();
48
+ removeListener = null;
49
+ });
50
+ }
51
+ //#endregion
52
+ //#region packages/components/src/composables/overlay-position.ts
53
+ /**
54
+ * 把 open/ref/options 与场景、viewport、scroll、resize 的定位失效合并到所属
55
+ * Root 的 frame-final completion;每次只安装仍由当前 task/refs 拥有的结果。
56
+ */
57
+ function useOverlayPosition(open, triggerRef, floatingRef, options = {}) {
58
+ const { fulateRoot } = useOverlay();
59
+ let positionPending = false;
60
+ let positionTask = null;
61
+ let installingFloating = null;
62
+ let unmounted = false;
63
+ const rootListeners = [];
64
+ const flushPosition = () => {
65
+ if (!positionPending) return;
66
+ positionPending = false;
67
+ if (unmounted || !toValue(open)) return;
68
+ const trigger = triggerRef.value;
69
+ const floating = floatingRef.value;
70
+ if (!trigger || !floating) return;
71
+ const task = computePosition(trigger, floating, toValue(options));
72
+ positionTask = task;
73
+ task.then((result) => {
74
+ if (unmounted || !toValue(open) || positionTask !== task || triggerRef.value !== trigger || floatingRef.value !== floating) return;
75
+ installingFloating = floating;
76
+ fulateRoot?.frameRuntime.requestCompletion(() => {
77
+ if (positionTask === task && installingFloating === floating) installingFloating = null;
78
+ });
79
+ try {
80
+ floating.setOptions({
81
+ left: result.x,
82
+ top: result.y
83
+ });
84
+ } finally {
85
+ if (!fulateRoot && installingFloating === floating) installingFloating = null;
86
+ }
87
+ });
88
+ };
89
+ const markPending = () => {
90
+ positionTask = null;
91
+ installingFloating = null;
92
+ if (unmounted || !toValue(open)) {
93
+ positionPending = false;
94
+ return;
95
+ }
96
+ positionPending = true;
97
+ fulateRoot?.frameRuntime.requestCompletion(flushPosition);
98
+ };
99
+ const isRelated = (node, target) => {
100
+ return isDescendantOf(target, node) || isDescendantOf(node, target);
101
+ };
102
+ watch([
103
+ () => toValue(open),
104
+ () => triggerRef.value,
105
+ () => floatingRef.value,
106
+ () => toValue(options)
107
+ ], markPending, {
108
+ immediate: true,
109
+ flush: "post"
110
+ });
111
+ onMounted(() => {
112
+ if (fulateRoot) rootListeners.push(fulateRoot.addEventListener("viewportchange", markPending), fulateRoot.sceneRegistry.observeSceneChanges((node, affected) => {
113
+ const trigger = triggerRef.value;
114
+ const floating = floatingRef.value;
115
+ if (!trigger || !floating) return;
116
+ if (node === void 0 && affected === void 0) {
117
+ markPending();
118
+ return;
119
+ }
120
+ if ((node === void 0 ? affected ?? [] : affected === void 0 ? [node] : [node, ...affected]).some((current) => current !== installingFloating && (isRelated(current, trigger) || isRelated(current, floating)))) markPending();
121
+ }));
122
+ window.addEventListener("resize", markPending);
123
+ window.addEventListener("scroll", markPending, true);
124
+ });
125
+ onUnmounted(() => {
126
+ unmounted = true;
127
+ positionTask = null;
128
+ positionPending = false;
129
+ installingFloating = null;
130
+ rootListeners.splice(0).forEach((remove) => remove());
131
+ window.removeEventListener("resize", markPending);
132
+ window.removeEventListener("scroll", markPending, true);
133
+ });
134
+ }
135
+ //#endregion
136
+ //#region packages/components/src/theme/md3.ts
137
+ var MD3 = {
138
+ primary: "#1565C0",
139
+ onPrimary: "#ffffff",
140
+ primaryContainer: "#D1E4FF",
141
+ surface: "#ffffff",
142
+ surfaceDim: "#F5F5F5",
143
+ surfaceContainer: "#F0F0F0",
144
+ onSurface: "#1D1B20",
145
+ onSurfaceVariant: "#6B6B6B",
146
+ outline: "#BDBDBD",
147
+ outlineVariant: "#E0E0E0",
148
+ error: "#D32F2F",
149
+ onError: "#ffffff",
150
+ stateHoverOpacity: .15,
151
+ statePressOpacity: .24,
152
+ stateFocusOpacity: .2,
153
+ rippleOpacity: .32,
154
+ radiusFull: 4,
155
+ radiusLg: 4,
156
+ radiusMd: 4,
157
+ radiusSm: 2,
158
+ fontSize: 14,
159
+ fontFamily: "Roboto, system-ui, -apple-system, sans-serif",
160
+ iconFamily: "Material Symbols Outlined"
161
+ };
162
+ //#endregion
163
+ //#region packages/components/src/components/button/style.ts
164
+ var BUTTON_SIZE = {
165
+ small: {
166
+ height: 32,
167
+ fontSize: 13,
168
+ minWidth: 56,
169
+ horizontalPadding: 20,
170
+ textHorizontalPadding: 12
171
+ },
172
+ medium: {
173
+ height: 40,
174
+ fontSize: 14,
175
+ minWidth: 64,
176
+ horizontalPadding: 24,
177
+ textHorizontalPadding: 16
178
+ },
179
+ large: {
180
+ height: 48,
181
+ fontSize: 16,
182
+ minWidth: 72,
183
+ horizontalPadding: 28,
184
+ textHorizontalPadding: 20
185
+ }
186
+ };
187
+ function getRestBackground(variant, color, disabled) {
188
+ if (disabled) return variant === "filled" ? MD3.surfaceContainer : MD3.surface;
189
+ if (variant === "filled") return color;
190
+ if (variant === "outlined") return MD3.surface;
191
+ return "transparent";
192
+ }
193
+ function getInteractiveBackground(variant, color, opacity) {
194
+ return variant === "filled" ? blendColor(color, MD3.onPrimary, opacity) : blendColor(MD3.surface, color, opacity);
195
+ }
196
+ function getInteractionOpacity(state) {
197
+ if (state === "press") return MD3.statePressOpacity;
198
+ return MD3.stateHoverOpacity;
199
+ }
200
+ function resolveButtonVisual(variant, color, disabled, state) {
201
+ const atRest = disabled || state === "rest";
202
+ return {
203
+ backgroundColor: atRest ? getRestBackground(variant, color, disabled) : getInteractiveBackground(variant, color, getInteractionOpacity(state)),
204
+ borderColor: variant === "outlined" ? atRest ? disabled ? MD3.outlineVariant : MD3.outline : color : void 0,
205
+ textColor: disabled ? MD3.onSurfaceVariant : variant === "filled" ? MD3.onPrimary : color,
206
+ rippleColor: variant === "filled" ? MD3.onPrimary : color
207
+ };
208
+ }
209
+ //#endregion
210
+ //#region packages/components/src/internal/control-size.ts
211
+ var controlSizeProp = {
212
+ type: String,
213
+ default: "medium"
214
+ };
215
+ var inheritedControlSizeProp = {
216
+ type: String,
217
+ default: void 0
218
+ };
219
+ var CONTROL_STATE_LAYER_SIZE = {
220
+ small: 32,
221
+ medium: 40,
222
+ large: 48
223
+ };
224
+ var CONTROL_GROUP_GAP = {
225
+ small: 2,
226
+ medium: 4,
227
+ large: 6
228
+ };
229
+ var CONTROL_LABEL_FONT_SIZE = {
230
+ small: 13,
231
+ medium: 14,
232
+ large: 16
233
+ };
234
+ //#endregion
235
+ //#region packages/components/src/internal/interaction.ts
236
+ var TRANSITION_DURATION$1 = {
237
+ hover: 150,
238
+ leave: 200,
239
+ press: 100,
240
+ release: 150
241
+ };
242
+ function useInteractionState(disabled, apply) {
243
+ let hovered = false;
244
+ let pressed = false;
245
+ let state = "rest";
246
+ function resolveState() {
247
+ if (disabled()) return "rest";
248
+ if (pressed) return "press";
249
+ if (hovered) return "hover";
250
+ return "rest";
251
+ }
252
+ function update(duration, force = false) {
253
+ const next = resolveState();
254
+ if (!force && next === state) return;
255
+ state = next;
256
+ apply(next, duration);
257
+ }
258
+ function reset() {
259
+ hovered = false;
260
+ pressed = false;
261
+ update(0, true);
262
+ }
263
+ function releasePress() {
264
+ if (!pressed) return;
265
+ pressed = false;
266
+ update(TRANSITION_DURATION$1.release);
267
+ }
268
+ const handlers = {
269
+ onMouseenter() {
270
+ if (disabled()) return;
271
+ hovered = true;
272
+ update(TRANSITION_DURATION$1.hover);
273
+ },
274
+ onMouseleave() {
275
+ hovered = false;
276
+ pressed = false;
277
+ update(TRANSITION_DURATION$1.leave);
278
+ },
279
+ onPointerdown() {
280
+ if (disabled()) return;
281
+ pressed = true;
282
+ update(TRANSITION_DURATION$1.press);
283
+ },
284
+ onPointerup: releasePress,
285
+ onPointercancel: releasePress
286
+ };
287
+ function refresh(duration = 0) {
288
+ update(duration, true);
289
+ }
290
+ watch(disabled, (isDisabled) => {
291
+ if (isDisabled) reset();
292
+ else refresh();
293
+ });
294
+ return {
295
+ handlers,
296
+ refresh
297
+ };
298
+ }
299
+ //#endregion
300
+ //#region packages/components/src/internal/ripple.ts
301
+ function useRippleAnimation() {
302
+ const rippleRef = shallowRef(null);
303
+ function trigger(x, y) {
304
+ rippleRef.value?.trigger(x, y);
305
+ }
306
+ return {
307
+ rippleRef,
308
+ trigger
309
+ };
310
+ }
311
+ //#endregion
312
+ //#region packages/components/src/components/button/button.tsx
313
+ var FButton = /* @__PURE__ */ defineComponent({
314
+ name: "FButton",
315
+ props: {
316
+ label: {
317
+ type: String,
318
+ default: "Button"
319
+ },
320
+ variant: {
321
+ type: String,
322
+ default: "filled"
323
+ },
324
+ color: {
325
+ type: String,
326
+ default: MD3.primary
327
+ },
328
+ disabled: {
329
+ type: Boolean,
330
+ default: false
331
+ },
332
+ size: controlSizeProp,
333
+ width: {
334
+ type: Number,
335
+ default: void 0
336
+ },
337
+ height: {
338
+ type: Number,
339
+ default: void 0
340
+ },
341
+ fontSize: {
342
+ type: Number,
343
+ default: void 0
344
+ }
345
+ },
346
+ emits: ["press", "click"],
347
+ setup(props, { emit }) {
348
+ const divRef = shallowRef(null);
349
+ const { rippleRef, trigger: triggerRipple } = useRippleAnimation();
350
+ const metrics = computed(() => BUTTON_SIZE[props.size]);
351
+ const height = computed(() => props.height ?? metrics.value.height);
352
+ const fontSize = computed(() => props.fontSize ?? metrics.value.fontSize);
353
+ const paddingH = computed(() => props.variant === "text" ? metrics.value.textHorizontalPadding : metrics.value.horizontalPadding);
354
+ const autoWidth = computed(() => {
355
+ if (!isUndefined(props.width)) return props.width;
356
+ const font = `500 ${fontSize.value}px ${MD3.fontFamily}`;
357
+ const textWidth = Math.ceil(measureTextWidth(props.label, font));
358
+ const borderWidth = props.variant === "outlined" ? 1 : 0;
359
+ return Math.max(metrics.value.minWidth, textWidth + paddingH.value * 2 + borderWidth * 2);
360
+ });
361
+ const restVisual = computed(() => resolveButtonVisual(props.variant, props.color, props.disabled, "rest"));
362
+ function applyElementState(state, duration) {
363
+ const el = divRef.value;
364
+ if (!el) return;
365
+ const visual = resolveButtonVisual(props.variant, props.color, props.disabled, state);
366
+ const target = !isUndefined(visual.borderColor) ? {
367
+ backgroundColor: visual.backgroundColor,
368
+ borderColor: visual.borderColor
369
+ } : { backgroundColor: visual.backgroundColor };
370
+ el.stopAnimations();
371
+ if (duration === 0) el.setOptions(target);
372
+ else el.animate(target, { duration });
373
+ }
374
+ function activate(event) {
375
+ if (props.disabled) return;
376
+ emit("press");
377
+ emit("click", event);
378
+ }
379
+ const interaction = useInteractionState(() => props.disabled, applyElementState);
380
+ watch([() => props.variant, () => props.color], () => interaction.refresh(), { flush: "post" });
381
+ return () => createVNode(resolveComponent("f-div"), {
382
+ "ref": divRef,
383
+ "display": Display.Flex,
384
+ "justifyContent": Justify.Center,
385
+ "alignItems": Align.Center,
386
+ "width": autoWidth.value,
387
+ "height": height.value,
388
+ "flexShrink": 0,
389
+ "backgroundColor": restVisual.value.backgroundColor,
390
+ "borderColor": restVisual.value.borderColor,
391
+ "borderWidth": props.variant === "outlined" ? 1 : 0,
392
+ "radius": MD3.radiusFull,
393
+ "paddingLeft": paddingH.value,
394
+ "paddingRight": paddingH.value,
395
+ "cursor": props.disabled ? "default" : "pointer",
396
+ "onClick": activate,
397
+ "onPointerdown": (event) => {
398
+ interaction.handlers.onPointerdown();
399
+ if (!props.disabled) triggerRipple(event.detail.x, event.detail.y);
400
+ },
401
+ "onPointerup": interaction.handlers.onPointerup,
402
+ "onPointercancel": interaction.handlers.onPointercancel,
403
+ "onMouseenter": interaction.handlers.onMouseenter,
404
+ "onMouseleave": interaction.handlers.onMouseleave
405
+ }, { default: () => [createVNode(resolveComponent("f-ripple"), {
406
+ "ref": rippleRef,
407
+ "rippleColor": restVisual.value.rippleColor,
408
+ "rippleOpacity": MD3.rippleOpacity,
409
+ "duration": 400
410
+ }, null), createVNode(resolveComponent("f-span"), {
411
+ "text": props.label,
412
+ "color": restVisual.value.textColor,
413
+ "fontSize": fontSize.value,
414
+ "fontFamily": MD3.fontFamily,
415
+ "fontWeight": 500,
416
+ "textAlign": "center",
417
+ "verticalAlign": "middle",
418
+ "wordWrap": true,
419
+ "ellipsis": true,
420
+ "overflow": "hidden"
421
+ }, null)] });
422
+ }
423
+ });
424
+ //#endregion
425
+ //#region packages/components/src/components/field/style.ts
426
+ var FIELD_BORDER_WIDTH = .5;
427
+ var FIELD_ACTIVE_BORDER_WIDTH = 1.5;
428
+ var FIELD_SIZE = {
429
+ small: {
430
+ height: 44,
431
+ fontSize: 14,
432
+ horizontalPadding: 12,
433
+ labelHeight: 16,
434
+ labelFontSize: 10,
435
+ labelPaddingTop: 4
436
+ },
437
+ medium: {
438
+ height: 52,
439
+ fontSize: 15,
440
+ horizontalPadding: 14,
441
+ labelHeight: 18,
442
+ labelFontSize: 11,
443
+ labelPaddingTop: 6
444
+ },
445
+ large: {
446
+ height: 60,
447
+ fontSize: 16,
448
+ horizontalPadding: 16,
449
+ labelHeight: 20,
450
+ labelFontSize: 12,
451
+ labelPaddingTop: 8
452
+ }
453
+ };
454
+ function resolveFieldBorderVisual(state) {
455
+ if (state === "disabled") return {
456
+ borderColor: MD3.outlineVariant,
457
+ borderWidth: FIELD_BORDER_WIDTH
458
+ };
459
+ if (state === "active") return {
460
+ borderColor: MD3.primary,
461
+ borderWidth: FIELD_ACTIVE_BORDER_WIDTH
462
+ };
463
+ if (state === "hover") return {
464
+ borderColor: MD3.onSurfaceVariant,
465
+ borderWidth: 1
466
+ };
467
+ return {
468
+ borderColor: MD3.outline,
469
+ borderWidth: FIELD_BORDER_WIDTH
470
+ };
471
+ }
472
+ //#endregion
473
+ //#region packages/components/src/components/field/border.ts
474
+ var TRANSITION_DURATION = {
475
+ hover: 150,
476
+ leave: 200,
477
+ active: 150
478
+ };
479
+ function useFieldBorder(disabled, active) {
480
+ const borderRef = shallowRef(null);
481
+ let hovered = false;
482
+ let state = resolveState();
483
+ function resolveState() {
484
+ if (disabled()) return "disabled";
485
+ if (active()) return "active";
486
+ if (hovered) return "hover";
487
+ return "rest";
488
+ }
489
+ function apply(duration, force = false) {
490
+ const next = resolveState();
491
+ if (!force && next === state) return;
492
+ state = next;
493
+ const border = borderRef.value;
494
+ if (!border) return;
495
+ const visual = resolveFieldBorderVisual(state);
496
+ border.stopAnimations();
497
+ if (duration === 0) border.setOptions(visual);
498
+ else border.animate(visual, { duration });
499
+ }
500
+ const handlers = {
501
+ onMouseenter() {
502
+ if (disabled()) return;
503
+ hovered = true;
504
+ apply(TRANSITION_DURATION.hover);
505
+ },
506
+ onMouseleave() {
507
+ hovered = false;
508
+ apply(TRANSITION_DURATION.leave);
509
+ }
510
+ };
511
+ watch(disabled, (isDisabled) => {
512
+ if (isDisabled) hovered = false;
513
+ apply(0, true);
514
+ });
515
+ watch(active, (isActive) => {
516
+ apply(disabled() ? 0 : isActive ? TRANSITION_DURATION.active : TRANSITION_DURATION.leave);
517
+ });
518
+ onUnmounted(() => borderRef.value?.stopAnimations());
519
+ return {
520
+ borderRef,
521
+ handlers,
522
+ initialVisual: resolveFieldBorderVisual(state)
523
+ };
524
+ }
525
+ //#endregion
526
+ //#region packages/components/src/components/field/field.tsx
527
+ var LABEL_TRANSITION_DURATION = 150;
528
+ var FField = /* @__PURE__ */ defineComponent({
529
+ name: "FField",
530
+ props: {
531
+ active: {
532
+ type: Boolean,
533
+ default: false
534
+ },
535
+ disabled: {
536
+ type: Boolean,
537
+ default: false
538
+ },
539
+ filled: {
540
+ type: Boolean,
541
+ default: false
542
+ },
543
+ hasLabel: {
544
+ type: Boolean,
545
+ default: false
546
+ },
547
+ width: {
548
+ type: Number,
549
+ required: true
550
+ },
551
+ height: {
552
+ type: Number,
553
+ required: true
554
+ },
555
+ paddingLeft: {
556
+ type: Number,
557
+ required: true
558
+ },
559
+ paddingRight: {
560
+ type: Number,
561
+ required: true
562
+ },
563
+ suffixWidth: {
564
+ type: Number,
565
+ default: 0
566
+ },
567
+ fontSize: {
568
+ type: Number,
569
+ required: true
570
+ },
571
+ labelFontSize: {
572
+ type: Number,
573
+ required: true
574
+ },
575
+ labelHeight: {
576
+ type: Number,
577
+ required: true
578
+ },
579
+ labelPaddingTop: {
580
+ type: Number,
581
+ required: true
582
+ },
583
+ cursor: {
584
+ type: String,
585
+ default: "default"
586
+ },
587
+ elementRef: {
588
+ type: Function,
589
+ default: void 0
590
+ }
591
+ },
592
+ emits: ["click"],
593
+ setup(props, { emit, slots }) {
594
+ const labelRef = shallowRef(null);
595
+ const floated = computed(() => props.hasLabel && (props.active || props.filled));
596
+ const fieldBorder = useFieldBorder(() => props.disabled, () => props.active);
597
+ function resolveLabelVisual() {
598
+ return {
599
+ top: floated.value ? props.labelPaddingTop : (props.height - props.labelHeight) / 2,
600
+ fontSize: floated.value ? props.labelFontSize : props.fontSize,
601
+ color: !props.disabled && props.active ? MD3.primary : MD3.onSurfaceVariant
602
+ };
603
+ }
604
+ const initialLabelVisual = resolveLabelVisual();
605
+ function applyLabelVisual(duration) {
606
+ const label = labelRef.value;
607
+ if (!label) return;
608
+ const visual = resolveLabelVisual();
609
+ label.stopAnimations();
610
+ if (duration === 0) label.setOptions(visual);
611
+ else label.animate(visual, { duration });
612
+ }
613
+ watch([
614
+ () => props.active,
615
+ () => props.disabled,
616
+ () => props.filled,
617
+ () => props.hasLabel
618
+ ], () => applyLabelVisual(props.disabled ? 0 : LABEL_TRANSITION_DURATION), { flush: "post" });
619
+ watch([
620
+ () => props.height,
621
+ () => props.fontSize,
622
+ () => props.labelFontSize,
623
+ () => props.labelHeight,
624
+ () => props.labelPaddingTop
625
+ ], () => applyLabelVisual(0), { flush: "post" });
626
+ onMounted(() => {
627
+ nextTick(() => {
628
+ const label = labelRef.value;
629
+ if (!label) return;
630
+ label.syncTextLayout();
631
+ label.transform.markPaintDirty();
632
+ });
633
+ });
634
+ onUnmounted(() => labelRef.value?.stopAnimations());
635
+ return () => {
636
+ const showBody = !props.hasLabel || floated.value;
637
+ const labelSlotProps = {
638
+ ref: labelRef,
639
+ left: props.paddingLeft,
640
+ top: initialLabelVisual.top,
641
+ width: props.width - props.paddingLeft - props.paddingRight - props.suffixWidth,
642
+ height: props.labelHeight,
643
+ fitWidth: false,
644
+ lineHeight: 1,
645
+ fontSize: initialLabelVisual.fontSize,
646
+ color: initialLabelVisual.color,
647
+ silent: true,
648
+ pickable: false
649
+ };
650
+ return createVNode(resolveComponent("f-div"), {
651
+ "ref": props.elementRef,
652
+ "display": Display.Flex,
653
+ "flexDirection": FlexDirection.Row,
654
+ "alignItems": Align.Stretch,
655
+ "width": props.width,
656
+ "height": props.height,
657
+ "backgroundColor": MD3.surface,
658
+ "radius": MD3.radiusMd,
659
+ "paddingLeft": props.paddingLeft,
660
+ "paddingRight": props.paddingRight,
661
+ "cursor": props.cursor,
662
+ "onClick": (event) => emit("click", event),
663
+ "onMouseenter": fieldBorder.handlers.onMouseenter,
664
+ "onMouseleave": fieldBorder.handlers.onMouseleave
665
+ }, { default: () => [
666
+ slots.underlay?.(),
667
+ createVNode(resolveComponent("f-div"), {
668
+ "flex": 1,
669
+ "display": Display.Flex,
670
+ "flexDirection": FlexDirection.Column,
671
+ "justifyContent": Justify.Center
672
+ }, { default: () => [createVNode(resolveComponent("f-div"), { "height": props.hasLabel && floated.value ? props.labelHeight : 0 }, null), createVNode(resolveComponent("f-div"), {
673
+ "flex": 1,
674
+ "display": Display.Flex,
675
+ "alignItems": Align.Center
676
+ }, { default: () => [showBody ? slots.default?.() : null] })] }),
677
+ slots.suffix?.(),
678
+ props.hasLabel ? slots.label?.(labelSlotProps) : null,
679
+ createVNode(resolveComponent("f-rectangle"), {
680
+ "ref": fieldBorder.borderRef,
681
+ "left": 0,
682
+ "top": 0,
683
+ "width": props.width,
684
+ "height": props.height,
685
+ "backgroundColor": "transparent",
686
+ "borderColor": fieldBorder.initialVisual.borderColor,
687
+ "borderWidth": fieldBorder.initialVisual.borderWidth,
688
+ "borderPosition": "inside",
689
+ "radius": MD3.radiusMd,
690
+ "silent": true,
691
+ "pickable": false
692
+ }, null)
693
+ ] });
694
+ };
695
+ }
696
+ });
697
+ //#endregion
698
+ //#region packages/components/src/components/input/input.tsx
699
+ var FInput = /* @__PURE__ */ defineComponent({
700
+ name: "FInput",
701
+ props: {
702
+ modelValue: {
703
+ type: String,
704
+ default: ""
705
+ },
706
+ label: {
707
+ type: String,
708
+ default: ""
709
+ },
710
+ placeholder: {
711
+ type: String,
712
+ default: ""
713
+ },
714
+ disabled: {
715
+ type: Boolean,
716
+ default: false
717
+ },
718
+ size: controlSizeProp,
719
+ width: {
720
+ type: Number,
721
+ default: 280
722
+ },
723
+ height: {
724
+ type: Number,
725
+ default: void 0
726
+ },
727
+ fontSize: {
728
+ type: Number,
729
+ default: void 0
730
+ }
731
+ },
732
+ emits: [
733
+ "update:modelValue",
734
+ "input",
735
+ "change"
736
+ ],
737
+ setup(props, { emit }) {
738
+ const focused = ref(false);
739
+ const textRef = shallowRef(null);
740
+ let editStartValue = props.modelValue;
741
+ let draftValue = props.modelValue;
742
+ const metrics = computed(() => FIELD_SIZE[props.size]);
743
+ const height = computed(() => props.height ?? metrics.value.height);
744
+ const fontSize = computed(() => props.fontSize ?? metrics.value.fontSize);
745
+ const hasValue = computed(() => props.modelValue.length > 0);
746
+ function setFocused(value) {
747
+ focused.value = !props.disabled && value;
748
+ }
749
+ function beginEditing() {
750
+ if (props.disabled || focused.value) return;
751
+ editStartValue = props.modelValue;
752
+ draftValue = props.modelValue;
753
+ setFocused(true);
754
+ }
755
+ function updateValue(value) {
756
+ if (props.disabled) return;
757
+ draftValue = value;
758
+ emit("update:modelValue", value);
759
+ emit("input", value);
760
+ }
761
+ function finishEditing(value = draftValue) {
762
+ draftValue = value;
763
+ const changed = !props.disabled && draftValue !== editStartValue;
764
+ setFocused(false);
765
+ if (changed) emit("change", draftValue);
766
+ editStartValue = draftValue;
767
+ }
768
+ function handleClick() {
769
+ if (props.disabled) return;
770
+ const text = textRef.value;
771
+ if (text) {
772
+ text.enterEditing();
773
+ beginEditing();
774
+ return;
775
+ }
776
+ beginEditing();
777
+ nextTick(() => textRef.value?.enterEditing());
778
+ }
779
+ watch(() => props.modelValue, (value) => {
780
+ const text = textRef.value;
781
+ if (text) text.setEditingText(value);
782
+ if (!focused.value || value !== draftValue) {
783
+ editStartValue = value;
784
+ draftValue = value;
785
+ }
786
+ }, { flush: "post" });
787
+ watch(() => props.disabled, (disabled) => {
788
+ if (!disabled) return;
789
+ textRef.value?.exitEditing();
790
+ setFocused(false);
791
+ });
792
+ return () => createVNode(FField, {
793
+ "active": focused.value,
794
+ "disabled": props.disabled,
795
+ "filled": hasValue.value,
796
+ "hasLabel": Boolean(props.label),
797
+ "width": props.width,
798
+ "height": height.value,
799
+ "paddingLeft": metrics.value.horizontalPadding,
800
+ "paddingRight": metrics.value.horizontalPadding,
801
+ "fontSize": fontSize.value,
802
+ "labelFontSize": metrics.value.labelFontSize,
803
+ "labelHeight": metrics.value.labelHeight,
804
+ "labelPaddingTop": metrics.value.labelPaddingTop,
805
+ "cursor": props.disabled ? "default" : "text",
806
+ "onClick": handleClick
807
+ }, {
808
+ label: (labelProps) => createVNode(resolveComponent("f-text"), mergeProps(labelProps, {
809
+ "text": props.label,
810
+ "fontFamily": MD3.fontFamily,
811
+ "fontWeight": 500,
812
+ "textAlign": "left",
813
+ "verticalAlign": "middle",
814
+ "overflow": "hidden",
815
+ "wordWrap": false,
816
+ "ellipsis": true
817
+ }), null),
818
+ default: () => createVNode(resolveComponent("f-text"), {
819
+ "ref": textRef,
820
+ "text": props.modelValue,
821
+ "placeholder": props.placeholder,
822
+ "placeholderColor": MD3.onSurfaceVariant,
823
+ "fontSize": fontSize.value,
824
+ "fontFamily": MD3.fontFamily,
825
+ "color": props.disabled ? MD3.outline : MD3.onSurface,
826
+ "textAlign": "left",
827
+ "verticalAlign": "middle",
828
+ "editable": !props.disabled,
829
+ "fitWidth": true,
830
+ "fitHeight": true,
831
+ "overflow": "hidden",
832
+ "wordWrap": false,
833
+ "ellipsis": true,
834
+ "onInput": (event) => {
835
+ updateValue(event.detail);
836
+ },
837
+ "onChange": (event) => {
838
+ finishEditing(event.detail);
839
+ }
840
+ }, null)
841
+ });
842
+ }
843
+ });
844
+ //#endregion
845
+ //#region packages/components/src/components/tooltip/tooltip.tsx
846
+ function _isSlot$3(s) {
847
+ return typeof s === "function" || Object.prototype.toString.call(s) === "[object Object]" && !isVNode(s);
848
+ }
849
+ var FLOATING_TRANSITION_DURATION = 120;
850
+ /** 由所属 Root 的唯一 completion 路径定位的受控 Canvas tooltip。 */
851
+ var FTooltip = /* @__PURE__ */ defineComponent({
852
+ name: "FTooltip",
853
+ props: {
854
+ visible: {
855
+ type: Boolean,
856
+ default: false
857
+ },
858
+ triggerRef: {
859
+ type: Object,
860
+ default: null
861
+ },
862
+ placement: {
863
+ type: String,
864
+ default: "bottom-start"
865
+ },
866
+ width: {
867
+ type: Number,
868
+ default: void 0
869
+ },
870
+ gap: {
871
+ type: Number,
872
+ default: 4
873
+ }
874
+ },
875
+ emits: [
876
+ "update:visible",
877
+ "clickOutside",
878
+ "mouseenter",
879
+ "mouseleave"
880
+ ],
881
+ setup(props, { emit, slots }) {
882
+ const { overlay } = useOverlay();
883
+ const floatingRef = shallowRef(null);
884
+ const triggerElement = computed(() => unref(props.triggerRef) ?? null);
885
+ const positionOptions = computed(() => ({
886
+ placement: props.placement,
887
+ middleware: [
888
+ offset(props.gap),
889
+ flip(),
890
+ shift({ crossAxis: true })
891
+ ]
892
+ }));
893
+ useOutsideClick(() => props.visible, [triggerElement, floatingRef], () => {
894
+ emit("clickOutside");
895
+ emit("update:visible", false);
896
+ });
897
+ useOverlayPosition(() => props.visible, triggerElement, floatingRef, positionOptions);
898
+ return () => {
899
+ if (!overlay) {
900
+ if (!props.visible) return null;
901
+ throw new Error("[fulate-components] FTooltip requires createApp or registerVueOverlay");
902
+ }
903
+ const panel = props.visible ? createVNode(resolveComponent("f-div"), {
904
+ "ref": floatingRef,
905
+ "width": props.width,
906
+ "backgroundColor": MD3.surface,
907
+ "radius": MD3.radiusSm,
908
+ "borderColor": MD3.outlineVariant,
909
+ "borderWidth": .5,
910
+ "borderPosition": "inside",
911
+ "onMouseenter": (event) => emit("mouseenter", event),
912
+ "onMouseleave": (event) => emit("mouseleave", event)
913
+ }, { default: () => [slots.default?.()] }) : null;
914
+ return createVNode(Teleport, { "to": overlay }, { default: () => [createVNode(FTransition, {
915
+ "appear": true,
916
+ "enterFrom": {
917
+ opacity: 0,
918
+ scaleX: .96,
919
+ scaleY: .96
920
+ },
921
+ "enterTo": {
922
+ opacity: 1,
923
+ scaleX: 1,
924
+ scaleY: 1
925
+ },
926
+ "leaveTo": {
927
+ opacity: 0,
928
+ scaleX: .96,
929
+ scaleY: .96
930
+ },
931
+ "duration": FLOATING_TRANSITION_DURATION
932
+ }, _isSlot$3(panel) ? panel : { default: () => [panel] })] });
933
+ };
934
+ }
935
+ });
936
+ //#endregion
937
+ //#region packages/components/src/components/select/select.tsx
938
+ function _isSlot$2(s) {
939
+ return typeof s === "function" || Object.prototype.toString.call(s) === "[object Object]" && !isVNode(s);
940
+ }
941
+ var MAX_VISIBLE = 5;
942
+ var SELECT_SIZE = {
943
+ small: {
944
+ endPadding: 8,
945
+ indicatorWidth: 20,
946
+ indicatorFontSize: 14,
947
+ itemHeight: 36,
948
+ menuVerticalPadding: 4
949
+ },
950
+ medium: {
951
+ endPadding: 10,
952
+ indicatorWidth: 24,
953
+ indicatorFontSize: 16,
954
+ itemHeight: 44,
955
+ menuVerticalPadding: 6
956
+ },
957
+ large: {
958
+ endPadding: 12,
959
+ indicatorWidth: 28,
960
+ indicatorFontSize: 18,
961
+ itemHeight: 52,
962
+ menuVerticalPadding: 8
963
+ }
964
+ };
965
+ var FSelect = /* @__PURE__ */ defineComponent({
966
+ name: "FSelect",
967
+ props: {
968
+ modelValue: {
969
+ type: String,
970
+ default: ""
971
+ },
972
+ label: {
973
+ type: String,
974
+ default: ""
975
+ },
976
+ options: {
977
+ type: Array,
978
+ default: () => []
979
+ },
980
+ placeholder: {
981
+ type: String,
982
+ default: "请选择"
983
+ },
984
+ disabled: {
985
+ type: Boolean,
986
+ default: false
987
+ },
988
+ size: controlSizeProp,
989
+ width: {
990
+ type: Number,
991
+ default: 280
992
+ },
993
+ height: {
994
+ type: Number,
995
+ default: void 0
996
+ },
997
+ fontSize: {
998
+ type: Number,
999
+ default: void 0
1000
+ }
1001
+ },
1002
+ emits: [
1003
+ "update:modelValue",
1004
+ "change",
1005
+ "visibleChange"
1006
+ ],
1007
+ setup(props, { emit }) {
1008
+ const open = ref(false);
1009
+ const activeIndex = ref(-1);
1010
+ const triggerRef = shallowRef(null);
1011
+ const { rippleRef, trigger: triggerRipple } = useRippleAnimation();
1012
+ const fieldMetrics = computed(() => FIELD_SIZE[props.size]);
1013
+ const selectMetrics = computed(() => SELECT_SIZE[props.size]);
1014
+ const height = computed(() => props.height ?? fieldMetrics.value.height);
1015
+ const fontSize = computed(() => props.fontSize ?? fieldMetrics.value.fontSize);
1016
+ const selectedIndex = computed(() => props.options.findIndex((option) => option.value === props.modelValue));
1017
+ const selectedOption = computed(() => props.options[selectedIndex.value]);
1018
+ const hasValue = computed(() => !isUndefined(selectedOption.value));
1019
+ const maxDropdownHeight = computed(() => Math.min(props.options.length, MAX_VISIBLE) * selectMetrics.value.itemHeight + selectMetrics.value.menuVerticalPadding * 2);
1020
+ function getDisplayText() {
1021
+ return selectedOption.value?.label ?? props.placeholder;
1022
+ }
1023
+ function getTextColor() {
1024
+ if (props.disabled) return MD3.outline;
1025
+ if (!selectedOption.value) return MD3.onSurfaceVariant;
1026
+ return MD3.onSurface;
1027
+ }
1028
+ function doClose() {
1029
+ if (!open.value) return;
1030
+ open.value = false;
1031
+ activeIndex.value = -1;
1032
+ emit("visibleChange", false);
1033
+ }
1034
+ function openDropdown() {
1035
+ if (props.disabled || open.value) return;
1036
+ open.value = true;
1037
+ activeIndex.value = selectedIndex.value >= 0 ? selectedIndex.value : props.options.length ? 0 : -1;
1038
+ emit("visibleChange", true);
1039
+ }
1040
+ function setTriggerElement(element) {
1041
+ triggerRef.value = element;
1042
+ }
1043
+ function toggle(event) {
1044
+ if (props.disabled) return;
1045
+ if (open.value) doClose();
1046
+ else {
1047
+ openDropdown();
1048
+ if (event?.detail) triggerRipple(event.detail.x, event.detail.y);
1049
+ }
1050
+ }
1051
+ function select(value) {
1052
+ if (props.disabled) return;
1053
+ if (value !== props.modelValue) {
1054
+ emit("update:modelValue", value);
1055
+ emit("change", value);
1056
+ }
1057
+ doClose();
1058
+ }
1059
+ watch(() => props.disabled, (disabled) => {
1060
+ if (disabled) doClose();
1061
+ });
1062
+ watch(() => props.options.length, (length) => {
1063
+ if (!open.value) return;
1064
+ if (length === 0) activeIndex.value = -1;
1065
+ else if (activeIndex.value < 0) activeIndex.value = selectedIndex.value >= 0 ? selectedIndex.value : 0;
1066
+ else if (activeIndex.value >= length) activeIndex.value = length - 1;
1067
+ });
1068
+ return () => {
1069
+ let _slot;
1070
+ return createVNode(resolveComponent("f-div"), {
1071
+ "display": Display.Flex,
1072
+ "flexDirection": FlexDirection.Column,
1073
+ "width": props.width
1074
+ }, { default: () => [createVNode(FField, {
1075
+ "active": open.value,
1076
+ "disabled": props.disabled,
1077
+ "filled": hasValue.value,
1078
+ "hasLabel": Boolean(props.label),
1079
+ "width": props.width,
1080
+ "height": height.value,
1081
+ "paddingLeft": fieldMetrics.value.horizontalPadding,
1082
+ "paddingRight": selectMetrics.value.endPadding,
1083
+ "suffixWidth": selectMetrics.value.indicatorWidth,
1084
+ "fontSize": fontSize.value,
1085
+ "labelFontSize": fieldMetrics.value.labelFontSize,
1086
+ "labelHeight": fieldMetrics.value.labelHeight,
1087
+ "labelPaddingTop": fieldMetrics.value.labelPaddingTop,
1088
+ "cursor": props.disabled ? "default" : "pointer",
1089
+ "elementRef": setTriggerElement,
1090
+ "onClick": toggle
1091
+ }, {
1092
+ underlay: () => createVNode(resolveComponent("f-ripple"), {
1093
+ "ref": rippleRef,
1094
+ "rippleColor": MD3.primary,
1095
+ "rippleOpacity": MD3.rippleOpacity,
1096
+ "duration": 400
1097
+ }, null),
1098
+ label: (labelProps) => createVNode(resolveComponent("f-text"), mergeProps(labelProps, {
1099
+ "text": props.label,
1100
+ "fontFamily": MD3.fontFamily,
1101
+ "fontWeight": 500,
1102
+ "textAlign": "left",
1103
+ "verticalAlign": "middle",
1104
+ "overflow": "hidden",
1105
+ "wordWrap": false,
1106
+ "ellipsis": true
1107
+ }), null),
1108
+ default: () => createVNode(resolveComponent("f-text"), {
1109
+ "text": getDisplayText(),
1110
+ "fitWidth": true,
1111
+ "fitHeight": true,
1112
+ "overflow": "hidden",
1113
+ "wordWrap": false,
1114
+ "ellipsis": true,
1115
+ "fontSize": fontSize.value,
1116
+ "fontFamily": MD3.fontFamily,
1117
+ "color": getTextColor(),
1118
+ "textAlign": "left",
1119
+ "verticalAlign": "middle"
1120
+ }, null),
1121
+ suffix: () => createVNode(resolveComponent("f-div"), {
1122
+ "width": selectMetrics.value.indicatorWidth,
1123
+ "flexShrink": 0
1124
+ }, { default: () => [createVNode(resolveComponent("f-text"), {
1125
+ "fitWidth": true,
1126
+ "fitHeight": true,
1127
+ "overflow": "hidden",
1128
+ "wordWrap": false,
1129
+ "text": open.value ? "▴" : "▾",
1130
+ "fontSize": selectMetrics.value.indicatorFontSize,
1131
+ "color": open.value ? MD3.primary : MD3.onSurfaceVariant,
1132
+ "textAlign": "center",
1133
+ "verticalAlign": "middle"
1134
+ }, null)] })
1135
+ }), createVNode(FTooltip, {
1136
+ "visible": open.value,
1137
+ "triggerRef": triggerRef,
1138
+ "width": props.width,
1139
+ "onClickOutside": doClose
1140
+ }, { default: () => [createVNode(resolveComponent("f-div"), {
1141
+ "width": props.width,
1142
+ "height": maxDropdownHeight.value
1143
+ }, { default: () => [createVNode(resolveComponent("f-scrollview"), {
1144
+ "width": props.width,
1145
+ "height": maxDropdownHeight.value,
1146
+ "maxHeight": maxDropdownHeight.value,
1147
+ "scrollbarSize": 4,
1148
+ "scrollbarColor": "rgba(0,0,0,0.15)"
1149
+ }, { default: () => [createVNode(resolveComponent("f-div"), {
1150
+ "display": Display.Flex,
1151
+ "flexDirection": FlexDirection.Column,
1152
+ "width": props.width,
1153
+ "paddingTop": selectMetrics.value.menuVerticalPadding,
1154
+ "paddingBottom": selectMetrics.value.menuVerticalPadding
1155
+ }, _isSlot$2(_slot = props.options.map((opt, i) => createVNode(resolveComponent("f-div"), {
1156
+ "key": opt.value,
1157
+ "display": Display.Flex,
1158
+ "width": props.width,
1159
+ "height": selectMetrics.value.itemHeight,
1160
+ "flexShrink": 0,
1161
+ "backgroundColor": opt.value === props.modelValue ? MD3.primaryContainer : activeIndex.value === i ? MD3.surfaceDim : "transparent",
1162
+ "cursor": "pointer",
1163
+ "onClick": () => select(opt.value),
1164
+ "onPointermove": () => {
1165
+ activeIndex.value = i;
1166
+ }
1167
+ }, { default: () => [createVNode(resolveComponent("f-text"), {
1168
+ "left": fieldMetrics.value.horizontalPadding,
1169
+ "fitWidth": true,
1170
+ "fitHeight": true,
1171
+ "width": props.width - fieldMetrics.value.horizontalPadding * 2,
1172
+ "height": selectMetrics.value.itemHeight,
1173
+ "text": opt.label,
1174
+ "overflow": "hidden",
1175
+ "wordWrap": false,
1176
+ "ellipsis": true,
1177
+ "fontSize": fontSize.value,
1178
+ "fontFamily": MD3.fontFamily,
1179
+ "textAlign": "left",
1180
+ "color": opt.value === props.modelValue ? MD3.primary : MD3.onSurface,
1181
+ "fontWeight": opt.value === props.modelValue ? 600 : 400,
1182
+ "verticalAlign": "middle"
1183
+ }, null)] }))) ? _slot : { default: () => [_slot] })] })] })] })] });
1184
+ };
1185
+ }
1186
+ });
1187
+ //#endregion
1188
+ //#region packages/components/src/components/popover/popover.tsx
1189
+ var HOVER_CLOSE_DELAY = 100;
1190
+ /** A popover surface anchored by its reference slot or an external Element. */
1191
+ var FPopover = /* @__PURE__ */ defineComponent({
1192
+ name: "FPopover",
1193
+ props: {
1194
+ modelValue: {
1195
+ type: Boolean,
1196
+ default: void 0
1197
+ },
1198
+ visible: {
1199
+ type: Boolean,
1200
+ default: void 0
1201
+ },
1202
+ triggerRef: {
1203
+ type: Object,
1204
+ default: null
1205
+ },
1206
+ trigger: {
1207
+ type: String,
1208
+ default: "click"
1209
+ },
1210
+ placement: {
1211
+ type: String,
1212
+ default: "bottom-start"
1213
+ },
1214
+ title: {
1215
+ type: String,
1216
+ default: ""
1217
+ },
1218
+ content: {
1219
+ type: String,
1220
+ default: ""
1221
+ },
1222
+ width: {
1223
+ type: Number,
1224
+ default: void 0
1225
+ },
1226
+ gap: {
1227
+ type: Number,
1228
+ default: 8
1229
+ },
1230
+ disabled: {
1231
+ type: Boolean,
1232
+ default: false
1233
+ },
1234
+ closeOnEscape: {
1235
+ type: Boolean,
1236
+ default: true
1237
+ }
1238
+ },
1239
+ emits: [
1240
+ "update:modelValue",
1241
+ "update:visible",
1242
+ "clickOutside"
1243
+ ],
1244
+ setup(props, { emit, slots }) {
1245
+ const internalVisible = ref(false);
1246
+ const referenceRef = shallowRef(null);
1247
+ const isVisible = computed(() => props.visible ?? props.modelValue ?? internalVisible.value);
1248
+ const triggerElement = computed(() => slots.reference ? referenceRef.value : unref(props.triggerRef) ?? null);
1249
+ let triggerHovered = false;
1250
+ let panelHovered = false;
1251
+ let hoverCloseTimer = null;
1252
+ function clearHoverCloseTimer() {
1253
+ if (hoverCloseTimer === null) return;
1254
+ clearTimeout(hoverCloseTimer);
1255
+ hoverCloseTimer = null;
1256
+ }
1257
+ function setVisible(visible) {
1258
+ if (visible && props.disabled || visible === isVisible.value) return;
1259
+ const hasVisibleBinding = props.visible !== void 0;
1260
+ const hasModelBinding = props.modelValue !== void 0;
1261
+ if (!hasVisibleBinding && !hasModelBinding) {
1262
+ internalVisible.value = visible;
1263
+ emit("update:modelValue", visible);
1264
+ return;
1265
+ }
1266
+ if (hasModelBinding) emit("update:modelValue", visible);
1267
+ if (hasVisibleBinding) emit("update:visible", visible);
1268
+ }
1269
+ function open() {
1270
+ setVisible(true);
1271
+ }
1272
+ function close() {
1273
+ setVisible(false);
1274
+ }
1275
+ function toggle() {
1276
+ setVisible(!isVisible.value);
1277
+ }
1278
+ function scheduleHoverClose() {
1279
+ clearHoverCloseTimer();
1280
+ hoverCloseTimer = setTimeout(() => {
1281
+ hoverCloseTimer = null;
1282
+ if (!triggerHovered && !panelHovered) close();
1283
+ }, HOVER_CLOSE_DELAY);
1284
+ }
1285
+ function handleTriggerEnter() {
1286
+ triggerHovered = true;
1287
+ clearHoverCloseTimer();
1288
+ open();
1289
+ }
1290
+ function handleTriggerLeave() {
1291
+ triggerHovered = false;
1292
+ scheduleHoverClose();
1293
+ }
1294
+ function handlePanelEnter() {
1295
+ if (props.trigger !== "hover") return;
1296
+ panelHovered = true;
1297
+ clearHoverCloseTimer();
1298
+ }
1299
+ function handlePanelLeave() {
1300
+ if (props.trigger !== "hover") return;
1301
+ panelHovered = false;
1302
+ scheduleHoverClose();
1303
+ }
1304
+ function handleContextMenu(event) {
1305
+ event.preventDefault();
1306
+ open();
1307
+ }
1308
+ function setReference(value) {
1309
+ const element = value && typeof value === "object" && "$el" in value ? value.$el : value;
1310
+ if (element === null) {
1311
+ referenceRef.value = null;
1312
+ return;
1313
+ }
1314
+ if (!(element instanceof Element)) throw new Error("[fulate-components] FPopover reference must render one canvas Element");
1315
+ referenceRef.value = element;
1316
+ }
1317
+ function handleClickOutside() {
1318
+ emit("clickOutside");
1319
+ close();
1320
+ }
1321
+ function handleKeyDown(event) {
1322
+ if (isVisible.value && props.closeOnEscape && event.key === "Escape") {
1323
+ event.preventDefault();
1324
+ close();
1325
+ }
1326
+ }
1327
+ watch([
1328
+ triggerElement,
1329
+ () => props.trigger,
1330
+ () => props.disabled
1331
+ ], ([element, trigger, disabled], _previous, onCleanup) => {
1332
+ triggerHovered = false;
1333
+ panelHovered = false;
1334
+ clearHoverCloseTimer();
1335
+ if (!element || disabled || trigger === "manual") return;
1336
+ const removeListeners = [];
1337
+ if (trigger === "click") removeListeners.push(element.addEventListener("click", toggle));
1338
+ else if (trigger === "hover") removeListeners.push(element.addEventListener("mouseenter", handleTriggerEnter), element.addEventListener("mouseleave", handleTriggerLeave));
1339
+ else if (trigger === "focus") removeListeners.push(element.addEventListener("focus", open), element.addEventListener("blur", close));
1340
+ else removeListeners.push(element.addEventListener("contextmenu", handleContextMenu));
1341
+ onCleanup(() => {
1342
+ removeListeners.forEach((remove) => remove());
1343
+ });
1344
+ }, { immediate: true });
1345
+ watch(() => props.disabled, (disabled) => {
1346
+ if (disabled) close();
1347
+ });
1348
+ onMounted(() => {
1349
+ document.addEventListener("keydown", handleKeyDown, true);
1350
+ });
1351
+ onUnmounted(() => {
1352
+ clearHoverCloseTimer();
1353
+ document.removeEventListener("keydown", handleKeyDown, true);
1354
+ });
1355
+ return () => {
1356
+ const referenceNodes = slots.reference?.();
1357
+ if (referenceNodes && referenceNodes.length !== 1) throw new Error("[fulate-components] FPopover reference slot requires exactly one root");
1358
+ const reference = referenceNodes ? cloneVNode(referenceNodes[0], { ref: setReference }, true) : null;
1359
+ const popover = createVNode(FTooltip, {
1360
+ "visible": isVisible.value && !props.disabled,
1361
+ "triggerRef": triggerElement,
1362
+ "placement": props.placement,
1363
+ "width": props.width,
1364
+ "gap": props.gap,
1365
+ "onClickOutside": handleClickOutside,
1366
+ "onMouseenter": handlePanelEnter,
1367
+ "onMouseleave": handlePanelLeave
1368
+ }, { default: () => [createVNode(resolveComponent("f-div"), {
1369
+ "width": props.width,
1370
+ "display": Display.Flex,
1371
+ "flexDirection": FlexDirection.Column,
1372
+ "gap": 8,
1373
+ "padding": 16
1374
+ }, { default: () => [slots.title ? slots.title() : props.title ? createVNode(resolveComponent("f-span"), {
1375
+ "text": props.title,
1376
+ "fontSize": 14,
1377
+ "fontWeight": 600,
1378
+ "fontFamily": MD3.fontFamily,
1379
+ "color": MD3.onSurface
1380
+ }, null) : null, slots.default ? slots.default() : props.content ? createVNode(resolveComponent("f-span"), {
1381
+ "text": props.content,
1382
+ "fontSize": 13,
1383
+ "fontFamily": MD3.fontFamily,
1384
+ "color": MD3.onSurfaceVariant
1385
+ }, null) : null] })] });
1386
+ if (!reference) return popover;
1387
+ return [reference, popover];
1388
+ };
1389
+ }
1390
+ });
1391
+ //#endregion
1392
+ //#region packages/components/src/internal/state-layer.ts
1393
+ function useStateLayer(disabled, color = () => MD3.primary) {
1394
+ const layerRef = shallowRef(null);
1395
+ function backgroundFor(state) {
1396
+ if (state === "press") return colorWithAlpha(color(), MD3.statePressOpacity);
1397
+ if (state === "hover") return colorWithAlpha(color(), MD3.stateHoverOpacity);
1398
+ return "transparent";
1399
+ }
1400
+ function applyState(state, duration) {
1401
+ const layer = layerRef.value;
1402
+ if (!layer) return;
1403
+ const backgroundColor = backgroundFor(state);
1404
+ layer.stopAnimations();
1405
+ if (duration === 0) layer.setOptions({ backgroundColor });
1406
+ else layer.animate({ backgroundColor }, { duration });
1407
+ }
1408
+ const interaction = useInteractionState(disabled, applyState);
1409
+ watch(color, () => interaction.refresh(), { flush: "post" });
1410
+ return {
1411
+ layerRef,
1412
+ handlers: interaction.handlers
1413
+ };
1414
+ }
1415
+ //#endregion
1416
+ //#region packages/components/src/components/radio/radio.tsx
1417
+ var RADIO_SIZE = {
1418
+ small: {
1419
+ indicatorSize: 16,
1420
+ innerSize: 8
1421
+ },
1422
+ medium: {
1423
+ indicatorSize: 20,
1424
+ innerSize: 10
1425
+ },
1426
+ large: {
1427
+ indicatorSize: 24,
1428
+ innerSize: 12
1429
+ }
1430
+ };
1431
+ var RADIO_GROUP_KEY = Symbol("fulate-radio-group");
1432
+ var FRadioGroup = /* @__PURE__ */ defineComponent({
1433
+ name: "FRadioGroup",
1434
+ props: {
1435
+ modelValue: {
1436
+ type: String,
1437
+ default: ""
1438
+ },
1439
+ disabled: {
1440
+ type: Boolean,
1441
+ default: false
1442
+ },
1443
+ size: controlSizeProp,
1444
+ direction: {
1445
+ type: String,
1446
+ default: "column"
1447
+ },
1448
+ gap: {
1449
+ type: Number,
1450
+ default: void 0
1451
+ }
1452
+ },
1453
+ emits: ["update:modelValue", "change"],
1454
+ setup(props, { emit, slots }) {
1455
+ function select(value) {
1456
+ if (props.disabled || value === props.modelValue) return;
1457
+ emit("update:modelValue", value);
1458
+ emit("change", value);
1459
+ }
1460
+ provide(RADIO_GROUP_KEY, {
1461
+ disabled: () => props.disabled,
1462
+ size: () => props.size,
1463
+ isSelected: (value) => props.modelValue === value,
1464
+ select
1465
+ });
1466
+ return () => createVNode(resolveComponent("f-div"), {
1467
+ "display": Display.Flex,
1468
+ "flexDirection": props.direction === "row" ? FlexDirection.Row : FlexDirection.Column,
1469
+ "gap": props.gap ?? CONTROL_GROUP_GAP[props.size]
1470
+ }, { default: () => [slots.default?.()] });
1471
+ }
1472
+ });
1473
+ var FRadio = /* @__PURE__ */ defineComponent({
1474
+ name: "FRadio",
1475
+ props: {
1476
+ value: {
1477
+ type: String,
1478
+ required: true
1479
+ },
1480
+ label: {
1481
+ type: String,
1482
+ default: ""
1483
+ },
1484
+ disabled: {
1485
+ type: Boolean,
1486
+ default: false
1487
+ },
1488
+ size: inheritedControlSizeProp,
1489
+ modelValue: {
1490
+ type: Boolean,
1491
+ default: void 0
1492
+ }
1493
+ },
1494
+ emits: ["update:modelValue", "change"],
1495
+ setup(props, { emit }) {
1496
+ const group = inject(RADIO_GROUP_KEY, null);
1497
+ const localSelected = ref(false);
1498
+ const size = computed(() => props.size ?? group?.size() ?? "medium");
1499
+ const metrics = computed(() => RADIO_SIZE[size.value]);
1500
+ const stateLayerSize = computed(() => CONTROL_STATE_LAYER_SIZE[size.value]);
1501
+ const isDisabled = computed(() => props.disabled || (group?.disabled() ?? false));
1502
+ const isSelected = computed(() => group ? group.isSelected(props.value) : props.modelValue ?? localSelected.value);
1503
+ const { layerRef, handlers } = useStateLayer(() => isDisabled.value, () => isSelected.value ? MD3.primary : MD3.onSurface);
1504
+ function onClick() {
1505
+ if (isDisabled.value || isSelected.value) return;
1506
+ if (group) {
1507
+ group.select(props.value);
1508
+ emit("change", true);
1509
+ return;
1510
+ }
1511
+ localSelected.value = true;
1512
+ emit("update:modelValue", true);
1513
+ emit("change", true);
1514
+ }
1515
+ function getCircleColor() {
1516
+ if (isDisabled.value) return MD3.outlineVariant;
1517
+ if (isSelected.value) return MD3.primary;
1518
+ return MD3.outline;
1519
+ }
1520
+ return () => createVNode(resolveComponent("f-div"), mergeProps({
1521
+ "display": Display.Flex,
1522
+ "flexDirection": FlexDirection.Row,
1523
+ "alignItems": Align.Center,
1524
+ "gap": 0,
1525
+ "height": stateLayerSize.value,
1526
+ "cursor": isDisabled.value ? "default" : "pointer",
1527
+ "onClick": onClick
1528
+ }, handlers), { default: () => [createVNode(resolveComponent("f-div"), {
1529
+ "ref": layerRef,
1530
+ "width": stateLayerSize.value,
1531
+ "height": stateLayerSize.value,
1532
+ "flexShrink": 0,
1533
+ "radius": stateLayerSize.value / 2,
1534
+ "backgroundColor": "transparent",
1535
+ "silent": true,
1536
+ "pickable": false
1537
+ }, { default: () => [createVNode(resolveComponent("f-rectangle"), {
1538
+ "left": (stateLayerSize.value - metrics.value.indicatorSize) / 2,
1539
+ "top": (stateLayerSize.value - metrics.value.indicatorSize) / 2,
1540
+ "width": metrics.value.indicatorSize,
1541
+ "height": metrics.value.indicatorSize,
1542
+ "radius": metrics.value.indicatorSize / 2,
1543
+ "borderColor": getCircleColor(),
1544
+ "borderWidth": isSelected.value ? 2 : 1.5,
1545
+ "borderPosition": "inside",
1546
+ "backgroundColor": "transparent"
1547
+ }, null), isSelected.value && createVNode(resolveComponent("f-rectangle"), {
1548
+ "left": (stateLayerSize.value - metrics.value.innerSize) / 2,
1549
+ "top": (stateLayerSize.value - metrics.value.innerSize) / 2,
1550
+ "width": metrics.value.innerSize,
1551
+ "height": metrics.value.innerSize,
1552
+ "radius": metrics.value.innerSize / 2,
1553
+ "backgroundColor": isDisabled.value ? MD3.outlineVariant : MD3.primary
1554
+ }, null)] }), props.label && createVNode(resolveComponent("f-span"), {
1555
+ "text": props.label,
1556
+ "fontSize": CONTROL_LABEL_FONT_SIZE[size.value],
1557
+ "fontFamily": MD3.fontFamily,
1558
+ "color": isDisabled.value ? MD3.onSurfaceVariant : MD3.onSurface
1559
+ }, null)] });
1560
+ }
1561
+ });
1562
+ //#endregion
1563
+ //#region packages/components/src/components/checkbox/checkbox.tsx
1564
+ var CHECKBOX_SIZE = {
1565
+ small: {
1566
+ indicatorSize: 16,
1567
+ iconSize: 14,
1568
+ labelPadding: 2
1569
+ },
1570
+ medium: {
1571
+ indicatorSize: 18,
1572
+ iconSize: 16,
1573
+ labelPadding: 4
1574
+ },
1575
+ large: {
1576
+ indicatorSize: 20,
1577
+ iconSize: 18,
1578
+ labelPadding: 6
1579
+ }
1580
+ };
1581
+ var CHECKBOX_GROUP_KEY = Symbol("fulate-checkbox-group");
1582
+ var FCheckboxGroup = /* @__PURE__ */ defineComponent({
1583
+ name: "FCheckboxGroup",
1584
+ props: {
1585
+ modelValue: {
1586
+ type: Array,
1587
+ default: () => []
1588
+ },
1589
+ disabled: {
1590
+ type: Boolean,
1591
+ default: false
1592
+ },
1593
+ size: controlSizeProp,
1594
+ direction: {
1595
+ type: String,
1596
+ default: "column"
1597
+ },
1598
+ gap: {
1599
+ type: Number,
1600
+ default: void 0
1601
+ }
1602
+ },
1603
+ emits: ["update:modelValue", "change"],
1604
+ setup(props, { emit, slots }) {
1605
+ function toggle(value) {
1606
+ if (props.disabled) return;
1607
+ const unique = [...new Set(props.modelValue)];
1608
+ const nextValue = unique.includes(value) ? unique.filter((item) => item !== value) : [...unique, value];
1609
+ emit("update:modelValue", nextValue);
1610
+ emit("change", nextValue);
1611
+ }
1612
+ provide(CHECKBOX_GROUP_KEY, {
1613
+ disabled: () => props.disabled,
1614
+ size: () => props.size,
1615
+ isChecked: (value) => props.modelValue.includes(value),
1616
+ toggle
1617
+ });
1618
+ return () => createVNode(resolveComponent("f-div"), {
1619
+ "display": Display.Flex,
1620
+ "flexDirection": props.direction === "row" ? FlexDirection.Row : FlexDirection.Column,
1621
+ "gap": props.gap ?? CONTROL_GROUP_GAP[props.size]
1622
+ }, { default: () => [slots.default?.()] });
1623
+ }
1624
+ });
1625
+ var FCheckbox = /* @__PURE__ */ defineComponent({
1626
+ name: "FCheckbox",
1627
+ props: {
1628
+ value: {
1629
+ type: String,
1630
+ default: ""
1631
+ },
1632
+ label: {
1633
+ type: String,
1634
+ default: ""
1635
+ },
1636
+ disabled: {
1637
+ type: Boolean,
1638
+ default: false
1639
+ },
1640
+ size: inheritedControlSizeProp,
1641
+ modelValue: {
1642
+ type: Boolean,
1643
+ default: void 0
1644
+ }
1645
+ },
1646
+ emits: ["update:modelValue", "change"],
1647
+ setup(props, { emit }) {
1648
+ const group = inject(CHECKBOX_GROUP_KEY, null);
1649
+ const localChecked = ref(false);
1650
+ const size = computed(() => props.size ?? group?.size() ?? "medium");
1651
+ const metrics = computed(() => CHECKBOX_SIZE[size.value]);
1652
+ const stateLayerSize = computed(() => CONTROL_STATE_LAYER_SIZE[size.value]);
1653
+ const isDisabled = computed(() => props.disabled || (group?.disabled() ?? false));
1654
+ const isChecked = computed(() => {
1655
+ if (group) return group.isChecked(props.value);
1656
+ return props.modelValue ?? localChecked.value;
1657
+ });
1658
+ const { layerRef, handlers } = useStateLayer(() => isDisabled.value, () => isChecked.value ? MD3.primary : MD3.onSurface);
1659
+ function onClick() {
1660
+ if (isDisabled.value) return;
1661
+ const value = !isChecked.value;
1662
+ if (group) {
1663
+ group.toggle(props.value);
1664
+ emit("change", value);
1665
+ } else {
1666
+ localChecked.value = value;
1667
+ emit("update:modelValue", value);
1668
+ emit("change", value);
1669
+ }
1670
+ }
1671
+ function getBoxBorderColor() {
1672
+ if (isDisabled.value) return MD3.outlineVariant;
1673
+ if (isChecked.value) return MD3.primary;
1674
+ return MD3.outline;
1675
+ }
1676
+ return () => createVNode(resolveComponent("f-div"), mergeProps({
1677
+ "display": Display.Flex,
1678
+ "flexDirection": FlexDirection.Row,
1679
+ "alignItems": Align.Center,
1680
+ "gap": 0,
1681
+ "height": stateLayerSize.value,
1682
+ "cursor": isDisabled.value ? "default" : "pointer",
1683
+ "onClick": onClick
1684
+ }, handlers), { default: () => [createVNode(resolveComponent("f-div"), {
1685
+ "ref": layerRef,
1686
+ "width": stateLayerSize.value,
1687
+ "height": stateLayerSize.value,
1688
+ "flexShrink": 0,
1689
+ "radius": stateLayerSize.value / 2,
1690
+ "backgroundColor": "transparent",
1691
+ "silent": true,
1692
+ "pickable": false
1693
+ }, { default: () => [createVNode(resolveComponent("f-rectangle"), {
1694
+ "left": (stateLayerSize.value - metrics.value.indicatorSize) / 2,
1695
+ "top": (stateLayerSize.value - metrics.value.indicatorSize) / 2,
1696
+ "width": metrics.value.indicatorSize,
1697
+ "height": metrics.value.indicatorSize,
1698
+ "radius": MD3.radiusSm,
1699
+ "borderColor": getBoxBorderColor(),
1700
+ "borderWidth": isChecked.value ? 0 : 1.5,
1701
+ "borderPosition": "inside",
1702
+ "backgroundColor": isChecked.value ? isDisabled.value ? MD3.outlineVariant : MD3.primary : "transparent"
1703
+ }, null), isChecked.value && createVNode(resolveComponent("f-text"), {
1704
+ "left": (stateLayerSize.value - metrics.value.indicatorSize) / 2,
1705
+ "top": (stateLayerSize.value - metrics.value.indicatorSize) / 2,
1706
+ "width": metrics.value.indicatorSize,
1707
+ "height": metrics.value.indicatorSize,
1708
+ "text": "",
1709
+ "fontFamily": MD3.iconFamily,
1710
+ "fontSize": metrics.value.iconSize,
1711
+ "color": MD3.onPrimary,
1712
+ "textAlign": "center",
1713
+ "verticalAlign": "middle",
1714
+ "overflow": "visible"
1715
+ }, null)] }), props.label && createVNode(resolveComponent("f-div"), { "paddingLeft": metrics.value.labelPadding }, { default: () => [createVNode(resolveComponent("f-span"), {
1716
+ "text": props.label,
1717
+ "fontSize": CONTROL_LABEL_FONT_SIZE[size.value],
1718
+ "fontFamily": MD3.fontFamily,
1719
+ "color": isDisabled.value ? MD3.onSurfaceVariant : MD3.onSurface
1720
+ }, null)] })] });
1721
+ }
1722
+ });
1723
+ //#endregion
1724
+ //#region packages/components/src/components/switch/switch.tsx
1725
+ var SWITCH_SIZE = {
1726
+ small: {
1727
+ trackWidth: 44,
1728
+ trackHeight: 24,
1729
+ thumbSize: 16,
1730
+ thumbOffset: 4,
1731
+ labelGap: 8
1732
+ },
1733
+ medium: {
1734
+ trackWidth: 52,
1735
+ trackHeight: 32,
1736
+ thumbSize: 24,
1737
+ thumbOffset: 4,
1738
+ labelGap: 10
1739
+ },
1740
+ large: {
1741
+ trackWidth: 60,
1742
+ trackHeight: 40,
1743
+ thumbSize: 32,
1744
+ thumbOffset: 4,
1745
+ labelGap: 12
1746
+ }
1747
+ };
1748
+ var FSwitch = /* @__PURE__ */ defineComponent({
1749
+ name: "FSwitch",
1750
+ props: {
1751
+ modelValue: {
1752
+ type: Boolean,
1753
+ default: false
1754
+ },
1755
+ disabled: {
1756
+ type: Boolean,
1757
+ default: false
1758
+ },
1759
+ activeColor: {
1760
+ type: String,
1761
+ default: MD3.primary
1762
+ },
1763
+ size: controlSizeProp,
1764
+ label: {
1765
+ type: String,
1766
+ default: ""
1767
+ }
1768
+ },
1769
+ emits: ["update:modelValue", "change"],
1770
+ setup(props, { emit }) {
1771
+ const thumbRef = shallowRef(null);
1772
+ const stateLayerPositionRef = shallowRef(null);
1773
+ const metrics = computed(() => SWITCH_SIZE[props.size]);
1774
+ const stateLayerSize = computed(() => CONTROL_STATE_LAYER_SIZE[props.size]);
1775
+ const thumbLeft = computed(() => props.modelValue ? metrics.value.trackWidth - metrics.value.thumbSize - metrics.value.thumbOffset : metrics.value.thumbOffset);
1776
+ const stateLayerLeft = computed(() => props.modelValue ? metrics.value.trackWidth - stateLayerSize.value / 2 - metrics.value.thumbSize / 2 - metrics.value.thumbOffset : metrics.value.thumbOffset + metrics.value.thumbSize / 2 - stateLayerSize.value / 2);
1777
+ const { layerRef, handlers } = useStateLayer(() => props.disabled, () => props.modelValue ? props.activeColor : MD3.onSurface);
1778
+ watch([() => props.modelValue, () => props.size], ([, size], [, previousSize]) => {
1779
+ const thumb = thumbRef.value;
1780
+ const stateLayerPosition = stateLayerPositionRef.value;
1781
+ thumb?.stopAnimations();
1782
+ stateLayerPosition?.stopAnimations();
1783
+ if (size !== previousSize) {
1784
+ thumb?.setOptions({ left: thumbLeft.value });
1785
+ stateLayerPosition?.setOptions({ left: stateLayerLeft.value });
1786
+ } else {
1787
+ if (thumb) thumb.animate({ left: thumbLeft.value }, { duration: 150 });
1788
+ if (stateLayerPosition) stateLayerPosition.animate({ left: stateLayerLeft.value }, { duration: 150 });
1789
+ }
1790
+ });
1791
+ function toggle() {
1792
+ if (props.disabled) return;
1793
+ const value = !props.modelValue;
1794
+ emit("update:modelValue", value);
1795
+ emit("change", value);
1796
+ }
1797
+ function getTrackBg() {
1798
+ if (props.disabled) return props.modelValue ? MD3.outlineVariant : MD3.surfaceContainer;
1799
+ return props.modelValue ? props.activeColor : MD3.surfaceContainer;
1800
+ }
1801
+ function getTrackBorder() {
1802
+ if (props.disabled) return MD3.outlineVariant;
1803
+ return props.modelValue ? props.activeColor : MD3.outline;
1804
+ }
1805
+ function getThumbBg() {
1806
+ if (props.disabled) return props.modelValue ? MD3.surface : MD3.outlineVariant;
1807
+ return props.modelValue ? MD3.onPrimary : MD3.outline;
1808
+ }
1809
+ return () => createVNode(resolveComponent("f-div"), mergeProps({
1810
+ "display": Display.Flex,
1811
+ "flexDirection": FlexDirection.Row,
1812
+ "alignItems": Align.Center,
1813
+ "gap": props.label ? metrics.value.labelGap : 0,
1814
+ "cursor": props.disabled ? "default" : "pointer",
1815
+ "onClick": toggle
1816
+ }, handlers), { default: () => [createVNode(resolveComponent("f-div"), {
1817
+ "width": metrics.value.trackWidth,
1818
+ "height": stateLayerSize.value,
1819
+ "flexShrink": 0
1820
+ }, { default: () => [
1821
+ createVNode(resolveComponent("f-rectangle"), {
1822
+ "left": 0,
1823
+ "top": (stateLayerSize.value - metrics.value.trackHeight) / 2,
1824
+ "width": metrics.value.trackWidth,
1825
+ "height": metrics.value.trackHeight,
1826
+ "radius": metrics.value.trackHeight / 2,
1827
+ "backgroundColor": getTrackBg(),
1828
+ "borderColor": getTrackBorder(),
1829
+ "borderWidth": props.modelValue ? 0 : 2,
1830
+ "borderPosition": "inside"
1831
+ }, null),
1832
+ createVNode(resolveComponent("f-rectangle"), {
1833
+ "ref": stateLayerPositionRef,
1834
+ "left": stateLayerLeft.value,
1835
+ "top": 0,
1836
+ "width": stateLayerSize.value,
1837
+ "height": stateLayerSize.value,
1838
+ "radius": stateLayerSize.value / 2,
1839
+ "backgroundColor": "transparent",
1840
+ "silent": true,
1841
+ "pickable": false
1842
+ }, { default: () => [createVNode(resolveComponent("f-rectangle"), {
1843
+ "ref": layerRef,
1844
+ "width": stateLayerSize.value,
1845
+ "height": stateLayerSize.value,
1846
+ "radius": stateLayerSize.value / 2,
1847
+ "backgroundColor": "transparent",
1848
+ "silent": true,
1849
+ "pickable": false
1850
+ }, null)] }),
1851
+ createVNode(resolveComponent("f-rectangle"), {
1852
+ "ref": thumbRef,
1853
+ "left": thumbLeft.value,
1854
+ "top": (stateLayerSize.value - metrics.value.thumbSize) / 2,
1855
+ "width": metrics.value.thumbSize,
1856
+ "height": metrics.value.thumbSize,
1857
+ "radius": metrics.value.thumbSize / 2,
1858
+ "backgroundColor": getThumbBg()
1859
+ }, null)
1860
+ ] }), props.label && createVNode(resolveComponent("f-div"), { "flex": 1 }, { default: () => [createVNode(resolveComponent("f-span"), {
1861
+ "text": props.label,
1862
+ "fontSize": CONTROL_LABEL_FONT_SIZE[props.size],
1863
+ "fontFamily": MD3.fontFamily,
1864
+ "color": props.disabled ? MD3.onSurfaceVariant : MD3.onSurface,
1865
+ "verticalAlign": "middle"
1866
+ }, null)] })] });
1867
+ }
1868
+ });
1869
+ //#endregion
1870
+ //#region packages/components/src/components/dialog/dialog.tsx
1871
+ function _isSlot$1(s) {
1872
+ return typeof s === "function" || Object.prototype.toString.call(s) === "[object Object]" && !isVNode(s);
1873
+ }
1874
+ var DIALOG_VIEWPORT_MARGIN = 16;
1875
+ var DIALOG_TRANSITION_DURATION = 180;
1876
+ var FDialog = /* @__PURE__ */ defineComponent({
1877
+ name: "FDialog",
1878
+ props: {
1879
+ modelValue: {
1880
+ type: Boolean,
1881
+ default: false
1882
+ },
1883
+ title: {
1884
+ type: String,
1885
+ default: ""
1886
+ },
1887
+ width: {
1888
+ type: Number,
1889
+ default: 480
1890
+ },
1891
+ closeOnOverlayClick: {
1892
+ type: Boolean,
1893
+ default: true
1894
+ },
1895
+ closeOnEscape: {
1896
+ type: Boolean,
1897
+ default: true
1898
+ },
1899
+ showClose: {
1900
+ type: Boolean,
1901
+ default: true
1902
+ }
1903
+ },
1904
+ emits: [
1905
+ "update:modelValue",
1906
+ "open",
1907
+ "opened",
1908
+ "close",
1909
+ "closed"
1910
+ ],
1911
+ setup(props, { emit, slots }) {
1912
+ const { overlay, fulateRoot } = useOverlay();
1913
+ const viewport = shallowRef(fulateRoot?.viewport.getWorldRect() ?? {
1914
+ left: 0,
1915
+ top: 0,
1916
+ width: 0,
1917
+ height: 0
1918
+ });
1919
+ let mounted = false;
1920
+ let removeViewportListener = null;
1921
+ const panelWidth = computed(() => Math.min(props.width, Math.max(0, viewport.value.width - DIALOG_VIEWPORT_MARGIN * 2)));
1922
+ const panelMaxHeight = computed(() => Math.max(0, viewport.value.height - DIALOG_VIEWPORT_MARGIN * 2));
1923
+ function syncViewport() {
1924
+ viewport.value = fulateRoot.viewport.getWorldRect();
1925
+ }
1926
+ function requestClose() {
1927
+ if (props.modelValue) emit("update:modelValue", false);
1928
+ }
1929
+ function handleEscape(event) {
1930
+ if (!props.modelValue || !props.closeOnEscape || event.key !== "Escape") return;
1931
+ event.preventDefault();
1932
+ event.stopPropagation();
1933
+ requestClose();
1934
+ }
1935
+ function handlePanelClick(event) {
1936
+ event.stopPropagation();
1937
+ }
1938
+ watch(() => props.modelValue, (visible) => {
1939
+ if (!mounted) return;
1940
+ if (visible) {
1941
+ syncViewport();
1942
+ emit("open");
1943
+ } else emit("close");
1944
+ });
1945
+ onMounted(() => {
1946
+ if (!fulateRoot || !overlay) throw new Error("[fulate-components] FDialog requires createApp or registerVueOverlay");
1947
+ mounted = true;
1948
+ syncViewport();
1949
+ removeViewportListener = fulateRoot.addEventListener("viewportchange", syncViewport);
1950
+ window.addEventListener("resize", syncViewport);
1951
+ fulateRoot.container.ownerDocument.addEventListener("keydown", handleEscape, true);
1952
+ if (props.modelValue) emit("open");
1953
+ });
1954
+ onUnmounted(() => {
1955
+ mounted = false;
1956
+ removeViewportListener?.();
1957
+ removeViewportListener = null;
1958
+ window.removeEventListener("resize", syncViewport);
1959
+ fulateRoot.container.ownerDocument.removeEventListener("keydown", handleEscape, true);
1960
+ });
1961
+ return () => {
1962
+ let _slot, _slot2;
1963
+ if (!overlay) throw new Error("[fulate-components] FDialog requires createApp or registerVueOverlay");
1964
+ const panel = props.modelValue ? createVNode(resolveComponent("f-div"), {
1965
+ "width": panelWidth.value,
1966
+ "maxHeight": panelMaxHeight.value,
1967
+ "display": Display.Flex,
1968
+ "flexDirection": FlexDirection.Column,
1969
+ "flexShrink": 0,
1970
+ "overflow": Overflow.Hidden,
1971
+ "backgroundColor": MD3.surface,
1972
+ "radius": MD3.radiusLg,
1973
+ "shadow": {
1974
+ color: "rgba(0,0,0,0.24)",
1975
+ blur: 24,
1976
+ offsetY: 8
1977
+ },
1978
+ "onClick": handlePanelClick
1979
+ }, { default: () => [
1980
+ createVNode(resolveComponent("f-div"), {
1981
+ "minHeight": 56,
1982
+ "display": Display.Flex,
1983
+ "flexDirection": FlexDirection.Row,
1984
+ "alignItems": Align.Center,
1985
+ "paddingLeft": 24,
1986
+ "paddingRight": 12,
1987
+ "flexShrink": 0
1988
+ }, { default: () => [slots.header ? createVNode(resolveComponent("f-div"), { "flex": 1 }, _isSlot$1(_slot = slots.header()) ? _slot : { default: () => [_slot] }) : createVNode(resolveComponent("f-span"), {
1989
+ "text": props.title,
1990
+ "flex": 1,
1991
+ "fontSize": 20,
1992
+ "fontWeight": 600,
1993
+ "fontFamily": MD3.fontFamily,
1994
+ "color": MD3.onSurface,
1995
+ "verticalAlign": "middle"
1996
+ }, null), props.showClose ? createVNode(resolveComponent("f-div"), {
1997
+ "width": 32,
1998
+ "height": 32,
1999
+ "display": Display.Flex,
2000
+ "justifyContent": Justify.Center,
2001
+ "alignItems": Align.Center,
2002
+ "flexShrink": 0,
2003
+ "radius": 16,
2004
+ "cursor": "pointer",
2005
+ "onClick": (event) => {
2006
+ event.stopPropagation();
2007
+ requestClose();
2008
+ }
2009
+ }, { default: () => [createVNode(resolveComponent("f-span"), {
2010
+ "text": "×",
2011
+ "fontSize": 24,
2012
+ "fontFamily": MD3.fontFamily,
2013
+ "color": MD3.onSurfaceVariant
2014
+ }, null)] }) : null] }),
2015
+ createVNode(resolveComponent("f-div"), {
2016
+ "minHeight": 48,
2017
+ "paddingLeft": 24,
2018
+ "paddingRight": 24,
2019
+ "paddingBottom": 24,
2020
+ "flexShrink": 1
2021
+ }, { default: () => [slots.default?.()] }),
2022
+ slots.footer ? createVNode(resolveComponent("f-div"), {
2023
+ "minHeight": 64,
2024
+ "display": Display.Flex,
2025
+ "flexDirection": FlexDirection.Row,
2026
+ "justifyContent": Justify.FlexEnd,
2027
+ "alignItems": Align.Center,
2028
+ "gap": 8,
2029
+ "paddingLeft": 24,
2030
+ "paddingRight": 24,
2031
+ "paddingTop": 12,
2032
+ "paddingBottom": 12,
2033
+ "flexShrink": 0
2034
+ }, _isSlot$1(_slot2 = slots.footer()) ? _slot2 : { default: () => [_slot2] }) : null
2035
+ ] }) : null;
2036
+ return createVNode(Teleport, { "to": overlay }, { default: () => [createVNode(FTransition, {
2037
+ "appear": true,
2038
+ "enterFrom": { opacity: 0 },
2039
+ "enterTo": { opacity: 1 },
2040
+ "leaveTo": { opacity: 0 },
2041
+ "duration": DIALOG_TRANSITION_DURATION,
2042
+ "onAfterEnter": () => emit("opened"),
2043
+ "onAfterLeave": () => emit("closed")
2044
+ }, { default: () => [panel ? createVNode(resolveComponent("f-div"), {
2045
+ "left": viewport.value.left,
2046
+ "top": viewport.value.top,
2047
+ "width": viewport.value.width,
2048
+ "height": viewport.value.height,
2049
+ "display": Display.Flex,
2050
+ "justifyContent": Justify.Center,
2051
+ "alignItems": Align.Center,
2052
+ "backgroundColor": "rgba(0,0,0,0.32)",
2053
+ "onClick": () => {
2054
+ if (props.closeOnOverlayClick) requestClose();
2055
+ }
2056
+ }, _isSlot$1(panel) ? panel : { default: () => [panel] }) : null] })] });
2057
+ };
2058
+ }
2059
+ });
2060
+ //#endregion
2061
+ //#region packages/components/src/components/message/message.tsx
2062
+ function _isSlot(s) {
2063
+ return typeof s === "function" || Object.prototype.toString.call(s) === "[object Object]" && !isVNode(s);
2064
+ }
2065
+ var MESSAGE_KEY = Symbol("fulate-message");
2066
+ var MESSAGE_TRANSITION_DURATION = 180;
2067
+ var MESSAGE_VISUAL = {
2068
+ success: {
2069
+ color: "#2E7D32",
2070
+ icon: "✓"
2071
+ },
2072
+ info: {
2073
+ color: MD3.primary,
2074
+ icon: "i"
2075
+ },
2076
+ warning: {
2077
+ color: "#ED6C02",
2078
+ icon: "!"
2079
+ },
2080
+ error: {
2081
+ color: MD3.error,
2082
+ icon: "×"
2083
+ }
2084
+ };
2085
+ var FMessage = /* @__PURE__ */ defineComponent({
2086
+ name: "FMessage",
2087
+ props: {
2088
+ duration: {
2089
+ type: Number,
2090
+ default: 3e3
2091
+ },
2092
+ offset: {
2093
+ type: Number,
2094
+ default: 16
2095
+ },
2096
+ gap: {
2097
+ type: Number,
2098
+ default: 12
2099
+ },
2100
+ width: {
2101
+ type: Number,
2102
+ default: 360
2103
+ }
2104
+ },
2105
+ setup(props, { slots }) {
2106
+ const { overlay, fulateRoot } = useOverlay();
2107
+ const viewport = shallowRef(fulateRoot?.viewport.getWorldRect() ?? {
2108
+ left: 0,
2109
+ top: 0,
2110
+ width: 0,
2111
+ height: 0
2112
+ });
2113
+ const messages = shallowRef([]);
2114
+ let nextId = 1;
2115
+ let removeViewportListener = null;
2116
+ function syncViewport() {
2117
+ viewport.value = fulateRoot.viewport.getWorldRect();
2118
+ }
2119
+ function removeMessage(instance) {
2120
+ if (instance.timer) clearTimeout(instance.timer);
2121
+ instance.timer = null;
2122
+ messages.value = messages.value.filter((item) => item !== instance);
2123
+ instance.onClose?.();
2124
+ }
2125
+ function closeMessage(instance) {
2126
+ if (!instance.visible) return;
2127
+ if (instance.timer) clearTimeout(instance.timer);
2128
+ instance.timer = null;
2129
+ instance.visible = false;
2130
+ if (!instance.transitionStarted) {
2131
+ removeMessage(instance);
2132
+ return;
2133
+ }
2134
+ messages.value = [...messages.value];
2135
+ }
2136
+ function normalize(input) {
2137
+ return isString(input) ? { message: input } : input;
2138
+ }
2139
+ function show(input, forcedType) {
2140
+ const options = normalize(input);
2141
+ const instance = {
2142
+ id: nextId++,
2143
+ message: options.message,
2144
+ type: forcedType ?? options.type ?? "info",
2145
+ visible: true,
2146
+ transitionStarted: false,
2147
+ timer: null,
2148
+ onClose: options.onClose
2149
+ };
2150
+ messages.value = [...messages.value, instance];
2151
+ const duration = options.duration ?? props.duration;
2152
+ if (duration > 0) instance.timer = setTimeout(() => closeMessage(instance), duration);
2153
+ return { close: () => closeMessage(instance) };
2154
+ }
2155
+ const message = (input) => show(input);
2156
+ message.success = (input) => show(input, "success");
2157
+ message.info = (input) => show(input, "info");
2158
+ message.warning = (input) => show(input, "warning");
2159
+ message.error = (input) => show(input, "error");
2160
+ message.closeAll = () => {
2161
+ for (const instance of messages.value) closeMessage(instance);
2162
+ };
2163
+ provide(MESSAGE_KEY, message);
2164
+ onMounted(() => {
2165
+ if (!fulateRoot || !overlay) throw new Error("[fulate-components] FMessage requires createApp or registerVueOverlay");
2166
+ syncViewport();
2167
+ removeViewportListener = fulateRoot.addEventListener("viewportchange", syncViewport);
2168
+ window.addEventListener("resize", syncViewport);
2169
+ });
2170
+ onBeforeUnmount(() => {
2171
+ removeViewportListener?.();
2172
+ removeViewportListener = null;
2173
+ window.removeEventListener("resize", syncViewport);
2174
+ for (const instance of messages.value) if (instance.timer) clearTimeout(instance.timer);
2175
+ messages.value = [];
2176
+ });
2177
+ return () => {
2178
+ let _slot;
2179
+ if (!overlay) throw new Error("[fulate-components] FMessage requires createApp or registerVueOverlay");
2180
+ return [slots.default?.(), createVNode(Teleport, { "to": overlay }, { default: () => [createVNode(resolveComponent("f-div"), {
2181
+ "left": viewport.value.left,
2182
+ "top": viewport.value.top + props.offset,
2183
+ "width": viewport.value.width,
2184
+ "display": Display.Flex,
2185
+ "flexDirection": FlexDirection.Column,
2186
+ "alignItems": Align.Center,
2187
+ "gap": props.gap,
2188
+ "silent": true,
2189
+ "pickable": false
2190
+ }, _isSlot(_slot = messages.value.map((instance) => {
2191
+ const visual = MESSAGE_VISUAL[instance.type];
2192
+ return createVNode(FTransition, {
2193
+ "key": instance.id,
2194
+ "appear": true,
2195
+ "enterFrom": { opacity: 0 },
2196
+ "enterTo": { opacity: 1 },
2197
+ "leaveTo": { opacity: 0 },
2198
+ "duration": MESSAGE_TRANSITION_DURATION,
2199
+ "onBeforeEnter": () => {
2200
+ instance.transitionStarted = true;
2201
+ },
2202
+ "onAfterLeave": () => removeMessage(instance)
2203
+ }, { default: () => [instance.visible ? createVNode(resolveComponent("f-div"), {
2204
+ "key": instance.id,
2205
+ "width": props.width,
2206
+ "minHeight": 48,
2207
+ "display": Display.Flex,
2208
+ "flexDirection": FlexDirection.Row,
2209
+ "alignItems": Align.Center,
2210
+ "gap": 12,
2211
+ "paddingTop": 10,
2212
+ "paddingBottom": 10,
2213
+ "paddingLeft": 16,
2214
+ "paddingRight": 16,
2215
+ "flexShrink": 0,
2216
+ "backgroundColor": MD3.surface,
2217
+ "borderColor": colorWithAlpha(visual.color, .35),
2218
+ "borderWidth": 1,
2219
+ "radius": MD3.radiusLg,
2220
+ "shadow": {
2221
+ color: "rgba(0,0,0,0.18)",
2222
+ blur: 16,
2223
+ offsetY: 4
2224
+ },
2225
+ "silent": true,
2226
+ "pickable": false
2227
+ }, { default: () => [createVNode(resolveComponent("f-div"), {
2228
+ "width": 24,
2229
+ "height": 24,
2230
+ "flexShrink": 0,
2231
+ "radius": 12,
2232
+ "backgroundColor": colorWithAlpha(visual.color, .14),
2233
+ "silent": true,
2234
+ "pickable": false
2235
+ }, { default: () => [createVNode(resolveComponent("f-text"), {
2236
+ "width": 24,
2237
+ "height": 24,
2238
+ "text": visual.icon,
2239
+ "fontSize": 15,
2240
+ "fontWeight": 700,
2241
+ "fontFamily": MD3.fontFamily,
2242
+ "color": visual.color,
2243
+ "textAlign": "center",
2244
+ "verticalAlign": "middle"
2245
+ }, null)] }), createVNode(resolveComponent("f-span"), {
2246
+ "text": instance.message,
2247
+ "flex": 1,
2248
+ "fontSize": 14,
2249
+ "fontFamily": MD3.fontFamily,
2250
+ "color": MD3.onSurface,
2251
+ "verticalAlign": "middle"
2252
+ }, null)] }) : null] });
2253
+ })) ? _slot : { default: () => [_slot] })] })];
2254
+ };
2255
+ }
2256
+ });
2257
+ function useMessage() {
2258
+ const message = inject(MESSAGE_KEY, null);
2259
+ if (!message) throw new Error("[fulate-components] useMessage requires FMessage");
2260
+ return message;
2261
+ }
2262
+ //#endregion
2263
+ export { FButton, FCheckbox, FCheckboxGroup, FDialog, FInput, FMessage, FPopover, FRadio, FRadioGroup, FSelect, FSwitch, FTooltip, MD3, isDescendantOf, useMessage, useOutsideClick, useOverlay, useOverlayPosition, useVueShapeSize };