canny-ui 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.
@@ -0,0 +1,440 @@
1
+ @use '../tokens/primitives' as *;
2
+
3
+ // ── Field wrapper ────────────────────────────────────────────────────────
4
+ // Consumed by the React <Field> primitive. Groups label + required asterisk
5
+ // + helper/error text + character counter around an input-shaped control.
6
+
7
+ .cny-field {
8
+ display: flex;
9
+ flex-direction: column;
10
+ gap: $space-2;
11
+ }
12
+
13
+ .cny-field-label-row {
14
+ display: flex;
15
+ align-items: baseline;
16
+ justify-content: space-between;
17
+ gap: $space-2;
18
+ }
19
+
20
+ .cny-field-label {
21
+ display: inline-flex;
22
+ align-items: center;
23
+ gap: 4px;
24
+ font-family: var(--font-body);
25
+ font-size: 0.8125rem;
26
+ font-weight: 500;
27
+ color: var(--text-secondary);
28
+
29
+ // Required asterisk
30
+ .cny-field-required {
31
+ color: var(--color-danger);
32
+ font-weight: 500;
33
+ }
34
+
35
+ // Optional hint tag
36
+ .cny-field-optional {
37
+ font-size: 0.6875rem;
38
+ font-weight: 400;
39
+ color: var(--text-muted);
40
+ padding: 1px 6px;
41
+ border-radius: var(--radius-full);
42
+ background: var(--bg-surface-low);
43
+ }
44
+ }
45
+
46
+ .cny-field-counter {
47
+ font-family: var(--font-mono);
48
+ font-size: 0.6875rem;
49
+ color: var(--text-muted);
50
+ font-variant-numeric: tabular-nums;
51
+
52
+ &.danger { color: var(--color-danger); }
53
+ }
54
+
55
+ .cny-field-hint {
56
+ font-size: 0.75rem;
57
+ color: var(--text-muted);
58
+ line-height: 1.5;
59
+
60
+ &.danger { color: var(--color-danger); }
61
+ &.warning { color: var(--color-warning); }
62
+ &.success { color: var(--color-success); }
63
+ }
64
+
65
+ // ── Input affix container ───────────────────────────────────────────────
66
+ // Wraps .input so that prefix/suffix icons or action buttons can sit
67
+ // inside the border. The actual <input> has its paddings adjusted.
68
+
69
+ .cny-input-affix {
70
+ position: relative;
71
+ display: flex;
72
+ align-items: stretch;
73
+ width: 100%;
74
+
75
+ // Prefix / suffix icons
76
+ .cny-input-prefix,
77
+ .cny-input-suffix {
78
+ position: absolute;
79
+ top: 50%;
80
+ transform: translateY(-50%);
81
+ display: inline-flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ color: var(--text-muted);
85
+ pointer-events: none;
86
+ }
87
+ .cny-input-prefix { left: $space-3; }
88
+ .cny-input-suffix { right: $space-3; }
89
+
90
+ // Interactive suffix (clear button, password toggle)
91
+ .cny-input-action {
92
+ position: absolute;
93
+ top: 50%;
94
+ transform: translateY(-50%);
95
+ right: 6px;
96
+ display: inline-flex;
97
+ align-items: center;
98
+ justify-content: center;
99
+ width: 24px;
100
+ height: 24px;
101
+ border: 0;
102
+ border-radius: var(--radius-xs);
103
+ background: transparent;
104
+ color: var(--text-muted);
105
+ cursor: pointer;
106
+ transition: background-color 120ms, color 120ms;
107
+
108
+ &:hover { background: var(--bg-surface-low); color: var(--text-primary); }
109
+ &:focus-visible {
110
+ outline: none;
111
+ box-shadow: 0 0 0 2px var(--ring-color);
112
+ }
113
+ }
114
+
115
+ // Adjust input padding when affix slots are present
116
+ &.has-prefix > .input { padding-left: calc(#{$space-3} + 18px + 6px); }
117
+ &.has-suffix > .input { padding-right: calc(#{$space-3} + 18px + 6px); }
118
+ &.has-action > .input { padding-right: 32px; }
119
+ }
120
+
121
+ // ── Select (Radix Select trigger + content) ─────────────────────────────
122
+ // Styled to match .input. The portalled content panel uses the surface
123
+ // tokens and the same motion as our menus.
124
+
125
+ .cny-select-trigger {
126
+ display: inline-flex;
127
+ align-items: center;
128
+ justify-content: space-between;
129
+ width: 100%;
130
+ min-height: 36px;
131
+ gap: $space-2;
132
+ padding: 0 $space-3;
133
+ background: var(--bg-surface-lowest);
134
+ color: var(--text-primary);
135
+ border: 1px solid var(--border-color);
136
+ border-radius: var(--radius-sm);
137
+ font-family: var(--font-body);
138
+ font-size: 0.875rem;
139
+ line-height: 1;
140
+ cursor: pointer;
141
+ transition:
142
+ border-color var(--transition-fast),
143
+ box-shadow var(--transition-fast);
144
+
145
+ &[data-placeholder] { color: var(--text-muted); }
146
+
147
+ &:hover:not([data-disabled]) { border-color: var(--border-color-strong); }
148
+
149
+ &:focus-visible,
150
+ &[data-state='open'] {
151
+ outline: none;
152
+ border-color: var(--border-focus);
153
+ box-shadow: none;
154
+ }
155
+
156
+ &[data-disabled] {
157
+ background: var(--bg-surface-low);
158
+ color: var(--text-disabled);
159
+ cursor: not-allowed;
160
+ }
161
+
162
+ &.sm { min-height: 28px; font-size: 0.8125rem; padding: 0 $space-2; }
163
+ &.lg { min-height: 44px; font-size: 0.9375rem; padding: 0 $space-4; }
164
+
165
+ &.danger { border-color: var(--color-danger);
166
+ &:focus-visible, &[data-state='open'] { border-color: var(--color-danger); box-shadow: none; }
167
+ }
168
+ &.warning { border-color: var(--color-warning);
169
+ &:focus-visible, &[data-state='open'] { border-color: var(--color-warning); box-shadow: none; }
170
+ }
171
+ &.success { border-color: var(--color-success);
172
+ &:focus-visible, &[data-state='open'] { border-color: var(--color-success); box-shadow: none; }
173
+ }
174
+ }
175
+
176
+ .cny-select-chevron {
177
+ color: var(--text-muted);
178
+ flex-shrink: 0;
179
+ transition: transform 150ms var(--ease-standard);
180
+ [data-state='open'] & { transform: rotate(180deg); }
181
+ }
182
+
183
+ .cny-select-content {
184
+ z-index: 60;
185
+ min-width: var(--radix-select-trigger-width);
186
+ max-height: var(--radix-select-content-available-height);
187
+ padding: 4px;
188
+ border-radius: var(--radius-md);
189
+ background: var(--bg-surface-lowest);
190
+ box-shadow: var(--shadow-lg), 0 0 0 1px var(--border-color);
191
+ overflow: hidden;
192
+ animation: cny-select-pop 160ms cubic-bezier(0.2, 0, 0, 1);
193
+ }
194
+
195
+ @keyframes cny-select-pop {
196
+ from { opacity: 0; transform: translateY(-4px) scale(0.98); }
197
+ to { opacity: 1; transform: translateY(0) scale(1); }
198
+ }
199
+
200
+ .cny-select-viewport { padding: 2px; }
201
+
202
+ .cny-select-item {
203
+ position: relative;
204
+ display: flex;
205
+ align-items: center;
206
+ gap: $space-2;
207
+ padding: 6px 28px 6px 10px;
208
+ border-radius: var(--radius-sm);
209
+ font-size: 0.875rem;
210
+ color: var(--text-primary);
211
+ cursor: pointer;
212
+ user-select: none;
213
+
214
+ &[data-highlighted] {
215
+ outline: none;
216
+ background: var(--bg-surface-low);
217
+ }
218
+ &[data-state='checked'] {
219
+ color: var(--color-primary);
220
+ font-weight: 500;
221
+ }
222
+ &[data-disabled] {
223
+ color: var(--text-disabled);
224
+ cursor: not-allowed;
225
+ }
226
+ }
227
+
228
+ .cny-select-indicator {
229
+ position: absolute;
230
+ right: 8px;
231
+ display: inline-flex;
232
+ color: var(--color-primary);
233
+ }
234
+
235
+ .cny-select-group-label {
236
+ padding: 6px 10px 4px;
237
+ font-size: 0.6875rem;
238
+ font-weight: 600;
239
+ text-transform: uppercase;
240
+ letter-spacing: 0.08em;
241
+ color: var(--text-muted);
242
+ }
243
+
244
+ .cny-select-separator {
245
+ height: 1px;
246
+ background: var(--hairline-color);
247
+ margin: 4px 0;
248
+ }
249
+
250
+ // ── MultiSelect (combobox trigger, cmdk dropdown, chip tokens) ──────────
251
+
252
+ .cny-multiselect-trigger {
253
+ display: inline-flex;
254
+ align-items: center;
255
+ flex-wrap: wrap;
256
+ gap: 4px;
257
+ width: 100%;
258
+ min-height: 36px;
259
+ padding: 3px 32px 3px 6px;
260
+ background: var(--bg-surface-lowest);
261
+ color: var(--text-primary);
262
+ border: 1px solid var(--border-color);
263
+ border-radius: var(--radius-sm);
264
+ font-family: var(--font-body);
265
+ font-size: 0.875rem;
266
+ cursor: text;
267
+ position: relative;
268
+ transition:
269
+ border-color var(--transition-fast),
270
+ box-shadow var(--transition-fast);
271
+
272
+ &:hover:not([data-disabled]) { border-color: var(--border-color-strong); }
273
+
274
+ &[data-state='open'],
275
+ &:focus-within {
276
+ border-color: var(--border-focus);
277
+ box-shadow: none;
278
+ }
279
+
280
+ &[data-disabled] {
281
+ background: var(--bg-surface-low);
282
+ color: var(--text-disabled);
283
+ cursor: not-allowed;
284
+ }
285
+
286
+ &.sm { min-height: 28px; font-size: 0.8125rem; }
287
+ &.lg { min-height: 44px; font-size: 0.9375rem; }
288
+
289
+ &.danger { border-color: var(--color-danger);
290
+ &:focus-within, &[data-state='open'] { border-color: var(--color-danger); box-shadow: none; }
291
+ }
292
+ &.warning { border-color: var(--color-warning);
293
+ &:focus-within, &[data-state='open'] { border-color: var(--color-warning); box-shadow: none; }
294
+ }
295
+ &.success { border-color: var(--color-success);
296
+ &:focus-within, &[data-state='open'] { border-color: var(--color-success); box-shadow: none; }
297
+ }
298
+
299
+ .cny-multiselect-placeholder {
300
+ color: var(--text-muted);
301
+ padding: 4px 6px;
302
+ }
303
+
304
+ .cny-multiselect-input {
305
+ flex: 1 1 60px;
306
+ min-width: 60px;
307
+ border: 0;
308
+ outline: none;
309
+ background: transparent;
310
+ color: inherit;
311
+ font: inherit;
312
+ padding: 4px 6px;
313
+ }
314
+
315
+ .cny-multiselect-chevron {
316
+ position: absolute;
317
+ right: 8px;
318
+ top: 50%;
319
+ transform: translateY(-50%);
320
+ color: var(--text-muted);
321
+ pointer-events: none;
322
+ transition: transform 150ms var(--ease-standard);
323
+ }
324
+ &[data-state='open'] .cny-multiselect-chevron { transform: translateY(-50%) rotate(180deg); }
325
+ }
326
+
327
+ // Chip tokens
328
+ .cny-chip {
329
+ display: inline-flex;
330
+ align-items: center;
331
+ gap: 4px;
332
+ padding: 2px 4px 2px 8px;
333
+ border-radius: var(--radius-sm);
334
+ background: var(--color-primary-soft);
335
+ color: var(--color-primary);
336
+ font-size: 0.8125rem;
337
+ line-height: 1.3;
338
+ max-width: 100%;
339
+
340
+ .cny-chip-label {
341
+ overflow: hidden;
342
+ text-overflow: ellipsis;
343
+ white-space: nowrap;
344
+ }
345
+
346
+ .cny-chip-remove {
347
+ display: inline-flex;
348
+ align-items: center;
349
+ justify-content: center;
350
+ width: 16px;
351
+ height: 16px;
352
+ border: 0;
353
+ border-radius: 3px;
354
+ background: transparent;
355
+ color: inherit;
356
+ cursor: pointer;
357
+ opacity: 0.7;
358
+
359
+ &:hover { opacity: 1; background: rgba(255, 255, 255, 0.4); }
360
+ &:focus-visible {
361
+ outline: none;
362
+ box-shadow: 0 0 0 2px currentColor;
363
+ }
364
+ }
365
+
366
+ &.danger { background: var(--color-danger-soft); color: var(--color-danger); }
367
+ &.warning { background: var(--color-warning-soft); color: var(--color-warning); }
368
+ &.success { background: var(--color-success-soft); color: var(--color-success); }
369
+ &.neutral { background: var(--bg-surface-low); color: var(--text-secondary); }
370
+ }
371
+
372
+ // Popover panel for the combobox list (shared with date pickers etc.)
373
+ .cny-popover {
374
+ z-index: 60;
375
+ min-width: var(--radix-popover-trigger-width);
376
+ max-height: min(320px, var(--radix-popover-content-available-height));
377
+ padding: 4px;
378
+ border-radius: var(--radius-md);
379
+ background: var(--bg-surface-lowest);
380
+ box-shadow: var(--shadow-lg), 0 0 0 1px var(--border-color);
381
+ overflow: hidden;
382
+ animation: cny-select-pop 160ms cubic-bezier(0.2, 0, 0, 1);
383
+
384
+ [cmdk-list] { max-height: 280px; overflow-y: auto; scrollbar-width: thin; padding: 2px; }
385
+ [cmdk-input] {
386
+ width: 100%;
387
+ margin: 2px 0 4px;
388
+ padding: 6px 10px;
389
+ border: 0;
390
+ outline: none;
391
+ background: transparent;
392
+ color: var(--text-primary);
393
+ font-size: 0.875rem;
394
+ border-bottom: 1px solid var(--hairline-color);
395
+ }
396
+ [cmdk-input]::placeholder { color: var(--text-muted); }
397
+
398
+ [cmdk-item] {
399
+ display: flex;
400
+ align-items: center;
401
+ gap: $space-2;
402
+ padding: 6px 10px;
403
+ border-radius: var(--radius-sm);
404
+ font-size: 0.875rem;
405
+ color: var(--text-primary);
406
+ cursor: pointer;
407
+ user-select: none;
408
+
409
+ &[data-selected='true'] { background: var(--bg-surface-low); }
410
+
411
+ .cny-check {
412
+ margin-left: auto;
413
+ color: var(--color-primary);
414
+ }
415
+ }
416
+
417
+ [cmdk-empty] {
418
+ padding: 20px 12px;
419
+ text-align: center;
420
+ color: var(--text-muted);
421
+ font-size: 0.875rem;
422
+ }
423
+
424
+ [cmdk-group-heading] {
425
+ padding: 6px 10px 4px;
426
+ font-size: 0.6875rem;
427
+ font-weight: 600;
428
+ text-transform: uppercase;
429
+ letter-spacing: 0.08em;
430
+ color: var(--text-muted);
431
+ }
432
+ }
433
+
434
+ // Auto-grow textarea — consumers toggle rows via JS; our styles keep things clean
435
+ .cny-textarea-autogrow {
436
+ display: block;
437
+ width: 100%;
438
+ resize: none;
439
+ overflow: hidden;
440
+ }
@@ -0,0 +1,158 @@
1
+ @use '../tokens/primitives' as *;
2
+
3
+ // Form primitives — checkbox, radio, switch, select.
4
+ // All use the native element with class-based styling (no JS required).
5
+
6
+ // ── Checkbox & Radio ─────────────────────────────────────────────────────
7
+ .checkbox,
8
+ .radio {
9
+ appearance: none;
10
+ -webkit-appearance: none;
11
+ display: inline-block;
12
+ width: 16px;
13
+ height: 16px;
14
+ margin: 0;
15
+ padding: 0;
16
+ background: var(--bg-surface-lowest);
17
+ border: 1px solid var(--border-color-strong);
18
+ cursor: pointer;
19
+ vertical-align: middle;
20
+ transition:
21
+ background-color var(--transition-fast),
22
+ border-color var(--transition-fast),
23
+ box-shadow var(--transition-fast);
24
+
25
+ &:hover:not(:disabled) { border-color: var(--color-primary); }
26
+ &:focus-visible {
27
+ outline: none;
28
+ box-shadow: 0 0 0 var(--ring-width) var(--ring-color);
29
+ }
30
+ &:disabled { opacity: 0.5; cursor: not-allowed; }
31
+ }
32
+
33
+ .checkbox {
34
+ border-radius: var(--radius-xs);
35
+
36
+ &:checked {
37
+ background-color: var(--color-primary);
38
+ border-color: var(--color-primary);
39
+ background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none'><path d='M3.5 8.5L6.5 11.5L12.5 5' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/></svg>");
40
+ background-repeat: no-repeat;
41
+ background-position: center;
42
+ }
43
+
44
+ &:indeterminate {
45
+ background-color: var(--color-primary);
46
+ border-color: var(--color-primary);
47
+ background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><rect x='3' y='7' width='10' height='2' fill='white' rx='1'/></svg>");
48
+ background-repeat: no-repeat;
49
+ background-position: center;
50
+ }
51
+ }
52
+
53
+ .radio {
54
+ border-radius: 50%;
55
+
56
+ &:checked {
57
+ border-color: var(--color-primary);
58
+ background-color: var(--bg-surface-lowest);
59
+ background-image: radial-gradient(circle, var(--color-primary) 0 4px, transparent 5px);
60
+ }
61
+ }
62
+
63
+ // Label wrapper: <label class="control"><input class="checkbox"/> Label</label>
64
+ .control {
65
+ display: inline-flex;
66
+ align-items: center;
67
+ gap: $space-2;
68
+ font-family: var(--font-body);
69
+ font-size: 0.875rem;
70
+ color: var(--text-primary);
71
+ cursor: pointer;
72
+ user-select: none;
73
+
74
+ &:has(:disabled) { cursor: not-allowed; color: var(--text-disabled); }
75
+ }
76
+
77
+ // ── Switch ──────────────────────────────────────────────────────────────
78
+ .switch {
79
+ appearance: none;
80
+ -webkit-appearance: none;
81
+ position: relative;
82
+ width: 32px;
83
+ height: 18px;
84
+ margin: 0;
85
+ padding: 0;
86
+ background: var(--border-color-strong);
87
+ border-radius: var(--radius-full);
88
+ cursor: pointer;
89
+ vertical-align: middle;
90
+ transition: background-color var(--transition-fast);
91
+
92
+ &::before {
93
+ content: '';
94
+ position: absolute;
95
+ top: 2px;
96
+ left: 2px;
97
+ width: 14px;
98
+ height: 14px;
99
+ background: #fff;
100
+ border-radius: 50%;
101
+ box-shadow: var(--shadow-xs);
102
+ transition: transform var(--transition-fast);
103
+ }
104
+
105
+ &:checked {
106
+ background: var(--color-primary);
107
+ &::before { transform: translateX(14px); }
108
+ }
109
+
110
+ &:focus-visible {
111
+ outline: none;
112
+ box-shadow: 0 0 0 var(--ring-width) var(--ring-color);
113
+ }
114
+
115
+ &:disabled { opacity: 0.5; cursor: not-allowed; }
116
+ }
117
+
118
+ // ── Select ──────────────────────────────────────────────────────────────
119
+ .select {
120
+ appearance: none;
121
+ -webkit-appearance: none;
122
+ display: block;
123
+ width: 100%;
124
+ min-height: 36px;
125
+ padding: 0 $space-8 0 $space-3;
126
+ background: var(--bg-surface-lowest);
127
+ color: var(--text-primary);
128
+ border: 1px solid var(--border-color);
129
+ border-radius: var(--radius-sm);
130
+ font-family: var(--font-body);
131
+ font-size: 0.875rem;
132
+ line-height: 1.4;
133
+ cursor: pointer;
134
+ background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none'><path d='M4 6l4 4 4-4' stroke='%2371717a' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
135
+ background-repeat: no-repeat;
136
+ background-position: right $space-3 center;
137
+ background-size: 16px;
138
+ transition:
139
+ border-color var(--transition-fast),
140
+ box-shadow var(--transition-fast);
141
+
142
+ &:hover:not(:disabled):not(:focus) { border-color: var(--border-color-strong); }
143
+
144
+ &:focus {
145
+ outline: none;
146
+ border-color: var(--border-focus);
147
+ box-shadow: none;
148
+ }
149
+
150
+ &:disabled {
151
+ background-color: var(--bg-surface-low);
152
+ color: var(--text-disabled);
153
+ cursor: not-allowed;
154
+ }
155
+
156
+ &.sm { min-height: 28px; font-size: 0.8125rem; padding: 0 $space-6 0 $space-2; }
157
+ &.lg { min-height: 44px; font-size: 0.9375rem; padding: 0 $space-10 0 $space-4; }
158
+ }
@@ -0,0 +1,90 @@
1
+ @use '../tokens/primitives' as *;
2
+
3
+ // Enterprise input — full real border, focus ring, predictable affordances.
4
+
5
+ .input {
6
+ display: block;
7
+ width: 100%;
8
+ min-height: 36px;
9
+ padding: 0 $space-3;
10
+ background: var(--bg-surface-lowest);
11
+ color: var(--text-primary);
12
+ border: 1px solid var(--border-color);
13
+ border-radius: var(--radius-sm);
14
+ outline: none;
15
+ font-family: var(--font-body);
16
+ font-size: 0.875rem;
17
+ line-height: 1.4;
18
+ transition:
19
+ border-color var(--transition-fast),
20
+ box-shadow var(--transition-fast);
21
+
22
+ &::placeholder {
23
+ color: var(--text-muted);
24
+ }
25
+
26
+ &:hover:not(:disabled):not(:focus) {
27
+ border-color: var(--border-color-strong);
28
+ }
29
+
30
+ &:focus {
31
+ border-color: var(--border-focus);
32
+ box-shadow: none;
33
+ }
34
+
35
+ &:disabled {
36
+ background: var(--bg-surface-low);
37
+ color: var(--text-disabled);
38
+ cursor: not-allowed;
39
+ }
40
+
41
+ // Sizes
42
+ &.sm { min-height: 28px; font-size: 0.8125rem; padding: 0 $space-2; }
43
+ &.lg { min-height: 44px; font-size: 0.9375rem; padding: 0 $space-4; }
44
+
45
+ // States
46
+ &.danger {
47
+ border-color: var(--color-danger);
48
+ &:focus { border-color: var(--color-danger); box-shadow: none; }
49
+ }
50
+
51
+ &.warning {
52
+ border-color: var(--color-warning);
53
+ &:focus { border-color: var(--color-warning); box-shadow: none; }
54
+ }
55
+
56
+ &.success {
57
+ border-color: var(--color-success);
58
+ &:focus { border-color: var(--color-success); box-shadow: none; }
59
+ }
60
+ }
61
+
62
+ // Textarea inherits .input shape
63
+ textarea.input {
64
+ min-height: 88px;
65
+ padding: $space-2 $space-3;
66
+ line-height: 1.55;
67
+ resize: vertical;
68
+ }
69
+
70
+ // Label — small, uppercase-neutral, sits above the input
71
+ .input-label {
72
+ display: block;
73
+ font-family: var(--font-body);
74
+ font-size: 0.8125rem;
75
+ font-weight: 500;
76
+ color: var(--text-secondary);
77
+ margin-bottom: $space-2;
78
+ }
79
+
80
+ // Helper text / error caption
81
+ .input-hint {
82
+ display: block;
83
+ font-size: 0.75rem;
84
+ color: var(--text-muted);
85
+ margin-top: $space-2;
86
+
87
+ &.danger { color: var(--color-danger); }
88
+ &.warning { color: var(--color-warning); }
89
+ &.success { color: var(--color-success); }
90
+ }