@scripso-homepad/ui 0.4.2 → 0.4.4

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 (81) hide show
  1. package/dist/{chunk-WDSMJWKC.js → chunk-66WR57JJ.js} +35 -3
  2. package/dist/chunk-66WR57JJ.js.map +1 -0
  3. package/dist/index.cjs +2258 -384
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +134 -1
  6. package/dist/index.d.ts +134 -1
  7. package/dist/index.js +1959 -97
  8. package/dist/index.js.map +1 -1
  9. package/dist/web/index.cjs +30650 -1009
  10. package/dist/web/index.cjs.map +1 -1
  11. package/dist/web/index.css +78 -0
  12. package/dist/web/index.css.map +1 -0
  13. package/dist/web/index.d.cts +267 -6
  14. package/dist/web/index.d.ts +267 -6
  15. package/dist/web/index.js +30179 -601
  16. package/dist/web/index.js.map +1 -1
  17. package/package.json +5 -4
  18. package/src/components/Calendar.stories.tsx +337 -0
  19. package/src/components/Calendar.tsx +441 -0
  20. package/src/components/DatePicker.stories.tsx +219 -0
  21. package/src/components/DatePicker.tsx +366 -0
  22. package/src/components/Select.stories.tsx +134 -0
  23. package/src/components/Select.tsx +465 -0
  24. package/src/css.d.ts +1 -0
  25. package/src/icons/CalendarIcon.tsx +28 -0
  26. package/src/icons/CalendarIcon.web.tsx +34 -0
  27. package/src/icons/ChevronDownIcon.tsx +11 -4
  28. package/src/icons/ChevronDownIcon.web.tsx +6 -4
  29. package/src/icons/ChevronNavIcons.tsx +44 -0
  30. package/src/icons/ChevronNavIcons.web.tsx +57 -0
  31. package/src/icons/calendarIconPaths.ts +2 -0
  32. package/src/icons/chevronDownPath.ts +1 -0
  33. package/src/icons/chevronNavPaths.ts +2 -0
  34. package/src/index.ts +41 -0
  35. package/src/theme/calendar.ts +357 -0
  36. package/src/theme/select.ts +247 -0
  37. package/src/utils/calendarDays.ts +273 -0
  38. package/src/utils/calendarLocaleContext.tsx +95 -0
  39. package/src/utils/calendarLocalization.ts +187 -0
  40. package/src/utils/countryDropdownScrollStyles.ts +53 -13
  41. package/src/utils/formatMultiSelectDisplay.ts +56 -0
  42. package/src/web/components/Badge.stories.tsx +43 -0
  43. package/src/web/components/Badge.tsx +35 -0
  44. package/src/web/components/Pagination.stories.tsx +78 -0
  45. package/src/web/components/Pagination.tsx +153 -0
  46. package/src/web/components/StatusBadge.tsx +20 -0
  47. package/src/web/components/Table.stories.tsx +415 -0
  48. package/src/web/components/Table.tsx +3 -0
  49. package/src/web/components/TableActionButton.tsx +67 -0
  50. package/src/web/components/TableActionsMenu.tsx +208 -0
  51. package/src/web/components/TableIconText.tsx +23 -0
  52. package/src/web/components/TableRowStatusActions.tsx +33 -0
  53. package/src/web/components/TableStatusActionsCell.tsx +29 -0
  54. package/src/web/components/pagination-utils.ts +39 -0
  55. package/src/web/components/table/DataTable.tsx +123 -0
  56. package/src/web/components/table/TablePrimitives.tsx +214 -0
  57. package/src/web/components/table/TableScrollArea.tsx +51 -0
  58. package/src/web/components/table/constants.ts +59 -0
  59. package/src/web/components/table/data-table-class-names.ts +26 -0
  60. package/src/web/components/table/index.ts +44 -0
  61. package/src/web/components/table/table-row-context.tsx +19 -0
  62. package/src/web/components/table/use-horizontal-scroll-state.ts +72 -0
  63. package/src/web/components/table-scroll.css +81 -0
  64. package/src/web/hooks/useOnClickOutside.ts +20 -5
  65. package/src/web/icons/TableMenuActivateIcon.tsx +27 -0
  66. package/src/web/icons/TableMenuCheckIcon.tsx +27 -0
  67. package/src/web/icons/TableMenuCloseIcon.tsx +27 -0
  68. package/src/web/icons/TableMenuEditIcon.tsx +27 -0
  69. package/src/web/icons/TableMenuInfoIcon.tsx +27 -0
  70. package/src/web/icons/TableMenuRefreshIcon.tsx +27 -0
  71. package/src/web/icons/TableMenuSuspendIcon.tsx +34 -0
  72. package/src/web/icons/TableMenuTrashIcon.tsx +27 -0
  73. package/src/web/icons/TableMoreIcon.tsx +41 -0
  74. package/src/web/icons/TableSortIcon.tsx +37 -0
  75. package/src/web/index.ts +74 -0
  76. package/src/web/layout/AppHeader.stories.tsx +2 -0
  77. package/src/web/layout/AppHeader.tsx +18 -14
  78. package/src/web/layout/DashboardLayout.stories.tsx +1 -0
  79. package/src/web/layout/DashboardLayout.tsx +4 -4
  80. package/src/web/layout/story-helpers.tsx +10 -2
  81. package/dist/chunk-WDSMJWKC.js.map +0 -1
