@justin_evo/evo-ui 1.1.0 → 1.2.1

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 (80) hide show
  1. package/README.md +3 -3
  2. package/dist/TopNav/TopNav.d.ts +19 -0
  3. package/dist/declarations.d.ts +6 -6
  4. package/dist/evo-ui.css +1 -1
  5. package/dist/index.cjs.js +1 -1
  6. package/dist/index.es.js +3301 -3197
  7. package/package.json +52 -52
  8. package/src/Alert/Alert.tsx +49 -49
  9. package/src/AutoComplete/AutoComplete.tsx +810 -810
  10. package/src/Badge/Badge.tsx +53 -53
  11. package/src/Breadcrumb/Breadcrumb.tsx +53 -53
  12. package/src/Button/Button.tsx +125 -125
  13. package/src/Card/Card.tsx +257 -257
  14. package/src/Checkbox/Checkbox.tsx +59 -59
  15. package/src/CommandPalette/CommandPalette.tsx +185 -185
  16. package/src/Container/Container.tsx +31 -31
  17. package/src/Divider/Divider.tsx +31 -31
  18. package/src/Form/Form.tsx +185 -185
  19. package/src/Grid/Grid.tsx +66 -66
  20. package/src/ImageCropper/ImageCropper.tsx +911 -911
  21. package/src/Input/Input.tsx +74 -74
  22. package/src/Modal/Modal.tsx +77 -77
  23. package/src/Nav/Nav.tsx +708 -708
  24. package/src/Notification/Notification.tsx +1503 -1503
  25. package/src/Pagination/Pagination.tsx +76 -76
  26. package/src/Radio/Radio.tsx +69 -69
  27. package/src/RichTextArea/RichTextArea.tsx +886 -869
  28. package/src/Select/Select.tsx +515 -515
  29. package/src/Skeleton/Skeleton.tsx +70 -70
  30. package/src/Stack/Stack.tsx +52 -52
  31. package/src/Table/Table.tsx +335 -335
  32. package/src/Tabs/Tabs.tsx +90 -90
  33. package/src/Theme/ThemeProvider.tsx +253 -253
  34. package/src/Theme/ThemeToggle.tsx +79 -79
  35. package/src/Toggle/Toggle.tsx +48 -48
  36. package/src/Tooltip/Tooltip.tsx +38 -38
  37. package/src/TopNav/TopNav.tsx +1163 -994
  38. package/src/TreeSelect/TreeSelect.tsx +825 -825
  39. package/src/css/alert.module.scss +93 -93
  40. package/src/css/autocomplete.module.scss +416 -416
  41. package/src/css/badge.module.scss +82 -82
  42. package/src/css/base/_color.scss +159 -159
  43. package/src/css/base/_theme.scss +237 -237
  44. package/src/css/base/_variables.scss +161 -161
  45. package/src/css/breadcrumb.module.scss +50 -50
  46. package/src/css/button.module.scss +385 -385
  47. package/src/css/card.module.scss +217 -217
  48. package/src/css/checkbox.module.scss +123 -120
  49. package/src/css/commandpalette.module.scss +211 -211
  50. package/src/css/container.module.scss +18 -18
  51. package/src/css/divider.module.scss +41 -41
  52. package/src/css/form.module.scss +245 -245
  53. package/src/css/imagecropper.module.scss +397 -397
  54. package/src/css/input.module.scss +89 -89
  55. package/src/css/modal.module.scss +105 -105
  56. package/src/css/nav.module.scss +494 -494
  57. package/src/css/notification.module.scss +691 -691
  58. package/src/css/pagination.module.scss +63 -63
  59. package/src/css/radio.module.scss +89 -89
  60. package/src/css/richtextarea.module.scss +307 -307
  61. package/src/css/select.module.scss +525 -525
  62. package/src/css/skeleton.module.scss +30 -30
  63. package/src/css/table.module.scss +386 -386
  64. package/src/css/tabs.module.scss +63 -63
  65. package/src/css/theme-toggle.module.scss +83 -83
  66. package/src/css/toggle.module.scss +54 -54
  67. package/src/css/tooltip.module.scss +97 -97
  68. package/src/css/topnav.module.scss +568 -396
  69. package/src/css/treeselect.module.scss +558 -558
  70. package/src/css/utilities/_borders.scss +111 -111
  71. package/src/css/utilities/_colors.scss +66 -66
  72. package/src/css/utilities/_effects.scss +216 -216
  73. package/src/css/utilities/_layout.scss +181 -181
  74. package/src/css/utilities/_position.scss +75 -75
  75. package/src/css/utilities/_sizing.scss +138 -138
  76. package/src/css/utilities/_spacing.scss +99 -99
  77. package/src/css/utilities/_typography.scss +121 -121
  78. package/src/css/utilities/index.scss +24 -24
  79. package/src/declarations.d.ts +6 -6
  80. package/src/index.ts +60 -60
