@scripso-homepad/ui 0.4.9 → 0.4.11

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 (48) hide show
  1. package/dist/index.cjs +245 -127
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +4 -2
  4. package/dist/index.d.ts +4 -2
  5. package/dist/index.js +247 -129
  6. package/dist/index.js.map +1 -1
  7. package/dist/web/index.cjs +451 -48
  8. package/dist/web/index.cjs.map +1 -1
  9. package/dist/web/index.css +161 -0
  10. package/dist/web/index.css.map +1 -1
  11. package/dist/web/index.d.cts +50 -2
  12. package/dist/web/index.d.ts +50 -2
  13. package/dist/web/index.js +444 -49
  14. package/dist/web/index.js.map +1 -1
  15. package/package.json +10 -3
  16. package/src/components/Calendar.tsx +10 -2
  17. package/src/components/DatePicker.tsx +40 -6
  18. package/src/components/Select.tsx +133 -38
  19. package/src/theme/select.ts +1 -1
  20. package/src/utils/calendarDays.ts +15 -6
  21. package/src/web/components/Badge.tsx +1 -1
  22. package/src/web/components/ConfirmModal.tsx +2 -2
  23. package/src/web/components/Drawer.stories.tsx +234 -0
  24. package/src/web/components/Drawer.tsx +169 -0
  25. package/src/web/components/HistoryTimeline.tsx +46 -0
  26. package/src/web/components/Modal.stories.tsx +1 -1
  27. package/src/web/components/Modal.tsx +30 -13
  28. package/src/web/components/Pagination.tsx +2 -2
  29. package/src/web/components/TableActionButton.tsx +4 -4
  30. package/src/web/components/TableActionsMenu.tsx +1 -1
  31. package/src/web/components/TableIconText.tsx +1 -1
  32. package/src/web/components/Toaster.stories.tsx +106 -0
  33. package/src/web/components/Toaster.tsx +92 -0
  34. package/src/web/components/ToasterProvider.tsx +6 -0
  35. package/src/web/components/drawer-scroll.css +41 -0
  36. package/src/web/components/sonner-toast.css +141 -0
  37. package/src/web/components/table/constants.ts +2 -2
  38. package/src/web/icons/TimelineCheckIcon.tsx +24 -0
  39. package/src/web/icons/ToastIcons.tsx +119 -0
  40. package/src/web/index.ts +20 -0
  41. package/src/web/layout/AppHeader.tsx +1 -1
  42. package/src/web/layout/BuildingSelect.tsx +2 -2
  43. package/src/web/layout/DashboardLayout.stories.tsx +1 -1
  44. package/src/web/layout/SidebarNavItem.tsx +1 -1
  45. package/src/web/layout/SidebarUserCard.tsx +3 -3
  46. package/src/web/mutation-toast.ts +33 -0
  47. package/src/web/styles/typography.css +99 -0
  48. package/src/web/toast.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scripso-homepad/ui",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
4
4
  "type": "module",
5
5
  "description": "Cross-platform UI components for Homepad (React Web + React Native)",
6
6
  "license": "MIT",
@@ -29,7 +29,9 @@
29
29
  "import": "./src/web/index.ts",
30
30
  "require": "./dist/web/index.cjs",
31
31
  "default": "./src/web/index.ts"
32
- }
32
+ },
33
+ "./web/table-scroll.css": "./src/web/components/table-scroll.css",
34
+ "./web/typography.css": "./src/web/styles/typography.css"
33
35
  },
34
36
  "files": [
35
37
  "dist",
@@ -51,7 +53,8 @@
51
53
  "react-native": ">=0.74",
52
54
  "react-native-svg": ">=13",
53
55
  "react-native-web": ">=0.19",
54
- "react-router-dom": ">=6"
56
+ "react-router-dom": ">=6",
57
+ "sonner": ">=2.0.0"
55
58
  },
56
59
  "peerDependenciesMeta": {
57
60
  "react-native-svg": {
@@ -65,6 +68,9 @@
65
68
  },
66
69
  "lucide-react": {
67
70
  "optional": true
71
+ },
72
+ "sonner": {
73
+ "optional": true
68
74
  }
69
75
  },
