@nsaa/core 0.1.0-alpha.3 → 0.1.0-alpha.5

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 (49) hide show
  1. package/LICENSE +1 -1
  2. package/NOTICE +27 -0
  3. package/dist/alert/alert.d.ts +27 -3
  4. package/dist/alert/alert.js +198 -17
  5. package/dist/alert/index.d.ts +2 -2
  6. package/dist/badge/badge.d.ts +2 -2
  7. package/dist/badge/badge.js +1 -1
  8. package/dist/button/button.d.ts +2 -2
  9. package/dist/button/button.js +2 -1
  10. package/dist/date-range-picker/date-range-picker.d.ts +55 -0
  11. package/dist/date-range-picker/date-range-picker.js +359 -0
  12. package/dist/date-range-picker/index.d.ts +4 -0
  13. package/dist/date-range-picker/presets.d.ts +25 -0
  14. package/dist/date-range-picker/presets.js +115 -0
  15. package/dist/empty/empty.d.ts +1 -1
  16. package/dist/empty/empty.js +2 -2
  17. package/dist/index.d.ts +17 -4
  18. package/dist/index.js +12 -5
  19. package/dist/input-group/input-group.d.ts +1 -1
  20. package/dist/input-otp/input-otp.js +1 -1
  21. package/dist/lib/dev.js +14 -0
  22. package/dist/meter/index.d.ts +3 -0
  23. package/dist/meter/meter.d.ts +33 -0
  24. package/dist/meter/meter.js +145 -0
  25. package/dist/money/index.d.ts +3 -0
  26. package/dist/money/money.d.ts +28 -0
  27. package/dist/money/money.js +77 -0
  28. package/dist/progress/index.d.ts +3 -0
  29. package/dist/progress/progress.d.ts +8 -0
  30. package/dist/progress/progress.js +43 -0
  31. package/dist/sidebar/index.d.ts +3 -0
  32. package/dist/sidebar/sidebar.d.ts +66 -0
  33. package/dist/sidebar/sidebar.js +449 -0
  34. package/dist/sidebar/use-mobile.js +13 -0
  35. package/dist/sources/alert.css +20 -0
  36. package/dist/sources/date-range-picker.css +29 -0
  37. package/dist/sources/meter.css +3 -0
  38. package/dist/sources/money.css +3 -0
  39. package/dist/sources/progress.css +3 -0
  40. package/dist/sources/sidebar.css +32 -0
  41. package/dist/sources/stat.css +4 -0
  42. package/dist/stat/index.d.ts +3 -0
  43. package/dist/stat/stat.d.ts +47 -0
  44. package/dist/stat/stat.js +161 -0
  45. package/dist/tokens/dist/motion.js +19 -1
  46. package/dist/toolbar/toolbar-item.js +1 -1
  47. package/dist/toolbar/toolbar.js +2 -2
  48. package/package.json +5 -2
  49. package/src/feel.css +131 -3
