@flamingo-stack/openframe-frontend-core 0.0.838 → 0.0.840

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 (137) hide show
  1. package/dist/build/export-map.json +1 -1
  2. package/dist/{chunk-JFR3OFE6.cjs → chunk-275AQ6LC.cjs} +6 -6
  3. package/dist/{chunk-5BM7X4GA.cjs → chunk-5GBLXY5F.cjs} +2 -2
  4. package/dist/{chunk-5L3UKDC6.cjs → chunk-6LDL3UZE.cjs} +9 -9
  5. package/dist/{chunk-SWMUW3RR.cjs → chunk-744JXTDP.cjs} +19 -19
  6. package/dist/{chunk-46LATWQR.cjs → chunk-7WKRMJI6.cjs} +5 -5
  7. package/dist/{chunk-2X5LYDNA.cjs → chunk-AT6D7BAZ.cjs} +5 -5
  8. package/dist/{chunk-C4OQIYGW.cjs → chunk-CCRXBFNL.cjs} +8 -8
  9. package/dist/{chunk-LC65AC6M.cjs → chunk-COWMXUHN.cjs} +2 -0
  10. package/dist/{chunk-5E4YEHHO.cjs → chunk-DGY5KEPY.cjs} +55 -55
  11. package/dist/{chunk-B4THEQOW.cjs → chunk-DYDQLS2R.cjs} +744 -561
  12. package/dist/{chunk-LWMWY3JO.cjs → chunk-E7M54JWW.cjs} +14 -14
  13. package/dist/{chunk-Z7A7IZWL.cjs → chunk-EV6BPF47.cjs} +236 -210
  14. package/dist/{chunk-7XRFAHJF.cjs → chunk-FHI4I3TQ.cjs} +9 -9
  15. package/dist/{chunk-ZABZIM2Y.cjs → chunk-FRL2ZN4O.cjs} +3 -3
  16. package/dist/{chunk-7Z536SPR.cjs → chunk-FW7RKWPY.cjs} +15 -1
  17. package/dist/{chunk-U4MJ2Z4V.cjs → chunk-GMK4JGC2.cjs} +46 -46
  18. package/dist/{chunk-NV5AHL4G.cjs → chunk-GUKFMMDZ.cjs} +3 -3
  19. package/dist/{chunk-JI34QKPJ.cjs → chunk-HXTZZE3R.cjs} +253 -131
  20. package/dist/{chunk-MMPMQUQM.cjs → chunk-IELPVUPR.cjs} +18 -18
  21. package/dist/{chunk-64PF3UC7.cjs → chunk-INBFKUZA.cjs} +17 -17
  22. package/dist/{chunk-APOMNC7H.cjs → chunk-IUDBG3HJ.cjs} +3 -3
  23. package/dist/{chunk-4JHO2NVI.cjs → chunk-J2IXYCEH.cjs} +26 -10
  24. package/dist/{chunk-V322TUBY.cjs → chunk-JM2XA7VY.cjs} +28 -28
  25. package/dist/{chunk-GPXKG34G.cjs → chunk-JUEAZCD6.cjs} +46 -46
  26. package/dist/{chunk-2QAUQPGC.cjs → chunk-MYAPG3FQ.cjs} +21 -21
  27. package/dist/{chunk-GRHF5SWL.cjs → chunk-NFNSPLGC.cjs} +3 -3
  28. package/dist/{chunk-4TUHHKLR.cjs → chunk-Q6WUHYOG.cjs} +72 -72
  29. package/dist/{chunk-H3WTYAVR.cjs → chunk-RSJYTQEA.cjs} +30 -30
  30. package/dist/{chunk-MG3HJIGG.cjs → chunk-UAI4VMEF.cjs} +4 -4
  31. package/dist/{chunk-S5RMRQKT.cjs → chunk-UZPYYHF5.cjs} +18 -18
  32. package/dist/{chunk-TMD55O5U.cjs → chunk-VBH2RCFM.cjs} +73 -73
  33. package/dist/{chunk-FNNDGOR3.cjs → chunk-WCK5T3UD.cjs} +43 -43
  34. package/dist/{chunk-TRZEFT2N.cjs → chunk-WI7EXAZT.cjs} +9 -5
  35. package/dist/{chunk-7QNGILIY.cjs → chunk-Z35PL3UR.cjs} +32 -32
  36. package/dist/{chunk-ENK75WN4.cjs → chunk-ZAO5SYBJ.cjs} +4 -4
  37. package/dist/{cloud-tenants-GLPDB2F5.cjs → cloud-tenants-PV63RZ73.cjs} +19 -19
  38. package/dist/components/case-studies/index.cjs +33 -33
  39. package/dist/components/chat/chat-nav-sidebar.d.ts.map +1 -1
  40. package/dist/components/chat/chat-nav-sidebar.js +17 -3
  41. package/dist/components/chat/index.cjs +13 -13
  42. package/dist/components/contact/index.cjs +20 -20
  43. package/dist/components/docs/index.cjs +23 -23
  44. package/dist/components/embeds/index.cjs +20 -20
  45. package/dist/components/faq/index.cjs +21 -21
  46. package/dist/components/features/command-box.d.ts.map +1 -1
  47. package/dist/components/features/command-box.js +4 -23
  48. package/dist/components/features/index.cjs +29 -19
  49. package/dist/components/features/software/index.d.ts +1 -0
  50. package/dist/components/features/software/index.d.ts.map +1 -1
  51. package/dist/components/features/software/index.js +12 -0
  52. package/dist/components/features/software/software-action-form.d.ts +9 -1
  53. package/dist/components/features/software/software-action-form.d.ts.map +1 -1
  54. package/dist/components/features/software/software-action-form.js +18 -1
  55. package/dist/components/features/software/update-settings-fields.d.ts +43 -0
  56. package/dist/components/features/software/update-settings-fields.d.ts.map +1 -0
  57. package/dist/components/features/software/update-settings-fields.js +202 -0
  58. package/dist/components/help-center-pages/download-apps-page.d.ts +1 -1
  59. package/dist/components/help-center-pages/download-apps-page.d.ts.map +1 -1
  60. package/dist/components/help-center-pages/download-apps-page.js +2 -3
  61. package/dist/components/help-center-pages/index.cjs +122 -123
  62. package/dist/components/index.cjs +86 -76
  63. package/dist/components/meeting-scheduler/index.cjs +82 -82
  64. package/dist/components/navigation/account-popover.d.ts.map +1 -1
  65. package/dist/components/navigation/account-popover.js +26 -2
  66. package/dist/components/navigation/app-layout-side-panel.d.ts.map +1 -1
  67. package/dist/components/navigation/app-layout-side-panel.js +2 -9
  68. package/dist/components/navigation/index.cjs +22 -22
  69. package/dist/components/navigation/navigation-sidebar-grouped.d.ts.map +1 -1
  70. package/dist/components/navigation/navigation-sidebar-grouped.js +30 -6
  71. package/dist/components/navigation/navigation-sidebar-item.d.ts.map +1 -1
  72. package/dist/components/navigation/navigation-sidebar-item.js +14 -1
  73. package/dist/components/navigation/navigation-sidebar.d.ts.map +1 -1
  74. package/dist/components/navigation/navigation-sidebar.js +72 -3
  75. package/dist/components/onboarding-guides/index.cjs +22 -22
  76. package/dist/components/product-demo/fixtures/software-update.d.ts +2 -0
  77. package/dist/components/product-demo/fixtures/software-update.d.ts.map +1 -1
  78. package/dist/components/product-demo/fixtures/software-update.js +3 -2
  79. package/dist/components/product-demo/index.cjs +30 -30
  80. package/dist/components/related-content/index.cjs +8 -8
  81. package/dist/components/tickets/index.cjs +24 -24
  82. package/dist/components/ui/index.cjs +19 -19
  83. package/dist/components/ui/panel-resize-handle.d.ts +27 -6
  84. package/dist/components/ui/panel-resize-handle.d.ts.map +1 -1
  85. package/dist/components/ui/panel-resize-handle.js +27 -3
  86. package/dist/{devices-GHPW5WCO.cjs → devices-VECG5JNC.cjs} +20 -20
  87. package/dist/{embeddable-chat-RBC7F4TL.cjs → embeddable-chat-VRYAQQF6.cjs} +90 -90
  88. package/dist/hooks/index.cjs +11 -7
  89. package/dist/hooks/ui/index.d.ts +1 -0
  90. package/dist/hooks/ui/index.d.ts.map +1 -1
  91. package/dist/hooks/ui/index.js +1 -0
  92. package/dist/hooks/ui/use-clipped-label-tooltip.d.ts +14 -0
  93. package/dist/hooks/ui/use-clipped-label-tooltip.d.ts.map +1 -0
  94. package/dist/hooks/ui/use-clipped-label-tooltip.js +13 -0
  95. package/dist/hooks/ui/use-is-truncated.d.ts.map +1 -1
  96. package/dist/hooks/ui/use-is-truncated.js +1 -0
  97. package/dist/hooks/ui/use-media-query.d.ts.map +1 -1
  98. package/dist/hooks/ui/use-media-query.js +1 -0
  99. package/dist/hooks/ui/use-resizable-panel-size.d.ts +1 -0
  100. package/dist/hooks/ui/use-resizable-panel-size.d.ts.map +1 -1
  101. package/dist/hooks/ui/use-resizable-panel-size.js +5 -1
  102. package/dist/index.cjs +38 -24
  103. package/dist/{knowledge-4TOSY2S5.cjs → knowledge-YJSDJIXT.cjs} +20 -20
  104. package/dist/{logs-MUODFY6N.cjs → logs-42JG3NZ5.cjs} +26 -26
  105. package/dist/{policies-TJHO3VLE.cjs → policies-D4QNVJ26.cjs} +23 -23
  106. package/dist/{remote-session-CN3G7JNR.cjs → remote-session-EE6477O3.cjs} +10 -10
  107. package/dist/{scripts-X5CMYE2V.cjs → scripts-4VFYBXBE.cjs} +26 -26
  108. package/dist/{software-update-2UZKHTYM.cjs → software-update-OSDZQ5CP.cjs} +25 -25
  109. package/dist/{tickets-board-PLYD2NSY.cjs → tickets-board-44HO3LR4.cjs} +10 -10
  110. package/dist/types/navigation.d.ts +36 -0
  111. package/dist/types/navigation.d.ts.map +1 -1
  112. package/dist/utils/notifications.cjs +2 -0
  113. package/dist/utils/notifications.d.ts +1 -1
  114. package/dist/utils/notifications.d.ts.map +1 -1
  115. package/dist/utils/notifications.js +2 -0
  116. package/package.json +1 -1
  117. package/src/components/chat/chat-nav-sidebar.tsx +17 -4
  118. package/src/components/features/command-box.tsx +17 -19
  119. package/src/components/features/notifications/notification-category-icon.tsx +1 -1
  120. package/src/components/features/software/index.ts +11 -0
  121. package/src/components/features/software/software-action-form.tsx +28 -1
  122. package/src/components/features/software/update-settings-fields.tsx +265 -0
  123. package/src/components/help-center-pages/download-apps-page.tsx +4 -5
  124. package/src/components/navigation/account-popover.tsx +31 -2
  125. package/src/components/navigation/app-layout-side-panel.tsx +2 -7
  126. package/src/components/navigation/navigation-sidebar-grouped.tsx +37 -6
  127. package/src/components/navigation/navigation-sidebar-item.tsx +18 -1
  128. package/src/components/navigation/navigation-sidebar.tsx +116 -7
  129. package/src/components/product-demo/fixtures/software-update.ts +2 -2
  130. package/src/components/ui/panel-resize-handle.tsx +50 -8
  131. package/src/hooks/ui/index.ts +1 -0
  132. package/src/hooks/ui/use-clipped-label-tooltip.ts +20 -0
  133. package/src/hooks/ui/use-is-truncated.ts +2 -0
  134. package/src/hooks/ui/use-media-query.ts +2 -0
  135. package/src/hooks/ui/use-resizable-panel-size.ts +7 -1
  136. package/src/types/navigation.ts +37 -0
  137. package/src/utils/notifications.ts +2 -0