@@ -0,0 +1,366 @@
1
+ import { useEffect, useRef, useState, type ComponentRef } from "react";
2
+ import {
3
+ Modal,
4
+ Pressable,
5
+ StyleSheet,
6
+ Text,
7
+ View,
8
+ type StyleProp,
9
+ type ViewStyle,
10
+ } from "react-native";
11
+ import { CalendarIcon } from "../icons/CalendarIcon";
12
+ import {
13
+ CALENDAR_FIELD_ICON_SIZE,
14
+ CALENDAR_FIELD_WIDTH,
15
+ CALENDAR_PANEL_HEIGHT,
16
+ CALENDAR_PANEL_WIDTH,
17
+ calendarFieldMetrics,
18
+ calendarFieldPlaceholderTypography,
19
+ calendarFieldTypography,
20
+ calendarLabelTypography,
21
+ resolveCalendarDropdownLeft,
22
+ resolveCalendarDropdownTop,
23
+ } from "../theme/calendar";
24
+ import { getSelectFieldStyles, resolveInputVisualState, SELECT_LABEL_GAP } from "../theme/select";
25
+ import { colors } from "../theme/tokens";
26
+ import {
27
+ formatCalendarDate,
28
+ formatCalendarDateRange,
29
+ type DateRange,
30
+ } from "../utils/calendarDays";
31
+ import { getCalendarTranslations } from "../utils/calendarLocalization";
32
+ import { useCalendarLocale } from "../utils/calendarLocaleContext";
33
+ import { useApplyWebClassName } from "../utils/useApplyWebClassName";
34
+ import {
35
+ Calendar,
36
+ type CalendarLabels,
37
+ type CalendarRangeProps,
38
+ type CalendarSingleProps,
39
+ } from "./Calendar";
40
+ import { Label } from "./Label";
41
+
42
+ export type { CalendarLabels, DateRange };
43
+
44
+ type DatePickerFieldProps = {
45
+ label?: string;
46
+ placeholder?: string;
47
+ disabled?: boolean;
48
+ error?: string;
49
+ invalid?: boolean;
50
+ hint?: string;
51
+ containerStyle?: StyleProp<ViewStyle>;
52
+ className?: string;
53
+ fieldClassName?: string;
54
+ panelClassName?: string;
55
+ labelClassName?: string;
56
+ errorClassName?: string;
57
+ hintClassName?: string;
58
+ /** Opens the calendar popup on mount (useful for Storybook previews). */
59
+ defaultOpen?: boolean;
60
+ /** Closes the popup after a complete range is selected. Defaults to true in range mode. */
61
+ closeOnRangeComplete?: boolean;
62
+ };
63
+
64
+ export type DatePickerSingleProps = Omit<CalendarSingleProps, "style" | "className"> &
65
+ DatePickerFieldProps & {
66
+ mode?: "single";
67
+ formatDate?: (date: Date) => string;
68
+ };
69
+
70
+ export type DatePickerRangeProps = Omit<CalendarRangeProps, "style" | "className"> &
71
+ DatePickerFieldProps & {
72
+ mode: "range";
73
+ formatRange?: (range: DateRange) => string;
74
+ rangeSeparator?: string;
75
+ };
76
+
77
+ export type DatePickerProps = DatePickerSingleProps | DatePickerRangeProps;
78
+
79
+ export function DatePicker(props: DatePickerProps) {
80
+ const {
81
+ label,
82
+ placeholder,
83
+ locale,
84
+ labels,
85
+ minDate,
86
+ maxDate,
87
+ today,
88
+ viewDate,
89
+ defaultViewDate,
90
+ onViewDateChange,
91
+ disabled = false,
92
+ error,
93
+ invalid,
94
+ hint,
95
+ containerStyle,
96
+ className,
97
+ fieldClassName,
98
+ panelClassName,
99
+ labelClassName,
100
+ errorClassName,
101
+ hintClassName,
102
+ defaultOpen = false,
103
+ } = props;
104
+ const mode = props.mode ?? "single";
105
+ const closeOnRangeComplete =
106
+ props.mode === "range" ? (props.closeOnRangeComplete ?? true) : false;
107
+ const resolvedLocale = useCalendarLocale(locale);
108
+ const translations = getCalendarTranslations(resolvedLocale);
109
+ const resolvedPlaceholder =
110
+ placeholder ?? (mode === "range" ? translations.rangePlaceholder : translations.datePlaceholder);
111
+ const wrapperRef = useRef<ComponentRef<typeof View>>(null);
112
+ const triggerRef = useRef<ComponentRef<typeof Pressable>>(null);
113
+ const helperRef = useRef<ComponentRef<typeof Text>>(null);
114
+ const [open, setOpen] = useState(defaultOpen);
115
+ const [anchor, setAnchor] = useState<{
116
+ x: number;
117
+ y: number;
118
+ width: number;
119
+ height: number;
120
+ } | null>(null);
121
+
122
+ useApplyWebClassName(wrapperRef, className);
123
+ useApplyWebClassName(triggerRef, fieldClassName);
124
+ useApplyWebClassName(helperRef, error ? errorClassName : hintClassName);
125
+
126
+ useEffect(() => {
127
+ if (!defaultOpen || disabled) {
128
+ return;
129
+ }
130
+
131
+ const frame = requestAnimationFrame(() => {
132
+ triggerRef.current?.measureInWindow((x, y, width, height) => {
133
+ setAnchor({ x, y, width, height });
134
+ setOpen(true);
135
+ });
136
+ });
137
+
138
+ return () => cancelAnimationFrame(frame);
139
+ }, [defaultOpen, disabled]);
140
+
141
+ const hasError = Boolean(error) || Boolean(invalid);
142
+ const fieldStyles = getSelectFieldStyles(
143
+ resolveInputVisualState({
144
+ error: hasError,
145
+ disabled,
146
+ }),
147
+ );
148
+
149
+ const hasValue =
150
+ mode === "single"
151
+ ? Boolean(props.value)
152
+ : Boolean(props.mode === "range" && props.value?.start);
153
+ const displayValue = (() => {
154
+ if (props.mode === "range") {
155
+ if (!props.value?.start) {
156
+ return resolvedPlaceholder;
157
+ }
158
+
159
+ return (
160
+ props.formatRange?.(props.value) ??
161
+ formatCalendarDateRange(props.value, resolvedLocale, props.rangeSeparator)
162
+ );
163
+ }
164
+
165
+ if (props.value) {
166
+ return props.formatDate?.(props.value) ?? formatCalendarDate(props.value, resolvedLocale);
167
+ }
168
+
169
+ return resolvedPlaceholder;
170
+ })();
171
+ const isPlaceholder = !hasValue;
172
+ const valueTextStyle = isPlaceholder
173
+ ? {
174
+ ...calendarFieldPlaceholderTypography,
175
+ color: disabled ? colors.stormGray300 : calendarFieldPlaceholderTypography.color,
176
+ }
177
+ : {
178
+ ...calendarFieldTypography,
179
+ color: fieldStyles.value.color,
180
+ };
181
+ const helperMessage = error ?? hint;
182
+ const panelWidth = CALENDAR_PANEL_WIDTH;
183
+ const panelLeft = anchor ? resolveCalendarDropdownLeft(anchor, panelWidth) : 0;
184
+ const dropdownTop = anchor ? resolveCalendarDropdownTop(anchor) : 0;
185
+ const resolvedDefaultViewDate =
186
+ defaultViewDate ??
187
+ (props.mode === "range" ? props.value?.start : props.value) ??
188
+ today;
189
+
190
+ function closePicker() {
191
+ setOpen(false);
192
+ setAnchor(null);
193
+ }
194
+
195
+ function openPicker() {
196
+ if (disabled) {
197
+ return;
198
+ }
199
+
200
+ triggerRef.current?.measureInWindow((x, y, width, height) => {
201
+ setAnchor({ x, y, width, height });
202
+ setOpen(true);
203
+ });
204
+ }
205
+
206
+ function handleSingleSelect(date: Date) {
207
+ if (props.mode === "range") {
208
+ return;
209
+ }
210
+
211
+ props.onChange?.(date);
212
+ closePicker();
213
+ }
214
+
215
+ function handleRangeSelect(range: DateRange) {
216
+ if (props.mode !== "range") {
217
+ return;
218
+ }
219
+
220
+ props.onChange?.(range);
221
+
222
+ if (closeOnRangeComplete && range.end) {
223
+ closePicker();
224
+ }
225
+ }
226
+
227
+ return (
228
+ <View ref={wrapperRef} style={[styles.wrapper, containerStyle]}>
229
+ {label ? (
230
+ <Label disabled={disabled} style={styles.label} className={labelClassName}>
231
+ {label}
232
+ </Label>
233
+ ) : null}
234
+
235
+ <View style={[fieldStyles.outline, styles.fieldOutline]}>
236
+ <Pressable
237
+ ref={triggerRef}
238
+ accessibilityRole="button"
239
+ accessibilityState={{ disabled, expanded: open }}
240
+ disabled={disabled}
241
+ onPress={openPicker}
242
+ style={[calendarFieldMetrics.container, fieldStyles.container]}
243
+ >
244
+ <Text
245
+ style={[calendarFieldMetrics.value, valueTextStyle]}
246
+ numberOfLines={1}
247
+ >
248
+ {displayValue}
249
+ </Text>
250
+ <View style={styles.iconSlot}>
251
+ <CalendarIcon
252
+ size={CALENDAR_FIELD_ICON_SIZE}
253
+ color={disabled ? colors.stormGray200 : colors.stormGray150}
254
+ />
255
+ </View>
256
+ </Pressable>
257
+ </View>
258
+
259
+ {helperMessage ? (
260
+ <Text ref={helperRef} style={error ? styles.error : styles.hint}>
261
+ {helperMessage}
262
+ </Text>
263
+ ) : null}
264
+
265
+ <Modal visible={open} transparent animationType="fade" onRequestClose={closePicker}>
266
+ <View style={styles.modalRoot}>
267
+ <Pressable
268
+ style={styles.backdrop}
269
+ onPress={closePicker}
270
+ accessibilityRole="button"
271
+ accessibilityLabel={labels?.closeCalendar ?? translations.closeCalendar}
272
+ />
273
+ {anchor ? (
274
+ <View
275
+ style={[
276
+ styles.panelPosition,
277
+ {
278
+ top: dropdownTop,
279
+ left: panelLeft,
280
+ width: panelWidth,
281
+ height: CALENDAR_PANEL_HEIGHT,
282
+ },
283
+ ]}
284
+ >
285
+ {props.mode === "range" ? (
286
+ <Calendar
287
+ mode="range"
288
+ value={props.value}
289
+ onChange={handleRangeSelect}
290
+ viewDate={viewDate}
291
+ defaultViewDate={resolvedDefaultViewDate}
292
+ onViewDateChange={onViewDateChange}
293
+ locale={resolvedLocale}
294
+ labels={labels}
295
+ minDate={minDate}
296
+ maxDate={maxDate}
297
+ today={today}
298
+ className={panelClassName}
299
+ />
300
+ ) : (
301
+ <Calendar
302
+ mode="single"
303
+ value={props.value}
304
+ onChange={handleSingleSelect}
305
+ viewDate={viewDate}
306
+ defaultViewDate={resolvedDefaultViewDate}
307
+ onViewDateChange={onViewDateChange}
308
+ locale={resolvedLocale}
309
+ labels={labels}
310
+ minDate={minDate}
311
+ maxDate={maxDate}
312
+ today={today}
313
+ className={panelClassName}
314
+ />
315
+ )}
316
+ </View>
317
+ ) : null}
318
+ </View>
319
+ </Modal>
320
+ </View>
321
+ );
322
+ }
323
+
324
+ const styles = StyleSheet.create({
325
+ wrapper: {
326
+ width: CALENDAR_FIELD_WIDTH,
327
+ maxWidth: "100%",
328
+ gap: SELECT_LABEL_GAP,
329
+ },
330
+ fieldOutline: {
331
+ width: CALENDAR_FIELD_WIDTH,
332
+ maxWidth: "100%",
333
+ alignSelf: "flex-start",
334
+ },
335
+ label: {
336
+ ...calendarLabelTypography,
337
+ },
338
+ iconSlot: {
339
+ width: CALENDAR_FIELD_ICON_SIZE,
340
+ height: CALENDAR_FIELD_ICON_SIZE,
341
+ alignItems: "center",
342
+ justifyContent: "center",
343
+ flexShrink: 0,
344
+ },
345
+ modalRoot: {
346
+ flex: 1,
347
+ },
348
+ backdrop: {
349
+ ...StyleSheet.absoluteFillObject,
350
+ backgroundColor: colors.transparent,
351
+ },
352
+ panelPosition: {
353
+ position: "absolute",
354
+ zIndex: 1,
355
+ },
356
+ error: {
357
+ fontSize: 12,
358
+ lineHeight: 12,
359
+ color: colors.inputError,
360
+ },
361
+ hint: {
362
+ fontSize: 12,
363
+ lineHeight: 12,
364
+ color: colors.stormGray300,
365
+ },
366
+ });
@@ -0,0 +1,134 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { useState } from "react";
3
+ import { View, StyleSheet } from "react-native";
4
+ import { KeyIcon } from "../icons/KeyIcon";
5
+ import { Select, type SelectOption } from "./Select";
6
+
7
+ const demoOptions: SelectOption[] = Array.from({ length: 7 }, (_, index) => ({
8
+ value: String(index + 1),
9
+ label: `Select Item ${index + 1}`,
10
+ }));
11
+
12
+ const meta = {
13
+ title: "Components/Select",
14
+ component: Select,
15
+ args: {
16
+ label: "Label",
17
+ placeholder: "placeholder",
18
+ leftIcon: <KeyIcon />,
19
+ options: demoOptions,
20
+ },
21
+ } satisfies Meta<typeof Select>;
22
+
23
+ export default meta;
24
+ type Story = StoryObj<typeof meta>;
25
+
26
+ export const Default: Story = {};
27
+
28
+ export const Controlled: Story = {
29
+ render: function ControlledSelectStory() {
30
+ const [value, setValue] = useState<string | undefined>();
31
+
32
+ return (
33
+ <View style={styles.field}>
34
+ <Select
35
+ label="Label"
36
+ placeholder="placeholder"
37
+ leftIcon={<KeyIcon />}
38
+ options={demoOptions}
39
+ value={value}
40
+ onValueChange={setValue}
41
+ />
42
+ </View>
43
+ );
44
+ },
45
+ };
46
+
47
+ export const Open: Story = {
48
+ render: function OpenSelectStory() {
49
+ const [value, setValue] = useState("1");
50
+
51
+ return (
52
+ <View style={styles.field}>
53
+ <Select
54
+ label="Label"
55
+ placeholder="placeholder"
56
+ leftIcon={<KeyIcon />}
57
+ options={demoOptions}
58
+ value={value}
59
+ onValueChange={setValue}
60
+ />
61
+ </View>
62
+ );
63
+ },
64
+ };
65
+
66
+ export const WithError: Story = {
67
+ args: {
68
+ error: "Please select an item",
69
+ invalid: true,
70
+ },
71
+ };
72
+
73
+ export const Disabled: Story = {
74
+ args: {
75
+ value: "1",
76
+ disabled: true,
77
+ },
78
+ };
79
+
80
+ const multiSelectOptions: SelectOption[] = Array.from({ length: 8 }, (_, index) => ({
81
+ value: String(index + 1),
82
+ label: `Selected Item ${index + 1}`,
83
+ }));
84
+
85
+ export const Multiple: Story = {
86
+ render: function MultipleSelectStory() {
87
+ const [value, setValue] = useState<string[]>(["1", "2", "3", "4", "5"]);
88
+
89
+ return (
90
+ <View style={styles.field}>
91
+ <Select
92
+ multiple
93
+ label="Label"
94
+ placeholder="placeholder"
95
+ leftIcon={<KeyIcon />}
96
+ options={multiSelectOptions}
97
+ value={value}
98
+ onValueChange={setValue}
99
+ />
100
+ </View>
101
+ );
102
+ },
103
+ };
104
+
105
+ export const MultipleNarrow: Story = {
106
+ render: function MultipleNarrowSelectStory() {
107
+ const [value, setValue] = useState<string[]>(["1", "2", "3", "4", "5"]);
108
+
109
+ return (
110
+ <View style={styles.narrowField}>
111
+ <Select
112
+ multiple
113
+ label="Label"
114
+ placeholder="placeholder"
115
+ leftIcon={<KeyIcon />}
116
+ options={multiSelectOptions}
117
+ value={value}
118
+ onValueChange={setValue}
119
+ />
120
+ </View>
121
+ );
122
+ },
123
+ };
124
+
125
+ const styles = StyleSheet.create({
126
+ field: {
127
+ width: 360,
128
+ maxWidth: "100%",
129
+ },
130
+ narrowField: {
131
+ width: 240,
132
+ maxWidth: "100%",
133
+ },
134
+ });