70
76
  "dependencies": {
@@ -90,6 +96,7 @@
90
96
  "react-native-svg": "^15.11.2",
91
97
  "react-native-web": "^0.19.13",
92
98
  "react-router-dom": "^7.9.1",
99
+ "sonner": "^2.0.7",
93
100
  "storybook": "^8.4.7",
94
101
  "tailwindcss": "^4.1.13",
95
102
  "tsup": "^8.3.5",
@@ -13,6 +13,7 @@ import {
13
13
  formatCalendarYear,
14
14
  getYearPageStart,
15
15
  isMonthDisabled,
16
+ isSameDay,
16
17
  isYearDisabled,
17
18
  selectRangeDate,
18
19
  type CalendarDayCell,
@@ -64,13 +65,13 @@ type CalendarBaseProps = {
64
65
  export type CalendarSingleProps = CalendarBaseProps & {
65
66
  mode?: "single";
66
67
  value?: Date;
67
- onChange?: (date: Date) => void;
68
+ onChange?: (date: Date | undefined) => void;
68
69
  };
69
70
 
70
71
  export type CalendarRangeProps = CalendarBaseProps & {
71
72
  mode: "range";
72
73
  value?: DateRange;
73
- onChange?: (range: DateRange) => void;
74
+ onChange?: (range: DateRange | undefined) => void;
74
75
  };
75
76
 
76
77
  export type CalendarProps = CalendarSingleProps | CalendarRangeProps;
@@ -275,6 +276,13 @@ export function Calendar(props: CalendarProps) {
275
276
  return;
276
277
  }
277
278
 
279
+ const currentValue = singleProps?.value;
280
+
281
+ if (currentValue && isSameDay(date, currentValue)) {
282
+ singleProps?.onChange?.(undefined);
283
+ return;
284
+ }
285
+
278
286
  singleProps?.onChange?.(date);
279
287
  }
280
288
 
@@ -51,6 +51,8 @@ type DatePickerFieldProps = {
51
51
  containerStyle?: StyleProp<ViewStyle>;
52
52
  className?: string;
53
53
  fieldClassName?: string;
54
+ fieldStyle?: StyleProp<ViewStyle>;
55
+ fieldOutlineStyle?: StyleProp<ViewStyle>;
54
56
  panelClassName?: string;
55
57
  labelClassName?: string;
56
58
  errorClassName?: string;
@@ -95,6 +97,8 @@ export function DatePicker(props: DatePickerProps) {
95
97
  containerStyle,
96
98
  className,
97
99
  fieldClassName,
100
+ fieldStyle,
101
+ fieldOutlineStyle,
98
102
  panelClassName,
99
103
  labelClassName,
100
104
  errorClassName,
@@ -203,7 +207,7 @@ export function DatePicker(props: DatePickerProps) {
203
207
  });
204
208
  }
205
209
 
206
- function handleSingleSelect(date: Date) {
210
+ function handleSingleSelect(date: Date | undefined) {
207
211
  if (props.mode === "range") {
208
212
  return;
209
213
  }
@@ -212,34 +216,51 @@ export function DatePicker(props: DatePickerProps) {
212
216
  closePicker();
213
217
  }
214
218
 
215
- function handleRangeSelect(range: DateRange) {
219
+ function handleRangeSelect(range: DateRange | undefined) {
216
220
  if (props.mode !== "range") {
217
221
  return;
218
222
  }
219
223
 
220
224
  props.onChange?.(range);
221
225
 
222
- if (closeOnRangeComplete && range.end) {
226
+ if (closeOnRangeComplete && range?.end) {
223
227
  closePicker();
224
228
  }
225
229
  }
226
230
 
231
+ const isFluidField = Boolean(fieldStyle) || Boolean(fieldOutlineStyle);
232
+
227
233
  return (
228
- <View ref={wrapperRef} style={[styles.wrapper, containerStyle]}>
234
+ <View
235
+ ref={wrapperRef}
236
+ style={[styles.wrapper, isFluidField && styles.wrapperFluid, containerStyle]}
237
+ >
229
238
  {label ? (
230
239
  <Label disabled={disabled} style={styles.label} className={labelClassName}>
231
240
  {label}
232
241
  </Label>
233
242
  ) : null}
234
243
 
235
- <View style={[fieldStyles.outline, styles.fieldOutline]}>
244
+ <View
245
+ style={[
246
+ fieldStyles.outline,
247
+ styles.fieldOutline,
248
+ isFluidField && styles.fieldOutlineFluid,
249
+ fieldOutlineStyle,
250
+ ]}
251
+ >
236
252
  <Pressable
237
253
  ref={triggerRef}
238
254
  accessibilityRole="button"
239
255
  accessibilityState={{ disabled, expanded: open }}
240
256
  disabled={disabled}
241
257
  onPress={openPicker}
242
- style={[calendarFieldMetrics.container, fieldStyles.container]}
258
+ style={[
259
+ calendarFieldMetrics.container,
260
+ isFluidField && styles.fieldContainerFluid,
261
+ fieldStyles.container,
262
+ fieldStyle,
263
+ ]}
243
264
  >
244
265
  <Text
245
266
  style={[calendarFieldMetrics.value, valueTextStyle]}
@@ -327,11 +348,24 @@ const styles = StyleSheet.create({
327
348
  maxWidth: "100%",
328
349
  gap: SELECT_LABEL_GAP,
329
350
  },
351
+ wrapperFluid: {
352
+ width: "100%",
353
+ },
330
354
  fieldOutline: {
331
355
  width: CALENDAR_FIELD_WIDTH,
332
356
  maxWidth: "100%",
333
357
  alignSelf: "flex-start",
334
358
  },
359
+ fieldOutlineFluid: {
360
+ width: "100%",
361
+ alignSelf: "stretch",
362
+ },
363
+ fieldContainerFluid: {
364
+ width: "100%",
365
+ minWidth: 0,
366
+ maxWidth: "100%",
367
+ alignSelf: "stretch",
368
+ },
335
369
  label: {
336
370
  ...calendarLabelTypography,
337
371
  },
@@ -73,6 +73,112 @@ function renderSelectIcon(icon: ReactNode, color: string) {
73
73
  return icon;
74
74
  }
75
75
 
76
+ function resolveWebTextNode(
77
+ ref: React.RefObject<ComponentRef<typeof Text> | null>,
78
+ ): HTMLElement | null {
79
+ if (Platform.OS !== "web") {
80
+ return null;
81
+ }
82
+
83
+ const node = ref.current as unknown;
84
+ return node instanceof HTMLElement ? node : null;
85
+ }
86
+
87
+ function syncTruncatedNativeTitle(
88
+ ref: React.RefObject<ComponentRef<typeof Text> | null>,
89
+ label: string,
90
+ ): void {
91
+ const element = resolveWebTextNode(ref);
92
+ if (!element) {
93
+ return;
94
+ }
95
+
96
+ if (element.scrollWidth > element.clientWidth) {
97
+ element.setAttribute("title", label);
98
+ return;
99
+ }
100
+
101
+ element.removeAttribute("title");
102
+ }
103
+
104
+ function clearNativeTitle(ref: React.RefObject<ComponentRef<typeof Text> | null>): void {
105
+ resolveWebTextNode(ref)?.removeAttribute("title");
106
+ }
107
+
108
+ interface SelectOptionItemProps {
109
+ option: SelectOption;
110
+ compact: boolean;
111
+ optionHeight: number;
112
+ isSelected: boolean;
113
+ isHighlighted: boolean;
114
+ isLast: boolean;
115
+ selectableIndex: number;
116
+ onHighlight: (selectableIndex: number) => void;
117
+ onSelect: (value: string) => void;
118
+ }
119
+
120
+ function SelectOptionItem({
121
+ option,
122
+ compact,
123
+ optionHeight,
124
+ isSelected,
125
+ isHighlighted,
126
+ isLast,
127
+ selectableIndex,
128
+ onHighlight,
129
+ onSelect,
130
+ }: SelectOptionItemProps) {
131
+ const labelRef = useRef<ComponentRef<typeof Text>>(null);
132
+
133
+ return (
134
+ <Pressable
135
+ accessibilityRole="button"
136
+ accessibilityState={{ disabled: option.disabled, selected: isSelected }}
137
+ disabled={option.disabled}
138
+ onPress={() => {
139
+ if (!option.disabled) {
140
+ onSelect(option.value);
141
+ }
142
+ }}
143
+ onHoverIn={() => {
144
+ if (!option.disabled && selectableIndex >= 0) {
145
+ onHighlight(selectableIndex);
146
+ }
147
+
148
+ syncTruncatedNativeTitle(labelRef, option.label);
149
+ }}
150
+ onHoverOut={() => {
151
+ clearNativeTitle(labelRef);
152
+ }}
153
+ style={[
154
+ selectDropdownMetrics.option,
155
+ compact && styles.optionCompact,
156
+ { height: optionHeight, minHeight: optionHeight, maxHeight: optionHeight },
157
+ isSelected && selectDropdownMetrics.optionSelected,
158
+ compact && isSelected && styles.optionSelectedCompact,
159
+ isHighlighted && !isSelected && !option.disabled
160
+ ? selectDropdownMetrics.optionHighlighted
161
+ : null,
162
+ !isLast && styles.optionSpacing,
163
+ option.disabled && styles.optionDisabled,
164
+ ]}
165
+ >
166
+ <Text
167
+ ref={labelRef}
168
+ style={[
169
+ selectDropdownMetrics.optionLabel,
170
+ styles.optionLabel,
171
+ isSelected && selectDropdownMetrics.optionLabelSelected,
172
+ option.disabled && styles.optionLabelDisabled,
173
+ ]}
174
+ numberOfLines={1}
175
+ >
176
+ {option.label}
177
+ </Text>
178
+ </Pressable>
179
+ );
180
+ }
181
+
76
182
  export type SelectOption = {
77
183
  value: string;
78
184
  label: string;
@@ -282,6 +388,12 @@ export function Select({
282
388
  return;
283
389
  }
284
390
 
391
+ if (selectedValues.includes(nextValue)) {
392
+ (onValueChange as SingleSelectProps["onValueChange"] | undefined)?.("");
393
+ closeMenu();
394
+ return;
395
+ }
396
+
285
397
  (onValueChange as SingleSelectProps["onValueChange"] | undefined)?.(nextValue);
286
398
  closeMenu();
287
399
  }
@@ -293,45 +405,18 @@ export function Select({
293
405
  const isLast = index === options.length - 1;
294
406
 
295
407
  return (
296
- <Pressable
408
+ <SelectOptionItem
297
409
  key={option.value}
298
- accessibilityRole="button"
299
- accessibilityState={{ disabled: option.disabled, selected: isSelected }}
300
- disabled={option.disabled}
301
- onPress={() => {
302
- if (!option.disabled) {
303
- handleSelect(option.value);
304
- }
305
- }}
306
- onHoverIn={() => {
307
- if (!option.disabled && selectableIndex >= 0) {
308
- setHighlightedIndex(selectableIndex);
309
- }
310
- }}
311
- style={[
312
- selectDropdownMetrics.option,
313
- compact && styles.optionCompact,
314
- { height: optionHeight, minHeight: optionHeight, maxHeight: optionHeight },
315
- isSelected && selectDropdownMetrics.optionSelected,
316
- compact && isSelected && styles.optionSelectedCompact,
317
- isHighlighted && !isSelected && !option.disabled
318
- ? selectDropdownMetrics.optionHighlighted
319
- : null,
320
- !isLast && styles.optionSpacing,
321
- option.disabled && styles.optionDisabled,
322
- ]}
323
- >
324
- <Text
325
- style={[
326
- selectDropdownMetrics.optionLabel,
327
- isSelected && selectDropdownMetrics.optionLabelSelected,
328
- option.disabled && styles.optionLabelDisabled,
329
- ]}
330
- numberOfLines={1}
331
- >
332
- {option.label}
333
- </Text>
334
- </Pressable>
410
+ option={option}
411
+ compact={compact}
412
+ optionHeight={optionHeight}
413
+ isSelected={isSelected}
414
+ isHighlighted={isHighlighted}
415
+ isLast={isLast}
416
+ selectableIndex={selectableIndex}
417
+ onHighlight={setHighlightedIndex}
418
+ onSelect={handleSelect}
419
+ />
335
420
  );
336
421
  });
337
422
 
@@ -479,6 +564,16 @@ const styles = StyleSheet.create({
479
564
  optionDisabled: {
480
565
  opacity: 0.5,
481
566
  },
567
+ optionLabel: {
568
+ minWidth: 0,
569
+ ...(Platform.OS === "web"
570
+ ? ({
571
+ overflow: "hidden",
572
+ textOverflow: "ellipsis",
573
+ whiteSpace: "nowrap",
574
+ } as const)
575
+ : null),
576
+ },
482
577
  optionLabelDisabled: {
483
578
  color: colors.stormGray300,
484
579
  },
@@ -29,7 +29,7 @@ export const selectLabelTypography = {
29
29
  fontFamily: fonts.sans,
30
30
  fontSize: fontSize.sm,
31
31
  fontWeight: fontWeight.medium,
32
- lineHeight: fontSize.sm,
32
+ lineHeight: 16,
33
33
  letterSpacing: 0,
34
34
  color: colors.slateBlue,
35
35
  } as const;
@@ -41,24 +41,33 @@ export function isInDateRange(date: Date, start?: Date, end?: Date): boolean {
41
41
  return !isBeforeDay(day, start) && !isAfterDay(day, end);
42
42
  }
43
43
 
44
- export function selectRangeDate(clicked: Date, current?: DateRange): DateRange {
44
+ export function selectRangeDate(clicked: Date, current?: DateRange): DateRange | undefined {
45
45
  const clickedDate = stripTime(clicked);
46
46
 
47
- if (!current?.start || current.end) {
47
+ if (!current?.start) {
48
48
  return { start: clickedDate };
49
49
  }
50
50
 
51
51
  const start = stripTime(current.start);
52
+ const end = current.end ? stripTime(current.end) : undefined;
53
+
54
+ if (!end) {
55
+ if (isSameDay(clickedDate, start)) {
56
+ return undefined;
57
+ }
58
+
59
+ if (isBeforeDay(clickedDate, start)) {
60
+ return { start: clickedDate };
61
+ }
52
62
 
53
- if (isSameDay(clickedDate, start)) {
54
63
  return { start, end: clickedDate };
55
64
  }
56
65
 
57
- if (isBeforeDay(clickedDate, start)) {
58
- return { start: clickedDate };
66
+ if (isSameDay(clickedDate, end)) {
67
+ return undefined;
59
68
  }
60
69
 
61
- return { start, end: clickedDate };
70
+ return { start: clickedDate };
62
71
  }
63
72
 
64
73
  export function isSameDay(left: Date, right: Date): boolean {
@@ -23,7 +23,7 @@ export function Badge({ children, variant = 'neutral', icon, className }: BadgeP
23
23
  return (
24
24
  <span
25
25
  className={cn(
26
- 'inline-flex items-center gap-2 rounded-[28px] px-3 py-1.5 font-sans text-xs font-semibold leading-none whitespace-nowrap',
26
+ 'inline-flex items-center gap-2 rounded-[28px] px-3 py-1.5 typography-12 font-semibold whitespace-nowrap',
27
27
  badgeVariantClasses[variant],
28
28
  className,
29
29
  )}
@@ -78,13 +78,13 @@ export function ConfirmModal({
78
78
  <header className="flex flex-col gap-3 text-center">
79
79
  <h2
80
80
  id="homepad-confirm-modal-title"
81
- className="font-sans text-xl font-bold leading-6.75 tracking-normal text-black"
81
+ className="typography-title tracking-normal text-black"
82
82
  >
83
83
  {title}
84
84
  </h2>
85
85
  <div
86
86
  id="homepad-confirm-modal-description"
87
- className="text-center font-sans text-sm font-normal leading-4.75 tracking-normal text-storm-gray-400"
87
+ className="text-center typography-body tracking-normal text-storm-gray-400"
88
88
  >
89
89
  {description}
90
90
  </div>
@@ -0,0 +1,234 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { fn } from '@storybook/test';
3
+ import { useState } from 'react';
4
+
5
+ import { Button } from '../../components/Button';
6
+ import { WebLayoutCanvas } from '../layout/story-helpers';
7
+
8
+ import { Drawer } from './Drawer';
9
+ import { HistoryTimeline } from './HistoryTimeline';
10
+ import { StatusBadge } from './StatusBadge';
11
+
12
+ const historyEvents = [
13
+ {
14
+ id: 'received',
15
+ title: 'Հայտը ստացվել է',
16
+ timestamp: '24.06.2026, 14:20',
17
+ },
18
+ {
19
+ id: 'approved',
20
+ title: 'Հայտը հաստատվել է, սպասում է վճարման',
21
+ timestamp: '24.06.2026, 15:05',
22
+ },
23
+ ];
24
+
25
+ const meta = {
26
+ title: 'Web Components/Drawer',
27
+ component: Drawer,
28
+ parameters: {
29
+ layout: 'fullscreen',
30
+ webLayout: true,
31
+ },
32
+ args: {
33
+ open: true,
34
+ title: 'Հայտ #00042',
35
+ subtitle: 'Ստացվել է 24.06.2026, 14:20',
36
+ closeAriaLabel: 'Փակել',
37
+ onClose: fn(),
38
+ },
39
+ } satisfies Meta<typeof Drawer>;
40
+
41
+ export default meta;
42
+ type Story = StoryObj<typeof meta>;
43
+
44
+ function DetailField({ label, value }: { label: string; value: string }) {
45
+ return (
46
+ <div className="flex items-center justify-between gap-4">
47
+ <span className="min-w-0 flex-1 typography-14 font-medium tracking-normal text-storm-gray-500">
48
+ {label}
49
+ </span>
50
+ <span className="shrink-0 text-right typography-14 font-medium leading-[14px] tracking-normal text-storm-gray-900">
51
+ {value}
52
+ </span>
53
+ </div>
54
+ );
55
+ }
56
+
57
+ function DetailsSection({ title, children }: { title: string; children: React.ReactNode }) {
58
+ return (
59
+ <section className="flex flex-col gap-3">
60
+ <h3 className="typography-12 font-semibold uppercase tracking-normal text-storm-gray-200">
61
+ {title}
62
+ </h3>
63
+ <div className="flex flex-col gap-4">{children}</div>
64
+ </section>
65
+ );
66
+ }
67
+
68
+ function RequestDetailsBody() {
69
+ return (
70
+ <>
71
+ <div className="flex flex-col gap-8 pb-6">
72
+ <div className="flex items-center justify-between gap-4">
73
+ <span className="typography-14 font-medium tracking-normal text-storm-gray-500">
74
+ Կարգավիճակ
75
+ </span>
76
+ <StatusBadge tone="warning">Սպասում է վճարման</StatusBadge>
77
+ </div>
78
+
79
+ <DetailsSection title="Կազմակերպության տվյալներ">
80
+ <DetailField label="Հասցե" value="Արաբկիր 12" />
81
+ <DetailField label="Կազմակերպություն" value="Manuk's org" />
82
+ <DetailField label="Սակագնային պլան" value="150+ Apartments" />
83
+ <DetailField label="Անհատական գին" value="10 ֏" />
84
+ <DetailField label="Բնակարանների առավելագույն քանակ" value="120" />
85
+ </DetailsSection>
86
+
87
+ <DetailsSection title="Կոնտակտային անձ">
88
+ <DetailField label="Անուն / Ազգանուն" value="Manuk Grigoryan" />
89
+ <DetailField label="Էլ. փոստ" value="manuk@example.com" />
90
+ <DetailField label="Հեռախոսահամար" value="+374 99 000000" />
91
+ </DetailsSection>
92
+ </div>
93
+
94
+ <section className="-mx-6 flex flex-col justify-between border-t border-dashed border-storm-gray-50 px-6 pt-6 pb-0">
95
+ <div className="flex flex-col gap-3">
96
+ <h3 className="typography-12 font-semibold uppercase tracking-normal text-storm-gray-200">
97
+ Գործողությունների պատմություն
98
+ </h3>
99
+ <HistoryTimeline events={historyEvents} />
100
+ </div>
101
+ </section>
102
+ </>
103
+ );
104
+ }
105
+
106
+ const footerButton = (
107
+ <div className="w-full">
108
+ <Button
109
+ title="Կրկին ուղարկել վճարման հղումը"
110
+ onPress={fn()}
111
+ variant="primary"
112
+ className="btn shrink-0 justify-center!"
113
+ textClassName="text-center!"
114
+ style={{
115
+ width: '100%',
116
+ height: 52,
117
+ minHeight: 52,
118
+ maxHeight: 52,
119
+ paddingTop: 0,
120
+ paddingBottom: 0,
121
+ }}
122
+ />
123
+ </div>
124
+ );
125
+
126
+ function DrawerStoryShell({
127
+ open,
128
+ onClose,
129
+ footer,
130
+ title,
131
+ subtitle,
132
+ }: {
133
+ open: boolean;
134
+ onClose: () => void;
135
+ footer?: React.ReactNode;
136
+ title?: string;
137
+ subtitle?: string;
138
+ }) {
139
+ return (
140
+ <WebLayoutCanvas className="min-h-screen bg-storm-gray-100">
141
+ <Drawer
142
+ open={open}
143
+ title={title ?? 'Հայտ #00042'}
144
+ subtitle={subtitle ?? 'Ստացվել է 24.06.2026, 14:20'}
145
+ closeAriaLabel="Փակել"
146
+ onClose={onClose}
147
+ footer={footer}
148
+ >
149
+ <RequestDetailsBody />
150
+ </Drawer>
151
+ </WebLayoutCanvas>
152
+ );
153
+ }
154
+
155
+ export const Default: Story = {
156
+ args: {
157
+ children: null,
158
+ },
159
+ render: (args) => (
160
+ <DrawerStoryShell open={args.open} onClose={args.onClose} title={args.title} subtitle={args.subtitle} />
161
+ ),
162
+ };
163
+
164
+ export const WithFooter: Story = {
165
+ args: {
166
+ children: null,
167
+ },
168
+ render: (args) => (
169
+ <DrawerStoryShell
170
+ open={args.open}
171
+ onClose={args.onClose}
172
+ title={args.title}
173
+ subtitle={args.subtitle}
174
+ footer={footerButton}
175
+ />
176
+ ),
177
+ };
178
+
179
+ export const WithoutSubtitle: Story = {
180
+ args: {
181
+ children: null,
182
+ subtitle: undefined,
183
+ title: 'Կարգավորումներ',
184
+ },
185
+ render: (args) => (
186
+ <WebLayoutCanvas className="min-h-screen bg-storm-gray-100">
187
+ <Drawer
188
+ open={args.open}
189
+ title={args.title}
190
+ closeAriaLabel="Փակել"
191
+ onClose={args.onClose}
192
+ >
193
+ <p className="typography-14 text-storm-gray-500">
194
+ Drawer content without a subtitle line in the header.
195
+ </p>
196
+ </Drawer>
197
+ </WebLayoutCanvas>
198
+ ),
199
+ };
200
+
201
+ function InteractiveDrawerStory() {
202
+ const [open, setOpen] = useState(false);
203
+
204
+ return (
205
+ <WebLayoutCanvas className="min-h-screen bg-storm-gray-100 p-6">
206
+ <button
207
+ type="button"
208
+ className="rounded-xl border border-storm-gray-50 bg-white px-4 py-2 typography-14 font-medium text-slate-blue"
209
+ onClick={() => setOpen(true)}
210
+ >
211
+ Բացել drawer-ը
212
+ </button>
213
+
214
+ <Drawer
215
+ open={open}
216
+ title="Հայտ #00042"
217
+ subtitle="Ստացվել է 24.06.2026, 14:20"
218
+ closeAriaLabel="Փակել"
219
+ onClose={() => setOpen(false)}
220
+ footer={footerButton}
221
+ >
222
+ <RequestDetailsBody />
223
+ </Drawer>
224
+ </WebLayoutCanvas>
225
+ );
226
+ }
227
+
228
+ export const Interactive: Story = {
229
+ args: {
230
+ children: null,
231
+ open: false,
232
+ },
233
+ render: () => <InteractiveDrawerStory />,
234
+ };