@domicile-desktop/component-library 0.0.0-alpha-e8d68edfa399 → 0.0.0-alpha-d2a9697c6959

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.
package/README.md CHANGED
@@ -26,6 +26,7 @@ Each export is `@domicile-desktop/component-library/<name>`.
26
26
  | `Input` | Text input with prefix icon, clear button and invalid state. |
27
27
  | `Kbd` | Keyboard key cap. |
28
28
  | `ModalDialog` | Modal dialog with `title`, `footer` and `trigger` props. |
29
+ | `ContextMenu` | Menu opened at a point, such as a right click's. |
29
30
  | `Popover` | Non-modal panel anchored to the control that opened it. |
30
31
  | `Provider` | base-ui `DirectionProvider` wrapper. Every app roots its tree in it. |
31
32
  | `Screen` | Renders its children once per selected display. |
package/package.json CHANGED
@@ -31,6 +31,7 @@
31
31
  "./Avatar": "./src/Avatar/Avatar.tsx",
32
32
  "./Button": "./src/Button/Button.tsx",
33
33
  "./Card": "./src/Card/Card.tsx",
34
+ "./ContextMenu": "./src/ContextMenu/ContextMenu.tsx",
34
35
  "./control-sizes": "./src/control-sizes.ts",
35
36
  "./DisplayProvider": "./src/Screen/DisplayProvider.tsx",
36
37
  "./Drilldown": "./src/Drilldown/Drilldown.tsx",
@@ -88,5 +89,5 @@
88
89
  "test:unit": "bun test"
89
90
  },
90
91
  "type": "module",
91
- "version": "0.0.0-alpha-e8d68edfa399"
92
+ "version": "0.0.0-alpha-d2a9697c6959"
92
93
  }
@@ -0,0 +1,153 @@
1
+ import { Menu as BaseMenu } from "@base-ui/react/menu";
2
+ import type { ReactNode } from "react";
3
+ import { useMemo } from "react";
4
+ import { css } from "../../styled-system/css";
5
+ import type { ExtendProps } from "../extend-props";
6
+
7
+ /** A point in the viewport, in CSS pixels. */
8
+ export type Point = { x: number; y: number };
9
+
10
+ type Props = ExtendProps<
11
+ typeof BaseMenu.Root,
12
+ {
13
+ /** Where the menu opens: its top left corner, unless that would not fit. */
14
+ at: Point;
15
+ children: ReactNode;
16
+ /** What the menu is called, to assistive technology. */
17
+ label: string;
18
+ }
19
+ >;
20
+
21
+ /**
22
+ * A menu opened at a point, such as a right click's, with no trigger element.
23
+ *
24
+ * The caller sets `open` and `at`, and hears it close (a choice, Escape or an
25
+ * outside press) through `onOpenChange`. The press may not be in this
26
+ * document, as with a click in a `<webview>`. The menu takes the keyboard while
27
+ * open.
28
+ */
29
+ const ContextMenuComponent = ({ at, children, label, ...rootProps }: Props) => {
30
+ // A zero-size box at the point, for the positioner to align the menu's
31
+ // corner with.
32
+ const anchor = useMemo(
33
+ () => ({
34
+ getBoundingClientRect: () =>
35
+ DOMRect.fromRect({ height: 0, width: 0, x: at.x, y: at.y }),
36
+ }),
37
+ [at.x, at.y],
38
+ );
39
+ return (
40
+ <BaseMenu.Root {...rootProps}>
41
+ <BaseMenu.Portal>
42
+ <BaseMenu.Positioner
43
+ align="start"
44
+ anchor={anchor}
45
+ className={positionerStyles}
46
+ side="bottom"
47
+ sideOffset={0}
48
+ >
49
+ <BaseMenu.Popup aria-label={label} className={popupStyles}>
50
+ {children}
51
+ </BaseMenu.Popup>
52
+ </BaseMenu.Positioner>
53
+ </BaseMenu.Portal>
54
+ </BaseMenu.Root>
55
+ );
56
+ };
57
+
58
+ type ItemProps = ExtendProps<
59
+ typeof BaseMenu.Item,
60
+ {
61
+ children: ReactNode;
62
+ /** The keys that do the same, shown at the item's end. */
63
+ shortcut?: string | undefined;
64
+ }
65
+ >;
66
+
67
+ /** One thing the menu can do. Closes the menu when chosen. */
68
+ const Item = ({ children, shortcut, ...itemProps }: ItemProps) => (
69
+ <BaseMenu.Item className={itemStyles} {...itemProps}>
70
+ <span className={labelStyles}>{children}</span>
71
+ {shortcut !== undefined && (
72
+ <span className={shortcutStyles}>{shortcut}</span>
73
+ )}
74
+ </BaseMenu.Item>
75
+ );
76
+
77
+ /** A line between groups of items. */
78
+ const Separator = () => <BaseMenu.Separator className={separatorStyles} />;
79
+
80
+ export const ContextMenu = Object.assign(ContextMenuComponent, {
81
+ Item,
82
+ Separator,
83
+ });
84
+
85
+ const positionerStyles = css({
86
+ outlineStyle: "none",
87
+ zIndex: "modal",
88
+ });
89
+
90
+ // `&[data-starting-style]` rather than Panda's `_starting`: `@starting-style`
91
+ // does not reliably fire for a portaled popup. See `Popover`.
92
+ const popupStyles = css({
93
+ "&[data-ending-style]": {
94
+ opacity: 0,
95
+ transition: "opacity {durations.fast} {easings.in}",
96
+ },
97
+ "&[data-starting-style]": {
98
+ opacity: 0,
99
+ },
100
+ backgroundColor: "card",
101
+ border: "1px solid {colors.border}",
102
+ borderRadius: "md",
103
+ boxShadow: "lifted",
104
+ color: "foreground",
105
+ display: "flex",
106
+ flexDirection: "column",
107
+ maxBlockSize: "var(--available-height)",
108
+ minInlineSize: "{spacing.56}",
109
+ opacity: 1,
110
+ outlineStyle: "none",
111
+ overflowY: "auto",
112
+ paddingBlock: 1,
113
+ transition: "opacity {durations.fast} {easings.out}",
114
+ });
115
+
116
+ // `Select`'s item, with a shortcut where its check would be.
117
+ const itemStyles = css({
118
+ "&[data-disabled]": {
119
+ color: "muted",
120
+ opacity: "disabled",
121
+ },
122
+ "&[data-highlighted]": {
123
+ backgroundColor: "color-mix(in oklab, {colors.foreground} 8%, transparent)",
124
+ },
125
+ alignItems: "center",
126
+ color: "foreground",
127
+ cursor: { _disabled: "not-allowed", base: "pointer" },
128
+ display: "grid",
129
+ fontSize: "sm",
130
+ gap: 6,
131
+ gridTemplateColumns: "1fr auto",
132
+ outlineStyle: "none",
133
+ paddingBlock: 1.5,
134
+ paddingInline: 3,
135
+ userSelect: "none",
136
+ });
137
+
138
+ const labelStyles = css({
139
+ overflow: "hidden",
140
+ textOverflow: "ellipsis",
141
+ whiteSpace: "nowrap",
142
+ });
143
+
144
+ const shortcutStyles = css({
145
+ color: "muted",
146
+ fontSize: "xs",
147
+ });
148
+
149
+ const separatorStyles = css({
150
+ backgroundColor: "border",
151
+ blockSize: "1px",
152
+ marginBlock: 1,
153
+ });