terrazzo 0.6.0 → 0.7.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.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -0
  3. data/Rakefile +1 -0
  4. data/app/controllers/terrazzo/application_controller.rb +63 -14
  5. data/app/helpers/terrazzo/collection_actions_helper.rb +38 -13
  6. data/app/helpers/terrazzo/resource_paths_helper.rb +55 -0
  7. data/app/views/terrazzo/application/_edit_base.json.props +7 -4
  8. data/app/views/terrazzo/application/_index_base.json.props +83 -7
  9. data/app/views/terrazzo/application/_navigation.json.props +7 -4
  10. data/app/views/terrazzo/application/_new_base.json.props +6 -3
  11. data/app/views/terrazzo/application/_show_base.json.props +7 -5
  12. data/lib/generators/terrazzo/dashboard/dashboard_generator.rb +48 -24
  13. data/lib/generators/terrazzo/dashboard/templates/controller.rb.erb +8 -4
  14. data/lib/generators/terrazzo/dashboard/templates/dashboard.rb.erb +33 -25
  15. data/lib/generators/terrazzo/eject/eject_generator.rb +223 -113
  16. data/lib/generators/terrazzo/field/field_generator.rb +51 -7
  17. data/lib/generators/terrazzo/field/templates/FormField.jsx.erb +2 -3
  18. data/lib/generators/terrazzo/install/install_generator.rb +641 -4
  19. data/lib/generators/terrazzo/install/templates/admin.css +2 -0
  20. data/lib/generators/terrazzo/install/templates/application.js.erb +3 -0
  21. data/lib/generators/terrazzo/install/templates/components.json.erb +17 -0
  22. data/lib/generators/terrazzo/install/templates/custom_page_mapping.js.erb +5 -0
  23. data/lib/generators/terrazzo/install/templates/generated_page_mapping.js.erb +10 -0
  24. data/lib/generators/terrazzo/install/templates/jsconfig.json +8 -0
  25. data/lib/generators/terrazzo/install/templates/page_to_page_mapping.js.erb +9 -1
  26. data/lib/generators/terrazzo/install/templates/superglue.html.erb.erb +2 -2
  27. data/lib/generators/terrazzo/routes/routes_generator.rb +55 -20
  28. data/lib/generators/terrazzo/views/edit_generator.rb +16 -10
  29. data/lib/generators/terrazzo/views/eject_compatibility.rb +19 -0
  30. data/lib/generators/terrazzo/views/field_generator.rb +18 -6
  31. data/lib/generators/terrazzo/views/generated_defaults_helper.rb +110 -0
  32. data/lib/generators/terrazzo/views/index_generator.rb +9 -8
  33. data/lib/generators/terrazzo/views/layout_generator.rb +4 -3
  34. data/lib/generators/terrazzo/views/navigation_generator.rb +5 -1
  35. data/lib/generators/terrazzo/views/new_generator.rb +16 -10
  36. data/lib/generators/terrazzo/views/page_mapping_helper.rb +131 -8
  37. data/lib/generators/terrazzo/views/show_generator.rb +7 -6
  38. data/lib/generators/terrazzo/views/templates/components/CollectionFilters.jsx +25 -0
  39. data/lib/generators/terrazzo/views/templates/components/CollectionItemActions.jsx +68 -0
  40. data/lib/generators/terrazzo/views/templates/components/CollectionToolbarActions.jsx +60 -0
  41. data/lib/generators/terrazzo/views/templates/components/HasManyPagination.jsx +36 -0
  42. data/lib/generators/terrazzo/views/templates/components/Layout.jsx +9 -4
  43. data/lib/generators/terrazzo/views/templates/components/Pagination.jsx +32 -24
  44. data/lib/generators/terrazzo/views/templates/components/ResourceTable.jsx +172 -0
  45. data/lib/generators/terrazzo/views/templates/components/SearchBar.jsx +14 -6
  46. data/lib/generators/terrazzo/views/templates/components/SortableHeader.jsx +7 -5
  47. data/lib/generators/terrazzo/views/templates/components/app-sidebar.jsx +1 -1
  48. data/lib/generators/terrazzo/views/templates/components/site-header.jsx +2 -2
  49. data/lib/generators/terrazzo/views/templates/fields/asset/FormField.jsx +15 -0
  50. data/lib/generators/terrazzo/views/templates/fields/asset/IndexField.jsx +5 -0
  51. data/lib/generators/terrazzo/views/templates/fields/asset/ShowField.jsx +13 -0
  52. data/lib/generators/terrazzo/views/templates/fields/belongs_to/FormField.jsx +1 -1
  53. data/lib/generators/terrazzo/views/templates/fields/boolean/FormField.jsx +1 -1
  54. data/lib/generators/terrazzo/views/templates/fields/boolean/IndexField.jsx +1 -1
  55. data/lib/generators/terrazzo/views/templates/fields/has_many/FormField.jsx +3 -3
  56. data/lib/generators/terrazzo/views/templates/fields/has_many/IndexField.jsx +4 -3
  57. data/lib/generators/terrazzo/views/templates/fields/has_many/ShowField.jsx +21 -35
  58. data/lib/generators/terrazzo/views/templates/fields/has_one/FormField.jsx +1 -1
  59. data/lib/generators/terrazzo/views/templates/fields/hstore/FormField.jsx +5 -5
  60. data/lib/generators/terrazzo/views/templates/fields/hstore/IndexField.jsx +0 -2
  61. data/lib/generators/terrazzo/views/templates/fields/hstore/ShowField.jsx +1 -1
  62. data/lib/generators/terrazzo/views/templates/fields/number/IndexField.jsx +1 -1
  63. data/lib/generators/terrazzo/views/templates/fields/number/ShowField.jsx +1 -1
  64. data/lib/generators/terrazzo/views/templates/fields/polymorphic/FormField.jsx +3 -2
  65. data/lib/generators/terrazzo/views/templates/fields/rich_text/FormField.jsx +2 -2
  66. data/lib/generators/terrazzo/views/templates/fields/select/FormField.jsx +1 -1
  67. data/lib/generators/terrazzo/views/templates/fields/select/IndexField.jsx +1 -1
  68. data/lib/generators/terrazzo/views/templates/fields/shared/TextInputFormField.jsx +2 -2
  69. data/lib/generators/terrazzo/views/templates/fields/text/FormField.jsx +2 -2
  70. data/lib/generators/terrazzo/views/templates/pages/_collection.jsx +5 -3
  71. data/lib/generators/terrazzo/views/templates/pages/_form.jsx +37 -30
  72. data/lib/generators/terrazzo/views/templates/pages/_navigation.json.props +14 -4
  73. data/lib/generators/terrazzo/views/templates/pages/edit.jsx +6 -4
  74. data/lib/generators/terrazzo/views/templates/pages/index.jsx +20 -8
  75. data/lib/generators/terrazzo/views/templates/pages/new.jsx +8 -4
  76. data/lib/generators/terrazzo/views/templates/pages/show.jsx +30 -32
  77. data/lib/generators/terrazzo/views/views_generator.rb +9 -5
  78. data/lib/terrazzo/base_dashboard.rb +167 -5
  79. data/lib/terrazzo/csv_export.rb +38 -0
  80. data/lib/terrazzo/field/associative.rb +16 -4
  81. data/lib/terrazzo/field/base.rb +4 -0
  82. data/lib/terrazzo/field/belongs_to.rb +1 -2
  83. data/lib/terrazzo/field/deferred.rb +4 -0
  84. data/lib/terrazzo/field/has_many.rb +79 -11
  85. data/lib/terrazzo/field/password.rb +6 -0
  86. data/lib/terrazzo/field/rich_text.rb +13 -0
  87. data/lib/terrazzo/generator_helpers.rb +1 -1
  88. data/lib/terrazzo/has_many_pagination.rb +9 -1
  89. data/lib/terrazzo/namespace/resource.rb +30 -0
  90. data/lib/terrazzo/namespace.rb +26 -1
  91. data/lib/terrazzo/search.rb +29 -9
  92. data/lib/terrazzo/version.rb +1 -1
  93. data/lib/terrazzo.rb +1 -0
  94. data/terrazzo.gemspec +11 -1
  95. metadata +40 -6
  96. data/lib/generators/terrazzo/install/templates/application.json.props.erb +0 -17
  97. data/lib/generators/terrazzo/views/templates/fields/FieldRenderer.jsx +0 -103
  98. data/lib/generators/terrazzo/views/templates/fields/index.js +0 -81
