@suphark/ui 0.59.1 → 0.61.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.
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
3
4
  import { SelectionBar } from "@suphark/ui/components/shared/selection-bar";
4
5
  import { useUiText } from "@suphark/ui/components/shared/ui-text";
5
6
  import {
@@ -14,6 +15,7 @@ import { cn } from "@suphark/ui/lib/utils";
14
15
  import { flexRender } from "@tanstack/react-table";
15
16
  import * as React from "react";
16
17
  import { DataTablePagination } from "./data-table-pagination";
18
+ import { DataTableEmptyState, DataTableErrorState, QuietMobileSkeleton } from "./data-table-states";
17
19
  import { DataTableToolbar } from "./data-table-toolbar";
18
20
  import type { DataTableMobileSlot, DataTableProps } from "./types";
19
21
  import { useDataTable } from "./use-data-table";
@@ -84,10 +86,30 @@ export function DataTable<TData, TValue>({
84
86
  variant = "default",
85
87
  defaultDensity = "comfortable",
86
88
  selectionActions,
89
+ densityStorageKey,
90
+ resourceName,
91
+ emptyAction,
92
+ error,
93
+ onRetry,
87
94
  }: DataTableProps<TData, TValue>) {
88
95
  const text = useUiText();
89
96
  const quiet = variant === "quiet";
90
- const [density, setDensity] = React.useState<"comfortable" | "compact">(defaultDensity);
97
+ const [density, setDensityState] = React.useState<"comfortable" | "compact">(defaultDensity);
98
+ // จำความหนาแน่น: อ่านหลัง mount (กัน hydration mismatch) — localStorage ใช้ไม่ได้ก็ข้าม
99
+ React.useEffect(() => {
100
+ if (!densityStorageKey) return;
101
+ try {
102
+ const saved = localStorage.getItem(densityStorageKey);
103
+ if (saved === "compact" || saved === "comfortable") setDensityState(saved);
104
+ } catch {}
105
+ }, [densityStorageKey]);
106
+ const setDensity = (next: "comfortable" | "compact") => {
107
+ setDensityState(next);
108
+ if (!densityStorageKey) return;
109
+ try {
110
+ localStorage.setItem(densityStorageKey, next);
111
+ } catch {}
112
+ };
91
113
  const { table } = useDataTable({
92
114
  columns,
93
115
  data,
@@ -176,7 +198,13 @@ export function DataTable<TData, TValue>({
176
198
  ))}
177
199
  </TableHeader>
178
200
  <TableBody>
179
- {isLoading ? (
201
+ {error ? (
202
+ <TableRow>
203
+ <TableCell colSpan={table.getVisibleLeafColumns().length}>
204
+ <DataTableErrorState onRetry={onRetry} />
205
+ </TableCell>
206
+ </TableRow>
207
+ ) : isLoading ? (
180
208
  Array.from({ length: initialPageSize }).map((_, i) => (
181
209
  // biome-ignore lint/suspicious/noArrayIndexKey: แถว skeleton ไม่มีตัวตน ใช้ตำแหน่งเป็น key ได้
182
210
  <TableRow key={i}>
@@ -229,7 +257,17 @@ export function DataTable<TData, TValue>({
229
257
  ) : (
230
258
  <TableRow>
231
259
  <TableCell colSpan={table.getVisibleLeafColumns().length} className="h-24 text-center">
232
- {emptyStateComponent || (text.table?.noResults ?? "ไม่พบข้อมูล")}
260
+ {emptyStateComponent ||
261
+ (quiet ? (
262
+ <DataTableEmptyState
263
+ table={table}
264
+ preservedFilterIds={preservedFilterIds}
265
+ resourceName={resourceName}
266
+ emptyAction={emptyAction}
267
+ />
268
+ ) : (
269
+ (text.table?.noResults ?? "ไม่พบข้อมูล")
270
+ ))}
233
271
  </TableCell>
234
272
  </TableRow>
235
273
  )}
@@ -241,8 +279,10 @@ export function DataTable<TData, TValue>({
241
279
  .getAllLeafColumns()
242
280
  .some((column) => (column.columnDef.meta as { mobile?: string } | undefined)?.mobile);
243
281
  const mobileList =
244
- quiet && hasMobileSlots && !isLoading && table.getRowModel().rows.length > 0 ? (
282
+ quiet && hasMobileSlots && !isLoading && !error && table.getRowModel().rows.length > 0 ? (
245
283
  <QuietMobileList table={table} />
284
+ ) : quiet && hasMobileSlots && isLoading && !error ? (
285
+ <QuietMobileSkeleton rows={initialPageSize} />
246
286
  ) : null;
247
287
 
248
288
  const customView = renderCustomView?.(table);
@@ -263,6 +303,7 @@ export function DataTable<TData, TValue>({
263
303
  variant={variant}
264
304
  density={density}
265
305
  onDensityChange={setDensity}
306
+ isLoading={isLoading}
266
307
  />
267
308
  )}
268
309
  {customView ? (
@@ -304,7 +345,7 @@ export function DataTable<TData, TValue>({
304
345
  renderTable(false)
305
346
  )}
306
347
  </div>
307
- {showPagination && (
348
+ {showPagination && !error && (
308
349
  <div
309
350
  className={
310
351
  stickyPagination
@@ -327,6 +368,7 @@ export function DataTable<TData, TValue>({
327
368
  table.getFilteredSelectedRowModel().rows.length > 0 && (
328
369
  <SelectionBar
329
370
  variant="pill"
371
+ label={text.table?.selectedPill ?? "{count} รายการ"}
330
372
  count={table.getFilteredSelectedRowModel().rows.length}
331
373
  onClear={() => table.resetRowSelection()}
332
374
  >
@@ -356,92 +398,233 @@ const QUIET_TABLE = [
356
398
 
357
399
  const SLOT_ORDER: DataTableMobileSlot[] = ["title", "end", "sub", "status", "meta"];
358
400
 
359
- /** `variant="quiet"` บนมือถือ: รายการแถวละ 3 บรรทัดจากคอลัมน์ที่ใส่ `meta.mobile` */
401
+ const LONG_PRESS_MS = 500;
402
+ const HINT_KEY = "ui:data-table:long-press-hint";
403
+
404
+ /** กดค้าง (≥ 500 ms โดยไม่ขยับเกิน 8 px) → เรียก `onLongPress`; `fired` บอกว่า click ถัดไปคือแรงที่ตามหลังการกดค้าง */
405
+ function useLongPress(onLongPress: () => void, enabled: boolean) {
406
+ const timer = React.useRef<ReturnType<typeof setTimeout> | null>(null);
407
+ const origin = React.useRef<{ x: number; y: number } | null>(null);
408
+ const fired = React.useRef(false);
409
+ const cancel = () => {
410
+ if (timer.current) clearTimeout(timer.current);
411
+ timer.current = null;
412
+ };
413
+ React.useEffect(() => cancel, []);
414
+ const handlers = {
415
+ onPointerDown: (event: React.PointerEvent) => {
416
+ // ล้างธงของการกดค้างรอบก่อนทุกครั้งที่เริ่มแตะใหม่ (แม้ปิดการกดค้างอยู่)
417
+ fired.current = false;
418
+ cancel();
419
+ if (!enabled) return;
420
+ origin.current = { x: event.clientX, y: event.clientY };
421
+ timer.current = setTimeout(() => {
422
+ timer.current = null;
423
+ fired.current = true;
424
+ onLongPress();
425
+ }, LONG_PRESS_MS);
426
+ },
427
+ onPointerMove: (event: React.PointerEvent) => {
428
+ const start = origin.current;
429
+ if (start && Math.hypot(event.clientX - start.x, event.clientY - start.y) > 8) cancel();
430
+ },
431
+ onPointerUp: cancel,
432
+ onPointerCancel: cancel,
433
+ onPointerLeave: cancel,
434
+ // เมนูบริบทของเบราว์เซอร์มือถือเด้งตอนกดค้าง — กันไว้เฉพาะช่วงที่เพิ่งกดค้างสำเร็จ
435
+ onContextMenu: (event: React.MouseEvent) => {
436
+ if (fired.current) event.preventDefault();
437
+ },
438
+ };
439
+ return { fired, handlers };
440
+ }
441
+
442
+ /**
443
+ * `variant="quiet"` บนมือถือ: รายการแถวละ 3 บรรทัดจากคอลัมน์ที่ใส่ `meta.mobile`
444
+ * ช่องเลือก (คอลัมน์ `select`) ซ่อนจากสายตาจนกว่าจะเลือก — กดค้างที่แถวเพื่อเข้าโหมดเลือกหมู่,
445
+ * แตะแถวเพื่อสลับ · คีย์บอร์ด/โปรแกรมอ่านจอยังเข้าถึงช่องเลือกได้ตลอด
446
+ */
360
447
  function QuietMobileList<TData>({
361
448
  table,
362
449
  }: {
363
450
  table: import("@tanstack/react-table").Table<TData>;
364
451
  }) {
452
+ const text = useUiText();
365
453
  const slotOf = (column: { columnDef: { meta?: unknown } }) =>
366
454
  (column.columnDef.meta as { mobile?: DataTableMobileSlot } | undefined)?.mobile;
367
455
  const rows = table.getRowModel().rows;
456
+ const selectedCount = table.getFilteredSelectedRowModel().rows.length;
457
+ const selecting = selectedCount > 0;
458
+ const hasSelect = table.getAllLeafColumns().some((column) => column.id === "select");
459
+
460
+ // บรรทัดบอกใบ้: แสดงครั้งแรกครั้งเดียว (หายเองใน 6 วินาที หรือเมื่อกดค้างสำเร็จ) แล้วจำไว้
461
+ const [hint, setHint] = React.useState(false);
462
+ const markHintSeen = React.useCallback(() => {
463
+ setHint(false);
464
+ try {
465
+ localStorage.setItem(HINT_KEY, "1");
466
+ } catch {}
467
+ }, []);
468
+ React.useEffect(() => {
469
+ if (!hasSelect) return;
470
+ try {
471
+ if (localStorage.getItem(HINT_KEY)) return;
472
+ } catch {}
473
+ setHint(true);
474
+ const id = setTimeout(markHintSeen, 6000);
475
+ return () => clearTimeout(id);
476
+ }, [hasSelect, markHintSeen]);
477
+
368
478
  return (
369
- <ul className="divide-y overflow-hidden rounded-xl border bg-card sm:hidden">
370
- {rows.map((row) => {
371
- const cells = row.getVisibleCells();
372
- const pick = (slot: DataTableMobileSlot) =>
373
- cells.filter((cell) => slotOf(cell.column) === slot);
374
- const select = cells.find((cell) => cell.column.id === "select");
375
- return (
376
- <li
377
- key={row.id}
378
- data-state={row.getIsSelected() ? "selected" : undefined}
379
- className="flex items-start gap-3 px-4 py-2.5 data-[state=selected]:bg-primary/[0.07] data-[state=selected]:shadow-[inset_3px_0_0_var(--primary)]"
479
+ <div className="flex flex-col gap-2 sm:hidden">
480
+ {selecting && (
481
+ <div
482
+ role="status"
483
+ aria-live="polite"
484
+ className="flex h-11 items-center justify-between px-1 text-sm"
485
+ >
486
+ <span className="font-semibold">
487
+ {(text.table?.selectedCount ?? "เลือก {count} รายการ").replace(
488
+ "{count}",
489
+ String(selectedCount),
490
+ )}
491
+ </span>
492
+ <Button
493
+ variant="ghost"
494
+ size="sm"
495
+ className="text-primary pointer-coarse:h-11"
496
+ onClick={() => table.toggleAllPageRowsSelected(true)}
380
497
  >
381
- {select && (
382
- // ขยายพื้นที่แตะเป็น 44px (ช่องติ๊กจริงเล็ก 16px); แตะส่วนที่เกินช่องก็สลับการเลือก
383
- // biome-ignore lint/a11y/useKeyWithClickEvents: คีย์บอร์ดใช้ช่องติ๊กข้างใน
384
- // biome-ignore lint/a11y/noStaticElementInteractions: เป็นแค่พื้นที่แตะเสริม
498
+ {text.table?.selectAll ?? "เลือกทั้งหมด"}
499
+ </Button>
500
+ </div>
501
+ )}
502
+ <ul className="divide-y overflow-hidden rounded-xl border bg-card">
503
+ {rows.map((row) => (
504
+ <QuietMobileRow
505
+ key={row.id}
506
+ row={row}
507
+ selecting={selecting}
508
+ canLongPress={hasSelect && row.getCanSelect()}
509
+ slotOf={slotOf}
510
+ onLongPress={() => {
511
+ row.toggleSelected(true);
512
+ markHintSeen();
513
+ try {
514
+ navigator.vibrate?.(10);
515
+ } catch {}
516
+ }}
517
+ />
518
+ ))}
519
+ </ul>
520
+ {!selecting && hint && (
521
+ <p className="text-center text-muted-foreground text-xs">
522
+ {text.table?.longPressHint ?? "กดค้างที่แถวเพื่อเลือกหมู่"}
523
+ </p>
524
+ )}
525
+ </div>
526
+ );
527
+ }
528
+
529
+ function QuietMobileRow<TData>({
530
+ row,
531
+ selecting,
532
+ canLongPress,
533
+ slotOf,
534
+ onLongPress,
535
+ }: {
536
+ row: import("@tanstack/react-table").Row<TData>;
537
+ selecting: boolean;
538
+ canLongPress: boolean;
539
+ slotOf: (column: { columnDef: { meta?: unknown } }) => DataTableMobileSlot | undefined;
540
+ onLongPress: () => void;
541
+ }) {
542
+ const { fired, handlers } = useLongPress(onLongPress, canLongPress && !selecting);
543
+ const cells = row.getVisibleCells();
544
+ const pick = (slot: DataTableMobileSlot) => cells.filter((cell) => slotOf(cell.column) === slot);
545
+ const select = cells.find((cell) => cell.column.id === "select");
546
+ return (
547
+ <li
548
+ data-state={row.getIsSelected() ? "selected" : undefined}
549
+ {...handlers}
550
+ onClick={(event) => {
551
+ // click ที่ตามหลังการกดค้างสำเร็จ ไม่นับเป็นการแตะ (ไม่งั้นเลือกแล้วถูกยกเลิกทันที)
552
+ if (fired.current) {
553
+ fired.current = false;
554
+ return;
555
+ }
556
+ if (!selecting || !canLongPress) return;
557
+ if ((event.target as HTMLElement).closest("a,button,input,[role=checkbox]")) return;
558
+ row.toggleSelected();
559
+ }}
560
+ className={cn(
561
+ "flex items-start gap-3 px-4 py-2.5 data-[state=selected]:bg-primary/[0.07] data-[state=selected]:shadow-[inset_3px_0_0_var(--primary)]",
562
+ selecting && "cursor-pointer",
563
+ )}
564
+ >
565
+ {select && (
566
+ // ขยายพื้นที่แตะเป็น 44px (ช่องติ๊กจริงเล็ก 16px); แตะส่วนที่เกินช่องก็สลับการเลือก
567
+ // ยังไม่เลือก = ซ่อนจากสายตาแต่ยังโฟกัสด้วยคีย์บอร์ดได้ (sr-only + แสดงตอนโฟกัส)
568
+ // biome-ignore lint/a11y/useKeyWithClickEvents: คีย์บอร์ดใช้ช่องติ๊กข้างใน
569
+ // biome-ignore lint/a11y/noStaticElementInteractions: เป็นแค่พื้นที่แตะเสริม
570
+ <div
571
+ className={cn(
572
+ "-m-3.5 -mt-3 shrink-0 p-3.5",
573
+ !selecting && "sr-only focus-within:not-sr-only",
574
+ )}
575
+ onClick={(event) => {
576
+ if (event.target === event.currentTarget) row.toggleSelected();
577
+ }}
578
+ >
579
+ {flexRender(select.column.columnDef.cell, select.getContext())}
580
+ </div>
581
+ )}
582
+ <div className="flex min-w-0 flex-1 flex-col gap-0.5">
583
+ {SLOT_ORDER.map((slot) => {
584
+ const found = pick(slot);
585
+ if (slot === "meta" || found.length === 0) return null;
586
+ const content = found.map((cell) => (
587
+ <span key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</span>
588
+ ));
589
+ if (slot === "title") {
590
+ const end = pick("end");
591
+ return (
385
592
  <div
386
- className="-m-3.5 -mt-3 shrink-0 p-3.5"
387
- onClick={(event) => {
388
- if (event.target === event.currentTarget) row.toggleSelected();
389
- }}
593
+ key={slot}
594
+ className="flex items-baseline justify-between gap-2 font-semibold text-sm"
390
595
  >
391
- {flexRender(select.column.columnDef.cell, select.getContext())}
596
+ <span className="min-w-0 truncate">{content}</span>
597
+ <span className="shrink-0 tabular-nums">
598
+ {end.map((cell) => (
599
+ <span key={cell.id}>
600
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
601
+ </span>
602
+ ))}
603
+ </span>
392
604
  </div>
393
- )}
394
- <div className="flex min-w-0 flex-1 flex-col gap-0.5">
395
- {SLOT_ORDER.map((slot) => {
396
- const found = pick(slot);
397
- if (slot === "meta" || found.length === 0) return null;
398
- const content = found.map((cell) => (
399
- <span key={cell.id}>
400
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
401
- </span>
402
- ));
403
- if (slot === "title") {
404
- const end = pick("end");
405
- return (
406
- <div
407
- key={slot}
408
- className="flex items-baseline justify-between gap-2 font-semibold text-sm"
409
- >
410
- <span className="min-w-0 truncate">{content}</span>
411
- <span className="shrink-0 tabular-nums">
412
- {end.map((cell) => (
413
- <span key={cell.id}>
414
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
415
- </span>
416
- ))}
417
- </span>
418
- </div>
419
- );
420
- }
421
- if (slot === "end") return null;
422
- if (slot === "sub") {
423
- return (
424
- <div key={slot} className="truncate text-muted-foreground text-sm">
425
- {content}
426
- </div>
427
- );
428
- }
429
- const meta = pick("meta");
430
- return (
431
- <div key={slot} className="flex items-center gap-1.5 text-xs">
432
- {content}
433
- {meta.map((cell) => (
434
- <span key={cell.id} className="text-muted-foreground">
435
- · {flexRender(cell.column.columnDef.cell, cell.getContext())}
436
- </span>
437
- ))}
438
- </div>
439
- );
440
- })}
605
+ );
606
+ }
607
+ if (slot === "end") return null;
608
+ if (slot === "sub") {
609
+ return (
610
+ <div key={slot} className="truncate text-muted-foreground text-sm">
611
+ {content}
612
+ </div>
613
+ );
614
+ }
615
+ const meta = pick("meta");
616
+ return (
617
+ <div key={slot} className="flex items-center gap-1.5 text-xs">
618
+ {content}
619
+ {meta.map((cell) => (
620
+ <span key={cell.id} className="text-muted-foreground">
621
+ · {flexRender(cell.column.columnDef.cell, cell.getContext())}
622
+ </span>
623
+ ))}
441
624
  </div>
442
- </li>
443
- );
444
- })}
445
- </ul>
625
+ );
626
+ })}
627
+ </div>
628
+ </li>
446
629
  );
447
630
  }
@@ -117,6 +117,19 @@ export interface DataTableProps<TData, TValue> {
117
117
  variant?: "default" | "quiet";
118
118
  /** `quiet`: ความหนาแน่นเริ่มต้น (ผู้ใช้สลับได้ที่แถบเครื่องมือ) — ค่าเริ่มต้น comfortable */
119
119
  defaultDensity?: "comfortable" | "compact";
120
+ /**
121
+ * `quiet`: จำความหนาแน่นที่ผู้ใช้เลือกไว้ใน localStorage ด้วยคีย์นี้ (เช่น `"po-table-density"`) —
122
+ * ไม่ส่ง = ไม่จำ (กลับเป็น `defaultDensity` ทุกครั้ง) · localStorage ใช้ไม่ได้ก็ทำงานต่อโดยไม่จำ
123
+ */
124
+ densityStorageKey?: string;
125
+ /** ชื่อชนิดข้อมูลสำหรับสถานะว่าง เช่น "ใบสั่งซื้อ" → "ยังไม่มีใบสั่งซื้อ" (`quiet`) */
126
+ resourceName?: string;
127
+ /** `quiet`: ปุ่ม/ลิงก์ในสถานะ "ว่างจริง" (ไม่มีข้อมูลและไม่มีตัวกรอง) เช่น ปุ่ม "สร้างใบสั่งซื้อ" — ไม่ส่ง = ไม่มีปุ่ม */
128
+ emptyAction?: React.ReactNode;
129
+ /** โหลดไม่สำเร็จ: แสดงสถานะผิดพลาดแทนตาราง (ทั้งสอง variant) — ข้อความกลาง ๆ ไม่แสดง error ดิบ */
130
+ error?: boolean;
131
+ /** ปุ่ม "ลองใหม่" ของสถานะผิดพลาด — ไม่ส่ง = ไม่มีปุ่ม */
132
+ onRetry?: () => void;
120
133
  /** `quiet`: ปุ่มคำสั่งในแคปซูลเลือกหมู่ (ส่งออก/ลบ …) — ไม่ส่ง = แคปซูลมีแค่จำนวนที่เลือกและปุ่มล้าง */
121
134
  selectionActions?: (table: Table<TData>) => React.ReactNode;
122
135
  }
@@ -67,12 +67,20 @@ export function SelectionBar({
67
67
  ? "h-11 rounded-full bg-primary py-0 ps-4 pe-2 text-primary-foreground"
68
68
  : "rounded-xl border bg-card",
69
69
  variant !== "inline" &&
70
- "-translate-x-1/2 fixed bottom-[calc(1rem+env(safe-area-inset-bottom))] left-1/2 z-40 max-w-[calc(100vw-2rem)]",
70
+ "-translate-x-1/2 fixed bottom-[calc(1rem+env(safe-area-inset-bottom))] left-1/2 z-40 w-max max-w-[calc(100vw-2rem)]",
71
71
  className,
72
72
  )}
73
73
  >
74
74
  <span className="shrink-0 font-medium text-sm tabular-nums">{message}</span>
75
- <div className="flex min-w-0 flex-wrap items-center gap-1.5">
75
+ <div
76
+ className={cn(
77
+ "flex min-w-0 items-center gap-1.5",
78
+ // pill: บรรทัดเดียวเสมอ ปุ่มไม่ตัดคำ และปุ่มที่แอปส่งมาใช้สีตัวอักษรบนพื้นสีหลัก
79
+ pill
80
+ ? "[&_button]:whitespace-nowrap [&_button]:text-primary-foreground [&_button]:hover:bg-primary-foreground/15 pointer-coarse:[&_button]:h-11"
81
+ : "flex-wrap",
82
+ )}
83
+ >
76
84
  {actions?.map((action) => (
77
85
  <Button
78
86
  key={action.label}
@@ -98,6 +106,7 @@ export function SelectionBar({
98
106
  onClick={onClear}
99
107
  className={cn(
100
108
  "ml-auto shrink-0",
109
+ pill && "pointer-coarse:h-11 pointer-coarse:w-11",
101
110
  pill && "rounded-full hover:bg-primary-foreground/15 hover:text-primary-foreground",
102
111
  )}
103
112
  >
@@ -85,6 +85,27 @@ export interface UiText {
85
85
  rangeOf: string;
86
86
  shownOf: string;
87
87
  density: string;
88
+ /** `variant="quiet"` มือถือ — `{count}` */
89
+ selectedCount: string;
90
+ /** สถานะตาราง: `emptyTitleOf` ใช้ `{resource}`, `noMatchQuery` ใช้ `{query}` */
91
+ loading: string;
92
+ emptyTitle: string;
93
+ emptyTitleOf: string;
94
+ emptyHint: string;
95
+ noMatch: string;
96
+ noMatchQuery: string;
97
+ noMatchHint: string;
98
+ clearAllFilters: string;
99
+ clearQuery: string;
100
+ errorTitle: string;
101
+ errorHint: string;
102
+ retry: string;
103
+ /** ข้อความในแคปซูลเลือกหมู่ (สั้นกว่า `selectedCount` เพราะหัวรายการมือถือบอก "เลือก N รายการ" อยู่แล้ว) */
104
+ selectedPill: string;
105
+ selectAll: string;
106
+ longPressHint: string;
107
+ clearFilters: string;
108
+ showResults: string;
88
109
  rowsSelected: string;
89
110
  noResults: string;
90
111
  deleteTitle: string;
@@ -323,6 +344,24 @@ export const UI_TEXT_TH: UiText = {
323
344
  rangeOf: "{from}–{to} จาก {total}",
324
345
  shownOf: "{shown} จาก {total} รายการ",
325
346
  density: "ความหนาแน่นของแถว",
347
+ selectedCount: "เลือก {count} รายการ",
348
+ loading: "กำลังโหลด…",
349
+ emptyTitle: "ยังไม่มีรายการ",
350
+ emptyTitleOf: "ยังไม่มี{resource}",
351
+ emptyHint: "เมื่อสร้างแล้วจะแสดงที่นี่",
352
+ noMatch: "ไม่พบรายการที่ตรงกัน",
353
+ noMatchQuery: 'ไม่พบรายการที่ตรงกับ "{query}"',
354
+ noMatchHint: "ลองคำค้นอื่น หรือเอาตัวกรองออกบางตัว",
355
+ clearAllFilters: "ล้างตัวกรองทั้งหมด",
356
+ clearQuery: "ล้างคำค้น",
357
+ errorTitle: "โหลดรายการไม่สำเร็จ",
358
+ errorHint: "การเชื่อมต่อขัดข้อง ลองใหม่อีกครั้ง",
359
+ retry: "ลองใหม่",
360
+ selectedPill: "{count} รายการ",
361
+ selectAll: "เลือกทั้งหมด",
362
+ longPressHint: "กดค้างที่แถวเพื่อเลือกหมู่",
363
+ clearFilters: "ล้าง",
364
+ showResults: "ดู {count} รายการ",
326
365
  rowsSelected: "เลือก {selected} จาก {total} แถว",
327
366
  noResults: "ไม่พบข้อมูล",
328
367
  deleteTitle: "ยืนยันการลบ",
@@ -540,6 +579,24 @@ export const UI_TEXT_EN: UiText = {
540
579
  rangeOf: "{from}–{to} of {total}",
541
580
  shownOf: "{shown} of {total} items",
542
581
  density: "Row density",
582
+ selectedCount: "{count} selected",
583
+ loading: "Loading…",
584
+ emptyTitle: "Nothing here yet",
585
+ emptyTitleOf: "No {resource} yet",
586
+ emptyHint: "Items will show up here once created.",
587
+ noMatch: "No matching items",
588
+ noMatchQuery: 'No items match "{query}"',
589
+ noMatchHint: "Try another search, or remove some filters.",
590
+ clearAllFilters: "Clear all filters",
591
+ clearQuery: "Clear search",
592
+ errorTitle: "Couldn't load the list",
593
+ errorHint: "The connection dropped. Try again.",
594
+ retry: "Try again",
595
+ selectedPill: "{count} items",
596
+ selectAll: "Select all",
597
+ longPressHint: "Press and hold a row to select several",
598
+ clearFilters: "Clear",
599
+ showResults: "Show {count} items",
543
600
  rowsSelected: "{selected} of {total} row(s) selected",
544
601
  noResults: "No results",
545
602
  deleteTitle: "Are you sure?",