@webority/theme 0.9.9 → 0.9.11

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.9.9",
3
+ "version": "0.9.11",
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": {
@@ -59,6 +59,7 @@
59
59
  @import "flags";
60
60
  @import "wui/shell";
61
61
  @import "wui/dock";
62
+ @import "wui/feedback";
62
63
  @import "wui/toolbar";
63
64
  // Last: restates the settled state of menus other partials style, so it must win.
64
65
  @import "wui/menu-motion";
@@ -112,40 +112,17 @@
112
112
  .table-ledger tbody tr:hover td.wui-table-empty:has(> .tbl-empty) {
113
113
  background: transparent;
114
114
  }
115
- // Keep the empty state on screen. An empty table has no data to scroll to, yet its
116
- // header row can still be wider than the viewport (many columns, or wide header
117
- // labels), so `.table-wrap` would scroll horizontally and the centred empty message —
118
- // spanning that full width in a colSpan cell — would slide off screen on a narrow
119
- // screen. Keyed off the empty body itself (`.tbl-empty` for a rich AppTableEmptyState,
120
- // `.tbl-empty-plain` for a plain string — both surfaces render one), lay the empty
121
- // table out at the visible wrap width and drop the scroll, so the message stays centred
122
- // and fully in view. This is the "do it without turning the scroll container into a
123
- // query container" path the pin note on `.table-wrap:has(> .table-ledger)` calls for —
124
- // it replaces the earlier `container-type` + `100cqw` pin, which collapsed the wrap as a
125
- // flex/grid item. Scoped to the empty body, so a table WITH rows is untouched and still
126
- // scrolls its columns.
127
- // Keyed on the empty CELL (`td.wui-table-empty`, the one element unique to the empty
128
- // state on both surfaces) with CHILD combinators, NOT a loose descendant `:has(.tbl-empty)`.
129
- // The child chain matches only a table's OWN empty body — so an expandable row whose
130
- // nested table happens to be empty can never force the outer, data-bearing table into
131
- // this layout. A data table (no empty cell of its own) is untouched and still scrolls.
132
- .table-wrap:has(> table > tbody > tr > td.wui-table-empty) {
133
- overflow-x: hidden;
134
- }
135
- .table:has(> tbody > tr > td.wui-table-empty) {
136
- width: 100%;
137
- min-width: 0;
138
- table-layout: fixed;
139
- }
140
- // Under the fixed layout above the columns are narrow, so a long header label would
141
- // spill out of its cell and back into horizontal overflow — the very thing we just
142
- // removed. Clip each header to its column (ellipsis) in the empty state; short labels
143
- // are unaffected, and the data table keeps its natural header sizing.
144
- .table:has(> tbody > tr > td.wui-table-empty) > thead th {
145
- overflow: hidden;
146
- white-space: nowrap;
147
- text-overflow: ellipsis;
148
- }
115
+ // Empty state sizes exactly like a POPULATED table — a standard responsive table. It fills
116
+ // its container (base `.table { width: 100% }`), so the columns spread to use the width; when
117
+ // the container is too narrow the columns compress as far as they can, and once even their
118
+ // content can't fit, `.table-wrap` (base `overflow-x: auto`) scrolls horizontally rather than
119
+ // clipping — every header label reads in full, never ellipsis-truncated into "REFEREN…".
120
+ //
121
+ // So the empty state carries NO special width rule of its own: the base table layout already
122
+ // gives responsive-then-scroll. We deliberately do NOT pin the message to the viewport with
123
+ // `container-type` + `100cqw` (it forced the colSpan cell, and the whole table, up to the panel
124
+ // width), and we do NOT shrink the table to `width: auto` (that collapsed the columns). Only the
125
+ // cell reset above (no padding/border/hover) is empty-specific.
149
126
 
150
127
  // ── Table (base) ──
151
128
  // The shared data-table look, on the plain `.table` class Bootstrap already
@@ -300,8 +277,9 @@
300
277
  // ITEM (a very common product layout), forcing the whole table into a tiny horizontal
301
278
  // scroll. It was briefly added for a `100cqw` empty-state pin; the collapse is far worse
