ngwr 9.1.0 → 10.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.
Files changed (106) hide show
  1. package/README.md +14 -0
  2. package/badge/styles/_index.scss +16 -6
  3. package/badge/styles/_tag.scss +10 -1
  4. package/button/styles/_index.scss +48 -1
  5. package/dialog/styles/_index.scss +65 -0
  6. package/drawer/styles/_index.scss +61 -1
  7. package/fesm2022/ngwr-action-sheet.mjs +1 -1
  8. package/fesm2022/ngwr-action-sheet.mjs.map +1 -1
  9. package/fesm2022/ngwr-breadcrumbs.mjs +2 -2
  10. package/fesm2022/ngwr-breadcrumbs.mjs.map +1 -1
  11. package/fesm2022/ngwr-card.mjs +2 -2
  12. package/fesm2022/ngwr-card.mjs.map +1 -1
  13. package/fesm2022/ngwr-cascader.mjs +6 -5
  14. package/fesm2022/ngwr-cascader.mjs.map +1 -1
  15. package/fesm2022/ngwr-color-picker.mjs +6 -5
  16. package/fesm2022/ngwr-color-picker.mjs.map +1 -1
  17. package/fesm2022/ngwr-context-menu.mjs +7 -7
  18. package/fesm2022/ngwr-context-menu.mjs.map +1 -1
  19. package/fesm2022/ngwr-date-picker.mjs +14 -9
  20. package/fesm2022/ngwr-date-picker.mjs.map +1 -1
  21. package/fesm2022/ngwr-density.mjs +10 -2
  22. package/fesm2022/ngwr-density.mjs.map +1 -1
  23. package/fesm2022/ngwr-dialog.mjs +10 -1
  24. package/fesm2022/ngwr-dialog.mjs.map +1 -1
  25. package/fesm2022/ngwr-drag-drop.mjs +2 -2
  26. package/fesm2022/ngwr-drag-drop.mjs.map +1 -1
  27. package/fesm2022/ngwr-drawer.mjs +37 -16
  28. package/fesm2022/ngwr-drawer.mjs.map +1 -1
  29. package/fesm2022/ngwr-dropdown.mjs +4 -3
  30. package/fesm2022/ngwr-dropdown.mjs.map +1 -1
  31. package/fesm2022/ngwr-i18n-en.mjs +16 -0
  32. package/fesm2022/ngwr-i18n-en.mjs.map +1 -1
  33. package/fesm2022/ngwr-i18n-ru.mjs +17 -0
  34. package/fesm2022/ngwr-i18n-ru.mjs.map +1 -1
  35. package/fesm2022/ngwr-i18n.mjs +74 -1
  36. package/fesm2022/ngwr-i18n.mjs.map +1 -1
  37. package/fesm2022/ngwr-icon.mjs +74 -35
  38. package/fesm2022/ngwr-icon.mjs.map +1 -1
  39. package/fesm2022/ngwr-lightbox.mjs +2 -2
  40. package/fesm2022/ngwr-lightbox.mjs.map +1 -1
  41. package/fesm2022/ngwr-list.mjs +2 -2
  42. package/fesm2022/ngwr-list.mjs.map +1 -1
  43. package/fesm2022/ngwr-mention.mjs +166 -10
  44. package/fesm2022/ngwr-mention.mjs.map +1 -1
  45. package/fesm2022/ngwr-overlay.mjs +173 -3
  46. package/fesm2022/ngwr-overlay.mjs.map +1 -1
  47. package/fesm2022/ngwr-pagination.mjs +17 -4
  48. package/fesm2022/ngwr-pagination.mjs.map +1 -1
  49. package/fesm2022/ngwr-popconfirm.mjs +4 -3
  50. package/fesm2022/ngwr-popconfirm.mjs.map +1 -1
  51. package/fesm2022/ngwr-popover.mjs +10 -3
  52. package/fesm2022/ngwr-popover.mjs.map +1 -1
  53. package/fesm2022/ngwr-select.mjs +12 -5
  54. package/fesm2022/ngwr-select.mjs.map +1 -1
  55. package/fesm2022/ngwr-table.mjs +1 -1
  56. package/fesm2022/ngwr-table.mjs.map +1 -1
  57. package/fesm2022/ngwr-theme.mjs +13 -3
  58. package/fesm2022/ngwr-theme.mjs.map +1 -1
  59. package/fesm2022/ngwr-tree.mjs +4 -3
  60. package/fesm2022/ngwr-tree.mjs.map +1 -1
  61. package/fesm2022/ngwr-version.mjs +1 -1
  62. package/fesm2022/ngwr-version.mjs.map +1 -1
  63. package/fesm2022/ngwr-waves.mjs +25 -3
  64. package/fesm2022/ngwr-waves.mjs.map +1 -1
  65. package/i18n/en.json +165 -0
  66. package/i18n/ru.json +165 -0
  67. package/icon/styles/_index.scss +10 -1
  68. package/input/styles/_index.scss +16 -0
  69. package/input-number/styles/_index.scss +8 -1
  70. package/keyboard/styles/_index.scss +10 -3
  71. package/llms-full.txt +3 -3
  72. package/mention/styles/_index.scss +23 -0
  73. package/package.json +3 -1
  74. package/pagination/styles/_index.scss +12 -0
  75. package/popover/styles/_index.scss +129 -45
  76. package/schematics/ng-add/index.js +18 -4
  77. package/schematics/package.json +3 -0
  78. package/schematics/use/symbol-map.json +2 -0
  79. package/select/styles/_index.scss +13 -0
  80. package/splitter/styles/_index.scss +7 -1
  81. package/table/styles/_index.scss +28 -10
  82. package/theme/styles/_box-model.scss +29 -0
  83. package/theme/styles/_colors.scss +74 -15
  84. package/theme/styles/_dark.scss +52 -6
  85. package/theme/styles/_index.scss +1 -0
  86. package/types/ngwr-cascader.d.ts +1 -0
  87. package/types/ngwr-color-picker.d.ts +1 -0
  88. package/types/ngwr-context-menu.d.ts +1 -0
  89. package/types/ngwr-date-picker.d.ts +5 -3
  90. package/types/ngwr-density.d.ts +8 -1
  91. package/types/ngwr-dialog.d.ts +9 -0
  92. package/types/ngwr-drawer.d.ts +20 -4
  93. package/types/ngwr-dropdown.d.ts +1 -0
  94. package/types/ngwr-i18n.d.ts +65 -2
  95. package/types/ngwr-icon.d.ts +11 -19
  96. package/types/ngwr-mention.d.ts +45 -1
  97. package/types/ngwr-overlay.d.ts +80 -2
  98. package/types/ngwr-pagination.d.ts +14 -1
  99. package/types/ngwr-popconfirm.d.ts +1 -0
  100. package/types/ngwr-popover.d.ts +1 -0
  101. package/types/ngwr-select.d.ts +6 -0
  102. package/types/ngwr-theme.d.ts +8 -1
  103. package/types/ngwr-tree.d.ts +1 -0
  104. package/types/ngwr-version.d.ts +1 -1
  105. package/types/ngwr-waves.d.ts +10 -0
  106. package/waves/styles/_index.scss +21 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngwr",