@@ -1,397 +1,397 @@
1
- @use 'base/variables' as *;
2
-
3
- // =============================================================
4
- // EvoImageCropper
5
- // -------------------------------------------------------------
6
- // A pointer-driven image cropper. The image transforms with
7
- // CSS (translate + scale + rotate); the crop rectangle is a
8
- // fixed overlay with eight resize handles. Final pixel work
9
- // (rendering the crop to a blob) is deferred until requested.
10
- //
11
- // Everything responds to viewport width — handles grow on touch,
12
- // the toolbar reflows on narrow screens, and a tall portrait
13
- // orientation keeps the canvas usable.
14
- // =============================================================
15
-
16
- .root {
17
- display: flex;
18
- flex-direction: column;
19
- gap: 0.75rem;
20
- font-family: $font-sans;
21
- color: $color-text-primary;
22
- min-width: 0;
23
- width: 100%;
24
- }
25
-
26
- .fullWidth { width: 100%; }
27
-
28
- .label {
29
- font-size: $text-sm;
30
- font-weight: 500;
31
- color: $color-text-primary;
32
- }
33
-
34
- // ---------- Canvas area (image + overlay) ----------
35
- .stage {
36
- position: relative;
37
- width: 100%;
38
- height: 360px;
39
- border-radius: $radius-md;
40
- background-color: $color-surface-sunken;
41
- border: 1px solid $color-border;
42
- overflow: hidden;
43
- touch-action: none;
44
- user-select: none;
45
- -webkit-user-select: none;
46
- -webkit-tap-highlight-color: transparent;
47
- cursor: grab;
48
-
49
- &:active { cursor: grabbing; }
50
-
51
- &.disabled {
52
- opacity: 0.55;
53
- pointer-events: none;
54
- }
55
- }
56
-
57
- // Checkerboard background — clearly shows alpha & PNG borders
58
- .bgChecker {
59
- background-image:
60
- linear-gradient(45deg, $color-surface-active 25%, transparent 25%),
61
- linear-gradient(-45deg, $color-surface-active 25%, transparent 25%),
62
- linear-gradient(45deg, transparent 75%, $color-surface-active 75%),
63
- linear-gradient(-45deg, transparent 75%, $color-surface-active 75%);
64
- background-size: 16px 16px;
65
- background-position: 0 0, 0 8px, 8px -8px, -8px 0;
66
- }
67
-
68
- .imageWrap {
69
- position: absolute;
70
- inset: 0;
71
- display: flex;
72
- align-items: center;
73
- justify-content: center;
74
- pointer-events: none;
75
- }
76
-
77
- .image {
78
- display: block;
79
- max-width: none;
80
- pointer-events: none;
81
- user-select: none;
82
- -webkit-user-drag: none;
83
- transform-origin: center center;
84
- will-change: transform;
85
- }
86
-
87
- // ---------- Crop overlay ----------
88
- // A full-stage element with a giant inset box-shadow that darkens
89
- // everything *outside* the crop rectangle. The element itself sits
90
- // at the crop coords and has no fill of its own.
91
- .overlay {
92
- position: absolute;
93
- box-sizing: border-box;
94
- border: 1px solid rgba(255, 255, 255, 0.92);
95
- box-shadow:
96
- 0 0 0 9999px rgba(0, 0, 0, 0.55),
97
- 0 0 0 1px rgba(0, 0, 0, 0.25) inset;
98
- cursor: move;
99
- touch-action: none;
100
- }
101
-
102
- .overlayCircle {
103
- border-radius: 50%;
104
- // Box-shadow can't follow a border-radius cutout, so the circle
105
- // mode uses a radial gradient on a sibling element instead.
106
- box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25) inset;
107
- }
108
-
109
- .circleMask {
110
- position: absolute;
111
- inset: 0;
112
- pointer-events: none;
113
- background:
114
- radial-gradient(circle at var(--mask-cx) var(--mask-cy),
115
- transparent 0,
116
- transparent var(--mask-r),
117
- rgba(0, 0, 0, 0.55) calc(var(--mask-r) + 1px));
118
- }
119
-
120
- // ---------- Rule-of-thirds grid ----------
121
- .grid {
122
- position: absolute;
123
- inset: 0;
124
- pointer-events: none;
125
- }
126
-
127
- .gridLine {
128
- position: absolute;
129
- background-color: rgba(255, 255, 255, 0.35);
130
-
131
- &.h { left: 0; right: 0; height: 1px; }
132
- &.v { top: 0; bottom: 0; width: 1px; }
133
- }
134
-
135
- // ---------- Resize handles ----------
136
- .handle {
137
- position: absolute;
138
- width: 12px;
139
- height: 12px;
140
- background-color: #fff;
141
- border: 1px solid rgba(0, 0, 0, 0.4);
142
- border-radius: 2px;
143
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
144
- z-index: 2;
145
- touch-action: none;
146
-
147
- // Larger tap targets on touch-capable devices
148
- @media (hover: none) and (pointer: coarse) {
149
- width: 20px;
150
- height: 20px;
151
- }
152
- }
153
-
154
- // Corner handles — at the four corners of the crop rect
155
- .handleTL { top: -7px; left: -7px; cursor: nwse-resize; }
156
- .handleTR { top: -7px; right: -7px; cursor: nesw-resize; }
157
- .handleBL { bottom: -7px; left: -7px; cursor: nesw-resize; }
158
- .handleBR { bottom: -7px; right: -7px; cursor: nwse-resize; }
159
-
160
- // Edge handles — wider/taller to feel like pull-bars
161
- .handleT, .handleB {
162
- left: 50%;
163
- transform: translateX(-50%);
164
- width: 24px;
165
- height: 6px;
166
- cursor: ns-resize;
167
-
168
- @media (hover: none) and (pointer: coarse) {
169
- width: 36px;
170
- height: 10px;
171
- }
172
- }
173
- .handleL, .handleR {
174
- top: 50%;
175
- transform: translateY(-50%);
176
- width: 6px;
177
- height: 24px;
178
- cursor: ew-resize;
179
-
180
- @media (hover: none) and (pointer: coarse) {
181
- width: 10px;
182
- height: 36px;
183
- }
184
- }
185
- .handleT { top: -3px; }
186
- .handleB { bottom: -3px; }
187
- .handleL { left: -3px; }
188
- .handleR { right: -3px; }
189
-
190
- // In circular mode, edge handles look odd; hide them and keep corners
191
- .overlay.overlayCircle {
192
- .handleT, .handleB, .handleL, .handleR { display: none; }
193
- }
194
-
195
- // ---------- Controls toolbar ----------
196
- .controls {
197
- display: flex;
198
- flex-wrap: wrap;
199
- align-items: center;
200
- gap: 0.5rem 0.75rem;
201
- padding: 0.625rem 0.75rem;
202
- background-color: $color-surface;
203
- border: 1px solid $color-border;
204
- border-radius: $radius-sm;
205
- }
206
-
207
- .controlGroup {
208
- display: flex;
209
- align-items: center;
210
- gap: 0.375rem;
211
- min-width: 0;
212
- }
213
-
214
- .controlLabel {
215
- font-size: $text-xs;
216
- color: $color-text-muted;
217
- font-weight: 500;
218
- letter-spacing: 0.02em;
219
- text-transform: uppercase;
220
- white-space: nowrap;
221
- }
222
-
223
- .iconBtn {
224
- display: inline-flex;
225
- align-items: center;
226
- justify-content: center;
227
- width: 32px;
228
- height: 32px;
229
- padding: 0;
230
- background: transparent;
231
- border: 1px solid $color-border;
232
- border-radius: $evo-border-radius-sm;
233
- color: $color-text-secondary;
234
- cursor: pointer;
235
- transition: background-color $transition-fast, color $transition-fast, border-color $transition-fast;
236
- -webkit-tap-highlight-color: transparent;
237
-
238
- &:hover:not(:disabled) {
239
- background-color: $color-surface-hover;
240
- color: $color-text-primary;
241
- border-color: $color-border-strong;
242
- }
243
-
244
- &:active:not(:disabled) { background-color: $color-surface-active; }
245
-
246
- &:disabled { cursor: not-allowed; opacity: 0.45; }
247
-
248
- &:focus-visible {
249
- outline: none;
250
- border-color: $evo-primary-color;
251
- box-shadow: 0 0 0 2px color-mix(in srgb, $evo-primary-color 22%, transparent);
252
- }
253
- }
254
-
255
- .zoomSlider {
256
- appearance: none;
257
- -webkit-appearance: none;
258
- flex: 1 1 120px;
259
- min-width: 80px;
260
- max-width: 220px;
261
- height: 4px;
262
- background-color: $color-surface-active;
263
- border-radius: $radius-full;
264
- cursor: pointer;
265
-
266
- &::-webkit-slider-thumb {
267
- appearance: none;
268
- -webkit-appearance: none;
269
- width: 16px;
270
- height: 16px;
271
- background-color: $evo-primary-color;
272
- border-radius: 50%;
273
- cursor: pointer;
274
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
275
- }
276
-
277
- &::-moz-range-thumb {
278
- width: 16px;
279
- height: 16px;
280
- background-color: $evo-primary-color;
281
- border: none;
282
- border-radius: 50%;
283
- cursor: pointer;
284
- }
285
-
286
- &:focus-visible {
287
- outline: none;
288
- box-shadow: 0 0 0 3px color-mix(in srgb, $evo-primary-color 22%, transparent);
289
- }
290
- }
291
-
292
- // ---------- Aspect ratio preset chips ----------
293
- .ratioRow {
294
- display: flex;
295
- flex-wrap: wrap;
296
- gap: 0.375rem;
297
- }
298
-
299
- .ratioChip {
300
- display: inline-flex;
301
- align-items: center;
302
- padding: 0.25rem 0.625rem;
303
- background-color: $color-surface;
304
- border: 1px solid $color-border;
305
- border-radius: $radius-full;
306
- font-size: $text-xs;
307
- font-weight: 500;
308
- color: $color-text-secondary;
309
- cursor: pointer;
310
- transition: background-color $transition-fast, color $transition-fast, border-color $transition-fast;
311
- -webkit-tap-highlight-color: transparent;
312
-
313
- &:hover:not(:disabled) {
314
- background-color: $color-surface-hover;
315
- color: $color-text-primary;
316
- }
317
-
318
- &.active {
319
- background-color: $evo-primary-soft;
320
- border-color: $evo-primary-color;
321
- color: $evo-primary-color;
322
- }
323
-
324
- &:focus-visible {
325
- outline: none;
326
- border-color: $evo-primary-color;
327
- box-shadow: 0 0 0 2px color-mix(in srgb, $evo-primary-color 22%, transparent);
328
- }
329
-
330
- &:disabled { cursor: not-allowed; opacity: 0.45; }
331
- }
332
-
333
- .divider {
334
- width: 1px;
335
- height: 20px;
336
- background-color: $color-border;
337
- }
338
-
339
- .helper {
340
- font-size: $text-xs;
341
- color: $color-text-muted;
342
- }
343
-
344
- // ---------- Loading state ----------
345
- .placeholder {
346
- position: absolute;
347
- inset: 0;
348
- display: flex;
349
- flex-direction: column;
350
- align-items: center;
351
- justify-content: center;
352
- gap: 0.5rem;
353
- color: $color-text-muted;
354
- font-size: $text-sm;
355
- }
356
-
357
- .spinner {
358
- width: 20px;
359
- height: 20px;
360
- border: 2px solid $color-border;
361
- border-top-color: $evo-primary-color;
362
- border-radius: 50%;
363
- animation: evoCropperSpin 700ms linear infinite;
364
- }
365
-
366
- @keyframes evoCropperSpin {
367
- to { transform: rotate(360deg); }
368
- }
369
-
370
- // ---------- Responsive ----------
371
- @media (max-width: 640px) {
372
- .stage { height: 300px; }
373
-
374
- .controls {
375
- padding: 0.5rem;
376
- gap: 0.5rem;
377
- }
378
-
379
- // On narrow screens the divider becomes a full-width row break
380
- .divider {
381
- width: 100%;
382
- height: 0;
383
- background: transparent;
384
- }
385
-
386
- .controlGroup { flex: 1 1 auto; justify-content: flex-start; }
387
- }
388
-
389
- @media (max-width: 420px) {
390
- .stage { height: 260px; }
391
- .controlLabel { display: none; }
392
- }
393
-
394
- @media (prefers-reduced-motion: reduce) {
395
- .image, .overlay, .iconBtn, .ratioChip { transition: none !important; }
396
- .spinner { animation: none; }
397
- }
1
+ @use 'base/variables' as *;
2
+
3
+ // =============================================================
4
+ // EvoImageCropper
5
+ // -------------------------------------------------------------
6
+ // A pointer-driven image cropper. The image transforms with
7
+ // CSS (translate + scale + rotate); the crop rectangle is a
8
+ // fixed overlay with eight resize handles. Final pixel work
9
+ // (rendering the crop to a blob) is deferred until requested.
10
+ //
11
+ // Everything responds to viewport width — handles grow on touch,
12
+ // the toolbar reflows on narrow screens, and a tall portrait
13
+ // orientation keeps the canvas usable.
14
+ // =============================================================
15
+
16
+ .root {
17
+ display: flex;
18
+ flex-direction: column;
19
+ gap: 0.75rem;
20
+ font-family: $font-sans;
21
+ color: $color-text-primary;
22
+ min-width: 0;
23
+ width: 100%;
24
+ }
25
+
26
+ .fullWidth { width: 100%; }
27
+
28
+ .label {
29
+ font-size: $text-sm;
30
+ font-weight: 500;
31
+ color: $color-text-primary;
32
+ }
33
+
34
+ // ---------- Canvas area (image + overlay) ----------
35
+ .stage {
36
+ position: relative;
37
+ width: 100%;
38
+ height: 360px;
39
+ border-radius: $radius-md;
40
+ background-color: $color-surface-sunken;
41
+ border: 1px solid $color-border;
42
+ overflow: hidden;
43
+ touch-action: none;
44
+ user-select: none;
45
+ -webkit-user-select: none;
46
+ -webkit-tap-highlight-color: transparent;
47
+ cursor: grab;
48
+
49
+ &:active { cursor: grabbing; }
50
+
51
+ &.disabled {
52
+ opacity: 0.55;
53
+ pointer-events: none;
54
+ }
55
+ }
56
+
57
+ // Checkerboard background — clearly shows alpha & PNG borders
58
+ .bgChecker {
59
+ background-image:
60
+ linear-gradient(45deg, $color-surface-active 25%, transparent 25%),
61
+ linear-gradient(-45deg, $color-surface-active 25%, transparent 25%),
62
+ linear-gradient(45deg, transparent 75%, $color-surface-active 75%),
63
+ linear-gradient(-45deg, transparent 75%, $color-surface-active 75%);
64
+ background-size: 16px 16px;
65
+ background-position: 0 0, 0 8px, 8px -8px, -8px 0;
66
+ }
67
+
68
+ .imageWrap {
69
+ position: absolute;
70
+ inset: 0;
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ pointer-events: none;
75
+ }
76
+
77
+ .image {
78
+ display: block;
79
+ max-width: none;
80
+ pointer-events: none;
81
+ user-select: none;
82
+ -webkit-user-drag: none;
83
+ transform-origin: center center;
84
+ will-change: transform;
85
+ }
86
+
87
+ // ---------- Crop overlay ----------
88
+ // A full-stage element with a giant inset box-shadow that darkens
89
+ // everything *outside* the crop rectangle. The element itself sits
90
+ // at the crop coords and has no fill of its own.
91
+ .overlay {
92
+ position: absolute;
93
+ box-sizing: border-box;
94
+ border: 1px solid rgba(255, 255, 255, 0.92);
95
+ box-shadow:
96
+ 0 0 0 9999px rgba(0, 0, 0, 0.55),
97
+ 0 0 0 1px rgba(0, 0, 0, 0.25) inset;
98
+ cursor: move;
99
+ touch-action: none;
100
+ }
101
+
102
+ .overlayCircle {
103
+ border-radius: 50%;
104
+ // Box-shadow can't follow a border-radius cutout, so the circle
105
+ // mode uses a radial gradient on a sibling element instead.
106
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25) inset;
107
+ }
108
+
109
+ .circleMask {
110
+ position: absolute;
111
+ inset: 0;
112
+ pointer-events: none;
113
+ background:
114
+ radial-gradient(circle at var(--mask-cx) var(--mask-cy),
115
+ transparent 0,
116
+ transparent var(--mask-r),
117
+ rgba(0, 0, 0, 0.55) calc(var(--mask-r) + 1px));
118
+ }
119
+
120
+ // ---------- Rule-of-thirds grid ----------
121
+ .grid {
122
+ position: absolute;
123
+ inset: 0;
124
+ pointer-events: none;
125
+ }
126
+
127
+ .gridLine {
128
+ position: absolute;
129
+ background-color: rgba(255, 255, 255, 0.35);
130
+
131
+ &.h { left: 0; right: 0; height: 1px; }
132
+ &.v { top: 0; bottom: 0; width: 1px; }
133
+ }
134
+
135
+ // ---------- Resize handles ----------
136
+ .handle {
137
+ position: absolute;
138
+ width: 12px;
139
+ height: 12px;
140
+ background-color: #fff;
141
+ border: 1px solid rgba(0, 0, 0, 0.4);
142
+ border-radius: 2px;
143
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
144
+ z-index: 2;
145
+ touch-action: none;
146
+
147
+ // Larger tap targets on touch-capable devices
148
+ @media (hover: none) and (pointer: coarse) {
149
+ width: 20px;
150
+ height: 20px;
151
+ }
152
+ }
153
+
154
+ // Corner handles — at the four corners of the crop rect
155
+ .handleTL { top: -7px; left: -7px; cursor: nwse-resize; }
156
+ .handleTR { top: -7px; right: -7px; cursor: nesw-resize; }
157
+ .handleBL { bottom: -7px; left: -7px; cursor: nesw-resize; }
158
+ .handleBR { bottom: -7px; right: -7px; cursor: nwse-resize; }
159
+
160
+ // Edge handles — wider/taller to feel like pull-bars
161
+ .handleT, .handleB {
162
+ left: 50%;
163
+ transform: translateX(-50%);
164
+ width: 24px;
165
+ height: 6px;
166
+ cursor: ns-resize;
167
+
168
+ @media (hover: none) and (pointer: coarse) {
169
+ width: 36px;
170
+ height: 10px;
171
+ }
172
+ }
173
+ .handleL, .handleR {
174
+ top: 50%;
175
+ transform: translateY(-50%);
176
+ width: 6px;
177
+ height: 24px;
178
+ cursor: ew-resize;
179
+
180
+ @media (hover: none) and (pointer: coarse) {
181
+ width: 10px;
182
+ height: 36px;
183
+ }
184
+ }
185
+ .handleT { top: -3px; }
186
+ .handleB { bottom: -3px; }
187
+ .handleL { left: -3px; }
188
+ .handleR { right: -3px; }
189
+
190
+ // In circular mode, edge handles look odd; hide them and keep corners
191
+ .overlay.overlayCircle {
192
+ .handleT, .handleB, .handleL, .handleR { display: none; }
193
+ }
194
+
195
+ // ---------- Controls toolbar ----------
196
+ .controls {
197
+ display: flex;
198
+ flex-wrap: wrap;
199
+ align-items: center;
200
+ gap: 0.5rem 0.75rem;
201
+ padding: 0.625rem 0.75rem;
202
+ background-color: $color-surface;
203
+ border: 1px solid $color-border;
204
+ border-radius: $radius-sm;
205
+ }
206
+
207
+ .controlGroup {
208
+ display: flex;
209
+ align-items: center;
210
+ gap: 0.375rem;
211
+ min-width: 0;
212
+ }
213
+
214
+ .controlLabel {
215
+ font-size: $text-xs;
216
+ color: $color-text-muted;
217
+ font-weight: 500;
218
+ letter-spacing: 0.02em;
219
+ text-transform: uppercase;
220
+ white-space: nowrap;
221
+ }
222
+
223
+ .iconBtn {
224
+ display: inline-flex;
225
+ align-items: center;
226
+ justify-content: center;
227
+ width: 32px;
228
+ height: 32px;
229
+ padding: 0;
230
+ background: transparent;
231
+ border: 1px solid $color-border;
232
+ border-radius: $evo-border-radius-sm;
233
+ color: $color-text-secondary;
234
+ cursor: pointer;
235
+ transition: background-color $transition-fast, color $transition-fast, border-color $transition-fast;
236
+ -webkit-tap-highlight-color: transparent;
237
+
238
+ &:hover:not(:disabled) {
239
+ background-color: $color-surface-hover;
240
+ color: $color-text-primary;
241
+ border-color: $color-border-strong;
242
+ }
243
+
244
+ &:active:not(:disabled) { background-color: $color-surface-active; }
245
+
246
+ &:disabled { cursor: not-allowed; opacity: 0.45; }
247
+
248
+ &:focus-visible {
249
+ outline: none;
250
+ border-color: $evo-primary-color;
251
+ box-shadow: 0 0 0 2px color-mix(in srgb, $evo-primary-color 22%, transparent);
252
+ }
253
+ }
254
+
255
+ .zoomSlider {
256
+ appearance: none;
257
+ -webkit-appearance: none;
258
+ flex: 1 1 120px;
259
+ min-width: 80px;
260
+ max-width: 220px;
261
+ height: 4px;
262
+ background-color: $color-surface-active;
263
+ border-radius: $radius-full;
264
+ cursor: pointer;
265
+
266
+ &::-webkit-slider-thumb {
267
+ appearance: none;
268
+ -webkit-appearance: none;
269
+ width: 16px;
270
+ height: 16px;
271
+ background-color: $evo-primary-color;
272
+ border-radius: 50%;
273
+ cursor: pointer;
274
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
275
+ }
276
+
277
+ &::-moz-range-thumb {
278
+ width: 16px;
279
+ height: 16px;
280
+ background-color: $evo-primary-color;
281
+ border: none;
282
+ border-radius: 50%;
283
+ cursor: pointer;
284
+ }
285
+
286
+ &:focus-visible {
287
+ outline: none;
288
+ box-shadow: 0 0 0 3px color-mix(in srgb, $evo-primary-color 22%, transparent);
289
+ }
290
+ }
291
+
292
+ // ---------- Aspect ratio preset chips ----------
293
+ .ratioRow {
294
+ display: flex;
295
+ flex-wrap: wrap;
296
+ gap: 0.375rem;
297
+ }
298
+
299
+ .ratioChip {
300
+ display: inline-flex;
301
+ align-items: center;
302
+ padding: 0.25rem 0.625rem;
303
+ background-color: $color-surface;
304
+ border: 1px solid $color-border;
305
+ border-radius: $radius-full;
306
+ font-size: $text-xs;
307
+ font-weight: 500;
308
+ color: $color-text-secondary;
309
+ cursor: pointer;
310
+ transition: background-color $transition-fast, color $transition-fast, border-color $transition-fast;
311
+ -webkit-tap-highlight-color: transparent;
312
+
313
+ &:hover:not(:disabled) {
314
+ background-color: $color-surface-hover;
315
+ color: $color-text-primary;
316
+ }
317
+
318
+ &.active {
319
+ background-color: $evo-primary-soft;
320
+ border-color: $evo-primary-color;
321
+ color: $evo-primary-color;
322
+ }
323
+
324
+ &:focus-visible {
325
+ outline: none;
326
+ border-color: $evo-primary-color;
327
+ box-shadow: 0 0 0 2px color-mix(in srgb, $evo-primary-color 22%, transparent);
328
+ }
329
+
330
+ &:disabled { cursor: not-allowed; opacity: 0.45; }
331
+ }
332
+
333
+ .divider {
334
+ width: 1px;
335
+ height: 20px;
336
+ background-color: $color-border;
337
+ }
338
+
339
+ .helper {
340
+ font-size: $text-xs;
341
+ color: $color-text-muted;
342
+ }
343
+
344
+ // ---------- Loading state ----------
345
+ .placeholder {
346
+ position: absolute;
347
+ inset: 0;
348
+ display: flex;
349
+ flex-direction: column;
350
+ align-items: center;
351
+ justify-content: center;
352
+ gap: 0.5rem;
353
+ color: $color-text-muted;
354
+ font-size: $text-sm;
355
+ }
356
+
357
+ .spinner {
358
+ width: 20px;
359
+ height: 20px;
360
+ border: 2px solid $color-border;
361
+ border-top-color: $evo-primary-color;
362
+ border-radius: 50%;
363
+ animation: evoCropperSpin 700ms linear infinite;
364
+ }
365
+
366
+ @keyframes evoCropperSpin {
367
+ to { transform: rotate(360deg); }
368
+ }
369
+
370
+ // ---------- Responsive ----------
371
+ @media (max-width: 640px) {
372
+ .stage { height: 300px; }
373
+
374
+ .controls {
375
+ padding: 0.5rem;
376
+ gap: 0.5rem;
377
+ }
378
+
379
+ // On narrow screens the divider becomes a full-width row break
380
+ .divider {
381
+ width: 100%;
382
+ height: 0;
383
+ background: transparent;
384
+ }
385
+
386
+ .controlGroup { flex: 1 1 auto; justify-content: flex-start; }
387
+ }
388
+
389
+ @media (max-width: 420px) {
390
+ .stage { height: 260px; }
391
+ .controlLabel { display: none; }
392
+ }
393
+
394
+ @media (prefers-reduced-motion: reduce) {
395
+ .image, .overlay, .iconBtn, .ratioChip { transition: none !important; }
396
+ .spinner { animation: none; }
397
+ }