plutonium 0.33.1 → 0.34.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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/# Plutonium: The pre-alpha demo.md +4 -2
  3. data/.claude/skills/assets/SKILL.md +416 -0
  4. data/.claude/skills/connect-resource/SKILL.md +112 -0
  5. data/.claude/skills/controller/SKILL.md +302 -0
  6. data/.claude/skills/create-resource/SKILL.md +240 -0
  7. data/.claude/skills/definition/SKILL.md +218 -0
  8. data/.claude/skills/definition-actions/SKILL.md +386 -0
  9. data/.claude/skills/definition-fields/SKILL.md +474 -0
  10. data/.claude/skills/definition-query/SKILL.md +334 -0
  11. data/.claude/skills/forms/SKILL.md +439 -0
  12. data/.claude/skills/installation/SKILL.md +300 -0
  13. data/.claude/skills/interaction/SKILL.md +382 -0
  14. data/.claude/skills/model/SKILL.md +267 -0
  15. data/.claude/skills/model-features/SKILL.md +286 -0
  16. data/.claude/skills/nested-resources/SKILL.md +274 -0
  17. data/.claude/skills/package/SKILL.md +191 -0
  18. data/.claude/skills/policy/SKILL.md +352 -0
  19. data/.claude/skills/portal/SKILL.md +400 -0
  20. data/.claude/skills/resource/SKILL.md +281 -0
  21. data/.claude/skills/rodauth/SKILL.md +452 -0
  22. data/.claude/skills/views/SKILL.md +563 -0
  23. data/Appraisals +46 -4
  24. data/CHANGELOG.md +32 -1
  25. data/app/assets/plutonium.css +2 -2
  26. data/config/brakeman.ignore +239 -0
  27. data/config/initializers/action_policy.rb +1 -1
  28. data/docs/.vitepress/config.ts +132 -47
  29. data/docs/concepts/architecture.md +226 -0
  30. data/docs/concepts/auto-detection.md +254 -0
  31. data/docs/concepts/index.md +61 -0
  32. data/docs/concepts/packages-portals.md +304 -0
  33. data/docs/concepts/resources.md +224 -0
  34. data/docs/cookbook/blog.md +412 -0
  35. data/docs/cookbook/index.md +289 -0
  36. data/docs/cookbook/saas.md +481 -0
  37. data/docs/getting-started/index.md +56 -0
  38. data/docs/getting-started/installation.md +146 -0
  39. data/docs/getting-started/tutorial/01-setup.md +118 -0
  40. data/docs/getting-started/tutorial/02-first-resource.md +180 -0
  41. data/docs/getting-started/tutorial/03-authentication.md +246 -0
  42. data/docs/getting-started/tutorial/04-authorization.md +170 -0
  43. data/docs/getting-started/tutorial/05-custom-actions.md +202 -0
  44. data/docs/getting-started/tutorial/06-nested-resources.md +147 -0
  45. data/docs/getting-started/tutorial/07-customizing-ui.md +254 -0
  46. data/docs/getting-started/tutorial/index.md +64 -0
  47. data/docs/guides/adding-resources.md +420 -0
  48. data/docs/guides/authentication.md +551 -0
  49. data/docs/guides/authorization.md +468 -0
  50. data/docs/guides/creating-packages.md +380 -0
  51. data/docs/guides/custom-actions.md +523 -0
  52. data/docs/guides/index.md +45 -0
  53. data/docs/guides/multi-tenancy.md +302 -0
  54. data/docs/guides/nested-resources.md +411 -0
  55. data/docs/guides/search-filtering.md +266 -0
  56. data/docs/guides/theming.md +321 -0
  57. data/docs/index.md +67 -26
  58. data/docs/public/CLAUDE.md +64 -21
  59. data/docs/reference/assets/index.md +496 -0
  60. data/docs/reference/controller/index.md +363 -0
  61. data/docs/reference/definition/actions.md +400 -0
  62. data/docs/reference/definition/fields.md +350 -0
  63. data/docs/reference/definition/index.md +252 -0
  64. data/docs/reference/definition/query.md +342 -0
  65. data/docs/reference/generators/index.md +469 -0
  66. data/docs/reference/index.md +49 -0
  67. data/docs/reference/interaction/index.md +445 -0
  68. data/docs/reference/model/features.md +248 -0
  69. data/docs/reference/model/index.md +219 -0
  70. data/docs/reference/policy/index.md +385 -0
  71. data/docs/reference/portal/index.md +382 -0
  72. data/docs/reference/views/forms.md +396 -0
  73. data/docs/reference/views/index.md +479 -0
  74. data/gemfiles/rails_7.gemfile +9 -2
  75. data/gemfiles/rails_7.gemfile.lock +146 -111
  76. data/gemfiles/rails_8.0.gemfile +20 -0
  77. data/gemfiles/rails_8.0.gemfile.lock +417 -0
  78. data/gemfiles/rails_8.1.gemfile +20 -0
  79. data/gemfiles/rails_8.1.gemfile.lock +419 -0
  80. data/lib/generators/pu/gem/dotenv/templates/.env +2 -0
  81. data/lib/generators/pu/gem/dotenv/templates/config/initializers/001_ensure_required_env.rb +3 -1
  82. data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +13 -16
  83. data/lib/generators/pu/pkg/portal/USAGE +65 -0
  84. data/lib/generators/pu/pkg/portal/portal_generator.rb +22 -9
  85. data/lib/generators/pu/res/conn/USAGE +71 -0
  86. data/lib/generators/pu/res/model/USAGE +106 -110
  87. data/lib/generators/pu/res/model/templates/model.rb.tt +6 -2
  88. data/lib/generators/pu/res/scaffold/USAGE +85 -0
  89. data/lib/generators/pu/rodauth/install_generator.rb +2 -6
  90. data/lib/generators/pu/rodauth/templates/config/initializers/url_options.rb +17 -0
  91. data/lib/generators/pu/skills/sync/USAGE +14 -0
  92. data/lib/generators/pu/skills/sync/sync_generator.rb +66 -0
  93. data/lib/plutonium/action_policy/sti_policy_lookup.rb +1 -1
  94. data/lib/plutonium/core/controller.rb +2 -2
  95. data/lib/plutonium/interaction/base.rb +1 -0
  96. data/lib/plutonium/package/engine.rb +2 -2
  97. data/lib/plutonium/query/adhoc_block.rb +6 -2
  98. data/lib/plutonium/query/model_scope.rb +1 -1
  99. data/lib/plutonium/railtie.rb +4 -0
  100. data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +1 -1
  101. data/lib/plutonium/resource/query_object.rb +38 -8
  102. data/lib/plutonium/ui/table/components/scopes_bar.rb +39 -34
  103. data/lib/plutonium/version.rb +1 -1
  104. data/lib/tasks/release.rake +19 -4
  105. data/package.json +1 -1
  106. metadata +76 -39
  107. data/brakeman.ignore +0 -28
  108. data/docs/api-examples.md +0 -49
  109. data/docs/guide/claude-code-guide.md +0 -74
  110. data/docs/guide/deep-dive/authorization.md +0 -189
  111. data/docs/guide/deep-dive/multitenancy.md +0 -256
  112. data/docs/guide/deep-dive/resources.md +0 -390
  113. data/docs/guide/getting-started/01-installation.md +0 -165
  114. data/docs/guide/index.md +0 -28
  115. data/docs/guide/introduction/01-what-is-plutonium.md +0 -211
  116. data/docs/guide/introduction/02-core-concepts.md +0 -440
  117. data/docs/guide/tutorial/01-project-setup.md +0 -75
  118. data/docs/guide/tutorial/02-creating-a-feature-package.md +0 -45
  119. data/docs/guide/tutorial/03-defining-resources.md +0 -90
  120. data/docs/guide/tutorial/04-creating-a-portal.md +0 -101
  121. data/docs/guide/tutorial/05-customizing-the-ui.md +0 -128
  122. data/docs/guide/tutorial/06-adding-custom-actions.md +0 -101
  123. data/docs/guide/tutorial/07-implementing-authorization.md +0 -90
  124. data/docs/markdown-examples.md +0 -85
  125. data/docs/modules/action.md +0 -244
  126. data/docs/modules/authentication.md +0 -236
  127. data/docs/modules/configuration.md +0 -599
  128. data/docs/modules/controller.md +0 -443
  129. data/docs/modules/core.md +0 -316
  130. data/docs/modules/definition.md +0 -1308
  131. data/docs/modules/display.md +0 -759
  132. data/docs/modules/form.md +0 -495
  133. data/docs/modules/generator.md +0 -400
  134. data/docs/modules/index.md +0 -167
  135. data/docs/modules/interaction.md +0 -642
  136. data/docs/modules/package.md +0 -151
  137. data/docs/modules/policy.md +0 -176
  138. data/docs/modules/portal.md +0 -710
  139. data/docs/modules/query.md +0 -297
  140. data/docs/modules/resource_record.md +0 -618
  141. data/docs/modules/routing.md +0 -690
  142. data/docs/modules/table.md +0 -301
  143. data/docs/modules/ui.md +0 -631
