@suphark/ui 0.60.0 → 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.
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/components/data-table/data-table-states.tsx +150 -0
- package/src/components/data-table/data-table-toolbar.tsx +8 -3
- package/src/components/data-table/data-table.tsx +28 -4
- package/src/components/data-table/types.ts +8 -0
- package/src/components/shared/ui-text.tsx +37 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
รูปแบบตาม [Keep a Changelog](https://keepachangelog.com/) · semver: patch = หน้าตา/บั๊ก, minor = component/prop ใหม่, major = เปลี่ยน/ลบ props
|
|
4
4
|
|
|
5
|
+
## [0.61.0] — 2026-10-24
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `DataTable` สถานะตาราง (ตาม mockup): **ผิดพลาด** — prop `error` แสดงกล่อง "โหลดรายการไม่สำเร็จ" แทนตาราง (ข้อความกลาง ไม่แสดง error ดิบ, ซ่อนเลขหน้า) + `onRetry` = ปุ่ม "ลองใหม่" (ทั้งสอง variant) · `variant="quiet"`: **ไม่พบ** — บอกคำค้น "ไม่พบรายการที่ตรงกับ …" + ปุ่ม "ล้างตัวกรองทั้งหมด"/"ล้างคำค้น" (เคารพ `preservedFilterIds`), **ว่างจริง** — `resourceName` ("ยังไม่มีใบสั่งซื้อ") + `emptyAction` (ปุ่มสร้าง; ไม่ส่ง = ไม่มีปุ่ม), **โหลด** — ตัวนับ "กำลังโหลด…" และ skeleton รายการ 3 บรรทัดบนมือถือ · กล่องสถานะกว้างไม่เกินจอบนมือถือ ปุ่มสูง 44px บนจอสัมผัส · `emptyStateComponent` ของแอปยังทับได้ทั้งหมด
|
|
9
|
+
- `DataTableEmptyState`, `DataTableErrorState` (ภายใน `data-table-states.tsx`), `UiText.table.loading` / `emptyTitle` / `emptyTitleOf` / `emptyHint` / `noMatch` / `noMatchQuery` / `noMatchHint` / `clearAllFilters` / `clearQuery` / `errorTitle` / `errorHint` / `retry`
|
|
10
|
+
- showcase: เดโม DataTable สลับสถานะ ปกติ/กำลังโหลด/ว่างจริง/ผิดพลาดได้
|
|
11
|
+
|
|
5
12
|
## [0.60.0] — 2026-10-24
|
|
6
13
|
|
|
7
14
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suphark/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.61.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Design system กลางของ Suphark apps: shadcn/ui (base-nova, Base UI, Tailwind v4) + shared components + token/ฟอนต์ Noto Sans Thai Looped — ส่งเป็น source ให้แอป Next transpile เอง",
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@suphark/ui/components/shared/button/button";
|
|
4
|
+
import { useUiText } from "@suphark/ui/components/shared/ui-text";
|
|
5
|
+
import { cn } from "@suphark/ui/lib/utils";
|
|
6
|
+
import type { Table } from "@tanstack/react-table";
|
|
7
|
+
import { Inbox, SearchX, TriangleAlert } from "lucide-react";
|
|
8
|
+
import type React from "react";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* กล่องสถานะกลางตาราง (ไม่พบ / ว่าง / ผิดพลาด) — บนมือถือจำกัดความกว้างเท่าจอ
|
|
12
|
+
* เพื่อไม่ให้ไปอยู่กลางตารางที่เลื่อนแนวนอน (sticky ที่ขอบซ้ายของกล่องเลื่อน)
|
|
13
|
+
*/
|
|
14
|
+
function StateBox({
|
|
15
|
+
icon,
|
|
16
|
+
title,
|
|
17
|
+
hint,
|
|
18
|
+
tone,
|
|
19
|
+
children,
|
|
20
|
+
}: {
|
|
21
|
+
icon: React.ReactNode;
|
|
22
|
+
title: string;
|
|
23
|
+
hint?: string;
|
|
24
|
+
tone?: "danger";
|
|
25
|
+
children?: React.ReactNode;
|
|
26
|
+
}) {
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
role={tone === "danger" ? "alert" : "status"}
|
|
30
|
+
className="sticky left-0 mx-auto flex w-[calc(100vw-3rem)] max-w-full flex-col items-center gap-1.5 px-4 py-8 text-center sm:w-auto"
|
|
31
|
+
>
|
|
32
|
+
<div
|
|
33
|
+
aria-hidden="true"
|
|
34
|
+
className={cn(
|
|
35
|
+
"flex size-10 items-center justify-center rounded-full [&_svg]:size-5",
|
|
36
|
+
tone === "danger"
|
|
37
|
+
? "bg-destructive-subtle text-destructive-subtle-foreground"
|
|
38
|
+
: "bg-muted text-muted-foreground",
|
|
39
|
+
)}
|
|
40
|
+
>
|
|
41
|
+
{icon}
|
|
42
|
+
</div>
|
|
43
|
+
<h3 className="mt-1.5 font-semibold text-sm">{title}</h3>
|
|
44
|
+
{hint && <p className="max-w-[34ch] text-muted-foreground text-xs">{hint}</p>}
|
|
45
|
+
{children && (
|
|
46
|
+
<div className="mt-2 flex flex-wrap justify-center gap-2 [&_button]:pointer-coarse:h-11">
|
|
47
|
+
{children}
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** ไม่พบรายการ: มีตัวกรอง/คำค้นอยู่ → ล้างได้ทันที · ไม่มีเลย → ว่างจริง (+ปุ่มที่แอปส่งมา) */
|
|
55
|
+
export function DataTableEmptyState<TData>({
|
|
56
|
+
table,
|
|
57
|
+
preservedFilterIds = [],
|
|
58
|
+
resourceName,
|
|
59
|
+
emptyAction,
|
|
60
|
+
}: {
|
|
61
|
+
table: Table<TData>;
|
|
62
|
+
preservedFilterIds?: string[];
|
|
63
|
+
resourceName?: string;
|
|
64
|
+
emptyAction?: React.ReactNode;
|
|
65
|
+
}) {
|
|
66
|
+
const text = useUiText();
|
|
67
|
+
const keep = new Set(preservedFilterIds);
|
|
68
|
+
const query = String(table.getState().globalFilter ?? "").trim();
|
|
69
|
+
const hasColumnFilters = table.getState().columnFilters.some((filter) => !keep.has(filter.id));
|
|
70
|
+
if (!query && !hasColumnFilters) {
|
|
71
|
+
return (
|
|
72
|
+
<StateBox
|
|
73
|
+
icon={<Inbox />}
|
|
74
|
+
title={
|
|
75
|
+
resourceName
|
|
76
|
+
? (text.table?.emptyTitleOf ?? "ยังไม่มี{resource}").replace("{resource}", resourceName)
|
|
77
|
+
: (text.table?.emptyTitle ?? "ยังไม่มีรายการ")
|
|
78
|
+
}
|
|
79
|
+
hint={text.table?.emptyHint ?? "เมื่อสร้างแล้วจะแสดงที่นี่"}
|
|
80
|
+
>
|
|
81
|
+
{emptyAction}
|
|
82
|
+
</StateBox>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const clearQuery = () => table.setGlobalFilter("");
|
|
86
|
+
const clearAll = () => {
|
|
87
|
+
table.setColumnFilters((filters) => filters.filter((filter) => keep.has(filter.id)));
|
|
88
|
+
table.setGlobalFilter("");
|
|
89
|
+
};
|
|
90
|
+
return (
|
|
91
|
+
<StateBox
|
|
92
|
+
icon={<SearchX />}
|
|
93
|
+
title={
|
|
94
|
+
query
|
|
95
|
+
? (text.table?.noMatchQuery ?? 'ไม่พบรายการที่ตรงกับ "{query}"').replace("{query}", query)
|
|
96
|
+
: (text.table?.noMatch ?? "ไม่พบรายการที่ตรงกัน")
|
|
97
|
+
}
|
|
98
|
+
hint={text.table?.noMatchHint ?? "ลองคำค้นอื่น หรือเอาตัวกรองออกบางตัว"}
|
|
99
|
+
>
|
|
100
|
+
<Button size="sm" onClick={clearAll}>
|
|
101
|
+
{text.table?.clearAllFilters ?? "ล้างตัวกรองทั้งหมด"}
|
|
102
|
+
</Button>
|
|
103
|
+
{query && hasColumnFilters && (
|
|
104
|
+
<Button size="sm" variant="outline" onClick={clearQuery}>
|
|
105
|
+
{text.table?.clearQuery ?? "ล้างคำค้น"}
|
|
106
|
+
</Button>
|
|
107
|
+
)}
|
|
108
|
+
</StateBox>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** โหลดไม่สำเร็จ: ข้อความกลาง ๆ (ไม่แสดงข้อความดิบของระบบ) + ปุ่มลองใหม่เมื่อส่ง `onRetry` */
|
|
113
|
+
export function DataTableErrorState({ onRetry }: { onRetry?: () => void }) {
|
|
114
|
+
const text = useUiText();
|
|
115
|
+
return (
|
|
116
|
+
<StateBox
|
|
117
|
+
tone="danger"
|
|
118
|
+
icon={<TriangleAlert />}
|
|
119
|
+
title={text.table?.errorTitle ?? "โหลดรายการไม่สำเร็จ"}
|
|
120
|
+
hint={text.table?.errorHint ?? "การเชื่อมต่อขัดข้อง ลองใหม่อีกครั้ง"}
|
|
121
|
+
>
|
|
122
|
+
{onRetry && (
|
|
123
|
+
<Button size="sm" onClick={onRetry}>
|
|
124
|
+
{text.table?.retry ?? "ลองใหม่"}
|
|
125
|
+
</Button>
|
|
126
|
+
)}
|
|
127
|
+
</StateBox>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** skeleton ของรายการมือถือ (แถวละ 3 บรรทัด) ระหว่างโหลด */
|
|
132
|
+
export function QuietMobileSkeleton({ rows }: { rows: number }) {
|
|
133
|
+
const text = useUiText();
|
|
134
|
+
return (
|
|
135
|
+
<ul
|
|
136
|
+
aria-busy="true"
|
|
137
|
+
aria-label={text.table?.loading ?? "กำลังโหลด…"}
|
|
138
|
+
className="divide-y overflow-hidden rounded-xl border bg-card sm:hidden"
|
|
139
|
+
>
|
|
140
|
+
{Array.from({ length: Math.min(rows, 5) }).map((_, i) => (
|
|
141
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: แถว skeleton ไม่มีตัวตน ใช้ตำแหน่งเป็น key ได้
|
|
142
|
+
<li key={i} className="flex flex-col gap-2 px-4 py-3">
|
|
143
|
+
<div className="h-3 w-1/2 animate-pulse rounded bg-muted" />
|
|
144
|
+
<div className="h-3 w-4/5 animate-pulse rounded bg-muted" />
|
|
145
|
+
<div className="h-3 w-1/3 animate-pulse rounded bg-muted" />
|
|
146
|
+
</li>
|
|
147
|
+
))}
|
|
148
|
+
</ul>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -40,6 +40,8 @@ interface DataTableToolbarProps<TData> {
|
|
|
40
40
|
variant?: "default" | "quiet";
|
|
41
41
|
density?: "comfortable" | "compact";
|
|
42
42
|
onDensityChange?: (density: "comfortable" | "compact") => void;
|
|
43
|
+
/** `quiet`: ตัวนับแสดง "กำลังโหลด…" */
|
|
44
|
+
isLoading?: boolean;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
export function DataTableToolbar<TData>({
|
|
@@ -52,6 +54,7 @@ export function DataTableToolbar<TData>({
|
|
|
52
54
|
variant = "default",
|
|
53
55
|
density = "comfortable",
|
|
54
56
|
onDensityChange,
|
|
57
|
+
isLoading,
|
|
55
58
|
}: DataTableToolbarProps<TData>) {
|
|
56
59
|
const text = useUiText();
|
|
57
60
|
const isMobile = useIsMobile();
|
|
@@ -280,9 +283,11 @@ export function DataTableToolbar<TData>({
|
|
|
280
283
|
})}
|
|
281
284
|
<div className="ml-auto flex items-center gap-1">
|
|
282
285
|
<span className="me-1 text-muted-foreground text-xs tabular-nums">
|
|
283
|
-
{
|
|
284
|
-
.
|
|
285
|
-
.
|
|
286
|
+
{isLoading
|
|
287
|
+
? (text.table?.loading ?? "กำลังโหลด…")
|
|
288
|
+
: (text.table?.shownOf ?? "{shown} จาก {total} รายการ")
|
|
289
|
+
.replace("{shown}", String(shown))
|
|
290
|
+
.replace("{total}", String(total))}
|
|
286
291
|
</span>
|
|
287
292
|
{actionsComponent}
|
|
288
293
|
{onDensityChange && (
|
|
@@ -15,6 +15,7 @@ import { cn } from "@suphark/ui/lib/utils";
|
|
|
15
15
|
import { flexRender } from "@tanstack/react-table";
|
|
16
16
|
import * as React from "react";
|
|
17
17
|
import { DataTablePagination } from "./data-table-pagination";
|
|
18
|
+
import { DataTableEmptyState, DataTableErrorState, QuietMobileSkeleton } from "./data-table-states";
|
|
18
19
|
import { DataTableToolbar } from "./data-table-toolbar";
|
|
19
20
|
import type { DataTableMobileSlot, DataTableProps } from "./types";
|
|
20
21
|
import { useDataTable } from "./use-data-table";
|
|
@@ -86,6 +87,10 @@ export function DataTable<TData, TValue>({
|
|
|
86
87
|
defaultDensity = "comfortable",
|
|
87
88
|
selectionActions,
|
|
88
89
|
densityStorageKey,
|
|
90
|
+
resourceName,
|
|
91
|
+
emptyAction,
|
|
92
|
+
error,
|
|
93
|
+
onRetry,
|
|
89
94
|
}: DataTableProps<TData, TValue>) {
|
|
90
95
|
const text = useUiText();
|
|
91
96
|
const quiet = variant === "quiet";
|
|
@@ -193,7 +198,13 @@ export function DataTable<TData, TValue>({
|
|
|
193
198
|
))}
|
|
194
199
|
</TableHeader>
|
|
195
200
|
<TableBody>
|
|
196
|
-
{
|
|
201
|
+
{error ? (
|
|
202
|
+
<TableRow>
|
|
203
|
+
<TableCell colSpan={table.getVisibleLeafColumns().length}>
|
|
204
|
+
<DataTableErrorState onRetry={onRetry} />
|
|
205
|
+
</TableCell>
|
|
206
|
+
</TableRow>
|
|
207
|
+
) : isLoading ? (
|
|
197
208
|
Array.from({ length: initialPageSize }).map((_, i) => (
|
|
198
209
|
// biome-ignore lint/suspicious/noArrayIndexKey: แถว skeleton ไม่มีตัวตน ใช้ตำแหน่งเป็น key ได้
|
|
199
210
|
<TableRow key={i}>
|
|
@@ -246,7 +257,17 @@ export function DataTable<TData, TValue>({
|
|
|
246
257
|
) : (
|
|
247
258
|
<TableRow>
|
|
248
259
|
<TableCell colSpan={table.getVisibleLeafColumns().length} className="h-24 text-center">
|
|
249
|
-
{emptyStateComponent ||
|
|
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
|
+
))}
|
|
250
271
|
</TableCell>
|
|
251
272
|
</TableRow>
|
|
252
273
|
)}
|
|
@@ -258,8 +279,10 @@ export function DataTable<TData, TValue>({
|
|
|
258
279
|
.getAllLeafColumns()
|
|
259
280
|
.some((column) => (column.columnDef.meta as { mobile?: string } | undefined)?.mobile);
|
|
260
281
|
const mobileList =
|
|
261
|
-
quiet && hasMobileSlots && !isLoading && table.getRowModel().rows.length > 0 ? (
|
|
282
|
+
quiet && hasMobileSlots && !isLoading && !error && table.getRowModel().rows.length > 0 ? (
|
|
262
283
|
<QuietMobileList table={table} />
|
|
284
|
+
) : quiet && hasMobileSlots && isLoading && !error ? (
|
|
285
|
+
<QuietMobileSkeleton rows={initialPageSize} />
|
|
263
286
|
) : null;
|
|
264
287
|
|
|
265
288
|
const customView = renderCustomView?.(table);
|
|
@@ -280,6 +303,7 @@ export function DataTable<TData, TValue>({
|
|
|
280
303
|
variant={variant}
|
|
281
304
|
density={density}
|
|
282
305
|
onDensityChange={setDensity}
|
|
306
|
+
isLoading={isLoading}
|
|
283
307
|
/>
|
|
284
308
|
)}
|
|
285
309
|
{customView ? (
|
|
@@ -321,7 +345,7 @@ export function DataTable<TData, TValue>({
|
|
|
321
345
|
renderTable(false)
|
|
322
346
|
)}
|
|
323
347
|
</div>
|
|
324
|
-
{showPagination && (
|
|
348
|
+
{showPagination && !error && (
|
|
325
349
|
<div
|
|
326
350
|
className={
|
|
327
351
|
stickyPagination
|
|
@@ -122,6 +122,14 @@ export interface DataTableProps<TData, TValue> {
|
|
|
122
122
|
* ไม่ส่ง = ไม่จำ (กลับเป็น `defaultDensity` ทุกครั้ง) · localStorage ใช้ไม่ได้ก็ทำงานต่อโดยไม่จำ
|
|
123
123
|
*/
|
|
124
124
|
densityStorageKey?: string;
|
|
125
|
+
/** ชื่อชนิดข้อมูลสำหรับสถานะว่าง เช่น "ใบสั่งซื้อ" → "ยังไม่มีใบสั่งซื้อ" (`quiet`) */
|
|
126
|
+
resourceName?: string;
|
|
127
|
+
/** `quiet`: ปุ่ม/ลิงก์ในสถานะ "ว่างจริง" (ไม่มีข้อมูลและไม่มีตัวกรอง) เช่น ปุ่ม "สร้างใบสั่งซื้อ" — ไม่ส่ง = ไม่มีปุ่ม */
|
|
128
|
+
emptyAction?: React.ReactNode;
|
|
129
|
+
/** โหลดไม่สำเร็จ: แสดงสถานะผิดพลาดแทนตาราง (ทั้งสอง variant) — ข้อความกลาง ๆ ไม่แสดง error ดิบ */
|
|
130
|
+
error?: boolean;
|
|
131
|
+
/** ปุ่ม "ลองใหม่" ของสถานะผิดพลาด — ไม่ส่ง = ไม่มีปุ่ม */
|
|
132
|
+
onRetry?: () => void;
|
|
125
133
|
/** `quiet`: ปุ่มคำสั่งในแคปซูลเลือกหมู่ (ส่งออก/ลบ …) — ไม่ส่ง = แคปซูลมีแค่จำนวนที่เลือกและปุ่มล้าง */
|
|
126
134
|
selectionActions?: (table: Table<TData>) => React.ReactNode;
|
|
127
135
|
}
|
|
@@ -87,6 +87,19 @@ export interface UiText {
|
|
|
87
87
|
density: string;
|
|
88
88
|
/** `variant="quiet"` มือถือ — `{count}` */
|
|
89
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;
|
|
90
103
|
/** ข้อความในแคปซูลเลือกหมู่ (สั้นกว่า `selectedCount` เพราะหัวรายการมือถือบอก "เลือก N รายการ" อยู่แล้ว) */
|
|
91
104
|
selectedPill: string;
|
|
92
105
|
selectAll: string;
|
|
@@ -332,6 +345,18 @@ export const UI_TEXT_TH: UiText = {
|
|
|
332
345
|
shownOf: "{shown} จาก {total} รายการ",
|
|
333
346
|
density: "ความหนาแน่นของแถว",
|
|
334
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: "ลองใหม่",
|
|
335
360
|
selectedPill: "{count} รายการ",
|
|
336
361
|
selectAll: "เลือกทั้งหมด",
|
|
337
362
|
longPressHint: "กดค้างที่แถวเพื่อเลือกหมู่",
|
|
@@ -555,6 +580,18 @@ export const UI_TEXT_EN: UiText = {
|
|
|
555
580
|
shownOf: "{shown} of {total} items",
|
|
556
581
|
density: "Row density",
|
|
557
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",
|
|
558
595
|
selectedPill: "{count} items",
|
|
559
596
|
selectAll: "Select all",
|
|
560
597
|
longPressHint: "Press and hold a row to select several",
|