@botiverse/raft-ui 0.59.2 → 0.60.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 (57) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/components/composer/composer-attachment.recipe.mjs +2 -3
  3. package/dist/components/composer/composer-attachments-scroll-area.d.mts +6 -0
  4. package/dist/components/composer/composer-attachments-scroll-area.mjs +20 -31
  5. package/dist/components/composer/composer.d.mts +8 -2
  6. package/dist/components/composer/composer.mjs +7 -9
  7. package/dist/components/composer/composer.recipe.mjs +1 -0
  8. package/dist/components/context-menu/context-menu.d.mts +1 -1
  9. package/dist/components/context-menu/context-menu.mjs +1 -1
  10. package/dist/components/dropdown-menu/dropdown-menu.d.mts +1 -1
  11. package/dist/components/dropdown-menu/dropdown-menu.mjs +2 -2
  12. package/dist/components/kbd/index.d.mts +5 -2
  13. package/dist/components/kbd/kbd-keys.d.mts +12 -0
  14. package/dist/components/kbd/kbd-keys.mjs +297 -0
  15. package/dist/components/kbd/kbd-shortcut.d.mts +34 -0
  16. package/dist/components/kbd/kbd-shortcut.mjs +79 -0
  17. package/dist/components/kbd/kbd.d.mts +55 -2
  18. package/dist/components/kbd/kbd.mjs +39 -15
  19. package/dist/components/markdown/markdown-parts.d.mts +7 -2
  20. package/dist/components/markdown/markdown-parts.mjs +8 -3
  21. package/dist/components/markdown/markdown.recipe.mjs +1 -5
  22. package/dist/components/message-list/message-list.mjs +9 -15
  23. package/dist/components/message-list/message-list.recipe.mjs +1 -1
  24. package/dist/components/scroll-area/class-names.mjs +19 -0
  25. package/dist/components/scroll-area/gutters-binding.mjs +4 -0
  26. package/dist/components/scroll-area/index.d.mts +6 -2
  27. package/dist/components/scroll-area/scroll-area-gutters.d.mts +15 -0
  28. package/dist/components/scroll-area/scroll-area-gutters.mjs +60 -0
  29. package/dist/components/scroll-area/scroll-area-textarea.d.mts +14 -0
  30. package/dist/components/scroll-area/scroll-area-textarea.mjs +50 -0
  31. package/dist/components/scroll-area/scroll-area.css +720 -0
  32. package/dist/components/scroll-area/scroll-area.d.mts +24 -15
  33. package/dist/components/scroll-area/scroll-area.mjs +67 -45
  34. package/dist/components/scroll-area/scroll-geometry.mjs +58 -0
  35. package/dist/components/scroll-area/scroll-to-element.mjs +67 -0
  36. package/dist/components/scroll-area/types.d.mts +224 -0
  37. package/dist/components/scroll-area/use-rail-pointer.mjs +93 -0
  38. package/dist/components/scroll-area/use-scroll-area-container.d.mts +26 -0
  39. package/dist/components/scroll-area/use-scroll-area-container.mjs +150 -0
  40. package/dist/components/scroll-area/use-scrollbars.mjs +480 -0
  41. package/dist/components/search/search-trigger.d.mts +1 -1
  42. package/dist/components/sidebar/sidebar.d.mts +1 -1
  43. package/dist/components/sidebar/sidebar.mjs +24 -20
  44. package/dist/components/sidebar/sidebar.recipe.mjs +1 -6
  45. package/dist/components/tooltip/tooltip.mjs +8 -5
  46. package/dist/components/virtual-list/VirtualList.mjs +1 -1
  47. package/dist/index.d.mts +7 -3
  48. package/dist/index.mjs +5 -2
  49. package/dist/lib/inline-part.mjs +13 -0
  50. package/dist/lib/use-merged-ref.mjs +73 -47
  51. package/dist/lib/use-platform.d.mts +13 -0
  52. package/dist/lib/use-platform.mjs +29 -0
  53. package/dist/styles.css +1 -0
  54. package/dist/wip.d.mts +5 -1
  55. package/dist/wip.mjs +3 -1
  56. package/package.json +1 -1
  57. package/dist/components/scroll-area/scroll-area.recipe.mjs +0 -23
