@saasflare/ui 2.0.0 → 3.0.1

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 (42) hide show
  1. package/README.md +85 -45
  2. package/dist/{button-B2DR7obe.d.mts → button-DUQJ0X7e.d.mts} +0 -23
  3. package/dist/{button-B2DR7obe.d.ts → button-DUQJ0X7e.d.ts} +0 -23
  4. package/dist/chunk-7UGPCRZ6.mjs +130 -0
  5. package/dist/chunk-CWW36RYE.js +59 -0
  6. package/dist/chunk-JOVJRQO3.js +0 -1
  7. package/dist/{chunk-TU6BBAA7.js → chunk-OYH6LQWR.js} +33 -65
  8. package/dist/{chunk-OT4ZNLTB.mjs → chunk-QWLQV6FS.mjs} +2 -24
  9. package/dist/chunk-S26666D6.mjs +0 -1
  10. package/dist/chunk-VQQ6MF5I.js +161 -0
  11. package/dist/chunk-W53NTFPB.mjs +28 -0
  12. package/dist/dialog-CwyBJeNl.d.mts +22 -0
  13. package/dist/dialog-CwyBJeNl.d.ts +22 -0
  14. package/dist/entries/calendar.d.mts +13 -0
  15. package/dist/entries/calendar.d.ts +13 -0
  16. package/dist/entries/calendar.js +211 -0
  17. package/dist/entries/calendar.mjs +188 -0
  18. package/dist/entries/carousel.d.mts +1 -1
  19. package/dist/entries/carousel.d.ts +1 -1
  20. package/dist/entries/carousel.js +4 -3
  21. package/dist/entries/carousel.mjs +2 -1
  22. package/dist/entries/command.d.mts +21 -0
  23. package/dist/entries/command.d.ts +21 -0
  24. package/dist/entries/command.js +172 -0
  25. package/dist/entries/command.mjs +162 -0
  26. package/dist/entries/drawer.d.mts +16 -0
  27. package/dist/entries/drawer.d.ts +16 -0
  28. package/dist/entries/drawer.js +124 -0
  29. package/dist/entries/drawer.mjs +113 -0
  30. package/dist/entries/input-otp.d.mts +14 -0
  31. package/dist/entries/input-otp.d.ts +14 -0
  32. package/dist/entries/input-otp.js +89 -0
  33. package/dist/entries/input-otp.mjs +64 -0
  34. package/dist/entries/resizable.d.mts +10 -0
  35. package/dist/entries/resizable.d.ts +10 -0
  36. package/dist/entries/resizable.js +69 -0
  37. package/dist/entries/resizable.mjs +45 -0
  38. package/dist/index.d.mts +12 -103
  39. package/dist/index.d.ts +12 -103
  40. package/dist/index.js +1265 -832
  41. package/dist/index.mjs +1172 -740
  42. package/package.json +36 -12
package/README.md CHANGED
@@ -57,30 +57,41 @@ pnpm add react react-dom next next-themes framer-motion
57
57
  `tailwindcss` is **not** a peer dependency — it's a build-time tool. Install it
58
58
  in your app and add a `@source` directive (see Setup below).
59
59
 