@@ -0,0 +1,161 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.js";
3
+ import { Skeleton } from "../skeleton/skeleton.js";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { mergeProps } from "@base-ui/react/merge-props";
6
+ import { useRender } from "@base-ui/react/use-render";
7
+ import * as React from "react";
8
+ // src/stat/stat.tsx
9
+ const StatContext = React.createContext({
10
+ loading: false,
11
+ size: "default"
12
+ });
13
+ /** While loading, a part keeps its own line box and shows a Skeleton in it, so the tile does not
14
+ change height when the data arrives. */
15
+ function Slot({ width, height = "h-[1lh]", children }) {
16
+ const { loading } = React.useContext(StatContext);
17
+ return loading ? /* @__PURE__ */ jsx(Skeleton, { className: cn("inline-block align-top", height, width) }) : children;
18
+ }
19
+ function Stat({ className, size = "default", variant = "card", loading = false, children, ...props }) {
20
+ return /* @__PURE__ */ jsx(StatContext.Provider, {
21
+ value: {
22
+ loading,
23
+ size
24
+ },
25
+ children: /* @__PURE__ */ jsx("div", {
26
+ "data-slot": "stat",
27
+ "data-size": size,
28
+ "data-variant": variant,
29
+ "aria-busy": loading || void 0,
30
+ className: cn("group/stat @container/stat relative flex min-w-0 flex-col gap-1 text-sm text-card-foreground", variant === "card" && "rounded-xl bg-card ring-1 ring-foreground/10", variant === "card" && (size === "sm" ? "p-3" : "p-4"), className),
31
+ ...props,
32
+ children
33
+ })
34
+ });
35
+ }
36
+ function StatLabel({ className, children, ...props }) {
37
+ return /* @__PURE__ */ jsx("div", {
38
+ "data-slot": "stat-label",
39
+ className: cn("pe-8 font-medium text-muted-foreground", className),
40
+ ...props,
41
+ children: /* @__PURE__ */ jsx(Slot, {
42
+ width: "w-24",
43
+ children
44
+ })
45
+ });
46
+ }
47
+ /** Tabular figures; a long value drops one type step in a narrow tile instead of truncating. */
48
+ function StatValue({ className, children, ...props }) {
49
+ const { size } = React.useContext(StatContext);
50
+ return /* @__PURE__ */ jsx("div", {
51
+ "data-slot": "stat-value",
52
+ className: cn("font-semibold tracking-tight tabular-nums", size === "sm" ? "text-xl @max-[12rem]/stat:text-lg" : "text-2xl @max-[14rem]/stat:text-xl", className),
53
+ ...props,
54
+ children: /* @__PURE__ */ jsx(Slot, {
55
+ width: "w-32",
56
+ children
57
+ })
58
+ });
59
+ }
60
+ const ARROW = {
61
+ up: "M12 19V5m-6 6 6-6 6 6",
62
+ down: "M12 5v14m6-6-6 6-6-6",
63
+ flat: "M5 12h14"
64
+ };
65
+ const TONE = {
66
+ positive: "bg-positive-subtle text-on-positive-subtle-foreground",
67
+ negative: "bg-danger-subtle text-on-danger-subtle-foreground",
68
+ neutral: "bg-muted text-muted-foreground"
69
+ };
70
+ function StatChange({ className, direction, tone, value, label, labels, ...props }) {
71
+ const resolved = tone ?? (direction === "up" ? "positive" : direction === "down" ? "negative" : "neutral");
72
+ const words = {
73
+ up: "up",
74
+ down: "down",
75
+ flat: "unchanged",
76
+ ...labels
77
+ };
78
+ return /* @__PURE__ */ jsx("div", {
79
+ "data-slot": "stat-change",
80
+ "data-direction": direction,
81
+ "data-tone": resolved,
82
+ className: cn("flex flex-wrap items-center gap-x-1.5 gap-y-0.5 text-xs", className),
83
+ ...props,
84
+ children: /* @__PURE__ */ jsxs(Slot, {
85
+ width: "w-28",
86
+ height: "h-5",
87
+ children: [/* @__PURE__ */ jsxs("span", {
88
+ className: cn("inline-flex items-center gap-0.5 rounded-md px-1.5 py-0.5 font-medium tabular-nums", TONE[resolved]),
89
+ children: [
90
+ /* @__PURE__ */ jsx("svg", {
91
+ xmlns: "http://www.w3.org/2000/svg",
92
+ viewBox: "0 0 24 24",
93
+ fill: "none",
94
+ stroke: "currentColor",
95
+ strokeWidth: "2",
96
+ strokeLinecap: "round",
97
+ strokeLinejoin: "round",
98
+ "aria-hidden": "true",
99
+ className: "size-3",
100
+ children: /* @__PURE__ */ jsx("path", { d: ARROW[direction] })
101
+ }),
102
+ /* @__PURE__ */ jsxs("span", {
103
+ className: "sr-only",
104
+ children: [words[direction], " "]
105
+ }),
106
+ value
107
+ ]
108
+ }), label && /* @__PURE__ */ jsx("span", {
109
+ className: "text-muted-foreground",
110
+ children: label
111
+ })]
112
+ })
113
+ });
114
+ }
115
+ function StatDescription({ className, children, ...props }) {
116
+ return /* @__PURE__ */ jsx("div", {
117
+ "data-slot": "stat-description",
118
+ className: cn("text-muted-foreground", className),
119
+ ...props,
120
+ children: /* @__PURE__ */ jsx(Slot, {
121
+ width: "w-40",
122
+ children
123
+ })
124
+ });
125
+ }
126
+ function StatIcon({ className, ...props }) {
127
+ return /* @__PURE__ */ jsx("div", {
128
+ "data-slot": "stat-icon",
129
+ "aria-hidden": "true",
130
+ className: cn("absolute end-4 top-4 text-muted-foreground group-data-[size=sm]/stat:end-3 group-data-[size=sm]/stat:top-3 [&_svg:not([class*='size-'])]:size-4", className),
131
+ ...props
132
+ });
133
+ }
134
+ /** A link or button at the foot of the tile. Pass `render={<a href="…" />}` for a link. */
135
+ function StatAction({ className, render, cover = false, ...props }) {
136
+ const { loading } = React.useContext(StatContext);
137
+ const element = useRender({
138
+ defaultTagName: "button",
139
+ props: mergeProps({ className: cn("mt-1 w-fit rounded-sm text-sm font-medium underline-offset-4 outline-none hover:underline focus-visible:ring-3 focus-visible:ring-ring/50", cover && "after:absolute after:inset-0 after:rounded-xl", className) }, render ? props : {
140
+ type: "button",
141
+ ...props
142
+ }),
143
+ render,
144
+ state: { slot: "stat-action" }
145
+ });
146
+ if (loading) return /* @__PURE__ */ jsx(Skeleton, { className: "mt-1 h-[1lh] w-12 text-sm" });
147
+ return element;
148
+ }
149
+ /** Lays tiles out by its own width (1 to 4 columns), so a sidebar never breaks the grid. */
150
+ function StatGroup({ className, dividers = false, children, ...props }) {
151
+ return /* @__PURE__ */ jsx("div", {
152
+ "data-slot": "stat-group",
153
+ className: cn("@container/stat-group", className),
154
+ ...props,
155
+ children: /* @__PURE__ */ jsx("div", {
156
+ className: cn("grid grid-cols-1 @lg/stat-group:grid-cols-2 @3xl/stat-group:grid-cols-3 @5xl/stat-group:grid-cols-4", dividers ? "gap-px overflow-hidden rounded-xl bg-border ring-1 ring-foreground/10 *:data-[slot=stat]:bg-card *:data-[slot=stat]:p-4" : "gap-4"),
157
+ children
158
+ })
159
+ });
160
+ }
161
+ export { Stat, StatAction, StatChange, StatDescription, StatGroup, StatIcon, StatLabel, StatValue };
@@ -36,6 +36,24 @@ const spring = {
36
36
  "bounce": 0
37
37
  }