@@ -0,0 +1,172 @@
1
+ import React, { useContext, useState } from "react";
2
+ import { NavigationContext } from "@thoughtbot/superglue";
3
+
4
+ import { SortableHeader as DefaultSortableHeader } from "./SortableHeader";
5
+ import { CollectionItemActions as DefaultCollectionItemActions } from "./CollectionItemActions";
6
+ import { CollectionToolbarActions as DefaultCollectionToolbarActions } from "./CollectionToolbarActions";
7
+ import { getComponent, cn } from "terrazzo";
8
+ import { FieldRenderer } from "../fields";
9
+ import {
10
+ Table,
11
+ TableHeader,
12
+ TableBody,
13
+ TableRow,
14
+ TableHead,
15
+ TableCell,
16
+ } from "./ui";
17
+
18
+ export function ResourceTable({ headers = [], rows = [], emptyState, showActions = true, bulkActions = [] }) {
19
+ const { visit } = useContext(NavigationContext);
20
+ const SortableHeader = getComponent("SortableHeader") || DefaultSortableHeader;
21
+ const CollectionItemActions = getComponent("CollectionItemActions") || DefaultCollectionItemActions;
22
+ const CollectionToolbarActions = getComponent("CollectionToolbarActions") || DefaultCollectionToolbarActions;
23
+ const safeHeaders = Array.isArray(headers) ? headers : [];
24
+ const safeRows = Array.isArray(rows) ? rows : [];
25
+ const safeBulkActions = Array.isArray(bulkActions) ? bulkActions : [];
26
+ const selectable = safeBulkActions.length > 0 && safeRows.length > 0;
27
+ const rowIds = safeRows.map((row) => String(row.id));
28
+ const [selectedIds, setSelectedIds] = useState(() => new Set());
29
+ const selectedRowIds = rowIds.filter((id) => selectedIds.has(id));
30
+ const selectedCount = selectedRowIds.length;
31
+ const allSelected = selectable && selectedCount === rowIds.length;
32
+ const someSelected = selectedCount > 0 && !allSelected;
33
+ const columnCount = safeHeaders.length + (showActions ? 1 : 0) + (selectable ? 1 : 0);
34
+
35
+ const handleRowClick = (e, showPath) => {
36
+ if (!showPath) return;
37
+ if (e.target.closest("a, button, form, input, label")) return;
38
+ if (window.getSelection().toString()) return;
39
+ visit(showPath, {});
40
+ };
41
+
42
+ const toggleRow = (id, checked) => {
43
+ setSelectedIds((current) => {
44
+ const next = new Set(current);
45
+ if (checked) {
46
+ next.add(id);
47
+ } else {
48
+ next.delete(id);
49
+ }
50
+ return next;
51
+ });
52
+ };
53
+
54
+ const toggleAllRows = (checked) => {
55
+ setSelectedIds((current) => {
56
+ const next = new Set(current);
57
+ rowIds.forEach((id) => {
58
+ if (checked) {
59
+ next.add(id);
60
+ } else {
61
+ next.delete(id);
62
+ }
63
+ });
64
+ return next;
65
+ });
66
+ };
67
+
68
+ return (
69
+ <div className="rounded-md border">
70
+ {selectable && (
71
+ <div className="flex flex-col gap-2 border-b px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
72
+ <p className="text-sm text-muted-foreground">
73
+ {selectedCount} {selectedCount === 1 ? "row" : "rows"} selected
74
+ </p>
75
+ <div className="flex flex-wrap gap-2">
76
+ <CollectionToolbarActions
77
+ actions={safeBulkActions}
78
+ selectedIds={selectedRowIds}
79
+ disabled={selectedCount === 0}
80
+ />
81
+ </div>
82
+ </div>
83
+ )}
84
+ <Table>
85
+ <TableHeader>
86
+ <TableRow>
87
+ {selectable && (
88
+ <TableHead className="w-10">
89
+ <input
90
+ type="checkbox"
91
+ aria-label="Select all rows"
92
+ checked={allSelected}
93
+ ref={(element) => {
94
+ if (element) element.indeterminate = someSelected;
95
+ }}
96
+ onChange={(event) => toggleAllRows(event.target.checked)}
97
+ className="h-4 w-4 rounded border"
98
+ />
99
+ </TableHead>
100
+ )}
101
+ {safeHeaders.map((header) => (
102
+ <SortableHeader key={header.attribute} {...header} />
103
+ ))}
104
+ {showActions && (
105
+ <TableHead className="w-10">
106
+ <span className="sr-only">Actions</span>
107
+ </TableHead>
108
+ )}
109
+ </TableRow>
110
+ </TableHeader>
111
+ <TableBody>
112
+ {safeRows.length === 0 ? (
113
+ <TableRow>
114
+ <TableCell colSpan={columnCount} className="h-32 text-center">
115
+ <div className="mx-auto flex max-w-sm flex-col items-center gap-1">
116
+ <p className="text-sm font-medium">{emptyState?.title ?? "No records found"}</p>
117
+ {emptyState?.description && (
118
+ <p className="text-sm text-muted-foreground">{emptyState.description}</p>
119
+ )}
120
+ </div>
121
+ </TableCell>
122
+ </TableRow>
123
+ ) : safeRows.map((row) => (
124
+ <TableRow
125
+ key={row.id}
126
+ className={cn(
127
+ row.showPath && "cursor-pointer",
128
+ row.rowOptions?.className || row.rowOptions?.class_name
129
+ )}
130
+ onClick={(e) => handleRowClick(e, row.showPath)}
131
+ >
132
+ {selectable && (
133
+ <TableCell>
134
+ <input
135
+ type="checkbox"
136
+ aria-label={`Select row ${row.id}`}
137
+ checked={selectedIds.has(String(row.id))}
138
+ onChange={(event) => toggleRow(String(row.id), event.target.checked)}
139
+ className="h-4 w-4 rounded border"
140
+ />
141
+ </TableCell>
142
+ )}
143
+ {(Array.isArray(row.cells) ? row.cells : []).map((cell) => {
144
+ const cellClassName = cell.cellOptions?.className || cell.cellOptions?.class_name;
145
+ return (
146
+ <TableCell key={cell.attribute} className={cellClassName}>
147
+ {cell.showPath ? (
148
+ <a
149
+ href={cell.showPath}
150
+ data-sg-visit
151
+ className="hover:underline"
152
+ >
153
+ <FieldRenderer mode="index" {...cell} />
154
+ </a>
155
+ ) : (
156
+ <FieldRenderer mode="index" {...cell} />
157
+ )}
158
+ </TableCell>
159
+ );
160
+ })}
161
+ {showActions && (
162
+ <TableCell>
163
+ <CollectionItemActions actions={row.collectionItemActions} />
164
+ </TableCell>
165
+ )}
166
+ </TableRow>
167
+ ))}
168
+ </TableBody>
169
+ </Table>
170
+ </div>
171
+ );
172
+ }
@@ -1,20 +1,28 @@
1
1
  import React, { useRef, useContext } from "react";
