plutonium 0.51.0 → 0.52.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/.claude/skills/plutonium-app/SKILL.md +2 -0
- data/.claude/skills/plutonium-auth/SKILL.md +6 -4
- data/.claude/skills/plutonium-behavior/SKILL.md +1 -1
- data/.claude/skills/plutonium-tenancy/SKILL.md +25 -6
- data/.claude/skills/plutonium-testing/SKILL.md +3 -1
- data/.claude/skills/plutonium-ui/SKILL.md +3 -3
- data/CHANGELOG.md +17 -0
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +1 -0
- data/app/assets/plutonium.js.map +3 -3
- data/app/assets/plutonium.min.js +1 -1
- data/app/assets/plutonium.min.js.map +3 -3
- data/docs/.vitepress/config.ts +1 -2
- data/docs/.vitepress/theme/components/HomeAudienceSplit.vue +53 -0
- data/docs/.vitepress/theme/components/HomeCta.vue +108 -0
- data/docs/.vitepress/theme/components/HomeHero.vue +70 -0
- data/docs/.vitepress/theme/components/HomeInTheBox.vue +74 -0
- data/docs/.vitepress/theme/components/HomePillars.vue +42 -0
- data/docs/.vitepress/theme/components/HomeStopWriting.vue +49 -0
- data/docs/.vitepress/theme/components/HomeWalkthrough.vue +111 -0
- data/docs/.vitepress/theme/components/SectionLanding.vue +115 -0
- data/docs/.vitepress/theme/custom.css +144 -0
- data/docs/.vitepress/theme/index.ts +58 -1
- data/docs/getting-started/index.md +33 -50
- data/docs/getting-started/tutorial/02-first-resource.md +17 -8
- data/docs/getting-started/tutorial/03-authentication.md +31 -23
- data/docs/getting-started/tutorial/05-custom-actions.md +9 -4
- data/docs/getting-started/tutorial/06-nested-resources.md +7 -1
- data/docs/getting-started/tutorial/07-author-portal.md +8 -0
- data/docs/getting-started/tutorial/08-customizing-ui.md +4 -0
- data/docs/guides/authentication.md +10 -5
- data/docs/guides/authorization.md +3 -3
- data/docs/guides/creating-packages.md +8 -11
- data/docs/guides/custom-actions.md +6 -1
- data/docs/guides/customizing-ui.md +258 -0
- data/docs/guides/index.md +49 -32
- data/docs/guides/multi-tenancy.md +10 -2
- data/docs/guides/nested-resources.md +69 -0
- data/docs/guides/search-filtering.md +6 -0
- data/docs/guides/testing.md +5 -1
- data/docs/guides/theming.md +13 -0
- data/docs/guides/user-invites.md +10 -4
- data/docs/guides/user-profile.md +8 -0
- data/docs/index.md +10 -219
- data/docs/public/asciinema/home-scaffold.cast +305 -0
- data/docs/public/images/guides/custom-actions-bulk.png +0 -0
- data/docs/public/images/guides/multi-tenancy-dashboard.png +0 -0
- data/docs/public/images/guides/multi-tenancy-welcome.png +0 -0
- data/docs/public/images/guides/nested-inputs.png +0 -0
- data/docs/public/images/guides/nested-resources-tab.png +0 -0
- data/docs/public/images/guides/search-filtering-index.png +0 -0
- data/docs/public/images/guides/search-filtering-panel.png +0 -0
- data/docs/public/images/guides/theming-after.png +0 -0
- data/docs/public/images/guides/theming-before.png +0 -0
- data/docs/public/images/guides/user-invites-landing.png +0 -0
- data/docs/public/images/guides/user-profile-edit.png +0 -0
- data/docs/public/images/guides/user-profile-show.png +0 -0
- data/docs/public/images/home-index.png +0 -0
- data/docs/public/images/home-new.png +0 -0
- data/docs/public/images/home-show.png +0 -0
- data/docs/public/images/tutorial/02-empty-index.png +0 -0
- data/docs/public/images/tutorial/02-index-with-posts.png +0 -0
- data/docs/public/images/tutorial/02-new-form-modal.png +0 -0
- data/docs/public/images/tutorial/02-new-form.png +0 -0
- data/docs/public/images/tutorial/03-create-account.png +0 -0
- data/docs/public/images/tutorial/03-login.png +0 -0
- data/docs/public/images/tutorial/04-admin-index.png +0 -0
- data/docs/public/images/tutorial/05-actions-menu.png +0 -0
- data/docs/public/images/tutorial/05-row-actions.png +0 -0
- data/docs/public/images/tutorial/06-comments-tab.png +0 -0
- data/docs/public/images/tutorial/06-post-with-comments.png +0 -0
- data/docs/public/images/tutorial/07-author-dashboard.png +0 -0
- data/docs/public/images/tutorial/07-author-portal.png +0 -0
- data/docs/public/images/tutorial/08-customized-index.png +0 -0
- data/docs/reference/app/generators.md +4 -4
- data/docs/reference/auth/accounts.md +6 -7
- data/docs/reference/auth/index.md +1 -1
- data/docs/reference/behavior/policies.md +1 -1
- data/docs/reference/index.md +67 -55
- data/docs/reference/resource/definition.md +1 -1
- data/docs/reference/tenancy/entity-scoping.md +8 -1
- data/docs/reference/tenancy/index.md +1 -1
- data/docs/reference/tenancy/invites.md +12 -5
- data/docs/reference/ui/tables.md +8 -4
- data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md +1648 -0
- data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md.tasks.json +109 -0
- data/docs/superpowers/specs/2026-05-15-public-pages-overhaul-design.md +263 -0
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/core/assets/assets_generator.rb +10 -0
- data/lib/generators/pu/invites/install_generator.rb +44 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +1 -0
- data/lib/generators/pu/profile/conn_generator.rb +2 -2
- data/lib/generators/pu/res/conn/conn_generator.rb +33 -6
- data/lib/generators/pu/res/model/templates/model.rb.tt +4 -0
- data/lib/generators/pu/rodauth/account_generator.rb +2 -1
- data/lib/generators/pu/rodauth/admin_generator.rb +0 -2
- data/lib/generators/pu/rodauth/migration_generator.rb +0 -2
- data/lib/generators/pu/rodauth/views_generator.rb +0 -2
- data/lib/generators/pu/saas/membership/USAGE +4 -1
- data/lib/generators/pu/saas/setup_generator.rb +16 -4
- data/lib/generators/pu/saas/welcome/templates/app/controllers/welcome_controller.rb.tt +1 -1
- data/lib/plutonium/helpers/turbo_helper.rb +19 -0
- data/lib/plutonium/resource/controllers/crud_actions.rb +4 -4
- data/lib/plutonium/resource/controllers/interactive_actions.rb +3 -3
- data/lib/plutonium/ui/component/methods.rb +1 -0
- data/lib/plutonium/ui/form/base.rb +17 -1
- data/lib/plutonium/ui/form/components/secure_association.rb +11 -6
- data/lib/plutonium/ui/form/interaction.rb +1 -1
- data/lib/plutonium/ui/form/theme.rb +1 -1
- data/lib/plutonium/ui/page/edit.rb +1 -1
- data/lib/plutonium/ui/page/new.rb +1 -1
- data/lib/plutonium/ui/table/components/filter_form.rb +12 -4
- data/lib/plutonium/version.rb +1 -1
- data/package.json +4 -1
- data/src/js/controllers/form_controller.js +5 -4
- data/yarn.lock +108 -1
- metadata +45 -3
data/docs/.vitepress/config.ts
CHANGED
|
@@ -32,8 +32,7 @@ export default defineConfig(withMermaid({
|
|
|
32
32
|
{ text: "Home", link: "/" },
|
|
33
33
|
{ text: "Getting Started", link: "/getting-started/" },
|
|
34
34
|
{ text: "Guides", link: "/guides/" },
|
|
35
|
-
{ text: "Reference", link: "/reference/" }
|
|
36
|
-
{ text: "Demo", link: "https://github.com/radioactive-labs/plutonium-core/tree/master/test/dummy" }
|
|
35
|
+
{ text: "Reference", link: "/reference/" }
|
|
37
36
|
],
|
|
38
37
|
sidebar: {
|
|
39
38
|
'/getting-started/': [
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="pu-section pu-section--band">
|
|
3
|
+
<div class="pu-section-inner">
|
|
4
|
+
<h2 class="pu-section-title">Plutonium fits two kinds of teams.</h2>
|
|
5
|
+
<div class="ha-grid">
|
|
6
|
+
<div class="ha-col">
|
|
7
|
+
<div class="ha-head">For Rails developers</div>
|
|
8
|
+
<p class="ha-lede">The missing layer between Rails and the apps you keep building.</p>
|
|
9
|
+
<ul class="ha-list">
|
|
10
|
+
<li><IconArrowRight class="ha-arr" :size="16" :stroke-width="2.25" /><span>Convention extended to CRUD, policies, and portals</span></li>
|
|
11
|
+
<li><IconArrowRight class="ha-arr" :size="16" :stroke-width="2.25" /><span>Generated code lives in your repo — edit anything</span></li>
|
|
12
|
+
<li><IconArrowRight class="ha-arr" :size="16" :stroke-width="2.25" /><span>Mountable Rails engines for packages and portals</span></li>
|
|
13
|
+
<li><IconArrowRight class="ha-arr" :size="16" :stroke-width="2.25" /><span>ActionPolicy authorization, baked in</span></li>
|
|
14
|
+
</ul>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="ha-col ha-col--right">
|
|
17
|
+
<div class="ha-head">For founders & teams</div>
|
|
18
|
+
<p class="ha-lede">Skip the SaaS template debate. Plutonium turns Rails into a SaaS toolkit.</p>
|
|
19
|
+
<ul class="ha-list">
|
|
20
|
+
<li><IconArrowRight class="ha-arr" :size="16" :stroke-width="2.25" /><span>Admin panel, signup, and invites on day one</span></li>
|
|
21
|
+
<li><IconArrowRight class="ha-arr" :size="16" :stroke-width="2.25" /><span>Multi-tenant scoping when you need it</span></li>
|
|
22
|
+
<li><IconArrowRight class="ha-arr" :size="16" :stroke-width="2.25" /><span>No template lock-in — it's just your Rails app</span></li>
|
|
23
|
+
<li><IconArrowRight class="ha-arr" :size="16" :stroke-width="2.25" /><span>Ship faster with AI tools that understand your code</span></li>
|
|
24
|
+
</ul>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</section>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<script setup>
|
|
32
|
+
import { IconArrowRight } from "@tabler/icons-vue"
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<style scoped>
|
|
36
|
+
.ha-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; }
|
|
37
|
+
.ha-col--right { border-left: 1px solid var(--pu-border); padding-left: 32px; }
|
|
38
|
+
.ha-head {
|
|
39
|
+
font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em;
|
|
40
|
+
color: var(--pu-accent); font-weight: 600; margin-bottom: 8px;
|
|
41
|
+
}
|
|
42
|
+
.ha-lede {
|
|
43
|
+
font-size: 17px; line-height: 1.35; font-weight: 500; color: var(--pu-text);
|
|
44
|
+
margin: 0 0 14px; letter-spacing: -0.01em;
|
|
45
|
+
}
|
|
46
|
+
.ha-list { list-style: none; padding: 0; margin: 0; font-size: 14px; line-height: 1.6; color: var(--pu-text-muted); }
|
|
47
|
+
.ha-list li { display: flex; gap: 10px; align-items: flex-start; padding: 5px 0; }
|
|
48
|
+
.ha-arr { color: var(--pu-accent); flex-shrink: 0; margin-top: 3px; }
|
|
49
|
+
@media (max-width: 768px) {
|
|
50
|
+
.ha-grid { grid-template-columns: 1fr; }
|
|
51
|
+
.ha-col--right { border-left: none; padding-left: 0; border-top: 1px solid var(--pu-border); padding-top: 24px; }
|
|
52
|
+
}
|
|
53
|
+
</style>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="pu-section pu-section--band hc-section">
|
|
3
|
+
<div class="hc-inner">
|
|
4
|
+
<p class="hc-quote">
|
|
5
|
+
“Stop writing the parts of every Rails app you've already written.
|
|
6
|
+
Plutonium is what should have been there all along.”
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<div class="hc-pills" role="tablist">
|
|
10
|
+
<button
|
|
11
|
+
v-for="opt in options"
|
|
12
|
+
:key="opt.id"
|
|
13
|
+
:class="['hc-pill', { 'hc-pill--active': selected === opt.id }]"
|
|
14
|
+
role="tab"
|
|
15
|
+
:aria-selected="selected === opt.id"
|
|
16
|
+
@click="selected = opt.id"
|
|
17
|
+
>
|
|
18
|
+
<span class="hc-pill-name">{{ opt.name }}</span>
|
|
19
|
+
<small class="hc-pill-sub">{{ opt.sub }}</small>
|
|
20
|
+
</button>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="hc-term-wrap">
|
|
24
|
+
<pre class="pu-term hc-term"><span class="prompt">$</span> rails new my_app -m {{ activeUrl }}<span class="pu-term-cursor"></span></pre>
|
|
25
|
+
<button class="hc-copy" :class="{ 'hc-copy--ok': copied }" @click="copy" :title="copied ? 'Copied' : 'Copy command'" :aria-label="copied ? 'Copied' : 'Copy command'">
|
|
26
|
+
<component :is="copied ? IconCheck : IconCopy" :size="16" :stroke-width="2" />
|
|
27
|
+
</button>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class="hc-ctas">
|
|
31
|
+
<a class="pu-btn pu-btn-primary" href="/plutonium-core/getting-started/">Get started <IconArrowRight :size="16" :stroke-width="2.25" /></a>
|
|
32
|
+
<a class="pu-btn pu-btn-ghost" href="https://github.com/radioactive-labs/plutonium-core" target="_blank" rel="noopener">
|
|
33
|
+
<IconBrandGithub :size="16" :stroke-width="2" /> GitHub
|
|
34
|
+
</a>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</section>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<script setup>
|
|
41
|
+
import { ref, computed } from "vue"
|
|
42
|
+
import { IconArrowRight, IconBrandGithub, IconCopy, IconCheck } from "@tabler/icons-vue"
|
|
43
|
+
|
|
44
|
+
const options = [
|
|
45
|
+
{ id: "plutonium", name: "plutonium", sub: "core + portals",
|
|
46
|
+
url: "https://radioactive-labs.github.io/plutonium-core/templates/plutonium.rb" },
|
|
47
|
+
{ id: "pluton8", name: "pluton8", sub: "+ SaaS lite stack",
|
|
48
|
+
url: "https://radioactive-labs.github.io/plutonium-core/templates/pluton8.rb" },
|
|
49
|
+
]
|
|
50
|
+
const selected = ref("plutonium")
|
|
51
|
+
const activeUrl = computed(() => options.find(o => o.id === selected.value).url)
|
|
52
|
+
const activeCommand = computed(() => `rails new my_app -m ${activeUrl.value}`)
|
|
53
|
+
const copied = ref(false)
|
|
54
|
+
|
|
55
|
+
async function copy() {
|
|
56
|
+
try {
|
|
57
|
+
await navigator.clipboard.writeText(activeCommand.value)
|
|
58
|
+
copied.value = true
|
|
59
|
+
setTimeout(() => { copied.value = false }, 1600)
|
|
60
|
+
} catch (e) {
|
|
61
|
+
// clipboard API unavailable; do nothing
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<style scoped>
|
|
67
|
+
.hc-section { padding: 96px 24px; }
|
|
68
|
+
.hc-inner {
|
|
69
|
+
max-width: 760px; margin: 0 auto; text-align: center;
|
|
70
|
+
background: linear-gradient(180deg, var(--pu-bg-band), var(--pu-bg-light));
|
|
71
|
+
border: 1px solid var(--pu-border); border-radius: 12px; padding: 56px 32px;
|
|
72
|
+
}
|
|
73
|
+
.hc-quote {
|
|
74
|
+
font-size: 28px; letter-spacing: -0.02em; line-height: 1.25;
|
|
75
|
+
color: var(--pu-text); font-weight: 500;
|
|
76
|
+
margin: 0 auto 28px; max-width: 600px;
|
|
77
|
+
}
|
|
78
|
+
.hc-pills {
|
|
79
|
+
display: inline-flex; background: rgba(0,0,0,0.05); border-radius: 999px;
|
|
80
|
+
padding: 4px; gap: 2px; margin-bottom: 14px;
|
|
81
|
+
}
|
|
82
|
+
.hc-pill {
|
|
83
|
+
background: transparent; border: 0; padding: 8px 16px; border-radius: 999px;
|
|
84
|
+
font-size: 12.5px; color: var(--pu-text-muted); cursor: pointer;
|
|
85
|
+
display: flex; flex-direction: column; align-items: center; line-height: 1.1;
|
|
86
|
+
font-family: inherit;
|
|
87
|
+
}
|
|
88
|
+
.hc-pill-sub { font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--pu-text-faint); margin-top: 2px; }
|
|
89
|
+
.hc-pill--active {
|
|
90
|
+
background: var(--pu-bg-light); color: var(--pu-text); font-weight: 600;
|
|
91
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
92
|
+
}
|
|
93
|
+
.hc-term-wrap { position: relative; max-width: 640px; margin: 0 auto 24px; }
|
|
94
|
+
.hc-term { margin: 0; text-align: left; white-space: pre-wrap; word-break: break-all; padding-right: 48px; }
|
|
95
|
+
.hc-copy {
|
|
96
|
+
position: absolute; top: 8px; right: 8px;
|
|
97
|
+
background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.12);
|
|
98
|
+
color: var(--pu-term-text); border-radius: 6px;
|
|
99
|
+
width: 32px; height: 32px; display: inline-flex; align-items: center; justify-content: center;
|
|
100
|
+
cursor: pointer; transition: background 0.15s ease, color 0.15s ease;
|
|
101
|
+
padding: 0;
|
|
102
|
+
}
|
|
103
|
+
.hc-copy:hover { background: rgba(255,255,255,0.16); }
|
|
104
|
+
.hc-copy--ok { background: var(--pu-success-bg); color: var(--pu-success-fg); border-color: transparent; }
|
|
105
|
+
.hc-ctas { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
|
|
106
|
+
.hc-ctas .pu-btn { display: inline-flex; align-items: center; gap: 6px; }
|
|
107
|
+
@media (max-width: 600px) { .hc-quote { font-size: 22px; } }
|
|
108
|
+
</style>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="pu-section pu-section--dark home-hero">
|
|
3
|
+
<div class="pu-section-inner home-hero-grid">
|
|
4
|
+
<div class="home-hero-text">
|
|
5
|
+
<div class="pu-eyebrow">Plutonium · The Rails RAD framework</div>
|
|
6
|
+
<h1 class="home-hero-headline">
|
|
7
|
+
The Rails framework for things you should never write again.
|
|
8
|
+
</h1>
|
|
9
|
+
<p class="home-hero-lede">
|
|
10
|
+
Convention over configuration, extended to everything you keep rebuilding.
|
|
11
|
+
</p>
|
|
12
|
+
<p class="home-hero-pillars">
|
|
13
|
+
<b>CRUD.</b> <b>Auth.</b> <b>Authorization.</b> <b>Multi-tenancy.</b>
|
|
14
|
+
<b>Admin portals.</b> <b>Search, filters, bulk actions.</b>
|
|
15
|
+
All generated. All customizable. All Rails.
|
|
16
|
+
</p>
|
|
17
|
+
<div class="home-hero-ctas">
|
|
18
|
+
<a class="pu-btn pu-btn-primary" href="/plutonium-core/getting-started/">Get started →</a>
|
|
19
|
+
<a class="pu-btn pu-btn-ghost on-dark" href="/plutonium-core/getting-started/tutorial/">Tutorial</a>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
<pre class="pu-term home-hero-term"><span class="prompt">$</span> rails g pu:pkg:portal admin
|
|
23
|
+
<span class="dim"> create packages/admin_portal/...</span>
|
|
24
|
+
<span class="prompt">$</span> rails g pu:res:scaffold Post title:string body:text published_at:datetime --dest=main_app
|
|
25
|
+
<span class="dim"> create app/models/post.rb</span>
|
|
26
|
+
<span class="dim"> create app/resource_registries/post_definition.rb</span>
|
|
27
|
+
<span class="prompt">$</span> rails g pu:res:conn Post --dest=admin_portal
|
|
28
|
+
<span class="dim"> ✓ Connected Post to AdminPortal</span>
|
|
29
|
+
<span class="prompt">$</span> <span class="pu-term-cursor"></span></pre>
|
|
30
|
+
</div>
|
|
31
|
+
</section>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<style scoped>
|
|
35
|
+
.home-hero { padding: 96px 24px; }
|
|
36
|
+
.home-hero-grid {
|
|
37
|
+
display: grid;
|
|
38
|
+
grid-template-columns: 1.05fr 1fr;
|
|
39
|
+
gap: 48px;
|
|
40
|
+
align-items: center;
|
|
41
|
+
}
|
|
42
|
+
.home-hero-headline {
|
|
43
|
+
font-size: 48px;
|
|
44
|
+
line-height: 1.05;
|
|
45
|
+
letter-spacing: -0.025em;
|
|
46
|
+
margin: 0 0 18px;
|
|
47
|
+
font-weight: 700;
|
|
48
|
+
}
|
|
49
|
+
.home-hero-lede {
|
|
50
|
+
font-size: 18px;
|
|
51
|
+
line-height: 1.5;
|
|
52
|
+
opacity: 0.78;
|
|
53
|
+
margin: 0 0 14px;
|
|
54
|
+
max-width: 540px;
|
|
55
|
+
}
|
|
56
|
+
.home-hero-pillars {
|
|
57
|
+
font-size: 14.5px;
|
|
58
|
+
line-height: 1.6;
|
|
59
|
+
opacity: 0.65;
|
|
60
|
+
margin: 0 0 28px;
|
|
61
|
+
max-width: 540px;
|
|
62
|
+
}
|
|
63
|
+
.home-hero-pillars b { color: var(--pu-term-text); font-weight: 600; opacity: 1; }
|
|
64
|
+
.home-hero-ctas { display: flex; gap: 12px; flex-wrap: wrap; }
|
|
65
|
+
.home-hero-term { margin: 0; white-space: pre; }
|
|
66
|
+
@media (max-width: 768px) {
|
|
67
|
+
.home-hero-grid { grid-template-columns: 1fr; gap: 28px; }
|
|
68
|
+
.home-hero-headline { font-size: 36px; }
|
|
69
|
+
}
|
|
70
|
+
</style>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="pu-section">
|
|
3
|
+
<div class="pu-section-inner">
|
|
4
|
+
<h2 class="pu-section-title">Organized the way you'll use it.</h2>
|
|
5
|
+
<div v-for="cat in cats" :key="cat.name" class="hb-cat">
|
|
6
|
+
<div class="hb-cat-name">{{ cat.name }}</div>
|
|
7
|
+
<div class="hb-row">
|
|
8
|
+
<a v-for="item in cat.items" :key="item.name" :href="item.link" class="hb-item">
|
|
9
|
+
<span class="hb-item-body">
|
|
10
|
+
<b>{{ item.name }}</b>
|
|
11
|
+
<small>{{ item.desc }}</small>
|
|
12
|
+
</span>
|
|
13
|
+
<IconArrowUpRight class="hb-arrow" :size="16" :stroke-width="2" />
|
|
14
|
+
</a>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</section>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script setup>
|
|
22
|
+
import { IconArrowUpRight } from "@tabler/icons-vue"
|
|
23
|
+
|
|
24
|
+
const cats = [
|
|
25
|
+
{ name: "Resources", items: [
|
|
26
|
+
{ name: "Scaffolds", desc: "Model, definition, policy, routes",
|
|
27
|
+
link: "/plutonium-core/reference/app/generators" },
|
|
28
|
+
{ name: "Search & filters", desc: "Declarative on the definition",
|
|
29
|
+
link: "/plutonium-core/guides/search-filtering" },
|
|
30
|
+
{ name: "Custom & bulk actions", desc: "Resource-scoped interactions",
|
|
31
|
+
link: "/plutonium-core/guides/custom-actions" },
|
|
32
|
+
]},
|
|
33
|
+
{ name: "App structure", items: [
|
|
34
|
+
{ name: "Portals", desc: "Themed, mountable engines",
|
|
35
|
+
link: "/plutonium-core/reference/app/portals" },
|
|
36
|
+
{ name: "Packages", desc: "Feature engines under your app",
|
|
37
|
+
link: "/plutonium-core/guides/creating-packages" },
|
|
38
|
+
{ name: "Multi-tenancy", desc: "Path or domain scoping",
|
|
39
|
+
link: "/plutonium-core/guides/multi-tenancy" },
|
|
40
|
+
]},
|
|
41
|
+
{ name: "People & access", items: [
|
|
42
|
+
{ name: "Auth (Rodauth)", desc: "Login, signup, password reset",
|
|
43
|
+
link: "/plutonium-core/guides/authentication" },
|
|
44
|
+
{ name: "Authorization", desc: "ActionPolicy per resource",
|
|
45
|
+
link: "/plutonium-core/guides/authorization" },
|
|
46
|
+
{ name: "Invites & memberships", desc: "Token lifecycle, mailers, onboarding",
|
|
47
|
+
link: "/plutonium-core/guides/user-invites" },
|
|
48
|
+
]},
|
|
49
|
+
]
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<style scoped>
|
|
53
|
+
.hb-cat { margin-bottom: 28px; }
|
|
54
|
+
.hb-cat:last-child { margin-bottom: 0; }
|
|
55
|
+
.hb-cat-name {
|
|
56
|
+
font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em;
|
|
57
|
+
color: var(--pu-accent); font-weight: 600; margin-bottom: 12px;
|
|
58
|
+
}
|
|
59
|
+
.hb-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
|
|
60
|
+
.hb-item {
|
|
61
|
+
display: flex; align-items: flex-start; justify-content: space-between; gap: 10px;
|
|
62
|
+
padding: 14px 16px; border: 1px solid var(--pu-border-soft); border-radius: 8px;
|
|
63
|
+
background: var(--pu-bg-light); color: inherit; text-decoration: none;
|
|
64
|
+
transition: border-color 0.15s ease, transform 0.15s ease;
|
|
65
|
+
}
|
|
66
|
+
.hb-item:hover { border-color: var(--pu-accent); }
|
|
67
|
+
.hb-item:hover b { color: var(--pu-accent); }
|
|
68
|
+
.hb-item:hover .hb-arrow { color: var(--pu-accent); transform: translate(2px, -2px); }
|
|
69
|
+
.hb-item-body { display: flex; flex-direction: column; min-width: 0; }
|
|
70
|
+
.hb-item b { display: block; color: var(--pu-text); font-weight: 600; margin-bottom: 2px; font-size: 14px; transition: color 0.15s ease; }
|
|
71
|
+
.hb-item small { font-size: 12px; color: var(--pu-text-faint); }
|
|
72
|
+
.hb-arrow { color: var(--pu-text-faint); flex-shrink: 0; transition: color 0.15s ease, transform 0.15s ease; margin-top: 2px; }
|
|
73
|
+
@media (max-width: 768px) { .hb-row { grid-template-columns: 1fr; } }
|
|
74
|
+
</style>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="pu-section pu-section--band">
|
|
3
|
+
<div class="pu-section-inner">
|
|
4
|
+
<h2 class="pu-section-title">Built on Rails. Wired for shipping.</h2>
|
|
5
|
+
<div class="hp-grid">
|
|
6
|
+
<div class="hp-card" v-for="p in pillars" :key="p.name">
|
|
7
|
+
<component :is="p.icon" class="hp-icon" :size="22" :stroke-width="1.75" />
|
|
8
|
+
<div class="hp-name">{{ p.name }}</div>
|
|
9
|
+
<div class="hp-desc">{{ p.desc }}</div>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
</section>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup>
|
|
17
|
+
import { IconRoute, IconCode, IconBuildingSkyscraper, IconRobot } from "@tabler/icons-vue"
|
|
18
|
+
|
|
19
|
+
const pillars = [
|
|
20
|
+
{ icon: IconRoute, name: "Convention over configuration",
|
|
21
|
+
desc: "Extended to resources, policies, portals, and tenancy — not just routes and views." },
|
|
22
|
+
{ icon: IconCode, name: "It's just Rails",
|
|
23
|
+
desc: 'Generated code lives in your repo. Edit it, override it, delete it. The “magic” is regular Ruby mixins you can read.' },
|
|
24
|
+
{ icon: IconBuildingSkyscraper, name: "Multi-tenant ready",
|
|
25
|
+
desc: "Path or domain tenancy. Scoped relations. Invites and memberships out of the box." },
|
|
26
|
+
{ icon: IconRobot, name: "AI-readable",
|
|
27
|
+
desc: "Predictable file layout and naming. Built-in skills teach AI assistants the patterns." },
|
|
28
|
+
]
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<style scoped>
|
|
32
|
+
.hp-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
|
|
33
|
+
.hp-card {
|
|
34
|
+
padding: 18px; border: 1px solid var(--pu-border-soft); border-radius: 8px;
|
|
35
|
+
background: var(--pu-bg-light);
|
|
36
|
+
}
|
|
37
|
+
.hp-icon { color: var(--pu-accent); margin-bottom: 10px; display: block; }
|
|
38
|
+
.hp-name { font-weight: 600; font-size: 15px; color: var(--pu-text); margin-bottom: 6px; line-height: 1.25; }
|
|
39
|
+
.hp-desc { font-size: 13px; color: var(--pu-text-muted); line-height: 1.5; }
|
|
40
|
+
@media (max-width: 900px) { .hp-grid { grid-template-columns: repeat(2, 1fr); } }
|
|
41
|
+
@media (max-width: 480px) { .hp-grid { grid-template-columns: 1fr; } }
|
|
42
|
+
</style>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="pu-section home-stop-writing">
|
|
3
|
+
<div class="pu-section-inner">
|
|
4
|
+
<h2 class="pu-section-title">What you stop writing.</h2>
|
|
5
|
+
<p class="hsw-sub">Same scaffold command. Two starting points. Very different surface area.</p>
|
|
6
|
+
|
|
7
|
+
<div class="hsw-grid">
|
|
8
|
+
<div>
|
|
9
|
+
<span class="hsw-label hsw-label--bad">Rails scaffold</span>
|
|
10
|
+
<pre class="pu-term pu-term--inline hsw-term"><span class="prompt">$</span> rails g scaffold Post title:string body:text published_at:datetime</pre>
|
|
11
|
+
<div class="hsw-stats">
|
|
12
|
+
<span>Just <b>CRUD</b></span>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
<div>
|
|
16
|
+
<span class="hsw-label hsw-label--good">Plutonium</span>
|
|
17
|
+
<pre class="pu-term pu-term--inline hsw-term"><span class="prompt">$</span> rails g pu:res:scaffold Post title:string body:text published_at:datetime --dest=main_app</pre>
|
|
18
|
+
<div class="hsw-stats hsw-stats--win">
|
|
19
|
+
<span><b>Full CRUD</b></span>
|
|
20
|
+
<span><b>+ Search</b></span>
|
|
21
|
+
<span><b>+ Filters</b></span>
|
|
22
|
+
<span><b>+ Bulk actions</b></span>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</section>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<style scoped>
|
|
31
|
+
.hsw-sub { color: var(--pu-text-muted); font-size: 15px; margin: -16px 0 32px; }
|
|
32
|
+
.hsw-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; align-items: stretch; }
|
|
33
|
+
.hsw-grid > div { display: flex; flex-direction: column; min-width: 0; }
|
|
34
|
+
.hsw-stats { margin-top: auto; }
|
|
35
|
+
.hsw-label {
|
|
36
|
+
display: inline-block; font-size: 11px; padding: 3px 8px; border-radius: 4px;
|
|
37
|
+
text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600;
|
|
38
|
+
}
|
|
39
|
+
.hsw-label--bad { background: #fff0f0; color: var(--pu-accent); }
|
|
40
|
+
.hsw-label--good { background: var(--pu-success-bg); color: var(--pu-success-fg); }
|
|
41
|
+
.hsw-term { margin-top: 10px; }
|
|
42
|
+
.hsw-stats {
|
|
43
|
+
padding-top: 14px; display: flex; gap: 14px; flex-wrap: wrap;
|
|
44
|
+
font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--pu-text-faint);
|
|
45
|
+
}
|
|
46
|
+
.hsw-stats b { color: var(--pu-text); font-weight: 600; }
|
|
47
|
+
.hsw-stats--win b { color: var(--pu-success-fg); }
|
|
48
|
+
@media (max-width: 768px) { .hsw-grid { grid-template-columns: 1fr; } }
|
|
49
|
+
</style>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="pu-section">
|
|
3
|
+
<div class="pu-section-inner">
|
|
4
|
+
<h2 class="pu-section-title">Scaffold a portal in minutes.</h2>
|
|
5
|
+
|
|
6
|
+
<div class="hw-cast">
|
|
7
|
+
<div class="hw-browser-bar hw-browser-bar--term">
|
|
8
|
+
<span></span><span></span><span></span>
|
|
9
|
+
<code>asciinema · scaffold a blog</code>
|
|
10
|
+
</div>
|
|
11
|
+
<div ref="castEl" class="hw-cast-player"></div>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div class="hw-strip">
|
|
15
|
+
<figure v-for="shot in shots" :key="shot.label">
|
|
16
|
+
<div class="hw-label">{{ shot.label }}</div>
|
|
17
|
+
<div class="hw-browser">
|
|
18
|
+
<div class="hw-browser-bar"><span></span><span></span><code>{{ shot.url }}</code></div>
|
|
19
|
+
<img :src="shot.src" :alt="shot.alt" class="hw-shot pu-zoomable" />
|
|
20
|
+
</div>
|
|
21
|
+
</figure>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</section>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script setup>
|
|
28
|
+
import { ref, onMounted, onBeforeUnmount } from "vue"
|
|
29
|
+
import { withBase } from "vitepress"
|
|
30
|
+
|
|
31
|
+
const shots = [
|
|
32
|
+
{ label: "Index", url: "/admin/posts", src: withBase("/images/home-index.png"), alt: "Posts index" },
|
|
33
|
+
{ label: "New", url: "/admin/posts/new", src: withBase("/images/home-new.png"), alt: "New post form" },
|
|
34
|
+
{ label: "Show", url: "/admin/posts/1", src: withBase("/images/home-show.png"), alt: "Post show page" },
|
|
35
|
+
]
|
|
36
|
+
const castUrl = withBase("/asciinema/home-scaffold.cast")
|
|
37
|
+
|
|
38
|
+
const castEl = ref(null)
|
|
39
|
+
let player = null
|
|
40
|
+
|
|
41
|
+
onMounted(async () => {
|
|
42
|
+
if (typeof window === "undefined" || !castEl.value) return
|
|
43
|
+
|
|
44
|
+
const [{ create }] = await Promise.all([
|
|
45
|
+
import("asciinema-player"),
|
|
46
|
+
import("asciinema-player/dist/bundle/asciinema-player.css"),
|
|
47
|
+
])
|
|
48
|
+
|
|
49
|
+
player = create(castUrl, castEl.value, {
|
|
50
|
+
autoPlay: true,
|
|
51
|
+
loop: true,
|
|
52
|
+
controls: true,
|
|
53
|
+
fit: "width",
|
|
54
|
+
rows: 14,
|
|
55
|
+
terminalFontSize: "small",
|
|
56
|
+
idleTimeLimit: 1.5,
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
onBeforeUnmount(() => {
|
|
61
|
+
player?.dispose?.()
|
|
62
|
+
})
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<style scoped>
|
|
66
|
+
.hw-cast {
|
|
67
|
+
border: 1px solid var(--pu-border); border-radius: 10px; overflow: hidden;
|
|
68
|
+
background: var(--pu-bg-dark); margin-bottom: 18px;
|
|
69
|
+
}
|
|
70
|
+
.hw-cast-player { padding: 8px; background: var(--pu-bg-dark); }
|
|
71
|
+
.hw-cast-player :deep(.ap-player) { background: var(--pu-bg-dark); }
|
|
72
|
+
|
|
73
|
+
.hw-browser-bar {
|
|
74
|
+
background: var(--pu-bg-band); padding: 8px 12px; display: flex; align-items: center; gap: 5px;
|
|
75
|
+
border-bottom: 1px solid var(--pu-border-soft);
|
|
76
|
+
}
|
|
77
|
+
.hw-browser-bar--term { background: #161b22; border-bottom-color: #30363d; }
|
|
78
|
+
.hw-browser-bar span {
|
|
79
|
+
width: 10px; height: 10px; border-radius: 50%; background: var(--pu-border);
|
|
80
|
+
}
|
|
81
|
+
.hw-browser-bar--term span { background: #30363d; }
|
|
82
|
+
.hw-browser-bar code {
|
|
83
|
+
margin-left: 12px; background: var(--pu-bg-light); padding: 3px 8px; border-radius: 4px;
|
|
84
|
+
font-size: 11px; color: var(--pu-text-faint);
|
|
85
|
+
}
|
|
86
|
+
.hw-browser-bar--term code { background: #21262d; color: #8b949e; }
|
|
87
|
+
|
|
88
|
+
.hw-strip {
|
|
89
|
+
display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; align-items: stretch;
|
|
90
|
+
}
|
|
91
|
+
.hw-strip figure { margin: 0; display: flex; flex-direction: column; }
|
|
92
|
+
.hw-label {
|
|
93
|
+
font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em;
|
|
94
|
+
color: var(--pu-text-faint); margin-bottom: 8px;
|
|
95
|
+
}
|
|
96
|
+
.hw-browser {
|
|
97
|
+
display: block; width: 100%;
|
|
98
|
+
border: 1px solid var(--pu-border-soft); border-radius: 8px; overflow: hidden;
|
|
99
|
+
background: var(--pu-bg-light);
|
|
100
|
+
transition: border-color 0.15s ease;
|
|
101
|
+
}
|
|
102
|
+
.hw-browser:hover { border-color: var(--pu-accent); }
|
|
103
|
+
.hw-browser .hw-browser-bar { padding: 5px 8px; }
|
|
104
|
+
.hw-browser .hw-browser-bar span { width: 8px; height: 8px; }
|
|
105
|
+
.hw-browser .hw-browser-bar code { margin-left: 6px; font-size: 10px; }
|
|
106
|
+
.hw-shot { display: block; width: 100%; height: auto; }
|
|
107
|
+
|
|
108
|
+
@media (max-width: 768px) {
|
|
109
|
+
.hw-strip { grid-template-columns: 1fr; }
|
|
110
|
+
}
|
|
111
|
+
</style>
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="pu-section sl-section">
|
|
3
|
+
<div class="pu-section-inner">
|
|
4
|
+
<div class="pu-eyebrow">{{ eyebrow }}</div>
|
|
5
|
+
<h1 class="sl-h1">{{ title }}</h1>
|
|
6
|
+
<p class="sl-lede">{{ lede }}</p>
|
|
7
|
+
|
|
8
|
+
<div class="sl-grid">
|
|
9
|
+
<div class="sl-rail">
|
|
10
|
+
<template v-if="mode === 'numbered'">
|
|
11
|
+
<a
|
|
12
|
+
v-for="(step, i) in rail"
|
|
13
|
+
:key="i"
|
|
14
|
+
:href="step.link"
|
|
15
|
+
:class="['sl-step', { 'sl-step--link': step.link }]"
|
|
16
|
+
>
|
|
17
|
+
<span class="sl-num">{{ i + 1 }}</span>
|
|
18
|
+
<span class="sl-step-body">
|
|
19
|
+
<span class="sl-step-name">{{ step.name }}</span>
|
|
20
|
+
<span v-if="step.desc" class="sl-step-desc">{{ step.desc }}</span>
|
|
21
|
+
</span>
|
|
22
|
+
</a>
|
|
23
|
+
</template>
|
|
24
|
+
<template v-else>
|
|
25
|
+
<div v-for="grp in rail" :key="grp.group" class="sl-group">
|
|
26
|
+
<div class="sl-group-name">{{ grp.group }}</div>
|
|
27
|
+
<a
|
|
28
|
+
v-for="item in grp.items"
|
|
29
|
+
:key="item.name"
|
|
30
|
+
:href="item.link"
|
|
31
|
+
class="sl-step sl-step--link sl-step--cat"
|
|
32
|
+
>
|
|
33
|
+
<span class="sl-step-body">
|
|
34
|
+
<span class="sl-step-name">{{ item.name }}</span>
|
|
35
|
+
<span v-if="item.desc" class="sl-step-desc">{{ item.desc }}</span>
|
|
36
|
+
</span>
|
|
37
|
+
<IconArrowRight class="sl-step-arrow" :size="16" :stroke-width="2" />
|
|
38
|
+
</a>
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<aside class="sl-aside">
|
|
44
|
+
<div v-for="block in sidebar" :key="block.heading" class="sl-aside-block">
|
|
45
|
+
<h4 class="sl-aside-heading">{{ block.heading }}</h4>
|
|
46
|
+
<ul>
|
|
47
|
+
<li v-for="item in block.items" :key="item.label">
|
|
48
|
+
<a :href="item.href">{{ item.label }}</a>
|
|
49
|
+
<span v-if="item.note" class="sl-aside-note"> — {{ item.note }}</span>
|
|
50
|
+
</li>
|
|
51
|
+
</ul>
|
|
52
|
+
</div>
|
|
53
|
+
</aside>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</section>
|
|
57
|
+
</template>
|
|
58
|
+
|
|
59
|
+
<script setup>
|
|
60
|
+
import { IconArrowRight } from "@tabler/icons-vue"
|
|
61
|
+
|
|
62
|
+
defineProps({
|
|
63
|
+
eyebrow: { type: String, required: true },
|
|
64
|
+
title: { type: String, required: true },
|
|
65
|
+
lede: { type: String, required: true },
|
|
66
|
+
rail: { type: Array, required: true },
|
|
67
|
+
mode: { type: String, default: "numbered", validator: v => ["numbered", "categorized"].includes(v) },
|
|
68
|
+
sidebar: { type: Array, default: () => [] },
|
|
69
|
+
})
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<style scoped>
|
|
73
|
+
.sl-section { padding: 64px 24px 96px; }
|
|
74
|
+
.sl-h1 { font-size: 36px; letter-spacing: -0.025em; margin: 0 0 12px; color: var(--pu-text); }
|
|
75
|
+
.sl-lede { font-size: 16px; color: var(--pu-text-muted); max-width: 640px; margin: 0 0 40px; line-height: 1.55; }
|
|
76
|
+
.sl-grid { display: grid; grid-template-columns: 1.4fr 1fr; gap: 48px; }
|
|
77
|
+
.sl-rail { border-left: 2px solid var(--pu-accent); padding-left: 24px; }
|
|
78
|
+
.sl-group + .sl-group { margin-top: 22px; }
|
|
79
|
+
.sl-group-name {
|
|
80
|
+
font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em;
|
|
81
|
+
color: var(--pu-accent); font-weight: 600; margin-bottom: 8px;
|
|
82
|
+
}
|
|
83
|
+
.sl-step {
|
|
84
|
+
display: flex; gap: 12px; align-items: flex-start;
|
|
85
|
+
padding: 12px 0; border-bottom: 1px solid var(--pu-border-soft);
|
|
86
|
+
color: var(--pu-text); text-decoration: none;
|
|
87
|
+
}
|
|
88
|
+
.sl-step:last-child { border-bottom: none; }
|
|
89
|
+
.sl-step--cat { justify-content: space-between; align-items: center; }
|
|
90
|
+
.sl-step--link:hover .sl-step-name { color: var(--pu-accent); }
|
|
91
|
+
.sl-step--link:hover .sl-step-arrow { transform: translateX(2px); color: var(--pu-accent); }
|
|
92
|
+
.sl-num {
|
|
93
|
+
flex-shrink: 0; width: 24px; height: 24px; line-height: 24px; text-align: center;
|
|
94
|
+
background: var(--pu-accent); color: #fff; border-radius: 50%;
|
|
95
|
+
font-size: 11px; font-weight: 600;
|
|
96
|
+
}
|
|
97
|
+
.sl-step-body { display: flex; flex-direction: column; gap: 2px; flex: 1; }
|
|
98
|
+
.sl-step-name { font-weight: 600; font-size: 14px; }
|
|
99
|
+
.sl-step-desc { font-size: 12.5px; color: var(--pu-text-muted); }
|
|
100
|
+
.sl-step-arrow { color: var(--pu-text-faint); transition: transform 0.15s ease, color 0.15s ease; flex-shrink: 0; }
|
|
101
|
+
|
|
102
|
+
.sl-aside-block + .sl-aside-block { margin-top: 28px; }
|
|
103
|
+
.sl-aside-heading {
|
|
104
|
+
font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em;
|
|
105
|
+
color: var(--pu-text-faint); margin: 0 0 10px; font-weight: 600;
|
|
106
|
+
}
|
|
107
|
+
.sl-aside ul { list-style: none; padding: 0; margin: 0; font-size: 14px; line-height: 1.85; }
|
|
108
|
+
.sl-aside a { color: var(--pu-accent); text-decoration: none; font-weight: 500; }
|
|
109
|
+
.sl-aside a:hover { text-decoration: underline; }
|
|
110
|
+
.sl-aside-note { color: var(--pu-text-muted); font-weight: 400; }
|
|
111
|
+
|
|
112
|
+
@media (max-width: 900px) {
|
|
113
|
+
.sl-grid { grid-template-columns: 1fr; gap: 32px; }
|
|
114
|
+
}
|
|
115
|
+
</style>
|