@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
package/package.json
CHANGED
|
@@ -1,6 +1,71 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@birdycom/omniaui-react",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"description": "OmniaUI for React: typed function components and page blocks that render the exact @birdycom/omniaui and @birdycom/omniaui-blocks markup.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Serge Cools (Birdycom)",
|
|
8
|
+
"homepage": "https://omniaui.eu",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/Birdycom/OmniaUI.git",
|
|
12
|
+
"directory": "packages/react"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/Birdycom/OmniaUI/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"react",
|
|
19
|
+
"components",
|
|
20
|
+
"css",
|
|
21
|
+
"omniaui",
|
|
22
|
+
"tsx"
|
|
23
|
+
],
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"module": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./src/*": "./src/*",
|
|
33
|
+
"./manifest.json": "./manifest.json",
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"src",
|
|
39
|
+
"manifest.json",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"sideEffects": false,
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "node scripts/build-manifest.mjs && tsup",
|
|
46
|
+
"typecheck": "tsc --noEmit",
|
|
47
|
+
"test": "vitest run"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"react": ">=18",
|
|
51
|
+
"react-dom": ">=18"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
55
|
+
"@testing-library/react": "^16.1.0",
|
|
56
|
+
"@types/react": "^19.0.0",
|
|
57
|
+
"@types/react-dom": "^19.0.0",
|
|
58
|
+
"jsdom": "^26.0.0",
|
|
59
|
+
"react": "^19.0.0",
|
|
60
|
+
"react-dom": "^19.0.0",
|
|
61
|
+
"tsup": "^8.3.5",
|
|
62
|
+
"typescript": "^5.7.0",
|
|
63
|
+
"vitest": "^3.0.0"
|
|
64
|
+
},
|
|
65
|
+
"engines": {
|
|
66
|
+
"node": ">=22"
|
|
67
|
+
},
|
|
68
|
+
"publishConfig": {
|
|
69
|
+
"access": "public"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// File: packages/react/src/blocks/BlockAboutTimeline.tsx | Map: props -> BlockAboutTimeline (media + badge -> copy -> timeline)
|
|
2
|
+
import { forwardRef, useId } from "react";
|
|
3
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
4
|
+
import { cx } from "../utils";
|
|
5
|
+
import { Timeline } from "../components/Timeline";
|
|
6
|
+
import type { TimelineEntry } from "../components/Timeline";
|
|
7
|
+
|
|
8
|
+
export interface BlockAboutTimelineProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
9
|
+
eyebrow?: ReactNode;
|
|
10
|
+
/** Heading; wrap a part in `<span className="oc-text-gradient">`. */
|
|
11
|
+
title: ReactNode;
|
|
12
|
+
/** Paragraphs (or any content) below the title. */
|
|
13
|
+
children?: ReactNode;
|
|
14
|
+
timeline?: TimelineEntry[];
|
|
15
|
+
/** Picture URL; without it the media is an accent gradient. */
|
|
16
|
+
image?: string;
|
|
17
|
+
imageAlt?: string;
|
|
18
|
+
/** Badge on the picture (e.g. <><strong>12+</strong>years</>). */
|
|
19
|
+
badge?: ReactNode;
|
|
20
|
+
/** Picture on the right. */
|
|
21
|
+
reverse?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** `.oc-block-about`: picture next to the company story with a year timeline. */
|
|
25
|
+
export const BlockAboutTimeline = forwardRef<HTMLElement, BlockAboutTimelineProps>(function BlockAboutTimeline(
|
|
26
|
+
{ eyebrow, title, children, timeline = [], image, imageAlt = "", badge, reverse = false, className, ...rest },
|
|
27
|
+
ref,
|
|
28
|
+
) {
|
|
29
|
+
const titleId = `oc-about-title-${useId().replace(/[^\w-]/g, "")}`;
|
|
30
|
+
return (
|
|
31
|
+
<section ref={ref} className={cx("oc-block-about", reverse && "oc-block-about--reverse", className)} aria-labelledby={titleId} {...rest}>
|
|
32
|
+
<div className="oc-block-about-inner">
|
|
33
|
+
<figure className="oc-block-about-media" aria-hidden={image ? undefined : true}>
|
|
34
|
+
{image && <img src={image} alt={imageAlt} loading="lazy" />}
|
|
35
|
+
{badge != null && <span className="oc-block-about-badge">{badge}</span>}
|
|
36
|
+
</figure>
|
|
37
|
+
<div className="oc-block-about-copy">
|
|
38
|
+
{eyebrow != null && <p className="oc-eyebrow">{eyebrow}</p>}
|
|
39
|
+
<h2 className="oc-block-about-title" id={titleId}>{title}</h2>
|
|
40
|
+
{children}
|
|
41
|
+
{timeline.length > 0 && <Timeline items={timeline} />}
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</section>
|
|
45
|
+
);
|
|
46
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { cx } from "../utils";
|
|
4
|
+
import { Icon } from "../components/Icon";
|
|
5
|
+
import { Navmenu, NavmenuItem, NavmenuLink } from "../components/Navmenu";
|
|
6
|
+
|
|
7
|
+
export interface AppHeaderNavItem {
|
|
8
|
+
label: ReactNode;
|
|
9
|
+
href: string;
|
|
10
|
+
current?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface BlockAppHeaderProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
14
|
+
/** Id of the sidebar toggle checkbox (see BlockAppShell). Omit to hide the toggle button. */
|
|
15
|
+
toggleFor?: string;
|
|
16
|
+
toggleLabel?: string;
|
|
17
|
+
/** Module navigation links. */
|
|
18
|
+
nav?: AppHeaderNavItem[];
|
|
19
|
+
navLabel?: string;
|
|
20
|
+
/** Right-aligned actions: theme switch, switchers, user menu. */
|
|
21
|
+
actions?: ReactNode;
|
|
22
|
+
/** Optional page title (`oc-block-appbar-title`). */
|
|
23
|
+
title?: ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `.oc-block-appbar`: top bar with sidebar toggle, module navigation and actions. */
|
|
27
|
+
export const BlockAppHeader = forwardRef<HTMLElement, BlockAppHeaderProps>(function BlockAppHeader(
|
|
28
|
+
{ toggleFor, toggleLabel = "Toggle sidebar", nav, navLabel = "Module navigation", actions, title, className, ...rest },
|
|
29
|
+
ref,
|
|
30
|
+
) {
|
|
31
|
+
return (
|
|
32
|
+
<header ref={ref} className={cx("oc-block-appbar", className)} {...rest}>
|
|
33
|
+
{toggleFor && (
|
|
34
|
+
<label className="oc-btn oc-btn--ghost oc-btn--icon" htmlFor={toggleFor} title={toggleLabel}>
|
|
35
|
+
<Icon d="M4 6h16M4 12h16M4 18h16" />
|
|
36
|
+
<span className="oc-visually-hidden">{toggleLabel}</span>
|
|
37
|
+
</label>
|
|
38
|
+
)}
|
|
39
|
+
{title != null && <span className="oc-block-appbar-title">{title}</span>}
|
|
40
|
+
{nav && (
|
|
41
|
+
<Navmenu label={navLabel} navClassName="oc-block-appbar-nav">
|
|
42
|
+
{nav.map((item, i) => (
|
|
43
|
+
<NavmenuItem key={i}>
|
|
44
|
+
<NavmenuLink href={item.href} current={item.current}>{item.label}</NavmenuLink>
|
|
45
|
+
</NavmenuItem>
|
|
46
|
+
))}
|
|
47
|
+
</Navmenu>
|
|
48
|
+
)}
|
|
49
|
+
{actions != null && <div className="oc-block-appbar-actions">{actions}</div>}
|
|
50
|
+
</header>
|
|
51
|
+
);
|
|
52
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { forwardRef, useId } from "react";
|
|
2
|
+
import type { CSSProperties, HTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { cx } from "../utils";
|
|
4
|
+
import type { BreadcrumbItemData } from "../components/Breadcrumb";
|
|
5
|
+
|
|
6
|
+
export interface BlockAppShellProps extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
/** A `<Sidebar>`; rendered as the first grid child. */
|
|
8
|
+
sidebar: ReactNode;
|
|
9
|
+
/** A `<BlockAppHeader toggleFor={toggleId}>`. */
|
|
10
|
+
header?: ReactNode;
|
|
11
|
+
/** Breadcrumb strip under the header bar. */
|
|
12
|
+
breadcrumbs?: BreadcrumbItemData[];
|
|
13
|
+
/** Id of the collapse checkbox; pass the same value to `BlockAppHeader`'s `toggleFor`. */
|
|
14
|
+
toggleId?: string;
|
|
15
|
+
/** Shell height (`--oc-block-shell-height`, default 100dvh). */
|
|
16
|
+
height?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** `.oc-block-shell`: sidebar, header bar, breadcrumb strip and the scrolling content. */
|
|
20
|
+
export const BlockAppShell = forwardRef<HTMLDivElement, BlockAppShellProps>(function BlockAppShell(
|
|
21
|
+
{ sidebar, header, breadcrumbs, toggleId, height, className, style, children, ...rest },
|
|
22
|
+
ref,
|
|
23
|
+
) {
|
|
24
|
+
const auto = useId();
|
|
25
|
+
const id = toggleId ?? `oc-shell-toggle-${auto.replace(/:/g, "")}`;
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
ref={ref}
|
|
29
|
+
className={cx("oc-block-shell", className)}
|
|
30
|
+
style={height ? ({ "--oc-block-shell-height": height, ...style } as CSSProperties) : style}
|
|
31
|
+
{...rest}
|
|
32
|
+
>
|
|
33
|
+
<input className="oc-block-shell-toggle" type="checkbox" id={id} aria-label="Collapse sidebar" />
|
|
34
|
+
{sidebar}
|
|
35
|
+
<div className="oc-block-shell-main">
|
|
36
|
+
{header}
|
|
37
|
+
{breadcrumbs && (
|
|
38
|
+
<div className="oc-block-shell-breadcrumbs">
|
|
39
|
+
<nav aria-label="Breadcrumb">
|
|
40
|
+
<ol className="oc-breadcrumb">
|
|
41
|
+
{breadcrumbs.map((item, i) =>
|
|
42
|
+
item.href && i < breadcrumbs.length - 1 ? (
|
|
43
|
+
<li className="oc-breadcrumb-item" key={i}><a href={item.href}>{item.label}</a></li>
|
|
44
|
+
) : (
|
|
45
|
+
<li className="oc-breadcrumb-item" aria-current="page" key={i}>{item.label}</li>
|
|
46
|
+
),
|
|
47
|
+
)}
|
|
48
|
+
</ol>
|
|
49
|
+
</nav>
|
|
50
|
+
</div>
|
|
51
|
+
)}
|
|
52
|
+
<main className="oc-block-shell-content">{children}</main>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { forwardRef, useId } from "react";
|
|
2
|
+
import type { FormEvent, HTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { cx } from "../utils";
|
|
4
|
+
|
|
5
|
+
export interface BlockAuthProps extends Omit<HTMLAttributes<HTMLDivElement>, "title" | "onSubmit"> {
|
|
6
|
+
title?: ReactNode;
|
|
7
|
+
subtitle?: ReactNode;
|
|
8
|
+
submitLabel?: ReactNode;
|
|
9
|
+
/** Label of the secondary (social) button; omit to hide it together with the divider. */
|
|
10
|
+
socialLabel?: ReactNode;
|
|
11
|
+
dividerLabel?: ReactNode;
|
|
12
|
+
/** Text under the card, e.g. `Don't have an account? <a href="#">Sign up</a>`. */
|
|
13
|
+
footer?: ReactNode;
|
|
14
|
+
onSubmit?: (values: { email: string; password: string }) => void;
|
|
15
|
+
onSocial?: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** `.oc-block-auth`: sign-in card with e-mail, password and an optional social button. */
|
|
19
|
+
export const BlockAuth = forwardRef<HTMLDivElement, BlockAuthProps>(function BlockAuth(
|
|
20
|
+
{ title = "Sign in", subtitle, submitLabel = "Sign in", socialLabel, dividerLabel = "or continue with", footer, onSubmit, onSocial, className, ...rest },
|
|
21
|
+
ref,
|
|
22
|
+
) {
|
|
23
|
+
const id = useId().replace(/:/g, "");
|
|
24
|
+
const submit = (e: FormEvent<HTMLFormElement>) => {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
const data = new FormData(e.currentTarget);
|
|
27
|
+
onSubmit?.({ email: String(data.get("email") ?? ""), password: String(data.get("password") ?? "") });
|
|
28
|
+
};
|
|
29
|
+
return (
|
|
30
|
+
<div ref={ref} className={cx("oc-block-auth", className)} {...rest}>
|
|
31
|
+
<form className="oc-block-auth-card" onSubmit={submit}>
|
|
32
|
+
<h2>{title}</h2>
|
|
33
|
+
{subtitle != null && <p className="oc-block-subtitle">{subtitle}</p>}
|
|
34
|
+
<div className="oc-form-group">
|
|
35
|
+
<label className="oc-label" htmlFor={`${id}-email`}>Email</label>
|
|
36
|
+
<input className="oc-input" id={`${id}-email`} name="email" type="email" placeholder="name@company.com" />
|
|
37
|
+
</div>
|
|
38
|
+
<div className="oc-form-group">
|
|
39
|
+
<label className="oc-label" htmlFor={`${id}-password`}>Password</label>
|
|
40
|
+
<input className="oc-input" id={`${id}-password`} name="password" type="password" placeholder="********" />
|
|
41
|
+
</div>
|
|
42
|
+
<button className="oc-btn" type="submit" style={{ width: "100%" }}>{submitLabel}</button>
|
|
43
|
+
{socialLabel != null && (
|
|
44
|
+
<>
|
|
45
|
+
<div className="oc-block-auth-divider">{dividerLabel}</div>
|
|
46
|
+
<button className="oc-btn oc-btn--outline" type="button" style={{ width: "100%" }} onClick={onSocial}>{socialLabel}</button>
|
|
47
|
+
</>
|
|
48
|
+
)}
|
|
49
|
+
</form>
|
|
50
|
+
{footer != null && <div className="oc-block-auth-footer">{footer}</div>}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { forwardRef, useId } from "react";
|
|
2
|
+
import type { FormEvent, HTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { cx } from "../utils";
|
|
4
|
+
import { PasswordInput } from "../components/PasswordInput";
|
|
5
|
+
|
|
6
|
+
export interface BlockAuthSplitProps extends Omit<HTMLAttributes<HTMLDivElement>, "title" | "onSubmit"> {
|
|
7
|
+
/** Brand link content (icon + name). */
|
|
8
|
+
logo?: ReactNode;
|
|
9
|
+
logoHref?: string;
|
|
10
|
+
quote?: ReactNode;
|
|
11
|
+
quoteAuthor?: ReactNode;
|
|
12
|
+
title?: ReactNode;
|
|
13
|
+
subtitle?: ReactNode;
|
|
14
|
+
submitLabel?: ReactNode;
|
|
15
|
+
forgotHref?: string;
|
|
16
|
+
forgotLabel?: ReactNode;
|
|
17
|
+
rememberLabel?: ReactNode;
|
|
18
|
+
onSubmit?: (values: { email: string; password: string; remember: boolean }) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** `.oc-block-authsplit`: brand panel (logo, quote) next to the sign-in form. */
|
|
22
|
+
export const BlockAuthSplit = forwardRef<HTMLDivElement, BlockAuthSplitProps>(function BlockAuthSplit(
|
|
23
|
+
{
|
|
24
|
+
logo,
|
|
25
|
+
logoHref = "#",
|
|
26
|
+
quote,
|
|
27
|
+
quoteAuthor,
|
|
28
|
+
title = "Welcome back",
|
|
29
|
+
subtitle,
|
|
30
|
+
submitLabel = "Sign in",
|
|
31
|
+
forgotHref = "#",
|
|
32
|
+
forgotLabel = "Forgot password?",
|
|
33
|
+
rememberLabel = "Remember me",
|
|
34
|
+
onSubmit,
|
|
35
|
+
className,
|
|
36
|
+
...rest
|
|
37
|
+
},
|
|
38
|
+
ref,
|
|
39
|
+
) {
|
|
40
|
+
const id = useId().replace(/:/g, "");
|
|
41
|
+
const submit = (e: FormEvent<HTMLFormElement>) => {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
const data = new FormData(e.currentTarget);
|
|
44
|
+
onSubmit?.({
|
|
45
|
+
email: String(data.get("email") ?? ""),
|
|
46
|
+
password: String(data.get("password") ?? ""),
|
|
47
|
+
remember: data.get("remember") != null,
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
return (
|
|
51
|
+
<div ref={ref} className={cx("oc-block-authsplit", className)} {...rest}>
|
|
52
|
+
<aside className="oc-block-authsplit-brand">
|
|
53
|
+
{logo != null && <a className="oc-block-authsplit-logo" href={logoHref}>{logo}</a>}
|
|
54
|
+
{quote != null && (
|
|
55
|
+
<blockquote className="oc-block-authsplit-quote">
|
|
56
|
+
<p>{quote}</p>
|
|
57
|
+
{quoteAuthor != null && <footer>{quoteAuthor}</footer>}
|
|
58
|
+
</blockquote>
|
|
59
|
+
)}
|
|
60
|
+
</aside>
|
|
61
|
+
<div className="oc-block-authsplit-form">
|
|
62
|
+
<form className="oc-block-authsplit-card" onSubmit={submit}>
|
|
63
|
+
<h1 className="oc-block-authsplit-title">{title}</h1>
|
|
64
|
+
{subtitle != null && <p className="oc-block-authsplit-subtitle">{subtitle}</p>}
|
|
65
|
+
<div className="oc-block-authsplit-fields">
|
|
66
|
+
<div className="oc-field">
|
|
67
|
+
<label className="oc-field-label" htmlFor={`${id}-email`}>E-mail</label>
|
|
68
|
+
<input className="oc-input" id={`${id}-email`} name="email" type="email" autoComplete="username" placeholder="name@company.com" required />
|
|
69
|
+
</div>
|
|
70
|
+
<div className="oc-field">
|
|
71
|
+
<label className="oc-field-label" htmlFor={`${id}-password`}>Password</label>
|
|
72
|
+
<PasswordInput id={`${id}-password`} name="password" required />
|
|
73
|
+
</div>
|
|
74
|
+
<div className="oc-block-authsplit-row">
|
|
75
|
+
<label><input className="oc-checkbox" type="checkbox" name="remember" /> {rememberLabel}</label>
|
|
76
|
+
<a className="oc-link" href={forgotHref}>{forgotLabel}</a>
|
|
77
|
+
</div>
|
|
78
|
+
<button className="oc-btn" type="submit" style={{ width: "100%" }}>{submitLabel}</button>
|
|
79
|
+
</div>
|
|
80
|
+
</form>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// File: packages/react/src/blocks/BlockContactGrid.tsx | Map: types -> BlockContactGrid (head -> grid of cards (mark, title, lines))
|
|
2
|
+
import { forwardRef, useId } from "react";
|
|
3
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
4
|
+
import { cx } from "../utils";
|
|
5
|
+
|
|
6
|
+
export interface ContactGridLine {
|
|
7
|
+
text: ReactNode;
|
|
8
|
+
/** Makes the line a link (mailto:, tel:, https:). */
|
|
9
|
+
href?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ContactGridCard {
|
|
13
|
+
title: ReactNode;
|
|
14
|
+
lines: ContactGridLine[];
|
|
15
|
+
/** Glyph in a soft product mark. */
|
|
16
|
+
icon?: ReactNode;
|
|
17
|
+
/** Accent preset of the mark (green, violet, ...). */
|
|
18
|
+
accent?: string;
|
|
19
|
+
/** Renders the card as <address>. */
|
|
20
|
+
address?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface BlockContactGridProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
24
|
+
eyebrow?: ReactNode;
|
|
25
|
+
title?: ReactNode;
|
|
26
|
+
lead?: ReactNode;
|
|
27
|
+
cards: ContactGridCard[];
|
|
28
|
+
/** No card frames. */
|
|
29
|
+
plain?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `.oc-block-contact`: contact info cards (e-mail, phone, address, hours). */
|
|
33
|
+
export const BlockContactGrid = forwardRef<HTMLElement, BlockContactGridProps>(function BlockContactGrid(
|
|
34
|
+
{ eyebrow, title, lead, cards, plain = false, className, ...rest },
|
|
35
|
+
ref,
|
|
36
|
+
) {
|
|
37
|
+
const titleId = `oc-contact-title-${useId().replace(/[^\w-]/g, "")}`;
|
|
38
|
+
return (
|
|
39
|
+
<section ref={ref} className={cx("oc-block-contact", plain && "oc-block-contact--plain", className)} aria-labelledby={title != null ? titleId : undefined} {...rest}>
|
|
40
|
+
{(eyebrow != null || title != null || lead != null) && (
|
|
41
|
+
<div className="oc-block-contact-head">
|
|
42
|
+
{eyebrow != null && <p className="oc-eyebrow">{eyebrow}</p>}
|
|
43
|
+
{title != null && <h2 className="oc-block-contact-heading" id={titleId}>{title}</h2>}
|
|
44
|
+
{lead != null && <p className="oc-block-contact-lead">{lead}</p>}
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
<div className="oc-block-contact-grid">
|
|
48
|
+
{cards.map((card, i) => {
|
|
49
|
+
const Card = card.address ? "address" : "article";
|
|
50
|
+
const Title = card.address ? "span" : "h3";
|
|
51
|
+
const Line = card.address ? "span" : "p";
|
|
52
|
+
return (
|
|
53
|
+
<Card className="oc-block-contact-card" key={i}>
|
|
54
|
+
{card.icon != null && (
|
|
55
|
+
<span className={cx("oc-product-mark oc-product-mark--sm oc-product-mark--soft", card.accent && `oc-accent--${card.accent}`)} aria-hidden="true">{card.icon}</span>
|
|
56
|
+
)}
|
|
57
|
+
<Title className="oc-block-contact-title">{card.title}</Title>
|
|
58
|
+
{card.lines.map((line, j) =>
|
|
59
|
+
line.href ? (
|
|
60
|
+
<a className="oc-block-contact-line" href={line.href} key={j}>{line.text}</a>
|
|
61
|
+
) : (
|
|
62
|
+
<Line className="oc-block-contact-line" key={j}>{line.text}</Line>
|
|
63
|
+
),
|
|
64
|
+
)}
|
|
65
|
+
</Card>
|
|
66
|
+
);
|
|
67
|
+
})}
|
|
68
|
+
</div>
|
|
69
|
+
</section>
|
|
70
|
+
);
|
|
71
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { cx } from "../utils";
|
|
4
|
+
|
|
5
|
+
export interface BlockCtaProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
6
|
+
title: ReactNode;
|
|
7
|
+
description?: ReactNode;
|
|
8
|
+
actions?: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** `.oc-block-cta`: call to action banner. */
|
|
12
|
+
export const BlockCta = forwardRef<HTMLDivElement, BlockCtaProps>(function BlockCta(
|
|
13
|
+
{ title, description, actions, className, ...rest },
|
|
14
|
+
ref,
|
|
15
|
+
) {
|
|
16
|
+
return (
|
|
17
|
+
<div ref={ref} className={cx("oc-block-cta", className)} {...rest}>
|
|
18
|
+
<h2>{title}</h2>
|
|
19
|
+
{description != null && <p>{description}</p>}
|
|
20
|
+
{actions != null && <div className="oc-block-hero-actions">{actions}</div>}
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import type { AnchorHTMLAttributes, HTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { cx } from "../utils";
|
|
4
|
+
|
|
5
|
+
export interface BlockDataTableProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
6
|
+
title?: ReactNode;
|
|
7
|
+
description?: ReactNode;
|
|
8
|
+
/** Header buttons (export, new...). */
|
|
9
|
+
actions?: ReactNode;
|
|
10
|
+
/** Search + filters row (`oc-table-toolbar`). Use `BlockDataTableSpacer` to push items right. */
|
|
11
|
+
toolbar?: ReactNode;
|
|
12
|
+
/** Selection bar, shown by you when rows are selected (`oc-block-datatable-bulk`). */
|
|
13
|
+
bulk?: ReactNode;
|
|
14
|
+
/** Entries text, page-size select and pagination (`oc-table-footer`). */
|
|
15
|
+
footer?: ReactNode;
|
|
16
|
+
/** The `<Table>`. */
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** `.oc-card.oc-block-datatable`: card with header, toolbar, table, bulk bar and footer. */
|
|
21
|
+
export const BlockDataTable = forwardRef<HTMLElement, BlockDataTableProps>(function BlockDataTable(
|
|
22
|
+
{ title, description, actions, toolbar, bulk, footer, className, children, ...rest },
|
|
23
|
+
ref,
|
|
24
|
+
) {
|
|
25
|
+
return (
|
|
26
|
+
<section ref={ref} className={cx("oc-card oc-block-datatable", className)} {...rest}>
|
|
27
|
+
<header className="oc-block-datatable-head">
|
|
28
|
+
<div>
|
|
29
|
+
{title != null && <h2 className="oc-block-datatable-title">{title}</h2>}
|
|
30
|
+
{description != null && <p className="oc-block-datatable-description">{description}</p>}
|
|
31
|
+
</div>
|
|
32
|
+
{actions != null && <div>{actions}</div>}
|
|
33
|
+
</header>
|
|
34
|
+
<div className="oc-block-datatable-body">
|
|
35
|
+
{toolbar != null && <div className="oc-table-toolbar">{toolbar}</div>}
|
|
36
|
+
{bulk != null && <div className="oc-block-datatable-bulk">{bulk}</div>}
|
|
37
|
+
{children}
|
|
38
|
+
{footer != null && <div className="oc-table-footer">{footer}</div>}
|
|
39
|
+
</div>
|
|
40
|
+
</section>
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/** `.oc-table-toolbar-spacer`: pushes the following toolbar items to the right. */
|
|
45
|
+
export function BlockDataTableSpacer() {
|
|
46
|
+
return <span className="oc-table-toolbar-spacer" />;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `.oc-filter-pills` group. */
|
|
50
|
+
export function FilterPills({ className, ...rest }: HTMLAttributes<HTMLDivElement>) {
|
|
51
|
+
return <div className={cx("oc-filter-pills", className)} role="group" {...rest} />;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** `a.oc-filter-pill` (set `current` for the active filter). */
|
|
55
|
+
export function FilterPill({ current, className, ...rest }: AnchorHTMLAttributes<HTMLAnchorElement> & { current?: boolean }) {
|
|
56
|
+
return <a className={cx("oc-filter-pill", className)} aria-current={current ? "true" : undefined} {...rest} />;
|
|
57
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// File: packages/react/src/blocks/BlockFaq.tsx | Map: types -> BlockFaq (head -> accordion on details name -> more line)
|
|
2
|
+
import { forwardRef, useId } from "react";
|
|
3
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
4
|
+
import { cx } from "../utils";
|
|
5
|
+
|
|
6
|
+
export interface FaqItem {
|
|
7
|
+
question: ReactNode;
|
|
8
|
+
answer: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface BlockFaqProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
12
|
+
eyebrow?: ReactNode;
|
|
13
|
+
title: ReactNode;
|
|
14
|
+
lead?: ReactNode;
|
|
15
|
+
items: FaqItem[];
|
|
16
|
+
/** Index of the item that starts open (default 0, -1 = none). */
|
|
17
|
+
openIndex?: number;
|
|
18
|
+
/** Closing line, e.g. "Still have questions?" + a link. */
|
|
19
|
+
more?: ReactNode;
|
|
20
|
+
/** Head left, questions right. */
|
|
21
|
+
split?: boolean;
|
|
22
|
+
/** Name of the details group (one open at a time). Generated when empty. */
|
|
23
|
+
name?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `.oc-block-faq`: title + exclusive accordion on native <details name>. */
|
|
27
|
+
export const BlockFaq = forwardRef<HTMLElement, BlockFaqProps>(function BlockFaq(
|
|
28
|
+
{ eyebrow, title, lead, items, openIndex = 0, more, split = false, name, className, ...rest },
|
|
29
|
+
ref,
|
|
30
|
+
) {
|
|
31
|
+
const auto = useId().replace(/[^\w-]/g, "");
|
|
32
|
+
const group = name ?? `oc-faq-${auto}`;
|
|
33
|
+
const titleId = `oc-faq-title-${auto}`;
|
|
34
|
+
return (
|
|
35
|
+
<section ref={ref} className={cx("oc-block-faq", split && "oc-block-faq--split", className)} aria-labelledby={titleId} {...rest}>
|
|
36
|
+
<div className="oc-block-faq-inner">
|
|
37
|
+
<div className="oc-block-faq-head">
|
|
38
|
+
{eyebrow != null && <p className="oc-eyebrow">{eyebrow}</p>}
|
|
39
|
+
<h2 className="oc-block-faq-title" id={titleId}>{title}</h2>
|
|
40
|
+
{lead != null && <p className="oc-block-faq-lead">{lead}</p>}
|
|
41
|
+
</div>
|
|
42
|
+
<div className="oc-accordion oc-block-faq-list">
|
|
43
|
+
{items.map((item, i) => (
|
|
44
|
+
// "name" groups the items; React 19 passes it through, older versions too (unknown attribute on details).
|
|
45
|
+
<details key={i} className="oc-accordion-item" {...{ name: group }} open={i === openIndex || undefined}>
|
|
46
|
+
<summary className="oc-accordion-trigger">{item.question}</summary>
|
|
47
|
+
<div className="oc-accordion-content">{item.answer}</div>
|
|
48
|
+
</details>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
{more != null && <p className="oc-block-faq-more">{more}</p>}
|
|
52
|
+
</div>
|
|
53
|
+
</section>
|
|
54
|
+
);
|
|
55
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { cx } from "../utils";
|
|
4
|
+
|
|
5
|
+
export interface Feature {
|
|
6
|
+
/** Glyph, emoji or inline SVG. */
|
|
7
|
+
icon?: ReactNode;
|
|
8
|
+
title: ReactNode;
|
|
9
|
+
description?: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface BlockFeaturesProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
13
|
+
title?: ReactNode;
|
|
14
|
+
description?: ReactNode;
|
|
15
|
+
features: Feature[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** `.oc-block-features`: heading plus a grid of feature cards. */
|
|
19
|
+
export const BlockFeatures = forwardRef<HTMLDivElement, BlockFeaturesProps>(function BlockFeatures(
|
|
20
|
+
{ title, description, features, className, ...rest },
|
|
21
|
+
ref,
|
|
22
|
+
) {
|
|
23
|
+
return (
|
|
24
|
+
<div ref={ref} className={cx("oc-block-features", className)} {...rest}>
|
|
25
|
+
{title != null && <h2>{title}</h2>}
|
|
26
|
+
{description != null && <p>{description}</p>}
|
|
27
|
+
<div className="oc-block-features-grid">
|
|
28
|
+
{features.map((f, i) => (
|
|
29
|
+
<div className="oc-block-feature-card" key={i}>
|
|
30
|
+
{f.icon != null && <div className="oc-block-feature-icon">{f.icon}</div>}
|
|
31
|
+
<h3>{f.title}</h3>
|
|
32
|
+
{f.description != null && <p>{f.description}</p>}
|
|
33
|
+
</div>
|
|
34
|
+
))}
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { cx } from "../utils";
|
|
4
|
+
|
|
5
|
+
export interface FooterLink {
|
|
6
|
+
label: ReactNode;
|
|
7
|
+
href: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface FooterColumn {
|
|
11
|
+
title: ReactNode;
|
|
12
|
+
links: FooterLink[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface BlockFooterProps extends HTMLAttributes<HTMLDivElement> {
|
|
16
|
+
columns: FooterColumn[];
|
|
17
|
+
/** Left text of the bottom bar. */
|
|
18
|
+
copyright?: ReactNode;
|
|
19
|
+
/** Right text of the bottom bar. */
|
|
20
|
+
credit?: ReactNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** `.oc-block-footer`: multi-column site footer. */
|
|
24
|
+
export const BlockFooter = forwardRef<HTMLDivElement, BlockFooterProps>(function BlockFooter(
|
|
25
|
+
{ columns, copyright, credit, className, ...rest },
|
|
26
|
+
ref,
|
|
27
|
+
) {
|
|
28
|
+
return (
|
|
29
|
+
<div ref={ref} className={cx("oc-block-footer", className)} {...rest}>
|
|
30
|
+
<div className="oc-block-footer-grid">
|
|
31
|
+
{columns.map((col, i) => (
|
|
32
|
+
<div className="oc-block-footer-col" key={i}>
|
|
33
|
+
<h4>{col.title}</h4>
|
|
34
|
+
<ul>
|
|
35
|
+
{col.links.map((l, j) => (
|
|
36
|
+
<li key={j}><a href={l.href}>{l.label}</a></li>
|
|
37
|
+
))}
|
|
38
|
+
</ul>
|
|
39
|
+
</div>
|
|
40
|
+
))}
|
|
41
|
+
</div>
|
|
42
|
+
{(copyright != null || credit != null) && (
|
|
43
|
+
<div className="oc-block-footer-bottom">
|
|
44
|
+
<span>{copyright}</span>
|
|
45
|
+
<span>{credit}</span>
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
});
|