iron-cms 0.17.3 → 0.18.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 (139) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +88 -4
  3. data/app/assets/builds/iron.css +236 -106
  4. data/app/assets/tailwind/iron/lexxy.css +111 -87
  5. data/app/controllers/concerns/iron/schema_editing.rb +19 -0
  6. data/app/controllers/iron/api/schema/base_controller.rb +25 -0
  7. data/app/controllers/iron/api/schema/block_definitions_controller.rb +49 -0
  8. data/app/controllers/iron/api/schema/content_types_controller.rb +64 -0
  9. data/app/controllers/iron/api/schema/field_definitions_controller.rb +88 -0
  10. data/app/controllers/iron/api/schema/locales_controller.rb +52 -0
  11. data/app/controllers/iron/application_controller.rb +1 -1
  12. data/app/controllers/iron/block_definitions_controller.rb +1 -0
  13. data/app/controllers/iron/content_types_controller.rb +1 -0
  14. data/app/controllers/iron/field_definitions_controller.rb +2 -1
  15. data/app/controllers/iron/first_runs_controller.rb +1 -1
  16. data/app/controllers/iron/locales_controller.rb +1 -0
  17. data/app/controllers/iron/sessions_controller.rb +1 -1
  18. data/app/helpers/iron/avatar_helper.rb +24 -2
  19. data/app/models/iron/account.rb +1 -1
  20. data/app/models/iron/api/openapi_spec.rb +37 -15
  21. data/app/models/iron/block_definition/exportable.rb +1 -1
  22. data/app/models/iron/block_definition.rb +3 -1
  23. data/app/models/iron/content.rb +176 -0
  24. data/app/models/iron/content_type/exportable.rb +3 -1
  25. data/app/models/iron/content_type/importable.rb +1 -1
  26. data/app/models/iron/content_type.rb +6 -1
  27. data/app/models/iron/entry/content_assignable.rb +10 -2
  28. data/app/models/iron/exporter.rb +1 -26
  29. data/app/models/iron/field/length_constrained.rb +17 -0
  30. data/app/models/iron/field/validatable.rb +16 -0
  31. data/app/models/iron/field.rb +1 -1
  32. data/app/models/iron/field_definition/exportable.rb +3 -4
  33. data/app/models/iron/field_definition/importable.rb +16 -9
  34. data/app/models/iron/field_definition/ranked.rb +1 -1
  35. data/app/models/iron/field_definition/searchable.rb +2 -0
  36. data/app/models/iron/field_definition/validatable.rb +40 -0
  37. data/app/models/iron/field_definition/validations.rb +175 -0
  38. data/app/models/iron/field_definition.rb +1 -1
  39. data/app/models/iron/field_definitions/date.rb +2 -0
  40. data/app/models/iron/field_definitions/file.rb +3 -11
  41. data/app/models/iron/field_definitions/number.rb +2 -0
  42. data/app/models/iron/field_definitions/reference.rb +2 -0
  43. data/app/models/iron/field_definitions/rich_text_area.rb +1 -0
  44. data/app/models/iron/field_definitions/text_area.rb +2 -0
  45. data/app/models/iron/field_definitions/text_field.rb +1 -9
  46. data/app/models/iron/fields/block.rb +5 -1
  47. data/app/models/iron/fields/block_list.rb +5 -1
  48. data/app/models/iron/fields/date.rb +19 -2
  49. data/app/models/iron/fields/file.rb +5 -3
  50. data/app/models/iron/fields/number.rb +28 -1
  51. data/app/models/iron/fields/reference.rb +2 -0
  52. data/app/models/iron/fields/rich_text_area.rb +2 -0
  53. data/app/models/iron/fields/text_area.rb +4 -0
  54. data/app/models/iron/fields/text_field.rb +9 -5
  55. data/app/models/iron/importer.rb +1 -54
  56. data/app/models/iron/locale.rb +2 -0
  57. data/app/models/iron/schema/auto_dumpable.rb +26 -0
  58. data/app/models/iron/schema/diff.rb +194 -0
  59. data/app/models/iron/schema/validation.rb +214 -0
  60. data/app/models/iron/schema.rb +282 -0
  61. data/app/models/iron/seed.rb +5 -3
  62. data/app/models/iron/system.rb +7 -0
  63. data/app/models/iron/user/deactivatable.rb +5 -0
  64. data/app/models/iron/user.rb +18 -1
  65. data/app/views/iron/api/fields/_rich_text_area.json.jbuilder +2 -2
  66. data/app/views/iron/api/schema/block_definitions/_block_definition.json.jbuilder +4 -0
  67. data/app/views/iron/api/schema/block_definitions/index.json.jbuilder +1 -0
  68. data/app/views/iron/api/schema/block_definitions/show.json.jbuilder +1 -0
  69. data/app/views/iron/api/schema/content_types/_content_type.json.jbuilder +5 -0
  70. data/app/views/iron/api/schema/content_types/index.json.jbuilder +1 -0
  71. data/app/views/iron/api/schema/content_types/show.json.jbuilder +1 -0
  72. data/app/views/iron/api/schema/field_definitions/_field_definition.json.jbuilder +7 -0
  73. data/app/views/iron/api/schema/field_definitions/show.json.jbuilder +1 -0
  74. data/app/views/iron/api/schema/locales/_locale.json.jbuilder +4 -0
  75. data/app/views/iron/api/schema/locales/index.json.jbuilder +1 -0
  76. data/app/views/iron/api/schema/locales/show.json.jbuilder +1 -0
  77. data/app/views/iron/block_definitions/_empty_state.html.erb +5 -3
  78. data/app/views/iron/block_definitions/_form.html.erb +3 -1
  79. data/app/views/iron/block_definitions/edit.html.erb +19 -17
  80. data/app/views/iron/block_definitions/index.html.erb +7 -3
  81. data/app/views/iron/block_definitions/show.html.erb +14 -8
  82. data/app/views/iron/content_types/_content_type.html.erb +1 -1
  83. data/app/views/iron/content_types/_empty_state.html.erb +5 -3
  84. data/app/views/iron/content_types/_form.html.erb +12 -8
  85. data/app/views/iron/content_types/edit.html.erb +19 -17
  86. data/app/views/iron/content_types/index.html.erb +7 -3
  87. data/app/views/iron/content_types/show.html.erb +14 -8
  88. data/app/views/iron/entries/_empty_state.html.erb +1 -1
  89. data/app/views/iron/entries/edit.html.erb +1 -1
  90. data/app/views/iron/entries/entry.html.erb +1 -1
  91. data/app/views/iron/entries/fields/_block.html.erb +4 -0
  92. data/app/views/iron/entries/fields/_block_list.html.erb +2 -0
  93. data/app/views/iron/entries/fields/_boolean.html.erb +1 -0
  94. data/app/views/iron/entries/fields/_date.html.erb +3 -2
  95. data/app/views/iron/entries/fields/_field_errors.html.erb +7 -0
  96. data/app/views/iron/entries/fields/_field_label.html.erb +8 -0
  97. data/app/views/iron/entries/fields/_file.html.erb +4 -19
  98. data/app/views/iron/entries/fields/_number.html.erb +6 -2
  99. data/app/views/iron/entries/fields/_reference.html.erb +2 -1
  100. data/app/views/iron/entries/fields/_reference_list.html.erb +2 -0
  101. data/app/views/iron/entries/fields/_rich_text_area.html.erb +2 -1
  102. data/app/views/iron/entries/fields/_text_area.html.erb +6 -2
  103. data/app/views/iron/entries/fields/_text_field.html.erb +5 -12
  104. data/app/views/iron/field_definitions/_field_definition.html.erb +51 -29
  105. data/app/views/iron/field_definitions/date/_form.html.erb +5 -1
  106. data/app/views/iron/field_definitions/edit.html.erb +10 -8
  107. data/app/views/iron/field_definitions/file/_form.html.erb +6 -0
  108. data/app/views/iron/field_definitions/new.html.erb +5 -3
  109. data/app/views/iron/field_definitions/number/_form.html.erb +23 -1
  110. data/app/views/iron/field_definitions/reference/_form.html.erb +5 -0
  111. data/app/views/iron/field_definitions/rich_text_area/_form.html.erb +5 -0
  112. data/app/views/iron/field_definitions/text_area/_form.html.erb +23 -0
  113. data/app/views/iron/field_definitions/text_field/_form.html.erb +20 -1
  114. data/app/views/iron/home/_content_types.html.erb +1 -1
  115. data/app/views/iron/locales/_form.html.erb +5 -3
  116. data/app/views/iron/locales/edit.html.erb +1 -1
  117. data/app/views/iron/locales/index.html.erb +12 -6
  118. data/app/views/iron/shared/_schema_lock_badge.html.erb +19 -0
  119. data/config/locales/en.yml +13 -1
  120. data/config/locales/it.yml +18 -1
  121. data/config/routes.rb +11 -0
  122. data/db/migrate/20260612131538_create_iron_systems.rb +9 -0
  123. data/exe/iron +5 -0
  124. data/lib/generators/iron/agents/agents_generator.rb +52 -0
  125. data/lib/generators/iron/agents/templates/AGENTS.md +24 -0
  126. data/lib/generators/iron/agents/templates/SKILL.md +423 -0
  127. data/lib/generators/iron/install/install_generator.rb +118 -0
  128. data/lib/generators/iron/install/templates/iron_release.rake +5 -0
  129. data/lib/generators/iron/install/templates/schema.json +12 -0
  130. data/lib/generators/iron/install/templates/seeds.rb +13 -0
  131. data/lib/generators/iron/pages/pages_generator.rb +5 -0
  132. data/lib/generators/iron/pages/templates/pages_controller.rb +1 -1
  133. data/lib/generators/iron/pages/templates/show.html.erb +1 -1
  134. data/lib/install/template.rb +9 -0
  135. data/lib/iron/cli.rb +43 -0
  136. data/lib/iron/version.rb +1 -1
  137. data/lib/tasks/iron_content.rake +82 -0
  138. data/lib/tasks/iron_schema.rake +45 -0
  139. metadata +61 -3
