@forwardreach/saas-ui 0.4.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.
- package/CHANGELOG.md +54 -0
- package/README.md +7 -0
- package/dist/components/audit-log.d.ts +79 -0
- package/dist/components/audit-log.js +24 -0
- package/dist/components/auth-shell.d.ts +25 -0
- package/dist/components/auth-shell.js +30 -0
- package/dist/components/avatar.d.ts +31 -0
- package/dist/components/avatar.js +56 -0
- package/dist/components/badge.d.ts +9 -0
- package/dist/components/badge.js +23 -0
- package/dist/components/brand-icons.d.ts +14 -0
- package/dist/components/brand-icons.js +23 -0
- package/dist/components/button.d.ts +16 -0
- package/dist/components/button.js +37 -0
- package/dist/components/confirm-dialog.d.ts +18 -0
- package/dist/components/confirm-dialog.js +39 -0
- package/dist/components/copy-field.d.ts +22 -0
- package/dist/components/copy-field.js +53 -0
- package/dist/components/dialog.d.ts +14 -0
- package/dist/components/dialog.js +24 -0
- package/dist/components/dropdown-menu.d.ts +23 -0
- package/dist/components/dropdown-menu.js +30 -0
- package/dist/components/empty-state.d.ts +9 -0
- package/dist/components/empty-state.js +7 -0
- package/dist/components/filter-chip.d.ts +11 -0
- package/dist/components/filter-chip.js +13 -0
- package/dist/components/getting-started.d.ts +31 -0
- package/dist/components/getting-started.js +17 -0
- package/dist/components/index.d.ts +29 -0
- package/dist/components/index.js +29 -0
- package/dist/components/inline-notice.d.ts +27 -0
- package/dist/components/inline-notice.js +47 -0
- package/dist/components/inline-rename-input.d.ts +11 -0
- package/dist/components/inline-rename-input.js +57 -0
- package/dist/components/input.d.ts +4 -0
- package/dist/components/input.js +5 -0
- package/dist/components/list-shell.d.ts +17 -0
- package/dist/components/list-shell.js +13 -0
- package/dist/components/login.d.ts +39 -0
- package/dist/components/login.js +50 -0
- package/dist/components/page-header.d.ts +10 -0
- package/dist/components/page-header.js +6 -0
- package/dist/components/request-access.d.ts +25 -0
- package/dist/components/request-access.js +40 -0
- package/dist/components/scroll-area.d.ts +4 -0
- package/dist/components/scroll-area.js +8 -0
- package/dist/components/search-input.d.ts +7 -0
- package/dist/components/search-input.js +11 -0
- package/dist/components/separator.d.ts +3 -0
- package/dist/components/separator.js +6 -0
- package/dist/components/settings-section.d.ts +18 -0
- package/dist/components/settings-section.js +10 -0
- package/dist/components/skeleton.d.ts +4 -0
- package/dist/components/skeleton.js +6 -0
- package/dist/components/status-pill.d.ts +7 -0
- package/dist/components/status-pill.js +7 -0
- package/dist/components/tabs.d.ts +6 -0
- package/dist/components/tabs.js +11 -0
- package/dist/components/textarea.d.ts +4 -0
- package/dist/components/textarea.js +5 -0
- package/dist/components/toolbar.d.ts +7 -0
- package/dist/components/toolbar.js +9 -0
- package/dist/components/tooltip.d.ts +6 -0
- package/dist/components/tooltip.js +9 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/styles/index.css +71 -0
- package/dist/utils/cn.d.ts +2 -0
- package/dist/utils/cn.js +5 -0
- package/dist/utils/email.d.ts +2 -0
- package/dist/utils/email.js +5 -0
- package/dist/utils/flow-runner.d.ts +18 -0
- package/dist/utils/flow-runner.js +39 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/initials.d.ts +10 -0
- package/dist/utils/initials.js +27 -0
- package/dist/utils/time.d.ts +5 -0
- package/dist/utils/time.js +46 -0
- package/package.json +84 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { KeyRound, Mail } from "lucide-react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { isPlausibleEmail } from "../utils/email.js";
|
|
5
|
+
import { useFlowRunner } from "../utils/flow-runner.js";
|
|
6
|
+
import { AuthFeedback, AuthHeader, AuthLinkLine, AuthShell, AuthSpinner } from "./auth-shell.js";
|
|
7
|
+
import { Button } from "./button.js";
|
|
8
|
+
import { Input } from "./input.js";
|
|
9
|
+
const REQUEST_FLOW = "request";
|
|
10
|
+
const ACCESS_CODE_FLOW = "access-code";
|
|
11
|
+
function Card({ children }) {
|
|
12
|
+
return (_jsx("div", { className: "space-y-3 rounded-[var(--ssui-radius)] border border-[color:var(--ssui-border)] bg-[color:var(--ssui-surface)] p-4", children: children }));
|
|
13
|
+
}
|
|
14
|
+
export function RequestAccess({ accessCode, description, logo, request, signIn, title = "Request access" }) {
|
|
15
|
+
const flows = useFlowRunner();
|
|
16
|
+
const emailInputId = React.useId();
|
|
17
|
+
const codeInputId = React.useId();
|
|
18
|
+
const [email, setEmail] = React.useState("");
|
|
19
|
+
const [code, setCode] = React.useState("");
|
|
20
|
+
const [requestSent, setRequestSent] = React.useState(false);
|
|
21
|
+
const trimmedEmail = email.trim();
|
|
22
|
+
const trimmedCode = code.trim();
|
|
23
|
+
async function handleRequestSubmit(event) {
|
|
24
|
+
event.preventDefault();
|
|
25
|
+
if (flows.busy || !isPlausibleEmail(trimmedEmail))
|
|
26
|
+
return;
|
|
27
|
+
const sent = await flows.run(REQUEST_FLOW, () => request.submit(trimmedEmail));
|
|
28
|
+
if (sent)
|
|
29
|
+
setRequestSent(true);
|
|
30
|
+
}
|
|
31
|
+
async function handleCodeSubmit(event) {
|
|
32
|
+
if (!accessCode)
|
|
33
|
+
return;
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
if (flows.busy || trimmedCode.length === 0)
|
|
36
|
+
return;
|
|
37
|
+
await flows.run(ACCESS_CODE_FLOW, () => accessCode.submit(trimmedCode));
|
|
38
|
+
}
|
|
39
|
+
return (_jsxs(AuthShell, { children: [_jsx(AuthHeader, { description: description, logo: logo, title: title }), _jsxs("div", { className: "space-y-5", children: [_jsxs(Card, { children: [requestSent ? (_jsx(AuthFeedback, { tone: "status", children: request.successMessage ?? "Access request sent. We'll email you." })) : (_jsxs("form", { "aria-busy": flows.busyKey === REQUEST_FLOW || undefined, "aria-label": "Request access", className: "space-y-2", onSubmit: handleRequestSubmit, children: [_jsx("label", { className: "block", htmlFor: emailInputId, children: _jsxs("span", { className: "mb-1.5 flex items-center gap-1.5 text-xs font-medium text-[color:var(--ssui-text-muted)]", children: [_jsx(Mail, { "aria-hidden": "true", className: "size-3.5" }), "Email"] }) }), _jsx(Input, { autoComplete: "email", disabled: flows.busy, id: emailInputId, inputMode: "email", name: "email", onChange: (event) => setEmail(event.target.value), placeholder: "you@example.com", type: "email", value: email }), _jsxs(Button, { "aria-busy": flows.busyKey === REQUEST_FLOW || undefined, className: "w-full justify-center", disabled: flows.busy || !isPlausibleEmail(trimmedEmail), type: "submit", variant: "primary", children: [flows.busyKey === REQUEST_FLOW ? _jsx(AuthSpinner, {}) : null, "Request access"] })] })), _jsx(AuthFeedback, { tone: "error", children: flows.errorFor(REQUEST_FLOW) })] }), accessCode ? (_jsxs(Card, { children: [_jsxs("form", { "aria-busy": flows.busyKey === ACCESS_CODE_FLOW || undefined, "aria-label": "Access code", className: "space-y-2", onSubmit: handleCodeSubmit, children: [_jsx("label", { className: "block", htmlFor: codeInputId, children: _jsxs("span", { className: "mb-1.5 flex items-center gap-1.5 text-xs font-medium text-[color:var(--ssui-text-muted)]", children: [_jsx(KeyRound, { "aria-hidden": "true", className: "size-3.5" }), "Have an access code?"] }) }), _jsx(Input, { autoComplete: "one-time-code", disabled: flows.busy, id: codeInputId, name: "code", onChange: (event) => setCode(event.target.value), placeholder: "Access code", value: code }), _jsxs(Button, { "aria-busy": flows.busyKey === ACCESS_CODE_FLOW || undefined, className: "w-full justify-center", disabled: flows.busy || trimmedCode.length === 0, type: "submit", variant: "secondary", children: [flows.busyKey === ACCESS_CODE_FLOW ? _jsx(AuthSpinner, {}) : null, "Continue"] })] }), _jsx(AuthFeedback, { tone: "error", children: flows.errorFor(ACCESS_CODE_FLOW) })] })) : null] }), signIn ? (_jsx("div", { className: "mt-6", children: _jsx(AuthLinkLine, { link: signIn.link, prompt: signIn.prompt ?? "Existing user?" }) })) : null] }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
export declare const ScrollArea: React.ForwardRefExoticComponent<Omit<ScrollAreaPrimitive.ScrollAreaProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
4
|
+
export declare const ScrollBar: React.ForwardRefExoticComponent<Omit<ScrollAreaPrimitive.ScrollAreaScrollbarProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../utils/cn.js";
|
|
5
|
+
export const ScrollArea = React.forwardRef(({ children, className, ...props }, ref) => (_jsxs(ScrollAreaPrimitive.Root, { ref: ref, className: cn("relative overflow-hidden", className), ...props, children: [_jsx(ScrollAreaPrimitive.Viewport, { className: "size-full rounded-[inherit]", children: children }), _jsx(ScrollBar, {}), _jsx(ScrollAreaPrimitive.Corner, {})] })));
|
|
6
|
+
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
|
7
|
+
export const ScrollBar = React.forwardRef(({ className, orientation = "vertical", ...props }, ref) => (_jsx(ScrollAreaPrimitive.ScrollAreaScrollbar, { ref: ref, className: cn("flex touch-none select-none rounded-full p-0.5 transition-colors", orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent", orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent", className), orientation: orientation, ...props, children: _jsx(ScrollAreaPrimitive.ScrollAreaThumb, { className: "relative flex-1 rounded-full bg-[color:var(--ssui-border-strong)]" }) })));
|
|
8
|
+
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type InputProps } from "./input.js";
|
|
3
|
+
export interface SearchInputProps extends Omit<InputProps, "type"> {
|
|
4
|
+
clearLabel?: string;
|
|
5
|
+
onClear?: () => void;
|
|
6
|
+
}
|
|
7
|
+
export declare const SearchInput: React.ForwardRefExoticComponent<SearchInputProps & React.RefAttributes<HTMLInputElement>>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Search, X } from "lucide-react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../utils/cn.js";
|
|
5
|
+
import { IconButton } from "./button.js";
|
|
6
|
+
import { Input } from "./input.js";
|
|
7
|
+
export const SearchInput = React.forwardRef(({ className, clearLabel = "Clear search", onClear, placeholder = "Search", value, ...props }, ref) => {
|
|
8
|
+
const hasValue = typeof value === "string" && value.length > 0;
|
|
9
|
+
return (_jsxs("div", { className: cn("relative w-full", className), children: [_jsx(Search, { className: "pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-[color:var(--ssui-text-subtle)]", "aria-hidden": "true" }), _jsx(Input, { ref: ref, className: "pl-9 pr-9", placeholder: placeholder, type: "search", value: value, ...props }), hasValue && onClear ? (_jsx(IconButton, { className: "absolute right-1 top-1/2 -translate-y-1/2", label: clearLabel, onClick: onClear, size: "icon-sm", type: "button", variant: "ghost", children: _jsx(X, {}) })) : null] }));
|
|
10
|
+
});
|
|
11
|
+
SearchInput.displayName = "SearchInput";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
export declare const Separator: React.ForwardRefExoticComponent<Omit<SeparatorPrimitive.SeparatorProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../utils/cn.js";
|
|
5
|
+
export const Separator = React.forwardRef(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (_jsx(SeparatorPrimitive.Root, { ref: ref, className: cn("shrink-0 bg-[color:var(--ssui-border)]", orientation === "horizontal" ? "h-px w-full" : "h-full w-px", className), decorative: decorative, orientation: orientation, ...props })));
|
|
6
|
+
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface SettingsSectionProps extends Omit<React.HTMLAttributes<HTMLElement>, "title"> {
|
|
3
|
+
/** Section heading. */
|
|
4
|
+
title: React.ReactNode;
|
|
5
|
+
/** Optional supporting copy shown under the title. */
|
|
6
|
+
description?: React.ReactNode;
|
|
7
|
+
/** Optional leading icon rendered before the title. */
|
|
8
|
+
icon?: React.ReactNode;
|
|
9
|
+
/** Optional header-aligned action (button, link, etc.). */
|
|
10
|
+
action?: React.ReactNode;
|
|
11
|
+
/** Optional footer content rendered under the body. */
|
|
12
|
+
footer?: React.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Titled card used as the repeating visual unit across settings surfaces:
|
|
16
|
+
* a heading with optional description and header action, plus a body slot.
|
|
17
|
+
*/
|
|
18
|
+
export declare function SettingsSection({ title, description, icon, action, footer, className, children, ...props }: SettingsSectionProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "../utils/cn.js";
|
|
4
|
+
/**
|
|
5
|
+
* Titled card used as the repeating visual unit across settings surfaces:
|
|
6
|
+
* a heading with optional description and header action, plus a body slot.
|
|
7
|
+
*/
|
|
8
|
+
export function SettingsSection({ title, description, icon, action, footer, className, children, ...props }) {
|
|
9
|
+
return (_jsxs("section", { className: cn("rounded-[var(--ssui-radius-lg)] border border-[color:var(--ssui-border)] bg-[color:var(--ssui-surface)] p-4", className), ...props, children: [_jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between", children: [_jsxs("div", { className: "min-w-0 space-y-1", children: [_jsxs("h2", { className: "flex items-center gap-2 text-sm font-semibold text-[color:var(--ssui-text)]", children: [icon ? (_jsx("span", { className: "shrink-0 [&_svg]:size-4", "aria-hidden": "true", children: icon })) : null, title] }), description ? (_jsx("div", { className: "text-sm text-[color:var(--ssui-text-muted)]", children: description })) : null] }), action ? (_jsx("div", { className: "flex shrink-0 flex-wrap gap-2", children: action })) : null] }), children ? _jsx("div", { className: "mt-4", children: children }) : null, footer ? _jsx("div", { className: "mt-4", children: footer }) : null] }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "../utils/cn.js";
|
|
4
|
+
export function Skeleton({ className, ...props }) {
|
|
5
|
+
return (_jsx("div", { className: cn("animate-pulse rounded-[var(--ssui-radius)] bg-[color:var(--ssui-surface-muted)]", className), ...props }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type BadgeProps } from "./badge.js";
|
|
2
|
+
export interface StatusPillProps extends BadgeProps {
|
|
3
|
+
/** Render a small leading status dot in the active variant's color. */
|
|
4
|
+
dot?: boolean;
|
|
5
|
+
status?: BadgeProps["variant"];
|
|
6
|
+
}
|
|
7
|
+
export declare function StatusPill({ children, className, dot, status, variant, ...props }: StatusPillProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "../utils/cn.js";
|
|
4
|
+
import { Badge } from "./badge.js";
|
|
5
|
+
export function StatusPill({ children, className, dot = false, status = "neutral", variant, ...props }) {
|
|
6
|
+
return (_jsxs(Badge, { className: cn("capitalize", className), variant: variant ?? status, ...props, children: [dot ? (_jsx("span", { "aria-hidden": "true", className: "size-1.5 shrink-0 rounded-full bg-[currentColor]", "data-status-dot": "" })) : null, children] }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
export declare const Tabs: React.ForwardRefExoticComponent<TabsPrimitive.TabsProps & React.RefAttributes<HTMLDivElement>>;
|
|
4
|
+
export declare const TabsList: React.ForwardRefExoticComponent<Omit<TabsPrimitive.TabsListProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
5
|
+
export declare const TabsTrigger: React.ForwardRefExoticComponent<Omit<TabsPrimitive.TabsTriggerProps & React.RefAttributes<HTMLButtonElement>, "ref"> & React.RefAttributes<HTMLButtonElement>>;
|
|
6
|
+
export declare const TabsContent: React.ForwardRefExoticComponent<Omit<TabsPrimitive.TabsContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../utils/cn.js";
|
|
5
|
+
export const Tabs = TabsPrimitive.Root;
|
|
6
|
+
export const TabsList = React.forwardRef(({ className, ...props }, ref) => (_jsx(TabsPrimitive.List, { ref: ref, className: cn("inline-flex h-10 items-center justify-center rounded-[var(--ssui-radius)] bg-[color:var(--ssui-surface-muted)] p-1 text-[color:var(--ssui-text-muted)]", className), ...props })));
|
|
7
|
+
TabsList.displayName = TabsPrimitive.List.displayName;
|
|
8
|
+
export const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => (_jsx(TabsPrimitive.Trigger, { ref: ref, className: cn("inline-flex items-center justify-center whitespace-nowrap rounded-[var(--ssui-radius-sm)] px-3 py-1.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--ssui-focus-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--ssui-bg)] disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-[color:var(--ssui-surface)] data-[state=active]:text-[color:var(--ssui-text)] data-[state=active]:shadow-sm", className), ...props })));
|
|
9
|
+
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
|
10
|
+
export const TabsContent = React.forwardRef(({ className, ...props }, ref) => (_jsx(TabsPrimitive.Content, { ref: ref, className: cn("mt-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--ssui-focus-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--ssui-bg)]", className), ...props })));
|
|
11
|
+
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "../utils/cn.js";
|
|
4
|
+
export const Textarea = React.forwardRef(({ className, rows = 4, ...props }, ref) => (_jsx("textarea", { ref: ref, rows: rows, className: cn("flex min-h-24 w-full resize-y rounded-[var(--ssui-radius)] border border-[color:var(--ssui-border)] bg-[color:var(--ssui-surface)] px-3 py-2 text-sm text-[color:var(--ssui-text)] shadow-sm transition-colors placeholder:text-[color:var(--ssui-text-subtle)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--ssui-focus-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--ssui-bg)] disabled:cursor-not-allowed disabled:bg-[color:var(--ssui-surface-muted)] disabled:opacity-70", className), ...props })));
|
|
5
|
+
Textarea.displayName = "Textarea";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface ToolbarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
}
|
|
4
|
+
export declare function Toolbar({ className, ...props }: ToolbarProps): import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
export interface ToolbarGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
}
|
|
7
|
+
export declare function ToolbarGroup({ className, ...props }: ToolbarGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "../utils/cn.js";
|
|
4
|
+
export function Toolbar({ className, ...props }) {
|
|
5
|
+
return (_jsx("div", { className: cn("flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between", className), ...props }));
|
|
6
|
+
}
|
|
7
|
+
export function ToolbarGroup({ className, ...props }) {
|
|
8
|
+
return (_jsx("div", { className: cn("flex flex-wrap items-center gap-2", className), ...props }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
export declare const TooltipProvider: React.FC<TooltipPrimitive.TooltipProviderProps>;
|
|
4
|
+
export declare const Tooltip: React.FC<TooltipPrimitive.TooltipProps>;
|
|
5
|
+
export declare const TooltipTrigger: React.ForwardRefExoticComponent<TooltipPrimitive.TooltipTriggerProps & React.RefAttributes<HTMLButtonElement>>;
|
|
6
|
+
export declare const TooltipContent: React.ForwardRefExoticComponent<Omit<TooltipPrimitive.TooltipContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../utils/cn.js";
|
|
5
|
+
export const TooltipProvider = TooltipPrimitive.Provider;
|
|
6
|
+
export const Tooltip = TooltipPrimitive.Root;
|
|
7
|
+
export const TooltipTrigger = TooltipPrimitive.Trigger;
|
|
8
|
+
export const TooltipContent = React.forwardRef(({ className, sideOffset = 6, ...props }, ref) => (_jsx(TooltipPrimitive.Portal, { children: _jsx(TooltipPrimitive.Content, { ref: ref, sideOffset: sideOffset, className: cn("z-50 max-w-72 overflow-hidden rounded-[var(--ssui-radius)] border border-[color:var(--ssui-border)] bg-[color:var(--ssui-surface-elevated)] px-3 py-1.5 text-xs text-[color:var(--ssui-text)] shadow-[var(--ssui-shadow-md)]", className), ...props }) })));
|
|
9
|
+
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--ssui-bg: #ffffff;
|
|
3
|
+
--ssui-surface: #ffffff;
|
|
4
|
+
--ssui-surface-muted: #f4f6f8;
|
|
5
|
+
--ssui-surface-elevated: #ffffff;
|
|
6
|
+
--ssui-overlay: rgb(15 23 42 / 0.48);
|
|
7
|
+
--ssui-overlay-hover: rgb(17 24 39 / 0.08);
|
|
8
|
+
--ssui-text: #111827;
|
|
9
|
+
--ssui-text-muted: #667085;
|
|
10
|
+
--ssui-text-subtle: #98a2b3;
|
|
11
|
+
--ssui-text-inverse: #ffffff;
|
|
12
|
+
--ssui-border: #d0d5dd;
|
|
13
|
+
--ssui-border-strong: #98a2b3;
|
|
14
|
+
--ssui-primary: #2563eb;
|
|
15
|
+
--ssui-primary-hover: #1d4ed8;
|
|
16
|
+
--ssui-primary-foreground: #ffffff;
|
|
17
|
+
--ssui-accent-subtle: #eff6ff;
|
|
18
|
+
--ssui-accent-subtle-foreground: #1e3a8a;
|
|
19
|
+
--ssui-destructive: #dc2626;
|
|
20
|
+
--ssui-destructive-hover: #b91c1c;
|
|
21
|
+
--ssui-destructive-foreground: #ffffff;
|
|
22
|
+
--ssui-focus-ring: #2563eb;
|
|
23
|
+
--ssui-radius-sm: 0.25rem;
|
|
24
|
+
--ssui-radius: 0.375rem;
|
|
25
|
+
--ssui-radius-lg: 0.5rem;
|
|
26
|
+
--ssui-shadow-sm: 0 1px 2px rgb(16 24 40 / 0.08);
|
|
27
|
+
--ssui-shadow-md: 0 12px 24px rgb(16 24 40 / 0.12);
|
|
28
|
+
--ssui-notice-info-bg: #eff6ff;
|
|
29
|
+
--ssui-notice-info-text: #1e3a8a;
|
|
30
|
+
--ssui-notice-info-border: #bfdbfe;
|
|
31
|
+
--ssui-notice-success-bg: #ecfdf3;
|
|
32
|
+
--ssui-notice-success-text: #05603a;
|
|
33
|
+
--ssui-notice-success-border: #abefc6;
|
|
34
|
+
--ssui-notice-warning-bg: #fffaeb;
|
|
35
|
+
--ssui-notice-warning-text: #93370d;
|
|
36
|
+
--ssui-notice-warning-border: #fedf89;
|
|
37
|
+
--ssui-notice-danger-bg: #fef3f2;
|
|
38
|
+
--ssui-notice-danger-text: #912018;
|
|
39
|
+
--ssui-notice-danger-border: #fecdca;
|
|
40
|
+
--ssui-status-neutral-bg: #f4f6f8;
|
|
41
|
+
--ssui-status-neutral-text: #344054;
|
|
42
|
+
--ssui-status-info-bg: #eff6ff;
|
|
43
|
+
--ssui-status-info-text: #1e3a8a;
|
|
44
|
+
--ssui-status-success-bg: #ecfdf3;
|
|
45
|
+
--ssui-status-success-text: #05603a;
|
|
46
|
+
--ssui-status-warning-bg: #fffaeb;
|
|
47
|
+
--ssui-status-warning-text: #93370d;
|
|
48
|
+
--ssui-status-danger-bg: #fef3f2;
|
|
49
|
+
--ssui-status-danger-text: #912018;
|
|
50
|
+
--ssui-avatar-1-bg: #eef2ff;
|
|
51
|
+
--ssui-avatar-1-text: #3730a3;
|
|
52
|
+
--ssui-avatar-2-bg: #ecfdf5;
|
|
53
|
+
--ssui-avatar-2-text: #065f46;
|
|
54
|
+
--ssui-avatar-3-bg: #fff7ed;
|
|
55
|
+
--ssui-avatar-3-text: #9a3412;
|
|
56
|
+
--ssui-avatar-4-bg: #fdf2f8;
|
|
57
|
+
--ssui-avatar-4-text: #9d174d;
|
|
58
|
+
--ssui-avatar-5-bg: #f0f9ff;
|
|
59
|
+
--ssui-avatar-5-text: #075985;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@media (prefers-reduced-motion: reduce) {
|
|
63
|
+
*,
|
|
64
|
+
*::before,
|
|
65
|
+
*::after {
|
|
66
|
+
animation-duration: 0.01ms !important;
|
|
67
|
+
animation-iteration-count: 1 !important;
|
|
68
|
+
scroll-behavior: auto !important;
|
|
69
|
+
transition-duration: 0.01ms !important;
|
|
70
|
+
}
|
|
71
|
+
}
|
package/dist/utils/cn.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface FlowRunner {
|
|
3
|
+
/** True while any flow is in flight; callers disable every interactive control. */
|
|
4
|
+
busy: boolean;
|
|
5
|
+
/** Key of the in-flight flow, for marking the active control busy. */
|
|
6
|
+
busyKey: string | null;
|
|
7
|
+
clearError: () => void;
|
|
8
|
+
/** The message for a rejected flow, or null if that flow has no error. */
|
|
9
|
+
errorFor: (key: string) => React.ReactNode;
|
|
10
|
+
/** Runs a flow's async handler; resolves true on success, false on a caught rejection. */
|
|
11
|
+
run: (key: string, action: () => Promise<void>) => Promise<boolean>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Owns the async flow state (single in-flight flow, error scoped to the flow
|
|
15
|
+
* that rejected) shared by Login and RequestAccess. Rejections surface the
|
|
16
|
+
* thrown Error's message as user-facing copy per the package convention.
|
|
17
|
+
*/
|
|
18
|
+
export declare function useFlowRunner(): FlowRunner;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
const GENERIC_ERROR_MESSAGE = "Something went wrong. Please try again.";
|
|
3
|
+
/**
|
|
4
|
+
* Owns the async flow state (single in-flight flow, error scoped to the flow
|
|
5
|
+
* that rejected) shared by Login and RequestAccess. Rejections surface the
|
|
6
|
+
* thrown Error's message as user-facing copy per the package convention.
|
|
7
|
+
*/
|
|
8
|
+
export function useFlowRunner() {
|
|
9
|
+
const [busyKey, setBusyKey] = React.useState(null);
|
|
10
|
+
const [error, setError] = React.useState(null);
|
|
11
|
+
const run = React.useCallback(async (key, action) => {
|
|
12
|
+
setBusyKey(key);
|
|
13
|
+
setError(null);
|
|
14
|
+
try {
|
|
15
|
+
await action();
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch (cause) {
|
|
19
|
+
setError({
|
|
20
|
+
key,
|
|
21
|
+
message: cause instanceof Error && cause.message
|
|
22
|
+
? cause.message
|
|
23
|
+
: GENERIC_ERROR_MESSAGE
|
|
24
|
+
});
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
setBusyKey(null);
|
|
29
|
+
}
|
|
30
|
+
}, []);
|
|
31
|
+
const clearError = React.useCallback(() => setError(null), []);
|
|
32
|
+
return {
|
|
33
|
+
busy: busyKey !== null,
|
|
34
|
+
busyKey,
|
|
35
|
+
clearError,
|
|
36
|
+
errorFor: (key) => (error?.key === key ? error.message : null),
|
|
37
|
+
run
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface InitialsInput {
|
|
2
|
+
name?: string | null;
|
|
3
|
+
email?: string | null;
|
|
4
|
+
fallback?: string | null;
|
|
5
|
+
}
|
|
6
|
+
export interface GetInitialsOptions {
|
|
7
|
+
maxLength?: number;
|
|
8
|
+
fallback?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function getInitials(input: InitialsInput | string | null | undefined, options?: GetInitialsOptions): string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
function cleanPart(part) {
|
|
2
|
+
return part.trim().replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, "");
|
|
3
|
+
}
|
|
4
|
+
export function getInitials(input, options = {}) {
|
|
5
|
+
const maxLength = Math.max(1, options.maxLength ?? 2);
|
|
6
|
+
const fallback = options.fallback ??
|
|
7
|
+
(typeof input === "object" ? input?.fallback : undefined) ??
|
|
8
|
+
"?";
|
|
9
|
+
const name = typeof input === "string" ? input : input?.name;
|
|
10
|
+
const email = typeof input === "object" ? input?.email : undefined;
|
|
11
|
+
const source = name?.trim() || email?.split("@")[0] || "";
|
|
12
|
+
const parts = source
|
|
13
|
+
.split(/[\s._-]+/u)
|
|
14
|
+
.map(cleanPart)
|
|
15
|
+
.filter(Boolean);
|
|
16
|
+
if (parts.length === 0) {
|
|
17
|
+
return fallback.slice(0, maxLength).toLocaleUpperCase();
|
|
18
|
+
}
|
|
19
|
+
if (parts.length === 1) {
|
|
20
|
+
return parts[0].slice(0, maxLength).toLocaleUpperCase();
|
|
21
|
+
}
|
|
22
|
+
return parts
|
|
23
|
+
.slice(0, maxLength)
|
|
24
|
+
.map((part) => part[0])
|
|
25
|
+
.join("")
|
|
26
|
+
.toLocaleUpperCase();
|
|
27
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const SECOND = 1000;
|
|
2
|
+
const MINUTE = 60 * SECOND;
|
|
3
|
+
const HOUR = 60 * MINUTE;
|
|
4
|
+
const DAY = 24 * HOUR;
|
|
5
|
+
const WEEK = 7 * DAY;
|
|
6
|
+
const MONTH = 30 * DAY;
|
|
7
|
+
const YEAR = 365 * DAY;
|
|
8
|
+
function toDate(value) {
|
|
9
|
+
return value instanceof Date ? value : new Date(value);
|
|
10
|
+
}
|
|
11
|
+
function format(amount, unit, future) {
|
|
12
|
+
const value = `${amount}${unit}`;
|
|
13
|
+
return future ? `in ${value}` : `${value} ago`;
|
|
14
|
+
}
|
|
15
|
+
export function formatRelativeTime(value, options = {}) {
|
|
16
|
+
const target = toDate(value);
|
|
17
|
+
const now = toDate(options.now ?? Date.now());
|
|
18
|
+
if (Number.isNaN(target.getTime()) || Number.isNaN(now.getTime())) {
|
|
19
|
+
return "Unknown";
|
|
20
|
+
}
|
|
21
|
+
const diff = target.getTime() - now.getTime();
|
|
22
|
+
const abs = Math.abs(diff);
|
|
23
|
+
const future = diff > 0;
|
|
24
|
+
if (abs < 45 * SECOND) {
|
|
25
|
+
return future && abs > 5 * SECOND ? "in a few seconds" : "just now";
|
|
26
|
+
}
|
|
27
|
+
if (abs < 45 * MINUTE) {
|
|
28
|
+
return format(Math.round(abs / MINUTE), "m", future);
|
|
29
|
+
}
|
|
30
|
+
if (abs < 22 * HOUR) {
|
|
31
|
+
return format(Math.round(abs / HOUR), "h", future);
|
|
32
|
+
}
|
|
33
|
+
if (abs < 36 * HOUR && options.numeric !== "always") {
|
|
34
|
+
return future ? "tomorrow" : "yesterday";
|
|
35
|
+
}
|
|
36
|
+
if (abs < 6 * DAY) {
|
|
37
|
+
return format(Math.round(abs / DAY), "d", future);
|
|
38
|
+
}
|
|
39
|
+
if (abs < 4 * WEEK) {
|
|
40
|
+
return format(Math.round(abs / WEEK), "w", future);
|
|
41
|
+
}
|
|
42
|
+
if (abs < YEAR) {
|
|
43
|
+
return format(Math.round(abs / MONTH), "mo", future);
|
|
44
|
+
}
|
|
45
|
+
return format(Math.round(abs / YEAR), "y", future);
|
|
46
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forwardreach/saas-ui",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Brand-neutral React UI primitives and SaaS app patterns for ForwardReach-owned business applications.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"license": "UNLICENSED",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/forwardreach/saas-shared.git",
|
|
11
|
+
"directory": "packages/saas-ui"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/forwardreach/saas-shared/tree/main/packages/saas-ui#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/forwardreach/saas-shared/issues"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public",
|
|
19
|
+
"registry": "https://registry.npmjs.org/"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md",
|
|
24
|
+
"CHANGELOG.md"
|
|
25
|
+
],
|
|
26
|
+
"sideEffects": [
|
|
27
|
+
"**/*.css"
|
|
28
|
+
],
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./components": {
|
|
35
|
+
"types": "./dist/components/index.d.ts",
|
|
36
|
+
"import": "./dist/components/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./utils": {
|
|
39
|
+
"types": "./dist/utils/index.d.ts",
|
|
40
|
+
"import": "./dist/utils/index.js"
|
|
41
|
+
},
|
|
42
|
+
"./styles.css": "./dist/styles/index.css",
|
|
43
|
+
"./styles": "./dist/styles/index.css",
|
|
44
|
+
"./package.json": "./package.json"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"react": "^19.0.0",
|
|
48
|
+
"react-dom": "^19.0.0"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@radix-ui/react-avatar": "^1.1.10",
|
|
52
|
+
"@radix-ui/react-dialog": "^1.1.15",
|
|
53
|
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
54
|
+
"@radix-ui/react-scroll-area": "^1.2.10",
|
|
55
|
+
"@radix-ui/react-separator": "^1.1.7",
|
|
56
|
+
"@radix-ui/react-slot": "^1.2.4",
|
|
57
|
+
"@radix-ui/react-tabs": "^1.1.13",
|
|
58
|
+
"@radix-ui/react-tooltip": "^1.2.8",
|
|
59
|
+
"class-variance-authority": "^0.7.1",
|
|
60
|
+
"clsx": "^2.1.1",
|
|
61
|
+
"lucide-react": "^0.468.0",
|
|
62
|
+
"tailwind-merge": "^3.3.1"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
66
|
+
"@testing-library/react": "^16.3.0",
|
|
67
|
+
"@testing-library/user-event": "^14.6.1",
|
|
68
|
+
"@types/react": "^19.0.0",
|
|
69
|
+
"@types/react-dom": "^19.0.0",
|
|
70
|
+
"jsdom": "^26.1.0",
|
|
71
|
+
"rimraf": "^6.0.1",
|
|
72
|
+
"typescript": "^5.8.3",
|
|
73
|
+
"vitest": "^3.2.4"
|
|
74
|
+
},
|
|
75
|
+
"scripts": {
|
|
76
|
+
"build": "pnpm clean && tsc -p tsconfig.build.json && node scripts/copy-styles.mjs",
|
|
77
|
+
"dev": "node scripts/copy-styles.mjs && tsc -p tsconfig.build.json --watch",
|
|
78
|
+
"check:brand": "node scripts/check-brand-neutrality.mjs",
|
|
79
|
+
"clean": "rimraf dist coverage dist-pack *.tgz",
|
|
80
|
+
"pack:local": "pnpm build && mkdir -p dist-pack && pnpm pack --pack-destination dist-pack",
|
|
81
|
+
"test": "vitest run && pnpm check:brand",
|
|
82
|
+
"typecheck": "tsc --noEmit"
|
|
83
|
+
}
|
|
84
|
+
}
|