@mks2508/mks-ui 0.5.4 → 0.5.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.
- package/dist/react-ui/primitives/waapi/Gooey/Gooey.types.d.ts +21 -4
- package/dist/react-ui/primitives/waapi/Gooey/Gooey.types.d.ts.map +1 -1
- package/dist/react-ui/primitives/waapi/Gooey/GooeyCanvas.d.ts +2 -2
- package/dist/react-ui/primitives/waapi/Gooey/GooeyCanvas.d.ts.map +1 -1
- package/dist/react-ui/primitives/waapi/Gooey/GooeyCanvas.js +163 -32
- package/dist/react-ui/primitives/waapi/Gooey/gooey-utils.d.ts +7 -0
- package/dist/react-ui/primitives/waapi/Gooey/gooey-utils.d.ts.map +1 -1
- package/dist/react-ui/primitives/waapi/Gooey/gooey-utils.js +6 -1
- package/dist/react-ui/ui/DynamicToggle/{DynamicToggle-Cm6-VceQ.css → DynamicToggle-DOR3Ld-k.css} +104 -32
- package/dist/react-ui/ui/DynamicToggle/DynamicToggle.css +105 -32
- package/dist/react-ui/ui/DynamicToggle/DynamicToggle.styles.js +2 -2
- package/dist/react-ui/ui/DynamicToggle/DynamicToggle.types.d.ts +6 -0
- package/dist/react-ui/ui/DynamicToggle/DynamicToggle.types.d.ts.map +1 -1
- package/dist/react-ui/ui/DynamicToggle/index.d.ts.map +1 -1
- package/dist/react-ui/ui/DynamicToggle/index.js +23 -5
- package/package.json +1 -1
- package/src/react-ui/primitives/waapi/Gooey/Gooey.types.ts +21 -3
- package/src/react-ui/primitives/waapi/Gooey/GooeyCanvas.tsx +177 -40
- package/src/react-ui/primitives/waapi/Gooey/gooey-utils.ts +9 -0
- package/src/react-ui/ui/DynamicToggle/DynamicToggle.css +105 -32
- package/src/react-ui/ui/DynamicToggle/DynamicToggle.styles.ts +2 -2
- package/src/react-ui/ui/DynamicToggle/DynamicToggle.types.ts +6 -0
- package/src/react-ui/ui/DynamicToggle/index.tsx +30 -8
- package/src/react-ui/ui/DynamicToggle/prototype-v7-ios.html +413 -0
- package/src/react-ui/ui/DynamicToggle/prototype-v8-gooey-safari.html +560 -0
- package/src/react-ui/ui/DynamicToggle/prototype-v8b-react-structure.html +227 -0
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
--dt-dur: 0.22s;
|
|
13
13
|
--dt-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
|
|
14
14
|
--dt-fade: 0.45;
|
|
15
|
+
--dt-indicator-dur: 0.3s;
|
|
16
|
+
--dt-indicator-ease: cubic-bezier(0.4, 0, 0.2, 1);
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
/* ── Track: explicit row prevents h-full items from overflowing container ── */
|
|
@@ -24,18 +26,9 @@
|
|
|
24
26
|
grid-column: span 2;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
/* ── Main indicator slide ── */
|
|
28
|
-
[data-slot="dt-root"] [data-slot="dt-indicator"] {
|
|
29
|
-
transition: translate var(--dt-dur) var(--dt-ease);
|
|
30
|
-
translate: 100% 0;
|
|
31
|
-
}
|
|
32
|
-
[data-slot="dt-root"] [data-slot="dt-track"]:has(> input:checked) [data-slot="dt-indicator"] {
|
|
33
|
-
translate: 0 0;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
29
|
/* ── Primary option text ── */
|
|
37
30
|
[data-slot="dt-root"] [data-slot="dt-track"]:has(> input:checked) > label {
|
|
38
|
-
color: var(--
|
|
31
|
+
color: var(--accent-foreground);
|
|
39
32
|
z-index: 2;
|
|
40
33
|
}
|
|
41
34
|
[data-slot="dt-root"] [data-slot="dt-track"]:not(:has(> input:checked)) > label {
|
|
@@ -49,31 +42,109 @@
|
|
|
49
42
|
overflow: hidden;
|
|
50
43
|
}
|
|
51
44
|
|
|
52
|
-
/*
|
|
53
|
-
|
|
54
|
-
|
|
45
|
+
/* ══════════════════════════════════════════════════════════
|
|
46
|
+
* INDICATOR POSITIONING
|
|
47
|
+
*
|
|
48
|
+
* Modern: CSS Anchor Positioning — indicator follows active option
|
|
49
|
+
* Fallback: translate-based positioning for older browsers
|
|
50
|
+
* ══════════════════════════════════════════════════════════ */
|
|
51
|
+
|
|
52
|
+
/* ── Anchor-based indicator (requires full anchor API) ── */
|
|
53
|
+
@supports (anchor-scope: all) {
|
|
54
|
+
/* Scope anchors per toggle instance */
|
|
55
|
+
[data-slot="dt-root"]:not([data-indicator="translate"]) {
|
|
56
|
+
anchor-scope: --dt-active;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Active option becomes the anchor via native :checked */
|
|
60
|
+
[data-slot="dt-root"]:not([data-indicator="translate"]) [data-slot="dt-track"] > label:has(+ input:checked) {
|
|
61
|
+
anchor-name: --dt-active;
|
|
62
|
+
}
|
|
63
|
+
[data-slot="dt-root"]:not([data-indicator="translate"]) [data-slot="dt-group"] > label:has(+ input:checked) {
|
|
64
|
+
anchor-name: --dt-active;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* Single unified indicator: morphs from full-width to half-width */
|
|
68
|
+
[data-slot="dt-root"]:not([data-indicator="translate"]) [data-slot="dt-indicator"] {
|
|
69
|
+
position-anchor: --dt-active;
|
|
70
|
+
top: anchor(top);
|
|
71
|
+
right: anchor(right);
|
|
72
|
+
bottom: anchor(bottom);
|
|
73
|
+
left: anchor(left);
|
|
74
|
+
translate: none;
|
|
75
|
+
width: auto;
|
|
76
|
+
transition:
|
|
77
|
+
top var(--dt-indicator-dur) var(--dt-indicator-ease),
|
|
78
|
+
right var(--dt-indicator-dur) var(--dt-indicator-ease),
|
|
79
|
+
bottom var(--dt-indicator-dur) var(--dt-indicator-ease),
|
|
80
|
+
left var(--dt-indicator-dur) var(--dt-indicator-ease);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Hide the group indicator — unified indicator handles everything */
|
|
84
|
+
[data-slot="dt-root"]:not([data-indicator="translate"]) [data-slot="dt-group-indicator"] {
|
|
85
|
+
display: none;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ── Inset-based fallback (older browsers) — same morph as anchor but hardcoded ── */
|
|
90
|
+
@supports not (anchor-scope: all) {
|
|
91
|
+
/* Unified indicator: left/right transition morphs width + position */
|
|
92
|
+
[data-slot="dt-root"] [data-slot="dt-indicator"] {
|
|
93
|
+
left: 50%;
|
|
94
|
+
right: 0;
|
|
95
|
+
width: auto;
|
|
96
|
+
translate: none;
|
|
97
|
+
transition:
|
|
98
|
+
left var(--dt-indicator-dur) var(--dt-indicator-ease),
|
|
99
|
+
right var(--dt-indicator-dur) var(--dt-indicator-ease);
|
|
100
|
+
}
|
|
101
|
+
/* Top-level checked: indicator covers left half */
|
|
102
|
+
[data-slot="dt-root"] [data-slot="dt-track"]:has(> input:checked) [data-slot="dt-indicator"] {
|
|
103
|
+
left: 0;
|
|
104
|
+
right: 50%;
|
|
105
|
+
}
|
|
106
|
+
/* Group option 1 checked: indicator at 3rd quarter */
|
|
107
|
+
[data-slot="dt-root"] [data-slot="dt-group"]:has(input:nth-of-type(1):checked) ~ [data-slot="dt-indicator"],
|
|
108
|
+
[data-slot="dt-root"] [data-slot="dt-track"]:has([data-slot="dt-group"] input:nth-of-type(1):checked) [data-slot="dt-indicator"] {
|
|
109
|
+
left: 50%;
|
|
110
|
+
right: 25%;
|
|
111
|
+
}
|
|
112
|
+
/* Group option 2 checked: indicator at 4th quarter */
|
|
113
|
+
[data-slot="dt-root"] [data-slot="dt-group"]:has(input:nth-of-type(2):checked) ~ [data-slot="dt-indicator"],
|
|
114
|
+
[data-slot="dt-root"] [data-slot="dt-track"]:has([data-slot="dt-group"] input:nth-of-type(2):checked) [data-slot="dt-indicator"] {
|
|
115
|
+
left: 75%;
|
|
116
|
+
right: 0;
|
|
117
|
+
}
|
|
118
|
+
/* Hide group indicator — unified indicator handles everything */
|
|
119
|
+
[data-slot="dt-root"] [data-slot="dt-group-indicator"] {
|
|
120
|
+
display: none;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* ── Force inset mode via data-indicator="translate" (works regardless of @supports) ── */
|
|
125
|
+
[data-slot="dt-root"][data-indicator="translate"] [data-slot="dt-indicator"] {
|
|
126
|
+
left: 50%;
|
|
127
|
+
right: 0;
|
|
128
|
+
width: auto;
|
|
129
|
+
translate: none;
|
|
55
130
|
transition:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
background var(--dt-dur) var(--dt-ease);
|
|
59
|
-
clip-path: inset(
|
|
60
|
-
73cqh calc(50% + 1px) calc(27cqh - 2px) calc(50% - 3px)
|
|
61
|
-
round var(--dt-radius, 9999px)
|
|
62
|
-
);
|
|
63
|
-
translate: -50% 0;
|
|
64
|
-
}
|
|
65
|
-
[data-slot="dt-root"] [data-slot="dt-track"]:has(> input:checked) [data-slot="dt-group-indicator"] {
|
|
66
|
-
background: transparent;
|
|
131
|
+
left var(--dt-indicator-dur) var(--dt-indicator-ease),
|
|
132
|
+
right var(--dt-indicator-dur) var(--dt-indicator-ease);
|
|
67
133
|
}
|
|
68
|
-
[data-slot="dt-root"] [data-slot="dt-
|
|
69
|
-
|
|
70
|
-
|
|
134
|
+
[data-slot="dt-root"][data-indicator="translate"] [data-slot="dt-track"]:has(> input:checked) [data-slot="dt-indicator"] {
|
|
135
|
+
left: 0;
|
|
136
|
+
right: 50%;
|
|
71
137
|
}
|
|
72
|
-
[data-slot="dt-root"] [data-slot="dt-
|
|
73
|
-
|
|
138
|
+
[data-slot="dt-root"][data-indicator="translate"] [data-slot="dt-track"]:has([data-slot="dt-group"] input:nth-of-type(1):checked) [data-slot="dt-indicator"] {
|
|
139
|
+
left: 50%;
|
|
140
|
+
right: 25%;
|
|
74
141
|
}
|
|
75
|
-
[data-slot="dt-root"] [data-slot="dt-
|
|
76
|
-
|
|
142
|
+
[data-slot="dt-root"][data-indicator="translate"] [data-slot="dt-track"]:has([data-slot="dt-group"] input:nth-of-type(2):checked) [data-slot="dt-indicator"] {
|
|
143
|
+
left: 75%;
|
|
144
|
+
right: 0;
|
|
145
|
+
}
|
|
146
|
+
[data-slot="dt-root"][data-indicator="translate"] [data-slot="dt-group-indicator"] {
|
|
147
|
+
display: none;
|
|
77
148
|
}
|
|
78
149
|
|
|
79
150
|
/* ══════════════════════════════════════════════════════════
|
|
@@ -233,6 +304,8 @@
|
|
|
233
304
|
background: var(--card);
|
|
234
305
|
border: 1px solid var(--border);
|
|
235
306
|
z-index: 3;
|
|
307
|
+
transform: translateZ(0);
|
|
308
|
+
-webkit-transform: translateZ(0);
|
|
236
309
|
}
|
|
237
310
|
[data-slot="dt-group-label"] > span {
|
|
238
311
|
overflow: hidden;
|
|
@@ -300,4 +373,4 @@
|
|
|
300
373
|
[data-slot="dt-root"][data-morph="path"] [data-slot="dt-track"] {
|
|
301
374
|
position: relative;
|
|
302
375
|
z-index: 1;
|
|
303
|
-
}
|
|
376
|
+
}
|
|
@@ -30,9 +30,9 @@ export const dynamicToggleStyles: StyleSlots<DynamicToggleSlot> = {
|
|
|
30
30
|
root: 'relative border p-[2px] select-none',
|
|
31
31
|
track: 'relative grid grid-cols-[repeat(4,1fr)] place-items-center w-full h-full',
|
|
32
32
|
option: 'inline-grid place-items-center cursor-pointer font-medium z-[2] h-full w-full whitespace-nowrap',
|
|
33
|
-
indicator: 'absolute w-1/2 left-0 top-0 bottom-0 bg-
|
|
33
|
+
indicator: 'absolute w-1/2 left-0 top-0 bottom-0 bg-accent rounded-[var(--dt-radius,9999px)] pointer-events-none z-0',
|
|
34
34
|
group: 'col-span-2 relative w-full h-full grid grid-cols-2',
|
|
35
|
-
groupIndicator: 'absolute left-1/2 top-0 bottom-0 w-1/2 bg-
|
|
35
|
+
groupIndicator: 'absolute left-1/2 top-0 bottom-0 w-1/2 bg-accent rounded-[var(--dt-radius,9999px)] pointer-events-none z-0',
|
|
36
36
|
groupLabel: [
|
|
37
37
|
'absolute',
|
|
38
38
|
'flex items-center justify-center',
|
|
@@ -42,6 +42,12 @@ export interface IDynamicToggleConfig extends IBaseConfig {
|
|
|
42
42
|
labelAnimation?: 'morph' | 'float' | 'none';
|
|
43
43
|
/** Gooey morph mode for the pill↔groupLabel junction (default: 'none') */
|
|
44
44
|
morphMode?: DynamicToggleMorphMode;
|
|
45
|
+
/** Indicator slide duration in seconds (default: 0.3) */
|
|
46
|
+
indicatorDuration?: number;
|
|
47
|
+
/** Indicator slide easing (default: material standard cubic-bezier) */
|
|
48
|
+
indicatorEasing?: string;
|
|
49
|
+
/** Force translate-based indicator instead of CSS Anchor (debug/compat) */
|
|
50
|
+
forceTranslateIndicator?: boolean;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
// ---------------------------------------------------------------------------
|
|
@@ -62,6 +62,18 @@ const SIZE_HEIGHT_PX: Record<string, number> = {
|
|
|
62
62
|
lg: 44,
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
+
const SIZE_WIDTH_PX: Record<string, number> = {
|
|
66
|
+
sm: 210,
|
|
67
|
+
default: 260,
|
|
68
|
+
lg: 320,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const SHAPE_RADIUS: Record<string, number> = {
|
|
72
|
+
pill: 9999,
|
|
73
|
+
rounded: 12,
|
|
74
|
+
square: 6,
|
|
75
|
+
};
|
|
76
|
+
|
|
65
77
|
// ---------------------------------------------------------------------------
|
|
66
78
|
// DynamicToggle (Root)
|
|
67
79
|
// ---------------------------------------------------------------------------
|
|
@@ -115,9 +127,12 @@ function DynamicToggle({
|
|
|
115
127
|
const showGroupLabel = labelAnimation !== 'none' && groupPosition !== 'hidden' && groupLabel;
|
|
116
128
|
const heightPx = SIZE_HEIGHT_PX[size ?? 'default'] ?? 32;
|
|
117
129
|
|
|
118
|
-
const style =
|
|
119
|
-
|
|
120
|
-
:
|
|
130
|
+
const style = {
|
|
131
|
+
...(config?.duration && { '--dt-dur': `${config.duration}s` }),
|
|
132
|
+
...(config?.indicatorDuration && { '--dt-indicator-dur': `${config.indicatorDuration}s` }),
|
|
133
|
+
...(config?.indicatorEasing && { '--dt-indicator-ease': config.indicatorEasing }),
|
|
134
|
+
} as React.CSSProperties;
|
|
135
|
+
const hasStyle = Object.keys(style).length > 0;
|
|
121
136
|
|
|
122
137
|
// Group label element (shared between modes)
|
|
123
138
|
const groupLabelElement = showGroupLabel ? (
|
|
@@ -135,19 +150,26 @@ function DynamicToggle({
|
|
|
135
150
|
<div
|
|
136
151
|
data-slot="dt-root"
|
|
137
152
|
data-morph={effectiveMorphMode !== 'none' ? effectiveMorphMode : undefined}
|
|
153
|
+
data-indicator={config?.forceTranslateIndicator ? 'translate' : undefined}
|
|
138
154
|
data-group-active={groupActive || undefined}
|
|
139
155
|
data-disabled={disabled || undefined}
|
|
140
156
|
role="radiogroup"
|
|
141
157
|
aria-label={ariaLabel}
|
|
142
|
-
style={style}
|
|
158
|
+
style={hasStyle ? style : undefined}
|
|
143
159
|
className={cn(dynamicToggleVariants({ variant, size, shape }), slots?.root, className)}
|
|
144
160
|
>
|
|
145
|
-
{/* Filter morph: GooeyCanvas
|
|
161
|
+
{/* Filter morph: GooeyCanvas with SVG rects (Safari-safe) */}
|
|
146
162
|
{effectiveMorphMode === 'filter' && (
|
|
147
|
-
|
|
148
|
-
<
|
|
163
|
+
<>
|
|
164
|
+
<GooeyCanvas
|
|
165
|
+
height={heightPx}
|
|
166
|
+
width={SIZE_WIDTH_PX[size ?? 'default'] ?? 260}
|
|
167
|
+
radius={SHAPE_RADIUS[shape ?? 'pill'] ?? 9999}
|
|
168
|
+
expanded={groupActive}
|
|
169
|
+
/>
|
|
170
|
+
{/* Group label text — outside the filtered canvas */}
|
|
149
171
|
{groupLabelElement}
|
|
150
|
-
|
|
172
|
+
</>
|
|
151
173
|
)}
|
|
152
174
|
|
|
153
175
|
{/* Path morph: group label rendered directly (no gooey filter) */}
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>DynamicToggle — Sileo-style gooey (Safari-safe)</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--fg: #e2e8f0;
|
|
10
|
+
--bg: #0f172a;
|
|
11
|
+
--card: #1e293b;
|
|
12
|
+
--muted: #64748b;
|
|
13
|
+
--border: #334155;
|
|
14
|
+
--duration: 0.22;
|
|
15
|
+
--ease: cubic-bezier(0.22, 0.61, 0.36, 1);
|
|
16
|
+
--drop-off: 0.4;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
* { box-sizing: border-box; margin: 0; }
|
|
20
|
+
body {
|
|
21
|
+
min-height: 100vh;
|
|
22
|
+
display: flex; flex-direction: column; align-items: center;
|
|
23
|
+
padding: 4rem 1rem; gap: 2rem;
|
|
24
|
+
background: var(--bg);
|
|
25
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
26
|
+
color: var(--fg);
|
|
27
|
+
}
|
|
28
|
+
h3 { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: 2px; }
|
|
29
|
+
.row { display: flex; gap: 2rem; align-items: end; flex-wrap: wrap; justify-content: center; }
|
|
30
|
+
.sr-only {
|
|
31
|
+
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
|
|
32
|
+
overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border-width: 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* ================================================================
|
|
36
|
+
* CONTROL BASE
|
|
37
|
+
* ================================================================ */
|
|
38
|
+
.control {
|
|
39
|
+
--w: 260px;
|
|
40
|
+
--h: 38px;
|
|
41
|
+
--radius: 9999px;
|
|
42
|
+
--font: 0.75rem;
|
|
43
|
+
--bubble-h-pct: 40;
|
|
44
|
+
--bubble-inset-pct: 20;
|
|
45
|
+
--_r: min(var(--radius), calc(var(--h) / 2));
|
|
46
|
+
|
|
47
|
+
position: relative;
|
|
48
|
+
width: var(--w);
|
|
49
|
+
height: var(--h);
|
|
50
|
+
padding: 2px;
|
|
51
|
+
margin-top: calc(var(--h) * 0.5);
|
|
52
|
+
/* Sileo: GPU layer + containment on root */
|
|
53
|
+
transform: translateZ(0);
|
|
54
|
+
contain: layout style;
|
|
55
|
+
overflow: visible;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.control__track {
|
|
59
|
+
display: grid; place-items: center;
|
|
60
|
+
grid-template-columns: repeat(4, 1fr);
|
|
61
|
+
width: 100%; height: 100%;
|
|
62
|
+
position: relative;
|
|
63
|
+
z-index: 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.indicator {
|
|
67
|
+
position: absolute;
|
|
68
|
+
width: 50%; left: 0; top: 0; bottom: 0;
|
|
69
|
+
background: var(--fg);
|
|
70
|
+
border-radius: var(--radius);
|
|
71
|
+
transition: translate calc(var(--duration) * 1s) var(--ease);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
label {
|
|
75
|
+
display: inline-grid; place-items: center;
|
|
76
|
+
height: 100%; width: 100%;
|
|
77
|
+
cursor: pointer; font-size: var(--font);
|
|
78
|
+
color: var(--fg); z-index: 2; font-weight: 500;
|
|
79
|
+
}
|
|
80
|
+
.control__track > label { color: var(--card); }
|
|
81
|
+
|
|
82
|
+
.group {
|
|
83
|
+
width: 100%; height: 100%;
|
|
84
|
+
display: grid; position: relative;
|
|
85
|
+
grid-template-columns: 1fr 1fr;
|
|
86
|
+
container-type: size;
|
|
87
|
+
overflow: hidden;
|
|
88
|
+
}
|
|
89
|
+
.group, .control__track > label:nth-of-type(1) { grid-column: span 2; }
|
|
90
|
+
|
|
91
|
+
.group .indicator {
|
|
92
|
+
background: var(--fg); left: 50%; top: 0;
|
|
93
|
+
translate: -50% 0;
|
|
94
|
+
pointer-events: none;
|
|
95
|
+
transition: translate calc(var(--duration) * 1s) var(--ease),
|
|
96
|
+
clip-path calc(var(--duration) * 1s) var(--ease),
|
|
97
|
+
background calc(var(--duration) * 1s) var(--ease);
|
|
98
|
+
clip-path: inset(73cqh calc(50% + 1px) calc(27cqh - 2px) calc(50% - 3px) round var(--radius));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* ── Pseudo text ── */
|
|
102
|
+
.group::before {
|
|
103
|
+
content: attr(data-label);
|
|
104
|
+
position: absolute; left: 50%; top: 50%;
|
|
105
|
+
translate: -50% -80%;
|
|
106
|
+
color: var(--fg); font-size: var(--font); font-weight: 500;
|
|
107
|
+
z-index: 2; white-space: nowrap; pointer-events: none;
|
|
108
|
+
transition: scale calc(var(--duration) * 1s) var(--ease),
|
|
109
|
+
translate calc(var(--duration) * 1s) var(--ease),
|
|
110
|
+
opacity calc(var(--duration) * 1s) var(--ease);
|
|
111
|
+
}
|
|
112
|
+
.group::after {
|
|
113
|
+
content: attr(data-opts);
|
|
114
|
+
position: absolute; left: 50%; top: 50%;
|
|
115
|
+
translate: -50% 20%;
|
|
116
|
+
color: var(--muted); font-size: calc(var(--font) * 0.85);
|
|
117
|
+
opacity: 0.6; z-index: 2; white-space: nowrap; pointer-events: none;
|
|
118
|
+
transition: opacity calc(var(--duration) * 1s) var(--ease);
|
|
119
|
+
}
|
|
120
|
+
.group:not([data-opts])::after { content: none; }
|
|
121
|
+
|
|
122
|
+
.group label {
|
|
123
|
+
color: var(--muted); cursor: pointer; z-index: 2;
|
|
124
|
+
transition: color calc(var(--duration) * 1s) var(--ease),
|
|
125
|
+
opacity calc(var(--duration) * 1s) var(--ease),
|
|
126
|
+
translate calc(var(--duration) * 1s) var(--ease);
|
|
127
|
+
}
|
|
128
|
+
.group label span {
|
|
129
|
+
display: grid; place-items: center; height: 100%; width: 100%;
|
|
130
|
+
border-radius: var(--radius);
|
|
131
|
+
transition: scale calc(var(--duration) * 1s) var(--ease);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* Collapsed modes */
|
|
135
|
+
.group[data-collapsed="title"]::before { translate: -50% -50%; }
|
|
136
|
+
.group[data-collapsed="title"]::after { display: none; }
|
|
137
|
+
.group[data-collapsed="title"]:not(:has(:checked)) label { opacity: 0; translate: 0 30%; }
|
|
138
|
+
.group[data-collapsed="title"]:not(:has(:checked)) label span { scale: 0.5; }
|
|
139
|
+
|
|
140
|
+
.group[data-collapsed="opts"]::before { display: none; }
|
|
141
|
+
.group[data-collapsed="opts"]::after { translate: -50% -50%; font-size: var(--font); opacity: 0.7; }
|
|
142
|
+
.group[data-collapsed="opts"]:not(:has(:checked)) label { opacity: 0; translate: 0 30%; }
|
|
143
|
+
.group[data-collapsed="opts"]:not(:has(:checked)) label span { scale: 0.5; }
|
|
144
|
+
|
|
145
|
+
/* Track states */
|
|
146
|
+
.control__track:has(> :checked) > label { color: var(--card); }
|
|
147
|
+
.control__track:not(:has(> :checked)) > label { color: var(--fg); opacity: var(--drop-off); }
|
|
148
|
+
.control__track:not(:has(> :checked)) > .indicator { translate: 100% 0; }
|
|
149
|
+
.control__track:has(> :checked) .group .indicator { background: transparent; }
|
|
150
|
+
|
|
151
|
+
/* Group expanded */
|
|
152
|
+
.control--goo .group:has(:checked)::before { opacity: 0; translate: -50% -80%; scale: 1; }
|
|
153
|
+
.group:has(:checked)::after { opacity: 0; }
|
|
154
|
+
.group:has(:checked) label { opacity: 0.75; color: var(--muted); translate: 0 0; }
|
|
155
|
+
.group:has(:checked) label span { scale: 1; }
|
|
156
|
+
.group:has(:checked) .indicator { background: var(--card); clip-path: inset(0 0 0 0 round var(--radius)); }
|
|
157
|
+
.group:has(:nth-of-type(1):checked) label:nth-of-type(1),
|
|
158
|
+
.group:has(:nth-of-type(2):checked) label:nth-of-type(2) { color: var(--fg); opacity: 1; }
|
|
159
|
+
.group:has(:nth-of-type(1):checked) .indicator { translate: -100% 0; }
|
|
160
|
+
.group:has(:nth-of-type(2):checked) .indicator { translate: 0 0; }
|
|
161
|
+
|
|
162
|
+
/* ================================================================
|
|
163
|
+
* SILEO-STYLE GOOEY: SVG filter on a canvas div, SVG rects inside
|
|
164
|
+
* Key: animated content is SVG <rect>, not HTML div.
|
|
165
|
+
* Safari re-renders SVG filter smoothly when SVG children change.
|
|
166
|
+
* ================================================================ */
|
|
167
|
+
.control--goo {
|
|
168
|
+
background: transparent;
|
|
169
|
+
border: none;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.goo-canvas {
|
|
173
|
+
position: absolute;
|
|
174
|
+
inset: 0;
|
|
175
|
+
pointer-events: none;
|
|
176
|
+
z-index: 0;
|
|
177
|
+
/* Sileo's Safari formula */
|
|
178
|
+
transform: translateZ(0);
|
|
179
|
+
contain: layout style;
|
|
180
|
+
overflow: visible;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.goo-svg {
|
|
184
|
+
position: absolute;
|
|
185
|
+
top: 0; left: 0;
|
|
186
|
+
overflow: visible;
|
|
187
|
+
pointer-events: none;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* Bubble TEXT — outside the filtered SVG, on top */
|
|
191
|
+
.control--goo .bubble-label {
|
|
192
|
+
position: absolute;
|
|
193
|
+
z-index: 2;
|
|
194
|
+
display: flex;
|
|
195
|
+
align-items: center;
|
|
196
|
+
justify-content: center;
|
|
197
|
+
font-size: var(--font);
|
|
198
|
+
font-weight: 500;
|
|
199
|
+
color: var(--fg);
|
|
200
|
+
white-space: nowrap;
|
|
201
|
+
pointer-events: none;
|
|
202
|
+
opacity: 0;
|
|
203
|
+
transition: opacity calc(var(--duration) * 1s) var(--ease);
|
|
204
|
+
}
|
|
205
|
+
.control--goo:has(.group :checked) .bubble-label {
|
|
206
|
+
opacity: 1;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* SIZES */
|
|
210
|
+
.control.sm { --w: 210px; --h: 30px; --font: 10px; }
|
|
211
|
+
.control.lg { --w: 320px; --h: 44px; --font: 14px; }
|
|
212
|
+
.control.xl { --w: 400px; --h: 52px; --font: 16px; }
|
|
213
|
+
.control.rounded { --radius: 12px; }
|
|
214
|
+
.control.square { --radius: 6px; }
|
|
215
|
+
</style>
|
|
216
|
+
</head>
|
|
217
|
+
<body>
|
|
218
|
+
|
|
219
|
+
<h3>Sileo-style gooey — SVG rects + filter (Safari-safe)</h3>
|
|
220
|
+
<p style="color:var(--muted);font-size:12px;max-width:600px;text-align:center">
|
|
221
|
+
SVG filter applied to a canvas div. Pill + bubble are SVG <code><rect></code> elements
|
|
222
|
+
animated via WAAPI. Safari renders filter smoothly because changes are SVG-internal.
|
|
223
|
+
</p>
|
|
224
|
+
|
|
225
|
+
<h3>collapsed="title" — Sizes</h3>
|
|
226
|
+
<div class="row">
|
|
227
|
+
<div><h3>SM</h3>
|
|
228
|
+
<div class="control sm control--goo" data-goo>
|
|
229
|
+
<div class="goo-canvas"><svg class="goo-svg"></svg></div>
|
|
230
|
+
<div class="bubble-label">Changes</div>
|
|
231
|
+
<div class="control__track"><div class="indicator"></div>
|
|
232
|
+
<label for="gs-a">Tree</label><input class="sr-only" type="radio" name="gs" id="gs-a" checked>
|
|
233
|
+
<div class="group" data-collapsed="title" data-label="Changes"><div class="indicator"></div>
|
|
234
|
+
<label for="gs-b"><span>Flat</span></label><input class="sr-only" type="radio" name="gs" id="gs-b">
|
|
235
|
+
<label for="gs-c"><span>Grp</span></label><input class="sr-only" type="radio" name="gs" id="gs-c">
|
|
236
|
+
</div></div></div></div>
|
|
237
|
+
|
|
238
|
+
<div><h3>Default</h3>
|
|
239
|
+
<div class="control control--goo" data-goo>
|
|
240
|
+
<div class="goo-canvas"><svg class="goo-svg"></svg></div>
|
|
241
|
+
<div class="bubble-label">Premium</div>
|
|
242
|
+
<div class="control__track"><div class="indicator"></div>
|
|
243
|
+
<label for="gd-a">Free</label><input class="sr-only" type="radio" name="gd" id="gd-a" checked>
|
|
244
|
+
<div class="group" data-collapsed="title" data-label="Premium"><div class="indicator"></div>
|
|
245
|
+
<label for="gd-b"><span>Solo</span></label><input class="sr-only" type="radio" name="gd" id="gd-b">
|
|
246
|
+
<label for="gd-c"><span>Team</span></label><input class="sr-only" type="radio" name="gd" id="gd-c">
|
|
247
|
+
</div></div></div></div>
|
|
248
|
+
|
|
249
|
+
<div><h3>LG</h3>
|
|
250
|
+
<div class="control lg control--goo" data-goo>
|
|
251
|
+
<div class="goo-canvas"><svg class="goo-svg"></svg></div>
|
|
252
|
+
<div class="bubble-label">Billing</div>
|
|
253
|
+
<div class="control__track"><div class="indicator"></div>
|
|
254
|
+
<label for="gl-a">Annual</label><input class="sr-only" type="radio" name="gl" id="gl-a" checked>
|
|
255
|
+
<div class="group" data-collapsed="title" data-label="Billing"><div class="indicator"></div>
|
|
256
|
+
<label for="gl-b"><span>Monthly</span></label><input class="sr-only" type="radio" name="gl" id="gl-b">
|
|
257
|
+
<label for="gl-c"><span>Weekly</span></label><input class="sr-only" type="radio" name="gl" id="gl-c">
|
|
258
|
+
</div></div></div></div>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<h3>Shapes</h3>
|
|
262
|
+
<div class="row">
|
|
263
|
+
<div class="control rounded control--goo" data-goo>
|
|
264
|
+
<div class="goo-canvas"><svg class="goo-svg"></svg></div>
|
|
265
|
+
<div class="bubble-label">Theme</div>
|
|
266
|
+
<div class="control__track"><div class="indicator"></div>
|
|
267
|
+
<label for="gr-a">System</label><input class="sr-only" type="radio" name="gr" id="gr-a">
|
|
268
|
+
<div class="group" data-collapsed="title" data-label="Theme"><div class="indicator"></div>
|
|
269
|
+
<label for="gr-b"><span>Light</span></label><input class="sr-only" type="radio" name="gr" id="gr-b" checked>
|
|
270
|
+
<label for="gr-c"><span>Dark</span></label><input class="sr-only" type="radio" name="gr" id="gr-c">
|
|
271
|
+
</div></div></div>
|
|
272
|
+
|
|
273
|
+
<div class="control square control--goo" data-goo>
|
|
274
|
+
<div class="goo-canvas"><svg class="goo-svg"></svg></div>
|
|
275
|
+
<div class="bubble-label">Output</div>
|
|
276
|
+
<div class="control__track"><div class="indicator"></div>
|
|
277
|
+
<label for="gsq-a">Input</label><input class="sr-only" type="radio" name="gsq" id="gsq-a">
|
|
278
|
+
<div class="group" data-collapsed="title" data-label="Output"><div class="indicator"></div>
|
|
279
|
+
<label for="gsq-b"><span>JSON</span></label><input class="sr-only" type="radio" name="gsq" id="gsq-b" checked>
|
|
280
|
+
<label for="gsq-c"><span>XML</span></label><input class="sr-only" type="radio" name="gsq" id="gsq-c">
|
|
281
|
+
</div></div></div>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<h3>collapsed="opts"</h3>
|
|
285
|
+
<div class="row">
|
|
286
|
+
<div class="control control--goo" data-goo>
|
|
287
|
+
<div class="goo-canvas"><svg class="goo-svg"></svg></div>
|
|
288
|
+
<div class="bubble-label">Premium</div>
|
|
289
|
+
<div class="control__track"><div class="indicator"></div>
|
|
290
|
+
<label for="go-a">Free</label><input class="sr-only" type="radio" name="go" id="go-a" checked>
|
|
291
|
+
<div class="group" data-collapsed="opts" data-label="Premium" data-opts="Solo · Team"><div class="indicator"></div>
|
|
292
|
+
<label for="go-b"><span>Solo</span></label><input class="sr-only" type="radio" name="go" id="go-b">
|
|
293
|
+
<label for="go-c"><span>Team</span></label><input class="sr-only" type="radio" name="go" id="go-c">
|
|
294
|
+
</div></div></div>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
<script>
|
|
298
|
+
// ================================================================
|
|
299
|
+
// SILEO-STYLE GOOEY ENGINE
|
|
300
|
+
// SVG filter + animated SVG rects (not HTML divs)
|
|
301
|
+
// ================================================================
|
|
302
|
+
|
|
303
|
+
const BLUR_RATIO = 0.15;
|
|
304
|
+
const DURATION = 0.25; // seconds
|
|
305
|
+
const OUTLINE_COLOR = '#334155';
|
|
306
|
+
|
|
307
|
+
let filterCounter = 0;
|
|
308
|
+
|
|
309
|
+
document.querySelectorAll('[data-goo]').forEach(control => {
|
|
310
|
+
const canvas = control.querySelector('.goo-canvas');
|
|
311
|
+
const svg = control.querySelector('.goo-svg');
|
|
312
|
+
const group = control.querySelector('.group');
|
|
313
|
+
const bubbleLabel = control.querySelector('.bubble-label');
|
|
314
|
+
const style = getComputedStyle(control);
|
|
315
|
+
|
|
316
|
+
const w = parseFloat(style.getPropertyValue('--w'));
|
|
317
|
+
const h = parseFloat(style.getPropertyValue('--h'));
|
|
318
|
+
const r = parseFloat(style.getPropertyValue('--radius'));
|
|
319
|
+
const effectiveR = Math.min(r, h / 2);
|
|
320
|
+
const bubbleHPct = parseFloat(style.getPropertyValue('--bubble-h-pct')) || 40;
|
|
321
|
+
const bubbleInsetPct = parseFloat(style.getPropertyValue('--bubble-inset-pct')) || 20;
|
|
322
|
+
const blur = Math.round(h * BLUR_RATIO);
|
|
323
|
+
const bubbleH = h * bubbleHPct / 100;
|
|
324
|
+
const bubbleInset = w * bubbleInsetPct / 100;
|
|
325
|
+
const bubbleW = w - 2 * bubbleInset;
|
|
326
|
+
const bubbleR = Math.min(effectiveR * 0.6, bubbleH * 0.45, 12);
|
|
327
|
+
|
|
328
|
+
// Create unique filter
|
|
329
|
+
const filterId = `goo-${filterCounter++}`;
|
|
330
|
+
|
|
331
|
+
// Build SVG content: filter defs + pill rect + bubble rect
|
|
332
|
+
const totalH = h + bubbleH;
|
|
333
|
+
svg.setAttribute('width', w);
|
|
334
|
+
svg.setAttribute('height', totalH);
|
|
335
|
+
svg.setAttribute('viewBox', `0 0 ${w} ${totalH}`);
|
|
336
|
+
svg.style.top = `-${bubbleH}px`;
|
|
337
|
+
|
|
338
|
+
svg.innerHTML = `
|
|
339
|
+
<defs>
|
|
340
|
+
<filter id="${filterId}" x="-20%" y="-20%" width="140%" height="140%"
|
|
341
|
+
color-interpolation-filters="sRGB">
|
|
342
|
+
<feGaussianBlur in="SourceGraphic" stdDeviation="${blur}" result="blur"/>
|
|
343
|
+
<feColorMatrix in="blur" mode="matrix"
|
|
344
|
+
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -10" result="goo"/>
|
|
345
|
+
<feComposite in="SourceGraphic" in2="goo" operator="atop"/>
|
|
346
|
+
</filter>
|
|
347
|
+
</defs>
|
|
348
|
+
<rect class="pill-rect"
|
|
349
|
+
x="2" y="${bubbleH}" width="${w - 4}" height="${h - 4}" rx="${effectiveR}" ry="${effectiveR}"
|
|
350
|
+
fill="var(--card)"/>
|
|
351
|
+
<rect class="bubble-rect"
|
|
352
|
+
x="${bubbleInset}" y="${bubbleH}" width="${bubbleW}" height="0" rx="${bubbleR}" ry="${bubbleR}"
|
|
353
|
+
fill="var(--card)"/>
|
|
354
|
+
`;
|
|
355
|
+
|
|
356
|
+
// Apply filter + drop-shadow to canvas
|
|
357
|
+
canvas.style.filter = `url(#${filterId}) drop-shadow(0 0 0.5px ${OUTLINE_COLOR}) drop-shadow(0 0 0.5px ${OUTLINE_COLOR})`;
|
|
358
|
+
|
|
359
|
+
const pillRect = svg.querySelector('.pill-rect');
|
|
360
|
+
const bubbleRect = svg.querySelector('.bubble-rect');
|
|
361
|
+
|
|
362
|
+
// Position bubble label
|
|
363
|
+
bubbleLabel.style.left = bubbleInset + 'px';
|
|
364
|
+
bubbleLabel.style.right = bubbleInset + 'px';
|
|
365
|
+
bubbleLabel.style.height = bubbleH + 'px';
|
|
366
|
+
bubbleLabel.style.bottom = h + 'px';
|
|
367
|
+
|
|
368
|
+
// Animate bubble rect via WAAPI (like Sileo uses Motion)
|
|
369
|
+
let currentAnim = null;
|
|
370
|
+
|
|
371
|
+
function animateBubble(expand) {
|
|
372
|
+
if (currentAnim) currentAnim.cancel();
|
|
373
|
+
|
|
374
|
+
const targetY = expand ? bubbleH - bubbleH : bubbleH;
|
|
375
|
+
const targetH = expand ? bubbleH : 0;
|
|
376
|
+
|
|
377
|
+
currentAnim = bubbleRect.animate([
|
|
378
|
+
{ // from current
|
|
379
|
+
y: bubbleRect.getAttribute('y'),
|
|
380
|
+
height: bubbleRect.getAttribute('height'),
|
|
381
|
+
},
|
|
382
|
+
{ // to target
|
|
383
|
+
y: targetY,
|
|
384
|
+
height: targetH,
|
|
385
|
+
}
|
|
386
|
+
], {
|
|
387
|
+
duration: DURATION * 1500,
|
|
388
|
+
easing: 'cubic-bezier(0.22, 0.61, 0.36, 1)',
|
|
389
|
+
fill: 'forwards',
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
currentAnim.onfinish = () => {
|
|
393
|
+
bubbleRect.setAttribute('y', targetY);
|
|
394
|
+
bubbleRect.setAttribute('height', targetH);
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Listen for radio changes
|
|
399
|
+
function checkState() {
|
|
400
|
+
const expanded = group.querySelector(':checked') !== null;
|
|
401
|
+
animateBubble(expanded);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
control.querySelectorAll('input[type="radio"]').forEach(input => {
|
|
405
|
+
input.addEventListener('change', checkState);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// Initial state
|
|
409
|
+
checkState();
|
|
410
|
+
});
|
|
411
|
+
</script>
|
|
412
|
+
</body>
|
|
413
|
+
</html>
|