@webority/theme 0.34.0 → 0.35.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/theme",
3
- "version": "0.34.0",
3
+ "version": "0.35.0",
4
4
  "description": "Webority shared design system — Bootstrap 5.3 SCSS overrides, --wui-* tokens, and shared component CSS. The single source of brand chrome for React portals and Razor sites.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "publishConfig": {
@@ -30,6 +30,7 @@
30
30
  @import "wui/choice";
31
31
  @import "wui/media";
32
32
  @import "wui/image-crop";
33
+ @import "wui/avatar-picker";
33
34
  @import "wui/navigation";
34
35
  @import "wui/card";
35
36
  @import "wui/data";
@@ -0,0 +1,116 @@
1
+ // ── Avatar picker ──
2
+ // AppAvatarPicker / <app-avatar-picker>: a profile photo (or its placeholder) beside the actions
3
+ // that change it. The crop dialog it opens is <wui-image-crop> (_image-crop.scss). The diameter
4
+ // comes from --wui-avatar-size on .wui-avatar-picker, which both wrappers set from `size`.
5
+ // Every colour is a theme token, so dark mode follows the token flip.
6
+ //
7
+ // Part of the wui-components layer, see _wui-components.scss for load order.
8
+
9
+ .wui-avatar-picker {
10
+ // The md diameter; both wrappers set their own size inline, which wins.
11
+ --wui-avatar-size: 96px;
12
+ display: flex;
13
+ flex-wrap: wrap;
14
+ align-items: center;
15
+ gap: 16px;
16
+ }
17
+
18
+ .wui-avatar-picker-face {
19
+ position: relative;
20
+ display: grid;
21
+ flex: none;
22
+ place-items: center;
23
+ width: var(--wui-avatar-size);
24
+ height: var(--wui-avatar-size);
25
+ padding: 0;
26
+ border: 1px solid var(--color-border);
27
+ border-radius: 50%;
28
+ background: color-mix(in srgb, var(--color-primary) 12%, var(--color-surface));
29
+ color: var(--color-primary);
30
+ font: inherit;
31
+ cursor: pointer;
32
+ }
33
+ .wui-avatar-picker-face:disabled {
34
+ cursor: default;
35
+ }
36
+ .wui-avatar-picker-face[aria-busy="true"] {
37
+ cursor: progress;
38
+ }
39
+ .wui-avatar-picker-face:focus-visible {
40
+ outline: 2px solid var(--color-primary);
41
+ outline-offset: 3px;
42
+ }
43
+ // A file held over the photo: the ring says it can be dropped there.
44
+ .wui-avatar-picker.is-dragging .wui-avatar-picker-face {
45
+ outline: 2px dashed var(--color-primary);
46
+ outline-offset: 3px;
47
+ }
48
+
49
+ .wui-avatar-picker-img {
50
+ width: 100%;
51
+ height: 100%;
52
+ border-radius: 50%;
53
+ object-fit: cover;
54
+ }
55
+ .wui-avatar-picker-placeholder {
56
+ display: grid;
57
+ place-items: center;
58
+ font-size: calc(var(--wui-avatar-size) * 0.36);
59
+ font-weight: 700;
60
+ letter-spacing: -0.02em;
61
+ line-height: 1;
62
+ }
63
+
64
+ // The camera mark on the ring's edge, so the photo reads as something you can change.
65
+ .wui-avatar-picker-badge {
66
+ position: absolute;
67
+ right: 0;
68
+ bottom: 0;
69
+ display: grid;
70
+ place-items: center;
71
+ width: calc(max(28px, var(--wui-avatar-size) * 0.3));
72
+ height: calc(max(28px, var(--wui-avatar-size) * 0.3));
73
+ border: 2px solid var(--color-surface);
74
+ border-radius: 50%;
75
+ background: var(--color-primary);
76
+ color: var(--color-primary-text);
77
+ }
78
+ .wui-avatar-picker-face:disabled:not([aria-busy="true"]) .wui-avatar-picker-badge {
79
+ display: none;
80
+ }
81
+
82
+ .wui-avatar-picker-actions {
83
+ display: flex;
84
+ flex-wrap: wrap;
85
+ align-items: center;
86
+ gap: 8px;
87
+ }
88
+ .wui-avatar-picker-actions .btn {
89
+ display: inline-flex;
90
+ align-items: center;
91
+ gap: 6px;
92
+ }
93
+ .wui-avatar-picker-actions [hidden] {
94
+ display: none;
95
+ }
96
+
97
+ // "Take photo" opens the front camera, which only a phone or tablet has: it shows on touch screens.
98
+ .wui-avatar-picker-actions .wui-avatar-picker-camera {
99
+ display: none;
100
+ }
101
+ @media (pointer: coarse) {
102
+ .wui-avatar-picker-actions .wui-avatar-picker-camera:not([hidden]) {
103
+ display: inline-flex;
104
+ }
105
+ .wui-avatar-picker-actions .btn {
106
+ min-height: 44px;
107
+ }
108
+ }
109
+
110
+ // On a phone the actions sit under the photo, full width, so each is an easy target.
111
+ @media (max-width: 575.98px) {
112
+ .wui-avatar-picker {
113
+ flex-direction: column;
114
+ align-items: flex-start;
115
+ }
116
+ }
@@ -1,5 +1,5 @@
1
1
  // ── Image crop ──
