studio-engine 0.15.0 → 0.17.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 +41 -0
- data/app/assets/tailwind/studio_engine/engine-motion.css +440 -0
- data/app/controllers/design_system_controller.rb +12 -0
- data/app/views/design_system/_specimen.html.erb +27 -0
- data/app/views/design_system/index.html.erb +232 -0
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +1 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 51c0e518d2b2f2f0868ec4b737e3b6f10a2987a01ab46d34ef49d9c3e7418e01
|
|
4
|
+
data.tar.gz: 6daa86afc8e098c6245622afa579653f4f24cb89549bb06342bb2603dd209d7e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5eba2d0865c2d8cadd1eab36aeaf2be50fb44fd9dfc8eb77ff05c4cbc54c17c5127c2deb473c988b0e663b8c8a8d1b4d54297130db01932bb2462d0a9f34400f
|
|
7
|
+
data.tar.gz: 190ff612d9dbdef59bb7599e018f82c01d3e4d47900bcf4c77ec7cdda0231c71877919da4ae4b0aaf36006f6c72c6db558f39a39ba80838336725e49dd2546c2
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — `MAJOR.MINOR.PATCH`. Consumer Rails apps install the released RubyGems package with `gem "studio-engine", "~> 0.6"`; bumping the gem version and updating consumer lockfiles is a release.
|
|
4
4
|
|
|
5
|
+
## 0.17.0 — 2026-07-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`admin/design_system` — the living style guide (first slice).** A new
|
|
10
|
+
admin-gated page (`DesignSystemController#index`, route
|
|
11
|
+
`admin_design_system_path`) that renders the engine's shared UI primitives
|
|
12
|
+
live in the host app's theme. The route is propagated to every consumer by
|
|
13
|
+
`Studio.routes`, and the view is a bare content wrapper that renders inside
|
|
14
|
+
each host's `application.html.erb`, inheriting that app's navbar + theme. This
|
|
15
|
+
slice ships the **Style** section — a grouped, live gallery where each
|
|
16
|
+
specimen shows the rendered primitive, its class name, and a copyable usage
|
|
17
|
+
snippet: **Buttons** (`.btn` + every role variant + `.btn-sm` / `.btn-lg`
|
|
18
|
+
sizes), **Surfaces / text / form** (`.card`, `.card-hover`, `.badge`,
|
|
19
|
+
`.input-field`, `.empty-state`, `.label-upper`), the seven **Motion**
|
|
20
|
+
primitives from `engine-motion.css` (`.studio-border-glow`, `.spinner`,
|
|
21
|
+
`.loading-dots`, `.sheen`, `.ping`, `.fade-edge-*`, `.progress-meter`) each
|
|
22
|
+
with tunable CSS-var knobs demonstrated, and the seven **Theme tokens** as
|
|
23
|
+
swatches read live off `var(--color-*)`. Specimens use ONLY the engine's own
|
|
24
|
+
classes, so a consumer that bundles `engine.css` and imports
|
|
25
|
+
`engine-motion.css` styles them for real (nothing is inlined). The Modals,
|
|
26
|
+
Theme, and Tasks sections are later slices.
|
|
27
|
+
|
|
28
|
+
## 0.16.0 — 2026-07-26
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **Motion / effect primitive layer** — a new, OPT-IN stylesheet at
|
|
33
|
+
`app/assets/tailwind/studio_engine/engine-motion.css` consolidating seven
|
|
34
|
+
reusable animation + visual-effect primitives that had drifted across the MS
|
|
35
|
+
and TM apps: `.studio-border-glow` (mask-composite rainbow border),
|
|
36
|
+
`.spinner` (one canonical border spinner), `.loading-dots` (three bouncing
|
|
37
|
+
dots), `.sheen` (sweep-of-light), `.ping` (expanding pulse ring),
|
|
38
|
+
`.fade-edge` (static mask-image edge fade), and `.progress-meter` (a
|
|
39
|
+
dual-layer color-flip progress bar). Plain `.class` + `@keyframes` (not
|
|
40
|
+
`@utility`), themed through the 7-role CSS custom properties, every knob a
|
|
41
|
+
tunable CSS var. It does NOT auto-bundle: `tailwindcss:engines` only
|
|
42
|
+
generates an entry for the file literally named `engine.css`, so consumers
|
|
43
|
+
adopt this layer deliberately with a single `@import` (see the file header).
|
|
44
|
+
App-side de-fork of the local copies is a follow-up task.
|
|
45
|
+
|
|
5
46
|
## 0.14.0 — 2026-07-22
|
|
6
47
|
|
|
7
48
|
### Added
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/*
|
|
2
|
+
studio-engine MOTION / EFFECT primitives — the shared animation + visual-effect
|
|
3
|
+
layer. Seven reusable primitives, consolidated from the per-app copies that
|
|
4
|
+
drifted across mcritchie-studio (MS) and turf-monster (TM):
|
|
5
|
+
|
|
6
|
+
.studio-border-glow mask-composite rainbow border glow (extracted, MS)
|
|
7
|
+
.spinner one canonical border spinner (reconciled, MS+TM)
|
|
8
|
+
.loading-dots three bouncing dots (extracted, TM)
|
|
9
|
+
.sheen sweep-of-light across any element (reconciled, TM)
|
|
10
|
+
.ping expanding pulse ring (extracted, TM)
|
|
11
|
+
.fade-edge static mask-image edge fade (authored fresh)
|
|
12
|
+
.progress-meter dual-layer color-flip progress bar (authored fresh)
|
|
13
|
+
|
|
14
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
OPT-IN — this layer does NOT auto-bundle.
|
|
16
|
+
|
|
17
|
+
tailwindcss-rails' `tailwindcss:engines` task (Tailwindcss::Engines.bundle)
|
|
18
|
+
generates a `builds/tailwind/<engine>.css` entry ONLY for the file named
|
|
19
|
+
exactly `engine.css`. This file is `engine-motion.css`, so that matcher skips
|
|
20
|
+
it BY CONSTRUCTION — it never rides the auto-generated `studio_engine.css`
|
|
21
|
+
entry that `engine.css` uses, and consumers must adopt it deliberately.
|
|
22
|
+
|
|
23
|
+
Adopt with ONE line in the consuming app's app/assets/tailwind/application.css,
|
|
24
|
+
AFTER `@import "tailwindcss";` (and after your `../builds/tailwind/studio_engine`
|
|
25
|
+
import so the theme role vars are in scope):
|
|
26
|
+
|
|
27
|
+
@import "STUDIO_ENGINE_PATH/app/assets/tailwind/studio_engine/engine-motion.css";
|
|
28
|
+
|
|
29
|
+
Resolve STUDIO_ENGINE_PATH the same way config/tailwind.config.js already
|
|
30
|
+
resolves the shared preset — `bundle show studio-engine`. A one-time helper the
|
|
31
|
+
app-side de-fork task can wire (mirrors the engines contract):
|
|
32
|
+
|
|
33
|
+
# config/tailwind.config.js already computes this:
|
|
34
|
+
# const studioPath = execSync('bundle show studio-engine').toString().trim()
|
|
35
|
+
# emit an absolute @import into your app CSS (or a tiny generated entry).
|
|
36
|
+
|
|
37
|
+
App-side wiring + removing the forked local copies lands in the follow-up
|
|
38
|
+
adoption task; until then the MS/TM local copies stay canonical.
|
|
39
|
+
|
|
40
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
41
|
+
DESIGN RULES
|
|
42
|
+
|
|
43
|
+
- Plain `.class` + `@keyframes`, NOT `@utility`. A Tailwind v4 `@utility`
|
|
44
|
+
redeclaration is ADDITIVE (both definitions emit), which is the wrong shape
|
|
45
|
+
for net-new primitives a consumer might transiently co-load with a local
|
|
46
|
+
copy. Plain classes sidestep that entirely.
|
|
47
|
+
- Color flows through the 7-role theme CSS custom properties emitted by
|
|
48
|
+
studio_theme_css_tag (`--color-cta`, `--color-surface`, `--color-inset`,
|
|
49
|
+
`--color-text`, `--color-border-strong`, `--color-primary`, ...), so every
|
|
50
|
+
primitive restyles itself in dark AND light mode with zero extra work.
|
|
51
|
+
- Every tuning knob (duration, size, color, %, delay) is a CSS custom property
|
|
52
|
+
with a sensible themed default, so a caller retunes via `style="--x: ..."`
|
|
53
|
+
or a `:root` override WITHOUT forking the rule.
|
|
54
|
+
- Namespacing: the two extracted primitives keep their source names
|
|
55
|
+
(`.studio-border-glow`, `.loading-dots`) so the follow-up de-fork is a pure
|
|
56
|
+
delete-the-local-copy swap with no view churn; their gem rules are byte-
|
|
57
|
+
compatible with the app copies, so a transient co-load is a harmless
|
|
58
|
+
duplicate. The reconciled/fresh primitives use fresh generic names
|
|
59
|
+
(`.spinner`, `.sheen`, `.ping`, `.fade-edge`, `.progress-meter`) that no app
|
|
60
|
+
class currently uses. Animation names that would clash with a Tailwind
|
|
61
|
+
built-in of DIFFERENT content are namespaced (`ping-pulse`, not `ping`);
|
|
62
|
+
`spin` is kept because it is byte-identical to Tailwind's built-in.
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/* =============================================================================
|
|
66
|
+
1. studio-border-glow — mask-composite rainbow border glow
|
|
67
|
+
Extracted verbatim from mcritchie-studio app/assets/tailwind/application.css.
|
|
68
|
+
A ::before draws the crisp animated rainbow border (masked to the padding
|
|
69
|
+
ring via mask-composite), a ::after draws a blurred halo of the same gradient.
|
|
70
|
+
|
|
71
|
+
Usage: <div class="studio-border-glow rounded-xl"> ... </div>
|
|
72
|
+
Tune: --studio-border-glow-duration / -opacity / -halo-opacity /
|
|
73
|
+
-halo-blur / -angle / -offset / -gradient
|
|
74
|
+
========================================================================== */
|
|
75
|
+
.studio-border-glow {
|
|
76
|
+
--studio-border-glow-opacity: 0.82;
|
|
77
|
+
--studio-border-glow-halo-opacity: 0.28;
|
|
78
|
+
--studio-border-glow-halo-blur: 18px;
|
|
79
|
+
--studio-border-glow-angle: 45deg;
|
|
80
|
+
--studio-border-glow-offset: 0%;
|
|
81
|
+
--studio-border-glow-duration: 20s;
|
|
82
|
+
--studio-border-glow-animation: studioBorderGlowSteam var(--studio-border-glow-duration) linear infinite;
|
|
83
|
+
--studio-border-glow-gradient:
|
|
84
|
+
linear-gradient(
|
|
85
|
+
var(--studio-border-glow-angle, 45deg),
|
|
86
|
+
#fb0094,
|
|
87
|
+
#00c4ff,
|
|
88
|
+
#34d399,
|
|
89
|
+
#fbbf24,
|
|
90
|
+
#ff2d55,
|
|
91
|
+
#fb0094,
|
|
92
|
+
#00c4ff,
|
|
93
|
+
#34d399,
|
|
94
|
+
#fbbf24,
|
|
95
|
+
#ff2d55
|
|
96
|
+
);
|
|
97
|
+
isolation: isolate;
|
|
98
|
+
position: relative;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.studio-border-glow::before,
|
|
102
|
+
.studio-border-glow::after {
|
|
103
|
+
content: "";
|
|
104
|
+
position: absolute;
|
|
105
|
+
pointer-events: none;
|
|
106
|
+
border-radius: inherit;
|
|
107
|
+
background: var(--studio-border-glow-gradient);
|
|
108
|
+
background-size: 400%;
|
|
109
|
+
animation: var(--studio-border-glow-animation);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.studio-border-glow::before {
|
|
113
|
+
-webkit-mask:
|
|
114
|
+
linear-gradient(#000 0 0) content-box,
|
|
115
|
+
linear-gradient(#000 0 0);
|
|
116
|
+
-webkit-mask-composite: xor;
|
|
117
|
+
mask:
|
|
118
|
+
linear-gradient(#000 0 0) content-box,
|
|
119
|
+
linear-gradient(#000 0 0);
|
|
120
|
+
mask-composite: exclude;
|
|
121
|
+
inset: -2px;
|
|
122
|
+
z-index: -1;
|
|
123
|
+
padding: 2px;
|
|
124
|
+
opacity: var(--studio-border-glow-opacity);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.studio-border-glow::after {
|
|
128
|
+
-webkit-mask:
|
|
129
|
+
linear-gradient(#000 0 0) content-box,
|
|
130
|
+
linear-gradient(#000 0 0);
|
|
131
|
+
-webkit-mask-composite: xor;
|
|
132
|
+
mask:
|
|
133
|
+
linear-gradient(#000 0 0) content-box,
|
|
134
|
+
linear-gradient(#000 0 0);
|
|
135
|
+
mask-composite: exclude;
|
|
136
|
+
inset: -10px;
|
|
137
|
+
z-index: -2;
|
|
138
|
+
padding: 10px;
|
|
139
|
+
filter: blur(var(--studio-border-glow-halo-blur));
|
|
140
|
+
opacity: var(--studio-border-glow-halo-opacity);
|
|
141
|
+
transform: translateZ(0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@keyframes studioBorderGlowSteam {
|
|
145
|
+
0% {
|
|
146
|
+
background-position: var(--studio-border-glow-offset, 0%) 0;
|
|
147
|
+
}
|
|
148
|
+
100% {
|
|
149
|
+
background-position: calc(var(--studio-border-glow-offset, 0%) + 400%) 0;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* =============================================================================
|
|
154
|
+
2. spinner — one canonical border spinner
|
|
155
|
+
Reconciles MS .hb-spinner / .aa-spinner (heartbeat.css) and TM .cta-spinner /
|
|
156
|
+
hold-spinner into a single generic border spinner. Sized + colored entirely
|
|
157
|
+
through CSS vars; defaults to the CTA role so it reads on any surface.
|
|
158
|
+
|
|
159
|
+
Usage: <span class="spinner"></span>
|
|
160
|
+
<span class="spinner" style="--spinner-size: 2rem"></span>
|
|
161
|
+
on a colored button: <span class="spinner" style="--spinner-color: currentColor"></span>
|
|
162
|
+
Tune: --spinner-size / --spinner-thickness / --spinner-track /
|
|
163
|
+
--spinner-color / --spinner-speed
|
|
164
|
+
========================================================================== */
|
|
165
|
+
.spinner {
|
|
166
|
+
display: inline-block;
|
|
167
|
+
width: var(--spinner-size, 1rem);
|
|
168
|
+
height: var(--spinner-size, 1rem);
|
|
169
|
+
border: var(--spinner-thickness, 2px) solid var(--spinner-track, var(--color-border-strong));
|
|
170
|
+
border-top-color: var(--spinner-color, var(--color-cta));
|
|
171
|
+
border-radius: 9999px;
|
|
172
|
+
animation: spin var(--spinner-speed, 0.7s) linear infinite;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@keyframes spin {
|
|
176
|
+
to {
|
|
177
|
+
transform: rotate(360deg);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* =============================================================================
|
|
182
|
+
3. loading-dots — three bouncing dots
|
|
183
|
+
Extracted verbatim from turf-monster (the modal card-header "we're working on
|
|
184
|
+
it" idiom). Emit exactly three child <span>s.
|
|
185
|
+
|
|
186
|
+
Usage: <span class="loading-dots"><span></span><span></span><span></span></span>
|
|
187
|
+
Tune: --loading-dots-color / --loading-dots-size / --loading-dots-gap /
|
|
188
|
+
--loading-dots-speed
|
|
189
|
+
========================================================================== */
|
|
190
|
+
.loading-dots {
|
|
191
|
+
display: inline-flex;
|
|
192
|
+
align-items: center;
|
|
193
|
+
justify-content: center;
|
|
194
|
+
gap: var(--loading-dots-gap, 6px);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.loading-dots span {
|
|
198
|
+
width: var(--loading-dots-size, 8px);
|
|
199
|
+
height: var(--loading-dots-size, 8px);
|
|
200
|
+
border-radius: 9999px;
|
|
201
|
+
background: var(--loading-dots-color, var(--color-primary));
|
|
202
|
+
animation: loading-dots-bounce var(--loading-dots-speed, 1.2s) ease-in-out infinite both;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.loading-dots span:nth-child(1) { animation-delay: -0.32s; }
|
|
206
|
+
.loading-dots span:nth-child(2) { animation-delay: -0.16s; }
|
|
207
|
+
.loading-dots span:nth-child(3) { animation-delay: 0s; }
|
|
208
|
+
|
|
209
|
+
@keyframes loading-dots-bounce {
|
|
210
|
+
0%, 80%, 100% { transform: translateY(0) scale(0.85); opacity: 0.45; }
|
|
211
|
+
40% { transform: translateY(-6px) scale(1); opacity: 1; }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* =============================================================================
|
|
215
|
+
4. sheen — a sweep-of-light across any element
|
|
216
|
+
Reconciles TM's badge-sheen-sweep (a one-shot diagonal wink) and seedsShimmer
|
|
217
|
+
(a repeating sweep) into one wrapper primitive. A ::after paints a moving band
|
|
218
|
+
of light clipped to the element's own radius. One-shot by default; add
|
|
219
|
+
.sheen--loop for a continuous shimmer.
|
|
220
|
+
|
|
221
|
+
Usage: <span class="sheen"> ... </span> (one wink on render)
|
|
222
|
+
<span class="sheen sheen--loop"> ... </span> (continuous)
|
|
223
|
+
Tune: --sheen-color / --sheen-duration / --sheen-delay / --sheen-width
|
|
224
|
+
========================================================================== */
|
|
225
|
+
.sheen {
|
|
226
|
+
position: relative;
|
|
227
|
+
overflow: hidden;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.sheen::after {
|
|
231
|
+
content: "";
|
|
232
|
+
position: absolute;
|
|
233
|
+
inset: 0;
|
|
234
|
+
border-radius: inherit;
|
|
235
|
+
pointer-events: none;
|
|
236
|
+
background: linear-gradient(
|
|
237
|
+
90deg,
|
|
238
|
+
transparent calc(50% - var(--sheen-width, 15%)),
|
|
239
|
+
var(--sheen-color, rgba(255, 255, 255, 0.45)) 50%,
|
|
240
|
+
transparent calc(50% + var(--sheen-width, 15%))
|
|
241
|
+
);
|
|
242
|
+
background-size: 250% 100%;
|
|
243
|
+
background-repeat: no-repeat;
|
|
244
|
+
background-position: 200% 0;
|
|
245
|
+
animation: sheen-sweep var(--sheen-duration, 2.5s) ease-in-out var(--sheen-delay, 0s) 1 both;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.sheen.sheen--loop::after {
|
|
249
|
+
animation-iteration-count: infinite;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@keyframes sheen-sweep {
|
|
253
|
+
from { background-position: 200% 0; }
|
|
254
|
+
to { background-position: -100% 0; }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* =============================================================================
|
|
258
|
+
5. ping — an expanding pulse ring
|
|
259
|
+
Extracted from TM's chat-live-ping ("Live" radar ping). A ring that scales up
|
|
260
|
+
and fades, richer than Tailwind's animate-ping. Place it as a sibling ring
|
|
261
|
+
behind a solid dot, or use the wrapper form for a dot + ring in one element.
|
|
262
|
+
|
|
263
|
+
Usage (ring only): <span class="ping"></span>
|
|
264
|
+
Usage (dot + ring): <span class="ping ping--dot"></span>
|
|
265
|
+
Tune: --ping-color / --ping-size / --ping-scale / --ping-speed /
|
|
266
|
+
--ping-opacity
|
|
267
|
+
NB: keyframe is `ping-pulse` (NOT `ping`) — Tailwind's built-in `ping`
|
|
268
|
+
keyframe has different content, so we namespace to avoid an override.
|
|
269
|
+
========================================================================== */
|
|
270
|
+
.ping {
|
|
271
|
+
display: inline-block;
|
|
272
|
+
width: var(--ping-size, 0.625rem);
|
|
273
|
+
height: var(--ping-size, 0.625rem);
|
|
274
|
+
border-radius: 9999px;
|
|
275
|
+
background: var(--ping-color, var(--color-cta));
|
|
276
|
+
animation: ping-pulse var(--ping-speed, 1.8s) cubic-bezier(0, 0, 0.2, 1) infinite;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* Dot + ring: a solid center dot with the expanding ring animating behind it. */
|
|
280
|
+
.ping.ping--dot {
|
|
281
|
+
position: relative;
|
|
282
|
+
animation: none;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.ping.ping--dot::after {
|
|
286
|
+
content: "";
|
|
287
|
+
position: absolute;
|
|
288
|
+
inset: 0;
|
|
289
|
+
border-radius: 9999px;
|
|
290
|
+
background: var(--ping-color, var(--color-cta));
|
|
291
|
+
animation: ping-pulse var(--ping-speed, 1.8s) cubic-bezier(0, 0, 0.2, 1) infinite;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
@keyframes ping-pulse {
|
|
295
|
+
0% { transform: scale(1); opacity: var(--ping-opacity, 0.6); }
|
|
296
|
+
70%, 100% { transform: scale(var(--ping-scale, 2.6)); opacity: 0; }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* =============================================================================
|
|
300
|
+
6. fade-edge — static mask-image edge fade (authored fresh)
|
|
301
|
+
Softens the edge of a scroll container so content fades out instead of hard-
|
|
302
|
+
clipping. Pure mask, no animation. Directional variants; -x / -y fade both
|
|
303
|
+
ends of an axis.
|
|
304
|
+
|
|
305
|
+
Usage: <div class="fade-edge-b overflow-y-auto"> ...long content... </div>
|
|
306
|
+
<div class="fade-edge-y overflow-y-auto"> ... </div>
|
|
307
|
+
Tune: --fade-edge-size (how far the fade reaches in from the edge)
|
|
308
|
+
========================================================================== */
|
|
309
|
+
.fade-edge-t,
|
|
310
|
+
.fade-edge-b,
|
|
311
|
+
.fade-edge-l,
|
|
312
|
+
.fade-edge-r,
|
|
313
|
+
.fade-edge-x,
|
|
314
|
+
.fade-edge-y {
|
|
315
|
+
--fade-edge-size: 2rem;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.fade-edge-t {
|
|
319
|
+
-webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 var(--fade-edge-size));
|
|
320
|
+
mask-image: linear-gradient(to bottom, transparent 0, #000 var(--fade-edge-size));
|
|
321
|
+
}
|
|
322
|
+
.fade-edge-b {
|
|
323
|
+
-webkit-mask-image: linear-gradient(to top, transparent 0, #000 var(--fade-edge-size));
|
|
324
|
+
mask-image: linear-gradient(to top, transparent 0, #000 var(--fade-edge-size));
|
|
325
|
+
}
|
|
326
|
+
.fade-edge-l {
|
|
327
|
+
-webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--fade-edge-size));
|
|
328
|
+
mask-image: linear-gradient(to right, transparent 0, #000 var(--fade-edge-size));
|
|
329
|
+
}
|
|
330
|
+
.fade-edge-r {
|
|
331
|
+
-webkit-mask-image: linear-gradient(to left, transparent 0, #000 var(--fade-edge-size));
|
|
332
|
+
mask-image: linear-gradient(to left, transparent 0, #000 var(--fade-edge-size));
|
|
333
|
+
}
|
|
334
|
+
.fade-edge-y {
|
|
335
|
+
-webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 var(--fade-edge-size), #000 calc(100% - var(--fade-edge-size)), transparent 100%);
|
|
336
|
+
mask-image: linear-gradient(to bottom, transparent 0, #000 var(--fade-edge-size), #000 calc(100% - var(--fade-edge-size)), transparent 100%);
|
|
337
|
+
}
|
|
338
|
+
.fade-edge-x {
|
|
339
|
+
-webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--fade-edge-size), #000 calc(100% - var(--fade-edge-size)), transparent 100%);
|
|
340
|
+
mask-image: linear-gradient(to right, transparent 0, #000 var(--fade-edge-size), #000 calc(100% - var(--fade-edge-size)), transparent 100%);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/* =============================================================================
|
|
344
|
+
7. progress-meter — dual-layer color-flip progress bar (authored fresh)
|
|
345
|
+
The label reads correctly over BOTH the track and the fill: two identical,
|
|
346
|
+
pixel-aligned text layers stack, and the FILL layer (with its on-fill label
|
|
347
|
+
color) is revealed only over the filled width via clip-path. Where fill covers
|
|
348
|
+
the label you see the on-fill color; where the track shows through you see the
|
|
349
|
+
on-track color — the text flips color exactly at the fill boundary. Drive fill
|
|
350
|
+
with the --progress custom property.
|
|
351
|
+
|
|
352
|
+
Usage:
|
|
353
|
+
<div class="progress-meter" style="--progress: 62%">
|
|
354
|
+
<div class="progress-meter-track"><span class="progress-meter-label">62%</span></div>
|
|
355
|
+
<div class="progress-meter-fill"><span class="progress-meter-label">62%</span></div>
|
|
356
|
+
</div>
|
|
357
|
+
Indeterminate (no known %): add .progress-meter--indeterminate.
|
|
358
|
+
Tune: --progress / --progress-fill / --progress-track /
|
|
359
|
+
--progress-label / --progress-label-on-fill /
|
|
360
|
+
--progress-height / --progress-radius
|
|
361
|
+
========================================================================== */
|
|
362
|
+
.progress-meter {
|
|
363
|
+
position: relative;
|
|
364
|
+
height: var(--progress-height, 1.5rem);
|
|
365
|
+
border-radius: var(--progress-radius, 9999px);
|
|
366
|
+
overflow: hidden;
|
|
367
|
+
background: var(--progress-track, var(--color-inset));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.progress-meter-track,
|
|
371
|
+
.progress-meter-fill {
|
|
372
|
+
position: absolute;
|
|
373
|
+
inset: 0;
|
|
374
|
+
display: flex;
|
|
375
|
+
align-items: center;
|
|
376
|
+
justify-content: center;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.progress-meter-track {
|
|
380
|
+
background: var(--progress-track, var(--color-inset));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.progress-meter-fill {
|
|
384
|
+
background: var(--progress-fill, var(--color-cta));
|
|
385
|
+
/* Reveal the fill layer only across the filled width. */
|
|
386
|
+
clip-path: inset(0 calc(100% - var(--progress, 0%)) 0 0);
|
|
387
|
+
transition: clip-path 0.35s ease;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.progress-meter-label {
|
|
391
|
+
font-weight: 700;
|
|
392
|
+
font-variant-numeric: tabular-nums;
|
|
393
|
+
line-height: 1;
|
|
394
|
+
white-space: nowrap;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.progress-meter-track .progress-meter-label {
|
|
398
|
+
color: var(--progress-label, var(--color-text));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.progress-meter-fill .progress-meter-label {
|
|
402
|
+
color: var(--progress-label-on-fill, #fff);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/* Indeterminate: sweep the fill band across the bar. */
|
|
406
|
+
.progress-meter--indeterminate .progress-meter-fill {
|
|
407
|
+
clip-path: none;
|
|
408
|
+
width: 40%;
|
|
409
|
+
inset: 0 auto 0 0;
|
|
410
|
+
animation: progress-indeterminate 1.4s ease-in-out infinite;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.progress-meter--indeterminate .progress-meter-label {
|
|
414
|
+
display: none;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
@keyframes progress-indeterminate {
|
|
418
|
+
0% { transform: translateX(-100%); }
|
|
419
|
+
100% { transform: translateX(250%); }
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/* =============================================================================
|
|
423
|
+
Reduced-motion: honor the user preference. Animated primitives settle into a
|
|
424
|
+
sensible static resting state; the static primitives (fade-edge) are unaffected.
|
|
425
|
+
========================================================================== */
|
|
426
|
+
@media (prefers-reduced-motion: reduce) {
|
|
427
|
+
.studio-border-glow::before,
|
|
428
|
+
.studio-border-glow::after,
|
|
429
|
+
.spinner,
|
|
430
|
+
.loading-dots span,
|
|
431
|
+
.sheen::after,
|
|
432
|
+
.ping,
|
|
433
|
+
.ping.ping--dot::after,
|
|
434
|
+
.progress-meter--indeterminate .progress-meter-fill {
|
|
435
|
+
animation: none;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.loading-dots span { opacity: 0.85; }
|
|
439
|
+
.sheen::after { display: none; }
|
|
440
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# The living style guide — a single admin-gated page that renders the engine's
|
|
2
|
+
# shared UI primitives (buttons, surfaces, the seven motion primitives, and the
|
|
3
|
+
# theme role tokens) live in the host app's theme. Mirrors ThemeSettingsController:
|
|
4
|
+
# a plain host-inherited controller, gated by `require_admin` from the already-
|
|
5
|
+
# included Studio::ErrorHandling concern. The view is a bare content wrapper that
|
|
6
|
+
# renders inside each host's application.html.erb, so it inherits that app's
|
|
7
|
+
# navbar + theme automatically.
|
|
8
|
+
class DesignSystemController < ApplicationController
|
|
9
|
+
before_action :require_admin
|
|
10
|
+
|
|
11
|
+
def index; end
|
|
12
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<%# Specimen card for the living style guide (admin/design_system).
|
|
2
|
+
Reusable chrome around ONE primitive:
|
|
3
|
+
stage - the rendered primitive (yielded from the caller block)
|
|
4
|
+
meta - its class name and a copyable usage snippet
|
|
5
|
+
locals: klass (String, required) and usage (String, optional).
|
|
6
|
+
Rendered via `render layout: "design_system/specimen"` so the caller's
|
|
7
|
+
block becomes the staged primitive. Checks local_assigns (never
|
|
8
|
+
block_given?) so a shared partial does not inherit the host layout yield. %>
|
|
9
|
+
<article class="card overflow-hidden flex flex-col">
|
|
10
|
+
<div class="flex flex-1 flex-wrap items-center justify-center gap-4 p-6 bg-surface-alt min-h-32">
|
|
11
|
+
<%= yield %>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="border-t border-subtle p-3 space-y-2">
|
|
14
|
+
<code class="block font-mono text-2xs text-heading break-all"><%= local_assigns[:klass] %></code>
|
|
15
|
+
<% snippet = local_assigns[:usage] %>
|
|
16
|
+
<% if snippet.present? %>
|
|
17
|
+
<div class="relative" x-data="{ copied: false }">
|
|
18
|
+
<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>
|
|
19
|
+
<button type="button"
|
|
20
|
+
class="btn btn-neutral btn-sm absolute top-2 right-2"
|
|
21
|
+
@click="navigator.clipboard?.writeText($refs.snippet.innerText); copied = true; setTimeout(() => copied = false, 1200)">
|
|
22
|
+
<span x-text="copied ? 'Copied' : 'Copy'">Copy</span>
|
|
23
|
+
</button>
|
|
24
|
+
</div>
|
|
25
|
+
<% end %>
|
|
26
|
+
</div>
|
|
27
|
+
</article>
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
<%# admin/design_system - the living style guide (slice A: shell + Style section).
|
|
2
|
+
A bare content wrapper (no `layout` call): it renders inside each host's
|
|
3
|
+
application.html.erb, inheriting that app's navbar and theme, so every
|
|
4
|
+
specimen restyles per app and in dark or light automatically.
|
|
5
|
+
|
|
6
|
+
Specimens use ONLY the engine's own classes (engine.css component layer plus
|
|
7
|
+
engine-motion.css motion layer), so a consumer that bundles engine.css and
|
|
8
|
+
imports engine-motion.css styles them for real. The primitive CSS is NOT
|
|
9
|
+
inlined here - the consumer's Tailwind build provides it. Page chrome uses
|
|
10
|
+
the engine's own .card / .label-upper / theme tokens. %>
|
|
11
|
+
<%
|
|
12
|
+
button_variants = {
|
|
13
|
+
"primary" => "Primary",
|
|
14
|
+
"secondary" => "Secondary",
|
|
15
|
+
"success" => "Success",
|
|
16
|
+
"danger" => "Danger",
|
|
17
|
+
"warning" => "Warning",
|
|
18
|
+
"neutral" => "Neutral",
|
|
19
|
+
"outline" => "Outline",
|
|
20
|
+
"google" => "Continue with Google"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# The 7 theme role tokens, read LIVE off the host theme so each swatch restyles
|
|
24
|
+
# per app and in dark/light. Names verified against Studio::ThemeResolver
|
|
25
|
+
# (the heading role reads --color-text; there is no --color-heading var).
|
|
26
|
+
theme_tokens = [
|
|
27
|
+
["--color-cta", "CTA / primary action"],
|
|
28
|
+
["--color-surface", "Surface"],
|
|
29
|
+
["--color-success", "Success"],
|
|
30
|
+
["--color-danger", "Danger"],
|
|
31
|
+
["--color-warning", "Warning"],
|
|
32
|
+
["--color-text", "Heading / text"],
|
|
33
|
+
["--color-border-strong", "Border (strong)"]
|
|
34
|
+
]
|
|
35
|
+
%>
|
|
36
|
+
<div class="max-w-6xl mx-auto px-4 py-8 space-y-14">
|
|
37
|
+
<header class="space-y-2">
|
|
38
|
+
<p class="label-upper">studio-engine · v<%= Studio::VERSION %></p>
|
|
39
|
+
<h1 class="text-3xl font-bold text-heading">Design System</h1>
|
|
40
|
+
<p class="text-body max-w-2xl">
|
|
41
|
+
A living gallery of the shared UI primitives, rendered live in this app's
|
|
42
|
+
theme. Everything below is styled by the engine's own
|
|
43
|
+
<code class="font-mono text-2xs bg-inset px-1.5 py-0.5 rounded">engine.css</code>
|
|
44
|
+
and
|
|
45
|
+
<code class="font-mono text-2xs bg-inset px-1.5 py-0.5 rounded">engine-motion.css</code>
|
|
46
|
+
layers - toggle this app between light and dark and every specimen follows.
|
|
47
|
+
</p>
|
|
48
|
+
</header>
|
|
49
|
+
|
|
50
|
+
<!-- Buttons -->
|
|
51
|
+
<section class="space-y-5">
|
|
52
|
+
<div class="space-y-1">
|
|
53
|
+
<h2 class="text-xl font-bold text-heading">Buttons</h2>
|
|
54
|
+
<p class="text-muted text-sm">
|
|
55
|
+
<code class="font-mono text-2xs">.btn</code> base plus one role variant, and the
|
|
56
|
+
<code class="font-mono text-2xs">.btn-sm</code> / <code class="font-mono text-2xs">.btn-lg</code> sizes.
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
60
|
+
<% button_variants.each do |name, label| %>
|
|
61
|
+
<%= render layout: "design_system/specimen",
|
|
62
|
+
locals: { klass: ".btn .btn-#{name}",
|
|
63
|
+
usage: %(<button class="btn btn-#{name}">#{label}</button>) } do %>
|
|
64
|
+
<button type="button" class="btn btn-<%= name %>"><%= label %></button>
|
|
65
|
+
<% end %>
|
|
66
|
+
<% end %>
|
|
67
|
+
|
|
68
|
+
<%= render layout: "design_system/specimen",
|
|
69
|
+
locals: { klass: ".btn-sm / .btn (default) / .btn-lg",
|
|
70
|
+
usage: %(<button class="btn btn-primary btn-sm">Small</button>\n<button class="btn btn-primary">Default</button>\n<button class="btn btn-primary btn-lg">Large</button>) } do %>
|
|
71
|
+
<div class="flex flex-wrap items-center justify-center gap-3">
|
|
72
|
+
<button type="button" class="btn btn-primary btn-sm">Small</button>
|
|
73
|
+
<button type="button" class="btn btn-primary">Default</button>
|
|
74
|
+
<button type="button" class="btn btn-primary btn-lg">Large</button>
|
|
75
|
+
</div>
|
|
76
|
+
<% end %>
|
|
77
|
+
</div>
|
|
78
|
+
</section>
|
|
79
|
+
|
|
80
|
+
<!-- Surfaces, text and form -->
|
|
81
|
+
<section class="space-y-5">
|
|
82
|
+
<div class="space-y-1">
|
|
83
|
+
<h2 class="text-xl font-bold text-heading">Surfaces, text & form</h2>
|
|
84
|
+
<p class="text-muted text-sm">Cards, badges, inputs, the empty state, and the uppercase meta label.</p>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
87
|
+
<%= render layout: "design_system/specimen",
|
|
88
|
+
locals: { klass: ".card", usage: %(<div class="card p-5">...</div>) } do %>
|
|
89
|
+
<div class="card p-5 text-sm text-body w-full max-w-xs text-center">Surface card</div>
|
|
90
|
+
<% end %>
|
|
91
|
+
|
|
92
|
+
<%= render layout: "design_system/specimen",
|
|
93
|
+
locals: { klass: ".card-hover", usage: %(<div class="card-hover p-5">...</div>) } do %>
|
|
94
|
+
<div class="card-hover p-5 text-sm text-body w-full max-w-xs text-center">Hover me</div>
|
|
95
|
+
<% end %>
|
|
96
|
+
|
|
97
|
+
<%= render layout: "design_system/specimen",
|
|
98
|
+
locals: { klass: ".badge", usage: %(<span class="badge">Neutral</span>) } do %>
|
|
99
|
+
<div class="flex flex-wrap items-center justify-center gap-2">
|
|
100
|
+
<span class="badge text-body">Neutral</span>
|
|
101
|
+
<span class="badge" style="color: var(--color-success); border-color: var(--color-success)">Success</span>
|
|
102
|
+
<span class="badge" style="color: var(--color-warning); border-color: var(--color-warning)">Warning</span>
|
|
103
|
+
<span class="badge" style="color: var(--color-danger); border-color: var(--color-danger)">Danger</span>
|
|
104
|
+
</div>
|
|
105
|
+
<% end %>
|
|
106
|
+
|
|
107
|
+
<%= render layout: "design_system/specimen",
|
|
108
|
+
locals: { klass: ".input-field", usage: %(<input class="input-field" placeholder="you@example.com">) } do %>
|
|
109
|
+
<input type="text" class="input-field" placeholder="you@example.com" aria-label="Sample input">
|
|
110
|
+
<% end %>
|
|
111
|
+
|
|
112
|
+
<%= render layout: "design_system/specimen",
|
|
113
|
+
locals: { klass: ".label-upper", usage: %(<p class="label-upper">Section label</p>) } do %>
|
|
114
|
+
<p class="label-upper">Section label</p>
|
|
115
|
+
<% end %>
|
|
116
|
+
|
|
117
|
+
<%= render layout: "design_system/specimen",
|
|
118
|
+
locals: { klass: ".empty-state", usage: %(<div class="empty-state">Nothing here yet</div>) } do %>
|
|
119
|
+
<div class="empty-state w-full max-w-xs text-sm text-muted">Nothing here yet</div>
|
|
120
|
+
<% end %>
|
|
121
|
+
</div>
|
|
122
|
+
</section>
|
|
123
|
+
|
|
124
|
+
<!-- Motion primitives -->
|
|
125
|
+
<section class="space-y-5">
|
|
126
|
+
<div class="space-y-1">
|
|
127
|
+
<h2 class="text-xl font-bold text-heading">Motion primitives</h2>
|
|
128
|
+
<p class="text-muted text-sm">
|
|
129
|
+
The seven <code class="font-mono text-2xs">engine-motion.css</code> primitives. Each knob is a themeable
|
|
130
|
+
CSS variable - tune it inline with <code class="font-mono text-2xs">style="--knob: value"</code>.
|
|
131
|
+
</p>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
134
|
+
<%= render layout: "design_system/specimen",
|
|
135
|
+
locals: { klass: ".studio-border-glow (rounded-xl wrapper)",
|
|
136
|
+
usage: %(<div class="studio-border-glow rounded-xl">...</div>) } do %>
|
|
137
|
+
<div class="studio-border-glow rounded-xl p-5 bg-surface text-center text-sm text-heading">Border glow</div>
|
|
138
|
+
<% end %>
|
|
139
|
+
|
|
140
|
+
<%= render layout: "design_system/specimen",
|
|
141
|
+
locals: { klass: ".spinner (knob: --spinner-size)",
|
|
142
|
+
usage: %(<span class="spinner"></span>\n<span class="spinner" style="--spinner-size: 2rem"></span>) } do %>
|
|
143
|
+
<span class="spinner"></span>
|
|
144
|
+
<span class="spinner" style="--spinner-size: 1.6rem"></span>
|
|
145
|
+
<span class="spinner" style="--spinner-size: 2.4rem; --spinner-thickness: 3px"></span>
|
|
146
|
+
<button type="button" class="btn btn-primary">
|
|
147
|
+
<span class="spinner" style="--spinner-color: currentColor; --spinner-track: rgba(255,255,255,.35)"></span>
|
|
148
|
+
<span class="ml-2">Saving</span>
|
|
149
|
+
</button>
|
|
150
|
+
<% end %>
|
|
151
|
+
|
|
152
|
+
<%= render layout: "design_system/specimen",
|
|
153
|
+
locals: { klass: ".loading-dots (3 span children)",
|
|
154
|
+
usage: %(<span class="loading-dots"><span></span><span></span><span></span></span>) } do %>
|
|
155
|
+
<span class="loading-dots"><span></span><span></span><span></span></span>
|
|
156
|
+
<span class="loading-dots" style="--loading-dots-size: 12px; --loading-dots-color: var(--color-cta)"><span></span><span></span><span></span></span>
|
|
157
|
+
<% end %>
|
|
158
|
+
|
|
159
|
+
<%= render layout: "design_system/specimen",
|
|
160
|
+
locals: { klass: ".sheen .sheen--loop",
|
|
161
|
+
usage: %(<span class="sheen sheen--loop">...</span>) } do %>
|
|
162
|
+
<span class="sheen sheen--loop badge text-body">Level 5 unlocked</span>
|
|
163
|
+
<span class="sheen sheen--loop card px-4 py-2 text-sm text-body" style="--sheen-duration: 3s">Sheen sweep</span>
|
|
164
|
+
<% end %>
|
|
165
|
+
|
|
166
|
+
<%= render layout: "design_system/specimen",
|
|
167
|
+
locals: { klass: ".ping .ping--dot (knob: --ping-color)",
|
|
168
|
+
usage: %(<span class="ping ping--dot"></span>) } do %>
|
|
169
|
+
<span class="ping ping--dot"></span>
|
|
170
|
+
<span class="ping ping--dot" style="--ping-color: var(--color-success)"></span>
|
|
171
|
+
<span class="ping ping--dot" style="--ping-color: var(--color-danger); --ping-size: 0.9rem"></span>
|
|
172
|
+
<% end %>
|
|
173
|
+
|
|
174
|
+
<%= render layout: "design_system/specimen",
|
|
175
|
+
locals: { klass: ".fade-edge-{t,b,l,r,x,y} (knob: --fade-edge-size)",
|
|
176
|
+
usage: %(<div class="fade-edge-y overflow-y-auto">...long content...</div>) } do %>
|
|
177
|
+
<div class="fade-edge-y overflow-y-auto h-32 w-full max-w-xs bg-surface rounded-lg p-3 text-sm text-body space-y-2">
|
|
178
|
+
<p>Scroll me - the top and bottom edges dissolve.</p>
|
|
179
|
+
<p>The mask softens content near either end of the container.</p>
|
|
180
|
+
<p>No JavaScript, no gradient overlay - just a mask-image.</p>
|
|
181
|
+
<p>It masks the element itself, so it works over any background.</p>
|
|
182
|
+
<p>Keep scrolling to watch the fade track the edge.</p>
|
|
183
|
+
</div>
|
|
184
|
+
<% end %>
|
|
185
|
+
|
|
186
|
+
<%= render layout: "design_system/specimen",
|
|
187
|
+
locals: { klass: ".progress-meter (knob: --progress)",
|
|
188
|
+
usage: %(<div class="progress-meter" style="--progress: 62%">\n <div class="progress-meter-track"><span class="progress-meter-label">62%</span></div>\n <div class="progress-meter-fill"><span class="progress-meter-label">62%</span></div>\n</div>) } do %>
|
|
189
|
+
<div class="w-full max-w-xs space-y-3">
|
|
190
|
+
<div class="progress-meter" style="--progress: 25%">
|
|
191
|
+
<div class="progress-meter-track"><span class="progress-meter-label">25%</span></div>
|
|
192
|
+
<div class="progress-meter-fill"><span class="progress-meter-label">25%</span></div>
|
|
193
|
+
</div>
|
|
194
|
+
<div class="progress-meter" style="--progress: 62%">
|
|
195
|
+
<div class="progress-meter-track"><span class="progress-meter-label">62%</span></div>
|
|
196
|
+
<div class="progress-meter-fill"><span class="progress-meter-label">62%</span></div>
|
|
197
|
+
</div>
|
|
198
|
+
<div class="progress-meter" style="--progress: 88%; --progress-fill: var(--color-success)">
|
|
199
|
+
<div class="progress-meter-track"><span class="progress-meter-label">88% complete</span></div>
|
|
200
|
+
<div class="progress-meter-fill"><span class="progress-meter-label">88% complete</span></div>
|
|
201
|
+
</div>
|
|
202
|
+
<div class="progress-meter progress-meter--indeterminate">
|
|
203
|
+
<div class="progress-meter-track"></div>
|
|
204
|
+
<div class="progress-meter-fill"></div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
<% end %>
|
|
208
|
+
</div>
|
|
209
|
+
</section>
|
|
210
|
+
|
|
211
|
+
<!-- Theme tokens -->
|
|
212
|
+
<section class="space-y-5">
|
|
213
|
+
<div class="space-y-1">
|
|
214
|
+
<h2 class="text-xl font-bold text-heading">Theme tokens</h2>
|
|
215
|
+
<p class="text-muted text-sm">
|
|
216
|
+
The 7 role colors, read live off this app's theme via <code class="font-mono text-2xs">var(--color-*)</code>.
|
|
217
|
+
Change the app or its light/dark mode and every swatch (and every specimen above) follows.
|
|
218
|
+
</p>
|
|
219
|
+
</div>
|
|
220
|
+
<div class="grid gap-4 grid-cols-2 sm:grid-cols-3 lg:grid-cols-4">
|
|
221
|
+
<% theme_tokens.each do |var, label| %>
|
|
222
|
+
<div class="card overflow-hidden">
|
|
223
|
+
<div class="h-16 border-b border-subtle" style="background: var(<%= var %>)"></div>
|
|
224
|
+
<div class="p-3 space-y-0.5">
|
|
225
|
+
<p class="text-sm text-heading font-medium"><%= label %></p>
|
|
226
|
+
<code class="font-mono text-2xs text-muted break-all"><%= var %></code>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
<% end %>
|
|
230
|
+
</div>
|
|
231
|
+
</section>
|
|
232
|
+
</div>
|
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
|
@@ -324,6 +324,7 @@ module Studio
|
|
|
324
324
|
patch "admin/theme", to: "theme_settings#update", as: :admin_theme_update
|
|
325
325
|
post "admin/theme/regenerate", to: "theme_settings#regenerate", as: :admin_theme_regenerate
|
|
326
326
|
get "admin/schema", to: "schema#index", as: :admin_schema
|
|
327
|
+
get "admin/design_system", to: "design_system#index", as: :admin_design_system
|
|
327
328
|
|
|
328
329
|
# Admin-managed transactional-email banner images (Studio::EmailImage).
|
|
329
330
|
# index lists each managed email variant + its current banner; update
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: studio-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.17.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
@@ -171,12 +171,14 @@ files:
|
|
|
171
171
|
- app/assets/images/ses-favicon.png
|
|
172
172
|
- app/assets/javascripts/studio/sticky_table_header.js
|
|
173
173
|
- app/assets/stylesheets/studio/sticky_table_header.css
|
|
174
|
+
- app/assets/tailwind/studio_engine/engine-motion.css
|
|
174
175
|
- app/assets/tailwind/studio_engine/engine.css
|
|
175
176
|
- app/controllers/concerns/solana/session_auth.rb
|
|
176
177
|
- app/controllers/concerns/studio/admin_models.rb
|
|
177
178
|
- app/controllers/concerns/studio/error_handling.rb
|
|
178
179
|
- app/controllers/concerns/studio/impersonation.rb
|
|
179
180
|
- app/controllers/concerns/studio/link_consumption.rb
|
|
181
|
+
- app/controllers/design_system_controller.rb
|
|
180
182
|
- app/controllers/error_logs_controller.rb
|
|
181
183
|
- app/controllers/magic_links_controller.rb
|
|
182
184
|
- app/controllers/navbar_controller.rb
|
|
@@ -226,6 +228,8 @@ files:
|
|
|
226
228
|
- app/views/components/_theme_toggle.html.erb
|
|
227
229
|
- app/views/components/_theme_toggle_morph.html.erb
|
|
228
230
|
- app/views/components/_user_nav.html.erb
|
|
231
|
+
- app/views/design_system/_specimen.html.erb
|
|
232
|
+
- app/views/design_system/index.html.erb
|
|
229
233
|
- app/views/error_logs/index.html.erb
|
|
230
234
|
- app/views/error_logs/show.html.erb
|
|
231
235
|
- app/views/layouts/_navbar.html.erb
|