@exotic-holidays/ui 0.1.0

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 (38) hide show
  1. package/README.md +46 -0
  2. package/package.json +42 -0
  3. package/src/base-components/Accordion.tsx +561 -0
  4. package/src/base-components/Badge.tsx +191 -0
  5. package/src/base-components/Button.tsx +331 -0
  6. package/src/base-components/ButtonGroup.tsx +149 -0
  7. package/src/base-components/Card.tsx +250 -0
  8. package/src/base-components/Checkbox.tsx +49 -0
  9. package/src/base-components/ChipInput.tsx +208 -0
  10. package/src/base-components/CommonButton.tsx +33 -0
  11. package/src/base-components/DataTable.tsx +82 -0
  12. package/src/base-components/Divider.tsx +82 -0
  13. package/src/base-components/Dropdown.tsx +85 -0
  14. package/src/base-components/EmptyState.tsx +18 -0
  15. package/src/base-components/FilterPopover.tsx +50 -0
  16. package/src/base-components/Input.tsx +60 -0
  17. package/src/base-components/Modal.tsx +107 -0
  18. package/src/base-components/OtpVerificationModal.tsx +251 -0
  19. package/src/base-components/Pagination.tsx +51 -0
  20. package/src/base-components/PhoneInput.tsx +142 -0
  21. package/src/base-components/PopConfirm.tsx +350 -0
  22. package/src/base-components/SearchPopover.tsx +70 -0
  23. package/src/base-components/SearchableSelect.tsx +734 -0
  24. package/src/base-components/Select.tsx +49 -0
  25. package/src/base-components/Table.tsx +78 -0
  26. package/src/base-components/Textarea.tsx +45 -0
  27. package/src/base-components/ThemeProvider.tsx +92 -0
  28. package/src/base-components/Toaster.tsx +198 -0
  29. package/src/base-components/index.ts +32 -0
  30. package/src/components/DashboardLayout.tsx +326 -0
  31. package/src/components/ListPage.tsx +140 -0
  32. package/src/components/QuickAccess.tsx +118 -0
  33. package/src/components/UserMenu.tsx +138 -0
  34. package/src/helpers/bem.ts +13 -0
  35. package/src/helpers/cn.ts +9 -0
  36. package/src/index.ts +16 -0
  37. package/src/theme.css +285 -0
  38. package/tsconfig.json +11 -0