@@ -6,22 +6,15 @@
6
6
  <%= builder.hidden_field :definition_type %>
7
7
  <%= builder.hidden_field :type %>
8
8
 
9
- <%= builder.label :value_string do %>
10
- <%= field.definition.name %>
11
- <% if field.definition.required %>
12
- <span class="text-red-500 ml-1">*</span>
13
- <% end %>
14
- <% end %>
9
+ <%= render "iron/entries/fields/field_label", builder: builder, field: field, attribute: :value_string %>
15
10
  <% if field.definition.allowed_values.present? %>
16
11
  <%= builder.select :value_string,
17
12
  field.definition.allowed_values,
18
13
  { include_blank: "Select #{field.definition.name}" } %>
19
14
  <% else %>
20
- <%= builder.text_field :value_string %>
21
- <% end %>
22
- <% if field.errors.any? %>
23
- <div class="text-red-500 text-sm font-medium mt-1">
24
- <%= field.errors.full_messages.join(", ") %>
25
- </div>
15
+ <%= builder.text_field :value_string,
16
+ minlength: field.definition.min_length,
17
+ maxlength: field.definition.max_length %>
26
18
  <% end %>
19
+ <%= render "iron/entries/fields/field_errors", field: field %>
27
20
  </div>
@@ -1,26 +1,51 @@
1
- <li
2
- id="<%= dom_id field_definition %>"
3
- class="group field-definition-row"
4
- data-field-color="<%= field_definition_color(field_definition) %>"
5
- data-sortable-list-target="item"
6
- >
7
- <%= form_with model: field_definition.becomes(Iron::FieldDefinition),
8
- data: { controller: "form", action: "change->form#submit" } do |form| %>
9
-
10
- <%= form.hidden_field :rank, data: { sortable_list_target: "rank" } %>
11
-
12
- <button
13
- type="button"
14
- class="flex items-center justify-center w-10 shrink-0 self-stretch text-stone-400 hover:text-stone-500 dark:text-stone-500 dark:hover:text-stone-400 transition-colors cursor-grab"
15
- data-sortable-list-target="handle"
16
- >
17
- <%= icon "grip-vertical", class: "size-4" %>
18
- </button>
19
-
20
- <%= link_to edit_field_definition_path(field_definition),
21
- class: "flex-1 flex items-center gap-3 px-3 py-3 hover:bg-stone-50 dark:hover:bg-stone-800/50 rounded-lg transition-colors",
22
- data: { turbo_frame: :drawer } do %>
1
+ <% if Iron::Schema.editable? %>
2
+ <li
3
+ id="<%= dom_id field_definition %>"
4
+ class="group field-definition-row"
5
+ data-field-color="<%= field_definition_color(field_definition) %>"
6
+ data-sortable-list-target="item"
7
+ >
8
+ <%= form_with model: field_definition.becomes(Iron::FieldDefinition),
9
+ data: { controller: "form", action: "change->form#submit" } do |form| %>
23
10
 