2
- // <wui-image-crop>: the crop surface AppLogoPicker / <app-logo-picker> shows in a dialog before
2
+ // <wui-image-crop>: the crop surface AppLogoPicker / AppAvatarPicker (and their Razor tags) show in a dialog before
3
3
  // upload. The image is fitted into the stage; the frame is positioned by the element (inline
4
4
  // left/top/width/height) and dims everything outside it with a very large box-shadow, which the
5
5
  // stage clips. Every colour is a theme token, so dark mode follows the token flip.
@@ -33,6 +33,11 @@ wui-image-crop {
33
33
  position: absolute;
34
34
  max-width: none;
35
35
  pointer-events: none;
36
+ // A phone photo carries its orientation in EXIF; pinned here so a page rule cannot turn it off,
37
+ // because the element and the canvas both rely on the browser drawing it the right way up.
38
+ image-orientation: from-image;
39
+ // The element turns the image about its centre for rotate().
40
+ transform-origin: 50% 50%;
36
41
  }
37
42
  .wui-crop-img[hidden],
38
43
  .wui-crop-frame[hidden] {
@@ -144,3 +149,67 @@ wui-image-crop {
144
149
  outline: 2px solid var(--color-primary);
145
150
  outline-offset: 2px;
146
151
  }
152
+
153
+ // Rotate and Reset (the rotatable attribute), on their own row so the zoom slider keeps its width
154
+ // on a phone.
155
+ .wui-crop-tools {
156
+ display: flex;
157
+ flex-wrap: wrap;
158
+ justify-content: center;
159
+ gap: 8px;
160
+ margin-top: 12px;
161
+ }
162
+ .wui-crop-tools[hidden],
163
+ .wui-crop-previews[hidden] {
164
+ display: none;
165
+ }
166
+ .wui-crop-toolbtn {
167
+ display: inline-flex;
168
+ align-items: center;
169
+ gap: 6px;
170
+ min-height: 36px;
171
+ padding: 0 14px;
172
+ border: 1px solid var(--color-border);
173
+ border-radius: 999px;
174
+ background: var(--color-surface);
175
+ color: var(--color-text);
176
+ font: inherit;
177
+ font-size: 0.875rem;
178
+ cursor: pointer;
179
+ }
180
+ .wui-crop-toolbtn:hover {
181
+ border-color: var(--color-border-strong);
182
+ }
183
+ .wui-crop-toolbtn:focus-visible {
184
+ outline: 2px solid var(--color-primary);
185
+ outline-offset: 2px;
186
+ }
187
+
188
+ // Live copies of the crop (the previews attribute), drawn by the element on a canvas each.
189
+ .wui-crop-previews {
190
+ display: flex;
191
+ align-items: center;
192
+ justify-content: center;
193
+ gap: 16px;
194
+ margin-top: 16px;
195
+ }
196
+ .wui-crop-preview {
197
+ flex: none;
198
+ border-radius: var(--radius);
199
+ background: var(--color-bg-muted);
200
+ box-shadow: 0 0 0 1px var(--color-border);
201
+ }
202
+ .wui-crop-previews--round .wui-crop-preview {
203
+ border-radius: 50%;
204
+ }
205
+
206
+ // A finger needs a 44px target; a mouse keeps the compact controls.
207
+ @media (pointer: coarse) {
208
+ .wui-crop-zoombtn {
209
+ width: 44px;
210
+ height: 44px;
211
+ }
212
+ .wui-crop-toolbtn {
213
+ min-height: 44px;
214
+ }
215
+ }
@@ -53,7 +53,9 @@
53
53
  min-width: 0;
54
54
  max-width: 100%;
55
55
  min-height: var(--wui-kanban-column-min-height);
56
- background: var(--color-bg-muted);
56
+ // A lane is the neutral grey-blue in every brand: a brand layer retints --color-bg-muted (Signal
57
+ // makes it violet), but leaves --bs-tertiary-bg alone. Dark mode maps it to --wui-dark-bg-muted.
58
+ background: var(--bs-tertiary-bg);
57
59
  border: 1px solid var(--color-border);
58
60
  border-radius: var(--wui-radius-lg);
59
61
  transition:
@@ -132,36 +134,63 @@
132
134
  display: none;
133
135
  }
