@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.
- package/dist/index.cjs +245 -127
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +247 -129
- package/dist/index.js.map +1 -1
- package/dist/web/index.cjs +451 -48
- package/dist/web/index.cjs.map +1 -1
- package/dist/web/index.css +161 -0
- package/dist/web/index.css.map +1 -1
- package/dist/web/index.d.cts +50 -2
- package/dist/web/index.d.ts +50 -2
- package/dist/web/index.js +444 -49
- package/dist/web/index.js.map +1 -1
- package/package.json +10 -3
- package/src/components/Calendar.tsx +10 -2
- package/src/components/DatePicker.tsx +40 -6
- package/src/components/Select.tsx +133 -38
- package/src/theme/select.ts +1 -1
- package/src/utils/calendarDays.ts +15 -6
- package/src/web/components/Badge.tsx +1 -1
- package/src/web/components/ConfirmModal.tsx +2 -2
- package/src/web/components/Drawer.stories.tsx +234 -0
- package/src/web/components/Drawer.tsx +169 -0
- package/src/web/components/HistoryTimeline.tsx +46 -0
- package/src/web/components/Modal.stories.tsx +1 -1
- package/src/web/components/Modal.tsx +30 -13
- package/src/web/components/Pagination.tsx +2 -2
- package/src/web/components/TableActionButton.tsx +4 -4
- package/src/web/components/TableActionsMenu.tsx +1 -1
- package/src/web/components/TableIconText.tsx +1 -1
- package/src/web/components/Toaster.stories.tsx +106 -0
- package/src/web/components/Toaster.tsx +92 -0
- package/src/web/components/ToasterProvider.tsx +6 -0
- package/src/web/components/drawer-scroll.css +41 -0
- package/src/web/components/sonner-toast.css +141 -0
- package/src/web/components/table/constants.ts +2 -2
- package/src/web/icons/TimelineCheckIcon.tsx +24 -0
- package/src/web/icons/ToastIcons.tsx +119 -0
- package/src/web/index.ts +20 -0
- package/src/web/layout/AppHeader.tsx +1 -1
- package/src/web/layout/BuildingSelect.tsx +2 -2
- package/src/web/layout/DashboardLayout.stories.tsx +1 -1
- package/src/web/layout/SidebarNavItem.tsx +1 -1
- package/src/web/layout/SidebarUserCard.tsx +3 -3
- package/src/web/mutation-toast.ts +33 -0
- package/src/web/styles/typography.css +99 -0
- 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.
|
|
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
|
|
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
|
|
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
|
|
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={[
|
|
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
|
-
<
|
|
408
|
+
<SelectOptionItem
|
|
297
409
|
key={option.value}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
|
|
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
|
},
|
package/src/theme/select.ts
CHANGED
|
@@ -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
|
|
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 (
|
|
58
|
-
return
|
|
66
|
+
if (isSameDay(clickedDate, end)) {
|
|
67
|
+
return undefined;
|
|
59
68
|
}
|
|
60
69
|
|
|
61
|
-
return { start
|
|
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
|
|
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="
|
|
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
|
|
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
|
+
};
|