60
- ### Optional (per-component)
61
-
62
- These peers are only needed if you import the matching components.
63
- `peerDependenciesMeta.optional` is set, so package managers won't warn if you
64
- skip them.
65
-
66
- | Component(s) | Install |
67
- | ------------------------- | -------------------------------------------------------- |
68
- | `Form` + resolvers | `react-hook-form@^7`, `@hookform/resolvers@^5`, `zod@^4` |
69
- | `Toast` (`Toaster`) | `sonner@^2` |
70
- | `Drawer` | `vaul@^1` |
71
- | `Calendar` | `react-day-picker@^9`, `date-fns@^4` |
72
- | `Resizable` | `react-resizable-panels@^4` |
73
-
74
- ### Subpath imports (heavy peers install only when used)
75
-
76
- | Subpath | Install |
77
- | ------------------------ | -------------------------------- |
78
- | `@saasflare/ui/chart` | `recharts@^3` (~95 KB gzip) |
79
- | `@saasflare/ui/carousel` | `embla-carousel-react@^8` (~25 KB) |
80
-
81
- These two components are deliberately **not** in the main barrel — importing
82
- them through their subpath keeps the heavy peer out of consumers who don't use
83
- them.
60
+ ### Optional (in main barrel — tree-shake-friendly)
61
+
62
+ `Form` lives in the main barrel because `react-hook-form` declares
63
+ `"sideEffects": false`, so consumer bundlers reliably eliminate the import when
64
+ `Form` is unused. Install only if you use it. `peerDependenciesMeta.optional`
65
+ is set, so package managers won't warn if you skip it.
66
+
67
+ | Component | Install |
68
+ | ------------------ | -------------------------------------------------------- |
69
+ | `Form` + resolvers | `react-hook-form@^7`, `@hookform/resolvers@^5`, `zod@^4` |
70
+
71
+ ### Bundled (no install required)
72
+
73
+ `Toaster` (sonner-based toast notifications) is bundled directly into
74
+ `@saasflare/ui` no separate `sonner` install needed. Sonner injects ~6 KB
75
+ of toast CSS into the document at module load, which adds ~13 KB gzip to the
76
+ main barrel for all consumers, including those who don't render `<Toaster />`.
77
+ We accepted this trade-off because Toaster usage is >80 % across the Saasflare
78
+ codebase and consumers; the extra-import friction was worse than the byte cost.
79
+
80
+ ### Subpath imports (heavy, low-frequency, or non-tree-shakeable peers)
81
+
82
+ These components are **not in the main barrel** import them via their
83
+ subpath. This keeps the peer (and its side-effects, e.g. CSS injection) out of
84
+ consumers who don't use the component.
85
+
86
+ | Subpath | Install | Notes |
87
+ | -------------------------- | ---------------------------------------- | ---------------------------------------- |
88
+ | `@saasflare/ui/chart` | `recharts@^3` | ~95 KB gzip peer |
89
+ | `@saasflare/ui/carousel` | `embla-carousel-react@^8` | ~25 KB peer |
90
+ | `@saasflare/ui/calendar` | `react-day-picker@^9`, `date-fns@^4` | ~30 KB peer combined |
91
+ | `@saasflare/ui/drawer` | `vaul@^1` | Mobile drawer / bottom sheet |
92
+ | `@saasflare/ui/command` | (no extra install — `cmdk` bundled) | Full Command palette / cmdk-based modal |
93
+ | `@saasflare/ui/input-otp` | `input-otp@^1` | OTP / 2FA input |
94
+ | `@saasflare/ui/resizable` | `react-resizable-panels@^4` | Split-view panels |
84
95
 
85
96
  ---
86
97
 
@@ -104,7 +115,11 @@ light/dark surface variables, and motion tokens.
104
115
  > content: ["./node_modules/@saasflare/ui/dist/**/*.{js,mjs}", /* ... */]
105
116
  > ```
106
117
 
107
- ### 2. Wrap your app in `SaasflareShell` *(mandatory)*
118
+ ### 2. Make `SaasflareShell` your document root *(mandatory)*
119
+
120
+ `SaasflareShell` **is the document** — it renders `<html>` and `<body>`
121
+ itself, plus the design-system context inside. Do **not** wrap it in your
122
+ own `<html>`/`<body>` tags; replace them entirely.
108
123
 
109
124
  ```tsx
110
125
  // app/layout.tsx
@@ -114,24 +129,39 @@ import "./globals.css";
114
129
 