134
136
 
135
- .wui-kanban-collapse {
137
+ // The page's own controls at the right of the column head (a "+" that adds a card, say). They sit
138
+ // before the collapse button, which then gives up its auto margin.
139
+ .wui-kanban-actions {
140
+ display: inline-flex;
141
+ align-items: center;
142
+ gap: 4px;
143
+ flex: none;
144
+ margin-inline-start: auto;
145
+ }
146
+ .wui-kanban-actions + .wui-kanban-collapse {
147
+ margin-inline-start: 0;
148
+ }
149
+
150
+ // Shown only on a collapsed column that refuses the carried card (see the collapsed rail below).
151
+ .wui-kanban-lock {
152
+ display: none;
153
+ flex: none;
154
+ color: var(--color-text-muted);
155
+ }
156
+
157
+ // .wui-kanban-action is the icon button a page puts in the actions slot; it matches the toggle.
158
+ .wui-kanban-collapse,
159
+ .wui-kanban-action {
136
160
  display: inline-grid;
137
161
  place-items: center;
138
162
  flex: none;
139
163
  width: 32px;
140
164
  height: 32px;
141
- margin-inline-start: auto;
142
165
  padding: 0;
143
166
  border: 0;
144
167
  border-radius: var(--wui-radius);
145
168
  background: transparent;
146
169
  color: var(--color-text-muted);
147
170
  cursor: pointer;
171
+ }
172
+ .wui-kanban-collapse {
173
+ margin-inline-start: auto;
148
174
 
149
175
  svg {
150
176
  transition: transform 160ms var(--wui-ease-ui);
151
177
  }
152
178
  }
153
179
  // The column itself is the muted fill, so hover lifts to the surface.
154
- .wui-kanban-collapse:hover {
180
+ .wui-kanban-collapse:hover,
181
+ .wui-kanban-action:hover {
155
182
  background: var(--color-surface);
156
183
  color: var(--color-text);
157
184
  }
158
185
  .wui-kanban-collapse:focus-visible,
186
+ .wui-kanban-action:focus-visible,
159
187
  .wui-kanban-switch:focus-visible {
160
188
  outline: 2px solid var(--color-primary);
161
189
  outline-offset: 2px;
162
190
  }
163
191
  @media (pointer: coarse) {
164
- .wui-kanban-collapse {
192
+ .wui-kanban-collapse,
193
+ .wui-kanban-action {
165
194
  width: 44px;
166
195
  height: 44px;
167
196
  }
@@ -224,6 +253,19 @@
224
253
  .wui-kanban-card[data-wui-overflow] {
225
254
  display: none;
226
255
  }
256
+ // A card's title, for the product's card content: two lines at every width, phones included, so
257
+ // one long title never stretches a card past its neighbours.
258
+ .wui-kanban-card-title {
259
+ display: -webkit-box;
260
+ overflow: hidden;
261
+ -webkit-line-clamp: 2;
262
+ line-clamp: 2;
263
+ -webkit-box-orient: vertical;
264
+ overflow-wrap: anywhere;
265
+ font-size: 0.875rem;
266
+ font-weight: 600;
267
+ line-height: 1.35;
268
+ }
227
269
 
228
270
  // "Show N more" under a capped column's cards; "Show fewer" once it is open.
229
271
  .wui-kanban-more {
@@ -373,7 +415,9 @@
373
415
  border: 1px dashed var(--color-border-strong);
374
416
  }
375
417
 
376
- // ── Collapsed column: a narrow strip that still takes drops ──
418
+ // ── Collapsed column: a narrow rail that still takes drops ──
419
+ // Top to bottom: the count, a lock while the carried card cannot land here, the title on its side,
420
+ // then the expand button.
377
421
  .wui-kanban-column[data-collapsed] {
378
422
  flex: 0 0 var(--wui-kanban-collapsed-width);
379
423
  align-self: stretch;
@@ -381,20 +425,33 @@
381
425
  .wui-kanban-head {
382
426
  flex-direction: column;
383
427
  flex-wrap: nowrap;
384
- padding: 10px 6px;
428
+ gap: 12px;
429
+ padding: 14px 6px;
430
+ }
431
+ .wui-kanban-count {
432
+ order: 1;
433
+ }
434
+ .wui-kanban-lock {
435
+ order: 2;
436
+ }
437
+ &[data-wui-drop="refused"] .wui-kanban-lock {
438
+ display: inline-flex;
385
439
  }
386
440
  .wui-kanban-title {
441
+ order: 3;
387
442
  writing-mode: vertical-rl;
388
443
  flex: none;
389
444
  white-space: nowrap;
390
445
  }
391
446
  .wui-kanban-collapse {
392
- order: -1;
447
+ order: 4;
393
448
  margin-inline-start: 0;
394
449
  }
395
450
  .wui-kanban-collapse svg {
396
451
  transform: rotate(180deg);
397
452
  }
453
+ .wui-kanban-dot,
454
+ .wui-kanban-actions,
398
455
  .wui-kanban-summary,
399
456
  .wui-kanban-body,
400
457
  .wui-kanban-more,
@@ -487,13 +544,25 @@
487
544
  .wui-kanban-head {
488
545
  flex-direction: row;
489
546
  flex-wrap: wrap;
547
+ gap: 6px 8px;
490
548
  padding: 12px 12px 8px;
491
549
  }
550
+ .wui-kanban-count,
551
+ .wui-kanban-lock,
552
+ .wui-kanban-title {
553
+ order: 0;
554
+ }
492
555
  .wui-kanban-title {
493
556
  writing-mode: horizontal-tb;
494
557
  flex: 0 1 auto;
495
558
  white-space: normal;
496
559
  }
560
+ .wui-kanban-dot {
561
+ display: block;
562
+ }
563
+ .wui-kanban-actions {
564
+ display: inline-flex;
565
+ }
497
566
  .wui-kanban-collapse {
498
567
  display: none;
499
568
  }
@@ -516,6 +585,10 @@
516
585
  .wui-kanban-summary:empty {
517
586
  display: none;
518
587
  }
588
+ // The drop note already says why; the rail's lock is a wide-screen cue.
589
+ .wui-kanban-column[data-collapsed][data-wui-drop="refused"] .wui-kanban-lock {
590
+ display: none;
591
+ }
519
592
  }
520
593
 
521
594
  @media (prefers-reduced-motion: reduce) {