canny-ui 6.7.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/canny-ui.min.css +1 -1
  2. package/dist/table-resize.js +16 -1
  3. package/package.json +1 -1
  4. package/src/_utilities.scss +4 -4
  5. package/src/base/_motion.scss +32 -0
  6. package/src/base/_reset.scss +7 -0
  7. package/src/base/_scrollbar-mixin.scss +84 -0
  8. package/src/base/_touch.scss +60 -0
  9. package/src/canny-ui.scss +5 -0
  10. package/src/components/_accordion.scss +10 -1
  11. package/src/components/_alerts.scss +18 -8
  12. package/src/components/_autocomplete.scss +16 -10
  13. package/src/components/_avatar.scss +22 -1
  14. package/src/components/_breadcrumb.scss +36 -2
  15. package/src/components/_buttons.scss +35 -24
  16. package/src/components/_charts-extra.scss +432 -0
  17. package/src/components/_charts.scss +90 -0
  18. package/src/components/_chip.scss +8 -7
  19. package/src/components/_command-palette.scss +27 -5
  20. package/src/components/_confirm.scss +14 -0
  21. package/src/components/_datatable.scss +383 -53
  22. package/src/components/_datepicker.scss +44 -21
  23. package/src/components/_dialog.scss +12 -0
  24. package/src/components/_drawer.scss +12 -0
  25. package/src/components/_fields.scss +115 -27
  26. package/src/components/_forms.scss +28 -10
  27. package/src/components/_gantt.scss +554 -0
  28. package/src/components/_inputs.scss +12 -7
  29. package/src/components/_list.scss +37 -3
  30. package/src/components/_menu.scss +23 -9
  31. package/src/components/_navbar.scss +98 -7
  32. package/src/components/_pagination.scss +105 -2
  33. package/src/components/_panels.scss +19 -5
  34. package/src/components/_popover.scss +3 -5
  35. package/src/components/_progress.scss +31 -0
  36. package/src/components/_scrollbar.scss +16 -159
  37. package/src/components/_sidebar.scss +24 -20
  38. package/src/components/_skeleton.scss +40 -1
  39. package/src/components/_slider.scss +8 -2
  40. package/src/components/_stepper.scss +22 -4
  41. package/src/components/_table.scss +23 -3
  42. package/src/components/_tabs.scss +120 -4
  43. package/src/components/_timeline.scss +26 -0
  44. package/src/components/_toast.scss +24 -12
  45. package/src/components/_tooltip-radix.scss +25 -4
  46. package/src/components/_tree.scss +51 -3
  47. package/src/components/_uploader.scss +97 -1
  48. package/src/scripts/table-resize.js +16 -1
  49. package/src/tokens/_semantic.scss +85 -21
@@ -1,41 +1,58 @@
1
+ @use '../base/scrollbar-mixin' as sb;
1
2
  @use '../tokens/primitives' as *;
2
3
 
3
4
  // DataTable — server-driven data grid (sorting header, selection, pagination footer).
4
5
 
5
6
  .cny-dt {
7
+ // Elevated card: a border strong enough to read against a near-white page
8
+ // (~3:1), a bright surface and a layered shadow. Header tint is distinct from
9
+ // both the body and the page.
10
+ --cny-dt-border: color-mix(in srgb, var(--text-muted) 70%, var(--border-color-strong));
11
+ --cny-dt-head-bg: color-mix(in srgb, var(--bg-surface-edge) 55%, var(--bg-surface-low));
6
12
  display: flex;
7
13
  flex-direction: column;
8
14
  width: 100%;
9
- border: 1px solid var(--border-color);
15
+ border: 1px solid var(--cny-dt-border);
10
16
  border-radius: $radius-lg;
11
17
  background: var(--bg-surface-lowest);
18
+ box-shadow: var(--shadow-sm), 0 6px 16px -6px rgba(9, 9, 11, 0.12);
12
19
  overflow: hidden;
13
20
  font-family: var(--font-body);
14
21
  }
15
22
 
