@qeetrix/react 0.0.2 → 0.0.4

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 (104) hide show
  1. package/CHANGELOG.md +116 -1
  2. package/README.md +99 -0
  3. package/dist/button/button.d.ts +6 -2
  4. package/dist/button/button.js +5 -1
  5. package/dist/css/avatar.css +6 -0
  6. package/dist/css/badge.css +6 -0
  7. package/dist/css/button.css +81 -6
  8. package/dist/css/checkbox.css +9 -0
  9. package/dist/css/dialog.css +7 -0
  10. package/dist/css/drawer.css +6 -0
  11. package/dist/css/experimental/alert.css +89 -0
  12. package/dist/css/experimental/app-shell.css +258 -0
  13. package/dist/css/experimental/breadcrumb.css +67 -0
  14. package/dist/css/experimental/copy-button.css +13 -0
  15. package/dist/css/experimental/empty-state.css +63 -0
  16. package/dist/css/experimental/otp-input.css +84 -0
  17. package/dist/css/experimental/page-header.css +110 -0
  18. package/dist/css/experimental/password-input.css +21 -0
  19. package/dist/css/experimental/sidebar.css +234 -0
  20. package/dist/css/experimental/stat.css +115 -0
  21. package/dist/css/experimental/top-bar.css +299 -0
  22. package/dist/css/input.css +10 -0
  23. package/dist/css/menu.css +17 -0
  24. package/dist/css/progress.css +14 -0
  25. package/dist/css/scroll-area.css +7 -0
  26. package/dist/css/select.css +16 -0
  27. package/dist/css/separator.css +7 -0
  28. package/dist/css/skeleton.css +6 -0
  29. package/dist/css/slider.css +22 -0
  30. package/dist/css/switch.css +33 -0
  31. package/dist/css/tabs.css +11 -0
  32. package/dist/css/toast.css +7 -0
  33. package/dist/css/toggle.css +13 -0
  34. package/dist/css/tooltip.css +6 -0
  35. package/dist/dialog/dialog.d.ts +4 -1
  36. package/dist/dialog/dialog.js +4 -3
  37. package/dist/drawer/drawer.d.ts +4 -1
  38. package/dist/drawer/drawer.js +5 -4
  39. package/dist/experimental/alert/alert.d.ts +29 -0
  40. package/dist/experimental/alert/alert.js +72 -0
  41. package/dist/experimental/alert/index.d.ts +2 -0
  42. package/dist/experimental/app-shell/app-shell.d.ts +60 -0
  43. package/dist/experimental/app-shell/app-shell.js +224 -0
  44. package/dist/experimental/app-shell/index.d.ts +2 -0
  45. package/dist/experimental/breadcrumb/breadcrumb.d.ts +27 -0
  46. package/dist/experimental/breadcrumb/breadcrumb.js +48 -0
  47. package/dist/experimental/breadcrumb/index.d.ts +2 -0
  48. package/dist/experimental/copy-button/copy-button.d.ts +27 -0
  49. package/dist/experimental/copy-button/copy-button.js +61 -0
  50. package/dist/experimental/copy-button/index.d.ts +2 -0
  51. package/dist/experimental/empty-state/data-state.d.ts +23 -0
  52. package/dist/experimental/empty-state/data-state.js +40 -0
  53. package/dist/experimental/empty-state/empty-state.d.ts +26 -0
  54. package/dist/experimental/empty-state/empty-state.js +46 -0
  55. package/dist/experimental/empty-state/index.d.ts +3 -0
  56. package/dist/experimental/index.d.ts +24 -0
  57. package/dist/experimental/index.js +13 -0
  58. package/dist/experimental/otp-input/index.d.ts +2 -0
  59. package/dist/experimental/otp-input/otp-input.d.ts +27 -0
  60. package/dist/experimental/otp-input/otp-input.js +39 -0
  61. package/dist/experimental/page-header/index.d.ts +2 -0
  62. package/dist/experimental/page-header/page-header.d.ts +32 -0
  63. package/dist/experimental/page-header/page-header.js +73 -0
  64. package/dist/experimental/password-input/index.d.ts +2 -0
  65. package/dist/experimental/password-input/password-input.d.ts +26 -0
  66. package/dist/experimental/password-input/password-input.js +103 -0
  67. package/dist/experimental/sidebar/index.d.ts +2 -0
  68. package/dist/experimental/sidebar/sidebar.d.ts +64 -0
  69. package/dist/experimental/sidebar/sidebar.js +200 -0
  70. package/dist/experimental/stat/index.d.ts +2 -0
  71. package/dist/experimental/stat/stat.d.ts +31 -0
  72. package/dist/experimental/stat/stat.js +90 -0
  73. package/dist/experimental/top-bar/index.d.ts +2 -0
  74. package/dist/experimental/top-bar/top-bar.d.ts +60 -0
  75. package/dist/experimental/top-bar/top-bar.js +162 -0
  76. package/dist/field/field.d.ts +4 -1
  77. package/dist/field/field.js +6 -5
  78. package/dist/format/format.d.ts +12 -0
  79. package/dist/format/format.js +50 -0
  80. package/dist/format/index.d.ts +2 -0
  81. package/dist/format/index.js +1 -0
  82. package/dist/index.d.ts +6 -2
  83. package/dist/index.js +3 -1
  84. package/dist/internal/media.js +15 -0
  85. package/dist/internal/messages.d.ts +75 -0
  86. package/dist/internal/messages.js +44 -5
  87. package/dist/link/link.d.ts +7 -1
  88. package/dist/link/link.js +3 -2
  89. package/dist/locales/hi.d.ts +8 -0
  90. package/dist/locales/hi.js +60 -0
  91. package/dist/progress/progress.d.ts +2 -2
  92. package/dist/progress/progress.js +7 -2
  93. package/dist/provider/index.d.ts +3 -0
  94. package/dist/provider/index.js +1 -0
  95. package/dist/provider/provider.d.ts +46 -0
  96. package/dist/provider/provider.js +90 -0
  97. package/dist/slider/slider.d.ts +5 -2
  98. package/dist/slider/slider.js +10 -3
  99. package/dist/styles.css +1704 -104
  100. package/dist/text-field/text-field.d.ts +2 -2
  101. package/dist/text-field/text-field.js +4 -2
  102. package/dist/toast/toast.d.ts +4 -1
  103. package/dist/toast/toast.js +7 -6
  104. package/package.json +18 -2
