terrazzo 0.1.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 (161) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/Rakefile +11 -0
  4. data/app/controllers/terrazzo/application_controller.rb +208 -0
  5. data/app/views/terrazzo/application/edit.json.props +70 -0
  6. data/app/views/terrazzo/application/index.json.props +97 -0
  7. data/app/views/terrazzo/application/new.json.props +65 -0
  8. data/app/views/terrazzo/application/show.json.props +44 -0
  9. data/app/views/terrazzo/application/superglue.html.erb +5 -0
  10. data/config/locales/en.yml +28 -0
  11. data/lib/generators/terrazzo/dashboard/dashboard_generator.rb +118 -0
  12. data/lib/generators/terrazzo/dashboard/templates/controller.rb.erb +4 -0
  13. data/lib/generators/terrazzo/dashboard/templates/dashboard.rb.erb +36 -0
  14. data/lib/generators/terrazzo/field/field_generator.rb +42 -0
  15. data/lib/generators/terrazzo/field/templates/FormField.jsx.erb +19 -0
  16. data/lib/generators/terrazzo/field/templates/IndexField.jsx.erb +5 -0
  17. data/lib/generators/terrazzo/field/templates/ShowField.jsx.erb +5 -0
  18. data/lib/generators/terrazzo/field/templates/field.rb.erb +23 -0
  19. data/lib/generators/terrazzo/install/install_generator.rb +85 -0
  20. data/lib/generators/terrazzo/install/templates/application.js.erb +27 -0
  21. data/lib/generators/terrazzo/install/templates/application.json.props +27 -0
  22. data/lib/generators/terrazzo/install/templates/application.json.props.erb +17 -0
  23. data/lib/generators/terrazzo/install/templates/application_controller.rb.erb +24 -0
  24. data/lib/generators/terrazzo/install/templates/application_visit.js.erb +8 -0
  25. data/lib/generators/terrazzo/install/templates/flash_slice.js.erb +42 -0
  26. data/lib/generators/terrazzo/install/templates/page_to_page_mapping.js.erb +22 -0
  27. data/lib/generators/terrazzo/install/templates/store.js.erb +24 -0
  28. data/lib/generators/terrazzo/install/templates/superglue.html.erb.erb +20 -0
  29. data/lib/generators/terrazzo/routes/routes_generator.rb +38 -0
  30. data/lib/generators/terrazzo/views/edit_generator.rb +28 -0
  31. data/lib/generators/terrazzo/views/field_generator.rb +32 -0
  32. data/lib/generators/terrazzo/views/index_generator.rb +24 -0
  33. data/lib/generators/terrazzo/views/layout_generator.rb +26 -0
  34. data/lib/generators/terrazzo/views/navigation_generator.rb +24 -0
  35. data/lib/generators/terrazzo/views/new_generator.rb +28 -0
  36. data/lib/generators/terrazzo/views/show_generator.rb +24 -0
  37. data/lib/generators/terrazzo/views/templates/components/FlashMessages.jsx +26 -0
  38. data/lib/generators/terrazzo/views/templates/components/Layout.jsx +23 -0
  39. data/lib/generators/terrazzo/views/templates/components/Pagination.jsx +69 -0
  40. data/lib/generators/terrazzo/views/templates/components/SearchBar.jsx +35 -0
  41. data/lib/generators/terrazzo/views/templates/components/SortableHeader.jsx +29 -0
  42. data/lib/generators/terrazzo/views/templates/components/app-sidebar.jsx +62 -0
  43. data/lib/generators/terrazzo/views/templates/components/site-header.jsx +19 -0
  44. data/lib/generators/terrazzo/views/templates/components/ui/avatar.jsx +35 -0
  45. data/lib/generators/terrazzo/views/templates/components/ui/badge.jsx +34 -0
  46. data/lib/generators/terrazzo/views/templates/components/ui/button.jsx +47 -0
  47. data/lib/generators/terrazzo/views/templates/components/ui/card.jsx +50 -0
  48. data/lib/generators/terrazzo/views/templates/components/ui/dropdown-menu.jsx +155 -0
  49. data/lib/generators/terrazzo/views/templates/components/ui/field.jsx +28 -0
  50. data/lib/generators/terrazzo/views/templates/components/ui/index.js +106 -0
  51. data/lib/generators/terrazzo/views/templates/components/ui/input.jsx +19 -0
  52. data/lib/generators/terrazzo/views/templates/components/ui/label.jsx +16 -0
  53. data/lib/generators/terrazzo/views/templates/components/ui/pagination.jsx +85 -0
  54. data/lib/generators/terrazzo/views/templates/components/ui/popover.jsx +27 -0
  55. data/lib/generators/terrazzo/views/templates/components/ui/select.jsx +127 -0
  56. data/lib/generators/terrazzo/views/templates/components/ui/separator.jsx +23 -0
  57. data/lib/generators/terrazzo/views/templates/components/ui/sheet.jsx +109 -0
  58. data/lib/generators/terrazzo/views/templates/components/ui/sidebar.jsx +629 -0
  59. data/lib/generators/terrazzo/views/templates/components/ui/skeleton.jsx +10 -0
  60. data/lib/generators/terrazzo/views/templates/components/ui/table.jsx +83 -0
  61. data/lib/generators/terrazzo/views/templates/components/ui/textarea.jsx +18 -0
  62. data/lib/generators/terrazzo/views/templates/components/ui/tooltip.jsx +24 -0
  63. data/lib/generators/terrazzo/views/templates/fields/FieldRenderer.jsx +103 -0
  64. data/lib/generators/terrazzo/views/templates/fields/belongs_to/FormField.jsx +29 -0
  65. data/lib/generators/terrazzo/views/templates/fields/belongs_to/IndexField.jsx +7 -0
  66. data/lib/generators/terrazzo/views/templates/fields/belongs_to/ShowField.jsx +7 -0
  67. data/lib/generators/terrazzo/views/templates/fields/boolean/FormField.jsx +21 -0
  68. data/lib/generators/terrazzo/views/templates/fields/boolean/IndexField.jsx +12 -0
  69. data/lib/generators/terrazzo/views/templates/fields/boolean/ShowField.jsx +6 -0
  70. data/lib/generators/terrazzo/views/templates/fields/date/FormField.jsx +8 -0
  71. data/lib/generators/terrazzo/views/templates/fields/date/IndexField.jsx +7 -0
  72. data/lib/generators/terrazzo/views/templates/fields/date/ShowField.jsx +7 -0
  73. data/lib/generators/terrazzo/views/templates/fields/date_time/FormField.jsx +8 -0
  74. data/lib/generators/terrazzo/views/templates/fields/date_time/IndexField.jsx +7 -0
  75. data/lib/generators/terrazzo/views/templates/fields/date_time/ShowField.jsx +7 -0
  76. data/lib/generators/terrazzo/views/templates/fields/email/FormField.jsx +7 -0
  77. data/lib/generators/terrazzo/views/templates/fields/email/IndexField.jsx +10 -0
  78. data/lib/generators/terrazzo/views/templates/fields/email/ShowField.jsx +10 -0
  79. data/lib/generators/terrazzo/views/templates/fields/has_many/FormField.jsx +151 -0
  80. data/lib/generators/terrazzo/views/templates/fields/has_many/IndexField.jsx +8 -0
  81. data/lib/generators/terrazzo/views/templates/fields/has_many/ShowField.jsx +72 -0
  82. data/lib/generators/terrazzo/views/templates/fields/has_one/FormField.jsx +28 -0
  83. data/lib/generators/terrazzo/views/templates/fields/has_one/IndexField.jsx +7 -0
  84. data/lib/generators/terrazzo/views/templates/fields/has_one/ShowField.jsx +7 -0
  85. data/lib/generators/terrazzo/views/templates/fields/hstore/FormField.jsx +120 -0
  86. data/lib/generators/terrazzo/views/templates/fields/hstore/IndexField.jsx +15 -0
  87. data/lib/generators/terrazzo/views/templates/fields/hstore/ShowField.jsx +24 -0
  88. data/lib/generators/terrazzo/views/templates/fields/index.js +81 -0
  89. data/lib/generators/terrazzo/views/templates/fields/number/FormField.jsx +9 -0
  90. data/lib/generators/terrazzo/views/templates/fields/number/IndexField.jsx +9 -0
  91. data/lib/generators/terrazzo/views/templates/fields/number/ShowField.jsx +9 -0
  92. data/lib/generators/terrazzo/views/templates/fields/password/FormField.jsx +7 -0
  93. data/lib/generators/terrazzo/views/templates/fields/password/IndexField.jsx +6 -0
  94. data/lib/generators/terrazzo/views/templates/fields/password/ShowField.jsx +6 -0
  95. data/lib/generators/terrazzo/views/templates/fields/polymorphic/FormField.jsx +58 -0
  96. data/lib/generators/terrazzo/views/templates/fields/polymorphic/IndexField.jsx +7 -0
  97. data/lib/generators/terrazzo/views/templates/fields/polymorphic/ShowField.jsx +7 -0
  98. data/lib/generators/terrazzo/views/templates/fields/rich_text/FormField.jsx +21 -0
  99. data/lib/generators/terrazzo/views/templates/fields/rich_text/IndexField.jsx +8 -0
  100. data/lib/generators/terrazzo/views/templates/fields/rich_text/ShowField.jsx +6 -0
  101. data/lib/generators/terrazzo/views/templates/fields/select/FormField.jsx +29 -0
  102. data/lib/generators/terrazzo/views/templates/fields/select/IndexField.jsx +8 -0
  103. data/lib/generators/terrazzo/views/templates/fields/select/ShowField.jsx +5 -0
  104. data/lib/generators/terrazzo/views/templates/fields/shared/TextInputFormField.jsx +24 -0
  105. data/lib/generators/terrazzo/views/templates/fields/string/FormField.jsx +7 -0
  106. data/lib/generators/terrazzo/views/templates/fields/string/IndexField.jsx +5 -0
  107. data/lib/generators/terrazzo/views/templates/fields/string/ShowField.jsx +5 -0
  108. data/lib/generators/terrazzo/views/templates/fields/text/FormField.jsx +20 -0
  109. data/lib/generators/terrazzo/views/templates/fields/text/IndexField.jsx +5 -0
  110. data/lib/generators/terrazzo/views/templates/fields/text/ShowField.jsx +5 -0
  111. data/lib/generators/terrazzo/views/templates/fields/time/FormField.jsx +8 -0
  112. data/lib/generators/terrazzo/views/templates/fields/time/IndexField.jsx +7 -0
  113. data/lib/generators/terrazzo/views/templates/fields/time/ShowField.jsx +7 -0
  114. data/lib/generators/terrazzo/views/templates/fields/url/FormField.jsx +7 -0
  115. data/lib/generators/terrazzo/views/templates/fields/url/IndexField.jsx +10 -0
  116. data/lib/generators/terrazzo/views/templates/fields/url/ShowField.jsx +10 -0
  117. data/lib/generators/terrazzo/views/templates/pages/_form.jsx +76 -0
  118. data/lib/generators/terrazzo/views/templates/pages/edit.jsx +44 -0
  119. data/lib/generators/terrazzo/views/templates/pages/index.jsx +106 -0
  120. data/lib/generators/terrazzo/views/templates/pages/new.jsx +36 -0
  121. data/lib/generators/terrazzo/views/templates/pages/show.jsx +82 -0
  122. data/lib/generators/terrazzo/views/views_generator.rb +52 -0
  123. data/lib/terrazzo/base_dashboard.rb +88 -0
  124. data/lib/terrazzo/engine.rb +21 -0
  125. data/lib/terrazzo/field/associative.rb +56 -0
  126. data/lib/terrazzo/field/base.rb +114 -0
  127. data/lib/terrazzo/field/belongs_to.rb +53 -0
  128. data/lib/terrazzo/field/boolean.rb +9 -0
  129. data/lib/terrazzo/field/date.rb +10 -0
  130. data/lib/terrazzo/field/date_time.rb +10 -0
  131. data/lib/terrazzo/field/deferred.rb +50 -0
  132. data/lib/terrazzo/field/email.rb +15 -0
  133. data/lib/terrazzo/field/has_many.rb +98 -0
  134. data/lib/terrazzo/field/has_one.rb +33 -0
  135. data/lib/terrazzo/field/hstore.rb +37 -0
  136. data/lib/terrazzo/field/money.rb +33 -0
  137. data/lib/terrazzo/field/number.rb +17 -0
  138. data/lib/terrazzo/field/password.rb +16 -0
  139. data/lib/terrazzo/field/polymorphic.rb +36 -0
  140. data/lib/terrazzo/field/rich_text.rb +30 -0
  141. data/lib/terrazzo/field/select.rb +33 -0
  142. data/lib/terrazzo/field/string.rb +27 -0
  143. data/lib/terrazzo/field/text.rb +27 -0
  144. data/lib/terrazzo/field/time.rb +10 -0
  145. data/lib/terrazzo/field/url.rb +9 -0
  146. data/lib/terrazzo/filter.rb +26 -0
  147. data/lib/terrazzo/generator_helpers.rb +36 -0
  148. data/lib/terrazzo/namespace/resource.rb +39 -0
  149. data/lib/terrazzo/namespace.rb +34 -0
  150. data/lib/terrazzo/not_authorized_error.rb +4 -0
  151. data/lib/terrazzo/order.rb +71 -0
  152. data/lib/terrazzo/page/base.rb +12 -0
  153. data/lib/terrazzo/page/collection.rb +28 -0
  154. data/lib/terrazzo/page/form.rb +43 -0
  155. data/lib/terrazzo/page/show.rb +46 -0
  156. data/lib/terrazzo/resource_resolver.rb +40 -0
  157. data/lib/terrazzo/search.rb +56 -0
  158. data/lib/terrazzo/version.rb +3 -0
  159. data/lib/terrazzo.rb +47 -0
  160. data/terrazzo.gemspec +32 -0
  161. metadata +297 -0
