@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.
- package/dist/{chunk-C7GHBVMM.js → chunk-66WR57JJ.js} +53 -10
- package/dist/chunk-66WR57JJ.js.map +1 -0
- package/dist/index.cjs +2297 -391
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +144 -2
- package/dist/index.d.ts +144 -2
- package/dist/index.js +1978 -96
- package/dist/index.js.map +1 -1
- package/dist/web/index.cjs +30411 -759
- package/dist/web/index.cjs.map +1 -1
- package/dist/web/index.css +78 -0
- package/dist/web/index.css.map +1 -0
- package/dist/web/index.d.cts +267 -6
- package/dist/web/index.d.ts +267 -6
- package/dist/web/index.js +30179 -601
- package/dist/web/index.js.map +1 -1
- package/package.json +5 -4
- package/src/components/Calendar.stories.tsx +337 -0
- package/src/components/Calendar.tsx +441 -0
- package/src/components/DatePicker.stories.tsx +219 -0
- package/src/components/DatePicker.tsx +366 -0
- package/src/components/Input.tsx +5 -1
- package/src/components/Select.stories.tsx +134 -0
- package/src/components/Select.tsx +462 -0
- package/src/css.d.ts +1 -0
- package/src/icons/CalendarIcon.tsx +28 -0
- package/src/icons/CalendarIcon.web.tsx +34 -0
- package/src/icons/ChevronDownIcon.tsx +11 -4
- package/src/icons/ChevronDownIcon.web.tsx +6 -4
- package/src/icons/ChevronNavIcons.tsx +44 -0
- package/src/icons/ChevronNavIcons.web.tsx +57 -0
- package/src/icons/LockIcon.tsx +30 -0
- package/src/icons/LockIcon.web.tsx +35 -0
- package/src/icons/calendarIconPaths.ts +2 -0
- package/src/icons/chevronDownPath.ts +1 -0
- package/src/icons/chevronNavPaths.ts +2 -0
- package/src/icons/lockIconPaths.ts +2 -0
- package/src/index.ts +43 -0
- package/src/theme/calendar.ts +357 -0
- package/src/theme/input.ts +16 -6
- package/src/theme/select.ts +249 -0
- package/src/utils/calendarDays.ts +273 -0
- package/src/utils/calendarLocaleContext.tsx +95 -0
- package/src/utils/calendarLocalization.ts +187 -0
- package/src/utils/countryDropdownScrollStyles.ts +53 -13
- package/src/utils/formatMultiSelectDisplay.ts +56 -0
- package/src/web/components/Badge.stories.tsx +43 -0
- package/src/web/components/Badge.tsx +35 -0
- package/src/web/components/Pagination.stories.tsx +78 -0
- package/src/web/components/Pagination.tsx +153 -0
- package/src/web/components/StatusBadge.tsx +20 -0
- package/src/web/components/Table.stories.tsx +415 -0
- package/src/web/components/Table.tsx +3 -0
- package/src/web/components/TableActionButton.tsx +67 -0
- package/src/web/components/TableActionsMenu.tsx +208 -0
- package/src/web/components/TableIconText.tsx +23 -0
- package/src/web/components/TableRowStatusActions.tsx +33 -0
- package/src/web/components/TableStatusActionsCell.tsx +29 -0
- package/src/web/components/pagination-utils.ts +39 -0
- package/src/web/components/table/DataTable.tsx +123 -0
- package/src/web/components/table/TablePrimitives.tsx +214 -0
- package/src/web/components/table/TableScrollArea.tsx +51 -0
- package/src/web/components/table/constants.ts +59 -0
- package/src/web/components/table/data-table-class-names.ts +26 -0
- package/src/web/components/table/index.ts +44 -0
- package/src/web/components/table/table-row-context.tsx +19 -0
- package/src/web/components/table/use-horizontal-scroll-state.ts +72 -0
- package/src/web/components/table-scroll.css +81 -0
- package/src/web/hooks/useOnClickOutside.ts +20 -5
- package/src/web/icons/TableMenuActivateIcon.tsx +27 -0
- package/src/web/icons/TableMenuCheckIcon.tsx +27 -0
- package/src/web/icons/TableMenuCloseIcon.tsx +27 -0
- package/src/web/icons/TableMenuEditIcon.tsx +27 -0
- package/src/web/icons/TableMenuInfoIcon.tsx +27 -0
- package/src/web/icons/TableMenuRefreshIcon.tsx +27 -0
- package/src/web/icons/TableMenuSuspendIcon.tsx +34 -0
- package/src/web/icons/TableMenuTrashIcon.tsx +27 -0
- package/src/web/icons/TableMoreIcon.tsx +41 -0
- package/src/web/icons/TableSortIcon.tsx +37 -0
- package/src/web/index.ts +74 -0
- package/src/web/layout/AppHeader.stories.tsx +2 -0
- package/src/web/layout/AppHeader.tsx +18 -14
- package/src/web/layout/DashboardLayout.stories.tsx +1 -0
- package/src/web/layout/DashboardLayout.tsx +8 -4
- package/src/web/layout/story-helpers.tsx +10 -2
- package/dist/chunk-C7GHBVMM.js.map +0 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
export type CalendarLocale = "hy-AM" | "en-US" | "ru-RU";
|
|
2
|
+
|
|
3
|
+
export type CalendarTranslationKey =
|
|
4
|
+
| "datePlaceholder"
|
|
5
|
+
| "rangePlaceholder"
|
|
6
|
+
| "previousMonth"
|
|
7
|
+
| "nextMonth"
|
|
8
|
+
| "previousYear"
|
|
9
|
+
| "nextYear"
|
|
10
|
+
| "previousYears"
|
|
11
|
+
| "nextYears"
|
|
12
|
+
| "selectMonth"
|
|
13
|
+
| "selectYear"
|
|
14
|
+
| "closeCalendar";
|
|
15
|
+
|
|
16
|
+
export type CalendarTranslations = Record<CalendarTranslationKey, string> & {
|
|
17
|
+
/** Weekday labels in Monday-first order. */
|
|
18
|
+
weekdays: string[];
|
|
19
|
+
/** Short month labels used in compact calendar headers and month picker cells. */
|
|
20
|
+
monthsShort: string[];
|
|
21
|
+
/** Full month labels available for consumers that need long-form display. */
|
|
22
|
+
monthsLong: string[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const calendarTranslations: Record<CalendarLocale, CalendarTranslations> = {
|
|
26
|
+
"hy-AM": {
|
|
27
|
+
weekdays: ["ԵՐ", "ԵՔ", "ՉՐ", "ՀՆ", "ՈՒՐ", "ՇԲ", "ԿՐ"],
|
|
28
|
+
monthsShort: [
|
|
29
|
+
"Հնվ",
|
|
30
|
+
"Փտվ",
|
|
31
|
+
"Մրտ",
|
|
32
|
+
"Ապր",
|
|
33
|
+
"Մյս",
|
|
34
|
+
"Հուն",
|
|
35
|
+
"Հուլ",
|
|
36
|
+
"Օգս",
|
|
37
|
+
"Սեպ",
|
|
38
|
+
"Հոկ",
|
|
39
|
+
"Նոյ",
|
|
40
|
+
"Դեկ",
|
|
41
|
+
],
|
|
42
|
+
monthsLong: [
|
|
43
|
+
"Հունվար",
|
|
44
|
+
"Փետրվար",
|
|
45
|
+
"Մարտ",
|
|
46
|
+
"Ապրիլ",
|
|
47
|
+
"Մայիս",
|
|
48
|
+
"Հունիս",
|
|
49
|
+
"Հուլիս",
|
|
50
|
+
"Օգոստոս",
|
|
51
|
+
"Սեպտեմբեր",
|
|
52
|
+
"Հոկտեմբեր",
|
|
53
|
+
"Նոյեմբեր",
|
|
54
|
+
"Դեկտեմբեր",
|
|
55
|
+
],
|
|
56
|
+
datePlaceholder: "Ընտրել ամսաթիվ",
|
|
57
|
+
rangePlaceholder: "Ընտրել ժամանակահատված",
|
|
58
|
+
previousMonth: "Նախորդ ամիս",
|
|
59
|
+
nextMonth: "Հաջորդ ամիս",
|
|
60
|
+
previousYear: "Նախորդ տարի",
|
|
61
|
+
nextYear: "Հաջորդ տարի",
|
|
62
|
+
previousYears: "Նախորդ տարիներ",
|
|
63
|
+
nextYears: "Հաջորդ տարիներ",
|
|
64
|
+
selectMonth: "Ընտրել ամիս",
|
|
65
|
+
selectYear: "Ընտրել տարի",
|
|
66
|
+
closeCalendar: "Փակել օրացույցը",
|
|
67
|
+
},
|
|
68
|
+
"en-US": {
|
|
69
|
+
weekdays: ["MO", "TU", "WE", "TH", "FR", "SA", "SU"],
|
|
70
|
+
monthsShort: [
|
|
71
|
+
"Jan",
|
|
72
|
+
"Feb",
|
|
73
|
+
"Mar",
|
|
74
|
+
"Apr",
|
|
75
|
+
"May",
|
|
76
|
+
"Jun",
|
|
77
|
+
"Jul",
|
|
78
|
+
"Aug",
|
|
79
|
+
"Sep",
|
|
80
|
+
"Oct",
|
|
81
|
+
"Nov",
|
|
82
|
+
"Dec",
|
|
83
|
+
],
|
|
84
|
+
monthsLong: [
|
|
85
|
+
"January",
|
|
86
|
+
"February",
|
|
87
|
+
"March",
|
|
88
|
+
"April",
|
|
89
|
+
"May",
|
|
90
|
+
"June",
|
|
91
|
+
"July",
|
|
92
|
+
"August",
|
|
93
|
+
"September",
|
|
94
|
+
"October",
|
|
95
|
+
"November",
|
|
96
|
+
"December",
|
|
97
|
+
],
|
|
98
|
+
datePlaceholder: "Select date",
|
|
99
|
+
rangePlaceholder: "Select date range",
|
|
100
|
+
previousMonth: "Previous month",
|
|
101
|
+
nextMonth: "Next month",
|
|
102
|
+
previousYear: "Previous year",
|
|
103
|
+
nextYear: "Next year",
|
|
104
|
+
previousYears: "Previous years",
|
|
105
|
+
nextYears: "Next years",
|
|
106
|
+
selectMonth: "Select month",
|
|
107
|
+
selectYear: "Select year",
|
|
108
|
+
closeCalendar: "Close calendar",
|
|
109
|
+
},
|
|
110
|
+
"ru-RU": {
|
|
111
|
+
weekdays: ["ПН", "ВТ", "СР", "ЧТ", "ПТ", "СБ", "ВС"],
|
|
112
|
+
monthsShort: [
|
|
113
|
+
"Янв",
|
|
114
|
+
"Фев",
|
|
115
|
+
"Мар",
|
|
116
|
+
"Апр",
|
|
117
|
+
"Май",
|
|
118
|
+
"Июн",
|
|
119
|
+
"Июл",
|
|
120
|
+
"Авг",
|
|
121
|
+
"Сен",
|
|
122
|
+
"Окт",
|
|
123
|
+
"Ноя",
|
|
124
|
+
"Дек",
|
|
125
|
+
],
|
|
126
|
+
monthsLong: [
|
|
127
|
+
"Январь",
|
|
128
|
+
"Февраль",
|
|
129
|
+
"Март",
|
|
130
|
+
"Апрель",
|
|
131
|
+
"Май",
|
|
132
|
+
"Июнь",
|
|
133
|
+
"Июль",
|
|
134
|
+
"Август",
|
|
135
|
+
"Сентябрь",
|
|
136
|
+
"Октябрь",
|
|
137
|
+
"Ноябрь",
|
|
138
|
+
"Декабрь",
|
|
139
|
+
],
|
|
140
|
+
datePlaceholder: "Выбрать дату",
|
|
141
|
+
rangePlaceholder: "Выбрать период",
|
|
142
|
+
previousMonth: "Предыдущий месяц",
|
|
143
|
+
nextMonth: "Следующий месяц",
|
|
144
|
+
previousYear: "Предыдущий год",
|
|
145
|
+
nextYear: "Следующий год",
|
|
146
|
+
previousYears: "Предыдущие годы",
|
|
147
|
+
nextYears: "Следующие годы",
|
|
148
|
+
selectMonth: "Выбрать месяц",
|
|
149
|
+
selectYear: "Выбрать год",
|
|
150
|
+
closeCalendar: "Закрыть календарь",
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export function resolveCalendarLocale(locale: string): CalendarLocale {
|
|
155
|
+
const normalizedLocale = locale.toLowerCase();
|
|
156
|
+
|
|
157
|
+
if (normalizedLocale.startsWith("hy")) {
|
|
158
|
+
return "hy-AM";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (normalizedLocale.startsWith("ru")) {
|
|
162
|
+
return "ru-RU";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return "en-US";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function getCalendarTranslations(locale: string): CalendarTranslations {
|
|
169
|
+
return calendarTranslations[resolveCalendarLocale(locale)];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function resolveWeekdayLabels(locale: string): string[] {
|
|
173
|
+
return getCalendarTranslations(locale).weekdays;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function getCalendarMonthLabels(
|
|
177
|
+
locale: string,
|
|
178
|
+
width: "short" | "long" = "short",
|
|
179
|
+
): string[] {
|
|
180
|
+
const translations = getCalendarTranslations(locale);
|
|
181
|
+
|
|
182
|
+
return width === "short" ? translations.monthsShort : translations.monthsLong;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function formatLocalizedCalendarMonthYear(date: Date, locale: string): string {
|
|
186
|
+
return `${getCalendarMonthLabels(locale)[date.getMonth()]} ${date.getFullYear()}`;
|
|
187
|
+
}
|
|
@@ -2,51 +2,91 @@ import { Platform } from "react-native";
|
|
|
2
2
|
import { colors } from "../theme/tokens";
|
|
3
3
|
|
|
4
4
|
export const COUNTRY_DROPDOWN_SCROLL_CLASS = "hp-country-dropdown-scroll";
|
|
5
|
+
export const SELECT_DROPDOWN_SCROLL_CLASS = "hp-select-dropdown-scroll";
|
|
5
6
|
|
|
6
|
-
const STYLE_ID = "hp-
|
|
7
|
+
const STYLE_ID = "hp-dropdown-scroll-styles";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
function createDropdownScrollCss(className: string): string {
|
|
10
|
+
return `
|
|
11
|
+
.${className} {
|
|
10
12
|
scrollbar-width: thin;
|
|
11
13
|
scrollbar-color: ${colors.stormGray300} transparent;
|
|
12
14
|
overflow-y: auto;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
.${
|
|
17
|
+
.${className}::-webkit-scrollbar {
|
|
16
18
|
width: 4px;
|
|
19
|
+
-webkit-appearance: none;
|
|
20
|
+
appearance: none;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
.${
|
|
23
|
+
.${className}::-webkit-scrollbar-corner {
|
|
20
24
|
background: transparent;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
.${
|
|
24
|
-
.${
|
|
25
|
-
.${
|
|
26
|
-
.${
|
|
27
|
+
.${className}::-webkit-scrollbar-button,
|
|
28
|
+
.${className}::-webkit-scrollbar-button:single-button,
|
|
29
|
+
.${className}::-webkit-scrollbar-button:double-button,
|
|
30
|
+
.${className}::-webkit-scrollbar-button:horizontal:decrement,
|
|
31
|
+
.${className}::-webkit-scrollbar-button:horizontal:increment,
|
|
32
|
+
.${className}::-webkit-scrollbar-button:horizontal:start:decrement,
|
|
33
|
+
.${className}::-webkit-scrollbar-button:horizontal:end:increment,
|
|
34
|
+
.${className}::-webkit-scrollbar-button:vertical:decrement,
|
|
35
|
+
.${className}::-webkit-scrollbar-button:vertical:increment,
|
|
36
|
+
.${className}::-webkit-scrollbar-button:vertical:start:decrement,
|
|
37
|
+
.${className}::-webkit-scrollbar-button:vertical:end:increment,
|
|
38
|
+
.${className}::-webkit-scrollbar-button:start:decrement,
|
|
39
|
+
.${className}::-webkit-scrollbar-button:end:increment,
|
|
40
|
+
.${className}::-webkit-scrollbar-button:decrement,
|
|
41
|
+
.${className}::-webkit-scrollbar-button:increment {
|
|
27
42
|
display: none;
|
|
28
43
|
width: 0;
|
|
29
44
|
height: 0;
|
|
45
|
+
max-width: 0;
|
|
46
|
+
max-height: 0;
|
|
47
|
+
min-width: 0;
|
|
48
|
+
min-height: 0;
|
|
49
|
+
padding: 0;
|
|
50
|
+
margin: 0;
|
|
51
|
+
border: 0;
|
|
52
|
+
background: transparent;
|
|
30
53
|
-webkit-appearance: none;
|
|
31
54
|
appearance: none;
|
|
32
55
|
}
|
|
33
56
|
|
|
34
|
-
.${
|
|
57
|
+
.${className}::-webkit-scrollbar-track {
|
|
35
58
|
background: transparent;
|
|
36
59
|
}
|
|
37
60
|
|
|
38
|
-
.${
|
|
61
|
+
.${className}::-webkit-scrollbar-thumb {
|
|
39
62
|
background-color: ${colors.stormGray300};
|
|
40
63
|
border-radius: 9999px;
|
|
41
64
|
}
|
|
42
65
|
`;
|
|
66
|
+
}
|
|
43
67
|
|
|
44
|
-
export
|
|
68
|
+
export const dropdownScrollCss = [
|
|
69
|
+
createDropdownScrollCss(COUNTRY_DROPDOWN_SCROLL_CLASS),
|
|
70
|
+
createDropdownScrollCss(SELECT_DROPDOWN_SCROLL_CLASS),
|
|
71
|
+
].join("\n");
|
|
72
|
+
|
|
73
|
+
/** @deprecated Use `dropdownScrollCss` */
|
|
74
|
+
export const countryDropdownScrollCss = dropdownScrollCss;
|
|
75
|
+
|
|
76
|
+
export function ensureDropdownScrollStyles(): void {
|
|
45
77
|
if (Platform.OS !== "web" || typeof document === "undefined") return;
|
|
46
78
|
if (document.getElementById(STYLE_ID)) return;
|
|
47
79
|
|
|
48
80
|
const style = document.createElement("style");
|
|
49
81
|
style.id = STYLE_ID;
|
|
50
|
-
style.textContent =
|
|
82
|
+
style.textContent = dropdownScrollCss;
|
|
51
83
|
document.head.appendChild(style);
|
|
52
84
|
}
|
|
85
|
+
|
|
86
|
+
export function ensureCountryDropdownScrollStyles(): void {
|
|
87
|
+
ensureDropdownScrollStyles();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function ensureSelectDropdownScrollStyles(): void {
|
|
91
|
+
ensureDropdownScrollStyles();
|
|
92
|
+
}
|
|
@@ -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
|
+
}
|