@scripso-homepad/ui 0.4.1 → 0.4.3

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 (86) hide show
  1. package/dist/{chunk-C7GHBVMM.js → chunk-66WR57JJ.js} +53 -10
  2. package/dist/chunk-66WR57JJ.js.map +1 -0
  3. package/dist/index.cjs +2297 -391
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +144 -2
  6. package/dist/index.d.ts +144 -2
  7. package/dist/index.js +1978 -96
  8. package/dist/index.js.map +1 -1
  9. package/dist/web/index.cjs +30411 -759
  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/Input.tsx +5 -1
  23. package/src/components/Select.stories.tsx +134 -0
  24. package/src/components/Select.tsx +462 -0
  25. package/src/css.d.ts +1 -0
  26. package/src/icons/CalendarIcon.tsx +28 -0
  27. package/src/icons/CalendarIcon.web.tsx +34 -0
  28. package/src/icons/ChevronDownIcon.tsx +11 -4
  29. package/src/icons/ChevronDownIcon.web.tsx +6 -4
  30. package/src/icons/ChevronNavIcons.tsx +44 -0
  31. package/src/icons/ChevronNavIcons.web.tsx +57 -0
  32. package/src/icons/LockIcon.tsx +30 -0
  33. package/src/icons/LockIcon.web.tsx +35 -0
  34. package/src/icons/calendarIconPaths.ts +2 -0
  35. package/src/icons/chevronDownPath.ts +1 -0
  36. package/src/icons/chevronNavPaths.ts +2 -0
  37. package/src/icons/lockIconPaths.ts +2 -0
  38. package/src/index.ts +43 -0
  39. package/src/theme/calendar.ts +357 -0
  40. package/src/theme/input.ts +16 -6
  41. package/src/theme/select.ts +249 -0
  42. package/src/utils/calendarDays.ts +273 -0
  43. package/src/utils/calendarLocaleContext.tsx +95 -0
  44. package/src/utils/calendarLocalization.ts +187 -0
  45. package/src/utils/countryDropdownScrollStyles.ts +53 -13
  46. package/src/utils/formatMultiSelectDisplay.ts +56 -0
  47. package/src/web/components/Badge.stories.tsx +43 -0
  48. package/src/web/components/Badge.tsx +35 -0
  49. package/src/web/components/Pagination.stories.tsx +78 -0
  50. package/src/web/components/Pagination.tsx +153 -0
  51. package/src/web/components/StatusBadge.tsx +20 -0
  52. package/src/web/components/Table.stories.tsx +415 -0
  53. package/src/web/components/Table.tsx +3 -0
  54. package/src/web/components/TableActionButton.tsx +67 -0
  55. package/src/web/components/TableActionsMenu.tsx +208 -0
  56. package/src/web/components/TableIconText.tsx +23 -0
  57. package/src/web/components/TableRowStatusActions.tsx +33 -0
  58. package/src/web/components/TableStatusActionsCell.tsx +29 -0
  59. package/src/web/components/pagination-utils.ts +39 -0
  60. package/src/web/components/table/DataTable.tsx +123 -0
  61. package/src/web/components/table/TablePrimitives.tsx +214 -0
  62. package/src/web/components/table/TableScrollArea.tsx +51 -0
  63. package/src/web/components/table/constants.ts +59 -0
  64. package/src/web/components/table/data-table-class-names.ts +26 -0
  65. package/src/web/components/table/index.ts +44 -0
  66. package/src/web/components/table/table-row-context.tsx +19 -0
  67. package/src/web/components/table/use-horizontal-scroll-state.ts +72 -0
  68. package/src/web/components/table-scroll.css +81 -0
  69. package/src/web/hooks/useOnClickOutside.ts +20 -5
  70. package/src/web/icons/TableMenuActivateIcon.tsx +27 -0
  71. package/src/web/icons/TableMenuCheckIcon.tsx +27 -0
  72. package/src/web/icons/TableMenuCloseIcon.tsx +27 -0
  73. package/src/web/icons/TableMenuEditIcon.tsx +27 -0
  74. package/src/web/icons/TableMenuInfoIcon.tsx +27 -0
  75. package/src/web/icons/TableMenuRefreshIcon.tsx +27 -0
  76. package/src/web/icons/TableMenuSuspendIcon.tsx +34 -0
  77. package/src/web/icons/TableMenuTrashIcon.tsx +27 -0
  78. package/src/web/icons/TableMoreIcon.tsx +41 -0
  79. package/src/web/icons/TableSortIcon.tsx +37 -0
  80. package/src/web/index.ts +74 -0
  81. package/src/web/layout/AppHeader.stories.tsx +2 -0
  82. package/src/web/layout/AppHeader.tsx +18 -14
  83. package/src/web/layout/DashboardLayout.stories.tsx +1 -0
  84. package/src/web/layout/DashboardLayout.tsx +8 -4
  85. package/src/web/layout/story-helpers.tsx +10 -2
  86. package/dist/chunk-C7GHBVMM.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
+ });
@@ -56,6 +56,8 @@ export interface InputProps extends TextInputProps {
56
56
  rightIcon?: ReactNode;
57
57
  /** Validation or helper error message. Shown instead of `hint` when set. */
58
58
  error?: string;
59
+ /** Applies error field styling without showing a message (use with a form-level error). */
60
+ invalid?: boolean;
59
61
  /** Helper text shown below the input when there is no error. */
60
62
  hint?: string;
61
63
  containerStyle?: StyleProp<ViewStyle>;
@@ -78,6 +80,7 @@ export function Input({
78
80
  leftIcon,
79
81
  rightIcon,
80
82
  error,
83
+ invalid,
81
84
  hint,
82
85
  containerStyle,
83
86
  style,
@@ -108,6 +111,7 @@ export function Input({
108
111
  useApplyWebClassName(helperRef, error ? errorClassName : hintClassName);
109
112
 
110
113
  const isDisabled = editable === false;
114
+ const hasError = Boolean(error) || Boolean(invalid);
111
115
  const passwordToggleEnabled =
112
116
  secureTextEntry === true && showPasswordToggle !== false && rightIcon == null;
113
117
  const effectiveSecureTextEntry = passwordToggleEnabled
@@ -115,7 +119,7 @@ export function Input({
115
119
  : secureTextEntry;
116
120
  const visualState = resolveInputVisualState({
117
121
  focused,
118
- error: Boolean(error),
122
+ error: hasError,
119
123
  disabled: isDisabled,
120
124
  });
121
125
  const fieldStyles = getInputFieldStyles(visualState);
@@ -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
+ });