@open-mercato/ui 0.6.4-develop.4199.1.86677441c2 → 0.6.4-develop.4210.1.d412061cfe

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 (92) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +14 -0
  3. package/dist/backend/DataTable.js +28 -62
  4. package/dist/backend/DataTable.js.map +2 -2
  5. package/dist/backend/detail/AttachmentMetadataDialog.js +1 -1
  6. package/dist/backend/detail/AttachmentMetadataDialog.js.map +2 -2
  7. package/dist/primitives/activity-feed.js +196 -0
  8. package/dist/primitives/activity-feed.js.map +7 -0
  9. package/dist/primitives/avatar.js +140 -14
  10. package/dist/primitives/avatar.js.map +3 -3
  11. package/dist/primitives/badge.js +94 -5
  12. package/dist/primitives/badge.js.map +2 -2
  13. package/dist/primitives/button-group.js +52 -0
  14. package/dist/primitives/button-group.js.map +7 -0
  15. package/dist/primitives/color-picker.js +520 -0
  16. package/dist/primitives/color-picker.js.map +7 -0
  17. package/dist/primitives/command-menu.js +355 -0
  18. package/dist/primitives/command-menu.js.map +7 -0
  19. package/dist/primitives/dialog.js +210 -35
  20. package/dist/primitives/dialog.js.map +2 -2
  21. package/dist/primitives/drawer.js +219 -0
  22. package/dist/primitives/drawer.js.map +7 -0
  23. package/dist/primitives/notification-feed.js +213 -0
  24. package/dist/primitives/notification-feed.js.map +7 -0
  25. package/dist/primitives/notification.js +23 -2
  26. package/dist/primitives/notification.js.map +2 -2
  27. package/dist/primitives/pagination.js +245 -0
  28. package/dist/primitives/pagination.js.map +7 -0
  29. package/dist/primitives/progress.js +178 -5
  30. package/dist/primitives/progress.js.map +2 -2
  31. package/dist/primitives/rating.js +174 -0
  32. package/dist/primitives/rating.js.map +7 -0
  33. package/dist/primitives/scroll-area.js +82 -0
  34. package/dist/primitives/scroll-area.js.map +7 -0
  35. package/dist/primitives/segmented-control.js +101 -0
  36. package/dist/primitives/segmented-control.js.map +7 -0
  37. package/dist/primitives/separator.js +103 -4
  38. package/dist/primitives/separator.js.map +2 -2
  39. package/dist/primitives/slider.js +115 -0
  40. package/dist/primitives/slider.js.map +7 -0
  41. package/dist/primitives/step-indicator.js +187 -0
  42. package/dist/primitives/step-indicator.js.map +7 -0
  43. package/dist/primitives/table.js +102 -10
  44. package/dist/primitives/table.js.map +2 -2
  45. package/dist/primitives/tabs.js +122 -14
  46. package/dist/primitives/tabs.js.map +2 -2
  47. package/dist/primitives/time-picker.js +9 -11
  48. package/dist/primitives/time-picker.js.map +2 -2
  49. package/package.json +8 -4
  50. package/src/backend/DataTable.tsx +43 -60
  51. package/src/backend/detail/AttachmentMetadataDialog.tsx +1 -1
  52. package/src/primitives/__tests__/activity-feed.test.tsx +216 -0
  53. package/src/primitives/__tests__/avatar.test.tsx +131 -0
  54. package/src/primitives/__tests__/badge.test.tsx +189 -0
  55. package/src/primitives/__tests__/button-group.test.tsx +117 -0
  56. package/src/primitives/__tests__/color-picker.test.tsx +589 -0
  57. package/src/primitives/__tests__/command-menu.test.tsx +231 -0
  58. package/src/primitives/__tests__/dialog.test.tsx +290 -0
  59. package/src/primitives/__tests__/drawer.test.tsx +264 -0
  60. package/src/primitives/__tests__/notification-feed.test.tsx +303 -0
  61. package/src/primitives/__tests__/notification.test.tsx +102 -2
  62. package/src/primitives/__tests__/pagination.test.tsx +268 -0
  63. package/src/primitives/__tests__/progress.test.tsx +222 -0
  64. package/src/primitives/__tests__/rating.test.tsx +164 -0
  65. package/src/primitives/__tests__/scroll-area.test.tsx +109 -0
  66. package/src/primitives/__tests__/segmented-control.test.tsx +129 -0
  67. package/src/primitives/__tests__/separator.test.tsx +143 -0
  68. package/src/primitives/__tests__/slider.test.tsx +86 -0
  69. package/src/primitives/__tests__/step-indicator.test.tsx +205 -0
  70. package/src/primitives/__tests__/table.test.tsx +199 -0
  71. package/src/primitives/__tests__/tabs.test.tsx +139 -0
  72. package/src/primitives/activity-feed.tsx +326 -0
  73. package/src/primitives/avatar.tsx +225 -22
  74. package/src/primitives/badge.tsx +141 -6
  75. package/src/primitives/button-group.tsx +100 -0
  76. package/src/primitives/color-picker.tsx +693 -0
  77. package/src/primitives/command-menu.tsx +503 -0
  78. package/src/primitives/dialog.tsx +315 -51
  79. package/src/primitives/drawer.tsx +338 -0
  80. package/src/primitives/notification-feed.tsx +358 -0
  81. package/src/primitives/notification.tsx +42 -2
  82. package/src/primitives/pagination.tsx +376 -0
  83. package/src/primitives/progress.tsx +267 -6
  84. package/src/primitives/rating.tsx +245 -0
  85. package/src/primitives/scroll-area.tsx +169 -0
  86. package/src/primitives/segmented-control.tsx +162 -0
  87. package/src/primitives/separator.tsx +163 -3
  88. package/src/primitives/slider.tsx +160 -0
  89. package/src/primitives/step-indicator.tsx +287 -0
  90. package/src/primitives/table.tsx +142 -11
  91. package/src/primitives/tabs.tsx +174 -12
  92. package/src/primitives/time-picker.tsx +9 -11