16
- // horizontal scroll keeps the grid responsive on narrow screens
23
+ // The grid is its own scrollport. With no height from outside it is exactly as
24
+ // tall as its rows; give the table a `height`/`max-height` and the rows scroll
25
+ // inside it while the sticky header stays put — no opt-in needed.
17
26
  .cny-dt-scroll {
27
+ flex: 1 1 auto;
28
+ min-height: 0;
18
29
  width: 100%;
19
- overflow-x: auto;
30
+ overflow: auto;
31
+ @include sb.expand-scrollbar;
32
+
33
+ // a clearly visible thumb on this surface (the shared one is faint at rest)
34
+ &::-webkit-scrollbar-thumb {
35
+ background-color: color-mix(in srgb, var(--text-muted) 55%, transparent);
36
+ }
37
+ &:hover::-webkit-scrollbar-thumb,
38
+ &:focus-within::-webkit-scrollbar-thumb {
39
+ background-color: color-mix(in srgb, var(--text-muted) 80%, transparent);
40
+ }
41
+ &::-webkit-scrollbar-thumb:hover,
42
+ &::-webkit-scrollbar-thumb:active {
43
+ background-color: var(--text-muted);
44
+ }
20
45
  }
21
46
 
22
- // `stickyHeader`: make .cny-dt-scroll the scrollport so the `position: sticky`
23
- // already on `thead th` engages. Without this the grid is as tall as its rows
24
- // and an ancestor does the scrolling, which carries the header off with them.
25
- // Height comes from the consumer — this only says how to divide it.
47
+ // `stickyHeader` is kept for compatibility: the sticky header is now the default.
26
48
  .cny-dt-sticky {
27
49
  min-height: 0;
28
-
29
- .cny-dt-scroll {
30
- flex: 1 1 auto;
31
- min-height: 0;
32
- overflow-y: auto;
33
- }
34
50
  }
35
51
 
36
52
  .cny-dt-table {
37
53
  width: 100%;
38
- border-collapse: collapse;
54
+ border-collapse: separate;
55
+ border-spacing: 0;
39
56
  font-size: 0.875rem;
40
57
  color: var(--text-primary);
41
58
 
@@ -47,17 +64,22 @@
47
64
  white-space: nowrap;
48
65
  }
49
66
 
67
+ td {
68
+ line-height: 1.4;
69
+ }
70
+
50
71
  thead th {
51
72
  position: sticky;
52
73
  top: 0;
53
- z-index: 1;
54
- font-size: 0.75rem;
74
+ z-index: 3;
75
+ padding-top: 10px;
76
+ padding-bottom: 10px;
77
+ font-size: 0.8125rem;
55
78
  font-weight: 600;
56
- color: var(--text-muted);
57
- text-transform: uppercase;
58
- letter-spacing: 0.04em;
59
- background: var(--bg-surface-low);
60
- border-bottom: 1px solid var(--border-color);
79
+ line-height: 1.25;
80
+ color: var(--text-secondary);
81
+ background: var(--cny-dt-head-bg);
82
+ border-bottom: 2px solid var(--cny-dt-border);
61
83
  }
62
84
 
63
85
  tbody td {
@@ -69,20 +91,28 @@
69
91
  }
70
92
  }
71
93
 