115
130
  export default function RootLayout({ children }: { children: React.ReactNode }) {
116
131
  return (
117
- <html lang="en" className={fontVariables} data-palette="saasflare" suppressHydrationWarning>
118
- <body>
119
- <SaasflareShell>{children}</SaasflareShell>
120
- </body>
121
- </html>
132
+ <SaasflareShell
133
+ lang="en"
134
+ palette="saasflare"
135
+ theme="dark"
136
+ className={fontVariables}
137
+ >
138
+ {children}
139
+ </SaasflareShell>
122
140
  );
123
141
  }
124
142
  ```
125
143
 
126
- > **⚠️ The wrapper is required, not optional.** `@saasflare/ui` uses
127
- > `LazyMotion features={domAnimation} strict` to keep the framer-motion bundle
128
- > tight. Animated components use the `m.*` API (e.g. `m.button`) which throws
129
- > at runtime if the `LazyMotion` provider is missing. `SaasflareShell` (or
130
- > `SaasflareProvider` directly) provides it. Without one, every `Button`,
131
- > `Card`, `Dialog`, etc. will error on mount.
132
-
133
- `SaasflareShell` also owns the theme class, smooth-scroll context, and the
134
- animation kill-switch context.
144
+ `SaasflareShell` accepts `palette`, `theme`, `surface`, `radius`,
145
+ `animated`, `smoothScrolling`, `storageKey`, plus a `lang`, `className`
146
+ (applied to `<html>`), `bodyClassName`, and a `head` slot for `<head>`
147
+ content. The chosen `palette`/`surface`/`radius`/`animated` props are
148
+ baked into the SSR HTML as `data-*` attributes, so there is **zero FOUT**
149
+ and the pre-hydration script is disabled by default.
150
+
151
+ > **⚠️ Required, not optional.** `@saasflare/ui` uses `LazyMotion
152
+ > features={domAnimation} strict` to keep the motion bundle tight.
153
+ > Animated components use the `m.*` API (e.g. `m.button`) which throws at
154
+ > runtime if `LazyMotion` is missing. `SaasflareShell` (or
155
+ > `SaasflareProvider` directly, when you need to own `<html>`/`<body>`
156
+ > yourself) provides it. Without one, every `Button`, `Card`, `Dialog`,
157
+ > etc. will error on mount.
158
+
159
+ **When to use `SaasflareProvider` instead:** if you need a runtime palette
160
+ switcher (user picks a palette via a toggle, persisted in localStorage),
161
+ keep your own `<html>`/`<body>` and wrap children in `SaasflareProvider`.
162
+ The provider's pre-hydration script reads localStorage before paint.
163
+ `SaasflareShell` is for brand-locked apps where the palette is decided at
164
+ SSR time.
135
165
 
136
166
  ### 3. Use components
137
167
 
@@ -152,11 +182,16 @@ export function Example() {
152
182
  }
153
183
  ```
154
184
 
155
- For chart/carousel:
185
+ Components with heavy or low-frequency peers ship as subpaths:
156
186
 
157
187
  ```tsx
158
188
  import { ChartContainer, ChartTooltip } from "@saasflare/ui/chart";
159
189
  import { Carousel, CarouselContent, CarouselItem } from "@saasflare/ui/carousel";
190
+ import { Calendar } from "@saasflare/ui/calendar";
191
+ import { Drawer, DrawerContent, DrawerTrigger } from "@saasflare/ui/drawer";
192
+ import { Command, CommandInput, CommandList } from "@saasflare/ui/command";
193
+ import { InputOTP, InputOTPGroup, InputOTPSlot } from "@saasflare/ui/input-otp";
194
+ import { ResizablePanel, ResizablePanelGroup } from "@saasflare/ui/resizable";
160
195
  ```
161
196
 
162
197
  ---
@@ -215,9 +250,14 @@ the design system pick them up automatically.
215
250
 
216
251
  | Path | What it is |
217
252
  | --------------------------------- | ------------------------------------------------------------------- |