@@ -170,4 +170,20 @@
170
170
  font-variation-settings: "GEOM" var(--qx-font-geometry-body);
171
171
  color: var(--qx-color-text-subtle);
172
172
  }
173
+
174
+ @media (forced-colors: active) {
175
+ .qx-select-trigger {
176
+ border: var(--qx-border-width-default) solid ButtonText;
177
+ }
178
+
179
+ .qx-select-option[data-highlighted] {
180
+ forced-color-adjust: none;
181
+ color: highlighttext;
182
+ background: highlight;
183
+ }
184
+
185
+ .qx-select-option[data-highlighted] .qx-select-option-check {
186
+ color: highlighttext;
187
+ }
188
+ }
173
189
  }
@@ -25,4 +25,11 @@
25
25
  .qx-separator[data-tone="strong"] {
26
26
  background: var(--qx-color-border-strong);
27
27
  }
28
+
29
+ @media (forced-colors: active) {
30
+ .qx-separator {
31
+ forced-color-adjust: none;
32
+ background: canvastext;
33
+ }
34
+ }
28
35
  }
@@ -30,4 +30,10 @@
30
30
  opacity: .45;
31
31
  }
32
32
  }
33
+
34
+ @media (forced-colors: active) {
35
+ .qx-skeleton {
36
+ border: var(--qx-border-width-default) solid GrayText;
37
+ }
38
+ }
33
39
  }
@@ -83,4 +83,26 @@
83
83
  background: var(--qx-color-icon-disabled);
84
84
  cursor: not-allowed;
85
85
  }
86
+
87
+ @media (forced-colors: active) {
88
+ .qx-slider-track, .qx-slider-indicator, .qx-slider-thumb {
89
+ forced-color-adjust: none;
90
+ }
91
+
92
+ .qx-slider-track {
93
+ background: graytext;
94
+ }
95
+
96
+ .qx-slider-indicator, .qx-slider-thumb {
97
+ background: highlight;
98
+ }
99
+
100
+ .qx-slider-thumb {
101
+ box-shadow: 0 0 0 var(--qx-border-width-strong) Canvas;
102
+ }
103
+
104
+ .qx-slider-thumb[data-disabled], [data-disabled] .qx-slider-indicator {
105
+ background: graytext;
106
+ }
107
+ }
86
108
  }
@@ -76,4 +76,37 @@
76
76
  .qx-choice-label > .qx-switch {
77
77
  margin-block: 1px;
78
78
  }
79
+
80
+ @media (forced-colors: active) {
81
+ .qx-switch, .qx-switch-thumb {
82
+ forced-color-adjust: none;
83
+ }
84
+
85
+ .qx-switch {
86
+ background: canvas;
87
+ border-color: canvastext;
88
+ }
89
+
90
+ .qx-switch-thumb {
91
+ background: canvastext;
92
+ }
93
+
94
+ .qx-switch[data-checked] {
95
+ background: highlight;
96
+ border-color: highlight;
97
+ }
98
+
99
+ .qx-switch-thumb[data-checked] {
100
+ background: highlighttext;
101
+ }
102
+
103
+ .qx-switch[data-disabled] {
104
+ background: canvas;
105
+ border-color: graytext;
106
+ }
107
+
108
+ .qx-switch-thumb[data-disabled] {
109
+ background: graytext;
110
+ }
111
+ }
79
112
  }