72
- .cny-dt-left {
94
+ .cny-dt-table .cny-dt-left {
73
95
  text-align: left;
74
96
  }
75
- .cny-dt-center {
97
+ .cny-dt-table .cny-dt-center {
76
98
  text-align: center;
77
99
  }
78
- .cny-dt-right {
100
+ .cny-dt-table .cny-dt-right {
79
101
  text-align: right;
80
102
  }
81
103
 
82
104
  .cny-dt-th-inner {
83
- display: inline-flex;
105
+ display: flex;
84
106
  align-items: center;
85
107
  gap: 4px;
108
+ min-width: 0;
109
+ }
110
+
111
+ // long headers truncate; the full text is in the cell's title tooltip
112
+ .cny-dt-th-label {
113
+ min-width: 0;
114
+ overflow: hidden;
115
+ text-overflow: ellipsis;
86
116
  }
87
117
 
88
118
  // right-aligned headers push the sort affordance to the correct edge
@@ -95,32 +125,37 @@
95
125
 
96
126
  .cny-dt-sortable {
97
127
  user-select: none;
98
- transition: color 120ms ease;
128
+ cursor: pointer;
129
+ transition: color 120ms ease, background-color 120ms ease;
130
+
131
+ &:hover,
132
+ &:focus-within {
133
+ background: color-mix(in srgb, var(--text-muted) 14%, var(--cny-dt-head-bg));
134
+ color: var(--text-primary);
135
+ }
99
136
 
100
137
  &.is-sorted {
101
- color: var(--color-primary);
138
+ color: var(--text-accent);
139
+ box-shadow: inset 0 -2px 0 var(--color-primary);
102
140
  }
103
141
  }
104
142
 
105
- // keyboard-operable sort control inside sortable header cells
143
+ // keyboard-operable sort control: a real button filling the header cell
106
144
  .cny-dt-sortbtn {
107
145
  appearance: none;
108
- display: inline-flex;
146
+ display: flex;
109
147
  align-items: center;
110
148
  width: 100%;
149
+ min-width: 0;
111
150
  margin: 0;
112
151
  padding: 0;
113
152
  border: 0;
114
153
  background: none;
115
154
  font: inherit;
116
155
  color: inherit;
156
+ text-align: inherit;
117
157
  cursor: pointer;
118
158
  border-radius: var(--radius-xs);
119
- transition: color 120ms ease;
120
-
121
- &:hover {
122
- color: var(--text-primary);
123
- }
124
159
 
125
160
  &:focus-visible {
126
161
  outline: none;
@@ -128,10 +163,26 @@
128
163
  }
129
164
  }
130
165
 
166
+ // neutral up/down chevrons appear on hover / focus; the active direction is solid.
131
167
  .cny-dt-sort-icon {
132
168
  display: inline-flex;
169
+ flex: none;
133
170
  align-items: center;
134
- opacity: 0.5;
171
+ width: 16px;
172
+ justify-content: center;
173
+ color: var(--text-muted);
174
+ opacity: 0;
175
+ transition: opacity 120ms ease;
176
+ }
177
+
178
+ .cny-dt-sortable:hover .cny-dt-sort-icon,
179
+ .cny-dt-sortbtn:focus-visible .cny-dt-sort-icon {
180
+ opacity: 0.8;
181
+ }
182
+
183
+ .cny-dt-sortable.is-sorted .cny-dt-sort-icon {
184
+ opacity: 1;
185
+ color: var(--text-accent);
135
186
  }
136
187
 
137
188
  // drag-to-resize handle on the right edge of each header cell
@@ -139,41 +190,76 @@
139
190
  position: absolute;
140
191
  top: 0;
141
192
  right: 0;
142
- width: 9px;
193
+ width: 14px; // comfortable hit area; the visible line stays 1-2px
143
194
  height: 100%;
144
195
  cursor: col-resize;
145
196
  user-select: none;
146
197
  touch-action: none;
147
198
 
199
+ &:focus-visible {
200
+ outline: none;
201
+ background: color-mix(in srgb, var(--color-primary) 12%, transparent);
202
+ }
203
+
204
+ &:focus-visible::after {
205
+ opacity: 1;
206
+ top: 15%;
207
+ height: 70%;
208
+ width: 2px;
209
+ background: var(--color-primary);
210
+ }
211
+
148
212
  &::after {
149
213
  content: '';
150
214
  position: absolute;
151
- top: 20%;
152
- right: 4px;
153
- width: 2px;
154
- height: 60%;
155
- border-radius: 2px;
156
- background: transparent;
157
- transition: background-color 120ms ease;
215
+ top: 25%;
216
+ right: 6px;
217
+ width: 1px;
218
+ height: 50%;
219
+ background: var(--cny-dt-border);
220
+ opacity: 0;
221
+ transition: opacity 120ms ease, background-color 120ms ease, width 120ms ease;
158
222
  }
159
223
 
160
224
  &:hover::after {
225
+ top: 15%;
226
+ height: 70%;
227
+ width: 2px;
161
228
  background: var(--color-primary);
162
229
  }
163
230
  }
164
231
 
165
- .cny-dt-sortable.is-sorted .cny-dt-sort-icon {
232
+ .cny-dt-table thead th:hover .cny-dt-resizer::after,
233
+ .cny-dt-table thead th:focus-within .cny-dt-resizer::after {
234
+ opacity: 0.8;
235
+ }
236
+ .cny-dt-table thead th .cny-dt-resizer:hover::after {
166
237
  opacity: 1;
167
238
  }
168
239
 
240
+ // while dragging: the handle goes full height and a guide runs down the column
241
+ .cny-dt-table thead th.is-resizing .cny-dt-resizer::after {
242
+ opacity: 1;
243
+ top: 0;
244
+ height: 100%;
245
+ width: 2px;
246
+ background: var(--color-primary);
247
+ }
248
+ .cny-dt-table td.is-resizing {
249
+ box-shadow: inset -2px 0 0 var(--color-primary-soft);
250
+ }
251
+
169
252
  .cny-dt-row {
170
253
  transition: background-color 120ms ease;
171
254
 
255
+ // keep a keyboard-focused row clear of the sticky header
256
+ scroll-margin-top: 44px;
257
+
172
258
  &.is-clickable {
173
259
  cursor: pointer;
174
260
 
175
261
  &:focus-visible {
176
- outline: 2px solid var(--color-primary);
262
+ outline: 2px solid var(--border-focus);
177
263
  outline-offset: -2px;
178
264
  }
179
265
  }
@@ -182,8 +268,30 @@
182
268
  background: var(--bg-surface-low);
183
269
  }
184
270
 
271
+ &:focus-visible {
272
+ outline: 2px solid var(--border-focus);
273
+ outline-offset: -2px;
274
+ }
275
+
185
276
  &.is-selected {
186
277
  background: var(--color-primary-soft);
278
+
279
+ &:hover {
280
+ background: color-mix(in srgb, var(--color-primary) 20%, transparent);
281
+ }
282
+ }
283
+ }
284
+
285
+ .cny-dt {
286
+ --cny-dt-stripe: color-mix(in srgb, var(--bg-surface-low) 60%, var(--bg-surface-lowest));
287
+ }
288
+ // `.is-alt` is set from the row's absolute index, so the stripe stays put when
289
+ // virtualization swaps rows in and out (nth-child would flip on every scroll).
290
+ .cny-dt-striped .cny-dt-table tbody tr.is-alt:not(.is-selected):not(:hover) {
291
+ background: var(--cny-dt-stripe);
292
+
293
+ td.cny-dt-pinned {
294
+ background-color: var(--cny-dt-stripe);
187
295
  }
188
296
  }
189
297
 
@@ -197,30 +305,252 @@
197
305
  }
198
306
  }
