canny-ui 2.0.0 → 4.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,148 @@
1
+ @use '../tokens/primitives' as *;
2
+
3
+ // Tabs — three variants: line (default), pills, segmented.
4
+
5
+ .cny-tabs {
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: 16px;
9
+
10
+ &[data-orientation='vertical'] {
11
+ flex-direction: row;
12
+ align-items: flex-start;
13
+ gap: 24px;
14
+ }
15
+ }
16
+
17
+ // ── List ──────────────────────────────────────────────────────
18
+ .cny-tabs-list {
19
+ display: inline-flex;
20
+ align-items: center;
21
+ position: relative;
22
+ flex-wrap: wrap;
23
+
24
+ .cny-tabs[data-orientation='vertical'] & {
25
+ flex-direction: column;
26
+ align-items: stretch;
27
+ flex-wrap: nowrap;
28
+ }
29
+ }
30
+
31
+ // ── Trigger ───────────────────────────────────────────────────
32
+ .cny-tabs-trigger {
33
+ display: inline-flex;
34
+ align-items: center;
35
+ gap: 8px;
36
+ background: transparent;
37
+ border: 0;
38
+ font-family: var(--font-body);
39
+ font-size: 0.875rem;
40
+ font-weight: 550;
41
+ color: var(--text-muted);
42
+ cursor: pointer;
43
+ white-space: nowrap;
44
+ transition: color 140ms ease, background-color 140ms ease, box-shadow 140ms ease;
45
+
46
+ &-icon { display: inline-flex; align-items: center; }
47
+ &-badge { display: inline-flex; align-items: center; }
48
+
49
+ &:hover { color: var(--text-secondary); }
50
+ &:focus-visible {
51
+ outline: 2px solid color-mix(in srgb, var(--color-primary) 60%, transparent);
52
+ outline-offset: 2px;
53
+ border-radius: var(--radius-xs);
54
+ }
55
+ &:disabled,
56
+ &[data-disabled] { opacity: 0.5; cursor: not-allowed; }
57
+ }
58
+
59
+ // ── Sizes ─────────────────────────────────────────────────────
60
+ .cny-tabs.size-sm .cny-tabs-trigger { font-size: 0.8125rem; }
61
+ .cny-tabs.size-lg .cny-tabs-trigger { font-size: 0.9375rem; }
62
+
63
+ // ── Variant: line (underline) ─────────────────────────────────
64
+ .cny-tabs.variant-line {
65
+ .cny-tabs-list {
66
+ border-bottom: 1px solid var(--hairline-color);
67
+ gap: 4px;
68
+ }
69
+ .cny-tabs-trigger {
70
+ padding: 10px 14px;
71
+ margin-bottom: -1px;
72
+ border-bottom: 2px solid transparent;
73
+
74
+ &[data-state='active'] {
75
+ color: var(--color-primary);
76
+ border-bottom-color: var(--color-primary);
77
+ }
78
+ }
79
+
80
+ &.size-sm .cny-tabs-trigger { padding: 7px 10px; }
81
+ &.size-lg .cny-tabs-trigger { padding: 13px 18px; }
82
+ }
83
+
84
+ .cny-tabs.variant-line[data-orientation='vertical'] {
85
+ .cny-tabs-list { border-bottom: 0; border-right: 1px solid var(--hairline-color); }
86
+ .cny-tabs-trigger {
87
+ margin-bottom: 0;
88
+ margin-right: -1px;
89
+ border-bottom: 0;
90
+ border-right: 2px solid transparent;
91
+ justify-content: flex-start;
92
+ text-align: left;
93
+ &[data-state='active'] { border-right-color: var(--color-primary); border-bottom: 0; }
94
+ }
95
+ }
96
+
97
+ // ── Variant: pills ────────────────────────────────────────────
98
+ .cny-tabs.variant-pills {
99
+ .cny-tabs-list { gap: 4px; }
100
+ .cny-tabs-trigger {
101
+ padding: 7px 12px;
102
+ border-radius: 999px;
103
+
104
+ &:hover { background: var(--bg-surface-low); }
105
+ &[data-state='active'] {
106
+ color: var(--color-primary);
107
+ background: color-mix(in srgb, var(--color-primary) 12%, transparent);
108
+ }
109
+ }
110
+ &.size-sm .cny-tabs-trigger { padding: 5px 10px; }
111
+ &.size-lg .cny-tabs-trigger { padding: 9px 14px; }
112
+ }
113
+
114
+ // ── Variant: segmented ────────────────────────────────────────
115
+ .cny-tabs.variant-segmented {
116
+ .cny-tabs-list {
117
+ padding: 4px;
118
+ background: var(--bg-surface-low);
119
+ border: 1px solid var(--hairline-color);
120
+ border-radius: 999px;
121
+ gap: 2px;
122
+ }
123
+ .cny-tabs-trigger {
124
+ padding: 6px 14px;
125
+ border-radius: 999px;
126
+
127
+ &:hover { color: var(--text-primary); }
128
+ &[data-state='active'] {
129
+ color: var(--text-primary);
130
+ background: var(--bg-surface-lowest);
131
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06), inset 0 0 0 1px var(--hairline-color);
132
+ }
133
+ }
134
+ &.size-sm .cny-tabs-trigger { padding: 4px 10px; }
135
+ &.size-lg .cny-tabs-trigger { padding: 8px 18px; }
136
+ }
137
+
138
+ // ── Content panel ─────────────────────────────────────────────
139
+ .cny-tabs-content {
140
+ outline: none;
141
+ flex: 1 1 auto;
142
+
143
+ &:focus-visible {
144
+ outline: 2px solid var(--color-primary);
145
+ outline-offset: 2px;
146
+ border-radius: var(--radius-xs);
147
+ }
148
+ }
@@ -0,0 +1,368 @@
1
+ @use '../tokens/primitives' as *;
2
+
3
+ // Timeline — activity / history rail.
4
+ // Layouts: position right (default) / left / alternate, plus horizontal orientation.
5
+ // Slots: opposite-content + card / elevated content. Density via size sm / md / lg.
6
+
7
+ .cny-timeline {
8
+ // density tokens (md defaults) — overridden by size modifiers
9
+ --tl-dot: 14px;
10
+ --tl-dot-icon: 24px;
11
+ --tl-glyph: 14px;
12
+ --tl-line: 2px;
13
+ --tl-font: 0.875rem;
14
+
15
+ list-style: none;
16
+ margin: 0;
17
+ padding: 0;
18
+ }
19
+
20
+ .cny-timeline-sm {
21
+ --tl-dot: 10px;
22
+ --tl-dot-icon: 20px;
23
+ --tl-glyph: 12px;
24
+ --tl-line: 2px;
25
+ --tl-font: 0.8125rem;
26
+ }
27
+
28
+ .cny-timeline-lg {
29
+ --tl-dot: 18px;
30
+ --tl-dot-icon: 30px;
31
+ --tl-glyph: 18px;
32
+ --tl-line: 3px;
33
+ --tl-font: 0.9375rem;
34
+ }
35
+
36
+ // ---- item: flex row [opposite] [separator] [content] ----
37
+ .cny-timeline-item {
38
+ display: flex;
39
+ align-items: stretch;
40
+ gap: $space-3;
41
+ min-height: 44px;
42
+ }
43
+
44
+ .cny-timeline-separator {
45
+ display: flex;
46
+ flex-direction: column;
47
+ align-items: center;
48
+ flex: none;
49
+ }
50
+
51
+ // ---- dot ----
52
+ .cny-timeline-dot {
53
+ --dot-color: var(--color-neutral);
54
+ --dot-soft: var(--bg-surface-low);
55
+ display: inline-flex;
56
+ align-items: center;
57
+ justify-content: center;
58
+ flex: none;
59
+ width: var(--tl-dot);
60
+ height: var(--tl-dot);
61
+ border-radius: $radius-full;
62
+ color: var(--text-on-solid);
63
+ font-size: 0.7rem;
64
+ font-weight: 600;
65
+
66
+ // a dot carrying a glyph (icon or number) grows to hold it; the glyph is
67
+ // normalised so callers don't have to hand-tune the icon size
68
+ &-icon {
69
+ width: var(--tl-dot-icon);
70
+ height: var(--tl-dot-icon);
71
+
72
+ svg {
73
+ width: var(--tl-glyph);
74
+ height: var(--tl-glyph);
75
+ }
76
+ }
77
+
78
+ &-filled {
79
+ background: var(--dot-color);
80
+ }
81
+
82
+ &-outlined {
83
+ background: transparent;
84
+ border: 2px solid var(--dot-color);
85
+ color: var(--dot-color);
86
+ }
87
+
88
+ &-soft {
89
+ background: var(--dot-soft);
90
+ color: var(--dot-color);
91
+ }
92
+
93
+ &-primary {
94
+ --dot-color: var(--color-primary);
95
+ --dot-soft: var(--color-primary-soft);
96
+ }
97
+
98
+ &-success {
99
+ --dot-color: var(--color-success);
100
+ --dot-soft: var(--color-success-soft);
101
+ }
102
+
103
+ &-warning {
104
+ --dot-color: var(--color-warning);
105
+ --dot-soft: var(--color-warning-soft);
106
+ }
107
+
108
+ &-danger {
109
+ --dot-color: var(--color-danger);
110
+ --dot-soft: var(--color-danger-soft);
111
+ }
112
+
113
+ &-info {
114
+ --dot-color: var(--color-info);
115
+ --dot-soft: var(--color-info-soft);
116
+ }
117
+
118
+ &-neutral {
119
+ --dot-color: var(--color-neutral);
120
+ --dot-soft: var(--bg-surface-low);
121
+ }
122
+
123
+ &-active {
124
+ animation: cny-timeline-pulse 1.8s ease-out infinite;
125
+ }
126
+ }
127
+
128
+ @keyframes cny-timeline-pulse {
129
+ 0% {
130
+ box-shadow: 0 0 0 0 color-mix(in srgb, var(--dot-color) 45%, transparent);
131
+ }
132
+
133
+ 70%,
134
+ 100% {
135
+ box-shadow: 0 0 0 9px color-mix(in srgb, var(--dot-color) 0%, transparent);
136
+ }
137
+ }
138
+
139
+ @media (prefers-reduced-motion: reduce) {
140
+ .cny-timeline-dot-active {
141
+ animation: none;
142
+ box-shadow: 0 0 0 4px var(--dot-soft);
143
+ }
144
+ }
145
+
146
+ // ---- connector ----
147
+ .cny-timeline-connector {
148
+ --conn-color: var(--hairline-color);
149
+ flex: 1 1 auto;
150
+ width: var(--tl-line);
151
+ min-height: 12px;
152
+ margin: 4px 0;
153
+ border-radius: $radius-full;
154
+ background: var(--conn-color);
155
+
156
+ // completed-segment tints (work in both orientations)
157
+ &-primary {
158
+ --conn-color: var(--color-primary);
159
+ }
160
+
161
+ &-success {
162
+ --conn-color: var(--color-success);
163
+ }
164
+
165
+ &-warning {
166
+ --conn-color: var(--color-warning);
167
+ }
168
+
169
+ &-danger {
170
+ --conn-color: var(--color-danger);
171
+ }
172
+
173
+ &-info {
174
+ --conn-color: var(--color-info);
175
+ }
176
+
177
+ &-neutral {
178
+ --conn-color: var(--color-neutral);
179
+ }
180
+
181
+ // line styles — vertical default runs top→bottom (horizontal override below)
182
+ &-dashed {
183
+ border-radius: 0;
184
+ background: repeating-linear-gradient(to bottom, var(--conn-color) 0 5px, transparent 5px 9px);
185
+ }
186
+
187
+ &-dotted {
188
+ border-radius: 0;
189
+ background: repeating-linear-gradient(to bottom, var(--conn-color) 0 2px, transparent 2px 6px);
190
+ }
191
+ }
192
+
193
+ // ---- content ----
194
+ .cny-timeline-content {
195
+ flex: 1;
196
+ min-width: 0;
197
+ padding-bottom: $space-4;
198
+ font-family: var(--font-body);
199
+ font-size: var(--tl-font);
200
+ color: var(--text-primary);
201
+ }
202
+
203
+ .cny-timeline-content-card {
204
+ // self-contained box model — keeps card and plain items on a consistent rhythm
205
+ padding: $space-3 $space-4;
206
+ margin-bottom: $space-4;
207
+ border: 1px solid var(--border-color);
208
+ border-radius: $radius-md;
209
+ background: var(--bg-surface-lowest);
210
+ }
211
+
212
+ .cny-timeline-content-elevated {
213
+ border-color: transparent;
214
+ box-shadow: var(--shadow-md);
215
+ }
216
+
217
+ .cny-timeline-content-accent {
218
+ --accent: var(--color-neutral);
219
+ border-inline-start: 3px solid var(--accent);
220
+ }
221
+
222
+ .cny-timeline-content-accent-primary {
223
+ --accent: var(--color-primary);
224
+ }
225
+
226
+ .cny-timeline-content-accent-success {
227
+ --accent: var(--color-success);
228
+ }
229
+
230
+ .cny-timeline-content-accent-warning {
231
+ --accent: var(--color-warning);
232
+ }
233
+
234
+ .cny-timeline-content-accent-danger {
235
+ --accent: var(--color-danger);
236
+ }
237
+
238
+ .cny-timeline-content-accent-info {
239
+ --accent: var(--color-info);
240
+ }
241
+
242
+ .cny-timeline-content-accent-neutral {
243
+ --accent: var(--color-neutral);
244
+ }
245
+
246
+ .cny-timeline-opposite-content {
247
+ flex: 1;
248
+ min-width: 0;
249
+ padding-bottom: $space-4;
250
+ font-family: var(--font-body);
251
+ font-size: 0.8125rem;
252
+ color: var(--text-muted);
253
+ text-align: right;
254
+ }
255
+
256
+ .cny-timeline-item:last-child .cny-timeline-connector {
257
+ display: none;
258
+ }
259
+
260
+ // cards pad their content down, so drop the dot to meet the card's first line
261
+ .cny-timeline-item:has(.cny-timeline-content-card) .cny-timeline-separator {
262
+ padding-top: $space-3;
263
+ }
264
+
265
+ // ---- position: left (mirror) ----
266
+ .cny-timeline-left .cny-timeline-item {
267
+ flex-direction: row-reverse;
268
+ }
269
+
270
+ .cny-timeline-left .cny-timeline-opposite-content {
271
+ text-align: left;
272
+ }
273
+
274
+ // ---- position: alternate (zig-zag, rail centered) ----
275
+ .cny-timeline-alternate .cny-timeline-item:nth-child(even) {
276
+ flex-direction: row-reverse;
277
+ }
278
+
279
+ .cny-timeline-alternate .cny-timeline-item:nth-child(even) .cny-timeline-opposite-content {
280
+ text-align: left;
281
+ }
282
+
283
+ // reserve the empty side so the rail stays centered when no opposite-content is supplied
284
+ .cny-timeline-alternate .cny-timeline-item:not(:has(.cny-timeline-opposite-content))::before {
285
+ content: '';
286
+ flex: 1;
287
+ min-width: 0;
288
+ }
289
+
290
+ // ---- orientation: horizontal (centered stepper) ----
291
+ .cny-timeline-horizontal {
292
+ display: flex;
293
+ flex-direction: row;
294
+ align-items: flex-start;
295
+ width: 100%;
296
+ }
297
+
298
+ .cny-timeline-horizontal .cny-timeline-item {
299
+ flex: 1 1 0;
300
+ flex-direction: column;
301
+ align-items: center;
302
+ text-align: center;
303
+ min-height: 0;
304
+ }
305
+
306
+ // dot centered in its segment; connector spans the GAP between this dot and the next
307
+ .cny-timeline-horizontal .cny-timeline-separator {
308
+ position: relative;
309
+ flex-direction: row;
310
+ align-items: center;
311
+ justify-content: center;
312
+ width: 100%;
313
+ }
314
+
315
+ .cny-timeline-horizontal .cny-timeline-dot {
316
+ position: relative;
317
+ z-index: 1;
318
+ width: calc(var(--tl-dot-icon) + 4px);
319
+ height: calc(var(--tl-dot-icon) + 4px);
320
+ font-size: 0.8rem;
321
+
322
+ &.cny-timeline-dot-icon svg {
323
+ width: calc(var(--tl-glyph) + 2px);
324
+ height: calc(var(--tl-glyph) + 2px);
325
+ }
326
+ }
327
+
328
+ .cny-timeline-horizontal .cny-timeline-connector {
329
+ position: absolute;
330
+ top: 50%;
331
+ // start just past this dot's edge, span the gap to the next dot's edge
332
+ left: calc(50% + var(--tl-dot-icon) / 2 + 6px);
333
+ width: calc(100% - var(--tl-dot-icon) - 12px);
334
+ height: var(--tl-line);
335
+ min-height: 0;
336
+ margin: 0;
337
+ transform: translateY(-50%);
338
+ }
339
+
340
+ // horizontal dashes run left→right
341
+ .cny-timeline-horizontal .cny-timeline-connector-dashed {
342
+ background: repeating-linear-gradient(to right, var(--conn-color) 0 5px, transparent 5px 9px);
343
+ }
344
+
345
+ .cny-timeline-horizontal .cny-timeline-connector-dotted {
346
+ background: repeating-linear-gradient(to right, var(--conn-color) 0 2px, transparent 2px 6px);
347
+ }
348
+
349
+ .cny-timeline-horizontal .cny-timeline-content {
350
+ max-width: 16ch;
351
+ margin-inline: auto;
352
+ padding: $space-3 $space-2 0;
353
+ font-size: var(--tl-font);
354
+ line-height: 1.35;
355
+ text-wrap: balance;
356
+ }
357
+
358
+ .cny-timeline-horizontal .cny-timeline-content-card {
359
+ max-width: none;
360
+ text-align: start;
361
+ }
362
+
363
+ .cny-timeline-horizontal .cny-timeline-opposite-content {
364
+ max-width: 16ch;
365
+ margin-inline: auto;
366
+ padding: 0 $space-2 $space-2;
367
+ text-align: center;
368
+ }
@@ -0,0 +1,37 @@
1
+ @use '../tokens/primitives' as *;
2
+
3
+ // Radix Tooltip — distinct from the legacy CSS-only [data-tooltip] pattern.
4
+
5
+ @keyframes cny-tooltip-in {
6
+ from { opacity: 0; transform: scale(0.96); }
7
+ to { opacity: 1; transform: scale(1); }
8
+ }
9
+
10
+ .cny-tooltip-content {
11
+ z-index: 1020;
12
+ padding: 5px 9px;
13
+ background: var(--text-primary);
14
+ color: var(--bg-surface-lowest);
15
+ border-radius: var(--radius-xs);
16
+ font-family: var(--font-body);
17
+ font-size: 0.75rem;
18
+ font-weight: 500;
19
+ line-height: 1.35;
20
+ letter-spacing: -0.005em;
21
+ max-width: 240px;
22
+ box-shadow: 0 6px 18px -6px rgba(0, 0, 0, 0.25);
23
+ animation: cny-tooltip-in 120ms cubic-bezier(0.2, 0.8, 0.2, 1);
24
+ user-select: none;
25
+ pointer-events: none;
26
+
27
+ &[data-side='top'] { transform-origin: bottom center; }
28
+ &[data-side='bottom'] { transform-origin: top center; }
29
+ &[data-side='left'] { transform-origin: right center; }
30
+ &[data-side='right'] { transform-origin: left center; }
31
+ }
32
+
33
+ .cny-tooltip-arrow { fill: var(--text-primary); }
34
+
35
+ @media (prefers-reduced-motion: reduce) {
36
+ .cny-tooltip-content { animation: none; }
37
+ }
@@ -0,0 +1,130 @@
1
+ @use '../tokens/primitives' as *;
2
+
3
+ // TreeView — folder / hierarchy browser.
4
+
5
+ .cny-tree {
6
+ list-style: none;
7
+ margin: 0;
8
+ padding: 0;
9
+ font-family: var(--font-body);
10
+ font-size: 0.875rem;
11
+ color: var(--text-primary);
12
+ }
13
+
14
+ .cny-tree-group {
15
+ list-style: none;
16
+ margin: 0;
17
+ padding: 0;
18
+ padding-inline-start: 16px; // indent each level
19
+ animation: cny-tree-reveal 150ms ease;
20
+ }
21
+
22
+ @keyframes cny-tree-reveal {
23
+ from {
24
+ opacity: 0;
25
+ transform: translateY(-3px);
26
+ }
27
+
28
+ to {
29
+ opacity: 1;
30
+ transform: translateY(0);
31
+ }
32
+ }
33
+
34
+ @media (prefers-reduced-motion: reduce) {
35
+ .cny-tree-group {
36
+ animation: none;
37
+ }
38
+ }
39
+
40
+ .cny-tree-item {
41
+ outline: none;
42
+ }
43
+
44
+ .cny-tree-row {
45
+ display: flex;
46
+ align-items: center;
47
+ gap: 4px;
48
+ height: 30px;
49
+ padding: 0 6px;
50
+ border-radius: $radius-sm;
51
+ cursor: pointer;
52
+ user-select: none;
53
+ color: var(--text-secondary);
54
+ transition:
55
+ background-color 120ms ease,
56
+ color 120ms ease;
57
+
58
+ &:hover {
59
+ background: var(--bg-surface-low);
60
+ color: var(--text-primary);
61
+ }
62
+
63
+ &.is-selected {
64
+ background: var(--color-primary-soft);
65
+ color: var(--color-primary);
66
+ font-weight: 550;
67
+ }
68
+ }
69
+
70
+ // focus ring on the treeitem (roving tabindex lives on the <li>)
71
+ .cny-tree-item:focus-visible > .cny-tree-row {
72
+ outline: 2px solid var(--color-primary);
73
+ outline-offset: -2px;
74
+ }
75
+
76
+ .cny-tree-toggle {
77
+ display: inline-flex;
78
+ align-items: center;
79
+ justify-content: center;
80
+ flex: none;
81
+ width: 18px;
82
+ height: 18px;
83
+ padding: 0;
84
+ border: 0;
85
+ border-radius: $radius-xs;
86
+ background: transparent;
87
+ color: var(--text-muted);
88
+ cursor: pointer;
89
+
90
+ &:hover {
91
+ color: var(--text-primary);
92
+ }
93
+
94
+ svg {
95
+ transition: transform 140ms ease;
96
+ }
97
+
98
+ &.is-open svg {
99
+ transform: rotate(90deg);
100
+ }
101
+ }
102
+
103
+ .cny-tree-toggle-spacer {
104
+ display: inline-block;
105
+ flex: none;
106
+ width: 18px;
107
+ height: 18px;
108
+ }
109
+
110
+ .cny-tree-icon {
111
+ display: inline-flex;
112
+ align-items: center;
113
+ flex: none;
114
+ color: var(--text-muted);
115
+
116
+ svg {
117
+ width: 16px;
118
+ height: 16px;
119
+ }
120
+ }
121
+
122
+ .cny-tree-row.is-selected .cny-tree-icon {
123
+ color: inherit;
124
+ }
125
+
126
+ .cny-tree-label {
127
+ overflow: hidden;
128
+ white-space: nowrap;
129
+ text-overflow: ellipsis;
130
+ }