@@ -0,0 +1,138 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useRef, useState } from 'react';
4
+ import { LogOut, Sun, Moon, Monitor, ChevronUp } from 'lucide-react';
5
+ import { useTheme, type ThemePreference } from '../base-components/ThemeProvider';
6
+
7
+ const THEME_OPTIONS: { value: ThemePreference; label: string; icon: React.ReactNode }[] = [
8
+ { value: 'light', label: 'Light', icon: <Sun size={14} /> },
9
+ { value: 'dark', label: 'Dark', icon: <Moon size={14} /> },
10
+ { value: 'system', label: 'System', icon: <Monitor size={14} /> },
11
+ ];
12
+
13
+ export interface UserMenuProps {
14
+ name: string;
15
+ role: string;
16
+ initials: string;
17
+ collapsed?: boolean;
18
+ headerMode?: boolean;
19
+ onLogout: () => void;
20
+ }
21
+
22
+ /**
23
+ * Sidebar-footer account menu: the trigger row opens a popover with the user
24
+ * identity, a Light/Dark/System theme selector and Sign Out. Prop-driven and
25
+ * auth-agnostic — every portal's DashboardLayout passes its own user + logout.
26
+ */
27
+ export function UserMenu({ name, role, initials, collapsed = false, headerMode = false, onLogout }: UserMenuProps) {
28
+ const [isOpen, setIsOpen] = useState(false);
29
+ const menuRef = useRef<HTMLDivElement>(null);
30
+ const { theme, setTheme } = useTheme();
31
+
32
+ useEffect(() => {
33
+ if (!isOpen) return;
34
+ const onClick = (e: MouseEvent) => {
35
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) setIsOpen(false);
36
+ };
37
+ const onKey = (e: KeyboardEvent) => {
38
+ if (e.key === 'Escape') setIsOpen(false);
39
+ };
40
+ document.addEventListener('mousedown', onClick);
41
+ document.addEventListener('keydown', onKey);
42
+ return () => {
43
+ document.removeEventListener('mousedown', onClick);
44
+ document.removeEventListener('keydown', onKey);
45
+ };
46
+ }, [isOpen]);
47
+
48
+ const avatar = (
49
+ <div className="w-9 h-9 rounded-full bg-primary-50 border border-primary-200 text-primary-700 flex items-center justify-center font-bold text-[0.7rem] select-none shrink-0">
50
+ {initials}
51
+ </div>
52
+ );
53
+
54
+ return (
55
+ <div ref={menuRef} className="relative">
56
+ {/* Trigger */}
57
+ <button
58
+ type="button"
59
+ onClick={() => setIsOpen((o) => !o)}
60
+ aria-expanded={isOpen}
61
+ aria-haspopup="true"
62
+ title={collapsed ? `${name} · ${role}` : undefined}
63
+ className={`w-full flex items-center rounded-[10px] transition-colors cursor-pointer hover:bg-surface-hover ${isOpen ? 'bg-surface-hover' : ''
64
+ } ${collapsed ? 'justify-center p-1' : 'gap-2.5 px-2 py-1'}`}
65
+ >
66
+ {avatar}
67
+ {!collapsed && (
68
+ <>
69
+ <div className="flex-1 min-w-0 text-left leading-tight">
70
+ <div className="text-[12px] font-semibold text-foreground-0 truncate">{name}</div>
71
+ <div className="text-[11px] text-foreground-subtle truncate">{role}</div>
72
+ </div>
73
+ <ChevronUp
74
+ size={14}
75
+ className={`text-foreground-subtle transition-transform duration-200 shrink-0 ${isOpen ? 'rotate-0' : 'rotate-180'}`}
76
+ />
77
+ </>
78
+ )}
79
+ </button>
80
+
81
+ {/* Popover */}
82
+ {isOpen && (
83
+ <div
84
+ className={`absolute z-[60] w-[288px] bg-surface-1 border border-border-subtle rounded-xl shadow-lg py-1.5 ${collapsed
85
+ ? 'bottom-0 left-full ml-2'
86
+ : headerMode
87
+ ? 'top-full right-0 mt-2'
88
+ : 'bottom-full left-0 mb-2'
89
+ }`}
90
+ >
91
+ <div className="px-3.5 py-2.5 border-b border-border-subtle">
92
+ <div className="text-[13px] font-semibold text-foreground-0 truncate">{name}</div>
93
+ <div className="text-[11px] text-foreground-subtle truncate">{role}</div>
94
+ </div>
95
+
96
+ {/* Theme selector */}
97
+ <div className="px-3.5 py-2.5">
98
+ <div className="text-[11px] font-medium text-foreground-subtle uppercase tracking-wider mb-2">Theme</div>
99
+ <div className="flex items-center bg-surface-0 rounded-lg p-1 gap-0.5">
100
+ {THEME_OPTIONS.map((option) => (
101
+ <button
102
+ key={option.value}
103
+ type="button"
104
+ onClick={() => setTheme(option.value)}
105
+ title={option.label}
106
+ className={`flex-1 flex items-center justify-center gap-1 py-1.5 px-1.5 rounded-md text-[11px] font-medium whitespace-nowrap transition-all duration-150 cursor-pointer ${theme === option.value
107
+ ? 'bg-surface-1 text-foreground-0 shadow-sm'
108
+ : 'text-foreground-muted hover:text-foreground-0'
109
+ }`}
110
+ >
111
+ {option.icon}
112
+ {option.label}
113
+ </button>
114
+ ))}
115
+ </div>
116
+ </div>
117
+
118
+ <div className="mx-3 border-t border-border-subtle" />
119
+
120
+ {/* Sign out */}
121
+ <div className="px-1.5 py-1.5">
122
+ <button
123
+ type="button"
124
+ onClick={() => {
125
+ setIsOpen(false);
126
+ onLogout();
127
+ }}
128
+ className="w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-[13px] font-medium text-danger-700 hover:bg-danger-50 transition-colors cursor-pointer"
129
+ >
130
+ <LogOut size={15} />
131
+ Sign Out
132
+ </button>
133
+ </div>
134
+ </div>
135
+ )}
136
+ </div>
137
+ );
138
+ }
@@ -0,0 +1,13 @@
1
+ export function block(prefix: string, name: string): string {
2
+ return `${prefix}-${name}`;
3
+ }
4
+
5
+ export function element(prefix: string, blockName: string, elementName: string): string {
6
+ return `${prefix}-${blockName}__${elementName}`;
7
+ }
8
+
9
+ export function modifier(prefix: string, name: string, modifier: string | undefined | null | boolean): string {
10
+ if (!modifier) return "";
11
+ if (typeof modifier === "boolean") return modifier ? `${prefix}-${name}--active` : "";
12
+ return `${prefix}-${name}--${modifier}`;
13
+ }
@@ -0,0 +1,9 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ /**
5
+ * Utility function to merge tailwind classes safely.
6
+ */
7
+ export function cn(...inputs: ClassValue[]) {
8
+ return twMerge(clsx(inputs));
9
+ }
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @exotic-holidays/ui — EHI Travel Platform design system (AceUI).
3
+ *
4
+ * Usage in a portal app:
5
+ * import { Button, Input, Card, ListPage } from "@exotic-holidays/ui";
6
+ * // globals.css: @import "@exotic-holidays/ui/theme.css";
7
+ *
8
+ * Components are client components; consume via Next `transpilePackages: ["@exotic-holidays/ui"]`.
9
+ */
10
+ export * from "./base-components";
11
+ export * from "./components/ListPage";
12
+ export * from "./components/UserMenu";
13
+ export * from "./components/DashboardLayout";
14
+ export * from "./components/QuickAccess";
15
+ export { cn } from "./helpers/cn";
16
+ export * as bem from "./helpers/bem";
package/src/theme.css ADDED
@@ -0,0 +1,285 @@
1
+ /* AceUI default theme — import after tailwindcss. Customize via aceui.config.ts. */
2
+
3
+ @theme {
4
+ /* Typography — wired to the Next font variable set in layout.tsx */
5
+ --font-sans: var(--font-lexend), ui-sans-serif, system-ui, sans-serif;
6
+
7
+ --color-surface-0: #f3f7f5;
8
+ --color-surface-1: #ffffff;
9
+ --color-foreground-0: #1a1a2e;
10
+ --color-foreground-1: #3a3f52;
11
+ --color-foreground-muted: #6b7588;
12
+ --color-foreground-subtle: #9ba3b5;
13
+ --color-foreground-disabled: #b0b8c8;
14
+
15
+ --color-surface-hover: #f5f8f9;
16
+ --color-surface-subtle: #f8fbff;
17
+ --color-surface-hover-table: #fcfdff;
18
+
19
+ --color-border-subtle: #e8edf0;
20
+ --color-border-table: #eef2f8;
21
+
22
+ --color-danger-alt: #f0607a;
23
+
24
+ --color-divider: #e3e3e6;
25
+ --color-focus: #006fee;
26
+
27
+ /* Text */
28
+ --color-text-default: #18181b;
29
+ --color-text-muted: #71717a;
30
+ --color-text-subtle: #a1a1aa;
31
+ --color-text-disabled: #d4d4d8;
32
+
33
+ /* Border */
34
+ --color-border-default: #e3e3e6;
35
+ --color-border-muted: #ebebec;
36
+ --color-border-strong: #dcdcdf;
37
+ --border-width-1.5: 1.5px;
38
+
39
+ /* Base semantic (default, primary, secondary, success, warning, danger) */
40
+ --color-default: #d4d4d8;
41
+ --color-default-foreground: #e8edf0;
42
+ --color-default-50: #fafafa;
43
+ --color-default-100: #f2f2f3;
44
+ --color-default-200: #ebebec;
45
+ --color-default-300: #e3e3e6;
46
+ --color-default-400: #dcdcdf;
47
+ --color-default-500: #d4d4d8;
48
+ --color-default-600: #afafb2;
49
+ --color-default-700: #8a8a8c;
50
+ --color-default-800: #656567;
51
+ --color-default-900: #404041;
52
+
53
+ /* Brand primary — EHI "Tropical Luxury" red (#C22030, hover #a51825).
54
+ Solid CTA text is always white (--color-primary-foreground). */
55
+ --color-primary: #c22030;
56
+ --color-primary-foreground: #ffffff;
57
+ --color-primary-50: #fdeaec;
58
+ --color-primary-100: #f9cdd1;
59
+ --color-primary-200: #f29aa2;
60
+ --color-primary-300: #e76573;
61
+ --color-primary-400: #d83d4e;
62
+ --color-primary-500: #c22030;
63
+ --color-primary-600: #a51825;
64
+ --color-primary-700: #87131e;
65
+ --color-primary-800: #630e16;
66
+ --color-primary-900: #42090f;
67
+
68
+ /* Brand accent — warm amber/gold (#f0b83f) for ratings, draft badges,
69
+ highlights. Dark text on solid accent (--color-accent-foreground). */
70
+ --color-accent: #f0b83f;
71
+ --color-accent-foreground: #2a1c00;
72
+ --color-accent-50: #fef7e7;
73
+ --color-accent-100: #fcebbf;
74
+ --color-accent-200: #f9dd93;
75
+ --color-accent-300: #f5cd63;
76
+ --color-accent-400: #f2c34d;
77
+ --color-accent-500: #f0b83f;
78
+ --color-accent-600: #d19e2f;
79
+ --color-accent-700: #a87c22;
80
+ --color-accent-800: #7a5917;
81
+ --color-accent-900: #4d370d;
82
+
83
+ --color-secondary: #18181b;
84
+ --color-secondary-foreground: #ffffff;
85
+ --color-secondary-50: #09090b;
86
+ --color-secondary-100: #0d0d10;
87
+ --color-secondary-200: #111114;
88
+ --color-secondary-300: #141417;
89
+ --color-secondary-400: #16161a;
90
+ --color-secondary-500: #18181b;
91
+ --color-secondary-600: #3f3f46;
92
+ --color-secondary-700: #52525b;
93
+ --color-secondary-800: #71717a;
94
+ --color-secondary-900: #a1a1aa;
95
+
96
+ --color-success: #17c964;
97
+ --color-success-foreground: #000;
98
+ --color-success-50: #e2f8ec;
99
+ --color-success-100: #b9efd1;
100
+ --color-success-200: #91e5b5;
101
+ --color-success-300: #68dc9a;
102
+ --color-success-400: #40d27f;
103
+ --color-success-500: #17c964;
104
+ --color-success-600: #13a653;
105
+ --color-success-700: #0f8341;
106
+ --color-success-800: #0b5f30;
107
+ --color-success-900: #073c1e;
108
+
109
+ --color-warning: #f5a524;
110
+ --color-warning-foreground: #000;
111
+ --color-warning-50: #fef4e4;
112
+ --color-warning-100: #fce4bd;
113
+ --color-warning-200: #fad497;
114
+ --color-warning-300: #f9c571;
115
+ --color-warning-400: #f7b54a;
116
+ --color-warning-500: #f5a524;
117
+ --color-warning-600: #ca881e;
118
+ --color-warning-700: #9f6b17;
119
+ --color-warning-800: #744e11;
120
+ --color-warning-900: #4a320b;
121
+
122
+ --color-danger: #f31260;
123
+ --color-danger-foreground: #fff;
124
+ --color-danger-50: #fee1eb;
125
+ --color-danger-100: #fbb8cf;
126
+ --color-danger-200: #f98eb3;
127
+ --color-danger-300: #f76598;
128
+ --color-danger-400: #f53b7c;
129
+ --color-danger-500: #f31260;
130
+ --color-danger-600: #c80f4f;
131
+ --color-danger-700: #9e0c3e;
132
+ --color-danger-800: #73092e;
133
+ --color-danger-900: #49051d;
134
+ }
135
+
136
+
137
+ /* Component defaults */
138
+ :root {
139
+ --aceui-color-focus: var(--color-focus);
140
+ --aceui-disabled-opacity: 0.5;
141
+ --aceui-shadow-opacity-small: 0.2;
142
+ --aceui-shadow-opacity-medium: 0.4;
143
+ --aceui-shadow-opacity-large: 0.6;
144
+ --aceui-button-shadow-opacity: var(--aceui-shadow-opacity-medium);
145
+ --aceui-accordion-border-radius: 12px;
146
+ --aceui-badge-shadow-opacity: var(--aceui-shadow-opacity-medium);
147
+ }
148
+
149
+ /* Dark theme — scoped to .dark ancestor (applied on DashboardLayout wrapper) */
150
+ .dark {
151
+ /* Layout surfaces */
152
+ --color-surface-0: #1C1D25;
153
+ --color-surface-1: #24252E;
154
+ --color-surface-hover: #2A2B35;
155
+ --color-surface-subtle: #21222B;
156
+ --color-surface-hover-table: #23242D;
157
+
158
+ /* Layout foregrounds */
159
+ --color-foreground-0: #E8E8EC;
160
+ --color-foreground-1: #BBBCC0;
161
+ --color-foreground-muted: #9BA3B5;
162
+ --color-foreground-subtle: #6B7588;
163
+ --color-foreground-disabled: #4A4E5C;
164
+
165
+ /* Layout borders */
166
+ --color-border-subtle: #2F3040;
167
+ --color-border-table: #2A2B38;
168
+
169
+ /* Danger alt */
170
+ --color-danger-alt: #f0607a;
171
+
172
+ /* Layout */
173
+ --color-background: #1C1D25;
174
+ --color-foreground: #bbbcbe;
175
+ --color-divider: #3b3b42;
176
+ --color-focus: #338EF7;
177
+
178
+ /* Text */
179
+ --color-text-default: #bbbcbe;
180
+ --color-text-muted: #a1a1aa;
181
+ --color-text-subtle: #71717a;
182
+ --color-text-disabled: #52525b;
183
+
184
+ /* Border */
185
+ --color-border-default: #3b3b42;
186
+ --color-border-muted: #27272a;
187
+ --color-border-strong: #52525b;
188
+
189
+ /* Base semantic */
190
+ --color-default: #3F3F46;
191
+ --color-default-foreground: #E8E8E8;
192
+ --color-default-50: #0A0A0B;
193
+ --color-default-100: #18181B;
194
+ --color-default-200: #27272A;
195
+ --color-default-300: #333338;
196
+ --color-default-400: #3B3B42;
197
+ --color-default-500: #3F3F46;
198
+ --color-default-600: #52525B;
199
+ --color-default-700: #71717A;
200
+ --color-default-800: #A1A1AA;
201
+ --color-default-900: #D4D4D8;
202
+
203
+ /* Brand primary (dark) — red ramp inverted so 900 is lightest. */
204
+ --color-primary: #c22030;
205
+ --color-primary-foreground: #ffffff;
206
+ --color-primary-50: #2a0608;
207
+ --color-primary-100: #42090f;
208
+ --color-primary-200: #630e16;
209
+ --color-primary-300: #87131e;
210
+ --color-primary-400: #a51825;
211
+ --color-primary-500: #c22030;
212
+ --color-primary-600: #d6404e;
213
+ --color-primary-700: #e36974;
214
+ --color-primary-800: #ee98a0;
215
+ --color-primary-900: #f8cace;
216
+
217
+ /* Brand accent (dark) — amber ramp inverted so 900 is lightest. */
218
+ --color-accent: #f0b83f;
219
+ --color-accent-foreground: #2a1c00;
220
+ --color-accent-50: #2e2207;
221
+ --color-accent-100: #4d370d;
222
+ --color-accent-200: #7a5917;
223
+ --color-accent-300: #a87c22;
224
+ --color-accent-400: #d19e2f;
225
+ --color-accent-500: #f0b83f;
226
+ --color-accent-600: #f3c662;
227
+ --color-accent-700: #f6d588;
228
+ --color-accent-800: #f9e3ae;
229
+ --color-accent-900: #fcf1d6;
230
+
231
+ --color-secondary: #09090b;
232
+ --color-secondary-foreground: #e4e4e7;
233
+ --color-secondary-50: #09090b;
234
+ --color-secondary-100: #111113;
235
+ --color-secondary-200: #18181b;
236
+ --color-secondary-300: #27272a;
237
+ --color-secondary-400: #3f3f46;
238
+ --color-secondary-500: #09090b;
239
+ --color-secondary-600: #52525b;
240
+ --color-secondary-700: #71717a;
241
+ --color-secondary-800: #a1a1aa;
242
+ --color-secondary-900: #d4d4d8;
243
+
244
+ --color-success: #17C964;
245
+ --color-success-foreground: #000000;
246
+ --color-success-50: #052814;
247
+ --color-success-100: #094527;
248
+ --color-success-200: #0E623B;
249
+ --color-success-300: #13804E;
250
+ --color-success-400: #179D61;
251
+ --color-success-500: #17C964;
252
+ --color-success-600: #45D483;
253
+ --color-success-700: #73DFA2;
254
+ --color-success-800: #A1EAC1;
255
+ --color-success-900: #D0F5E0;
256
+
257
+ --color-warning: #F5A524;
258
+ --color-warning-foreground: #000000;
259
+ --color-warning-50: #312107;
260
+ --color-warning-100: #51380C;
261
+ --color-warning-200: #724F11;
262
+ --color-warning-300: #936616;
263
+ --color-warning-400: #B47D1B;
264
+ --color-warning-500: #F5A524;
265
+ --color-warning-600: #F7B750;
266
+ --color-warning-700: #F9C97C;
267
+ --color-warning-800: #FBDBA8;
268
+ --color-warning-900: #FDEDD3;
269
+
270
+ --color-danger: #F31260;
271
+ --color-danger-foreground: #e3e3e4;
272
+ --color-danger-50: #310413;
273
+ --color-danger-100: #510721;
274
+ --color-danger-200: #720A30;
275
+ --color-danger-300: #930D3E;
276
+ --color-danger-400: #B4104D;
277
+ --color-danger-500: #F31260;
278
+ --color-danger-600: #F54180;
279
+ --color-danger-700: #F771A0;
280
+ --color-danger-800: #F9A0BF;
281
+ --color-danger-900: #FBD0DF;
282
+
283
+ --aceui-button-shadow-opacity: var(--aceui-shadow-opacity-small);
284
+ --aceui-badge-shadow-opacity: var(--aceui-shadow-opacity-small);
285
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "jsx": "react-jsx",
5
+ "lib": ["dom", "dom.iterable", "esnext"],
6
+ "noEmit": true,
7
+ "baseUrl": "."
8
+ },
9
+ "include": ["src"],
10
+ "exclude": ["node_modules"]
11
+ }