38
38
  };
39
+ const duration = {
40
+ "instant": 0,
41
+ "press": 150,
42
+ "tooltip": 125,
43
+ "tooltip-exit": 100,
44
+ "exit": 100,
45
+ "char": 120,
46
+ "fast": 150,
47
+ "base": 200,
48
+ "slow": 250,
49
+ "drawer": 300,
50
+ "chunk": 300,
51
+ "marketing": 400,
52
+ "marketing-slow": 500,
53
+ "spin": 800,
54
+ "caret": 1250,
55
+ "feedback-hold": 1500
56
+ };
39
57
  const scale = {
40
58
  "press": .97,
41
59
  "enter": .95,
@@ -123,4 +141,4 @@ const sound = {
123
141
  "progress.loop": "loading"
124
142
  }
125
143
  };
126
- export { blur, loader, scale, sound, spring };
144
+ export { blur, duration, loader, scale, sound, spring };
@@ -3,8 +3,8 @@ import { cn } from "../lib/cn.js";
3
3
  import { useControlDefaults } from "../lib/control-defaults.js";
4
4
  import { Button } from "../button/button.js";
5
5
  import { Kbd } from "../kbd/kbd.js";
6
- import { ariaShortcut, formatShortcut } from "../lib/shortcut.js";
7
6
  import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip.js";
