@moontra/moonui 3.1.0 → 3.3.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/chunk-VXDFZPDA.global.js +33 -0
- package/dist/{chunk-X5ULQZNC.global.js.map → chunk-VXDFZPDA.global.js.map} +1 -1
- package/dist/index.d.mts +140 -1
- package/dist/index.d.ts +140 -1
- package/dist/index.global.js +365 -52
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +419 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +399 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/src/components/ui/__tests__/button-group.test.tsx +183 -0
- package/src/components/ui/__tests__/kbd.test.tsx +86 -0
- package/src/components/ui/__tests__/rating.test.tsx +218 -0
- package/src/components/ui/__tests__/spinner.test.tsx +141 -0
- package/src/components/ui/__tests__/toggle-group.test.tsx +293 -0
- package/src/components/ui/button-group.tsx +125 -0
- package/src/components/ui/index.ts +43 -0
- package/src/components/ui/kbd.tsx +66 -0
- package/src/components/ui/rating.tsx +253 -0
- package/src/components/ui/spinner.tsx +112 -0
- package/src/components/ui/toggle-group.tsx +93 -0
- package/dist/chunk-X5ULQZNC.global.js +0 -33
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import '@testing-library/jest-dom'
|
|
4
|
+
|
|
5
|
+
import { ToggleGroup, ToggleGroupItem } from '../toggle-group'
|
|
6
|
+
|
|
7
|
+
describe('ToggleGroup Components', () => {
|
|
8
|
+
describe('Rendering', () => {
|
|
9
|
+
it('renders group with items', () => {
|
|
10
|
+
render(
|
|
11
|
+
<ToggleGroup type="single" data-testid="group">
|
|
12
|
+
<ToggleGroupItem value="a">A</ToggleGroupItem>
|
|
13
|
+
<ToggleGroupItem value="b">B</ToggleGroupItem>
|
|
14
|
+
</ToggleGroup>
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const group = screen.getByTestId('group')
|
|
18
|
+
expect(group).toBeInTheDocument()
|
|
19
|
+
expect(screen.getByText('A')).toBeInTheDocument()
|
|
20
|
+
expect(screen.getByText('B')).toBeInTheDocument()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('applies base + custom className on the root', () => {
|
|
24
|
+
render(
|
|
25
|
+
<ToggleGroup type="single" className="custom-group" data-testid="group">
|
|
26
|
+
<ToggleGroupItem value="a">A</ToggleGroupItem>
|
|
27
|
+
</ToggleGroup>
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
const group = screen.getByTestId('group')
|
|
31
|
+
// moonui-theme + FREE token/layout base sınıfları korunur
|
|
32
|
+
expect(group).toHaveClass('moonui-theme', 'inline-flex', 'items-center', 'gap-1')
|
|
33
|
+
expect(group).toHaveClass('custom-group')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('applies custom className on an item alongside toggleVariants', () => {
|
|
37
|
+
render(
|
|
38
|
+
<ToggleGroup type="single">
|
|
39
|
+
<ToggleGroupItem value="a" className="custom-item">
|
|
40
|
+
A
|
|
41
|
+
</ToggleGroupItem>
|
|
42
|
+
</ToggleGroup>
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const item = screen.getByText('A')
|
|
46
|
+
expect(item).toHaveClass('custom-item')
|
|
47
|
+
// toggleVariants'tan gelen aktif-durum token sınıfı
|
|
48
|
+
expect(item).toHaveClass('data-[state=on]:bg-accent')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('forwards ref on the group', () => {
|
|
52
|
+
const ref = React.createRef<HTMLDivElement>()
|
|
53
|
+
render(
|
|
54
|
+
<ToggleGroup type="single" ref={ref}>
|
|
55
|
+
<ToggleGroupItem value="a">A</ToggleGroupItem>
|
|
56
|
+
</ToggleGroup>
|
|
57
|
+
)
|
|
58
|
+
expect(ref.current).toBeInstanceOf(HTMLDivElement)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('forwards ref on the item', () => {
|
|
62
|
+
const ref = React.createRef<HTMLButtonElement>()
|
|
63
|
+
render(
|
|
64
|
+
<ToggleGroup type="single">
|
|
65
|
+
<ToggleGroupItem value="a" ref={ref}>
|
|
66
|
+
A
|
|
67
|
+
</ToggleGroupItem>
|
|
68
|
+
</ToggleGroup>
|
|
69
|
+
)
|
|
70
|
+
expect(ref.current).toBeInstanceOf(HTMLButtonElement)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('maintains displayNames', () => {
|
|
74
|
+
expect(ToggleGroup.displayName).toBe('ToggleGroup')
|
|
75
|
+
expect(ToggleGroupItem.displayName).toBe('ToggleGroupItem')
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('Single selection (type="single")', () => {
|
|
80
|
+
it('marks exactly one item active via defaultValue', () => {
|
|
81
|
+
render(
|
|
82
|
+
<ToggleGroup type="single" defaultValue="b">
|
|
83
|
+
<ToggleGroupItem value="a">A</ToggleGroupItem>
|
|
84
|
+
<ToggleGroupItem value="b">B</ToggleGroupItem>
|
|
85
|
+
</ToggleGroup>
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
expect(screen.getByText('A')).toHaveAttribute('data-state', 'off')
|
|
89
|
+
expect(screen.getByText('B')).toHaveAttribute('data-state', 'on')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('respects controlled value', () => {
|
|
93
|
+
render(
|
|
94
|
+
<ToggleGroup type="single" value="a">
|
|
95
|
+
<ToggleGroupItem value="a">A</ToggleGroupItem>
|
|
96
|
+
<ToggleGroupItem value="b">B</ToggleGroupItem>
|
|
97
|
+
</ToggleGroup>
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
expect(screen.getByText('A')).toHaveAttribute('data-state', 'on')
|
|
101
|
+
expect(screen.getByText('B')).toHaveAttribute('data-state', 'off')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('fires onValueChange with a string on click', () => {
|
|
105
|
+
const onValueChange = jest.fn()
|
|
106
|
+
render(
|
|
107
|
+
<ToggleGroup type="single" onValueChange={onValueChange}>
|
|
108
|
+
<ToggleGroupItem value="a">A</ToggleGroupItem>
|
|
109
|
+
<ToggleGroupItem value="b">B</ToggleGroupItem>
|
|
110
|
+
</ToggleGroup>
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
fireEvent.click(screen.getByText('B'))
|
|
114
|
+
expect(onValueChange).toHaveBeenCalledWith('b')
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('Multiple selection (type="multiple")', () => {
|
|
119
|
+
it('marks multiple items active via defaultValue', () => {
|
|
120
|
+
render(
|
|
121
|
+
<ToggleGroup type="multiple" defaultValue={['a', 'b']}>
|
|
122
|
+
<ToggleGroupItem value="a">A</ToggleGroupItem>
|
|
123
|
+
<ToggleGroupItem value="b">B</ToggleGroupItem>
|
|
124
|
+
<ToggleGroupItem value="c">C</ToggleGroupItem>
|
|
125
|
+
</ToggleGroup>
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
expect(screen.getByText('A')).toHaveAttribute('data-state', 'on')
|
|
129
|
+
expect(screen.getByText('B')).toHaveAttribute('data-state', 'on')
|
|
130
|
+
expect(screen.getByText('C')).toHaveAttribute('data-state', 'off')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('fires onValueChange with an array on click', () => {
|
|
134
|
+
const onValueChange = jest.fn()
|
|
135
|
+
render(
|
|
136
|
+
<ToggleGroup type="multiple" defaultValue={['a']} onValueChange={onValueChange}>
|
|
137
|
+
<ToggleGroupItem value="a">A</ToggleGroupItem>
|
|
138
|
+
<ToggleGroupItem value="b">B</ToggleGroupItem>
|
|
139
|
+
</ToggleGroup>
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
fireEvent.click(screen.getByText('B'))
|
|
143
|
+
expect(onValueChange).toHaveBeenCalledWith(['a', 'b'])
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('Variant / size context propagation', () => {
|
|
148
|
+
it('propagates group variant + size to items', () => {
|
|
149
|
+
render(
|
|
150
|
+
<ToggleGroup type="single" variant="outline" size="lg">
|
|
151
|
+
<ToggleGroupItem value="a">A</ToggleGroupItem>
|
|
152
|
+
</ToggleGroup>
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
const item = screen.getByText('A')
|
|
156
|
+
// outline variant + lg size (toggleVariants ile birebir)
|
|
157
|
+
expect(item).toHaveClass('border', 'border-input')
|
|
158
|
+
expect(item).toHaveClass('h-11', 'px-5')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('uses item prop when the group provides no variant/size', () => {
|
|
162
|
+
render(
|
|
163
|
+
<ToggleGroup type="single">
|
|
164
|
+
<ToggleGroupItem value="a" variant="outline" size="sm">
|
|
165
|
+
A
|
|
166
|
+
</ToggleGroupItem>
|
|
167
|
+
</ToggleGroup>
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
const item = screen.getByText('A')
|
|
171
|
+
// context tanımsız → item prop devreye girer
|
|
172
|
+
expect(item).toHaveClass('border', 'border-input')
|
|
173
|
+
expect(item).toHaveClass('h-9', 'px-2.5')
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('group variant overrides item variant (context wins)', () => {
|
|
177
|
+
render(
|
|
178
|
+
<ToggleGroup type="single" variant="outline">
|
|
179
|
+
<ToggleGroupItem value="a" variant="default">
|
|
180
|
+
A
|
|
181
|
+
</ToggleGroupItem>
|
|
182
|
+
</ToggleGroup>
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
const item = screen.getByText('A')
|
|
186
|
+
// context.variant ?? props.variant → outline kazanır
|
|
187
|
+
expect(item).toHaveClass('border', 'border-input')
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('falls back to default variant/size when nothing is set', () => {
|
|
191
|
+
render(
|
|
192
|
+
<ToggleGroup type="single">
|
|
193
|
+
<ToggleGroupItem value="a">A</ToggleGroupItem>
|
|
194
|
+
</ToggleGroup>
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
const item = screen.getByText('A')
|
|
198
|
+
// default variant: bg-transparent, default size: h-10 px-3
|
|
199
|
+
expect(item).toHaveClass('bg-transparent', 'h-10', 'px-3')
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
describe('Disabled state', () => {
|
|
204
|
+
it('disables all items when the group is disabled', () => {
|
|
205
|
+
render(
|
|
206
|
+
<ToggleGroup type="single" disabled>
|
|
207
|
+
<ToggleGroupItem value="a">A</ToggleGroupItem>
|
|
208
|
+
<ToggleGroupItem value="b">B</ToggleGroupItem>
|
|
209
|
+
</ToggleGroup>
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
expect(screen.getByText('A')).toBeDisabled()
|
|
213
|
+
expect(screen.getByText('B')).toBeDisabled()
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('disables an individual item', () => {
|
|
217
|
+
const onValueChange = jest.fn()
|
|
218
|
+
render(
|
|
219
|
+
<ToggleGroup type="single" onValueChange={onValueChange}>
|
|
220
|
+
<ToggleGroupItem value="a">A</ToggleGroupItem>
|
|
221
|
+
<ToggleGroupItem value="b" disabled>
|
|
222
|
+
B
|
|
223
|
+
</ToggleGroupItem>
|
|
224
|
+
</ToggleGroup>
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
const disabledItem = screen.getByText('B')
|
|
228
|
+
expect(disabledItem).toBeDisabled()
|
|
229
|
+
fireEvent.click(disabledItem)
|
|
230
|
+
expect(onValueChange).not.toHaveBeenCalled()
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
describe('Accessibility (Radix-provided)', () => {
|
|
235
|
+
it('single group exposes radiogroup + radio roles', () => {
|
|
236
|
+
render(
|
|
237
|
+
<ToggleGroup type="single" data-testid="group">
|
|
238
|
+
<ToggleGroupItem value="a">A</ToggleGroupItem>
|
|
239
|
+
<ToggleGroupItem value="b">B</ToggleGroupItem>
|
|
240
|
+
</ToggleGroup>
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
expect(screen.getByTestId('group')).toHaveAttribute('role', 'radiogroup')
|
|
244
|
+
expect(screen.getAllByRole('radio')).toHaveLength(2)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('single active item reflects aria-checked', () => {
|
|
248
|
+
render(
|
|
249
|
+
<ToggleGroup type="single" defaultValue="a">
|
|
250
|
+
<ToggleGroupItem value="a">A</ToggleGroupItem>
|
|
251
|
+
<ToggleGroupItem value="b">B</ToggleGroupItem>
|
|
252
|
+
</ToggleGroup>
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
expect(screen.getByText('A')).toHaveAttribute('aria-checked', 'true')
|
|
256
|
+
expect(screen.getByText('B')).toHaveAttribute('aria-checked', 'false')
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('multiple group exposes toolbar role + aria-pressed items', () => {
|
|
260
|
+
render(
|
|
261
|
+
<ToggleGroup type="multiple" defaultValue={['a']} data-testid="group">
|
|
262
|
+
<ToggleGroupItem value="a">A</ToggleGroupItem>
|
|
263
|
+
<ToggleGroupItem value="b">B</ToggleGroupItem>
|
|
264
|
+
</ToggleGroup>
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
expect(screen.getByTestId('group')).toHaveAttribute('role', 'toolbar')
|
|
268
|
+
expect(screen.getByText('A')).toHaveAttribute('aria-pressed', 'true')
|
|
269
|
+
expect(screen.getByText('B')).toHaveAttribute('aria-pressed', 'false')
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('renders items as focusable buttons with Radix roving tabindex', () => {
|
|
273
|
+
render(
|
|
274
|
+
<ToggleGroup type="single" defaultValue="a">
|
|
275
|
+
<ToggleGroupItem value="a">A</ToggleGroupItem>
|
|
276
|
+
<ToggleGroupItem value="b">B</ToggleGroupItem>
|
|
277
|
+
</ToggleGroup>
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
const first = screen.getByText('A')
|
|
281
|
+
const second = screen.getByText('B')
|
|
282
|
+
|
|
283
|
+
// Radix RovingFocusGroup her item'a bir tabindex atar (roving nav altyapısı).
|
|
284
|
+
// Ok-tuşuyla odak taşıma gerçek tarayıcıda Radix tarafından sağlanır;
|
|
285
|
+
// jsdom odak olaylarını tam simüle etmediği için burada odaklanabilirlik doğrulanır.
|
|
286
|
+
expect(first).toHaveAttribute('tabindex')
|
|
287
|
+
expect(second).toHaveAttribute('tabindex')
|
|
288
|
+
|
|
289
|
+
first.focus()
|
|
290
|
+
expect(first).toHaveFocus()
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
})
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
import { Button, type ButtonProps } from "./button";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Premium ButtonGroup Component
|
|
10
|
+
*
|
|
11
|
+
* Bitişik (segmentli) veya boşluklu buton grupları için yüksek kaliteli,
|
|
12
|
+
* erişilebilir bir kapsayıcı bileşen. Yatay/dikey yerleşim, bitişik kenar
|
|
13
|
+
* radius birleştirme ve çocuk `<Button>`'lara size/variant yayılımı sunar.
|
|
14
|
+
*
|
|
15
|
+
* Tamamen token-tabanlıdır: kenarlık ve köşe yuvarlama değerleri mevcut Button
|
|
16
|
+
* token'larından gelir; hardcoded renk YOKtur.
|
|
17
|
+
*/
|
|
18
|
+
const buttonGroupVariants = cva(
|
|
19
|
+
// Temel kapsayıcı: inline-flex; çocuklar akış içinde hizalanır
|
|
20
|
+
"moonui-theme inline-flex",
|
|
21
|
+
{
|
|
22
|
+
variants: {
|
|
23
|
+
orientation: {
|
|
24
|
+
// Yatay: butonlar dikeyde ortalanır (içerik-genişliği korunur)
|
|
25
|
+
horizontal: "flex-row items-center",
|
|
26
|
+
// Dikey: butonlar aynı genişliğe uzar (items-stretch) → kenarlar hizalanır
|
|
27
|
+
vertical: "flex-col items-stretch",
|
|
28
|
+
},
|
|
29
|
+
attached: {
|
|
30
|
+
// Bitişik: boşluk yok; radius birleştirme compoundVariants ile uygulanır
|
|
31
|
+
true: "",
|
|
32
|
+
// Boşluklu: normal aralık, radius korunur
|
|
33
|
+
false: "gap-2",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
compoundVariants: [
|
|
37
|
+
{
|
|
38
|
+
// Yatay + bitişik: bitişik yatay kenarlar düzleşir, -ml-px ile kenarlık örtüşür
|
|
39
|
+
orientation: "horizontal",
|
|
40
|
+
attached: true,
|
|
41
|
+
class: [
|
|
42
|
+
"[&>*:not(:first-child)]:rounded-l-none",
|
|
43
|
+
"[&>*:not(:last-child)]:rounded-r-none",
|
|
44
|
+
"[&>*:not(:first-child)]:-ml-px",
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
// Dikey + bitişik: bitişik dikey kenarlar düzleşir, -mt-px ile kenarlık örtüşür
|
|
49
|
+
orientation: "vertical",
|
|
50
|
+
attached: true,
|
|
51
|
+
class: [
|
|
52
|
+
"[&>*:not(:first-child)]:rounded-t-none",
|
|
53
|
+
"[&>*:not(:last-child)]:rounded-b-none",
|
|
54
|
+
"[&>*:not(:first-child)]:-mt-px",
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
defaultVariants: {
|
|
59
|
+
orientation: "horizontal",
|
|
60
|
+
attached: true,
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// ButtonGroup component props
|
|
66
|
+
export interface ButtonGroupProps
|
|
67
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
68
|
+
VariantProps<typeof buttonGroupVariants> {
|
|
69
|
+
/** Çocuk Button'lara yayılacak boyut (yalnızca çocuk kendi size'ını vermemişse) */
|
|
70
|
+
size?: ButtonProps["size"];
|
|
71
|
+
/** Çocuk Button'lara yayılacak varyant (yalnızca çocuk kendi variant'ını vermemişse) */
|
|
72
|
+
variant?: ButtonProps["variant"];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Premium ButtonGroup Component
|
|
77
|
+
*
|
|
78
|
+
* @param props - ButtonGroup bileşeni özellikleri
|
|
79
|
+
* @param props.orientation - Yerleşim yönü ("horizontal" | "vertical"), varsayılan "horizontal"
|
|
80
|
+
* @param props.attached - Bitişik (segmentli) mi yoksa boşluklu mu, varsayılan true
|
|
81
|
+
* @param props.size - Çocuk Button'lara yayılacak boyut (opsiyonel)
|
|
82
|
+
* @param props.variant - Çocuk Button'lara yayılacak varyant (opsiyonel)
|
|
83
|
+
*/
|
|
84
|
+
const ButtonGroup = React.forwardRef<HTMLDivElement, ButtonGroupProps>(
|
|
85
|
+
(
|
|
86
|
+
{ className, orientation, attached, size, variant, children, ...props },
|
|
87
|
+
ref
|
|
88
|
+
) => {
|
|
89
|
+
// size/variant yalnızca en az biri tanımlıysa çocuklara enjekte edilir
|
|
90
|
+
const shouldPropagate = size !== undefined || variant !== undefined;
|
|
91
|
+
|
|
92
|
+
const enhancedChildren = shouldPropagate
|
|
93
|
+
? React.Children.map(children, (child) => {
|
|
94
|
+
// Geçersiz element (string/number/null) veya Button olmayan çocuklar
|
|
95
|
+
// GRACEFUL geçilir — geçersiz prop enjekte edilmez
|
|
96
|
+
if (!React.isValidElement(child) || child.type !== Button) {
|
|
97
|
+
return child;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const childProps = child.props as ButtonProps;
|
|
101
|
+
|
|
102
|
+
// Çocuğun kendi prop'u önceliklidir (child.props.size ?? size)
|
|
103
|
+
return React.cloneElement(child as React.ReactElement<ButtonProps>, {
|
|
104
|
+
size: childProps.size ?? size,
|
|
105
|
+
variant: childProps.variant ?? variant,
|
|
106
|
+
});
|
|
107
|
+
})
|
|
108
|
+
: children;
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div
|
|
112
|
+
ref={ref}
|
|
113
|
+
role="group"
|
|
114
|
+
className={cn(buttonGroupVariants({ orientation, attached }), className)}
|
|
115
|
+
{...props}
|
|
116
|
+
>
|
|
117
|
+
{enhancedChildren}
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
ButtonGroup.displayName = "ButtonGroup";
|
|
124
|
+
|
|
125
|
+
export { ButtonGroup, buttonGroupVariants };
|
|
@@ -424,6 +424,44 @@ export {
|
|
|
424
424
|
TooltipProvider as MoonUITooltipProvider,
|
|
425
425
|
} from "./tooltip";
|
|
426
426
|
|
|
427
|
+
// Kbd (issue #247) — standalone klavye-tuşu rozeti
|
|
428
|
+
export {
|
|
429
|
+
Kbd as MoonUIKbd,
|
|
430
|
+
kbdVariants as moonUIKbdVariants,
|
|
431
|
+
} from "./kbd";
|
|
432
|
+
export type { KbdProps as MoonUIKbdProps } from "./kbd";
|
|
433
|
+
|
|
434
|
+
// Rating (issue #247) — standalone yıldız değerlendirme
|
|
435
|
+
export {
|
|
436
|
+
Rating as MoonUIRating,
|
|
437
|
+
ratingVariants as moonUIRatingVariants,
|
|
438
|
+
} from "./rating";
|
|
439
|
+
export type { RatingProps as MoonUIRatingProps } from "./rating";
|
|
440
|
+
|
|
441
|
+
// Spinner (issue #247) — standalone yükleme göstergesi
|
|
442
|
+
export {
|
|
443
|
+
Spinner as MoonUISpinner,
|
|
444
|
+
spinnerVariants as moonUISpinnerVariants,
|
|
445
|
+
} from "./spinner";
|
|
446
|
+
export type { SpinnerProps as MoonUISpinnerProps } from "./spinner";
|
|
447
|
+
|
|
448
|
+
// Button Group (issue #257) — bitişik/segmentli buton grupları
|
|
449
|
+
export {
|
|
450
|
+
ButtonGroup as MoonUIButtonGroup,
|
|
451
|
+
buttonGroupVariants as moonUIButtonGroupVariants,
|
|
452
|
+
} from "./button-group";
|
|
453
|
+
export type { ButtonGroupProps as MoonUIButtonGroupProps } from "./button-group";
|
|
454
|
+
|
|
455
|
+
// Toggle Group (issue #257) — tek/çoklu seçim segmented toggle seti
|
|
456
|
+
export {
|
|
457
|
+
ToggleGroup as MoonUIToggleGroup,
|
|
458
|
+
ToggleGroupItem as MoonUIToggleGroupItem,
|
|
459
|
+
} from "./toggle-group";
|
|
460
|
+
export type {
|
|
461
|
+
ToggleGroupProps as MoonUIToggleGroupProps,
|
|
462
|
+
ToggleGroupItemProps as MoonUIToggleGroupItemProps,
|
|
463
|
+
} from "./toggle-group";
|
|
464
|
+
|
|
427
465
|
// Also export without MoonUI prefix for backward compatibility
|
|
428
466
|
export * from "./accordion";
|
|
429
467
|
export * from "./alert";
|
|
@@ -473,3 +511,8 @@ export * from "./textarea";
|
|
|
473
511
|
export * from "./toast";
|
|
474
512
|
export * from "./toggle";
|
|
475
513
|
export * from "./tooltip";
|
|
514
|
+
export * from "./kbd";
|
|
515
|
+
export * from "./rating";
|
|
516
|
+
export * from "./spinner";
|
|
517
|
+
export * from "./button-group";
|
|
518
|
+
export * from "./toggle-group";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Premium Kbd Component
|
|
9
|
+
*
|
|
10
|
+
* Klavye tuşlarını (⌘, Ctrl, K vb.) görsel olarak temsil eden standalone
|
|
11
|
+
* bir rozet bileşeni. Dark ve light modda uyumlu, token tabanlı stiller
|
|
12
|
+
* kullanır ve hafif kabartma/gölge ile fiziksel bir tuş görünümü verir.
|
|
13
|
+
*/
|
|
14
|
+
const kbdVariants = cva(
|
|
15
|
+
[
|
|
16
|
+
// Temel klavye-tuşu görünümü — token tabanlı, hardcoded renk yok
|
|
17
|
+
"inline-flex items-center justify-center rounded",
|
|
18
|
+
"border border-border bg-muted",
|
|
19
|
+
"px-1.5 font-mono font-medium text-muted-foreground",
|
|
20
|
+
// Alt kenarda hafif kabartma (border token'ı ile — [#...] hex sınıfı DEĞİL)
|
|
21
|
+
"shadow-[0_1px_0_1px_hsl(var(--border))]",
|
|
22
|
+
"select-none whitespace-nowrap",
|
|
23
|
+
],
|
|
24
|
+
{
|
|
25
|
+
variants: {
|
|
26
|
+
size: {
|
|
27
|
+
sm: "h-5 min-w-5 text-[10px]",
|
|
28
|
+
md: "h-6 min-w-6 text-xs",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
defaultVariants: {
|
|
32
|
+
size: "md",
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export interface KbdProps
|
|
38
|
+
extends React.HTMLAttributes<HTMLElement>,
|
|
39
|
+
VariantProps<typeof kbdVariants> {
|
|
40
|
+
/** Tuş içeriği (⌘, K, Ctrl vb.) */
|
|
41
|
+
children: React.ReactNode;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Premium Kbd Component
|
|
46
|
+
*
|
|
47
|
+
* @param props - Kbd bileşeni özellikleri
|
|
48
|
+
* @param props.size - Tuş boyutu ("sm" | "md")
|
|
49
|
+
* @param props.children - Tuş içeriği (⌘, K, Ctrl vb.)
|
|
50
|
+
*/
|
|
51
|
+
const Kbd = React.forwardRef<HTMLElement, KbdProps>(
|
|
52
|
+
({ className, size, children, ...props }, ref) => {
|
|
53
|
+
return (
|
|
54
|
+
<kbd
|
|
55
|
+
ref={ref}
|
|
56
|
+
className={cn("moonui-theme", kbdVariants({ size }), className)}
|
|
57
|
+
{...props}
|
|
58
|
+
>
|
|
59
|
+
{children}
|
|
60
|
+
</kbd>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
Kbd.displayName = "Kbd";
|
|
65
|
+
|
|
66
|
+
export { Kbd, kbdVariants };
|