218
- | `@saasflare/ui` | Core: components, hooks, providers, utilities |
253
+ | `@saasflare/ui` | Core: components, hooks, providers, utilities (incl. Form, Toaster) |
254
+ | `@saasflare/ui/calendar` | Calendar (requires `react-day-picker`, `date-fns`) |
255
+ | `@saasflare/ui/carousel` | Carousel (requires `embla-carousel-react`) |
219
256
  | `@saasflare/ui/chart` | Chart primitives (requires `recharts`) |
220
- | `@saasflare/ui/carousel` | Carousel primitives (requires `embla-carousel-react`) |
257
+ | `@saasflare/ui/command` | Command palette / cmdk modal |
258
+ | `@saasflare/ui/drawer` | Mobile drawer (requires `vaul`) |
259
+ | `@saasflare/ui/input-otp` | OTP input (requires `input-otp`) |
260
+ | `@saasflare/ui/resizable` | Resizable panels (requires `react-resizable-panels`) |
221
261
  | `@saasflare/ui/styles` | Full CSS bundle (alias for `globals.css`) |
222
262
  | `@saasflare/ui/globals.css` | Same as above, explicit |
223
263
  | `@saasflare/ui/theme.css` | Token root only (advanced use) |
@@ -239,9 +279,9 @@ the design system pick them up automatically.
239
279
  `Density`, …)
240
280
  - **Composed widgets:** `ScrollToTopButton`, `ThemeModeToggle`,
241
281
  `TopLoadingBar`, `UserAvatar`
