@jinaan/uikit 0.1.21 → 0.1.23

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.23",
4
4
  "description": "Reusable React and Tailwind UI components for Jinaan projects.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -3,8 +3,10 @@ import {
3
3
  CaretUp,
4
4
  CaretUpDown,
5
5
  Columns as ColumnsIcon,
6
+ DotsSixVertical,
6
7
  FunnelSimple,
7
8
  Lock,
9
+ LockOpen,
8
10
  MagnifyingGlass,
9
11
  WarningCircle,
10
12
  X,
@@ -19,6 +21,7 @@ import {
19
21
  DropdownMenu,
20
22
  DropdownMenuCheckboxItem,
21
23
  DropdownMenuContent,
24
+ DropdownMenuChevron,
22
25
  DropdownMenuItem,
23
26
  DropdownMenuLabel,
24
27
  DropdownMenuSeparator,
@@ -38,7 +41,7 @@ import {
38
41
  type TableSurfaceAction,
39
42
  type TableSurfaceTone,
40
43
  } from "./table-surface";
41
- import { cn } from "./utils";
44
+ import { cn, reorderIds } from "./utils";
42
45
 
43
46
  export interface Column<T> {
44
47
  id: string;
@@ -210,6 +213,10 @@ export function DataTable<T>({
210
213
  columns.filter((c) => c.defaultHidden && !c.required).map((c) => [c.id, true]),
211
214
  ),
212
215
  );
216
+ // Column order and runtime pins are per-mount, like `hidden`.
217
+ const [order, setOrder] = useState<string[] | null>(null);
218
+ const [locked, setLocked] = useState<string[]>([]);
219
+ const [dragId, setDragId] = useState<string | null>(null);
213
220
  const [sort, setSort] = useState<{ id: string; dir: "asc" | "desc" } | null>(
214
221
  defaultSort ?? null,
215
222
  );
@@ -218,8 +225,36 @@ export function DataTable<T>({
218
225
  const [selected, setSelected] = useState<Set<string>>(new Set());
219
226
  const [activeFilterIds, setActiveFilterIds] = useState<string[]>([]);
220
227
 
221
- const visibleColumns = columns.filter((c) => !hidden[c.id]);
222
- const hiddenColumns = columns.filter((c) => hidden[c.id] && !c.required);
228
+ const propOrder = columns.map((c) => c.id);
229
+ const byId = new Map(columns.map((c) => [c.id, c] as const));
230
+ const orderedColumns = order
231
+ ? order.map((id) => byId.get(id)).filter((c): c is Column<T> => Boolean(c))
232
+ : columns;
233
+ const visibleColumns = orderedColumns.filter((c) => !hidden[c.id]);
234
+ const hiddenColumns = orderedColumns.filter((c) => hidden[c.id] && !c.required);
235
+ // ponytail: mouse-only drag reorder; add keyboard move (Alt+Arrow) if asked.
236
+ const isPinned = (c: Column<T>) => c.required || locked.includes(c.id);
237
+ const columnAt = (id: string) => byId.get(id);
238
+
239
+ const moveBefore = (draggedId: string, targetId: string) => {
240
+ const target = targetId ? columnAt(targetId) : undefined;
241
+ if (!target || isPinned(target) || draggedId === targetId) return;
242
+ setOrder((prev) => {
243
+ const ids = prev ?? propOrder;
244
+ return reorderIds(ids, ids.indexOf(draggedId), ids.indexOf(targetId));
245
+ });
246
+ };
247
+
248
+ const showColumn = (id: string) => {
249
+ setHidden((prev) => ({ ...prev, [id]: false }));
250
+ // Re-showing appends at the end of the current order.
251
+ setOrder((prev) => (prev && prev.includes(id) ? prev : [...(prev ?? propOrder), id]));
252
+ };
253
+
254
+ const togglePin = (id: string) =>
255
+ setLocked((prev) =>
256
+ prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id],
257
+ );
223
258
  const activeFilters = (filters ?? []).filter((f) => activeFilterIds.includes(f.id));
224
259
  const filterSignature = (filters ?? [])
225
260
  .map((f) => `${f.id}:${f.value.join(",")}`)
@@ -371,26 +406,79 @@ export function DataTable<T>({
371
406
  variant="secondary"
372
407
  size="md"
373
408
  leadingIcon={<ColumnsIcon />}
374
- trailingIcon={<CaretDown />}
409
+ trailingIcon={<DropdownMenuChevron />}
375
410
  >
376
411
  Columns
377
412
  </Button>
378
413
  </DropdownMenuTrigger>
379
414
  <DropdownMenuContent align="end" className="w-54">
380
415
  <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
- ))}
416
+ {visibleColumns.map((c) => {
417
+ const pinned = isPinned(c);
418
+ return (
419
+ <DropdownMenuCheckboxItem
420
+ key={c.id}
421
+ checked
422
+ draggable={!pinned}
423
+ onDragStart={(e) => {
424
+ e.dataTransfer.effectAllowed = "move";
425
+ e.dataTransfer.setData("text/plain", c.id);
426
+ setDragId(c.id);
427
+ }}
428
+ onDragOver={(e) => {
429
+ if (dragId) {
430
+ e.preventDefault();
431
+ moveBefore(dragId, c.id);
432
+ }
433
+ }}
434
+ onDrop={(e) => e.preventDefault()}
435
+ onDragEnd={() => setDragId(null)}
436
+ onCheckedChange={(next) => {
437
+ if (!next && !pinned) {
438
+ setHidden((prev) => ({ ...prev, [c.id]: true }));
439
+ }
440
+ }}
441
+ onSelect={(e) => e.preventDefault()}
442
+ className="justify-between"
443
+ >
444
+ <span className="flex min-w-0 items-center gap-2">
445
+ <DotsSixVertical
446
+ aria-hidden
447
+ className={cn(
448
+ "size-3.5 shrink-0",
449
+ pinned
450
+ ? "text-subtle/60"
451
+ : dragId === c.id
452
+ ? "text-primary"
453
+ : "text-subtle",
454
+ )}
455
+ />
456
+ <span className="truncate">{c.header}</span>
457
+ </span>
458
+ {c.required ? (
459
+ <Lock className="size-3.5 shrink-0 text-subtle" aria-hidden />
460
+ ) : (
461
+ <button
462
+ type="button"
463
+ aria-label={pinned ? `Unlock ${c.header}` : `Lock ${c.header}`}
464
+ onClick={(e) => {
465
+ e.stopPropagation();
466
+ e.preventDefault();
467
+ togglePin(c.id);
468
+ }}
469
+ onPointerDown={(e) => e.stopPropagation()}
470
+ className="-m-1 rounded p-1 text-subtle transition-colors hover:bg-(--menu-highlight) hover:text-fg"
471
+ >
472
+ {pinned ? (
473
+ <LockOpen className="size-3.5" aria-hidden />
474
+ ) : (
475
+ <Lock className="size-3.5" aria-hidden />
476
+ )}
477
+ </button>
478
+ )}
479
+ </DropdownMenuCheckboxItem>
480
+ );
481
+ })}
394
482
  {hiddenColumns.length > 0 ? (
395
483
  <>
396
484
  <DropdownMenuSeparator />
@@ -399,7 +487,7 @@ export function DataTable<T>({
399
487
  <DropdownMenuCheckboxItem
400
488
  key={c.id}
401
489
  checked={false}
402
- onCheckedChange={() => setHidden((prev) => ({ ...prev, [c.id]: false }))}
490
+ onCheckedChange={() => showColumn(c.id)}
403
491
  onSelect={(e) => e.preventDefault()}
404
492
  >
405
493
  {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
+ }