@galyan/ui 0.0.1

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 (95) hide show
  1. package/.turbo/turbo-build.log +20 -0
  2. package/dist/index.cjs +2291 -0
  3. package/dist/index.css +2558 -0
  4. package/dist/index.d.cts +498 -0
  5. package/dist/index.d.ts +498 -0
  6. package/dist/index.js +2235 -0
  7. package/eslint.config.mjs +4 -0
  8. package/package.json +43 -0
  9. package/src/accordion/Accordion.tsx +88 -0
  10. package/src/accordion/accordion.css +78 -0
  11. package/src/accordion/index.ts +2 -0
  12. package/src/animatednumber/AnimatedNumber.tsx +71 -0
  13. package/src/animatednumber/index.ts +2 -0
  14. package/src/banner/Banner.tsx +84 -0
  15. package/src/banner/banner.css +56 -0
  16. package/src/banner/index.ts +2 -0
  17. package/src/breadcrumb/Breadcrumb.tsx +94 -0
  18. package/src/breadcrumb/breadcrumb.css +58 -0
  19. package/src/breadcrumb/index.ts +2 -0
  20. package/src/button/Button.tsx +98 -0
  21. package/src/button/button.css +190 -0
  22. package/src/button/index.ts +2 -0
  23. package/src/button.tsx +20 -0
  24. package/src/calendar/Calendar.tsx +190 -0
  25. package/src/calendar/calendar.css +142 -0
  26. package/src/calendar/index.ts +2 -0
  27. package/src/card/Card.tsx +88 -0
  28. package/src/card/card.css +115 -0
  29. package/src/card/index.ts +2 -0
  30. package/src/card.tsx +27 -0
  31. package/src/chart/Chart.tsx +186 -0
  32. package/src/chart/index.ts +2 -0
  33. package/src/checkbox/Checkbox.tsx +101 -0
  34. package/src/checkbox/checkbox.css +76 -0
  35. package/src/checkbox/index.ts +2 -0
  36. package/src/chips/Chips.tsx +146 -0
  37. package/src/chips/chips.css +90 -0
  38. package/src/chips/index.ts +2 -0
  39. package/src/code.tsx +11 -0
  40. package/src/datepicker/DatePicker.tsx +104 -0
  41. package/src/datepicker/datepicker.css +22 -0
  42. package/src/datepicker/index.ts +2 -0
  43. package/src/dragdrop/DragDrop.tsx +168 -0
  44. package/src/dragdrop/dragdrop.css +122 -0
  45. package/src/dragdrop/index.ts +2 -0
  46. package/src/dropdown/Dropdown.tsx +176 -0
  47. package/src/dropdown/dropdown.css +118 -0
  48. package/src/dropdown/index.ts +2 -0
  49. package/src/fileupload/FileUpload.tsx +144 -0
  50. package/src/fileupload/fileupload.css +118 -0
  51. package/src/fileupload/index.ts +2 -0
  52. package/src/index.ts +28 -0
  53. package/src/input/Input.tsx +121 -0
  54. package/src/input/index.ts +2 -0
  55. package/src/input/input.css +120 -0
  56. package/src/menu/Menu.tsx +118 -0
  57. package/src/menu/index.ts +2 -0
  58. package/src/menu/menu.css +117 -0
  59. package/src/modal/Modal.tsx +103 -0
  60. package/src/modal/index.ts +2 -0
  61. package/src/modal/modal.css +79 -0
  62. package/src/progressbar/ProgressBar.tsx +68 -0
  63. package/src/progressbar/index.ts +2 -0
  64. package/src/progressbar/progressbar.css +73 -0
  65. package/src/radiogroup/RadioGroup.tsx +73 -0
  66. package/src/radiogroup/index.ts +2 -0
  67. package/src/radiogroup/radiogroup.css +80 -0
  68. package/src/skeleton/Skeleton.tsx +61 -0
  69. package/src/skeleton/index.ts +2 -0
  70. package/src/skeleton/skeleton.css +46 -0
  71. package/src/spinner/Spinner.tsx +29 -0
  72. package/src/spinner/index.ts +2 -0
  73. package/src/spinner/spinner.css +39 -0
  74. package/src/stepper/Stepper.tsx +127 -0
  75. package/src/stepper/index.ts +2 -0
  76. package/src/stepper/stepper.css +141 -0
  77. package/src/tab/Tabs.tsx +71 -0
  78. package/src/tab/index.ts +2 -0
  79. package/src/tab/tab.css +92 -0
  80. package/src/table/Table.tsx +205 -0
  81. package/src/table/index.ts +2 -0
  82. package/src/table/table.css +119 -0
  83. package/src/textarea/Textarea.tsx +102 -0
  84. package/src/textarea/index.ts +2 -0
  85. package/src/textarea/textarea.css +85 -0
  86. package/src/toaster/Toaster.tsx +108 -0
  87. package/src/toaster/index.ts +2 -0
  88. package/src/toaster/toaster.css +113 -0
  89. package/src/toggle/Toggle.tsx +88 -0
  90. package/src/toggle/index.ts +2 -0
  91. package/src/toggle/toggle.css +63 -0
  92. package/src/typography/Typography.tsx +58 -0
  93. package/src/typography/index.ts +2 -0
  94. package/src/typography/typography.css +102 -0
  95. package/tsconfig.json +13 -0
