@motor-hero/ui-kit 0.50.0 → 0.51.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.
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.1",
4
4
  "description": "Componentes React reutilizáveis para o ecossistema MotorHero — shadcn/ui + Tailwind CSS v4",
5
5
  "author": "MotorHero",
6
6
  "license": "UNLICENSED",
@@ -1,5 +1,5 @@
1
1
  import * as PopoverPrimitive from "@radix-ui/react-popover";
2
- import { Command as CommandPrimitive } from "cmdk";
2
+ import { Command as CommandPrimitive, defaultFilter } from "cmdk";
3
3
  import { Check, ChevronsUpDown, Loader2, Plus, Search } from "lucide-react";
4
4
  import * as React from "react";
5
5
  import { Drawer as DrawerPrimitive } from "vaul";
@@ -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 {
@@ -159,7 +163,10 @@ export function Combobox({
159
163
  className,
160
164
  )}
161
165
  >
162
- <span className={cn("truncate", !selected && "text-muted-foreground")}>
166
+ <span
167
+ title={selected?.description}
168
+ className={cn("truncate", !selected && "text-muted-foreground")}
169
+ >
163
170
  {selected ? selected.label : placeholder}
164
171
  </span>
165
172
  <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
@@ -169,6 +176,7 @@ export function Combobox({
169
176
  const command = (
170
177
  <CommandPrimitive
171
178
  shouldFilter={!isServer}
179
+ filter={filterByKeywords}
172
180
  className="flex h-full w-full flex-col overflow-hidden"
173
181
  >
174
182
  <div className="flex items-center border-b px-3">
@@ -190,8 +198,10 @@ export function Combobox({
190
198
  )}
191
199
  {showCreate && (
192
200
  <CommandPrimitive.Item
193
- // cmdk pontua pelo `value`; igualar à busca mantém a linha sempre no topo.
201
+ // A busca como `value`, não uma chave fixa: o cmdk só relê as
202
+ // keywords quando o `value` muda. Keyword igual à busca = sempre no topo.
194
203
  value={trimmedSearch}
204
+ keywords={[trimmedSearch]}
195
205
  onSelect={() => handleSelect(trimmedSearch)}
196
206
  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 sm:py-1.5"
197
207
  >
@@ -206,9 +216,8 @@ export function Combobox({
206
216
  {options.map((option) => (
207
217
  <CommandPrimitive.Item
208
218
  key={option.value}
209
- // cliente: cmdk filtra pelo `value`, então usamos o label; servidor:
210
- // o servidor já filtrou, então usamos o id (único, evita colisão de labels).
211
- value={isServer ? option.value : option.label}
219
+ value={option.value}
220
+ keywords={optionKeywords(option)}
212
221
  disabled={option.disabled}
213
222
  onSelect={() => handleSelect(option.value)}
214
223
  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 +229,7 @@ export function Combobox({
220
229
  )}
221
230
  />
222
231
  {option.icon}
223
- <span className="truncate">{option.label}</span>
232
+ <OptionText option={option} />
224
233
  </CommandPrimitive.Item>
225
234
  ))}
226
235
  {isServer && !loading && options.length === 0 && (
@@ -274,3 +283,31 @@ export function Combobox({
274
283
  </DrawerPrimitive.Root>
275
284
  );
276
285
  }
286
+
287
+ // O `value` do item é o id, único mesmo entre homônimos — rótulo igual como
288
+ // `value` fazia o cmdk tratar duas opções como uma (hover, teclado, seleção).
289
+ // A busca pontua só pelas keywords (rótulo + descrição), nunca pelo id.
290
+ export const filterByKeywords = (
291
+ _value: string,
292
+ search: string,
293
+ keywords: string[] = [],
294
+ ) => defaultFilter(keywords.join(" "), search);
295
+
296
+ export const optionKeywords = (option: ComboboxOption) =>
297
+ option.description ? [option.label, option.description] : [option.label];
298
+
299
+ export function OptionText({ option }: { option: ComboboxOption }) {
300
+ if (!option.description)
301
+ return <span className="truncate">{option.label}</span>;
302
+ return (
303
+ <span className="flex min-w-0 flex-col">
304
+ <span className="truncate">{option.label}</span>
305
+ <span
306
+ title={option.description}
307
+ className="truncate text-xs text-muted-foreground"
308
+ >
309
+ {option.description}
310
+ </span>
311
+ </span>
312
+ );
313
+ }
@@ -6,7 +6,12 @@ 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 {
10
+ type ComboboxOption,
11
+ filterByKeywords,
12
+ OptionText,
13
+ optionKeywords,
14
+ } from "./combobox"
10
15
 
11
16
  export interface MultiComboboxProps {
12
17
  options: ComboboxOption[]
@@ -101,6 +106,15 @@ export function MultiCombobox({
101
106
  .map((v) => byValue.get(v) ?? labelCache.current.get(v))
102
107
  .filter((option): option is ComboboxOption => option !== undefined)
103
108
 
109
+ // Chips homônimos não se distinguem só pelo rótulo: esses ganham a descrição.
110
+ const labelCounts = new Map<string, number>()
111
+ for (const option of selected)
112
+ labelCounts.set(option.label, (labelCounts.get(option.label) ?? 0) + 1)
113
+ const chipLabel = (option: ComboboxOption) =>
114
+ option.description && (labelCounts.get(option.label) ?? 0) > 1
115
+ ? `${option.label} · ${option.description}`
116
+ : option.label
117
+
104
118
  const { listRef, sentinelRef } = useInfiniteScrollSentinel({
105
119
  enabled: isServer && !!hasMore,
106
120
  loading,
@@ -146,9 +160,10 @@ export function MultiCombobox({
146
160
  selected.map((option) => (
147
161
  <span
148
162
  key={option.value}
163
+ title={option.description}
149
164
  className="inline-flex items-center gap-1 rounded bg-secondary px-1.5 py-0.5 text-xs text-secondary-foreground"
150
165
  >
151
- {option.label}
166
+ {chipLabel(option)}
152
167
  <button
153
168
  type="button"
154
169
  tabIndex={-1}
@@ -172,6 +187,7 @@ export function MultiCombobox({
172
187
  const command = (
173
188
  <CommandPrimitive
174
189
  shouldFilter={!isServer}
190
+ filter={filterByKeywords}
175
191
  className="flex h-full w-full flex-col overflow-hidden"
176
192
  >
177
193
  <div className="flex items-center border-b px-3">
@@ -194,7 +210,8 @@ export function MultiCombobox({
194
210
  {options.map((option) => (
195
211
  <CommandPrimitive.Item
196
212
  key={option.value}
197
- value={isServer ? option.value : option.label}
213
+ value={option.value}
214
+ keywords={optionKeywords(option)}
198
215
  disabled={option.disabled}
199
216
  onSelect={() => toggleValue(option.value)}
200
217
  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 +222,7 @@ export function MultiCombobox({
205
222
  value.includes(option.value) ? "opacity-100" : "opacity-0",
206
223
  )}
207
224
  />
208
- <span className="truncate">{option.label}</span>
225
+ <OptionText option={option} />
209
226
  </CommandPrimitive.Item>
210
227
  ))}
211
228
  {isServer && !loading && options.length === 0 && (