@ultimat3/ui 22.14.0 → 23.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 (47) hide show
  1. package/CATALOG.md +43 -12
  2. package/CLAUDE.md +13 -49
  3. package/README.md +59 -4
  4. package/package.json +6 -5
  5. package/src/catalog/parse-component.ts +85 -12
  6. package/src/catalog/render-catalog.ts +21 -1
  7. package/src/components/Avatar.module.scss +1 -1
  8. package/src/components/BarChart.module.scss +12 -1
  9. package/src/components/BarChart.tsx +41 -15
  10. package/src/components/Button.tsx +3 -8
  11. package/src/components/Card.module.scss +1 -1
  12. package/src/components/Checkbox.module.scss +4 -4
  13. package/src/components/Checkbox.tsx +6 -0
  14. package/src/components/CommandPalette.module.scss +1 -1
  15. package/src/components/DataTable.module.scss +2 -0
  16. package/src/components/DataTable.tsx +90 -29
  17. package/src/components/Dialog.module.scss +2 -2
  18. package/src/components/Dropzone.module.scss +3 -3
  19. package/src/components/FileInput.module.scss +1 -1
  20. package/src/components/Form.module.scss +2 -2
  21. package/src/components/Input.module.scss +1 -1
  22. package/src/components/Link.tsx +64 -12
  23. package/src/components/Pagination.tsx +89 -37
  24. package/src/components/Radio.module.scss +2 -2
  25. package/src/components/Select.module.scss +3 -3
  26. package/src/components/Skeleton.module.scss +1 -1
  27. package/src/components/Spinner.module.scss +3 -3
  28. package/src/components/Switch.module.scss +2 -2
  29. package/src/components/Table.module.scss +2 -2
  30. package/src/components/Tabs.module.scss +5 -5
  31. package/src/components/ThemeToggle.module.scss +1 -1
  32. package/src/components/Toast.module.scss +2 -2
  33. package/src/components/Tooltip.module.scss +1 -1
  34. package/src/components/bar-chart-view.ts +29 -3
  35. package/src/components/button-classes.ts +25 -0
  36. package/src/components/link-classes.ts +38 -0
  37. package/src/components/link-target.ts +13 -0
  38. package/src/index.ts +13 -3
  39. package/src/sass-probe.ts +55 -0
  40. package/src/tokens/_index.scss +3 -1
  41. package/src/tokens/_mixins.scss +56 -8
  42. package/src/tokens/_motion.scss +4 -0
  43. package/src/tokens/_stroke.scss +20 -0
  44. package/src/tokens/_units.scss +47 -0
  45. package/src/tokens/reset.scss +4 -4
  46. package/src/tokens/theme.scss +2 -0
  47. package/src/tokens/tokens.ts +6 -0
@@ -4,15 +4,22 @@
4
4
 
5
5
  @use 'sass:list';
6
6
  @use 'sass:map';
7
+ @use 'sass:string';
7
8
  @use 'breakpoints';
8
9
  @use 'colors';
9
10
  @use 'motion';
10
11
  @use 'radius';
11
12
  @use 'space';
13
+ @use 'stroke';
12
14
  @use 'typography';
15
+ @use 'units';
13
16
 
14
17
  /// Keyboard-only focus, drawn from the accent role so it themes automatically.
