@jinaan/uikit 0.1.21 → 0.1.24

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/utils.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { type ClassValue } from "clsx";
2
2
  /** Merge conditional class names with Tailwind conflict resolution. */
3
3
  export declare function cn(...inputs: ClassValue[]): string;
4
+ /** Move the id at `from` so it sits at `to`; no-op when indexes match. */
5
+ export declare function reorderIds(ids: string[], from: number, to: number): string[];
4
6
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAG7C,uEAAuE;AACvE,wBAAgB,EAAE,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,CAElD"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAG7C,uEAAuE;AACvE,wBAAgB,EAAE,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,CAElD;AAED,0EAA0E;AAC1E,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,CAQ5E"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jinaan/uikit",
3
- "version": "0.1.21",
3
+ "version": "0.1.24",
4
4
  "description": "Reusable React and Tailwind UI components for Jinaan projects.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -3,6 +3,7 @@ import {
3
3
  CaretUp,
4
4
  CaretUpDown,
5
5
  Columns as ColumnsIcon,
6
+ DotsSixVertical,
6
7
  FunnelSimple,
7
8
  Lock,
8
9
  MagnifyingGlass,
@@ -19,6 +20,7 @@ import {
19
20
  DropdownMenu,
20
21
  DropdownMenuCheckboxItem,
21
22
  DropdownMenuContent,
23
+ DropdownMenuChevron,
22
24
  DropdownMenuItem,
23
25
  DropdownMenuLabel,
24
26
  DropdownMenuSeparator,
@@ -38,7 +40,7 @@ import {
38
40
  type TableSurfaceAction,
39
41
  type TableSurfaceTone,
40
42
  } from "./table-surface";
41
- import { cn } from "./utils";
43
+ import { cn, reorderIds } from "./utils";
42
44
 
43
45
  export interface Column<T> {
44
46
  id: string;
@@ -210,6 +212,9 @@ export function DataTable<T>({
210
212
  columns.filter((c) => c.defaultHidden && !c.required).map((c) => [c.id, true]),
211
213
  ),
212
214
  );
215
+ // Column order and runtime pins are per-mount, like `hidden`.
216
+ const [order, setOrder] = useState<string[] | null>(null);
217
+ const [dragId, setDragId] = useState<string | null>(null);
213
218
  const [sort, setSort] = useState<{ id: string; dir: "asc" | "desc" } | null>(
214
219
  defaultSort ?? null,
215
220
  );
@@ -218,8 +223,31 @@ export function DataTable<T>({
218
223
  const [selected, setSelected] = useState<Set<string>>(new Set());
219
224
  const [activeFilterIds, setActiveFilterIds] = useState<string[]>([]);
220
225
 
221
- const visibleColumns = columns.filter((c) => !hidden[c.id]);
222
- const hiddenColumns = columns.filter((c) => hidden[c.id] && !c.required);
226
+ const propOrder = columns.map((c) => c.id);
227
+ const byId = new Map(columns.map((c) => [c.id, c] as const));
228
+ const orderedColumns = order
229
+ ? order.map((id) => byId.get(id)).filter((c): c is Column<T> => Boolean(c))
230
+ : columns;
231
+ const visibleColumns = orderedColumns.filter((c) => !hidden[c.id]);
232
+ const hiddenColumns = orderedColumns.filter((c) => hidden[c.id] && !c.required);
233
+ // ponytail: mouse-only drag reorder; add keyboard move (Alt+Arrow) if asked.
234
+ const isPinned = (c: Column<T>) => Boolean(c.required);
235
+ const columnAt = (id: string) => byId.get(id);
236
+
237
+ const moveBefore = (draggedId: string, targetId: string) => {
238
+ const target = targetId ? columnAt(targetId) : undefined;
239
+ if (!target || isPinned(target) || draggedId === targetId) return;
240
+ setOrder((prev) => {
241
+ const ids = prev ?? propOrder;
242
+ return reorderIds(ids, ids.indexOf(draggedId), ids.indexOf(targetId));
243
+ });
244
+ };
245
+
246
+ const showColumn = (id: string) => {
247
+ setHidden((prev) => ({ ...prev, [id]: false }));
248
+ // Re-showing appends at the end of the current order.
249
+ setOrder((prev) => (prev && prev.includes(id) ? prev : [...(prev ?? propOrder), id]));
250
+ };
223
251
  const activeFilters = (filters ?? []).filter((f) => activeFilterIds.includes(f.id));
224
252
  const filterSignature = (filters ?? [])
225
253
  .map((f) => `${f.id}:${f.value.join(",")}`)
@@ -371,26 +399,60 @@ export function DataTable<T>({
371
399
  variant="secondary"
372
400
  size="md"
373
401
  leadingIcon={<ColumnsIcon />}
374
- trailingIcon={<CaretDown />}
402
+ trailingIcon={<DropdownMenuChevron />}
375
403
  >
376
404
  Columns
377
405
  </Button>
378
406
  </DropdownMenuTrigger>
379
407
  <DropdownMenuContent align="end" className="w-54">
380
408
  <DropdownMenuLabel className="text-[13px]">Active columns</DropdownMenuLabel>
381
- {visibleColumns.map((c) => (
382
- <DropdownMenuCheckboxItem
383
- key={c.id}
384
- checked
385
- disabled={c.required}
386
- onCheckedChange={() => setHidden((prev) => ({ ...prev, [c.id]: true }))}
387
- onSelect={(e) => e.preventDefault()}
388
- className="justify-between"
389
- >
390
- <span>{c.header}</span>
391
- {c.required ? <Lock className="size-3.5 text-subtle" aria-hidden /> : null}
392
- </DropdownMenuCheckboxItem>
393
- ))}
409
+ {visibleColumns.map((c) => {
410
+ const pinned = isPinned(c);
411
+ return (
412
+ <DropdownMenuCheckboxItem
413
+ key={c.id}
414
+ checked
415
+ draggable={!pinned}
416
+ onDragStart={(e) => {
417
+ if (pinned) {
418
+ e.preventDefault();
419
+ return;
420
+ }
421
+ e.dataTransfer.effectAllowed = "move";
422
+ e.dataTransfer.setData("text/plain", c.id);
423
+ setDragId(c.id);
424
+ }}
425
+ onDragOver={(e) => {
426
+ if (dragId) {
427
+ e.preventDefault();
428
+ moveBefore(dragId, c.id);
429
+ }
430
+ }}
431
+ onDrop={(e) => e.preventDefault()}
432
+ onDragEnd={() => setDragId(null)}
433
+ onCheckedChange={(next) => {
434
+ if (!next && !pinned) {
435
+ setHidden((prev) => ({ ...prev, [c.id]: true }));
436
+ }
437
+ }}
438
+ onSelect={(e) => e.preventDefault()}
439
+ className="justify-between"
440
+ >
441
+ <span className="truncate">{c.header}</span>
442
+ {pinned ? (
443
+ <Lock className="size-3.5 shrink-0 text-subtle" aria-hidden />
444
+ ) : (
445
+ <DotsSixVertical
446
+ aria-hidden
447
+ className={cn(
448
+ "size-3.5 shrink-0",
449
+ dragId === c.id ? "text-primary" : "text-subtle",
450
+ )}
451
+ />
452
+ )}
453
+ </DropdownMenuCheckboxItem>
454
+ );
455
+ })}
394
456
  {hiddenColumns.length > 0 ? (
395
457
  <>
396
458
  <DropdownMenuSeparator />
@@ -399,7 +461,7 @@ export function DataTable<T>({
399
461
  <DropdownMenuCheckboxItem
400
462
  key={c.id}
401
463
  checked={false}
402
- onCheckedChange={() => setHidden((prev) => ({ ...prev, [c.id]: false }))}
464
+ onCheckedChange={() => showColumn(c.id)}
403
465
  onSelect={(e) => e.preventDefault()}
404
466
  >
405
467
  {c.header}
@@ -35,12 +35,15 @@ export function DropdownMenu({
35
35
  export const DropdownMenuTrigger = forwardRef<
36
36
  ElementRef<typeof DropdownMenuPrimitive.Trigger>,
37
37
  ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Trigger>
38
- >(({ disabled, ...props }, ref) => {
38
+ >(({ className, disabled, ...props }, ref) => {
39
39
  const empty = useContext(DropdownMenuEmptyContext);
40
40
 
41
41
  return (
42
42
  <DropdownMenuPrimitive.Trigger
43
43
  ref={ref}
44
+ // `group` lets DropdownMenuChevron rotate with the open state, even
45
+ // when the trigger renders through asChild onto a custom button.
46
+ className={cn("group", className)}
44
47
  disabled={disabled || empty}
45
48
  {...props}
46
49
  />
package/src/select.tsx CHANGED
@@ -54,7 +54,7 @@ export const SelectTrigger = forwardRef<
54
54
  >
55
55
  <span className="min-w-0 flex-1 truncate text-left">{children}</span>
56
56
  <SelectPrimitive.Icon asChild>
57
- <CaretDown className="ml-auto size-3.5 shrink-0 text-subtle transition-transform duration-200 ease-out group-data-[state=open]:rotate-180 motion-reduce:transition-none" />
57
+ <CaretDown className="ml-auto size-3.5 shrink-0 text-muted transition-transform duration-200 ease-out group-data-[state=open]:rotate-180 motion-reduce:transition-none" />
58
58
  </SelectPrimitive.Icon>
59
59
  </SelectPrimitive.Trigger>
60
60
  ));
package/src/utils.ts CHANGED
@@ -5,3 +5,14 @@ import { twMerge } from "tailwind-merge";
5
5
  export function cn(...inputs: ClassValue[]): string {
6
6
  return twMerge(clsx(inputs));
7
7
  }
8
+
9
+ /** Move the id at `from` so it sits at `to`; no-op when indexes match. */
10
+ export function reorderIds(ids: string[], from: number, to: number): string[] {
11
+ if (from === to || from < 0 || to < 0 || from >= ids.length || to >= ids.length) {
12
+ return ids;
13
+ }
14
+ const next = [...ids];
15
+ const [moved] = next.splice(from, 1);
16
+ next.splice(to, 0, moved);
17
+ return next;
18
+ }