@@ -0,0 +1,83 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "terrazzo"
4
+
5
+ const Table = React.forwardRef(({ className, ...props }, ref) => (
6
+ <div className="relative w-full overflow-auto">
7
+ <table
8
+ ref={ref}
9
+ className={cn("w-full caption-bottom text-sm", className)}
10
+ {...props} />
11
+ </div>
12
+ ))
13
+ Table.displayName = "Table"
14
+
15
+ const TableHeader = React.forwardRef(({ className, ...props }, ref) => (
16
+ <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
17
+ ))
18
+ TableHeader.displayName = "TableHeader"
19
+
20
+ const TableBody = React.forwardRef(({ className, ...props }, ref) => (
21
+ <tbody
22
+ ref={ref}
23
+ className={cn("[&_tr:last-child]:border-0", className)}
24
+ {...props} />
25
+ ))
26
+ TableBody.displayName = "TableBody"
27
+
28
+ const TableFooter = React.forwardRef(({ className, ...props }, ref) => (
29
+ <tfoot
30
+ ref={ref}
31
+ className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
32
+ {...props} />
33
+ ))
34
+ TableFooter.displayName = "TableFooter"
35
+
36
+ const TableRow = React.forwardRef(({ className, ...props }, ref) => (
37
+ <tr
38
+ ref={ref}
39
+ className={cn(
40
+ "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
41
+ className
42
+ )}
43
+ {...props} />
44
+ ))
45
+ TableRow.displayName = "TableRow"
46
+
47
+ const TableHead = React.forwardRef(({ className, ...props }, ref) => (
48
+ <th
49
+ ref={ref}
50
+ className={cn(
51
+ "h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
52
+ className
53
+ )}
54
+ {...props} />
55
+ ))
56
+ TableHead.displayName = "TableHead"
57
+
58
+ const TableCell = React.forwardRef(({ className, ...props }, ref) => (
59
+ <td
60
+ ref={ref}
61
+ className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
62
+ {...props} />
63
+ ))
64
+ TableCell.displayName = "TableCell"
65
+
66
+ const TableCaption = React.forwardRef(({ className, ...props }, ref) => (
67
+ <caption
68
+ ref={ref}
69
+ className={cn("mt-4 text-sm text-muted-foreground", className)}
70
+ {...props} />
71
+ ))
72
+ TableCaption.displayName = "TableCaption"
73
+
74
+ export {
75
+ Table,
76
+ TableHeader,
77
+ TableBody,
78
+ TableFooter,
79
+ TableHead,
80
+ TableRow,
81
+ TableCell,
82
+ TableCaption,
83
+ }
@@ -0,0 +1,18 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "terrazzo"
4
+
5
+ const Textarea = React.forwardRef(({ className, ...props }, ref) => {
6
+ return (
7
+ <textarea
8
+ className={cn(
9
+ "flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
10
+ className
11
+ )}
12
+ ref={ref}
13
+ {...props} />
14
+ );
15
+ })
16
+ Textarea.displayName = "Textarea"
17
+
18
+ export { Textarea }
@@ -0,0 +1,24 @@
1
+ import * as React from "react"
2
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip"
3
+
4
+ import { cn } from "terrazzo"
5
+
6
+ const TooltipProvider = TooltipPrimitive.Provider
7
+
8
+ const Tooltip = TooltipPrimitive.Root
9
+
10
+ const TooltipTrigger = TooltipPrimitive.Trigger
11
+
12
+ const TooltipContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (
13
+ <TooltipPrimitive.Content
14
+ ref={ref}
15
+ sideOffset={sideOffset}
16
+ className={cn(
17
+ "z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
18
+ className
19
+ )}
20
+ {...props} />
21
+ ))
22
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName
23
+
24
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
@@ -0,0 +1,103 @@
1
+ import React from "react";
2
+
3
+ import { IndexField as StringIndex } from "./string/IndexField";
4
+ import { ShowField as StringShow } from "./string/ShowField";
5
+ import { FormField as StringForm } from "./string/FormField";
6
+
7
+ import { IndexField as TextIndex } from "./text/IndexField";
8
+ import { ShowField as TextShow } from "./text/ShowField";
9
+ import { FormField as TextForm } from "./text/FormField";
10
+
11
+ import { IndexField as NumberIndex } from "./number/IndexField";
12
+ import { ShowField as NumberShow } from "./number/ShowField";
13
+ import { FormField as NumberForm } from "./number/FormField";
14
+
15
+ import { IndexField as BooleanIndex } from "./boolean/IndexField";
16
+ import { ShowField as BooleanShow } from "./boolean/ShowField";
17
+ import { FormField as BooleanForm } from "./boolean/FormField";
18
+
19
+ import { IndexField as DateIndex } from "./date/IndexField";
20
+ import { ShowField as DateShow } from "./date/ShowField";
21
+ import { FormField as DateForm } from "./date/FormField";
22
+
23
+ import { IndexField as DateTimeIndex } from "./date_time/IndexField";
24
+ import { ShowField as DateTimeShow } from "./date_time/ShowField";
25
+ import { FormField as DateTimeForm } from "./date_time/FormField";
26
+
27
+ import { IndexField as TimeIndex } from "./time/IndexField";
28
+ import { ShowField as TimeShow } from "./time/ShowField";
29
+ import { FormField as TimeForm } from "./time/FormField";
30
+
31
+ import { IndexField as EmailIndex } from "./email/IndexField";
32
+ import { ShowField as EmailShow } from "./email/ShowField";
33
+ import { FormField as EmailForm } from "./email/FormField";
34
+
35
+ import { IndexField as UrlIndex } from "./url/IndexField";
36
+ import { ShowField as UrlShow } from "./url/ShowField";
37
+ import { FormField as UrlForm } from "./url/FormField";
38
+
39
+ import { IndexField as PasswordIndex } from "./password/IndexField";
40
+ import { ShowField as PasswordShow } from "./password/ShowField";
41
+ import { FormField as PasswordForm } from "./password/FormField";
42
+
43
+ import { IndexField as SelectIndex } from "./select/IndexField";
44
+ import { ShowField as SelectShow } from "./select/ShowField";
45
+ import { FormField as SelectForm } from "./select/FormField";
46
+
47
+ import { IndexField as HstoreIndex } from "./hstore/IndexField";
48
+ import { ShowField as HstoreShow } from "./hstore/ShowField";
49
+ import { FormField as HstoreForm } from "./hstore/FormField";
50
+
51
+ import { IndexField as RichTextIndex } from "./rich_text/IndexField";
52
+ import { ShowField as RichTextShow } from "./rich_text/ShowField";
53
+ import { FormField as RichTextForm } from "./rich_text/FormField";
54
+
55
+ import { IndexField as BelongsToIndex } from "./belongs_to/IndexField";
56
+ import { ShowField as BelongsToShow } from "./belongs_to/ShowField";
57
+ import { FormField as BelongsToForm } from "./belongs_to/FormField";
58
+
59
+ import { IndexField as HasManyIndex } from "./has_many/IndexField";
60
+ import { ShowField as HasManyShow } from "./has_many/ShowField";
61
+ import { FormField as HasManyForm } from "./has_many/FormField";
62
+
63
+ import { IndexField as HasOneIndex } from "./has_one/IndexField";
64
+ import { ShowField as HasOneShow } from "./has_one/ShowField";
65
+ import { FormField as HasOneForm } from "./has_one/FormField";
66
+
67
+ import { IndexField as PolymorphicIndex } from "./polymorphic/IndexField";
68
+ import { ShowField as PolymorphicShow } from "./polymorphic/ShowField";
69
+ import { FormField as PolymorphicForm } from "./polymorphic/FormField";
70
+
71
+ const fieldMap = {
72
+ string: { index: StringIndex, show: StringShow, form: StringForm },
73
+ text: { index: TextIndex, show: TextShow, form: TextForm },
74
+ number: { index: NumberIndex, show: NumberShow, form: NumberForm },
75
+ boolean: { index: BooleanIndex, show: BooleanShow, form: BooleanForm },
76
+ date: { index: DateIndex, show: DateShow, form: DateForm },
77
+ date_time: { index: DateTimeIndex, show: DateTimeShow, form: DateTimeForm },
78
+ time: { index: TimeIndex, show: TimeShow, form: TimeForm },
79
+ email: { index: EmailIndex, show: EmailShow, form: EmailForm },
80
+ url: { index: UrlIndex, show: UrlShow, form: UrlForm },
81
+ password: { index: PasswordIndex, show: PasswordShow, form: PasswordForm },
82
+ select: { index: SelectIndex, show: SelectShow, form: SelectForm },
83
+ hstore: { index: HstoreIndex, show: HstoreShow, form: HstoreForm },
84
+ rich_text: { index: RichTextIndex, show: RichTextShow, form: RichTextForm },
85
+ belongs_to: { index: BelongsToIndex, show: BelongsToShow, form: BelongsToForm },
86
+ has_many: { index: HasManyIndex, show: HasManyShow, form: HasManyForm },
87
+ has_one: { index: HasOneIndex, show: HasOneShow, form: HasOneForm },
88
+ polymorphic: { index: PolymorphicIndex, show: PolymorphicShow, form: PolymorphicForm }
89
+ };
90
+
91
+ // Allow consumers to register custom field components
92
+ export function registerFieldType(
93
+ fieldType,
94
+ components)
95
+ {
96
+ fieldMap[fieldType] = { ...fieldMap[fieldType], ...components };
97
+ }
98
+
99
+ export function FieldRenderer({ mode, fieldType, ...rest }) {
100
+ const Component = fieldMap[fieldType]?.[mode];
101
+ if (!Component) return <span>{String(rest.value ?? "")}</span>;
102
+ return <Component {...rest} />;
103
+ }
@@ -0,0 +1,29 @@
1
+ import React from "react";
2
+
3
+ import { Label } from "../../components/ui/label";
4
+
5
+ export function FormField({ value, label, input, options, required }) {
6
+ const resourceOptions = options?.resourceOptions ?? [];
7
+
8
+ return (
9
+ <div className="space-y-2">
10
+ {label &&
11
+ <Label htmlFor={input?.id}>
12
+ {label}{required && <span className="text-destructive"> *</span>}
13
+ </Label>
14
+ }
15
+ <select
16
+ defaultValue={value != null ? String(value) : ""}
17
+ className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
18
+ {...input}>
19
+
20
+ <option value="">Select...</option>
21
+ {resourceOptions.map(([display, id]) =>
22
+ <option key={id} value={id}>
23
+ {display}
24
+ </option>
25
+ )}
26
+ </select>
27
+ </div>);
28
+
29
+ }
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+
3
+ export function IndexField({ value }) {
4
+ const assoc = value;
5
+ if (!assoc) return <span className="text-muted-foreground">-</span>;
6
+ return <span className="text-sm">{assoc.display}</span>;
7
+ }
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+
3
+ export function ShowField({ value }) {
4
+ const assoc = value;
5
+ if (!assoc) return <span className="text-muted-foreground">-</span>;
6
+ return <span>{assoc.display}</span>;
7
+ }
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+
3
+ import { Label } from "../../components/ui/label";
4
+
5
+ export function FormField({ value, label, input, required }) {
6
+ return (
7
+ <div className="flex items-center space-x-2">
8
+ <input
9
+ type="checkbox"
10
+ defaultChecked={!!value}
11
+ className="h-4 w-4 rounded border-gray-300"
12
+ {...input} />
13
+
14
+ {label &&
15
+ <Label htmlFor={input?.id}>
16
+ {label}{required && <span className="text-destructive"> *</span>}
17
+ </Label>
18
+ }
19
+ </div>);
20
+
21
+ }
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+
3
+ import { Badge } from "../../components/ui/badge";
4
+
5
+ export function IndexField({ value }) {
6
+ if (value == null) return <span className="text-muted-foreground">-</span>;
7
+ return (
8
+ <Badge variant={value ? "default" : "secondary"}>
9
+ {value ? "Yes" : "No"}
10
+ </Badge>);
11
+
12
+ }
@@ -0,0 +1,6 @@
1
+ import React from "react";
2
+
3
+ export function ShowField({ value }) {
4
+ if (value == null) return <span className="text-muted-foreground">-</span>;
5
+ return <span>{value ? "Yes" : "No"}</span>;
6
+ }
@@ -0,0 +1,8 @@
1
+ import React from "react";
2
+
3
+ import { TextInputFormField } from "../shared/TextInputFormField";
4
+
5
+ export function FormField({ value, ...props }) {
6
+ const defaultValue = value ? String(value).split("T")[0] : "";
7
+ return <TextInputFormField type="date" defaultValue={defaultValue} {...props} />;
8
+ }
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+
3
+ import { formatDate } from "terrazzo";
4
+
5
+ export function IndexField({ value }) {
6
+ return <span className="text-sm">{formatDate(value)}</span>;
7
+ }
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+
3
+ import { formatDate } from "terrazzo";
4
+
5
+ export function ShowField({ value }) {
6
+ return <span>{formatDate(value)}</span>;
7
+ }
@@ -0,0 +1,8 @@
1
+ import React from "react";
2
+
3
+ import { TextInputFormField } from "../shared/TextInputFormField";
4
+
5
+ export function FormField({ value, ...props }) {
6
+ const defaultValue = value ? String(value).slice(0, 16) : "";
7
+ return <TextInputFormField type="datetime-local" defaultValue={defaultValue} {...props} />;
8
+ }
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+
3
+ import { formatDateTime } from "terrazzo";
4
+
5
+ export function IndexField({ value }) {
6
+ return <span className="text-sm">{formatDateTime(value)}</span>;
7
+ }
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+
3
+ import { formatDateTime } from "terrazzo";
4
+
5
+ export function ShowField({ value }) {
6
+ return <span>{formatDateTime(value)}</span>;
7
+ }
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+
3
+ import { TextInputFormField } from "../shared/TextInputFormField";
4
+
5
+ export function FormField(props) {
6
+ return <TextInputFormField type="email" {...props} />;
7
+ }
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+
3
+ export function IndexField({ value }) {
4
+ if (!value) return <span className="text-muted-foreground">-</span>;
5
+ return (
6
+ <a href={`mailto:${value}`} className="text-sm text-primary hover:underline">
7
+ {String(value)}
8
+ </a>);
9
+
10
+ }
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+
3
+ export function ShowField({ value }) {
4
+ if (!value) return <span className="text-muted-foreground">-</span>;
5
+ return (
6
+ <a href={`mailto:${value}`} className="text-primary hover:underline">
7
+ {String(value)}
8
+ </a>);
9
+
10
+ }
@@ -0,0 +1,151 @@
1
+ import React, { useState, useRef, useCallback } from "react";
2
+ import { X, Check } from "lucide-react";
3
+
4
+ import { Label } from "../../components/ui/label";
5
+ import { Badge } from "../../components/ui/badge";
6
+ import { Popover, PopoverTrigger, PopoverContent } from "../../components/ui/popover";
7
+ import { cn } from "terrazzo";
8
+
9
+ export function FormField({ value, label, input, options, required }) {
10
+ const resourceOptions = options?.resourceOptions ?? [];
11
+ const initialIds = value ?? [];
12
+
13
+ const [selectedIds, setSelectedIds] = useState(() => new Set(initialIds));
14
+ const [search, setSearch] = useState("");
15
+ const [open, setOpen] = useState(false);
16
+ const inputRef = useRef(null);
17
+
18
+ const inputProps = input;
19
+ const inputName = inputProps?.name;
20
+ const inputId = inputProps?.id;
21
+
22
+ const toggleOption = useCallback((id) => {
23
+ setSelectedIds((prev) => {
24
+ const next = new Set(prev);
25
+ if (next.has(id)) {
26
+ next.delete(id);
27
+ } else {
28
+ next.add(id);
29
+ }
30
+ return next;
31
+ });
32
+ setSearch("");
33
+ }, []);
34
+
35
+ const removeOption = useCallback((id, e) => {
36
+ e.preventDefault();
37
+ e.stopPropagation();
38
+ setSelectedIds((prev) => {
39
+ const next = new Set(prev);
40
+ next.delete(id);
41
+ return next;
42
+ });
43
+ }, []);
44
+
45
+ const selectedOptions = resourceOptions.filter(([, id]) => selectedIds.has(id));
46
+ const filteredOptions = resourceOptions.filter(([display]) =>
47
+ display.toLowerCase().includes(search.toLowerCase())
48
+ );
49
+
50
+ return (
51
+ <div className="space-y-2">
52
+ {label &&
53
+ <Label htmlFor={inputId}>
54
+ {label}{required && <span className="text-destructive"> *</span>}
55
+ </Label>
56
+ }
57
+
58
+ {/* Hidden inputs for form submission */}
59
+ {inputName && <input type="hidden" name={inputName} value="" />}
60
+ {selectedIds.size > 0 && inputName &&
61
+ Array.from(selectedIds).map((id) =>
62
+ <input key={id} type="hidden" name={inputName} value={id} />
63
+ )
64
+ }
65
+
66
+ <Popover open={open} onOpenChange={setOpen}>
67
+ <PopoverTrigger asChild>
68
+ <button
69
+ type="button"
70
+ role="combobox"
71
+ aria-expanded={open}
72
+ id={inputId}
73
+ className={cn(
74
+ "flex min-h-10 w-full flex-wrap items-center gap-1 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background",
75
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
76
+ "hover:bg-accent/50 cursor-pointer"
77
+ )}>
78
+
79
+ {selectedOptions.length > 0 ?
80
+ selectedOptions.map(([display, id]) =>
81
+ <Badge key={id} variant="secondary" className="gap-1">
82
+ {display}
83
+ <span
84
+ role="button"
85
+ tabIndex={0}
86
+ className="ml-0.5 rounded-full outline-none hover:bg-muted-foreground/20 focus:bg-muted-foreground/20"
87
+ onPointerDown={(e) => removeOption(id, e)}
88
+ onKeyDown={(e) => {
89
+ if (e.key === "Enter" || e.key === " ") {
90
+ e.preventDefault();
91
+ removeOption(id, e);
92
+ }
93
+ }}>
94
+
95
+ <X className="h-3 w-3" />
96
+ </span>
97
+ </Badge>
98
+ ) :
99
+
100
+ <span className="text-muted-foreground">Select {label?.toLowerCase() ?? "items"}...</span>
101
+ }
102
+ </button>
103
+ </PopoverTrigger>
104
+ <PopoverContent
105
+ className="p-0"
106
+ onOpenAutoFocus={(e) => {
107
+ e.preventDefault();
108
+ inputRef.current?.focus();
109
+ }}>
110
+
111
+ <div className="p-2 border-b">
112
+ <input
113
+ ref={inputRef}
114
+ type="text"
115
+ className="flex h-8 w-full rounded-md bg-transparent px-2 py-1 text-sm outline-none placeholder:text-muted-foreground"
116
+ placeholder={`Search ${label?.toLowerCase() ?? "items"}...`}
117
+ value={search}
118
+ onChange={(e) => setSearch(e.target.value)} />
119
+
120
+ </div>
121
+ <div className="max-h-60 overflow-y-auto p-1">
122
+ {filteredOptions.length === 0 ?
123
+ <div className="px-2 py-4 text-center text-sm text-muted-foreground">
124
+ No results found.
125
+ </div> :
126
+
127
+ filteredOptions.map(([display, id]) => {
128
+ const isSelected = selectedIds.has(id);
129
+ return (
130
+ <button
131
+ key={id}
132
+ type="button"
133
+ className={cn(
134
+ "flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm cursor-pointer",
135
+ "outline-none hover:bg-accent hover:text-accent-foreground",
136
+ "focus:bg-accent focus:text-accent-foreground"
137
+ )}
138
+ onClick={() => toggleOption(id)}>
139
+
140
+ <Check className={cn("h-4 w-4 shrink-0", isSelected ? "opacity-100" : "opacity-0")} />
141
+ {display}
142
+ </button>);
143
+
144
+ })
145
+ }
146
+ </div>
147
+ </PopoverContent>
148
+ </Popover>
149
+ </div>);
150
+
151
+ }
@@ -0,0 +1,8 @@
1
+ import React from "react";
2
+
3
+ import { Badge } from "../../components/ui/badge";
4
+
5
+ export function IndexField({ value }) {
6
+ const count = typeof value === "number" ? value : 0;
7
+ return <Badge variant="secondary">{count}</Badge>;
8
+ }
@@ -0,0 +1,72 @@
1
+ import React from "react";
2
+
3
+ import {
4
+ Table,
5
+ TableHeader,
6
+ TableBody,
7
+ TableRow,
8
+ TableHead,
9
+ TableCell,
10
+ } from "../../components/ui/table";
11
+ import { FieldRenderer } from "../FieldRenderer";
12
+
13
+ export function ShowField({ value }) {
14
+ if (!value) return <span className="text-muted-foreground">None</span>;
15
+
16
+ const { items, headers, total, hasMore } = value;
17
+
18
+ if (!items || items.length === 0) {
19
+ return <span className="text-muted-foreground">None</span>;
20
+ }
21
+
22
+ // Table mode: collection_attributes specified
23
+ if (headers) {
24
+ return (
25
+ <div>
26
+ <div className="rounded-md border">
27
+ <Table>
28
+ <TableHeader>
29
+ <TableRow>
30
+ {headers.map((header) =>
31
+ <TableHead key={header.attribute}>{header.label}</TableHead>
32
+ )}
33
+ </TableRow>
34
+ </TableHeader>
35
+ <TableBody>
36
+ {items.map((item) =>
37
+ <TableRow key={item.id}>
38
+ {item.columns.map((col) =>
39
+ <TableCell key={col.attribute}>
40
+ <FieldRenderer mode="index" {...col} />
41
+ </TableCell>
42
+ )}
43
+ </TableRow>
44
+ )}
45
+ </TableBody>
46
+ </Table>
47
+ </div>
48
+ {hasMore && (
49
+ <p className="text-sm text-muted-foreground mt-2">
50
+ Showing {items.length} of {total}
51
+ </p>
52
+ )}
53
+ </div>
54
+ );
55
+ }
56
+
57
+ // Simple list mode
58
+ return (
59
+ <div>
60
+ <ul className="list-disc pl-5 space-y-1">
61
+ {items.map((item) =>
62
+ <li key={item.id}>{item.display}</li>
63
+ )}
64
+ </ul>
65
+ {hasMore && (
66
+ <p className="text-sm text-muted-foreground mt-2">
67
+ Showing {items.length} of {total}
68
+ </p>
69
+ )}
70
+ </div>
71
+ );
72
+ }
@@ -0,0 +1,28 @@
1
+ import React from "react";
2
+
3
+ import { Label } from "../../components/ui/label";
4
+
5
+ export function FormField({ value, label, input, options, required }) {
6
+ const resourceOptions = options?.resourceOptions ?? [];
7
+
8
+ return (
9
+ <div className="space-y-2">
10
+ {label &&
11
+ <Label htmlFor={input?.id}>
12
+ {label}{required && <span className="text-destructive"> *</span>}
13
+ </Label>
14
+ }
15
+ <select
16
+ defaultValue={value != null ? String(value) : ""}
17
+ className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
18
+ {...input}>
19
+
20
+ <option value="">Select...</option>
21
+ {resourceOptions.map(([display, id]) =>
22
+ <option key={id} value={id}>
23
+ {display}
24
+ </option>
25
+ )}
26
+ </select>
27
+ </div>);
28
+ }