@@ -0,0 +1,190 @@
1
+ /* ── Button Component ─────────────────────────────────────────────────────── */
2
+ .gy-btn {
3
+ display: inline-flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+ gap: var(--gy-spacing-2);
7
+ font-family: var(--gy-font-sans);
8
+ font-weight: var(--gy-font-weight-medium);
9
+ letter-spacing: var(--gy-letter-spacing-wide);
10
+ border-radius: var(--gy-radius-lg);
11
+ border: 1.5px solid transparent;
12
+ cursor: pointer;
13
+ transition:
14
+ background-color var(--gy-duration-fast) var(--gy-ease-in-out),
15
+ border-color var(--gy-duration-fast) var(--gy-ease-in-out),
16
+ color var(--gy-duration-fast) var(--gy-ease-in-out),
17
+ box-shadow var(--gy-duration-fast) var(--gy-ease-in-out),
18
+ transform var(--gy-duration-fast) var(--gy-ease-out);
19
+ user-select: none;
20
+ white-space: nowrap;
21
+ text-decoration: none;
22
+ position: relative;
23
+ overflow: hidden;
24
+ -webkit-tap-highlight-color: transparent;
25
+ }
26
+
27
+ .gy-btn:focus-visible {
28
+ outline: none;
29
+ box-shadow: 0 0 0 3px var(--gy-focus-ring);
30
+ }
31
+
32
+ .gy-btn:active:not(:disabled) {
33
+ transform: scale(0.97);
34
+ }
35
+
36
+ /* ── Sizes ────────────────────────────────────────────────────────────────── */
37
+ .gy-btn--xs {
38
+ height: 1.75rem;
39
+ padding: 0 var(--gy-spacing-2);
40
+ font-size: var(--gy-font-size-xs);
41
+ border-radius: var(--gy-radius-md);
42
+ }
43
+
44
+ .gy-btn--sm {
45
+ height: 2rem;
46
+ padding: 0 var(--gy-spacing-3);
47
+ font-size: var(--gy-font-size-sm);
48
+ }
49
+
50
+ .gy-btn--md {
51
+ height: 2.5rem;
52
+ padding: 0 var(--gy-spacing-4);
53
+ font-size: var(--gy-font-size-sm);
54
+ }
55
+
56
+ .gy-btn--lg {
57
+ height: 2.75rem;
58
+ padding: 0 var(--gy-spacing-5);
59
+ font-size: var(--gy-font-size-md);
60
+ }
61
+
62
+ .gy-btn--xl {
63
+ height: 3.25rem;
64
+ padding: 0 var(--gy-spacing-6);
65
+ font-size: var(--gy-font-size-lg);
66
+ }
67
+
68
+ /* ── Variants ─────────────────────────────────────────────────────────────── */
69
+
70
+ /* Solid */
71
+ .gy-btn--solid {
72
+ background-color: var(--gy-primary);
73
+ border-color: var(--gy-primary);
74
+ color: var(--gy-primary-fg);
75
+ }
76
+
77
+ .gy-btn--solid:hover:not(:disabled) {
78
+ background-color: var(--gy-primary-hover);
79
+ border-color: var(--gy-primary-hover);
80
+ }
81
+
82
+ .gy-btn--solid:active:not(:disabled) {
83
+ background-color: var(--gy-primary-active);
84
+ }
85
+
86
+ /* Outline */
87
+ .gy-btn--outline {
88
+ background-color: transparent;
89
+ border-color: var(--gy-primary);
90
+ color: var(--gy-primary);
91
+ }
92
+
93
+ .gy-btn--outline:hover:not(:disabled) {
94
+ background-color: var(--gy-primary-subtle);
95
+ }
96
+
97
+ /* Ghost */
98
+ .gy-btn--ghost {
99
+ background-color: transparent;
100
+ border-color: transparent;
101
+ color: var(--gy-primary);
102
+ }
103
+
104
+ .gy-btn--ghost:hover:not(:disabled) {
105
+ background-color: var(--gy-primary-subtle);
106
+ }
107
+
108
+ /* Soft */
109
+ .gy-btn--soft {
110
+ background-color: var(--gy-primary-subtle);
111
+ border-color: transparent;
112
+ color: var(--gy-primary-hover);
113
+ }
114
+
115
+ .gy-btn--soft:hover:not(:disabled) {
116
+ background-color: var(--gy-primary-muted);
117
+ }
118
+
119
+ /* Link */
120
+ .gy-btn--link {
121
+ background-color: transparent;
122
+ border-color: transparent;
123
+ color: var(--gy-text-link);
124
+ text-decoration: underline;
125
+ text-underline-offset: 2px;
126
+ height: auto;
127
+ padding: 0;
128
+ }
129
+
130
+ .gy-btn--link:hover:not(:disabled) {
131
+ color: var(--gy-text-link-hover);
132
+ }
133
+
134
+ /* Danger variant */
135
+ .gy-btn--danger {
136
+ background-color: var(--gy-danger);
137
+ border-color: var(--gy-danger);
138
+ color: var(--gy-danger-fg);
139
+ }
140
+
141
+ .gy-btn--danger:hover:not(:disabled) {
142
+ background-color: var(--gy-danger-emphasis);
143
+ border-color: var(--gy-danger-emphasis);
144
+ }
145
+
146
+ /* ── Full width ──────────────────────────────────────────────────────────── */
147
+ .gy-btn--full {
148
+ width: 100%;
149
+ }
150
+
151
+ /* ── Disabled ────────────────────────────────────────────────────────────── */
152
+ .gy-btn:disabled,
153
+ .gy-btn--disabled {
154
+ cursor: not-allowed;
155
+ opacity: 0.5;
156
+ transform: none !important;
157
+ }
158
+
159
+ /* ── Loading ─────────────────────────────────────────────────────────────── */
160
+ .gy-btn--loading {
161
+ cursor: wait;
162
+ pointer-events: none;
163
+ }
164
+
165
+ .gy-btn__spinner {
166
+ width: 1em;
167
+ height: 1em;
168
+ border: 2px solid currentColor;
169
+ border-top-color: transparent;
170
+ border-radius: 50%;
171
+ animation: gy-spin var(--gy-duration-slower) linear infinite;
172
+ flex-shrink: 0;
173
+ }
174
+
175
+ /* ── Icon-only ──────────────────────────────────────────────────────────── */
176
+ .gy-btn--icon-only.gy-btn--xs { width: 1.75rem; padding: 0; }
177
+ .gy-btn--icon-only.gy-btn--sm { width: 2rem; padding: 0; }
178
+ .gy-btn--icon-only.gy-btn--md { width: 2.5rem; padding: 0; }
179
+ .gy-btn--icon-only.gy-btn--lg { width: 2.75rem; padding: 0; }
180
+ .gy-btn--icon-only.gy-btn--xl { width: 3.25rem; padding: 0; }
181
+
182
+ /* ── Ripple effect ───────────────────────────────────────────────────────── */
183
+ .gy-btn__ripple {
184
+ position: absolute;
185
+ border-radius: 50%;
186
+ background: rgba(255, 255, 255, 0.3);
187
+ transform: scale(0);
188
+ animation: gy-ripple 0.6s linear;
189
+ pointer-events: none;
190
+ }
@@ -0,0 +1,2 @@
1
+ export { Button } from './Button';
2
+ export type { ButtonProps, ButtonVariant, ButtonSize } from './Button';
package/src/button.tsx ADDED
@@ -0,0 +1,20 @@
1
+ "use client";
2
+
3
+ import { ReactNode } from "react";
4
+
5
+ interface ButtonProps {
6
+ children: ReactNode;
7
+ className?: string;
8
+ appName: string;
9
+ }
10
+
11
+ export const Button = ({ children, className, appName }: ButtonProps) => {
12
+ return (
13
+ <button
14
+ className={className}
15
+ onClick={() => alert(`Hello from your ${appName} app!`)}
16
+ >
17
+ {children}
18
+ </button>
19
+ );
20
+ };
@@ -0,0 +1,190 @@
1
+ 'use client';
2
+
3
+ import React, { useState } from 'react';
4
+ import './calendar.css';
5
+
6
+ export interface CalendarProps {
7
+ mode?: 'single' | 'range';
8
+ value?: Date | [Date, Date];
9
+ onChange?: (date: Date | [Date, Date]) => void;
10
+ minDate?: Date;
11
+ maxDate?: Date;
12
+ className?: string;
13
+ style?: React.CSSProperties;
14
+ }
15
+
16
+ const getDaysInMonth = (year: number, month: number) => new Date(year, month + 1, 0).getDate();
17
+ const getFirstDay = (year: number, month: number) => new Date(year, month, 1).getDay();
18
+
19
+ const isSameDay = (d1?: Date, d2?: Date) => {
20
+ if (!d1 || !d2) return false;
21
+ return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate();
22
+ };
23
+
24
+ const isBetween = (d: Date, start?: Date, end?: Date) => {
25
+ if (!start || !end) return false;
26
+ return d > start && d < end;
27
+ };
28
+
29
+ export function Calendar({ mode = 'single', value, onChange, minDate, maxDate, className = '', style }: CalendarProps) {
30
+ const [view, setView] = useState<'days' | 'months' | 'years'>('days');
31
+ const [current, setCurrent] = useState(() => {
32
+ if (Array.isArray(value)) return value[0] ?? new Date();
33
+ return value ?? new Date();
34
+ });
35
+ const [hoverDate, setHoverDate] = useState<Date | null>(null);
36
+
37
+ const [selStart, selEnd] = Array.isArray(value) ? value : [value, undefined];
38
+
39
+ const y = current.getFullYear();
40
+ const m = current.getMonth();
41
+
42
+ const prevMonth = () => setCurrent(new Date(y, m - 1, 1));
43
+ const nextMonth = () => setCurrent(new Date(y, m + 1, 1));
44
+ const prevYear = () => setCurrent(new Date(y - 1, m, 1));
45
+ const nextYear = () => setCurrent(new Date(y + 1, m, 1));
46
+ const prevDecade = () => setCurrent(new Date(y - 10, m, 1));
47
+ const nextDecade = () => setCurrent(new Date(y + 10, m, 1));
48
+
49
+ const daysInMonth = getDaysInMonth(y, m);
50
+ const firstDay = getFirstDay(y, m);
51
+ const daysInPrev = getDaysInMonth(y, m - 1);
52
+
53
+ const handleSelectDay = (date: Date) => {
54
+ if (mode === 'single') {
55
+ onChange?.(date);
56
+ } else {
57
+ if (!selStart || (selStart && selEnd)) {
58
+ onChange?.([date, date] as any); // Reset to start
59
+ } else {
60
+ const start = date < selStart ? date : selStart;
61
+ const end = date < selStart ? selStart : date;
62
+ onChange?.([start, end]);
63
+ }
64
+ }
65
+ };
66
+
67
+ const renderDays = () => {
68
+ const days = [];
69
+ const today = new Date();
70
+
71
+ // Prev month
72
+ for (let i = firstDay - 1; i >= 0; i--) {
73
+ const d = new Date(y, m - 1, daysInPrev - i);
74
+ days.push(<Day key={`prev-${i}`} date={d} outside />);
75
+ }
76
+ // Current month
77
+ for (let i = 1; i <= daysInMonth; i++) {
78
+ const d = new Date(y, m, i);
79
+ days.push(<Day key={`curr-${i}`} date={d} isToday={isSameDay(d, today)} />);
80
+ }
81
+ // Next month
82
+ const total = days.length;
83
+ for (let i = 1; i <= 42 - total; i++) {
84
+ const d = new Date(y, m + 1, i);
85
+ days.push(<Day key={`next-${i}`} date={d} outside />);
86
+ }
87
+
88
+ return days;
89
+ };
90
+
91
+ const Day = ({ date, outside, isToday }: { date: Date; outside?: boolean; isToday?: boolean }) => {
92
+ const disabled = (minDate && date < minDate) || (maxDate && date > maxDate);
93
+ const selected = isSameDay(date, selStart) || isSameDay(date, selEnd);
94
+ const inRange = mode === 'range' && isBetween(date, selStart, selEnd ?? hoverDate ?? undefined);
95
+ const isStart = mode === 'range' && isSameDay(date, selStart);
96
+ const isEnd = mode === 'range' && isSameDay(date, selEnd ?? hoverDate ?? undefined) && date > (selStart as Date);
97
+
98
+ const cls = [
99
+ 'gy-calendar-day',
100
+ outside ? 'gy-calendar-day--outside' : '',
101
+ isToday ? 'gy-calendar-day--today' : '',
102
+ selected ? 'gy-calendar-day--selected' : '',
103
+ inRange ? 'gy-calendar-day--range-in' : '',
104
+ isStart && (selEnd ?? hoverDate) ? 'gy-calendar-day--range-start' : '',
105
+ isEnd ? 'gy-calendar-day--range-end' : '',
106
+ disabled ? 'gy-calendar-day--disabled' : '',
107
+ ]
108
+ .filter(Boolean)
109
+ .join(' ');
110
+
111
+ return (
112
+ <button
113
+ type="button"
114
+ className={cls}
115
+ disabled={disabled}
116
+ onClick={() => handleSelectDay(date)}
117
+ onMouseEnter={() => mode === 'range' && selStart && !selEnd ? setHoverDate(date) : null}
118
+ >
119
+ {date.getDate()}
120
+ </button>
121
+ );
122
+ };
123
+
124
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
125
+ const monthName = current.toLocaleString('default', { month: 'long' });
126
+
127
+ return (
128
+ <div className={`gy-calendar ${className}`} style={style}>
129
+ {view === 'days' && (
130
+ <>
131
+ <div className="gy-calendar-header">
132
+ <button className="gy-calendar-nav" onClick={prevMonth}>‹</button>
133
+ <div className="gy-calendar-title" onClick={() => setView('months')}>
134
+ {monthName} {y}
135
+ </div>
136
+ <button className="gy-calendar-nav" onClick={nextMonth}>›</button>
137
+ </div>
138
+ <div className="gy-calendar-grid">
139
+ {['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].map((d) => (
140
+ <div key={d} className="gy-calendar-weekday">{d}</div>
141
+ ))}
142
+ {renderDays()}
143
+ </div>
144
+ </>
145
+ )}
146
+
147
+ {view === 'months' && (
148
+ <>
149
+ <div className="gy-calendar-header">
150
+ <button className="gy-calendar-nav" onClick={prevYear}>‹</button>
151
+ <div className="gy-calendar-title" onClick={() => setView('years')}>{y}</div>
152
+ <button className="gy-calendar-nav" onClick={nextYear}>›</button>
153
+ </div>
154
+ <div className="gy-calendar-picker-grid">
155
+ {months.map((mon, i) => (
156
+ <button
157
+ key={mon}
158
+ className={`gy-calendar-picker-item ${m === i ? 'gy-calendar-picker-item--selected' : ''}`}
159
+ onClick={() => { setCurrent(new Date(y, i, 1)); setView('days'); }}
160
+ >
161
+ {mon}
162
+ </button>
163
+ ))}
164
+ </div>
165
+ </>
166
+ )}
167
+
168
+ {view === 'years' && (
169
+ <>
170
+ <div className="gy-calendar-header">
171
+ <button className="gy-calendar-nav" onClick={prevDecade}>‹</button>
172
+ <div className="gy-calendar-title">{y - (y % 10)} - {y - (y % 10) + 9}</div>
173
+ <button className="gy-calendar-nav" onClick={nextDecade}>›</button>
174
+ </div>
175
+ <div className="gy-calendar-picker-grid">
176
+ {Array.from({ length: 12 }, (_, i) => y - (y % 10) - 1 + i).map((year) => (
177
+ <button
178
+ key={year}
179
+ className={`gy-calendar-picker-item ${y === year ? 'gy-calendar-picker-item--selected' : ''}`}
180
+ onClick={() => { setCurrent(new Date(year, m, 1)); setView('months'); }}
181
+ >
182
+ {year}
183
+ </button>
184
+ ))}
185
+ </div>
186
+ </>
187
+ )}
188
+ </div>
189
+ );
190
+ }
@@ -0,0 +1,142 @@
1
+ /* ── Calendar ────────────────────────────────────────────────────────────── */
2
+ .gy-calendar {
3
+ display: inline-flex;
4
+ flex-direction: column;
5
+ background: var(--gy-surface);
6
+ border: 1px solid var(--gy-border);
7
+ border-radius: var(--gy-radius-xl);
8
+ padding: var(--gy-spacing-4);
9
+ width: 320px;
10
+ }
11
+
12
+ .gy-calendar-header {
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: space-between;
16
+ margin-bottom: var(--gy-spacing-4);
17
+ }
18
+
19
+ .gy-calendar-title {
20
+ font-size: var(--gy-font-size-sm);
21
+ font-weight: var(--gy-font-weight-semibold);
22
+ color: var(--gy-text);
23
+ cursor: pointer;
24
+ padding: var(--gy-spacing-1) var(--gy-spacing-2);
25
+ border-radius: var(--gy-radius-md);
26
+ transition: background var(--gy-duration-fast);
27
+ }
28
+
29
+ .gy-calendar-title:hover {
30
+ background: var(--gy-background-muted);
31
+ }
32
+
33
+ .gy-calendar-nav {
34
+ display: flex;
35
+ align-items: center;
36
+ justify-content: center;
37
+ width: 1.75rem;
38
+ height: 1.75rem;
39
+ border-radius: var(--gy-radius-md);
40
+ background: none;
41
+ border: 1px solid transparent;
42
+ color: var(--gy-text-muted);
43
+ cursor: pointer;
44
+ transition: background var(--gy-duration-fast), color var(--gy-duration-fast);
45
+ }
46
+
47
+ .gy-calendar-nav:hover {
48
+ background: var(--gy-background-muted);
49
+ color: var(--gy-text);
50
+ }
51
+
52
+ /* Grid */
53
+ .gy-calendar-grid {
54
+ display: grid;
55
+ grid-template-columns: repeat(7, 1fr);
56
+ gap: 2px;
57
+ text-align: center;
58
+ }
59
+
60
+ .gy-calendar-weekday {
61
+ font-size: var(--gy-font-size-xs);
62
+ font-weight: var(--gy-font-weight-medium);
63
+ color: var(--gy-text-subtle);
64
+ padding-bottom: var(--gy-spacing-2);
65
+ }
66
+
67
+ .gy-calendar-day {
68
+ display: flex;
69
+ align-items: center;
70
+ justify-content: center;
71
+ aspect-ratio: 1;
72
+ font-size: var(--gy-font-size-sm);
73
+ color: var(--gy-text);
74
+ background: none;
75
+ border: none;
76
+ border-radius: var(--gy-radius-full);
77
+ cursor: pointer;
78
+ transition: background var(--gy-duration-fast), color var(--gy-duration-fast);
79
+ }
80
+
81
+ .gy-calendar-day:hover:not(.gy-calendar-day--disabled):not(.gy-calendar-day--selected) {
82
+ background: var(--gy-background-muted);
83
+ }
84
+
85
+ .gy-calendar-day--outside {
86
+ color: var(--gy-text-subtle);
87
+ opacity: 0.5;
88
+ }
89
+
90
+ .gy-calendar-day--today {
91
+ color: var(--gy-primary);
92
+ font-weight: var(--gy-font-weight-bold);
93
+ }
94
+
95
+ .gy-calendar-day--selected {
96
+ background: var(--gy-primary);
97
+ color: var(--gy-primary-fg);
98
+ font-weight: var(--gy-font-weight-semibold);
99
+ }
100
+
101
+ .gy-calendar-day--range-in {
102
+ background: var(--gy-primary-subtle);
103
+ border-radius: 0;
104
+ color: var(--gy-text);
105
+ }
106
+
107
+ .gy-calendar-day--range-start {
108
+ border-radius: var(--gy-radius-full) 0 0 var(--gy-radius-full);
109
+ }
110
+
111
+ .gy-calendar-day--range-end {
112
+ border-radius: 0 var(--gy-radius-full) var(--gy-radius-full) 0;
113
+ }
114
+
115
+ .gy-calendar-day--disabled {
116
+ opacity: 0.3;
117
+ cursor: not-allowed;
118
+ }
119
+
120
+ /* Month/Year Picker */
121
+ .gy-calendar-picker-grid {
122
+ display: grid;
123
+ grid-template-columns: repeat(3, 1fr);
124
+ gap: var(--gy-spacing-2);
125
+ }
126
+
127
+ .gy-calendar-picker-item {
128
+ padding: var(--gy-spacing-2) 0;
129
+ font-size: var(--gy-font-size-sm);
130
+ color: var(--gy-text);
131
+ background: none;
132
+ border: none;
133
+ border-radius: var(--gy-radius-lg);
134
+ cursor: pointer;
135
+ transition: background var(--gy-duration-fast);
136
+ }
137
+
138
+ .gy-calendar-picker-item:hover { background: var(--gy-background-muted); }
139
+ .gy-calendar-picker-item--selected {
140
+ background: var(--gy-primary);
141
+ color: var(--gy-primary-fg);
142
+ }
@@ -0,0 +1,2 @@
1
+ export { Calendar } from './Calendar';
2
+ export type { CalendarProps } from './Calendar';
@@ -0,0 +1,88 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import './card.css';
5
+
6
+ // ── Card ─────────────────────────────────────────────────────────────────────
7
+ export type CardVariant = 'default' | 'elevated' | 'outlined' | 'filled';
8
+ export type CardSize = 'sm' | 'md' | 'lg';
9
+
10
+ export interface CardProps {
11
+ variant?: CardVariant;
12
+ size?: CardSize;
13
+ hoverable?: boolean;
14
+ className?: string;
15
+ children?: React.ReactNode;
16
+ onClick?: () => void;
17
+ style?: React.CSSProperties;
18
+ }
19
+
20
+ export function Card({ variant = 'default', size = 'md', hoverable = false, className = '', children, onClick, style }: CardProps) {
21
+ return (
22
+ <div
23
+ className={[
24
+ 'gy-card',
25
+ variant !== 'default' ? `gy-card--${variant}` : '',
26
+ size !== 'md' ? `gy-card--${size}` : '',
27
+ hoverable ? 'gy-card--hoverable' : '',
28
+ className,
29
+ ]
30
+ .filter(Boolean)
31
+ .join(' ')}
32
+ onClick={onClick}
33
+ style={style}
34
+ role={onClick ? 'button' : undefined}
35
+ tabIndex={onClick ? 0 : undefined}
36
+ >
37
+ {children}
38
+ </div>
39
+ );
40
+ }
41
+
42
+ export function CardHeader({ children, className = '' }: { children?: React.ReactNode; className?: string }) {
43
+ return <div className={`gy-card-header ${className}`}>{children}</div>;
44
+ }
45
+
46
+ export function CardBody({ children, size, className = '' }: { children?: React.ReactNode; size?: CardSize; className?: string }) {
47
+ return (
48
+ <div className={['gy-card-body', size ? `gy-card-body--${size}` : '', className].filter(Boolean).join(' ')}>
49
+ {children}
50
+ </div>
51
+ );
52
+ }
53
+
54
+ export function CardFooter({ children, className = '' }: { children?: React.ReactNode; className?: string }) {
55
+ return <div className={`gy-card-footer ${className}`}>{children}</div>;
56
+ }
57
+
58
+ // ── CardInfo ──────────────────────────────────────────────────────────────────
59
+ export interface CardInfoProps {
60
+ title: string;
61
+ value: string | number;
62
+ icon?: React.ReactNode;
63
+ trend?: { value: number; label?: string };
64
+ footer?: string;
65
+ className?: string;
66
+ }
67
+
68
+ export function CardInfo({ title, value, icon, trend, footer, className = '' }: CardInfoProps) {
69
+ const isUp = trend ? trend.value >= 0 : null;
70
+ return (
71
+ <div className={`gy-card-info ${className}`}>
72
+ <div className="gy-card-info__header">
73
+ <span className="gy-card-info__title">{title}</span>
74
+ {icon && <span className="gy-card-info__icon">{icon}</span>}
75
+ </div>
76
+ <div className="gy-card-info__value">{value}</div>
77
+ {trend && (
78
+ <div>
79
+ <span className={`gy-card-info__trend gy-card-info__trend--${isUp ? 'up' : 'down'}`}>
80
+ {isUp ? '↑' : '↓'} {Math.abs(trend.value)}%
81
+ {trend.label && ` ${trend.label}`}
82
+ </span>
83
+ </div>
84
+ )}
85
+ {footer && <div className="gy-card-info__footer">{footer}</div>}
86
+ </div>
87
+ );
88
+ }