@birdycom/omniaui-react 0.0.0-stage → 0.11.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/LICENSE +21 -0
- package/README.md +20 -2
- package/dist/index.d.ts +2640 -0
- package/dist/index.js +4813 -0
- package/manifest.json +838 -0
- package/package.json +69 -4
- package/src/blocks/BlockAboutTimeline.tsx +46 -0
- package/src/blocks/BlockAppHeader.tsx +52 -0
- package/src/blocks/BlockAppShell.tsx +56 -0
- package/src/blocks/BlockAuth.tsx +53 -0
- package/src/blocks/BlockAuthSplit.tsx +84 -0
- package/src/blocks/BlockContactGrid.tsx +71 -0
- package/src/blocks/BlockCta.tsx +23 -0
- package/src/blocks/BlockDataTable.tsx +57 -0
- package/src/blocks/BlockFaq.tsx +55 -0
- package/src/blocks/BlockFeatures.tsx +38 -0
- package/src/blocks/BlockFooter.tsx +50 -0
- package/src/blocks/BlockFormPage.tsx +26 -0
- package/src/blocks/BlockHero.tsx +27 -0
- package/src/blocks/BlockHeroCode.tsx +63 -0
- package/src/blocks/BlockHeroOrbit.tsx +73 -0
- package/src/blocks/BlockHeroSplit.tsx +28 -0
- package/src/blocks/BlockModuleHeader.tsx +34 -0
- package/src/blocks/BlockPageHero.tsx +25 -0
- package/src/blocks/BlockPortfolioGrid.tsx +50 -0
- package/src/blocks/BlockPricing.tsx +62 -0
- package/src/blocks/BlockProcess.tsx +50 -0
- package/src/blocks/BlockProductGrid.tsx +54 -0
- package/src/blocks/BlockProfileHeader.tsx +57 -0
- package/src/blocks/BlockShowcase.tsx +58 -0
- package/src/blocks/BlockSiteFooter.tsx +60 -0
- package/src/blocks/BlockSiteHeader.tsx +42 -0
- package/src/blocks/BlockSpotlight.tsx +58 -0
- package/src/blocks/BlockStats.tsx +26 -0
- package/src/blocks/BlockStatsCounter.tsx +31 -0
- package/src/blocks/BlockTeamList.tsx +48 -0
- package/src/blocks/BlockTwoFactor.tsx +57 -0
- package/src/blocks/BlockWizardDialog.tsx +101 -0
- package/src/components/Accordion.tsx +31 -0
- package/src/components/AccordionAccent.tsx +39 -0
- package/src/components/AffixInput.tsx +47 -0
- package/src/components/Alert.tsx +24 -0
- package/src/components/AlertDialog.tsx +60 -0
- package/src/components/AlphaTabs.tsx +63 -0
- package/src/components/Attachment.tsx +140 -0
- package/src/components/Avatar.tsx +32 -0
- package/src/components/BackLink.tsx +11 -0
- package/src/components/Badge.tsx +14 -0
- package/src/components/BarcodeInput.tsx +64 -0
- package/src/components/Breadcrumb.tsx +32 -0
- package/src/components/BrowserFrame.tsx +48 -0
- package/src/components/BusyOverlay.tsx +52 -0
- package/src/components/Button.tsx +49 -0
- package/src/components/Card.tsx +27 -0
- package/src/components/ChartLine.tsx +119 -0
- package/src/components/ChatComposer.tsx +73 -0
- package/src/components/Checkbox.tsx +14 -0
- package/src/components/Checklist.tsx +45 -0
- package/src/components/ChipFilter.tsx +60 -0
- package/src/components/Collapsible.tsx +43 -0
- package/src/components/ColorField.tsx +48 -0
- package/src/components/ColorSelect.tsx +49 -0
- package/src/components/ColumnMenu.tsx +66 -0
- package/src/components/Command.tsx +113 -0
- package/src/components/ConfirmDialog.tsx +95 -0
- package/src/components/CookieConsent.tsx +137 -0
- package/src/components/CopyField.tsx +44 -0
- package/src/components/Counter.tsx +21 -0
- package/src/components/DataTable.tsx +201 -0
- package/src/components/DecimalInput.tsx +87 -0
- package/src/components/DefinitionList.tsx +38 -0
- package/src/components/DocViewer.tsx +110 -0
- package/src/components/Dropdown.tsx +52 -0
- package/src/components/Dropzone.tsx +46 -0
- package/src/components/EmojiPicker.tsx +72 -0
- package/src/components/EmptyState.tsx +27 -0
- package/src/components/EntityCard.tsx +56 -0
- package/src/components/FactList.tsx +43 -0
- package/src/components/FilterCounts.tsx +50 -0
- package/src/components/FlipCard.tsx +55 -0
- package/src/components/FormField.tsx +54 -0
- package/src/components/Greeting.tsx +63 -0
- package/src/components/Heading.tsx +27 -0
- package/src/components/Icon.tsx +25 -0
- package/src/components/ImageZoom.tsx +51 -0
- package/src/components/Input.tsx +21 -0
- package/src/components/InputGroup.tsx +24 -0
- package/src/components/LanguageSelect.tsx +64 -0
- package/src/components/LauncherCard.tsx +50 -0
- package/src/components/Lightbox.tsx +83 -0
- package/src/components/LinkArrow.tsx +27 -0
- package/src/components/ListToolbar.tsx +67 -0
- package/src/components/Marquee.tsx +38 -0
- package/src/components/MediaCard.tsx +47 -0
- package/src/components/MediaRow.tsx +53 -0
- package/src/components/MediaThumb.tsx +49 -0
- package/src/components/Message.tsx +171 -0
- package/src/components/MessageThread.tsx +177 -0
- package/src/components/Modal.tsx +81 -0
- package/src/components/Navmenu.tsx +62 -0
- package/src/components/NextTeaser.tsx +29 -0
- package/src/components/NumberInput.tsx +64 -0
- package/src/components/OtpInput.tsx +48 -0
- package/src/components/PageHeader.tsx +43 -0
- package/src/components/Pagination.tsx +42 -0
- package/src/components/PasswordInput.tsx +36 -0
- package/src/components/PasswordRequirements.tsx +46 -0
- package/src/components/PhoneInput.tsx +93 -0
- package/src/components/Popover.tsx +34 -0
- package/src/components/PriceCard.tsx +83 -0
- package/src/components/ProductMark.tsx +35 -0
- package/src/components/Progress.tsx +32 -0
- package/src/components/ProjectCard.tsx +46 -0
- package/src/components/Prose.tsx +79 -0
- package/src/components/RecoveryCodes.tsx +65 -0
- package/src/components/Segmented.tsx +47 -0
- package/src/components/Select.tsx +27 -0
- package/src/components/SeverityCard.tsx +53 -0
- package/src/components/Sheet.tsx +62 -0
- package/src/components/Sidebar.tsx +145 -0
- package/src/components/Skeleton.tsx +12 -0
- package/src/components/SortableRow.tsx +46 -0
- package/src/components/StackCard.tsx +40 -0
- package/src/components/StackList.tsx +42 -0
- package/src/components/StatCard.tsx +49 -0
- package/src/components/Stepper.tsx +54 -0
- package/src/components/Switch.tsx +18 -0
- package/src/components/Table.tsx +75 -0
- package/src/components/TableActions.tsx +54 -0
- package/src/components/TableCompact.tsx +61 -0
- package/src/components/Tabs.tsx +45 -0
- package/src/components/TagInput.tsx +73 -0
- package/src/components/TerminalLog.tsx +63 -0
- package/src/components/Textarea.tsx +19 -0
- package/src/components/ThemeSwitch.tsx +45 -0
- package/src/components/TimeInput.tsx +76 -0
- package/src/components/Timeline.tsx +45 -0
- package/src/components/Toast.tsx +138 -0
- package/src/components/ToggleField.tsx +39 -0
- package/src/components/Tooltip.tsx +14 -0
- package/src/components/TopProgress.tsx +54 -0
- package/src/components/Tree.tsx +97 -0
- package/src/components/UpgradeBanner.tsx +62 -0
- package/src/components/User.tsx +31 -0
- package/src/components/VersionBanner.tsx +41 -0
- package/src/hooks.tsx +141 -0
- package/src/index.ts +142 -0
- package/src/utils.ts +25 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { cx } from "../utils";
|
|
4
|
+
import type { FooterLink } from "./BlockFooter";
|
|
5
|
+
|
|
6
|
+
export interface SiteFooterColumn {
|
|
7
|
+
title: ReactNode;
|
|
8
|
+
/** Link list column. */
|
|
9
|
+
links?: FooterLink[];
|
|
10
|
+
/** Address column: children are rendered in an `<address class="oc-site-footer__list">`. */
|
|
11
|
+
address?: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface BlockSiteFooterProps extends Omit<HTMLAttributes<HTMLElement>, "about"> {
|
|
15
|
+
/** Brand link content, e.g. icon tile + name (rendered in `oc-site-header__brand`). */
|
|
16
|
+
brand: ReactNode;
|
|
17
|
+
brandHref?: string;
|
|
18
|
+
about?: ReactNode;
|
|
19
|
+
columns: SiteFooterColumn[];
|
|
20
|
+
copyright?: ReactNode;
|
|
21
|
+
credit?: ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** `.oc-site-footer`: brand column, link columns and a bottom bar. */
|
|
25
|
+
export const BlockSiteFooter = forwardRef<HTMLElement, BlockSiteFooterProps>(function BlockSiteFooter(
|
|
26
|
+
{ brand, brandHref = "/", about, columns, copyright, credit, className, ...rest },
|
|
27
|
+
ref,
|
|
28
|
+
) {
|
|
29
|
+
return (
|
|
30
|
+
<footer ref={ref} className={cx("oc-site-footer", className)} {...rest}>
|
|
31
|
+
<div className="oc-container">
|
|
32
|
+
<div className="oc-site-footer__grid">
|
|
33
|
+
<div className="oc-site-footer__about">
|
|
34
|
+
<a className="oc-site-header__brand" href={brandHref}>{brand}</a>
|
|
35
|
+
{about != null && <p>{about}</p>}
|
|
36
|
+
</div>
|
|
37
|
+
{columns.map((col, i) => (
|
|
38
|
+
<div key={i}>
|
|
39
|
+
<h2 className="oc-site-footer__title">{col.title}</h2>
|
|
40
|
+
{col.links && (
|
|
41
|
+
<ul className="oc-site-footer__list">
|
|
42
|
+
{col.links.map((l, j) => (
|
|
43
|
+
<li key={j}><a href={l.href}>{l.label}</a></li>
|
|
44
|
+
))}
|
|
45
|
+
</ul>
|
|
46
|
+
)}
|
|
47
|
+
{col.address != null && <address className="oc-site-footer__list">{col.address}</address>}
|
|
48
|
+
</div>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
{(copyright != null || credit != null) && (
|
|
52
|
+
<div className="oc-site-footer__bottom">
|
|
53
|
+
<span>{copyright}</span>
|
|
54
|
+
<span>{credit}</span>
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
</footer>
|
|
59
|
+
);
|
|
60
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { forwardRef, useId } from "react";
|
|
2
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { attrs, cx } from "../utils";
|
|
4
|
+
|
|
5
|
+
export interface SiteHeaderLink {
|
|
6
|
+
label: ReactNode;
|
|
7
|
+
href: string;
|
|
8
|
+
current?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface BlockSiteHeaderProps extends HTMLAttributes<HTMLElement> {
|
|
12
|
+
/** Brand link content, e.g. icon tile + name. */
|
|
13
|
+
brand: ReactNode;
|
|
14
|
+
brandHref?: string;
|
|
15
|
+
links: SiteHeaderLink[];
|
|
16
|
+
/** Trailing call to action inside the nav (e.g. a `ButtonLink` with `variant="gradient"`). */
|
|
17
|
+
cta?: ReactNode;
|
|
18
|
+
/** Accessible name of the mobile menu button. */
|
|
19
|
+
menuLabel?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** `.oc-site-header`: sticky header, mobile menu on a native popover (no JavaScript). */
|
|
23
|
+
export const BlockSiteHeader = forwardRef<HTMLElement, BlockSiteHeaderProps>(function BlockSiteHeader(
|
|
24
|
+
{ brand, brandHref = "/", links, cta, menuLabel = "Menu", className, ...rest },
|
|
25
|
+
ref,
|
|
26
|
+
) {
|
|
27
|
+
const navId = `oc-site-nav-${useId().replace(/:/g, "")}`;
|
|
28
|
+
return (
|
|
29
|
+
<header ref={ref} className={cx("oc-site-header", className)} {...rest}>
|
|
30
|
+
<div className="oc-container oc-site-header__inner">
|
|
31
|
+
<a className="oc-site-header__brand" href={brandHref}>{brand}</a>
|
|
32
|
+
<button className="oc-site-header__toggle" type="button" aria-label={menuLabel} {...attrs({ popovertarget: navId })}>☰</button>
|
|
33
|
+
<nav className="oc-site-header__nav" id={navId} aria-label="Main" {...attrs({ popover: "" })}>
|
|
34
|
+
{links.map((l, i) => (
|
|
35
|
+
<a key={i} href={l.href} aria-current={l.current ? "page" : undefined}>{l.label}</a>
|
|
36
|
+
))}
|
|
37
|
+
{cta}
|
|
38
|
+
</nav>
|
|
39
|
+
</div>
|
|
40
|
+
</header>
|
|
41
|
+
);
|
|
42
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { cx } from "../utils";
|
|
4
|
+
|
|
5
|
+
export interface SpotlightModule {
|
|
6
|
+
icon?: ReactNode;
|
|
7
|
+
label: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface BlockSpotlightProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
11
|
+
/** Accent name (`green`, `violet`...), applied as `oc-accent--{accent}`. */
|
|
12
|
+
accent?: string;
|
|
13
|
+
/** Pill text (with a status dot). */
|
|
14
|
+
badge?: ReactNode;
|
|
15
|
+
/** Glyph or SVG in the big icon tile. */
|
|
16
|
+
icon?: ReactNode;
|
|
17
|
+
title: ReactNode;
|
|
18
|
+
tagline?: ReactNode;
|
|
19
|
+
lead?: ReactNode;
|
|
20
|
+
/** Call to action (e.g. a `ButtonLink` with `variant="gradient" size="lg"`). */
|
|
21
|
+
action?: ReactNode;
|
|
22
|
+
modules?: SpotlightModule[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** `.oc-spotlight`: highlighted flagship product with a module list. */
|
|
26
|
+
export const BlockSpotlight = forwardRef<HTMLElement, BlockSpotlightProps>(function BlockSpotlight(
|
|
27
|
+
{ accent, badge, icon, title, tagline, lead, action, modules, className, ...rest },
|
|
28
|
+
ref,
|
|
29
|
+
) {
|
|
30
|
+
return (
|
|
31
|
+
<section ref={ref} className={cx("oc-section-pad", className)} {...rest}>
|
|
32
|
+
<div className="oc-container">
|
|
33
|
+
<div className={cx("oc-spotlight", accent && `oc-accent--${accent}`)}>
|
|
34
|
+
<div>
|
|
35
|
+
{badge != null && <span className="oc-pill oc-pill--accent"><span className="oc-status-dot" />{badge}</span>}
|
|
36
|
+
<h2 className="oc-spotlight__title">
|
|
37
|
+
{icon != null && <span className="oc-icon-tile oc-icon-tile--lg" aria-hidden="true">{icon}</span>}
|
|
38
|
+
{title}
|
|
39
|
+
</h2>
|
|
40
|
+
{tagline != null && <p className="oc-spotlight__tagline">{tagline}</p>}
|
|
41
|
+
{lead != null && <p className="oc-lead">{lead}</p>}
|
|
42
|
+
{action}
|
|
43
|
+
</div>
|
|
44
|
+
{modules && (
|
|
45
|
+
<ul className="oc-spotlight__modules">
|
|
46
|
+
{modules.map((m, i) => (
|
|
47
|
+
<li key={i}>
|
|
48
|
+
{m.icon != null && <span className="oc-icon-tile oc-icon-tile--sm" aria-hidden="true">{m.icon}</span>}
|
|
49
|
+
{m.label}
|
|
50
|
+
</li>
|
|
51
|
+
))}
|
|
52
|
+
</ul>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</section>
|
|
57
|
+
);
|
|
58
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { cx } from "../utils";
|
|
4
|
+
|
|
5
|
+
export interface Stat {
|
|
6
|
+
value: ReactNode;
|
|
7
|
+
label: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface BlockStatsProps extends HTMLAttributes<HTMLDivElement> {
|
|
11
|
+
stats: Stat[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** `.oc-block-stats`: key figures row. */
|
|
15
|
+
export const BlockStats = forwardRef<HTMLDivElement, BlockStatsProps>(function BlockStats({ stats, className, ...rest }, ref) {
|
|
16
|
+
return (
|
|
17
|
+
<div ref={ref} className={cx("oc-block-stats", className)} {...rest}>
|
|
18
|
+
{stats.map((s, i) => (
|
|
19
|
+
<div className="oc-block-stat" key={i}>
|
|
20
|
+
<div className="oc-block-stat-value">{s.value}</div>
|
|
21
|
+
<div className="oc-block-stat-label">{s.label}</div>
|
|
22
|
+
</div>
|
|
23
|
+
))}
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { cx } from "../utils";
|
|
4
|
+
import { Counter } from "../components/Counter";
|
|
5
|
+
|
|
6
|
+
export interface CounterStat {
|
|
7
|
+
to: number;
|
|
8
|
+
suffix?: string;
|
|
9
|
+
label: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface BlockStatsCounterProps extends HTMLAttributes<HTMLDivElement> {
|
|
13
|
+
stats: CounterStat[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** `.oc-block-stats` whose values count up when scrolled into view (CSS only). */
|
|
17
|
+
export const BlockStatsCounter = forwardRef<HTMLDivElement, BlockStatsCounterProps>(function BlockStatsCounter(
|
|
18
|
+
{ stats, className, ...rest },
|
|
19
|
+
ref,
|
|
20
|
+
) {
|
|
21
|
+
return (
|
|
22
|
+
<div ref={ref} className={cx("oc-block-stats", className)} {...rest}>
|
|
23
|
+
{stats.map((s, i) => (
|
|
24
|
+
<div className="oc-block-stat" key={i}>
|
|
25
|
+
<Counter className="oc-block-stat-value" to={s.to} suffix={s.suffix} />
|
|
26
|
+
<span className="oc-block-stat-label">{s.label}</span>
|
|
27
|
+
</div>
|
|
28
|
+
))}
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { cx } from "../utils";
|
|
4
|
+
import { Badge } from "../components/Badge";
|
|
5
|
+
import type { BadgeVariant } from "../components/Badge";
|
|
6
|
+
import { Dropdown } from "../components/Dropdown";
|
|
7
|
+
import { User } from "../components/User";
|
|
8
|
+
|
|
9
|
+
export interface TeamMember {
|
|
10
|
+
name: ReactNode;
|
|
11
|
+
email?: ReactNode;
|
|
12
|
+
initials?: string;
|
|
13
|
+
avatarSrc?: string;
|
|
14
|
+
/** Role / status badge text. */
|
|
15
|
+
role?: ReactNode;
|
|
16
|
+
roleVariant?: BadgeVariant;
|
|
17
|
+
/** Row action menu content (`DropdownItem`s). Adds a "..." trigger when set. */
|
|
18
|
+
menu?: ReactNode;
|
|
19
|
+
menuLabel?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface BlockTeamListProps extends HTMLAttributes<HTMLDivElement> {
|
|
23
|
+
members: TeamMember[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `.oc-block-team`: member rows with avatar, role badge and a row action menu. */
|
|
27
|
+
export const BlockTeamList = forwardRef<HTMLDivElement, BlockTeamListProps>(function BlockTeamList(
|
|
28
|
+
{ members, className, ...rest },
|
|
29
|
+
ref,
|
|
30
|
+
) {
|
|
31
|
+
return (
|
|
32
|
+
<div ref={ref} className={cx("oc-block-team", className)} {...rest}>
|
|
33
|
+
{members.map((m, i) => (
|
|
34
|
+
<div className="oc-block-team-row" key={i}>
|
|
35
|
+
<User name={m.name} email={m.email} initials={m.initials} avatarSrc={m.avatarSrc} />
|
|
36
|
+
<div className="oc-block-team-meta">
|
|
37
|
+
{m.role != null && <Badge variant={m.roleVariant}>{m.role}</Badge>}
|
|
38
|
+
{m.menu != null && (
|
|
39
|
+
<Dropdown trigger={<>…</>} triggerProps={{ variant: "ghost", icon: true, size: "sm", type: "button", "aria-label": m.menuLabel ?? "Member actions" }}>
|
|
40
|
+
{m.menu}
|
|
41
|
+
</Dropdown>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useId, useState } from "react";
|
|
4
|
+
import type { FormEvent, HTMLAttributes, ReactNode } from "react";
|
|
5
|
+
import { cx } from "../utils";
|
|
6
|
+
import { CopyField } from "../components/CopyField";
|
|
7
|
+
import { Heading } from "../components/Heading";
|
|
8
|
+
import { OtpInput } from "../components/OtpInput";
|
|
9
|
+
import { RecoveryCodes } from "../components/RecoveryCodes";
|
|
10
|
+
|
|
11
|
+
export interface BlockTwoFactorProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSubmit"> {
|
|
12
|
+
/** QR code image / svg; a labelled placeholder box is rendered when omitted. */
|
|
13
|
+
qr?: ReactNode;
|
|
14
|
+
setupKey: string;
|
|
15
|
+
recoveryCodes?: string[];
|
|
16
|
+
codeLength?: number;
|
|
17
|
+
/** Called with the entered code when the form is submitted. */
|
|
18
|
+
onSubmit?: (code: string) => void;
|
|
19
|
+
submitLabel?: ReactNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** `.oc-block-twofactor`: authenticator setup with QR, copyable key, OTP input and recovery codes. */
|
|
23
|
+
export const BlockTwoFactor = forwardRef<HTMLDivElement, BlockTwoFactorProps>(function BlockTwoFactor(
|
|
24
|
+
{ qr, setupKey, recoveryCodes, codeLength = 6, onSubmit, submitLabel = "Enable two-factor", className, ...rest },
|
|
25
|
+
ref,
|
|
26
|
+
) {
|
|
27
|
+
const id = `oc-twofactor-${useId().replace(/:/g, "")}`;
|
|
28
|
+
const [code, setCode] = useState("");
|
|
29
|
+
const submit = (e: FormEvent) => {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
onSubmit?.(code);
|
|
32
|
+
};
|
|
33
|
+
return (
|
|
34
|
+
<div ref={ref} className={cx("oc-block-twofactor", className)} {...rest}>
|
|
35
|
+
<section className="oc-block-twofactor-step">
|
|
36
|
+
<Heading size="sm" title="1. Scan the QR code" description="Use an authenticator app, or enter the key manually." />
|
|
37
|
+
<div className="oc-block-twofactor-center">
|
|
38
|
+
{qr ?? <div className="oc-block-twofactor-qr" role="img" aria-label="QR code">QR code</div>}
|
|
39
|
+
<CopyField value={setupKey} label="Setup key" />
|
|
40
|
+
</div>
|
|
41
|
+
</section>
|
|
42
|
+
<form className="oc-block-twofactor-step" onSubmit={submit}>
|
|
43
|
+
<Heading size="sm" title="2. Confirm with a code" />
|
|
44
|
+
<div className="oc-field">
|
|
45
|
+
<label className="oc-field-label" htmlFor={id}>Authentication code</label>
|
|
46
|
+
<OtpInput id={id} name="code" length={codeLength} value={code} onChange={setCode} />
|
|
47
|
+
</div>
|
|
48
|
+
<button className="oc-btn" type="submit">{submitLabel}</button>
|
|
49
|
+
</form>
|
|
50
|
+
{recoveryCodes && (
|
|
51
|
+
<section className="oc-block-twofactor-step">
|
|
52
|
+
<RecoveryCodes codes={recoveryCodes} />
|
|
53
|
+
</section>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useId, useRef } from "react";
|
|
4
|
+
import type { DialogHTMLAttributes, ReactNode } from "react";
|
|
5
|
+
import { attrs, cx } from "../utils";
|
|
6
|
+
import { DialogContext, useControllable, useDialog, useMergedRef } from "../hooks";
|
|
7
|
+
import type { DialogState } from "../hooks";
|
|
8
|
+
import { Stepper } from "../components/Stepper";
|
|
9
|
+
|
|
10
|
+
export interface WizardStep {
|
|
11
|
+
label: ReactNode;
|
|
12
|
+
/** Step content (only the active step is rendered). */
|
|
13
|
+
content: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface BlockWizardDialogProps extends Omit<DialogHTMLAttributes<HTMLDialogElement>, "open" | "onClose" | "title">, DialogState {
|
|
17
|
+
steps: WizardStep[];
|
|
18
|
+
/** Active step (0-based, controlled). */
|
|
19
|
+
step?: number;
|
|
20
|
+
defaultStep?: number;
|
|
21
|
+
onStepChange?: (step: number) => void;
|
|
22
|
+
/** Called when "Finish" is pressed on the last step. */
|
|
23
|
+
onFinish?: () => void;
|
|
24
|
+
backLabel?: ReactNode;
|
|
25
|
+
nextLabel?: ReactNode;
|
|
26
|
+
finishLabel?: ReactNode;
|
|
27
|
+
/** Text under the title; defaults to `Step {n} of {total}`. */
|
|
28
|
+
description?: ReactNode;
|
|
29
|
+
stepperLabel?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `dialog.oc-modal` wizard: stepper rail on the left, step content and Back / Next footer. */
|
|
33
|
+
export const BlockWizardDialog = forwardRef<HTMLDialogElement, BlockWizardDialogProps>(function BlockWizardDialog(
|
|
34
|
+
{
|
|
35
|
+
open,
|
|
36
|
+
defaultOpen,
|
|
37
|
+
onOpenChange,
|
|
38
|
+
steps,
|
|
39
|
+
step,
|
|
40
|
+
defaultStep = 0,
|
|
41
|
+
onStepChange,
|
|
42
|
+
onFinish,
|
|
43
|
+
backLabel = "Back",
|
|
44
|
+
nextLabel = "Next",
|
|
45
|
+
finishLabel = "Finish",
|
|
46
|
+
description,
|
|
47
|
+
stepperLabel = "Setup steps",
|
|
48
|
+
className,
|
|
49
|
+
...rest
|
|
50
|
+
},
|
|
51
|
+
forwardedRef,
|
|
52
|
+
) {
|
|
53
|
+
const inner = useRef<HTMLDialogElement>(null);
|
|
54
|
+
const ref = useMergedRef(inner, forwardedRef);
|
|
55
|
+
const titleId = `oc-wizard-${useId().replace(/:/g, "")}-title`;
|
|
56
|
+
const dlg = useDialog(inner, { open, defaultOpen, onOpenChange });
|
|
57
|
+
const [current, setCurrent] = useControllable<number>(step, defaultStep, onStepChange);
|
|
58
|
+
const last = current >= steps.length - 1;
|
|
59
|
+
const active = steps[current];
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<dialog
|
|
63
|
+
ref={ref}
|
|
64
|
+
className={cx("oc-modal", className)}
|
|
65
|
+
aria-labelledby={titleId}
|
|
66
|
+
{...attrs({ closedby: "closerequest" })}
|
|
67
|
+
{...rest}
|
|
68
|
+
onClose={dlg.onClose}
|
|
69
|
+
>
|
|
70
|
+
<DialogContext.Provider value={{ close: dlg.close }}>
|
|
71
|
+
<div className="oc-modal-content oc-modal-content--xl">
|
|
72
|
+
<div className="oc-block-wizard">
|
|
73
|
+
<div className="oc-block-wizard-rail">
|
|
74
|
+
<Stepper steps={steps.map((s) => ({ label: s.label }))} current={current} onStepChange={setCurrent} aria-label={stepperLabel} />
|
|
75
|
+
</div>
|
|
76
|
+
<div className="oc-block-wizard-main">
|
|
77
|
+
<div className="oc-modal-header">
|
|
78
|
+
<h2 className="oc-modal-title" id={titleId}>{active?.label}</h2>
|
|
79
|
+
<p className="oc-modal-description">{description ?? `Step ${current + 1} of ${steps.length}`}</p>
|
|
80
|
+
</div>
|
|
81
|
+
<div className="oc-block-wizard-step">{active?.content}</div>
|
|
82
|
+
<div className="oc-modal-footer">
|
|
83
|
+
<button className="oc-btn oc-btn--outline" type="button" disabled={current === 0} onClick={() => setCurrent(current - 1)}>{backLabel}</button>
|
|
84
|
+
<button
|
|
85
|
+
className="oc-btn"
|
|
86
|
+
type="button"
|
|
87
|
+
onClick={() => {
|
|
88
|
+
if (last) onFinish?.();
|
|
89
|
+
else setCurrent(current + 1);
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{last ? finishLabel : nextLabel}
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</DialogContext.Provider>
|
|
99
|
+
</dialog>
|
|
100
|
+
);
|
|
101
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import type { DetailsHTMLAttributes, HTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { cx } from "../utils";
|
|
4
|
+
|
|
5
|
+
/** `<div class="oc-accordion">` */
|
|
6
|
+
export const Accordion = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(function Accordion(
|
|
7
|
+
{ className, ...rest },
|
|
8
|
+
ref,
|
|
9
|
+
) {
|
|
10
|
+
return <div ref={ref} className={cx("oc-accordion", className)} {...rest} />;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export interface AccordionItemProps extends Omit<DetailsHTMLAttributes<HTMLDetailsElement>, "title"> {
|
|
14
|
+
/** Trigger text (`summary.oc-accordion-trigger`). */
|
|
15
|
+
title: ReactNode;
|
|
16
|
+
/** Items with the same `name` open one at a time (native exclusive details). */
|
|
17
|
+
name?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** `<details class="oc-accordion-item" name>` */
|
|
21
|
+
export const AccordionItem = forwardRef<HTMLDetailsElement, AccordionItemProps>(function AccordionItem(
|
|
22
|
+
{ title, className, children, ...rest },
|
|
23
|
+
ref,
|
|
24
|
+
) {
|
|
25
|
+
return (
|
|
26
|
+
<details ref={ref} className={cx("oc-accordion-item", className)} {...(rest as object)}>
|
|
27
|
+
<summary className="oc-accordion-trigger">{title}</summary>
|
|
28
|
+
<div className="oc-accordion-content">{children}</div>
|
|
29
|
+
</details>
|
|
30
|
+
);
|
|
31
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// File: packages/react/src/components/AccordionAccent.tsx
|
|
2
|
+
// Map: AccordionAccent = div.oc-accordion.oc-accordion--accent (+ color) -> AccordionItem children
|
|
3
|
+
// Use the existing <AccordionItem> inside. Color an item with className "oc-accent--violet"
|
|
4
|
+
// or "oc-accordion-item--warning". AccordionAccentMeta is the small label at the end of a title.
|
|
5
|
+
import { forwardRef } from "react";
|
|
6
|
+
import type { HTMLAttributes } from "react";
|
|
7
|
+
import { cx } from "../utils";
|
|
8
|
+
|
|
9
|
+
export type AccordionAccentPreset = "blue" | "cyan" | "green" | "orange" | "red" | "violet" | "yellow";
|
|
10
|
+
export type AccordionAccentTone = "success" | "warning" | "danger";
|
|
11
|
+
|
|
12
|
+
export interface AccordionAccentProps extends HTMLAttributes<HTMLDivElement> {
|
|
13
|
+
/** Color preset for all items (an `.oc-accent--*` class). */
|
|
14
|
+
accent?: AccordionAccentPreset;
|
|
15
|
+
/** Semantic color for all items. */
|
|
16
|
+
tone?: AccordionAccentTone;
|
|
17
|
+
/** Only the accent edge, no card frame. */
|
|
18
|
+
flush?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** `.oc-accordion--accent`: accordion with separate cards and a colored start edge. */
|
|
22
|
+
export const AccordionAccent = forwardRef<HTMLDivElement, AccordionAccentProps>(function AccordionAccent(
|
|
23
|
+
{ accent, tone, flush, className, ...rest },
|
|
24
|
+
ref,
|
|
25
|
+
) {
|
|
26
|
+
const cls = cx(
|
|
27
|
+
"oc-accordion oc-accordion--accent",
|
|
28
|
+
flush && "oc-accordion--flush",
|
|
29
|
+
accent && `oc-accent--${accent}`,
|
|
30
|
+
tone && `oc-accordion--${tone}`,
|
|
31
|
+
className,
|
|
32
|
+
);
|
|
33
|
+
return <div ref={ref} className={cls} {...rest} />;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/** `.oc-accordion-accent-meta`: short accent-colored label inside an item title. */
|
|
37
|
+
export function AccordionAccentMeta({ className, ...rest }: HTMLAttributes<HTMLSpanElement>) {
|
|
38
|
+
return <span className={cx("oc-accordion-accent-meta", className)} {...rest} />;
|
|
39
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// File: packages/react/src/components/AffixInput.tsx
|
|
2
|
+
// Map: variant presets (currency / percent / email) -> component (input group --affix with inline symbol)
|
|
3
|
+
// Same markup as packages/ui/snippets/affix-input.html. No state: a plain input with a symbol inside the frame.
|
|
4
|
+
import { forwardRef } from "react";
|
|
5
|
+
import type { InputHTMLAttributes, ReactNode } from "react";
|
|
6
|
+
import { cx } from "../utils";
|
|
7
|
+
|
|
8
|
+
export type AffixInputVariant = "currency" | "percent" | "email";
|
|
9
|
+
|
|
10
|
+
export interface AffixInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "prefix"> {
|
|
11
|
+
/** Preset: symbol, input type, inputmode and alignment. */
|
|
12
|
+
variant?: AffixInputVariant;
|
|
13
|
+
/** Symbol before the value (overrides the preset). */
|
|
14
|
+
prefix?: ReactNode;
|
|
15
|
+
/** Symbol after the value (overrides the preset). */
|
|
16
|
+
suffix?: ReactNode;
|
|
17
|
+
/** Right-aligned tabular digits. Off by default: the value sits next to the symbol column. */
|
|
18
|
+
alignEnd?: boolean;
|
|
19
|
+
error?: boolean;
|
|
20
|
+
/** Class for the wrapper (`className` goes to the input). */
|
|
21
|
+
groupClassName?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Preset per variant. Email uses type="email" so :user-invalid checks the address.
|
|
25
|
+
const PRESETS: Record<AffixInputVariant, { prefix?: string; suffix?: string; end: boolean; input: InputHTMLAttributes<HTMLInputElement> }> = {
|
|
26
|
+
currency: { prefix: "\u20AC", end: false, input: { type: "text", inputMode: "decimal" } },
|
|
27
|
+
percent: { prefix: "%", end: false, input: { type: "text", inputMode: "decimal" } },
|
|
28
|
+
email: { prefix: "@", end: false, input: { type: "email", autoComplete: "email" } },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** `.oc-input-group.oc-input-group--affix`: symbol (€, %, @) inside the field. */
|
|
32
|
+
export const AffixInput = forwardRef<HTMLInputElement, AffixInputProps>(function AffixInput(
|
|
33
|
+
{ variant, prefix, suffix, alignEnd, error, groupClassName, className, ...rest },
|
|
34
|
+
ref,
|
|
35
|
+
) {
|
|
36
|
+
const preset = variant ? PRESETS[variant] : undefined;
|
|
37
|
+
const before = prefix ?? preset?.prefix;
|
|
38
|
+
const after = suffix ?? preset?.suffix;
|
|
39
|
+
const end = alignEnd ?? preset?.end ?? false;
|
|
40
|
+
return (
|
|
41
|
+
<div className={cx("oc-input-group", "oc-input-group--affix", end && "oc-input-group--end", error && "oc-input-group--error", groupClassName)}>
|
|
42
|
+
{before != null && <span className="oc-input-addon" aria-hidden="true">{before}</span>}
|
|
43
|
+
<input ref={ref} className={cx("oc-input", className)} aria-invalid={error ? true : undefined} {...preset?.input} {...rest} />
|
|
44
|
+
{after != null && <span className="oc-input-addon" aria-hidden="true">{after}</span>}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { cx } from "../utils";
|
|
4
|
+
|
|
5
|
+
export type AlertVariant = "info" | "success" | "warning" | "danger";
|
|
6
|
+
|
|
7
|
+
export interface AlertProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
8
|
+
variant?: AlertVariant;
|
|
9
|
+
/** Heading (`oc-alert-title`). */
|
|
10
|
+
title?: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** `<div class="oc-alert oc-alert--{variant}" role="status">`; children render inside `oc-alert-description`. */
|
|
14
|
+
export const Alert = forwardRef<HTMLDivElement, AlertProps>(function Alert(
|
|
15
|
+
{ variant = "info", title, children, className, role = "status", ...rest },
|
|
16
|
+
ref,
|
|
17
|
+
) {
|
|
18
|
+
return (
|
|
19
|
+
<div ref={ref} role={role} className={cx("oc-alert", `oc-alert--${variant}`, className)} {...rest}>
|
|
20
|
+
{title != null && <h4 className="oc-alert-title">{title}</h4>}
|
|
21
|
+
{children != null && <p className="oc-alert-description">{children}</p>}
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useId, useRef } from "react";
|
|
4
|
+
import type { DialogHTMLAttributes, ReactNode } from "react";
|
|
5
|
+
import { attrs, cx } from "../utils";
|
|
6
|
+
import { DialogContext, useDialog, useMergedRef } from "../hooks";
|
|
7
|
+
import type { DialogState } from "../hooks";
|
|
8
|
+
|
|
9
|
+
export type AlertDialogType = "success" | "error" | "warning" | "info" | "question";
|
|
10
|
+
|
|
11
|
+
const GLYPHS: Record<AlertDialogType, string> = {
|
|
12
|
+
success: "✓",
|
|
13
|
+
error: "×",
|
|
14
|
+
warning: "!",
|
|
15
|
+
info: "i",
|
|
16
|
+
question: "?",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface AlertDialogProps extends Omit<DialogHTMLAttributes<HTMLDialogElement>, "title" | "open" | "onClose">, DialogState {
|
|
20
|
+
/** Type modifier on the content box (sets the icon color). Default `warning`. */
|
|
21
|
+
type?: AlertDialogType;
|
|
22
|
+
/** Custom icon glyph or SVG (defaults to a glyph for the type). */
|
|
23
|
+
icon?: ReactNode;
|
|
24
|
+
title: ReactNode;
|
|
25
|
+
/** Message (`oc-modal-body`): pass as `message` or as children. */
|
|
26
|
+
message?: ReactNode;
|
|
27
|
+
/** Secondary + primary buttons (`oc-modal-footer`), e.g. `<ModalClose>`s. */
|
|
28
|
+
footer?: ReactNode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Confirmation `<dialog class="oc-modal">` with a status icon. */
|
|
32
|
+
export const AlertDialog = forwardRef<HTMLDialogElement, AlertDialogProps>(function AlertDialog(
|
|
33
|
+
{ open, defaultOpen, onOpenChange, type = "warning", icon, title, message, footer, className, children, ...rest },
|
|
34
|
+
forwardedRef,
|
|
35
|
+
) {
|
|
36
|
+
const inner = useRef<HTMLDialogElement>(null);
|
|
37
|
+
const ref = useMergedRef(inner, forwardedRef);
|
|
38
|
+
const id = useId();
|
|
39
|
+
const dlg = useDialog(inner, { open, defaultOpen, onOpenChange });
|
|
40
|
+
return (
|
|
41
|
+
<dialog
|
|
42
|
+
ref={ref}
|
|
43
|
+
className={cx("oc-modal", className)}
|
|
44
|
+
aria-labelledby={`${id}-title`}
|
|
45
|
+
aria-describedby={`${id}-text`}
|
|
46
|
+
{...attrs({ closedby: "closerequest" })}
|
|
47
|
+
{...rest}
|
|
48
|
+
onClose={dlg.onClose}
|
|
49
|
+
>
|
|
50
|
+
<DialogContext.Provider value={{ close: dlg.close }}>
|
|
51
|
+
<div className={`oc-modal-content oc-modal-content--sm oc-alert-dialog oc-alert-dialog--${type}`}>
|
|
52
|
+
<span className="oc-alert-dialog-icon" aria-hidden="true">{icon ?? GLYPHS[type]}</span>
|
|
53
|
+
<h2 className="oc-modal-title" id={`${id}-title`}>{title}</h2>
|
|
54
|
+
<div className="oc-modal-body" id={`${id}-text`}>{message ?? children}</div>
|
|
55
|
+
{footer != null && <div className="oc-modal-footer">{footer}</div>}
|
|
56
|
+
</div>
|
|
57
|
+
</DialogContext.Provider>
|
|
58
|
+
</dialog>
|
|
59
|
+
);
|
|
60
|
+
});
|