3
- "version": "9.1.0",
3
+ "version": "10.0.0",
4
4
  "license": "MIT",
5
5
  "description": "Signals-first Angular UI library: 100+ standalone, zoneless-ready components with native Signal Forms, SSR-safe rendering and CSS-variable theming.",
6
6
  "author": "Roman Khegay <rk@garuna.dev> (https://garuna.dev)",
@@ -68,6 +68,8 @@
68
68
  "migrations": "./schematics/migrations.json"
69
69
  },
70
70
  "exports": {
71
+ "./schematics/*": "./schematics/*",
72
+ "./i18n/*.json": "./i18n/*.json",
71
73
  ".": {
72
74
  "sass": "./styles.scss",
73
75
  "types": "./types/ngwr.d.ts",
@@ -2,6 +2,11 @@
2
2
 
3
3
  .wr-pagination {
4
4
  display: block;
5
+ // Breathing room of its own. A pager almost always sits right under a table or
6
+ // list, and with a bare `display: block` and no padding its cells touched the
7
+ // content above and below. Padding rather than margin so it stays inside the
8
+ // component's own box and can't collapse into the consumer's spacing.
9
+ padding-block: 0.5rem;
5
10
 
6
11
  &--disabled {
7
12
  opacity: 0.6;
@@ -80,6 +85,13 @@
80
85
 
81
86
  &__size {
82
87
  min-width: 0;
88
+
89
+ // The 10rem floor on `.wr-select__trigger` is sized for a standalone field.
90
+ // Here the select only ever holds "20 / page", so the pager was reserving
91
+ // 160px for a ~75px label — the single widest thing in the control.
92
+ .wr-select__trigger {
93
+ min-width: 0;
94
+ }
83
95
  }
84
96
 
85
97
  // Size variants — set the cell size; each button gets the matching [size].
@@ -1,8 +1,85 @@
1
+ @use 'sass:list';
2
+
1
3
  @use '../../theme/styles' as theme;
2
4
  @use '../../overlay/styles'; // shared .wr-overlay-sheet / .wr-overlay-backdrop for responsive mode
3
5
 
6
+ // --- Arrow -----------------------------------------------------------------
7
+ //
8
+ // A small square rotated 45°, half tucked under the panel so only the tip shows.
9
+ // Tooltip and popover share the geometry; the popover additionally carries the
10
+ // panel's border on the two outward faces so the outline reads as continuous.
11
+ //
12
+ // Placement is driven by the resolved position modifier the directive puts on the
13
+ // overlay host (`--top` means the panel sits ABOVE the trigger, so the arrow hangs
14
+ // off the panel's BOTTOM edge). All 12 positions are covered — the hand-written
15
+ // tooltip rules this replaces only handled 8, so `top-start` / `top-end` /
16
+ // `bottom-start` / `bottom-end` tooltips have been shipping with no arrow at all.
17
+ $_arrow: 6px;
18
+ $_arrow-half: 3px;
19
+ $_arrow-inset: 0.625rem; // distance from the panel corner for the -start / -end variants
20
+
21
+ // edge = which side of the panel the arrow hangs off; align = where along that side.
22
+ $_arrow-placement: (
23
+ top: (bottom, center),
24
+ top-start: (bottom, start),
25
+ top-end: (bottom, end),
26
+ bottom: (top, center),
27
+ bottom-start: (top, start),
28
+ bottom-end: (top, end),
29
+ left: (right, center),
30
+ left-start: (right, start),
31
+ left-end: (right, end),
32
+ right: (left, center),
33
+ right-start: (left, start),
34
+ right-end: (left, end),
35
+ );
36
+
37
+ /// Positions the arrow against `$edge`, offset outwards by `$pull`.
38
+ @mixin _arrow-at($edge, $align, $pull) {
39
+ #{$edge}: -$pull;
40
+
41
+ @if $edge == bottom or $edge == top {
42
+ @if $align == center {
43
+ left: 50%;
44
+ margin-left: -$_arrow-half;
45
+ } @else if $align == start {
46
+ left: $_arrow-inset;
47
+ } @else {
48
+ right: $_arrow-inset;
49
+ }
50
+ } @else {
51
+ @if $align == center {
52
+ top: 50%;
53
+ margin-top: -$_arrow-half;
54
+ } @else if $align == start {
55
+ top: $_arrow-inset;
56
+ } @else {
57
+ bottom: $_arrow-inset;
58
+ }
59
+ }
60
+ }
61
+
62
+ /// The two faces of the rotated square that stay visible outside the panel.
63
+ @mixin _arrow-border($edge) {
64
+ @if $edge == bottom {
65
+ border-right: 1px solid var(--wr-color-outline);
66
+ border-bottom: 1px solid var(--wr-color-outline);
67
+ } @else if $edge == top {
68
+ border-left: 1px solid var(--wr-color-outline);
69
+ border-top: 1px solid var(--wr-color-outline);
70
+ } @else if $edge == right {
71
+ border-top: 1px solid var(--wr-color-outline);
72
+ border-right: 1px solid var(--wr-color-outline);
73
+ } @else {
74
+ border-bottom: 1px solid var(--wr-color-outline);
75
+ border-left: 1px solid var(--wr-color-outline);
76
+ }
77
+ }
78
+
4
79
  .wr-popover-overlay {
5
80
  > * {
81
+ // Containing block for the arrow below.
82
+ position: relative;
6
83
  display: block;
7
84
  min-width: 8rem;
8
85
  max-width: 22rem;
@@ -14,6 +91,36 @@
14
91
  color: var(--wr-color-on-surface);
15
92
  animation: wr-overlay-in var(--wr-overlay-duration) var(--wr-overlay-ease);
16
93
  transform-origin: top left;
94
+
95
+ &::after {
96
+ content: '';
97
+ position: absolute;
98
+ width: $_arrow;
99
+ height: $_arrow;
100
+ background: var(--wr-color-surface);
101
+ transform: rotate(45deg);
102
+ }
103
+ }
104
+
105
+ // Responsive bottom-sheet mode has no anchor to point at, so no arrow.
106
+ &.wr-overlay-sheet > *::after {
107
+ content: none;
108
+ }
109
+ }
110
+
111
+ @each $position, $spec in $_arrow-placement {
112
+ $edge: list.nth($spec, 1);
113
+ $align: list.nth($spec, 2);
114
+
115
+ // Pulled one extra pixel so the square's own border lines up flush with the
116
+ // panel's, instead of leaving the panel border showing across the arrow's base.
117
+ .wr-popover-overlay--#{$position} > *::after {
118
+ @include _arrow-at($edge, $align, $_arrow-half + 1px);
119
+ @include _arrow-border($edge);
120
+ }
121
+
122
+ .wr-tooltip-overlay--#{$position} .wr-tooltip::after {
123
+ @include _arrow-at($edge, $align, $_arrow-half);
17
124
  }
18
125
  }
19
126
 
@@ -28,12 +135,25 @@
28
135
  }
29
136
 
30
137
  .wr-tooltip {
138
+ // Follows the theme instead of inverting against it: on a dark canvas the
139
+ // tooltip is dark, on a light canvas light. It used to invert (a light chip on
140
+ // dark), which is a common convention but reads as a foreign element here.
141
+ //
142
+ // Both halves are tokens, so an app that wants the inverted look back needs one
143
+ // rule, not an override of every tooltip rule:
144
+ // .wr-tooltip { --wr-tooltip-bg: var(--wr-color-on-surface);
145
+ // --wr-tooltip-color: var(--wr-color-dark-contrast); }
146
+ --wr-tooltip-bg: var(--wr-color-surface);
147
+ --wr-tooltip-color: var(--wr-color-on-surface);
148
+ --wr-tooltip-border: var(--wr-color-outline);
149
+
31
150
  position: relative;
32
151
  display: inline-block;
33
152
  max-width: 18rem;
34
153
  padding: 0.3125rem 0.5rem;
35
- background: var(--wr-color-on-surface);
36
- color: var(--wr-color-dark-contrast);
154
+ background: var(--wr-tooltip-bg);
155
+ color: var(--wr-tooltip-color);
156
+ border: 1px solid var(--wr-tooltip-border);
37
157
  border-radius: var(--wr-border-radius-sm);
38
158
  font-family: var(--wr-font-family-base);
39
159
  font-size: var(--wr-text-xs);
@@ -42,56 +162,20 @@
42
162
  animation: wr-overlay-in var(--wr-overlay-duration) var(--wr-overlay-ease);
43
163
  transform-origin: top left;
44
164
 
45
- // Arrow
165
+ // Arrow — geometry from the shared placement loop at the top of this file.
46
166
  &::after {
47
167
  content: '';
48
168
  position: absolute;
49
- width: 6px;
50
- height: 6px;
51
- background: var(--wr-color-on-surface);
169
+ width: $_arrow;
170
+ height: $_arrow;
171
+ background: var(--wr-tooltip-bg);
52
172
  transform: rotate(45deg);
53
173
  }
54
174
  }
55
175
 
56
- // Arrow placement is dictated by the overlay panel's position modifier
57
- // (the wr-tooltip-overlay--top|bottom|left|right class lives on the
58
- // portal host wrapping the tooltip).
59
- .wr-tooltip-overlay--top .wr-tooltip::after {
60
- left: 50%;
61
- bottom: -3px;
62
- margin-left: -3px;
63
- }
64
- .wr-tooltip-overlay--bottom .wr-tooltip::after {
65
- left: 50%;
66
- top: -3px;
67
- margin-left: -3px;
68
- }
69
- .wr-tooltip-overlay--left .wr-tooltip::after {
70
- top: 50%;
71
- right: -3px;
72
- margin-top: -3px;
73
- }
74
- .wr-tooltip-overlay--left-start .wr-tooltip::after {
75
- top: 0.625rem;
76
- right: -3px;
77
- }
78
- .wr-tooltip-overlay--left-end .wr-tooltip::after {
79
- bottom: 0.625rem;
80
- right: -3px;
81
- }
82
- .wr-tooltip-overlay--right .wr-tooltip::after {
83
- top: 50%;
84
- left: -3px;
85
- margin-top: -3px;
86
- }
87
- .wr-tooltip-overlay--right-start .wr-tooltip::after {
88
- top: 0.625rem;
89
- left: -3px;
90
- }
91
- .wr-tooltip-overlay--right-end .wr-tooltip::after {
92
- bottom: 0.625rem;
93
- left: -3px;
94
- }
176
+ // Arrow placement for both modes is generated from `$_arrow-placement` at the top
177
+ // of this file — the per-position rules that used to live here are emitted there,
178
+ // byte-identical for the 8 positions they covered plus the 4 they missed.
95
179
 
96
180
  @media (prefers-reduced-motion: reduce) {
97
181
  .wr-tooltip {
@@ -10,8 +10,17 @@ const schematics_1 = require("@angular-devkit/schematics");
10
10
  const tasks_1 = require("@angular-devkit/schematics/tasks");
11
11
  const dependencies_1 = require("@schematics/angular/utility/dependencies");
12
12
  const workspace_1 = require("@schematics/angular/utility/workspace");
13
- /** Base peers, regardless of prompt answers. */
14
- const BASE_PEERS = [['@angular/cdk', '>=22.0.0']];
13
+ /**
14
+ * Base peers, regardless of prompt answers.
15
+ *
16
+ * `lucide` is here because the bootstrap snippet this schematic prints registers
17
+ * icons through the lucide adapter — leaving it out made the very first snippet
18
+ * a consumer pastes fail to resolve.
19
+ */
20
+ const BASE_PEERS = [
21
+ ['@angular/cdk', '>=22.0.0'],
22
+ ['lucide', '>=1.0.0'],
23
+ ];
15
24
  /** Extra peers contributed by the `dateAdapter` choice. */
16
25
  const DATE_ADAPTER_PEERS = {
17
26
  none: [],
@@ -121,11 +130,16 @@ function printNextSteps(options, context) {
121
130
  const adapter = options.dateAdapter ?? 'none';
122
131
  const density = options.density ?? 'none';
123
132
  const theme = options.theme ?? 'none';
133
+ // `ngwr/icon` exports only WrIcon / provideWrIcons / svgIcon — there are no
134
+ // bare icon exports, so the old `{ plus, trash }` import did not compile.
135
+ // Icons come from an adapter (lucide / feather) or from svgIcon().
124
136
  const imports = [
125
137
  "import { provideWrOverlay } from 'ngwr/overlay';",
126
- "import { provideWrIcons, plus, trash } from 'ngwr/icon';",
138
+ "import { provideWrIcons } from 'ngwr/icon';",
139
+ "import { lucideIcons } from 'ngwr/icon/adapters/lucide';",
140
+ "import { Plus, Trash2 } from 'lucide';",
127
141
  ];
128
- const providers = ['provideWrOverlay()', 'provideWrIcons([plus, trash])'];
142
+ const providers = ['provideWrOverlay()', 'provideWrIcons(lucideIcons({ plus: Plus, trash: Trash2 }))'];
129
143
  const adapterCall = DATE_ADAPTER_PROVIDER[adapter];
130
144
  if (adapterCall) {
131
145
  imports.push("import { provideWrDateAdapter } from 'ngwr/date-adapter';");
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -158,6 +158,7 @@
158
158
  "WrI18nParams": "ngwr/i18n",
159
159
  "WrI18nMissingHandler": "ngwr/i18n",
160
160
  "WrI18nLoader": "ngwr/i18n",
161
+ "WrI18nBaseCatalogs": "ngwr/i18n",
161
162
  "WrI18nStaticLoader": "ngwr/i18n",
162
163
  "WrI18nStaticCatalogs": "ngwr/i18n",
163
164
  "WrI18nStaticScopedCatalogs": "ngwr/i18n",
@@ -216,6 +217,7 @@
216
217
  "WrMetaTwitter": "ngwr/meta",
217
218
  "WrMeterGroup": "ngwr/meter-group",
218
219
  "WrMeterSegment": "ngwr/meter-group",
220
+ "WrOutsideClick": "ngwr/overlay",
219
221
  "WrOverlayContainer": "ngwr/overlay",
220
222
  "WrPageHeader": "ngwr/page-header",
221
223
  "WrPagination": "ngwr/pagination",
@@ -270,6 +270,10 @@
270
270
  --wr-option-padding-y: 0.4375rem;
271
271
  --wr-option-padding-x: 0.625rem;
272
272
  --wr-option-gap: 0.1875rem;
273
+ // Space between an option's own bits — an icon or checkbox and its label.
274
+ // `--wr-option-gap` above is the panel's row rhythm, a different axis; without
275
+ // this one, a projected `<wr-icon>` sat flush against the text.
276
+ --wr-option-inline-gap: 0.5rem;
273
277
 
274
278
  display: flex;
275
279
  flex-direction: column;
@@ -292,6 +296,7 @@
292
296
  --wr-option-padding-y: 0.3125rem;
293
297
  --wr-option-padding-x: 0.5rem;
294
298
  --wr-option-gap: 0.125rem;
299
+ --wr-option-inline-gap: 0.375rem;
295
300
  }
296
301
 
297
302
  &--lg {
@@ -300,6 +305,7 @@
300
305
  --wr-option-padding-y: 0.5rem;
301
306
  --wr-option-padding-x: 0.75rem;
302
307
  --wr-option-gap: 0.25rem;
308
+ --wr-option-inline-gap: 0.625rem;
303
309
  }
304
310
 
305
311
  &__loading,
@@ -314,6 +320,7 @@
314
320
  .wr-option {
315
321
  display: flex;
316
322
  align-items: center;
323
+ gap: var(--wr-option-inline-gap, 0.5rem);
317
324
  padding: var(--wr-option-padding-y) var(--wr-option-padding-x);
318
325
  border-radius: var(--wr-border-radius-sm);
319
326
  color: var(--wr-color-on-surface);
@@ -399,6 +406,12 @@
399
406
  text-transform: uppercase;
400
407
  }
401
408
 
409
+ // A group needs a visible seam above it, or its label reads as one more dim
410
+ // option in the list. This only fired between two groups, so the common shape
411
+ // — loose options and then a group — got no divider at all. Match a preceding
412
+ // option too; deliberately not `* + &`, which would also draw a line under the
413
+ // search field, where the field's own border already does the job.
414
+ .wr-option + &,
402
415
  + .wr-option-group {
403
416
  border-top: 1px solid var(--wr-color-outline);
404
417
  margin-top: 0.25rem;
@@ -19,7 +19,13 @@
19
19
 
20
20
  &__pane {
21
21
  flex-grow: 0;
22
- flex-shrink: 0;
22
+ // Shrinkable on purpose. The two panes are sized in percentages that add up
23
+ // to 100, and the 4px divider sits between them — with `flex-shrink: 0` the
24
+ // row came to 100% + 4px and both panes hung over the container's edge.
25
+ // Shrinking splits that 4px between them and keeps the ratio the drag set.
26
+ // Safe to shrink: `min-width: 0` plus `overflow: auto` means content never
27
+ // props a pane open.
28
+ flex-shrink: 1;
23
29
  min-width: 0;
24
30
  min-height: 0;
25
31
  overflow: auto;
@@ -1,12 +1,27 @@
1
1
  @use '../../theme/styles' as theme;
2
2
 
3
3
  .wr-table {
4
+ --wr-table-bg: var(--wr-color-surface);
4
5
  --wr-table-border: var(--wr-color-outline);
5
6
  --wr-table-head-bg: rgba(var(--wr-color-outline-rgb), 0.25);
6
7
  --wr-table-row-hover: rgba(var(--wr-color-outline-rgb), 0.15);
8
+ // Head casing. The library renders column titles as the author wrote them —
9
+ // forcing `uppercase` mutates their content, and a header written "ID" or
10
+ // "created at" is the consumer's decision to make, not ours. Both halves stay
11
+ // tokens, so opting back into the old treatment is one rule:
12
+ // .wr-table { --wr-table-head-transform: uppercase;
13
+ // --wr-table-head-letter-spacing: 0.04em; }
14
+ // The tracking moves with the casing on purpose — 0.04em is there to open up
15
+ // capitals and looks wrong on sentence case.
16
+ --wr-table-head-transform: none;
17
+ --wr-table-head-letter-spacing: var(--wr-tracking-normal);
7
18
 
8
19
  display: block;
9
20
  position: relative;
21
+ // The table paints its own surface. Sticky pinned cells already fill with
22
+ // --wr-color-surface, so leaving the table transparent let a page background
23
+ // show through the normal rows while the pinned ones stayed opaque.
24
+ background: var(--wr-table-bg);
10
25
  // Own stacking context so the loading overlay reliably outranks the sticky
11
26
  // pinned cells regardless of the surrounding page z-index.
12
27
  isolation: isolate;
@@ -50,8 +65,8 @@
50
65
  color: var(--wr-color-on-surface-muted);
51
66
  font-weight: 600;
52
67
  font-size: var(--wr-text-xs);
53
- letter-spacing: 0.04em;
54
- text-transform: uppercase;
68
+ letter-spacing: var(--wr-table-head-letter-spacing);
69
+ text-transform: var(--wr-table-head-transform);
55
70
  white-space: nowrap;
56
71
  }
57
72
 
@@ -203,7 +218,7 @@
203
218
  // Pinned footer cells need an opaque base (over the head tint) to stay sticky.
204
219
  &__foot-cell#{&}__td--pin-left,
205
220
  &__foot-cell#{&}__td--pin-right {
206
- background-color: var(--wr-color-surface);
221
+ background-color: var(--wr-table-bg);
207
222
  background-image: linear-gradient(var(--wr-table-head-bg), var(--wr-table-head-bg));
208
223
  }
209
224
 
@@ -221,14 +236,14 @@
221
236
  &__td--pin-left,
222
237
  &__td--pin-right {
223
238
  z-index: 1;
224
- background-color: var(--wr-color-surface);
239
+ background-color: var(--wr-table-bg);
225
240
  }
226
241
 
227
242
  // Header cells lay the head tint over the opaque base so the row stays seamless.
228
243
  &__th--pin-left,
229
244
  &__th--pin-right {
230
245
  z-index: 2;
231
- background-color: var(--wr-color-surface);
246
+ background-color: var(--wr-table-bg);
232
247
  background-image: linear-gradient(var(--wr-table-head-bg), var(--wr-table-head-bg));
233
248
  }
234
249
 
@@ -392,7 +407,7 @@
392
407
  &__group-foot-row > &__foot-cell {
393
408
  border-top: 0;
394
409
  border-bottom: 1px solid var(--wr-table-border);
395
- background-color: var(--wr-color-surface);
410
+ background-color: var(--wr-table-bg);
396
411
  background-image: linear-gradient(var(--wr-table-group-bg), var(--wr-table-group-bg));
397
412
  }
398
413
 
@@ -424,7 +439,7 @@
424
439
  }
425
440
 
426
441
  thead th:not(.wr-table__th--pin-left):not(.wr-table__th--pin-right) {
427
- background-color: var(--wr-color-surface);
442
+ background-color: var(--wr-table-bg);
428
443
  background-image: linear-gradient(var(--wr-table-head-bg), var(--wr-table-head-bg));
429
444
  }
430
445
 
@@ -456,7 +471,7 @@
456
471
  }
457
472
 
458
473
  tfoot .wr-table__foot-cell:not(.wr-table__td--pin-left):not(.wr-table__td--pin-right) {
459
- background-color: var(--wr-color-surface);
474
+ background-color: var(--wr-table-bg);
460
475
  background-image: linear-gradient(var(--wr-table-head-bg), var(--wr-table-head-bg));
461
476
  }
462
477
 
@@ -518,8 +533,11 @@
518
533
  color: var(--wr-color-on-surface-muted);
519
534
  font-size: var(--wr-text-xs);
520
535
  font-weight: 600;
521
- letter-spacing: 0.04em;
522
- text-transform: uppercase;
536
+ // Route through the same tokens as `&__th` instead of re-hard-coding the
537
+ // treatment — card mode used to ignore `--wr-table-head-transform` entirely,
538
+ // so a consumer who opted out still got capitals on mobile.
539
+ letter-spacing: var(--wr-table-head-letter-spacing);
540
+ text-transform: var(--wr-table-head-transform);
523
541
  text-align: left;
524
542
  white-space: nowrap;
525
543
  }
@@ -0,0 +1,29 @@
1
+ // Box model baseline for ngwr's own elements.
2
+ //
3
+ // `ngwr/reset` (which does set `box-sizing: border-box` globally) is opt-in by
4
+ // design, and the umbrella `@use 'ngwr'` deliberately leaves it out — so a
5
+ // consumer following the documented "import once, everything works" path lands
6
+ // in a `content-box` world. Components can't assume otherwise, and several of
7
+ // them did: any `.wr-*` element that pairs `width: 100%` with padding or a
8
+ // border overflowed its container by exactly padding + border. Measured in a
9
+ // stock Angular app on `@use 'ngwr'`:
10
+ //
11
+ // .wr-btn--block → 30px over (14+14 padding, 1+1 border)
12
+ // .wr-input (sm / md / lg) → 18 / 26 / 34px over
13
+ // .wr-textarea__native → 16 / 24 / 32px over
14
+ // .wr-input-group → 2px over (border only)
15
+ //
16
+ // Which is where the stray horizontal scrollbars came from.
17
+ //
18
+ // Scoped to the `wr-` namespace so it can never change the box model of
19
+ // consumer markup — that stays their call, via `ngwr/reset` or their own reset.
20
+ //
21
+ // `:where()` contributes zero specificity on purpose: this is a baseline, not an
22
+ // override. `.wr-icon` and `.wr-spinner` declare `content-box` deliberately (the
23
+ // declared size IS the glyph box) and keep winning without needing `!important`
24
+ // or a particular load order.
25
+ :where([class^='wr-'], [class*=' wr-']),
26
+ :where([class^='wr-'], [class*=' wr-'])::before,
27
+ :where([class^='wr-'], [class*=' wr-'])::after {
28
+ box-sizing: border-box;
29
+ }
@@ -21,11 +21,10 @@ $base-colors: (
21
21
  dark: #0f172a,
22
22
  ) !default;
23
23
 
24
- // YIQ luminosity threshold (0–255) above which a color is treated as
25
- // "light" and pairs with dark text. Uses the classic Bootstrap formula —
26
- // it weights the green channel higher than blue, which matches human
27
- // perception of brightness far better than HSL lightness (HSL classes
28
- // pure blue as 50%-lightness even though it reads as dark).
24
+ // Deprecated: the old YIQ brightness cut-off. No longer read — `_contrast()`
25
+ // compares real WCAG contrast ratios, which needs no threshold. Kept declared
26
+ // so `@use 'ngwr/theme' with ($contrast-threshold: …)` in an existing app still
27
+ // compiles instead of erroring on an unknown variable.
29
28
  $contrast-threshold: 150 !default;
30
29
 
31
30
  // Dark text used on light backgrounds.
@@ -40,20 +39,69 @@ $contrast-light: #ffffff !default;
40
39
  @return color.channel($c, 'red'), color.channel($c, 'green'), color.channel($c, 'blue');
41
40
  }
42
41
 
42
+ /** One channel, linearised per WCAG 2.x. */
43
+ @function _linear($channel) {
44
+ $s: math.div($channel, 255);
45
+ @if $s <= 0.03928 {
46
+ @return math.div($s, 12.92);
47
+ }
48
+ @return math.pow(math.div($s + 0.055, 1.055), 2.4);
49
+ }
50
+
51
+ /** WCAG relative luminance of a colour. */
52
+ @function _luminance($c) {
53
+ @return 0.2126 * _linear(color.channel($c, 'red')) + 0.7152 * _linear(color.channel($c, 'green')) + 0.0722 *
54
+ _linear(color.channel($c, 'blue'));
55
+ }
56
+
57
+ /** WCAG contrast ratio between two colours. */
58
+ @function _ratio($a, $b) {
59
+ $la: _luminance($a);
60
+ $lb: _luminance($b);
61
+ $hi: math.max($la, $lb);
62
+ $lo: math.min($la, $lb);
63
+ @return math.div($hi + 0.05, $lo + 0.05);
64
+ }
65
+
66
+ /**
67
+ * Foreground that actually reads best on `$c` — whichever of the two candidates
68
+ * wins on WCAG contrast ratio.
69
+ *
70
+ * Was a YIQ brightness threshold, which is a brightness *estimate*, not the
71
+ * metric accessibility is judged by, and it mispicked on every mid-saturation
72
+ * colour: five of the nine shipped intents got white text below AA
73
+ * (success 3.33:1, medium 3.10:1, danger and info 3.68:1, secondary 3.99:1)
74
+ * where dark text would have cleared it (5.43 / 5.82 / 4.90 / 4.91 / 4.52).
75
+ * Comparing the real ratios needs no threshold to tune.
76
+ */
43
77
  @function _contrast($c) {
44
- $r: color.channel($c, 'red');
45
- $g: color.channel($c, 'green');
46
- $b: color.channel($c, 'blue');
47
- $yiq: math.div($r * 299 + $g * 587 + $b * 114, 1000);
48
- @if $yiq >= $contrast-threshold {
78
+ @if _ratio($c, $contrast-dark) >= _ratio($c, $contrast-light) {
49
79
  @return $contrast-dark;
50
80
  }
51
81
  @return $contrast-light;
52
82
  }
53
83
 
84
+ /**
85
+ * Public alias of {@link _contrast}. Any theme partial that re-tunes an intent
86
+ * has to re-derive its `-contrast` token too, otherwise the token stays baked
87
+ * from the light palette — which is how the dark theme ended up putting white
88
+ * text on its lightened primary at 3.36:1.
89
+ */
90
+ @function contrast($c) {
91
+ @return _contrast($c);
92
+ }
93
+
54
94
  // CSS variables
55
95
 
56
96
  :root {
97
+ // Declared here so the light token set also claims the light UA scheme.
98
+ // Without it the browser fell back to the OS preference, and an app running
99
+ // ngwr's LIGHT theme on a dark-mode machine got dark scrollbars and dark
100
+ // native controls (<textarea>, date/colour inputs) against light surfaces.
101
+ // `_dark.scss` still flips it to `dark` under [data-theme='dark'], which wins
102
+ // on specificity.
103
+ color-scheme: light;
104
+
57
105
  --wr-color-white: #ffffff;
58
106
  --wr-color-white-rgb: 255, 255, 255;
59
107
  --wr-color-black: #000000;
@@ -80,10 +128,21 @@ $contrast-light: #ffffff !default;
80
128
  // regardless of theme.
81
129
  --wr-color-overlay: rgba(var(--wr-color-backdrop-rgb), 0.45);
82
130
 
83
- // De-emphasized text — `medium` dimmed to two canonical strengths so muted
131
+ // Muted TEXT, deliberately its own token rather than an alias of the `medium`
132
+ // intent. The two roles pull in opposite directions and cannot share a value:
133
+ // as text it needs >= 4.5:1 against the lightest surface (#ebeff4), while as a
134
+ // button fill it carries dark text and needs >= 4.5:1 the other way. The best
135
+ // candidate for either fails the other — the window closes around #6b7a8a
136
+ // (3.81 as text, 4.11 as fill). Aliasing them left secondary text at 2.69:1.
137
+ --wr-color-muted-text: #5f6c7d;
138
+ --wr-color-muted-text-rgb: 95, 108, 125;
139
+
140
+ // De-emphasized text — the muted ink dimmed to two canonical strengths so
84
141
  // labels / captions / placeholders read consistently. One place each.
85
- --wr-color-text-muted: rgba(var(--wr-color-medium-rgb), 0.85);
86
- --wr-color-text-faint: rgba(var(--wr-color-medium-rgb), 0.6);
142
+ // Only the full-opacity token above is AA-safe for body text; these two are
143
+ // decorative by design (0.85 reaches ~3.5:1, 0.6 ~2.3:1 on a light surface).
144
+ --wr-color-text-muted: rgba(var(--wr-color-muted-text-rgb), 0.85);
145
+ --wr-color-text-faint: rgba(var(--wr-color-muted-text-rgb), 0.6);
87
146
 
88
147
  // Neutral gray ramp — a FIXED primitive scale (slate). Each step is a stable
89
148
  // job (50 = subtlest surface tint … 950 = strongest ink) and, unlike the
@@ -121,8 +180,8 @@ $contrast-light: #ffffff !default;
121
180
  --wr-color-surface-rgb: var(--wr-color-white-rgb);
122
181
  --wr-color-on-surface: var(--wr-color-dark);
123
182
  --wr-color-on-surface-rgb: var(--wr-color-dark-rgb);
124
- --wr-color-on-surface-muted: var(--wr-color-medium);
125
- --wr-color-on-surface-muted-rgb: var(--wr-color-medium-rgb);
183
+ --wr-color-on-surface-muted: var(--wr-color-muted-text);
184
+ --wr-color-on-surface-muted-rgb: var(--wr-color-muted-text-rgb);
126
185
 
127
186
  // Solid hairline / divider colour. Distinct from `--wr-color-border` above,
128
187
  // which is deliberately translucent (`light` at 50%) so it reads on any