@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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Breaking redesign of the `Login` component and new `RequestAccess` gate component.
|
|
8
|
+
|
|
9
|
+
- **BREAKING**: `Login` replaces its ~40-prop configurable API with a small opinionated one (`logo`, `title`, `description`, `providers`, `providersError`, `magicLink`, `devLogin`, `requestAccess`, `footer`). Removed: `classNames`, `header`, `dividerLabel`, `externalProvidersLabel`, per-affordance `icon`/`pending*`/`disabled`/`variant`/`error` props, magic-link input overrides, and controlled `email`. Handlers now return promises; the component owns pending state (all controls disabled while any flow is in flight), scoped error display from thrown `Error` messages, and icons.
|
|
10
|
+
- Built-in provider brand icons (`GoogleIcon`, `MicrosoftIcon`, `GitHubIcon`) auto-selected from the provider `id`, also exported standalone.
|
|
11
|
+
- Built-in magic-link "sent" confirmation state with Resend and "Use a different email" actions.
|
|
12
|
+
- When a `logo` is provided, the `title` renders visually hidden so the wordmark carries the branding.
|
|
13
|
+
- New `requestAccess` footer line takes a consumer-supplied link element.
|
|
14
|
+
- New `RequestAccess` component: request-by-email card, optional access-code card, and sign-in footer line, with the same promise-handler conventions.
|
|
15
|
+
|
|
16
|
+
- 6e4970a: saas-ui: add `AuditLog`, `CopyField`, `GettingStarted`, and `SettingsSection` components.
|
|
17
|
+
|
|
18
|
+
saas-mcp: split the MCP settings surface into focused sections (`McpConnectorOverview`, `McpToolReference`, `McpConnectedClients`, `McpDeveloperTokens`, `McpActivity`), replacing the previous `McpSettingsPanel`. These sections consume the new saas-ui exports, so the `@forwardreach/saas-ui` peer dependency floor is raised to `^0.4.0`.
|
|
19
|
+
|
|
20
|
+
## 0.3.1
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Publish the shared UI package from the ReachMeHQ GitHub Packages scope with updated package metadata and consumer setup documentation.
|
|
25
|
+
|
|
26
|
+
## 0.3.0
|
|
27
|
+
|
|
28
|
+
### Minor Changes
|
|
29
|
+
|
|
30
|
+
- Add an opt-in `ghost-accent` `Button`/`IconButton` variant for ghosts that belong to a brand cluster. It is transparent at rest like `ghost` but fills with the new `--ssui-accent-subtle` (and `--ssui-accent-subtle-foreground`) tokens on hover instead of the neutral `--ssui-surface-muted`. The tokens ship neutral defaults (a light primary tint) so the package stays brand-neutral; consumers remap them to their brand. The existing `ghost` variant, the default variant, and all current behavior are unchanged. Use neutral `ghost` for utility actions (close/dismiss/refresh/revoke/cancel/plain-toolbar overflow) and `ghost-accent` for a ghost inside a brand cluster.
|
|
31
|
+
|
|
32
|
+
## 0.2.0
|
|
33
|
+
|
|
34
|
+
### Minor Changes
|
|
35
|
+
|
|
36
|
+
- 7413cfe: Enhance `Avatar` and `InlineNotice` to unblock deferred consumer adoption, all additive and default-preserving:
|
|
37
|
+
|
|
38
|
+
- `Avatar` forwards image-loading attributes via a new `imageProps` passthrough (e.g. `referrerPolicy`, `crossOrigin`, `loading`), so referrer-sensitive sources such as Google OAuth profile images load instead of falling back to initials; the component still owns `src`, `alt`, and the image `className`.
|
|
39
|
+
- `Avatar` accepts a numeric `size` (in pixels) alongside the `sm`/`md`/`lg` presets, with initials font-size scaled to the rendered size.
|
|
40
|
+
- `Avatar` gains an opt-in `seed` prop that renders a deterministic, brand-neutral initials background drawn from new `--ssui-avatar-*` palette tokens; without `seed` the fallback keeps the existing muted surface.
|
|
41
|
+
- `InlineNotice` gains an `open` prop for externally-controlled visibility; in controlled mode dismiss and auto-dismiss only invoke `onDismiss`. Omitting `open` preserves today's uncontrolled self-hiding behavior.
|
|
42
|
+
|
|
43
|
+
- 2b8f1f4: Add adoption-feedback enhancements to shared components, all additive and default-preserving:
|
|
44
|
+
|
|
45
|
+
- `Login` now suppresses an affordance's default icon when its `icon` is explicitly `null` (external providers, magic-link submit, dev login); omitting the prop keeps the existing default glyph.
|
|
46
|
+
- `StatusPill` gains an opt-in `dot` that renders a leading status dot in the variant's token-derived color; the dot is off by default.
|
|
47
|
+
- `ConfirmDialog` gains an optional `confirmingLabel` shown on the confirm button while `onConfirm` is in flight; without it, the button keeps showing `confirmLabel`.
|
|
48
|
+
|
|
49
|
+
This package follows semantic versioning. Changes should be recorded through Changesets before publishing.
|
|
50
|
+
|
|
51
|
+
## 0.1.0
|
|
52
|
+
|
|
53
|
+
- Initial private package scaffold.
|
|
54
|
+
- Added brand-neutral utility helpers, primitives, SaaS pattern components, semantic token CSS, and GitHub Packages release workflow.
|
package/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @forwardreach/saas-ui
|
|
2
|
+
|
|
3
|
+
This package is published for use by ReachMe.chat, PeopleThread, and approved ReachMe/PeopleThread partners.
|
|
4
|
+
|
|
5
|
+
It is not intended as a general-purpose public package. Full integration and development documentation is maintained in the private `forwardreach/saas-shared` repository for users with access:
|
|
6
|
+
|
|
7
|
+
https://github.com/forwardreach/saas-shared/blob/main/docs/packages/saas-ui.md
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/** A single dropdown filter rendered in the audit-log header. Fully controlled by the caller. */
|
|
3
|
+
export interface AuditLogFilter {
|
|
4
|
+
/** Stable id (used as the React key). */
|
|
5
|
+
id: string;
|
|
6
|
+
/** Currently selected value. */
|
|
7
|
+
value: string;
|
|
8
|
+
/** Selectable options; include an "all" sentinel if you want an unfiltered choice. */
|
|
9
|
+
options: {
|
|
10
|
+
value: string;
|
|
11
|
+
label: string;
|
|
12
|
+
}[];
|
|
13
|
+
/** Called with the newly selected value. */
|
|
14
|
+
onChange: (value: string) => void;
|
|
15
|
+
/** Accessible label for the select. */
|
|
16
|
+
ariaLabel?: string;
|
|
17
|
+
}
|
|
18
|
+
/** One key/value row shown in an entry's expanded detail area. */
|
|
19
|
+
export interface AuditLogDetail {
|
|
20
|
+
label: string;
|
|
21
|
+
value: React.ReactNode;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A presentational, domain-agnostic audit event. Callers map their own audit
|
|
25
|
+
* records (MCP, and later other producers) onto this shape, resolving ids to
|
|
26
|
+
* human-readable labels before passing them in.
|
|
27
|
+
*/
|
|
28
|
+
export interface AuditLogEntry {
|
|
29
|
+
/** Stable id (React key). */
|
|
30
|
+
id: string;
|
|
31
|
+
/** ISO timestamp of the event. */
|
|
32
|
+
timestamp: string;
|
|
33
|
+
/** Primary line — the action/verb (e.g. a tool or management action name). */
|
|
34
|
+
title: string;
|
|
35
|
+
/** Resolved actor label (e.g. connected client or token name). */
|
|
36
|
+
actorLabel?: string;
|
|
37
|
+
/** Small muted tags shown under the title (e.g. auth method, category). */
|
|
38
|
+
badges?: string[];
|
|
39
|
+
/** Result of the action (e.g. `created`, `revoked`). */
|
|
40
|
+
outcome?: string;
|
|
41
|
+
/** Ids of affected target records/entities. */
|
|
42
|
+
targetIds?: string[];
|
|
43
|
+
/** Key/value rows shown when the entry is expanded. */
|
|
44
|
+
details?: AuditLogDetail[];
|
|
45
|
+
/** Arbitrary context rendered as a JSON block in the expanded view. */
|
|
46
|
+
metadata?: unknown;
|
|
47
|
+
}
|
|
48
|
+
/** Props for the presentational {@link AuditLog}. */
|
|
49
|
+
export interface AuditLogProps {
|
|
50
|
+
/** Section heading. */
|
|
51
|
+
title?: React.ReactNode;
|
|
52
|
+
/** Supporting copy under the title. */
|
|
53
|
+
description?: React.ReactNode;
|
|
54
|
+
/** Leading icon for the section header. */
|
|
55
|
+
icon?: React.ReactNode;
|
|
56
|
+
/** Events to render (already filtered/resolved by the caller as needed). */
|
|
57
|
+
entries: AuditLogEntry[];
|
|
58
|
+
/** Optional header filter dropdowns; the caller owns their state. */
|
|
59
|
+
filters?: AuditLogFilter[];
|
|
60
|
+
/** Shows a loading hint in the header action instead of the count. */
|
|
61
|
+
loading?: boolean;
|
|
62
|
+
/** Error message shown above the list. */
|
|
63
|
+
error?: string | null;
|
|
64
|
+
/** Content shown when there are no entries. */
|
|
65
|
+
emptyState?: React.ReactNode;
|
|
66
|
+
/** Render an affected target id as a link; falls back to a plain badge. */
|
|
67
|
+
renderTargetLink?: (targetId: string) => React.ReactNode;
|
|
68
|
+
/** Override the default absolute date/time formatting. */
|
|
69
|
+
formatTimestamp?: (iso: string) => string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A domain-agnostic audit log: a titled section with optional filter dropdowns,
|
|
73
|
+
* a list of expandable events (actor, action, outcome, affected targets, and a
|
|
74
|
+
* details/JSON drawer), and empty/error/loading states. It holds no data-fetching
|
|
75
|
+
* or product knowledge — the caller supplies resolved {@link AuditLogEntry}s and
|
|
76
|
+
* controls any filters. MCP composes this via `McpActivity`; other producers can
|
|
77
|
+
* reuse it directly.
|
|
78
|
+
*/
|
|
79
|
+
export declare function AuditLog({ title, description, icon, entries, filters, loading, error, emptyState, renderTargetLink, formatTimestamp, }: AuditLogProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../utils/cn.js";
|
|
5
|
+
import { SettingsSection } from "./settings-section.js";
|
|
6
|
+
function defaultFormatTimestamp(iso) {
|
|
7
|
+
const date = new Date(iso);
|
|
8
|
+
if (Number.isNaN(date.getTime()))
|
|
9
|
+
return iso;
|
|
10
|
+
return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(date);
|
|
11
|
+
}
|
|
12
|
+
const selectClass = "h-9 rounded-[var(--ssui-radius)] border border-[color:var(--ssui-border)] bg-[color:var(--ssui-surface)] px-2 text-xs text-[color:var(--ssui-text)]";
|
|
13
|
+
const targetBadgeClass = "inline-flex items-center gap-1 rounded-md bg-[color:var(--ssui-surface-muted)] px-1.5 py-0.5 text-xs text-[color:var(--ssui-text-muted)]";
|
|
14
|
+
/**
|
|
15
|
+
* A domain-agnostic audit log: a titled section with optional filter dropdowns,
|
|
16
|
+
* a list of expandable events (actor, action, outcome, affected targets, and a
|
|
17
|
+
* details/JSON drawer), and empty/error/loading states. It holds no data-fetching
|
|
18
|
+
* or product knowledge — the caller supplies resolved {@link AuditLogEntry}s and
|
|
19
|
+
* controls any filters. MCP composes this via `McpActivity`; other producers can
|
|
20
|
+
* reuse it directly.
|
|
21
|
+
*/
|
|
22
|
+
export function AuditLog({ title = "Activity", description, icon, entries, filters = [], loading = false, error, emptyState = "No activity has been recorded.", renderTargetLink, formatTimestamp = defaultFormatTimestamp, }) {
|
|
23
|
+
return (_jsxs(SettingsSection, { action: _jsx("span", { className: "text-xs text-[color:var(--ssui-text-muted)]", children: loading ? "Loading..." : `${entries.length} events` }), description: description, icon: icon, title: title, children: [filters.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", children: filters.map((filter) => (_jsx("select", { "aria-label": filter.ariaLabel, className: cn(selectClass, "min-w-32"), onChange: (event) => filter.onChange(event.target.value), value: filter.value, children: filter.options.map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value))) }, filter.id))) })), error && _jsx("div", { className: "mt-2 text-xs text-red-600", children: error }), _jsxs("div", { className: "mt-3 divide-y divide-[color:var(--ssui-border)] rounded-[var(--ssui-radius)] border border-[color:var(--ssui-border)]", children: [entries.length === 0 && _jsx("div", { className: "px-3 py-6 text-center text-sm text-[color:var(--ssui-text-muted)]", children: emptyState }), entries.map((entry) => (_jsxs("details", { children: [_jsxs("summary", { className: "grid cursor-pointer list-none gap-2 px-3 py-3 md:grid-cols-[11rem_minmax(0,1fr)_8rem_8rem]", children: [_jsx("div", { className: "text-xs text-[color:var(--ssui-text-muted)]", children: formatTimestamp(entry.timestamp) }), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate text-sm font-medium", children: entry.title }), _jsxs("div", { className: "mt-1 flex flex-wrap items-center gap-1 text-xs text-[color:var(--ssui-text-muted)]", children: [entry.actorLabel && _jsx("span", { className: "truncate font-medium text-[color:var(--ssui-text)]", children: entry.actorLabel }), (entry.badges ?? []).map((badge) => (_jsx("span", { children: badge }, badge)))] })] }), _jsx("div", { className: "text-xs text-[color:var(--ssui-text-muted)]", children: entry.outcome }), _jsx("div", { className: "flex flex-wrap gap-1", children: (entry.targetIds ?? []).slice(0, 2).map((targetId) => (_jsx(React.Fragment, { children: renderTargetLink ? renderTargetLink(targetId) : _jsx("span", { className: targetBadgeClass, children: "Record" }) }, targetId))) })] }), _jsxs("div", { className: "space-y-3 border-t border-[color:var(--ssui-border)] px-3 py-3", children: [entry.details && entry.details.length > 0 && (_jsx("div", { className: "grid gap-2 text-xs text-[color:var(--ssui-text-muted)] md:grid-cols-3", children: entry.details.map((detail) => (_jsxs("div", { children: [detail.label, ": ", detail.value] }, detail.label))) })), entry.targetIds && entry.targetIds.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-1.5", children: entry.targetIds.map((targetId) => (_jsx(React.Fragment, { children: renderTargetLink ? renderTargetLink(targetId) : _jsxs("span", { className: targetBadgeClass, children: ["Record ", targetId] }) }, targetId))) })), entry.metadata !== undefined && entry.metadata !== null && (_jsx("pre", { className: "max-h-56 overflow-auto rounded-[var(--ssui-radius)] border border-[color:var(--ssui-border)] bg-[color:var(--ssui-surface-muted)] p-3 font-mono text-xs text-[color:var(--ssui-text-muted)]", children: JSON.stringify(entry.metadata, null, 2) }))] })] }, entry.id)))] })] }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Internal building blocks shared by the Login and RequestAccess auth
|
|
4
|
+
* surfaces. Not part of the public API.
|
|
5
|
+
*/
|
|
6
|
+
export declare function AuthShell({ children }: {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export declare function AuthHeader({ description, logo, srOnlyTitleWithLogo, title }: {
|
|
10
|
+
description?: React.ReactNode;
|
|
11
|
+
logo?: React.ReactNode;
|
|
12
|
+
/** When true and a logo is present, the title renders visually hidden so the wordmark carries the branding. */
|
|
13
|
+
srOnlyTitleWithLogo?: boolean;
|
|
14
|
+
title: React.ReactNode;
|
|
15
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export declare function AuthFeedback({ children, tone }: {
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
tone: "error" | "status";
|
|
19
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
|
20
|
+
/** Footer line pairing prompt copy with a consumer-supplied link element. */
|
|
21
|
+
export declare function AuthLinkLine({ link, prompt }: {
|
|
22
|
+
link: React.ReactNode;
|
|
23
|
+
prompt: React.ReactNode;
|
|
24
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
25
|
+
export declare function AuthSpinner(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { RefreshCw } from "lucide-react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../utils/cn.js";
|
|
5
|
+
/**
|
|
6
|
+
* Internal building blocks shared by the Login and RequestAccess auth
|
|
7
|
+
* surfaces. Not part of the public API.
|
|
8
|
+
*/
|
|
9
|
+
export function AuthShell({ children }) {
|
|
10
|
+
return (_jsx("main", { className: "flex min-h-[100dvh] items-center justify-center bg-[color:var(--ssui-bg)] px-4 py-10 text-[color:var(--ssui-text)]", children: _jsx("section", { className: "w-full max-w-sm", children: children }) }));
|
|
11
|
+
}
|
|
12
|
+
export function AuthHeader({ description, logo, srOnlyTitleWithLogo = false, title }) {
|
|
13
|
+
return (_jsxs("div", { className: "mb-7", children: [logo ? _jsx("div", { className: "mb-4", children: logo }) : null, _jsx("h1", { className: logo && srOnlyTitleWithLogo
|
|
14
|
+
? "sr-only"
|
|
15
|
+
: "text-xl font-semibold tracking-normal text-[color:var(--ssui-text)]", children: title }), description ? (_jsx("div", { className: "mt-2 text-sm text-[color:var(--ssui-text-muted)]", children: description })) : null] }));
|
|
16
|
+
}
|
|
17
|
+
export function AuthFeedback({ children, tone }) {
|
|
18
|
+
if (!children)
|
|
19
|
+
return null;
|
|
20
|
+
return (_jsx("p", { className: cn("text-sm", tone === "error"
|
|
21
|
+
? "text-[color:var(--ssui-destructive)]"
|
|
22
|
+
: "text-[color:var(--ssui-text-muted)]"), role: tone === "error" ? "alert" : "status", children: children }));
|
|
23
|
+
}
|
|
24
|
+
/** Footer line pairing prompt copy with a consumer-supplied link element. */
|
|
25
|
+
export function AuthLinkLine({ link, prompt }) {
|
|
26
|
+
return (_jsxs("p", { className: "text-sm text-[color:var(--ssui-text-muted)]", children: [prompt, " ", link] }));
|
|
27
|
+
}
|
|
28
|
+
export function AuthSpinner() {
|
|
29
|
+
return _jsx(RefreshCw, { className: "animate-spin" });
|
|
30
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
|
2
|
+
import { type VariantProps } from "class-variance-authority";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
declare const avatarVariants: (props?: ({
|
|
5
|
+
size?: "sm" | "md" | "lg" | null | undefined;
|
|
6
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
|
+
export interface AvatarProps extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> {
|
|
8
|
+
alt?: string;
|
|
9
|
+
fallback?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Attributes forwarded to the underlying Radix `Avatar.Image` (e.g.
|
|
12
|
+
* `referrerPolicy`, `crossOrigin`, `loading`). The component still owns
|
|
13
|
+
* `src`, `alt`, and the image `className`.
|
|
14
|
+
*/
|
|
15
|
+
imageProps?: Omit<React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>, "src">;
|
|
16
|
+
name?: string | null;
|
|
17
|
+
/**
|
|
18
|
+
* Opt-in seed for a deterministic, brand-neutral initials background drawn
|
|
19
|
+
* from the `--ssui-avatar-*` palette tokens. Omit for the default muted
|
|
20
|
+
* surface (no visual change for existing consumers).
|
|
21
|
+
*/
|
|
22
|
+
seed?: string;
|
|
23
|
+
/**
|
|
24
|
+
* A `sm`/`md`/`lg` preset or a custom pixel size. Numeric sizes render at the
|
|
25
|
+
* requested width/height with proportionally scaled initials.
|
|
26
|
+
*/
|
|
27
|
+
size?: VariantProps<typeof avatarVariants>["size"] | number;
|
|
28
|
+
src?: string | null;
|
|
29
|
+
}
|
|
30
|
+
export declare const Avatar: React.ForwardRefExoticComponent<AvatarProps & React.RefAttributes<HTMLSpanElement>>;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
|
3
|
+
import { cva } from "class-variance-authority";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "../utils/cn.js";
|
|
6
|
+
import { getInitials } from "../utils/initials.js";
|
|
7
|
+
const avatarVariants = cva("relative inline-flex shrink-0 overflow-hidden rounded-full border border-[color:var(--ssui-border)] bg-[color:var(--ssui-surface-muted)]", {
|
|
8
|
+
variants: {
|
|
9
|
+
size: {
|
|
10
|
+
sm: "size-7 text-xs",
|
|
11
|
+
md: "size-9 text-sm",
|
|
12
|
+
lg: "size-12 text-base"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
defaultVariants: {
|
|
16
|
+
size: "md"
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Number of `--ssui-avatar-<key>-bg` / `--ssui-avatar-<key>-text` palette
|
|
21
|
+
* entries shipped in styles/index.css. Keys run from 1 to this value.
|
|
22
|
+
*/
|
|
23
|
+
const AVATAR_PALETTE_SIZE = 5;
|
|
24
|
+
/**
|
|
25
|
+
* Deterministic djb2-style string hash. Pure (no random/time) so a given seed
|
|
26
|
+
* resolves to the same palette entry across renders and between SSR and client.
|
|
27
|
+
*/
|
|
28
|
+
function hashSeed(seed) {
|
|
29
|
+
let hash = 5381;
|
|
30
|
+
for (let index = 0; index < seed.length; index += 1) {
|
|
31
|
+
hash = (((hash << 5) + hash) + seed.charCodeAt(index)) >>> 0;
|
|
32
|
+
}
|
|
33
|
+
return hash;
|
|
34
|
+
}
|
|
35
|
+
function paletteIndexForSeed(seed) {
|
|
36
|
+
return (hashSeed(seed) % AVATAR_PALETTE_SIZE) + 1;
|
|
37
|
+
}
|
|
38
|
+
export const Avatar = React.forwardRef(({ alt, className, fallback, imageProps, name, seed, size, src, style, ...props }, ref) => {
|
|
39
|
+
const isNumericSize = typeof size === "number";
|
|
40
|
+
const numericStyle = isNumericSize
|
|
41
|
+
? {
|
|
42
|
+
width: size,
|
|
43
|
+
height: size,
|
|
44
|
+
fontSize: Math.max(10, Math.round(size * 0.42))
|
|
45
|
+
}
|
|
46
|
+
: undefined;
|
|
47
|
+
const paletteIndex = seed ? paletteIndexForSeed(seed) : undefined;
|
|
48
|
+
const fallbackStyle = paletteIndex
|
|
49
|
+
? {
|
|
50
|
+
backgroundColor: `var(--ssui-avatar-${paletteIndex}-bg)`,
|
|
51
|
+
color: `var(--ssui-avatar-${paletteIndex}-text)`
|
|
52
|
+
}
|
|
53
|
+
: undefined;
|
|
54
|
+
return (_jsxs(AvatarPrimitive.Root, { ref: ref, className: cn(avatarVariants({ size: isNumericSize ? null : size, className })), style: { ...numericStyle, ...style }, ...props, children: [src ? (_jsx(AvatarPrimitive.Image, { ...imageProps, alt: alt ?? name ?? "", className: "aspect-square size-full object-cover", src: src })) : null, _jsx(AvatarPrimitive.Fallback, { className: "flex size-full items-center justify-center bg-[color:var(--ssui-surface-muted)] font-medium text-[color:var(--ssui-text-muted)]", delayMs: 80, style: fallbackStyle, children: fallback ?? getInitials(name) })] }));
|
|
55
|
+
});
|
|
56
|
+
Avatar.displayName = "Avatar";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type VariantProps } from "class-variance-authority";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
export declare const badgeVariants: (props?: ({
|
|
4
|
+
variant?: "neutral" | "primary" | "info" | "success" | "warning" | "danger" | "outline" | null | undefined;
|
|
5
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
6
|
+
export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof badgeVariants> {
|
|
7
|
+
}
|
|
8
|
+
export declare const Badge: React.ForwardRefExoticComponent<BadgeProps & React.RefAttributes<HTMLSpanElement>>;
|
|
9
|
+
export declare const Pill: React.ForwardRefExoticComponent<BadgeProps & React.RefAttributes<HTMLSpanElement>>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cva } from "class-variance-authority";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../utils/cn.js";
|
|
5
|
+
export const badgeVariants = cva("inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-medium", {
|
|
6
|
+
variants: {
|
|
7
|
+
variant: {
|
|
8
|
+
neutral: "border-transparent bg-[color:var(--ssui-status-neutral-bg)] text-[color:var(--ssui-status-neutral-text)]",
|
|
9
|
+
primary: "border-transparent bg-[color:var(--ssui-primary)] text-[color:var(--ssui-primary-foreground)]",
|
|
10
|
+
info: "border-transparent bg-[color:var(--ssui-status-info-bg)] text-[color:var(--ssui-status-info-text)]",
|
|
11
|
+
success: "border-transparent bg-[color:var(--ssui-status-success-bg)] text-[color:var(--ssui-status-success-text)]",
|
|
12
|
+
warning: "border-transparent bg-[color:var(--ssui-status-warning-bg)] text-[color:var(--ssui-status-warning-text)]",
|
|
13
|
+
danger: "border-transparent bg-[color:var(--ssui-status-danger-bg)] text-[color:var(--ssui-status-danger-text)]",
|
|
14
|
+
outline: "border-[color:var(--ssui-border)] bg-transparent text-[color:var(--ssui-text)]"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
defaultVariants: {
|
|
18
|
+
variant: "neutral"
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
export const Badge = React.forwardRef(({ className, variant, ...props }, ref) => (_jsx("span", { ref: ref, className: cn(badgeVariants({ variant, className })), ...props })));
|
|
22
|
+
Badge.displayName = "Badge";
|
|
23
|
+
export const Pill = Badge;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface BrandIconProps extends React.SVGProps<SVGSVGElement> {
|
|
3
|
+
}
|
|
4
|
+
/** Official multi-color Google "G" mark (Google identity branding guidelines). */
|
|
5
|
+
export declare function GoogleIcon(props: BrandIconProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
/** Official four-square Microsoft mark (Microsoft identity brand kit). */
|
|
7
|
+
export declare function MicrosoftIcon(props: BrandIconProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
/** GitHub invertocat mark; inherits the surrounding text color. */
|
|
9
|
+
export declare function GitHubIcon(props: BrandIconProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
/**
|
|
11
|
+
* Brand icons keyed by the provider ids `Login` understands. Unknown ids get
|
|
12
|
+
* no icon (label-only button).
|
|
13
|
+
*/
|
|
14
|
+
export declare const brandIconsByProviderId: Record<string, React.ComponentType<BrandIconProps>>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
/** Official multi-color Google "G" mark (Google identity branding guidelines). */
|
|
4
|
+
export function GoogleIcon(props) {
|
|
5
|
+
return (_jsxs("svg", { "aria-hidden": "true", focusable: "false", height: 24, viewBox: "0 0 24 24", width: 24, ...props, children: [_jsx("path", { d: "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.27-4.74 3.27-8.1z", fill: "#4285F4" }), _jsx("path", { d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z", fill: "#34A853" }), _jsx("path", { d: "M5.84 14.1c-.22-.66-.35-1.36-.35-2.1s.13-1.44.35-2.1V7.06H2.18A10.97 10.97 0 0 0 1 12c0 1.77.43 3.45 1.18 4.94l3.66-2.84z", fill: "#FBBC05" }), _jsx("path", { d: "M12 5.38c1.62 0 3.06.55 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.06l3.66 2.84c.87-2.6 3.3-4.52 6.16-4.52z", fill: "#EA4335" })] }));
|
|
6
|
+
}
|
|
7
|
+
/** Official four-square Microsoft mark (Microsoft identity brand kit). */
|
|
8
|
+
export function MicrosoftIcon(props) {
|
|
9
|
+
return (_jsxs("svg", { "aria-hidden": "true", focusable: "false", height: 24, viewBox: "0 0 21 21", width: 24, ...props, children: [_jsx("rect", { fill: "#F25022", height: "9", width: "9", x: "1", y: "1" }), _jsx("rect", { fill: "#7FBA00", height: "9", width: "9", x: "11", y: "1" }), _jsx("rect", { fill: "#00A4EF", height: "9", width: "9", x: "1", y: "11" }), _jsx("rect", { fill: "#FFB900", height: "9", width: "9", x: "11", y: "11" })] }));
|
|
10
|
+
}
|
|
11
|
+
/** GitHub invertocat mark; inherits the surrounding text color. */
|
|
12
|
+
export function GitHubIcon(props) {
|
|
13
|
+
return (_jsx("svg", { "aria-hidden": "true", fill: "currentColor", focusable: "false", height: 24, viewBox: "0 0 16 16", width: 24, ...props, children: _jsx("path", { d: "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z" }) }));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Brand icons keyed by the provider ids `Login` understands. Unknown ids get
|
|
17
|
+
* no icon (label-only button).
|
|
18
|
+
*/
|
|
19
|
+
export const brandIconsByProviderId = {
|
|
20
|
+
github: GitHubIcon,
|
|
21
|
+
google: GoogleIcon,
|
|
22
|
+
microsoft: MicrosoftIcon
|
|
23
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type VariantProps } from "class-variance-authority";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
export declare const buttonVariants: (props?: ({
|
|
4
|
+
variant?: "link" | "primary" | "outline" | "secondary" | "ghost" | "ghost-accent" | "destructive" | null | undefined;
|
|
5
|
+
size?: "icon" | "sm" | "md" | "lg" | "icon-sm" | "icon-lg" | null | undefined;
|
|
6
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
|
+
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
|
8
|
+
asChild?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare const Button: React.ForwardRefExoticComponent<ButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
11
|
+
export interface IconButtonProps extends Omit<ButtonProps, "aria-label" | "children" | "size"> {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
label: string;
|
|
14
|
+
size?: Extract<ButtonProps["size"], "icon" | "icon-sm" | "icon-lg">;
|
|
15
|
+
}
|
|
16
|
+
export declare const IconButton: React.ForwardRefExoticComponent<IconButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
3
|
+
import { cva } from "class-variance-authority";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "../utils/cn.js";
|
|
6
|
+
export const buttonVariants = cva("inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[var(--ssui-radius)] 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 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", {
|
|
7
|
+
variants: {
|
|
8
|
+
variant: {
|
|
9
|
+
primary: "bg-[color:var(--ssui-primary)] text-[color:var(--ssui-primary-foreground)] hover:bg-[color:var(--ssui-primary-hover)]",
|
|
10
|
+
secondary: "border border-[color:var(--ssui-border)] bg-[color:var(--ssui-surface)] text-[color:var(--ssui-text)] hover:bg-[color:var(--ssui-surface-muted)]",
|
|
11
|
+
outline: "border border-[color:var(--ssui-border)] bg-transparent text-[color:var(--ssui-text)] hover:bg-[color:var(--ssui-surface-muted)]",
|
|
12
|
+
ghost: "bg-transparent text-[color:var(--ssui-text)] hover:bg-[color:var(--ssui-surface-muted)]",
|
|
13
|
+
"ghost-accent": "bg-transparent text-[color:var(--ssui-text)] hover:bg-[color:var(--ssui-accent-subtle)] hover:text-[color:var(--ssui-accent-subtle-foreground)]",
|
|
14
|
+
destructive: "bg-[color:var(--ssui-destructive)] text-[color:var(--ssui-destructive-foreground)] hover:bg-[color:var(--ssui-destructive-hover)]",
|
|
15
|
+
link: "h-auto rounded-none p-0 text-[color:var(--ssui-primary)] underline-offset-4 hover:underline"
|
|
16
|
+
},
|
|
17
|
+
size: {
|
|
18
|
+
sm: "h-8 px-3 text-xs",
|
|
19
|
+
md: "h-10 px-4 py-2",
|
|
20
|
+
lg: "h-11 px-5 text-base",
|
|
21
|
+
icon: "size-10 p-0",
|
|
22
|
+
"icon-sm": "size-8 p-0",
|
|
23
|
+
"icon-lg": "size-11 p-0"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
defaultVariants: {
|
|
27
|
+
variant: "primary",
|
|
28
|
+
size: "md"
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
export const Button = React.forwardRef(({ asChild = false, className, size, variant, ...props }, ref) => {
|
|
32
|
+
const Comp = asChild ? Slot : "button";
|
|
33
|
+
return (_jsx(Comp, { ref: ref, className: cn(buttonVariants({ variant, size, className })), ...props }));
|
|
34
|
+
});
|
|
35
|
+
Button.displayName = "Button";
|
|
36
|
+
export const IconButton = React.forwardRef(({ children, label, size = "icon", title, ...props }, ref) => (_jsx(Button, { ref: ref, "aria-label": label, size: size, title: title ?? label, ...props, children: children })));
|
|
37
|
+
IconButton.displayName = "IconButton";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface ConfirmDialogProps {
|
|
3
|
+
cancelLabel?: React.ReactNode;
|
|
4
|
+
children?: React.ReactNode;
|
|
5
|
+
confirmLabel?: React.ReactNode;
|
|
6
|
+
/** Label shown on the confirm button while `onConfirm` is in flight. */
|
|
7
|
+
confirmingLabel?: React.ReactNode;
|
|
8
|
+
defaultOpen?: boolean;
|
|
9
|
+
description?: React.ReactNode;
|
|
10
|
+
onConfirm: () => void | Promise<void>;
|
|
11
|
+
onConfirmError?: (error: unknown) => void;
|
|
12
|
+
onOpenChange?: (open: boolean) => void;
|
|
13
|
+
open?: boolean;
|
|
14
|
+
title: React.ReactNode;
|
|
15
|
+
trigger?: React.ReactNode;
|
|
16
|
+
variant?: "default" | "danger";
|
|
17
|
+
}
|
|
18
|
+
export declare function ConfirmDialog({ cancelLabel, children, confirmLabel, confirmingLabel, defaultOpen, description, onConfirm, onConfirmError, onOpenChange, open, title, trigger, variant }: ConfirmDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { Button } from "./button.js";
|
|
4
|
+
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "./dialog.js";
|
|
5
|
+
export function ConfirmDialog({ cancelLabel = "Cancel", children, confirmLabel = "Confirm", confirmingLabel, defaultOpen, description, onConfirm, onConfirmError, onOpenChange, open, title, trigger, variant = "default" }) {
|
|
6
|
+
const isOpenControlled = open !== undefined;
|
|
7
|
+
const [internalOpen, setInternalOpen] = React.useState(defaultOpen ?? false);
|
|
8
|
+
const [isConfirming, setIsConfirming] = React.useState(false);
|
|
9
|
+
const dialogOpen = isOpenControlled ? open : internalOpen;
|
|
10
|
+
const setDialogOpen = React.useCallback((nextOpen) => {
|
|
11
|
+
if (!isOpenControlled) {
|
|
12
|
+
setInternalOpen(nextOpen);
|
|
13
|
+
}
|
|
14
|
+
onOpenChange?.(nextOpen);
|
|
15
|
+
}, [isOpenControlled, onOpenChange]);
|
|
16
|
+
const handleOpenChange = React.useCallback((nextOpen) => {
|
|
17
|
+
if (isConfirming && !nextOpen)
|
|
18
|
+
return;
|
|
19
|
+
setDialogOpen(nextOpen);
|
|
20
|
+
}, [isConfirming, setDialogOpen]);
|
|
21
|
+
const handleConfirm = React.useCallback(async () => {
|
|
22
|
+
if (isConfirming)
|
|
23
|
+
return;
|
|
24
|
+
setIsConfirming(true);
|
|
25
|
+
try {
|
|
26
|
+
await onConfirm();
|
|
27
|
+
setDialogOpen(false);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
onConfirmError?.(error);
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
setIsConfirming(false);
|
|
34
|
+
}
|
|
35
|
+
}, [isConfirming, onConfirm, onConfirmError, setDialogOpen]);
|
|
36
|
+
return (_jsxs(Dialog, { onOpenChange: handleOpenChange, open: dialogOpen, children: [trigger ? _jsx(DialogTrigger, { asChild: true, children: trigger }) : null, _jsxs(DialogContent, { showClose: !isConfirming, children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: title }), description ? (_jsx(DialogDescription, { children: description })) : null] }), children, _jsxs(DialogFooter, { children: [_jsx(DialogClose, { asChild: true, children: _jsx(Button, { disabled: isConfirming, type: "button", variant: "secondary", children: cancelLabel }) }), _jsx(Button, { "aria-busy": isConfirming, disabled: isConfirming, type: "button", variant: variant === "danger" ? "destructive" : "primary", onClick: () => {
|
|
37
|
+
void handleConfirm();
|
|
38
|
+
}, children: isConfirming ? confirmingLabel ?? confirmLabel : confirmLabel })] })] })] }));
|
|
39
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface CopyFieldProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onCopy" | "children"> {
|
|
3
|
+
/** Value shown read-only and written to the clipboard. */
|
|
4
|
+
value: string;
|
|
5
|
+
/** Accessible label for the value and copy control (e.g. "Connector URL"). */
|
|
6
|
+
label?: string;
|
|
7
|
+
/** Copy button text. */
|
|
8
|
+
copyLabel?: string;
|
|
9
|
+
/** Copy button text while the copied-state feedback is shown. */
|
|
10
|
+
copiedLabel?: string;
|
|
11
|
+
/** Render the value in a monospace face. Defaults to true. */
|
|
12
|
+
monospace?: boolean;
|
|
13
|
+
/** Milliseconds the copied-state feedback persists. Defaults to 2000. */
|
|
14
|
+
resetAfterMs?: number;
|
|
15
|
+
/** Invoked after a successful copy with the copied value. */
|
|
16
|
+
onCopy?: (value: string) => void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Read-only value paired with a copy button that shows transient copied-state
|
|
20
|
+
* feedback. Used for connector URLs, token secrets, and similar copyable values.
|
|
21
|
+
*/
|
|
22
|
+
export declare function CopyField({ value, label, copyLabel, copiedLabel, monospace, resetAfterMs, onCopy, className, ...props }: CopyFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Check, Copy } from "lucide-react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "../utils/cn.js";
|
|
6
|
+
import { Button } from "./button.js";
|
|
7
|
+
async function writeToClipboard(value) {
|
|
8
|
+
try {
|
|
9
|
+
if (!navigator.clipboard?.writeText)
|
|
10
|
+
throw new Error("Clipboard API is unavailable");
|
|
11
|
+
await navigator.clipboard.writeText(value);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
const textarea = document.createElement("textarea");
|
|
16
|
+
textarea.value = value;
|
|
17
|
+
textarea.setAttribute("readonly", "");
|
|
18
|
+
textarea.style.position = "fixed";
|
|
19
|
+
textarea.style.left = "-9999px";
|
|
20
|
+
document.body.appendChild(textarea);
|
|
21
|
+
textarea.select();
|
|
22
|
+
let copied = false;
|
|
23
|
+
try {
|
|
24
|
+
copied = document.execCommand("copy");
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
copied = false;
|
|
28
|
+
}
|
|
29
|
+
document.body.removeChild(textarea);
|
|
30
|
+
return copied;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Read-only value paired with a copy button that shows transient copied-state
|
|
35
|
+
* feedback. Used for connector URLs, token secrets, and similar copyable values.
|
|
36
|
+
*/
|
|
37
|
+
export function CopyField({ value, label = "Value", copyLabel = "Copy", copiedLabel = "Copied", monospace = true, resetAfterMs = 2000, onCopy, className, ...props }) {
|
|
38
|
+
const [copied, setCopied] = React.useState(false);
|
|
39
|
+
React.useEffect(() => {
|
|
40
|
+
if (!copied)
|
|
41
|
+
return;
|
|
42
|
+
const timer = window.setTimeout(() => setCopied(false), resetAfterMs);
|
|
43
|
+
return () => window.clearTimeout(timer);
|
|
44
|
+
}, [copied, resetAfterMs]);
|
|
45
|
+
async function handleCopy() {
|
|
46
|
+
const ok = await writeToClipboard(value);
|
|
47
|
+
if (ok) {
|
|
48
|
+
setCopied(true);
|
|
49
|
+
onCopy?.(value);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return (_jsxs("div", { className: cn("flex items-stretch gap-2", className), ...props, children: [_jsx("div", { "aria-label": label, className: cn("min-w-0 flex-1 self-center break-all rounded-[var(--ssui-radius)] border border-[color:var(--ssui-border)] bg-[color:var(--ssui-surface-muted)] px-3 py-2 text-xs text-[color:var(--ssui-text-muted)]", monospace && "font-mono"), role: "textbox", "aria-readonly": "true", tabIndex: 0, children: value }), _jsxs(Button, { "aria-label": copied ? `${label} copied` : `Copy ${label.toLowerCase()}`, onClick: () => void handleCopy(), type: "button", variant: "secondary", children: [copied ? _jsx(Check, { "aria-hidden": "true" }) : _jsx(Copy, { "aria-hidden": "true" }), copied ? copiedLabel : copyLabel] })] }));
|
|
53
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
export declare const Dialog: React.FC<DialogPrimitive.DialogProps>;
|
|
4
|
+
export declare const DialogTrigger: React.ForwardRefExoticComponent<DialogPrimitive.DialogTriggerProps & React.RefAttributes<HTMLButtonElement>>;
|
|
5
|
+
export declare const DialogPortal: React.FC<DialogPrimitive.DialogPortalProps>;
|
|
6
|
+
export declare const DialogClose: React.ForwardRefExoticComponent<DialogPrimitive.DialogCloseProps & React.RefAttributes<HTMLButtonElement>>;
|
|
7
|
+
export declare const DialogOverlay: React.ForwardRefExoticComponent<Omit<DialogPrimitive.DialogOverlayProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
8
|
+
export declare const DialogContent: React.ForwardRefExoticComponent<Omit<DialogPrimitive.DialogContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & {
|
|
9
|
+
showClose?: boolean;
|
|
10
|
+
} & React.RefAttributes<HTMLDivElement>>;
|
|
11
|
+
export declare function DialogHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export declare function DialogFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export declare const DialogTitle: React.ForwardRefExoticComponent<Omit<DialogPrimitive.DialogTitleProps & React.RefAttributes<HTMLHeadingElement>, "ref"> & React.RefAttributes<HTMLHeadingElement>>;
|
|
14
|
+
export declare const DialogDescription: React.ForwardRefExoticComponent<Omit<DialogPrimitive.DialogDescriptionProps & React.RefAttributes<HTMLParagraphElement>, "ref"> & React.RefAttributes<HTMLParagraphElement>>;
|