@scripso-homepad/ui 0.4.8 → 0.4.10

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 (39) hide show
  1. package/dist/{chunk-DAOLVE64.js → chunk-YSDLHEJ7.js} +204 -14
  2. package/dist/chunk-YSDLHEJ7.js.map +1 -0
  3. package/dist/index.cjs +130 -87
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +2 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +176 -323
  8. package/dist/index.js.map +1 -1
  9. package/dist/web/index.cjs +882 -30
  10. package/dist/web/index.cjs.map +1 -1
  11. package/dist/web/index.css +161 -0
  12. package/dist/web/index.css.map +1 -1
  13. package/dist/web/index.d.cts +90 -3
  14. package/dist/web/index.d.ts +90 -3
  15. package/dist/web/index.js +645 -19
  16. package/dist/web/index.js.map +1 -1
  17. package/package.json +9 -3
  18. package/src/components/Button.tsx +10 -3
  19. package/src/components/DatePicker.tsx +37 -3
  20. package/src/components/Select.tsx +1 -1
  21. package/src/web/components/ConfirmModal.stories.tsx +55 -0
  22. package/src/web/components/ConfirmModal.tsx +116 -0
  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 +130 -0
  27. package/src/web/components/Modal.tsx +162 -0
  28. package/src/web/components/TableActionButton.tsx +9 -4
  29. package/src/web/components/Toaster.stories.tsx +106 -0
  30. package/src/web/components/Toaster.tsx +92 -0
  31. package/src/web/components/ToasterProvider.tsx +6 -0
  32. package/src/web/components/drawer-scroll.css +41 -0
  33. package/src/web/components/sonner-toast.css +141 -0
  34. package/src/web/icons/TimelineCheckIcon.tsx +24 -0
  35. package/src/web/icons/ToastIcons.tsx +119 -0
  36. package/src/web/index.ts +26 -0
  37. package/src/web/mutation-toast.ts +33 -0
  38. package/src/web/toast.ts +1 -0
  39. package/dist/chunk-DAOLVE64.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scripso-homepad/ui",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "type": "module",
5
5
  "description": "Cross-platform UI components for Homepad (React Web + React Native)",
6
6
  "license": "MIT",
@@ -29,7 +29,8 @@
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"
33
34
  },
34
35
  "files": [
35
36
  "dist",
@@ -51,7 +52,8 @@
51
52
  "react-native": ">=0.74",
52
53
  "react-native-svg": ">=13",
53
54
  "react-native-web": ">=0.19",
54
- "react-router-dom": ">=6"
55
+ "react-router-dom": ">=6",
56
+ "sonner": ">=2.0.0"
55
57
  },
56
58
  "peerDependenciesMeta": {
57
59
  "react-native-svg": {
@@ -65,6 +67,9 @@
65
67
  },
66
68
  "lucide-react": {
67
69
  "optional": true
70
+ },
71
+ "sonner": {
72
+ "optional": true
68
73
  }
69
74
  },