302
279
  // than the empty message not being perfectly centred on a narrow scrolling table, so the
303
- // pin was removed. If the empty state ever needs viewport-relative sizing again, do it
304
- // without turning the scroll container into a query container.
280
+ // pin was removed. The empty state now just sizes like a normal table (see the empty-state
281
+ // block above) — if it ever needs viewport-relative sizing again, do it without turning
282
+ // the scroll container into a query container.
305
283
  }
306
284
  .table-ledger {
307
285
  border-spacing: 0;
@@ -0,0 +1,259 @@
1
+ // ── In-product feedback ──
2
+ // AppFeedback / <app-feedback> / <wui-feedback> — a floating trigger pinned to a page
3
+ // corner that opens a small panel with a message box. The element authors the markup and
4
+ // owns the states (`open` / `busy` on the host, `hidden` on the panel, error and thank-you
5
+ // blocks); everything visual lives here.
6
+ //
7
+ // Part of the wui-components layer — see _wui-components.scss for load order.
8
+
9
+ wui-feedback {
10
+ position: fixed;
11
+ bottom: 20px;
12
+ z-index: var(--wui-z-fixed);
13
+ display: block;
14
+ }
15
+ wui-feedback,
16
+ wui-feedback[position="bottom-right"] {
17
+ right: 20px;
18
+ left: auto;
19
+ }
20
+ wui-feedback[position="bottom-left"] {
21
+ left: 20px;
22
+ right: auto;
23
+ }
24
+
25
+ // ── Trigger ──
26
+ .feedback-trigger {
27
+ display: inline-flex;
28
+ align-items: center;
29
+ gap: 8px;
30
+ height: var(--wui-control-height-sm);
31
+ padding: 0 16px;
32
+ border: none;
33
+ border-radius: 999px;
34
+ background: var(--color-primary);
35
+ color: var(--color-primary-text);
36
+ font-size: 14px;
37
+ font-weight: 600;
38
+ line-height: 1;
39
+ cursor: pointer;
40
+ box-shadow: var(--wui-shadow-overlay);
41
+ transition:
42
+ background-color 160ms var(--wui-ease-ui),
43
+ transform 160ms var(--wui-ease-ui);
44
+ }
45
+ .feedback-trigger:hover {
46
+ background: var(--color-primary-hover);
47
+ }
48
+ .feedback-trigger:focus-visible {
49
+ outline: 2px solid var(--color-primary);
50
+ outline-offset: 2px;
51
+ }
52
+ .feedback-trigger-icon {
53
+ display: grid;
54
+ place-items: center;
55
+ line-height: 0;
56
+ }
57
+ .feedback-trigger-label {
58
+ white-space: nowrap;
59
+ }
60
+
61
+ // ── Panel ──
62
+ // Anchored to the same corner as the trigger, opening upward — the trigger sits at the
63
+ // bottom of the viewport, so there is only ever room above it.
64
+ .feedback-panel {
65
+ position: absolute;
66
+ bottom: calc(100% + 12px);
67
+ width: 340px;
68
+ max-width: calc(100vw - 40px);
69
+ padding: 16px;
70
+ background: var(--color-surface);
71
+ border: 1px solid var(--color-border);
72
+ border-radius: var(--wui-radius-lg);
73
+ box-shadow: var(--wui-shadow-overlay-lg);
74
+ color: var(--color-text);
75
+ text-align: start;
76
+ }
77
+ wui-feedback[position="bottom-right"] .feedback-panel {
78
+ right: 0;
79
+ }
80
+ wui-feedback[position="bottom-left"] .feedback-panel {
81
+ left: 0;
82
+ }
83
+ // `hidden` on a custom element's child is honoured, but a later display rule would win —
84
+ // state it explicitly so the panel cannot be left visible by cascade order.
85
+ .feedback-panel[hidden] {
86
+ display: none;
87
+ }
88
+
89
+ .feedback-head {
90
+ display: flex;
91
+ align-items: flex-start;
92
+ justify-content: space-between;
93
+ gap: 12px;
94
+ margin-bottom: 12px;
95
+ }
96
+ .feedback-heading {
97
+ margin: 0;
98
+ font-size: 15px;
99
+ font-weight: 600;
100
+ line-height: 1.3;
101
+ color: var(--color-text);
102
+ }
103
+ .feedback-close {
104
+ display: grid;
105
+ place-items: center;
106
+ width: 28px;
107
+ height: 28px;
108
+ flex: 0 0 auto;
109
+ padding: 0;
110
+ border: none;
111
+ border-radius: var(--wui-radius-sm);
112
+ background: transparent;
113
+ color: var(--color-text-muted);
114
+ cursor: pointer;
115
+ }
116
+ .feedback-close:hover {
117
+ background: var(--color-bg-muted);
118
+ color: var(--color-text);
119
+ }
120
+ .feedback-close:focus-visible {
121
+ outline: 2px solid var(--color-primary);
122
+ outline-offset: 1px;
123
+ }
124
+
125
+ .feedback-body[hidden],
126
+ .feedback-actions[hidden],
127
+ .feedback-thanks[hidden] {
128
+ display: none;
129
+ }
130
+
131
+ // ── Fields ──
132
+ .feedback-field {
133
+ display: block;
134
+ margin-bottom: 12px;
135
+ }
136
+ .feedback-field-label {
137
+ display: block;
138
+ margin-bottom: 6px;
139
+ font-size: 13px;
140
+ font-weight: 500;
141
+ color: var(--color-text-muted);
142
+ }
143
+ .feedback-message,
144
+ .feedback-email {
145
+ display: block;
146
+ width: 100%;
147
+ padding: 10px 12px;
148
+ background: var(--color-surface);
149
+ border: 1px solid var(--color-border);
150
+ border-radius: var(--wui-radius-sm);
151
+ color: var(--color-text);
152
+ font-family: inherit;
153
+ font-size: 14px;
154
+ line-height: 1.45;
155
+ }
156
+ .feedback-message {
157
+ min-height: 96px;
158
+ resize: vertical;
159
+ }
160
+ .feedback-message:focus-visible,
161
+ .feedback-email:focus-visible {
162
+ outline: none;
163
+ border-color: var(--color-primary);
164
+ box-shadow: 0 0 0 3px var(--color-primary-soft);
165
+ }
166
+ .feedback-email {
167
+ height: var(--wui-control-height-sm);
168
+ }
169
+
170
+ .feedback-error {
171
+ margin: 0 0 4px;
172
+ font-size: 13px;
173
+ line-height: 1.4;
174
+ color: var(--color-danger-text);
175
+ }
176
+ .feedback-error[hidden] {
177
+ display: none;
178
+ }
179
+
180
+ // ── Actions ──
181
+ .feedback-actions {
182
+ display: flex;
183
+ justify-content: flex-end;
184
+ gap: 8px;
185
+ }
186
+ .feedback-cancel,
187
+ .feedback-submit {
188
+ height: var(--wui-control-height-sm);
189
+ padding: 0 14px;
190
+ border-radius: var(--wui-radius-sm);
191
+ font-size: 14px;
192
+ font-weight: 600;
193
+ line-height: 1;
194
+ cursor: pointer;
195
+ }
196
+ .feedback-cancel {
197
+ border: 1px solid var(--color-border);
198
+ background: transparent;
199
+ color: var(--color-text);
200
+ }
201
+ .feedback-cancel:hover {
202
+ background: var(--color-bg-muted);
203
+ }
204
+ .feedback-submit {
205
+ border: none;
206
+ background: var(--color-primary);
207
+ color: var(--color-primary-text);
208
+ }
209
+ .feedback-submit:hover:not(:disabled) {
210
+ background: var(--color-primary-hover);
211
+ }
212
+ .feedback-submit:disabled {
213
+ opacity: 0.65;
214
+ cursor: default;
215
+ }
216
+ .feedback-cancel:focus-visible,
217
+ .feedback-submit:focus-visible {
218
+ outline: 2px solid var(--color-primary);
219
+ outline-offset: 2px;
220
+ }
221
+
222
+ // ── Thank-you ──
223
+ .feedback-thanks {
224
+ display: flex;
225
+ align-items: center;
226
+ gap: 10px;
227
+ padding: 8px 0 4px;
228
+ font-size: 14px;
229
+ color: var(--color-success-text);
230
+ }
231
+ .feedback-thanks-icon {
232
+ display: grid;
233
+ place-items: center;
234
+ line-height: 0;
235
+ }
236
+ .feedback-thanks-text {
237
+ font-weight: 500;
238
+ line-height: 1.4;
239
+ }
240
+
241
+ // ── Small screens ──
242
+ // The panel is nearly the width of the phone, so pin it to both edges rather than to the
243
+ // trigger's corner — a 340px card anchored to one side overhangs on the narrowest devices.
244
+ @include media-breakpoint-down(sm) {
245
+ .feedback-panel {
246
+ position: fixed;
247
+ right: 12px;
248
+ left: 12px;
249
+ bottom: calc(var(--wui-control-height-sm) + 32px);
250
+ width: auto;
251
+ max-width: none;
252
+ }
253
+ }
254
+
255
+ @media (prefers-reduced-motion: reduce) {
256
+ .feedback-trigger {
257
+ transition: none;
258
+ }
259
+ }
@@ -164,12 +164,11 @@
164
164
  align-items: center;
165
165
  justify-content: space-between;
166
166
  gap: 16px;
167
- // Content-driven wrap: while the title block and actions both fit, space-between keeps the
168
- // actions pinned right; once a long title needs the full width they can no longer share the
169
- // row, so the actions drop onto their own line — left-aligned, since space-between places a
170
- // lone wrapped item at flex-start — instead of being crushed against the title. The gap above
171
- // doubles as the row-gap to that dropped line. (Below lg they stack unconditionally, further
172
- // down; this is the any-width, content-driven case.)
167
+ // Content-driven wrap at EVERY width — there is no breakpoint stack. While the title block
168
+ // and actions both fit, space-between keeps the actions pinned right; once a long title needs
169
+ // the full width they can no longer share the row, so the actions drop onto their own line —
170
+ // left-aligned, since space-between places a lone wrapped item at flex-start — instead of
171
+ // being crushed against the title. The gap above doubles as the row-gap to that dropped line.
173
172
  flex-wrap: wrap;
174
173
  }
