@nsaa/core 0.1.0-alpha.4 → 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.
@@ -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
@@ -5,9 +5,9 @@ import { buttonVariants } from "../button/button.js";
5
5
  import { mergeRefs } from "../lib/merge-refs.js";
6
6
  import { Input } from "../input/input.js";
7
7
  import { DropdownMenuCheckboxItem, DropdownMenuItem, DropdownMenuShortcut } from "../dropdown-menu/dropdown-menu.js";
8
+ import { TooltipProvider } from "../tooltip/tooltip.js";
8
9
  import { tv } from "../lib/tv.js";
9
10
  import { ariaShortcut, formatShortcut } from "../lib/shortcut.js";
10
- import { TooltipProvider } from "../tooltip/tooltip.js";
11
11
  import { ToolbarItem } from "./toolbar-item.js";
12
12
  import { OverflowContext, OverflowMore, OverflowStore, useOverflowUnit } from "./overflow.js";
13
13
  import { jsx, jsxs } from "react/jsx-runtime";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nsaa/core",
3
- "version": "0.1.0-alpha.4",
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",
@@ -53,7 +53,7 @@
53
53
  "tailwind-merge": "^3.7.0",
54
54
  "tailwind-variants": "^3.3.1",
55
55
  "uisfx": "0.4.0",
56
- "@nsaa/tokens": "^0.1.0-alpha.4"
56
+ "@nsaa/tokens": "^0.1.0-alpha.5"
57
57
  },
58
58
  "devDependencies": {
59
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
@@ -153,6 +176,25 @@
153
176
  }
154
177
  }
155
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
+
156
198
  /* ---------------------------------------------------------------- Drawer
157
199
  shadcn's 450 ms → ease.drawer, drawer 300 in, base out; a swipe-dismiss releases on
158
200
  spring.sheet, scaled by Base UI's release strength. Stacking and swipe variables untouched. */
@@ -492,6 +534,30 @@
492
534
  }
493
535
  }
494
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
+
495
561
  /* Spinner (Phase 5, #113): loaders bring their own keyframes; the "lucide" glyph turns at
496
562
  duration.spin and follows the cascading speed and play state like the loaders do. */
497
563
  /* Pausing: data-loader-paused on any ancestor (LoaderProvider paused, a Spinner offscreen or in a
@@ -619,6 +685,18 @@
619
685
  transition-duration: 0s;
620
686
  }
621
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
+
622
700
  /* Drawer: a fade. Dragging still works 1:1 and a release that closes keeps its path, but
623
701
  the stack, snap points and height don't slide; nothing is a spring. */
624
702
  & :is([data-slot="drawer-popup"], [data-slot="drawer-overlay"]) {
@@ -691,7 +769,10 @@
691
769
  [data-slot="input-otp-char"],
692
770
  [data-slot="avatar-image"],
693
771
  [data-slot="skeleton-content"],
694
- [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"]
695
776
  ) {
696
777
  transition-property: none;
697
778
  }
@@ -787,6 +868,18 @@
787
868
  transition-duration: 0s;
788
869
  }
789
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
+
790
883
  /* Drawer: a fade. Dragging still works 1:1 and a release that closes keeps its path, but
791
884
  the stack, snap points and height don't slide; nothing is a spring. */
792
885
  & :is([data-slot="drawer-popup"], [data-slot="drawer-overlay"]) {
@@ -859,7 +952,10 @@
859
952
  [data-slot="input-otp-char"],
860
953
  [data-slot="avatar-image"],
861
954
  [data-slot="skeleton-content"],
862
- [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"]
863
959
  ) {
864
960
  transition-property: none;
865
961
  }
@@ -960,7 +1056,12 @@
960
1056
  [data-slot="accordion-content"],
961
1057
  [data-slot="collapsible-content"],
962
1058
  [data-slot="skeleton-content"],
963
- [data-slot="alert"][data-ending-style]
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"]
964
1065
  ) {
965
1066
  transition-duration: 0s;
966
1067
  }