@birdycom/omniaui-react 0.11.0 → 0.11.2

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,43 +1,43 @@
1
- "use client";
2
-
3
- import { forwardRef } from "react";
4
- import type { DetailsHTMLAttributes, ReactNode, SyntheticEvent } from "react";
5
- import { cx } from "../utils";
6
- import { useControllable } from "../hooks";
7
-
8
- export interface CollapsibleProps extends Omit<DetailsHTMLAttributes<HTMLDetailsElement>, "title" | "open" | "onToggle"> {
9
- /** Summary text (`oc-collapsible-title`). */
10
- title: ReactNode;
11
- /** Optional leading icon, e.g. an inline `<svg className="oc-collapsible-icon">`. */
12
- icon?: ReactNode;
13
- /** Compact paddings. */
14
- dense?: boolean;
15
- open?: boolean;
16
- defaultOpen?: boolean;
17
- onOpenChange?: (open: boolean) => void;
18
- }
19
-
20
- /** `<details class="oc-collapsible">` card-style section. */
21
- export const Collapsible = forwardRef<HTMLDetailsElement, CollapsibleProps>(function Collapsible(
22
- { title, icon, dense, open, defaultOpen, onOpenChange, className, children, ...rest },
23
- ref,
24
- ) {
25
- const [isOpen, setOpen] = useControllable<boolean>(open, defaultOpen ?? false, onOpenChange);
26
- return (
27
- <details
28
- ref={ref}
29
- className={cx("oc-collapsible", dense && "oc-collapsible--dense", className)}
30
- open={isOpen}
31
- onToggle={(e: SyntheticEvent<HTMLDetailsElement>) => {
32
- if (e.currentTarget.open !== isOpen) setOpen(e.currentTarget.open);
33
- }}
34
- {...rest}
35
- >
36
- <summary>
37
- {icon}
38
- <span className="oc-collapsible-title">{title}</span>
39
- </summary>
40
- <div className="oc-collapsible-content">{children}</div>
41
- </details>
42
- );
43
- });
1
+ "use client";
2
+
3
+ import { forwardRef } from "react";
4
+ import type { DetailsHTMLAttributes, ReactNode, SyntheticEvent } from "react";
5
+ import { cx } from "../utils";
6
+ import { useControllable } from "../hooks";
7
+
8
+ export interface CollapsibleProps extends Omit<DetailsHTMLAttributes<HTMLDetailsElement>, "title" | "open" | "onToggle"> {
9
+ /** Summary text (`oc-collapsible-title`). */
10
+ title: ReactNode;
11
+ /** Optional leading icon, e.g. an inline `<svg className="oc-collapsible-icon">`. */
12
+ icon?: ReactNode;
13
+ /** Compact paddings. */
14
+ dense?: boolean;
15
+ open?: boolean;
16
+ defaultOpen?: boolean;
17
+ onOpenChange?: (open: boolean) => void;
18
+ }
19
+
20
+ /** `<details class="oc-collapsible">` card-style section. */
21
+ export const Collapsible = forwardRef<HTMLDetailsElement, CollapsibleProps>(function Collapsible(
22
+ { title, icon, dense, open, defaultOpen, onOpenChange, className, children, ...rest },
23
+ ref,
24
+ ) {
25
+ const [isOpen, setOpen] = useControllable<boolean>(open, defaultOpen ?? false, onOpenChange);
26
+ return (
27
+ <details
28
+ ref={ref}
29
+ className={cx("oc-collapsible", dense && "oc-collapsible--dense", className)}
30
+ open={isOpen}
31
+ onToggle={(e: SyntheticEvent<HTMLDetailsElement>) => {
32
+ if (e.currentTarget.open !== isOpen) setOpen(e.currentTarget.open);
33
+ }}
34
+ {...rest}
35
+ >
36
+ <summary>
37
+ {icon}
38
+ <span className="oc-collapsible-title">{title}</span>
39
+ </summary>
40
+ <div className="oc-collapsible-content">{children}</div>
41
+ </details>
42
+ );
43
+ });
@@ -1,48 +1,48 @@
1
- "use client";
2
-
3
- import { forwardRef, useState } from "react";
4
- import type { HTMLAttributes } from "react";
5
- import { cx } from "../utils";
6
- import { useControllable } from "../hooks";
7
-
8
- export interface ColorFieldProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "defaultValue"> {
9
- /** Hex value, e.g. `#3366cc` (controlled). */
10
- value?: string;
11
- defaultValue?: string;
12
- onChange?: (value: string) => void;
13
- /** Id of the text input (so a <label for> can point at it). */
14
- id?: string;
15
- name?: string;
16
- disabled?: boolean;
17
- }
18
-
19
- const HEX = /^#[0-9a-f]{6}$/i;
20
-
21
- /** Native color picker + monospaced hex text input, kept in sync. */
22
- export const ColorField = forwardRef<HTMLDivElement, ColorFieldProps>(function ColorField(
23
- { value, defaultValue = "#000000", onChange, id, name, disabled, className, ...rest },
24
- ref,
25
- ) {
26
- const [color, setColor] = useControllable<string>(value, defaultValue, onChange);
27
- const [lastValid, setLastValid] = useState(HEX.test(color) ? color : "#000000");
28
- const picker = HEX.test(color) ? color : lastValid;
29
- const update = (next: string) => {
30
- if (HEX.test(next)) setLastValid(next);
31
- setColor(next);
32
- };
33
- return (
34
- <div ref={ref} className={cx("oc-color-field", className)} {...rest}>
35
- <input type="color" aria-label="Pick a color" value={picker} disabled={disabled} onChange={(e) => update(e.target.value)} />
36
- <input
37
- className="oc-input"
38
- type="text"
39
- id={id}
40
- name={name}
41
- aria-label="Color value"
42
- value={color}
43
- disabled={disabled}
44
- onChange={(e) => update(e.target.value)}
45
- />
46
- </div>
47
- );
48
- });
1
+ "use client";
2
+
3
+ import { forwardRef, useState } from "react";
4
+ import type { HTMLAttributes } from "react";
5
+ import { cx } from "../utils";
6
+ import { useControllable } from "../hooks";
7
+
8
+ export interface ColorFieldProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "defaultValue"> {
9
+ /** Hex value, e.g. `#3366cc` (controlled). */
10
+ value?: string;
11
+ defaultValue?: string;
12
+ onChange?: (value: string) => void;
13
+ /** Id of the text input (so a <label for> can point at it). */
14
+ id?: string;
15
+ name?: string;
16
+ disabled?: boolean;
17
+ }
18
+
19
+ const HEX = /^#[0-9a-f]{6}$/i;
20
+
21
+ /** Native color picker + monospaced hex text input, kept in sync. */
22
+ export const ColorField = forwardRef<HTMLDivElement, ColorFieldProps>(function ColorField(
23
+ { value, defaultValue = "#000000", onChange, id, name, disabled, className, ...rest },
24
+ ref,
25
+ ) {
26
+ const [color, setColor] = useControllable<string>(value, defaultValue, onChange);
27
+ const [lastValid, setLastValid] = useState(HEX.test(color) ? color : "#000000");
28
+ const picker = HEX.test(color) ? color : lastValid;
29
+ const update = (next: string) => {
30
+ if (HEX.test(next)) setLastValid(next);
31
+ setColor(next);
32
+ };
33
+ return (
34
+ <div ref={ref} className={cx("oc-color-field", className)} {...rest}>
35
+ <input type="color" aria-label="Pick a color" value={picker} disabled={disabled} onChange={(e) => update(e.target.value)} />
36
+ <input
37
+ className="oc-input"
38
+ type="text"
39
+ id={id}
40
+ name={name}
41
+ aria-label="Color value"
42
+ value={color}
43
+ disabled={disabled}
44
+ onChange={(e) => update(e.target.value)}
45
+ />
46
+ </div>
47
+ );
48
+ });
@@ -1,49 +1,49 @@
1
- // File: packages/react/src/components/ColorSelect.tsx
2
- // Map: types -> component (fieldset radiogroup -> one label per color: radio + swatch + name)
3
- // Same markup as packages/ui/snippets/color-select.html.
4
- "use client";
5
-
6
- import { forwardRef, useId } from "react";
7
- import type { CSSProperties, FieldsetHTMLAttributes } from "react";
8
- import { cx } from "../utils";
9
- import { useControllable } from "../hooks";
10
-
11
- export interface ColorSelectOption {
12
- value: string;
13
- label: string;
14
- /** CSS color, use Omnia tokens: "var(--color-red-70)". */
15
- color: string;
16
- disabled?: boolean;
17
- }
18
-
19
- export interface ColorSelectProps extends Omit<FieldsetHTMLAttributes<HTMLFieldSetElement>, "onChange" | "defaultValue"> {
20
- options: ColorSelectOption[];
21
- value?: string;
22
- defaultValue?: string;
23
- onChange?: (value: string) => void;
24
- /** Radio group name (generated when omitted). */
25
- name?: string;
26
- /** Rows with visible names instead of a swatch grid. */
27
- list?: boolean;
28
- size?: "sm" | "md";
29
- }
30
-
31
- /** `.oc-color-select`: radio swatches; the color is passed through the --oc-swatch variable. */
32
- export const ColorSelect = forwardRef<HTMLFieldSetElement, ColorSelectProps>(function ColorSelect(
33
- { options, value, defaultValue, onChange, name, list, size = "md", className, ...rest },
34
- ref,
35
- ) {
36
- const group = name ?? `oc-color-${useId().replace(/:/g, "")}`;
37
- const [current, setCurrent] = useControllable<string>(value, defaultValue ?? "", onChange);
38
- return (
39
- <fieldset ref={ref} className={cx("oc-color-select", list && "oc-color-select--list", size === "sm" && "oc-color-select--sm", className)} role="radiogroup" {...rest}>
40
- {options.map((o) => (
41
- <label key={o.value} className="oc-color-select-option" style={{ "--oc-swatch": o.color } as CSSProperties}>
42
- <input type="radio" name={group} value={o.value} disabled={o.disabled} checked={current === o.value} onChange={() => setCurrent(o.value)} />
43
- <span className="oc-color-select-swatch" />
44
- <span className="oc-color-select-name">{o.label}</span>
45
- </label>
46
- ))}
47
- </fieldset>
48
- );
49
- });
1
+ // File: packages/react/src/components/ColorSelect.tsx
2
+ // Map: types -> component (fieldset radiogroup -> one label per color: radio + swatch + name)
3
+ // Same markup as packages/ui/snippets/color-select.html.
4
+ "use client";
5
+
6
+ import { forwardRef, useId } from "react";
7
+ import type { CSSProperties, FieldsetHTMLAttributes } from "react";
8
+ import { cx } from "../utils";
9
+ import { useControllable } from "../hooks";
10
+
11
+ export interface ColorSelectOption {
12
+ value: string;
13
+ label: string;
14
+ /** CSS color, use Omnia tokens: "var(--color-red-70)". */
15
+ color: string;
16
+ disabled?: boolean;
17
+ }
18
+
19
+ export interface ColorSelectProps extends Omit<FieldsetHTMLAttributes<HTMLFieldSetElement>, "onChange" | "defaultValue"> {
20
+ options: ColorSelectOption[];
21
+ value?: string;
22
+ defaultValue?: string;
23
+ onChange?: (value: string) => void;
24
+ /** Radio group name (generated when omitted). */
25
+ name?: string;
26
+ /** Rows with visible names instead of a swatch grid. */
27
+ list?: boolean;
28
+ size?: "sm" | "md";
29
+ }
30
+
31
+ /** `.oc-color-select`: radio swatches; the color is passed through the --oc-swatch variable. */
32
+ export const ColorSelect = forwardRef<HTMLFieldSetElement, ColorSelectProps>(function ColorSelect(
33
+ { options, value, defaultValue, onChange, name, list, size = "md", className, ...rest },
34
+ ref,
35
+ ) {
36
+ const group = name ?? `oc-color-${useId().replace(/:/g, "")}`;
37
+ const [current, setCurrent] = useControllable<string>(value, defaultValue ?? "", onChange);
38
+ return (
39
+ <fieldset ref={ref} className={cx("oc-color-select", list && "oc-color-select--list", size === "sm" && "oc-color-select--sm", className)} role="radiogroup" {...rest}>
40
+ {options.map((o) => (
41
+ <label key={o.value} className="oc-color-select-option" style={{ "--oc-swatch": o.color } as CSSProperties}>
42
+ <input type="radio" name={group} value={o.value} disabled={o.disabled} checked={current === o.value} onChange={() => setCurrent(o.value)} />
43
+ <span className="oc-color-select-swatch" />
44
+ <span className="oc-color-select-name">{o.label}</span>
45
+ </label>
46
+ ))}
47
+ </fieldset>
48
+ );
49
+ });
@@ -1,66 +1,66 @@
1
- // File: packages/react/src/components/ColumnMenu.tsx
2
- // Map: ColumnMenu = scope -> toolbar trigger -> popover (title, form with one checkbox per column, reset) -> children (the table)
3
- // Hiding columns is pure CSS: an unchecked [data-col="n"] hides column n inside the scope (n = 1-10).
4
- // The checkboxes are uncontrolled, so "Reset" (a form reset) restores the defaults.
5
- import { forwardRef, useId } from "react";
6
- import type { HTMLAttributes, ReactNode } from "react";
7
- import { attrs, cx } from "../utils";
8
-
9
- export interface ColumnMenuColumn {
10
- label: ReactNode;
11
- /** Visible at the start (default true). */
12
- visible?: boolean;
13
- /** Always visible; the checkbox is disabled. */
14
- locked?: boolean;
15
- }
16
-
17
- export interface ColumnMenuProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
18
- /** One entry per table column, in column order (max 10). */
19
- columns: ColumnMenuColumn[];
20
- /** The table (`.oc-table`) the menu controls. */
21
- children: ReactNode;
22
- triggerLabel?: ReactNode;
23
- title?: ReactNode;
24
- resetLabel?: ReactNode;
25
- /** Called with the column number (1-based) and its new visibility. */
26
- onColumnToggle?: (column: number, visible: boolean) => void;
27
- }
28
-
29
- /** `.oc-column-menu-scope` with a `.oc-column-menu[popover]` column checklist. */
30
- export const ColumnMenu = forwardRef<HTMLDivElement, ColumnMenuProps>(function ColumnMenu(
31
- { columns, children, triggerLabel = "Columns", title = "Visible columns", resetLabel = "Reset", onColumnToggle, className, ...rest },
32
- ref,
33
- ) {
34
- const menuId = `oc-column-menu-${useId().replace(/:/g, "")}`;
35
- return (
36
- <div ref={ref} className={cx("oc-column-menu-scope", className)} {...rest}>
37
- <div className="oc-table-toolbar">
38
- <span className="oc-table-toolbar-spacer" />
39
- <button className="oc-btn oc-btn--outline oc-btn--sm" type="button" {...attrs({ popovertarget: menuId })}>
40
- {triggerLabel}
41
- </button>
42
- </div>
43
- <div className="oc-column-menu" id={menuId} {...attrs({ popover: "" })}>
44
- <p className="oc-column-menu-title">{title}</p>
45
- <form className="oc-column-menu-list">
46
- {columns.map((col, i) => (
47
- <label className="oc-column-menu-item" key={i}>
48
- <input
49
- type="checkbox"
50
- data-col={i + 1}
51
- defaultChecked={col.locked || col.visible !== false}
52
- disabled={col.locked}
53
- onChange={(e) => onColumnToggle?.(i + 1, e.currentTarget.checked)}
54
- />{" "}
55
- {col.label}
56
- </label>
57
- ))}
58
- <div className="oc-column-menu-footer">
59
- <button className="oc-btn oc-btn--ghost oc-btn--sm" type="reset">{resetLabel}</button>
60
- </div>
61
- </form>
62
- </div>
63
- {children}
64
- </div>
65
- );
66
- });
1
+ // File: packages/react/src/components/ColumnMenu.tsx
2
+ // Map: ColumnMenu = scope -> toolbar trigger -> popover (title, form with one checkbox per column, reset) -> children (the table)
3
+ // Hiding columns is pure CSS: an unchecked [data-col="n"] hides column n inside the scope (n = 1-10).
4
+ // The checkboxes are uncontrolled, so "Reset" (a form reset) restores the defaults.
5
+ import { forwardRef, useId } from "react";
6
+ import type { HTMLAttributes, ReactNode } from "react";
7
+ import { attrs, cx } from "../utils";
8
+
9
+ export interface ColumnMenuColumn {
10
+ label: ReactNode;
11
+ /** Visible at the start (default true). */
12
+ visible?: boolean;
13
+ /** Always visible; the checkbox is disabled. */
14
+ locked?: boolean;
15
+ }
16
+
17
+ export interface ColumnMenuProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
18
+ /** One entry per table column, in column order (max 10). */
19
+ columns: ColumnMenuColumn[];
20
+ /** The table (`.oc-table`) the menu controls. */
21
+ children: ReactNode;
22
+ triggerLabel?: ReactNode;
23
+ title?: ReactNode;
24
+ resetLabel?: ReactNode;
25
+ /** Called with the column number (1-based) and its new visibility. */
26
+ onColumnToggle?: (column: number, visible: boolean) => void;
27
+ }
28
+
29
+ /** `.oc-column-menu-scope` with a `.oc-column-menu[popover]` column checklist. */
30
+ export const ColumnMenu = forwardRef<HTMLDivElement, ColumnMenuProps>(function ColumnMenu(
31
+ { columns, children, triggerLabel = "Columns", title = "Visible columns", resetLabel = "Reset", onColumnToggle, className, ...rest },
32
+ ref,
33
+ ) {
34
+ const menuId = `oc-column-menu-${useId().replace(/:/g, "")}`;
35
+ return (
36
+ <div ref={ref} className={cx("oc-column-menu-scope", className)} {...rest}>
37
+ <div className="oc-table-toolbar">
38
+ <span className="oc-table-toolbar-spacer" />
39
+ <button className="oc-btn oc-btn--outline oc-btn--sm" type="button" {...attrs({ popovertarget: menuId })}>
40
+ {triggerLabel}
41
+ </button>
42
+ </div>
43
+ <div className="oc-column-menu" id={menuId} {...attrs({ popover: "" })}>
44
+ <p className="oc-column-menu-title">{title}</p>
45
+ <form className="oc-column-menu-list">
46
+ {columns.map((col, i) => (
47
+ <label className="oc-column-menu-item" key={i}>
48
+ <input
49
+ type="checkbox"
50
+ data-col={i + 1}
51
+ defaultChecked={col.locked || col.visible !== false}
52
+ disabled={col.locked}
53
+ onChange={(e) => onColumnToggle?.(i + 1, e.currentTarget.checked)}
54
+ />{" "}
55
+ {col.label}
56
+ </label>
57
+ ))}
58
+ <div className="oc-column-menu-footer">
59
+ <button className="oc-btn oc-btn--ghost oc-btn--sm" type="reset">{resetLabel}</button>
60
+ </div>
61
+ </form>
62
+ </div>
63
+ {children}
64
+ </div>
65
+ );
66
+ });
@@ -1,113 +1,113 @@
1
- "use client";
2
-
3
- import { forwardRef, useId, useMemo, useState } from "react";
4
- import type { HTMLAttributes, KeyboardEvent, ReactNode } from "react";
5
- import { cx } from "../utils";
6
-
7
- export interface CommandItemData {
8
- id: string;
9
- label: string;
10
- /** Right-aligned keyboard hint. */
11
- shortcut?: string;
12
- disabled?: boolean;
13
- /** Extra search terms. */
14
- keywords?: string[];
15
- }
16
-
17
- export interface CommandGroupData {
18
- label?: string;
19
- items: CommandItemData[];
20
- }
21
-
22
- export interface CommandProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
23
- groups: CommandGroupData[];
24
- placeholder?: string;
25
- emptyText?: ReactNode;
26
- /** Search value (controlled). */
27
- query?: string;
28
- onQueryChange?: (query: string) => void;
29
- onSelect?: (item: CommandItemData) => void;
30
- }
31
-
32
- const matches = (item: CommandItemData, q: string) =>
33
- !q || [item.label, ...(item.keywords ?? [])].some((s) => s.toLowerCase().includes(q));
34
-
35
- /**
36
- * Command palette. The list is filtered by the search input (non-matching rows get `hidden`),
37
- * the active row carries `aria-selected="true"`; arrow keys move it and Enter selects.
38
- */
39
- export const Command = forwardRef<HTMLDivElement, CommandProps>(function Command(
40
- { groups, placeholder = "Type a command or search...", emptyText = "No results found.", query, onQueryChange, onSelect, className, ...rest },
41
- ref,
42
- ) {
43
- const listId = useId();
44
- const [innerQuery, setInnerQuery] = useState("");
45
- const q = (query ?? innerQuery).trim().toLowerCase();
46
- const [activeId, setActiveId] = useState<string | null>(null);
47
-
48
- const visible = useMemo(
49
- () => groups.flatMap((g) => g.items).filter((i) => !i.disabled && matches(i, q)),
50
- [groups, q],
51
- );
52
- const active = visible.find((i) => i.id === activeId) ?? visible[0];
53
-
54
- const move = (delta: number) => {
55
- if (!visible.length) return;
56
- const idx = Math.max(0, visible.findIndex((i) => i.id === active?.id));
57
- setActiveId(visible[(idx + delta + visible.length) % visible.length].id);
58
- };
59
- const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
60
- if (e.key === "ArrowDown") { e.preventDefault(); move(1); }
61
- else if (e.key === "ArrowUp") { e.preventDefault(); move(-1); }
62
- else if (e.key === "Enter" && active) { e.preventDefault(); onSelect?.(active); }
63
- };
64
-
65
- return (
66
- <div ref={ref} className={cx("oc-command", className)} role="combobox" aria-expanded="true" aria-haspopup="listbox" aria-controls={listId} {...rest}>
67
- <input
68
- className="oc-command-input"
69
- type="search"
70
- placeholder={placeholder}
71
- aria-label="Search commands"
72
- value={query ?? innerQuery}
73
- onChange={(e) => {
74
- setInnerQuery(e.target.value);
75
- setActiveId(null);
76
- onQueryChange?.(e.target.value);
77
- }}
78
- onKeyDown={onKeyDown}
79
- />
80
- <ul className="oc-command-list" role="listbox" id={listId}>
81
- {groups.map((group, gi) => {
82
- const anyVisible = group.items.some((i) => matches(i, q));
83
- return [
84
- gi > 0 && (
85
- <li key={`sep-${gi}`} className="oc-command-separator" role="separator" hidden={!anyVisible} />
86
- ),
87
- group.label && (
88
- <li key={`label-${gi}`} className="oc-command-group-label" role="presentation" hidden={!anyVisible}>{group.label}</li>
89
- ),
90
- ...group.items.map((item) => (
91
- <li
92
- key={item.id}
93
- className="oc-command-item"
94
- role="option"
95
- hidden={!matches(item, q)}
96
- aria-selected={active?.id === item.id ? true : undefined}
97
- aria-disabled={item.disabled ? true : undefined}
98
- onMouseMove={() => !item.disabled && setActiveId(item.id)}
99
- onClick={() => !item.disabled && onSelect?.(item)}
100
- >
101
- {item.label}
102
- {item.shortcut && <> <span className="oc-command-shortcut">{item.shortcut}</span></>}
103
- </li>
104
- )),
105
- ];
106
- })}
107
- </ul>
108
- <div className="oc-command-empty" hidden={groups.some((g) => g.items.some((i) => matches(i, q)))}>
109
- {emptyText}
110
- </div>
111
- </div>
112
- );
113
- });
1
+ "use client";
2
+
3
+ import { forwardRef, useId, useMemo, useState } from "react";
4
+ import type { HTMLAttributes, KeyboardEvent, ReactNode } from "react";
5
+ import { cx } from "../utils";
6
+
7
+ export interface CommandItemData {
8
+ id: string;
9
+ label: string;
10
+ /** Right-aligned keyboard hint. */
11
+ shortcut?: string;
12
+ disabled?: boolean;
13
+ /** Extra search terms. */
14
+ keywords?: string[];
15
+ }
16
+
17
+ export interface CommandGroupData {
18
+ label?: string;
19
+ items: CommandItemData[];
20
+ }
21
+
22
+ export interface CommandProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
23
+ groups: CommandGroupData[];
24
+ placeholder?: string;
25
+ emptyText?: ReactNode;
26
+ /** Search value (controlled). */
27
+ query?: string;
28
+ onQueryChange?: (query: string) => void;
29
+ onSelect?: (item: CommandItemData) => void;
30
+ }
31
+
32
+ const matches = (item: CommandItemData, q: string) =>
33
+ !q || [item.label, ...(item.keywords ?? [])].some((s) => s.toLowerCase().includes(q));
34
+
35
+ /**
36
+ * Command palette. The list is filtered by the search input (non-matching rows get `hidden`),
37
+ * the active row carries `aria-selected="true"`; arrow keys move it and Enter selects.
38
+ */
39
+ export const Command = forwardRef<HTMLDivElement, CommandProps>(function Command(
40
+ { groups, placeholder = "Type a command or search...", emptyText = "No results found.", query, onQueryChange, onSelect, className, ...rest },
41
+ ref,
42
+ ) {
43
+ const listId = useId();
44
+ const [innerQuery, setInnerQuery] = useState("");
45
+ const q = (query ?? innerQuery).trim().toLowerCase();
46
+ const [activeId, setActiveId] = useState<string | null>(null);
47
+
48
+ const visible = useMemo(
49
+ () => groups.flatMap((g) => g.items).filter((i) => !i.disabled && matches(i, q)),
50
+ [groups, q],
51
+ );
52
+ const active = visible.find((i) => i.id === activeId) ?? visible[0];
53
+
54
+ const move = (delta: number) => {
55
+ if (!visible.length) return;
56
+ const idx = Math.max(0, visible.findIndex((i) => i.id === active?.id));
57
+ setActiveId(visible[(idx + delta + visible.length) % visible.length].id);
58
+ };
59
+ const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
60
+ if (e.key === "ArrowDown") { e.preventDefault(); move(1); }
61
+ else if (e.key === "ArrowUp") { e.preventDefault(); move(-1); }
62
+ else if (e.key === "Enter" && active) { e.preventDefault(); onSelect?.(active); }
63
+ };
64
+
65
+ return (
66
+ <div ref={ref} className={cx("oc-command", className)} role="combobox" aria-expanded="true" aria-haspopup="listbox" aria-controls={listId} {...rest}>
67
+ <input
68
+ className="oc-command-input"
69
+ type="search"
70
+ placeholder={placeholder}
71
+ aria-label="Search commands"
72
+ value={query ?? innerQuery}
73
+ onChange={(e) => {
74
+ setInnerQuery(e.target.value);
75
+ setActiveId(null);
76
+ onQueryChange?.(e.target.value);
77
+ }}
78
+ onKeyDown={onKeyDown}
79
+ />
80
+ <ul className="oc-command-list" role="listbox" id={listId}>
81
+ {groups.map((group, gi) => {
82
+ const anyVisible = group.items.some((i) => matches(i, q));
83
+ return [
84
+ gi > 0 && (
85
+ <li key={`sep-${gi}`} className="oc-command-separator" role="separator" hidden={!anyVisible} />
86
+ ),
87
+ group.label && (
88
+ <li key={`label-${gi}`} className="oc-command-group-label" role="presentation" hidden={!anyVisible}>{group.label}</li>
89
+ ),
90
+ ...group.items.map((item) => (
91
+ <li
92
+ key={item.id}
93
+ className="oc-command-item"
94
+ role="option"
95
+ hidden={!matches(item, q)}
96
+ aria-selected={active?.id === item.id ? true : undefined}
97
+ aria-disabled={item.disabled ? true : undefined}
98
+ onMouseMove={() => !item.disabled && setActiveId(item.id)}
99
+ onClick={() => !item.disabled && onSelect?.(item)}
100
+ >
101
+ {item.label}
102
+ {item.shortcut && <> <span className="oc-command-shortcut">{item.shortcut}</span></>}
103
+ </li>
104
+ )),
105
+ ];
106
+ })}
107
+ </ul>
108
+ <div className="oc-command-empty" hidden={groups.some((g) => g.items.some((i) => matches(i, q)))}>
109
+ {emptyText}
110
+ </div>
111
+ </div>
112
+ );
113
+ });