@webority/theme 0.7.2 → 0.7.3

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/index.scss ADDED
@@ -0,0 +1,15 @@
1
+ // Default-brand full chain — compiled to dist/webority-theme.css (the RCL static
2
+ // asset + the zero-toolchain convenience build). Consumers who want brand-exact
3
+ // output import the partials individually with their OWN _variables before bootstrap.
4
+ //
5
+ // Load order is LOAD-BEARING — never reorder:
6
+ // variables (before bootstrap) -> bootstrap -> tokens -> aliases -> components -> wui-components
7
+ // Flags are NOT imported here: _wui-components already imports them (the phone control needs
8
+ // them), and legacy @import does not dedupe, so a second import emitted all ~245 flag classes
9
+ // twice (≈490 selectors) and bloated the bundle.
10
+ @import "scss/variables";
11
+ @import "bootstrap/scss/bootstrap";
12
+ @import "scss/tokens";
13
+ @import "scss/wui-aliases";
14
+ @import "scss/components";
15
+ @import "scss/wui-components";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/theme",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
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": {
@@ -13,6 +13,7 @@
13
13
  "directory": "packages/theme"
14
14
  },
15
15
  "files": [
16
+ "index.scss",
16
17
  "scss",
17
18
  "dist"
18
19
  ],
@@ -121,6 +121,38 @@
121
121
  --bs-btn-disabled-border-color: var(--wui-danger-strong);
122
122
  }
123
123
 
124
+ // Outline variants — tokenized like the solid buttons above so they follow a product's
125
+ // RUNTIME --wui-* brand instead of Bootstrap's compile-time colours. Resting = brand outline
126
+ // on a transparent fill; hover fills with the brand and flips the label to the on-brand text
127
+ // token; active deepens to the pressed fill.
128
+ .btn-outline-primary {
129
+ --bs-btn-color: var(--color-primary);
130
+ --bs-btn-border-color: var(--color-primary);
131
+ --bs-btn-hover-color: var(--wui-primary-text);
132
+ --bs-btn-hover-bg: var(--color-primary);
133
+ --bs-btn-hover-border-color: var(--color-primary);
134
+ --bs-btn-active-color: var(--wui-primary-text);
135
+ --bs-btn-active-bg: var(--wui-primary-pressed);
136
+ --bs-btn-active-border-color: var(--wui-primary-pressed);
137
+ --bs-btn-disabled-color: var(--color-primary);
138
+ --bs-btn-disabled-border-color: var(--color-primary);
139
+ }
140
+
141
+ // Outline danger uses the deepened --wui-danger-strong (5.80:1, carries white) that solid
142
+ // .btn-danger uses, so an outline-danger hover reads identically to solid danger.
143
+ .btn-outline-danger {
144
+ --bs-btn-color: var(--wui-danger-strong);
145
+ --bs-btn-border-color: var(--wui-danger-strong);
146
+ --bs-btn-hover-color: #fff;
147
+ --bs-btn-hover-bg: var(--wui-danger-strong);
148
+ --bs-btn-hover-border-color: var(--wui-danger-strong);
149
+ --bs-btn-active-color: #fff;
150
+ --bs-btn-active-bg: var(--wui-danger-strong-hover);
151
+ --bs-btn-active-border-color: var(--wui-danger-strong-hover);
152
+ --bs-btn-disabled-color: var(--wui-danger-strong);
153
+ --bs-btn-disabled-border-color: var(--wui-danger-strong);
154
+ }
155
+
124
156
  // Themed disabled state (fleet default). Bootstrap's own disabled look is just the
125
157
  // variant's colour at --bs-btn-disabled-opacity (.65) — barely different from the active
126
158
  // button on a tinted brand palette. Give the ordinary case a real desaturated/muted look
@@ -148,6 +180,10 @@
148
180
  // sm/lg are re-asserted so they keep their own rung instead of the base value.
