merge page settings into Page tab and remove Settings tab
All checks were successful
deploy / deploy (push) Successful in 3m37s

Custom page settings (title, slug, meta, published, navigation) now
appear as a collapsible section at the top of the Page tab. The
separate Settings tab has been fully removed.

- Add page_settings_section component to page_renderer.ex
- Remove dead :settings case from editor_panel_content
- Delete settings_editor.ex component entirely
- Remove SettingsEditor import from shop_components.ex
- Add .page-settings-* CSS styles
- Clean up old .settings-* CSS styles

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jamey 2026-03-28 23:54:04 +00:00
parent 242fed0501
commit dd7146cb41
6 changed files with 267 additions and 439 deletions

View File

@ -161,9 +161,11 @@ Restructure the 3-tab editor panel for better discoverability. Replace Settings
| 10-14 | Header & footer navigation editors | 3h | done | | 10-14 | Header & footer navigation editors | 3h | done |
| 15-16 | Footer content (about, copyright, newsletter toggle) | 1.25h | done | | 15-16 | Footer content (about, copyright, newsletter toggle) | 1.25h | done |
| 17-18 | Move branding from Theme to Site | 1.5h | done | | 17-18 | Move branding from Theme to Site | 1.5h | done |
| 19-20 | Merge page settings into Page tab, remove Settings tab | 1h | planned | | 19-20 | Merge page settings into Page tab, remove Settings tab | 1h | done |
| 21-22 | Polish and testing | 2h | planned | | 21-22 | Polish and testing | 2h | planned |
Custom page settings (title, slug, meta description, published, navigation options) now appear inline in the Page tab as a collapsible section. The separate Settings tab has been fully removed along with the SettingsEditor component.
Social links now support 40+ platforms (grouped by category), auto-detect platform from pasted URLs (including deep links like `tg://` and `spotify:`), normalize bare domains to https://, preserve custom protocols, and filter empty URLs from shop display. Social links now support 40+ platforms (grouped by category), auto-detect platform from pasted URLs (including deep links like `tg://` and `spotify:`), normalize bare domains to https://, preserve custom protocols, and filter empty URLs from shop display.
Navigation editors support add/edit/delete/reorder for both header and footer nav items. Page picker dropdown allows linking to system pages (home, about, contact, etc.) or custom published pages. Changes preview immediately with live update in the shop layout. Removed legacy /admin/navigation page — nav editing is now exclusively through the Site tab with live preview. Navigation editors support add/edit/delete/reorder for both header and footer nav items. Page picker dropdown allows linking to system pages (home, about, contact, etc.) or custom published pages. Changes preview immediately with live update in the shop layout. Removed legacy /admin/navigation page — nav editing is now exclusively through the Site tab with live preview.

View File