@@ -1,13 +1,15 @@
1
1
  'use client';
2
2
 
3
3
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
- import type { CSSProperties, MouseEvent, ReactNode } from 'react';
4
+ import type { CSSProperties, MouseEvent, ReactNode, RefObject } from 'react';
5
5
  import { useIsHydrated } from '../../hooks/ui/use-is-hydrated';
6
6
  import { useIsomorphicLayoutEffect } from '../../hooks/ui/use-isomorphic-layout-effect';
7
7
  import { useLocalStorage } from '../../hooks/ui/use-local-storage';
8
8
  import { useLgUp, useMdUp } from '../../hooks/ui/use-media-query';
9
+ import { useResizablePanelSize } from '../../hooks/ui/use-resizable-panel-size';
9
10
  import type { NavigationSidebarConfig, NavigationSidebarItem } from '../../types/navigation';
10
11
  import { cn } from '../../utils';
12
+ import { PanelResizeGrip, PanelResizeHandle } from '../ui/panel-resize-handle';
11
13
  import { NAVIGATION_SIDEBAR_ATTRIBUTE } from './app-layout-context';
12
14
  import {
13
15
  NavigationSidebarGroupedEntries,
@@ -24,6 +26,12 @@ const MINIMIZED_WIDTH = 56; // 3.5rem = 56px
24
26
  const EXPANDED_WIDTH = 224; // 14rem = 224px — the default; a grouped config may set its own
25
27
  /** The narrowest custom `expandedWidth`: the rows read 140px and up as expanded. */
26
28
  const MIN_CUSTOM_EXPANDED_WIDTH = 141;
29
+ /**
30
+ * Px of its row a resizable sidebar leaves to the content beside it
31
+ * (`NavigationSidebarResizeConfig.minContentWidth`): the `content-md`
32
+ * breakpoint, under which page content lays out for a phone.
33
+ */
34
+ const DEFAULT_MIN_CONTENT_WIDTH = 720;
27
35
  const STORAGE_KEY = 'of.navigationSidebar.minimized';
28
36
 
29
37
  /**
@@ -146,6 +154,27 @@ const SIDEBAR_TABLET_WIDTH = {
146
154
  customOverlay: 'md:w-[var(--of-navigation-sidebar-expanded-width)]',
147
155
  } as const;
148
156
 
157
+ /**
158
+ * The width of the sidebar's row — its parent, the room it shares with the
159
+ * content — for the resize clamp. 0 until measured, and when there is nothing to
160
+ * measure (not resizable, no parent, the server).
161
+ */
162
+ function useRowWidth(asideRef: RefObject<HTMLElement | null>, enabled: boolean): number {
163
+ const [width, setWidth] = useState(0);
164
+ useEffect(() => {
165
+ const row = enabled ? asideRef.current?.parentElement : null;
166
+ if (!row) return undefined;
167
+ const update = () => setWidth(row.clientWidth);
168
+ update();
169
+ // An environment without ResizeObserver (jsdom in a consumer's tests) measures once.
170
+ if (typeof ResizeObserver === 'undefined') return undefined;
171
+ const observer = new ResizeObserver(update);
172
+ observer.observe(row);
173
+ return () => observer.disconnect();
174
+ }, [asideRef, enabled]);
175
+ return width;
176
+ }
177
+
149
178
  /** A click the browser will resolve itself — new tab, new window, middle button. */
150
179
  export const isModifiedClick = (event?: MouseEvent): boolean =>
151
180
  !!event && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0);