149
181
  .btn {
150
182
  --bs-btn-border-radius: var(--wui-radius);
183
+ // Smooth iOS-style corners so buttons match the squircle geometry every other
184
+ // control in the system already uses (inputs, selects, pills). Older browsers
185
+ // fall back to the plain border-radius arc — no breakage.
186
+ corner-shape: squircle;
151
187
  }
152
188
  .btn-sm,
153
189
  .btn-group-sm > .btn {
@@ -164,6 +200,10 @@
164
200
  // padding, an icon–label gap, a rounded hover highlight tinted with the brand,
165
201
  // and a danger variant so a row-action menu reads as one considered control.
166
202
  .dropdown-menu {
203
+ // Elevated surface (like .card), not Bootstrap's default page --bs-body-bg: otherwise the
204
+ // menu blends into a tinted page and, in dark mode, resolves to the page colour instead of
205
+ // the elevated surface.
206
+ --bs-dropdown-bg: var(--color-surface);
167
207
  --bs-dropdown-min-width: 12rem;
168
208
  --bs-dropdown-padding-x: 0.375rem;
169
209
  --bs-dropdown-padding-y: 0.375rem;
@@ -232,7 +272,7 @@
232
272
  //
233
273
  // Reference values:
234
274
  // - height : 48px (h-12 = 3rem)
235
- // - radius : 16px (matches `.btn-squircle` literal)
275
+ // - radius : 16px (--wui-radius-xl)
236
276
  // - font : 14px / 500 (text-sm font-medium)
237
277
  // - padding-x : 20px (px-5) default — Watch demo / outline-dark
238
278
  // Primary CTA overrides to 24px (px-6) inline
@@ -247,7 +287,7 @@
247
287
  --bs-btn-font-size: 0.875rem; // 14px
248
288
  --bs-btn-font-weight: 500;
249
289
  --bs-btn-line-height: 1.25;
250
- --bs-btn-border-radius: 16px; // matches .btn-squircle exactly
290
+ --bs-btn-border-radius: var(--wui-radius-xl); // 16px
251
291
  min-height: 3rem; // 48px (h-12)
252
292
 
253
293
 
@@ -546,16 +586,21 @@ button.wui-input-leading:focus-visible {
546
586
  }
547
587
 
548
588
 
549
- // ---- Squircle corners (iOS-style) -----------------------------------
550
- // Named opt-in utilities for a specific squircle radius without relying
551
- // on Bootstrap's $border-radius. A consuming product can layer a global
552
- // `corner-shape: squircle` rule on top (Chrome 138+ / Safari 16+; older
553
- // browsers fall back to the plain border-radius — no breakage).
554
- .btn-squircle {
555
- border-radius: 16px;
589
+ // ---- Radius step utilities ------------------------------------------
590
+ // Every .btn is already a squircle (see base .btn above), so a button that
591
+ // wants a larger corner only needs a bigger radius — hence .btn-radius-xl,
592
+ // named for what it does (an xl radius step) rather than the corner shape.
593
+ .btn-radius-xl {
594
+ --bs-btn-border-radius: var(--wui-radius-xl); // 16px
556
595
  }
596
+
597
+ // Generic (non-button) squircle rounding. Unlike .btn, an arbitrary element
598
+ // gets no corner-shape from the base rules, so this utility sets it itself —
599
+ // otherwise the name would lie and it'd render a plain 32px arc. Older
600
+ // browsers fall back to that arc (Chrome 138+ / Safari 16+ for the squircle).
557
601
  .rounded-squircle {
558
602
  border-radius: 32px;
603
+ corner-shape: squircle;
559
604
  }
560
605
 
561
606
 
package/scss/_tokens.scss CHANGED
@@ -51,6 +51,9 @@
51
51
  // Easing curves — match locked design's CSS / Framer Motion exports
52
52
  --wui-ease-out-quart: cubic-bezier(0.22, 1, 0.36, 1);
53
53
  --wui-ease-ui: cubic-bezier(0.32, 0.72, 0, 1);
54
+ // Elastic / spring — overshoots the target then settles (the y > 1 control point).
55
+ // Used by the tab ink-bar so a tab switch springs into place rather than sliding flat.
56
+ --wui-ease-elastic: cubic-bezier(0.34, 1.56, 0.64, 1);
54
57
 
55
58
  // Elevation — exposes the Bootstrap $box-shadow* SCSS vars (set in
56
59
  // _variables.scss) as CSS vars so inline styles / JS / the portal
@@ -11,14 +11,16 @@
11
11
  // ---- Brand colors ---------------------------------------------------
12
12
  // Neutral indigo default. The full ramp lives in _tokens.scss as CSS
13
13
  // variables since Bootstrap only consumes the named theme colors.
14
- $primary: #4F46E5;
15
- $secondary: #4B5675; // foreground-3 — quiet text + secondary buttons
16
- $success: #04B440;
17
- $info: #056EE9;
18
- $warning: #DFA000;
19
- $danger: #E82646;
20
- $light: #F9FBFC; // surface-mute
21
- $dark: #071437; // text-heading
14
+ // `!default` so a product that sets these in its own _variables BEFORE importing this file
15
+ // (the documented override path) wins — a plain assignment here would clobber the brand.
16
+ $primary: #4F46E5 !default;
17
+ $secondary: #4B5675 !default; // foreground-3 — quiet text + secondary buttons
18
+ $success: #04B440 !default;
19
+ $info: #056EE9 !default;
20
+ $warning: #DFA000 !default;
21
+ $danger: #E82646 !default;
22
+ $light: #F9FBFC !default; // surface-mute
23
+ $dark: #071437 !default; // text-heading
22
24
 
23
25
  // Override `$theme-colors` so Bootstrap's $theme-colors map includes
24
26
  // our brand. Anything we add here gets utility classes generated
@@ -363,7 +363,9 @@
363
363
  outline: none;
364
364
  background-color: var(--color-bg-muted);
365
365
  color: var(--color-text);
366
- border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
366
+ // Full-strength brand border is the focus cue (no glow) — the prior 30% tint read at
367
+ // ~1.5:1, below WCAG 2.2 SC 1.4.11's 3:1 for a non-text focus indicator.
368
+ border-color: var(--color-primary);
367
369
  box-shadow: none;
368
370
  }
369
371
  // Textarea renders as .form-control.textarea (react-bootstrap). Match the
@@ -374,7 +376,9 @@ textarea.textarea.form-control:hover:not(:focus):not(:disabled):not([readonly])
374
376
  }
375
377
  textarea.textarea:focus {
376
378
  outline: none;
377
- border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
379
+ // Full-strength brand border is the focus cue (no glow) — the prior 30% tint read at
380
+ // ~1.5:1, below WCAG 2.2 SC 1.4.11's 3:1 for a non-text focus indicator.
381
+ border-color: var(--color-primary);
378
382
  box-shadow: none;
379
383
  }
380
384
  .input::placeholder,
@@ -995,6 +999,7 @@ wui-select[compact] {
995
999
  color: var(--color-success-text);
996
1000
  border-color: transparent;
997
1001
  }
1002
+ .badge-warning,
998
1003
  .badge-warn {
999
1004
  background: var(--color-warning-soft);
1000
1005
  color: var(--color-warning-text);
@@ -494,6 +494,30 @@
494
494
  .file-preview:hover .file-preview-img {
495
495
  transform: scale(1.03);
496
496
  }
497
+ // Non-image single file: a centered file-type icon + name filling the same shell as the image
498
+ // thumbnail, so a PDF/doc doesn't render as a broken <img>.
499
+ .file-preview-file {
500
+ display: flex;
501
+ flex-direction: column;
502
+ align-items: center;
503
+ justify-content: center;
504
+ gap: 8px;
505
+ width: 100%;
506
+ height: 100%;
507
+ padding: 16px;
508
+ color: var(--color-text-subtle);
509
+ background: var(--color-bg-muted);
510
+ }
511
+ .file-preview-file-name {
512
+ max-width: 100%;
513
+ overflow: hidden;
514
+ font-size: 13px;
515
+ line-height: 1.4;
516
+ color: var(--color-text);
517
+ text-align: center;
518
+ text-overflow: ellipsis;
519
+ white-space: nowrap;
520
+ }
497
521
  .file-preview-overlay {
498
522
  position: absolute;
499
523
  inset: 0;
@@ -108,28 +108,58 @@
108
108
  // The tab strip both surfaces render. React drives it as a controlled strip and
109
109
  // Razor additionally renders Bootstrap panels beneath it, but the strip markup —
110
110
  // and therefore the look — is identical.
111
+ //
112
+ // The active state is an "ink bar" (.app-tab-indicator): one element per strip that
113
+ // SLIDES between tabs with an elastic overshoot (--wui-ease-elastic). It is JS-measured
114
+ // — variable-width tabs cannot be positioned in pure CSS — so until the JS wires the
115
+ // strip (`[data-wui-tabs]`) the active tab keeps a plain static underline as a fallback,
116
+ // and no-JS / first paint never show a bare strip.
117
+
118
+ // Optional overflow wrapper: the scroll chevrons live HERE, outside the scrolling strip,
119
+ // so they stay pinned while the tabs scroll under them.
120
+ .app-tabs-wrap {
121
+ position: relative;
122
+ display: flex;
123
+ align-items: center;
124
+ gap: 4px;
125
+ }
126
+
111
127
  .app-tabs {
128
+ position: relative;
112
129
  display: flex;
113
130
  align-items: center;
114
131
  gap: 2px;
115
132
  border-bottom: 1px solid var(--color-border);
116
133
  overflow-x: auto;
117
134
  scrollbar-width: none;
118
- // The React AppTabs renders a react-bootstrap <Nav>, so the element also
119
- // carries `.nav`, which declares `flex-wrap: wrap`. Both are (0,1,0), so
120
- // today this wins only because the theme is imported after Bootstrap —
121
- // see the doubled-selector rule below, which does not depend on that.
122
135
  flex-wrap: nowrap;
136
+ // Auto-scrolling the active tab into view (and the chevrons) glides rather than jumps.
137
+ scroll-behavior: smooth;
123
138
  }
124
- // Doubled to (0,2,0) so the tab strip keeps scrolling even if a consumer loads
125
- // Bootstrap after the theme, which would otherwise let `.nav` win on order.
139
+ // Doubled to (0,2,0) so the strip keeps scrolling even if a consumer loads Bootstrap
140
+ // after the theme, which would otherwise let `.nav`'s flex-wrap:wrap win on order.
126
141
  .app-tabs.app-tabs {
127
142
  flex-wrap: nowrap;
128
143
  }
129
144
  .app-tabs::-webkit-scrollbar {
130
145
  display: none;
131
146
  }
147
+ // When the wrapper reports overflow, fade the clipped edge(s) so cut tabs read as
148
+ // "more this way" rather than an abrupt crop. Masked on the strip (the scroller) itself.
149
+ .app-tabs-wrap.is-overflow-start .app-tabs {
150
+ mask-image: linear-gradient(90deg, transparent, #000 28px);
151
+ }
152
+ .app-tabs-wrap.is-overflow-end .app-tabs {
153
+ mask-image: linear-gradient(90deg, #000 calc(100% - 28px), transparent);
154
+ }
155
+ .app-tabs-wrap.is-overflow-start.is-overflow-end .app-tabs {
156
+ mask-image: linear-gradient(90deg, transparent, #000 28px, #000 calc(100% - 28px), transparent);
157
+ }
158
+
132
159
  .app-tab {
160
+ position: relative;
161
+ // Above the pills ink bar, which sits at z-index 0.
162
+ z-index: 1;
133
163
  display: inline-flex;
134
164
  align-items: center;
135
165
  gap: 8px;
@@ -155,11 +185,17 @@
155
185
  }
156
186
  .app-tab.active {
157
187
  color: var(--color-primary);
188
+ // Static fallback underline (no-JS / first paint); the ink bar takes over once wired.
158
189
  border-bottom-color: var(--color-primary);
159
190
  // The active tab carries weight as well as colour — colour alone is a weak
160
191
  // signal for anyone who reads the strip at a glance or cannot distinguish it.
161
192
  font-weight: 600;
162
193
  }
194
+ // Once JS wires the ink bar it owns the underline — drop the per-tab fallback so the
195
+ // two never double up during a slide.
196
+ .app-tabs[data-wui-tabs] .app-tab.active {
197
+ border-bottom-color: transparent;
198
+ }
163
199
  .app-tab:disabled {
164
200
  color: var(--color-text-subtle);
165
201
  cursor: not-allowed;
@@ -180,10 +216,41 @@
180
216
  color: var(--color-danger-text);
181
217
  border-bottom-color: var(--color-danger);
182
218
  }
219
+ .app-tabs[data-wui-tabs] .app-tab.danger.active {
220
+ border-bottom-color: transparent;
221
+ }
183
222
  .app-tab.danger .app-tab-count {
184
223
  background: var(--color-danger-soft);
185
224
  color: var(--color-danger-text);
186
225
  }
226
+
227
+ // ── The sliding ink bar ──
228
+ // Positioned from JS-set custom props: --wui-tab-x/-w (horizontal), --wui-tab-y/-h
229
+ // (vertical). Hidden until the strip is wired AND measured, so it never flashes at 0,0.
230
+ .app-tab-indicator {
231
+ position: absolute;
232
+ left: 0;
233
+ bottom: -1px;
234
+ height: 2px;
235
+ width: var(--wui-tab-w, 0);
236
+ background: var(--color-primary);
237
+ border-radius: 2px;
238
+ transform: translateX(var(--wui-tab-x, 0));
239
+ opacity: 0;
240
+ pointer-events: none;
241
+ transition:
242
+ transform 0.42s var(--wui-ease-elastic),
243
+ width 0.42s var(--wui-ease-elastic),
244
+ opacity 0.15s ease;
245
+ }
246
+ .app-tabs[data-wui-tabs] .app-tab-indicator {
247
+ opacity: 1;
248
+ }
249
+ // A destructive active tab tints the bar red too (attribute set by the driver).
250
+ .app-tabs[data-active-danger] .app-tab-indicator {
251
+ background: var(--color-danger);
252
+ }
253
+
187
254
  // Trailing count pill.
188
255
  .app-tab-count {
189
256
  display: inline-flex;
@@ -221,13 +288,31 @@
221
288
  color: var(--color-info-text);
222
289
  }
223
290
 
291
+ // ── Sizes ──
292
+ .app-tabs-sm .app-tab {
293
+ padding: 6px 10px;
294
+ gap: 6px;
295
+ font-size: 13px;
296
+ }
297
+ .app-tabs-lg .app-tab {
298
+ padding: 14px 18px;
299
+ font-size: 15px;
300
+ }
301
+
302
+ // ── Fitted: tabs share the width equally (no scroll). ──
303
+ .app-tabs-fitted .app-tab {
304
+ flex: 1 1 0;
305
+ justify-content: center;
306
+ }
307
+
224
308
  // Wrapper for a self-contained tabs widget (strip + panels), as <app-tabs>
225
309
  // renders server-side. React composes the strip and its panels itself.
226
310
  .app-tabs-group > .tab-content {
227
311
  padding-top: 16px;
228
312
  }
229
313
 
230
- // Pills variant — a segmented control rather than an underline.
314
+ // Pills variant — a segmented control rather than an underline; the ink bar becomes
315
+ // the moving pill behind the active tab.
231
316
  .app-tabs-pills {
232
317
  gap: 4px;
233
318
  border-bottom: 0;
@@ -237,9 +322,104 @@
237
322
  margin-bottom: 0;
238
323
  border-radius: var(--radius);
239
324
  }
325
+ // Fallback fill until the moving pill is wired.
240
326
  .app-tabs-pills .app-tab.active {
241
327
  background: var(--color-primary-soft);
242
328
  }
329
+ .app-tabs-pills[data-wui-tabs] .app-tab.active {
330
+ background: transparent;
331
+ }
332
+ .app-tabs-pills .app-tab-indicator {
333
+ top: 0;
334
+ bottom: 0;
335
+ height: auto;
336
+ z-index: 0;
337
+ background: var(--color-primary-soft);
338
+ border-radius: var(--radius);
339
+ }
340
+ .app-tabs-pills[data-active-danger] .app-tab-indicator {
341
+ background: var(--color-danger-soft);
342
+ }
343
+
344
+ // ── Vertical orientation — a side rail; the ink bar becomes a moving left edge. ──
345
+ .app-tabs-vertical {
346
+ flex-direction: column;
347
+ align-items: stretch;
348
+ border-bottom: 0;
349
+ border-left: 1px solid var(--color-border);
350
+ overflow-x: visible;
351
+ overflow-y: auto;
352
+ }
353
+ .app-tabs-vertical .app-tab {
354
+ justify-content: flex-start;
355
+ border-bottom: 0;
356
+ border-left: 2px solid transparent;
357
+ margin-bottom: 0;
358
+ margin-left: -1px;
359
+ }
360
+ .app-tabs-vertical .app-tab.active {
361
+ border-left-color: var(--color-primary);
362
+ border-bottom-color: transparent;
363
+ }
364
+ .app-tabs-vertical[data-wui-tabs] .app-tab.active {
365
+ border-left-color: transparent;
366
+ }
367
+ .app-tabs-vertical .app-tab-indicator {
368
+ left: -1px;
369
+ top: 0;
370
+ bottom: auto;
371
+ width: 2px;
372
+ height: var(--wui-tab-h, 0);
373
+ transform: translateY(var(--wui-tab-y, 0));
374
+ transition:
375
+ transform 0.42s var(--wui-ease-elastic),
376
+ height 0.42s var(--wui-ease-elastic),
377
+ opacity 0.15s ease;
378
+ }
379
+ // Vertical pills: the moving pill fills the row width.
380
+ .app-tabs-vertical.app-tabs-pills .app-tab-indicator {
381
+ left: 0;
382
+ width: 100%;
383
+ }
384
+
385
+ // ── Scroll chevrons (overflow affordance) — shown by JS only when that side overflows. ──
386
+ .app-tabs-scroll-btn {
387
+ flex: none;
388
+ display: none;
389
+ align-items: center;
390
+ justify-content: center;
391
+ width: 28px;
392
+ height: 28px;
393
+ padding: 0;
394
+ border: 1px solid var(--color-border);
395
+ border-radius: 999px;
396
+ background: var(--color-surface);
397
+ color: var(--color-text-muted);
398
+ cursor: pointer;
399
+ }
400
+ .app-tabs-scroll-btn:hover {
401
+ color: var(--color-text);
402
+ background: var(--color-bg-muted);
403
+ }
404
+ .app-tabs-scroll-btn:focus-visible {
405
+ outline: 2px solid var(--color-primary);
406
+ outline-offset: 2px;
407
+ }
408
+ .app-tabs-wrap.is-overflow-start .app-tabs-scroll-btn.prev,
409
+ .app-tabs-wrap.is-overflow-end .app-tabs-scroll-btn.next {
410
+ display: inline-flex;
411
+ }
412
+
413
+ // ── Motion off — no slide, no overshoot; the bar just fades in place. ──
414
+ @media (prefers-reduced-motion: reduce) {
415
+ .app-tab-indicator,
416
+ .app-tabs-vertical .app-tab-indicator {
417
+ transition: opacity 0.15s ease;
418
+ }
419
+ .app-tabs {
420
+ scroll-behavior: auto;
421
+ }
422
+ }
243
423
 
244
424
  // ── Search field ──
245
425
  // The search control both surfaces render: a rounded field with a leading