@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,47 @@
1
+ "use client";
2
+
3
+ import { forwardRef, useId } from "react";
4
+ import type { HTMLAttributes, ReactNode } from "react";
5
+ import { cx } from "../utils";
6
+ import { useControllable } from "../hooks";
7
+
8
+ export interface SegmentedOption {
9
+ value: string;
10
+ label: ReactNode;
11
+ }
12
+
13
+ export interface SegmentedProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "defaultValue"> {
14
+ options: SegmentedOption[];
15
+ value?: string;
16
+ defaultValue?: string;
17
+ onChange?: (value: string) => void;
18
+ /** Radio group name (generated when omitted). */
19
+ name?: string;
20
+ /** Full width. */
21
+ block?: boolean;
22
+ size?: "sm" | "md";
23
+ }
24
+
25
+ /** `.oc-segmented` radio group as a pill switcher. */
26
+ export const Segmented = forwardRef<HTMLDivElement, SegmentedProps>(function Segmented(
27
+ { options, value, defaultValue, onChange, name, block, size = "md", className, ...rest },
28
+ ref,
29
+ ) {
30
+ const auto = useId();
31
+ const group = name ?? `oc-segmented-${auto.replace(/:/g, "")}`;
32
+ const [current, setCurrent] = useControllable<string>(value, defaultValue ?? options[0]?.value ?? "", onChange);
33
+ return (
34
+ <div
35
+ ref={ref}
36
+ className={cx("oc-segmented", block && "oc-segmented--block", size === "sm" && "oc-segmented--sm", className)}
37
+ role="radiogroup"
38
+ {...rest}
39
+ >
40
+ {options.map((o) => (
41
+ <label className="oc-segmented-item" key={o.value}>
42
+ <input type="radio" name={group} value={o.value} checked={current === o.value} onChange={() => setCurrent(o.value)} /> {o.label}
43
+ </label>
44
+ ))}
45
+ </div>
46
+ );
47
+ });
@@ -0,0 +1,27 @@
1
+ import { forwardRef } from "react";
2
+ import type { ReactNode, SelectHTMLAttributes } from "react";
3
+ import { cx } from "../utils";
4
+
5
+ export interface SelectOption {
6
+ value: string;
7
+ label: ReactNode;
8
+ disabled?: boolean;
9
+ }
10
+
11
+ export interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
12
+ /** Shortcut for `<option>` children. */
13
+ options?: Array<SelectOption | string>;
14
+ }
15
+
16
+ /** `<select class="oc-select">` */
17
+ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select({ options, className, children, ...rest }, ref) {
18
+ return (
19
+ <select ref={ref} className={cx("oc-select", className)} {...rest}>
20
+ {options?.map((o) => {
21
+ const opt = typeof o === "string" ? { value: o, label: o } : o;
22
+ return <option key={opt.value} value={opt.value} disabled={opt.disabled}>{opt.label}</option>;
23
+ })}
24
+ {children}
25
+ </select>
26
+ );
27
+ });
@@ -0,0 +1,53 @@
1
+ // File: packages/react/src/components/SeverityCard.tsx
2
+ // Map: SeverityCard = details -> summary head (icon, title + tags, level badge) -> body (children) -> actions
3
+ // Native <details>: opens without JavaScript. Cards with the same `name` open one at a time.
4
+ import { forwardRef } from "react";
5
+ import type { DetailsHTMLAttributes, ReactNode } from "react";
6
+ import { cx } from "../utils";
7
+
8
+ export type SeverityLevel = "low" | "medium" | "high" | "critical";
9
+
10
+ const LEVEL_TEXT: Record<SeverityLevel, string> = { low: "Low", medium: "Medium", high: "High", critical: "Critical" };
11
+
12
+ export interface SeverityCardProps extends Omit<DetailsHTMLAttributes<HTMLDetailsElement>, "title"> {
13
+ level: SeverityLevel;
14
+ title: ReactNode;
15
+ /** Small tags under the title, e.g. ["email", "password"]. */
16
+ tags?: ReactNode[];
17
+ /** Badge text; default the level name ("High"). */
18
+ levelLabel?: ReactNode;
19
+ icon?: ReactNode;
20
+ /** Neutral colors (the finding is handled). */
21
+ resolved?: boolean;
22
+ actions?: ReactNode;
23
+ /** Exclusive group: cards with the same name open one at a time. */
24
+ name?: string;
25
+ }
26
+
27
+ /** `details.oc-severity-card`: expandable card colored by risk level. */
28
+ export const SeverityCard = forwardRef<HTMLDetailsElement, SeverityCardProps>(function SeverityCard(
29
+ { level, title, tags, levelLabel, icon, resolved, actions, className, children, ...rest },
30
+ ref,
31
+ ) {
32
+ const cls = cx("oc-severity-card", `oc-severity-card--${level}`, resolved && "is-resolved", className);
33
+ return (
34
+ <details ref={ref} className={cls} {...(rest as object)}>
35
+ <summary className="oc-severity-card-head">
36
+ {icon != null && <span className="oc-severity-card-icon" aria-hidden="true">{icon}</span>}
37
+ <span className="oc-severity-card-main">
38
+ <span className="oc-severity-card-title">{title}</span>
39
+ {tags && tags.length > 0 && (
40
+ <span className="oc-severity-card-tags">
41
+ {tags.map((t, i) => <span className="oc-severity-card-tag" key={i}>{t}</span>)}
42
+ </span>
43
+ )}
44
+ </span>
45
+ <span className="oc-severity-card-level">{levelLabel ?? LEVEL_TEXT[level]}</span>
46
+ </summary>
47
+ <div className="oc-severity-card-body">
48
+ {children}
49
+ {actions != null && <div className="oc-severity-card-actions">{actions}</div>}
50
+ </div>
51
+ </details>
52
+ );
53
+ });
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ import { forwardRef, useId, useRef } from "react";
4
+ import type { DialogHTMLAttributes, MouseEvent, 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 SheetSide = "right" | "left" | "top" | "bottom";
10
+
11
+ export interface SheetProps extends Omit<DialogHTMLAttributes<HTMLDialogElement>, "title" | "open" | "onClose">, DialogState {
12
+ /** Edge the panel slides from (default `right`). */
13
+ side?: SheetSide;
14
+ /** Wider panel (`oc-sheet-content--wide`). */
15
+ wide?: boolean;
16
+ title?: ReactNode;
17
+ description?: ReactNode;
18
+ /** Action row (`oc-sheet-footer`). */
19
+ footer?: ReactNode;
20
+ /** Accessible name of the close button. */
21
+ closeLabel?: string;
22
+ }
23
+
24
+ /** Side panel on `<dialog class="oc-sheet">`. */
25
+ export const Sheet = forwardRef<HTMLDialogElement, SheetProps>(function Sheet(
26
+ { open, defaultOpen, onOpenChange, side = "right", wide, title, description, footer, closeLabel = "Close", className, children, ...rest },
27
+ forwardedRef,
28
+ ) {
29
+ const inner = useRef<HTMLDialogElement>(null);
30
+ const ref = useMergedRef(inner, forwardedRef);
31
+ const titleId = useId();
32
+ const dlg = useDialog(inner, { open, defaultOpen, onOpenChange });
33
+ const onClick = (e: MouseEvent<HTMLDialogElement>) => {
34
+ rest.onClick?.(e);
35
+ if (e.target === e.currentTarget) dlg.close();
36
+ };
37
+ return (
38
+ <dialog
39
+ ref={ref}
40
+ className={cx("oc-sheet", side !== "right" && `oc-sheet--${side}`, className)}
41
+ aria-labelledby={title != null ? titleId : undefined}
42
+ {...attrs({ closedby: "any" })}
43
+ {...rest}
44
+ onClick={onClick}
45
+ onClose={dlg.onClose}
46
+ >
47
+ <DialogContext.Provider value={{ close: dlg.close }}>
48
+ <div className={cx("oc-sheet-content", wide && "oc-sheet-content--wide")}>
49
+ <button className="oc-sheet-close" type="button" aria-label={closeLabel} onClick={dlg.close}>&times;</button>
50
+ {(title != null || description != null) && (
51
+ <div className="oc-sheet-header">
52
+ {title != null && <h2 className="oc-sheet-title" id={titleId}>{title}</h2>}
53
+ {description != null && <p className="oc-sheet-description">{description}</p>}
54
+ </div>
55
+ )}
56
+ <div className="oc-sheet-body">{children}</div>
57
+ {footer != null && <div className="oc-sheet-footer">{footer}</div>}
58
+ </div>
59
+ </DialogContext.Provider>
60
+ </dialog>
61
+ );
62
+ });
@@ -0,0 +1,145 @@
1
+ "use client";
2
+
3
+ import { forwardRef } from "react";
4
+ import type { AnchorHTMLAttributes, HTMLAttributes, LiHTMLAttributes, ReactNode } from "react";
5
+ import { cx } from "../utils";
6
+ import { useControllable } from "../hooks";
7
+
8
+ export interface SidebarProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
9
+ /** Icon-only mode. */
10
+ collapsed?: boolean;
11
+ /** Dock on the right. */
12
+ right?: boolean;
13
+ /** Floating card look. */
14
+ floating?: boolean;
15
+ /** Dark panel that stays dark in the light theme (`--dark`). */
16
+ dark?: boolean;
17
+ /** Fixed top area (logo, workspace switcher). */
18
+ header?: ReactNode;
19
+ /** Fixed bottom area (user). */
20
+ footer?: ReactNode;
21
+ }
22
+
23
+ /** `<aside class="oc-sidebar">`: children go in the scrolling `oc-sidebar-content`. */
24
+ export const Sidebar = forwardRef<HTMLElement, SidebarProps>(function Sidebar(
25
+ { collapsed, right, floating, dark, header, footer, className, children, ...rest },
26
+ ref,
27
+ ) {
28
+ return (
29
+ <aside
30
+ ref={ref}
31
+ className={cx("oc-sidebar", collapsed && "oc-sidebar--collapsed", right && "oc-sidebar--right", floating && "oc-sidebar--floating", dark && "oc-sidebar--dark", className)}
32
+ {...rest}
33
+ >
34
+ {header != null && <div className="oc-sidebar-header">{header}</div>}
35
+ <div className="oc-sidebar-content">{children}</div>
36
+ {footer != null && <div className="oc-sidebar-footer">{footer}</div>}
37
+ </aside>
38
+ );
39
+ });
40
+
41
+ export interface SidebarGroupProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
42
+ /** Group heading (`oc-sidebar-group-label`). */
43
+ label?: ReactNode;
44
+ }
45
+
46
+ /** `.oc-sidebar-group` with an optional label. */
47
+ export const SidebarGroup = forwardRef<HTMLDivElement, SidebarGroupProps>(function SidebarGroup(
48
+ { label, className, children, ...rest },
49
+ ref,
50
+ ) {
51
+ return (
52
+ <div ref={ref} className={cx("oc-sidebar-group", className)} {...rest}>
53
+ {label != null && <div className="oc-sidebar-group-label">{label}</div>}
54
+ {children}
55
+ </div>
56
+ );
57
+ });
58
+
59
+ /** `.oc-sidebar-separator` */
60
+ export function SidebarSeparator({ className, ...rest }: HTMLAttributes<HTMLDivElement>) {
61
+ return <div className={cx("oc-sidebar-separator", className)} {...rest} />;
62
+ }
63
+
64
+ /** `ul.oc-sidebar-menu` */
65
+ export const SidebarMenu = forwardRef<HTMLUListElement, HTMLAttributes<HTMLUListElement>>(function SidebarMenu(
66
+ { className, ...rest },
67
+ ref,
68
+ ) {
69
+ return <ul ref={ref} className={cx("oc-sidebar-menu", className)} {...rest} />;
70
+ });
71
+
72
+ /** `<li>` of a sidebar menu. */
73
+ export function SidebarMenuItem(props: LiHTMLAttributes<HTMLLIElement>) {
74
+ return <li {...props} />;
75
+ }
76
+
77
+ export interface SidebarMenuButtonProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
78
+ /** Inline SVG icon (`oc-sidebar-menu-icon`). */
79
+ icon?: ReactNode;
80
+ /** Count pill on the right. */
81
+ badge?: ReactNode;
82
+ /** Current page (`aria-current="page"`). */
83
+ current?: boolean;
84
+ disabled?: boolean;
85
+ size?: "md" | "lg";
86
+ /** Wrap the children in `oc-sidebar-menu-label` (hidden when collapsed). Default true. */
87
+ label?: boolean;
88
+ }
89
+
90
+ /** `.oc-sidebar-menu-button`: a link (or button when no `href`). */
91
+ export const SidebarMenuButton = forwardRef<HTMLAnchorElement, SidebarMenuButtonProps>(function SidebarMenuButton(
92
+ { icon, badge, current, disabled, size = "md", label = true, className, children, href, ...rest },
93
+ ref,
94
+ ) {
95
+ const content = (
96
+ <>
97
+ {icon}
98
+ {label ? <span className="oc-sidebar-menu-label">{children}</span> : children}
99
+ {badge != null && <span className="oc-sidebar-menu-badge">{badge}</span>}
100
+ </>
101
+ );
102
+ const props = {
103
+ className: cx("oc-sidebar-menu-button", size === "lg" && "oc-sidebar-menu-button--lg", className),
104
+ "aria-current": current ? ("page" as const) : undefined,
105
+ "aria-disabled": disabled ? true : undefined,
106
+ };
107
+ if (href === undefined) {
108
+ return <button type="button" {...props} {...(rest as object)}>{content}</button>;
109
+ }
110
+ return <a ref={ref} href={href} {...props} {...rest}>{content}</a>;
111
+ });
112
+
113
+ export interface SidebarCollapsibleProps extends Omit<HTMLAttributes<HTMLDetailsElement>, "title" | "onToggle"> {
114
+ icon?: ReactNode;
115
+ /** Summary label. */
116
+ label: ReactNode;
117
+ open?: boolean;
118
+ defaultOpen?: boolean;
119
+ onOpenChange?: (open: boolean) => void;
120
+ }
121
+
122
+ /** `details.oc-sidebar-collapsible`: children are the `<li>` items of the nested `oc-sidebar-menu-sub`. */
123
+ export const SidebarCollapsible = forwardRef<HTMLDetailsElement, SidebarCollapsibleProps>(function SidebarCollapsible(
124
+ { icon, label, open, defaultOpen, onOpenChange, className, children, ...rest },
125
+ ref,
126
+ ) {
127
+ const [isOpen, setOpen] = useControllable<boolean>(open, defaultOpen ?? false, onOpenChange);
128
+ return (
129
+ <details
130
+ ref={ref}
131
+ className={cx("oc-sidebar-collapsible", className)}
132
+ open={isOpen}
133
+ onToggle={(e) => {
134
+ if (e.currentTarget.open !== isOpen) setOpen(e.currentTarget.open);
135
+ }}
136
+ {...rest}
137
+ >
138
+ <summary className="oc-sidebar-menu-button">
139
+ {icon}
140
+ <span className="oc-sidebar-menu-label">{label}</span>
141
+ </summary>
142
+ <ul className="oc-sidebar-menu-sub">{children}</ul>
143
+ </details>
144
+ );
145
+ });
@@ -0,0 +1,12 @@
1
+ import { forwardRef } from "react";
2
+ import type { HTMLAttributes } from "react";
3
+ import { cx } from "../utils";
4
+
5
+ export interface SkeletonProps extends HTMLAttributes<HTMLDivElement> {
6
+ variant?: "circle" | "title" | "text" | "rect";
7
+ }
8
+
9
+ /** `<div class="oc-skeleton oc-skeleton--{variant}">` */
10
+ export const Skeleton = forwardRef<HTMLDivElement, SkeletonProps>(function Skeleton({ variant, className, ...rest }, ref) {
11
+ return <div ref={ref} className={cx("oc-skeleton", variant && `oc-skeleton--${variant}`, className)} {...rest} />;
12
+ });
@@ -0,0 +1,46 @@
1
+ // File: packages/react/src/components/SortableRow.tsx
2
+ // Map: SortableRow = tr.oc-sortable-row -> grip cell (button) -> your cells
3
+ // Styling only: your code handles the drag events (they are passed through on the row)
4
+ // and sets `dragging` / `drop` to show the states.
5
+ import { forwardRef } from "react";
6
+ import type { HTMLAttributes, ReactNode } from "react";
7
+ import { cx } from "../utils";
8
+
9
+ export interface SortableRowProps extends HTMLAttributes<HTMLTableRowElement> {
10
+ /** Accessible name of the grip, e.g. "Move Install". */
11
+ gripLabel: string;
12
+ /** This row is being moved (`.is-dragging`). */
13
+ dragging?: boolean;
14
+ /** Show the drop line above or below this row. */
15
+ drop?: "before" | "after" | null;
16
+ /** Locked row: no dragging, grip disabled. */
17
+ locked?: boolean;
18
+ /** Picked up with the keyboard (grip `aria-pressed`). */
19
+ grabbed?: boolean;
20
+ /** Keyboard handler for the grip (e.g. ArrowUp / ArrowDown to move). */
21
+ onGripKeyDown?: (e: React.KeyboardEvent<HTMLButtonElement>) => void;
22
+ children: ReactNode;
23
+ }
24
+
25
+ /** `tr.oc-sortable-row[draggable]` with a `button.oc-sortable-row-grip` in the first cell. */
26
+ export const SortableRow = forwardRef<HTMLTableRowElement, SortableRowProps>(function SortableRow(
27
+ { gripLabel, dragging, drop, locked, grabbed, onGripKeyDown, className, children, ...rest },
28
+ ref,
29
+ ) {
30
+ const cls = cx("oc-sortable-row", dragging && "is-dragging", drop && `is-drop-${drop}`, className);
31
+ return (
32
+ <tr ref={ref} className={cls} draggable={locked ? undefined : true} {...rest}>
33
+ <td className="oc-sortable-row-grip-cell">
34
+ <button
35
+ className="oc-sortable-row-grip"
36
+ type="button"
37
+ aria-label={gripLabel}
38
+ aria-pressed={grabbed === undefined ? undefined : grabbed}
39
+ disabled={locked}
40
+ onKeyDown={onGripKeyDown}
41
+ />
42
+ </td>
43
+ {children}
44
+ </tr>
45
+ );
46
+ });
@@ -0,0 +1,40 @@
1
+ // File: packages/react/src/components/StackCard.tsx
2
+ // Map: props -> StackCard (deck: MediaThumb cover + count badge -> title link + meta)
3
+ // Same markup as packages/ui/snippets/stack-card.html.
4
+ import { forwardRef } from "react";
5
+ import type { HTMLAttributes, ReactNode } from "react";
6
+ import { cx } from "../utils";
7
+ import { MediaThumb } from "./MediaThumb";
8
+ import type { MediaThumbProps } from "./MediaThumb";
9
+
10
+ export interface StackCardProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
11
+ title: ReactNode;
12
+ /** Link of the title; it covers the whole card. */
13
+ href: string;
14
+ meta?: ReactNode;
15
+ /** Badge text, e.g. "12 videos". */
16
+ count?: ReactNode;
17
+ /** Show the count as a side panel (`--panel`). */
18
+ panel?: boolean;
19
+ /** Props for the cover (MediaThumb). */
20
+ thumb?: MediaThumbProps;
21
+ }
22
+
23
+ /** `article.oc-stack-card`: cover with two layers behind it and a count badge. */
24
+ export const StackCard = forwardRef<HTMLElement, StackCardProps>(function StackCard(
25
+ { title, href, meta, count, panel, thumb, className, ...rest },
26
+ ref,
27
+ ) {
28
+ return (
29
+ <article ref={ref} className={cx("oc-stack-card", panel && "oc-stack-card--panel", className)} {...rest}>
30
+ <div className="oc-stack-card__deck">
31
+ <MediaThumb {...thumb} />
32
+ {count != null && <span className="oc-stack-card__count">{count}</span>}
33
+ </div>
34
+ <div>
35
+ <h3 className="oc-stack-card__title"><a href={href}>{title}</a></h3>
36
+ {meta != null && <p className="oc-stack-card__meta">{meta}</p>}
37
+ </div>
38
+ </article>
39
+ );
40
+ });
@@ -0,0 +1,42 @@
1
+ // File: packages/react/src/components/StackList.tsx | Map: types -> StackList (ul.oc-stack-list -> li pills)
2
+ import { forwardRef } from "react";
3
+ import type { HTMLAttributes, ReactNode } from "react";
4
+ import { cx } from "../utils";
5
+
6
+ export interface StackListItem {
7
+ label: ReactNode;
8
+ /** Optional glyph or SVG before the label (decorative). */
9
+ icon?: ReactNode;
10
+ }
11
+
12
+ export interface StackListProps extends HTMLAttributes<HTMLUListElement> {
13
+ /** Strings, or objects with an icon. */
14
+ items: Array<ReactNode | StackListItem>;
15
+ neutral?: boolean;
16
+ size?: "default" | "sm";
17
+ }
18
+
19
+ const isItem = (v: unknown): v is StackListItem => typeof v === "object" && v !== null && "label" in v;
20
+
21
+ /** `<ul class="oc-stack-list">`: technology pills. Set an accent with `className="oc-accent--violet"`. */
22
+ export const StackList = forwardRef<HTMLUListElement, StackListProps>(function StackList(
23
+ { items, neutral = false, size = "default", className, ...rest },
24
+ ref,
25
+ ) {
26
+ return (
27
+ <ul ref={ref} className={cx("oc-stack-list", neutral && "oc-stack-list--neutral", size === "sm" && "oc-stack-list--sm", className)} {...rest}>
28
+ {items.map((item, i) => (
29
+ <li className="oc-stack-list__item" key={i}>
30
+ {isItem(item) ? (
31
+ <>
32
+ {item.icon != null && <span className="oc-stack-list__icon" aria-hidden="true">{item.icon}</span>}
33
+ {item.label}
34
+ </>
35
+ ) : (
36
+ item
37
+ )}
38
+ </li>
39
+ ))}
40
+ </ul>
41
+ );
42
+ });
@@ -0,0 +1,49 @@
1
+ // File: packages/react/src/components/StatCard.tsx
2
+ // Map: StatCardGrid (row of cards) -> StatCard = card (div, a or button) -> icon -> body (value + trend, label)
3
+ // With `href` the card is a link, with `onClick` a button; otherwise a plain div.
4
+ import { forwardRef } from "react";
5
+ import type { HTMLAttributes, ReactNode } from "react";
6
+ import { cx } from "../utils";
7
+
8
+ /** `.oc-stat-card-grid`: responsive grid for a row of stat cards. */
9
+ export const StatCardGrid = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(function StatCardGrid(
10
+ { className, ...rest },
11
+ ref,
12
+ ) {
13
+ return <div ref={ref} className={cx("oc-stat-card-grid", className)} {...rest} />;
14
+ });
15
+
16
+ export interface StatCardProps extends Omit<HTMLAttributes<HTMLElement>, "onClick"> {
17
+ value: ReactNode;
18
+ label: ReactNode;
19
+ icon?: ReactNode;
20
+ /** Trend chip text after the value, e.g. "+12%". */
21
+ trend?: ReactNode;
22
+ trendDirection?: "up" | "down";
23
+ tone?: "default" | "accent" | "success" | "warning";
24
+ /** Colored start edge. */
25
+ stripe?: boolean;
26
+ href?: string;
27
+ onClick?: () => void;
28
+ }
29
+
30
+ /** `.oc-stat-card`: compact KPI tile. */
31
+ export function StatCard({ value, label, icon, trend, trendDirection = "up", tone = "default", stripe, href, onClick, className, ...rest }: StatCardProps) {
32
+ const cls = cx("oc-stat-card", tone !== "default" && `oc-stat-card--${tone}`, stripe && "oc-stat-card--stripe", className);
33
+ // Spans only inside the body, so the markup is also valid inside a link or button.
34
+ const content = (
35
+ <>
36
+ {icon != null && <span className="oc-stat-card-icon" aria-hidden="true">{icon}</span>}
37
+ <span className="oc-stat-card-body">
38
+ <span className="oc-stat-card-value">
39
+ {value}
40
+ {trend != null && <span className={`oc-stat-card-trend oc-stat-card-trend--${trendDirection}`}>{trend}</span>}
41
+ </span>
42
+ <span className="oc-stat-card-label">{label}</span>
43
+ </span>
44
+ </>
45
+ );
46
+ if (href !== undefined) return <a className={cls} href={href} {...rest}>{content}</a>;
47
+ if (onClick) return <button className={cls} type="button" onClick={onClick} {...rest}>{content}</button>;
48
+ return <div className={cls} {...rest}>{content}</div>;
49
+ }
@@ -0,0 +1,54 @@
1
+ import { forwardRef } from "react";
2
+ import type { HTMLAttributes, ReactNode } from "react";
3
+ import { cx } from "../utils";
4
+
5
+ export interface StepperStep {
6
+ label: ReactNode;
7
+ /** Small text under the label, e.g. `2/4`. */
8
+ meta?: ReactNode;
9
+ }
10
+
11
+ export interface StepperProps extends Omit<HTMLAttributes<HTMLOListElement>, "onChange"> {
12
+ steps: StepperStep[];
13
+ /** Index of the active step (0-based). Earlier steps are done, later ones disabled. */
14
+ current: number;
15
+ horizontal?: boolean;
16
+ /** Called when a done step is clicked. */
17
+ onStepChange?: (index: number) => void;
18
+ }
19
+
20
+ /** `<ol class="oc-stepper">`: done, active and upcoming steps. */
21
+ export const Stepper = forwardRef<HTMLOListElement, StepperProps>(function Stepper(
22
+ { steps, current, horizontal, onStepChange, className, ...rest },
23
+ ref,
24
+ ) {
25
+ return (
26
+ <ol ref={ref} className={cx("oc-stepper", horizontal && "oc-stepper--horizontal", className)} {...rest}>
27
+ {steps.map((step, i) => {
28
+ const done = i < current;
29
+ const active = i === current;
30
+ const label = <span className="oc-stepper-label">{step.label}</span>;
31
+ return (
32
+ <li key={i}>
33
+ <button
34
+ className={cx("oc-stepper-item", done && "oc-stepper-item--done", active && "oc-stepper-item--active")}
35
+ type="button"
36
+ aria-current={active ? "step" : undefined}
37
+ disabled={i > current}
38
+ onClick={() => onStepChange?.(i)}
39
+ >
40
+ {step.meta != null ? (
41
+ <span>
42
+ {label}
43
+ <span className="oc-stepper-meta">{step.meta}</span>
44
+ </span>
45
+ ) : (
46
+ label
47
+ )}
48
+ </button>
49
+ </li>
50
+ );
51
+ })}
52
+ </ol>
53
+ );
54
+ });
@@ -0,0 +1,18 @@
1
+ import { forwardRef } from "react";
2
+ import type { InputHTMLAttributes } from "react";
3
+ import { cx } from "../utils";
4
+
5
+ export interface SwitchProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "role"> {
6
+ /** Class for the wrapping label (`className` goes to the input). */
7
+ labelClassName?: string;
8
+ }
9
+
10
+ /** `<label class="oc-switch"><input type="checkbox" role="switch">`. Works controlled (`checked`) or uncontrolled. */
11
+ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(function Switch({ labelClassName, className, ...rest }, ref) {
12
+ return (
13
+ <label className={cx("oc-switch", labelClassName)}>
14
+ <input ref={ref} type="checkbox" role="switch" className={className} {...rest} />
15
+ <span className="oc-switch-track" />
16
+ </label>
17
+ );
18
+ });