@scripso-homepad/ui 0.4.40 → 0.4.42

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scripso-homepad/ui",
3
- "version": "0.4.40",
3
+ "version": "0.4.42",
4
4
  "type": "module",
5
5
  "description": "Cross-platform UI components for Homepad (React Web + React Native)",
6
6
  "license": "MIT",
@@ -1,5 +1,6 @@
1
1
  import { X } from 'lucide-react';
2
2
  import { useEffect, useState, type ReactNode } from 'react';
3
+ import { createPortal } from 'react-dom';
3
4
 
4
5
  import { useBackdropDismiss } from '../hooks/useBackdropDismiss';
5
6
  import { cn } from '../utils/cn';
@@ -10,6 +11,7 @@ const DRAWER_TRANSITION_MS = 300;
10
11
 
11
12
  let bodyScrollLockCount = 0;
12
13
  let previousBodyOverflow = '';
14
+ let previousHtmlOverflow = '';
13
15
 
14
16
  function lockBodyScroll() {
15
17
  if (typeof document === 'undefined') {
@@ -18,7 +20,9 @@ function lockBodyScroll() {
18
20
 
19
21
  if (bodyScrollLockCount === 0) {
20
22
  previousBodyOverflow = document.body.style.overflow;
23
+ previousHtmlOverflow = document.documentElement.style.overflow;
21
24
  document.body.style.overflow = 'hidden';
25
+ document.documentElement.style.overflow = 'hidden';
22
26
  }
23
27
 
24
28
  bodyScrollLockCount += 1;
@@ -33,7 +37,9 @@ function unlockBodyScroll() {
33
37
 
34
38
  if (bodyScrollLockCount === 0) {
35
39
  document.body.style.overflow = previousBodyOverflow;
40
+ document.documentElement.style.overflow = previousHtmlOverflow;
36
41
  previousBodyOverflow = '';
42
+ previousHtmlOverflow = '';
37
43
  }
38
44
  }
39
45
 
@@ -137,13 +143,13 @@ export function Drawer({
137
143
  };
138
144
  }, [isPresent, onClose]);
139
145
 
140
- if (!isPresent) {
146
+ if (!open && !isPresent) {
141
147
  return null;
142
148
  }
143
149
 
144
- return (
150
+ const drawer = (
145
151
  <div
146
- className="fixed inset-0 z-50 flex justify-end p-2 max-md:p-0"
152
+ className="fixed inset-0 z-50 flex justify-end overscroll-none p-2 max-md:p-0"
147
153
  role="presentation"
148
154
  >
149
155
  <button
@@ -163,7 +169,7 @@ export function Drawer({
163
169
  aria-labelledby="homepad-drawer-title"
164
170
  {...(subtitle ? { 'aria-describedby': 'homepad-drawer-subtitle' } : {})}
165
171
  className={cn(
166
- 'relative z-10 flex h-[calc(100dvh-16px)] min-h-0 cursor-auto flex-col overflow-hidden rounded-[16px] bg-white shadow-2xl transition-transform duration-300 ease-in-out max-md:h-dvh max-md:rounded-none',
172
+ 'relative z-10 flex h-full max-h-full min-h-0 cursor-auto flex-col overflow-hidden rounded-[16px] bg-white shadow-2xl transition-transform duration-300 ease-in-out max-md:rounded-none',
167
173
  // Keep GPU transform only while animating — settled translate-x-0 blurs text.
168
174
  !isTransformSettled && 'will-change-transform',
169
175
  isAnimatedIn
@@ -175,9 +181,7 @@ export function Drawer({
175
181
  className,
176
182
  )}
177
183
  >
178
- <header
179
- className={cn('shrink-0 border-b border-storm-gray-50', headerClassName)}
180
- >
184
+ <header className={cn('shrink-0 border-b border-storm-gray-50', headerClassName)}>
181
185
  <div className="flex items-start justify-between pt-6 pr-6 pb-4 pl-6">
182
186
  <div className="min-w-0 flex flex-col gap-1">
183
187
  <h2
@@ -205,7 +209,10 @@ export function Drawer({
205
209
  className="flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-lg text-storm-gray-400 transition-colors hover:bg-storm-gray-50 hover:text-storm-gray-600"
206
210
  onClick={onClose}
207
211
  >
208
- <X className="size-5" strokeWidth={1.75} />
212
+ <X
213
+ className="size-5"
214
+ strokeWidth={1.75}
215
+ />
209
216
  </button>
210
217
  </div>
211
218
 
@@ -229,4 +236,6 @@ export function Drawer({
229
236
  </aside>
230
237
  </div>
231
238
  );
239
+
240
+ return typeof document !== 'undefined' ? createPortal(drawer, document.body) : drawer;
232
241
  }
@@ -5,6 +5,7 @@ import { Button } from '../../components/Button';
5
5
  import { useBackdropDismiss } from '../hooks/useBackdropDismiss';
6
6
  import { cn } from '../utils/cn';
7
7
  import './drawer-scroll.css';
8
+ import './modal-motion.css';
8
9
 
9
10
  export type ModalFooterAlign = 'left' | 'center' | 'right';
10
11
  export type ModalFooterLayout = 'inline' | 'split';
@@ -29,6 +30,7 @@ export type ModalProps = {
29
30
  closeOnBackdrop?: boolean;
30
31
  closeOnEscape?: boolean;
31
32
  containerClassName?: string;
33
+ containerDataOnboarding?: string;
32
34
  contentClassName?: string;
33
35
  buttonClassName?: string;
34
36
  };
@@ -59,6 +61,7 @@ export function Modal({
59
61
  closeOnBackdrop = true,
60
62
  closeOnEscape = true,
61
63
  containerClassName,
64
+ containerDataOnboarding,
62
65
  contentClassName,
63
66
  buttonClassName,
64
67
  }: ModalProps) {
@@ -90,14 +93,11 @@ export function Modal({
90
93
  return null;
91
94
  }
92
95
 
93
- const sharedButtonClassName = cn(
94
- 'btn h-13 w-full! min-w-0! justify-center!',
95
- buttonClassName,
96
- );
96
+ const sharedButtonClassName = cn('btn h-13 w-full! min-w-0! justify-center!', buttonClassName);
97
97
 
98
98
  const modal = (
99
99
  <div
100
- className="fixed inset-0 z-[100] flex items-center justify-center p-4 backdrop-blur-[12px] bg-storm-gray-850/40"
100
+ className="homepad-modal-in fixed inset-0 z-[100] flex items-center justify-center bg-storm-gray-850/40 p-4 backdrop-blur-[12px]"
101
101
  onPointerDown={backdropDismiss.onPointerDown}
102
102
  onClick={backdropDismiss.onClick}
103
103
  role="presentation"
@@ -107,6 +107,7 @@ export function Modal({
107
107
  aria-modal="true"
108
108
  aria-labelledby="homepad-modal-title"
109
109
  {...(subtitle ? { 'aria-describedby': 'homepad-modal-subtitle' } : {})}
110
+ {...(containerDataOnboarding ? { 'data-onboarding': containerDataOnboarding } : {})}
110
111
  className={cn(
111
112
  'flex max-h-[min(90dvh,calc(100dvh-2rem))] w-full max-w-2xl flex-col gap-6 rounded-2xl bg-white p-4 opacity-100 md:p-6',
112
113
  containerClassName,
@@ -146,7 +147,14 @@ export function Modal({
146
147
  </div>
147
148
  </header>
148
149
 
149
- <div className={cn('drawer-scroll-area min-h-0 min-w-0 flex-1 overflow-y-auto', contentClassName)}>{children}</div>
150
+ <div
151
+ className={cn(
152
+ 'drawer-scroll-area min-h-0 min-w-0 flex-1 overflow-y-auto',
153
+ contentClassName,
154
+ )}
155
+ >
156
+ {children}
157
+ </div>
150
158
 
151
159
  <footer
152
160
  className={cn(
@@ -1,21 +1,29 @@
1
- import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
2
- import type { ReactNode } from 'react';
3
- import { createPortal } from 'react-dom';
4
-
5
- import { useOnClickOutside } from '../hooks/useOnClickOutside';
6
- import { TableMenuCheckIcon } from '../icons/TableMenuCheckIcon';
7
- import { TableMenuCloseIcon } from '../icons/TableMenuCloseIcon';
8
- import { TableMoreIcon } from '../icons/TableMoreIcon';
9
- import { cn } from '../utils/cn';
10
- import { TableActionButton } from './TableActionButton';
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useId,
5
+ useLayoutEffect,
6
+ useRef,
7
+ useState,
8
+ } from "react";
9
+ import type { ReactNode } from "react";
10
+ import { createPortal } from "react-dom";
11
+
12
+ import { useOnClickOutside } from "../hooks/useOnClickOutside";
13
+ import { TableMenuCheckIcon } from "../icons/TableMenuCheckIcon";
14
+ import { TableMenuCloseIcon } from "../icons/TableMenuCloseIcon";
15
+ import { TableMoreIcon } from "../icons/TableMoreIcon";
16
+ import { cn } from "../utils/cn";
17
+ import { TableActionButton } from "./TableActionButton";
11
18
 
12
19
  const MENU_WIDTH_PX = 231;
13
20
  const MENU_OFFSET_PX = 4;
14
21
  const MENU_VIEWPORT_MARGIN_PX = 8;
15
22
 
16
- const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
23
+ const useIsomorphicLayoutEffect =
24
+ typeof window === "undefined" ? useEffect : useLayoutEffect;
17
25
 
18
- export type TableActionsMenuItemTone = 'default' | 'success' | 'danger';
26
+ export type TableActionsMenuItemTone = "default" | "success" | "danger";
19
27
 
20
28
  export type TableActionsMenuItem = {
21
29
  id: string;
@@ -37,6 +45,10 @@ export type TableActionsMenuProps = {
37
45
  menuWidth?: number;
38
46
  /** Size the menu to its content width instead of a fixed pixel width. Defaults to true so labels stay on one line. */
39
47
  menuFitContent?: boolean;
48
+
49
+ /** Keep the menu open while a page tour is explaining this row. */
50
+ forceOpen?: boolean;
51
+ menuOnboardingId?: string;
40
52
  };
41
53
 
42
54
  type MenuPosition = {
@@ -45,12 +57,15 @@ type MenuPosition = {
45
57
  };
46
58
 
47
59
  function clampMenuLeft(left: number, menuWidth: number): number {
48
- if (typeof window === 'undefined') {
60
+ if (typeof window === "undefined") {
49
61
  return left;
50
62
  }
51
63
 
52
64
  const minLeft = MENU_VIEWPORT_MARGIN_PX;
53
- const maxLeft = Math.max(minLeft, window.innerWidth - menuWidth - MENU_VIEWPORT_MARGIN_PX);
65
+ const maxLeft = Math.max(
66
+ minLeft,
67
+ window.innerWidth - menuWidth - MENU_VIEWPORT_MARGIN_PX,
68
+ );
54
69
 
55
70
  return Math.min(Math.max(left, minLeft), maxLeft);
56
71
  }
@@ -61,9 +76,9 @@ function resolveMenuItemIcon(item: TableActionsMenuItem): ReactNode | null {
61
76
  }
62
77
 
63
78
  switch (item.tone) {
64
- case 'success':
79
+ case "success":
65
80
  return <TableMenuCheckIcon />;
66
- case 'danger':
81
+ case "danger":
67
82
  return <TableMenuCloseIcon />;
68
83
  default:
69
84
  return null;
@@ -78,6 +93,8 @@ export function TableActionsMenu({
78
93
  triggerIcon,
79
94
  menuWidth = MENU_WIDTH_PX,
80
95
  menuFitContent = true,
96
+ forceOpen,
97
+ menuOnboardingId,
81
98
  }: TableActionsMenuProps) {
82
99
  const [open, setOpen] = useState(false);
83
100
  const [menuPosition, setMenuPosition] = useState<MenuPosition | null>(null);
@@ -87,8 +104,20 @@ export function TableActionsMenu({
87
104
  const menuId = useId();
88
105
 
89
106
  const closeMenu = useCallback(() => {
107
+ if (forceOpen) {
108
+ return;
109
+ }
110
+
90
111
  setOpen(false);
91
- }, []);
112
+ }, [forceOpen]);
113
+
114
+ useEffect(() => {
115
+ if (forceOpen === undefined) {
116
+ return;
117
+ }
118
+
119
+ setOpen(forceOpen);
120
+ }, [forceOpen]);
92
121
 
93
122
  const updateMenuPosition = useCallback(() => {
94
123
  const trigger = triggerRef.current;
@@ -99,14 +128,19 @@ export function TableActionsMenu({
99
128
 
100
129
  const rect = trigger.getBoundingClientRect();
101
130
  const viewportMaxWidth =
102
- typeof window === 'undefined'
131
+ typeof window === "undefined"
103
132
  ? menuWidth
104
133
  : Math.max(menuWidth, window.innerWidth - MENU_VIEWPORT_MARGIN_PX * 2);
105
134
 
106
135
  const measuredWidth = menuFitContent
107
- ? (menuEl?.offsetWidth || menuEl?.getBoundingClientRect().width || menuWidth)
136
+ ? menuEl?.offsetWidth ||
137
+ menuEl?.getBoundingClientRect().width ||
138
+ menuWidth
108
139
  : menuWidth;
109
- const resolvedMenuWidth = Math.min(Math.max(measuredWidth, 1), viewportMaxWidth);
140
+ const resolvedMenuWidth = Math.min(
141
+ Math.max(measuredWidth, 1),
142
+ viewportMaxWidth,
143
+ );
110
144
 
111
145
  setMenuPosition({
112
146
  top: rect.bottom + MENU_OFFSET_PX,
@@ -132,27 +166,31 @@ export function TableActionsMenu({
132
166
  }
133
167
 
134
168
  const closeOnScroll = () => {
169
+ if (forceOpen) {
170
+ return;
171
+ }
172
+
135
173
  closeMenu();
136
174
  };
137
175
 
138
- window.addEventListener('resize', updateMenuPosition);
139
- window.addEventListener('scroll', closeOnScroll, true);
176
+ window.addEventListener("resize", updateMenuPosition);
177
+ window.addEventListener("scroll", closeOnScroll, true);
140
178
 
141
179
  const closeOnEscape = (event: KeyboardEvent) => {
142
- if (event.key === 'Escape') {
180
+ if (event.key === "Escape") {
143
181
  closeMenu();
144
182
  triggerRef.current?.focus();
145
183
  }
146
184
  };
147
185
 
148
- document.addEventListener('keydown', closeOnEscape);
186
+ document.addEventListener("keydown", closeOnEscape);
149
187
 
150
188
  return () => {
151
- window.removeEventListener('resize', updateMenuPosition);
152
- window.removeEventListener('scroll', closeOnScroll, true);
153
- document.removeEventListener('keydown', closeOnEscape);
189
+ window.removeEventListener("resize", updateMenuPosition);
190
+ window.removeEventListener("scroll", closeOnScroll, true);
191
+ document.removeEventListener("keydown", closeOnEscape);
154
192
  };
155
- }, [closeMenu, open, updateMenuPosition]);
193
+ }, [closeMenu, forceOpen, open, updateMenuPosition]);
156
194
 
157
195
  if (items.length === 0) {
158
196
  return null;
@@ -164,17 +202,18 @@ export function TableActionsMenu({
164
202
  id={menuId}
165
203
  role="menu"
166
204
  style={{
167
- position: 'fixed',
205
+ position: "fixed",
168
206
  top: menuPosition?.top ?? 0,
169
207
  left: menuPosition?.left ?? 0,
170
208
  maxWidth: `calc(100vw - ${MENU_VIEWPORT_MARGIN_PX * 2}px)`,
171
- ...(menuFitContent ? { width: 'max-content' } : { width: menuWidth }),
209
+ ...(menuFitContent ? { width: "max-content" } : { width: menuWidth }),
172
210
  // Hide until we have a measured viewport-safe position.
173
- visibility: menuPosition != null ? 'visible' : 'hidden',
174
- pointerEvents: menuPosition != null ? 'auto' : 'none',
211
+ visibility: menuPosition != null ? "visible" : "hidden",
212
+ pointerEvents: menuPosition != null ? "auto" : "none",
175
213
  }}
214
+ {...(menuOnboardingId ? { "data-onboarding": menuOnboardingId } : {})}
176
215
  className={cn(
177
- 'z-50 overflow-hidden rounded-2xl border border-storm-gray-50 bg-white shadow-[0_4px_20px_0_rgba(0,0,0,0.05)]',
216
+ "z-50 overflow-hidden rounded-2xl border border-storm-gray-50 bg-white shadow-[0_4px_20px_0_rgba(0,0,0,0.05)]",
178
217
  menuClassName,
179
218
  )}
180
219
  >
@@ -196,25 +235,25 @@ export function TableActionsMenu({
196
235
  item.onSelect();
197
236
  }}
198
237
  className={cn(
199
- 'flex h-[51px] w-full cursor-pointer items-center gap-2.5 px-3 text-left typography-14 font-medium transition-colors hover:bg-storm-gray-0',
200
- index > 0 && 'border-t border-storm-gray-50',
201
- item.disabled && 'cursor-not-allowed opacity-50',
202
- item.tone === 'danger'
203
- ? 'text-[#AE0011]'
204
- : item.tone === 'success'
205
- ? 'text-green-600'
206
- : 'text-storm-gray-900',
238
+ "flex h-[51px] w-full cursor-pointer items-center gap-2.5 px-3 text-left typography-14 font-medium transition-colors hover:bg-storm-gray-0",
239
+ index > 0 && "border-t border-storm-gray-50",
240
+ item.disabled && "cursor-not-allowed opacity-50",
241
+ item.tone === "danger"
242
+ ? "text-[#AE0011]"
243
+ : item.tone === "success"
244
+ ? "text-green-600"
245
+ : "text-storm-gray-900",
207
246
  )}
208
247
  >
209
248
  {icon ? (
210
249
  <span
211
250
  className={cn(
212
- 'inline-flex size-5 shrink-0 items-center justify-center',
213
- item.tone === 'danger'
214
- ? 'text-[#AE0011]'
215
- : item.tone === 'success'
216
- ? 'text-green-600'
217
- : 'text-storm-gray-200',
251
+ "inline-flex size-5 shrink-0 items-center justify-center",
252
+ item.tone === "danger"
253
+ ? "text-[#AE0011]"
254
+ : item.tone === "success"
255
+ ? "text-green-600"
256
+ : "text-storm-gray-200",
218
257
  )}
219
258
  >
220
259
  {icon}
@@ -228,7 +267,7 @@ export function TableActionsMenu({
228
267
  ) : null;
229
268
 
230
269
  return (
231
- <div ref={rootRef} className={cn('relative', className)}>
270
+ <div ref={rootRef} className={cn("relative", className)}>
232
271
  <TableActionButton
233
272
  ref={triggerRef}
234
273
  variant="menu"
@@ -240,7 +279,9 @@ export function TableActionsMenu({
240
279
  aria-controls={menuId}
241
280
  />
242
281
 
243
- {typeof document !== 'undefined' && menu != null ? createPortal(menu, document.body) : null}
282
+ {typeof document !== "undefined" && menu != null
283
+ ? createPortal(menu, document.body)
284
+ : null}
244
285
  </div>
245
286
  );
246
287
  }
@@ -0,0 +1,19 @@
1
+ @keyframes homepad-modal-in {
2
+ from {
3
+ opacity: 0;
4
+ }
5
+
6
+ to {
7
+ opacity: 1;
8
+ }
9
+ }
10
+
11
+ .homepad-modal-in {
12
+ animation: homepad-modal-in 300ms ease;
13
+ }
14
+
15
+ @media (prefers-reduced-motion: reduce) {
16
+ .homepad-modal-in {
17
+ animation: none;
18
+ }
19
+ }
@@ -111,22 +111,22 @@ export function TableHead({
111
111
  sortDirection = null,
112
112
  onSort,
113
113
  children,
114
+ onClick,
115
+ onKeyDown,
114
116
  ...props
115
117
  }: TableHeadProps) {
116
118
  const isSortable = sortable && onSort != null;
117
119
 
118
120
  const content = isSortable ? (
119
- <button
120
- type="button"
121
- onClick={onSort}
121
+ <span
122
122
  className={cn(
123
- 'inline-flex w-full cursor-pointer items-center gap-2.5 border-0 bg-transparent p-0 font-[inherit] text-[length:inherit] leading-[inherit] tracking-[inherit] text-inherit transition-colors hover:text-storm-gray-500',
123
+ 'inline-flex w-full items-center gap-2.5 font-[inherit] text-[length:inherit] leading-[inherit] tracking-[inherit] text-inherit',
124
124
  tableHeadSortButtonAlignClasses[align],
125
125
  )}
126
126
  >
127
127
  <span>{children}</span>
128
128
  <TableSortIcon direction={sortDirection} />
129
- </button>
129
+ </span>
130
130
  ) : (
131
131
  children
132
132
  );
@@ -138,10 +138,31 @@ export function TableHead({
138
138
  tableHeadPaddingClassName,
139
139
  tableHeaderClassName,
140
140
  !isSortable && tableAlignClasses[align],
141
+ isSortable && 'cursor-pointer select-none hover:text-storm-gray-500',
141
142
  className,
142
143
  )}
143
144
  aria-sort={resolveAriaSort(sortDirection, isSortable)}
145
+ tabIndex={isSortable ? 0 : undefined}
144
146
  {...props}
147
+ onClick={(event) => {
148
+ onClick?.(event);
149
+ if (event.defaultPrevented || !isSortable) {
150
+ return;
151
+ }
152
+
153
+ onSort();
154
+ }}
155
+ onKeyDown={(event) => {
156
+ onKeyDown?.(event);
157
+ if (event.defaultPrevented || !isSortable) {
158
+ return;
159
+ }
160
+
161
+ if (event.key === 'Enter' || event.key === ' ') {
162
+ event.preventDefault();
163
+ onSort();
164
+ }
165
+ }}
145
166
  >
146
167
  {content}
147
168
  </th>
@@ -27,11 +27,11 @@ export function DashboardLayout({
27
27
  mainClassName,
28
28
  }: DashboardLayoutProps) {
29
29
  return (
30
- <div className={cn('flex h-screen min-w-0 overflow-hidden flex-row bg-storm-gray-0', className)}>
31
- <div className="max-md:contents md:h-screen md:shrink-0">
30
+ <div className={cn('flex h-dvh min-w-0 overflow-hidden flex-row bg-storm-gray-0', className)}>
31
+ <div className="max-md:contents md:h-dvh md:shrink-0">
32
32
  <Sidebar {...sidebar} />
33
33
  </div>
34
- <div className="flex h-screen min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
34
+ <div className="flex h-dvh min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
35
35
  <SidebarMobileHeader {...mobileHeader} />
36
36
  <AppHeader {...header} />
37
37
  <main className={mainClassName ?? 'min-h-0 min-w-0 flex-1 overflow-x-hidden overflow-y-auto p-4 xl:p-6'}>{children}</main>
@@ -173,12 +173,12 @@ export function Sidebar({
173
173
  />
174
174
 
175
175
  {showCollapsedRail && mobileOpen ? (
176
- <div className="h-screen w-[76px] shrink-0" aria-hidden />
176
+ <div className="h-dvh w-[76px] shrink-0" aria-hidden />
177
177
  ) : null}
178
178
 
179
179
  <aside
180
180
  className={cn(
181
- 'fixed inset-y-0 left-0 z-50 flex h-screen w-[min(300px,85vw)] flex-col gap-8 overflow-hidden bg-navy p-4 shadow-xl',
181
+ 'fixed inset-y-0 left-0 z-50 flex h-dvh w-[min(300px,85vw)] flex-col gap-8 overflow-hidden bg-navy p-4 shadow-xl',
182
182
  'transition-[transform,width,padding,gap] duration-300 ease-in-out will-change-[transform,width]',
183
183
  mobileOpen ? 'translate-x-0' : '-translate-x-full pointer-events-none',
184
184
  'md:relative md:z-auto md:w-[76px] md:translate-x-0 md:pointer-events-auto md:shadow-none md:items-center md:gap-2 md:px-3 md:py-4',
@@ -1,7 +1,7 @@
1
- import type { MouseEvent } from 'react';
1
+ import type { MouseEvent } from "react";
2
2
 
3
- import { cn } from '../utils/cn';
4
- import type { AdminNavItem } from './types';
3
+ import { cn } from "../utils/cn";
4
+ import type { AdminNavItem } from "./types";
5
5
 
6
6
  export type SidebarNavItemProps = {
7
7
  item: AdminNavItem;
@@ -10,7 +10,12 @@ export type SidebarNavItemProps = {
10
10
  onNavigate?: (item: AdminNavItem) => void;
11
11
  };
12
12
 
13
- export function SidebarNavItem({ item, isOpen, isActive, onNavigate }: SidebarNavItemProps) {
13
+ export function SidebarNavItem({
14
+ item,
15
+ isOpen,
16
+ isActive,
17
+ onNavigate,
18
+ }: SidebarNavItemProps) {
14
19
  if (item.hidden) {
15
20
  return null;
16
21
  }
@@ -30,31 +35,34 @@ export function SidebarNavItem({ item, isOpen, isActive, onNavigate }: SidebarNa
30
35
  <button
31
36
  type="button"
32
37
  onClick={handleClick}
33
- aria-current={isActive ? 'page' : undefined}
38
+ aria-current={isActive ? "page" : undefined}
39
+ data-onboarding={item.onboardingId}
34
40
  aria-disabled={item.disabled}
35
41
  disabled={item.disabled}
36
42
  tabIndex={item.disabled ? -1 : undefined}
37
43
  className={cn(
38
- 'relative flex items-center rounded-xl px-4 py-3 transition-[background-color,color,gap,padding] duration-300 ease-in-out',
39
- isOpen ? 'w-full gap-4' : 'w-full justify-center gap-0 px-3',
40
- item.disabled && 'pointer-events-none opacity-50',
44
+ "relative flex items-center rounded-xl px-4 py-3 transition-[background-color,color,gap,padding] duration-300 ease-in-out",
45
+ isOpen ? "w-full gap-4" : "w-full justify-center gap-0 px-3",
46
+ item.disabled && "pointer-events-none opacity-50",
41
47
  isActive
42
- ? 'bg-black/12 text-white shadow-none'
43
- : 'text-navy-100 shadow-none hover:bg-white/5',
48
+ ? "bg-black/12 text-white shadow-none"
49
+ : "text-navy-100 shadow-none hover:bg-white/5",
44
50
  )}
45
51
  >
46
52
  <span
47
53
  aria-hidden
48
54
  className={cn(
49
- 'absolute top-1.5 -left-1 h-8 w-2 rounded-full bg-white transition-[opacity,transform] duration-300 ease-in-out',
50
- isActive ? 'scale-100 opacity-100' : 'scale-75 opacity-0',
55
+ "absolute top-1.5 -left-1 h-8 w-2 rounded-full bg-white transition-[opacity,transform] duration-300 ease-in-out",
56
+ isActive ? "scale-100 opacity-100" : "scale-75 opacity-0",
51
57
  )}
52
58
  />
53
- <span className="flex size-5 shrink-0 items-center justify-center">{item.icon}</span>
59
+ <span className="flex size-5 shrink-0 items-center justify-center">
60
+ {item.icon}
61
+ </span>
54
62
  <span
55
63
  className={cn(
56
- 'overflow-hidden typography-14 font-semibold whitespace-nowrap transition-[max-width,opacity] duration-300 ease-in-out',
57
- isOpen ? 'max-w-[180px] opacity-100' : 'max-w-0 opacity-0',
64
+ "overflow-hidden typography-14 font-semibold whitespace-nowrap transition-[max-width,opacity] duration-300 ease-in-out",
65
+ isOpen ? "max-w-[180px] opacity-100" : "max-w-0 opacity-0",
58
66
  )}
59
67
  >
60
68
  {item.label}
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from 'react';
1
+ import type { ReactNode } from "react";
2
2
 
3
3
  export type AdminNavItem = {
4
4
  to: string;
@@ -8,6 +8,7 @@ export type AdminNavItem = {
8
8
  hidden?: boolean;
9
9
  disabled?: boolean;
10
10
  onClick?: () => void;
11
+ onboardingId?: string;
11
12
  };
12
13
 
13
14
  export type AdminSidebarUser = {