199
307
 
200
- .cny-dt-checkbox-cell {
201
- width: 1%;
308
+ .cny-dt-table th.cny-dt-checkbox-cell,
309
+ .cny-dt-table td.cny-dt-checkbox-cell {
310
+ // exactly `checkboxColumnWidth` (set inline) so pinned offsets can rely on it
311
+ padding-left: 0;
312
+ padding-right: 0;
202
313
  white-space: nowrap;
203
314
  text-align: center;
204
315
  }
205
316
 
206
- .cny-dt-empty {
317
+ // the selector must out-rank `.cny-dt-table td { text-align: left }`
318
+ .cny-dt-table td.cny-dt-empty {
207
319
  padding: $space-8 $space-4;
208
320
  text-align: center;
209
321
  color: var(--text-muted);
322
+ white-space: normal;
210
323
  }
211
324
 
212
325
  .cny-dt-footer {
326
+ padding: $space-3 $space-4;
327
+ border-top: 1px solid var(--cny-dt-border);
328
+ background: var(--cny-dt-head-bg);
329
+ }
330
+
331
+ // one Pagination nav carries rows-per-page, range, page buttons and go-to; the
332
+ // range text takes the free space so the page controls sit at the right edge.
333
+ .cny-dt-pager {
334
+ display: flex;
335
+ width: 100%;
336
+ gap: $space-1;
337
+ row-gap: $space-2;
338
+
339
+ .cny-pagination-field {
340
+ margin: 0 $space-3 0 0;
341
+ color: var(--text-secondary);
342
+ }
343
+
344
+ .cny-pagination-range {
345
+ margin: 0 auto 0 0;
346
+ padding-right: $space-3;
347
+ font-weight: 500;
348
+ color: var(--text-primary);
349
+ }
350
+
351
+ .cny-pagination-select,
352
+ .cny-pagination-input {
353
+ border-color: var(--border-input);
354
+ box-shadow: var(--shadow-field);
355
+ }
356
+
357
+ .cny-pagination-control {
358
+ min-width: 32px;
359
+ color: var(--text-primary);
360
+ background: var(--bg-surface-lowest);
361
+ border-color: var(--border-input);
362
+
363
+ &:hover:not(:disabled) {
364
+ background: var(--bg-surface-low);
365
+ border-color: var(--border-input-hover);
366
+ }
367
+ &:disabled {
368
+ opacity: 0.45;
369
+ }
370
+ }
371
+
372
+ .cny-pagination-btn:not(.cny-pagination-control) {
373
+ color: var(--text-primary);
374
+ &.is-active {
375
+ color: var(--color-primary-on);
376
+ font-weight: 600;
377
+ }
378
+ }
379
+
380
+ .cny-pagination-field:last-child {
381
+ margin: 0 0 0 $space-3;
382
+ }
383
+ }
384
+
385
+ @media (max-width: 480px) {
386
+ .cny-dt-pager {
387
+ justify-content: center;
388
+ .cny-pagination-range {
389
+ flex: 1 0 100%;
390
+ margin: 0;
391
+ padding: 0;
392
+ text-align: center;
393
+ }
394
+ }
395
+ }
396
+
397
+ .cny-dt-count {
398
+ font-size: 0.8125rem;
399
+ color: var(--text-muted);
400
+ }
401
+
402
+ // Pinned columns — `position: sticky` + inline left/right offsets from the
403
+ // component. Cells need an opaque background so scrolled content does not show
404
+ // through; row hover/selected tints are re-applied on top.
405
+ .cny-dt-table {
406
+ td.cny-dt-pinned,
407
+ th.cny-dt-pinned {
408
+ position: sticky;
409
+ z-index: 1;
410
+ background: var(--bg-surface-lowest);
411
+ }
412
+ thead th.cny-dt-pinned {
413
+ z-index: 4;
414
+ background: var(--cny-dt-head-bg);
415
+ }
416
+ .cny-dt-pin-left.cny-dt-checkbox-cell {
417
+ left: 0;
418
+ }
419
+ .cny-dt-pin-left.is-pin-edge {
420
+ box-shadow: 1px 0 0 var(--border-color);
421
+ }
422
+ .cny-dt-pin-right.is-pin-edge {
423
+ box-shadow: -1px 0 0 var(--border-color);
424
+ }
425
+ // Row tints are translucent, so a pinned cell keeps an opaque base underneath
426
+ // (background-color) and the tint is layered on top (background-image) -
427
+ // scrolled-under text can never bleed through.
428
+ tbody tr:hover td.cny-dt-pinned {
429
+ background-color: var(--bg-surface-low);
430
+ }
431
+ tbody tr.is-selected td.cny-dt-pinned {
432
+ background-color: var(--bg-surface-lowest);
433
+ background-image: linear-gradient(var(--color-primary-soft), var(--color-primary-soft));
434
+ }
435
+ tbody tr.is-selected:hover td.cny-dt-pinned {
436
+ background-image: linear-gradient(
437
+ color-mix(in srgb, var(--color-primary) 20%, transparent),
438
+ color-mix(in srgb, var(--color-primary) 20%, transparent)
439
+ );
440
+ }
441
+ }
442
+
443
+ .cny-dt-toolbar {
213
444
  display: flex;
214
445
  flex-wrap: wrap;
215
446
  align-items: center;
216
- justify-content: space-between;
447
+ justify-content: flex-end;
217
448
  gap: $space-2;
218
449
  padding: $space-2 $space-3;
219
- border-top: 1px solid var(--border-color);
220
- background: var(--bg-surface-low);
450
+ border-bottom: 1px solid var(--border-color-strong);
451
+ background: var(--bg-surface-lowest);
221
452
  }
222
453
 
223
- .cny-dt-count {
454
+ // consumer content (search, filters) takes the left; the Columns button stays right
455
+ .cny-dt-toolbar-slot {
456
+ display: flex;
457
+ flex: 1 1 auto;
458
+ flex-wrap: wrap;
459
+ align-items: center;
460
+ gap: $space-2;
461
+ min-width: 0;
462
+ }
463
+
464
+ // "3 selected · Clear" - always in the layout, so selecting a row never shifts the table
465
+ .cny-dt-selbar {
466
+ min-height: 37px;
467
+ box-sizing: border-box;
468
+ display: flex;
469
+ align-items: center;
470
+ gap: $space-3;
471
+ padding: $space-2 $space-4;
224
472
  font-size: 0.8125rem;
225
- color: var(--text-muted);
473
+ color: var(--text-primary);
474
+ background: var(--color-primary-soft);
475
+ border-bottom: 1px solid var(--cny-dt-border);
476
+ }
477
+
478
+ .cny-dt-selbar-count {
479
+ font-weight: 600;
480
+ }
481
+
482
+ .cny-dt-selbar.is-empty {
483
+ background: var(--bg-surface-lowest);
484
+
485
+ .cny-dt-selbar-count {
486
+ font-weight: 400;
487
+ color: var(--text-muted);
488
+ }
489
+ }
490
+
491
+ // Fixed column layout: cells clip with an ellipsis (the full text is in `title`)
492
+ .cny-dt-table.is-fixed {
493
+ td {
494
+ overflow: hidden;
495
+ text-overflow: ellipsis;
496
+ }
497
+ }
498
+
499
+ // Virtualized rows have a fixed height; spacer rows stand in for the rows
500
+ // that are not in the DOM.
501
+ .cny-dt-virtual .cny-dt-table tbody tr.cny-dt-row > td {
502
+ height: var(--cny-dt-row-h, 45px);
503
+ box-sizing: border-box;
504
+ overflow: hidden;
505
+ text-overflow: ellipsis;
506
+ }
507
+ .cny-dt-table tbody tr.cny-dt-spacer > td {
508
+ padding: 0;
509
+ border: 0;
510
+ }
511
+
512
+ .cny-dt-colmenu {
513
+ display: flex;
514
+ flex-direction: column;
515
+ gap: $space-2;
516
+ min-width: 180px;
517
+ max-height: 320px;
518
+ overflow-y: auto;
519
+ }
520
+
521
+ // "all N rows on this page are selected" banner
522
+ .cny-dt-banner {
523
+ display: flex;
524
+ flex-wrap: wrap;
525
+ align-items: center;
526
+ justify-content: center;
527
+ gap: $space-2;
528
+ padding: $space-2 $space-3;
529
+ font-size: 0.8125rem;
530
+ color: var(--text-primary);
531
+ background: var(--color-primary-soft);
532
+ border-bottom: 1px solid var(--cny-dt-border);
533
+ }
534
+
535
+ .cny-dt-banner-btn {
536
+ appearance: none;
537
+ padding: 0;
538
+ border: 0;
539
+ background: none;
540
+ font: inherit;
541
+ font-weight: 600;
542
+ color: var(--text-accent);
543
+ cursor: pointer;
544
+ text-decoration: underline;
545
+
546
+ &:focus-visible {
547
+ outline: 2px solid var(--border-focus);
548
+ outline-offset: 2px;
549
+ border-radius: var(--radius-xs);
550
+ }
551
+ }
552
+
553
+ .cny-dt-empty-title {
554
+ display: block;
555
+ margin-bottom: $space-3;
226
556
  }