11
+ <%= form.hidden_field :rank, data: { sortable_list_target: "rank" } %>
12
+
13
+ <button
14
+ type="button"
15
+ class="flex items-center justify-center w-10 shrink-0 self-stretch text-stone-400 hover:text-stone-500 dark:text-stone-500 dark:hover:text-stone-400 transition-colors cursor-grab"
16
+ data-sortable-list-target="handle"
17
+ >
18
+ <%= icon "grip-vertical", class: "size-4" %>
19
+ </button>
20
+
21
+ <%= link_to edit_field_definition_path(field_definition),
22
+ class: "flex-1 flex items-center gap-3 px-3 py-3 hover:bg-stone-50 dark:hover:bg-stone-800/50 rounded-lg transition-colors",
23
+ data: { turbo_frame: :drawer } do %>
24
+
25
+ <div class="field-icon">
26
+ <%= icon field_definition_icon(field_definition), class: "size-5" %>
27
+ </div>
28
+
29
+ <div class="flex-1 min-w-0">
30
+ <h3 class="text-sm font-semibold text-stone-900 dark:text-white truncate">
31
+ <%= field_definition.name %>
32
+ </h3>
33
+ </div>
34
+
35
+ <div class="flex items-center gap-3">
36
+ <span class="field-badge"><%= field_definition.humanized_type %></span>
37
+ <%= icon "chevron-right", class: "size-4 text-stone-400 dark:text-stone-500" %>
38
+ </div>
39
+ <% end %>
40
+ <% end %>
41
+ </li>
42
+ <% else %>
43
+ <li
44
+ id="<%= dom_id field_definition %>"
45
+ class="field-definition-row"
46
+ data-field-color="<%= field_definition_color(field_definition) %>"
47
+ >
48
+ <div class="flex-1 flex items-center gap-3 px-3 py-3">
24
49
  <div class="field-icon">
