@scripso-homepad/ui 0.4.16 → 0.4.18

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.16",
3
+ "version": "0.4.18",
4
4
  "type": "module",
5
5
  "description": "Cross-platform UI components for Homepad (React Web + React Native)",
6
6
  "license": "MIT",
@@ -7,10 +7,41 @@ import './drawer-scroll.css';
7
7
 
8
8
  const DRAWER_TRANSITION_MS = 300;
9
9
 
10
+ let bodyScrollLockCount = 0;
11
+ let previousBodyOverflow = '';
12
+
13
+ function lockBodyScroll() {
14
+ if (typeof document === 'undefined') {
15
+ return;
16
+ }
17
+
18
+ if (bodyScrollLockCount === 0) {
19
+ previousBodyOverflow = document.body.style.overflow;
20
+ document.body.style.overflow = 'hidden';
21
+ }
22
+
23
+ bodyScrollLockCount += 1;
24
+ }
25
+
26
+ function unlockBodyScroll() {
27
+ if (typeof document === 'undefined') {
28
+ return;
29
+ }
30
+
31
+ bodyScrollLockCount = Math.max(0, bodyScrollLockCount - 1);
32
+
33
+ if (bodyScrollLockCount === 0) {
34
+ document.body.style.overflow = previousBodyOverflow;
35
+ previousBodyOverflow = '';
36
+ }
37
+ }
38
+
10
39
  export type DrawerProps = {
11
40
  open: boolean;
12
41
  title: string;
13
42
  subtitle?: string;
43
+ subtitleClassName?: string;
44
+ headerBottom?: ReactNode;
14
45
  children: ReactNode;
15
46
  footer?: ReactNode;
16
47
  onClose: () => void;
@@ -26,6 +57,8 @@ export function Drawer({
26
57
  open,
27
58
  title,
28
59
  subtitle,
60
+ subtitleClassName,
61
+ headerBottom,
29
62
  children,
30
63
  footer,
31
64
  onClose,
@@ -70,8 +103,7 @@ export function Drawer({
70
103
  return;
71
104
  }
72
105
 
73
- const previousOverflow = document.body.style.overflow;
74
- document.body.style.overflow = 'hidden';
106
+ lockBodyScroll();
75
107
 
76
108
  const handleKeyDown = (event: KeyboardEvent) => {
77
109
  if (event.key === 'Escape') {
@@ -82,7 +114,7 @@ export function Drawer({
82
114
  window.addEventListener('keydown', handleKeyDown);
83
115
 
84
116
  return () => {
85
- document.body.style.overflow = previousOverflow;
117
+ unlockBodyScroll();
86
118
  window.removeEventListener('keydown', handleKeyDown);
87
119
  };
88
120
  }, [isPresent, onClose]);
@@ -119,36 +151,42 @@ export function Drawer({
119
151
  )}
120
152
  >
121
153
  <header
122
- className={cn(
123
- 'flex h-[87px] shrink-0 items-start justify-between border-b border-storm-gray-50 pt-6 pr-6 pb-4 pl-6',
124
- headerClassName,
125
- )}
154
+ className={cn('shrink-0 border-b border-storm-gray-50', headerClassName)}
126
155
  >
127
- <div className="min-w-0 flex flex-col gap-1">
128
- <h2
129
- id="homepad-drawer-title"
130
- className="typography-title-sm tracking-normal text-black"
131
- >
132
- {title}
133
- </h2>
134
- {subtitle ? (
135
- <p
136
- id="homepad-drawer-subtitle"
137
- className="typography-body tracking-normal text-storm-gray-400"
156
+ <div className="flex items-start justify-between pt-6 pr-6 pb-4 pl-6">
157
+ <div className="min-w-0 flex flex-col gap-1">
158
+ <h2
159
+ id="homepad-drawer-title"
160
+ className="typography-title-sm tracking-normal text-black"
138
161
  >
139
- {subtitle}
140
- </p>
141
- ) : null}
162
+ {title}
163
+ </h2>
164
+ {subtitle ? (
165
+ <p
166
+ id="homepad-drawer-subtitle"
167
+ className={cn(
168
+ 'typography-body tracking-normal text-storm-gray-400',
169
+ subtitleClassName,
170
+ )}
171
+ >
172
+ {subtitle}
173
+ </p>
174
+ ) : null}
175
+ </div>
176
+
177
+ <button
178
+ type="button"
179
+ aria-label={closeAriaLabel}
180
+ 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"
181
+ onClick={onClose}
182
+ >
183
+ <X className="size-5" strokeWidth={1.75} />
184
+ </button>
142
185
  </div>
143
186
 
144
- <button
145
- type="button"
146
- aria-label={closeAriaLabel}
147
- 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"
148
- onClick={onClose}
149
- >
150
- <X className="size-5" strokeWidth={1.75} />
151
- </button>
187
+ {headerBottom ? (
188
+ <div className="border-t border-storm-gray-50 px-6">{headerBottom}</div>
189
+ ) : null}
152
190
  </header>
153
191
 
154
192
  <div
@@ -1,5 +1,3 @@
1
- import { cn } from '../utils/cn';
2
-
3
1
  export type TableSortDirection = 'asc' | 'desc';
4
2
 
5
3
  type TableSortIconProps = {
@@ -22,7 +20,7 @@ export function TableSortIcon({ direction = null, className }: TableSortIconProp
22
20
  fill="none"
23
21
  xmlns="http://www.w3.org/2000/svg"
24
22
  aria-hidden
25
- className={cn('shrink-0', className)}
23
+ className={className}
26
24
  >
27
25
  <path
28
26
  d="M9.70277 11.4541L6.42668 15.2773C6.37387 15.3389 6.30837 15.3883 6.23466 15.4222C6.16095 15.4561 6.08078 15.4736 5.99965 15.4736C5.91852 15.4736 5.83835 15.4561 5.76464 15.4222C5.69093 15.3883 5.62542 15.3389 5.57262 15.2773L2.29652 11.4541C1.98387 11.0892 2.24309 10.5255 2.72355 10.5255H9.27668C9.75715 10.5255 10.0164 11.0892 9.70277 11.4541Z"
@@ -10,38 +10,29 @@ export type SidebarNavItemProps = {
10
10
  onNavigate?: (item: AdminNavItem) => void;
11
11
  };
12
12
 
13
- function isModifiedClick(event: MouseEvent<HTMLAnchorElement>): boolean {
14
- return event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0;
15
- }
16
-
17
13
  export function SidebarNavItem({ item, isOpen, isActive, onNavigate }: SidebarNavItemProps) {
18
14
  if (item.hidden) {
19
15
  return null;
20
16
  }
21
17
 
22
- const handleClick = (event: MouseEvent<HTMLAnchorElement>) => {
23
- if (item.disabled) {
24
- event.preventDefault();
25
- return;
26
- }
18
+ const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
19
+ event.preventDefault();
27
20
 
28
- // Keep routing in the host app's React Router instance so workspace /
29
- // published UI copies never fight over duplicate router contexts.
30
- if (event.defaultPrevented || isModifiedClick(event)) {
21
+ if (item.disabled) {
31
22
  return;
32
23
  }
33
24
 
34
- event.preventDefault();
35
25
  item.onClick?.();
36
26
  onNavigate?.(item);
37
27
  };
38
28
 
39
29
  return (
40
- <a
41
- href={item.to}
30
+ <button
31
+ type="button"
42
32
  onClick={handleClick}
43
33
  aria-current={isActive ? 'page' : undefined}
44
34
  aria-disabled={item.disabled}
35
+ disabled={item.disabled}
45
36
  tabIndex={item.disabled ? -1 : undefined}
46
37
  className={cn(
47
38
  'relative flex items-center rounded-xl px-4 py-3 transition-[background-color,color,gap,padding] duration-300 ease-in-out',
@@ -66,6 +57,6 @@ export function SidebarNavItem({ item, isOpen, isActive, onNavigate }: SidebarNa
66
57
  >
67
58
  {item.label}
68
59
  </span>
69
- </a>
60
+ </button>
70
61
  );
71
62
  }