15
- @mixin focus-ring($role: 'accent', $width: 2px, $offset: 2px) {
18
+ @mixin focus-ring(
19
+ $role: 'accent',
20
+ $width: stroke.stroke(thick),
21
+ $offset: stroke.stroke(thick)
22
+ ) {
16
23
  &:focus-visible {
17
24
  outline: $width solid colors.role($role);
18
25
  outline-offset: $offset;
@@ -81,13 +88,54 @@
81
88
  container-name: $name;
82
89
  }
83
90
 
84
- /// Viewport breakpoint, min-width only. Named rungs, never a raw px value.
91
+ // --- Viewport breakpoints ----------------------------------------------------
92
+ // Three intents over ONE map, so a rung is defined in one place: at and above (`respond-to`),
93
+ // below (`respond-down`), and from one rung up to the next (`respond-between`). Prefer a container
94
+ // query to any of them — reach here only when the layout depends on the viewport itself.
95
+
96
+ /// What a max-width arm stops short of its rung by. `max-width: 768px` and `min-width: 768px` both
97
+ /// match at exactly 768px, which is how one rule ends up applying in two ranges at once; 0.02px is
98
+ /// below any width a browser reports and above the rounding that would collapse it.
99
+ $-under: 0.02px;
100
+
101
+ /// A rung's width. Compared as TEXT as well as by value: `2xl` in a map is the number 2 with the
102
+ /// unit `xl`, so the string `'2xl'` every stylesheet writes is a different key to `map.get`.
103
+ @function -rung($bp, $mixin) {
104
+ @each $name, $width in breakpoints.$breakpoints {
105
+ @if '#{$name}' == '#{$bp}' {
106
+ @return $width;
107
+ }
108
+ }
109
+ @error string.unquote('X_TOKEN_UNKNOWN: #{$mixin}("#{$bp}") — that breakpoint is not in $breakpoints. fix: use one of #{map.keys(breakpoints.$breakpoints)}');
110
+ }
111
+
112
+ /// At a rung and wider. Named rungs, never a raw px value.
85
113
  @mixin respond-to($bp) {
86
- @if not map.has-key(breakpoints.$breakpoints, $bp) {
87
- @error 'X_TOKEN_UNKNOWN: breakpoint "#{$bp}" is not in $breakpoints. fix: add it to src/tokens/_breakpoints.scss';
114
+ @media (min-width: -rung($bp, 'respond-to')) {
115
+ @content;
116
+ }
117
+ }
118
+
119
+ /// Narrower than a rung — the complement of `respond-to` on the same rung, with no shared pixel.
120
+ @mixin respond-down($bp) {
121
+ @media (max-width: (-rung($bp, 'respond-down') - $-under)) {
122
+ @content;
123
+ }
124
+ }
125
+
126
+ /// From `$from` up to, and not including, `$to`.
127
+ @mixin respond-between($from, $to) {
128
+ $min: -rung($from, 'respond-between');
129
+ $max: -rung($to, 'respond-between');
130
+
131
+ @if $min == $max {
132
+ @error string.unquote('X_UI_INVALID_VALUE: respond-between("#{$from}", "#{$to}") is an empty range — both ends are #{$min}. fix: write respond-to("#{$from}"), or name a higher rung as $to');
133
+ }
134
+ @if $min > $max {
135
+ @error string.unquote('X_UI_INVALID_VALUE: respond-between("#{$from}", "#{$to}") is reversed — #{$min} is not below #{$max}. fix: write respond-between("#{$to}", "#{$from}")');
88
136
  }
89
137
 
90
- @media (min-width: map.get(breakpoints.$breakpoints, $bp)) {
138
+ @media (min-width: $min) and (max-width: ($max - $-under)) {
91
139
  @content;
92
140
  }
93
141
  }
@@ -156,8 +204,8 @@
156
204
  scrollbar-color: colors.role($thumb) colors.role($track);
157
205
 
158
206
  &::-webkit-scrollbar {
159
- width: 10px;
160
- height: 10px;
207
+ width: units.rem(10px);
208
+ height: units.rem(10px);
161
209
  }
162
210
 
163
211
  &::-webkit-scrollbar-track {
@@ -165,7 +213,7 @@
165
213
  }
166
214
 
167
215
  &::-webkit-scrollbar-thumb {
168
- border: 2px solid transparent;
216
+ border: stroke.stroke(thick) solid transparent;
169
217
  border-radius: radius.radius(pill);
170
218
  background: colors.role($thumb);
171
219
  background-clip: content-box;
@@ -16,6 +16,10 @@ $easing: (
16
16
  spring: cubic-bezier(0.34, 1.56, 0.64, 1),
17
17
  );
18
18
 
19
+ /// What reduced motion collapses every duration to. Never `0`: an animation that takes no time
20
+ /// fires no `animationend`, and a component waiting on that event waits forever.
21
+ $still: 0.01ms;
22
+
19
23
  @mixin emit {
20
24
  :root {
21
25
  @each $name, $value in $duration {
@@ -0,0 +1,20 @@
1
+ // Line weights: a border, an outline, a focus ring, an indicator. px on purpose — a line that
2
+ // grew with the reader's text size would be a different design at 200% zoom, not a bigger one.
3
+
4
+ $stroke: (
5
+ hairline: 1px,
6
+ thick: 2px,
7
+ heavy: 3px,
8
+ );
9
+
10
+ @mixin emit {
11
+ :root {
12
+ @each $name, $value in $stroke {
13
+ --stroke-#{$name}: #{$value};
14
+ }
15
+ }
16
+ }
17
+
18
+ @function stroke($name) {
19
+ @return var(--stroke-#{$name});
20
+ }
@@ -0,0 +1,47 @@
1
+ // Computed lengths: px → rem, and one fluid `clamp()` between two viewport widths. Functions only —
2
+ // nothing here emits CSS. The scale maps restate a constant; these compute a value from one.
3
+
4
+ @use 'sass:math';
5
+ @use 'sass:meta';
6
+ @use 'sass:string';
7
+
8
+ /// The one conversion both functions read, so `fluid(16px, 32px)` and `rem(16px)` cannot disagree
9
+ /// about what a px is — and so a refusal names the function the author actually called.
10
+ @function -to-rem($value, $root, $caller, $example) {
11
+ @if meta.type-of($value) != 'number' or not (math.is-unitless($value) or math.unit($value) == 'px' or math.unit($value) == 'rem') {
12
+ @error string.unquote('X_UI_INVALID_VALUE: #{$caller}() expected px, rem or a unitless px count — got `#{meta.inspect($value)}`. fix: pass a px length, e.g. #{$example}');
13
+ }
14
+ @if math.is-unitless($value) {
15
+ @return math.div($value * 1px, $root) * 1rem;
16
+ }
17
+ @if math.unit($value) == 'rem' {
18
+ @return $value;
19
+ }
20
+ @return math.div($value, $root) * 1rem;
21
+ }
22
+
23
+ /// px → rem against the root size. Lengths are rem so a layout holds at 200% text zoom; a unit
24
+ /// that cannot be converted is a build error rather than a size that is subtly off.
25
+ @function rem($value, $root: 16px) {
26
+ @return -to-rem($value, $root, 'rem', 'rem(24px)');
27
+ }
28
+
29
+ /// One `clamp()` with a real slope and intercept: `$min` at a viewport `$from` wide, `$max` at
30
+ /// `$to`, linear between and held outside. Type and space then scale with the viewport instead of
31
+ /// stepping at a breakpoint. `$min` above `$max` is a size that shrinks, and is legal.
32
+ @function fluid($min, $max, $from: 20rem, $to: 80rem) {
33
+ $min: -to-rem($min, 16px, 'fluid', 'fluid(16px, 32px)');
34
+ $max: -to-rem($max, 16px, 'fluid', 'fluid(16px, 32px)');
35
+ $from: -to-rem($from, 16px, 'fluid', 'fluid(16px, 32px)');
36
+ $to: -to-rem($to, 16px, 'fluid', 'fluid(16px, 32px)');
37
+
38
+ @if $from >= $to {
39
+ @error string.unquote('X_UI_INVALID_VALUE: fluid() needs $from below $to — got `#{$from}` and `#{$to}`. fix: pass the narrower viewport first, e.g. fluid(1rem, 2rem, 20rem, 80rem)');
40
+ }
41
+
42
+ $slope: math.div($max - $min, $to - $from);
43
+ $intercept: $min - $slope * $from;
44
+ // `clamp(MIN, VAL, MAX)` with MIN above MAX answers MIN at every width, so the bounds are
45
+ // ordered here rather than trusted to be.
46
+ @return clamp(math.min($min, $max), $intercept + $slope * 100vw, math.max($min, $max));
47
+ }
@@ -115,8 +115,8 @@
115
115
 
116
116
  // Themed keyboard focus for anything the component layer forgot.
117
117
  :where(:focus-visible) {
118
- outline: 2px solid t.role('accent');
119
- outline-offset: 2px;
118
+ outline: t.stroke(thick) solid t.role('accent');
119
+ outline-offset: t.stroke(thick);
120
120
  }
121
121
 
122
122
  :where(:focus:not(:focus-visible)) {
@@ -153,9 +153,9 @@ html:has(dialog:modal) {
153
153
  *,
154
154
  *::before,
155
155
  *::after {
156
- animation-duration: 0.01ms !important;
156
+ animation-duration: t.$still !important;
157
157
  animation-iteration-count: 1 !important;
158
- transition-duration: 0.01ms !important;
158
+ transition-duration: t.$still !important;
159
159
  scroll-behavior: auto !important;
160
160
  }
161
161
  }
@@ -7,6 +7,7 @@
7
7
  @use 'radius';
8
8
  @use 'shadow';
9
9
  @use 'space';
10
+ @use 'stroke';
10
11
  @use 'typography';
11
12
  @use 'z';
12
13
 
@@ -14,6 +15,7 @@
14
15
  @include shadow.emit;
15
16
  @include space.emit;
16
17
  @include radius.emit;
18
+ @include stroke.emit;
17
19
  @include typography.emit;
18
20
  @include motion.emit;
19
21
  @include z.emit;
@@ -119,6 +119,12 @@ export const radiusTokens = {
119
119
 
120
120
  export type RadiusName = keyof typeof radiusTokens;
121
121
 
122
+ export const strokeTokens = {
123
+ hairline: '1px',
124
+ thick: '2px',
125
+ heavy: '3px',
126
+ } as const;
127
+
122
128
  export const zTokens = {
123
129
  base: '0',
124
130
  raised: '10',