25
50
  <%= icon field_definition_icon(field_definition), class: "size-5" %>
26
51
  </div>
@@ -31,10 +56,7 @@
31
56
  </h3>
32
57
  </div>
33
58
 
34
- <div class="flex items-center gap-3">
35
- <span class="field-badge"><%= field_definition.humanized_type %></span>
36
- <%= icon "chevron-right", class: "size-4 text-stone-400 dark:text-stone-500" %>
37
- </div>
38
- <% end %>
39
- <% end %>
40
- </li>
59
+ <span class="field-badge"><%= field_definition.humanized_type %></span>
60
+ </div>
61
+ </li>
62
+ <% end %>
@@ -1,2 +1,6 @@
1
1
  <%= render layout: "iron/field_definitions/layouts/form", locals: { field_definition: field_definition } do |form| %>
2
- <% end %>
2
+ <div class="field flex items-center justify-between">
3
+ <%= form.label :required, t("iron.field_definitions.form.required") %>
4
+ <%= form.toggle :required %>
5
+ </div>
6
+ <% end %>
@@ -16,12 +16,14 @@
16
16
  <%= render "iron/field_definitions/#{@field_definition.type_handle}/form", field_definition: @field_definition %>
17
17
  </div>
18
18
 
19
- <div class="drawer-footer flex items-center gap-3">
20
- <input type="submit" value="<%= t(".submit") %>" form="field-definition-form" class="button-primary flex-1">
21
- <%= button_to t(".delete_action"), field_definition_path(@field_definition),
22
- method: :delete,
23
- class: "button-secondary justify-center w-full",
24
- form_class: "flex-1",
25
- data: { turbo_confirm: t(".delete_confirm") } %>
26
- </div>
19
+ <% if Iron::Schema.editable? %>
20
+ <div class="drawer-footer flex items-center gap-3">
21
+ <input type="submit" value="<%= t(".submit") %>" form="field-definition-form" class="button-primary flex-1">
22
+ <%= button_to t(".delete_action"), field_definition_path(@field_definition),
23
+ method: :delete,
24
+ class: "button-secondary justify-center w-full",
25
+ form_class: "flex-1",
26
+ data: { turbo_confirm: t(".delete_confirm") } %>
27
+ </div>
28
+ <% end %>
27
29
  <% end %>
@@ -1,4 +1,9 @@
1
1
  <%= render layout: "iron/field_definitions/layouts/form", locals: { field_definition: field_definition } do |form| %>
2
+ <div class="field flex items-center justify-between">
3
+ <%= form.label :required, t("iron.field_definitions.form.required") %>
4
+ <%= form.toggle :required %>
5
+ </div>
6
+
2
7
  <fieldset class="field">
3
8
  <legend class="text-sm/6 font-medium text-stone-950 dark:text-white">
4
9
  <%= t("iron.field_definitions.file.accepted_file_types") %>
@@ -44,5 +49,6 @@
44
49
  </label>
45
50
  </div>
46
51
  </div>
52
+ <%= form.error_for :selected_presets %>
47
53
  </fieldset>
48
54
  <% end %>
@@ -15,7 +15,9 @@
15
15
  </section>
16
16
  </div>
17
17
 
18
- <div class="drawer-footer">
19
- <input type="submit" value="<%= t(".submit") %>" form="field-definition-form" class="button-primary w-full">
20
- </div>
18
+ <% if Iron::Schema.editable? %>
19
+ <div class="drawer-footer">
20
+ <input type="submit" value="<%= t(".submit") %>" form="field-definition-form" class="button-primary w-full">
21
+ </div>
22
+ <% end %>
21
23
  <% end %>