7
+ import { ariaShortcut, formatShortcut } from "../lib/shortcut.js";
8
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
9
  import { Toggle } from "@base-ui/react/toggle";
10
10
  // src/toolbar/toolbar-item.tsx
@@ -2,14 +2,14 @@
2
2
  import { cn } from "../lib/cn.js";
3
3
  import { ControlDefaultsContext, useControlDefaults } from "../lib/control-defaults.js";
4
4
  import { buttonVariants } from "../button/button.js";
5
+ import { mergeRefs } from "../lib/merge-refs.js";
5
6
  import { Input } from "../input/input.js";
6
7
  import { DropdownMenuCheckboxItem, DropdownMenuItem, DropdownMenuShortcut } from "../dropdown-menu/dropdown-menu.js";
8
+ import { TooltipProvider } from "../tooltip/tooltip.js";
7
9
  import { tv } from "../lib/tv.js";
8
10
  import { ariaShortcut, formatShortcut } from "../lib/shortcut.js";
9
- import { TooltipProvider } from "../tooltip/tooltip.js";
10
11
  import { ToolbarItem } from "./toolbar-item.js";
11
12
  import { OverflowContext, OverflowMore, OverflowStore, useOverflowUnit } from "./overflow.js";
12
- import { mergeRefs } from "../lib/merge-refs.js";
13
13
  import { jsx, jsxs } from "react/jsx-runtime";
14
14
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
15
15
  import { Toolbar } from "@base-ui/react/toolbar";
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@nsaa/core",
3
- "version": "0.1.0-alpha.3",
3
+ "version": "0.1.0-alpha.5",
4
4
  "description": "Nsaa core components: accessible, token-driven, animated to the Nsaa feel spec.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "sideEffects": [
8
8
  "**/*.css",
9
+ "./dist/index.js",
10
+ "./src/index.ts",
9
11
  "./dist/lib/track-input-modality.js",
12
+ "./src/lib/track-input-modality.ts",
10
13
  "./dist/spinner/spinner.js"
11
14
  ],
12
15
  "files": [
@@ -50,7 +53,7 @@
50
53
  "tailwind-merge": "^3.7.0",
51
54
  "tailwind-variants": "^3.3.1",
52
55
  "uisfx": "0.4.0",
53
- "@nsaa/tokens": "^0.1.0-alpha.3"
56
+ "@nsaa/tokens": "^0.1.0-alpha.5"
54
57
  },
55
58
  "devDependencies": {
56
59
  "@testing-library/jest-dom": "^7.0.1",
package/src/feel.css CHANGED
@@ -19,6 +19,29 @@
19
19
  * own unlayered CSS still wins.
20
20
  */
21
21
 
22
+ /* ------------------------------------------------------------------ controls set their text colour (#155)
23
+ shadcn leaves these inherited, so a page with a hard-coded text colour made Input and the outline
24
+ Button unreadable in dark mode (Corvta). Every form control, and every Button / Toggle variant that
25
+ paints a surface, takes its paired foreground at rest. Tailwind's components layer, below
26
+ utilities: a variant's own text-* class, hover and placeholder colours, and an app's className
27
+ still win. Ghost and link stay inherited by design (they sit on someone else's surface). */
28
+ @layer components {
29
+ :is(
30
+ [data-slot="input"],
31
+ [data-slot="textarea"],
32
+ [data-slot="select-trigger"],
33
+ [data-slot="native-select"],
34
+ [data-slot="input-group"],
35
+ [data-slot="input-group-control"],
36
+ [data-slot="input-otp-slot"],
37
+ [data-slot="combobox-chips"],
38
+ [data-slot="button"][data-variant="outline"],
39
+ [data-slot="toggle-group"][data-variant="outline"]
40
+ ) {
41
+ color: var(--foreground);
42
+ }
43
+ }
44
+
22
45
  /* ------------------------------------------------------------------ coarse-pointer hit areas (#144)
23
46
  DESIGN.md: 44 px targets on coarse pointers. An invisible ::before, centred, at least 44 × 44 and
24
47
  never smaller than the control; layout is unchanged. ::before because base-nova already uses
@@ -53,6 +76,17 @@
53
76
  }
54
77
  }
55
78
 
79
+ /* Alert links (#148): 44 px tall, their own width (the AlertLinks gap keeps them apart);
80
+ ::after, as ::before draws the separator. */
81
+ [data-slot="alert-link"]::after {
82
+ content: "";
83
+ position: absolute;
84
+ inset-inline: 0;
85
+ top: 50%;
86
+ height: calc(var(--spacing) * 11);
87
+ translate: 0 -50%;
88
+ }
89
+
56
90
  :is([data-slot="input"], [data-slot="input-group"]) {
57
91
  min-height: calc(var(--spacing) * 11);
58
92
  }
@@ -142,6 +176,25 @@
142
176
  }