@ -2440,6 +2440,99 @@
} }
} }
/* Page settings section (inline in Page tab for custom pages) */
.page-settings-details {
margin-bottom: 0.75rem;
border: 1px solid var(--admin-border);
border-radius: var(--admin-radius);
background: var(--admin-bg-alt);
}
.page-settings-summary {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 0.75rem;
cursor: pointer;
user-select: none;
font-size: 0.8125rem;
font-weight: 500;
color: var(--admin-text-secondary);
&:hover {
color: var(--admin-text-primary);
}
& .page-settings-chevron {
margin-left: auto;
transition: transform 0.15s ease;
}
}
.page-settings-details[open] .page-settings-chevron {
transform: rotate(180deg);
}
.page-settings-form {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 0 0.75rem 0.75rem;
}
.page-settings-field {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.page-settings-label {
font-size: 0.75rem;
font-weight: 500;
color: var(--admin-text-secondary);
}
.page-settings-slug-input {
display: flex;
align-items: center;
gap: 0;
}
.page-settings-slug-prefix {
padding: 0.375rem 0.5rem;
background: var(--admin-bg);
border: 1px solid var(--admin-border);
border-right: none;
border-radius: var(--admin-radius) 0 0 var(--admin-radius);
font-size: 0.875rem;
color: var(--admin-text-tertiary);
}
.page-settings-slug-input .admin-input {
border-radius: 0 var(--admin-radius) var(--admin-radius) 0;
}
.page-settings-checks {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.page-settings-nav-options {
display: flex;
gap: 0.75rem;
padding-left: 1.5rem;
}
.page-settings-field-inline {
flex: 1;
}
.page-settings-field-sm {
width: 5rem;
}
/* Block list in editor */ /* Block list in editor */
.block-list { .block-list {
@ -5133,72 +5226,6 @@
/* Last group in a customise section (no border, tighter margin than theme-group) */ /* Last group in a customise section (no border, tighter margin than theme-group) */
.theme-group-flush { margin-bottom: 1rem; } .theme-group-flush { margin-bottom: 1rem; }
/* ── Settings editor (on-site editor Settings tab) ── */
.settings-editor-form {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.settings-slug-preview {
display: flex;
align-items: center;
gap: 0.25rem;
}
.settings-slug-preview .admin-input {
flex: 1;
}
.settings-nav-options {
padding-left: 1rem;
border-left: 2px solid var(--admin-border);
}
.settings-nav-row {
display: flex;
gap: 0.75rem;
}
.settings-nav-field {
flex: 1;
}
.settings-nav-field-sm {
flex: 0 0 5rem;
}
.settings-actions {
display: flex;
align-items: center;
gap: 0.75rem;
padding-top: 0.5rem;
}
.settings-save-indicator {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.875rem;
color: var(--admin-success);
}
.settings-save-error {
color: var(--admin-error);
}
.settings-admin-link {
padding-top: 0.5rem;
border-top: 1px solid var(--admin-border);
}
.settings-info-view {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
/* ── Content width containers ── */ /* ── Content width containers ── */
.admin-content-narrow { max-width: 32rem; } .admin-content-narrow { max-width: 32rem; }

View File

@ -215,8 +215,8 @@ end
| 16 | Footer: read new fields | 15 | 30m | done | | 16 | Footer: read new fields | 15 | 30m | done |
| 17 | Move branding settings from Theme to Site | 4 | 1h | done | | 17 | Move branding settings from Theme to Site | 4 | 1h | done |
| 18 | Theme tab: remove branding, polish remaining | 17 | 30m | done | | 18 | Theme tab: remove branding, polish remaining | 17 | 30m | done |
| 19 | Merge page settings into Page tab | — | 45m | planned | | 19 | Merge page settings into Page tab | — | 45m | done |
| 20 | Remove Settings tab | 19 | 15m | planned | | 20 | Remove Settings tab | 19 | 15m | done |
| 21 | Polish: responsive, empty states, validation | 1-20 | 1.5h | planned | | 21 | Polish: responsive, empty states, validation | 1-20 | 1.5h | planned |
| 22 | Testing: all page types, edge cases | 21 | 30m | planned | | 22 | Testing: all page types, edge cases | 21 | 30m | planned |
@ -328,6 +328,25 @@ Moved branding settings from Theme tab to Site tab.
- Accent, hover, sale colours - Accent, hover, sale colours
- Customise accordion with typography, colours, layout, shape, products, product page groups - Customise accordion with typography, colours, layout, shape, products, product page groups
### Completed: Page Settings Migration (Tasks 19-20)
Merged custom page settings inline into Page tab and removed Settings tab.
**What changed:**
- Custom pages show a collapsible "Page settings" section at the top of the Page tab
- Contains: title, URL slug, meta description, published toggle, show in nav toggle, nav label, nav position
- System pages, product pages, and collection pages no longer show settings (editing done elsewhere)
- Settings tab button was already removed from UI; dead code removed from page_renderer.ex
- Deleted `settings_editor.ex` component entirely
**Files modified:**
- `lib/berrypod_web/page_renderer.ex` - Added `page_settings_section` component inline in `editor_sheet_content`
- `lib/berrypod_web/components/shop_components.ex` - Removed SettingsEditor import
- `assets/css/admin/components.css` - Added `.page-settings-*` styles, removed old `.settings-*` styles
**Files deleted:**
- `lib/berrypod_web/components/shop_components/settings_editor.ex`
## UI Wireframes ## UI Wireframes
### Site tab layout ### Site tab layout

View File

@ -10,7 +10,6 @@ defmodule BerrypodWeb.ShopComponents do
- `Product` product cards, gallery, variant selector, hero sections - `Product` product cards, gallery, variant selector, hero sections
- `Content` rich text, responsive images, contact form, reviews - `Content` rich text, responsive images, contact form, reviews
- `ThemeEditor` shared theme editor components for admin and on-site editing - `ThemeEditor` shared theme editor components for admin and on-site editing
- `SettingsEditor` shared settings editor components for on-site editing
""" """
defmacro __using__(_opts \\ []) do defmacro __using__(_opts \\ []) do
@ -20,7 +19,6 @@ defmodule BerrypodWeb.ShopComponents do
import BerrypodWeb.ShopComponents.Content import BerrypodWeb.ShopComponents.Content
import BerrypodWeb.ShopComponents.Layout import BerrypodWeb.ShopComponents.Layout
import BerrypodWeb.ShopComponents.Product import BerrypodWeb.ShopComponents.Product
import BerrypodWeb.ShopComponents.SettingsEditor
import BerrypodWeb.ShopComponents.ThemeEditor import BerrypodWeb.ShopComponents.ThemeEditor
end end
end end

View File

@ -1,347 +0,0 @@
defmodule BerrypodWeb.ShopComponents.SettingsEditor do
@moduledoc """
Shared settings editor components used in the on-site editor panel.
Shows context-specific settings based on the current page type:
- Custom CMS pages: Title, slug, visibility, SEO meta, navigation
- System pages (home, about, etc.): Page info only (editing done in admin)
- Product pages: Read-only product info, link to admin
- Collection pages: Read-only collection info, link to admin
Components emit events with a configurable prefix:
- `settings_update_page` (phx-change/phx-submit)
The event prefix is controlled by `@event_prefix`:
- `"settings_"` (default) for on-site editor context
"""
use Phoenix.Component
import BerrypodWeb.CoreComponents, only: [icon: 1]
# ── Main Editor Component ────────────────────────────────────────────
@doc """
Renders the settings editor panel.
Content varies based on the page type (custom, system, product, collection).
"""
attr :page, :map, default: nil
attr :product, :map, default: nil
attr :collection_title, :string, default: nil
attr :live_action, :atom, default: nil
attr :settings_form, :any, default: nil
attr :settings_dirty, :boolean, default: false
attr :settings_save_status, :atom, default: :idle
attr :event_prefix, :string, default: "settings_"
def settings_editor(assigns) do
~H"""
<div class="editor-settings-content">
<%= cond do %>
<% @live_action == :product and @product -> %>
<.product_settings product={@product} />
<% @live_action == :collection and @collection_title -> %>
<.collection_settings collection_title={@collection_title} />
<% @page && @page[:type] == "custom" -> %>
<.custom_page_settings
page={@page}
form={@settings_form}
dirty={@settings_dirty}
save_status={@settings_save_status}
event_prefix={@event_prefix}
/>
<% @page -> %>
<.system_page_settings page={@page} />
<% true -> %>
<.no_settings_view />
<% end %>
</div>
"""
end
# ── Custom Page Settings ─────────────────────────────────────────────
attr :page, :map, required: true
attr :form, :any, default: nil
attr :dirty, :boolean, default: false
attr :save_status, :atom, default: :idle
attr :event_prefix, :string, default: "settings_"
defp custom_page_settings(assigns) do
# Use form values if available, otherwise fall back to page values
form = assigns.form || %{}
assigns =
assigns
|> assign(:form_title, form["title"] || assigns.page.title || "")
|> assign(:form_slug, form["slug"] || assigns.page.slug || "")
|> assign(:form_meta, form["meta_description"] || assigns.page.meta_description || "")
|> assign(:form_published, form_checked?(form, "published", assigns.page.published))
|> assign(:form_show_in_nav, form_checked?(form, "show_in_nav", assigns.page.show_in_nav))
|> assign(:form_nav_label, form["nav_label"] || assigns.page.nav_label || "")
|> assign(
:form_nav_position,
form["nav_position"] || to_string(assigns.page.nav_position || 0)
)
~H"""
<div class="settings-editor-form">
<form
id="settings-editor-form"
phx-change={@event_prefix <> "validate_page"}
phx-submit={@event_prefix <> "save_page"}
>
<div class="theme-section">
<label class="theme-section-label" for="settings-title">Title</label>
<input
type="text"
id="settings-title"
name="page[title]"
value={@form_title}
class="admin-input"
/>
</div>
<div class="theme-section">
<label class="theme-section-label" for="settings-slug">URL slug</label>
<div class="settings-slug-preview">
<span class="admin-text-tertiary">/</span>
<input
type="text"
id="settings-slug"
name="page[slug]"
value={@form_slug}
class="admin-input"
pattern="[a-z0-9-]+"
/>
</div>
</div>
<div class="theme-section">
<label class="theme-section-label" for="settings-meta">Meta description</label>
<textarea
id="settings-meta"
name="page[meta_description]"
rows="3"
class="admin-input admin-textarea"
placeholder="Brief description for search engines..."
>{@form_meta}</textarea>
<p class="admin-help-text">Shown in search results. Keep under 160 characters.</p>
</div>
<div class="theme-section">
<label class="admin-check-label">
<input
type="checkbox"
name="page[published]"
value="true"
checked={@form_published}
class="admin-checkbox admin-checkbox-sm"
/>
<span class="theme-check-text">Published</span>
</label>
<p class="admin-help-text">Unpublished pages are only visible to admins.</p>
</div>
<div class="theme-section">
<label class="admin-check-label">
<input
type="checkbox"
name="page[show_in_nav]"
value="true"
checked={@form_show_in_nav}
class="admin-checkbox admin-checkbox-sm"
/>
<span class="theme-check-text">Show in navigation</span>
</label>
</div>
<div :if={@form_show_in_nav} class="theme-section settings-nav-options">
<div class="settings-nav-row">
<div class="settings-nav-field">
<label class="theme-section-label" for="settings-nav-label">Nav label</label>
<input
type="text"
id="settings-nav-label"
name="page[nav_label]"
value={@form_nav_label}
class="admin-input"
placeholder={@page.title}
/>
</div>
<div class="settings-nav-field settings-nav-field-sm">
<label class="theme-section-label" for="settings-nav-position">Position</label>
<input
type="number"
id="settings-nav-position"
name="page[nav_position]"
value={@form_nav_position}
class="admin-input"
min="0"
/>
</div>
</div>
</div>
<div class="theme-section settings-actions">
<button
type="submit"
class={[
"admin-btn admin-btn-sm",
@dirty && "admin-btn-primary",
!@dirty && "admin-btn-outline"
]}
disabled={!@dirty}
>
{if @save_status == :saving, do: "Saving...", else: "Save settings"}
</button>
<.save_status_indicator status={@save_status} />
</div>
</form>
<div class="theme-section settings-admin-link">
<a href={"/admin/pages/#{@page.slug}/settings"} class="admin-link">
<.icon name="hero-cog-6-tooth-mini" class="size-4" /> Full page settings
</a>
</div>
</div>
"""
end
# ── System Page Settings ─────────────────────────────────────────────
attr :page, :map, required: true
defp system_page_settings(assigns) do
~H"""
<div class="settings-info-view">
<div class="theme-section">
<label class="theme-section-label">Page</label>
<p class="admin-text-primary">{@page.title}</p>
</div>
<div class="theme-section">
<label class="theme-section-label">Type</label>
<p class="admin-text-secondary">System page</p>
</div>
<div class="theme-section">
<p class="admin-text-secondary">
This is a built-in page. Edit its content using the Page tab,
or <a href="/admin/pages" class="admin-link">manage pages in admin</a>.
</p>
</div>
</div>
"""
end
# ── Product Page Settings ────────────────────────────────────────────
attr :product, :map, required: true
defp product_settings(assigns) do
~H"""
<div class="settings-info-view">
<div class="theme-section">
<label class="theme-section-label">Product</label>
<p class="admin-text-primary">{@product.title}</p>
</div>
<div class="theme-section">
<label class="theme-section-label">Provider</label>
<p class="admin-text-secondary">{provider_label(@product)}</p>
</div>
<div class="theme-section">
<p class="admin-text-secondary">
Product details are synced from your print provider.
<a href={"/admin/products/#{@product.id}"} class="admin-link">View in admin</a>
to see pricing, variants, and images.
</p>
</div>
</div>
"""
end
defp provider_label(product) do
# provider_type is on the association, not the product itself
provider_type =
case product do
%{provider_connection: %{provider_type: type}} when is_binary(type) -> type
_ -> nil
end
case provider_type do
"printify" -> "Printify"
"printful" -> "Printful"
_ -> "Print provider"
end
end
# ── Collection Page Settings ─────────────────────────────────────────
attr :collection_title, :string, required: true
defp collection_settings(assigns) do
~H"""
<div class="settings-info-view">
<div class="theme-section">
<label class="theme-section-label">Collection</label>
<p class="admin-text-primary">{@collection_title}</p>
</div>
<div class="theme-section">
<p class="admin-text-secondary">
Collection pages show products filtered by category.
Edit the page layout using the Page tab.
</p>
</div>
</div>
"""
end
# ── No Settings View ─────────────────────────────────────────────────
defp no_settings_view(assigns) do
~H"""
<div class="settings-info-view">
<div class="theme-section">
<p class="admin-text-secondary">
This page doesn't have editable settings.
<a href="/admin/settings" class="admin-link">Shop settings</a>
can be changed in admin.
</p>
</div>
</div>
"""
end
# ── Helper Functions ─────────────────────────────────────────────────
# Check if a form checkbox should be checked
# Handles both form params (strings) and page values (booleans)
defp form_checked?(form, key, page_value) when is_map(form) do
case form[key] do
"true" -> true
"false" -> false
nil -> page_value == true
_ -> page_value == true
end
end
# ── Helper Components ────────────────────────────────────────────────
attr :status, :atom, required: true
defp save_status_indicator(assigns) do
~H"""
<span :if={@status == :saved} class="settings-save-indicator">
<.icon name="hero-check-mini" class="size-4" /> Saved
</span>
<span :if={@status == :error} class="settings-save-indicator settings-save-error">
<.icon name="hero-exclamation-circle-mini" class="size-4" /> Error
</span>
"""
end
end

View File

@ -120,9 +120,6 @@ defmodule BerrypodWeb.PageRenderer do
theme_editor_contrast_warning={Map.get(assigns, :theme_editor_contrast_warning, :ok)} theme_editor_contrast_warning={Map.get(assigns, :theme_editor_contrast_warning, :ok)}
uploads={Map.get(assigns, :uploads)} uploads={Map.get(assigns, :uploads)}
site_name={Map.get(assigns, :site_name, "")} site_name={Map.get(assigns, :site_name, "")}
product={assigns[:product]}
collection_title={assigns[:collection_title]}
live_action={assigns[:live_action]}
settings_form={Map.get(assigns, :settings_form)} settings_form={Map.get(assigns, :settings_form)}
settings_dirty={Map.get(assigns, :settings_dirty, false)} settings_dirty={Map.get(assigns, :settings_dirty, false)}
settings_save_status={Map.get(assigns, :settings_save_status, :idle)} settings_save_status={Map.get(assigns, :settings_save_status, :idle)}
@ -159,9 +156,6 @@ defmodule BerrypodWeb.PageRenderer do
attr :theme_editor_contrast_warning, :atom, default: :ok attr :theme_editor_contrast_warning, :atom, default: :ok
attr :uploads, :map, default: nil attr :uploads, :map, default: nil
attr :site_name, :string, default: "" attr :site_name, :string, default: ""
attr :product, :map, default: nil
attr :collection_title, :string, default: nil
attr :live_action, :atom, default: nil
attr :settings_form, :map, default: nil attr :settings_form, :map, default: nil
attr :settings_dirty, :boolean, default: false attr :settings_dirty, :boolean, default: false
attr :settings_save_status, :atom, default: :idle attr :settings_save_status, :atom, default: :idle
@ -185,6 +179,9 @@ defmodule BerrypodWeb.PageRenderer do
editor_image_picker_images={@editor_image_picker_images} editor_image_picker_images={@editor_image_picker_images}
editor_image_picker_search={@editor_image_picker_search} editor_image_picker_search={@editor_image_picker_search}
editor_at_defaults={@editor_at_defaults} editor_at_defaults={@editor_at_defaults}
settings_form={@settings_form}
settings_dirty={@settings_dirty}
settings_save_status={@settings_save_status}
/> />
""" """
end end
@ -206,21 +203,6 @@ defmodule BerrypodWeb.PageRenderer do
""" """
end end
defp editor_panel_content(%{editor_active_tab: :settings} = assigns) do
# Legacy settings tab - will be removed once page settings are merged into Page tab
~H"""
<BerrypodWeb.ShopComponents.SettingsEditor.settings_editor
page={@page}
product={@product}
collection_title={@collection_title}
live_action={@live_action}
settings_form={@settings_form}
settings_dirty={@settings_dirty}
settings_save_status={@settings_save_status}
/>
"""
end
defp editor_panel_content(%{editor_active_tab: :site} = assigns) do defp editor_panel_content(%{editor_active_tab: :site} = assigns) do
~H""" ~H"""
<BerrypodWeb.ShopComponents.SiteEditor.site_editor <BerrypodWeb.ShopComponents.SiteEditor.site_editor
@ -282,6 +264,9 @@ defmodule BerrypodWeb.PageRenderer do
attr :editor_image_picker_images, :list, default: [] attr :editor_image_picker_images, :list, default: []
attr :editor_image_picker_search, :string, default: "" attr :editor_image_picker_search, :string, default: ""
attr :editor_at_defaults, :boolean, default: true attr :editor_at_defaults, :boolean, default: true
attr :settings_form, :map, default: nil
attr :settings_dirty, :boolean, default: false
attr :settings_save_status, :atom, default: :idle
defp editor_sheet_content(assigns) do defp editor_sheet_content(assigns) do
~H""" ~H"""
@ -320,6 +305,15 @@ defmodule BerrypodWeb.PageRenderer do
{if @editor_live_region_message, do: @editor_live_region_message} {if @editor_live_region_message, do: @editor_live_region_message}
</div> </div>
<%!-- Page settings for custom pages --%>
<.page_settings_section
:if={@page[:type] == "custom"}
page={@page}
form={@settings_form}
dirty={@settings_dirty}
save_status={@settings_save_status}
/>
<%!-- Block list --%> <%!-- Block list --%>
<div class="block-list" role="list" aria-label="Page blocks"> <div class="block-list" role="list" aria-label="Page blocks">
<.block_card <.block_card
@ -370,6 +364,141 @@ defmodule BerrypodWeb.PageRenderer do
""" """
end end
# Page settings section for custom pages (collapsible)
attr :page, :map, required: true
attr :form, :map, default: nil
attr :dirty, :boolean, default: false
attr :save_status, :atom, default: :idle
defp page_settings_section(assigns) do
form = assigns.form || %{}
assigns =
assigns
|> assign(:form_title, form["title"] || assigns.page.title || "")
|> assign(:form_slug, form["slug"] || assigns.page.slug || "")
|> assign(:form_meta, form["meta_description"] || assigns.page.meta_description || "")
|> assign(:form_published, form_checked?(form, "published", assigns.page.published))
|> assign(:form_show_in_nav, form_checked?(form, "show_in_nav", assigns.page.show_in_nav))
|> assign(:form_nav_label, form["nav_label"] || assigns.page.nav_label || "")
|> assign(
:form_nav_position,
form["nav_position"] || to_string(assigns.page.nav_position || 0)
)
~H"""
<details class="page-settings-details" open>
<summary class="page-settings-summary">
<.icon name="hero-cog-6-tooth-mini" class="size-4" />
<span>Page settings</span>
<.icon name="hero-chevron-down-mini" class="size-4 page-settings-chevron" />
</summary>
<form
id="page-settings-form"
class="page-settings-form"
phx-change="settings_validate_page"
phx-submit="settings_save_page"
>
<div class="page-settings-field">
<label class="page-settings-label" for="page-settings-title">Title</label>
<input
type="text"
id="page-settings-title"
name="page[title]"
value={@form_title}
class="admin-input"
/>
</div>
<div class="page-settings-field">
<label class="page-settings-label" for="page-settings-slug">URL slug</label>
<div class="page-settings-slug-input">
<span class="page-settings-slug-prefix">/</span>
<input
type="text"
id="page-settings-slug"
name="page[slug]"
value={@form_slug}
class="admin-input"
pattern="[a-z0-9-]+"
/>
</div>
</div>
<div class="page-settings-field">
<label class="page-settings-label" for="page-settings-meta">Meta description</label>
<textarea
id="page-settings-meta"
name="page[meta_description]"
rows="2"
class="admin-input admin-textarea"
placeholder="Brief description for search engines..."
>{@form_meta}</textarea>
</div>
<div class="page-settings-checks">
<label class="admin-check-label">
<input
type="checkbox"
name="page[published]"
value="true"
checked={@form_published}
class="admin-checkbox admin-checkbox-sm"
/>
<span>Published</span>
</label>
<label class="admin-check-label">
<input
type="checkbox"
name="page[show_in_nav]"
value="true"
checked={@form_show_in_nav}
class="admin-checkbox admin-checkbox-sm"
/>
<span>Show in navigation</span>
</label>
</div>
<div :if={@form_show_in_nav} class="page-settings-nav-options">
<div class="page-settings-field page-settings-field-inline">
<label class="page-settings-label" for="page-settings-nav-label">Nav label</label>
<input
type="text"
id="page-settings-nav-label"
name="page[nav_label]"
value={@form_nav_label}
class="admin-input"
placeholder={@page.title}
/>
</div>
<div class="page-settings-field page-settings-field-sm">
<label class="page-settings-label" for="page-settings-nav-position">Position</label>
<input
type="number"
id="page-settings-nav-position"
name="page[nav_position]"
value={@form_nav_position}
class="admin-input"
min="0"
/>
</div>
</div>
</form>
</details>
"""
end
defp form_checked?(form, key, page_value) when is_map(form) do
case form[key] do
"true" -> true
"false" -> false
nil -> page_value == true
_ -> page_value == true
end
end
# ── Block dispatch ────────────────────────────────────────────── # ── Block dispatch ──────────────────────────────────────────────
defp render_block(%{block: %{"type" => "hero"}} = assigns) do defp render_block(%{block: %{"type" => "hero"}} = assigns) do