studio-engine 0.17.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +67 -0
- data/app/assets/tailwind/studio_engine/engine-motion.css +620 -5
- data/app/controllers/style_controller.rb +25 -0
- data/app/views/studio/modals/_crop_photo.html.erb +5 -1
- data/app/views/studio/modals/_host.html.erb +5 -0
- data/app/views/studio/modals/_image_upload.html.erb +16 -5
- data/app/views/studio/modals/_saving.html.erb +6 -2
- data/app/views/studio/modals/auth/_resend_footer.html.erb +25 -0
- data/app/views/studio/modals/blocks/_card_header.html.erb +83 -0
- data/app/views/studio/modals/blocks/_cta_redirect.html.erb +73 -0
- data/app/views/studio/modals/blocks/_onchain_success.html.erb +52 -0
- data/app/views/studio/modals/blocks/_progress_pill.html.erb +23 -0
- data/app/views/studio/modals/blocks/_shell.html.erb +35 -0
- data/app/views/studio/modals/blocks/_solana_tx_link.html.erb +42 -0
- data/app/views/studio/modals/shared/_age_attestation.html.erb +34 -0
- data/app/views/studio/modals/shared/_email_field.html.erb +70 -0
- data/app/views/studio/modals/templates/_action.html.erb +25 -0
- data/app/views/studio/modals/templates/_form.html.erb +25 -0
- data/app/views/studio/modals/templates/_status.html.erb +45 -0
- data/app/views/studio/modals/templates/_success.html.erb +23 -0
- data/app/views/studio/modals/templates/_wizard.html.erb +75 -0
- data/app/views/style/_modal_specimen.html.erb +120 -0
- data/app/views/style/_modals.html.erb +690 -0
- data/app/views/style/_specimen.html.erb +64 -0
- data/app/views/style/_tasks.html.erb +112 -0
- data/app/views/style/_theme.html.erb +222 -0
- data/app/views/style/_tricks.html.erb +323 -0
- data/app/views/style/index.html.erb +60 -0
- data/app/views/style/modals/_auth.html.erb +252 -0
- data/app/views/style/modals/_onchain_tx.html.erb +62 -0
- data/app/views/style/modals/_wallet_connect.html.erb +138 -0
- data/app/views/style/modals/_wallet_deposit.html.erb +56 -0
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +27 -1
- metadata +32 -6
- data/app/controllers/design_system_controller.rb +0 -12
- data/app/views/design_system/_specimen.html.erb +0 -27
- data/app/views/design_system/index.html.erb +0 -232
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<%# Specimen card for the living style guide (admin/style).
|
|
2
|
+
Reusable chrome around ONE primitive, framed for copy-paste-for-an-agent:
|
|
3
|
+
stage - the rendered primitive (yielded from the caller block)
|
|
4
|
+
meta - its class name (prominent + one-click copy) and a copyable usage snippet
|
|
5
|
+
locals:
|
|
6
|
+
klass (String, required) - the class label, shown prominently with a
|
|
7
|
+
copy button so an agent can grab the exact class list.
|
|
8
|
+
usage (String, optional) - a copyable usage snippet.
|
|
9
|
+
disabled (Boolean, optional) - render "disabled but present": the
|
|
10
|
+
primitive stays visible + previewable but is muted/greyed
|
|
11
|
+
and flagged, for a capability the host app has turned off
|
|
12
|
+
(Studio.feature? is false). NOT hidden.
|
|
13
|
+
openable (Boolean, optional) - only meaningful with disabled. When
|
|
14
|
+
true, the greyed specimen KEEPS its click affordance so a
|
|
15
|
+
capability that is off on this app can still open its
|
|
16
|
+
modal as a preview (web3 modals). When false (default) the
|
|
17
|
+
disabled content is inert (pointer-events-none), for a
|
|
18
|
+
static specimen that is purely illustrative when off.
|
|
19
|
+
disabled_label (String, optional) - the flag text (default below).
|
|
20
|
+
Rendered via `render layout: "style/specimen"` so the caller's block becomes
|
|
21
|
+
the staged primitive. Checks local_assigns (never block_given?) so a shared
|
|
22
|
+
partial does not inherit the host layout yield. %>
|
|
23
|
+
<%
|
|
24
|
+
disabled = local_assigns[:disabled]
|
|
25
|
+
openable = local_assigns[:openable]
|
|
26
|
+
disabled_label = local_assigns[:disabled_label].presence || "disabled on this app"
|
|
27
|
+
# Openable disabled specimens stay clickable (web3 preview): drop the inertia
|
|
28
|
+
# utilities and lift the opacity a touch so the trigger reads as tappable.
|
|
29
|
+
inner_cls = openable ? "opacity-60 grayscale" : "opacity-40 grayscale select-none pointer-events-none"
|
|
30
|
+
%>
|
|
31
|
+
<article class="card overflow-hidden flex flex-col h-full<%= " opacity-95" if disabled %>" <%= 'aria-disabled="true"'.html_safe if disabled %>>
|
|
32
|
+
<div class="relative flex flex-1 flex-wrap items-center justify-center gap-4 p-6 bg-surface-alt min-h-32">
|
|
33
|
+
<% if disabled %>
|
|
34
|
+
<span class="absolute top-2 right-2 badge z-10" style="color: var(--color-warning); border-color: var(--color-warning)"><%= disabled_label %></span>
|
|
35
|
+
<div class="flex flex-wrap items-center justify-center gap-4 <%= inner_cls %>"><%= yield %></div>
|
|
36
|
+
<% else %>
|
|
37
|
+
<%= yield %>
|
|
38
|
+
<% end %>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="border-t border-subtle p-3 space-y-2">
|
|
41
|
+
<%# Class name — prominent, with a one-click copy so an agent can grab it. %>
|
|
42
|
+
<div class="flex items-start justify-between gap-2" x-data="{ copied: false }">
|
|
43
|
+
<code x-ref="klass" class="font-mono text-xs font-bold text-heading break-all leading-relaxed flex-1 min-w-0"><%= local_assigns[:klass] %></code>
|
|
44
|
+
<button type="button"
|
|
45
|
+
class="btn btn-neutral btn-sm shrink-0"
|
|
46
|
+
aria-label="Copy class name"
|
|
47
|
+
@click="navigator.clipboard?.writeText($refs.klass.innerText); copied = true; setTimeout(() => copied = false, 1200)">
|
|
48
|
+
<span x-text="copied ? 'Copied' : 'Copy'">Copy</span>
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
<% snippet = local_assigns[:usage] %>
|
|
52
|
+
<% if snippet.present? %>
|
|
53
|
+
<div class="relative" x-data="{ copied: false }">
|
|
54
|
+
<pre class="bg-inset text-body text-2xs rounded-lg px-3 py-2 pr-16 overflow-x-auto"><code x-ref="snippet"><%= snippet %></code></pre>
|
|
55
|
+
<button type="button"
|
|
56
|
+
class="btn btn-neutral btn-sm absolute top-2 right-2"
|
|
57
|
+
aria-label="Copy usage snippet"
|
|
58
|
+
@click="navigator.clipboard?.writeText($refs.snippet.innerText); copied = true; setTimeout(() => copied = false, 1200)">
|
|
59
|
+
<span x-text="copied ? 'Copied' : 'Copy'">Copy</span>
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
<% end %>
|
|
63
|
+
</div>
|
|
64
|
+
</article>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<%# Tasks section of the living style guide (admin/style).
|
|
2
|
+
|
|
3
|
+
A DEMONSTRATOR of the shared board primitive — the columns, cards, stage
|
|
4
|
+
badges, and drag/rank affordance that every board (McRitchie Studio's
|
|
5
|
+
/deployments is the top-of-line) builds from. It is a static sketch, not a
|
|
6
|
+
backend-wired sortable board; the point is to show the standard so custom
|
|
7
|
+
boards can adopt it. The stage-* badge palette (components/_badge) is the
|
|
8
|
+
spine. %>
|
|
9
|
+
<%
|
|
10
|
+
# The shared pipeline palette: each column maps to a stage-* badge scheme.
|
|
11
|
+
board_columns = [
|
|
12
|
+
{ key: "stage-fresh", label: "Fresh", count: 2 },
|
|
13
|
+
{ key: "stage-shaping", label: "Shaping", count: 1 },
|
|
14
|
+
{ key: "stage-structured", label: "Structured", count: 1 },
|
|
15
|
+
{ key: "stage-refined", label: "Refined", count: 1 },
|
|
16
|
+
{ key: "stage-shipped", label: "Shipped", count: 1 }
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
demo_cards = {
|
|
20
|
+
"stage-fresh" => [
|
|
21
|
+
{ title: "Expand design system page", repo: "studio-engine", who: "SH" },
|
|
22
|
+
{ title: "Coinflow buy rail", repo: "turf-monster", who: "JA" }
|
|
23
|
+
],
|
|
24
|
+
"stage-shaping" => [{ title: "Navbar RPC preload", repo: "turf-monster", who: "CA" }],
|
|
25
|
+
"stage-structured" => [{ title: "Magic-link TTL bump", repo: "studio-engine", who: "SH" }],
|
|
26
|
+
"stage-refined" => [{ title: "Release ticker card", repo: "mcritchie", who: "AV" }],
|
|
27
|
+
"stage-shipped" => [{ title: "SES email backend", repo: "mcritchie", who: "ST" }]
|
|
28
|
+
}
|
|
29
|
+
%>
|
|
30
|
+
<section id="tasks" class="space-y-8" style="scroll-margin-top: calc(var(--nav-h, 0px) + 5rem)">
|
|
31
|
+
<div class="space-y-1">
|
|
32
|
+
<h2 class="text-2xl font-bold text-heading">Tasks</h2>
|
|
33
|
+
<p class="text-muted text-sm max-w-2xl">
|
|
34
|
+
The shared board primitive that every board builds from — columns, cards, the
|
|
35
|
+
<code class="font-mono text-2xs">stage-*</code> badge palette, and the drag / rank affordance.
|
|
36
|
+
McRitchie Studio's <code class="font-mono text-2xs">/deployments</code> board is the top-of-line;
|
|
37
|
+
this is a static demonstrator (full extraction lands in a later phase).
|
|
38
|
+
</p>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<%# ---- The board ------------------------------------------------------- %>
|
|
42
|
+
<div class="fade-edge-x overflow-x-auto pb-2">
|
|
43
|
+
<div class="flex gap-4 min-w-max">
|
|
44
|
+
<% board_columns.each do |col| %>
|
|
45
|
+
<div class="w-56 flex-none" data-stage="<%= col[:key] %>">
|
|
46
|
+
<div class="mb-3 px-1 flex items-center gap-2">
|
|
47
|
+
<h3 class="text-sm font-bold text-heading uppercase tracking-wider"><%= col[:label] %></h3>
|
|
48
|
+
<%= render "components/badge", text: col[:count].to_s, scheme: col[:key] %>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="rounded-xl border-2 border-dashed border-subtle min-h-40 p-2 space-y-2">
|
|
51
|
+
<% (demo_cards[col[:key]] || []).each do |c| %>
|
|
52
|
+
<div class="card p-3 space-y-2 cursor-grab active:cursor-grabbing" draggable="true">
|
|
53
|
+
<div class="flex items-start gap-2">
|
|
54
|
+
<span class="text-muted select-none leading-none" aria-hidden="true">⠿</span>
|
|
55
|
+
<p class="text-sm font-semibold text-heading leading-snug flex-1"><%= c[:title] %></p>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="flex items-center justify-between gap-2">
|
|
58
|
+
<%= render "components/badge", text: c[:repo], scheme: "neutral" %>
|
|
59
|
+
<span class="inline-flex h-6 w-6 items-center justify-center rounded-full text-2xs font-bold text-white shrink-0" style="background: var(--color-cta)"><%= c[:who] %></span>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<% end %>
|
|
63
|
+
<div class="flex items-center justify-center h-16 text-muted text-xs">Drop tasks here</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<% end %>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<%# ---- The stage palette ---------------------------------------------- %>
|
|
71
|
+
<div class="space-y-3">
|
|
72
|
+
<p class="label-upper">Stage palette — the pipeline spine</p>
|
|
73
|
+
<div class="flex flex-wrap gap-2">
|
|
74
|
+
<% %w[stage-fresh stage-shaping stage-structured stage-refined stage-cohered stage-shipped stage-closed].each do |stage| %>
|
|
75
|
+
<span data-stage="<%= stage %>"><%= render "components/badge", text: stage.sub("stage-", ""), scheme: stage %></span>
|
|
76
|
+
<% end %>
|
|
77
|
+
</div>
|
|
78
|
+
<p class="text-muted text-xs">
|
|
79
|
+
Seven stage roles from <code class="font-mono text-2xs">components/_badge</code>. Every pipeline
|
|
80
|
+
(tasks, deployments, and any future board) draws its columns from this one palette, so a card
|
|
81
|
+
reads the same wherever it lives.
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<%# ---- Standard vs custom -------------------------------------------- %>
|
|
86
|
+
<div class="grid gap-4 sm:grid-cols-2">
|
|
87
|
+
<div class="card p-5 space-y-2">
|
|
88
|
+
<div class="flex items-center gap-2">
|
|
89
|
+
<span class="badge" style="color: var(--color-success); border-color: var(--color-success)">Shared</span>
|
|
90
|
+
<p class="text-heading font-semibold text-sm">What every board inherits</p>
|
|
91
|
+
</div>
|
|
92
|
+
<ul class="text-secondary text-sm space-y-1 list-disc list-inside">
|
|
93
|
+
<li>Column layout + horizontal scroll with faded edges</li>
|
|
94
|
+
<li>The <code class="font-mono text-2xs">stage-*</code> badge palette + count chips</li>
|
|
95
|
+
<li>Card chrome (<code class="font-mono text-2xs">.card</code>) and the drag / rank handle</li>
|
|
96
|
+
<li>Empty-column drop state</li>
|
|
97
|
+
</ul>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="card p-5 space-y-2">
|
|
100
|
+
<div class="flex items-center gap-2">
|
|
101
|
+
<span class="badge" style="color: var(--color-warning); border-color: var(--color-warning)">Custom</span>
|
|
102
|
+
<p class="text-heading font-semibold text-sm">What each board sets</p>
|
|
103
|
+
</div>
|
|
104
|
+
<ul class="text-secondary text-sm space-y-1 list-disc list-inside">
|
|
105
|
+
<li>Which stages become columns</li>
|
|
106
|
+
<li>The card body — fields, chips, avatars, live timers</li>
|
|
107
|
+
<li>Filters (agent, app) and header actions</li>
|
|
108
|
+
<li>Live-update wiring (Turbo Streams on the deploy board)</li>
|
|
109
|
+
</ul>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</section>
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
<%# Theme section of the living style guide (admin/style) — the LANDING section.
|
|
2
|
+
Theme owns "the colors": it leads with the 7 role tokens/swatches (moved here
|
|
3
|
+
from the Tricks section), then folds in the /admin/theme editor as a second
|
|
4
|
+
entry point and a live dark/light preview.
|
|
5
|
+
|
|
6
|
+
Save-in-place: unlike the standalone /admin/theme page (which redirects to
|
|
7
|
+
itself on save), THIS section intercepts Save + Regenerate and submits them
|
|
8
|
+
via fetch — the page never navigates away, the live preview stays put, and
|
|
9
|
+
the outcome is a toast. The standalone page's own redirect is unchanged.
|
|
10
|
+
|
|
11
|
+
Rendered in two contexts:
|
|
12
|
+
* live in the host app (StyleController sets @theme_setting + @theme_defaults)
|
|
13
|
+
so custom saved colors and the CSRF token are present.
|
|
14
|
+
* in the guard test as a bare template (no controller, no DB) so every
|
|
15
|
+
DB / request-scoped read is guarded and degrades to config defaults. %>
|
|
16
|
+
<%
|
|
17
|
+
# Reference the model directly (guarded): in the minimal dummy app it may not
|
|
18
|
+
# autoload, so fall back to the known role list + identity column mapping.
|
|
19
|
+
roles = begin
|
|
20
|
+
ThemeSetting::ROLES
|
|
21
|
+
rescue StandardError
|
|
22
|
+
%i[primary dark light success warning danger accent]
|
|
23
|
+
end
|
|
24
|
+
defaults = (Studio.respond_to?(:theme_config) ? Studio.theme_config : {}) || {}
|
|
25
|
+
setting = local_assigns[:theme_setting] || (@theme_setting if defined?(@theme_setting))
|
|
26
|
+
|
|
27
|
+
db_col_for = ->(role) {
|
|
28
|
+
begin
|
|
29
|
+
ThemeSetting.db_column_for(role)
|
|
30
|
+
rescue StandardError
|
|
31
|
+
role
|
|
32
|
+
end
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
current_hex = ->(role) {
|
|
36
|
+
db_val = setting ? setting.read_attribute(db_col_for.call(role)) : nil
|
|
37
|
+
(db_val.presence if db_val.respond_to?(:presence)) || defaults[role] || "#888888"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Seed the live-preview editor with role -> hex, resolved from saved values.
|
|
41
|
+
seed_colors = roles.index_with { |role| current_hex.call(role) }
|
|
42
|
+
|
|
43
|
+
# The 7 role tokens read live off the resolved theme (cta/text/border-strong
|
|
44
|
+
# are the semantic names — there is no --color-heading var).
|
|
45
|
+
token_swatches = [
|
|
46
|
+
["--color-cta", "CTA / primary"],
|
|
47
|
+
["--color-surface", "Surface"],
|
|
48
|
+
["--color-success", "Success"],
|
|
49
|
+
["--color-danger", "Danger"],
|
|
50
|
+
["--color-warning", "Warning"],
|
|
51
|
+
["--color-text", "Heading / text"],
|
|
52
|
+
["--color-border-strong", "Border (strong)"]
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
csrf_ok = respond_to?(:protect_against_forgery?) && protect_against_forgery?
|
|
56
|
+
%>
|
|
57
|
+
<section id="theme" class="space-y-8" style="scroll-margin-top: calc(var(--nav-h, 0px) + 5rem)"
|
|
58
|
+
x-data="dsThemeEditor(<%= seed_colors.to_json %>)">
|
|
59
|
+
<div class="flex flex-wrap items-start justify-between gap-3">
|
|
60
|
+
<div class="space-y-1">
|
|
61
|
+
<h2 class="text-2xl font-bold text-heading">Theme</h2>
|
|
62
|
+
<p class="text-muted text-sm max-w-2xl">
|
|
63
|
+
The color foundation. The 7-role system, editable here and at
|
|
64
|
+
<code class="font-mono text-2xs">/admin/theme</code>. Edits preview live; Save writes them
|
|
65
|
+
and Regenerate clears the theme cache. Both persist <strong>in place</strong> — the page never
|
|
66
|
+
leaves. Every specimen on this page restyles from these roles.
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
<form action="/admin/theme/regenerate" method="post" accept-charset="UTF-8" @submit.prevent="regenerate($event)">
|
|
70
|
+
<% if csrf_ok %>
|
|
71
|
+
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>" autocomplete="off">
|
|
72
|
+
<% end %>
|
|
73
|
+
<button type="submit" class="btn btn-outline btn-sm" :disabled="saving">Regenerate cache</button>
|
|
74
|
+
</form>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<%# ---- The colors — the role tokens/swatches (Theme owns these) ---------- %>
|
|
78
|
+
<div class="space-y-3">
|
|
79
|
+
<p class="label-upper">The colors — 7 role tokens</p>
|
|
80
|
+
<p class="text-muted text-sm max-w-2xl">
|
|
81
|
+
Read live off this app's theme via <code class="font-mono text-2xs">var(--color-*)</code>. Change
|
|
82
|
+
the app or its light/dark mode and every swatch (and every specimen on this page) follows.
|
|
83
|
+
</p>
|
|
84
|
+
<div class="grid gap-4 grid-cols-2 sm:grid-cols-3 lg:grid-cols-4">
|
|
85
|
+
<% token_swatches.each do |var, label| %>
|
|
86
|
+
<div class="card overflow-hidden">
|
|
87
|
+
<div class="h-16 border-b border-subtle" style="background: var(<%= var %>)"></div>
|
|
88
|
+
<div class="p-3 space-y-0.5">
|
|
89
|
+
<p class="text-sm text-heading font-medium"><%= label %></p>
|
|
90
|
+
<code class="font-mono text-2xs text-muted break-all"><%= var %></code>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<% end %>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div class="grid gap-6 lg:grid-cols-2">
|
|
98
|
+
<%# ---- Role-color editor (posts to /admin/theme, intercepted in place) - %>
|
|
99
|
+
<form action="/admin/theme" method="post" accept-charset="UTF-8" class="card p-6 space-y-4"
|
|
100
|
+
@submit.prevent="saveTheme($event)">
|
|
101
|
+
<input type="hidden" name="_method" value="patch" autocomplete="off">
|
|
102
|
+
<% if csrf_ok %>
|
|
103
|
+
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>" autocomplete="off">
|
|
104
|
+
<% end %>
|
|
105
|
+
<h3 class="text-lg font-bold text-heading">Role colors</h3>
|
|
106
|
+
|
|
107
|
+
<% roles.each do |role| %>
|
|
108
|
+
<% db_col = db_col_for.call(role) %>
|
|
109
|
+
<% has_custom = setting && setting.read_attribute(db_col).present? %>
|
|
110
|
+
<div class="flex items-center gap-4">
|
|
111
|
+
<input type="color"
|
|
112
|
+
:value="colors.<%= role %>"
|
|
113
|
+
@input="colors.<%= role %> = $event.target.value; updatePreview()"
|
|
114
|
+
class="w-10 h-10 rounded cursor-pointer border border-subtle shrink-0"
|
|
115
|
+
style="padding: 1px"
|
|
116
|
+
aria-label="<%= role.to_s.titleize %> color">
|
|
117
|
+
<div class="flex-1 min-w-0">
|
|
118
|
+
<div class="flex items-center gap-2">
|
|
119
|
+
<span class="text-heading font-medium text-sm"><%= role.to_s.titleize %></span>
|
|
120
|
+
<% if has_custom %><span class="text-xs text-muted">(custom)</span><% end %>
|
|
121
|
+
</div>
|
|
122
|
+
<input type="text"
|
|
123
|
+
name="theme_setting[<%= db_col %>]"
|
|
124
|
+
x-model="colors.<%= role %>"
|
|
125
|
+
@input="updatePreview()"
|
|
126
|
+
class="text-xs text-secondary font-mono bg-transparent border-0 border-b border-dashed border-subtle w-24 outline-none"
|
|
127
|
+
spellcheck="false">
|
|
128
|
+
</div>
|
|
129
|
+
<% if defaults[role] %>
|
|
130
|
+
<button type="button" class="text-xs text-muted hover:text-heading transition shrink-0"
|
|
131
|
+
@click="colors.<%= role %> = '<%= defaults[role] %>'; updatePreview()"
|
|
132
|
+
title="Reset to default (<%= defaults[role] %>)">Reset</button>
|
|
133
|
+
<% end %>
|
|
134
|
+
</div>
|
|
135
|
+
<% end %>
|
|
136
|
+
|
|
137
|
+
<div class="pt-4 border-t border-subtle">
|
|
138
|
+
<button type="submit" class="btn btn-primary" :disabled="saving"
|
|
139
|
+
x-text="saving ? 'Saving…' : 'Save theme'">Save theme</button>
|
|
140
|
+
</div>
|
|
141
|
+
</form>
|
|
142
|
+
|
|
143
|
+
<%# ---- Live dark/light preview (Alpine, updates as you edit) ----------- %>
|
|
144
|
+
<div class="space-y-3">
|
|
145
|
+
<p class="label-upper">Preview</p>
|
|
146
|
+
<p class="text-muted text-sm">A live dark/light mock — it tracks the editor as you type, before you save.</p>
|
|
147
|
+
<div class="grid gap-3 sm:grid-cols-2">
|
|
148
|
+
<div class="rounded-xl border border-subtle overflow-hidden">
|
|
149
|
+
<div class="px-3 py-1.5 bg-surface-alt"><span class="text-2xs font-bold uppercase tracking-wider text-secondary">Dark</span></div>
|
|
150
|
+
<div class="p-4" :style="{ backgroundColor: colors.dark }">
|
|
151
|
+
<p class="font-bold text-sm mb-2" style="color: #ffffff">Heading</p>
|
|
152
|
+
<div class="flex gap-2">
|
|
153
|
+
<button type="button" class="rounded-lg px-2.5 py-1 text-2xs font-bold text-white" :style="{ backgroundColor: colors.primary }">Primary</button>
|
|
154
|
+
<button type="button" class="rounded-lg px-2.5 py-1 text-2xs font-bold text-white" :style="{ backgroundColor: colors.success }">Accent</button>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="rounded-xl border border-subtle overflow-hidden">
|
|
159
|
+
<div class="px-3 py-1.5 bg-surface-alt"><span class="text-2xs font-bold uppercase tracking-wider text-secondary">Light</span></div>
|
|
160
|
+
<div class="p-4" :style="{ backgroundColor: colors.light }">
|
|
161
|
+
<p class="font-bold text-sm mb-2" style="color: #0f172a">Heading</p>
|
|
162
|
+
<div class="flex gap-2">
|
|
163
|
+
<button type="button" class="rounded-lg px-2.5 py-1 text-2xs font-bold text-white" :style="{ backgroundColor: colors.primary }">Primary</button>
|
|
164
|
+
<button type="button" class="rounded-lg px-2.5 py-1 text-2xs font-bold text-white" :style="{ backgroundColor: colors.success }">Accent</button>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<script>
|
|
173
|
+
// Live-preview editor for the Style-guide Theme section. Seeded from the
|
|
174
|
+
// server-resolved role colors; updates the themeable role vars on the
|
|
175
|
+
// document as you type so every specimen on the page restyles instantly.
|
|
176
|
+
// The <input name> fields still carry the real values.
|
|
177
|
+
//
|
|
178
|
+
// Save-in-place: Save + Regenerate submit via fetch and stay on THIS page —
|
|
179
|
+
// preventDefault stops the browser navigating to /admin/theme (which is what
|
|
180
|
+
// bounced the operator off this section), the live preview persists, and the
|
|
181
|
+
// outcome is a toast (the engine's shared _flash toast primitive). The
|
|
182
|
+
// <form> POST still degrades gracefully if Alpine never loads.
|
|
183
|
+
function dsThemeEditor(seed) {
|
|
184
|
+
return {
|
|
185
|
+
colors: Object.assign({ primary: '#000', dark: '#0f172a', light: '#ffffff',
|
|
186
|
+
success: '#4BAF50', warning: '#FF7C47', danger: '#EF4444',
|
|
187
|
+
accent: '#6366F1' }, seed || {}),
|
|
188
|
+
saving: false,
|
|
189
|
+
updatePreview() {
|
|
190
|
+
var d = document.documentElement.style;
|
|
191
|
+
d.setProperty('--color-cta', this.colors.primary);
|
|
192
|
+
if (this.colors.success) d.setProperty('--color-success', this.colors.success);
|
|
193
|
+
if (this.colors.warning) d.setProperty('--color-warning', this.colors.warning);
|
|
194
|
+
if (this.colors.danger) d.setProperty('--color-danger', this.colors.danger);
|
|
195
|
+
},
|
|
196
|
+
persist(form, label) {
|
|
197
|
+
if (this.saving) return;
|
|
198
|
+
this.saving = true;
|
|
199
|
+
// Send the form's REAL method (the editor carries _method=patch) so we
|
|
200
|
+
// never depend on Rack::MethodOverride parsing a multipart fetch body.
|
|
201
|
+
var data = new FormData(form);
|
|
202
|
+
var override = data.get('_method');
|
|
203
|
+
var method = override ? String(override).toUpperCase() : 'POST';
|
|
204
|
+
if (override) data.delete('_method');
|
|
205
|
+
fetch(form.action, { method: method, body: data })
|
|
206
|
+
.then(function (res) {
|
|
207
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
208
|
+
return res;
|
|
209
|
+
})
|
|
210
|
+
.then((function () { this.toast('notice', label, 'Saved in place — the page stayed put.'); }).bind(this))
|
|
211
|
+
.catch((function (err) { this.toast('alert', 'Save failed', String((err && err.message) || err)); }).bind(this))
|
|
212
|
+
.finally((function () { this.saving = false; }).bind(this));
|
|
213
|
+
},
|
|
214
|
+
toast(type, title, message) {
|
|
215
|
+
window.dispatchEvent(new CustomEvent('toast', { detail: { type: type, title: title, message: message } }));
|
|
216
|
+
},
|
|
217
|
+
saveTheme(e) { this.persist(e.target, 'Theme saved'); },
|
|
218
|
+
regenerate(e) { this.persist(e.target, 'Theme cache cleared'); }
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
</script>
|
|
222
|
+
</section>
|