@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,95 @@
1
+ // File: packages/react/src/components/ConfirmDialog.tsx
2
+ // Map: ConfirmDialog = dialog.oc-modal -> confirm content (close X, icon ring, title) -> form (message, typed field, footer: discard, cancel, confirm)
3
+ // A variant of AlertDialog with `.oc-alert-dialog--confirm`. The form is submitted with the confirm button;
4
+ // with `confirmWord` the browser blocks the submit until the typed value matches (pattern + required).
5
+ "use client";
6
+
7
+ import { forwardRef, useId, useRef } from "react";
8
+ import type { DialogHTMLAttributes, FormEvent, ReactNode } from "react";
9
+ import { attrs, cx } from "../utils";
10
+ import { useDialog, useMergedRef } from "../hooks";
11
+ import type { DialogState } from "../hooks";
12
+ import type { AlertDialogType } from "./AlertDialog";
13
+
14
+ const GLYPHS: Record<AlertDialogType, string> = { success: "✓", error: "×", warning: "!", info: "i", question: "?" };
15
+
16
+ export interface ConfirmDialogProps extends Omit<DialogHTMLAttributes<HTMLDialogElement>, "title" | "open" | "onClose">, DialogState {
17
+ /** Icon color; default `error` (destructive confirm). */
18
+ type?: AlertDialogType;
19
+ icon?: ReactNode;
20
+ title: ReactNode;
21
+ /** Message: pass as `message` or as children. */
22
+ message?: ReactNode;
23
+ confirmLabel?: ReactNode;
24
+ cancelLabel?: ReactNode;
25
+ /** Optional third action at the footer start ("Don't save"). */
26
+ discardLabel?: ReactNode;
27
+ /** The user must type this word before confirming, e.g. "DELETE". */
28
+ confirmWord?: string;
29
+ /** Label above the typed field; defaults to "Type WORD to confirm". */
30
+ confirmWordLabel?: ReactNode;
31
+ closeLabel?: string;
32
+ onConfirm?: () => void;
33
+ onDiscard?: () => void;
34
+ }
35
+
36
+ /** Confirm variant of the alert dialog (`.oc-alert-dialog--confirm`). */
37
+ export const ConfirmDialog = forwardRef<HTMLDialogElement, ConfirmDialogProps>(function ConfirmDialog(
38
+ {
39
+ open, defaultOpen, onOpenChange, type = "error", icon, title, message, children,
40
+ confirmLabel = "Confirm", cancelLabel = "Cancel", discardLabel, confirmWord, confirmWordLabel,
41
+ closeLabel = "Close", onConfirm, onDiscard, className, ...rest
42
+ },
43
+ forwardedRef,
44
+ ) {
45
+ const inner = useRef<HTMLDialogElement>(null);
46
+ const ref = useMergedRef(inner, forwardedRef);
47
+ const id = useId();
48
+ const dlg = useDialog(inner, { open, defaultOpen, onOpenChange });
49
+
50
+ // We close through state (not method="dialog"), so React stays the source of truth.
51
+ const submit = (e: FormEvent<HTMLFormElement>) => {
52
+ e.preventDefault();
53
+ onConfirm?.();
54
+ dlg.close();
55
+ };
56
+ const destructive = type === "error";
57
+
58
+ return (
59
+ <dialog
60
+ ref={ref}
61
+ className={cx("oc-modal", className)}
62
+ aria-labelledby={`${id}-title`}
63
+ aria-describedby={`${id}-text`}
64
+ {...attrs({ closedby: "any" })}
65
+ {...rest}
66
+ onClose={dlg.onClose}
67
+ >
68
+ <div className={`oc-modal-content oc-alert-dialog oc-alert-dialog--${type} oc-alert-dialog--confirm`}>
69
+ <button className="oc-alert-dialog-close" type="button" aria-label={closeLabel} onClick={dlg.close}>×</button>
70
+ <span className="oc-alert-dialog-icon" aria-hidden="true">{icon ?? GLYPHS[type]}</span>
71
+ <h2 className="oc-modal-title" id={`${id}-title`}>{title}</h2>
72
+ <form className="oc-alert-dialog-form" onSubmit={submit}>
73
+ <div className="oc-modal-body" id={`${id}-text`}>{message ?? children}</div>
74
+ {confirmWord && (
75
+ <label className="oc-alert-dialog-typed">
76
+ <span>{confirmWordLabel ?? <>Type <code>{confirmWord}</code> to confirm</>}</span>
77
+ <input className="oc-input" name="confirm" pattern={confirmWord} required autoComplete="off" />
78
+ </label>
79
+ )}
80
+ <div className="oc-modal-footer">
81
+ {discardLabel != null && (
82
+ <button className="oc-btn oc-btn--ghost oc-alert-dialog-discard" type="button" onClick={() => { onDiscard?.(); dlg.close(); }}>
83
+ {discardLabel}
84
+ </button>
85
+ )}
86
+ <button className="oc-btn oc-btn--outline" type="button" onClick={dlg.close}>{cancelLabel}</button>
87
+ <button className={cx("oc-btn", destructive && "oc-btn--destructive", "oc-alert-dialog-confirm")} type="submit" value="confirm">
88
+ {confirmLabel}
89
+ </button>
90
+ </div>
91
+ </form>
92
+ </div>
93
+ </dialog>
94
+ );
95
+ });
@@ -0,0 +1,137 @@
1
+ // File: packages/react/src/components/CookieConsent.tsx
2
+ // Map: types + demo categories -> storage helpers -> CookieConsent (reopen button, popover: title, text, categories, actions) -> effect
3
+ // Same markup as packages/ui/snippets/cookie-consent.html on popover="manual". The effect does what the snippet script does:
4
+ // show the banner when no choice is stored, save the choice in localStorage and fire "oc-consent" on document.
5
+ "use client";
6
+
7
+ import { forwardRef, useEffect, useRef } from "react";
8
+ import type { HTMLAttributes, MouseEvent, ReactNode } from "react";
9
+ import { attrs, callAll, cx } from "../utils";
10
+ import { useMergedRef } from "../hooks";
11
+
12
+ export interface CookieCategory {
13
+ /** Input name and key in the stored choice (e.g. "statistics"). */
14
+ name: string;
15
+ label: string;
16
+ text?: ReactNode;
17
+ /** Always on (checked + disabled), e.g. necessary cookies. */
18
+ required?: boolean;
19
+ }
20
+
21
+ /** Stored choice: `{ necessary: true, statistics, marketing, date }`. */
22
+ export type ConsentChoice = { necessary: boolean; date: string } & Record<string, boolean | string>;
23
+
24
+ export interface CookieConsentProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
25
+ categories?: CookieCategory[];
26
+ title?: ReactNode;
27
+ /** Replaces the whole text paragraph (put your policy link in it). */
28
+ text?: ReactNode;
29
+ policyHref?: string;
30
+ customizeLabel?: ReactNode;
31
+ necessaryLabel?: ReactNode;
32
+ selectedLabel?: ReactNode;
33
+ allLabel?: ReactNode;
34
+ /** Text of the "Cookie settings" button that opens the banner again; null hides it. */
35
+ openLabel?: ReactNode;
36
+ /** Full-width bar at the bottom (`--bar`). */
37
+ bar?: boolean;
38
+ /** Never open by itself (`data-oc-consent-manual`). */
39
+ manual?: boolean;
40
+ storageKey?: string;
41
+ /** Called with the stored choice on load and after every choice. */
42
+ onConsent?: (choice: ConsentChoice) => void;
43
+ }
44
+
45
+ const DEMO: CookieCategory[] = [
46
+ { name: "necessary", label: "Necessary", text: "Session, security and your cookie choice. Always on.", required: true },
47
+ { name: "statistics", label: "Statistics", text: "Anonymous visit counts." },
48
+ { name: "marketing", label: "Marketing", text: "Embedded videos and ads that track you." },
49
+ ];
50
+
51
+ const read = (key: string): ConsentChoice | null => {
52
+ try { return JSON.parse(localStorage.getItem(key) ?? "null"); } catch { return null; }
53
+ };
54
+
55
+ /** `aside.oc-cookie-consent[popover=manual]` plus an optional `[data-oc-consent-open]` button. */
56
+ export const CookieConsent = forwardRef<HTMLElement, CookieConsentProps>(function CookieConsent(
57
+ {
58
+ categories = DEMO, title = "We use cookies", text, policyHref = "#cookie-policy", customizeLabel = "Customize",
59
+ necessaryLabel = "Only necessary", selectedLabel = "Save choice", allLabel = "Accept all", openLabel = "Cookie settings",
60
+ bar, manual, storageKey = "oc-consent", onConsent, id = "oc-cookie-consent", className, onClick, ...rest
61
+ },
62
+ forwardedRef,
63
+ ) {
64
+ const box = useRef<HTMLElement>(null);
65
+ const ref = useMergedRef(box, forwardedRef);
66
+ const cb = useRef(onConsent);
67
+ cb.current = onConsent;
68
+ const inputs = () => Array.from(box.current?.querySelectorAll<HTMLInputElement>("input[name]") ?? []);
69
+ const tell = (choice: ConsentChoice) => {
70
+ document.dispatchEvent(new CustomEvent("oc-consent", { detail: choice }));
71
+ cb.current?.(choice);
72
+ };
73
+
74
+ // On load: report a stored choice or show the banner; any [data-oc-consent-open] opens it again.
75
+ useEffect(() => {
76
+ const stored = read(storageKey);
77
+ if (stored) tell(stored);
78
+ else if (!manual) box.current?.showPopover?.();
79
+ const reopen = (e: Event) => {
80
+ if (!(e.target as Element | null)?.closest?.("[data-oc-consent-open]")) return;
81
+ const choice = read(storageKey);
82
+ for (const i of inputs()) if (!i.disabled) i.checked = !!choice?.[i.name];
83
+ box.current?.showPopover?.();
84
+ };
85
+ document.addEventListener("click", reopen);
86
+ return () => document.removeEventListener("click", reopen);
87
+ // eslint-disable-next-line react-hooks/exhaustive-deps
88
+ }, [storageKey, manual]);
89
+
90
+ const choose = (e: MouseEvent<HTMLElement>) => {
91
+ const mode = (e.target as HTMLElement).closest<HTMLElement>("[data-oc-consent]")?.dataset.ocConsent;
92
+ if (!mode) return;
93
+ const choice: ConsentChoice = { necessary: true, date: new Date().toISOString() };
94
+ for (const i of inputs()) if (!i.disabled) choice[i.name] = mode === "all" || (mode === "selected" && i.checked);
95
+ try { localStorage.setItem(storageKey, JSON.stringify(choice)); } catch { /* storage blocked */ }
96
+ box.current?.hidePopover?.();
97
+ tell(choice);
98
+ };
99
+
100
+ return (
101
+ <>
102
+ {openLabel != null && <button type="button" className="oc-btn oc-btn--outline oc-btn--sm" data-oc-consent-open="">{openLabel}</button>}
103
+ <aside
104
+ ref={ref}
105
+ className={cx("oc-cookie-consent", bar && "oc-cookie-consent--bar", className)}
106
+ id={id}
107
+ {...attrs({ popover: "manual", "data-oc-consent-manual": manual ? "" : undefined })}
108
+ aria-labelledby={`${id}-title`}
109
+ onClick={callAll(choose, onClick)}
110
+ {...rest}
111
+ >
112
+ <h2 className="oc-cookie-consent__title" id={`${id}-title`}><span aria-hidden="true">&#127850;</span> {title}</h2>
113
+ <p className="oc-cookie-consent__text">
114
+ {text ?? <>Necessary cookies keep the site working. With your permission we also measure visits to improve the site. Read the <a href={policyHref}>cookie policy</a>.</>}
115
+ </p>
116
+ <details className="oc-cookie-consent__prefs">
117
+ <summary>{customizeLabel}</summary>
118
+ {categories.map((c) => (
119
+ <div key={c.name} className="oc-cookie-consent__category">
120
+ <span className="oc-cookie-consent__category-name">{c.label}</span>
121
+ {c.text != null && <span className="oc-cookie-consent__category-text">{c.text}</span>}
122
+ <label className="oc-switch">
123
+ <input type="checkbox" role="switch" name={c.name} defaultChecked={c.required} disabled={c.required} aria-label={`${c.label} cookies`} />
124
+ <span className="oc-switch-track" />
125
+ </label>
126
+ </div>
127
+ ))}
128
+ </details>
129
+ <div className="oc-cookie-consent__actions">
130
+ <button type="button" className="oc-btn oc-btn--outline" data-oc-consent="necessary">{necessaryLabel}</button>
131
+ <button type="button" className="oc-btn oc-btn--secondary" data-oc-consent="selected">{selectedLabel}</button>
132
+ <button type="button" className="oc-btn" data-oc-consent="all">{allLabel}</button>
133
+ </div>
134
+ </aside>
135
+ </>
136
+ );
137
+ });
@@ -0,0 +1,44 @@
1
+ "use client";
2
+
3
+ import { forwardRef, useEffect, useRef, useState } from "react";
4
+ import type { HTMLAttributes } from "react";
5
+ import { cx } from "../utils";
6
+
7
+ export interface CopyFieldProps extends Omit<HTMLAttributes<HTMLDivElement>, "onCopy"> {
8
+ value: string;
9
+ /** Accessible name of the read-only input. */
10
+ label?: string;
11
+ buttonLabel?: string;
12
+ /** How long the `is-copied` feedback stays (ms). */
13
+ feedbackMs?: number;
14
+ onCopy?: (value: string) => void;
15
+ }
16
+
17
+ /** Read-only monospace value with a copy button (`is-copied` feedback for 2s). */
18
+ export const CopyField = forwardRef<HTMLDivElement, CopyFieldProps>(function CopyField(
19
+ { value, label = "Value", buttonLabel = "Copy", feedbackMs = 2000, onCopy, className, ...rest },
20
+ ref,
21
+ ) {
22
+ const [copied, setCopied] = useState(false);
23
+ const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
24
+ useEffect(() => () => clearTimeout(timer.current), []);
25
+
26
+ const copy = async () => {
27
+ try {
28
+ await navigator.clipboard.writeText(value);
29
+ } catch {
30
+ return;
31
+ }
32
+ setCopied(true);
33
+ onCopy?.(value);
34
+ clearTimeout(timer.current);
35
+ timer.current = setTimeout(() => setCopied(false), feedbackMs);
36
+ };
37
+
38
+ return (
39
+ <div ref={ref} className={cx("oc-copy-field", className)} {...rest}>
40
+ <input className="oc-copy-field-value" type="text" readOnly value={value} aria-label={label} />
41
+ <button className={cx("oc-copy-field-button", copied && "is-copied")} type="button" onClick={copy}>{buttonLabel}</button>
42
+ </div>
43
+ );
44
+ });
@@ -0,0 +1,21 @@
1
+ import { forwardRef } from "react";
2
+ import type { CSSProperties, HTMLAttributes } from "react";
3
+ import { cx } from "../utils";
4
+
5
+ export interface CounterProps extends HTMLAttributes<HTMLElement> {
6
+ /** Target number (`--oc-counter-to`). */
7
+ to: number;
8
+ /** Text after the number, e.g. `+` or `%` (`--oc-counter-suffix`). */
9
+ suffix?: string;
10
+ /** Start animating when scrolled into view (default true). */
11
+ onView?: boolean;
12
+ }
13
+
14
+ /** `<strong class="oc-counter oc-counter--on-view">`: animated number, CSS only. */
15
+ export const Counter = forwardRef<HTMLElement, CounterProps>(function Counter(
16
+ { to, suffix, onView = true, className, style, ...rest },
17
+ ref,
18
+ ) {
19
+ const vars = { "--oc-counter-to": to, ...(suffix ? { "--oc-counter-suffix": `'${suffix}'` } : {}) } as CSSProperties;
20
+ return <strong ref={ref} className={cx("oc-counter", onView && "oc-counter--on-view", className)} style={{ ...vars, ...style }} {...rest} />;
21
+ });
@@ -0,0 +1,201 @@
1
+ // File: packages/react/src/components/DataTable.tsx
2
+ // Map: column and row types -> shell markup -> bind the list-table script on mount
3
+ "use client";
4
+
5
+ import { useEffect, useId, useRef } from "react";
6
+ import type { ReactNode } from "react";
7
+ import { bindDataTable } from "../../../ui/src/data-table.js";
8
+
9
+ export interface DataTableColumn {
10
+ key: string;
11
+ header: string;
12
+ /** Right aligned, sorted as a number (5,15 and 5.15). */
13
+ numeric?: boolean;
14
+ /** Click the header to sort. Default true. */
15
+ sortable?: boolean;
16
+ /** Show the column in the Columns menu. Default true. */
17
+ hideable?: boolean;
18
+ /** Start hidden until the user checks it in the Columns menu. */
19
+ defaultHidden?: boolean;
20
+ }
21
+
22
+ export interface DataTableRow {
23
+ key: string;
24
+ status?: "active" | "deleted" | "archived";
25
+ cells: Record<string, string>;
26
+ /** Sort and export value when it differs from the visible text. */
27
+ values?: Record<string, string>;
28
+ href?: string;
29
+ }
30
+
31
+ export interface DataTableProps {
32
+ id?: string;
33
+ title?: string;
34
+ columns: DataTableColumn[];
35
+ rows: DataTableRow[];
36
+ pageSize?: number;
37
+ searchPlaceholder?: string;
38
+ selectable?: boolean;
39
+ draggable?: boolean;
40
+ /** Trash and archive pills. Default true. */
41
+ filters?: boolean;
42
+ /** Demo mode: trash, restore, duplicate and delete change the rows in the page. */
43
+ live?: boolean;
44
+ /** Page actions on the right of the toolbar (New article, …). */
45
+ actions?: ReactNode;
46
+ onBulk?: (action: string, keys: string[]) => void;
47
+ }
48
+
49
+ const PAGE_SIZES = [10, 15, 20, 50, 100];
50
+
51
+ /** Korrekt / OmniaDesk list table. The script in @birdycom/omniaui/data-table.js does the work. */
52
+ export function DataTable({
53
+ id,
54
+ title = "List",
55
+ columns,
56
+ rows,
57
+ pageSize = 15,
58
+ searchPlaceholder = "Search...",
59
+ selectable = true,
60
+ draggable = true,
61
+ filters = true,
62
+ live = false,
63
+ actions,
64
+ onBulk,
65
+ }: DataTableProps) {
66
+ const autoId = useId().replace(/:/g, "");
67
+ const tableId = id || `oc-dt-${autoId}`;
68
+ const rootRef = useRef<HTMLDivElement>(null);
69
+ const onBulkRef = useRef(onBulk);
70
+ onBulkRef.current = onBulk;
71
+
72
+ useEffect(() => {
73
+ const root = rootRef.current;
74
+ if (!root) return;
75
+ bindDataTable(root);
76
+ const onBulkEvent = (event: Event) => {
77
+ const detail = (event as CustomEvent<{ action: string; keys: string[] }>).detail;
78
+ onBulkRef.current?.(detail.action, detail.keys);
79
+ };
80
+ root.addEventListener("oc-datatable-bulk", onBulkEvent);
81
+ return () => root.removeEventListener("oc-datatable-bulk", onBulkEvent);
82
+ }, [columns, rows]);
83
+
84
+ const lead = (selectable ? 1 : 0) + (draggable ? 1 : 0);
85
+ const sizes = [...new Set([...PAGE_SIZES, pageSize])].sort((a, b) => a - b);
86
+
87
+ return (
88
+ <div
89
+ ref={rootRef}
90
+ className="oc-data-table oc-column-menu-scope"
91
+ id={tableId}
92
+ data-page-size={pageSize}
93
+ data-oc-live={live ? "true" : undefined}
94
+ >
95
+ <div className="oc-data-table-toolbar">
96
+ <div className="oc-data-table-heading">
97
+ <span className="oc-data-table-title">{title}</span>
98
+ <button className="oc-btn oc-btn--outline oc-btn--icon oc-btn--sm" type="button" popoverTarget={`${tableId}-menu`} aria-label="Table actions">⚙</button>
99
+ <div className="oc-dropdown-menu" id={`${tableId}-menu`} popover="auto">
100
+ <button className="oc-dropdown-item" type="button" data-oc-export="">Export CSV</button>
101
+ <button className="oc-dropdown-item" type="button" data-oc-import="">Import CSV</button>
102
+ <input className="oc-data-table-file" type="file" accept=".csv,text/csv" data-oc-file="" />
103
+ <div className="oc-dropdown-separator" />
104
+ <button className="oc-dropdown-item" type="button" data-oc-density="comfortable">Comfortable rows</button>
105
+ <button className="oc-dropdown-item" type="button" data-oc-density="compact">Compact rows</button>
106
+ <button className="oc-dropdown-item" type="button" data-oc-reset-widths="">Reset column widths</button>
107
+ </div>
108
+ </div>
109
+ <input className="oc-input oc-data-table-search" type="search" placeholder={searchPlaceholder} aria-label={`Search ${title}`} />
110
+ {filters && (
111
+ <div className="oc-filter-pills" role="group" aria-label="Trash filter">
112
+ <button className="oc-filter-pill" type="button" data-oc-filter="active" aria-current="true">Active only</button>
113
+ <button className="oc-filter-pill" type="button" data-oc-filter="all">Including trash</button>
114
+ <button className="oc-filter-pill" type="button" data-oc-filter="deleted">Trash only</button>
115
+ <button className="oc-filter-pill" type="button" data-oc-filter="archived">Archive only</button>
116
+ </div>
117
+ )}
118
+ <div className="oc-data-table-view" role="group" aria-label="View">
119
+ <button className="oc-btn oc-btn--outline oc-btn--sm" type="button" data-oc-view="table" aria-pressed="true">Table</button>
120
+ <button className="oc-btn oc-btn--outline oc-btn--sm" type="button" data-oc-view="grid">Cards</button>
121
+ </div>
122
+ {actions && <div className="oc-data-table-actions">{actions}</div>}
123
+ <p className="oc-data-table-sort-summary" data-oc-sort-summary="" role="status" />
124
+ </div>
125
+ <div className="oc-table-toolbar">
126
+ <span className="oc-table-toolbar-spacer" />
127
+ <button className="oc-btn oc-btn--outline oc-btn--sm" type="button" popoverTarget={`${tableId}-columns`}>Columns</button>
128
+ </div>
129
+ <div className="oc-column-menu" id={`${tableId}-columns`} popover="auto">
130
+ <p className="oc-column-menu-title">Visible columns</p>
131
+ <form className="oc-column-menu-list">
132
+ {columns.map((col, index) => col.hideable === false ? null : (
133
+ <label className="oc-column-menu-item" key={col.key}>
134
+ <input type="checkbox" data-col={lead + index + 1} defaultChecked={!col.defaultHidden} /> {col.header}
135
+ </label>
136
+ ))}
137
+ </form>
138
+ </div>
139
+ <div className="oc-data-table-bulk" hidden>
140
+ <span data-oc-selected-count="">0 selected</span>
141
+ <button className="oc-btn oc-btn--outline oc-btn--sm" type="button" data-oc-bulk="export">Export selection</button>
142
+ <button className="oc-btn oc-btn--outline oc-btn--sm" type="button" data-oc-bulk="duplicate">Duplicate</button>
143
+ <button className="oc-btn oc-btn--outline oc-btn--sm" type="button" data-oc-bulk="archive">Move to trash</button>
144
+ <button className="oc-btn oc-btn--outline oc-btn--sm" type="button" data-oc-bulk="restore">Restore</button>
145
+ <button className="oc-btn oc-btn--destructive oc-btn--sm" type="button" data-oc-bulk="delete">Delete permanently</button>
146
+ <button className="oc-btn oc-btn--ghost oc-btn--sm" type="button" data-oc-bulk="clear">Clear</button>
147
+ </div>
148
+ <div className="oc-data-table-frame">
149
+ <div className="oc-data-table-scroll">
150
+ <table className="oc-table oc-data-table-table">
151
+ <thead>
152
+ <tr>
153
+ {selectable && <th data-oc-col="select"><input className="oc-checkbox" type="checkbox" data-oc-select-all="" aria-label="Select all" /></th>}
154
+ {draggable && <th data-oc-col="order"><span className="oc-visually-hidden">Order</span></th>}
155
+ {columns.map((col) => (
156
+ <th key={col.key} data-oc-col={col.key} data-oc-sort={col.numeric ? "numeric" : "text"} data-numeric={col.numeric ? "" : undefined}>
157
+ <span className="oc-data-table-th">
158
+ {col.sortable === false ? col.header : <button className="oc-table-sort" type="button">{col.header}</button>}
159
+ <span className="oc-data-table-resize" role="separator" aria-orientation="vertical" aria-label={`Resize ${col.header}`} />
160
+ </span>
161
+ </th>
162
+ ))}
163
+ <th data-oc-col="actions"><span className="oc-visually-hidden">Actions</span></th>
164
+ </tr>
165
+ </thead>
166
+ <tbody>
167
+ {rows.map((row) => (
168
+ <tr key={row.key} data-oc-row="" data-oc-key={row.key} data-oc-status={row.status || "active"} data-oc-href={row.href}>
169
+ {selectable && (
170
+ <td data-oc-col="select"><input className="oc-checkbox" type="checkbox" data-oc-select="" aria-label={`Select ${row.cells.name || row.key}`} /></td>
171
+ )}
172
+ {draggable && (
173
+ <td data-oc-col="order" className="oc-sortable-row-grip-cell">
174
+ <button className="oc-sortable-row-grip" type="button" aria-label={`Move ${row.cells.name || row.key}`} />
175
+ </td>
176
+ )}
177
+ {columns.map((col) => (
178
+ <td key={col.key} data-oc-col={col.key} data-numeric={col.numeric ? "" : undefined} data-oc-value={row.values?.[col.key]}>
179
+ {row.cells[col.key] ?? ""}
180
+ </td>
181
+ ))}
182
+ <td data-oc-col="actions" className="oc-table-actions"><button className="oc-table-row-action" type="button">Edit</button></td>
183
+ </tr>
184
+ ))}
185
+ </tbody>
186
+ </table>
187
+ </div>
188
+ <p className="oc-data-table-empty" hidden>No results</p>
189
+ </div>
190
+ <div className="oc-data-table-footer">
191
+ <p className="oc-data-table-summary" data-oc-summary="" />
192
+ <label className="oc-data-table-per-page">Rows per page
193
+ <select className="oc-select" data-oc-page-size="" aria-label="Rows per page" defaultValue={String(pageSize)}>
194
+ {sizes.map((n) => <option key={n} value={n}>{n}</option>)}
195
+ </select>
196
+ </label>
197
+ <nav className="oc-data-table-pager" data-oc-pager="" aria-label="Pagination" />
198
+ </div>
199
+ </div>
200
+ );
201
+ }
@@ -0,0 +1,87 @@
1
+ // File: packages/react/src/components/DecimalInput.tsx
2
+ // Map: helpers (locale separators, parse, format) -> component (text field, optional unit frame)
3
+ // Same markup as packages/ui/snippets/decimal-input.html. Formats on blur with Intl.NumberFormat.
4
+ "use client";
5
+
6
+ import { forwardRef, useEffect, useState } from "react";
7
+ import type { InputHTMLAttributes } from "react";
8
+ import { cx } from "../utils";
9
+
10
+ export interface DecimalInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "defaultValue" | "type"> {
11
+ /** The number; null = empty. */
12
+ value?: number | null;
13
+ defaultValue?: number | null;
14
+ onValueChange?: (value: number | null) => void;
15
+ /** BCP 47 locale for separators, e.g. "nl-BE" (1.234,56) or "en-US" (1,234.56). */
16
+ locale?: string;
17
+ /** Fixed number of decimals shown after blur. */
18
+ decimals?: number;
19
+ /** Unit text inside the frame (m², kg). */
20
+ unit?: string;
21
+ /** Left-aligned digits. */
22
+ alignStart?: boolean;
23
+ error?: boolean;
24
+ }
25
+
26
+ /** Group and decimal separator of a locale, e.g. nl-BE -> { group: ".", decimal: "," }. */
27
+ function separators(locale: string) {
28
+ const parts = new Intl.NumberFormat(locale).formatToParts(1234.5);
29
+ return { group: parts.find((p) => p.type === "group")?.value ?? ",", decimal: parts.find((p) => p.type === "decimal")?.value ?? "." };
30
+ }
31
+
32
+ /** "1.234,56" (nl-BE) -> 1234.56. Returns null for empty or unreadable text. */
33
+ export function parseDecimal(text: string, locale = "nl-BE"): number | null {
34
+ const { group, decimal } = separators(locale);
35
+ const clean = text.trim().split(group).join("").replace(decimal, ".").replace(/\s/g, "");
36
+ if (!clean) return null;
37
+ const n = Number(clean);
38
+ return Number.isFinite(n) ? n : null;
39
+ }
40
+
41
+ /** 1234.56 -> "1.234,56" (nl-BE, 2 decimals). */
42
+ export function formatDecimal(n: number | null | undefined, locale = "nl-BE", decimals = 2): string {
43
+ if (n == null) return "";
44
+ return new Intl.NumberFormat(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }).format(n);
45
+ }
46
+
47
+ /** `.oc-decimal-input`: free typing, formatted on blur, value reported as a number. */
48
+ export const DecimalInput = forwardRef<HTMLInputElement, DecimalInputProps>(function DecimalInput(
49
+ { value, defaultValue, onValueChange, locale = "nl-BE", decimals = 2, unit, alignStart, error, className, onBlur, onFocus, ...rest },
50
+ ref,
51
+ ) {
52
+ const [text, setText] = useState(() => formatDecimal(value ?? defaultValue, locale, decimals));
53
+ const [focused, setFocused] = useState(false);
54
+
55
+ // Controlled: show a new outside value, but never while the user is typing.
56
+ useEffect(() => {
57
+ if (value !== undefined && !focused) setText(formatDecimal(value, locale, decimals));
58
+ }, [value, locale, decimals, focused]);
59
+
60
+ const input = (
61
+ <input
62
+ ref={ref}
63
+ className={cx("oc-input", "oc-decimal-input", alignStart && "oc-decimal-input--start", className)}
64
+ type="text"
65
+ inputMode="decimal"
66
+ aria-invalid={error ? true : undefined}
67
+ value={text}
68
+ onFocus={(e) => { setFocused(true); onFocus?.(e); }}
69
+ onChange={(e) => { setText(e.target.value); onValueChange?.(parseDecimal(e.target.value, locale)); }}
70
+ onBlur={(e) => {
71
+ setFocused(false);
72
+ const n = parseDecimal(text, locale);
73
+ if (n != null || !text.trim()) setText(formatDecimal(n, locale, decimals));
74
+ onBlur?.(e);
75
+ }}
76
+ {...rest}
77
+ />
78
+ );
79
+
80
+ if (!unit) return input;
81
+ return (
82
+ <div className="oc-decimal-field">
83
+ {input}
84
+ <span className="oc-decimal-field-unit">{unit}</span>
85
+ </div>
86
+ );
87
+ });
@@ -0,0 +1,38 @@
1
+ import { forwardRef } from "react";
2
+ import type { HTMLAttributes, ReactNode } from "react";
3
+ import { cx } from "../utils";
4
+
5
+ export interface DefinitionItem {
6
+ term: ReactNode;
7
+ detail: ReactNode;
8
+ }
9
+
10
+ export interface DefinitionListProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
11
+ title?: ReactNode;
12
+ description?: ReactNode;
13
+ items: DefinitionItem[];
14
+ /** Optional footer (actions). */
15
+ footer?: ReactNode;
16
+ }
17
+
18
+ /** `<div class="oc-dl-panel" role="region">` with a `dl` of label / value rows. */
19
+ export const DefinitionList = forwardRef<HTMLDivElement, DefinitionListProps>(function DefinitionList(
20
+ { title, description, items, footer, className, ...rest },
21
+ ref,
22
+ ) {
23
+ return (
24
+ <div ref={ref} className={cx("oc-dl-panel", className)} role="region" {...rest}>
25
+ {title != null && <h3 className="oc-dl-title">{title}</h3>}
26
+ {description != null && <p className="oc-dl-description">{description}</p>}
27
+ <dl className="oc-dl">
28
+ {items.map((item, i) => (
29
+ <div className="oc-dl-row" key={i}>
30
+ <dt className="oc-dl-term">{item.term}</dt>
31
+ <dd className="oc-dl-detail">{item.detail}</dd>
32
+ </div>
33
+ ))}
34
+ </dl>
35
+ {footer != null && <div className="oc-dl-footer">{footer}</div>}
36
+ </div>
37
+ );
38
+ });