ngwr 9.0.1 → 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 (108) 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/date-picker/styles/_index.scss +62 -0
  6. package/dialog/styles/_index.scss +65 -0
  7. package/drawer/styles/_index.scss +83 -0
  8. package/fesm2022/ngwr-action-sheet.mjs +1 -1
  9. package/fesm2022/ngwr-action-sheet.mjs.map +1 -1
  10. package/fesm2022/ngwr-breadcrumbs.mjs +2 -2
  11. package/fesm2022/ngwr-breadcrumbs.mjs.map +1 -1
  12. package/fesm2022/ngwr-card.mjs +2 -2
  13. package/fesm2022/ngwr-card.mjs.map +1 -1
  14. package/fesm2022/ngwr-cascader.mjs +6 -5
  15. package/fesm2022/ngwr-cascader.mjs.map +1 -1
  16. package/fesm2022/ngwr-color-picker.mjs +6 -5
  17. package/fesm2022/ngwr-color-picker.mjs.map +1 -1
  18. package/fesm2022/ngwr-context-menu.mjs +7 -7
  19. package/fesm2022/ngwr-context-menu.mjs.map +1 -1
  20. package/fesm2022/ngwr-date-picker.mjs +521 -8
  21. package/fesm2022/ngwr-date-picker.mjs.map +1 -1
  22. package/fesm2022/ngwr-density.mjs +10 -2
  23. package/fesm2022/ngwr-density.mjs.map +1 -1
  24. package/fesm2022/ngwr-dialog.mjs +95 -13
  25. package/fesm2022/ngwr-dialog.mjs.map +1 -1
  26. package/fesm2022/ngwr-drag-drop.mjs +2 -2
  27. package/fesm2022/ngwr-drag-drop.mjs.map +1 -1
  28. package/fesm2022/ngwr-drawer.mjs +377 -22
  29. package/fesm2022/ngwr-drawer.mjs.map +1 -1
  30. package/fesm2022/ngwr-dropdown.mjs +4 -3
  31. package/fesm2022/ngwr-dropdown.mjs.map +1 -1
  32. package/fesm2022/ngwr-i18n-en.mjs +21 -0
  33. package/fesm2022/ngwr-i18n-en.mjs.map +1 -1
  34. package/fesm2022/ngwr-i18n-ru.mjs +22 -0
  35. package/fesm2022/ngwr-i18n-ru.mjs.map +1 -1
  36. package/fesm2022/ngwr-i18n.mjs +74 -1
  37. package/fesm2022/ngwr-i18n.mjs.map +1 -1
  38. package/fesm2022/ngwr-icon.mjs +74 -35
  39. package/fesm2022/ngwr-icon.mjs.map +1 -1
  40. package/fesm2022/ngwr-lightbox.mjs +2 -2
  41. package/fesm2022/ngwr-lightbox.mjs.map +1 -1
  42. package/fesm2022/ngwr-list.mjs +2 -2
  43. package/fesm2022/ngwr-list.mjs.map +1 -1
  44. package/fesm2022/ngwr-mention.mjs +166 -10
  45. package/fesm2022/ngwr-mention.mjs.map +1 -1
  46. package/fesm2022/ngwr-overlay.mjs +173 -3
  47. package/fesm2022/ngwr-overlay.mjs.map +1 -1
  48. package/fesm2022/ngwr-pagination.mjs +17 -4
  49. package/fesm2022/ngwr-pagination.mjs.map +1 -1
  50. package/fesm2022/ngwr-popconfirm.mjs +4 -3
  51. package/fesm2022/ngwr-popconfirm.mjs.map +1 -1
  52. package/fesm2022/ngwr-popover.mjs +10 -3
  53. package/fesm2022/ngwr-popover.mjs.map +1 -1
  54. package/fesm2022/ngwr-select.mjs +233 -43
  55. package/fesm2022/ngwr-select.mjs.map +1 -1
  56. package/fesm2022/ngwr-table.mjs +1 -1
  57. package/fesm2022/ngwr-table.mjs.map +1 -1
  58. package/fesm2022/ngwr-theme.mjs +13 -3
  59. package/fesm2022/ngwr-theme.mjs.map +1 -1
  60. package/fesm2022/ngwr-tree.mjs +4 -3
  61. package/fesm2022/ngwr-tree.mjs.map +1 -1
  62. package/fesm2022/ngwr-version.mjs +1 -1
  63. package/fesm2022/ngwr-version.mjs.map +1 -1
  64. package/fesm2022/ngwr-waves.mjs +25 -3
  65. package/fesm2022/ngwr-waves.mjs.map +1 -1
  66. package/i18n/en.json +165 -0
  67. package/i18n/ru.json +165 -0
  68. package/icon/styles/_index.scss +10 -1
  69. package/input/styles/_index.scss +16 -0
  70. package/input-number/styles/_index.scss +8 -1
  71. package/keyboard/styles/_index.scss +10 -3
  72. package/llms-full.txt +14 -14
  73. package/llms.txt +12 -6
  74. package/mention/styles/_index.scss +23 -0
  75. package/package.json +23 -5
  76. package/pagination/styles/_index.scss +12 -0
  77. package/popover/styles/_index.scss +129 -45
  78. package/schematics/ng-add/index.js +18 -4
  79. package/schematics/package.json +3 -0
  80. package/schematics/use/symbol-map.json +7 -0
  81. package/select/styles/_index.scss +13 -0
  82. package/splitter/styles/_index.scss +7 -1
  83. package/table/styles/_index.scss +28 -10
  84. package/theme/styles/_box-model.scss +29 -0
  85. package/theme/styles/_colors.scss +74 -15
  86. package/theme/styles/_dark.scss +52 -6
  87. package/theme/styles/_index.scss +1 -0
  88. package/types/ngwr-cascader.d.ts +1 -0
  89. package/types/ngwr-color-picker.d.ts +1 -0
  90. package/types/ngwr-context-menu.d.ts +1 -0
  91. package/types/ngwr-date-picker.d.ts +172 -4
  92. package/types/ngwr-density.d.ts +8 -1
  93. package/types/ngwr-dialog.d.ts +80 -8
  94. package/types/ngwr-drawer.d.ts +245 -10
  95. package/types/ngwr-dropdown.d.ts +1 -0
  96. package/types/ngwr-i18n.d.ts +65 -2
  97. package/types/ngwr-icon.d.ts +11 -19
  98. package/types/ngwr-mention.d.ts +45 -1
  99. package/types/ngwr-overlay.d.ts +80 -2
  100. package/types/ngwr-pagination.d.ts +14 -1
  101. package/types/ngwr-popconfirm.d.ts +1 -0
  102. package/types/ngwr-popover.d.ts +1 -0
  103. package/types/ngwr-select.d.ts +121 -9
  104. package/types/ngwr-theme.d.ts +8 -1
  105. package/types/ngwr-tree.d.ts +1 -0
  106. package/types/ngwr-version.d.ts +1 -1
  107. package/types/ngwr-waves.d.ts +10 -0
  108. package/waves/styles/_index.scss +21 -0