package/dist/css/tabs.css CHANGED
@@ -109,4 +109,15 @@
109
109
  outline: var(--qx-focus-ring-width) solid var(--qx-color-border-focus);
110
110
  outline-offset: var(--qx-focus-ring-offset);
111
111
  }
112
+
113
+ @media (forced-colors: active) {
114
+ .qx-tabs-list:not([data-orientation="vertical"]) {
115
+ border-block-end: var(--qx-border-width-strong) solid CanvasText;
116
+ }
117
+
118
+ .qx-tabs-indicator {
119
+ forced-color-adjust: none;
120
+ background: highlight;
121
+ }
122
+ }
112
123
  }
@@ -105,4 +105,11 @@
105
105
  flex: none;
106
106
  margin-block: -2px;
107
107
  }
108
+
109
+ @media (forced-colors: active) {
110
+ .qx-toast {
111
+ border: var(--qx-border-width-default) solid CanvasText;
112
+ border-inline-start-width: var(--qx-border-width-accent);
113
+ }
114
+ }
108
115
  }
@@ -109,4 +109,17 @@
109
109
  .qx-toggle-group[data-variant="segmented"][data-size="lg"] > .qx-toggle {
110
110
  block-size: calc(var(--qx-size-control-lg) - 2 * var(--qx-border-width-default));
111
111
  }
112
+
113
+ @media (forced-colors: active) {
114
+ .qx-toggle {
115
+ border: var(--qx-border-width-default) solid ButtonText;
116
+ }
117
+
118
+ .qx-toggle[data-pressed] {
119
+ forced-color-adjust: none;
120
+ color: highlighttext;
121
+ background: highlight;
122
+ border-color: highlight;
123
+ }
124
+ }
112
125
  }
@@ -38,4 +38,10 @@
38
38
  .qx-tooltip[data-instant] {
39
39
  transition: none;
40
40
  }
41
+
42
+ @media (forced-colors: active) {
43
+ .qx-tooltip {
44
+ border: var(--qx-border-width-default) solid CanvasText;
45
+ }
46
+ }
41
47
  }
@@ -1,3 +1,4 @@
1
+ import { QeetrixMessages } from "../internal/messages.js";
1
2
  import { CSSProperties, ComponentProps, ReactElement, ReactNode } from "react";
2
3
  import { Dialog } from "@base-ui/react/dialog";
3
4
  /** The kind of decision: `standard`, or a `danger` / `warning` alert dialog. */
@@ -24,6 +25,8 @@ export interface DialogContentProps extends Omit<Dialog.Popup.Props, "className"
24
25
  description?: ReactNode;
25
26
  /** Width: `sm` 400, `md` 560 (default) or `lg` 800px. */
26
27
  size?: DialogSize;
28
+ /** The close button's label, over QeetrixProvider's and the English default. */
29
+ messages?: Partial<QeetrixMessages["overlay"]>;
27
30
  /** Placement only. */
28
31
  className?: string;
29
32
  style?: CSSProperties;
@@ -43,7 +46,7 @@ export declare function DialogClose(props: DialogCloseProps): ReactElement;
43
46
  * body and footer. The close button comes last in the DOM, so initial focus lands on the first field
44
47
  * or else the footer's first (least destructive) action.
45
48
  */
46
- export declare function DialogContent({ title, description, size, className, children, ...props }: DialogContentProps): ReactElement;
49
+ export declare function DialogContent({ title, description, size, messages, className, children, ...props }: DialogContentProps): ReactElement;
47
50
  /** The dialog's content; it scrolls when the dialog reaches its maximum height. */
48
51
  export declare function DialogBody({ className, ...props }: DialogBodyProps): ReactElement;
49
52
  /** The actions, end-aligned: tertiary first, then the primary or destructive action. */
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { cx } from "../internal/cx.js";
3
- import { messages } from "../internal/messages.js";
4
3
  import { IconButton } from "../button/button.js";
4
+ import { useMessages } from "../provider/provider.js";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
6
  import { createContext, use } from "react";
7
7
  import { AlertDialog } from "@base-ui/react/alert-dialog";