@@ -0,0 +1,474 @@
1
+ ---
2
+ name: definition-fields
3
+ description: Configure how resource fields are rendered in forms, show pages, and tables
4
+ ---
5
+
6
+ # Definition Fields
7
+
8
+ Configure how fields are rendered using `field`, `input`, `display`, and `column` declarations.
9
+
10
+ ## Core Methods
11
+
12
+ | Method | Applies To | Use When |
13
+ |--------|-----------|----------|
14
+ | `field` | Forms + Show + Table | Universal type override |
15
+ | `input` | Forms only | Form-specific options |
16
+ | `display` | Show page only | Display-specific options |
17
+ | `column` | Table only | Table-specific options |
18
+
19
+ ## Basic Usage
20
+
21
+ ```ruby
22
+ class PostDefinition < ResourceDefinition
23
+ # field - changes type everywhere
24
+ field :content, as: :rich_text
25
+
26
+ # input - form-specific
27
+ input :title,
28
+ label: "Article Title",
29
+ hint: "Enter a descriptive title",
30
+ placeholder: "e.g. Getting Started"
31
+
32
+ # display - show page specific
33
+ display :content,
34
+ as: :markdown,
35
+ description: "Published content",
36
+ wrapper: {class: "col-span-full"}
37
+
38
+ # column - table specific
39
+ column :title, label: "Article", align: :center
40
+ column :view_count, align: :end
41
+ end
42
+ ```
43
+
44
+ ## Available Field Types
45
+
46
+ ### Input Types (Forms)
47
+
48
+ | Category | Types |
49
+ |----------|-------|
50
+ | **Text** | `:string`, `:text`, `:email`, `:url`, `:tel`, `:password` |
51
+ | **Rich Text** | `:rich_text`, `:markdown` |
52
+ | **Numeric** | `:number`, `:integer`, `:decimal`, `:range` |
53
+ | **Boolean** | `:boolean` |
54
+ | **Date/Time** | `:date`, `:time`, `:datetime` |
55
+ | **Selection** | `:select`, `:slim_select`, `:radio_buttons`, `:check_boxes` |
56
+ | **Files** | `:file`, `:uppy`, `:attachment` |
57
+ | **Associations** | `:association`, `:secure_association`, `:belongs_to`, `:has_many`, `:has_one` |
58
+ | **Special** | `:hidden`, `:color`, `:phone` |
59
+
60
+ ### Display Types (Show/Index)
61
+
62
+ `:string`, `:text`, `:email`, `:url`, `:phone`, `:markdown`, `:number`, `:integer`, `:decimal`, `:boolean`, `:date`, `:time`, `:datetime`, `:association`, `:attachment`
63
+
64
+ ## Field Options
65
+
66
+ ### Field-Level Options (wrapper)
67
+
68
+ ```ruby
69
+ input :title,
70
+ label: "Custom Label", # Custom label text
71
+ hint: "Help text for forms", # Form help text
72
+ placeholder: "Enter value", # Input placeholder
73
+ description: "For displays" # Display description
74
+ ```
75
+
76
+ ### Tag-Level Options (HTML element)
77
+
78
+ ```ruby
79
+ input :title,
80
+ class: "custom-class", # CSS class
81
+ data: {controller: "custom"}, # Data attributes
82
+ required: true, # HTML required
83
+ readonly: true, # HTML readonly
84
+ disabled: true # HTML disabled
85
+ ```
86
+
87
+ ### Wrapper Options
88
+
89
+ ```ruby
90
+ display :content, wrapper: {class: "col-span-full"}
91
+ input :notes, wrapper: {class: "bg-gray-50"}
92
+ ```
93
+
94
+ ## Select/Choices
95
+
96
+ ### Static Choices
97
+
98
+ ```ruby
99
+ input :category, as: :select, choices: %w[Tech Business Lifestyle]
100
+ input :status, as: :select, choices: Post.statuses.keys
101
+ ```
102
+
103
+ ### Dynamic Choices (requires block)
104
+
105
+ ```ruby
106
+ # Basic dynamic
107
+ input :author do |f|
108
+ choices = User.active.pluck(:name, :id)
109
+ f.select_tag choices: choices
110
+ end
111
+
112
+ # With context access
113
+ input :team_members do |f|
114
+ choices = current_user.organization.users.pluck(:name, :id)
115
+ f.select_tag choices: choices
116
+ end
117
+
118
+ # Based on object state
119
+ input :related_posts do |f|
120
+ choices = if object.persisted?
121
+ Post.where.not(id: object.id).published.pluck(:title, :id)
122
+ else
123
+ []
124
+ end
125
+ f.select_tag choices: choices
126
+ end
127
+ ```
128
+
129
+ ## Conditional Rendering
130
+
131
+ ```ruby
132
+ class PostDefinition < ResourceDefinition
133
+ # Show based on object state
134
+ display :published_at, condition: -> { object.published? }
135
+ display :rejection_reason, condition: -> { object.rejected? }
136
+
137
+ # Show based on environment
138
+ field :debug_info, condition: -> { Rails.env.development? }
139
+ end
140
+ ```
141
+
142
+ **Note:** Use `condition` for UI state logic. Use **policies** for authorization.
143
+
144
+ ## Dynamic Forms (pre_submit)
145
+
146
+ Use `pre_submit: true` to create forms that dynamically show/hide fields based on other field values. When a `pre_submit` field changes, the form re-renders server-side and conditions are re-evaluated.
147
+
148
+ ### Basic Pattern
149
+
150
+ ```ruby
151
+ class PostDefinition < ResourceDefinition
152
+ # Trigger field - causes form to re-render on change
153
+ input :send_notifications, pre_submit: true
154
+
155
+ # Dependent field - only shown when condition is true
156
+ input :notification_channel,
157
+ as: :select,
158
+ choices: %w[Email SMS],
159
+ condition: -> { object.send_notifications? }
160
+ end
161
+ ```
162
+
163
+ ### How It Works
164
+
165
+ 1. User changes a `pre_submit: true` field
166
+ 2. Form submits via Turbo (no page reload)
167
+ 3. Server re-renders the form with updated `object` state
168
+ 4. Fields with `condition` procs are re-evaluated
169
+ 5. Newly visible fields appear, hidden fields disappear
170
+
171
+ ### Multiple Dependent Fields
172
+
173
+ ```ruby
174
+ class QuestionDefinition < ResourceDefinition
175
+ # Primary selector
176
+ input :question_type, as: :select,
177
+ choices: %w[text choice scale date boolean],
178
+ pre_submit: true
179
+
180
+ # Conditional fields based on question_type
181
+ input :max_length,
182
+ as: :integer,
183
+ condition: -> { object.question_type == "text" }
184
+
185
+ input :choices,
186
+ as: :text,
187
+ hint: "One choice per line",
188
+ condition: -> { object.question_type == "choice" }
189
+
190
+ input :min_value,
191
+ as: :integer,
192
+ condition: -> { object.question_type == "scale" }
193
+
194
+ input :max_value,
195
+ as: :integer,
196
+ condition: -> { object.question_type == "scale" }
197
+ end
198
+ ```
199
+
200
+ ### Cascading Dependencies
201
+
202
+ ```ruby
203
+ class PropertyDefinition < ResourceDefinition
204
+ # First level
205
+ input :property_type, as: :select,
206
+ choices: %w[residential commercial],
207
+ pre_submit: true
208
+
209
+ # Second level - depends on property_type
210
+ input :residential_type, as: :select,
211
+ choices: %w[apartment house condo],
212
+ condition: -> { object.property_type == "residential" },
213
+ pre_submit: true
214
+
215
+ input :commercial_type, as: :select,
216
+ choices: %w[office retail warehouse],
217
+ condition: -> { object.property_type == "commercial" },
218
+ pre_submit: true
219
+
220
+ # Third level - depends on residential_type
221
+ input :apartment_floor,
222
+ as: :integer,
223
+ condition: -> { object.residential_type == "apartment" }
224
+ end
225
+ ```
226
+
227
+ ### Dynamic Choices with pre_submit
228
+
229
+ Combine `pre_submit` with block-based dynamic choices:
230
+
231
+ ```ruby
232
+ class SurveyResponseDefinition < ResourceDefinition
233
+ input :category, as: :select,
234
+ choices: Category.pluck(:name, :id),
235
+ pre_submit: true
236
+
237
+ # Choices change based on selected category
238
+ input :subcategory do |f|
239
+ choices = if object.category.present?
240
+ Category.find(object.category).subcategories.pluck(:name, :id)
241
+ else
242
+ []
243
+ end
244
+ f.select_tag choices: choices
245
+ end
246
+ end
247
+ ```
248
+
249
+ ### Tips
250
+
251
+ - Only add `pre_submit: true` to fields that control visibility of other fields
252
+ - Keep dependencies simple - deeply nested conditions are hard to debug
253
+ - The form submits on change, so avoid `pre_submit` on frequently-changed fields
254
+
255
+ ## Custom Rendering
256
+
257
+ ### Block Syntax
258
+
259
+ **For Display (can return any component):**
260
+ ```ruby
261
+ display :status do |field|
262
+ StatusBadgeComponent.new(value: field.value, class: field.dom.css_class)
263
+ end
264
+
265
+ display :metrics do |field|
266
+ if field.value.present?
267
+ MetricsChartComponent.new(data: field.value)
268
+ else
269
+ EmptyStateComponent.new(message: "No metrics")
270
+ end
271
+ end
272
+ ```
273
+
274
+ **For Input (must use form builder methods):**
275
+ ```ruby
276
+ input :birth_date do |f|
277
+ case object.age_category
278
+ when 'adult'
279
+ f.date_tag(min: 18.years.ago.to_date)
280
+ when 'minor'
281
+ f.date_tag(max: 18.years.ago.to_date)
282
+ else
283
+ f.date_tag
284
+ end
285
+ end
286
+ ```
287
+
288
+ ### phlexi_tag (Advanced Display)
289
+
290
+ ```ruby
291
+ # With component class
292
+ display :status, as: :phlexi_tag, with: StatusBadgeComponent
293
+
294
+ # With inline proc
295
+ display :priority, as: :phlexi_tag, with: ->(value, attrs) {
296
+ case value
297
+ when 'high'
298
+ span(class: "badge badge-danger") { "High" }
299
+ when 'medium'
300
+ span(class: "badge badge-warning") { "Medium" }
301
+ else
302
+ span(class: "badge badge-info") { "Low" }
303
+ end
304
+ }
305
+ ```
306
+
307
+ ### Custom Component Class
308
+
309
+ ```ruby
310
+ input :color_picker, as: ColorPickerComponent
311
+ display :chart, as: ChartComponent
312
+ ```
313
+
314
+ ## Column Alignment
315
+
316
+ ```ruby
317
+ column :title, align: :start # Left (default)
318
+ column :status, align: :center # Center
319
+ column :amount, align: :end # Right
320
+ ```
321
+
322
+ ## Nested Inputs
323
+
324
+ Render inline forms for associated records. Requires `accepts_nested_attributes_for` on the model.
325
+
326
+ ### Model Setup
327
+
328
+ ```ruby
329
+ class Post < ResourceRecord
330
+ has_many :comments
331
+ has_one :metadata
332
+
333
+ accepts_nested_attributes_for :comments, allow_destroy: true, limit: 10
334
+ accepts_nested_attributes_for :metadata, update_only: true
335
+ end
336
+ ```
337
+
338
+ ### Basic Declaration
339
+
340
+ ```ruby
341
+ class PostDefinition < ResourceDefinition
342
+ # Block syntax
343
+ nested_input :comments do |n|
344
+ n.input :body, as: :text
345
+ n.input :author_name
346
+ end
347
+
348
+ # Using another definition
349
+ nested_input :metadata, using: PostMetadataDefinition, fields: %i[seo_title seo_description]
350
+ end
351
+ ```
352
+
353
+ ### Options
354
+
355
+ | Option | Description |
356
+ |--------|-------------|
357
+ | `limit` | Max records (auto-detected from model, default: 10) |
358
+ | `allow_destroy` | Show delete checkbox (auto-detected from model) |
359
+ | `update_only` | Hide "Add" button, only edit existing |
360
+ | `description` | Help text above the section |
361
+ | `condition` | Proc to show/hide section |
362
+ | `using` | Reference another Definition class |
363
+ | `fields` | Which fields to render from the definition |
364
+
365
+ ```ruby
366
+ nested_input :amenities,
367
+ allow_destroy: true,
368
+ limit: 20,
369
+ description: "Add property amenities" do |n|
370
+ n.input :name
371
+ n.input :icon, as: :select, choices: ICONS
372
+ end
373
+ ```
374
+
375
+ ### Singular Associations
376
+
377
+ For `has_one` and `belongs_to`, limit is automatically 1:
378
+
379
+ ```ruby
380
+ nested_input :profile do |n| # has_one
381
+ n.input :bio
382
+ n.input :website
383
+ end
384
+ ```
385
+
386
+ ### Conditional Nested Inputs
387
+
388
+ ```ruby
389
+ nested_input :shipping_address,
390
+ condition: -> { object.requires_shipping? } do |n|
391
+ n.input :street
392
+ n.input :city
393
+ end
394
+ ```
395
+
396
+ ### How It Works
397
+
398
+ 1. Renders a template (hidden) for new records
399
+ 2. Renders fieldsets for existing records
400
+ 3. Stimulus controller handles Add/Remove
401
+ 4. `_destroy` checkbox marks records for deletion
402
+ 5. Parameters submitted as `model[association_attributes][id][field]`
403
+
404
+ ### Gotchas
405
+
406
+ - Model must have `accepts_nested_attributes_for`
407
+ - For custom class names, use `class_name:` in both model and `using:` in definition
408
+ - `update_only: true` hides the Add button
409
+ - Limit is enforced in UI (Add button hidden when reached)
410
+
411
+ ## File Uploads
412
+
413
+ ```ruby
414
+ input :avatar, as: :file
415
+ input :avatar, as: :uppy
416
+
417
+ input :documents, as: :file, multiple: true
418
+ input :documents, as: :uppy,
419
+ allowed_file_types: ['.pdf', '.doc'],
420
+ max_file_size: 5.megabytes
421
+ ```
422
+
423
+ ## Common Patterns
424
+
425
+ ### Rich Text Content
426
+
427
+ ```ruby
428
+ field :content, as: :rich_text # Form: rich editor
429
+ display :content, as: :markdown # Show: rendered markdown
430
+ ```
431
+
432
+ ### Money Fields
433
+
434
+ ```ruby
435
+ input :price, as: :decimal, class: "font-mono"
436
+ display :price, class: "font-bold text-green-600"
437
+ ```
438
+
439
+ ### Status Badges
440
+
441
+ ```ruby
442
+ display :status do |field|
443
+ color = case field.value
444
+ when 'active' then 'green'
445
+ when 'pending' then 'yellow'
446
+ else 'gray'
447
+ end
448
+ span(class: "badge badge-#{color}") { field.value.humanize }
449
+ end
450
+ ```
451
+
452
+ ### Hidden Fields
453
+
454
+ ```ruby
455
+ field :author_id, as: :hidden
456
+ input :tenant_id, as: :hidden
457
+ ```
458
+
459
+ ## Context in Blocks
460
+
461
+ Inside `condition` procs and `input` blocks:
462
+ - `object` - The record being edited/displayed
463
+ - `current_user` - The authenticated user
464
+ - `current_parent` - Parent record for nested resources
465
+ - `request`, `params` - Request information
466
+ - All helper methods
467
+
468
+ ## Related Skills
469
+
470
+ - `definition` - Overview and structure
471
+ - `definition-actions` - Actions and interactions
472
+ - `definition-query` - Search, filters, scopes
473
+ - `forms` - Custom form templates and field builders
474
+ - `views` - Custom page and display templates