@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,115 @@
1
+ /* ── Card ─────────────────────────────────────────────────────────────────── */
2
+ .gy-card {
3
+ background: var(--gy-surface);
4
+ border-radius: var(--gy-radius-xl);
5
+ border: 1px solid var(--gy-border);
6
+ overflow: hidden;
7
+ transition: box-shadow var(--gy-duration-normal) var(--gy-ease-in-out);
8
+ }
9
+
10
+ .gy-card--elevated {
11
+ border-color: transparent;
12
+ box-shadow: var(--gy-shadow-md);
13
+ }
14
+
15
+ .gy-card--elevated:hover { box-shadow: var(--gy-shadow-lg); }
16
+
17
+ .gy-card--outlined {
18
+ border-color: var(--gy-border-strong);
19
+ box-shadow: none;
20
+ }
21
+
22
+ .gy-card--filled {
23
+ background: var(--gy-background-subtle);
24
+ border-color: transparent;
25
+ }
26
+
27
+ .gy-card--hoverable { cursor: pointer; }
28
+ .gy-card--hoverable:hover { box-shadow: var(--gy-shadow-lg); transform: translateY(-1px); }
29
+
30
+ .gy-card--sm { border-radius: var(--gy-radius-lg); }
31
+ .gy-card--lg { border-radius: var(--gy-radius-2xl); }
32
+
33
+ .gy-card-header {
34
+ padding: var(--gy-spacing-4) var(--gy-spacing-5);
35
+ border-bottom: 1px solid var(--gy-border);
36
+ }
37
+
38
+ .gy-card-body { padding: var(--gy-spacing-5); }
39
+ .gy-card-body--sm { padding: var(--gy-spacing-3); }
40
+ .gy-card-body--lg { padding: var(--gy-spacing-6); }
41
+
42
+ .gy-card-footer {
43
+ padding: var(--gy-spacing-4) var(--gy-spacing-5);
44
+ border-top: 1px solid var(--gy-border);
45
+ background: var(--gy-background-subtle);
46
+ }
47
+
48
+ /* ── CardInfo ─────────────────────────────────────────────────────────────── */
49
+ .gy-card-info {
50
+ display: flex;
51
+ flex-direction: column;
52
+ gap: var(--gy-spacing-3);
53
+ padding: var(--gy-spacing-5);
54
+ background: var(--gy-surface);
55
+ border-radius: var(--gy-radius-xl);
56
+ border: 1px solid var(--gy-border);
57
+ transition: box-shadow var(--gy-duration-normal);
58
+ }
59
+
60
+ .gy-card-info:hover { box-shadow: var(--gy-shadow-md); }
61
+
62
+ .gy-card-info__header {
63
+ display: flex;
64
+ align-items: center;
65
+ justify-content: space-between;
66
+ }
67
+
68
+ .gy-card-info__title {
69
+ font-size: var(--gy-font-size-sm);
70
+ font-weight: var(--gy-font-weight-medium);
71
+ color: var(--gy-text-muted);
72
+ }
73
+
74
+ .gy-card-info__icon {
75
+ display: flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ width: 2.5rem;
79
+ height: 2.5rem;
80
+ border-radius: var(--gy-radius-lg);
81
+ background: var(--gy-primary-subtle);
82
+ color: var(--gy-primary);
83
+ }
84
+
85
+ .gy-card-info__value {
86
+ font-size: var(--gy-font-size-3xl);
87
+ font-weight: var(--gy-font-weight-bold);
88
+ color: var(--gy-text);
89
+ line-height: 1;
90
+ }
91
+
92
+ .gy-card-info__trend {
93
+ display: inline-flex;
94
+ align-items: center;
95
+ gap: var(--gy-spacing-1);
96
+ font-size: var(--gy-font-size-xs);
97
+ font-weight: var(--gy-font-weight-medium);
98
+ padding: 2px var(--gy-spacing-1-5);
99
+ border-radius: var(--gy-radius-full);
100
+ }
101
+
102
+ .gy-card-info__trend--up {
103
+ color: var(--gy-success-emphasis);
104
+ background: var(--gy-success-subtle);
105
+ }
106
+
107
+ .gy-card-info__trend--down {
108
+ color: var(--gy-danger);
109
+ background: var(--gy-danger-subtle);
110
+ }
111
+
112
+ .gy-card-info__footer {
113
+ font-size: var(--gy-font-size-xs);
114
+ color: var(--gy-text-subtle);
115
+ }
@@ -0,0 +1,2 @@
1
+ export { Card, CardHeader, CardBody, CardFooter, CardInfo } from './Card';
2
+ export type { CardProps, CardInfoProps, CardVariant, CardSize } from './Card';
package/src/card.tsx ADDED
@@ -0,0 +1,27 @@
1
+ import { type JSX } from "react";
2
+
3
+ export function Card({
4
+ className,
5
+ title,
6
+ children,
7
+ href,
8
+ }: {
9
+ className?: string;
10
+ title: string;
11
+ children: React.ReactNode;
12
+ href: string;
13
+ }): JSX.Element {
14
+ return (
15
+ <a
16
+ className={className}
17
+ href={`${href}?utm_source=create-turbo&utm_medium=basic&utm_campaign=create-turbo"`}
18
+ rel="noopener noreferrer"
19
+ target="_blank"
20
+ >
21
+ <h2>
22
+ {title} <span>-&gt;</span>
23
+ </h2>
24
+ <p>{children}</p>
25
+ </a>
26
+ );
27
+ }
@@ -0,0 +1,186 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import {
5
+ LineChart, Line, BarChart, Bar, AreaChart, Area, PieChart, Pie, Cell,
6
+ XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend
7
+ } from 'recharts';
8
+
9
+ export type ChartType = 'line' | 'bar' | 'area' | 'pie' | 'donut';
10
+
11
+ export interface ChartSeries {
12
+ key: string;
13
+ name?: string;
14
+ color?: string;
15
+ }
16
+
17
+ export interface ChartProps {
18
+ type: ChartType;
19
+ data: any[];
20
+ xAxisKey?: string;
21
+ series?: ChartSeries[]; // for line, bar, area
22
+ nameKey?: string; // for pie
23
+ valueKey?: string; // for pie
24
+ height?: number | string;
25
+ showGrid?: boolean;
26
+ showLegend?: boolean;
27
+ className?: string;
28
+ }
29
+
30
+ const DEFAULT_COLORS = [
31
+ 'var(--gy-primary)',
32
+ 'var(--gy-success)',
33
+ 'var(--gy-warning)',
34
+ 'var(--gy-danger)',
35
+ 'var(--gy-info)',
36
+ '#8b5cf6',
37
+ '#ec4899',
38
+ ];
39
+
40
+ export function Chart({
41
+ type,
42
+ data,
43
+ xAxisKey,
44
+ series = [],
45
+ nameKey,
46
+ valueKey,
47
+ height = 300,
48
+ showGrid = true,
49
+ showLegend = true,
50
+ className = '',
51
+ }: ChartProps) {
52
+ const commonAxisProps = {
53
+ stroke: 'var(--gy-border-strong)',
54
+ tick: { fill: 'var(--gy-text-subtle)', fontSize: 12 },
55
+ tickLine: { stroke: 'var(--gy-border)' },
56
+ };
57
+
58
+ const commonTooltipProps = {
59
+ contentStyle: {
60
+ backgroundColor: 'var(--gy-surface)',
61
+ borderColor: 'var(--gy-border)',
62
+ borderRadius: 'var(--gy-radius-lg)',
63
+ boxShadow: 'var(--gy-shadow-md)',
64
+ color: 'var(--gy-text)',
65
+ fontSize: 13,
66
+ },
67
+ itemStyle: { color: 'var(--gy-text)' },
68
+ };
69
+
70
+ const renderContent = () => {
71
+ switch (type) {
72
+ case 'line':
73
+ return (
74
+ <LineChart data={data}>
75
+ {showGrid && <CartesianGrid strokeDasharray="3 3" stroke="var(--gy-border)" vertical={false} />}
76
+ <XAxis dataKey={xAxisKey} {...commonAxisProps} />
77
+ <YAxis {...commonAxisProps} />
78
+ <Tooltip {...commonTooltipProps} />
79
+ {showLegend && <Legend wrapperStyle={{ fontSize: 12 }} />}
80
+ {series.map((s, i) => (
81
+ <Line
82
+ key={s.key}
83
+ type="monotone"
84
+ dataKey={s.key}
85
+ name={s.name ?? s.key}
86
+ stroke={s.color ?? DEFAULT_COLORS[i % DEFAULT_COLORS.length]}
87
+ strokeWidth={2}
88
+ dot={{ r: 4, strokeWidth: 2, fill: 'var(--gy-surface)' }}
89
+ activeDot={{ r: 6, strokeWidth: 0 }}
90
+ />
91
+ ))}
92
+ </LineChart>
93
+ );
94
+
95
+ case 'bar':
96
+ return (
97
+ <BarChart data={data}>
98
+ {showGrid && <CartesianGrid strokeDasharray="3 3" stroke="var(--gy-border)" vertical={false} />}
99
+ <XAxis dataKey={xAxisKey} {...commonAxisProps} />
100
+ <YAxis {...commonAxisProps} />
101
+ <Tooltip {...commonTooltipProps} cursor={{ fill: 'var(--gy-background-muted)' }} />
102
+ {showLegend && <Legend wrapperStyle={{ fontSize: 12 }} />}
103
+ {series.map((s, i) => (
104
+ <Bar
105
+ key={s.key}
106
+ dataKey={s.key}
107
+ name={s.name ?? s.key}
108
+ fill={s.color ?? DEFAULT_COLORS[i % DEFAULT_COLORS.length]}
109
+ radius={[4, 4, 0, 0]}
110
+ />
111
+ ))}
112
+ </BarChart>
113
+ );
114
+
115
+ case 'area':
116
+ return (
117
+ <AreaChart data={data}>
118
+ <defs>
119
+ {series.map((s, i) => {
120
+ const color = s.color ?? DEFAULT_COLORS[i % DEFAULT_COLORS.length];
121
+ return (
122
+ <linearGradient key={`grad-${s.key}`} id={`grad-${s.key}`} x1="0" y1="0" x2="0" y2="1">
123
+ <stop offset="5%" stopColor={color} stopOpacity={0.3} />
124
+ <stop offset="95%" stopColor={color} stopOpacity={0} />
125
+ </linearGradient>
126
+ );
127
+ })}
128
+ </defs>
129
+ {showGrid && <CartesianGrid strokeDasharray="3 3" stroke="var(--gy-border)" vertical={false} />}
130
+ <XAxis dataKey={xAxisKey} {...commonAxisProps} />
131
+ <YAxis {...commonAxisProps} />
132
+ <Tooltip {...commonTooltipProps} />
133
+ {showLegend && <Legend wrapperStyle={{ fontSize: 12 }} />}
134
+ {series.map((s, i) => {
135
+ const color = s.color ?? DEFAULT_COLORS[i % DEFAULT_COLORS.length];
136
+ return (
137
+ <Area
138
+ key={s.key}
139
+ type="monotone"
140
+ dataKey={s.key}
141
+ name={s.name ?? s.key}
142
+ stroke={color}
143
+ strokeWidth={2}
144
+ fillOpacity={1}
145
+ fill={`url(#grad-${s.key})`}
146
+ />
147
+ );
148
+ })}
149
+ </AreaChart>
150
+ );
151
+
152
+ case 'pie':
153
+ case 'donut':
154
+ return (
155
+ <PieChart>
156
+ <Tooltip {...commonTooltipProps} />
157
+ {showLegend && <Legend wrapperStyle={{ fontSize: 12 }} />}
158
+ <Pie
159
+ data={data}
160
+ dataKey={valueKey!}
161
+ nameKey={nameKey!}
162
+ cx="50%"
163
+ cy="50%"
164
+ innerRadius={type === 'donut' ? '60%' : 0}
165
+ outerRadius="80%"
166
+ stroke="var(--gy-surface)"
167
+ strokeWidth={2}
168
+ paddingAngle={type === 'donut' ? 2 : 0}
169
+ >
170
+ {data.map((entry, index) => (
171
+ <Cell key={`cell-${index}`} fill={entry.color ?? DEFAULT_COLORS[index % DEFAULT_COLORS.length]} />
172
+ ))}
173
+ </Pie>
174
+ </PieChart>
175
+ );
176
+ }
177
+ };
178
+
179
+ return (
180
+ <div className={`gy-chart ${className}`} style={{ width: '100%', height }}>
181
+ <ResponsiveContainer width="100%" height="100%">
182
+ {renderContent()}
183
+ </ResponsiveContainer>
184
+ </div>
185
+ );
186
+ }
@@ -0,0 +1,2 @@
1
+ export { Chart } from './Chart';
2
+ export type { ChartProps, ChartType, ChartSeries } from './Chart';
@@ -0,0 +1,101 @@
1
+ 'use client';
2
+
3
+ import React, { forwardRef, useId } from 'react';
4
+ import './checkbox.css';
5
+
6
+ export type CheckboxSize = 'sm' | 'md' | 'lg';
7
+
8
+ export interface CheckboxProps {
9
+ checked?: boolean;
10
+ defaultChecked?: boolean;
11
+ indeterminate?: boolean;
12
+ onChange?: (checked: boolean) => void;
13
+ disabled?: boolean;
14
+ label?: React.ReactNode;
15
+ description?: string;
16
+ size?: CheckboxSize;
17
+ id?: string;
18
+ name?: string;
19
+ value?: string;
20
+ className?: string;
21
+ }
22
+
23
+ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox(
24
+ {
25
+ checked,
26
+ defaultChecked,
27
+ indeterminate = false,
28
+ onChange,
29
+ disabled = false,
30
+ label,
31
+ description,
32
+ size = 'md',
33
+ id,
34
+ name,
35
+ value,
36
+ className = '',
37
+ },
38
+ ref
39
+ ) {
40
+ const uid = useId();
41
+ const inputId = id ?? uid;
42
+
43
+ const isChecked = checked ?? defaultChecked ?? false;
44
+
45
+ const boxClasses = [
46
+ 'gy-checkbox-box',
47
+ `gy-checkbox-box--${size}`,
48
+ isChecked ? 'gy-checkbox-box--checked' : '',
49
+ indeterminate ? 'gy-checkbox-box--indeterminate' : '',
50
+ ]
51
+ .filter(Boolean)
52
+ .join(' ');
53
+
54
+ return (
55
+ <label
56
+ className={[
57
+ 'gy-checkbox-root',
58
+ disabled ? 'gy-checkbox-root--disabled' : '',
59
+ className,
60
+ ]
61
+ .filter(Boolean)
62
+ .join(' ')}
63
+ htmlFor={inputId}
64
+ >
65
+ <input
66
+ ref={ref}
67
+ type="checkbox"
68
+ id={inputId}
69
+ name={name}
70
+ value={value}
71
+ checked={checked}
72
+ defaultChecked={defaultChecked}
73
+ disabled={disabled}
74
+ className="gy-checkbox-input"
75
+ onChange={(e) => onChange?.(e.target.checked)}
76
+ aria-checked={indeterminate ? 'mixed' : checked}
77
+ />
78
+ <span className={boxClasses} aria-hidden="true">
79
+ {(isChecked || indeterminate) && (
80
+ <span className="gy-checkbox-icon">
81
+ {indeterminate ? (
82
+ <svg width="10" height="2" viewBox="0 0 10 2" fill="currentColor">
83
+ <rect x="0" y="0" width="10" height="2" rx="1" />
84
+ </svg>
85
+ ) : (
86
+ <svg width="10" height="8" viewBox="0 0 10 8" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
87
+ <polyline points="1,4 4,7 9,1" />
88
+ </svg>
89
+ )}
90
+ </span>
91
+ )}
92
+ </span>
93
+ {(label || description) && (
94
+ <span className="gy-checkbox-content">
95
+ {label && <span className="gy-checkbox-label">{label}</span>}
96
+ {description && <span className="gy-checkbox-description">{description}</span>}
97
+ </span>
98
+ )}
99
+ </label>
100
+ );
101
+ });
@@ -0,0 +1,76 @@
1
+ /* ── Checkbox ─────────────────────────────────────────────────────────────── */
2
+ .gy-checkbox-root {
3
+ display: inline-flex;
4
+ align-items: flex-start;
5
+ gap: var(--gy-spacing-2);
6
+ cursor: pointer;
7
+ }
8
+
9
+ .gy-checkbox-root--disabled {
10
+ cursor: not-allowed;
11
+ opacity: 0.5;
12
+ }
13
+
14
+ .gy-checkbox-input {
15
+ position: absolute;
16
+ opacity: 0;
17
+ width: 0;
18
+ height: 0;
19
+ pointer-events: none;
20
+ }
21
+
22
+ .gy-checkbox-box {
23
+ display: flex;
24
+ align-items: center;
25
+ justify-content: center;
26
+ flex-shrink: 0;
27
+ border: 2px solid var(--gy-border-strong);
28
+ border-radius: var(--gy-radius-sm);
29
+ background: var(--gy-surface);
30
+ transition:
31
+ background-color var(--gy-duration-fast) var(--gy-ease-in-out),
32
+ border-color var(--gy-duration-fast) var(--gy-ease-in-out),
33
+ box-shadow var(--gy-duration-fast) var(--gy-ease-in-out);
34
+ }
35
+
36
+ .gy-checkbox-box--sm { width: 1rem; height: 1rem; }
37
+ .gy-checkbox-box--md { width: 1.125rem; height: 1.125rem; }
38
+ .gy-checkbox-box--lg { width: 1.25rem; height: 1.25rem; }
39
+
40
+ .gy-checkbox-root:not(.gy-checkbox-root--disabled):hover .gy-checkbox-box {
41
+ border-color: var(--gy-primary);
42
+ }
43
+
44
+ .gy-checkbox-input:focus-visible + .gy-checkbox-box {
45
+ box-shadow: 0 0 0 3px var(--gy-focus-ring);
46
+ border-color: var(--gy-primary);
47
+ }
48
+
49
+ .gy-checkbox-box--checked,
50
+ .gy-checkbox-box--indeterminate {
51
+ background: var(--gy-primary);
52
+ border-color: var(--gy-primary);
53
+ }
54
+
55
+ .gy-checkbox-icon {
56
+ color: var(--gy-primary-fg);
57
+ animation: gy-zoom-in var(--gy-duration-fast) var(--gy-ease-spring);
58
+ }
59
+
60
+ .gy-checkbox-content {
61
+ display: flex;
62
+ flex-direction: column;
63
+ gap: 2px;
64
+ }
65
+
66
+ .gy-checkbox-label {
67
+ font-size: var(--gy-font-size-sm);
68
+ font-weight: var(--gy-font-weight-medium);
69
+ color: var(--gy-text);
70
+ line-height: 1.4;
71
+ }
72
+
73
+ .gy-checkbox-description {
74
+ font-size: var(--gy-font-size-xs);
75
+ color: var(--gy-text-subtle);
76
+ }
@@ -0,0 +1,2 @@
1
+ export { Checkbox } from './Checkbox';
2
+ export type { CheckboxProps, CheckboxSize } from './Checkbox';
@@ -0,0 +1,146 @@
1
+ 'use client';
2
+
3
+ import React, { useRef, useState } from 'react';
4
+ import './chips.css';
5
+
6
+ export type ChipVariant = 'solid' | 'soft' | 'outline' | 'success' | 'warning' | 'danger' | 'neutral';
7
+ export type ChipSize = 'sm' | 'md' | 'lg';
8
+
9
+ export interface ChipProps {
10
+ children: React.ReactNode;
11
+ variant?: ChipVariant;
12
+ size?: ChipSize;
13
+ removable?: boolean;
14
+ onRemove?: () => void;
15
+ clickable?: boolean;
16
+ selected?: boolean;
17
+ onClick?: () => void;
18
+ icon?: React.ReactNode;
19
+ className?: string;
20
+ }
21
+
22
+ export function Chip({
23
+ children,
24
+ variant = 'soft',
25
+ size = 'md',
26
+ removable = false,
27
+ onRemove,
28
+ clickable = false,
29
+ selected = false,
30
+ onClick,
31
+ icon,
32
+ className = '',
33
+ }: ChipProps) {
34
+ return (
35
+ <span
36
+ className={[
37
+ 'gy-chip',
38
+ `gy-chip--${variant}`,
39
+ `gy-chip--${size}`,
40
+ clickable ? 'gy-chip--clickable' : '',
41
+ selected ? 'gy-chip--selected' : '',
42
+ className,
43
+ ]
44
+ .filter(Boolean)
45
+ .join(' ')}
46
+ role={clickable ? 'button' : undefined}
47
+ tabIndex={clickable ? 0 : undefined}
48
+ onClick={clickable ? onClick : undefined}
49
+ onKeyDown={clickable ? (e) => { if (e.key === 'Enter' || e.key === ' ') onClick?.(); } : undefined}
50
+ >
51
+ {icon && <span aria-hidden="true">{icon}</span>}
52
+ {children}
53
+ {removable && (
54
+ <button
55
+ type="button"
56
+ className="gy-chip__remove"
57
+ onClick={(e) => { e.stopPropagation(); onRemove?.(); }}
58
+ aria-label="Remove"
59
+ >
60
+ <svg width="8" height="8" viewBox="0 0 8 8" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round">
61
+ <line x1="1" y1="1" x2="7" y2="7" />
62
+ <line x1="7" y1="1" x2="1" y2="7" />
63
+ </svg>
64
+ </button>
65
+ )}
66
+ </span>
67
+ );
68
+ }
69
+
70
+ // ── ChipsInput ────────────────────────────────────────────────────────────────
71
+ export interface ChipsInputProps {
72
+ values: string[];
73
+ onChange: (values: string[]) => void;
74
+ placeholder?: string;
75
+ disabled?: boolean;
76
+ maxItems?: number;
77
+ chipVariant?: ChipVariant;
78
+ className?: string;
79
+ }
80
+
81
+ export function ChipsInput({
82
+ values,
83
+ onChange,
84
+ placeholder = 'Add a tag...',
85
+ disabled = false,
86
+ maxItems,
87
+ chipVariant = 'soft',
88
+ className = '',
89
+ }: ChipsInputProps) {
90
+ const [input, setInput] = useState('');
91
+ const inputRef = useRef<HTMLInputElement>(null);
92
+
93
+ const addChip = (val: string) => {
94
+ const trimmed = val.trim();
95
+ if (!trimmed || values.includes(trimmed)) return;
96
+ if (maxItems && values.length >= maxItems) return;
97
+ onChange([...values, trimmed]);
98
+ setInput('');
99
+ };
100
+
101
+ const removeChip = (idx: number) => {
102
+ onChange(values.filter((_, i) => i !== idx));
103
+ };
104
+
105
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
106
+ if (e.key === 'Enter' || e.key === ',') {
107
+ e.preventDefault();
108
+ addChip(input);
109
+ }
110
+ if (e.key === 'Backspace' && !input && values.length > 0) {
111
+ removeChip(values.length - 1);
112
+ }
113
+ };
114
+
115
+ return (
116
+ <div
117
+ className={`gy-chips-input ${className}`}
118
+ onClick={() => inputRef.current?.focus()}
119
+ >
120
+ {values.map((val, i) => (
121
+ <Chip
122
+ key={i}
123
+ variant={chipVariant}
124
+ size="sm"
125
+ removable={!disabled}
126
+ onRemove={() => removeChip(i)}
127
+ >
128
+ {val}
129
+ </Chip>
130
+ ))}
131
+ {(!maxItems || values.length < maxItems) && (
132
+ <input
133
+ ref={inputRef}
134
+ className="gy-chips-input__field"
135
+ value={input}
136
+ onChange={(e) => setInput(e.target.value)}
137
+ onKeyDown={handleKeyDown}
138
+ onBlur={() => addChip(input)}
139
+ placeholder={values.length === 0 ? placeholder : ''}
140
+ disabled={disabled}
141
+ aria-label={placeholder}
142
+ />
143
+ )}
144
+ </div>
145
+ );
146
+ }