70
75
  "dependencies": {
@@ -90,6 +95,7 @@
90
95
  "react-native-svg": "^15.11.2",
91
96
  "react-native-web": "^0.19.13",
92
97
  "react-router-dom": "^7.9.1",
98
+ "sonner": "^2.0.7",
93
99
  "storybook": "^8.4.7",
94
100
  "tailwindcss": "^4.1.13",
95
101
  "tsup": "^8.3.5",
@@ -115,14 +115,21 @@ export function Button({
115
115
  const preset = variantConfig[variant];
116
116
  const metrics = sizeConfig[size];
117
117
 
118
- useApplyWebClassName(containerRef, className);
119
- useApplyWebClassName(textRef, textClassName);
120
-
121
118
  const label = typeof children !== "undefined" ? children : title;
122
119
  const hasCustomChildren = typeof children !== "undefined";
123
120
  const customIcon =
124
121
  icon != null && typeof icon !== "boolean" ? icon : undefined;
125
122
  const hasIcon = showIcon || icon === true || customIcon != null;
123
+ const resolvedContainerClassName = [
124
+ Platform.OS === "web" ? "max-md:px-4!" : "",
125
+ className,
126
+ ]
127
+ .filter(Boolean)
128
+ .join(" ");
129
+
130
+ useApplyWebClassName(containerRef, resolvedContainerClassName || undefined);
131
+ useApplyWebClassName(textRef, textClassName);
132
+
126
133
  const iconNode =
127
134
  customIcon ?? (
128
135
  <ArrowUpRightIcon size={metrics.iconSize} color={preset.iconColor} />
@@ -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,
@@ -224,22 +228,39 @@ export function DatePicker(props: DatePickerProps) {
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
  },
@@ -176,7 +176,7 @@ export function Select({
176
176
  () =>
177
177
  resolveSelectDropdownContentHeight(options.length, optionHeight) >
178
178
  SELECT_DROPDOWN_MAX_HEIGHT - SELECT_DROPDOWN_PADDING * 2,
179
- [compact, optionHeight, options.length],
179
+ [optionHeight, options.length],
180
180
  );
181
181
 
182
182
  useApplyWebClassName(wrapperRef, className);
@@ -0,0 +1,55 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { fn } from '@storybook/test';
3
+
4
+ import { WebLayoutCanvas } from '../layout/story-helpers';
5
+
6
+ import { ConfirmModal } from './ConfirmModal';
7
+
8
+ const meta = {
9
+ title: 'Web Components/ConfirmModal',
10
+ component: ConfirmModal,
11
+ parameters: {
12
+ layout: 'fullscreen',
13
+ webLayout: true,
14
+ },
15
+ args: {
16
+ open: true,
17
+ title: 'Կասեցնե՞լ շենքի մուտքը',
18
+ description: (
19
+ <>
20
+ Դուք պատրաստվում եք կասեցնել «Կենտրոն» շենքի մուտքը։
21
+ <br />
22
+ Բոլոր օգտատերերը կկորցնեն հասանելիությունը համակարգին։
23
+ </>
24
+ ),
25
+ cancelText: 'Չեղարկել',
26
+ confirmText: 'Կասեցնել',
27
+ onCancel: fn(),
28
+ onConfirm: fn(),
29
+ },
30
+ } satisfies Meta<typeof ConfirmModal>;
31
+
32
+ export default meta;
33
+ type Story = StoryObj<typeof meta>;
34
+
35
+ export const Default: Story = {
36
+ render: (args) => (
37
+ <WebLayoutCanvas className="min-h-screen bg-storm-gray-50 p-6">
38
+ <ConfirmModal {...args} />
39
+ </WebLayoutCanvas>
40
+ ),
41
+ };
42
+
43
+ export const RejectRequest: Story = {
44
+ args: {
45
+ title: 'Մերժե՞լ շենքի հայտը',
46
+ description:' Դուք պատրաստվում եք մերժել «Արաբկիր 12» հասցեի շենքի հայտը։ Կազմակերպությունը կտեղեկացվի մերժման մասին։',
47
+ cancelText: 'Չեղարկել',
48
+ confirmText: 'Մերժել',
49
+ },
50
+ render: (args) => (
51
+ <WebLayoutCanvas className="min-h-screen bg-storm-gray-50 p-6">
52
+ <ConfirmModal {...args} />
53
+ </WebLayoutCanvas>
54
+ ),
55
+ };
@@ -0,0 +1,116 @@
1
+ import { useEffect, type ReactNode } from 'react';
2
+
3
+ import { Button } from '../../components/Button';
4
+ import { cn } from '../utils/cn';
5
+
6
+ export type ConfirmModalProps = {
7
+ open: boolean;
8
+ title: string;
9
+ description: ReactNode;
10
+ cancelText: string;
11
+ confirmText: string;
12
+ onCancel: () => void;
13
+ onConfirm: () => void;
14
+ cancelDisabled?: boolean;
15
+ confirmDisabled?: boolean;
16
+ closeOnBackdrop?: boolean;
17
+ containerClassName?: string;
18
+ buttonClassName?: string;
19
+ };
20
+
21
+ export function ConfirmModal({
22
+ open,
23
+ title,
24
+ description,
25
+ cancelText,
26
+ confirmText,
27
+ onCancel,
28
+ onConfirm,
29
+ cancelDisabled = false,
30
+ confirmDisabled = false,
31
+ closeOnBackdrop = true,
32
+ containerClassName,
33
+ buttonClassName,
34
+ }: ConfirmModalProps) {
35
+ useEffect(() => {
36
+ if (!open) {
37
+ return;
38
+ }
39
+
40
+ const handleKeyDown = (event: KeyboardEvent) => {
41
+ if (event.key === 'Escape') {
42
+ onCancel();
43
+ }
44
+ };
45
+
46
+ window.addEventListener('keydown', handleKeyDown);
47
+
48
+ return () => {
49
+ window.removeEventListener('keydown', handleKeyDown);
50
+ };
51
+ }, [open, onCancel]);
52
+
53
+ if (!open) {
54
+ return null;
55
+ }
56
+
57
+ const sharedButtonClassName = cn('btn h-10 min-w-0 flex-1 justify-center!', buttonClassName);
58
+
59
+ return (
60
+ <div
61
+ className="fixed inset-0 z-50 flex items-center justify-center p-4 backdrop-blur-md bg-storm-gray-850/40"
62
+ onClick={closeOnBackdrop ? onCancel : undefined}
63
+ role="presentation"
64
+ >
65
+ <div
66
+ role="dialog"
67
+ aria-modal="true"
68
+ aria-labelledby="homepad-confirm-modal-title"
69
+ aria-describedby="homepad-confirm-modal-description"
70
+ className={cn(
71
+ 'flex w-full max-w-md flex-col gap-8 rounded-2xl bg-white p-4 opacity-100 md:p-6',
72
+ containerClassName,
73
+ )}
74
+ onClick={(event) => {
75
+ event.stopPropagation();
76
+ }}
77
+ >
78
+ <header className="flex flex-col gap-3 text-center">
79
+ <h2
80
+ id="homepad-confirm-modal-title"
81
+ className="font-sans text-xl font-bold leading-6.75 tracking-normal text-black"
82
+ >
83
+ {title}
84
+ </h2>
85
+ <div
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"
88
+ >
89
+ {description}
90
+ </div>
91
+ </header>
92
+
93
+ <footer className="flex w-full gap-4">
94
+ <Button
95
+ title={cancelText}
96
+ onPress={onCancel}
97
+ variant="gray"
98
+ size='sm'
99
+ disabled={cancelDisabled}
100
+ className={sharedButtonClassName}
101
+ textClassName="text-center!"
102
+ />
103
+ <Button
104
+ title={confirmText}
105
+ onPress={onConfirm}
106
+ variant="primary"
107
+ size='sm'
108
+ disabled={confirmDisabled}
109
+ className={sharedButtonClassName}
110
+ textClassName="text-center!"
111
+ />
112
+ </footer>
113
+ </div>
114
+ </div>
115
+ );
116
+ }
@@ -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 font-sans text-sm font-medium leading-[19px] tracking-normal text-storm-gray-500">
48
+ {label}
49
+ </span>
50
+ <span className="shrink-0 text-right font-sans text-sm 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="font-sans text-xs font-semibold uppercase leading-4 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="font-sans text-sm font-medium leading-[19px] 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="font-sans text-xs font-semibold uppercase leading-4 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="font-sans text-sm leading-[19px] 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 text-sm 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
+ };