175
174
  // Left block: an optional leading icon tile + the title stack, kept together.
@@ -262,6 +261,14 @@
262
261
  display: flex;
263
262
  gap: 8px;
264
263
  align-items: center;
264
+ // Several action buttons wrap among themselves if their own line still can't hold them all
265
+ // — but they never force the block full-width, so a single button stays its natural size.
266
+ flex-wrap: wrap;
267
+ // Don't let the actions be squeezed. The title block (`.page-header-lead`, min-width: 0)
268
+ // absorbs the shrink instead, so the actions keep their natural size and the row stays on
269
+ // one line while it fits — then `.page-header-bar`'s flex-wrap drops the WHOLE actions
270
+ // block onto its own line only once the content genuinely can't share the row.
271
+ flex: none;
265
272
  }
266
273
 
267
274
  // ── Size scale ── (`size`): the title and the block's rhythm scale together.
@@ -318,21 +325,18 @@
318
325
  // its actions stack, and the actions stretch to the full width rather than
319
326
  // crowding the title. Bootstrap's lg tier (991.98px), not an invented breakpoint.
320
327
  //
321
- // Selectors are DOUBLED (`.page-header-bar.page-header-bar`) on purpose. A media
322
- // query adds no specificity, so a consumer's own single-class rule — written later
323
- // in the cascade, as a consuming app's stylesheet always is — silently beats the
324
- // responsive rule and the layout never collapses. Doubling puts these at (0,2,0)
325
- // so they hold, while still leaving a consumer free to override deliberately.
328
+ // The masthead bar is NOT stacked at a breakpoint. It wraps by CONTENT: `.page-header-bar`
329
+ // carries `flex-wrap: wrap` and its actions `flex: none`, so the title and actions share one
330
+ // row wherever they fit and the actions drop below only when the row genuinely can't hold both
331
+ // — a hard `max-width: 991.98px` column-stack pushed the actions under the title even on a
332
+ // 768px tablet that had ample room for them side by side.
333
+ //
334
+ // Selectors below are DOUBLED (`.field-row.field-row`) on purpose. A media query adds no
335
+ // specificity, so a consumer's own single-class rule — written later in the cascade, as a
336
+ // consuming app's stylesheet always is — silently beats the responsive rule and the layout
337
+ // never collapses. Doubling puts these at (0,2,0) so they hold, while still leaving a consumer
338
+ // free to override deliberately.
326
339
  @media (max-width: 991.98px) {
327
- .page-header-bar.page-header-bar {
328
- flex-direction: column;
329
- align-items: stretch;
330
- gap: 12px;
331
- }
332
- .page-actions.page-actions {
333
- flex-wrap: wrap;
334
- width: 100%;
335
- }
336
340
  // A two-up field row is too tight for a tablet column.
337
341
  .field-row.field-row,
338
342
  .field-row-3.field-row-3 {
@@ -422,6 +426,62 @@
422
426
  display: inline-block;
423
427
  }
424
428
 
429
+ // Password-requirements checklist — the content of the popover AppInput (type="password" +
430
+ // passwordRequirements) opens on focus. Rendered identically by React (a JSX <ul>) and Razor
431
+ // (the same <ul> built in webority-ui.js), so both surfaces read the same. `.pw-popover` is the
432
+ // custom class both put on Bootstrap's `.popover` panel.
433
+ .pw-popover .popover-body {
434
+ padding: 12px 14px;
435
+ }
436
+ .pw-criteria {
437
+ list-style: none;
438
+ margin: 0;
439
+ padding: 0;
440
+ display: flex;
441
+ flex-direction: column;
442
+ gap: 8px;
443
+ }
444
+ .pw-criteria .pw-rule {
445
+ display: flex;
446
+ align-items: center;
447
+ gap: 8px;
448
+ font-size: 13px;
449
+ line-height: 1.3;
450
+ }
451
+ // The state mark is a baked-colour circled ✓/✕ — matched to the AA status ramp. The list TEXT
452
+ // uses the -text token for contrast; the mark itself needs less and uses the vivid ramp value.
453
+ .pw-criteria .pw-mark {
454
+ flex: none;
455
+ width: 18px;
456
+ height: 18px;
457
+ background: center / contain no-repeat;
458
+ }
459
+ .pw-criteria .pw-rule-text {
460
+ min-width: 0;
461
+ }
462
+ .pw-criteria .pw-rule.met {
463
+ color: var(--color-success-text);
464
+ }
465
+ .pw-criteria .pw-rule.met .pw-mark {
466
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2317C653' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='m8 12 2.5 2.5 5.5-6'/%3E%3C/svg%3E");
467
+ }
468
+ .pw-criteria .pw-rule.unmet {
469
+ color: var(--color-danger-text);
470
+ }
471
+ .pw-criteria .pw-rule.unmet .pw-mark {
472
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23F8285A' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='m9 9 6 6m0-6-6 6'/%3E%3C/svg%3E");
473
+ }
474
+ // The checklist opens with a PLAIN FADE only — no drop / zoom / scale. It opts out of the shared
475
+ // menu-open motion (_menu-motion.scss animates every `.popover.show`) and lets Bootstrap's own
476
+ // `.fade` opacity transition carry the fade in and out. Both surfaces render `.popover.pw-popover`
477
+ // (React via react-bootstrap's Popover, Razor via Bootstrap's popover), so this makes the two
478
+ // identical. Higher specificity than `.popover.show` so it wins regardless of partial load order.
479
+ .popover.pw-popover.show {
480
+ animation: none;
481
+ scale: none;
482
+ translate: none;
483
+ }
484
+
425
485
  // Page/system-level announcement bar (AppBanner / <app-banner>). Full-width and persistent —
426
486
  // distinct from an inline .alert or a transient toast. Tones reuse the shared soft/text ramp.
427
487
  .wui-banner {