@@ -1,28 +1,289 @@
1
1
  "use client"
2
+
2
3
  import * as React from 'react'
4
+ import { cva, type VariantProps } from 'class-variance-authority'
5
+
3
6
  import { cn } from '@open-mercato/shared/lib/utils'
4
7
 
5
- export interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> {
8
+ /**
9
+ * Determinate progress bar — both linear and circular. Linear is the
10
+ * default `Progress` export; circular is `CircularProgress`.
11
+ *
12
+ * Figma source: DS Open Mercato `Progress Bar` page (`450:17758`):
13
+ * `Progress Bar [1.1]` (`450:17821`), `Progress Bar Label [1.1]`
14
+ * (`515:3758`), `Progress Bar Line [1.1]` (`450:17810`, 5 tone
15
+ * variants: muted / accent / destructive / warning / success),
16
+ * `Circular Progress Bar [1.1]` (`466:4652`).
17
+ *
18
+ * Backward compatibility (Phase B rewrite): the original
19
+ * `<Progress value={n} max={100} className="..." />` API stays
20
+ * callable verbatim. The 3 existing call sites
21
+ * (`packages/ui/src/backend/NextStepCallout.tsx`,
22
+ * `packages/core/.../data_sync/.../runs/[id]/page.tsx`,
23
+ * `packages/sync-akeneo/.../akeneo-config/widget.client.tsx`)
24
+ * keep working without changes. New optional props (`size`, `tone`,
25
+ * `showValue`, `label`, `description`) are additive.
26
+ *
27
+ * ```tsx
28
+ * // Backward-compatible bare bar:
29
+ * <Progress value={50} />
30
+ *
31
+ * // Labelled variant per `Progress Bar Label [1.1]`:
32
+ * <Progress
33
+ * value={80}
34
+ * label="Data Storage"
35
+ * showValue
36
+ * description="Upgrade to unlock unlimited storage."
37
+ * />
38
+ *
39
+ * // Tone variants per `Progress Bar Line [1.1]`:
40
+ * <Progress value={42} tone="success" />
41
+ * <Progress value={42} tone="warning" />
42
+ * <Progress value={42} tone="destructive" />
43
+ *
44
+ * // Circular variant per `Circular Progress Bar [1.1]`:
45
+ * <CircularProgress value={75} size="default" showValue />
46
+ * ```
47
+ */
48
+
49
+ export type ProgressTone =
50
+ | 'accent'
51
+ | 'success'
52
+ | 'warning'
53
+ | 'destructive'
54
+ | 'muted'
55
+
56
+ const PROGRESS_TONE_FILL: Record<ProgressTone, string> = {
57
+ accent: 'bg-accent-indigo',
58
+ success: 'bg-status-success-icon',
59
+ warning: 'bg-status-warning-icon',
60
+ destructive: 'bg-status-error-icon',
61
+ muted: 'bg-muted-foreground',
62
+ }
63
+
64
+ const progressTrackVariants = cva(
65
+ 'relative w-full overflow-hidden rounded-full bg-input',
66
+ {
67
+ variants: {
68
+ size: {
69
+ sm: 'h-1',
70
+ default: 'h-2',
71
+ lg: 'h-3',
72
+ },
73
+ },
74
+ defaultVariants: { size: 'default' },
75
+ },
76
+ )
77
+
78
+ const progressFillVariants = cva(
79
+ 'h-full transition-all duration-300 ease-in-out',
80
+ {
81
+ variants: {
82
+ tone: {
83
+ accent: PROGRESS_TONE_FILL.accent,
84
+ success: PROGRESS_TONE_FILL.success,
85
+ warning: PROGRESS_TONE_FILL.warning,
86
+ destructive: PROGRESS_TONE_FILL.destructive,
87
+ muted: PROGRESS_TONE_FILL.muted,
88
+ },
89
+ },
90
+ defaultVariants: { tone: 'accent' },
91
+ },
92
+ )
93
+
94
+ export interface ProgressProps
95
+ extends React.HTMLAttributes<HTMLDivElement>,
96
+ VariantProps<typeof progressTrackVariants> {
6
97
  value: number
7
98
  max?: number
99
+ /** Color of the filled portion. Default `accent` (indigo). */
100
+ tone?: ProgressTone
101
+ /** Show the percentage badge on the right of the label row. */
102
+ showValue?: boolean
103
+ /** Optional label rendered above the bar. */
104
+ label?: React.ReactNode
105
+ /** Optional description rendered below the bar in muted text. */
106
+ description?: React.ReactNode
107
+ /** Override the inner fill className (e.g. to apply a gradient). */
108
+ fillClassName?: string
8
109
  }
9
110
 
10
- export function Progress({ value, max = 100, className, ...props }: ProgressProps) {
111
+ export function Progress({
112
+ value,
113
+ max = 100,
114
+ size,
115
+ tone,
116
+ showValue,
117
+ label,
118
+ description,
119
+ className,
120
+ fillClassName,
121
+ ...props
122
+ }: ProgressProps) {
11
123
  const percentage = Math.min(100, Math.max(0, Math.round((value / max) * 100)))
12
-
13
- return (
124
+ const hasLabelRow = label !== undefined || showValue === true
125
+ const bar = (
14
126
  <div
15
127
  role="progressbar"
16
128
  aria-valuenow={value}
17
129
  aria-valuemin={0}
18
130
  aria-valuemax={max}
19
- className={cn('relative w-full overflow-hidden rounded-full bg-secondary', className)}
131
+ data-slot="progress"
132
+ data-tone={tone ?? 'accent'}
133
+ className={cn(progressTrackVariants({ size }), className)}
20
134
  {...props}
21
135
  >
22
136
  <div
23
- className="h-full bg-primary transition-all duration-300 ease-in-out"
137
+ data-slot="progress-fill"
138
+ className={cn(progressFillVariants({ tone }), fillClassName)}
24
139
  style={{ width: `${percentage}%` }}
25
140
  />
26
141
  </div>
27
142
  )
143
+
144
+ if (!hasLabelRow && description === undefined) return bar
145
+
146
+ return (
147
+ <div data-slot="progress-wrapper" className="flex w-full flex-col gap-1.5">
148
+ {hasLabelRow ? (
149
+ <div className="flex items-center justify-between gap-3 text-sm font-medium text-foreground">
150
+ <span data-slot="progress-label" className="min-w-0 truncate">
151
+ {label}
152
+ </span>
153
+ {showValue ? (
154
+ <span
155
+ data-slot="progress-value"
156
+ className="shrink-0 text-muted-foreground"
157
+ >
158
+ {percentage}%
159
+ </span>
160
+ ) : null}
161
+ </div>
162
+ ) : null}
163
+ {bar}
164
+ {description !== undefined ? (
165
+ <div data-slot="progress-description" className="text-sm text-muted-foreground">
166
+ {description}
167
+ </div>
168
+ ) : null}
169
+ </div>
170
+ )
171
+ }
172
+
173
+ // ---------------------------------------------------------------------
174
+ // CircularProgress
175
+ // ---------------------------------------------------------------------
176
+
177
+ const CIRCULAR_SIZE_MAP = {
178
+ xs: { box: 24, stroke: 3, textClass: 'text-[9px]' },
179
+ sm: { box: 32, stroke: 3, textClass: 'text-[10px]' },
180
+ default: { box: 48, stroke: 4, textClass: 'text-xs' },
181
+ lg: { box: 64, stroke: 5, textClass: 'text-sm' },
182
+ } as const
183
+
184
+ export type CircularProgressSize = keyof typeof CIRCULAR_SIZE_MAP
185
+
186
+ const CIRCULAR_TONE_STROKE: Record<ProgressTone, string> = {
187
+ accent: 'stroke-accent-indigo',
188
+ success: 'stroke-status-success-icon',
189
+ warning: 'stroke-status-warning-icon',
190
+ destructive: 'stroke-status-error-icon',
191
+ muted: 'stroke-muted-foreground',
192
+ }
193
+
194
+ export interface CircularProgressProps {
195
+ value: number
196
+ max?: number
197
+ size?: CircularProgressSize
198
+ tone?: ProgressTone
199
+ /** Render the percentage in the centre of the ring. */
200
+ showValue?: boolean
201
+ /** Override the inner content (replaces the default percentage label
202
+ * when `showValue` is true). Renders only when `showValue` is true. */
203
+ children?: React.ReactNode
204
+ /** Accessible label for the progressbar. Falls back to `${percentage}%`. */
205
+ ariaLabel?: string
206
+ className?: string
207
+ /** Override the track ring className (defaults to `stroke-input`). */
208
+ trackClassName?: string
209
+ /** Override the fill ring className (defaults per `tone`). */
210
+ fillClassName?: string
211
+ }
212
+
213
+ export function CircularProgress({
214
+ value,
215
+ max = 100,
216
+ size = 'default',
217
+ tone = 'accent',
218
+ showValue,
219
+ children,
220
+ ariaLabel,
221
+ className,
222
+ trackClassName,
223
+ fillClassName,
224
+ }: CircularProgressProps) {
225
+ const percentage = Math.min(100, Math.max(0, Math.round((value / max) * 100)))
226
+ const { box, stroke, textClass } = CIRCULAR_SIZE_MAP[size]
227
+ const radius = (box - stroke) / 2
228
+ const circumference = 2 * Math.PI * radius
229
+ const dashOffset = circumference * (1 - percentage / 100)
230
+ const half = box / 2
231
+ return (
232
+ <div
233
+ role="progressbar"
234
+ aria-valuenow={value}
235
+ aria-valuemin={0}
236
+ aria-valuemax={max}
237
+ aria-label={ariaLabel ?? `${percentage}%`}
238
+ data-slot="circular-progress"
239
+ data-tone={tone}
240
+ data-size={size}
241
+ className={cn('relative inline-flex items-center justify-center', className)}
242
+ >
243
+ <svg
244
+ width={box}
245
+ height={box}
246
+ viewBox={`0 0 ${box} ${box}`}
247
+ className="-rotate-90"
248
+ aria-hidden="true"
249
+ >
250
+ <circle
251
+ data-slot="circular-progress-track"
252
+ cx={half}
253
+ cy={half}
254
+ r={radius}
255
+ fill="none"
256
+ strokeWidth={stroke}
257
+ className={cn('stroke-input', trackClassName)}
258
+ />
259
+ <circle
260
+ data-slot="circular-progress-fill"
261
+ cx={half}
262
+ cy={half}
263
+ r={radius}
264
+ fill="none"
265
+ strokeWidth={stroke}
266
+ strokeLinecap="round"
267
+ strokeDasharray={circumference}
268
+ strokeDashoffset={dashOffset}
269
+ className={cn(
270
+ CIRCULAR_TONE_STROKE[tone],
271
+ 'transition-all duration-300 ease-in-out',
272
+ fillClassName,
273
+ )}
274
+ />
275
+ </svg>
276
+ {showValue ? (
277
+ <span
278
+ data-slot="circular-progress-value"
279
+ className={cn(
280
+ 'absolute inset-0 inline-flex items-center justify-center font-medium text-foreground',
281
+ textClass,
282
+ )}
283
+ >
284
+ {children ?? `${percentage}%`}
285
+ </span>
286
+ ) : null}
287
+ </div>
288
+ )
28
289
  }
@@ -0,0 +1,245 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { Circle, Heart, Star, StarHalf } from 'lucide-react'
5
+ import { cva, type VariantProps } from 'class-variance-authority'
6
+
7
+ import { cn } from '@open-mercato/shared/lib/utils'
8
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
9
+
10
+ /**
11
+ * 1-to-N star / heart / dot rating per Figma `Rating & Review [1.0]`
12
+ * (DS Open Mercato componentSet `199969:1797`). Two distinct modes:
13
+ *
14
+ * - **Read-only display** — no `onChange` prop. Renders `role="img"` with
15
+ * an `aria-label` like "4.5 out of 5 stars". Used in product reviews,
16
+ * feedback summaries.
17
+ * - **Interactive input** — `onChange` provided. Renders as a row of
18
+ * focusable buttons; arrow keys / number keys navigate, click /
19
+ * Enter / Space commits. Used in submission forms.
20
+ *
21
+ * ```tsx
22
+ * // Read-only
23
+ * <Rating value={4.5} max={5} />
24
+ *
25
+ * // Interactive
26
+ * const [v, setV] = React.useState(0)
27
+ * <Rating value={v} onChange={setV} aria-label="Your rating" />
28
+ *
29
+ * // Half precision (stars only — Lucide ships StarHalf but not HeartHalf)
30
+ * <Rating value={3.5} max={5} allowHalf />
31
+ *
32
+ * // Heart variant
33
+ * <Rating value={3} max={5} icon="heart" />
34
+ * ```
35
+ */
36
+
37
+ const ratingRootVariants = cva('inline-flex items-center gap-0.5', {
38
+ variants: {
39
+ size: {
40
+ sm: '[&>*]:size-4',
41
+ default: '[&>*]:size-5',
42
+ lg: '[&>*]:size-6',
43
+ },
44
+ disabled: {
45
+ true: 'cursor-not-allowed opacity-60',
46
+ false: '',
47
+ },
48
+ },
49
+ defaultVariants: { size: 'default', disabled: false },
50
+ })
51
+
52
+ const ratingItemColorVariants = cva('transition-colors', {
53
+ variants: {
54
+ fill: {
55
+ full: 'fill-status-warning-icon text-status-warning-icon',
56
+ half: 'fill-status-warning-icon text-status-warning-icon',
57
+ empty: 'fill-transparent text-muted-foreground/30',
58
+ },
59
+ },
60
+ defaultVariants: { fill: 'empty' },
61
+ })
62
+
63
+ type FillState = 'full' | 'half' | 'empty'
64
+
65
+ function resolveFillState(index: number, value: number, allowHalf: boolean): FillState {
66
+ if (value >= index + 1) return 'full'
67
+ if (allowHalf && value >= index + 0.5) return 'half'
68
+ return 'empty'
69
+ }
70
+
71
+ function StarFull(props: React.ComponentProps<typeof Star>) {
72
+ return <Star aria-hidden="true" {...props} />
73
+ }
74
+
75
+ function StarHalfIcon(props: React.ComponentProps<typeof StarHalf>) {
76
+ return <StarHalf aria-hidden="true" {...props} />
77
+ }
78
+
79
+ function HeartIcon(props: React.ComponentProps<typeof Heart>) {
80
+ return <Heart aria-hidden="true" {...props} />
81
+ }
82
+
83
+ function CircleIcon(props: React.ComponentProps<typeof Circle>) {
84
+ return <Circle aria-hidden="true" {...props} />
85
+ }
86
+
87
+ function renderIcon(
88
+ iconType: 'star' | 'heart' | 'circle',
89
+ fill: FillState,
90
+ className: string,
91
+ ): React.ReactElement {
92
+ if (iconType === 'heart') {
93
+ // Lucide does not ship HeartHalf — half precision is treated as full
94
+ // for hearts (consumer should set allowHalf=false when icon="heart").
95
+ return <HeartIcon className={className} />
96
+ }
97
+ if (iconType === 'circle') {
98
+ return <CircleIcon className={className} />
99
+ }
100
+ // star
101
+ if (fill === 'half') return <StarHalfIcon className={className} />
102
+ return <StarFull className={className} />
103
+ }
104
+
105
+ export type RatingProps = Omit<React.HTMLAttributes<HTMLSpanElement>, 'onChange'> &
106
+ VariantProps<typeof ratingRootVariants> & {
107
+ /** Current rating value (0..max). Floats allowed when `allowHalf` is `true`. */
108
+ value: number
109
+ /** Total number of items. Defaults to 5. */
110
+ max?: number
111
+ /** Optional change handler. Presence flips the primitive into interactive mode. */
112
+ onChange?: (next: number) => void
113
+ /** Which icon glyph to render. Defaults to `'star'`. */
114
+ icon?: 'star' | 'heart' | 'circle'
115
+ /** Enable half-step precision (stars only — Lucide has no HeartHalf). */
116
+ allowHalf?: boolean
117
+ /** When true, dim the control and block clicks. Inherited via `disabled` prop on the root. */
118
+ disabled?: boolean
119
+ /** Required when interactive — screen-readers announce this as the group label. */
120
+ 'aria-label'?: string
121
+ }
122
+
123
+ export const Rating = React.forwardRef<HTMLSpanElement, RatingProps>(
124
+ (
125
+ {
126
+ className,
127
+ value,
128
+ max = 5,
129
+ onChange,
130
+ icon = 'star',
131
+ allowHalf = false,
132
+ size,
133
+ disabled,
134
+ ...rest
135
+ },
136
+ ref,
137
+ ) => {
138
+ const t = useT()
139
+ const interactive = typeof onChange === 'function'
140
+ const handleSelect = React.useCallback(
141
+ (next: number) => {
142
+ if (disabled) return
143
+ onChange?.(next)
144
+ },
145
+ [disabled, onChange],
146
+ )
147
+
148
+ // Read-only display path — plain span container with aria-label.
149
+ if (!interactive) {
150
+ return (
151
+ <span
152
+ ref={ref}
153
+ role="img"
154
+ aria-label={rest['aria-label'] ?? t('ui.rating.summary.ariaLabel', '{value} out of {max}', { value, max })}
155
+ data-slot="rating"
156
+ className={cn(ratingRootVariants({ size, disabled }), className)}
157
+ {...rest}
158
+ >
159
+ {Array.from({ length: max }).map((_, index) => {
160
+ const fill = resolveFillState(index, value, allowHalf)
161
+ return (
162
+ <span
163
+ key={index}
164
+ data-slot="rating-item"
165
+ data-fill={fill}
166
+ className="inline-flex items-center justify-center"
167
+ >
168
+ {renderIcon(icon, fill, ratingItemColorVariants({ fill }))}
169
+ </span>
170
+ )
171
+ })}
172
+ </span>
173
+ )
174
+ }
175
+
176
+ // Interactive path — N buttons. Arrow keys move focus between items;
177
+ // Home / End jump to first / last. Click / Enter / Space commits the
178
+ // value (index + 1 for full clicks; index + 0.5 if allowHalf && the
179
+ // click landed on the left half of the icon).
180
+ return (
181
+ <span
182
+ ref={ref}
183
+ role="radiogroup"
184
+ aria-label={rest['aria-label']}
185
+ data-slot="rating"
186
+ className={cn(ratingRootVariants({ size, disabled }), className)}
187
+ {...rest}
188
+ >
189
+ {Array.from({ length: max }).map((_, index) => {
190
+ const fill = resolveFillState(index, value, allowHalf)
191
+ // Empty rating (value === 0) falls back to the first item so it stays Tab-reachable.
192
+ const isCurrent = value === 0 ? index === 0 : Math.ceil(value) - 1 === index
193
+ return (
194
+ <button
195
+ key={index}
196
+ type="button"
197
+ role="radio"
198
+ aria-checked={fill !== 'empty'}
199
+ aria-label={t('ui.rating.item.ariaLabel', '{position} of {max}', { position: index + 1, max })}
200
+ tabIndex={isCurrent ? 0 : -1}
201
+ data-slot="rating-item"
202
+ data-fill={fill}
203
+ disabled={disabled}
204
+ onClick={(event) => {
205
+ if (!allowHalf) {
206
+ handleSelect(index + 1)
207
+ return
208
+ }
209
+ const rect = event.currentTarget.getBoundingClientRect()
210
+ const isLeftHalf = event.clientX - rect.left < rect.width / 2
211
+ handleSelect(isLeftHalf ? index + 0.5 : index + 1)
212
+ }}
213
+ onKeyDown={(event) => {
214
+ if (event.key === 'ArrowRight' || event.key === 'ArrowUp') {
215
+ event.preventDefault()
216
+ handleSelect(Math.min(max, value + (allowHalf ? 0.5 : 1)))
217
+ } else if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') {
218
+ event.preventDefault()
219
+ handleSelect(Math.max(0, value - (allowHalf ? 0.5 : 1)))
220
+ } else if (event.key === 'Home') {
221
+ event.preventDefault()
222
+ handleSelect(allowHalf ? 0.5 : 1)
223
+ } else if (event.key === 'End') {
224
+ event.preventDefault()
225
+ handleSelect(max)
226
+ }
227
+ }}
228
+ className={cn(
229
+ 'inline-flex items-center justify-center rounded-sm outline-none',
230
+ 'focus-visible:shadow-focus',
231
+ 'enabled:hover:scale-110 enabled:hover:transition-transform',
232
+ 'disabled:cursor-not-allowed',
233
+ )}
234
+ >
235
+ {renderIcon(icon, fill, ratingItemColorVariants({ fill }))}
236
+ </button>
237
+ )
238
+ })}
239
+ </span>
240
+ )
241
+ },
242
+ )
243
+ Rating.displayName = 'Rating'
244
+
245
+ export { ratingRootVariants, ratingItemColorVariants }