@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,27 @@
1
+ import { forwardRef } from "react";
2
+ import type { HTMLAttributes, ReactNode } from "react";
3
+ import { cx } from "../utils";
4
+
5
+ export interface HeadingProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
6
+ title: ReactNode;
7
+ description?: ReactNode;
8
+ /** Heading element level (default 2). */
9
+ level?: 1 | 2 | 3 | 4;
10
+ /** `sm` = section heading. */
11
+ size?: "md" | "sm";
12
+ titleId?: string;
13
+ }
14
+
15
+ /** `<div class="oc-heading">` title + muted description. */
16
+ export const Heading = forwardRef<HTMLDivElement, HeadingProps>(function Heading(
17
+ { title, description, level = 2, size = "md", titleId, className, ...rest },
18
+ ref,
19
+ ) {
20
+ const Tag = `h${level}` as "h2";
21
+ return (
22
+ <div ref={ref} className={cx("oc-heading", size === "sm" && "oc-heading--sm", className)} {...rest}>
23
+ <Tag className="oc-heading-title" id={titleId}>{title}</Tag>
24
+ {description != null && <p className="oc-heading-description">{description}</p>}
25
+ </div>
26
+ );
27
+ });
@@ -0,0 +1,25 @@
1
+ import type { SVGProps } from "react";
2
+
3
+ export interface IconProps extends Omit<SVGProps<SVGSVGElement>, "d"> {
4
+ /** SVG path data (24x24 viewBox, stroke icon). Use `children` for several shapes. */
5
+ d?: string;
6
+ }
7
+
8
+ /** Inline 24x24 stroke icon (`currentColor`), the shape used by the snippets. */
9
+ export function Icon({ d, children, className, ...rest }: IconProps) {
10
+ return (
11
+ <svg
12
+ className={className}
13
+ viewBox="0 0 24 24"
14
+ fill="none"
15
+ stroke="currentColor"
16
+ strokeWidth={2}
17
+ strokeLinecap="round"
18
+ strokeLinejoin="round"
19
+ aria-hidden="true"
20
+ {...rest}
21
+ >
22
+ {d ? <path d={d} /> : children}
23
+ </svg>
24
+ );
25
+ }
@@ -0,0 +1,51 @@
1
+ // File: packages/react/src/components/ImageZoom.tsx
2
+ // Map: props -> ImageZoom (toolbar of radio levels -> viewport -> canvas with the image)
3
+ // Same markup as packages/ui/snippets/image-zoom.html. The chosen level is also written to --oc-zoom,
4
+ // so any number works (the CSS-only :has() rules cover 1, 1.5, 2, 3 and 4).
5
+ "use client";
6
+
7
+ import { forwardRef, useId, useState } from "react";
8
+ import type { CSSProperties, HTMLAttributes, ReactNode } from "react";
9
+ import { cx } from "../utils";
10
+
11
+ export interface ImageZoomProps extends HTMLAttributes<HTMLDivElement> {
12
+ src?: string;
13
+ alt?: string;
14
+ /** Zoom levels for the toolbar. Empty array = no toolbar. Default [1, 1.5, 2, 3]. */
15
+ levels?: number[];
16
+ /** Starting level. Default 1. */
17
+ defaultZoom?: number;
18
+ /** Window ratio: "square", "photo" or a CSS ratio such as "21 / 9". Default 16 / 9. */
19
+ ratio?: "square" | "photo" | string;
20
+ /** Replaces the <img> inside the canvas (svg, video, placeholder art). */
21
+ media?: ReactNode;
22
+ }
23
+
24
+ /** `.oc-image-zoom`: zoom by making the canvas wider than the viewport; pan by scrolling. */
25
+ export const ImageZoom = forwardRef<HTMLDivElement, ImageZoomProps>(function ImageZoom(
26
+ { src, alt = "", levels = [1, 1.5, 2, 3], defaultZoom = 1, ratio, media, className, style, ...rest },
27
+ ref,
28
+ ) {
29
+ const name = `oc-zoom-${useId().replace(/:/g, "")}`;
30
+ const [zoom, setZoom] = useState(defaultZoom);
31
+ const preset = ratio === "square" || ratio === "photo" ? ratio : undefined;
32
+ const vars = { "--oc-zoom": String(zoom), ...(ratio && !preset ? { "--oc-image-zoom-ratio": ratio } : {}) } as CSSProperties;
33
+ return (
34
+ <div ref={ref} className={cx("oc-image-zoom", preset && `oc-image-zoom--${preset}`, className)} style={{ ...vars, ...style }} {...rest}>
35
+ {levels.length > 0 && (
36
+ <div className="oc-image-zoom__toolbar" role="radiogroup" aria-label="Zoom level">
37
+ <span className="oc-image-zoom__label">Zoom</span>
38
+ {levels.map((l) => (
39
+ <label key={l} className="oc-image-zoom__level">
40
+ <input type="radio" name={name} value={String(l)} checked={zoom === l} onChange={() => setZoom(l)} />
41
+ {Math.round(l * 100)}%
42
+ </label>
43
+ ))}
44
+ </div>
45
+ )}
46
+ <div className="oc-image-zoom__viewport" tabIndex={0} role="region" aria-label={alt ? `${alt}, scroll to pan` : "Scroll to pan"}>
47
+ <div className="oc-image-zoom__canvas">{media ?? (src ? <img src={src} alt={alt} /> : <div className="oc-media-art" role="img" aria-label={alt} />)}</div>
48
+ </div>
49
+ </div>
50
+ );
51
+ });
@@ -0,0 +1,21 @@
1
+ import { forwardRef } from "react";
2
+ import type { InputHTMLAttributes } from "react";
3
+ import { cx } from "../utils";
4
+
5
+ export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
6
+ /** Error state: adds `oc-input--error` and `aria-invalid`. */
7
+ error?: boolean;
8
+ }
9
+
10
+ /** `<input class="oc-input">` */
11
+ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({ error, className, type = "text", ...rest }, ref) {
12
+ return (
13
+ <input
14
+ ref={ref}
15
+ type={type}
16
+ className={cx("oc-input", error && "oc-input--error", className)}
17
+ aria-invalid={error ? true : rest["aria-invalid"]}
18
+ {...rest}
19
+ />
20
+ );
21
+ });
@@ -0,0 +1,24 @@
1
+ import { forwardRef } from "react";
2
+ import type { HTMLAttributes, ReactNode } from "react";
3
+ import { cx } from "../utils";
4
+
5
+ export interface InputGroupProps extends Omit<HTMLAttributes<HTMLDivElement>, "prefix"> {
6
+ /** Addon before the input (currency sign...). */
7
+ prefix?: ReactNode;
8
+ /** Addon after the input (%, unit...). */
9
+ suffix?: ReactNode;
10
+ }
11
+
12
+ /** `<div class="oc-input-group">` with `oc-input-addon` spans around the child `<Input>`. */
13
+ export const InputGroup = forwardRef<HTMLDivElement, InputGroupProps>(function InputGroup(
14
+ { prefix, suffix, className, children, ...rest },
15
+ ref,
16
+ ) {
17
+ return (
18
+ <div ref={ref} className={cx("oc-input-group", className)} {...rest}>
19
+ {prefix != null && <span className="oc-input-addon">{prefix}</span>}
20
+ {children}
21
+ {suffix != null && <span className="oc-input-addon">{suffix}</span>}
22
+ </div>
23
+ );
24
+ });
@@ -0,0 +1,64 @@
1
+ // File: packages/react/src/components/LanguageSelect.tsx
2
+ // Map: types -> globe icon -> component (label pill: icon + native select)
3
+ // Same markup as packages/ui/snippets/language-select.html. The app decides what "change" does.
4
+ "use client";
5
+
6
+ import { forwardRef } from "react";
7
+ import type { ReactNode, SelectHTMLAttributes } from "react";
8
+ import { cx } from "../utils";
9
+ import { useControllable } from "../hooks";
10
+
11
+ export interface LanguageSelectOption {
12
+ value: string;
13
+ label: string;
14
+ }
15
+
16
+ export interface LanguageSelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "value" | "defaultValue" | "onChange"> {
17
+ options: LanguageSelectOption[];
18
+ value?: string;
19
+ defaultValue?: string;
20
+ onChange?: (value: string) => void;
21
+ /** Currency look (symbol in primary color). */
22
+ currency?: boolean;
23
+ /** No frame until hover / focus. */
24
+ ghost?: boolean;
25
+ /** Replaces the globe icon (e.g. "€"). */
26
+ icon?: ReactNode;
27
+ /** Class for the wrapping label (`className` goes to the select). */
28
+ labelClassName?: string;
29
+ }
30
+
31
+ /** Small globe drawn with currentColor. */
32
+ const Globe = () => (
33
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
34
+ <circle cx="12" cy="12" r="9" />
35
+ <path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" />
36
+ </svg>
37
+ );
38
+
39
+ /** `.oc-language-select`: native select in a pill with a leading icon. */
40
+ export const LanguageSelect = forwardRef<HTMLSelectElement, LanguageSelectProps>(function LanguageSelect(
41
+ { options, value, defaultValue, onChange, currency, ghost, icon, labelClassName, className, ...rest },
42
+ ref,
43
+ ) {
44
+ const [current, setCurrent] = useControllable<string>(value, defaultValue ?? options[0]?.value ?? "", onChange);
45
+ return (
46
+ <label className={cx("oc-language-select", ghost && "oc-language-select--ghost", currency && "oc-language-select--currency", labelClassName)}>
47
+ <span className="oc-language-select-icon" aria-hidden="true">{icon ?? (currency ? "\u20AC" : <Globe />)}</span>
48
+ <select
49
+ ref={ref}
50
+ className={cx("oc-language-select-control", className)}
51
+ aria-label={currency ? "Currency" : "Language"}
52
+ value={current}
53
+ onChange={(e) => setCurrent(e.target.value)}
54
+ {...rest}
55
+ >
56
+ {options.map((o) => (
57
+ <option key={o.value} value={o.value}>
58
+ {o.label}
59
+ </option>
60
+ ))}
61
+ </select>
62
+ </label>
63
+ );
64
+ });
@@ -0,0 +1,50 @@
1
+ import { forwardRef } from "react";
2
+ import type { AnchorHTMLAttributes, ReactNode } from "react";
3
+ import { cx } from "../utils";
4
+
5
+ export interface LauncherCardProps extends Omit<AnchorHTMLAttributes<HTMLElement>, "title"> {
6
+ /** Tinted icon tile content (inline SVG). */
7
+ icon?: ReactNode;
8
+ title: ReactNode;
9
+ /** Trailing badge inside the title. */
10
+ badge?: ReactNode;
11
+ description?: ReactNode;
12
+ /** Footer text, e.g. why the card is disabled. */
13
+ footer?: ReactNode;
14
+ /** Current card (`aria-current="true"`). */
15
+ active?: boolean;
16
+ /** Not clickable (`aria-disabled`); renders a <span>. */
17
+ disabled?: boolean;
18
+ }
19
+
20
+ /** `.oc-launcher`: an `<a>` (with `href`), a `<button>` (with `onClick`), or a `<span>` when disabled. */
21
+ export const LauncherCard = forwardRef<HTMLElement, LauncherCardProps>(function LauncherCard(
22
+ { icon, title, badge, description, footer, active, disabled, className, href, onClick, ...rest },
23
+ ref,
24
+ ) {
25
+ const content = (
26
+ <>
27
+ {icon != null && <span className="oc-launcher-icon" aria-hidden="true">{icon}</span>}
28
+ <h3 className="oc-launcher-title">{title}{badge != null && <> {badge}</>}</h3>
29
+ {description != null && <p className="oc-launcher-description">{description}</p>}
30
+ {footer != null && <span className="oc-launcher-footer">{footer}</span>}
31
+ </>
32
+ );
33
+ const cls = cx("oc-launcher", className);
34
+ const common = { className: cls, "aria-current": active ? ("true" as const) : undefined };
35
+ if (disabled) {
36
+ return <span ref={ref} {...common} aria-disabled="true">{content}</span>;
37
+ }
38
+ if (href === undefined && onClick) {
39
+ return (
40
+ <button ref={ref as React.Ref<HTMLButtonElement>} type="button" {...common} onClick={onClick as never}>
41
+ {content}
42
+ </button>
43
+ );
44
+ }
45
+ return (
46
+ <a ref={ref as React.Ref<HTMLAnchorElement>} href={href} {...common} onClick={onClick} {...rest}>
47
+ {content}
48
+ </a>
49
+ );
50
+ });
@@ -0,0 +1,83 @@
1
+ // File: packages/react/src/components/Lightbox.tsx
2
+ // Map: types -> Lightbox (thumbnails + <dialog> with a snap track of slides) -> jump-to-slide effect
3
+ // Same markup as packages/ui/snippets/lightbox.html. Prev / next stay plain anchor links (no JS needed);
4
+ // React only opens the dialog and scrolls to the clicked photo.
5
+ "use client";
6
+
7
+ import { forwardRef, useEffect, useId, useRef, useState } from "react";
8
+ import type { DialogHTMLAttributes, ReactNode } from "react";
9
+ import { attrs, cx } from "../utils";
10
+ import { useDialog, useMergedRef } from "../hooks";
11
+ import type { DialogState } from "../hooks";
12
+
13
+ export interface LightboxImage {
14
+ /** Image URL. Leave empty to show placeholder art (`.oc-media-art`). */
15
+ src?: string;
16
+ alt: string;
17
+ caption?: ReactNode;
18
+ /** Smaller image for the thumbnail (defaults to `src`). */
19
+ thumb?: string;
20
+ }
21
+
22
+ export interface LightboxProps extends Omit<DialogHTMLAttributes<HTMLDialogElement>, "title" | "open" | "onClose">, DialogState {
23
+ images: LightboxImage[];
24
+ /** Text in the top bar (`oc-lightbox__title`). */
25
+ title?: ReactNode;
26
+ /** Render the thumbnail grid that opens the viewer. Default true. */
27
+ thumbnails?: boolean;
28
+ }
29
+
30
+ const media = (img: LightboxImage, src?: string) =>
31
+ src ? <img src={src} alt={img.alt} /> : <span className="oc-media-art" role="img" aria-label={img.alt} />;
32
+
33
+ /** `.oc-lightbox-thumbs` + `<dialog class="oc-lightbox">`. Controlled with `open` / `onOpenChange`, or uncontrolled. */
34
+ export const Lightbox = forwardRef<HTMLDialogElement, LightboxProps>(function Lightbox(
35
+ { images, title, thumbnails = true, open, defaultOpen, onOpenChange, className, "aria-label": label = "Photo viewer", ...rest },
36
+ forwardedRef,
37
+ ) {
38
+ const inner = useRef<HTMLDialogElement>(null);
39
+ const ref = useMergedRef(inner, forwardedRef);
40
+ const base = `oc-lightbox-${useId().replace(/:/g, "")}`;
41
+ const [start, setStart] = useState(0);
42
+ const dlg = useDialog(inner, { open, defaultOpen, onOpenChange });
43
+ const single = images.length === 1;
44
+ const slideId = (i: number) => `${base}-${((i % images.length) + images.length) % images.length + 1}`;
45
+
46
+ // After opening: jump (without animation) to the photo that was clicked.
47
+ useEffect(() => {
48
+ if (dlg.open) document.getElementById(slideId(start))?.scrollIntoView?.({ behavior: "instant" as ScrollBehavior, block: "nearest", inline: "center" });
49
+ // eslint-disable-next-line react-hooks/exhaustive-deps
50
+ }, [dlg.open, start]);
51
+
52
+ return (
53
+ <>
54
+ {thumbnails && (
55
+ <div className="oc-lightbox-thumbs">
56
+ {images.map((img, i) => (
57
+ <button key={i} type="button" className="oc-lightbox-thumb" aria-label={`Open photo ${i + 1}: ${img.alt}`} onClick={() => { setStart(i); dlg.setOpen(true); }}>
58
+ {media(img, img.thumb ?? img.src)}
59
+ </button>
60
+ ))}
61
+ </div>
62
+ )}
63
+ <dialog ref={ref} className={cx("oc-lightbox", single && "oc-lightbox--single", className)} aria-label={label} {...attrs({ closedby: "any" })} {...rest} onClose={dlg.onClose}>
64
+ <div className="oc-lightbox__bar">
65
+ {title != null ? <h2 className="oc-lightbox__title">{title}</h2> : <span />}
66
+ <button type="button" className="oc-lightbox__close" aria-label="Close" onClick={dlg.close}>&#10005;</button>
67
+ </div>
68
+ <div className="oc-lightbox__track" tabIndex={0} aria-label="Photos, use the arrow keys">
69
+ {images.map((img, i) => (
70
+ <figure key={i} className="oc-lightbox__slide" id={slideId(i)}>
71
+ <div className="oc-lightbox__media">{media(img, img.src)}</div>
72
+ {!single && <a className="oc-lightbox__nav oc-lightbox__nav--prev" href={`#${slideId(i - 1)}`} aria-label="Previous photo">&#8592;</a>}
73
+ {!single && <a className="oc-lightbox__nav oc-lightbox__nav--next" href={`#${slideId(i + 1)}`} aria-label="Next photo">&#8594;</a>}
74
+ <figcaption className="oc-lightbox__caption">
75
+ {!single && <span className="oc-lightbox__counter">{i + 1} / {images.length}</span>} {img.caption ?? img.alt}
76
+ </figcaption>
77
+ </figure>
78
+ ))}
79
+ </div>
80
+ </dialog>
81
+ </>
82
+ );
83
+ });
@@ -0,0 +1,27 @@
1
+ // File: packages/react/src/components/LinkArrow.tsx | Map: types -> LinkArrow (a.oc-link-arrow + modifiers)
2
+ import { forwardRef } from "react";
3
+ import type { AnchorHTMLAttributes } from "react";
4
+ import { cx } from "../utils";
5
+
6
+ export type LinkArrowDirection = "forward" | "back" | "external";
7
+
8
+ export interface LinkArrowProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
9
+ direction?: LinkArrowDirection;
10
+ tone?: "default" | "accent" | "muted";
11
+ size?: "sm" | "default" | "lg";
12
+ }
13
+
14
+ /** `<a class="oc-link-arrow">`: text link with an arrow that moves on hover and focus. */
15
+ export const LinkArrow = forwardRef<HTMLAnchorElement, LinkArrowProps>(function LinkArrow(
16
+ { direction = "forward", tone = "default", size = "default", className, ...rest },
17
+ ref,
18
+ ) {
19
+ const classes = cx(
20
+ "oc-link-arrow",
21
+ direction !== "forward" && `oc-link-arrow--${direction}`,
22
+ tone !== "default" && `oc-link-arrow--${tone}`,
23
+ size !== "default" && `oc-link-arrow--${size}`,
24
+ className,
25
+ );
26
+ return <a ref={ref} className={classes} {...rest} />;
27
+ });
@@ -0,0 +1,67 @@
1
+ // File: packages/react/src/components/ListToolbar.tsx
2
+ // Map: ListToolbar = toolbar row -> search -> count -> spacer -> view radios -> actions
3
+ // The view toggle is a radio group. In React you usually render only the active view yourself
4
+ // (use `view` + `onViewChange`); the CSS-only `.oc-list-toolbar-scope` switch also works.
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 ListToolbarView {
13
+ /** "cards", "table" or "list" (matches `.oc-list-toolbar-panel[data-view]`). */
14
+ value: string;
15
+ label: string;
16
+ icon: ReactNode;
17
+ }
18
+
19
+ export interface ListToolbarProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
20
+ /** Search area content, e.g. `<input class="oc-input" type="search">`. */
21
+ search?: ReactNode;
22
+ /** Result count text, e.g. "3 items". */
23
+ count?: ReactNode;
24
+ views?: ListToolbarView[];
25
+ view?: string;
26
+ defaultView?: string;
27
+ onViewChange?: (view: string) => void;
28
+ viewsLabel?: string;
29
+ actions?: ReactNode;
30
+ sticky?: boolean;
31
+ bordered?: boolean;
32
+ }
33
+
34
+ /** `.oc-list-toolbar`: search, count, view toggle and actions above a list. */
35
+ export const ListToolbar = forwardRef<HTMLDivElement, ListToolbarProps>(function ListToolbar(
36
+ { search, count, views, view, defaultView, onViewChange, viewsLabel = "View", actions, sticky, bordered, className, ...rest },
37
+ ref,
38
+ ) {
39
+ const name = `oc-list-view-${useId().replace(/:/g, "")}`;
40
+ const [current, setCurrent] = useControllable(view, defaultView ?? views?.[0]?.value ?? "", onViewChange);
41
+ const cls = cx("oc-list-toolbar", sticky && "oc-list-toolbar--sticky", bordered && "oc-list-toolbar--bordered", className);
42
+ return (
43
+ <div ref={ref} className={cls} role="toolbar" {...rest}>
44
+ {search != null && <div className="oc-list-toolbar-search">{search}</div>}
45
+ {count != null && <span className="oc-list-toolbar-count">{count}</span>}
46
+ <span className="oc-list-toolbar-spacer" />
47
+ {views && views.length > 0 && (
48
+ <div className="oc-list-toolbar-views" role="radiogroup" aria-label={viewsLabel}>
49
+ {views.map((v) => (
50
+ <label className="oc-list-toolbar-view" title={v.label} key={v.value}>
51
+ <input
52
+ type="radio"
53
+ name={name}
54
+ value={v.value}
55
+ aria-label={v.label}
56
+ checked={current === v.value}
57
+ onChange={() => setCurrent(v.value)}
58
+ />
59
+ {v.icon}
60
+ </label>
61
+ ))}
62
+ </div>
63
+ )}
64
+ {actions != null && <div className="oc-list-toolbar-actions">{actions}</div>}
65
+ </div>
66
+ );
67
+ });
@@ -0,0 +1,38 @@
1
+ // File: packages/react/src/components/Marquee.tsx | Map: props -> Marquee (track with items + aria-hidden copy)
2
+ import { forwardRef } from "react";
3
+ import type { HTMLAttributes, ReactNode } from "react";
4
+ import { cx } from "../utils";
5
+
6
+ export interface MarqueeProps extends HTMLAttributes<HTMLDivElement> {
7
+ /** Items of the strip. They are rendered twice; the copy is aria-hidden. */
8
+ items: ReactNode[];
9
+ reverse?: boolean;
10
+ speed?: "default" | "slow" | "fast";
11
+ /** Text only, no pills. */
12
+ plain?: boolean;
13
+ /** No soft fade at the edges. */
14
+ noFade?: boolean;
15
+ }
16
+
17
+ /** `.oc-marquee`: endless strip that pauses on hover / focus and stands still with reduced motion. */
18
+ export const Marquee = forwardRef<HTMLDivElement, MarqueeProps>(function Marquee(
19
+ { items, reverse = false, speed = "default", plain = false, noFade = false, className, ...rest },
20
+ ref,
21
+ ) {
22
+ const classes = cx(
23
+ "oc-marquee",
24
+ reverse && "oc-marquee--reverse",
25
+ speed !== "default" && `oc-marquee--${speed}`,
26
+ plain && "oc-marquee--plain",
27
+ noFade && "oc-marquee--no-fade",
28
+ className,
29
+ );
30
+ return (
31
+ <div ref={ref} className={classes} {...rest}>
32
+ <div className="oc-marquee__track">
33
+ {items.map((item, i) => <span className="oc-marquee__item" key={i}>{item}</span>)}
34
+ {items.map((item, i) => <span className="oc-marquee__item" aria-hidden="true" key={`copy-${i}`}>{item}</span>)}
35
+ </div>
36
+ </div>
37
+ );
38
+ });
@@ -0,0 +1,47 @@
1
+ // File: packages/react/src/components/MediaCard.tsx
2
+ // Map: props -> MediaCard (MediaThumb -> body: avatar, title link + meta, menu button -> popover menu)
3
+ // Same markup as packages/ui/snippets/media-card.html.
4
+ import { forwardRef, useId } from "react";
5
+ import type { HTMLAttributes, ReactNode } from "react";
6
+ import { attrs, cx } from "../utils";
7
+ import { MediaThumb } from "./MediaThumb";
8
+ import type { MediaThumbProps } from "./MediaThumb";
9
+
10
+ export interface MediaCardProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
11
+ title: ReactNode;
12
+ /** Link of the title; it covers the whole card. */
13
+ href: string;
14
+ /** "Channel · 24K views · 3 days ago". */
15
+ meta?: ReactNode;
16
+ /** Initials, an <img>, or any node. Leave out for no avatar. */
17
+ avatar?: ReactNode;
18
+ /** Props for the MediaThumb on top (src, duration, progress, …). */
19
+ thumb?: MediaThumbProps;
20
+ /** Items of the ⋮ menu (`.oc-dropdown-item` elements). No menu button when empty. */
21
+ menu?: ReactNode;
22
+ compact?: boolean;
23
+ }
24
+
25
+ /** `article.oc-media-card`. */
26
+ export const MediaCard = forwardRef<HTMLElement, MediaCardProps>(function MediaCard(
27
+ { title, href, meta, avatar, thumb, menu, compact, className, ...rest },
28
+ ref,
29
+ ) {
30
+ const menuId = `oc-media-card-menu-${useId().replace(/:/g, "")}`;
31
+ return (
32
+ <article ref={ref} className={cx("oc-media-card", compact && "oc-media-card--compact", className)} {...rest}>
33
+ <MediaThumb {...thumb} />
34
+ <div className="oc-media-card__body">
35
+ {avatar != null && <span className="oc-media-card__avatar" aria-hidden="true">{avatar}</span>}
36
+ <div className="oc-media-card__text">
37
+ <h3 className="oc-media-card__title"><a href={href}>{title}</a></h3>
38
+ {meta != null && <p className="oc-media-card__meta">{meta}</p>}
39
+ </div>
40
+ {menu != null && (
41
+ <button type="button" className="oc-media-card__menu" aria-label="More actions" {...attrs({ popovertarget: menuId })}>&#8942;</button>
42
+ )}
43
+ </div>
44
+ {menu != null && <div className="oc-dropdown-menu" id={menuId} {...attrs({ popover: "" })}>{menu}</div>}
45
+ </article>
46
+ );
47
+ });
@@ -0,0 +1,53 @@
1
+ // File: packages/react/src/components/MediaRow.tsx
2
+ // Map: MediaRowList (ul / ol, numbered) -> MediaRow (MediaThumb -> title link + meta -> menu button + popover)
3
+ // Same markup as packages/ui/snippets/media-row.html.
4
+ import { forwardRef, useId } from "react";
5
+ import type { HTMLAttributes, ReactNode } from "react";
6
+ import { attrs, cx } from "../utils";
7
+ import { MediaThumb } from "./MediaThumb";
8
+ import type { MediaThumbProps } from "./MediaThumb";
9
+
10
+ export interface MediaRowListProps extends HTMLAttributes<HTMLElement> {
11
+ /** Numbered playlist: renders an <ol> with a counter column. */
12
+ numbered?: boolean;
13
+ }
14
+
15
+ /** `ul.oc-media-row-list` or `ol.oc-media-row-list--numbered`. */
16
+ export const MediaRowList = forwardRef<HTMLElement, MediaRowListProps>(function MediaRowList({ numbered, className, ...rest }, ref) {
17
+ const Tag = (numbered ? "ol" : "ul") as "ul";
18
+ return <Tag ref={ref as React.Ref<HTMLUListElement>} className={cx("oc-media-row-list", numbered && "oc-media-row-list--numbered", className)} {...(rest as HTMLAttributes<HTMLUListElement>)} />;
19
+ });
20
+
21
+ export interface MediaRowProps extends Omit<HTMLAttributes<HTMLLIElement>, "title"> {
22
+ title: ReactNode;
23
+ href: string;
24
+ meta?: ReactNode;
25
+ thumb?: MediaThumbProps;
26
+ /** Now playing (`aria-current="true"`). */
27
+ current?: boolean;
28
+ /** Items of the ⋮ menu. No menu button when empty. */
29
+ menu?: ReactNode;
30
+ }
31
+
32
+ /** `li.oc-media-row`: thumb | title + meta | menu. */
33
+ export const MediaRow = forwardRef<HTMLLIElement, MediaRowProps>(function MediaRow(
34
+ { title, href, meta, thumb, current, menu, className, ...rest },
35
+ ref,
36
+ ) {
37
+ const menuId = `oc-media-row-menu-${useId().replace(/:/g, "")}`;
38
+ return (
39
+ <li ref={ref} className={cx("oc-media-row", className)} aria-current={current ? "true" : undefined} {...rest}>
40
+ <MediaThumb {...thumb} />
41
+ <div className="oc-media-row__text">
42
+ <h3 className="oc-media-row__title"><a href={href}>{title}</a></h3>
43
+ {meta != null && <p className="oc-media-row__meta">{meta}</p>}
44
+ </div>
45
+ {menu != null && (
46
+ <>
47
+ <button type="button" className="oc-media-row__menu" aria-label="More actions" {...attrs({ popovertarget: menuId })}>&#8942;</button>
48
+ <div className="oc-dropdown-menu" id={menuId} {...attrs({ popover: "" })}>{menu}</div>
49
+ </>
50
+ )}
51
+ </li>
52
+ );
53
+ });
@@ -0,0 +1,49 @@
1
+ // File: packages/react/src/components/MediaThumb.tsx
2
+ // Map: props -> MediaThumb (<a> or <div> -> picture -> badge -> duration -> progress)
3
+ // Same markup as packages/ui/snippets/media-thumb.html.
4
+ import { forwardRef } from "react";
5
+ import type { HTMLAttributes, ReactNode } from "react";
6
+ import { cx } from "../utils";
7
+
8
+ export interface MediaThumbProps extends HTMLAttributes<HTMLElement> {
9
+ /** Picture URL. Empty = placeholder art (`.oc-media-art`, pick a variant with `art`). */
10
+ src?: string;
11
+ alt?: string;
12
+ /** Placeholder art variant 1-6 (only without `src`). */
13
+ art?: number;
14
+ /** Renders an <a> when set. */
15
+ href?: string;
16
+ /** "12:04" in the bottom-right corner. */
17
+ duration?: ReactNode;
18
+ /** Top-left label ("New", "LIVE"). */
19
+ badge?: ReactNode;
20
+ live?: boolean;
21
+ /** Watched part, 0-100. Shows the progress bar when set. */
22
+ progress?: number;
23
+ watched?: boolean;
24
+ ratio?: "video" | "square" | "portrait";
25
+ }
26
+
27
+ /** `.oc-media-thumb`: 16:9 video thumbnail. */
28
+ export const MediaThumb = forwardRef<HTMLElement, MediaThumbProps>(function MediaThumb(
29
+ { src, alt = "", art, href, duration, badge, live, progress, watched, ratio = "video", className, children, ...rest },
30
+ ref,
31
+ ) {
32
+ const Tag = (href ? "a" : "div") as "a";
33
+ return (
34
+ <Tag
35
+ ref={ref as React.Ref<HTMLAnchorElement>}
36
+ href={href}
37
+ className={cx("oc-media-thumb", ratio !== "video" && `oc-media-thumb--${ratio}`, watched && "is-watched", className)}
38
+ {...(rest as HTMLAttributes<HTMLAnchorElement>)}
39
+ >
40
+ {src ? <img src={src} alt={alt} /> : <div className={cx("oc-media-art", art != null && art > 1 && `oc-media-art--${art}`)} />}
41
+ {badge != null && <span className={cx("oc-media-thumb__badge", live && "oc-media-thumb__badge--live")}>{badge}</span>}
42
+ {duration != null && <span className="oc-media-thumb__duration">{duration}</span>}
43
+ {progress != null && (
44
+ <progress className="oc-media-thumb__progress" value={progress} max={100} aria-label={`Watched ${progress}%`}>{progress}%</progress>
45
+ )}
46
+ {children}
47
+ </Tag>
48
+ );
49
+ });