2
2
  import { NavigationContext } from "@thoughtbot/superglue";
3
3
 
4
- import { Input } from "terrazzo/ui";
4
+ import { Input } from "./ui";
5
5
  import { Search } from "lucide-react";
6
6
 
7
- export function SearchBar({ searchTerm, searchPath }) {
7
+ export function SearchBar({ searchTerm, searchPath, perPage }) {
8
8
  const { visit } = useContext(NavigationContext);
9
9
  const inputRef = useRef(null);
10
10
 
11
11
  const handleSubmit = (e) => {
12
12
  e.preventDefault();
13
13
  const search = inputRef.current?.value ?? "";
14
- const params = new URLSearchParams();
15
- params.set("search", search);
16
- const url = `${searchPath}?${params.toString()}`;
17
- visit(url, {});
14
+ const url = new URL(window.location.href);
15
+ url.pathname = searchPath;
16
+ url.searchParams.delete("_page");
17
+ if (perPage != null) url.searchParams.set("per_page", String(perPage));
18
+
19
+ if (search.trim()) {
20
+ url.searchParams.set("search", search);
21
+ } else {
22
+ url.searchParams.delete("search");
23
+ }
24
+
25
+ visit(url.pathname + url.search, {});
18
26
  };
19
27
 
20
28
  return (
@@ -1,18 +1,20 @@
1
1
  import React from "react";
2
2
  import { ChevronUp, ChevronDown, ChevronsUpDown } from "lucide-react";
3
3
 
4
- import { TableHead } from "terrazzo/ui";
4
+ import { TableHead } from "./ui";
5
+
6
+ export function SortableHeader({ label, sortable, sortUrl, sortDirection, headerOptions }) {
7
+ const headerClassName = headerOptions?.className || headerOptions?.class_name;
5
8
 
6
- export function SortableHeader({ label, sortable, sortUrl, sortDirection }) {
7
9
  if (!sortable) {
8
- return <TableHead>{label}</TableHead>;
10
+ return <TableHead className={headerClassName}>{label}</TableHead>;
9
11
  }
10
12
 
11
13
  return (
12
- <TableHead>
14
+ <TableHead className={headerClassName}>
13
15
  <a
14
16
  href={sortUrl}
15
- data-sg-remote
17
+ data-sg-visit
16
18
  className="inline-flex items-center gap-1 hover:text-foreground">
17
19
 
18
20
  {label}
@@ -11,7 +11,7 @@ import {
11
11
  SidebarMenu,
12
12
  SidebarMenuButton,
13
13
  SidebarMenuItem,
14
- } from "terrazzo/ui";
14
+ } from "./ui";
15
15
 
16
16
  export function AppSidebar({
17
17
  navigation,
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
- import { Separator } from "terrazzo/ui";
3
- import { SidebarTrigger } from "terrazzo/ui";
2
+ import { Separator } from "./ui";
3
+ import { SidebarTrigger } from "./ui";
4
4
 
5
5
  export function SiteHeader({ title, actions }) {
6
6
  return (
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+
3
+ import { Label, Input } from "../../components/ui";
4
+
5
+ export function FormField({ value, label, input, required }) {
6
+ return (
7
+ <div className="space-y-2">
8
+ {label && <Label htmlFor={input?.id}>{label}{required && <span> *</span>}</Label>}
9
+ {value?.filename && (
10
+ <p className="text-sm text-muted-foreground">Current: {value.filename}</p>
11
+ )}
12
+ <Input type="file" {...input} />
13
+ </div>
14
+ );
15
+ }
@@ -0,0 +1,5 @@
1
+ import React from "react";
2
+
3
+ export function IndexField({ value }) {
4
+ return <span className="text-sm">{value ?? "\u2014"}</span>;
5
+ }
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+
3
+ function formatBytes(bytes) {
4
+ if (bytes == null || bytes === 0) return "0 B";
5
+ const units = ["B", "KB", "MB", "GB"];
6
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
7
+ return `${(bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
8
+ }
9
+
10
+ export function ShowField({ value }) {
11
+ if (!value) return <span className="text-muted-foreground">No file</span>;
12
+ return <span>{value.filename} ({formatBytes(value.byteSize)})</span>;
13
+ }
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
 
3
- import { Label } from "terrazzo/ui";
3
+ import { Label } from "../../components/ui";
4
4
 
5
5
  export function FormField({ value, label, input, options, required }) {
6
6
  const resourceOptions = options?.resourceOptions ?? [];
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
 
3
- import { Label } from "terrazzo/ui";
3
+ import { Label } from "../../components/ui";
4
4
 
5
5
  export function FormField({ value, label, input, required }) {
6
6
  return (
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
 
3
- import { Badge } from "terrazzo/ui";
3
+ import { Badge } from "../../components/ui";
4
4
 
5
5
  export function IndexField({ value }) {
6
6
  if (value == null) return <span className="text-muted-foreground">-</span>;
@@ -1,9 +1,9 @@
1
1
  import React, { useState, useRef, useCallback } from "react";
2
2
  import { X, Check } from "lucide-react";
3
3
 
4
- import { Label } from "terrazzo/ui";
5
- import { Badge } from "terrazzo/ui";
6
- import { Popover, PopoverTrigger, PopoverContent } from "terrazzo/ui";
4
+ import { Label } from "../../components/ui";
5
+ import { Badge } from "../../components/ui";
6
+ import { Popover, PopoverTrigger, PopoverContent } from "../../components/ui";
7
7
  import { cn } from "terrazzo";
8
8
 
9
9
  export function FormField({ value, label, input, options, required }) {
@@ -1,8 +1,9 @@
1
1
  import React from "react";
2
2
 
3
- import { Badge } from "terrazzo/ui";
3
+ import { Badge } from "../../components/ui";
4
4
 
5
5
  export function IndexField({ value }) {
6
- const count = typeof value === "number" ? value : 0;
7
- return <Badge variant="secondary">{count}</Badge>;
6
+ const count = value?.count ?? 0;
7
+ const label = value?.label ?? "";
8
+ return <Badge variant="secondary">{count} {label}</Badge>;
8
9
  }
@@ -1,25 +1,30 @@
1
- import React, { useState } from "react";
1
+ import React from "react";
2
2
 
3
- import { ResourceTable } from "terrazzo/components";
4
- import { Badge } from "terrazzo/ui";
5
- import { Button } from "terrazzo/ui";
3
+ import { ResourceTable, HasManyPagination } from "../../components";
4
+ import { Badge } from "../../components/ui";
6
5
 
7
- export function ShowField({ value, hasManyRowExtras, options }) {
6
+ export function ShowField({ value, hasManyRowExtras, paginationPaths, options }) {
8
7
  if (!value) return <span className="text-muted-foreground">None</span>;
9
8
 
10
- const { rows, headers, total, initialLimit, items } = value;
11
- const [expanded, setExpanded] = useState(false);
9
+ const { rows, headers, items, total, currentPage, totalPages } = value;
10
+
11
+ const pagination = (
12
+ <HasManyPagination
13
+ currentPage={currentPage}
14
+ totalPages={totalPages}
15
+ total={total}
16
+ nextPagePath={paginationPaths?.nextPagePath}
17
+ prevPagePath={paginationPaths?.prevPagePath}
18
+ />
19
+ );
12
20
 
13
21
  // Table mode: collection_attributes specified
14
22
  if (headers && rows) {
15
- if (rows.length === 0) {
23
+ if (rows.length === 0 && total === 0) {
16
24
  return <span className="text-muted-foreground">None</span>;
17
25
  }
18
26
 
19
- const hasMore = initialLimit && initialLimit > 0 && total > initialLimit;
20
- const visibleRows = expanded || !hasMore ? rows : rows.slice(0, initialLimit);
21
-
22
- const enrichedRows = visibleRows.map((row) => {
27
+ const enrichedRows = rows.map((row) => {
23
28
  const extras = hasManyRowExtras?.[String(row.id)] || {};
24
29
  return {
25
30
  ...row,
@@ -31,31 +36,20 @@ export function ShowField({ value, hasManyRowExtras, options }) {
31
36
  return (
32
37
  <div>
33
38
  <ResourceTable headers={headers} rows={enrichedRows} showActions={options?.renderActions !== false} />
34
- {hasMore && (
35
- <Button
36
- variant="link"
37
- size="sm"
38
- className="mt-2 px-0"
39
- onClick={() => setExpanded(!expanded)}>
40
- {expanded ? "Show less" : `Show ${total - initialLimit} more`}
41
- </Button>
42
- )}
39
+ {pagination}
43
40
  </div>
44
41
  );
45
42
  }
46
43
 
47
44
  // Simple list mode (no collection_attributes)
48
- if (!items || items.length === 0) {
45
+ if ((!items || items.length === 0) && total === 0) {
49
46
  return <span className="text-muted-foreground">None</span>;
50
47
  }
51
48
 
52
- const hasMore = initialLimit && initialLimit > 0 && total > initialLimit;
53
- const visibleItems = expanded || !hasMore ? items : items.slice(0, initialLimit);
54
-
55
49
  return (
56
50
  <div>
57
51
  <div className="flex flex-wrap items-center gap-1.5">
58
- {visibleItems.map((item) => {
52
+ {(items || []).map((item) => {
59
53
  const showPath = hasManyRowExtras?.[String(item.id)]?.showPath;
60
54
  return showPath ? (
61
55
  <a key={item.id} href={showPath} data-sg-visit>
@@ -65,16 +59,8 @@ export function ShowField({ value, hasManyRowExtras, options }) {
65
59
  <Badge key={item.id} variant="secondary">{item.display}</Badge>
66
60
  );
67
61
  })}
68
- {hasMore && (
69
- <Button
70
- variant="link"
71
- size="sm"
72
- className="px-0"
73
- onClick={() => setExpanded(!expanded)}>
74
- {expanded ? "Show less" : `Show ${total - initialLimit} more`}
75
- </Button>
76
- )}
77
62
  </div>
63
+ {pagination}
78
64
  </div>
79
65
  );
80
66
  }
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
 
3
- import { Label } from "terrazzo/ui";
3
+ import { Label } from "../../components/ui";
4
4
 
5
5
  export function FormField({ value, label, input, options, required }) {
6
6
  const resourceOptions = options?.resourceOptions ?? [];
@@ -1,10 +1,10 @@
1
- import React, { useState, useRef, useEffect } from "react";
1
+ import React, { useState, useRef } from "react";
2
2
 
3
- import { Input } from "terrazzo/ui";
4
- import { Button } from "terrazzo/ui";
5
- import { Label } from "terrazzo/ui";
3
+ import { Input } from "../../components/ui";
4
+ import { Button } from "../../components/ui";
5
+ import { Label } from "../../components/ui";
6
6
 
7
- export function FormField({ value, label, input, options, required }) {
7
+ export function FormField({ value, label, input, required }) {
8
8
  const initialPairs = () => {
9
9
  if (value && typeof value === "object" && Object.keys(value).length > 0) {
10
10
  return Object.entries(value).map(([k, v], i) => ({ id: i, key: k, value: v ?? "" }));
@@ -1,7 +1,5 @@
1
1
  import React from "react";
2
2
 
3
- import { Badge } from "terrazzo/ui";
4
-
5
3
  export function IndexField({ value }) {
6
4
  if (value == null || value === "") {
7
5
  return <span className="text-muted-foreground">-</span>;
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
 
3
- import { Badge } from "terrazzo/ui";
3
+ import { Badge } from "../../components/ui";
4
4
 
5
5
  export function ShowField({ value }) {
6
6
  if (value == null || Object.keys(value).length === 0) {
@@ -5,5 +5,5 @@ export function IndexField({ value, options }) {
5
5
  const suffix = options?.suffix ?? "";
6
6
  const decimals = options?.decimals;
7
7
  const formatted = decimals != null && value != null ? Number(value).toFixed(decimals) : String(value ?? "");
8
- return <span className="text-sm tabular-nums">{prefix}{formatted}{suffix}</span>;
8
+ return <span className="text-sm tabular-nums">{prefix}{formatted}{suffix && ` ${suffix}`}</span>;
9
9
  }
@@ -5,5 +5,5 @@ export function ShowField({ value, options }) {
5
5
  const suffix = options?.suffix ?? "";
6
6
  const decimals = options?.decimals;
7
7
  const formatted = decimals != null && value != null ? Number(value).toFixed(decimals) : String(value ?? "");
8
- return <span className="tabular-nums">{prefix}{formatted}{suffix}</span>;
8
+ return <span className="tabular-nums">{prefix}{formatted}{suffix && ` ${suffix}`}</span>;
9
9
  }
@@ -1,6 +1,6 @@
1
1
  import React, { useState } from "react";
2
2
 
3
- import { Label } from "terrazzo/ui";
3
+ import { Label } from "../../components/ui";
4
4
 
5
5
  export function FormField({ value, label, input, options, required }) {
6
6
  const groupedOptions = options?.groupedOptions ?? {};
@@ -13,7 +13,8 @@ export function FormField({ value, label, input, options, required }) {
13
13
 
14
14
  const filteredItems = selectedType ? groupedOptions[selectedType] ?? [] : [];
15
15
 
16
- const { typeName: _typeName, ...idInputProps } = inputProps ?? {};
16
+ const idInputProps = { ...(inputProps ?? {}) };
17
+ delete idInputProps.typeName;
17
18
 
18
19
  return (
19
20
  <div className="space-y-2">
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
 
3
- import { Textarea } from "terrazzo/ui";
4
- import { Label } from "terrazzo/ui";
3
+ import { Textarea } from "../../components/ui";
4
+ import { Label } from "../../components/ui";
5
5
 
6
6
  export function FormField({ value, label, input, required }) {
7
7
  return (
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
 
3
- import { Label } from "terrazzo/ui";
3
+ import { Label } from "../../components/ui";
4
4
 
5
5
  export function FormField({ value, label, input, options, required }) {
6
6
  const selectableOptions = options?.selectableOptions ?? [];
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
 
3
- import { Badge } from "terrazzo/ui";
3
+ import { Badge } from "../../components/ui";
4
4
 
5
5
  export function IndexField({ value }) {
6
6
  if (value == null) return <span className="text-muted-foreground">-</span>;
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
 
3
- import { Input } from "terrazzo/ui";
4
- import { Label } from "terrazzo/ui";
3
+ import { Input } from "../../components/ui";
4
+ import { Label } from "../../components/ui";
5
5
 
6
6
  export function TextInputFormField({ type = "text", value, label, input, required, defaultValue, ...inputProps }) {
7
7
  const resolvedDefault = defaultValue !== undefined ? defaultValue : String(value ?? "");
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
 
3
- import { Textarea } from "terrazzo/ui";
4
- import { Label } from "terrazzo/ui";
3
+ import { Textarea } from "../../components/ui";
4
+ import { Label } from "../../components/ui";
5
5
 
6
6
  export function FormField({ value, label, input, required }) {
7
7
  return (
@@ -1,11 +1,13 @@
1
1
  import React from "react";
2
2
 
3
- import { ResourceTable } from "terrazzo/components";
3
+ import { ResourceTable } from "../components";
4
+
5
+ export function AdminCollection({ table = {}, emptyState, bulkActions = [] }) {
6
+ const { headers = [], rows = [] } = table || {};
4
7
 
5
- export function AdminCollection({ table }) {
6
8
  return (
7
9
  <div className="overflow-x-auto">
8
- <ResourceTable headers={table.headers} rows={table.rows} />
10
+ <ResourceTable headers={headers} rows={rows} emptyState={emptyState} bulkActions={bulkActions} />
9
11
  </div>
10
12
  );
11
13
  }