sonance-brand-mcp 1.0.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.
- package/dist/assets/BRAND_GUIDELINES.md +361 -0
- package/dist/assets/components/accordion.tsx +141 -0
- package/dist/assets/components/alert.tsx +78 -0
- package/dist/assets/components/autocomplete.tsx +246 -0
- package/dist/assets/components/avatar.tsx +105 -0
- package/dist/assets/components/badge.tsx +64 -0
- package/dist/assets/components/breadcrumbs.tsx +149 -0
- package/dist/assets/components/button.tsx +50 -0
- package/dist/assets/components/calendar.tsx +145 -0
- package/dist/assets/components/card.tsx +70 -0
- package/dist/assets/components/checkbox-group.tsx +170 -0
- package/dist/assets/components/checkbox.tsx +58 -0
- package/dist/assets/components/code.tsx +175 -0
- package/dist/assets/components/date-input.tsx +113 -0
- package/dist/assets/components/date-picker.tsx +114 -0
- package/dist/assets/components/date-range-picker.tsx +133 -0
- package/dist/assets/components/dialog.tsx +158 -0
- package/dist/assets/components/divider.tsx +50 -0
- package/dist/assets/components/drawer.tsx +149 -0
- package/dist/assets/components/dropdown.tsx +213 -0
- package/dist/assets/components/form.tsx +190 -0
- package/dist/assets/components/image.tsx +173 -0
- package/dist/assets/components/input-otp.tsx +176 -0
- package/dist/assets/components/input.tsx +37 -0
- package/dist/assets/components/kbd.tsx +126 -0
- package/dist/assets/components/link.tsx +99 -0
- package/dist/assets/components/listbox.tsx +174 -0
- package/dist/assets/components/navbar.tsx +212 -0
- package/dist/assets/components/number-input.tsx +204 -0
- package/dist/assets/components/pagination.tsx +191 -0
- package/dist/assets/components/popover.tsx +111 -0
- package/dist/assets/components/progress.tsx +119 -0
- package/dist/assets/components/radio-group.tsx +123 -0
- package/dist/assets/components/range-calendar.tsx +206 -0
- package/dist/assets/components/scroll-shadow.tsx +131 -0
- package/dist/assets/components/select.tsx +183 -0
- package/dist/assets/components/skeleton.tsx +114 -0
- package/dist/assets/components/slider.tsx +155 -0
- package/dist/assets/components/spacer.tsx +72 -0
- package/dist/assets/components/spinner.tsx +77 -0
- package/dist/assets/components/switch.tsx +64 -0
- package/dist/assets/components/table.tsx +122 -0
- package/dist/assets/components/tabs.tsx +127 -0
- package/dist/assets/components/textarea.tsx +38 -0
- package/dist/assets/components/theme-toggle.tsx +81 -0
- package/dist/assets/components/time-input.tsx +176 -0
- package/dist/assets/components/toast.tsx +193 -0
- package/dist/assets/components/tooltip.tsx +92 -0
- package/dist/assets/components/user.tsx +171 -0
- package/dist/assets/globals.css +483 -0
- package/dist/assets/logo-manifest.json +65 -0
- package/dist/assets/utils.ts +6 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +447 -0
- package/package.json +49 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# Sonance Brand Family - AI Context Guide
|
|
2
|
+
|
|
3
|
+
> **Purpose**: This document is optimized for AI/LLM consumption. Use it to generate brand-compliant UI components, websites, and applications for Sonance, IPORT, and Blaze Audio.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Quick Reference
|
|
8
|
+
|
|
9
|
+
| Brand | Primary Color | Accent Color | Theme |
|
|
10
|
+
|-------|---------------|--------------|-------|
|
|
11
|
+
| **Sonance** | Charcoal `#333F48` | Cyan "The Beam" `#00D3C8` | Light preferred |
|
|
12
|
+
| **Sonance Foundation** | Charcoal `#333F48` | Green `#00B2A9` | Light preferred |
|
|
13
|
+
| **IPORT** | Black `#0E1114` | Orange `#FC4C02` | Dark preferred |
|
|
14
|
+
| **Blaze Audio** | Black `#1A1A1C` | Blue `#00A3E1` | Dark preferred |
|
|
15
|
+
|
|
16
|
+
**Font**: Montserrat (all brands)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Typography
|
|
21
|
+
|
|
22
|
+
### Font Family
|
|
23
|
+
```css
|
|
24
|
+
font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Google Fonts Import**:
|
|
28
|
+
```css
|
|
29
|
+
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&display=swap');
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Font Weights
|
|
33
|
+
| Weight | Name | Usage |
|
|
34
|
+
|--------|------|-------|
|
|
35
|
+
| 300 | Light | Headlines, display text |
|
|
36
|
+
| 400 | Regular | Body text, paragraphs |
|
|
37
|
+
| 500 | Medium | Emphasis, subheadings |
|
|
38
|
+
| 600 | Semibold | Strong emphasis |
|
|
39
|
+
| 700 | Bold | CTAs, buttons (small text only) |
|
|
40
|
+
|
|
41
|
+
### Typography Rules
|
|
42
|
+
|
|
43
|
+
**Headlines**:
|
|
44
|
+
- Use Light (300) or Medium (500) weight
|
|
45
|
+
- **Never use bold for large display text**
|
|
46
|
+
- Letter-spacing: `-0.02em` for display sizes
|
|
47
|
+
- Line-height: `1.2`
|
|
48
|
+
|
|
49
|
+
**Body Text**:
|
|
50
|
+
- Use Regular (400) weight
|
|
51
|
+
- Line-height: `1.6` to `1.75`
|
|
52
|
+
- Max-width: `65-75` characters per line
|
|
53
|
+
|
|
54
|
+
**All Caps Text**:
|
|
55
|
+
- When using all caps, apply letter-spacing: `0.08em` to `0.1em`
|
|
56
|
+
- Use for: headlines, sub headlines, product names, labels
|
|
57
|
+
- **Never apply tracking to long-form body copy**
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Sonance Brand
|
|
62
|
+
|
|
63
|
+
### Color Palette
|
|
64
|
+
|
|
65
|
+
| Color | Hex | CSS Variable | Tailwind Class | Usage |
|
|
66
|
+
|-------|-----|--------------|----------------|-------|
|
|
67
|
+
| Charcoal | `#333F48` | `--sonance-charcoal` | `bg-sonance-charcoal` | Primary brand color, text |
|
|
68
|
+
| The Beam (Cyan) | `#00D3C8` | `--sonance-blue` | `bg-sonance-blue` | Accent, highlights |
|
|
69
|
+
| Light Gray | `#D9D9D6` | `--sonance-light-gray` | `bg-sonance-light-gray` | Backgrounds, borders |
|
|
70
|
+
| White | `#FFFFFF` | `--sonance-white` | `bg-sonance-white` | Backgrounds |
|
|
71
|
+
| Black | `#000000` | `--sonance-black` | `bg-sonance-black` | Text on light backgrounds |
|
|
72
|
+
|
|
73
|
+
**Print Specifications** (for reference):
|
|
74
|
+
- Charcoal: Pantone 7545 C, CMYK 72/51/39/36, RAL 7024
|
|
75
|
+
- The Beam: Pantone 3262 C, CMYK 79/0/32/0
|
|
76
|
+
|
|
77
|
+
### Gray Scale (derived from Charcoal)
|
|
78
|
+
| Level | Hex | CSS Variable |
|
|
79
|
+
|-------|-----|--------------|
|
|
80
|
+
| 50 | `#f8f9fa` | `--sonance-gray-50` |
|
|
81
|
+
| 100 | `#f0f2f3` | `--sonance-gray-100` |
|
|
82
|
+
| 200 | `#D9D9D6` | `--sonance-gray-200` |
|
|
83
|
+
| 300 | `#b8bfc4` | `--sonance-gray-300` |
|
|
84
|
+
| 400 | `#8f999f` | `--sonance-gray-400` |
|
|
85
|
+
| 500 | `#6b7780` | `--sonance-gray-500` |
|
|
86
|
+
| 600 | `#515c64` | `--sonance-gray-600` |
|
|
87
|
+
| 700 | `#424c54` | `--sonance-gray-700` |
|
|
88
|
+
| 800 | `#3a444c` | `--sonance-gray-800` |
|
|
89
|
+
| 900 | `#333F48` | `--sonance-gray-900` |
|
|
90
|
+
|
|
91
|
+
### Brand Pillars
|
|
92
|
+
|
|
93
|
+
1. **Sound Quality & Heritage** - "40+ years of sonic innovation"
|
|
94
|
+
2. **Designed to Disappear** - Invisible integration is core
|
|
95
|
+
3. **Partnership Excellence** - We succeed when our partners succeed
|
|
96
|
+
4. **Premium Experience** - Every touchpoint feels high-end
|
|
97
|
+
|
|
98
|
+
### Logo Usage
|
|
99
|
+
|
|
100
|
+
- Use 2-color logo (with cyan Beam) for primary applications
|
|
101
|
+
- Use reverse (white) logo on dark backgrounds
|
|
102
|
+
- Minimum clear space: equal to cap height of logo
|
|
103
|
+
- Minimum width: 120px (digital), 1 inch (print)
|
|
104
|
+
|
|
105
|
+
**Don'ts**:
|
|
106
|
+
- Never colorize, rotate, stretch, or add strokes to the logo
|
|
107
|
+
- Never place on busy backgrounds
|
|
108
|
+
- Never modify proportions
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## IPORT Brand
|
|
113
|
+
|
|
114
|
+
### Color Palette
|
|
115
|
+
|
|
116
|
+
| Color | Hex | CSS Variable | Tailwind Class | Usage |
|
|
117
|
+
|-------|-----|--------------|----------------|-------|
|
|
118
|
+
| Orange | `#FC4C02` | `--iport-orange` | `bg-iport-orange` | Primary accent, CTAs |
|
|
119
|
+
| Black | `#0E1114` | `--iport-black` | `bg-iport-black` | Text, headers |
|
|
120
|
+
| Dark | `#0F161D` | `--iport-dark` | `bg-iport-dark` | Page backgrounds |
|
|
121
|
+
| Dark Gray | `#1C1E20` | `--iport-dark-gray` | `bg-iport-dark-gray` | Card backgrounds |
|
|
122
|
+
| Medium Gray | `#25282A` | `--iport-medium-gray` | `bg-iport-medium-gray` | Secondary surfaces |
|
|
123
|
+
| Gray | `#3A3D3F` | `--iport-gray` | `bg-iport-gray` | Borders, dividers |
|
|
124
|
+
| Light Gray | `#CED6DB` | `--iport-light-gray` | `bg-iport-light-gray` | Secondary text |
|
|
125
|
+
| Off White | `#F2F2F2` | `--iport-off-white` | `bg-iport-off-white` | Highlights |
|
|
126
|
+
| White | `#FFFFFF` | `--iport-white` | `bg-iport-white` | Primary text on dark |
|
|
127
|
+
|
|
128
|
+
### Design Guidelines
|
|
129
|
+
|
|
130
|
+
- **Theme**: Dark UI preferred
|
|
131
|
+
- **Accent**: Orange `#FC4C02` for CTAs, highlights, interactive elements
|
|
132
|
+
- **Text**: White on dark backgrounds
|
|
133
|
+
- **Surfaces**: Use dark grays for layered depth
|
|
134
|
+
|
|
135
|
+
### Example Dark Theme Setup
|
|
136
|
+
```css
|
|
137
|
+
body {
|
|
138
|
+
background-color: #0F161D; /* iport-dark */
|
|
139
|
+
color: #FFFFFF; /* iport-white */
|
|
140
|
+
}
|
|
141
|
+
.card {
|
|
142
|
+
background-color: #1C1E20; /* iport-dark-gray */
|
|
143
|
+
border-color: #3A3D3F; /* iport-gray */
|
|
144
|
+
}
|
|
145
|
+
.button-primary {
|
|
146
|
+
background-color: #FC4C02; /* iport-orange */
|
|
147
|
+
color: #FFFFFF;
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Blaze Audio Brand
|
|
154
|
+
|
|
155
|
+
### Color Palette
|
|
156
|
+
|
|
157
|
+
| Color | Hex | CSS Variable | Tailwind Class | Usage |
|
|
158
|
+
|-------|-----|--------------|----------------|-------|
|
|
159
|
+
| Blue | `#00A3E1` | `--blaze-blue` | `bg-blaze-blue` | Primary accent |
|
|
160
|
+
| Red | `#C02B0A` | `--blaze-red` | `bg-blaze-red` | Secondary accent, alerts |
|
|
161
|
+
| Black | `#1A1A1C` | `--blaze-black` | `bg-blaze-black` | Text |
|
|
162
|
+
| Darkest | `#0A0B0F` | `--blaze-darkest` | `bg-blaze-darkest` | Deep backgrounds |
|
|
163
|
+
| Dark Gray | `#28282B` | `--blaze-dark-gray` | `bg-blaze-dark-gray` | Page backgrounds |
|
|
164
|
+
| Gray | `#313131` | `--blaze-gray` | `bg-blaze-gray` | Cards, surfaces |
|
|
165
|
+
| Medium Gray | `#575757` | `--blaze-medium-gray` | `bg-blaze-medium-gray` | Borders |
|
|
166
|
+
| Light Gray | `#838383` | `--blaze-light-gray` | `bg-blaze-light-gray` | Secondary text |
|
|
167
|
+
| Off White | `#F8F8F8` | `--blaze-off-white` | `bg-blaze-off-white` | Highlights |
|
|
168
|
+
| White | `#FFFFFF` | `--blaze-white` | `bg-blaze-white` | Primary text on dark |
|
|
169
|
+
|
|
170
|
+
### Design Guidelines
|
|
171
|
+
|
|
172
|
+
- **Theme**: Dark UI preferred (similar to IPORT)
|
|
173
|
+
- **Primary Accent**: Blue `#00A3E1` for CTAs, links, highlights
|
|
174
|
+
- **Secondary Accent**: Red `#C02B0A` for alerts, warnings, emphasis
|
|
175
|
+
- **Text**: White on dark backgrounds
|
|
176
|
+
|
|
177
|
+
### Example Dark Theme Setup
|
|
178
|
+
```css
|
|
179
|
+
body {
|
|
180
|
+
background-color: #28282B; /* blaze-dark-gray */
|
|
181
|
+
color: #FFFFFF; /* blaze-white */
|
|
182
|
+
}
|
|
183
|
+
.card {
|
|
184
|
+
background-color: #313131; /* blaze-gray */
|
|
185
|
+
border-color: #575757; /* blaze-medium-gray */
|
|
186
|
+
}
|
|
187
|
+
.button-primary {
|
|
188
|
+
background-color: #00A3E1; /* blaze-blue */
|
|
189
|
+
color: #FFFFFF;
|
|
190
|
+
}
|
|
191
|
+
.alert-error {
|
|
192
|
+
background-color: #C02B0A; /* blaze-red */
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Semantic Theme Variables
|
|
199
|
+
|
|
200
|
+
For building theme-aware components, use these semantic variables:
|
|
201
|
+
|
|
202
|
+
### Backgrounds
|
|
203
|
+
| Variable | Light Mode | Dark Mode |
|
|
204
|
+
|----------|------------|-----------|
|
|
205
|
+
| `--background` | `#FFFFFF` | `#1a1f24` |
|
|
206
|
+
| `--background-secondary` | `#f8f9fa` | `#242a31` |
|
|
207
|
+
| `--background-tertiary` | `#f0f2f3` | `#2d343c` |
|
|
208
|
+
| `--card` | `#FFFFFF` | `#242a31` |
|
|
209
|
+
|
|
210
|
+
### Text / Foreground
|
|
211
|
+
| Variable | Light Mode | Dark Mode |
|
|
212
|
+
|----------|------------|-----------|
|
|
213
|
+
| `--foreground` | `#333F48` | `#FFFFFF` |
|
|
214
|
+
| `--foreground-secondary` | `#515c64` | `#D9D9D6` |
|
|
215
|
+
| `--foreground-muted` | `#6b7780` | `#8f999f` |
|
|
216
|
+
|
|
217
|
+
### Interactive
|
|
218
|
+
| Variable | Light Mode | Dark Mode |
|
|
219
|
+
|----------|------------|-----------|
|
|
220
|
+
| `--primary` | `#333F48` | `#FFFFFF` |
|
|
221
|
+
| `--primary-foreground` | `#FFFFFF` | `#333F48` |
|
|
222
|
+
| `--border` | `#D9D9D6` | `#3a444c` |
|
|
223
|
+
|
|
224
|
+
### Feedback
|
|
225
|
+
| Variable | Usage |
|
|
226
|
+
|----------|-------|
|
|
227
|
+
| `--success` | Success states, confirmations |
|
|
228
|
+
| `--error` | Errors, destructive actions |
|
|
229
|
+
| `--warning` | Warnings, caution states |
|
|
230
|
+
| `--info` | Informational messages |
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Component Patterns
|
|
235
|
+
|
|
236
|
+
### Buttons
|
|
237
|
+
|
|
238
|
+
**Primary Button** (Sonance):
|
|
239
|
+
```jsx
|
|
240
|
+
<button className="bg-sonance-charcoal text-white px-6 py-3 text-sm font-medium tracking-wide uppercase hover:bg-sonance-gray-800 transition-colors">
|
|
241
|
+
Learn More
|
|
242
|
+
</button>
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Primary Button** (IPORT):
|
|
246
|
+
```jsx
|
|
247
|
+
<button className="bg-iport-orange text-white px-6 py-3 text-sm font-medium tracking-wide uppercase hover:opacity-90 transition-opacity">
|
|
248
|
+
Shop Now
|
|
249
|
+
</button>
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**Primary Button** (Blaze):
|
|
253
|
+
```jsx
|
|
254
|
+
<button className="bg-blaze-blue text-white px-6 py-3 text-sm font-medium tracking-wide uppercase hover:opacity-90 transition-opacity">
|
|
255
|
+
Explore
|
|
256
|
+
</button>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Cards
|
|
260
|
+
|
|
261
|
+
**Light Theme Card**:
|
|
262
|
+
```jsx
|
|
263
|
+
<div className="bg-white border border-sonance-light-gray rounded-sm p-6 shadow-sm">
|
|
264
|
+
<h3 className="text-lg font-medium text-sonance-charcoal">Card Title</h3>
|
|
265
|
+
<p className="text-foreground-secondary mt-2">Card content...</p>
|
|
266
|
+
</div>
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Dark Theme Card** (IPORT/Blaze):
|
|
270
|
+
```jsx
|
|
271
|
+
<div className="bg-iport-dark-gray border border-iport-gray rounded-sm p-6">
|
|
272
|
+
<h3 className="text-lg font-medium text-white">Card Title</h3>
|
|
273
|
+
<p className="text-iport-light-gray mt-2">Card content...</p>
|
|
274
|
+
</div>
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Section Headers
|
|
278
|
+
|
|
279
|
+
```jsx
|
|
280
|
+
<div className="mb-8">
|
|
281
|
+
<p className="text-xs font-medium uppercase tracking-widest text-foreground-muted mb-2">
|
|
282
|
+
Category Label
|
|
283
|
+
</p>
|
|
284
|
+
<h2 className="text-4xl font-light text-foreground tracking-tight">
|
|
285
|
+
Section Headline
|
|
286
|
+
</h2>
|
|
287
|
+
<p className="text-lg text-foreground-secondary mt-4 max-w-2xl">
|
|
288
|
+
Supporting description text that provides context.
|
|
289
|
+
</p>
|
|
290
|
+
</div>
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## Photography Guidelines
|
|
296
|
+
|
|
297
|
+
### Product Photography
|
|
298
|
+
- Clean, simple backgrounds (white or gradient)
|
|
299
|
+
- Products should "float" in space
|
|
300
|
+
- Show shadow and depth but avoid clutter
|
|
301
|
+
- Use natural light aesthetic
|
|
302
|
+
|
|
303
|
+
### Lifestyle Photography
|
|
304
|
+
- Rich, inviting scenes
|
|
305
|
+
- Premium architectural settings
|
|
306
|
+
- Products integrated naturally (invisible aesthetic)
|
|
307
|
+
- Warm, sophisticated color grading
|
|
308
|
+
|
|
309
|
+
**Don'ts**:
|
|
310
|
+
- No cartoons, illustrations, or non-photographic imagery
|
|
311
|
+
- No busy or cluttered backgrounds
|
|
312
|
+
- No artificial-looking lighting
|
|
313
|
+
- No low-resolution images
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Quick CSS Copy-Paste
|
|
318
|
+
|
|
319
|
+
### Minimal Brand Setup
|
|
320
|
+
```css
|
|
321
|
+
:root {
|
|
322
|
+
/* Sonance Core */
|
|
323
|
+
--sonance-charcoal: #333F48;
|
|
324
|
+
--sonance-blue: #00D3C8;
|
|
325
|
+
--sonance-light-gray: #D9D9D6;
|
|
326
|
+
|
|
327
|
+
/* IPORT Core */
|
|
328
|
+
--iport-orange: #FC4C02;
|
|
329
|
+
--iport-black: #0E1114;
|
|
330
|
+
--iport-dark: #0F161D;
|
|
331
|
+
|
|
332
|
+
/* Blaze Core */
|
|
333
|
+
--blaze-blue: #00A3E1;
|
|
334
|
+
--blaze-red: #C02B0A;
|
|
335
|
+
--blaze-dark-gray: #28282B;
|
|
336
|
+
|
|
337
|
+
/* Typography */
|
|
338
|
+
--font-primary: 'Montserrat', sans-serif;
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## AI Instructions Summary
|
|
345
|
+
|
|
346
|
+
When generating UI for these brands:
|
|
347
|
+
|
|
348
|
+
1. **Always use Montserrat** font family
|
|
349
|
+
2. **Check which brand** is being targeted (Sonance, IPORT, or Blaze)
|
|
350
|
+
3. **Apply the correct theme**:
|
|
351
|
+
- Sonance: Light theme, charcoal + cyan accents
|
|
352
|
+
- IPORT: Dark theme, dark grays + orange accents
|
|
353
|
+
- Blaze: Dark theme, dark grays + blue/red accents
|
|
354
|
+
4. **Use semantic classes** (`bg-primary`, `text-foreground`) when possible
|
|
355
|
+
5. **Headlines**: Light weight, negative tracking
|
|
356
|
+
6. **Body text**: Regular weight, generous line height
|
|
357
|
+
7. **All caps**: Apply letter-spacing (tracking)
|
|
358
|
+
8. **Buttons**: Uppercase, medium weight, tracked
|
|
359
|
+
9. **Cards**: Minimal borders, subtle shadows
|
|
360
|
+
10. **Spacing**: Generous whitespace, "breathable" layouts
|
|
361
|
+
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, createContext, useContext, useState } from "react";
|
|
4
|
+
import { ChevronDown } from "lucide-react";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
interface AccordionContextValue {
|
|
8
|
+
openItems: string[];
|
|
9
|
+
toggleItem: (value: string) => void;
|
|
10
|
+
type: "single" | "multiple";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const AccordionContext = createContext<AccordionContextValue | null>(null);
|
|
14
|
+
|
|
15
|
+
interface AccordionProps {
|
|
16
|
+
type?: "single" | "multiple";
|
|
17
|
+
defaultValue?: string | string[];
|
|
18
|
+
className?: string;
|
|
19
|
+
children: React.ReactNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Accordion({
|
|
23
|
+
type = "single",
|
|
24
|
+
defaultValue,
|
|
25
|
+
className,
|
|
26
|
+
children,
|
|
27
|
+
}: AccordionProps) {
|
|
28
|
+
const [openItems, setOpenItems] = useState<string[]>(() => {
|
|
29
|
+
if (!defaultValue) return [];
|
|
30
|
+
return Array.isArray(defaultValue) ? defaultValue : [defaultValue];
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const toggleItem = (value: string) => {
|
|
34
|
+
setOpenItems((prev) => {
|
|
35
|
+
if (type === "single") {
|
|
36
|
+
return prev.includes(value) ? [] : [value];
|
|
37
|
+
}
|
|
38
|
+
return prev.includes(value)
|
|
39
|
+
? prev.filter((v) => v !== value)
|
|
40
|
+
: [...prev, value];
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<AccordionContext.Provider value={{ openItems, toggleItem, type }}>
|
|
46
|
+
<div className={cn("w-full divide-y divide-border border-y border-border", className)}>
|
|
47
|
+
{children}
|
|
48
|
+
</div>
|
|
49
|
+
</AccordionContext.Provider>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface AccordionItemProps {
|
|
54
|
+
value: string;
|
|
55
|
+
className?: string;
|
|
56
|
+
children: React.ReactNode;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const AccordionItemContext = createContext<{ value: string; isOpen: boolean } | null>(null);
|
|
60
|
+
|
|
61
|
+
export function AccordionItem({ value, className, children }: AccordionItemProps) {
|
|
62
|
+
const context = useContext(AccordionContext);
|
|
63
|
+
if (!context) throw new Error("AccordionItem must be used within Accordion");
|
|
64
|
+
|
|
65
|
+
const isOpen = context.openItems.includes(value);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<AccordionItemContext.Provider value={{ value, isOpen }}>
|
|
69
|
+
<div className={cn("", className)} data-state={isOpen ? "open" : "closed"}>
|
|
70
|
+
{children}
|
|
71
|
+
</div>
|
|
72
|
+
</AccordionItemContext.Provider>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const AccordionTrigger = forwardRef<
|
|
77
|
+
HTMLButtonElement,
|
|
78
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>
|
|
79
|
+
>(({ className, children, ...props }, ref) => {
|
|
80
|
+
const accordionContext = useContext(AccordionContext);
|
|
81
|
+
const itemContext = useContext(AccordionItemContext);
|
|
82
|
+
|
|
83
|
+
if (!accordionContext || !itemContext) {
|
|
84
|
+
throw new Error("AccordionTrigger must be used within AccordionItem");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<button
|
|
89
|
+
ref={ref}
|
|
90
|
+
type="button"
|
|
91
|
+
onClick={() => accordionContext.toggleItem(itemContext.value)}
|
|
92
|
+
className={cn(
|
|
93
|
+
"flex w-full items-center justify-between py-4 text-left font-medium text-foreground",
|
|
94
|
+
"transition-colors hover:text-foreground-secondary",
|
|
95
|
+
className
|
|
96
|
+
)}
|
|
97
|
+
aria-expanded={itemContext.isOpen}
|
|
98
|
+
{...props}
|
|
99
|
+
>
|
|
100
|
+
{children}
|
|
101
|
+
<ChevronDown
|
|
102
|
+
className={cn(
|
|
103
|
+
"h-4 w-4 shrink-0 text-foreground-muted transition-transform duration-200",
|
|
104
|
+
itemContext.isOpen && "rotate-180"
|
|
105
|
+
)}
|
|
106
|
+
/>
|
|
107
|
+
</button>
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
AccordionTrigger.displayName = "AccordionTrigger";
|
|
112
|
+
|
|
113
|
+
export const AccordionContent = forwardRef<
|
|
114
|
+
HTMLDivElement,
|
|
115
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
116
|
+
>(({ className, children, ...props }, ref) => {
|
|
117
|
+
const itemContext = useContext(AccordionItemContext);
|
|
118
|
+
|
|
119
|
+
if (!itemContext) {
|
|
120
|
+
throw new Error("AccordionContent must be used within AccordionItem");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div
|
|
125
|
+
ref={ref}
|
|
126
|
+
className={cn(
|
|
127
|
+
"overflow-hidden text-sm text-foreground-secondary",
|
|
128
|
+
"transition-all duration-200",
|
|
129
|
+
itemContext.isOpen ? "pb-4" : "h-0",
|
|
130
|
+
className
|
|
131
|
+
)}
|
|
132
|
+
aria-hidden={!itemContext.isOpen}
|
|
133
|
+
{...props}
|
|
134
|
+
>
|
|
135
|
+
{itemContext.isOpen && children}
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
AccordionContent.displayName = "AccordionContent";
|
|
141
|
+
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { AlertCircle, CheckCircle, Info, AlertTriangle, X } from "lucide-react";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
const alertVariants = cva(
|
|
7
|
+
"relative flex w-full items-start gap-4 border p-4 transition-colors",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: "border-border bg-background-secondary text-foreground",
|
|
12
|
+
success: "border-success/30 bg-success-light text-success",
|
|
13
|
+
error: "border-error/30 bg-error-light text-error",
|
|
14
|
+
warning: "border-warning/30 bg-warning-light text-warning",
|
|
15
|
+
info: "border-info/30 bg-info-light text-info",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
defaultVariants: {
|
|
19
|
+
variant: "default",
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const iconMap = {
|
|
25
|
+
default: Info,
|
|
26
|
+
success: CheckCircle,
|
|
27
|
+
error: AlertCircle,
|
|
28
|
+
warning: AlertTriangle,
|
|
29
|
+
info: Info,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
interface AlertProps
|
|
33
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
34
|
+
VariantProps<typeof alertVariants> {
|
|
35
|
+
title?: string;
|
|
36
|
+
onClose?: () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const Alert = forwardRef<HTMLDivElement, AlertProps>(
|
|
40
|
+
({ className, variant = "default", title, children, onClose, ...props }, ref) => {
|
|
41
|
+
const Icon = iconMap[variant || "default"];
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
ref={ref}
|
|
46
|
+
role="alert"
|
|
47
|
+
className={cn(alertVariants({ variant }), className)}
|
|
48
|
+
{...props}
|
|
49
|
+
>
|
|
50
|
+
<Icon className="h-5 w-5 shrink-0" />
|
|
51
|
+
<div className="flex-1">
|
|
52
|
+
{title && (
|
|
53
|
+
<h5 className="mb-1 font-medium leading-none tracking-tight">
|
|
54
|
+
{title}
|
|
55
|
+
</h5>
|
|
56
|
+
)}
|
|
57
|
+
{children && (
|
|
58
|
+
<div className={cn("text-sm", variant === "default" && "text-foreground-secondary")}>
|
|
59
|
+
{children}
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
{onClose && (
|
|
64
|
+
<button
|
|
65
|
+
onClick={onClose}
|
|
66
|
+
className="shrink-0 rounded-sm p-1 opacity-70 transition-opacity hover:opacity-100"
|
|
67
|
+
>
|
|
68
|
+
<X className="h-4 w-4" />
|
|
69
|
+
<span className="sr-only">Close</span>
|
|
70
|
+
</button>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
Alert.displayName = "Alert";
|
|
78
|
+
|