@@ -59,8 +59,9 @@ function DialogClose(props) {
59
59
  * body and footer. The close button comes last in the DOM, so initial focus lands on the first field
60
60
  * or else the footer's first (least destructive) action.
61
61
  */
62
- function DialogContent({ title, description, size = "md", className, children, ...props }) {
62
+ function DialogContent({ title, description, size = "md", messages, className, children, ...props }) {
63
63
  const { intent, dismissible } = use(DialogContext);
64
+ const strings = useMessages("overlay", messages);
64
65
  return /* @__PURE__ */ jsxs(Dialog.Portal, { children: [/* @__PURE__ */ jsx(Dialog.Backdrop, {
65
66
  className: "qx-dialog-backdrop",
66
67
  "data-slot": "dialog-backdrop"
@@ -93,7 +94,7 @@ function DialogContent({ title, description, size = "md", className, children, .
93
94
  "data-slot": "dialog-close",
94
95
  children: /* @__PURE__ */ jsx(Dialog.Close, { render: /* @__PURE__ */ jsx(IconButton, {
95
96
  icon: XIcon,
96
- "aria-label": messages.overlay.close,
97
+ "aria-label": strings.close,
97
98
  size: "sm"
98
99
  }) })
99
100
  })
@@ -1,3 +1,4 @@
1
+ import { QeetrixMessages } from "../internal/messages.js";
1
2
  import { CSSProperties, ComponentProps, ReactElement, ReactNode } from "react";
2
3
  import { Drawer } from "@base-ui/react/drawer";
3
4
  /** The edge a drawer opens from, in the text direction: `end` is the right edge in English. */
@@ -16,6 +17,8 @@ export interface DrawerContentProps extends Omit<Drawer.Popup.Props, "className"
16
17
  description?: ReactNode;
17
18
  /** Width: `sm` 400, `md` 560 (default) or `lg` 800px. */
18
19
  size?: DrawerSize;
20
+ /** The close button's label, over QeetrixProvider's and the English default. */
21
+ messages?: Partial<QeetrixMessages["overlay"]>;
19
22
  /** Placement only. */
20
23
  className?: string;
21
24
  style?: CSSProperties;
@@ -38,7 +41,7 @@ export declare function DrawerClose(props: DrawerCloseProps): ReactElement;
38
41
  * focus goes to the first focusable element in the body, as in a dialog; on touch it goes to the
39
42
  * drawer itself, so a phone's keyboard doesn't open over it.
40
43
  */
41
- export declare function DrawerContent({ title, description, size, initialFocus, ref, className, children, ...props }: DrawerContentProps): ReactElement;
44
+ export declare function DrawerContent({ title, description, size, messages, initialFocus, ref, className, children, ...props }: DrawerContentProps): ReactElement;
42
45
  /** The drawer's content; it scrolls, and text in it can be selected without starting a swipe. */
43
46
  export declare function DrawerBody({ className, ...props }: DrawerBodyProps): ReactElement;
44
47
  /** The actions, end-aligned under a rule: tertiary first, then the primary action. */
@@ -1,12 +1,12 @@
1
1
  "use client";
2
2
  import { cx } from "../internal/cx.js";
3
- import { messages } from "../internal/messages.js";
4
3
  import { IconButton } from "../button/button.js";
4
+ import { useMessages } from "../provider/provider.js";
5
5
  import { mergeRefs } from "../internal/refs.js";
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
7
7
  import { createContext, use, useRef } from "react";
8
- import { XIcon } from "@qeetrix/icons/icons/x";
9
8
  import { useDirection } from "@base-ui/react/direction-provider";
9
+ import { XIcon } from "@qeetrix/icons/icons/x";
10
10
  import { Drawer } from "@base-ui/react/drawer";
11
11
  // src/drawer/drawer.tsx
12
12
  const SideContext = createContext("end");
@@ -37,8 +37,9 @@ function DrawerClose(props) {
37
37
  * focus goes to the first focusable element in the body, as in a dialog; on touch it goes to the
38
38
  * drawer itself, so a phone's keyboard doesn't open over it.
39
39
  */
40
- function DrawerContent({ title, description, size = "md", initialFocus, ref, className, children, ...props }) {
40
+ function DrawerContent({ title, description, size = "md", messages, initialFocus, ref, className, children, ...props }) {
41
41
  const side = use(SideContext);
42
+ const strings = useMessages("overlay", messages);
42
43
  const popup = useRef(null);
43
44
  const firstInBody = (type) => type === "touch" || (popup.current?.querySelector(`[data-slot="drawer-body"] :is(${FOCUSABLE})`) ?? true);
44
45
  return /* @__PURE__ */ jsxs(Drawer.Portal, { children: [/* @__PURE__ */ jsx(Drawer.Backdrop, {
@@ -74,7 +75,7 @@ function DrawerContent({ title, description, size = "md", initialFocus, ref, cla
74
75
  "data-slot": "drawer-close",
75
76
  children: /* @__PURE__ */ jsx(Drawer.Close, { render: /* @__PURE__ */ jsx(IconButton, {
76
77
  icon: XIcon,
77
- "aria-label": messages.overlay.close,
78
+ "aria-label": strings.close,
78
79
  size: "sm"
79
80
  }) })
80
81
  })
@@ -0,0 +1,29 @@
1
+ import { QeetrixMessages } from "../../internal/messages.js";
2
+ import { ComponentProps, ReactElement, ReactNode } from "react";
3
+ export type AlertTone = "info" | "success" | "warning" | "danger";
4
+ export interface AlertProps extends Omit<ComponentProps<"div">, "title"> {
5
+ /** Default `info`. The icon and a hidden word ("Warning:") carry it, never colour alone. */
6
+ tone?: AlertTone;
7
+ /** What happened, in a few words: "Directory sync failed". */
8
+ title: ReactNode;
9
+ /** One or two sentences: why, and what to do. */
10
+ children?: ReactNode;
11
+ /** Buttons or links under the text: one primary at most. */
12
+ actions?: ReactNode;
13
+ /** Adds a Dismiss button. The product decides whether the alert comes back. */
14
+ onDismiss?: () => void;
15
+ /**
16
+ * Only for an alert that appears after something happens: `polite` (`role="status"`) waits its
17
+ * turn; `urgent` (`role="alert"`) interrupts, for errors that block the task. An alert that is
18
+ * there when the page loads needs neither.
19
+ */
20
+ announce?: "polite" | "urgent";
21
+ /** This alert's strings, over QeetrixProvider's and the English defaults. */
22
+ messages?: Partial<QeetrixMessages["alert"]>;
23
+ }
24
+ /**
25
+ * A message in the page (07-components.md, C2): above a form, inside a card, or across the top of
26
+ * a console page in AppShell's `notifications`. A tinted surface with the 3px status edge on its
27
+ * start side. It never takes focus and never disappears by itself.
28
+ */
29
+ export declare function Alert({ tone, title, children, actions, onDismiss, announce, messages, className, ...props }: AlertProps): ReactElement;
@@ -0,0 +1,72 @@
1
+ "use client";
2
+ import { cx } from "../../internal/cx.js";
3
+ import { IconButton } from "../../button/button.js";
4
+ import { useMessages } from "../../provider/provider.js";
5
+ import { VisuallyHidden } from "../../visually-hidden/visually-hidden.js";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ import { CircleAlertIcon } from "@qeetrix/icons/icons/circle-alert";
8
+ import { XIcon } from "@qeetrix/icons/icons/x";
9
+ import { CircleCheckIcon } from "@qeetrix/icons/icons/circle-check";
10
+ import { InfoIcon } from "@qeetrix/icons/icons/info";
11
+ import { TriangleAlertIcon } from "@qeetrix/icons/icons/triangle-alert";
12
+ // src/experimental/alert/alert.tsx
13
+ const ICONS = {
14
+ info: InfoIcon,
15
+ success: CircleCheckIcon,
16
+ warning: TriangleAlertIcon,
17
+ danger: CircleAlertIcon
18
+ };
19
+ /**
20
+ * A message in the page (07-components.md, C2): above a form, inside a card, or across the top of
21
+ * a console page in AppShell's `notifications`. A tinted surface with the 3px status edge on its
22
+ * start side. It never takes focus and never disappears by itself.
23
+ */
24
+ function Alert({ tone = "info", title, children, actions, onDismiss, announce, messages, className, ...props }) {
25
+ const strings = useMessages("alert", messages);
26
+ const Icon = ICONS[tone];
27
+ return /* @__PURE__ */ jsxs("div", {
28
+ ...props,
29
+ role: announce === "urgent" ? "alert" : announce === "polite" ? "status" : void 0,
30
+ className: cx("qx-alert", className),
31
+ "data-slot": "alert",
32
+ "data-tone": tone,
33
+ children: [
34
+ /* @__PURE__ */ jsx(Icon, {
35
+ shape: "sharp",
36
+ size: 16,
37
+ "aria-hidden": true,
38
+ className: "qx-alert-icon"
39
+ }),
40
+ /* @__PURE__ */ jsxs("div", {
41
+ className: "qx-alert-body",
42
+ children: [
43
+ /* @__PURE__ */ jsxs("div", {
44
+ className: "qx-alert-title",
45
+ "data-slot": "alert-title",
46
+ children: [/* @__PURE__ */ jsx(VisuallyHidden, { children: `${strings[tone]}: ` }), title]
47
+ }),
48
+ children != null && /* @__PURE__ */ jsx("div", {
49
+ className: "qx-alert-description",
50
+ "data-slot": "alert-description",
51
+ children
52
+ }),
53
+ actions != null && /* @__PURE__ */ jsx("div", {
54
+ className: "qx-alert-actions",
55
+ "data-slot": "alert-actions",
56
+ children: actions
57
+ })
58
+ ]
59
+ }),
60
+ onDismiss && /* @__PURE__ */ jsx(IconButton, {
61
+ icon: XIcon,
62
+ "aria-label": strings.dismiss,
63
+ size: "sm",
64
+ tone: "neutral",
65
+ tooltip: false,
66
+ onClick: onDismiss,
67
+ className: "qx-alert-dismiss"
68
+ })
69
+ ]
70
+ });
71
+ }
72
+ export { Alert };
@@ -0,0 +1,2 @@
1
+ import { Alert, AlertProps, AlertTone } from "./alert.js";
2
+ export { Alert, type AlertProps, type AlertTone };
@@ -0,0 +1,60 @@
1
+ import { QeetrixIcon } from "../../internal/control.js";
2
+ import { QeetrixMessages } from "../../internal/messages.js";
3
+ import { ComponentProps, ReactElement, ReactNode } from "react";
4
+ /** The frame's density; `inherit` leaves it to the page. */
5
+ export type AppShellDensity = "compact" | "default" | "comfortable" | "inherit";
6
+ /** An end-edge panel: help, the assistant, activity. */
7
+ export interface AppShellDrawer {
8
+ id: string;
9
+ /** The trigger's label and the panel's title. */
10
+ label: string;
11
+ /** The trigger's icon, from `@qeetrix/icons`. */
12
+ icon: QeetrixIcon;
13
+ /** The panel's content. */
14
+ content: ReactNode;
15
+ /** A dot on the trigger: something new inside. */
16
+ badge?: boolean;
17
+ }
18
+ export interface AppShellProps extends Omit<ComponentProps<"div">, "children"> {
19
+ /** The global bar: a TopBar. */
20
+ topBar?: ReactNode;
21
+ /** The navigation: a Sidebar. */
22
+ sidebar?: ReactNode;
23
+ /** End-edge panels: help, the assistant, activity. Their triggers sit in the TopBar. */
24
+ drawers?: AppShellDrawer[];
25
+ /** The open panel's id, or `null`, controlled. */
26
+ activeDrawer?: string | null;
27
+ /** The open panel when uncontrolled. Default none. */
28
+ defaultActiveDrawer?: string | null;
29
+ /** Called with the panel that opens, or `null` when it closes. */
30
+ onActiveDrawerChange?: (id: string | null) => void;
31
+ /** Page messages (Alerts) above the page header. */
32
+ notifications?: ReactNode;
33
+ /** The frame's density. Default `compact`: consoles are dense (design rules §15). */
34
+ density?: AppShellDensity;
35
+ /** The page: a PageHeader and PageContent. */
36
+ children?: ReactNode;
37
+ /** Whether the sidebar is collapsed to its icon rail, controlled. */
38
+ sidebarCollapsed?: boolean;
39
+ /** The initial collapsed state when uncontrolled. Default `false`. */
40
+ defaultSidebarCollapsed?: boolean;
41
+ /** Called with the new collapsed state. Persist it in the product, e.g. the user's settings. */
42
+ onSidebarCollapsedChange?: (collapsed: boolean) => void;
43
+ /** The main region's id, which the built-in skip link targets. Default `main`. */
44
+ mainId?: string;
45
+ /** The shell's strings, over QeetrixProvider's and the English defaults. */
46
+ messages?: Partial<QeetrixMessages["appShell"]>;
47
+ }
48
+ export interface AppShellState {
49
+ /** The sidebar is showing as its icon rail. */
50
+ collapsed: boolean;
51
+ }
52
+ /** The AppShell's state, for content inside its sidebar or bar. */
53
+ export declare function useAppShell(): AppShellState;
54
+ /**
55
+ * The frame of a Qeet console (07-components.md, C1 · Ruled): the black scope bar across the full
56
+ * width; the ruled navigation column, which collapses to a 48px icon rail that expands while
57
+ * hovered or focused; the page; and a panel on the end edge that the bar's tools open. Seams are
58
+ * 1px rules, never shadows. Compact by default.
59
+ */
60
+ export declare function AppShell({ topBar, sidebar, drawers, activeDrawer, defaultActiveDrawer, onActiveDrawerChange, notifications, density, children, sidebarCollapsed, defaultSidebarCollapsed, onSidebarCollapsedChange, mainId, messages, className, ...props }: AppShellProps): ReactElement;
@@ -0,0 +1,224 @@
1
+ "use client";
2
+ import { cx } from "../../internal/cx.js";
3
+ import { Tooltip } from "../../tooltip/tooltip.js";
4
+ import { IconButton } from "../../button/button.js";
5
+ import { useMediaQuery } from "../../internal/media.js";
6
+ import { useMessages } from "../../provider/provider.js";
7
+ import { SkipLink } from "../../skip-link/skip-link.js";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ import { createContext, use, useEffect, useId, useRef, useState } from "react";
10
+ import { useDirection } from "@base-ui/react/direction-provider";
11
+ import { XIcon } from "@qeetrix/icons/icons/x";
12
+ import { Drawer } from "@base-ui/react/drawer";
13
+ import { PanelLeftCloseIcon } from "@qeetrix/icons/icons/panel-left-close";
14
+ import { PanelLeftOpenIcon } from "@qeetrix/icons/icons/panel-left-open";
15
+ // src/experimental/app-shell/app-shell.tsx
16
+ /** The `expanded` window class (tokens `breakpoint.expanded`): the sidebar is a column from here. */
17
+ const WIDE = "(width >= 840px)";
18
+ /** How long the pointer rests on the rail before it expands over the page. */
19
+ const PEEK_DELAY = 200;
20
+ const ShellContext = createContext(null);
21
+ const outside = {
22
+ collapsed: false,
23
+ inDrawer: false,
24
+ narrow: false,
25
+ expand: () => {},
26
+ closeDrawer: () => {},
27
+ openNavigation: () => {},
28
+ drawers: [],
29
+ activeDrawer: null,
30
+ toggleDrawer: () => {},
31
+ strings: {
32
+ collapse: "",
33
+ expand: "",
34
+ open: "",
35
+ navigation: "",
36
+ tools: "",
37
+ closePanel: ""
38
+ }
39
+ };
40
+ /** The shell's state, for its own parts. Outside an AppShell, a sidebar is expanded. */
41
+ function useShellState() {
42
+ return use(ShellContext) ?? outside;
43
+ }
44
+ /** The AppShell's state, for content inside its sidebar or bar. */
45
+ function useAppShell() {
46
+ const { collapsed, inDrawer } = useShellState();
47
+ return { collapsed: collapsed && !inDrawer };
48
+ }
49
+ /**
50
+ * The frame of a Qeet console (07-components.md, C1 · Ruled): the black scope bar across the full
51
+ * width; the ruled navigation column, which collapses to a 48px icon rail that expands while
52
+ * hovered or focused; the page; and a panel on the end edge that the bar's tools open. Seams are
53
+ * 1px rules, never shadows. Compact by default.
54
+ */
55
+ function AppShell({ topBar, sidebar, drawers = [], activeDrawer, defaultActiveDrawer = null, onActiveDrawerChange, notifications, density = "compact", children, sidebarCollapsed, defaultSidebarCollapsed = false, onSidebarCollapsedChange, mainId = "main", messages, className, ...props }) {
56
+ const strings = useMessages("appShell", messages);
57
+ const skip = useMessages("skipLink");
58
+ const wide = useMediaQuery(WIDE, true);
59
+ const rtl = useDirection() === "rtl";
60
+ const panelTitle = useId();
61
+ const [ownCollapsed, setOwnCollapsed] = useState(defaultSidebarCollapsed);
62
+ const collapsed = sidebarCollapsed ?? ownCollapsed;
63
+ const setCollapsed = (next) => {
64
+ if (sidebarCollapsed === void 0) setOwnCollapsed(next);
65
+ onSidebarCollapsedChange?.(next);
66
+ };
67
+ const [ownDrawer, setOwnDrawer] = useState(defaultActiveDrawer);
68
+ const active = activeDrawer === void 0 ? ownDrawer : activeDrawer;
69
+ const setActive = (next) => {
70
+ if (activeDrawer === void 0) setOwnDrawer(next);
71
+ onActiveDrawerChange?.(next);
72
+ };
73
+ const panel = drawers.find((drawer) => drawer.id === active);
74
+ const [navOpen, setNavOpen] = useState(false);
75
+ const [peek, setPeek] = useState(false);
76
+ const timer = useRef(void 0);
77
+ useEffect(() => () => window.clearTimeout(timer.current), []);
78
+ const startPeek = () => {
79
+ if (!collapsed) return;
80
+ window.clearTimeout(timer.current);
81
+ timer.current = window.setTimeout(() => setPeek(true), PEEK_DELAY);
82
+ };
83
+ const endPeek = () => {
84
+ window.clearTimeout(timer.current);
85
+ setPeek(false);
86
+ };
87
+ const shell = {
88
+ collapsed: collapsed && !peek,
89
+ inDrawer: false,
90
+ narrow: !wide,
91
+ expand: () => {
92
+ endPeek();
93
+ setCollapsed(false);
94
+ },
95
+ closeDrawer: () => setNavOpen(false),
96
+ openNavigation: () => setNavOpen(true),
97
+ drawers,
98
+ activeDrawer: active,
99
+ toggleDrawer: (id) => setActive(active === id ? null : id),
100
+ strings
101
+ };
102
+ const collapseControl = /* @__PURE__ */ jsxs("button", {
103
+ type: "button",
104
+ className: "qx-app-shell-collapse",
105
+ "aria-expanded": !collapsed,
106
+ onClick: () => {
107
+ endPeek();
108
+ setCollapsed(!collapsed);
109
+ },
110
+ children: [/* @__PURE__ */ jsx(collapsed ? PanelLeftOpenIcon : PanelLeftCloseIcon, {
111
+ shape: "round",
112
+ size: 16,
113
+ "aria-hidden": true,
114
+ className: "qx-app-shell-collapse-icon"
115
+ }), /* @__PURE__ */ jsx("span", {
116
+ className: "qx-app-shell-collapse-label",
117
+ children: collapsed ? strings.expand : strings.collapse
118
+ })]
119
+ });
120
+ return /* @__PURE__ */ jsx(ShellContext, {
121
+ value: shell,
122
+ children: /* @__PURE__ */ jsxs("div", {
123
+ ...props,
124
+ className: cx("qx-app-shell", className),
125
+ "data-slot": "app-shell",
126
+ "data-qx-density": density === "inherit" ? void 0 : density,
127
+ "data-collapsed": collapsed ? "" : void 0,
128
+ "data-drawer-open": panel ? "" : void 0,
129
+ children: [
130
+ /* @__PURE__ */ jsx(SkipLink, {
131
+ href: `#${mainId}`,
132
+ children: skip.label
133
+ }),
134
+ topBar != null && /* @__PURE__ */ jsx("div", {
135
+ className: "qx-app-shell-top",
136
+ children: topBar
137
+ }),
138
+ sidebar != null && /* @__PURE__ */ jsx("div", {
139
+ className: "qx-app-shell-nav",
140
+ "data-slot": "app-shell-nav",
141
+ "data-peek": peek ? "" : void 0,
142
+ onPointerEnter: startPeek,
143
+ onPointerLeave: endPeek,
144
+ onFocus: () => collapsed && setPeek(true),
145
+ onBlur: (event) => {
146
+ if (!event.currentTarget.contains(event.relatedTarget)) setPeek(false);
147
+ },
148
+ children: /* @__PURE__ */ jsxs("div", {
149
+ className: "qx-app-shell-nav-panel",
150
+ children: [/* @__PURE__ */ jsx("div", {
151
+ className: "qx-app-shell-nav-body",
152
+ children: sidebar
153
+ }), /* @__PURE__ */ jsx("div", {
154
+ className: "qx-app-shell-nav-foot",
155
+ children: /* @__PURE__ */ jsx(Tooltip, {
156
+ content: strings.expand,
157
+ side: "inline-end",
158
+ disabled: !collapsed || peek,
159
+ children: collapseControl
160
+ })
161
+ })]
162
+ })
163
+ }),
164
+ /* @__PURE__ */ jsxs("main", {
165
+ id: mainId,
166
+ tabIndex: -1,
167
+ className: "qx-app-shell-main",
168
+ "data-slot": "app-shell-main",
169
+ children: [notifications != null && /* @__PURE__ */ jsx("div", {
170
+ className: "qx-app-shell-notifications",
171
+ "data-slot": "app-shell-notifications",
172
+ children: notifications
173
+ }), children]
174
+ }),
175
+ panel && /* @__PURE__ */ jsxs("aside", {
176
+ className: "qx-app-shell-panel",
177
+ "data-slot": "app-shell-panel",
178
+ "aria-labelledby": panelTitle,
179
+ children: [/* @__PURE__ */ jsxs("div", {
180
+ className: "qx-app-shell-panel-header",
181
+ children: [/* @__PURE__ */ jsx("h2", {
182
+ id: panelTitle,
183
+ className: "qx-app-shell-panel-title",
184
+ children: panel.label
185
+ }), /* @__PURE__ */ jsx(IconButton, {
186
+ icon: XIcon,
187
+ "aria-label": strings.closePanel,
188
+ size: "sm",
189
+ tone: "neutral",
190
+ tooltip: false,
191
+ onClick: () => setActive(null)
192
+ })]
193
+ }), /* @__PURE__ */ jsx("div", {
194
+ className: "qx-app-shell-panel-body",
195
+ children: panel.content
196
+ })]
197
+ }),
198
+ sidebar != null && /* @__PURE__ */ jsx(Drawer.Root, {
199
+ open: navOpen && !wide,
200
+ onOpenChange: setNavOpen,
201
+ swipeDirection: rtl ? "right" : "left",
202
+ children: /* @__PURE__ */ jsxs(Drawer.Portal, { children: [/* @__PURE__ */ jsx(Drawer.Backdrop, { className: "qx-app-shell-backdrop" }), /* @__PURE__ */ jsx(Drawer.Viewport, {
203
+ className: "qx-app-shell-viewport",
204
+ children: /* @__PURE__ */ jsx(Drawer.Popup, {
205
+ className: "qx-app-shell-drawer",
206
+ "data-slot": "app-shell-drawer",
207
+ "data-qx-density": density === "inherit" ? void 0 : density,
208
+ "aria-label": strings.navigation,
209
+ children: /* @__PURE__ */ jsx(ShellContext, {
210
+ value: {
211
+ ...shell,
212
+ collapsed: false,
213
+ inDrawer: true
214
+ },
215
+ children: sidebar
216
+ })
217
+ })
218
+ })] })
219
+ })
220
+ ]
221
+ })
222
+ });
223
+ }
224
+ export { AppShell, useAppShell, useShellState };