package/CHANGELOG.md CHANGED
@@ -1,3 +1,50 @@
1
+ ## [0.60.1](https://github.com/botiverse/raft-design/compare/v0.60.0...v0.60.1) (2026-11-04)
2
+
3
+ ### Fixed
4
+
5
+ - **DropdownMenuShortcut** and **ContextMenuShortcut**: a menu shortcut draws no cap line. ([#976](https://github.com/botiverse/raft-design/pull/976))
6
+ - Why: Kbd draws its border with an inset ring, and the menu shortcuts reset `ring`, which left a 1px inset box around every menu shortcut, light and dark.
7
+ - Affects: only the two menu shortcut parts; Kbd, KbdShortcut, tooltips and the search trigger keep their caps.
8
+ - Docs: https://raft-design.botiverse.build/components/kbd/
9
+
10
+ ## [0.60.0](https://github.com/botiverse/raft-design/compare/v0.59.2...v0.60.0) (2026-11-03)
11
+
12
+ ### Breaking Changes
13
+
14
+ - **ScrollArea**: rebuilt on native scrolling. ScrollArea is now one element, the container that scrolls, with thin overlay scrollbars drawn over it the same way on every platform; wheel, trackpad, touch and keyboard scrolling stay the browser's own, the thumb can be dragged, and a press on the rail pages. ([#952](https://github.com/botiverse/raft-design/pull/952), [#971](https://github.com/botiverse/raft-design/pull/971))
15
+ - Why: the Base UI parts split one scroller across a root, a viewport and scrollbar elements, so a list that owned its own scroller (a virtual list, a textarea) could not have the same scrollbar; the scrollbars are now a layer any scroller can bind.
16
+ - Affects: `ref`, `className`, `style`, `onScroll`, `render` and the native props go on the element that scrolls. Options: `orientation`, `trigger` (`hover` / `scroll` / `none`), `size`, `fade`, `overscroll`, `railInset`, `hideDelay`, `onEdgeChange`, `actionsRef` (`scrollToElement`, `measure`), `contentRef`, `contentClassName` / `contentStyle`. Thumb thickness, colour, minimum length and the gap to the edge are `--r-scroll-area-*` variables. The styles ship in `styles.css`.
17
+ - Migrate: remove `ScrollAreaViewport`, `ScrollAreaScrollbar`, `ScrollAreaThumb`, `ScrollAreaCorner` and their props types; put what you passed to the viewport on `ScrollArea` itself. A container laid out as a grid, a flex row or a flex column with `gap` puts that layout on a wrapper (`contentClassName`), or binds the container with `useScrollAreaContainer({ guttersPosition: "absolute" })`.
18
+ - Docs: https://raft-design.botiverse.build/components/scroll-area/
19
+ - **ComposerInput**: the textarea gets ScrollArea's overlay scrollbar and is wrapped in a frame that holds it; it no longer takes `render`. ([#952](https://github.com/botiverse/raft-design/pull/952))
20
+ - Why: past its maximum height the composer showed the platform's own scrollbar, unlike every other scroller.
21
+ - Affects: `ref`, `className` and the native props still go to the `textarea`; `wrapperClassName` styles the frame; `trigger`, `size`, `hideDelay` and `overscroll` work as on ScrollArea.
22
+ - Migrate: drop `render`; a style meant for the box around the textarea moves to `wrapperClassName`.
23
+ - Docs: https://raft-design.botiverse.build/components/composer/
24
+
25
+ ### Added
26
+
27
+ - **useScrollAreaContainer** and **ScrollAreaGutters** (+ types `ScrollAreaContainer`, `ScrollAreaContainerOptions`, `ScrollAreaGuttersProps` and the option types): ScrollArea's scrollbars for an element you render yourself, such as a list's own scroller. The hook returns `containerProps` to spread on your element, `guttersProps` for `<ScrollAreaGutters>`, `frameProps` with absolute gutters, and `measure` / `scrollToElement` / `scrollable`. **ScrollAreaTextarea**: a `textarea` with the same scrollbar. ([#952](https://github.com/botiverse/raft-design/pull/952))
28
+ - Why: a virtual list or a textarea scrolls an element it renders itself, which a wrapping component can't replace.
29
+ - Affects: sticky gutters (the default) are a zero-height first child of the container; absolute gutters lie over a frame you render around it.
30
+ - Docs: https://raft-design.botiverse.build/components/scroll-area/
31
+ - **Kbd**: a `size` prop, `sm` (the default, unchanged) or `md`, a 24px cap on a muted fill for a key that is the content, as in a list of shortcuts (+ type `KbdSize`). ([#969](https://github.com/botiverse/raft-design/pull/969))
32
+ - Why: a shortcuts list drew its keys at the hint size, too small to read as the content of the row.
33
+ - Affects: `md` is 24px high, at least 24px wide, `rounded-sticker`, `bg-fill-muted`, `text-mono-code`. Menu and search-trigger shortcuts keep `sm` (their props omit `size`).
34
+ - Docs: https://raft-design.botiverse.build/components/kbd/
35
+
36
+ ### Changed
37
+
38
+ - **MessageList**, **SidebarContent**, **MarkdownTableScroll** and the composer's attachment row: built on the new ScrollArea, so they show the same thin overlay scrollbar. `MarkdownTableScroll` takes ScrollArea's props except `orientation`. ([#952](https://github.com/botiverse/raft-design/pull/952))
39
+ - Why: each had its own scroller and scrollbar, so the same list looked different from one place to the next.
40
+ - Docs: https://raft-design.botiverse.build/components/scroll-area/
41
+
42
+ ### Fixed
43
+
44
+ - **Tooltip**: a shortcut written as a `KbdGroup` sits 6px after the text like a single `Kbd`, and its caps are no longer taken for the tooltip's leading icon. ([#969](https://github.com/botiverse/raft-design/pull/969))
45
+ - Why: a multi-key shortcut in a tooltip was spaced differently from a single key.
46
+ - Docs: https://raft-design.botiverse.build/components/tooltip/
47
+
1
48
  ## [0.59.2](https://github.com/botiverse/raft-design/compare/v0.59.1...v0.59.2) (2026-11-03)
2
49
 
3
50
  ### Fixed
@@ -14,9 +14,8 @@ const composerAttachment = tv({
14
14
  "has-[[data-slot=sortable-composer-attachments-list]]:pt-0"
15
15
  ],
16
16
  attachmentsScrollArea: ["relative w-full min-w-0"],
17
- attachmentsViewport: [
18
- "flex flex-nowrap gap-2 overflow-x-auto overflow-y-hidden overscroll-x-contain py-1.5",
19
- "scrollbar-none [&::-webkit-scrollbar]:hidden",
17
+ attachmentsRow: [
18
+ "flex flex-nowrap gap-2 py-1.5",
20
19
  "**:data-[slot=composer-attachment]:shrink-0",
21
20
  "pe-1.5"
22
21
  ],
@@ -1,5 +1,11 @@
1
1
  import { ComponentPropsWithoutRef } from "react";
2
2
  //#region src/components/composer/composer-attachments-scroll-area.d.ts
3
3
  export type ComposerAttachmentsScrollAreaProps = ComponentPropsWithoutRef<"div">;
4
+ /**
5
+ * One scrolling row of attachments: a horizontal ScrollArea (its thin
6
+ * scrollbar shows only while the row scrolls), with an arrow on each side
7
+ * that has more attachments past it. The ScrollArea reports the edges, so
8
+ * the arrows follow scrolling, resizing and attachments coming and going.
9
+ */
4
10
  export declare function ComposerAttachmentsScrollArea({ children, className, onMouseEnter, onMouseLeave, ...props }: ComposerAttachmentsScrollAreaProps): import("react").JSX.Element;
5
11
  //#endregion
@@ -1,7 +1,8 @@
1
1
  import { Button } from "../button/button.mjs";
2
+ import { ScrollArea } from "../scroll-area/scroll-area.mjs";
2
3
  import { composerAttachment } from "./composer-attachment.recipe.mjs";
3
4
  import { ChevronLeft, ChevronRight } from "lucide-react";
4
- import { useCallback, useEffect, useRef, useState } from "react";
5
+ import { useCallback, useRef, useState } from "react";
5
6
  import { jsx, jsxs } from "react/jsx-runtime";
6
7
  //#region src/components/composer/composer-attachments-scroll-area.tsx
7
8
  function scrollByPage(viewport, direction) {
@@ -15,40 +16,22 @@ function scrollByPage(viewport, direction) {
15
16
  behavior: prefersReducedMotion ? "auto" : "smooth"
16
17
  });
17
18
  }
19
+ /**
20
+ * One scrolling row of attachments: a horizontal ScrollArea (its thin
21
+ * scrollbar shows only while the row scrolls), with an arrow on each side
22
+ * that has more attachments past it. The ScrollArea reports the edges, so
23
+ * the arrows follow scrolling, resizing and attachments coming and going.
24
+ */
18
25
  function ComposerAttachmentsScrollArea({ children, className, onMouseEnter, onMouseLeave, ...props }) {
19
- const { attachmentsScrollArea, attachmentsViewport } = composerAttachment();
26
+ const { attachmentsScrollArea, attachmentsRow } = composerAttachment();
20
27
  const prevNavClassName = composerAttachment({ side: "prev" }).attachmentsNav();
21
28
  const nextNavClassName = composerAttachment({ side: "next" }).attachmentsNav();
22
29
  const viewportRef = useRef(null);
23
30
  const [canPrev, setCanPrev] = useState(false);
24
31
  const [canNext, setCanNext] = useState(false);
25
- const updateScrollState = () => {
26
- const viewport = viewportRef.current;
27
- if (!viewport) return;
28
- const maxScrollLeft = viewport.scrollWidth - viewport.clientWidth;
29
- setCanPrev(viewport.scrollLeft > 1);
30
- setCanNext(maxScrollLeft - viewport.scrollLeft > 1);
31
- };
32
- useEffect(() => {
33
- const viewport = viewportRef.current;
34
- if (!viewport) return;
35
- updateScrollState();
36
- const resizeObserver = new ResizeObserver(updateScrollState);
37
- resizeObserver.observe(viewport);
38
- for (const child of viewport.children) resizeObserver.observe(child);
39
- const mutationObserver = new MutationObserver(() => {
40
- for (const child of viewport.children) resizeObserver.observe(child);
41
- updateScrollState();
42
- });
43
- mutationObserver.observe(viewport, { childList: true });
44
- viewport.addEventListener("scroll", updateScrollState, { passive: true });
45
- window.addEventListener("resize", updateScrollState);
46
- return () => {
47
- resizeObserver.disconnect();
48
- mutationObserver.disconnect();
49
- viewport.removeEventListener("scroll", updateScrollState);
50
- window.removeEventListener("resize", updateScrollState);
51
- };
32
+ const handleEdgeChange = useCallback((edges) => {
33
+ setCanPrev(!edges.left);
34
+ setCanNext(!edges.right);
52
35
  }, []);
53
36
  const handleScrollPrev = useCallback(() => scrollByPage(viewportRef.current, -1), []);
54
37
  const handleScrollNext = useCallback(() => scrollByPage(viewportRef.current, 1), []);
@@ -71,10 +54,16 @@ function ComposerAttachmentsScrollArea({ children, className, onMouseEnter, onMo
71
54
  onClick: handleScrollPrev,
72
55
  children: /* @__PURE__ */ jsx(ChevronLeft, {})
73
56
  }) : null,
74
- /* @__PURE__ */ jsx("div", {
57
+ /* @__PURE__ */ jsx(ScrollArea, {
75
58
  ref: viewportRef,
76
59
  "data-slot": "composer-attachments-viewport",
77
- className: attachmentsViewport(),
60
+ orientation: "horizontal",
61
+ trigger: "scroll",
62
+ size: "small",
63
+ overscroll: "contain",
64
+ tabIndex: -1,
65
+ contentClassName: attachmentsRow(),
66
+ onEdgeChange: handleEdgeChange,
78
67
  children
79
68
  }),
80
69
  showNext ? /* @__PURE__ */ jsx(Button, {
@@ -1,10 +1,16 @@
1
1
  import { ButtonProps } from "../button/button.mjs";
2
+ import { ScrollAreaTextareaProps } from "../scroll-area/types.mjs";
2
3
  import { useRender } from "@base-ui/react/use-render";
3
4
  //#region src/components/composer/composer.d.ts
4
5
  export type ComposerProps = useRender.ComponentProps<"div">;
5
6
  export declare function Composer({ className, render, ...props }: ComposerProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
6
- export type ComposerInputProps = useRender.ComponentProps<"textarea">;
7
- export declare function ComposerInput({ className, render, ...props }: ComposerInputProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
7
+ /**
8
+ * A native `<textarea>` with ScrollArea's overlay scrollbar (`ScrollAreaTextarea`): `ref`,
9
+ * `className` and the native props go to the textarea; `wrapperClassName` to the element that holds
10
+ * it and the scrollbar; `trigger`, `size`, `hideDelay` and `overscroll` as on ScrollArea.
11
+ */
12
+ export type ComposerInputProps = ScrollAreaTextareaProps;
13
+ export declare function ComposerInput({ className, wrapperClassName, ...props }: ComposerInputProps): import("react").JSX.Element;
8
14
  export type ComposerToolbarProps = useRender.ComponentProps<"div">;
9
15
  export declare function ComposerToolbar({ className, render, ...props }: ComposerToolbarProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
10
16
  export type ComposerActionsProps = useRender.ComponentProps<"div">;
@@ -1,4 +1,5 @@
1
1
  import { Button } from "../button/button.mjs";
2
+ import { ScrollAreaTextarea } from "../scroll-area/scroll-area-textarea.mjs";
2
3
  import { composer } from "./composer.recipe.mjs";
3
4
  import { jsx } from "react/jsx-runtime";
4
5
  import { mergeProps } from "@base-ui/react/merge-props";
@@ -16,17 +17,14 @@ function Composer({ className, render, ...props }) {
16
17
  props: mergeProps(defaultProps, props)
17
18
  });
18
19
  }
19
- function ComposerInput({ className, render, ...props }) {
20
- const { input } = composer();
21
- const defaultProps = {
20
+ function ComposerInput({ className, wrapperClassName, ...props }) {
21
+ const { input, inputFrame } = composer();
22
+ return /* @__PURE__ */ jsx(ScrollAreaTextarea, {
22
23
  rows: 1,
23
24
  "data-slot": "composer-input",
24
- className: input({ className })
25
- };
26
- return useRender({
27
- defaultTagName: "textarea",
28
- render,
29
- props: mergeProps(defaultProps, props)
25
+ ...props,
26
+ className: input({ className }),
27
+ wrapperClassName: inputFrame({ className: wrapperClassName })
30
28
  });
31
29
  }
32
30
  function ComposerToolbar({ className, render, ...props }) {
@@ -7,6 +7,7 @@ const composer = tv({ slots: {
7
7
  "border-line-field bg-layer-card text-foreground shadow-none",
8
8
  "has-focus-visible:shadow-focus"
9
9
  ],
10
+ inputFrame: ["w-full min-w-0", "[--r-scroll-area-resize-grip-size:0px]"],
10
11
  input: [
11
12
  "max-h-32 w-full resize-none bg-transparent outline-none",
12
13
  "field-sizing-content overflow-y-auto",
@@ -35,7 +35,7 @@ export type ContextMenuSubmenuTriggerProps = StyledProps<ContextMenu.SubmenuTrig
35
35
  export declare function ContextMenuSubmenuTrigger({ children, className, ...props }: ContextMenuSubmenuTriggerProps): import("react").JSX.Element;
36
36
  export type ContextMenuLabelProps = StyledProps<ComponentPropsWithRef<"div">>;
37
37
  export declare function ContextMenuLabel({ className, ...props }: ContextMenuLabelProps): import("react").JSX.Element;
38
- export type ContextMenuShortcutProps = KbdProps;
38
+ export type ContextMenuShortcutProps = Omit<KbdProps, "size">;
39
39
  export declare function ContextMenuShortcut({ className, ...props }: ContextMenuShortcutProps): import("react").JSX.Element;
40
40
  export type ContextMenuSeparatorProps = StyledProps<ContextMenu.Separator.Props>;
41
41
  export declare function ContextMenuSeparator({ className, ...props }: ContextMenuSeparatorProps): import("react").JSX.Element;
@@ -51,7 +51,7 @@ const contextMenu = tv({ slots: {
51
51
  label: ["px-2.5 pt-2 pb-1", "font-sans text-caption font-bold text-foreground-hint"],
52
52
  shortcut: [
53
53
  "ml-auto",
54
- "h-auto min-w-0 rounded-none bg-transparent p-0 shadow-none ring-0",
54
+ "h-auto min-w-0 rounded-none bg-transparent p-0 shadow-none inset-ring-0",
55
55
  "font-mono text-mono-meta font-normal text-foreground-hint"
56
56
  ],
57
57
  separator: ["-mx-1 my-1 h-px border-0 bg-line-hairline"],
@@ -30,7 +30,7 @@ export type DropdownMenuSubmenuTriggerProps = StyledProps<Menu.SubmenuTrigger.Pr
30
30
  export declare function DropdownMenuSubmenuTrigger({ children, className, ...props }: DropdownMenuSubmenuTriggerProps): import("react").JSX.Element;
31
31
  export type DropdownMenuLabelProps = StyledProps<ComponentPropsWithRef<"div">>;
32
32
  export declare function DropdownMenuLabel({ className, ...props }: DropdownMenuLabelProps): import("react").JSX.Element;
33
- export type DropdownMenuShortcutProps = KbdProps;
33
+ export type DropdownMenuShortcutProps = Omit<KbdProps, "size">;
34
34
  export declare function DropdownMenuShortcut({ className, ...props }: DropdownMenuShortcutProps): import("react").JSX.Element;
35
35
  export type DropdownMenuItemCountProps = BadgeProps;
36
36
  export declare function DropdownMenuItemCount({ className, ...props }: DropdownMenuItemCountProps): import("react").JSX.Element;
@@ -22,7 +22,7 @@ const dropdownMenu = tv({ slots: {
22
22
  "min-h-option gap-2.5 rounded-chip px-2.5 py-option-inset",
23
23
  "font-sans text-callout font-medium text-foreground",
24
24
  "hover:bg-brand-soft data-highlighted:bg-brand-soft",
25
- "data-checked:font-bold data-[selected]:font-bold data-[emphasis=strong]:font-bold",
25
+ "data-checked:font-bold data-selected:font-bold data-[emphasis=strong]:font-bold",
26
26
  "data-disabled:pointer-events-none data-disabled:text-foreground-disabled",
27
27
  "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&>svg:not([class*='size-'])]:size-4",
28
28
  "[&>svg]:text-foreground-muted data-disabled:[&>svg]:text-foreground-disabled"
@@ -53,7 +53,7 @@ const dropdownMenu = tv({ slots: {
53
53
  label: ["px-2.5 pt-2 pb-1", "font-sans text-caption font-bold text-foreground-hint"],
54
54
  shortcut: [
55
55
  "ml-auto",
56
- "h-auto min-w-0 rounded-none bg-transparent p-0 shadow-none ring-0",
56
+ "h-auto min-w-0 rounded-none bg-transparent p-0 shadow-none inset-ring-0",
57
57
  "font-mono text-mono-meta font-normal text-foreground-hint"
58
58
  ],
59
59
  itemCount: ["ml-auto inline-flex min-w-5 justify-center gap-0 px-1 tabular-nums"],
@@ -1,2 +1,5 @@
1
- import { Kbd, KbdGroup, KbdGroupProps, KbdProps } from "./kbd.mjs";
2
- export { Kbd, KbdGroup, type KbdGroupProps, type KbdProps };
1
+ import { Kbd, KbdGroup, KbdGroupProps, KbdProps, KbdSize } from "./kbd.mjs";
2
+ import { KeyboardPlatform, useKeyboardPlatform } from "../../lib/use-platform.mjs";
3
+ import { KbdKeyName } from "./kbd-keys.mjs";
4
+ import { KbdShortcut, KbdShortcutProps } from "./kbd-shortcut.mjs";
5
+ export { Kbd, KbdGroup, type KbdGroupProps, type KbdKeyName, type KbdProps, KbdShortcut, type KbdShortcutProps, type KbdSize, type KeyboardPlatform, useKeyboardPlatform };
@@ -0,0 +1,12 @@
1
+ //#region src/components/kbd/kbd-keys.d.ts
2
+ /**
3
+ * A key, by name, case-insensitive. `mod` is Command on a Mac and Ctrl
4
+ * elsewhere: write it for the platform's command key. `meta` (also `cmd`,
5
+ * `command`) is the literal Command key on a Mac and the Windows key
6
+ * elsewhere; `ctrl` (also `control`) is always the Control key. `KeyboardEvent.key`
7
+ * values work too: `ArrowUp`, `Control`, `Meta`, `Escape`, `" "`. Any other
8
+ * string is a key written as it reads: an ASCII letter is drawn uppercase,
9
+ * `f1`…`f24` as F1…F24, anything else as written.
10
+ */
11
+ export type KbdKeyName = "mod" | "meta" | "ctrl" | "alt" | "option" | "shift" | "enter" | "backspace" | "delete" | "escape" | "tab" | "space" | "up" | "down" | "left" | "right" | (string & {});
12
+ //#endregion
@@ -0,0 +1,297 @@
1
+ //#region src/components/kbd/kbd-keys.ts
2
+ /** Every name a key goes by, lowercase, including `KeyboardEvent.key` values. */
3
+ const ALIASES = {
4
+ mod: "mod",
5
+ meta: "meta",
6
+ cmd: "meta",
7
+ command: "meta",
8
+ ctrl: "ctrl",
9
+ control: "ctrl",
10
+ alt: "alt",
11
+ opt: "alt",
12
+ option: "alt",
13
+ shift: "shift",
14
+ enter: "enter",
15
+ return: "enter",
16
+ backspace: "backspace",
17
+ delete: "delete",
18
+ del: "delete",
19
+ escape: "escape",
20
+ esc: "escape",
21
+ tab: "tab",
22
+ space: "space",
23
+ spacebar: "space",
24
+ " ": "space",
25
+ up: "up",
26
+ arrowup: "up",
27
+ down: "down",
28
+ arrowdown: "down",
29
+ left: "left",
30
+ arrowleft: "left",
31
+ right: "right",
32
+ arrowright: "right"
33
+ };
34
+ /** The modifier a key presses on a platform, or none. */
35
+ function modifierOf(key, platform) {
36
+ switch (key) {
37
+ case "mod": return platform === "mac" ? "command" : "control";
38
+ case "meta": return platform === "mac" ? "command" : "meta";
39
+ case "ctrl": return "control";
40
+ case "alt": return "option";
41
+ case "shift": return "shift";
42
+ default: return;
43
+ }
44
+ }
45
+ const MODIFIER_ORDER = {
46
+ mac: [
47
+ "control",
48
+ "option",
49
+ "shift",
50
+ "command"
51
+ ],
52
+ other: [
53
+ "control",
54
+ "option",
55
+ "shift",
56
+ "meta"
57
+ ]
58
+ };
59
+ const MODIFIERS = {
60
+ mac: {
61
+ control: {
62
+ icon: "chevron-up",
63
+ label: "Control"
64
+ },
65
+ option: {
66
+ icon: "option",
67
+ label: "Option"
68
+ },
69
+ shift: {
70
+ icon: "arrow-big-up",
71
+ label: "Shift"
72
+ },
73
+ command: {
74
+ icon: "command",
75
+ label: "Command"
76
+ },
77
+ meta: {
78
+ icon: "command",
79
+ label: "Command"
80
+ }
81
+ },
82
+ other: {
83
+ control: {
84
+ text: "Ctrl",
85
+ label: "Control"
86
+ },
87
+ option: {
88
+ text: "Alt",
89
+ label: "Alt"
90
+ },
91
+ shift: {
92
+ text: "Shift",
93
+ label: "Shift"
94
+ },
95
+ command: {
96
+ text: "Ctrl",
97
+ label: "Control"
98
+ },
99
+ meta: {
100
+ text: "Win",
101
+ label: "Windows"
102
+ }
103
+ }
104
+ };
105
+ const ARROWS = {
106
+ up: {
107
+ icon: "arrow-up",
108
+ label: "Up Arrow"
109
+ },
110
+ down: {
111
+ icon: "arrow-down",
112
+ label: "Down Arrow"
113
+ },
114
+ left: {
115
+ icon: "arrow-left",
116
+ label: "Left Arrow"
117
+ },
118
+ right: {
119
+ icon: "arrow-right",
120
+ label: "Right Arrow"
121
+ }
122
+ };
123
+ const NAMED = {
124
+ mac: {
125
+ ...ARROWS,
126
+ enter: {
127
+ icon: "corner-down-left",
128
+ label: "Return"
129
+ },
130
+ backspace: {
131
+ icon: "delete",
132
+ label: "Delete"
133
+ },
134
+ delete: {
135
+ text: "Del",
136
+ label: "Forward Delete"
137
+ },
138
+ escape: {
139
+ text: "Esc",
140
+ label: "Escape"
141
+ },
142
+ tab: {
143
+ text: "Tab",
144
+ label: "Tab"
145
+ },
146
+ space: {
147
+ text: "Space",
148
+ label: "Space"
149
+ }
150
+ },
151
+ other: {
152
+ ...ARROWS,
153
+ enter: {
154
+ text: "Enter",
155
+ label: "Enter"
156
+ },
157
+ backspace: {
158
+ text: "Backspace",
159
+ label: "Backspace"
160
+ },
161
+ delete: {
162
+ text: "Del",
163
+ label: "Delete"
164
+ },
165
+ escape: {
166
+ text: "Esc",
167
+ label: "Escape"
168
+ },
169
+ tab: {
170
+ text: "Tab",
171
+ label: "Tab"
172
+ },
173
+ space: {
174
+ text: "Space",
175
+ label: "Space"
176
+ }
177
+ }
178
+ };
179
+ /** What a screen reader is told for a punctuation key, which it may skip. */
180
+ const CHARACTER_LABELS = {
181
+ "/": "Slash",
182
+ "\\": "Backslash",
183
+ ".": "Period",
184
+ ",": "Comma",
185
+ ";": "Semicolon",
186
+ ":": "Colon",
187
+ "'": "Quote",
188
+ "\"": "Double Quote",
189
+ "`": "Backtick",
190
+ "-": "Minus",
191
+ _: "Underscore",
192
+ "=": "Equals",
193
+ "+": "Plus",
194
+ "[": "Left Bracket",
195
+ "]": "Right Bracket",
196
+ "{": "Left Brace",
197
+ "}": "Right Brace",
198
+ "(": "Left Parenthesis",
199
+ ")": "Right Parenthesis",
200
+ "?": "Question Mark",
201
+ "!": "Exclamation Mark",
202
+ "@": "At",
203
+ "#": "Number Sign",
204
+ $: "Dollar",
205
+ "%": "Percent",
206
+ "^": "Caret",
207
+ "&": "Ampersand",
208
+ "*": "Asterisk",
209
+ "<": "Less Than",
210
+ ">": "Greater Than",
211
+ "~": "Tilde",
212
+ "|": "Vertical Bar"
213
+ };
214
+ /** One written key as a named key, or as a literal key drawn as it reads. */
215
+ function parse(name) {
216
+ if (name === "") return void 0;
217
+ const alias = ALIASES[name.toLowerCase()];
218
+ if (alias) return {
219
+ kind: "canonical",
220
+ key: alias,
221
+ name
222
+ };
223
+ if (/^.$/u.test(name)) {
224
+ const text = /^[a-z]$/.test(name) ? name.toUpperCase() : name;
225
+ return {
226
+ kind: "literal",
227
+ text,
228
+ label: CHARACTER_LABELS[name] ?? text,
229
+ name
230
+ };
231
+ }
232
+ if (/^f\d{1,2}$/i.test(name)) {
233
+ const text = name.toUpperCase();
234
+ return {
235
+ kind: "literal",
236
+ text,
237
+ label: text,
238
+ name
239
+ };
240
+ }
241
+ return {
242
+ kind: "literal",
243
+ text: name,
244
+ label: name,
245
+ name
246
+ };
247
+ }
248
+ const warned = /* @__PURE__ */ new Set();
249
+ function warnOnce(message) {
250
+ if (process.env.NODE_ENV === "production" || warned.has(message)) return;
251
+ warned.add(message);
252
+ console.warn(message);
253
+ }
254
+ /**
255
+ * The keys of a shortcut as a platform draws them: the modifiers first, in
256
+ * the platform's order whatever order they were written in (each once), then
257
+ * the other keys in the order written. An empty name is skipped. In
258
+ * development, an empty name and two names that press the same key on this
259
+ * platform (`mod` and `ctrl` on Windows and Linux) log a warning.
260
+ */
261
+ function resolveKbdKeys(keys, platform) {
262
+ const order = MODIFIER_ORDER[platform];
263
+ const modifiers = /* @__PURE__ */ new Map();
264
+ const rest = [];
265
+ for (const written of keys) {
266
+ const parsed = parse(written);
267
+ if (!parsed) {
268
+ warnOnce(`KbdShortcut: an empty key name in ${JSON.stringify(keys)} is skipped.`);
269
+ continue;
270
+ }
271
+ if (parsed.kind === "literal") {
272
+ rest.push({
273
+ name: parsed.name,
274
+ text: parsed.text,
275
+ label: parsed.label
276
+ });
277
+ continue;
278
+ }
279
+ const modifier = modifierOf(parsed.key, platform);
280
+ if (!modifier) {
281
+ rest.push({
282
+ name: parsed.name,
283
+ ...NAMED[platform][parsed.key]
284
+ });
285
+ continue;
286
+ }
287
+ const earlier = modifiers.get(modifier);
288
+ if (earlier === void 0) modifiers.set(modifier, written);
289
+ else warnOnce(`KbdShortcut: "${earlier}" and "${written}" are the same key on ${platform === "mac" ? "a Mac" : "Windows and Linux"}, so ${JSON.stringify(keys)} draws it once.`);
290
+ }
291
+ return [...[...modifiers.entries()].sort(([a], [b]) => order.indexOf(a) - order.indexOf(b)).map(([modifier, name]) => ({
292
+ name,
293
+ ...MODIFIERS[platform][modifier]
294
+ })), ...rest];
295
+ }
296
+ //#endregion
297
+ export { resolveKbdKeys };
@@ -0,0 +1,34 @@
1
+ import { KbdSize } from "./kbd.mjs";
2
+ import { KeyboardPlatform } from "../../lib/use-platform.mjs";
3
+ import { KbdKeyName } from "./kbd-keys.mjs";
4
+ import { ComponentPropsWithRef } from "react";
5
+ //#region src/components/kbd/kbd-shortcut.d.ts
6
+ export type KbdShortcutProps = Omit<ComponentPropsWithRef<"span">, "children"> & {
7
+ /**
8
+ * The keys, by name, case-insensitive, in any order: `["mod", "shift",
9
+ * "enter"]`. `KeyboardEvent.key` values work too (`ArrowUp`, `Meta`).
10
+ * Modifiers are drawn first, in the platform's order; the other keys
11
+ * follow in the order written.
12
+ */
13
+ keys: readonly KbdKeyName[];
14
+ /** The size of every cap, as `Kbd`'s. */
15
+ size?: KbdSize;
16
+ /**
17
+ * The keyboard to draw for. Without it, the platform the page runs on,
18
+ * detected once on the client (`other` on the server and while hydrating).
19
+ */
20
+ platform?: KeyboardPlatform;
21
+ /**
22
+ * What a screen reader says instead of the key names, which are English:
23
+ * pass the product's own words for another language ("Shift Command 回车").
24
+ */
25
+ label?: string;
26
+ };
27
+ /**
28
+ * One shortcut from key names: a cap per key, with the platform's drawings
29
+ * (icons on a Mac, words elsewhere), and the shortcut in words for a screen
30
+ * reader. Alternatives ("⌘↑ / ⌘↓") are two KbdShortcut with a separator
31
+ * between them, written by the caller.
32
+ */
33
+ export declare function KbdShortcut({ keys, size, platform: platformProp, label, className, ...props }: KbdShortcutProps): import("react").JSX.Element;
34
+ //#endregion