@birdycom/omniaui-react 0.11.0 → 0.11.1

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 (143) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +21 -21
  3. package/package.json +71 -71
  4. package/src/blocks/BlockAboutTimeline.tsx +46 -46
  5. package/src/blocks/BlockAppHeader.tsx +52 -52
  6. package/src/blocks/BlockAppShell.tsx +56 -56
  7. package/src/blocks/BlockAuth.tsx +53 -53
  8. package/src/blocks/BlockAuthSplit.tsx +84 -84
  9. package/src/blocks/BlockContactGrid.tsx +71 -71
  10. package/src/blocks/BlockCta.tsx +23 -23
  11. package/src/blocks/BlockDataTable.tsx +57 -57
  12. package/src/blocks/BlockFaq.tsx +55 -55
  13. package/src/blocks/BlockFeatures.tsx +38 -38
  14. package/src/blocks/BlockFooter.tsx +50 -50
  15. package/src/blocks/BlockFormPage.tsx +26 -26
  16. package/src/blocks/BlockHero.tsx +27 -27
  17. package/src/blocks/BlockHeroCode.tsx +63 -63
  18. package/src/blocks/BlockHeroOrbit.tsx +73 -73
  19. package/src/blocks/BlockHeroSplit.tsx +28 -28
  20. package/src/blocks/BlockModuleHeader.tsx +34 -34
  21. package/src/blocks/BlockPageHero.tsx +25 -25
  22. package/src/blocks/BlockPortfolioGrid.tsx +50 -50
  23. package/src/blocks/BlockPricing.tsx +62 -62
  24. package/src/blocks/BlockProcess.tsx +50 -50
  25. package/src/blocks/BlockProductGrid.tsx +54 -54
  26. package/src/blocks/BlockProfileHeader.tsx +57 -57
  27. package/src/blocks/BlockShowcase.tsx +58 -58
  28. package/src/blocks/BlockSiteFooter.tsx +60 -60
  29. package/src/blocks/BlockSiteHeader.tsx +42 -42
  30. package/src/blocks/BlockSpotlight.tsx +58 -58
  31. package/src/blocks/BlockStats.tsx +26 -26
  32. package/src/blocks/BlockStatsCounter.tsx +31 -31
  33. package/src/blocks/BlockTeamList.tsx +48 -48
  34. package/src/blocks/BlockTwoFactor.tsx +57 -57
  35. package/src/blocks/BlockWizardDialog.tsx +101 -101
  36. package/src/components/Accordion.tsx +31 -31
  37. package/src/components/AccordionAccent.tsx +39 -39
  38. package/src/components/AffixInput.tsx +47 -47
  39. package/src/components/Alert.tsx +24 -24
  40. package/src/components/AlertDialog.tsx +60 -60
  41. package/src/components/AlphaTabs.tsx +63 -63
  42. package/src/components/Attachment.tsx +140 -140
  43. package/src/components/Avatar.tsx +32 -32
  44. package/src/components/BackLink.tsx +11 -11
  45. package/src/components/Badge.tsx +14 -14
  46. package/src/components/BarcodeInput.tsx +64 -64
  47. package/src/components/Breadcrumb.tsx +32 -32
  48. package/src/components/BrowserFrame.tsx +48 -48
  49. package/src/components/BusyOverlay.tsx +52 -52
  50. package/src/components/Button.tsx +49 -49
  51. package/src/components/Card.tsx +27 -27
  52. package/src/components/ChartLine.tsx +119 -119
  53. package/src/components/ChatComposer.tsx +73 -73
  54. package/src/components/Checkbox.tsx +14 -14
  55. package/src/components/Checklist.tsx +45 -45
  56. package/src/components/ChipFilter.tsx +60 -60
  57. package/src/components/Collapsible.tsx +43 -43
  58. package/src/components/ColorField.tsx +48 -48
  59. package/src/components/ColorSelect.tsx +49 -49
  60. package/src/components/ColumnMenu.tsx +66 -66
  61. package/src/components/Command.tsx +113 -113
  62. package/src/components/ConfirmDialog.tsx +95 -95
  63. package/src/components/CookieConsent.tsx +137 -137
  64. package/src/components/CopyField.tsx +44 -44
  65. package/src/components/Counter.tsx +21 -21
  66. package/src/components/DataTable.tsx +201 -201
  67. package/src/components/DecimalInput.tsx +87 -87
  68. package/src/components/DefinitionList.tsx +38 -38
  69. package/src/components/DocViewer.tsx +110 -110
  70. package/src/components/Dropdown.tsx +52 -52
  71. package/src/components/Dropzone.tsx +46 -46
  72. package/src/components/EmojiPicker.tsx +72 -72
  73. package/src/components/EmptyState.tsx +27 -27
  74. package/src/components/EntityCard.tsx +56 -56
  75. package/src/components/FactList.tsx +43 -43
  76. package/src/components/FilterCounts.tsx +50 -50
  77. package/src/components/FlipCard.tsx +55 -55
  78. package/src/components/FormField.tsx +54 -54
  79. package/src/components/Greeting.tsx +63 -63
  80. package/src/components/Heading.tsx +27 -27
  81. package/src/components/Icon.tsx +25 -25
  82. package/src/components/ImageZoom.tsx +51 -51
  83. package/src/components/Input.tsx +21 -21
  84. package/src/components/InputGroup.tsx +24 -24
  85. package/src/components/LanguageSelect.tsx +64 -64
  86. package/src/components/LauncherCard.tsx +50 -50
  87. package/src/components/Lightbox.tsx +83 -83
  88. package/src/components/LinkArrow.tsx +27 -27
  89. package/src/components/ListToolbar.tsx +67 -67
  90. package/src/components/Marquee.tsx +38 -38
  91. package/src/components/MediaCard.tsx +47 -47
  92. package/src/components/MediaRow.tsx +53 -53
  93. package/src/components/MediaThumb.tsx +49 -49
  94. package/src/components/Message.tsx +171 -171
  95. package/src/components/MessageThread.tsx +177 -177
  96. package/src/components/Modal.tsx +81 -81
  97. package/src/components/Navmenu.tsx +62 -62
  98. package/src/components/NextTeaser.tsx +29 -29
  99. package/src/components/NumberInput.tsx +64 -64
  100. package/src/components/OtpInput.tsx +48 -48
  101. package/src/components/PageHeader.tsx +43 -43
  102. package/src/components/Pagination.tsx +42 -42
  103. package/src/components/PasswordInput.tsx +36 -36
  104. package/src/components/PasswordRequirements.tsx +46 -46
  105. package/src/components/PhoneInput.tsx +93 -93
  106. package/src/components/Popover.tsx +34 -34
  107. package/src/components/PriceCard.tsx +83 -83
  108. package/src/components/ProductMark.tsx +35 -35
  109. package/src/components/Progress.tsx +32 -32
  110. package/src/components/ProjectCard.tsx +46 -46
  111. package/src/components/Prose.tsx +79 -79
  112. package/src/components/RecoveryCodes.tsx +65 -65
  113. package/src/components/Segmented.tsx +47 -47
  114. package/src/components/Select.tsx +27 -27
  115. package/src/components/SeverityCard.tsx +53 -53
  116. package/src/components/Sheet.tsx +62 -62
  117. package/src/components/Skeleton.tsx +12 -12
  118. package/src/components/SortableRow.tsx +46 -46
  119. package/src/components/StackCard.tsx +40 -40
  120. package/src/components/StackList.tsx +42 -42
  121. package/src/components/StatCard.tsx +49 -49
  122. package/src/components/Stepper.tsx +54 -54
  123. package/src/components/Switch.tsx +18 -18
  124. package/src/components/Table.tsx +75 -75
  125. package/src/components/TableActions.tsx +54 -54
  126. package/src/components/TableCompact.tsx +61 -61
  127. package/src/components/Tabs.tsx +45 -45
  128. package/src/components/TagInput.tsx +73 -73
  129. package/src/components/TerminalLog.tsx +63 -63
  130. package/src/components/Textarea.tsx +19 -19
  131. package/src/components/ThemeSwitch.tsx +45 -45
  132. package/src/components/TimeInput.tsx +76 -76
  133. package/src/components/Timeline.tsx +45 -45
  134. package/src/components/Toast.tsx +138 -138
  135. package/src/components/ToggleField.tsx +39 -39
  136. package/src/components/Tooltip.tsx +14 -14
  137. package/src/components/TopProgress.tsx +54 -54
  138. package/src/components/Tree.tsx +97 -97
  139. package/src/components/UpgradeBanner.tsx +62 -62
  140. package/src/components/User.tsx +31 -31
  141. package/src/components/VersionBanner.tsx +41 -41
  142. package/src/hooks.tsx +141 -141
  143. package/src/utils.ts +25 -25
@@ -1,95 +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
- });
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
+ });
@@ -1,137 +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
- });
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
+ });
@@ -1,44 +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
- });
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
+ });
@@ -1,21 +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
- });
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
+ });