@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.
Files changed (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +20 -2
  3. package/dist/index.d.ts +2640 -0
  4. package/dist/index.js +4813 -0
  5. package/manifest.json +838 -0
  6. package/package.json +69 -4
  7. package/src/blocks/BlockAboutTimeline.tsx +46 -0
  8. package/src/blocks/BlockAppHeader.tsx +52 -0
  9. package/src/blocks/BlockAppShell.tsx +56 -0
  10. package/src/blocks/BlockAuth.tsx +53 -0
  11. package/src/blocks/BlockAuthSplit.tsx +84 -0
  12. package/src/blocks/BlockContactGrid.tsx +71 -0
  13. package/src/blocks/BlockCta.tsx +23 -0
  14. package/src/blocks/BlockDataTable.tsx +57 -0
  15. package/src/blocks/BlockFaq.tsx +55 -0
  16. package/src/blocks/BlockFeatures.tsx +38 -0
  17. package/src/blocks/BlockFooter.tsx +50 -0
  18. package/src/blocks/BlockFormPage.tsx +26 -0
  19. package/src/blocks/BlockHero.tsx +27 -0
  20. package/src/blocks/BlockHeroCode.tsx +63 -0
  21. package/src/blocks/BlockHeroOrbit.tsx +73 -0
  22. package/src/blocks/BlockHeroSplit.tsx +28 -0
  23. package/src/blocks/BlockModuleHeader.tsx +34 -0
  24. package/src/blocks/BlockPageHero.tsx +25 -0
  25. package/src/blocks/BlockPortfolioGrid.tsx +50 -0
  26. package/src/blocks/BlockPricing.tsx +62 -0
  27. package/src/blocks/BlockProcess.tsx +50 -0
  28. package/src/blocks/BlockProductGrid.tsx +54 -0
  29. package/src/blocks/BlockProfileHeader.tsx +57 -0
  30. package/src/blocks/BlockShowcase.tsx +58 -0
  31. package/src/blocks/BlockSiteFooter.tsx +60 -0
  32. package/src/blocks/BlockSiteHeader.tsx +42 -0
  33. package/src/blocks/BlockSpotlight.tsx +58 -0
  34. package/src/blocks/BlockStats.tsx +26 -0
  35. package/src/blocks/BlockStatsCounter.tsx +31 -0
  36. package/src/blocks/BlockTeamList.tsx +48 -0
  37. package/src/blocks/BlockTwoFactor.tsx +57 -0
  38. package/src/blocks/BlockWizardDialog.tsx +101 -0
  39. package/src/components/Accordion.tsx +31 -0
  40. package/src/components/AccordionAccent.tsx +39 -0
  41. package/src/components/AffixInput.tsx +47 -0
  42. package/src/components/Alert.tsx +24 -0
  43. package/src/components/AlertDialog.tsx +60 -0
  44. package/src/components/AlphaTabs.tsx +63 -0
  45. package/src/components/Attachment.tsx +140 -0
  46. package/src/components/Avatar.tsx +32 -0
  47. package/src/components/BackLink.tsx +11 -0
  48. package/src/components/Badge.tsx +14 -0
  49. package/src/components/BarcodeInput.tsx +64 -0
  50. package/src/components/Breadcrumb.tsx +32 -0
  51. package/src/components/BrowserFrame.tsx +48 -0
  52. package/src/components/BusyOverlay.tsx +52 -0
  53. package/src/components/Button.tsx +49 -0
  54. package/src/components/Card.tsx +27 -0
  55. package/src/components/ChartLine.tsx +119 -0
  56. package/src/components/ChatComposer.tsx +73 -0
  57. package/src/components/Checkbox.tsx +14 -0
  58. package/src/components/Checklist.tsx +45 -0
  59. package/src/components/ChipFilter.tsx +60 -0
  60. package/src/components/Collapsible.tsx +43 -0
  61. package/src/components/ColorField.tsx +48 -0
  62. package/src/components/ColorSelect.tsx +49 -0
  63. package/src/components/ColumnMenu.tsx +66 -0
  64. package/src/components/Command.tsx +113 -0
  65. package/src/components/ConfirmDialog.tsx +95 -0
  66. package/src/components/CookieConsent.tsx +137 -0
  67. package/src/components/CopyField.tsx +44 -0
  68. package/src/components/Counter.tsx +21 -0
  69. package/src/components/DataTable.tsx +201 -0
  70. package/src/components/DecimalInput.tsx +87 -0
  71. package/src/components/DefinitionList.tsx +38 -0
  72. package/src/components/DocViewer.tsx +110 -0
  73. package/src/components/Dropdown.tsx +52 -0
  74. package/src/components/Dropzone.tsx +46 -0
  75. package/src/components/EmojiPicker.tsx +72 -0
  76. package/src/components/EmptyState.tsx +27 -0
  77. package/src/components/EntityCard.tsx +56 -0
  78. package/src/components/FactList.tsx +43 -0
  79. package/src/components/FilterCounts.tsx +50 -0
  80. package/src/components/FlipCard.tsx +55 -0
  81. package/src/components/FormField.tsx +54 -0
  82. package/src/components/Greeting.tsx +63 -0
  83. package/src/components/Heading.tsx +27 -0
  84. package/src/components/Icon.tsx +25 -0
  85. package/src/components/ImageZoom.tsx +51 -0
  86. package/src/components/Input.tsx +21 -0
  87. package/src/components/InputGroup.tsx +24 -0
  88. package/src/components/LanguageSelect.tsx +64 -0
  89. package/src/components/LauncherCard.tsx +50 -0
  90. package/src/components/Lightbox.tsx +83 -0
  91. package/src/components/LinkArrow.tsx +27 -0
  92. package/src/components/ListToolbar.tsx +67 -0
  93. package/src/components/Marquee.tsx +38 -0
  94. package/src/components/MediaCard.tsx +47 -0
  95. package/src/components/MediaRow.tsx +53 -0
  96. package/src/components/MediaThumb.tsx +49 -0
  97. package/src/components/Message.tsx +171 -0
  98. package/src/components/MessageThread.tsx +177 -0
  99. package/src/components/Modal.tsx +81 -0
  100. package/src/components/Navmenu.tsx +62 -0
  101. package/src/components/NextTeaser.tsx +29 -0
  102. package/src/components/NumberInput.tsx +64 -0
  103. package/src/components/OtpInput.tsx +48 -0
  104. package/src/components/PageHeader.tsx +43 -0
  105. package/src/components/Pagination.tsx +42 -0
  106. package/src/components/PasswordInput.tsx +36 -0
  107. package/src/components/PasswordRequirements.tsx +46 -0
  108. package/src/components/PhoneInput.tsx +93 -0
  109. package/src/components/Popover.tsx +34 -0
  110. package/src/components/PriceCard.tsx +83 -0
  111. package/src/components/ProductMark.tsx +35 -0
  112. package/src/components/Progress.tsx +32 -0
  113. package/src/components/ProjectCard.tsx +46 -0
  114. package/src/components/Prose.tsx +79 -0
  115. package/src/components/RecoveryCodes.tsx +65 -0
  116. package/src/components/Segmented.tsx +47 -0
  117. package/src/components/Select.tsx +27 -0
  118. package/src/components/SeverityCard.tsx +53 -0
  119. package/src/components/Sheet.tsx +62 -0
  120. package/src/components/Sidebar.tsx +145 -0
  121. package/src/components/Skeleton.tsx +12 -0
  122. package/src/components/SortableRow.tsx +46 -0
  123. package/src/components/StackCard.tsx +40 -0
  124. package/src/components/StackList.tsx +42 -0
  125. package/src/components/StatCard.tsx +49 -0
  126. package/src/components/Stepper.tsx +54 -0
  127. package/src/components/Switch.tsx +18 -0
  128. package/src/components/Table.tsx +75 -0
  129. package/src/components/TableActions.tsx +54 -0
  130. package/src/components/TableCompact.tsx +61 -0
  131. package/src/components/Tabs.tsx +45 -0
  132. package/src/components/TagInput.tsx +73 -0
  133. package/src/components/TerminalLog.tsx +63 -0
  134. package/src/components/Textarea.tsx +19 -0
  135. package/src/components/ThemeSwitch.tsx +45 -0
  136. package/src/components/TimeInput.tsx +76 -0
  137. package/src/components/Timeline.tsx +45 -0
  138. package/src/components/Toast.tsx +138 -0
  139. package/src/components/ToggleField.tsx +39 -0
  140. package/src/components/Tooltip.tsx +14 -0
  141. package/src/components/TopProgress.tsx +54 -0
  142. package/src/components/Tree.tsx +97 -0
  143. package/src/components/UpgradeBanner.tsx +62 -0
  144. package/src/components/User.tsx +31 -0
  145. package/src/components/VersionBanner.tsx +41 -0
  146. package/src/hooks.tsx +141 -0
  147. package/src/index.ts +142 -0
  148. package/src/utils.ts +25 -0
@@ -0,0 +1,110 @@
1
+ // File: packages/react/src/components/DocViewer.tsx
2
+ // Map: types + demo items -> media (iframe / video / img / placeholder art) -> DocViewer (grid of cards -> one <dialog> per card)
3
+ // Same markup as packages/ui/snippets/doc-viewer.html. No JavaScript: the buttons open the full-screen dialog with
4
+ // invoker commands (command="show-modal" commandfor), closedby="any" closes it on Esc or a click on the backdrop.
5
+ import { forwardRef, useId } from "react";
6
+ import type { HTMLAttributes, ReactNode } from "react";
7
+ import { attrs, cx } from "../utils";
8
+
9
+ export interface DocViewerItem {
10
+ /** Name in the bar and the dialog title; also used in the aria-labels. */
11
+ label: string;
12
+ /** Small text next to the label, e.g. "PDF · 2.4 MB" or "1:42". */
13
+ meta?: ReactNode;
14
+ /** File URL. Leave empty to show placeholder art (`.oc-media-art`). */
15
+ src?: string;
16
+ /** Default "pdf" (A4 frame). "video" = 16/9 dark frame, "image" = natural height. */
17
+ type?: "pdf" | "video" | "image";
18
+ /** Dialog id (generated when empty). */
19
+ id?: string;
20
+ /** Text for the inline preview (aria-label / alt / iframe title). Defaults to the label. */
21
+ alt?: string;
22
+ /** Placeholder art variant (`oc-media-art--N`) when there is no src. */
23
+ art?: number;
24
+ /** Download link in the dialog bar. */
25
+ download?: string;
26
+ /** Where the preview opens: "bar" (expand button) or "frame" (the whole preview). Default: "frame" for video. */
27
+ open?: "bar" | "frame";
28
+ /** Set false for a plain card without full-screen dialog. Default true. */
29
+ fullscreen?: boolean;
30
+ }
31
+
32
+ export interface DocViewerProps extends HTMLAttributes<HTMLDivElement> {
33
+ items?: DocViewerItem[];
34
+ /** Text of the frame button (default "Click for full screen"). */
35
+ openText?: ReactNode;
36
+ downloadText?: ReactNode;
37
+ /** Prefix for generated dialog ids. */
38
+ idPrefix?: string;
39
+ }
40
+
41
+ const DEMO: DocViewerItem[] = [
42
+ { label: "Brochure 2026", meta: "PDF · 2.4 MB", alt: "First page of Brochure 2026", art: 2, download: "#brochure.pdf" },
43
+ { label: "Site visit", meta: "1:42", type: "video", alt: "Video still: site visit", art: 5 },
44
+ ];
45
+
46
+ const media = (doc: DocViewerItem, alt: string) => {
47
+ const type = doc.type ?? "pdf";
48
+ if (!doc.src) return <div className={cx("oc-media-art", doc.art != null && `oc-media-art--${doc.art}`)} role="img" aria-label={alt} />;
49
+ if (type === "video") return <video src={doc.src} controls preload="metadata" aria-label={alt} />;
50
+ if (type === "image") return <img src={doc.src} alt={alt} />;
51
+ return <iframe src={`${doc.src}#toolbar=0&view=FitH`} title={alt} loading="lazy" />;
52
+ };
53
+
54
+ /** `.oc-doc-viewer-grid` of `figure.oc-doc-viewer` cards, each with its `dialog.oc-doc-viewer-dialog`. */
55
+ export const DocViewer = forwardRef<HTMLDivElement, DocViewerProps>(function DocViewer(
56
+ { items = DEMO, openText = "Click for full screen", downloadText = "Download", idPrefix, className, ...rest },
57
+ ref,
58
+ ) {
59
+ const auto = useId().replace(/[^\w-]/g, "");
60
+ const base = idPrefix ?? `oc-doc-viewer-${auto}`;
61
+ const idOf = (doc: DocViewerItem, i: number) => doc.id ?? `${base}-${i + 1}`;
62
+
63
+ const cards = items.map((doc, i) => {
64
+ const id = idOf(doc, i);
65
+ const full = doc.fullscreen !== false;
66
+ const frame = full && (doc.open ?? (doc.type === "video" ? "frame" : "bar")) === "frame";
67
+ return (
68
+ <figure key={i} className={cx("oc-doc-viewer", doc.type === "video" && "oc-doc-viewer--video", doc.type === "image" && "oc-doc-viewer--image")}>
69
+ <figcaption className="oc-doc-viewer__bar">
70
+ <span className="oc-doc-viewer__label">{doc.label}</span>
71
+ {doc.meta != null && <span className="oc-doc-viewer__meta">{doc.meta}</span>}
72
+ {full && !frame && (
73
+ <button type="button" className="oc-doc-viewer__expand" {...attrs({ command: "show-modal", commandfor: id })} aria-label={`Open ${doc.label} full screen`}>&#10530;</button>
74
+ )}
75
+ </figcaption>
76
+ <div className="oc-doc-viewer__frame">
77
+ {media(doc, doc.alt ?? doc.label)}
78
+ {frame && <button type="button" className="oc-doc-viewer__open" {...attrs({ command: "show-modal", commandfor: id })}>{openText}</button>}
79
+ </div>
80
+ </figure>
81
+ );
82
+ });
83
+
84
+ const dialogs = items.map((doc, i) => {
85
+ if (doc.fullscreen === false) return null;
86
+ const id = idOf(doc, i);
87
+ const close = <button type="button" className="oc-doc-viewer-dialog__close" {...attrs({ command: "close", commandfor: id })} aria-label="Close">&#10005;</button>;
88
+ return (
89
+ <dialog key={i} className={cx("oc-doc-viewer-dialog", doc.type === "video" && "oc-doc-viewer-dialog--video")} id={id} {...attrs({ closedby: "any" })} aria-labelledby={`${id}-title`}>
90
+ <div className="oc-doc-viewer-dialog__bar">
91
+ <h2 className="oc-doc-viewer-dialog__title" id={`${id}-title`}>{doc.label}</h2>
92
+ {doc.download ? (
93
+ <div className="oc-doc-viewer-dialog__actions">
94
+ <a className="oc-btn oc-btn--outline oc-btn--sm" href={doc.download} download>{downloadText}</a>
95
+ {close}
96
+ </div>
97
+ ) : close}
98
+ </div>
99
+ <div className="oc-doc-viewer-dialog__body">{media(doc, doc.type === "video" ? `${doc.label} video` : doc.label)}</div>
100
+ </dialog>
101
+ );
102
+ });
103
+
104
+ return (
105
+ <>
106
+ <div ref={ref} className={cx("oc-doc-viewer-grid", className)} {...rest}>{cards}</div>
107
+ {dialogs}
108
+ </>
109
+ );
110
+ });
@@ -0,0 +1,52 @@
1
+ import { forwardRef, useId } from "react";
2
+ import type { AnchorHTMLAttributes, ButtonHTMLAttributes, HTMLAttributes, ReactNode } from "react";
3
+ import { attrs, cx } from "../utils";
4
+ import { Button } from "./Button";
5
+ import type { ButtonProps } from "./Button";
6
+
7
+ export interface DropdownProps extends HTMLAttributes<HTMLDivElement> {
8
+ /** Content of the trigger button. */
9
+ trigger: ReactNode;
10
+ /** Props for the trigger button (default variant `outline`). */
11
+ triggerProps?: ButtonProps;
12
+ }
13
+
14
+ /**
15
+ * Trigger button + `<div class="oc-dropdown-menu" popover>`, wired with `popovertarget`
16
+ * (native popover and anchor positioning, no state needed).
17
+ */
18
+ export const Dropdown = forwardRef<HTMLDivElement, DropdownProps>(function Dropdown(
19
+ { trigger, triggerProps, className, id, children, ...rest },
20
+ ref,
21
+ ) {
22
+ const auto = useId();
23
+ const menuId = id ?? `oc-menu-${auto.replace(/:/g, "")}`;
24
+ return (
25
+ <>
26
+ <Button variant="outline" {...triggerProps} {...attrs({ popovertarget: menuId })}>{trigger}</Button>
27
+ <div ref={ref} className={cx("oc-dropdown-menu", className)} id={menuId} {...attrs({ popover: "" })} {...rest}>
28
+ {children}
29
+ </div>
30
+ </>
31
+ );
32
+ });
33
+
34
+ /** `.oc-dropdown-label` */
35
+ export function DropdownLabel({ className, ...rest }: HTMLAttributes<HTMLDivElement>) {
36
+ return <div className={cx("oc-dropdown-label", className)} {...rest} />;
37
+ }
38
+
39
+ /** `.oc-dropdown-separator` */
40
+ export function DropdownSeparator({ className, ...rest }: HTMLAttributes<HTMLDivElement>) {
41
+ return <div className={cx("oc-dropdown-separator", className)} {...rest} />;
42
+ }
43
+
44
+ type ItemProps = (AnchorHTMLAttributes<HTMLAnchorElement> & { href: string }) | (ButtonHTMLAttributes<HTMLButtonElement> & { href?: undefined });
45
+
46
+ /** `.oc-dropdown-item`: an anchor when `href` is set, otherwise a button. */
47
+ export function DropdownItem({ className, ...props }: ItemProps) {
48
+ if (props.href !== undefined) {
49
+ return <a className={cx("oc-dropdown-item", className)} {...(props as AnchorHTMLAttributes<HTMLAnchorElement>)} />;
50
+ }
51
+ return <button type="button" className={cx("oc-dropdown-item", className)} {...(props as ButtonHTMLAttributes<HTMLButtonElement>)} />;
52
+ }
@@ -0,0 +1,46 @@
1
+ "use client";
2
+
3
+ import { forwardRef, useState } from "react";
4
+ import type { DragEvent, InputHTMLAttributes, ReactNode } from "react";
5
+ import { cx } from "../utils";
6
+
7
+ export interface DropzoneProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "onChange" | "children"> {
8
+ /** Icon in `oc-dropzone-icon` (inline SVG or glyph). */
9
+ icon?: ReactNode;
10
+ /** Main text, e.g. `<strong>Drop invoices here</strong> or click to choose`. */
11
+ children?: ReactNode;
12
+ /** Small hint (`oc-dropzone-hint`), e.g. accepted types and size. */
13
+ hint?: ReactNode;
14
+ /** Called with the chosen or dropped files. */
15
+ onFiles?: (files: File[]) => void;
16
+ }
17
+
18
+ /** `<label class="oc-dropzone">` around a real file input; `is-dragover` while dragging. */
19
+ export const Dropzone = forwardRef<HTMLInputElement, DropzoneProps>(function Dropzone(
20
+ { icon, children, hint, onFiles, className, ...rest },
21
+ ref,
22
+ ) {
23
+ const [over, setOver] = useState(false);
24
+ const stop = (e: DragEvent) => {
25
+ e.preventDefault();
26
+ setOver(e.type === "dragover" || e.type === "dragenter");
27
+ };
28
+ return (
29
+ <label
30
+ className={cx("oc-dropzone", over && "is-dragover", className)}
31
+ onDragEnter={stop}
32
+ onDragOver={stop}
33
+ onDragLeave={stop}
34
+ onDrop={(e) => {
35
+ stop(e);
36
+ const files = Array.from(e.dataTransfer?.files ?? []);
37
+ if (files.length && !rest.disabled) onFiles?.(files);
38
+ }}
39
+ >
40
+ <input ref={ref} type="file" onChange={(e) => onFiles?.(Array.from(e.target.files ?? []))} {...rest} />
41
+ {icon != null && <span className="oc-dropzone-icon" aria-hidden="true">{icon}</span>}
42
+ <span>{children}</span>
43
+ {hint != null && <span className="oc-dropzone-hint">{hint}</span>}
44
+ </label>
45
+ );
46
+ });
@@ -0,0 +1,72 @@
1
+ // File: packages/react/src/components/EmojiPicker.tsx
2
+ // Map: default groups -> props -> EmojiPicker (trigger button -> popover panel -> groups: label + grid of buttons)
3
+ // Same markup as packages/ui/snippets/emoji-picker.html. React calls onSelect and closes the popover.
4
+ "use client";
5
+
6
+ import { forwardRef, useId } from "react";
7
+ import type { HTMLAttributes, MouseEvent, ReactNode } from "react";
8
+ import { attrs, cx } from "../utils";
9
+
10
+ export interface EmojiGroup {
11
+ label?: string;
12
+ emojis: string[];
13
+ }
14
+
15
+ const defaultGroups: EmojiGroup[] = [
16
+ { label: "Smileys", emojis: ["😀", "😃", "😄", "😁", "😂", "🙂", "😉", "😍", "🤔", "😎", "😢", "😴", "😯", "🥳", "😤", "🤗"] },
17
+ { label: "Gestures", emojis: ["👍", "👎", "👏", "🙌", "👋", "🙏", "✌️", "💪"] },
18
+ { label: "Objects", emojis: ["❤️", "⭐", "🔥", "🎉", "✅", "🚀", "💡", "☕"] },
19
+ ];
20
+
21
+ export interface EmojiPickerProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
22
+ groups?: EmojiGroup[];
23
+ /** Called with the picked emoji. */
24
+ onSelect?: (emoji: string) => void;
25
+ /** Always-visible panel without trigger / popover. */
26
+ inline?: boolean;
27
+ /** Trigger content (default 🙂) and its accessible name. */
28
+ trigger?: ReactNode;
29
+ triggerLabel?: string;
30
+ }
31
+
32
+ /** `.oc-emoji-picker`: popover grid of emoji buttons, anchored to its trigger. */
33
+ export const EmojiPicker = forwardRef<HTMLDivElement, EmojiPickerProps>(function EmojiPicker(
34
+ { groups = defaultGroups, onSelect, inline, trigger = "🙂", triggerLabel = "Insert emoji", className, id, "aria-label": label = "Emoji", ...rest },
35
+ ref,
36
+ ) {
37
+ const auto = useId();
38
+ const panelId = id ?? `oc-emoji-${auto.replace(/:/g, "")}`;
39
+ const pick = (e: MouseEvent<HTMLButtonElement>, emoji: string) => {
40
+ onSelect?.(emoji);
41
+ (e.currentTarget.closest("[popover]") as (HTMLElement & { hidePopover?: () => void }) | null)?.hidePopover?.();
42
+ };
43
+ const panel = (
44
+ <div
45
+ ref={ref}
46
+ id={panelId}
47
+ className={cx("oc-emoji-picker", inline && "oc-emoji-picker--inline", className)}
48
+ role={inline ? "group" : undefined}
49
+ aria-label={label}
50
+ {...(inline ? {} : attrs({ popover: "" }))}
51
+ {...rest}
52
+ >
53
+ {groups.map((g, gi) => (
54
+ <div key={gi}>
55
+ {g.label && <p className="oc-emoji-picker__label">{g.label}</p>}
56
+ <div className="oc-emoji-picker__grid">
57
+ {g.emojis.map((emoji) => (
58
+ <button key={emoji} type="button" className="oc-emoji-picker__item" onClick={(e) => pick(e, emoji)}>{emoji}</button>
59
+ ))}
60
+ </div>
61
+ </div>
62
+ ))}
63
+ </div>
64
+ );
65
+ if (inline) return panel;
66
+ return (
67
+ <>
68
+ <button type="button" className="oc-emoji-picker__trigger" aria-label={triggerLabel} {...attrs({ popovertarget: panelId })}>{trigger}</button>
69
+ {panel}
70
+ </>
71
+ );
72
+ });
@@ -0,0 +1,27 @@
1
+ import { forwardRef } from "react";
2
+ import type { HTMLAttributes, ReactNode } from "react";
3
+ import { cx } from "../utils";
4
+
5
+ export interface EmptyStateProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
6
+ icon?: ReactNode;
7
+ title: ReactNode;
8
+ /** Explanation (`oc-empty-text`). */
9
+ description?: ReactNode;
10
+ /** Buttons row (`oc-empty-actions`). */
11
+ actions?: ReactNode;
12
+ }
13
+
14
+ /** `<div class="oc-empty">` for lists without data. */
15
+ export const EmptyState = forwardRef<HTMLDivElement, EmptyStateProps>(function EmptyState(
16
+ { icon, title, description, actions, className, ...rest },
17
+ ref,
18
+ ) {
19
+ return (
20
+ <div ref={ref} className={cx("oc-empty", className)} {...rest}>
21
+ {icon != null && <span className="oc-empty-icon" aria-hidden="true">{icon}</span>}
22
+ <h3 className="oc-empty-title">{title}</h3>
23
+ {description != null && <p className="oc-empty-text">{description}</p>}
24
+ {actions != null && <div className="oc-empty-actions">{actions}</div>}
25
+ </div>
26
+ );
27
+ });
@@ -0,0 +1,56 @@
1
+ // File: packages/react/src/components/EntityCard.tsx
2
+ // Map: props -> EntityCard (grid: frame (img / glyph / media) | body (eyebrow, title, description, facts, children) -> footer)
3
+ // Same markup as packages/ui/snippets/entity-card.html.
4
+ import { Fragment, forwardRef } from "react";
5
+ import type { HTMLAttributes, ReactNode } from "react";
6
+ import { cx } from "../utils";
7
+
8
+ export interface EntityCardProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
9
+ title: ReactNode;
10
+ eyebrow?: ReactNode;
11
+ description?: ReactNode;
12
+ /** Picture in the frame. */
13
+ src?: string;
14
+ alt?: string;
15
+ /** Large character or icon in the frame (when there is no `src`). */
16
+ glyph?: ReactNode;
17
+ /** Anything else for the frame (svg, placeholder art). Wins over src / glyph. */
18
+ media?: ReactNode;
19
+ /** Keep a margin around a logo / symbol picture (`__frame--contain`). */
20
+ contain?: boolean;
21
+ /** Key / value facts, rendered as a <dl>. */
22
+ facts?: Array<[ReactNode, ReactNode]>;
23
+ /** Tags or actions under a divider. */
24
+ footer?: ReactNode;
25
+ stacked?: boolean;
26
+ size?: "sm" | "md";
27
+ }
28
+
29
+ /** `article.oc-entity-card`: framed media cell + detail column. */
30
+ export const EntityCard = forwardRef<HTMLElement, EntityCardProps>(function EntityCard(
31
+ { title, eyebrow, description, src, alt = "", glyph, media, contain, facts, footer, stacked, size = "md", className, children, ...rest },
32
+ ref,
33
+ ) {
34
+ const frame = media ?? (src ? <img src={src} alt={alt} /> : glyph != null ? <span className="oc-entity-card__glyph" aria-hidden="true">{glyph}</span> : null);
35
+ return (
36
+ <article ref={ref} className={cx("oc-entity-card", stacked && "oc-entity-card--stacked", size === "sm" && "oc-entity-card--sm", className)} {...rest}>
37
+ <div className="oc-entity-card__grid">
38
+ <div className={cx("oc-entity-card__frame", contain && "oc-entity-card__frame--contain")}>{frame}</div>
39
+ <div className="oc-entity-card__body">
40
+ {eyebrow != null && <p className="oc-entity-card__eyebrow">{eyebrow}</p>}
41
+ <h3 className="oc-entity-card__title">{title}</h3>
42
+ {description != null && <p className="oc-entity-card__description">{description}</p>}
43
+ {facts && facts.length > 0 && (
44
+ <dl className="oc-entity-card__facts">
45
+ {facts.map(([k, v], i) => (
46
+ <Fragment key={i}><dt>{k}</dt><dd>{v}</dd></Fragment>
47
+ ))}
48
+ </dl>
49
+ )}
50
+ {children}
51
+ </div>
52
+ </div>
53
+ {footer != null && <div className="oc-entity-card__footer">{footer}</div>}
54
+ </article>
55
+ );
56
+ });
@@ -0,0 +1,43 @@
1
+ // File: packages/react/src/components/FactList.tsx | Map: types -> FactList (dl.oc-fact-list -> div > dt + dd)
2
+ import { forwardRef } from "react";
3
+ import type { HTMLAttributes, ReactNode } from "react";
4
+ import { cx } from "../utils";
5
+
6
+ export interface FactListRow {
7
+ label: ReactNode;
8
+ value: ReactNode;
9
+ }
10
+
11
+ export interface FactListProps extends HTMLAttributes<HTMLDListElement> {
12
+ items: FactListRow[];
13
+ plain?: boolean;
14
+ compact?: boolean;
15
+ stacked?: boolean;
16
+ /** Values in the accent color. */
17
+ accent?: boolean;
18
+ }
19
+
20
+ /** `<dl class="oc-fact-list">`: key / value product facts (variant of the definition list). */
21
+ export const FactList = forwardRef<HTMLDListElement, FactListProps>(function FactList(
22
+ { items, plain = false, compact = false, stacked = false, accent = false, className, ...rest },
23
+ ref,
24
+ ) {
25
+ const classes = cx(
26
+ "oc-fact-list",
27
+ plain && "oc-fact-list--plain",
28
+ compact && "oc-fact-list--compact",
29
+ stacked && "oc-fact-list--stacked",
30
+ accent && "oc-fact-list--accent",
31
+ className,
32
+ );
33
+ return (
34
+ <dl ref={ref} className={classes} {...rest}>
35
+ {items.map((row, i) => (
36
+ <div key={i}>
37
+ <dt>{row.label}</dt>
38
+ <dd>{row.value}</dd>
39
+ </div>
40
+ ))}
41
+ </dl>
42
+ );
43
+ });
@@ -0,0 +1,50 @@
1
+ // File: packages/react/src/components/FilterCounts.tsx
2
+ // Map: types -> component (fieldset radiogroup -> one tile per view: radio + value + label + hint)
3
+ // Same markup as packages/ui/snippets/filter-counts.html.
4
+ "use client";
5
+
6
+ import { forwardRef, useId } from "react";
7
+ import type { FieldsetHTMLAttributes, ReactNode } from "react";
8
+ import { cx } from "../utils";
9
+ import { useControllable } from "../hooks";
10
+
11
+ export interface FilterCountsOption {
12
+ value: string;
13
+ label: ReactNode;
14
+ count: number | string;
15
+ hint?: ReactNode;
16
+ /** Accent color of the tile; default primary. */
17
+ tone?: "primary" | "success" | "destructive" | "muted";
18
+ disabled?: boolean;
19
+ }
20
+
21
+ export interface FilterCountsProps extends Omit<FieldsetHTMLAttributes<HTMLFieldSetElement>, "onChange" | "defaultValue"> {
22
+ options: FilterCountsOption[];
23
+ value?: string;
24
+ defaultValue?: string;
25
+ onChange?: (value: string) => void;
26
+ name?: string;
27
+ /** Small inline pills instead of tiles. */
28
+ compact?: boolean;
29
+ }
30
+
31
+ /** `.oc-filter-counts`: choose a view; every tile shows its item count. */
32
+ export const FilterCounts = forwardRef<HTMLFieldSetElement, FilterCountsProps>(function FilterCounts(
33
+ { options, value, defaultValue, onChange, name, compact, className, ...rest },
34
+ ref,
35
+ ) {
36
+ const group = name ?? `oc-filter-${useId().replace(/:/g, "")}`;
37
+ const [current, setCurrent] = useControllable<string>(value, defaultValue ?? options[0]?.value ?? "", onChange);
38
+ return (
39
+ <fieldset ref={ref} className={cx("oc-filter-counts", compact && "oc-filter-counts--compact", className)} role="radiogroup" {...rest}>
40
+ {options.map((o) => (
41
+ <label key={o.value} className={cx("oc-filter-counts-item", o.tone && o.tone !== "primary" && `oc-filter-counts-item--${o.tone}`)}>
42
+ <input type="radio" name={group} value={o.value} disabled={o.disabled} checked={current === o.value} onChange={() => setCurrent(o.value)} />
43
+ <span className="oc-filter-counts-value">{o.count}</span>
44
+ <span className="oc-filter-counts-label">{o.label}</span>
45
+ {o.hint != null && <span className="oc-filter-counts-hint">{o.hint}</span>}
46
+ </label>
47
+ ))}
48
+ </fieldset>
49
+ );
50
+ });
@@ -0,0 +1,55 @@
1
+ // File: packages/react/src/components/FlipCard.tsx
2
+ // Map: props -> FlipCard (optional checkbox -> inner -> front face + flip label -> back face + flip label)
3
+ // Same markup as packages/ui/snippets/flip-card.html. trigger="toggle" uses the CSS checkbox, "hover" the --hover
4
+ // modifier, "manual" only the `flipped` prop (.is-flipped).
5
+ "use client";
6
+
7
+ import { forwardRef, useId } from "react";
8
+ import type { HTMLAttributes, ReactNode } from "react";
9
+ import { cx } from "../utils";
10
+ import { useControllable } from "../hooks";
11
+
12
+ export interface FlipCardProps extends HTMLAttributes<HTMLDivElement> {
13
+ front: ReactNode;
14
+ back: ReactNode;
15
+ /** How the card flips. Default "toggle" (click / Space). */
16
+ trigger?: "toggle" | "hover" | "manual";
17
+ flipped?: boolean;
18
+ defaultFlipped?: boolean;
19
+ onFlippedChange?: (flipped: boolean) => void;
20
+ /** Turn top over bottom instead of left over right. */
21
+ vertical?: boolean;
22
+ /** Text of the flip labels (toggle mode). */
23
+ frontLabel?: ReactNode;
24
+ backLabel?: ReactNode;
25
+ }
26
+
27
+ /** `.oc-flip-card`: front / back card that turns in 3D (cross-fades with reduced motion). */
28
+ export const FlipCard = forwardRef<HTMLDivElement, FlipCardProps>(function FlipCard(
29
+ { front, back, trigger = "toggle", flipped, defaultFlipped = false, onFlippedChange, vertical, frontLabel = "Show answer", backLabel = "Back", className, ...rest },
30
+ ref,
31
+ ) {
32
+ const id = `oc-flip-${useId().replace(/:/g, "")}`;
33
+ const [isFlipped, setFlipped] = useControllable(flipped, defaultFlipped, onFlippedChange);
34
+ const toggle = trigger === "toggle";
35
+ return (
36
+ <div
37
+ ref={ref}
38
+ className={cx("oc-flip-card", trigger === "hover" && "oc-flip-card--hover", vertical && "oc-flip-card--vertical", !toggle && isFlipped && "is-flipped", className)}
39
+ tabIndex={trigger === "hover" ? 0 : undefined}
40
+ {...rest}
41
+ >
42
+ {toggle && <input type="checkbox" className="oc-flip-card__toggle" id={id} checked={isFlipped} onChange={(e) => setFlipped(e.target.checked)} />}
43
+ <div className="oc-flip-card__inner">
44
+ <div className="oc-flip-card__face oc-flip-card__face--front">
45
+ {front}
46
+ {toggle && <label className="oc-flip-card__flip" htmlFor={id}>{frontLabel}</label>}
47
+ </div>
48
+ <div className="oc-flip-card__face oc-flip-card__face--back">
49
+ {back}
50
+ {toggle && <label className="oc-flip-card__flip" htmlFor={id}>{backLabel}</label>}
51
+ </div>
52
+ </div>
53
+ </div>
54
+ );
55
+ });
@@ -0,0 +1,54 @@
1
+ import { Children, cloneElement, forwardRef, isValidElement, useId } from "react";
2
+ import type { HTMLAttributes, ReactElement, ReactNode } from "react";
3
+ import { cx } from "../utils";
4
+
5
+ export interface FormFieldProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
6
+ label?: ReactNode;
7
+ description?: ReactNode;
8
+ /** Error message (`oc-field-error`); also sets `aria-invalid` on the control. */
9
+ error?: ReactNode;
10
+ /** Horizontal layout (`oc-field--horizontal`). */
11
+ horizontal?: boolean;
12
+ /** Id of the control; generated when omitted. */
13
+ htmlFor?: string;
14
+ /** A single control (Input, Select, Textarea...) gets id / aria-describedby / aria-invalid injected. */
15
+ children?: ReactNode;
16
+ }
17
+
18
+ /** `<div class="oc-field">` with label, control, description and error wired through ids. */
19
+ export const FormField = forwardRef<HTMLDivElement, FormFieldProps>(function FormField(
20
+ { label, description, error, horizontal, htmlFor, className, children, ...rest },
21
+ ref,
22
+ ) {
23
+ const auto = useId();
24
+ const only = Children.count(children) === 1 && isValidElement(children) ? (children as ReactElement<Record<string, unknown>>) : null;
25
+ const controlId = htmlFor ?? (only?.props.id as string | undefined) ?? `oc-field-${auto.replace(/:/g, "")}`;
26
+ const helpId = description != null ? `${controlId}-help` : undefined;
27
+ const errorId = error != null ? `${controlId}-error` : undefined;
28
+ const describedBy = [only?.props["aria-describedby"] as string | undefined, helpId, errorId].filter(Boolean).join(" ") || undefined;
29
+
30
+ const control = only
31
+ ? cloneElement(only, {
32
+ id: controlId,
33
+ "aria-describedby": describedBy,
34
+ ...(error != null ? { "aria-invalid": true } : {}),
35
+ })
36
+ : children;
37
+
38
+ return (
39
+ <div ref={ref} className={cx("oc-field", horizontal && "oc-field--horizontal", className)} {...rest}>
40
+ {label != null && <label className="oc-field-label" htmlFor={controlId}>{label}</label>}
41
+ {control}
42
+ {description != null && <p className="oc-field-description" id={helpId}>{description}</p>}
43
+ {error != null && <p className="oc-field-error" id={errorId} role="alert">{error}</p>}
44
+ </div>
45
+ );
46
+ });
47
+
48
+ /** `<div class="oc-field-group">`: stack (or `row`) of related controls. */
49
+ export const FieldGroup = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement> & { row?: boolean }>(function FieldGroup(
50
+ { row, className, ...rest },
51
+ ref,
52
+ ) {
53
+ return <div ref={ref} className={cx("oc-field-group", row && "oc-field-group--row", className)} {...rest} />;
54
+ });
@@ -0,0 +1,63 @@
1
+ // File: packages/react/src/components/Greeting.tsx
2
+ // Map: greetingTimeOfDay() helper -> Greeting = header -> icon -> body (eyebrow, title + name, date) -> actions
3
+ // The time of day is computed from `date` (default: now). Morning 05-12, afternoon 12-18, evening otherwise.
4
+ import { forwardRef } from "react";
5
+ import type { HTMLAttributes, ReactNode } from "react";
6
+ import { cx } from "../utils";
7
+
8
+ export type GreetingTimeOfDay = "morning" | "afternoon" | "evening";
9
+
10
+ /** Pick the time-of-day modifier for a date. */
11
+ export function greetingTimeOfDay(date: Date = new Date()): GreetingTimeOfDay {
12
+ const h = date.getHours();
13
+ if (h >= 5 && h < 12) return "morning";
14
+ if (h >= 12 && h < 18) return "afternoon";
15
+ return "evening";
16
+ }
17
+
18
+ const WORDS: Record<GreetingTimeOfDay, string> = { morning: "Good morning", afternoon: "Good afternoon", evening: "Good evening" };
19
+
20
+ export interface GreetingProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
21
+ name?: ReactNode;
22
+ /** The moment to greet for (default: now). Also shown as the date. */
23
+ date?: Date;
24
+ /** Force a time of day instead of computing it. */
25
+ timeOfDay?: GreetingTimeOfDay;
26
+ /** Translated greetings, e.g. `{ morning: "Goedemorgen", ... }`. */
27
+ words?: Partial<Record<GreetingTimeOfDay, string>>;
28
+ /** Locale for the date text (default: the browser locale). */
29
+ locale?: string;
30
+ /** Hide the date line. */
31
+ hideDate?: boolean;
32
+ eyebrow?: ReactNode;
33
+ icon?: ReactNode;
34
+ actions?: ReactNode;
35
+ size?: "lg";
36
+ }
37
+
38
+ /** `.oc-greeting`: dashboard greeting with date and actions. */
39
+ export const Greeting = forwardRef<HTMLElement, GreetingProps>(function Greeting(
40
+ { name, date, timeOfDay, words, locale, hideDate, eyebrow, icon, actions, size, className, ...rest },
41
+ ref,
42
+ ) {
43
+ const when = date ?? new Date();
44
+ const tod = timeOfDay ?? greetingTimeOfDay(when);
45
+ const word = words?.[tod] ?? WORDS[tod];
46
+ const dateText = new Intl.DateTimeFormat(locale, { weekday: "long", day: "numeric", month: "long", year: "numeric" }).format(when);
47
+ // datetime needs YYYY-MM-DD in local time (toISOString would use UTC).
48
+ const iso = `${when.getFullYear()}-${String(when.getMonth() + 1).padStart(2, "0")}-${String(when.getDate()).padStart(2, "0")}`;
49
+ return (
50
+ <header ref={ref} className={cx("oc-greeting", `oc-greeting--${tod}`, size && `oc-greeting--${size}`, className)} {...rest}>
51
+ {icon != null && <span className="oc-greeting-icon" aria-hidden="true">{icon}</span>}
52
+ <div className="oc-greeting-body">
53
+ {eyebrow != null && <p className="oc-greeting-eyebrow">{eyebrow}</p>}
54
+ <h2 className="oc-greeting-title">
55
+ {word}
56
+ {name != null && <>, <span className="oc-greeting-name">{name}</span></>}
57
+ </h2>
58
+ {!hideDate && <time className="oc-greeting-date" dateTime={iso}>{dateText}</time>}
59
+ </div>
60
+ {actions != null && <div className="oc-greeting-actions">{actions}</div>}
61
+ </header>
62
+ );
63
+ });