242
- - **All core UI primitives** from `components/ui` (Chart and Carousel are
243
- *not* here — see Subpath imports above) — full list and live examples in
244
- the [catalog](https://ui.saasflare.io).
282
+ - **All core UI primitives** from `components/ui` (Calendar, Carousel, Chart,
283
+ Command, Drawer, InputOTP, and Resizable are *not* here — see Subpath imports
284
+ above) — full list and live examples in the [catalog](https://ui.saasflare.io).
245
285
 
246
286
  ---
247
287
 
@@ -181,29 +181,6 @@ type Surface = StyleVariant;
181
181
  */
182
182
  type RadiusProp = Radius;
183
183
 
184
- /**
185
- * @fileoverview Base props and resolver hook for Saasflare components.
186
- * @module packages/ui/providers/use-saasflare-props
187
- * @package ui
188
- *
189
- * Every Saasflare component MUST:
190
- * 1. Extend SaasflareComponentProps in its props interface
191
- * 2. Call useSaasflareProps(props) to resolve effective values
192
- *
193
- * This ensures consistent precedence:
194
- * component prop > provider context > hardcoded defaults
195
- *
196
- * @example
197
- * interface CardProps extends SaasflareComponentProps {
198
- * title: string
199
- * }
200
- *
201
- * function Card({ title, ...sfProps }: CardProps) {
202
- * const { surface, radius, animated } = useSaasflareProps(sfProps)
203
- * // surface/radius are guaranteed to be resolved, never undefined
204
- * }
205
- */
206
-
207
184
  /** Props that every Saasflare component accepts for theme integration. */
208
185
  interface SaasflareComponentProps {
209
186
  /** Surface style override. Omit to inherit from provider. */
@@ -181,29 +181,6 @@ type Surface = StyleVariant;
181
181
  */
182
182
  type RadiusProp = Radius;
183
183
 
184
- /**
185
- * @fileoverview Base props and resolver hook for Saasflare components.
186
- * @module packages/ui/providers/use-saasflare-props
187
- * @package ui
188
- *
189
- * Every Saasflare component MUST:
190
- * 1. Extend SaasflareComponentProps in its props interface
191
- * 2. Call useSaasflareProps(props) to resolve effective values
192
- *
193
- * This ensures consistent precedence:
194
- * component prop > provider context > hardcoded defaults
195
- *
196
- * @example
197
- * interface CardProps extends SaasflareComponentProps {
198
- * title: string
199
- * }
200
- *
201
- * function Card({ title, ...sfProps }: CardProps) {
202
- * const { surface, radius, animated } = useSaasflareProps(sfProps)
203
- * // surface/radius are guaranteed to be resolved, never undefined
204
- * }
205
- */
206
-
207
184
  /** Props that every Saasflare component accepts for theme integration. */
208
185
  interface SaasflareComponentProps {
209
186
  /** Surface style override. Omit to inherit from provider. */
@@ -0,0 +1,130 @@
1
+ "use client";
2
+ import { useReducedMotion, noMotion, springBouncy } from './chunk-W53NTFPB.mjs';
3
+ import { cn } from './chunk-S26666D6.mjs';
4
+ import { m } from 'motion/react';
5
+ import * as DialogPrimitive from '@radix-ui/react-dialog';
6
+ import { XIcon } from 'lucide-react';
7
+ import { jsx, jsxs } from 'react/jsx-runtime';
8
+
9
+ function Dialog({
10
+ ...props
11
+ }) {
12
+ return /* @__PURE__ */ jsx(DialogPrimitive.Root, { "data-slot": "dialog", ...props });
13
+ }
14
+ function DialogTrigger({
15
+ ...props
16
+ }) {
17
+ return /* @__PURE__ */ jsx(DialogPrimitive.Trigger, { "data-slot": "dialog-trigger", ...props });
18
+ }
19
+ function DialogPortal({
20
+ ...props
21
+ }) {
22
+ return /* @__PURE__ */ jsx(DialogPrimitive.Portal, { "data-slot": "dialog-portal", ...props });
23
+ }
24
+ function DialogClose({
25
+ ...props
26
+ }) {
27
+ return /* @__PURE__ */ jsx(DialogPrimitive.Close, { "data-slot": "dialog-close", ...props });
28
+ }
29
+ function DialogOverlay({
30
+ className,
31
+ ...props
32
+ }) {
33
+ return /* @__PURE__ */ jsx(
34
+ DialogPrimitive.Overlay,
35
+ {
36
+ "data-slot": "dialog-overlay",
37
+ className: cn(
38
+ "fixed inset-0 z-50 bg-black/50 backdrop-blur-[2px] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
39
+ className
40
+ ),
41
+ ...props
42
+ }
43
+ );
44
+ }
45
+ function DialogContent({
46
+ className,
47
+ children,
48
+ ...props
49
+ }) {
50
+ const reduced = useReducedMotion();
51
+ return /* @__PURE__ */ jsxs(DialogPortal, { children: [
52
+ /* @__PURE__ */ jsx(DialogOverlay, {}),
53
+ /* @__PURE__ */ jsx(
54
+ DialogPrimitive.Content,
55
+ {
56
+ "data-slot": "dialog-content",
57
+ asChild: true,
58
+ ...props,
59
+ children: /* @__PURE__ */ jsxs(
60
+ m.div,
61
+ {
62
+ initial: reduced ? { opacity: 1 } : { opacity: 0, scale: 0.95, y: 10 },
63
+ animate: { opacity: 1, scale: 1, y: 0 },
64
+ exit: reduced ? { opacity: 0 } : { opacity: 0, scale: 0.95, y: 10 },
65
+ transition: reduced ? noMotion : springBouncy,
66
+ className: cn(
67
+ "fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg sm:max-w-lg",
68
+ className
69
+ ),
70
+ children: [
71
+ children,
72
+ /* @__PURE__ */ jsxs(DialogPrimitive.Close, { className: "absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none cursor-pointer [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", children: [
73
+ /* @__PURE__ */ jsx(XIcon, {}),
74
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Close" })
75
+ ] })
76
+ ]
77
+ }
78
+ )
79
+ }
80
+ )
81
+ ] });
82
+ }
83
+ function DialogHeader({ className, ...props }) {
84
+ return /* @__PURE__ */ jsx(
85
+ "div",
86
+ {
87
+ "data-slot": "dialog-header",
88
+ className: cn("flex flex-col gap-2 text-center sm:text-left", className),
89
+ ...props
90
+ }
91
+ );
92
+ }
93
+ function DialogFooter({ className, ...props }) {
94
+ return /* @__PURE__ */ jsx(
95
+ "div",
96
+ {
97
+ "data-slot": "dialog-footer",
98
+ className: cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className),
99
+ ...props
100
+ }
101
+ );
102
+ }
103
+ function DialogTitle({
104
+ className,
105
+ ...props
106
+ }) {
107
+ return /* @__PURE__ */ jsx(
108
+ DialogPrimitive.Title,
109
+ {
110
+ "data-slot": "dialog-title",
111
+ className: cn("text-lg leading-none font-semibold", className),
112
+ ...props
113
+ }
114
+ );
115
+ }
116
+ function DialogDescription({
117
+ className,
118
+ ...props
119
+ }) {
120
+ return /* @__PURE__ */ jsx(
121
+ DialogPrimitive.Description,
122
+ {
123
+ "data-slot": "dialog-description",
124
+ className: cn("text-sm text-muted-foreground", className),
125
+ ...props
126
+ }
127
+ );
128
+ }
129
+
130
+ export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger };
@@ -0,0 +1,59 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var React = require('react');
5
+
6
+ function _interopNamespace(e) {
7
+ if (e && e.__esModule) return e;
8
+ var n = Object.create(null);
9
+ if (e) {
10
+ Object.keys(e).forEach(function (k) {
11
+ if (k !== 'default') {
12
+ var d = Object.getOwnPropertyDescriptor(e, k);
13
+ Object.defineProperty(n, k, d.get ? d : {
14
+ enumerable: true,
15
+ get: function () { return e[k]; }
16
+ });
17
+ }
18
+ });
19
+ }
20
+ n.default = e;
21
+ return Object.freeze(n);
22
+ }
23
+
24
+ var React__namespace = /*#__PURE__*/_interopNamespace(React);
25
+
26
+ // src/hooks/use-reduced-motion.ts
27
+ var QUERY = "(prefers-reduced-motion: reduce)";
28
+ var subscribe = (cb) => {
29
+ const mql = window.matchMedia(QUERY);
30
+ mql.addEventListener("change", cb);
31
+ return () => mql.removeEventListener("change", cb);
32
+ };
33
+ var getSnapshot = () => window.matchMedia(QUERY).matches;
34
+ var getServerSnapshot = () => false;
35
+ function useReducedMotion() {
36
+ return React__namespace.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
37
+ }
38
+
39
+ // src/components/ui/motion-config.ts
40
+ var spring = { type: "spring", stiffness: 400, damping: 25 };
41
+ var springBouncy = { type: "spring", stiffness: 300, damping: 15 };
42
+ var springGentle = { type: "spring", stiffness: 200, damping: 20 };
43
+ var springStiff = { type: "spring", stiffness: 500, damping: 30 };
44
+ var noMotion = { duration: 0 };
45
+ var fadeIn = { initial: { opacity: 0 }, animate: { opacity: 1 } };
46
+ var scaleIn = { initial: { opacity: 0, scale: 0.95 }, animate: { opacity: 1, scale: 1 } };
47
+ var slideUp = { initial: { opacity: 0, y: 8 }, animate: { opacity: 1, y: 0 } };
48
+ var slideDown = { initial: { opacity: 0, y: -8 }, animate: { opacity: 1, y: 0 } };
49
+
50
+ exports.fadeIn = fadeIn;
51
+ exports.noMotion = noMotion;
52
+ exports.scaleIn = scaleIn;
53
+ exports.slideDown = slideDown;
54
+ exports.slideUp = slideUp;
55
+ exports.spring = spring;
56
+ exports.springBouncy = springBouncy;
57
+ exports.springGentle = springGentle;
58
+ exports.springStiff = springStiff;
59
+ exports.useReducedMotion = useReducedMotion;
@@ -1,4 +1,3 @@
1
- "use client";
2
1
  'use strict';
3
2
 
4
3
  var clsx = require('clsx');