diff --git a/assets/css/admin/components.css b/assets/css/admin/components.css
index 76c63a9..df00da7 100644
--- a/assets/css/admin/components.css
+++ b/assets/css/admin/components.css
@@ -1453,11 +1453,23 @@
flex-shrink: 0;
}
-.block-card-name {
+.block-card-info {
flex: 1;
+ min-width: 0;
+}
+
+.block-card-name {
font-size: 0.875rem;
font-weight: 500;
- min-width: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.block-card-preview {
+ display: block;
+ font-size: 0.75rem;
+ color: color-mix(in oklch, var(--t-text-primary) 50%, transparent);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -1567,13 +1579,25 @@
}
}
+.block-picker-thumb {
+ grid-column: 1 / -1;
+ width: 100%;
+ height: auto;
+ border-radius: 0.25rem;
+ background: color-mix(in oklch, var(--t-text-primary) 3%, transparent);
+ margin-bottom: 0.25rem;
+}
+
.block-picker-item > .size-5 {
- grid-row: 1 / -1;
margin-top: 0.0625rem;
}
-.block-picker-item-desc {
+.block-picker-item-name {
grid-column: 2;
+}
+
+.block-picker-item-desc {
+ grid-column: 1 / -1;
font-size: 0.6875rem;
line-height: 1.3;
color: color-mix(in oklch, var(--t-text-primary) 55%, transparent);
@@ -1606,6 +1630,26 @@
/* Block settings panel */
+.block-settings-hint {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.375rem;
+ font-size: 0.75rem;
+ color: color-mix(in oklch, var(--t-text-primary) 60%, transparent);
+ background: color-mix(in oklch, var(--t-accent) 8%, transparent);
+ border: 1px solid color-mix(in oklch, var(--t-accent) 20%, transparent);
+ border-radius: 0.375rem;
+ padding: 0.5rem 0.625rem;
+ margin-bottom: 0.5rem;
+ line-height: 1.4;
+}
+
+.block-settings-hint svg {
+ flex-shrink: 0;
+ color: var(--t-accent);
+ margin-top: 0.05rem;
+}
+
.block-card-settings {
padding: 0.75rem 0.75rem 0.25rem;
padding-left: 2.75rem;
diff --git a/lib/berrypod/pages/block_types.ex b/lib/berrypod/pages/block_types.ex
index 83a40b9..b266b66 100644
--- a/lib/berrypod/pages/block_types.ex
+++ b/lib/berrypod/pages/block_types.ex
@@ -110,10 +110,27 @@ defmodule Berrypod.Pages.BlockTypes do
},
"newsletter_card" => %{
name: "Newsletter signup",
- description: "Email signup form for collecting subscriber addresses",
+ description: "Email signup form — currently decorative, does not collect emails",
icon: "hero-envelope",
allowed_on: :all,
- settings_schema: []
+ hint:
+ "This block is decorative — form submissions aren't collected yet. Use it as a placeholder or remove it.",
+ settings_schema: [
+ %SettingsField{key: "title", label: "Title", type: :text, default: "Newsletter"},
+ %SettingsField{
+ key: "description",
+ label: "Description",
+ type: :textarea,
+ default:
+ "Sample newsletter signup. Replace with your own message to encourage subscribers."
+ },
+ %SettingsField{
+ key: "button_text",
+ label: "Button text",
+ type: :text,
+ default: "Subscribe"
+ }
+ ]
},
"social_links_card" => %{
name: "Social links",
diff --git a/lib/berrypod_web/components/block_editor_components.ex b/lib/berrypod_web/components/block_editor_components.ex
index eb14a2d..f851233 100644
--- a/lib/berrypod_web/components/block_editor_components.ex
+++ b/lib/berrypod_web/components/block_editor_components.ex
@@ -15,6 +15,7 @@ defmodule BerrypodWeb.BlockEditorComponents do
import BerrypodWeb.CoreComponents, only: [icon: 1]
alias Berrypod.Pages.{BlockEditor, BlockTypes}
+ alias BerrypodWeb.BlockThumbnails
# ── Block card ─────────────────────────────────────────────────
@@ -28,12 +29,14 @@ defmodule BerrypodWeb.BlockEditorComponents do
block_type = BlockTypes.get(assigns.block["type"])
has_settings = BlockEditor.has_settings?(assigns.block)
expanded = MapSet.member?(assigns.expanded, assigns.block["id"])
+ preview = block_preview(assigns.block, block_type)
assigns =
assigns
|> assign(:block_type, block_type)
|> assign(:has_settings, has_settings)
|> assign(:is_expanded, expanded)
+ |> assign(:preview, preview)
~H"""
-
- {(@block_type && @block_type.name) || @block["type"]}
-
+
+
+ {(@block_type && @block_type.name) || @block["type"]}
+
+ {@preview}
+
@@ -121,6 +128,7 @@ defmodule BerrypodWeb.BlockEditorComponents do
attr :block, :map, required: true
attr :schema, :list, required: true
+ attr :hint, :string, default: nil
attr :event_prefix, :string, default: ""
def block_settings_form(assigns) do
@@ -129,6 +137,9 @@ defmodule BerrypodWeb.BlockEditorComponents do
~H"""
+
+ <.icon name="hero-information-circle-mini" class="size-4" /> {@hint}
+
"""
end
+
+ # ── Block preview ──────────────────────────────────────────────
+
+ # Extracts a short one-line preview from block settings for display in the card
+ defp block_preview(_block, nil), do: nil
+
+ defp block_preview(block, block_type) do
+ settings = block["settings"] || %{}
+ schema = block_type.settings_schema
+
+ # Try text/textarea fields first, then selects, then repeaters
+ find_text_preview(settings, schema) ||
+ find_select_preview(settings, schema) ||
+ find_repeater_preview(settings, schema)
+ end
+
+ defp find_text_preview(settings, schema) do
+ schema
+ |> Enum.filter(&(&1.type in [:text, :textarea]))
+ |> Enum.find_value(fn field ->
+ case settings[field.key] do
+ val when is_binary(val) and val != "" ->
+ truncate(val, 60)
+
+ _ ->
+ nil
+ end
+ end)
+ end
+
+ defp find_select_preview(settings, schema) do
+ schema
+ |> Enum.filter(&(&1.type == :select))
+ |> Enum.find_value(fn field ->
+ case settings[field.key] do
+ val when is_binary(val) and val != "" -> "#{field.label}: #{val}"
+ _ -> nil
+ end
+ end)
+ end
+
+ defp find_repeater_preview(settings, schema) do
+ schema
+ |> Enum.filter(&(&1.type == :repeater))
+ |> Enum.find_value(fn field ->
+ case settings[field.key] do
+ items when is_list(items) and items != [] ->
+ count = length(items)
+ "#{count} #{if count == 1, do: "item", else: "items"}"
+
+ _ ->
+ nil
+ end
+ end)
+ end
+
+ defp truncate(str, max) do
+ str = String.replace(str, ~r/\s+/, " ") |> String.trim()
+
+ if String.length(str) > max do
+ String.slice(str, 0, max) <> "..."
+ else
+ str
+ end
+ end
end
diff --git a/lib/berrypod_web/components/block_thumbnails.ex b/lib/berrypod_web/components/block_thumbnails.ex
new file mode 100644
index 0000000..3519cee
--- /dev/null
+++ b/lib/berrypod_web/components/block_thumbnails.ex
@@ -0,0 +1,478 @@
+defmodule BerrypodWeb.BlockThumbnails do
+ @moduledoc """
+ Tiny SVG wireframe thumbnails for each block type.
+
+ Used in the block picker to give a visual hint of what each block looks like.
+ Each thumbnail is an 80x48 SVG with simple geometric shapes.
+ """
+
+ use Phoenix.Component
+
+ attr :type, :string, required: true
+
+ def block_thumbnail(assigns) do
+ ~H"""
+
+ """
+ end
+
+ # ── Hero banner ──────────────────────────────────────────────────
+ defp thumb_shapes(%{type: "hero"} = assigns) do
+ ~H"""
+
+
+
+
+ """
+ end
+
+ # ── Featured products ────────────────────────────────────────────
+ defp thumb_shapes(%{type: "featured_products"} = assigns) do
+ ~H"""
+
+
+
+
+
+
+
+
+
+
+
+
+ """
+ end
+
+ # ── Image + text ─────────────────────────────────────────────────
+ defp thumb_shapes(%{type: "image_text"} = assigns) do
+ ~H"""
+
+
+
+
+
+
+ """
+ end
+
+ # ── Category navigation ──────────────────────────────────────────
+ defp thumb_shapes(%{type: "category_nav"} = assigns) do
+ ~H"""
+
+
+
+
+ """
+ end
+
+ # ── Newsletter signup ────────────────────────────────────────────
+ defp thumb_shapes(%{type: "newsletter_card"} = assigns) do
+ ~H"""
+
+
+
+
+
+ """
+ end
+
+ # ── Social links ─────────────────────────────────────────────────
+ defp thumb_shapes(%{type: "social_links_card"} = assigns) do
+ ~H"""
+
+
+
+
+
+
+ """
+ end
+
+ # ── Info card ────────────────────────────────────────────────────
+ defp thumb_shapes(%{type: "info_card"} = assigns) do
+ ~H"""
+
+
+
+
+
+
+
+
+ """
+ end
+
+ # ── Trust badges ─────────────────────────────────────────────────
+ defp thumb_shapes(%{type: "trust_badges"} = assigns) do
+ ~H"""
+
+
+
+
+
+
+ """
+ end
+
+ # ── Customer reviews ─────────────────────────────────────────────
+ defp thumb_shapes(%{type: "reviews_section"} = assigns) do
+ ~H"""
+
+
+
+
+
+
+
+
+
+ """
+ end
+
+ # ── Spacer ───────────────────────────────────────────────────────
+ defp thumb_shapes(%{type: "spacer"} = assigns) do
+ ~H"""
+
+
+
+ """
+ end
+
+ # ── Divider ──────────────────────────────────────────────────────
+ defp thumb_shapes(%{type: "divider"} = assigns) do
+ ~H"""
+
+ """
+ end
+
+ # ── Button ───────────────────────────────────────────────────────
+ defp thumb_shapes(%{type: "button"} = assigns) do
+ ~H"""
+
+
+ """
+ end
+
+ # ── Video embed ──────────────────────────────────────────────────
+ defp thumb_shapes(%{type: "video_embed"} = assigns) do
+ ~H"""
+
+
+ """
+ end
+
+ # ── Product hero ─────────────────────────────────────────────────
+ defp thumb_shapes(%{type: "product_hero"} = assigns) do
+ ~H"""
+
+
+
+
+ """
+ end
+
+ # ── Breadcrumb ───────────────────────────────────────────────────
+ defp thumb_shapes(%{type: "breadcrumb"} = assigns) do
+ ~H"""
+
+
+
+
+
+ """
+ end
+
+ # ── Product grid ─────────────────────────────────────────────────
+ defp thumb_shapes(%{type: t} = assigns) when t in ~w(product_grid related_products) do
+ ~H"""
+
+
+
+
+
+
+ """
+ end
+
+ # ── Collection header ────────────────────────────────────────────
+ defp thumb_shapes(%{type: "collection_header"} = assigns) do
+ ~H"""
+
+
+ """
+ end
+
+ # ── Filter bar ───────────────────────────────────────────────────
+ defp thumb_shapes(%{type: "filter_bar"} = assigns) do
+ ~H"""
+
+
+
+
+ """
+ end
+
+ # ── Cart items ───────────────────────────────────────────────────
+ defp thumb_shapes(%{type: "cart_items"} = assigns) do
+ ~H"""
+
+
+
+
+
+
+
+ """
+ end
+
+ # ── Order summary ────────────────────────────────────────────────
+ defp thumb_shapes(%{type: "order_summary"} = assigns) do
+ ~H"""
+
+
+
+
+
+
+
+
+
+ """
+ end
+
+ # ── Contact form ─────────────────────────────────────────────────
+ defp thumb_shapes(%{type: "contact_form"} = assigns) do
+ ~H"""
+
+
+
+
+ """
+ end
+
+ # ── Content body ─────────────────────────────────────────────────
+ defp thumb_shapes(%{type: "content_body"} = assigns) do
+ ~H"""
+
+
+
+
+
+
+ """
+ end
+
+ # ── Fallback for any unmatched type ──────────────────────────────
+ defp thumb_shapes(assigns) do
+ ~H"""
+
+
+
+ """
+ end
+end
diff --git a/lib/berrypod_web/components/shop_components/content.ex b/lib/berrypod_web/components/shop_components/content.ex
index 24190f5..68a22c8 100644
--- a/lib/berrypod_web/components/shop_components/content.ex
+++ b/lib/berrypod_web/components/shop_components/content.ex
@@ -314,7 +314,7 @@ defmodule BerrypodWeb.ShopComponents.Content do
{@description}
-
@@ -329,7 +329,7 @@ defmodule BerrypodWeb.ShopComponents.Content do
{@description}
-
diff --git a/lib/berrypod_web/page_renderer.ex b/lib/berrypod_web/page_renderer.ex
index d9c2dd0..052bd2b 100644
--- a/lib/berrypod_web/page_renderer.ex
+++ b/lib/berrypod_web/page_renderer.ex
@@ -303,7 +303,15 @@ defmodule BerrypodWeb.PageRenderer do
end
defp render_block(%{block: %{"type" => "newsletter_card"}} = assigns) do
- ~H"<.newsletter_card />"
+ settings = assigns.block["settings"] || %{}
+
+ assigns =
+ assigns
+ |> assign(:title, settings["title"] || "Newsletter")
+ |> assign(:description, settings["description"] || "")
+ |> assign(:button_text, settings["button_text"] || "Subscribe")
+
+ ~H"<.newsletter_card title={@title} description={@description} button_text={@button_text} />"
end
defp render_block(%{block: %{"type" => "social_links_card"}} = assigns) do