@@ -1,2 +1,24 @@
1
- <%= render layout: "iron/field_definitions/layouts/form", locals: { field_definition: field_definition } do %>
1
+ <%= render layout: "iron/field_definitions/layouts/form", locals: { field_definition: field_definition } do |form| %>
2
+ <div class="field flex items-center justify-between">
3
+ <%= form.label :required, t("iron.field_definitions.form.required") %>
4
+ <%= form.toggle :required %>
5
+ </div>
6
+
7
+ <div class="grid grid-cols-2 gap-4">
8
+ <div class="field">
9
+ <%= form.label :min, t("iron.field_definitions.form.min") %>
10
+ <div>
11
+ <%= form.number_field :min, step: "any" %>
12
+ <%= form.error_for :min %>
13
+ </div>
14
+ </div>
15
+
16
+ <div class="field">
17
+ <%= form.label :max, t("iron.field_definitions.form.max") %>
18
+ <div>
19
+ <%= form.number_field :max, step: "any" %>
20
+ <%= form.error_for :max %>
21
+ </div>
22
+ </div>
23
+ </div>
2
24
  <% end %>
@@ -1,4 +1,9 @@
1
1
  <%= render layout: "iron/field_definitions/layouts/form", locals: { field_definition: field_definition } do |form| %>
2
+ <div class="field flex items-center justify-between">
3
+ <%= form.label :required, t("iron.field_definitions.form.required") %>
4
+ <%= form.toggle :required %>
5
+ </div>
6
+
2
7
  <div class="field">
3
8
  <%= form.label :supported_content_type_ids, t("iron.field_definitions.reference.supported_content_types") %>
4
9
  <div class="selection-list">
@@ -1,4 +1,9 @@
1
1
  <%= render layout: "iron/field_definitions/layouts/form", locals: { field_definition: field_definition } do |form| %>
2
+ <div class="field flex items-center justify-between">
3
+ <%= form.label :required, t("iron.field_definitions.form.required") %>
4
+ <%= form.toggle :required %>
5
+ </div>
6
+
2
7
  <div class="field flex items-center justify-between">
3
8
  <%= form.label :searchable, t("iron.field_definitions.rich_text_area.searchable") %>
4
9
  <%= form.toggle :searchable %>
@@ -1,6 +1,29 @@
1
1
  <%= render layout: "iron/field_definitions/layouts/form", locals: { field_definition: field_definition } do |form| %>
2
+ <div class="field flex items-center justify-between">
3
+ <%= form.label :required, t("iron.field_definitions.form.required") %>
4
+ <%= form.toggle :required %>
5
+ </div>
6
+
2
7
  <div class="field flex items-center justify-between">
3
8
  <%= form.label :searchable, t("iron.field_definitions.text_area.searchable") %>
4
9
  <%= form.toggle :searchable %>
5
10
  </div>
11
+
12
+ <div class="grid grid-cols-2 gap-4">
13
+ <div class="field">
14
+ <%= form.label :min_length, t("iron.field_definitions.form.min_length") %>
15
+ <div>
16
+ <%= form.number_field :min_length, min: 1 %>
17
+ <%= form.error_for :min_length %>
18
+ </div>
19
+ </div>
20
+
21
+ <div class="field">
22
+ <%= form.label :max_length, t("iron.field_definitions.form.max_length") %>
23
+ <div>
24
+ <%= form.number_field :max_length, min: 1 %>
25
+ <%= form.error_for :max_length %>
26
+ </div>
27
+ </div>
28
+ </div>
6
29
  <% end %>
@@ -1,6 +1,6 @@
1
1
  <%= render layout: "iron/field_definitions/layouts/form", locals: { field_definition: field_definition } do |form| %>
2
2
  <div class="field flex items-center justify-between">
3
- <%= form.label :required, t("iron.field_definitions.text_field.required") %>
3
+ <%= form.label :required, t("iron.field_definitions.form.required") %>
4
4
  <%= form.toggle :required %>
5
5
  </div>
6
6
 
@@ -17,6 +17,25 @@
17
17
  rows: 3,
18
18
  class: "input" %>
19
19
  <p class="text-xs text-stone-500"><%= t("iron.field_definitions.text_field.allowed_values_help") %></p>
20
+ <%= form.error_for :allowed_values %>
21
+ </div>
22
+ </div>
23
+
24
+ <div class="grid grid-cols-2 gap-4">
25
+ <div class="field">
26
+ <%= form.label :min_length, t("iron.field_definitions.form.min_length") %>
27
+ <div>
28
+ <%= form.number_field :min_length, min: 1 %>
29
+ <%= form.error_for :min_length %>
30
+ </div>
31
+ </div>
32
+
33
+ <div class="field">
34
+ <%= form.label :max_length, t("iron.field_definitions.form.max_length") %>
35
+ <div>
36
+ <%= form.number_field :max_length, min: 1 %>
37
+ <%= form.error_for :max_length %>
38
+ </div>
20
39
  </div>
