@motor-hero/ui-kit 0.50.0 → 0.51.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motor-hero/ui-kit",
3
- "version": "0.50.0",
3
+ "version": "0.51.0",
4
4
  "description": "Componentes React reutilizáveis para o ecossistema MotorHero — shadcn/ui + Tailwind CSS v4",
5
5
  "author": "MotorHero",
6
6
  "license": "UNLICENSED",
@@ -14,6 +14,10 @@ export interface ComboboxOption {
14
14
  /** Optional leading decoration rendered before the label (icon, colored
15
15
  * dot) — purely visual, doesn't affect filtering/matching. */
16
16
  icon?: React.ReactNode;
17
+ /** Linha secundária abaixo do rótulo na lista (ex.: e-mail de uma pessoa,
18
+ * para separar homônimos). No gatilho vira o `title`. Entra na busca do
19
+ * modo cliente. */
20
+ description?: string;
17
21
  }
18
22
 
19
23
  export interface ComboboxProps {
@@ -209,6 +213,7 @@ export function Combobox({
209
213
  // cliente: cmdk filtra pelo `value`, então usamos o label; servidor:
210
214
  // o servidor já filtrou, então usamos o id (único, evita colisão de labels).
211
215
  value={isServer ? option.value : option.label}
216
+ keywords={option.description ? [option.description] : undefined}
212
217
  disabled={option.disabled}
213
218
  onSelect={() => handleSelect(option.value)}
214
219
  className="relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-2.5 text-sm outline-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 sm:py-1.5"
@@ -220,7 +225,7 @@ export function Combobox({
220
225
  )}
221
226
  />
222
227
  {option.icon}
223
- <span className="truncate">{option.label}</span>
228
+ <OptionText option={option} />
224
229
  </CommandPrimitive.Item>
225
230
  ))}
226
231
  {isServer && !loading && options.length === 0 && (
@@ -274,3 +279,19 @@ export function Combobox({
274
279
  </DrawerPrimitive.Root>
275
280
  );
276
281
  }
282
+
283
+ export function OptionText({ option }: { option: ComboboxOption }) {
284
+ if (!option.description)
285
+ return <span className="truncate">{option.label}</span>;
286
+ return (
287
+ <span className="flex min-w-0 flex-col">
288
+ <span className="truncate">{option.label}</span>
289
+ <span
290
+ title={option.description}
291
+ className="truncate text-xs text-muted-foreground"
292
+ >
293
+ {option.description}
294
+ </span>
295
+ </span>
296
+ );
297
+ }
@@ -6,7 +6,7 @@ import { Drawer as DrawerPrimitive } from "vaul"
6
6
  import { useInfiniteScrollSentinel } from "../hooks/use-infinite-scroll-sentinel"
7
7
  import { useIsDesktop } from "../hooks/use-is-desktop"
8
8
  import { cn } from "../lib/utils"
9
- import type { ComboboxOption } from "./combobox"
9
+ import { type ComboboxOption, OptionText } from "./combobox"
10
10
 
11
11
  export interface MultiComboboxProps {
12
12
  options: ComboboxOption[]
@@ -101,6 +101,15 @@ export function MultiCombobox({
101
101
  .map((v) => byValue.get(v) ?? labelCache.current.get(v))
102
102
  .filter((option): option is ComboboxOption => option !== undefined)
103
103
 
104
+ // Chips homônimos não se distinguem só pelo rótulo: esses ganham a descrição.
105
+ const labelCounts = new Map<string, number>()
106
+ for (const option of selected)
107
+ labelCounts.set(option.label, (labelCounts.get(option.label) ?? 0) + 1)
108
+ const chipLabel = (option: ComboboxOption) =>
109
+ option.description && (labelCounts.get(option.label) ?? 0) > 1
110
+ ? `${option.label} · ${option.description}`
111
+ : option.label
112
+
104
113
  const { listRef, sentinelRef } = useInfiniteScrollSentinel({
105
114
  enabled: isServer && !!hasMore,
106
115
  loading,
@@ -146,9 +155,10 @@ export function MultiCombobox({
146
155
  selected.map((option) => (
147
156
  <span
148
157
  key={option.value}
158
+ title={option.description}
149
159
  className="inline-flex items-center gap-1 rounded bg-secondary px-1.5 py-0.5 text-xs text-secondary-foreground"
150
160
  >
151
- {option.label}
161
+ {chipLabel(option)}
152
162
  <button
153
163
  type="button"
154
164
  tabIndex={-1}
@@ -195,6 +205,7 @@ export function MultiCombobox({
195
205
  <CommandPrimitive.Item
196
206
  key={option.value}
197
207
  value={isServer ? option.value : option.label}
208
+ keywords={option.description ? [option.description] : undefined}
198
209
  disabled={option.disabled}
199
210
  onSelect={() => toggleValue(option.value)}
200
211
  className="relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-2.5 text-sm outline-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 sm:py-1.5"
@@ -205,7 +216,7 @@ export function MultiCombobox({
205
216
  value.includes(option.value) ? "opacity-100" : "opacity-0",
206
217
  )}
207
218
  />
208
- <span className="truncate">{option.label}</span>
219
+ <OptionText option={option} />
209
220
  </CommandPrimitive.Item>
210
221
  ))}
211
222
  {isServer && !loading && options.length === 0 && (