@justin_evo/evo-ui 1.2.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 (77) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +70 -70
  3. package/dist/declarations.d.ts +6 -6
  4. package/package.json +52 -52
  5. package/src/Alert/Alert.tsx +49 -49
  6. package/src/AutoComplete/AutoComplete.tsx +810 -810
  7. package/src/Badge/Badge.tsx +53 -53
  8. package/src/Breadcrumb/Breadcrumb.tsx +53 -53
  9. package/src/Button/Button.tsx +125 -125
  10. package/src/Card/Card.tsx +257 -257
  11. package/src/Checkbox/Checkbox.tsx +59 -59
  12. package/src/CommandPalette/CommandPalette.tsx +185 -185
  13. package/src/Container/Container.tsx +31 -31
  14. package/src/Divider/Divider.tsx +31 -31
  15. package/src/Form/Form.tsx +185 -185
  16. package/src/Grid/Grid.tsx +66 -66
  17. package/src/ImageCropper/ImageCropper.tsx +911 -911
  18. package/src/Input/Input.tsx +74 -74
  19. package/src/Modal/Modal.tsx +77 -77
  20. package/src/Nav/Nav.tsx +708 -708
  21. package/src/Notification/Notification.tsx +1503 -1503
  22. package/src/Pagination/Pagination.tsx +76 -76
  23. package/src/Radio/Radio.tsx +69 -69
  24. package/src/RichTextArea/RichTextArea.tsx +886 -886
  25. package/src/Select/Select.tsx +515 -515
  26. package/src/Skeleton/Skeleton.tsx +70 -70
  27. package/src/Stack/Stack.tsx +52 -52
  28. package/src/Table/Table.tsx +335 -335
  29. package/src/Tabs/Tabs.tsx +90 -90
  30. package/src/Theme/ThemeProvider.tsx +253 -253
  31. package/src/Theme/ThemeToggle.tsx +79 -79
  32. package/src/Toggle/Toggle.tsx +48 -48
  33. package/src/Tooltip/Tooltip.tsx +38 -38
  34. package/src/TopNav/TopNav.tsx +1163 -1163
  35. package/src/TreeSelect/TreeSelect.tsx +825 -825
  36. package/src/css/alert.module.scss +93 -93
  37. package/src/css/autocomplete.module.scss +416 -416
  38. package/src/css/badge.module.scss +82 -82
  39. package/src/css/base/_color.scss +159 -159
  40. package/src/css/base/_theme.scss +237 -237
  41. package/src/css/base/_variables.scss +161 -161
  42. package/src/css/breadcrumb.module.scss +50 -50
  43. package/src/css/button.module.scss +385 -385
  44. package/src/css/card.module.scss +217 -217
  45. package/src/css/checkbox.module.scss +123 -123
  46. package/src/css/commandpalette.module.scss +211 -211
  47. package/src/css/container.module.scss +18 -18
  48. package/src/css/divider.module.scss +41 -41
  49. package/src/css/form.module.scss +245 -245
  50. package/src/css/imagecropper.module.scss +397 -397
  51. package/src/css/input.module.scss +89 -89
  52. package/src/css/modal.module.scss +105 -105
  53. package/src/css/nav.module.scss +494 -494
  54. package/src/css/notification.module.scss +691 -691
  55. package/src/css/pagination.module.scss +63 -63
  56. package/src/css/radio.module.scss +89 -89
  57. package/src/css/richtextarea.module.scss +307 -307
  58. package/src/css/select.module.scss +525 -525
  59. package/src/css/skeleton.module.scss +30 -30
  60. package/src/css/table.module.scss +386 -386
  61. package/src/css/tabs.module.scss +63 -63
  62. package/src/css/theme-toggle.module.scss +83 -83
  63. package/src/css/toggle.module.scss +54 -54
  64. package/src/css/tooltip.module.scss +97 -97
  65. package/src/css/topnav.module.scss +568 -568
  66. package/src/css/treeselect.module.scss +558 -558
  67. package/src/css/utilities/_borders.scss +111 -111
  68. package/src/css/utilities/_colors.scss +66 -66
  69. package/src/css/utilities/_effects.scss +216 -216
  70. package/src/css/utilities/_layout.scss +181 -181
  71. package/src/css/utilities/_position.scss +75 -75
  72. package/src/css/utilities/_sizing.scss +138 -138
  73. package/src/css/utilities/_spacing.scss +99 -99
  74. package/src/css/utilities/_typography.scss +121 -121
  75. package/src/css/utilities/index.scss +24 -24
  76. package/src/declarations.d.ts +6 -6
  77. 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
+ }