@@ -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
@@ -1,5 +1,7 @@
1
1
  @use 'sass:color';
2
2
 
3
+ @use 'colors';
4
+
3
5
  // Dark theme overrides — opt-in via `[data-theme="dark"]` on <html>.
4
6
  // Drive the toggle from `WrTheme` (provided automatically with
5
7
  // `provideWrTheme(...)`).
@@ -17,6 +19,15 @@ $dark-intents: (
17
19
  danger: #ff5c5c,
18
20
  );
19
21
 
22
+ // The surface / ink bases below are re-tuned for the dark canvas too, but they
23
+ // live in hand-written blocks (each one documents a judgement call the loop
24
+ // can't express). Holding the values in variables lets the `-rgb` triplets and
25
+ // the shade derivations read from the value that actually ships, instead of
26
+ // being re-typed next to it.
27
+ $_light: #262f44;
28
+ $_medium: #9aa6b8;
29
+ $_dark: #e6ebf3;
30
+
20
31
  [data-theme='dark'] {
21
32
  // Canvas (was `--wr-color-white` in light mode)
22
33
  // Deeper than slate-900 so an elevated surface in slate-800 reads
@@ -26,8 +37,8 @@ $dark-intents: (
26
37
 
27
38
  // Text
28
39
  // Softer than pure-white — easier on the eyes for long reading.
29
- --wr-color-dark: #e6ebf3;
30
- --wr-color-dark-rgb: 230, 235, 243;
40
+ --wr-color-dark: #{$_dark};
41
+ --wr-color-dark-rgb: #{color.channel($_dark, 'red')}, #{color.channel($_dark, 'green')}, #{color.channel($_dark, 'blue')};
31
42
  // `*-contrast` values are baked at SCSS compile time from the light
32
43
  // palette, so they don't auto-flip when `--wr-color-dark` /
33
44
  // `--wr-color-white` are reassigned for dark mode. Tooltip-style
@@ -35,6 +46,14 @@ $dark-intents: (
35
46
  // white-on-white without this override; re-pair them by hand here.
36
47
  --wr-color-dark-contrast: #0b1120;
37
48
  --wr-color-white-contrast: #e6ebf3;
49
+ // Same story as the contrast pair: the shades were baked from the light
50
+ // palette's near-black ink, so `.wr-btn--dark:hover` painted itself almost
51
+ // black while keeping the near-black `-contrast` text. `dark` is an INK token,
52
+ // so unlike `light` below its shades mean exactly what they say.
53
+ --wr-color-dark-light: #{color.adjust($_dark, $lightness: 5%)};
54
+ --wr-color-dark-lighter: #{color.adjust($_dark, $lightness: 10%)};
55
+ --wr-color-dark-dark: #{color.adjust($_dark, $lightness: -5%)};
56
+ --wr-color-dark-darker: #{color.adjust($_dark, $lightness: -10%)};
38
57
 
39
58
  // Borders / tints (was `--wr-color-light`)
40
59
  // Used for two jobs at once: 1-px borders and translucent surface
@@ -43,8 +62,8 @@ $dark-intents: (
43
62
  // looks like glowing pencil lines). This value reads as a real
44
63
  // divider on cards / inputs without shouting — matches what Vercel
45
64
  // and shadcn ship for the same job.
46
- --wr-color-light: #262f44;
47
- --wr-color-light-rgb: 38, 47, 68;
65
+ --wr-color-light: #{$_light};
66
+ --wr-color-light-rgb: #{color.channel($_light, 'red')}, #{color.channel($_light, 'green')}, #{color.channel($_light, 'blue')};
48
67
  // The baked contrast pairs with the LIGHT value (#cbd5e1 → dark text);
49
68
  // the dark surface above needs light text instead.
50
69
  --wr-color-light-contrast: #e6ebf3;
@@ -57,18 +76,42 @@ $dark-intents: (
57
76
  // means "stronger divider" (further from canvas, so lighter).
58
77
  --wr-color-light-lighter: #1a2236;
59
78
  --wr-color-light-darker: #3a4661;
79
+ // `-light` / `-dark` were the two the hand block forgot, and they are what
80
+ // `.wr-btn--light` uses for hover / active. Left inherited, hover swapped in
81
+ // the LIGHT palette's slate-300 — a near-white fill under the near-white
82
+ // `-contrast` text above, so the button simply disappeared under the cursor.
83
+ // Half-steps of the tuned pair, same inverted reading: toward the canvas for
84
+ // `-light`, away from it for `-dark`, which also makes hover read as a lift.
85
+ --wr-color-light-light: #{color.adjust($_light, $lightness: -2.5%)};
86
+ --wr-color-light-dark: #{color.adjust($_light, $lightness: 5%)};
60
87
 
61
88
  // Secondary text (was `--wr-color-medium`)
62
89
  // Lifted from slate-400 → slate-300-ish for better legibility on
63
90
  // the deeper canvas.
64
- --wr-color-medium: #9aa6b8;
65
- --wr-color-medium-rgb: 154, 166, 184;
91
+ --wr-color-medium: #{$_medium};
92
+ --wr-color-medium-rgb: #{color.channel($_medium, 'red')}, #{color.channel($_medium, 'green')}, #{color.channel($_medium, 'blue')};
93
+ // This block shipped base + rgb only, so contrast and all four shades still
94
+ // came from the light palette's slate. Ink token, plain shades.
95
+ --wr-color-medium-contrast: #{colors.contrast($_medium)};
96
+ --wr-color-medium-light: #{color.adjust($_medium, $lightness: 5%)};
97
+ --wr-color-medium-lighter: #{color.adjust($_medium, $lightness: 10%)};
98
+ --wr-color-medium-dark: #{color.adjust($_medium, $lightness: -5%)};
99
+ --wr-color-medium-darker: #{color.adjust($_medium, $lightness: -10%)};
100
+
101
+ // Muted TEXT is its own token now (the light theme could not serve both text
102
+ // and fill from one value), so the dark canvas needs its own version too —
103
+ // otherwise it would inherit the light theme's dark ink. 7.64:1 here.
104
+ --wr-color-muted-text: #9aa6b8;
105
+ --wr-color-muted-text-rgb: 154, 166, 184;
66
106
 
67
107
  // Brand-color retune
68
108
  // The default primary (#3969e2) reads a touch dim on the deep
69
109
  // canvas. Bump the base + shade derivations so hover / focus rings
70
110
  // stay visible against slate-900.
71
111
  --wr-color-primary: #5b85ff;
112
+ // Same reason as the loop above — the light primary pairs with white, but the
113
+ // lightened dark-theme primary only reaches 3.36:1 against it.
114
+ --wr-color-primary-contrast: #{colors.contrast(#5b85ff)};
72
115
  --wr-color-primary-rgb: 91, 133, 255;
73
116
  --wr-color-primary-light: #7a9eff;
74
117
  --wr-color-primary-dark: #2f56c9;
@@ -79,6 +122,9 @@ $dark-intents: (
79
122
  // shade set in dark too (generated, so no intent can silently drift).
80
123
  @each $name, $base in $dark-intents {
81
124
  --wr-color-#{$name}: #{$base};
125
+ // Re-derived, not inherited: the light-palette token would pair the wrong
126
+ // foreground with a re-tuned base.
127
+ --wr-color-#{$name}-contrast: #{colors.contrast($base)};
82
128
  --wr-color-#{$name}-rgb: #{color.channel($base, 'red')}, #{color.channel($base, 'green')}, #{color.channel($base, 'blue')};
83
129
  --wr-color-#{$name}-light: #{color.adjust($base, $lightness: 5%)};
84
130
  --wr-color-#{$name}-lighter: #{color.adjust($base, $lightness: 10%)};
@@ -1,3 +1,4 @@
1
+ @forward 'box-model';
1
2
  @forward 'colors';
2
3
  @forward 'variables';
3
4
  @forward 'dark';
@@ -101,6 +101,7 @@ declare class WrCascader<T = string> implements FormValueControl<unknown> {
101
101
  protected readonly panelTpl: _angular_core.Signal<TemplateRef<any>>;
102
102
  private readonly host;
103
103
  private readonly overlay;
104
+ private readonly outsideClick;
104
105
  private readonly vcr;
105
106
  private readonly scrollStrategies;
106
107
  private readonly destroyRef;
@@ -286,6 +286,7 @@ declare class WrColorPickerTrigger {
286
286
  readonly closed: _angular_core.OutputEmitterRef<void>;
287
287
  private readonly host;
288
288
  private readonly overlay;
289
+ private readonly outsideClick;
289
290
  private readonly vcr;
290
291
  private readonly scrollStrategies;
291
292
  private readonly destroyRef;
@@ -44,6 +44,7 @@ declare class WrContextMenu {
44
44
  readonly menu: i0.InputSignal<WrContextMenuPanel>;
45
45
  private readonly host;
46
46
  private readonly overlay;
47
+ private readonly outsideClick;
47
48
  private readonly vcr;
48
49
  private readonly scrollStrategies;
49
50
  private readonly destroyRef;
@@ -25,7 +25,7 @@ import { WrDateFormat } from 'ngwr/date-adapter';
25
25
  * accepts both named keys (`'shortDate'`, `'mediumDateTime'`, …) and raw
26
26
  * token strings (`'dd.MM.yyyy'`, `'HH:mm'`). When `format` is left at the
27
27
  * default (`null`), the picker derives the right named key from `mode`:
28
- * `'date'` → `'shortDate'`, `'time'` → `'shortTime'`, `'datetime'` →
28
+ * `'date'` gives `'shortDate'`, `'time'` gives `'time'`, `'datetime'` gives
29
29
  * `'shortDateTime'`.
30
30
  *
31
31
  * @example
@@ -50,8 +50,8 @@ declare class WrDatePicker implements FormValueControl<Date | null> {
50
50
  readonly mode: _angular_core.InputSignal<"time" | "date" | "datetime">;
51
51
  /**
52
52
  * Format used for both display and parsing. When `null` (default), the
53
- * format is derived from `mode` (`shortDate` / `shortTime` /
54
- * `shortDateTime`). Pass a named key or raw token string to override.
53
+ * format is derived from `mode` (`shortDate` / `time` / `shortDateTime`).
54
+ * Pass a named key or raw token string to override.
55
55
  */
56
56
  readonly format: _angular_core.InputSignal<WrDateFormat | (string & {}) | null>;
57
57
  /** Placeholder shown when the input is empty. */
@@ -79,6 +79,7 @@ declare class WrDatePicker implements FormValueControl<Date | null> {
79
79
  readonly readonly: _angular_core.InputSignalWithTransform<boolean, any>;
80
80
  private readonly adapter;
81
81
  private readonly overlay;
82
+ private readonly outsideClick;
82
83
  private readonly scrollStrategies;
83
84
  private readonly host;
84
85
  private readonly destroyRef;
@@ -130,4 +131,171 @@ declare class WrDatePicker implements FormValueControl<Date | null> {
130
131
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<WrDatePicker, "wr-date-picker", never, { "mode": { "alias": "mode"; "required": false; "isSignal": true; }; "format": { "alias": "format"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "min": { "alias": "min"; "required": false; "isSignal": true; }; "max": { "alias": "max"; "required": false; "isSignal": true; }; "dateFilter": { "alias": "dateFilter"; "required": false; "isSignal": true; }; "timeFormat": { "alias": "timeFormat"; "required": false; "isSignal": true; }; "showSeconds": { "alias": "showSeconds"; "required": false; "isSignal": true; }; "step": { "alias": "step"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "readonly": { "alias": "readonly"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; "touch": "touch"; }, never, never, true, never>;
131
132
  }
132
133
 
133
- export { WrDatePicker };
134
+ /**
135
+ * @license
136
+ *
137
+ * Use of this source code is governed by an MIT-style license that can be
138
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
139
+ */
140
+ /**
141
+ * The `<wr-date-range-picker>` value — `[start, end]`.
142
+ *
143
+ * Either end can be `null` while the range is half-picked, so a partially
144
+ * filled range is still representable. Structurally identical to
145
+ * `WrCalendarRange`, which the picker forwards it to.
146
+ */
147
+ type WrDateRange = readonly [Date | null, Date | null];
148
+
149
+ /**
150
+ * Date-range picker — two text inputs sharing one range calendar.
151
+ *
152
+ * Separate from `<wr-date-picker>` because the value is a different type: a
153
+ * range is `[start, end]`, and folding that into the single picker's
154
+ * `Date | null` would break `[formField]` inference for every existing usage.
155
+ * Everything else matches — same input skeleton, same overlay, same
156
+ * {@link WrDateAdapter} formats.
157
+ *
158
+ * - `'date'` (default) — popover renders a range calendar. Picking the second
159
+ * date closes the overlay.
160
+ * - `'datetime'` — popover adds a time stepper per end. Picking dates does NOT
161
+ * close, since the user typically sets the times next.
162
+ *
163
+ * A signal-forms native control: it implements
164
+ * `FormValueControl<WrDateRange | null>`, so `[formField]` binds straight to
165
+ * its `value` model. `[(value)]` works standalone, and classic `[(ngModel)]` /
166
+ * reactive forms keep working through Angular's bridge.
167
+ *
168
+ * Either end may be `null` while the range is half-picked. Out-of-order ends
169
+ * are swapped on commit, matching the calendar's own behaviour.
170
+ *
171
+ * @example
172
+ * ```html
173
+ * <!-- signal forms -->
174
+ * <wr-date-range-picker [formField]="form.period" format="dd.MM.yyyy" />
175
+ *
176
+ * <!-- standalone two-way binding -->
177
+ * <wr-date-range-picker [(value)]="period" [minDate]="minDate" />
178
+ *
179
+ * <!-- date + time on both ends -->
180
+ * <wr-date-range-picker mode="datetime" [(value)]="window" timeFormat="24h" />
181
+ * ```
182
+ *
183
+ * @see https://ngwr.dev/reference/components/date-picker
184
+ */
185
+ declare class WrDateRangePicker implements FormValueControl<WrDateRange | null> {
186
+ /** Picker behavior — see class doc. @default 'date' */
187
+ readonly mode: _angular_core.InputSignal<"date" | "datetime">;
188
+ /**
189
+ * Format used for both display and parsing, on both ends. When `null`
190
+ * (default), it is derived from `mode` (`shortDate` / `shortDateTime`). Pass
191
+ * a named key or raw token string to override.
192
+ */
193
+ readonly format: _angular_core.InputSignal<WrDateFormat | (string & {}) | null>;
194
+ /** Placeholder for the start input. */
195
+ readonly startPlaceholder: _angular_core.InputSignal<string>;
196
+ /** Placeholder for the end input. */
197
+ readonly endPlaceholder: _angular_core.InputSignal<string>;
198
+ /** Glyph rendered between the two inputs. @default '–' */
199
+ readonly separator: _angular_core.InputSignal<string>;
200
+ /** Min selectable date, applied to both ends. */
201
+ readonly minDate: _angular_core.InputSignal<Date | null>;
202
+ /** Max selectable date, applied to both ends. */
203
+ readonly maxDate: _angular_core.InputSignal<Date | null>;
204
+ /** Predicate to disable specific dates (forwarded to the calendar). */
205
+ readonly dateFilter: _angular_core.InputSignal<((date: Date) => boolean) | null>;
206
+ /** Time-panel 12 / 24-hour format. Applies in `datetime` mode. @default 'auto' */
207
+ readonly timeFormat: _angular_core.InputSignal<"auto" | "12h" | "24h">;
208
+ /** Render the seconds column. Applies in `datetime` mode. @default false */
209
+ readonly showSeconds: _angular_core.InputSignalWithTransform<boolean, any>;
210
+ /** Minute / second step for the time panels. @default 1 */
211
+ readonly step: _angular_core.InputSignalWithTransform<number, unknown>;
212
+ /**
213
+ * Disable interaction. Bound automatically from the field's disabled state
214
+ * when used with `[formField]`.
215
+ *
216
+ * @default false
217
+ */
218
+ readonly disabled: _angular_core.InputSignalWithTransform<boolean, any>;
219
+ /** Read-only — inputs not typeable, but the trigger icon still opens the overlay. @default false */
220
+ readonly readonly: _angular_core.InputSignalWithTransform<boolean, any>;
221
+ private readonly adapter;
222
+ private readonly overlay;
223
+ private readonly outsideClick;
224
+ private readonly scrollStrategies;
225
+ private readonly host;
226
+ private readonly destroyRef;
227
+ protected readonly startEl: _angular_core.Signal<ElementRef<HTMLInputElement>>;
228
+ /** The picked range. Bound by `[formField]`, or two-way via `[(value)]`. */
229
+ readonly value: _angular_core.ModelSignal<WrDateRange | null>;
230
+ /** Emitted on blur so a bound field can mark itself touched. */
231
+ readonly touch: _angular_core.OutputEmitterRef<void>;
232
+ /** Text currently in each input (may be partial / invalid mid-type). */
233
+ protected readonly startText: _angular_core.WritableSignal<string>;
234
+ protected readonly endText: _angular_core.WritableSignal<string>;
235
+ /** Whether the popover is currently open. */
236
+ protected readonly overlayOpen: _angular_core.WritableSignal<boolean>;
237
+ /** Resolved format — falls back to a mode-appropriate default. */
238
+ protected readonly resolvedFormat: _angular_core.Signal<string>;
239
+ protected readonly isDateTime: _angular_core.Signal<boolean>;
240
+ protected readonly triggerLabel: string;
241
+ protected readonly startLabel: string;
242
+ protected readonly endLabel: string;
243
+ protected readonly classes: _angular_core.Signal<string>;
244
+ private overlayRef;
245
+ /** Live panel ref while the popover is open — lets typed edits move the
246
+ * displayed month in real time (not just on reopen). */
247
+ private readonly panelRef;
248
+ /** Last range we pushed into the model ourselves, so the sync effect can skip
249
+ * the echo of our own edits and never reformat text mid-type. */
250
+ private lastValue;
251
+ constructor();
252
+ protected onStartInput(event: Event): void;
253
+ protected onEndInput(event: Event): void;
254
+ protected onBlur(event: FocusEvent): void;
255
+ /** Called by an input's click — opens the overlay if it isn't open already. */
256
+ protected openOnInput(): void;
257
+ protected toggleOverlay(): void;
258
+ private onRangeInput;
259
+ private openOverlay;
260
+ /**
261
+ * Close the popover. `restoreFocus` returns focus to the start input — needed
262
+ * when the overlay is dismissed while focus lives inside it (picking the
263
+ * second date), because disposing then leaves focus on a removed element and
264
+ * the browser falls back to `<body>`.
265
+ */
266
+ private closeOverlay;
267
+ private dispose;
268
+ /**
269
+ * Push to the model and refresh the text of whichever ends changed. Returns
270
+ * what was actually committed.
271
+ *
272
+ * `normalise` swaps out-of-order ends. The calendar and blur ask for it; a
273
+ * keystroke does not, because reordering while the user is still typing would
274
+ * move their in-progress text into the other input.
275
+ */
276
+ private commitRange;
277
+ private current;
278
+ private display;
279
+ private parseText;
280
+ /** Keep `[start, end]` in chronological order once both ends exist. */
281
+ private normalise;
282
+ /**
283
+ * Chronological comparison at the precision the mode needs. The adapter's
284
+ * `compareDate` is day-precision by contract, which would leave an inverted
285
+ * same-day `datetime` range (18:00 → 09:00) unswapped.
286
+ */
287
+ private compareEnds;
288
+ /**
289
+ * Whether a typed date must be rejected. Covers `dateFilter` as well as the
290
+ * bounds — the calendar disables filtered days, so accepting them from the
291
+ * keyboard would make the two entry paths disagree.
292
+ */
293
+ private isOutOfBounds;
294
+ private sameDate;
295
+ private sameRange;
296
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<WrDateRangePicker, never>;
297
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<WrDateRangePicker, "wr-date-range-picker", never, { "mode": { "alias": "mode"; "required": false; "isSignal": true; }; "format": { "alias": "format"; "required": false; "isSignal": true; }; "startPlaceholder": { "alias": "startPlaceholder"; "required": false; "isSignal": true; }; "endPlaceholder": { "alias": "endPlaceholder"; "required": false; "isSignal": true; }; "separator": { "alias": "separator"; "required": false; "isSignal": true; }; "minDate": { "alias": "minDate"; "required": false; "isSignal": true; }; "maxDate": { "alias": "maxDate"; "required": false; "isSignal": true; }; "dateFilter": { "alias": "dateFilter"; "required": false; "isSignal": true; }; "timeFormat": { "alias": "timeFormat"; "required": false; "isSignal": true; }; "showSeconds": { "alias": "showSeconds"; "required": false; "isSignal": true; }; "step": { "alias": "step"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "readonly": { "alias": "readonly"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; "touch": "touch"; }, never, never, true, never>;
298
+ }
299
+
300
+ export { WrDatePicker, WrDateRangePicker };
301
+ export type { WrDateRange };
@@ -90,7 +90,14 @@ declare class WrDensityDirective {
90
90
  * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
91
91
  */
92
92
 
93
- /** Configure {@link WrDensity}. All fields optional — merged with defaults. */
93
+ /**
94
+ * Configure and activate {@link WrDensity}. All fields optional — merged with
95
+ * defaults.
96
+ *
97
+ * The initializer instantiates {@link WrDensity} so its constructor effect can
98
+ * write `[data-wr-density]` onto `<html>`. Without it the tree-shakable service
99
+ * was never created and `defaultDensity` had no effect at all.
100
+ */
94
101
  declare function provideWrDensity(config?: Partial<WrDensityConfig>): EnvironmentProviders;
95
102
 
96
103
  export { DEFAULT_WR_DENSITY_CONFIG, WR_DENSITY_CONFIG, WrDensity, WrDensityDirective, provideWrDensity };