21
40
  </div>
22
41
  <% end %>
@@ -7,7 +7,7 @@
7
7
  <% @content_types.each do |content_type| %>
8
8
  <div class="card flex items-center gap-3 px-4 py-3.5" data-type="<%= content_type.humanized_type.downcase %>">
9
9
  <div class="content-type-icon">
10
- <%= icon content_type.icon.presence || "file-text", class: "size-4" %>
10
+ <%= icon content_type.display_icon, class: "size-4" %>
11
11
  </div>
12
12
  <div class="flex-1 min-w-0">
13
13
  <%= link_to content_type.name, content_type_sidebar_url(content_type), class: "card-link block text-sm font-medium text-stone-900 dark:text-white truncate" %>
@@ -19,7 +19,9 @@
19
19
  </div>
20
20
  </div>
21
21
 
22
- <div class="mt-4">
23
- <%= form.submit t("iron.actions.save"), class: "button-primary" %>
24
- </div>
22
+ <% if Iron::Schema.editable? %>
23
+ <div class="mt-4">
24
+ <%= form.submit t("iron.actions.save"), class: "button-primary" %>
25
+ </div>
26
+ <% end %>
25
27
  <% end %>
@@ -5,7 +5,7 @@
5
5
  <div class="flex items-center justify-between mb-6">
6
6
  <%= link_to t(".breadcrumb"), locales_path, class: "breadcrumb" %>
7
7
 
8
- <% unless @locale.default? %>
8
+ <% if Iron::Schema.editable? && !@locale.default? %>
9
9
  <el-dropdown class="inline-block">
10
10
  <button type="button" class="button-ghost button-sm p-2">
11
11
  <%= icon "ellipsis", class: "size-5" %>
@@ -10,9 +10,13 @@
10
10
  </p>
11
11
  </div>
12
12
 
13
- <%= link_to new_locale_path, class: "button-primary self-start" do %>
14
- <%= icon "plus", class: "size-4" %>
15
- <%= t(".new_locale") %>
13
+ <% if Iron::Schema.editable? %>
14
+ <%= link_to new_locale_path, class: "button-primary self-start" do %>
15
+ <%= icon "plus", class: "size-4" %>
16
+ <%= t(".new_locale") %>
17
+ <% end %>
18
+ <% else %>
19
+ <%= render "iron/shared/schema_lock_badge" %>
16
20
  <% end %>
17
21
  </div>
18
22
 
@@ -31,9 +35,11 @@
31
35
  </div>
32
36
  <h3 class="text-sm font-medium text-stone-900 dark:text-white mb-1"><%= t(".empty_title") %></h3>
33
37
  <p class="text-sm text-stone-500 dark:text-stone-400 mb-4"><%= t(".empty_description") %></p>
34
- <%= link_to new_locale_path, class: "button-primary" do %>
35
- <%= icon "plus", class: "size-4" %>
36
- <%= t(".new_locale") %>
38
+ <% if Iron::Schema.editable? %>
39
+ <%= link_to new_locale_path, class: "button-primary" do %>
40
+ <%= icon "plus", class: "size-4" %>
41
+ <%= t(".new_locale") %>
42
+ <% end %>
37
43
  <% end %>
38
44
  </div>
39
45
  </div>
@@ -0,0 +1,19 @@
1
+ <div class="group relative inline-flex self-start">
2
+ <span
3
+ tabindex="0"
4
+ role="note"
5
+ aria-describedby="schema_lock_details"
6
+ class="inline-flex cursor-default items-center gap-1.5 rounded-full border border-amber-300/70 bg-amber-50 px-3 py-1.5 text-xs font-medium text-amber-900 transition hover:bg-amber-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/60 dark:border-amber-400/20 dark:bg-amber-400/10 dark:text-amber-200 dark:hover:bg-amber-400/15"
7
+ >
8
+ <%= icon "lock", class: "size-3.5" %>
9
+ <%= t("iron.schema.locked.badge") %>
10
+ </span>
11
+
12
+ <div
13
+ id="schema_lock_details"
14
+ role="tooltip"
15
+ class="pointer-events-none absolute right-0 top-full z-20 mt-2 w-80 rounded-xl border border-stone-200 bg-white p-3.5 text-left text-xs leading-relaxed text-stone-600 opacity-0 shadow-xl ring-1 ring-black/5 transition duration-150 group-hover:opacity-100 group-focus-within:opacity-100 dark:border-stone-700 dark:bg-stone-800 dark:text-stone-300 dark:ring-white/10"
16
+ >
17
+ <%= t("iron.schema.locked.details") %>
18
+ </div>
19
+ </div>
@@ -31,6 +31,8 @@ en:
31
31
  attributes:
32
32
  base:
33
33
  cannot_deactivate_self: "You cannot delete your own account while logged in"
34
+ cannot_deactivate_system: "The system account cannot be deactivated"
35
+ cannot_destroy_system: "The system account cannot be deleted"
34
36
  role:
35
37
  cannot_change_own_role: "cannot be changed for your own account"
36
38
  iron:
@@ -201,6 +203,11 @@ en:
201
203
  specific_file_types: "Specific file types"
202
204
  form:
203
205
  handle_help: "The API identifier for this field"
206
+ max: "Maximum value"
207
+ max_length: "Maximum length"
208
+ min: "Minimum value"
209
+ min_length: "Minimum length"
210
+ required: "Required"
204
211
  new:
205
212
  settings_title: "Field Settings"
206
213
  submit: "Create field"
@@ -214,7 +221,6 @@ en:
214
221
  text_field:
215
222
  allowed_values: "Allowed values"
216
223
  allowed_values_help: "One value per line, leave blank to allow any value"
217
- required: "Required"
218
224
  searchable: "Searchable"
219
225
  first_runs:
220
226
  show:
@@ -336,6 +342,12 @@ en:
336
342
  body_text: "You can reset your password within the next 15 minutes on this password reset page:\n%{url}"
337
343
  link_text: "this password reset page"
338
344
  subject: "Reset your password"
345
+ schema:
346
+ locked:
347
+ alert: "The schema is managed in code and can't be edited here."
348
+ api_error: "is managed in code and cannot be edited on this environment"
349
+ badge: "Managed in code"
350
+ details: "Content types, blocks, fields, and locales are defined in db/cms/schema.json and applied on deploy. Editing is disabled on this environment — change the schema in development, commit it, and run bin/rails iron:schema:apply."
339
351
  sessions:
340
352
  new:
341
353
  email_address: "Email address"
@@ -70,6 +70,8 @@ it:
70
70
  attributes:
71
71
  base:
72
72
  cannot_deactivate_self: "Non puoi eliminare il tuo account mentre sei autenticato"
73
+ cannot_deactivate_system: "L'account di sistema non può essere disattivato"
74
+ cannot_destroy_system: "L'account di sistema non può essere eliminato"
73
75
  role:
74
76
  cannot_change_own_role: "non può essere cambiato per il tuo account"
75
77
  iron:
@@ -240,16 +242,25 @@ it:
240
242
  specific_file_types: "Tipi di file specifici"
241
243
  form:
242
244
  handle_help: "L'identificativo API per questo campo"
245
+ max: "Valore massimo"
246
+ max_length: "Lunghezza massima"
247
+ min: "Valore minimo"
248
+ min_length: "Lunghezza minima"
249
+ required: "Obbligatorio"
243
250
  new:
244
251
  settings_title: "Impostazioni campo"
245
252
  submit: "Crea campo"
246
253
  title: "Nuovo campo"
247
254
  reference:
248
255
  supported_content_types: "Tipi di contenuto supportati"
256
+ rich_text_area:
257
+ searchable: "Ricercabile"
258
+ text_area:
259
+ searchable: "Ricercabile"
249
260
  text_field:
250
261
  allowed_values: "Valori consentiti"
251
262
  allowed_values_help: "Un valore per riga, lascia vuoto per consentire qualsiasi valore"
252
- required: "Obbligatorio"
263
+ searchable: "Ricercabile"
253
264
  first_runs:
254
265
  show:
255
266
  description: "Inizia a gestire i tuoi contenuti con Iron CMS"
@@ -370,6 +381,12 @@ it:
370
381
  body_text: "Puoi reimpostare la password entro i prossimi 15 minuti su questa pagina di reimpostazione:\n%{url}"
371
382
  link_text: "questa pagina di reimpostazione"
372
383
  subject: "Reimposta la password"
384
+ schema:
385
+ locked:
386
+ alert: "Lo schema è gestito nel codice e non può essere modificato qui."
387
+ api_error: "è gestito nel codice e non può essere modificato in questo ambiente"
388
+ badge: "Gestito nel codice"
389
+ details: "Tipi di contenuto, blocchi, campi e lingue sono definiti in db/cms/schema.json e applicati al deploy. La modifica è disabilitata in questo ambiente — modifica lo schema in sviluppo, esegui il commit e lancia bin/rails iron:schema:apply."
373
390
  sessions:
374
391
  new:
375
392
  email_address: "Indirizzo email"
data/config/routes.rb CHANGED
@@ -36,6 +36,17 @@ Iron::Engine.routes.draw do
36
36
  patch "content/:handle/:id", to: "content#update"
37
37
  delete "content/:handle/:id", to: "content#destroy"
38
38
  resource :search, only: :show, controller: "search"
