@scripso-homepad/ui 0.4.2 → 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 (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 +2260 -385
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +133 -1
  6. package/dist/index.d.ts +133 -1
  7. package/dist/index.js +1960 -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 +462 -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 +249 -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,56 @@
1
+ import { fontSize } from "../theme/tokens";
2
+
3
+ const MULTI_SELECT_SUFFIX = " ... + ";
4
+ const MULTI_SELECT_SUFFIX_UNIT = " items";
5
+
6
+ /** Rough width estimate for Noto Sans Armenian at 14px (single-select value text). */
7
+ function estimateTextWidth(text: string, size = fontSize.md): number {
8
+ return text.length * size * 0.58;
9
+ }
10
+
11
+ export function formatMultiSelectDisplay(
12
+ labels: string[],
13
+ maxWidth: number,
14
+ textSize = fontSize.md,
15
+ ): string {
16
+ if (labels.length === 0) {
17
+ return "";
18
+ }
19
+
20
+ if (labels.length === 1) {
21
+ return labels[0];
22
+ }
23
+
24
+ const fits = (text: string) => estimateTextWidth(text, textSize) <= maxWidth;
25
+ const fullText = labels.join(", ");
26
+
27
+ if (fits(fullText)) {
28
+ return fullText;
29
+ }
30
+
31
+ for (let visibleCount = labels.length - 1; visibleCount >= 1; visibleCount -= 1) {
32
+ const hiddenCount = labels.length - visibleCount;
33
+ const truncatedText = `${labels.slice(0, visibleCount).join(", ")}${MULTI_SELECT_SUFFIX}${hiddenCount}${MULTI_SELECT_SUFFIX_UNIT}`;
34
+
35
+ if (fits(truncatedText)) {
36
+ return truncatedText;
37
+ }
38
+ }
39
+
40
+ return `... + ${labels.length}${MULTI_SELECT_SUFFIX_UNIT}`;
41
+ }
42
+
43
+ export function resolveSelectedLabels(
44
+ options: { value: string; label: string }[],
45
+ selectedValues: string[],
46
+ ): string[] {
47
+ if (selectedValues.length === 0) {
48
+ return [];
49
+ }
50
+
51
+ const labelByValue = new Map(options.map((option) => [option.value, option.label]));
52
+
53
+ return selectedValues
54
+ .map((value) => labelByValue.get(value))
55
+ .filter((label): label is string => Boolean(label));
56
+ }
@@ -0,0 +1,43 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+
3
+ import { WebLayoutCanvas } from '../layout/story-helpers';
4
+ import { Badge } from './Badge';
5
+
6
+ const meta = {
7
+ title: 'Web Components/Badge',
8
+ component: Badge,
9
+ parameters: {
10
+ layout: 'padded',
11
+ webLayout: true,
12
+ },
13
+ } satisfies Meta<typeof Badge>;
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ export const AllVariants: Story = {
19
+ args: {
20
+ children: 'Badge',
21
+ },
22
+ render: () => (
23
+ <WebLayoutCanvas>
24
+ <div className="flex w-full max-w-sm flex-col gap-0 divide-y divide-storm-gray-50 rounded-xl border border-storm-gray-50 bg-white">
25
+ <div className="px-4 py-4">
26
+ <Badge variant="info">Սպասում է հաստատման</Badge>
27
+ </div>
28
+ <div className="px-4 py-4">
29
+ <Badge variant="danger">Մերժված</Badge>
30
+ </div>
31
+ <div className="px-4 py-4">
32
+ <Badge variant="warning">Սպասում է վճարման</Badge>
33
+ </div>
34
+ <div className="px-4 py-4">
35
+ <Badge variant="success">Հաստատված</Badge>
36
+ </div>
37
+ <div className="px-4 py-4">
38
+ <Badge variant="neutral">Կասեցված</Badge>
39
+ </div>
40
+ </div>
41
+ </WebLayoutCanvas>
42
+ ),
43
+ };
@@ -0,0 +1,35 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ import { cn } from '../utils/cn';
4
+
5
+ export type BadgeVariant = 'info' | 'danger' | 'warning' | 'success' | 'neutral';
6
+
7
+ const badgeVariantClasses: Record<BadgeVariant, string> = {
8
+ info: 'bg-[#0F52C71A] text-[#0F52C7]',
9
+ danger: 'bg-[#FFF5F6] text-[#AE0011]',
10
+ warning: 'bg-[#F2A8001A] text-[#F2A800]',
11
+ success: 'bg-[#F0FBF5] text-[#279D54]',
12
+ neutral: 'bg-[#ECEEF0] text-[#455765]',
13
+ };
14
+
15
+ export type BadgeProps = {
16
+ children: ReactNode;
17
+ variant?: BadgeVariant;
18
+ icon?: ReactNode;
19
+ className?: string;
20
+ };
21
+
22
+ export function Badge({ children, variant = 'neutral', icon, className }: BadgeProps) {
23
+ return (
24
+ <span
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',
27
+ badgeVariantClasses[variant],
28
+ className,
29
+ )}
30
+ >
31
+ {icon ? <span className="inline-flex shrink-0 items-center justify-center">{icon}</span> : null}
32
+ {children}
33
+ </span>
34
+ );
35
+ }
@@ -0,0 +1,78 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { fn } from '@storybook/test';
3
+ import { useState } from 'react';
4
+
5
+ import { WebLayoutCanvas } from '../layout/story-helpers';
6
+ import { Pagination, type PaginationProps } from './Pagination';
7
+
8
+ const armenianLabels: PaginationProps['labels'] = {
9
+ previousPage: 'Նախորդ էջ',
10
+ nextPage: 'Հաջորդ էջ',
11
+ page: (pageNumber) => `Էջ ${pageNumber}`,
12
+ showing: ({ start, end, total }) => `Ցուցադրված է ${start}-${end}-ը ${total}-ից`,
13
+ };
14
+
15
+ const meta = {
16
+ title: 'Web Components/Pagination',
17
+ component: Pagination,
18
+ parameters: {
19
+ layout: 'fullscreen',
20
+ webLayout: true,
21
+ },
22
+ args: {
23
+ page: 1,
24
+ pageSize: 4,
25
+ total: 4,
26
+ onPageChange: fn(),
27
+ labels: armenianLabels,
28
+ },
29
+ } satisfies Meta<typeof Pagination>;
30
+
31
+ export default meta;
32
+ type Story = StoryObj<typeof meta>;
33
+
34
+ export const Default: Story = {
35
+ render: (args) => (
36
+ <WebLayoutCanvas>
37
+ <div className="rounded-2xl border border-storm-gray-50 bg-white p-5">
38
+ <Pagination {...args} />
39
+ </div>
40
+ </WebLayoutCanvas>
41
+ ),
42
+ };
43
+
44
+ function InteractiveStory(args: PaginationProps) {
45
+ const [page, setPage] = useState(args.page);
46
+
47
+ return (
48
+ <WebLayoutCanvas>
49
+ <div className="rounded-2xl border border-storm-gray-50 bg-white p-5">
50
+ <Pagination
51
+ {...args}
52
+ page={page}
53
+ onPageChange={setPage}
54
+ />
55
+ </div>
56
+ </WebLayoutCanvas>
57
+ );
58
+ }
59
+
60
+ export const Interactive: Story = {
61
+ args: {
62
+ page: 1,
63
+ pageSize: 4,
64
+ total: 12,
65
+ labels: armenianLabels,
66
+ },
67
+ render: (args) => <InteractiveStory {...args} />,
68
+ };
69
+
70
+ export const SinglePage: Story = {
71
+ args: {
72
+ page: 1,
73
+ pageSize: 4,
74
+ total: 4,
75
+ labels: armenianLabels,
76
+ },
77
+ render: (args) => <InteractiveStory {...args} />,
78
+ };
@@ -0,0 +1,153 @@
1
+ import { ChevronLeft, ChevronRight } from 'lucide-react';
2
+
3
+ import { cn } from '../utils/cn';
4
+ import { useDataTableClassNames } from './table/data-table-class-names';
5
+ import {
6
+ getPaginationRange,
7
+ getTotalPages,
8
+ getVisiblePageNumbers,
9
+ } from './pagination-utils';
10
+
11
+ export type PaginationLabels = {
12
+ previousPage: string;
13
+ nextPage: string;
14
+ page: (pageNumber: number) => string;
15
+ showing: (range: { start: number; end: number; total: number }) => string;
16
+ };
17
+
18
+ const defaultLabels: PaginationLabels = {
19
+ previousPage: 'Previous page',
20
+ nextPage: 'Next page',
21
+ page: (pageNumber) => `Page ${pageNumber}`,
22
+ showing: ({ start, end, total }) => `Showing ${start}-${end} of ${total}`,
23
+ };
24
+
25
+ const paginationControlClassName =
26
+ 'inline-flex size-7 items-center justify-center rounded-lg border border-storm-gray-50 p-1 text-xs font-medium leading-none text-storm-gray-500 transition-colors';
27
+
28
+ const paginationHoverClassName = 'cursor-pointer hover:bg-storm-gray-0';
29
+
30
+ export type PaginationVariant = 'standalone' | 'attached';
31
+
32
+ export type PaginationProps = {
33
+ page: number;
34
+ pageSize: number;
35
+ total: number;
36
+ onPageChange: (page: number) => void;
37
+ labels?: Partial<PaginationLabels>;
38
+ maxPageButtons?: number;
39
+ /**
40
+ * `attached` — below a table; no top radius (footer shell handles bottom radius).
41
+ * `standalone` — full `rounded-xl` shell.
42
+ */
43
+ variant?: PaginationVariant;
44
+ className?: string;
45
+ hideOnSinglePage?: boolean;
46
+ };
47
+
48
+ export function Pagination({
49
+ page,
50
+ pageSize,
51
+ total,
52
+ onPageChange,
53
+ labels,
54
+ maxPageButtons = 3,
55
+ variant = 'standalone',
56
+ className,
57
+ hideOnSinglePage = false,
58
+ }: PaginationProps) {
59
+ const mergedLabels = { ...defaultLabels, ...labels };
60
+ const dataTableClassNames = useDataTableClassNames();
61
+ const totalPages = getTotalPages(total, pageSize);
62
+ const safePage = Math.min(Math.max(page, 1), totalPages);
63
+ const { start, end } = getPaginationRange(safePage, pageSize, total);
64
+ const visiblePages = getVisiblePageNumbers(safePage, totalPages, maxPageButtons);
65
+ const canGoPrevious = safePage > 1;
66
+ const canGoNext = safePage < totalPages;
67
+
68
+ if (hideOnSinglePage && totalPages <= 1) {
69
+ return null;
70
+ }
71
+
72
+ return (
73
+ <div
74
+ className={cn(
75
+ 'flex flex-col gap-4 px-4 pb-4 pt-4 font-[family-name:var(--font-sans)] sm:flex-row sm:items-center sm:justify-between',
76
+ variant === 'standalone' && cn('rounded-xl border bg-storm-gray-0', 'border-storm-gray-50'),
77
+ dataTableClassNames.pagination,
78
+ className,
79
+ )}
80
+ >
81
+ <p className="text-xs font-medium leading-none text-storm-gray-200">
82
+ {mergedLabels.showing({ start, end, total })}
83
+ </p>
84
+
85
+ <nav
86
+ aria-label="Pagination"
87
+ className="flex items-center gap-2.5"
88
+ >
89
+ <button
90
+ type="button"
91
+ onClick={() => onPageChange(safePage - 1)}
92
+ disabled={!canGoPrevious}
93
+ aria-label={mergedLabels.previousPage}
94
+ className={cn(
95
+ paginationControlClassName,
96
+ canGoPrevious ? paginationHoverClassName : 'cursor-not-allowed opacity-40',
97
+ )}
98
+ >
99
+ <ChevronLeft
100
+ className="size-3.5 text-storm-gray-500"
101
+ strokeWidth={1.75}
102
+ aria-hidden
103
+ />
104
+ </button>
105
+
106
+ {visiblePages.map((pageNumber) => {
107
+ const isActive = pageNumber === safePage;
108
+
109
+ return (
110
+ <button
111
+ key={pageNumber}
112
+ type="button"
113
+ onClick={() => onPageChange(pageNumber)}
114
+ aria-label={mergedLabels.page(pageNumber)}
115
+ aria-current={isActive ? 'page' : undefined}
116
+ className={cn(
117
+ paginationControlClassName,
118
+ isActive
119
+ ? 'border-storm-gray-50 font-bold text-storm-gray-900'
120
+ : paginationHoverClassName,
121
+ )}
122
+ >
123
+ {pageNumber}
124
+ </button>
125
+ );
126
+ })}
127
+
128
+ <button
129
+ type="button"
130
+ onClick={() => onPageChange(safePage + 1)}
131
+ disabled={!canGoNext}
132
+ aria-label={mergedLabels.nextPage}
133
+ className={cn(
134
+ paginationControlClassName,
135
+ canGoNext ? paginationHoverClassName : 'cursor-not-allowed opacity-40',
136
+ )}
137
+ >
138
+ <ChevronRight
139
+ className="size-3.5 text-storm-gray-500"
140
+ strokeWidth={1.75}
141
+ aria-hidden
142
+ />
143
+ </button>
144
+ </nav>
145
+ </div>
146
+ );
147
+ }
148
+
149
+ export {
150
+ getPaginationRange,
151
+ getTotalPages,
152
+ getVisiblePageNumbers,
153
+ } from './pagination-utils';
@@ -0,0 +1,20 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ import { Badge, type BadgeVariant } from './Badge';
4
+
5
+ export type StatusBadgeTone = BadgeVariant;
6
+
7
+ export type StatusBadgeProps = {
8
+ children: ReactNode;
9
+ tone?: StatusBadgeTone;
10
+ icon?: ReactNode;
11
+ className?: string;
12
+ };
13
+
14
+ export function StatusBadge({ children, tone = 'neutral', icon, className }: StatusBadgeProps) {
15
+ return (
16
+ <Badge variant={tone} icon={icon} className={className}>
17
+ {children}
18
+ </Badge>
19
+ );
20
+ }