@@ -255,18 +284,74 @@ export function NavigationSidebar({
255
284
 
256
285
  // A grouped config's own expanded width (`expandedWidth`); `null` = the
257
286
  // default 224px through the literal classes, exactly as before.
258
- const customExpandedWidth =
287
+ const configExpandedWidth =
259
288
  config.variant === 'grouped' && config.expandedWidth !== undefined
260
289
  ? Math.max(config.expandedWidth, MIN_CUSTOM_EXPANDED_WIDTH)
261
290
  : null;
262
- const expandedWidth = customExpandedWidth ?? EXPANDED_WIDTH;
291
+
292
+ // `config.resizable`: the expanded width is the user's — dragged or stepped on
293
+ // the handle below, kept under the config's storage key — and the config width
294
+ // is where it starts. Clamped to the config's bounds and to the row, so the
295
+ // content beside the rail keeps `minContentWidth`. Not resizable: the config
296
+ // width, and the hook reads and writes nothing.
297
+ const resize = config.resizable;
298
+ const asideRef = useRef<HTMLElement>(null);
299
+ const rowWidth = useRowWidth(asideRef, resize !== undefined);
300
+ const resizeMinWidth = Math.max(resize?.minWidth ?? MIN_CUSTOM_EXPANDED_WIDTH, MIN_CUSTOM_EXPANDED_WIDTH);
301
+ const {
302
+ size: chosenExpandedWidth,
303
+ setSize: setChosenExpandedWidth,
304
+ resetSize: resetChosenExpandedWidth,
305
+ clampSize: clampExpandedWidth,
306
+ } = useResizablePanelSize({
307
+ enabled: resize !== undefined,
308
+ minSize: resizeMinWidth,
309
+ maxSize: resize?.maxWidth ?? Number.POSITIVE_INFINITY,
310
+ defaultSize: configExpandedWidth ?? EXPANDED_WIDTH,
311
+ available: rowWidth,
312
+ reserve: resize?.minContentWidth ?? DEFAULT_MIN_CONTENT_WIDTH,
313
+ storageKey: resize?.storageKey,
314
+ });
315
+
316
+ // The desktop width — what the effect below publishes. The chosen width may
317
+ // be used here as soon as it is known: an effect never runs into the SSR HTML.
318
+ const expandedWidth = resize !== undefined ? chosenExpandedWidth : (configExpandedWidth ?? EXPANDED_WIDTH);
319
+ // The width in the MARKUP — the inline custom property on the rail and its
320
+ // tablet slot, which the tablet overlay opens to and the desktop rule falls
321
+ // back on, and the handle's `aria-valuenow`: the chosen width only once
322
+ // hydrated. The server could not read it, and a first client render that
323
+ // differs is a hydration mismatch, which React reports and leaves in the DOM.
324
+ // Until then the config width stands, as for a sidebar that is not resizable;
325
+ // on desktop the seeded width shows throughout, so nothing moves.
326
+ const markupExpandedWidth = hydrated ? chosenExpandedWidth : (configExpandedWidth ?? EXPANDED_WIDTH);
327
+ const customExpandedWidth = resize !== undefined ? markupExpandedWidth : configExpandedWidth;
263
328
  const expandedWidthStyle =
264
329
  customExpandedWidth === null ? undefined : ({ [EXPANDED_WIDTH_VAR]: `${customExpandedWidth}px` } as CSSProperties);
265
330
 
331
+ // A drag moves the rail's own width property on every pointer move and marks
332
+ // the rail (`data-resizing`, read by a class on it) so its width transition
333
+ // does not trail the pointer; the choice is committed once, on release, which
334
+ // also lifts the mark. Neither write concerns hydration: both run on the
335
+ // client only, once the user has the handle.
336
+ const dragExpandedWidth = useCallback((next: number) => {
337
+ const aside = asideRef.current;
338
+ if (!aside) return;
339
+ aside.setAttribute('data-resizing', '');
340
+ aside.style.setProperty(NAVIGATION_SIDEBAR_WIDTH_VAR, `${Math.round(next)}px`);
341
+ }, []);
342
+ const commitExpandedWidth = useCallback(
343
+ (next: number) => {
344
+ asideRef.current?.removeAttribute('data-resizing');
345
+ setChosenExpandedWidth(next);
346
+ },
347
+ [setChosenExpandedWidth],
348
+ );
349
+
266
350
  // A new expanded width (the host swapped configs — e.g. a placeholder for
267
- // the grouped tree) is a different sidebar, not a toggle: it snaps instead of
268
- // animating. The transition is dropped in the same commit that moves the
269
- // width and comes back a frame later (the effect below).
351
+ // the grouped tree; a resize committed, stepped or reset on the handle) is a
352
+ // different sidebar, not a toggle: it snaps instead of animating. The
353
+ // transition is dropped in the same commit that moves the width and comes
354
+ // back a frame later (the effect below).
270
355
  const [widthFor, setWidthFor] = useState(expandedWidth);
271
356
  if (widthFor !== expandedWidth) {
272
357
  setWidthFor(expandedWidth);
@@ -447,7 +532,6 @@ export function NavigationSidebar({
447
532
  // On the rail, not on `<html>` where the seed sits: the rail is its only reader,
448
533
  // and an inherited custom property on the root made WebKit restyle the whole
449
534
  // document on every toggle. Its own value overrides the inherited seed.
450
- const asideRef = useRef<HTMLElement>(null);
451
535
  useIsomorphicLayoutEffect(() => {
452
536
  asideRef.current?.style.setProperty(NAVIGATION_SIDEBAR_WIDTH_VAR, sidebarWidth);
453
537
  }, [sidebarWidth]);
@@ -555,6 +639,9 @@ export function NavigationSidebar({
555
639
  ? SIDEBAR_TABLET_WIDTH.overlay
556
640
  : SIDEBAR_TABLET_WIDTH.customOverlay,
557
641
  transitionsEnabled && widthTransition,
642
+ // A drag on the resize handle in progress: the width follows the
643
+ // pointer, with no transition trailing it (`dragExpandedWidth`).
644
+ 'data-[resizing]:transition-none',
558
645
  // Snaps where its width moves the content: the desktop rail, and the
559
646
  // tablet overlay too when it pushes (its slot above snaps with it).
560
647
  !animateWidth && (pushOnTablet ? 'transition-none' : 'lg:transition-none'),
@@ -632,6 +719,28 @@ export function NavigationSidebar({
632
719
  onToggle={handleToggle}
633
720
  compact={grouped}
634
721
  />
722
+
723
+ {/* The resize handle (`config.resizable`), astride the rail's right edge —
724
+ the grip the Mingo chat drawer had — on desktop while expanded: the
725
+ 56px rail has nothing to resize, and below `lg` the width is the
726
+ tablet's. The 24px track straddles the border, half of it over the
727
+ content's edge; the grip marks the border as draggable. */}
728
+ {resize !== undefined && !minimized && (
729
+ <PanelResizeHandle
730
+ side="left"
731
+ variant="inLayout"
732
+ size={markupExpandedWidth}
733
+ minSize={resizeMinWidth}
734
+ maxSize={clampExpandedWidth(Number.POSITIVE_INFINITY)}
735
+ clampSize={clampExpandedWidth}
736
+ onSize={commitExpandedWidth}
737
+ onDrag={dragExpandedWidth}
738
+ onReset={resetChosenExpandedWidth}
739
+ ariaLabel="Resize navigation sidebar"
740
+ className="inset-y-0 hidden w-6 -translate-x-1/2 justify-center pl-0 lg:flex"
741
+ grip={<PanelResizeGrip />}
742
+ />
743
+ )}
635
744
  </aside>
636
745
  </>
637
746
  );
@@ -16,7 +16,7 @@ const chrome: SelectedPackage = {
16
16
  const TONIGHT_11_PM = { value: '23:00', label: '11:00 PM' };
17
17
 
18
18
  /** A fixed reading of the schedule fields: tonight's late slots, nothing in the past. */
19
- const timing: SoftwareScheduleTiming = {
19
+ export const SOFTWARE_DEMO_TIMING: SoftwareScheduleTiming = {
20
20
  timeReferenceOptions: [
21
21
  { value: 'SERVER', label: 'Your account timezone' },
22
22
  { value: 'DEVICE_LOCAL', label: 'Device local timezone' },
@@ -49,7 +49,7 @@ export function buildSoftwareUpdateFixture(cast: DemoCast) {
49
49
  return {
50
50
  action: 'UPDATE',
51
51
  package: chrome,
52
- timing,
52
+ timing: SOFTWARE_DEMO_TIMING,
53
53
  initialValues,
54
54
  devices,
55
55
  selectedIds: new Set<string>([DEMO_DEVICES.reception.id, DEMO_DEVICES.samT14.id, DEMO_DEVICES.frontDesk.id]),
@@ -3,13 +3,37 @@
3
3
  import { type KeyboardEvent, type PointerEvent, type ReactNode, type RefObject, useEffect, useRef } from 'react';
4
4
 
5
5
  import { cn } from '../../utils/cn';
6
+ import { Menu01Icon } from '../icons-v2-generated';
6
7
 
7
8
  /**
8
- * The drag-to-resize handle of a side or edge panel, shared by `Drawer` and
9
- * `AppLayoutDrawer`. A drag writes the panel's size to the DOM and reports it
10
- * once, on release; arrow keys, Home and End report at once.
9
+ * The drag-to-resize handle of a side or edge panel, shared by `Drawer`,
10
+ * `AppLayoutDrawer`, `ComposerSheet` and `NavigationSidebar`. A drag writes the
11
+ * panel's size to the DOM (or hands it to `onDrag`) and reports it once, on
12
+ * release; arrow keys, Home and End report at once; a double click asks for the
13
+ * default (`onReset`).
11
14
  */
12
15
 
16
+ /**
17
+ * The grip the design draws on a resizable edge (its `resize-handle`): a 16×32
18
+ * pill in the page's background astride the panel's border, with a three-line
19
+ * mark. `NavigationSidebar` hands it to the handle as its `grip`, and
20
+ * `AppLayoutSidePanel`'s own handle draws it too.
21
+ */
22
+ export function PanelResizeGrip({ className }: { className?: string }) {
23
+ return (
24
+ <span
25
+ aria-hidden
26
+ className={cn(
27
+ 'flex h-8 w-4 items-center justify-center rounded-sm border border-ods-border bg-ods-bg text-ods-text-secondary transition-colors',
28
+ 'group-hover:border-ods-border-hover group-focus-visible:border-ods-accent group-focus-visible:text-ods-accent',
29
+ className,
30
+ )}
31
+ >
32
+ <Menu01Icon size={12} />
33
+ </span>
34
+ );
35
+ }
36
+
13
37
  type PanelSide = 'right' | 'left' | 'top' | 'bottom';
14
38
 
15
39
  const HORIZONTAL_SIDES: ReadonlySet<PanelSide> = new Set(['left', 'right']);
@@ -37,8 +61,18 @@ export interface PanelResizeHandleProps {
37
61
  onSize: (next: number) => void;
38
62
  /** Clamp a candidate size to the min/max and the container. */
39
63
  clampSize: (next: number) => number;
40
- /** The panel the size applies to — written directly while dragging. */
41
- panelRef: RefObject<HTMLDivElement | null>;
64
+ /** The panel the size applies to — written directly while dragging. Omit for a
65
+ * panel sized by other means (`onDrag`). */
66
+ panelRef?: RefObject<HTMLDivElement | null>;
67
+ /**
68
+ * Every size the drag reaches, in place of writing `panelRef`'s inline size:
69
+ * for a panel whose width is a custom property or a class rather than an
70
+ * inline style, or one that marks itself while dragged. `onSize` still
71
+ * reports once, on release.
72
+ */
73
+ onDrag?: (next: number) => void;
74
+ /** A double click on the handle: back to the default size. */
75
+ onReset?: () => void;
42
76
  /** `overlay` is the body-level `Drawer`'s handle, `inLayout` the wider one of
43
77
  * `AppLayoutDrawer`. They differ in track width and grip colour only. */
44
78
  variant: 'overlay' | 'inLayout';
@@ -57,6 +91,8 @@ export function PanelResizeHandle({
57
91
  onSize,
58
92
  clampSize,
59
93
  panelRef,
94
+ onDrag,
95
+ onReset,
60
96
  variant,
61
97
  ariaLabel,
62
98
  className,
@@ -95,12 +131,17 @@ export function PanelResizeHandle({
95
131
  // the size is unchanged, so it leaves the style alone.
96
132
  const handlePointerMove = (e: PointerEvent<HTMLDivElement>) => {
97
133
  const start = startRef.current;
98
- const panel = panelRef.current;
99
- if (!start || !panel) return;
134
+ if (!start) return;
135
+ if (!onDrag && !panelRef?.current) return;
100
136
  const delta = isHorizontal ? e.clientX - start.x : e.clientY - start.y;
101
137
  const next = clampSize(start.size + delta * direction);
102
138
  dragSizeRef.current = next;
103
- panel.style[isHorizontal ? 'width' : 'height'] = `${next}px`;
139
+ if (onDrag) {
140
+ onDrag(next);
141
+ } else if (panelRef) {
142
+ const panel = panelRef.current;
143
+ if (panel) panel.style[isHorizontal ? 'width' : 'height'] = `${next}px`;
144
+ }
104
145
  e.currentTarget.setAttribute('aria-valuenow', String(Math.round(next)));
105
146
  };
106
147
 
@@ -174,6 +215,7 @@ export function PanelResizeHandle({
174
215
  onPointerCancel={endDrag}
175
216
  onLostPointerCapture={endDrag}
176
217
  onKeyDown={handleKeyDown}
218
+ onDoubleClick={onReset}
177
219
  className={cn(
178
220
  'group absolute z-20 flex touch-none select-none',
179
221
  'outline-none ring-0 focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0',
@@ -9,6 +9,7 @@ export * from './use-header-height';
9
9
  export * from './use-sticky-toolbar';
10
10
  export * from './use-horizontal-scrollbar';
11
11
  export * from './use-image-edge-color';
12
+ export * from './use-clipped-label-tooltip';
12
13
  export * from './use-is-truncated';
13
14
  export * from './use-isomorphic-layout-effect';
14
15
  export * from './use-keyboard-collision-padding';
@@ -0,0 +1,20 @@
1
+ 'use client';
2
+
3
+ import { useIsTruncated } from './use-is-truncated';
4
+ import { useMediaQuery } from './use-media-query';
5
+
6
+ /** A pointer that can hover: where a tooltip on a clipped label is reachable. A touch screen has none. */
7
+ export const HOVER_MEDIA_QUERY = '(hover: hover) and (pointer: fine)';
8
+
9
+ /**
10
+ * A clipped label's "full text" tooltip: `ref` goes on the clipped element (see
11
+ * {@link useIsTruncated}), `enabled` says whether the tooltip may open — only
12
+ * while the text IS clipped, and only on a device whose pointer can hover. A
13
+ * phone or tablet has no hover to reach it with, so it offers none there; the
14
+ * rows keep their accessible names. Pass the rendered text as `value`.
15
+ */
16
+ export function useClippedLabelTooltip<T extends HTMLElement>(value: unknown) {
17
+ const { ref, isTruncated } = useIsTruncated<T>(value);
18
+ const canHover = useMediaQuery(HOVER_MEDIA_QUERY) === true;
19
+ return { ref, enabled: isTruncated && canHover };
20
+ }
@@ -52,6 +52,8 @@ export function useIsTruncated<T extends HTMLElement>(value: unknown, options?:
52
52
  );
53
53
  };
54
54
  check();
55
+ // An environment without ResizeObserver (jsdom in a consumer's tests) measures once.
56
+ if (typeof ResizeObserver === 'undefined') return undefined;
55
57
  const observer = new ResizeObserver(check);
56
58
  observer.observe(element);
57
59
  return () => observer.disconnect();
@@ -13,6 +13,8 @@ export function useMediaQuery(query: string): boolean | undefined {
13
13
  const [matches, setMatches] = useState<boolean | undefined>(undefined);
14
14
 
15
15
  useIsomorphicLayoutEffect(() => {
16
+ // An environment without matchMedia (jsdom in a consumer's tests) never answers: `undefined` throughout.
17
+ if (typeof window.matchMedia !== 'function') return undefined;
16
18
  const matchMedia = window.matchMedia(query);
17
19
 
18
20
  const handleChange = () => {
@@ -110,5 +110,11 @@ export function useResizablePanelSize({
110
110
  [clampSize, storage],
111
111
  );
112
112
 
113
- return { size, setSize, clampSize };
113
+ /** Back to `defaultSize`: the choice is forgotten and its stored entry removed. */
114
+ const resetSize = useCallback(() => {
115
+ setChosen({ storage, size: null });
116
+ storage?.clear();
117
+ }, [storage]);
118
+
119
+ return { size, setSize, resetSize, clampSize };
114
120
  }
@@ -115,6 +115,35 @@ export interface SlidingSidebarConfig {
115
115
  className?: string;
116
116
  }
117
117
 
118
+ /**
119
+ * `NavigationSidebarConfig.resizable`: the user drags the expanded sidebar's
120
+ * right edge on desktop (`lg` and up). The 56px rail is not resizable, nor are
121
+ * the tablet overlay and the burger menu; the overlay opens to the chosen width.
122
+ */
123
+ export interface NavigationSidebarResizeConfig {
124
+ /**
125
+ * Narrowest expanded width, in px. Raised to 141px when lower: the rows read
126
+ * 140px and up as expanded (`@[140px]/of-nav-sidebar`).
127
+ */
128
+ minWidth: number;
129
+ /** Widest expanded width, in px. Also kept to the room there is (`minContentWidth`). */
130
+ maxWidth: number;
131
+ /**
132
+ * Px of the row the content beside the sidebar always keeps, so a wide sidebar
133
+ * cannot crowd the page out of a narrow desktop window. Default 720, the lib's
134
+ * `content-md` breakpoint.
135
+ */
136
+ minContentWidth?: number;
137
+ /**
138
+ * `localStorage` key for the width the user chose: `{ size }` under
139
+ * `<storageKey>:chosen` (see `useResizablePanelSize`). Only a drag or a key
140
+ * press on the handle writes it, never the default or a clamped width; a double
141
+ * click removes it. Omit to keep the choice for the mount only. A host that seeds
142
+ * `NAVIGATION_SIDEBAR_WIDTH_VAR` before first paint reads the same entry.
143
+ */
144
+ storageKey?: string;
145
+ }
146
+
118
147
  /**
119
148
  * Configuration for the navigation sidebar component
120
149
  */
@@ -237,6 +266,14 @@ export interface NavigationSidebarConfig {
237
266
  * chat search in `sectionSlot`.
238
267
  */
239
268
  revealSignal?: number;
269
+ /**
270
+ * Let the user resize the expanded sidebar on desktop: a drag handle astride
271
+ * its right edge (the grip the Mingo chat drawer had), arrow keys, Home and
272
+ * End on it, a double click back to the default. The default stays
273
+ * `expandedWidth` (grouped) or 224px (flat); both variants. Omitted: the fixed
274
+ * widths, as before.
275
+ */
276
+ resizable?: NavigationSidebarResizeConfig;
240
277
  }
241
278
 
242
279
  /** What a slot render function is told about the place it is drawn in. */
@@ -39,6 +39,7 @@ export const NOTIFICATION_CATEGORIES = [
39
39
  'INSIGHTS',
40
40
  'MINGO',
41
41
  'INTEGRATIONS',
42
+ 'BILLING',
42
43
  'GENERIC',
43
44
  ] as const;
44
45
  export type NotificationCategory = (typeof NOTIFICATION_CATEGORIES)[number];
@@ -56,6 +57,7 @@ export const NOTIFICATION_CATEGORY_LABELS: Readonly<Record<NotificationCategory,
56
57
  INSIGHTS: 'Insights',
57
58
  MINGO: 'Mingo',
58
59
  INTEGRATIONS: 'Integrations',
60
+ BILLING: 'Billing',
59
61
  GENERIC: 'General',
60
62
  };
61
63