39
+
40
+ namespace :schema do
41
+ resources :content_types, only: %i[ index create show update destroy ] do
42
+ resources :field_definitions, only: :create
43
+ end
44
+ resources :block_definitions, only: %i[ index create show update destroy ] do
45
+ resources :field_definitions, only: :create
46
+ end
47
+ resources :field_definitions, only: %i[ show update destroy ]
48
+ resources :locales, only: %i[ index create show update destroy ]
49
+ end
39
50
  end
40
51
 
41
52
  resource :settings, only: %i[ show update ]
@@ -0,0 +1,9 @@
1
+ class CreateIronSystems < ActiveRecord::Migration[8.1]
2
+ def change
3
+ create_table :iron_systems do |t|
4
+ t.string :name, null: false, default: "System"
5
+
6
+ t.timestamps
7
+ end
8
+ end
9
+ end
data/exe/iron ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "iron/cli"
4
+
5
+ Iron::CLI.start(ARGV)
@@ -0,0 +1,52 @@
1
+ require "rails/generators"
2
+
3
+ module Iron
4
+ module Generators
5
+ class AgentsGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ IRON_SECTION_HEADING = "## Iron CMS".freeze
9
+
10
+ class_option :quiet, type: :boolean, default: false,
11
+ desc: "Suppress the closing instructions (set when composed by iron:install)"
12
+
13
+ def create_skill
14
+ copy_file "SKILL.md", ".claude/skills/iron-cms/SKILL.md"
15
+ end
16
+
17
+ def update_agents_md
18
+ if File.exist?(agents_md_path)
19
+ append_to_file "AGENTS.md", "\n#{iron_section}" unless iron_section_present?
20
+ else
21
+ create_file "AGENTS.md", iron_section
22
+ end
23
+ end
24
+
25
+ def display_instructions
26
+ return if options[:quiet]
27
+
28
+ say "\n=== Iron CMS Agent Setup Complete ===\n", :green
29
+ say "Created:"
30
+ say " - .claude/skills/iron-cms/SKILL.md (the full Iron CMS workflow for coding agents)"
31
+ say " - AGENTS.md with an Iron CMS section (command crib sheet)"
32
+ say "\nNext steps:"
33
+ say "1. Run bin/rails iron:schema:dump to materialize db/cms/schema.json."
34
+ say "2. Read .claude/skills/iron-cms/SKILL.md for the schema and content workflow."
35
+ end
36
+
37
+ private
38
+
39
+ def agents_md_path
40
+ File.join(destination_root, "AGENTS.md")
41
+ end
42
+
43
+ def iron_section_present?
44
+ File.read(agents_md_path).include?(IRON_SECTION_HEADING)
45
+ end
46
+
47
+ def iron_section
48
+ @iron_section ||= File.read(find_in_source_paths("AGENTS.md"))
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,24 @@
1
+ ## Iron CMS
2
+
3
+ This app's CMS is powered by Iron. The content model (content types, block definitions,
4
+ fields, locales) lives declaratively in `db/cms/schema.json`; content (entries) lives in
5
+ the database. Model by editing the schema file and applying it; create and edit content
6
+ through the `iron:content` tasks. The full workflow — schema file format, payload formats,
7
+ field types — is documented in `.claude/skills/iron-cms/SKILL.md`.
8
+
9
+ ### Commands
10
+
11
+ - `bin/rails iron:schema:dump` — write the current database schema to `db/cms/schema.json`
12
+ - `bin/rails iron:schema:diff` — show drift between the schema file and the database
13
+ - `bin/rails iron:schema:apply` — upsert the schema file into the database (`PRUNE=1` also deletes)
14
+ - `bin/rails iron:content:list HANDLE=<type>` — list entries of a content type
15
+ - `bin/rails iron:content:get HANDLE=<type> [ID=|ROUTE=]` — fetch one entry as JSON
16
+ - `bin/rails iron:content:create HANDLE=<type> [ROUTE=]` — create an entry (payload via `PAYLOAD=`, `FILE=`, or stdin)
17
+ - `bin/rails iron:content:update HANDLE=<type> [ID=|ROUTE=]` — update an entry (partial; singles need no ID)
18
+ - `bin/rails iron:content:delete HANDLE=<type> ID=|ROUTE=` — delete an entry
19
+ - `bin/rails iron:seed:dump` — checkpoint schema + content to `db/seeds/iron.zip`
20
+ - `bin/rails g iron:pages` — install the public pages controller (once per app)
21
+ - `bin/rails g iron:template <handle>` — scaffold a view template for a content type
22
+
23
+ Schema changes go through the file and `iron:schema:apply`, never through one-off code.
24
+ Content changes go through the `iron:content` tasks. See the skill for details.