canny-ui 3.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,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,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
+ }
@@ -0,0 +1,111 @@
1
+ @use '../tokens/primitives' as *;
2
+
3
+ // FileUploader — responsive drag-and-drop dropzone.
4
+
5
+ .cny-uploader {
6
+ display: flex;
7
+ flex-direction: column;
8
+ align-items: center;
9
+ justify-content: center;
10
+ gap: $space-3;
11
+ width: 100%;
12
+ box-sizing: border-box;
13
+ // fluid padding: comfortable on desktop, tight on small screens
14
+ padding: clamp(#{$space-4}, 4vw, #{$space-8}) clamp(#{$space-3}, 3vw, #{$space-6});
15
+ text-align: center;
16
+ border: 1.5px dashed var(--border-color-strong);
17
+ border-radius: $radius-lg;
18
+ background: var(--bg-surface-low);
19
+ color: var(--text-secondary);
20
+ font-family: var(--font-body);
21
+ cursor: pointer;
22
+ transition:
23
+ border-color 140ms ease,
24
+ background-color 140ms ease,
25
+ color 140ms ease;
26
+
27
+ &:hover:not(.is-disabled) {
28
+ border-color: var(--color-primary);
29
+ color: var(--text-primary);
30
+ }
31
+
32
+ &:focus-visible {
33
+ outline: 2px solid var(--color-primary);
34
+ outline-offset: 2px;
35
+ }
36
+
37
+ &.is-dragging {
38
+ border-color: var(--color-primary);
39
+ border-style: solid;
40
+ background: var(--color-primary-soft);
41
+ color: var(--color-primary);
42
+ }
43
+
44
+ &.is-disabled {
45
+ cursor: not-allowed;
46
+ opacity: 0.55;
47
+ }
48
+
49
+ // roomier preset
50
+ &-md {
51
+ min-height: 150px;
52
+ }
53
+
54
+ // compact preset — collapses to a single inline row on wider screens
55
+ &-sm {
56
+ gap: $space-2;
57
+ padding: clamp(#{$space-3}, 2.5vw, #{$space-4});
58
+ min-height: 0;
59
+ }
60
+ }
61
+
62
+ .cny-uploader-icon {
63
+ display: inline-flex;
64
+ align-items: center;
65
+ justify-content: center;
66
+ color: var(--color-primary);
67
+
68
+ svg {
69
+ width: clamp(24px, 6vw, 34px);
70
+ height: clamp(24px, 6vw, 34px);
71
+ }
72
+ }
73
+
74
+ .cny-uploader-sm .cny-uploader-icon svg {
75
+ width: 22px;
76
+ height: 22px;
77
+ }
78
+
79
+ .cny-uploader-text {
80
+ display: flex;
81
+ flex-direction: column;
82
+ gap: 2px;
83
+ min-width: 0;
84
+ }
85
+
86
+ .cny-uploader-label {
87
+ font-size: clamp(0.8125rem, 2.5vw, 0.9375rem);
88
+ font-weight: 550;
89
+ line-height: 1.35;
90
+ color: var(--text-primary);
91
+ text-wrap: balance;
92
+ }
93
+
94
+ .cny-uploader-hint {
95
+ font-size: clamp(0.6875rem, 2vw, 0.8125rem);
96
+ color: var(--text-muted);
97
+ text-wrap: balance;
98
+ }
99
+
100
+ // stack the icon beside the text on the compact preset once there's room
101
+ @media (min-width: 420px) {
102
+ .cny-uploader-sm {
103
+ flex-direction: row;
104
+ justify-content: center;
105
+ text-align: start;
106
+ }
107
+
108
+ .cny-uploader-sm .cny-uploader-text {
109
+ align-items: flex-start;
110
+ }
111
+ }