143
177
  }
144
178
 
179
+ /* ---------------------------------------------------------------- Sidebar (#153)
180
+ Collapse and expand: shadcn's 200 ms linear -> duration.base, ease.inOut (an on-screen move).
181
+ base-nova animates the width and the container's left / right (layout exception, #153); the
182
+ rail's transition-all narrows to its translate and background. */
183
+ :is(
184
+ [data-slot="sidebar-gap"],
185
+ [data-slot="sidebar-container"],
186
+ [data-slot="sidebar-group-label"],
187
+ [data-slot="sidebar-menu-button"],
188
+ [data-slot="sidebar-rail"]
189
+ ) {
190
+ transition-duration: calc(var(--nsaa-duration-base) * var(--nsaa-time-scale));
191
+ transition-timing-function: var(--nsaa-ease-in-out);
192
+ }
193
+
194
+ [data-slot="sidebar-rail"] {
195
+ transition-property: translate, background-color;
196
+ }
197
+
145
198
  /* ---------------------------------------------------------------- Drawer
146
199
  shadcn's 450 ms → ease.drawer, drawer 300 in, base out; a swipe-dismiss releases on
147
200
  spring.sheet, scaled by Base UI's release strength. Stacking and swipe variables untouched. */
@@ -461,6 +514,13 @@
461
514
  }
462
515
  }
463
516
 
517
+ /* Dismiss (#148): opacity only, duration.fast, then it unmounts. No height animation (not a
518
+ named layout exception); instant under reduced motion and after keyboard input. */
519
+ [data-slot="alert"][data-ending-style] {
520
+ opacity: 0;
521
+ transition-duration: calc(var(--nsaa-duration-fast) * var(--nsaa-time-scale));
522
+ }
523
+
464
524
  /* ---------------------------------------------------------------- Avatar, Skeleton, Spinner
465
525
  A loaded image fades in 150; Skeleton keeps its pulse; Spinner turns at duration.spin. */
466
526
  /* SkeletonContent: what replaces a Skeleton fades in the same way (#128). */
@@ -474,6 +534,30 @@
474
534
  }
475
535
  }
476
536
 
537
+ /* ---------------------------------------------------------------- Progress and Meter (#153)
538
+ A Meter grows from its start once on first paint (scale, ease-out); later value changes ease
539
+ the width. Progress narrows shadcn's transition-all to the width. None under reduced motion. */
540
+ :is(
541
+ [data-slot="progress-indicator"],
542
+ [data-slot="meter-indicator"],
543
+ [data-slot="meter-segment"]
544
+ ) {
545
+ transition-property: width, flex-basis, scale;
546
+ transition-duration: calc(var(--nsaa-duration-chunk) * var(--nsaa-time-scale));
547
+ transition-timing-function: var(--nsaa-ease-out);
548
+ transform-origin: left center;
549
+
550
+ &:dir(rtl) {
551
+ transform-origin: right center;
552
+ }
553
+ }
554
+
555
+ :is([data-slot="meter-indicator"], [data-slot="meter-segment"]) {
556
+ @starting-style {
557
+ scale: 0 1;
558
+ }
559
+ }
560
+
477
561
  /* Spinner (Phase 5, #113): loaders bring their own keyframes; the "lucide" glyph turns at
478
562
  duration.spin and follows the cascading speed and play state like the loaders do. */
