studio-engine 0.17.0 → 0.19.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 +101 -0
- data/app/assets/tailwind/studio_engine/engine-motion.css +620 -5
- data/app/controllers/concerns/studio/magic_link_issuing.rb +39 -0
- data/app/controllers/magic_links_controller.rb +5 -14
- data/app/controllers/studio/local_emails_controller.rb +1 -1
- data/app/controllers/studio/local_reviews_controller.rb +50 -0
- data/app/controllers/style_controller.rb +25 -0
- data/app/mailers/user_mailer.rb +6 -9
- 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 +43 -1
- metadata +29 -4
- 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,70 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Email input for the modal family — a themed <input type="email"> that either
|
|
3
|
+
posts through its own `name` or feeds an enclosing x-model. Ported from
|
|
4
|
+
turf-monster; the live client-side validity decorator (window.emailValidator)
|
|
5
|
+
is intentionally NOT a dependency here so the engine partial is self-contained
|
|
6
|
+
— the server stays the real boundary. A consumer that ships emailValidator can
|
|
7
|
+
pass validator: true to opt back into the right-edge spinner / check / X.
|
|
8
|
+
|
|
9
|
+
Locals (all optional, sensible defaults via local_assigns.fetch):
|
|
10
|
+
name — input name attr (default "email"; pass nil/false to omit,
|
|
11
|
+
e.g. a modal that dispatches its x-model value instead)
|
|
12
|
+
id — input id attr (default: the name)
|
|
13
|
+
value — prefill value (default "")
|
|
14
|
+
placeholder — (default "you@example.com")
|
|
15
|
+
required — (default false)
|
|
16
|
+
autocomplete — (default "email")
|
|
17
|
+
autofocus — (default false)
|
|
18
|
+
input_class — base input classes (default "input-field")
|
|
19
|
+
x_model — Alpine x-model target resolved in an ENCLOSING scope
|
|
20
|
+
x_ref — Alpine x-ref name
|
|
21
|
+
disabled_expr — Alpine :disabled expression (e.g. "props.submitting")
|
|
22
|
+
validator — when true, wraps in x-data="emailValidator()" and paints the
|
|
23
|
+
right-edge validity indicator (consumer must ship the factory)
|
|
24
|
+
%>
|
|
25
|
+
<%
|
|
26
|
+
name = local_assigns.fetch(:name, "email")
|
|
27
|
+
field_id = local_assigns.fetch(:id, name)
|
|
28
|
+
field_value = local_assigns.fetch(:value, "")
|
|
29
|
+
placeholder = local_assigns.fetch(:placeholder, "you@example.com")
|
|
30
|
+
required = local_assigns.fetch(:required, false)
|
|
31
|
+
autocomplete = local_assigns.fetch(:autocomplete, "email")
|
|
32
|
+
autofocus = local_assigns.fetch(:autofocus, false)
|
|
33
|
+
input_class = local_assigns.fetch(:input_class, "input-field")
|
|
34
|
+
x_model = local_assigns.fetch(:x_model, nil)
|
|
35
|
+
x_ref = local_assigns.fetch(:x_ref, nil)
|
|
36
|
+
disabled_expr = local_assigns.fetch(:disabled_expr, nil)
|
|
37
|
+
validator = local_assigns.fetch(:validator, false)
|
|
38
|
+
%>
|
|
39
|
+
<div class="relative"<%= ' x-data="emailValidator()"'.html_safe if validator %>>
|
|
40
|
+
<input type="email" inputmode="email"
|
|
41
|
+
class="<%= input_class %><%= ' pr-10' if validator %>"
|
|
42
|
+
<% if validator %>:class="{ 'email-reject': _rejecting }"<% end %>
|
|
43
|
+
placeholder="<%= placeholder %>"
|
|
44
|
+
autocomplete="<%= autocomplete %>"
|
|
45
|
+
<%= "required" if required %>
|
|
46
|
+
<%= "autofocus" if autofocus %>
|
|
47
|
+
<% if name %>name="<%= name %>"<% end %>
|
|
48
|
+
<% if field_id %>id="<%= field_id %>"<% end %>
|
|
49
|
+
value="<%= field_value %>"
|
|
50
|
+
<% if x_model %>x-model="<%= x_model %>"<% end %>
|
|
51
|
+
<% if x_ref %>x-ref="<%= x_ref %>"<% end %>
|
|
52
|
+
<% if disabled_expr %>:disabled="<%= disabled_expr %>"<% end %>
|
|
53
|
+
<% if validator %>@input="onInput($event)" @change="onInput($event)" @blur="onBlur($event)"<% end %>>
|
|
54
|
+
|
|
55
|
+
<% if validator %>
|
|
56
|
+
<%# Right-edge validity indicator — consumer's emailValidator drives status.
|
|
57
|
+
pointer-events-none so it never eats a click on the input behind it. %>
|
|
58
|
+
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
|
|
59
|
+
<span x-show="status === 'loading'" x-cloak class="spinner text-secondary" aria-hidden="true"></span>
|
|
60
|
+
<svg x-show="status === 'valid'" x-cloak xmlns="http://www.w3.org/2000/svg" class="w-5 h-5"
|
|
61
|
+
style="color: var(--color-success)" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
|
|
62
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z" />
|
|
63
|
+
</svg>
|
|
64
|
+
<svg x-show="status === 'invalid'" x-cloak xmlns="http://www.w3.org/2000/svg" class="w-5 h-5"
|
|
65
|
+
style="color: var(--color-danger)" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
|
|
66
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z" />
|
|
67
|
+
</svg>
|
|
68
|
+
</div>
|
|
69
|
+
<% end %>
|
|
70
|
+
</div>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<%# === Template: ACTION modal ======================================
|
|
2
|
+
One-question dialog: centered icon pill + title + subtitle + primary /
|
|
3
|
+
secondary CTAs. Use for confirmations, alerts, destructive prompts.
|
|
4
|
+
Composes card_header for the top chunk. Ported from turf-monster.
|
|
5
|
+
|
|
6
|
+
Variants: pass icon :error / :external_tab / icon_emoji / spinner OR
|
|
7
|
+
icon_color :primary / :success / :warning to card_header.
|
|
8
|
+
|
|
9
|
+
Local: modal_store — Alpine store name backing close(). Default "modals". %>
|
|
10
|
+
<% modal_store = local_assigns.fetch(:modal_store, "modals") %>
|
|
11
|
+
<div x-data>
|
|
12
|
+
<%= render "studio/modals/blocks/card_header",
|
|
13
|
+
size: :lg,
|
|
14
|
+
icon: :error,
|
|
15
|
+
title: "Action Template",
|
|
16
|
+
subtitle: "A centered confirmation card with a primary + secondary CTA. Best for one-question dialogs and destructive prompts." %>
|
|
17
|
+
|
|
18
|
+
<button @click="$store.<%= modal_store %>.close()" class="btn btn-danger btn-lg w-full">
|
|
19
|
+
Confirm Action
|
|
20
|
+
</button>
|
|
21
|
+
<button @click="$store.<%= modal_store %>.close()"
|
|
22
|
+
class="block mx-auto mt-3 text-sm text-secondary hover:text-heading transition">
|
|
23
|
+
Cancel
|
|
24
|
+
</button>
|
|
25
|
+
</div>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<%# === Template: FORM modal ======================================
|
|
2
|
+
Starting point for any modal that captures input — settings, edit forms,
|
|
3
|
+
rename dialogs. Wraps content in the shared shell for a consistent title row
|
|
4
|
+
+ close ×. Ported from turf-monster.
|
|
5
|
+
|
|
6
|
+
Local: modal_store — Alpine store name backing close(). Default "modals". %>
|
|
7
|
+
<% modal_store = local_assigns.fetch(:modal_store, "modals") %>
|
|
8
|
+
<div x-data="{ value: '' }">
|
|
9
|
+
<%= render layout: "studio/modals/blocks/shell", locals: { title: "Form Template", modal_store: modal_store } do %>
|
|
10
|
+
<p class="text-sm text-muted mb-4">
|
|
11
|
+
Title row + close × come from <code class="text-xs bg-inset px-1 rounded">studio/modals/blocks/shell</code>.
|
|
12
|
+
Slot in whatever inputs + primary CTA your form needs.
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<form @submit.prevent="$store.<%= modal_store %>.close()" class="space-y-4">
|
|
16
|
+
<textarea x-model="value"
|
|
17
|
+
rows="3"
|
|
18
|
+
placeholder="Type something…"
|
|
19
|
+
class="input-field w-full resize-none"></textarea>
|
|
20
|
+
<button type="submit" class="btn btn-primary btn-lg w-full" :disabled="!value.trim()">
|
|
21
|
+
Save
|
|
22
|
+
</button>
|
|
23
|
+
</form>
|
|
24
|
+
<% end %>
|
|
25
|
+
</div>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<%# === Template: STATUS modal ======================================
|
|
2
|
+
Small-title in-flight card for short async work — server save, poll,
|
|
3
|
+
side-effect that finishes in a second or two. card_header default :md keeps
|
|
4
|
+
the title subordinate to the spinner. For end-state celebrations use the
|
|
5
|
+
success template. Ported from turf-monster; the demo cycles loading → error
|
|
6
|
+
at 1.6s so a viewer can step through both states.
|
|
7
|
+
|
|
8
|
+
Local: modal_store — Alpine store name backing close(). Default "modals". %>
|
|
9
|
+
<% modal_store = local_assigns.fetch(:modal_store, "modals") %>
|
|
10
|
+
<div x-data="{
|
|
11
|
+
state: 'loading',
|
|
12
|
+
start() {
|
|
13
|
+
this.state = 'loading';
|
|
14
|
+
var self = this;
|
|
15
|
+
setTimeout(function() { self.state = 'error'; }, 1600);
|
|
16
|
+
}
|
|
17
|
+
}"
|
|
18
|
+
x-init="start()">
|
|
19
|
+
<template x-if="state === 'loading'">
|
|
20
|
+
<div>
|
|
21
|
+
<%= render "studio/modals/blocks/card_header",
|
|
22
|
+
spinner: true,
|
|
23
|
+
title: "Working on it…",
|
|
24
|
+
subtitle: "Status template — small-title in-flight card for short async work." %>
|
|
25
|
+
<button @click="$store.<%= modal_store %>.close()"
|
|
26
|
+
class="block mx-auto mt-3 text-sm text-secondary hover:text-heading transition">
|
|
27
|
+
Cancel
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<template x-if="state === 'error'">
|
|
33
|
+
<div>
|
|
34
|
+
<%= render "studio/modals/blocks/card_header",
|
|
35
|
+
icon: :error,
|
|
36
|
+
title: "Something went wrong",
|
|
37
|
+
subtitle: "Surface the real error message here. Try Again should re-run the failed op." %>
|
|
38
|
+
<button @click="start()" class="btn btn-primary btn-lg w-full">Try Again</button>
|
|
39
|
+
<button @click="$store.<%= modal_store %>.close()"
|
|
40
|
+
class="block mx-auto mt-3 text-sm text-secondary hover:text-heading transition">
|
|
41
|
+
Cancel
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
</template>
|
|
45
|
+
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<%# === Template: SUCCESS modal ======================================
|
|
2
|
+
Large-title celebration for end states — action completed, item created.
|
|
3
|
+
card_header at :lg gives a text-3xl title; the primary CTA pulls the user
|
|
4
|
+
forward. For a success that warrants a Solana tx link / auto-redirect drain,
|
|
5
|
+
use studio/modals/blocks/_onchain_success instead. Ported from turf-monster.
|
|
6
|
+
|
|
7
|
+
Local: modal_store — Alpine store name backing close(). Default "modals". %>
|
|
8
|
+
<% modal_store = local_assigns.fetch(:modal_store, "modals") %>
|
|
9
|
+
<div x-data>
|
|
10
|
+
<%= render "studio/modals/blocks/card_header",
|
|
11
|
+
size: :lg,
|
|
12
|
+
icon_color: 'primary',
|
|
13
|
+
title: "Done!",
|
|
14
|
+
subtitle: "Operation completed. Swap in the real success copy + CTA destination." %>
|
|
15
|
+
|
|
16
|
+
<button @click="$store.<%= modal_store %>.close()" class="btn btn-primary btn-lg w-full">
|
|
17
|
+
Continue
|
|
18
|
+
</button>
|
|
19
|
+
<button @click="$store.<%= modal_store %>.close()"
|
|
20
|
+
class="block mx-auto mt-3 text-sm text-secondary hover:text-heading transition">
|
|
21
|
+
Dismiss
|
|
22
|
+
</button>
|
|
23
|
+
</div>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<%# === Template: WIZARD modal ======================================
|
|
2
|
+
Multi-step flow with a segmented progress pill + centered header. Use for
|
|
3
|
+
onboarding, checkout journeys, anything that breaks into 2-4 screens. Ported
|
|
4
|
+
from turf-monster.
|
|
5
|
+
|
|
6
|
+
Visual identity (distinct from form/action/status): centered title above the
|
|
7
|
+
pill; N-segment pill (filled = primary, upcoming = inset+subtle); single
|
|
8
|
+
primary CTA pulling forward; Back as a subtle link, hidden on step 1.
|
|
9
|
+
|
|
10
|
+
Local: modal_store — Alpine store name backing close(). Default "modals". %>
|
|
11
|
+
<% modal_store = local_assigns.fetch(:modal_store, "modals") %>
|
|
12
|
+
<div x-data="{ step: 1, total: 3 }">
|
|
13
|
+
<div class="relative mb-3 -mt-2">
|
|
14
|
+
<h3 class="text-heading font-bold text-lg leading-tight text-center pt-1">Wizard Template</h3>
|
|
15
|
+
<button type="button" @click="$store.<%= modal_store %>.close()"
|
|
16
|
+
class="absolute top-0 right-0 -mr-2 text-secondary hover:text-heading text-xl leading-none"
|
|
17
|
+
aria-label="Close">×</button>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<%# Inline x-for pill — the demo's step counter lives in local x-data, so the
|
|
21
|
+
static-ERB progress_pill block doesn't fit. Real wizards driven by props
|
|
22
|
+
can render studio/modals/blocks/progress_pill directly. %>
|
|
23
|
+
<div class="flex items-center justify-center gap-1.5 mx-auto max-w-[200px] mb-6">
|
|
24
|
+
<template x-for="i in total" :key="i">
|
|
25
|
+
<div class="h-1.5 flex-1 rounded-full transition-colors duration-300"
|
|
26
|
+
:class="step >= i ? 'bg-primary' : 'bg-inset border border-subtle'"></div>
|
|
27
|
+
</template>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<template x-if="step === 1">
|
|
31
|
+
<div class="text-center space-y-3 min-h-[120px]">
|
|
32
|
+
<h4 class="text-heading font-semibold text-base">Welcome</h4>
|
|
33
|
+
<p class="text-sm text-secondary">
|
|
34
|
+
First step — collect any prerequisites or intro context here. Replace this
|
|
35
|
+
block with whatever your wizard's opening screen needs.
|
|
36
|
+
</p>
|
|
37
|
+
</div>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<template x-if="step === 2">
|
|
41
|
+
<div class="text-center space-y-3 min-h-[120px]">
|
|
42
|
+
<h4 class="text-heading font-semibold text-base">Configure</h4>
|
|
43
|
+
<p class="text-sm text-secondary">
|
|
44
|
+
Middle step(s) — forms, selections, anything that needs user input. Each step
|
|
45
|
+
gets its own <code class="text-xs bg-inset px-1 rounded"><template x-if></code> block.
|
|
46
|
+
</p>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<template x-if="step === 3">
|
|
51
|
+
<div class="text-center space-y-3 min-h-[120px]">
|
|
52
|
+
<h4 class="text-heading font-semibold text-base">Confirm</h4>
|
|
53
|
+
<p class="text-sm text-secondary">
|
|
54
|
+
Final step — show the summary + primary commit action. The Finish button
|
|
55
|
+
below typically calls your real submit handler then closes the modal.
|
|
56
|
+
</p>
|
|
57
|
+
</div>
|
|
58
|
+
</template>
|
|
59
|
+
|
|
60
|
+
<button type="button"
|
|
61
|
+
@click="step < total ? step++ : $store.<%= modal_store %>.close()"
|
|
62
|
+
class="btn btn-primary btn-lg w-full mt-2"
|
|
63
|
+
x-text="step === total ? 'Finish' : 'Next'"></button>
|
|
64
|
+
<button type="button"
|
|
65
|
+
x-show="step > 1"
|
|
66
|
+
@click="step--"
|
|
67
|
+
class="block mx-auto mt-3 text-sm text-secondary hover:text-heading transition">
|
|
68
|
+
← Back
|
|
69
|
+
</button>
|
|
70
|
+
<button type="button"
|
|
71
|
+
@click="$store.<%= modal_store %>.close()"
|
|
72
|
+
class="block mx-auto mt-2 text-xs text-muted hover:text-heading transition">
|
|
73
|
+
Close
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<%# Modal specimen card for the living style guide (admin/style) Modals section.
|
|
2
|
+
|
|
3
|
+
Unlike the generic style/_specimen (Tricks), a modal specimen is INTERACTIVE:
|
|
4
|
+
|
|
5
|
+
- the WHOLE card is the trigger (role=button, Enter/Space) — click it to
|
|
6
|
+
open the real modal with the current toggle config (item 3).
|
|
7
|
+
- the header carries a small Copy that yields a CONVERSATIONAL reference
|
|
8
|
+
an agent can act on (not a raw JS blob) — the operator references this
|
|
9
|
+
page when chatting (item 1).
|
|
10
|
+
- an optional row of option toggles lets the user configure the modal
|
|
11
|
+
before opening; their clicks/keys do NOT bubble to the card open (item 2).
|
|
12
|
+
- an optional active-card GLOW (.studio-team-glow) that reactively lights
|
|
13
|
+
when this card represents the currently-open modal + step, and slides to
|
|
14
|
+
the next card as the step machine advances (items 5/6). The glow rides an
|
|
15
|
+
un-clipped wrapper (the card itself is overflow-hidden), mirroring TM's
|
|
16
|
+
holo-wrap / holo-card split, and fades via --studio-team-glow-opacity so
|
|
17
|
+
it transitions smoothly between cards.
|
|
18
|
+
|
|
19
|
+
Locals:
|
|
20
|
+
label (String, required) - the specimen name (header, uppercase).
|
|
21
|
+
reference (String, required) - the conversational copy text.
|
|
22
|
+
open_expr (String, required) - Alpine expression run to open the modal.
|
|
23
|
+
May read `opts.*` from card_data (the toggle state).
|
|
24
|
+
card_data (String, optional) - INNER x-data (no braces), merged after
|
|
25
|
+
copiedRef. E.g. "opts: { magicLink: true, google: true }".
|
|
26
|
+
glow_when (String, optional) - Alpine boolean expr; truthy => active glow.
|
|
27
|
+
toggles (Array<Hash{model:,label:}>, optional) - option checkboxes bound
|
|
28
|
+
into the card x-data (e.g. { model: 'opts.google', label: 'Google' }).
|
|
29
|
+
disabled (Boolean, optional) - capability off: greyed + badged.
|
|
30
|
+
openable (Boolean, optional) - with disabled, keep it clickable (preview).
|
|
31
|
+
disabled_label (String, optional) - the flag text.
|
|
32
|
+
Block: the mini-mock stage (a not-to-scale visual representation). %>
|
|
33
|
+
<%
|
|
34
|
+
label = local_assigns.fetch(:label)
|
|
35
|
+
# Developer-authored reference sentence, rendered as hidden text content (the
|
|
36
|
+
# Copy button reads textContent). html_safe keeps its quotes literal in the
|
|
37
|
+
# markup; it holds no HTML-special chars.
|
|
38
|
+
reference = local_assigns.fetch(:reference).to_s.html_safe
|
|
39
|
+
# open_expr / glow_when are developer-authored Alpine expressions (not user
|
|
40
|
+
# input); mark html_safe so their single quotes render literally into the
|
|
41
|
+
# attribute rather than as ' (cleaner HTML; the browser decodes either way).
|
|
42
|
+
open_expr = local_assigns.fetch(:open_expr).to_s.html_safe
|
|
43
|
+
card_data = local_assigns.fetch(:card_data, "").to_s.strip
|
|
44
|
+
glow_when = local_assigns[:glow_when]&.to_s&.html_safe
|
|
45
|
+
toggles = local_assigns[:toggles] || []
|
|
46
|
+
disabled = local_assigns[:disabled]
|
|
47
|
+
openable = local_assigns[:openable]
|
|
48
|
+
disabled_label = local_assigns[:disabled_label].presence || "disabled on this app"
|
|
49
|
+
clickable = !(disabled && !openable)
|
|
50
|
+
inner_cls = openable ? "opacity-60 grayscale" : "opacity-40 grayscale select-none pointer-events-none"
|
|
51
|
+
card_x_data = "{ copiedRef: false#{card_data.present? ? ", #{card_data}" : ''} }"
|
|
52
|
+
%>
|
|
53
|
+
<%# Wrapper — carries the active-card glow (un-clipped) + the shared x-data. %>
|
|
54
|
+
<%# h-full so the card stretches to its grid row's tallest (per-row equal
|
|
55
|
+
height via the grid's default align-items: stretch). %>
|
|
56
|
+
<div class="relative h-full<%= " studio-team-glow rounded-xl" if glow_when %>"
|
|
57
|
+
x-data="<%= card_x_data %>"
|
|
58
|
+
<% if glow_when %>:style="{ '--studio-team-glow-opacity': (<%= glow_when %>) ? '0.95' : '0' }"<% end %>>
|
|
59
|
+
<article
|
|
60
|
+
class="card overflow-hidden flex flex-col h-full<%= " opacity-95" if disabled %><%= " cursor-pointer" if clickable %>"
|
|
61
|
+
<%= 'aria-disabled="true"'.html_safe if disabled %>
|
|
62
|
+
<% if clickable %>
|
|
63
|
+
role="button" tabindex="0"
|
|
64
|
+
@click="<%= open_expr %>"
|
|
65
|
+
@keydown.enter.prevent="<%= open_expr %>"
|
|
66
|
+
@keydown.space.prevent="<%= open_expr %>"
|
|
67
|
+
aria-label="Open the <%= label %> modal"
|
|
68
|
+
<% end %>>
|
|
69
|
+
|
|
70
|
+
<%# Header — the centered TITLE is itself the copy control (a button + a small
|
|
71
|
+
clipboard icon that flips to a check). @click.stop + @keydown.stop keep the
|
|
72
|
+
copy from bubbling to the card's open handler: clicking the title copies
|
|
73
|
+
and does NOT open; clicking anywhere else on the card still opens. The
|
|
74
|
+
button activates on Enter/Space natively (firing @click.stop = copy). %>
|
|
75
|
+
<div class="flex items-center justify-center px-3 pt-3">
|
|
76
|
+
<button type="button"
|
|
77
|
+
class="inline-flex items-center gap-1.5 text-2xs font-semibold uppercase tracking-wider text-muted hover:text-heading transition"
|
|
78
|
+
title="Copy an agent-ready reference to this modal"
|
|
79
|
+
aria-label="Copy an agent-ready reference to this modal"
|
|
80
|
+
@click.stop="navigator.clipboard?.writeText($refs.ref.textContent.trim()); copiedRef = true; setTimeout(() => copiedRef = false, 1400)"
|
|
81
|
+
@keydown.stop>
|
|
82
|
+
<span><%= label %></span>
|
|
83
|
+
<svg x-show="!copiedRef" class="w-3 h-3 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24" aria-hidden="true">
|
|
84
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
|
|
85
|
+
</svg>
|
|
86
|
+
<svg x-show="copiedRef" x-cloak class="w-3 h-3 shrink-0" style="color: var(--color-success)" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24" aria-hidden="true">
|
|
87
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
|
|
88
|
+
</svg>
|
|
89
|
+
<span class="sr-only" x-text="copiedRef ? 'Copied' : ''"></span>
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
<span x-ref="ref" class="hidden"><%= reference %></span>
|
|
93
|
+
|
|
94
|
+
<%# Stage — the not-to-scale mini-mock. %>
|
|
95
|
+
<div class="relative flex flex-1 flex-wrap items-center justify-center gap-4 p-6 min-h-32">
|
|
96
|
+
<% if disabled %>
|
|
97
|
+
<span class="absolute top-2 right-2 badge z-10" style="color: var(--color-warning); border-color: var(--color-warning)"><%= disabled_label %></span>
|
|
98
|
+
<div class="flex flex-wrap items-center justify-center gap-4 <%= inner_cls %>"><%= yield %></div>
|
|
99
|
+
<% else %>
|
|
100
|
+
<%= yield %>
|
|
101
|
+
<% end %>
|
|
102
|
+
<% if clickable %>
|
|
103
|
+
<span class="pointer-events-none absolute bottom-2 right-2 text-2xs text-secondary opacity-70">Open ↗</span>
|
|
104
|
+
<% end %>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<%# Toggles — configure the modal before opening (clicks/keys stay local). %>
|
|
108
|
+
<% if toggles.any? %>
|
|
109
|
+
<div class="border-t border-subtle px-3 py-2.5 flex flex-wrap gap-x-4 gap-y-1.5"
|
|
110
|
+
@click.stop @keydown.stop>
|
|
111
|
+
<% toggles.each do |tg| %>
|
|
112
|
+
<label class="flex items-center gap-1.5 text-2xs text-secondary cursor-pointer select-none">
|
|
113
|
+
<input type="checkbox" x-model="<%= tg[:model] %>" class="w-3.5 h-3.5 rounded accent-primary cursor-pointer">
|
|
114
|
+
<span><%= tg[:label] %></span>
|
|
115
|
+
</label>
|
|
116
|
+
<% end %>
|
|
117
|
+
</div>
|
|
118
|
+
<% end %>
|
|
119
|
+
</article>
|
|
120
|
+
</div>
|