@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,63 @@
1
+ // File: packages/react/src/components/AlphaTabs.tsx
2
+ // Map: AlphaTabs = nav -> "All" item -> one item per letter (current / disabled)
3
+ // With `href` the items are links (server filtering); without it they are buttons that call onChange.
4
+ "use client";
5
+
6
+ import { forwardRef } from "react";
7
+ import type { HTMLAttributes } from "react";
8
+ import { cx } from "../utils";
9
+ import { useControllable } from "../hooks";
10
+
11
+ const AZ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
12
+
13
+ export interface AlphaTabsProps extends Omit<HTMLAttributes<HTMLElement>, "onChange"> {
14
+ /** Letters to show (default A-Z). */
15
+ letters?: string[];
16
+ /** Letters that have entries; the others are disabled. Default: all letters. */
17
+ available?: string[];
18
+ /** Current letter, or "all". */
19
+ value?: string;
20
+ defaultValue?: string;
21
+ onChange?: (letter: string) => void;
22
+ /** Build a link per letter, e.g. `(l) => "?letter=" + l`. */
23
+ href?: (letter: string) => string;
24
+ allLabel?: string;
25
+ wrap?: boolean;
26
+ size?: "sm";
27
+ plain?: boolean;
28
+ }
29
+
30
+ /** `nav.oc-alpha-tabs`: All + A-Z letter filter. */
31
+ export const AlphaTabs = forwardRef<HTMLElement, AlphaTabsProps>(function AlphaTabs(
32
+ { letters = AZ, available, value, defaultValue = "all", onChange, href, allLabel = "All", wrap, size, plain, className, ...rest },
33
+ ref,
34
+ ) {
35
+ const [current, setCurrent] = useControllable(value, defaultValue, onChange);
36
+ const cls = cx("oc-alpha-tabs", wrap && "oc-alpha-tabs--wrap", size && `oc-alpha-tabs--${size}`, plain && "oc-alpha-tabs--plain", className);
37
+
38
+ const item = (key: string, label: string, extra?: string) => {
39
+ const isCurrent = current === key;
40
+ const disabled = key !== "all" && available !== undefined && !available.includes(key);
41
+ const itemCls = cx("oc-alpha-tabs-item", extra);
42
+ if (href) {
43
+ // A disabled link has no href, so it cannot be followed.
44
+ return (
45
+ <a key={key} className={itemCls} href={disabled ? undefined : href(key)} aria-current={isCurrent ? "true" : undefined} aria-disabled={disabled ? "true" : undefined}>
46
+ {label}
47
+ </a>
48
+ );
49
+ }
50
+ return (
51
+ <button key={key} type="button" className={itemCls} aria-current={isCurrent ? "true" : undefined} disabled={disabled} onClick={() => setCurrent(key)}>
52
+ {label}
53
+ </button>
54
+ );
55
+ };
56
+
57
+ return (
58
+ <nav ref={ref} className={cls} aria-label="Filter by letter" {...rest}>
59
+ {item("all", allLabel, "oc-alpha-tabs-item--all")}
60
+ {letters.map((l) => item(l, l))}
61
+ </nav>
62
+ );
63
+ });
@@ -0,0 +1,140 @@
1
+ // File: packages/react/src/components/Attachment.tsx
2
+ // Map: types + type / extension helpers -> Attachment (file chip) -> AttachmentImage -> AttachmentThumb -> AttachmentList (demo files)
3
+ // Same markup as packages/ui/snippets/attachment.html. No JavaScript: onRemove is yours, the upload progress is a custom property.
4
+ import { forwardRef } from "react";
5
+ import type { CSSProperties, HTMLAttributes, ReactNode } from "react";
6
+ import { cx } from "../utils";
7
+
8
+ export type AttachmentType = "pdf" | "doc" | "sheet" | "zip" | "image";
9
+
10
+ export interface AttachmentFile {
11
+ name: string;
12
+ /** "1.2 MB", or the error text when `state` is "error". */
13
+ size?: ReactNode;
14
+ /** Icon tint. Derived from the extension when empty. */
15
+ type?: AttachmentType;
16
+ /** Text in the icon tile (default: the extension, e.g. "PDF", "XLS"). */
17
+ ext?: string;
18
+ /** Download link (the chip becomes an `<a download>` unless it is removable). */
19
+ href?: string;
20
+ /** Small preview image in the icon tile. */
21
+ image?: string;
22
+ state?: "uploading" | "error";
23
+ /** Upload progress in % (shown while uploading). */
24
+ progress?: number;
25
+ /** Show the remove button (composer). */
26
+ removable?: boolean;
27
+ removeLabel?: string;
28
+ }
29
+
30
+ const TYPES: Record<string, AttachmentType> = {
31
+ pdf: "pdf", doc: "doc", docx: "doc", odt: "doc", rtf: "doc", txt: "doc", xls: "sheet", xlsx: "sheet", csv: "sheet", ods: "sheet",
32
+ zip: "zip", rar: "zip", "7z": "zip", gz: "zip", jpg: "image", jpeg: "image", png: "image", gif: "image", webp: "image", heic: "image", avif: "image",
33
+ };
34
+ const extOf = (name: string) => (name.includes(".") ? name.split(".").pop()!.toLowerCase() : "");
35
+ /** Icon text and tint of a file name (`Plan.xlsx` -> XLS, sheet). */
36
+ export const attachmentMeta = (f: AttachmentFile) => ({
37
+ ext: f.ext ?? extOf(f.name).toUpperCase().replace(/^(DOC|XLS|PPT)X$/, "$1"),
38
+ type: f.type ?? TYPES[extOf(f.name)],
39
+ });
40
+
41
+ export interface AttachmentProps extends Omit<HTMLAttributes<HTMLElement>, "type">, AttachmentFile {
42
+ onRemove?: () => void;
43
+ }
44
+
45
+ /** `.oc-attachment` file chip: icon tile, name, size and an optional remove button. */
46
+ export const Attachment = forwardRef<HTMLElement, AttachmentProps>(function Attachment(
47
+ { name, size, type, ext, href, image, state, progress = 0, removable, removeLabel, onRemove, className, style, ...rest },
48
+ ref,
49
+ ) {
50
+ const meta = attachmentMeta({ name, type, ext });
51
+ const uploading = state === "uploading";
52
+ const props = {
53
+ className: cx("oc-attachment", meta.type && `oc-attachment--${meta.type}`, state && `oc-attachment--${state}`, className),
54
+ style: uploading ? ({ "--oc-attachment-progress": `${progress}%`, ...style } as CSSProperties) : style,
55
+ ...rest,
56
+ };
57
+ const inner = (
58
+ <>
59
+ <span className="oc-attachment__icon" aria-hidden="true">{image ? <img src={image} alt="" /> : meta.ext}</span>
60
+ <span className="oc-attachment__body">
61
+ <span className="oc-attachment__name">{name}</span>
62
+ {uploading ? <span className="oc-attachment__size" role="status">{size ?? `Uploading… ${progress}%`}</span> : size != null && <span className="oc-attachment__size">{size}</span>}
63
+ </span>
64
+ {removable && (
65
+ <button type="button" className="oc-attachment__remove" aria-label={removeLabel ?? (uploading ? `Cancel upload of ${name}` : `Remove ${name}`)} onClick={onRemove}>&#10005;</button>
66
+ )}
67
+ </>
68
+ );
69
+ return href && !removable
70
+ ? <a ref={ref as React.Ref<HTMLAnchorElement>} href={href} download {...props}>{inner}</a>
71
+ : <div ref={ref as React.Ref<HTMLDivElement>} {...props}>{inner}</div>;
72
+ });
73
+
74
+ export interface AttachmentImageProps extends HTMLAttributes<HTMLAnchorElement> {
75
+ href?: string;
76
+ /** Image URL. Leave empty to show placeholder art. */
77
+ src?: string;
78
+ alt: string;
79
+ art?: number;
80
+ /** e.g. "worktop-sample.jpg · 2.1 MB". */
81
+ caption?: ReactNode;
82
+ }
83
+
84
+ const art = (n?: number) => cx("oc-media-art", n != null && `oc-media-art--${n}`);
85
+
86
+ /** `a.oc-attachment-image`: 4:3 image preview inside a message. */
87
+ export function AttachmentImage({ href, src, alt, art: n, caption, className, ...rest }: AttachmentImageProps) {
88
+ return (
89
+ <a className={cx("oc-attachment-image", className)} href={href} {...rest}>
90
+ {src ? <img src={src} alt={alt} /> : <span className={art(n)} role="img" aria-label={alt} />}
91
+ {caption != null && <span className="oc-attachment-image__caption">{caption}</span>}
92
+ </a>
93
+ );
94
+ }
95
+
96
+ export interface AttachmentThumbProps extends HTMLAttributes<HTMLElement> {
97
+ src?: string;
98
+ alt: string;
99
+ art?: number;
100
+ /** Remove button label, e.g. "Remove screenshot.png". No label = no button. */
101
+ removeLabel?: string;
102
+ onRemove?: () => void;
103
+ as?: "div" | "span";
104
+ }
105
+
106
+ /** `.oc-attachment-thumb`: square thumbnail of a pasted screenshot. */
107
+ export function AttachmentThumb({ src, alt, art: n, removeLabel, onRemove, as: Tag = "div", className, ...rest }: AttachmentThumbProps) {
108
+ return (
109
+ <Tag className={cx("oc-attachment-thumb", className)} {...rest}>
110
+ {src ? <img src={src} alt={alt} /> : <span className={art(n)} role="img" aria-label={alt} />}
111
+ {removeLabel && <button type="button" className="oc-attachment__remove" aria-label={removeLabel} onClick={onRemove}>&#10005;</button>}
112
+ </Tag>
113
+ );
114
+ }
115
+
116
+ export interface AttachmentListProps extends HTMLAttributes<HTMLElement> {
117
+ /** File chips; `children` replace them. */
118
+ items?: AttachmentFile[];
119
+ /** One per line, full width. */
120
+ stack?: boolean;
121
+ /** `ul` (default, items in `li`) or `div`. */
122
+ as?: "ul" | "div";
123
+ onRemove?: (file: AttachmentFile, index: number) => void;
124
+ }
125
+
126
+ const DEMO: AttachmentFile[] = [
127
+ { name: "Quote kitchen Dewitte v2.pdf", size: "1.2 MB", href: "#quote.pdf" },
128
+ { name: "Measurements ground floor.xlsx", size: "86 KB", href: "#measurements.xlsx" },
129
+ { name: "Contract addendum.docx", size: "240 KB", href: "#contract.docx" },
130
+ { name: "Plans architect.zip", size: "14.8 MB", href: "#plans.zip" },
131
+ ];
132
+
133
+ /** `.oc-attachment-list`: wrapping row of chips. */
134
+ export function AttachmentList({ items = DEMO, stack, as: Tag = "ul", onRemove, className, children, "aria-label": label = "Files in this message", ...rest }: AttachmentListProps) {
135
+ const chips = children ?? items.map((f, i) => {
136
+ const chip = <Attachment key={i} {...f} onRemove={() => onRemove?.(f, i)} />;
137
+ return Tag === "ul" ? <li key={i}>{chip}</li> : chip;
138
+ });
139
+ return <Tag className={cx("oc-attachment-list", stack && "oc-attachment-list--stack", className)} aria-label={label} {...rest}>{chips}</Tag>;
140
+ }
@@ -0,0 +1,32 @@
1
+ import { forwardRef } from "react";
2
+ import type { HTMLAttributes } from "react";
3
+ import { cx } from "../utils";
4
+
5
+ export type AvatarSize = "sm" | "md" | "lg" | "xl";
6
+
7
+ export interface AvatarProps extends HTMLAttributes<HTMLSpanElement> {
8
+ size?: AvatarSize;
9
+ /** Image URL; when set the avatar renders an <img>. Otherwise `children` (initials) are shown. */
10
+ src?: string;
11
+ alt?: string;
12
+ }
13
+
14
+ /** `<span class="oc-avatar oc-avatar--{size}">SC</span>` */
15
+ export const Avatar = forwardRef<HTMLSpanElement, AvatarProps>(function Avatar(
16
+ { size = "md", src, alt = "", className, children, ...rest },
17
+ ref,
18
+ ) {
19
+ return (
20
+ <span ref={ref} className={cx("oc-avatar", size !== "md" && `oc-avatar--${size}`, className)} {...rest}>
21
+ {src ? <img src={src} alt={alt} /> : children}
22
+ </span>
23
+ );
24
+ });
25
+
26
+ /** `<div class="oc-avatar-group">` */
27
+ export const AvatarGroup = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(function AvatarGroup(
28
+ { className, ...rest },
29
+ ref,
30
+ ) {
31
+ return <div ref={ref} className={cx("oc-avatar-group", className)} {...rest} />;
32
+ });
@@ -0,0 +1,11 @@
1
+ import { forwardRef } from "react";
2
+ import type { AnchorHTMLAttributes } from "react";
3
+ import { cx } from "../utils";
4
+
5
+ /** `<a class="oc-back-link">Back to Products</a>` */
6
+ export const BackLink = forwardRef<HTMLAnchorElement, AnchorHTMLAttributes<HTMLAnchorElement>>(function BackLink(
7
+ { className, ...rest },
8
+ ref,
9
+ ) {
10
+ return <a ref={ref} className={cx("oc-back-link", className)} {...rest} />;
11
+ });
@@ -0,0 +1,14 @@
1
+ import { forwardRef } from "react";
2
+ import type { HTMLAttributes } from "react";
3
+ import { cx } from "../utils";
4
+
5
+ export type BadgeVariant = "default" | "secondary" | "outline" | "destructive" | "success" | "warning" | "info";
6
+
7
+ export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
8
+ variant?: BadgeVariant;
9
+ }
10
+
11
+ /** `<span class="oc-badge oc-badge--{variant}">` */
12
+ export const Badge = forwardRef<HTMLSpanElement, BadgeProps>(function Badge({ variant = "default", className, ...rest }, ref) {
13
+ return <span ref={ref} className={cx("oc-badge", variant !== "default" && `oc-badge--${variant}`, className)} {...rest} />;
14
+ });
@@ -0,0 +1,64 @@
1
+ // File: packages/react/src/components/BarcodeInput.tsx
2
+ // Map: props -> component (label, digits-only field, preview figure with placeholder bars or your own graphic)
3
+ // Same markup as packages/ui/snippets/barcode-input.html. No barcode drawing: pass your own <svg>/<canvas> as children.
4
+ "use client";
5
+
6
+ import { forwardRef, useId } from "react";
7
+ import type { InputHTMLAttributes, ReactNode } from "react";
8
+ import { cx } from "../utils";
9
+ import { useControllable } from "../hooks";
10
+
11
+ export interface BarcodeInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "defaultValue" | "onChange" | "children"> {
12
+ value?: string;
13
+ defaultValue?: string;
14
+ onChange?: (value: string) => void;
15
+ /** Visible label above the field. */
16
+ label?: ReactNode;
17
+ /** Text in the preview while the field is empty. */
18
+ emptyText?: string;
19
+ error?: boolean;
20
+ /** Your own barcode graphic; replaces the placeholder bars. */
21
+ children?: ReactNode;
22
+ /** Class for the wrapper (`className` goes to the input). */
23
+ wrapperClassName?: string;
24
+ }
25
+
26
+ /** `.oc-barcode-input`: code field + preview. Keeps digits only (hand scanners type like a keyboard). */
27
+ export const BarcodeInput = forwardRef<HTMLInputElement, BarcodeInputProps>(function BarcodeInput(
28
+ { value, defaultValue, onChange, label, emptyText = "No code yet", error, children, wrapperClassName, className, id, ...rest },
29
+ ref,
30
+ ) {
31
+ const auto = `oc-barcode-${useId().replace(/:/g, "")}`;
32
+ const inputId = id ?? auto;
33
+ const [code, setCode] = useControllable<string>(value, defaultValue ?? "", onChange);
34
+ const empty = !code;
35
+ return (
36
+ <div className={cx("oc-barcode-input", wrapperClassName)}>
37
+ {label != null && <label className="oc-field-label" htmlFor={inputId}>{label}</label>}
38
+ <div className="oc-input-group oc-input-group--affix">
39
+ <span className="oc-input-addon" aria-hidden="true">
40
+ <svg viewBox="0 0 16 16"><path fill="currentColor" d="M1 1h1.4v14H1zm2.2 0h.7v14h-.7zm1.6 0h1.5v14H4.8zm2.4 0h.7v14H7.2zm1.6 0h.7v14H8.8zm1.5 0h1.4v14h-1.4zm2.2 0H15v14h-.9z" /></svg>
41
+ </span>
42
+ <input
43
+ ref={ref}
44
+ id={inputId}
45
+ className={cx("oc-input", "oc-barcode-input-field", className)}
46
+ type="text"
47
+ inputMode="text"
48
+ autoComplete="off"
49
+ spellCheck={false}
50
+ maxLength={80}
51
+ placeholder="EAN-13 or internal code"
52
+ aria-invalid={error ? true : undefined}
53
+ value={code}
54
+ onChange={(e) => setCode(e.target.value)}
55
+ {...rest}
56
+ />
57
+ </div>
58
+ <figure className={cx("oc-barcode-preview", empty && "oc-barcode-preview--empty")} aria-label="Barcode preview">
59
+ {children ?? <div className="oc-barcode-preview-bars" aria-hidden="true" />}
60
+ <figcaption className="oc-barcode-preview-code">{empty ? emptyText : code}</figcaption>
61
+ </figure>
62
+ </div>
63
+ );
64
+ });
@@ -0,0 +1,32 @@
1
+ import { forwardRef } from "react";
2
+ import type { HTMLAttributes, ReactNode } from "react";
3
+ import { cx } from "../utils";
4
+
5
+ export interface BreadcrumbItemData {
6
+ label: ReactNode;
7
+ /** Omit (or put last) to mark the current page. */
8
+ href?: string;
9
+ }
10
+
11
+ export interface BreadcrumbProps extends HTMLAttributes<HTMLElement> {
12
+ items?: BreadcrumbItemData[];
13
+ }
14
+
15
+ /** `<nav class="oc-breadcrumb">` with links and a final `aria-current="page"` item. */
16
+ export const Breadcrumb = forwardRef<HTMLElement, BreadcrumbProps>(function Breadcrumb(
17
+ { items, children, className, "aria-label": ariaLabel = "Breadcrumb", ...rest },
18
+ ref,
19
+ ) {
20
+ return (
21
+ <nav ref={ref} className={cx("oc-breadcrumb", className)} aria-label={ariaLabel} {...rest}>
22
+ {items?.map((item, i) =>
23
+ item.href && i < items.length - 1 ? (
24
+ <a key={i} className="oc-breadcrumb-item" href={item.href}>{item.label}</a>
25
+ ) : (
26
+ <span key={i} className="oc-breadcrumb-item" aria-current="page">{item.label}</span>
27
+ ),
28
+ )}
29
+ {children}
30
+ </nav>
31
+ );
32
+ });
@@ -0,0 +1,48 @@
1
+ // File: packages/react/src/components/BrowserFrame.tsx
2
+ // Map: props -> BrowserFrame (figure -> bar: dots, address, actions -> view (img or children) -> caption)
3
+ // Same markup as packages/ui/snippets/browser-frame.html.
4
+ import { forwardRef } from "react";
5
+ import type { CSSProperties, HTMLAttributes, ReactNode } from "react";
6
+ import { cx } from "../utils";
7
+
8
+ export interface BrowserFrameProps extends HTMLAttributes<HTMLElement> {
9
+ /** Text in the address pill. Leave out (or use `minimal`) for dots only. */
10
+ url?: string;
11
+ /** Screenshot URL. `children` replace it (live content). */
12
+ src?: string;
13
+ alt?: string;
14
+ caption?: ReactNode;
15
+ /** Small glyphs on the right of the bar. */
16
+ actions?: ReactNode;
17
+ minimal?: boolean;
18
+ dark?: boolean;
19
+ tilt?: boolean;
20
+ /** View height follows the content instead of the ratio. */
21
+ auto?: boolean;
22
+ /** View ratio, e.g. "16 / 9". Default 16 / 10. */
23
+ ratio?: string;
24
+ }
25
+
26
+ /** `figure.oc-browser-frame`: fake browser chrome around a screenshot or any content. */
27
+ export const BrowserFrame = forwardRef<HTMLElement, BrowserFrameProps>(function BrowserFrame(
28
+ { url, src, alt = "", caption, actions, minimal, dark, tilt, auto, ratio, className, style, children, ...rest },
29
+ ref,
30
+ ) {
31
+ const vars = ratio ? ({ "--oc-browser-frame-ratio": ratio } as CSSProperties) : undefined;
32
+ return (
33
+ <figure
34
+ ref={ref}
35
+ className={cx("oc-browser-frame", minimal && "oc-browser-frame--minimal", dark && "oc-browser-frame--dark", tilt && "oc-browser-frame--tilt", auto && "oc-browser-frame--auto", className)}
36
+ style={{ ...vars, ...style }}
37
+ {...rest}
38
+ >
39
+ <div className="oc-browser-frame__bar">
40
+ <i /><i /><i />
41
+ {url != null && <span className="oc-browser-frame__address">{url}</span>}
42
+ {actions != null && <span className="oc-browser-frame__actions" aria-hidden="true">{actions}</span>}
43
+ </div>
44
+ <div className="oc-browser-frame__view">{children ?? (src ? <img src={src} alt={alt} /> : <div className="oc-media-art" role="img" aria-label={alt} />)}</div>
45
+ {caption != null && <figcaption className="oc-browser-frame__caption">{caption}</figcaption>}
46
+ </figure>
47
+ );
48
+ });
@@ -0,0 +1,52 @@
1
+ // File: packages/react/src/components/BusyOverlay.tsx
2
+ // Map: types -> BusyOverlay (<dialog closedby="none">: spinner or progress, title, text)
3
+ // Same markup as packages/ui/snippets/busy-overlay.html. Open it with `open` while a long submit or upload runs;
4
+ // the dialog is modal (the page is inert) and Esc does not close it. The 300ms delay before it shows is CSS.
5
+ "use client";
6
+
7
+ import { forwardRef, useId, useRef } 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
+ import { Progress } from "./Progress";
13
+
14
+ export interface BusyOverlayProps extends Omit<DialogHTMLAttributes<HTMLDialogElement>, "title" | "open" | "onClose">, DialogState {
15
+ title?: ReactNode;
16
+ text?: ReactNode;
17
+ /** Known percentage: shows `.oc-progress` instead of the spinner. */
18
+ progress?: number;
19
+ /** aria-label of the spinner / progress bar (default "Busy"). */
20
+ label?: string;
21
+ }
22
+
23
+ /** `dialog.oc-busy-overlay[closedby=none]`. Controlled with `open` / `onOpenChange`, or uncontrolled. */
24
+ export const BusyOverlay = forwardRef<HTMLDialogElement, BusyOverlayProps>(function BusyOverlay(
25
+ {
26
+ title = "Saving…", text = "The photos are being processed. This can take a minute; please keep this page open.",
27
+ progress, label = "Busy", open, defaultOpen, onOpenChange, id, className, ...rest
28
+ },
29
+ forwardedRef,
30
+ ) {
31
+ const inner = useRef<HTMLDialogElement>(null);
32
+ const ref = useMergedRef(inner, forwardedRef);
33
+ const dlg = useDialog(inner, { open, defaultOpen, onOpenChange });
34
+ const auto = useId().replace(/[^\w-]/g, "");
35
+ const base = id ?? `oc-busy-overlay-${auto}`;
36
+ return (
37
+ <dialog
38
+ ref={ref}
39
+ className={cx("oc-busy-overlay", className)}
40
+ id={base}
41
+ {...attrs({ closedby: "none" })}
42
+ aria-labelledby={`${base}-title`}
43
+ aria-describedby={text != null ? `${base}-text` : undefined}
44
+ {...rest}
45
+ onClose={dlg.onClose}
46
+ >
47
+ {progress == null ? <span className="oc-spinner" role="status" aria-label={label} /> : <Progress value={progress} aria-label={label} />}
48
+ <h2 className="oc-busy-overlay__title" id={`${base}-title`}>{title}</h2>
49
+ {text != null && <p className="oc-busy-overlay__text" id={`${base}-text`}>{text}</p>}
50
+ </dialog>
51
+ );
52
+ });
@@ -0,0 +1,49 @@
1
+ import { forwardRef } from "react";
2
+ import type { AnchorHTMLAttributes, ButtonHTMLAttributes } from "react";
3
+ import { cx } from "../utils";
4
+
5
+ export type ButtonVariant = "default" | "secondary" | "outline" | "ghost" | "link" | "destructive" | "success" | "gradient" | "glass";
6
+ export type ButtonSize = "xs" | "sm" | "md" | "lg";
7
+
8
+ interface ButtonStyleProps {
9
+ /** Visual variant (`default` = primary). */
10
+ variant?: ButtonVariant;
11
+ /** `md` is the default size and adds no class. */
12
+ size?: ButtonSize;
13
+ /** Square icon-only button. */
14
+ icon?: boolean;
15
+ /** Fully rounded pill shape. */
16
+ pill?: boolean;
17
+ }
18
+
19
+ /** The class string of a button; reuse it on your own elements (router links etc.). */
20
+ export function buttonClass({ variant = "default", size = "md", icon, pill, className }: ButtonStyleProps & { className?: string }) {
21
+ return cx(
22
+ "oc-btn",
23
+ variant !== "default" && `oc-btn--${variant}`,
24
+ size !== "md" && `oc-btn--${size}`,
25
+ icon && "oc-btn--icon",
26
+ pill && "oc-btn--pill",
27
+ className,
28
+ );
29
+ }
30
+
31
+ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, ButtonStyleProps {}
32
+
33
+ /** `<button class="oc-btn oc-btn--{variant} oc-btn--{size}">` */
34
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
35
+ { variant, size, icon, pill, className, ...rest },
36
+ ref,
37
+ ) {
38
+ return <button ref={ref} className={buttonClass({ variant, size, icon, pill, className })} {...rest} />;
39
+ });
40
+
41
+ export interface ButtonLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement>, ButtonStyleProps {}
42
+
43
+ /** An anchor styled as a button. */
44
+ export const ButtonLink = forwardRef<HTMLAnchorElement, ButtonLinkProps>(function ButtonLink(
45
+ { variant, size, icon, pill, className, ...rest },
46
+ ref,
47
+ ) {
48
+ return <a ref={ref} className={buttonClass({ variant, size, icon, pill, className })} {...rest} />;
49
+ });
@@ -0,0 +1,27 @@
1
+ import { forwardRef } from "react";
2
+ import type { HTMLAttributes } from "react";
3
+ import { cx } from "../utils";
4
+
5
+ type Tag = "div" | "h3" | "p";
6
+
7
+ function part<E extends HTMLElement>(tag: Tag, base: string, displayName: string) {
8
+ const Comp = forwardRef<E, HTMLAttributes<E>>(function Part({ className, ...rest }, ref) {
9
+ const Element = tag as "div";
10
+ return <Element ref={ref as React.Ref<HTMLDivElement>} className={cx(base, className)} {...(rest as HTMLAttributes<HTMLDivElement>)} />;
11
+ });
12
+ Comp.displayName = displayName;
13
+ return Comp;
14
+ }
15
+
16
+ /** `.oc-card` */
17
+ export const Card = part<HTMLDivElement>("div", "oc-card", "Card");
18
+ /** `.oc-card-header` */
19
+ export const CardHeader = part<HTMLDivElement>("div", "oc-card-header", "CardHeader");
20
+ /** `h3.oc-card-title` */
21
+ export const CardTitle = part<HTMLHeadingElement>("h3", "oc-card-title", "CardTitle");
22
+ /** `p.oc-card-description` */
23
+ export const CardDescription = part<HTMLParagraphElement>("p", "oc-card-description", "CardDescription");
24
+ /** `.oc-card-content` */
25
+ export const CardContent = part<HTMLDivElement>("div", "oc-card-content", "CardContent");
26
+ /** `.oc-card-footer` */
27
+ export const CardFooter = part<HTMLDivElement>("div", "oc-card-footer", "CardFooter");