479
563
  /* Pausing: data-loader-paused on any ancestor (LoaderProvider paused, a Spinner offscreen or in a
@@ -601,6 +685,18 @@
601
685
  transition-duration: 0s;
602
686
  }
603
687
 
688
+ /* Sidebar: collapse and expand snap. */
689
+ &
690
+ :is(
691
+ [data-slot="sidebar-gap"],
692
+ [data-slot="sidebar-container"],
693
+ [data-slot="sidebar-group-label"],
694
+ [data-slot="sidebar-menu-button"],
695
+ [data-slot="sidebar-rail"]
696
+ ) {
697
+ transition-duration: 0s;
698
+ }
699
+
604
700
  /* Drawer: a fade. Dragging still works 1:1 and a release that closes keeps its path, but
605
701
  the stack, snap points and height don't slide; nothing is a spring. */
606
702
  & :is([data-slot="drawer-popup"], [data-slot="drawer-overlay"]) {
@@ -673,7 +769,10 @@
673
769
  [data-slot="input-otp-char"],
674
770
  [data-slot="avatar-image"],
675
771
  [data-slot="skeleton-content"],
676
- [data-slot="accordion-trigger-icon"]
772
+ [data-slot="accordion-trigger-icon"],
773
+ [data-slot="progress-indicator"],
774
+ [data-slot="meter-indicator"],
775
+ [data-slot="meter-segment"]
677
776
  ) {
678
777
  transition-property: none;
679
778
  }
@@ -710,6 +809,10 @@
710
809
  }
711
810
  }
712
811
 
812
+ & [data-slot="alert"][data-ending-style] {
813
+ transition-duration: 0s;
814
+ }
815
+
713
816
  & :is([data-slot="skeleton"], [data-loader="lucide"]) {
714
817
  animation: none;
715
818
  }
@@ -765,6 +868,18 @@
765
868
  transition-duration: 0s;
766
869
  }
767
870
 
871
+ /* Sidebar: collapse and expand snap. */
872
+ &
873
+ :is(
874
+ [data-slot="sidebar-gap"],
875
+ [data-slot="sidebar-container"],
876
+ [data-slot="sidebar-group-label"],
877
+ [data-slot="sidebar-menu-button"],
878
+ [data-slot="sidebar-rail"]
879
+ ) {
880
+ transition-duration: 0s;
881
+ }
882
+
768
883
  /* Drawer: a fade. Dragging still works 1:1 and a release that closes keeps its path, but
769
884
  the stack, snap points and height don't slide; nothing is a spring. */
770
885
  & :is([data-slot="drawer-popup"], [data-slot="drawer-overlay"]) {
@@ -837,7 +952,10 @@
837
952
  [data-slot="input-otp-char"],
838
953
  [data-slot="avatar-image"],
839
954
  [data-slot="skeleton-content"],
840
- [data-slot="accordion-trigger-icon"]
955
+ [data-slot="accordion-trigger-icon"],
956
+ [data-slot="progress-indicator"],
957
+ [data-slot="meter-indicator"],
958
+ [data-slot="meter-segment"]
841
959
  ) {
842
960
  transition-property: none;
843
961
  }
@@ -874,6 +992,10 @@
874
992
  }
875
993
  }
876
994
 
995
+ & [data-slot="alert"][data-ending-style] {
996
+ transition-duration: 0s;
997
+ }
998
+
877
999
  & :is([data-slot="skeleton"], [data-loader="lucide"]) {
878
1000
  animation: none;
879
1001
  }
@@ -933,7 +1055,13 @@
933
1055
  [data-slot="drawer-overlay"],
934
1056
  [data-slot="accordion-content"],
935
1057
  [data-slot="collapsible-content"],
936
- [data-slot="skeleton-content"]
1058
+ [data-slot="skeleton-content"],
1059
+ [data-slot="alert"][data-ending-style],
1060
+ [data-slot="sidebar-gap"],
1061
+ [data-slot="sidebar-container"],
1062
+ [data-slot="sidebar-group-label"],
1063
+ [data-slot="sidebar-menu-button"],
1064
+ [data-slot="sidebar-rail"]
937
1065
  ) {
938
1066
  transition-duration: 0s;
939
1067
  }