studio-engine 0.127.0 → 0.129.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 +161 -0
- data/README.md +9 -3
- data/app/assets/javascripts/studio/page_transition_reveal.js +1 -1
- data/app/assets/tailwind/studio_engine/engine.css +572 -37
- data/app/controllers/concerns/studio/error_handling.rb +17 -0
- data/app/controllers/studio/surveys_controller.rb +16 -3
- data/app/helpers/studio/loader_helper.rb +2 -2
- data/app/helpers/studio_icon_helper.rb +10 -0
- data/app/javascript/studio/agent_chat.js +88 -6
- data/app/javascript/studio/agent_chat_voice.js +59 -5
- data/app/javascript/studio/alpine_stores.js +80 -22
- data/app/javascript/studio/loader.js +130 -5
- data/app/javascript/studio/microphone_chip.js +28 -2
- data/app/javascript/studio/page_transitions.js +28 -19
- data/app/javascript/studio/survey.js +16 -3
- data/app/javascript/studio/theme_editor.js +4 -0
- data/app/javascript/studio/voice_recorder.js +18 -7
- data/app/models/studio/survey_response.rb +24 -17
- data/app/views/components/_geo_signpost.html.erb +1 -0
- data/app/views/components/_link_sidebar.html.erb +40 -12
- data/app/views/components/_sidebar_panel.html.erb +15 -5
- data/app/views/layouts/studio/_head.html.erb +18 -7
- data/app/views/layouts/studio/_smooth_load.html.erb +1 -0
- data/app/views/layouts/studio/component_preview.html.erb +1 -1
- data/app/views/registrations/new.html.erb +4 -3
- data/app/views/sessions/new.html.erb +4 -3
- data/app/views/studio/agent_chat/_chat.html.erb +2 -1
- data/app/views/studio/agent_chat/_turn.html.erb +3 -1
- data/app/views/studio/agent_chat/_voice.html.erb +28 -13
- data/app/views/studio/loader/_overlay.html.erb +4 -4
- data/app/views/style/_components.html.erb +2 -4
- data/app/views/style/_glass.html.erb +83 -0
- data/app/views/style/_host.html.erb +2 -10
- data/app/views/style/_modals.html.erb +38 -231
- data/app/views/style/_scrims.html.erb +2 -9
- data/app/views/style/_site_footer.html.erb +5 -13
- data/app/views/style/_tasks.html.erb +16 -27
- data/app/views/style/_theme.html.erb +56 -78
- data/app/views/style/_tricks.html.erb +13 -11
- data/app/views/style/_type_ladder.html.erb +33 -0
- data/app/views/style/index.html.erb +5 -1
- data/docs/SITE_FOOTER.md +1 -1
- data/lib/studio/agent_chat.rb +4 -2
- data/lib/studio/icons/README.md +12 -0
- data/lib/studio/icons/lucide/LICENSE +43 -0
- data/lib/studio/icons/lucide/activity.svg +15 -0
- data/lib/studio/icons/lucide/book-open.svg +16 -0
- data/lib/studio/icons/lucide/chart-column.svg +18 -0
- data/lib/studio/icons/lucide/diamond.svg +15 -0
- data/lib/studio/icons/lucide/door-stairwell.svg +19 -0
- data/lib/studio/icons/lucide/globe.svg +17 -0
- data/lib/studio/icons/lucide/hammer.svg +17 -0
- data/lib/studio/icons/lucide/inbox.svg +16 -0
- data/lib/studio/icons/lucide/key-round.svg +16 -0
- data/lib/studio/icons/lucide/layout-grid.svg +18 -0
- data/lib/studio/icons/lucide/log-out.svg +17 -0
- data/lib/studio/icons/lucide/message-circle.svg +15 -0
- data/lib/studio/icons/lucide/package.svg +18 -0
- data/lib/studio/icons/lucide/palette.svg +19 -0
- data/lib/studio/icons/lucide/plug.svg +18 -0
- data/lib/studio/icons/lucide/rocket.svg +18 -0
- data/lib/studio/icons/lucide/user.svg +16 -0
- data/lib/studio/icons.rb +52 -0
- data/lib/studio/loader.rb +7 -0
- data/lib/studio/sidebar_sections.rb +52 -4
- data/lib/studio/site_footer.rb +23 -2
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +54 -2
- metadata +24 -1
|
@@ -163,16 +163,22 @@ body.modal-open .studio-app-banner {
|
|
|
163
163
|
|
|
164
164
|
/* -- Surfaces -------------------------------------------------------------- */
|
|
165
165
|
|
|
166
|
+
/* --surface-card-fill is unset unless an app opts in to the glass surfaces (THE GLASS SURFACES,
|
|
167
|
+
below). A token and not a higher-specificity rule, so a card's own tone still wins: a bg-warning/5,
|
|
168
|
+
a border-danger or a hover:bg-surface-alt on the element beats this as it always did. */
|
|
166
169
|
@utility card {
|
|
167
|
-
@apply
|
|
170
|
+
@apply rounded-xl border border-subtle;
|
|
171
|
+
background-color: var(--surface-card-fill, var(--color-surface));
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
@utility card-hover {
|
|
171
|
-
|
|
175
|
+
background-color: var(--surface-card-fill, var(--color-surface));
|
|
176
|
+
@apply rounded-xl border border-subtle hover:border-primary-700/50 hover:shadow-lg hover:shadow-primary-900/10 transition;
|
|
172
177
|
}
|
|
173
178
|
|
|
174
179
|
@utility empty-state {
|
|
175
|
-
@apply
|
|
180
|
+
@apply rounded-xl border border-subtle p-8 text-center;
|
|
181
|
+
background-color: var(--surface-card-fill, var(--color-surface));
|
|
176
182
|
}
|
|
177
183
|
|
|
178
184
|
@utility json-debug {
|
|
@@ -223,6 +229,36 @@ body.modal-open .studio-app-banner {
|
|
|
223
229
|
line-height: 1.4;
|
|
224
230
|
}
|
|
225
231
|
|
|
232
|
+
/* THE TYPE LADDER, on trial on /admin/style (Alex McRitchie, 2026-11-04: "standardize font
|
|
233
|
+
sizes across pages"). A role sets size, weight and line height; the tag stays whatever the
|
|
234
|
+
document outline needs, so a page's one <h1> may wear type-h2. These sizes are fixed px, so
|
|
235
|
+
type-dense does not move them. On Studio::TypeScale: h1 (3x of 16) and h4 (1x of 16). Off it,
|
|
236
|
+
on purpose: h2 and h3, the two steps the scale lacks between 3x and 1.5x and below 1.5x.
|
|
237
|
+
Body and fine print are the scale's own type-body and type-fine. */
|
|
238
|
+
@utility type-h1 {
|
|
239
|
+
font-size: 48px;
|
|
240
|
+
font-weight: var(--type-display-weight, 800);
|
|
241
|
+
line-height: 1.1;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
@utility type-h2 {
|
|
245
|
+
font-size: 30px;
|
|
246
|
+
font-weight: var(--type-display-weight, 800);
|
|
247
|
+
line-height: 1.2;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@utility type-h3 {
|
|
251
|
+
font-size: 20px;
|
|
252
|
+
font-weight: 700;
|
|
253
|
+
line-height: 1.3;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
@utility type-h4 {
|
|
257
|
+
font-size: 16px;
|
|
258
|
+
font-weight: 600;
|
|
259
|
+
line-height: 1.4;
|
|
260
|
+
}
|
|
261
|
+
|
|
226
262
|
/* -- The page header (studio/_page_header, docs/PAGE_HEADER.md) ---------
|
|
227
263
|
A normal page header at rest and a bar once stuck. The border is there in
|
|
228
264
|
both states, in transparent at rest, so the row's height never changes and
|
|
@@ -282,6 +318,13 @@ body.modal-open .studio-app-banner {
|
|
|
282
318
|
overflow-y: clip;
|
|
283
319
|
}
|
|
284
320
|
|
|
321
|
+
/* The trial's one reach into the page header: the shared title keeps its own size everywhere
|
|
322
|
+
but under this attribute. Same numbers as type-h2. */
|
|
323
|
+
[data-type-ladder] .page-header-title {
|
|
324
|
+
font-size: 30px;
|
|
325
|
+
line-height: 1.2;
|
|
326
|
+
}
|
|
327
|
+
|
|
285
328
|
@utility page-header-meta {
|
|
286
329
|
white-space: nowrap;
|
|
287
330
|
|
|
@@ -668,16 +711,18 @@ body.modal-open .studio-app-banner {
|
|
|
668
711
|
}
|
|
669
712
|
|
|
670
713
|
/* -- App-to-app page transitions --------------------------------------------
|
|
671
|
-
Studio.page_transition_direction
|
|
672
|
-
Every rule is gated on an
|
|
673
|
-
|
|
674
|
-
::view-transition-*(root) rules, here
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
714
|
+
Studio.page_transition_direction and Studio.page_transition_style
|
|
715
|
+
(studio/page_transitions, docs/PAGE_TRANSITIONS.md). Every rule is gated on an
|
|
716
|
+
attribute only that script sets, so an app without the setting keeps the fade
|
|
717
|
+
above. Attribute selectors outrank the bare ::view-transition-*(root) rules, here
|
|
718
|
+
and in a host. The effects are from the Codrops Page Transitions demo; the
|
|
719
|
+
keyframes were written fresh.
|
|
720
|
+
|
|
721
|
+
The app's own page color sits behind the pages, so no gap reads as a dark band.
|
|
722
|
+
Each page stays opaque as it moves: a page color seen through a fading page
|
|
723
|
+
would still shade it. The fallback covers a layout with no runtime theme block. */
|
|
679
724
|
html[data-page-transition]:not([data-page-transition="still"])::view-transition {
|
|
680
|
-
background:
|
|
725
|
+
background: var(--color-page, Canvas);
|
|
681
726
|
}
|
|
682
727
|
|
|
683
728
|
html[data-page-transition]::view-transition-old(root),
|
|
@@ -723,14 +768,14 @@ html[data-page-transition-direction]:not([data-page-transition-morph]) [data-vt-
|
|
|
723
768
|
view-transition-name: none !important;
|
|
724
769
|
}
|
|
725
770
|
|
|
726
|
-
html[data-page-transition="
|
|
727
|
-
html[data-page-transition="
|
|
728
|
-
html[data-page-transition="
|
|
729
|
-
html[data-page-transition="
|
|
730
|
-
html[data-page-transition="
|
|
731
|
-
html[data-page-transition="
|
|
732
|
-
html[data-page-transition="
|
|
733
|
-
html[data-page-transition="
|
|
771
|
+
html[data-page-transition="slide-left"]::view-transition-old(root) { animation-name: pt-out-left; }
|
|
772
|
+
html[data-page-transition="slide-left"]::view-transition-new(root) { animation-name: pt-in-from-right; }
|
|
773
|
+
html[data-page-transition="slide-right"]::view-transition-old(root) { animation-name: pt-out-right; }
|
|
774
|
+
html[data-page-transition="slide-right"]::view-transition-new(root) { animation-name: pt-in-from-left; }
|
|
775
|
+
html[data-page-transition="slide-top"]::view-transition-old(root) { animation-name: pt-out-top; }
|
|
776
|
+
html[data-page-transition="slide-top"]::view-transition-new(root) { animation-name: pt-in-from-bottom; }
|
|
777
|
+
html[data-page-transition="slide-bottom"]::view-transition-old(root) { animation-name: pt-out-bottom; }
|
|
778
|
+
html[data-page-transition="slide-bottom"]::view-transition-new(root) { animation-name: pt-in-from-top; }
|
|
734
779
|
|
|
735
780
|
html[data-page-transition="unfold-right"]::view-transition-old(root) { animation-name: pt-out-left; }
|
|
736
781
|
html[data-page-transition="unfold-right"]::view-transition-new(root) { animation-name: pt-unfold-right; transform-origin: 0% 50%; }
|
|
@@ -741,19 +786,72 @@ html[data-page-transition="unfold-bottom"]::view-transition-new(root) { animatio
|
|
|
741
786
|
html[data-page-transition="unfold-top"]::view-transition-old(root) { animation-name: pt-out-bottom; }
|
|
742
787
|
html[data-page-transition="unfold-top"]::view-transition-new(root) { animation-name: pt-unfold-top; transform-origin: 50% 100%; }
|
|
743
788
|
|
|
744
|
-
@keyframes pt-out-left { to {
|
|
745
|
-
@keyframes pt-out-right { to {
|
|
746
|
-
@keyframes pt-out-top { to {
|
|
747
|
-
@keyframes pt-out-bottom { to {
|
|
748
|
-
@keyframes pt-in-from-right { from {
|
|
749
|
-
@keyframes pt-in-from-left { from {
|
|
750
|
-
@keyframes pt-in-from-bottom { from {
|
|
751
|
-
@keyframes pt-in-from-top { from {
|
|
789
|
+
@keyframes pt-out-left { to { transform: translateX(-100%); } }
|
|
790
|
+
@keyframes pt-out-right { to { transform: translateX(100%); } }
|
|
791
|
+
@keyframes pt-out-top { to { transform: translateY(-100%); } }
|
|
792
|
+
@keyframes pt-out-bottom { to { transform: translateY(100%); } }
|
|
793
|
+
@keyframes pt-in-from-right { from { transform: translateX(100%); } }
|
|
794
|
+
@keyframes pt-in-from-left { from { transform: translateX(-100%); } }
|
|
795
|
+
@keyframes pt-in-from-bottom { from { transform: translateY(100%); } }
|
|
796
|
+
@keyframes pt-in-from-top { from { transform: translateY(-100%); } }
|
|
752
797
|
@keyframes pt-unfold-right { from { opacity: 0; transform: perspective(1200px) translateX(100%) rotateY(90deg); } }
|
|
753
798
|
@keyframes pt-unfold-left { from { opacity: 0; transform: perspective(1200px) translateX(-100%) rotateY(-90deg); } }
|
|
754
799
|
@keyframes pt-unfold-bottom { from { opacity: 0; transform: perspective(1200px) translateY(100%) rotateX(-90deg); } }
|
|
755
800
|
@keyframes pt-unfold-top { from { opacity: 0; transform: perspective(1200px) translateY(-100%) rotateX(90deg); } }
|
|
756
801
|
|
|
802
|
+
/* -- The theme flip --------------------------------------------------------
|
|
803
|
+
$store.theme.set (studio/alpine_stores) puts data-theme-flip on the root for one
|
|
804
|
+
flip. The whole page changes in one crossfade, whatever its size: no element
|
|
805
|
+
runs its own transition, no named element keeps its own picture, and nothing
|
|
806
|
+
here moves. !important because a wildcard pseudo-element is weaker than the
|
|
807
|
+
rules above and than a host's rule for a name. A pointer's click during the
|
|
808
|
+
crossfade hits the root, not the page (Chromium ignores pointer-events on these
|
|
809
|
+
pseudo-elements); a key press still reaches the focused toggle. */
|
|
810
|
+
html[data-theme-flip],
|
|
811
|
+
html[data-theme-flip] *,
|
|
812
|
+
html[data-theme-flip] *::before,
|
|
813
|
+
html[data-theme-flip] *::after {
|
|
814
|
+
transition: none !important;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/* The root keeps its name: it is the one picture. */
|
|
818
|
+
html[data-theme-flip] * {
|
|
819
|
+
view-transition-name: none !important;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
html[data-theme-flip]::view-transition {
|
|
823
|
+
background: none !important;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
html[data-theme-flip]::view-transition-group(*) {
|
|
827
|
+
animation: none !important;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
html[data-theme-flip]::view-transition-old(*),
|
|
831
|
+
html[data-theme-flip]::view-transition-new(*) {
|
|
832
|
+
mix-blend-mode: plus-lighter !important;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
html[data-theme-flip]::view-transition-old(*) {
|
|
836
|
+
animation: vt-theme-out 160ms linear both !important;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
html[data-theme-flip]::view-transition-new(*) {
|
|
840
|
+
animation: vt-theme-in 160ms linear both !important;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
@keyframes vt-theme-out {
|
|
844
|
+
to {
|
|
845
|
+
opacity: 0;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
@keyframes vt-theme-in {
|
|
850
|
+
from {
|
|
851
|
+
opacity: 0;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
757
855
|
/* Turbo's progress bar is the slow-load feedback under no-preview (the old
|
|
758
856
|
page holds until the fresh response). Theme it so every app's bar matches
|
|
759
857
|
its brand instead of Turbo's default. The fallback (#0076ff, Turbo's own
|
|
@@ -1107,7 +1205,10 @@ html[data-page-transition="unfold-top"]::view-transition-new(root) { animation-n
|
|
|
1107
1205
|
It is a pseudo-element of the panel: it moves with the slide, it leaves with the
|
|
1108
1206
|
panel, and it takes no clicks, so an outside click still reaches the page. A
|
|
1109
1207
|
transform on the panel does not start a new backdrop root, so it blurs the page.
|
|
1110
|
-
|
|
1208
|
+
100px wide, a 6px blur, on an eased mask: whole at the panel, half at 60px, nothing
|
|
1209
|
+
at the far end, and flat at both ends, so no stop shows as a line where the blur
|
|
1210
|
+
ends. A hold and a straight fade showed that line over a photo. A blur reads only
|
|
1211
|
+
above about half strength, so the 50% stop sits past the middle to carry it out.
|
|
1111
1212
|
right: 100% measures from the padding box, so the band runs over the panel's 1px
|
|
1112
1213
|
border. Stopping short of it left the border as a lit line beside the blur.
|
|
1113
1214
|
|
|
@@ -1120,15 +1221,449 @@ html[data-page-transition="unfold-top"]::view-transition-new(root) { animation-n
|
|
|
1120
1221
|
position: absolute;
|
|
1121
1222
|
inset-block: 0;
|
|
1122
1223
|
right: 100%;
|
|
1123
|
-
width:
|
|
1224
|
+
width: 6.25rem;
|
|
1124
1225
|
pointer-events: none;
|
|
1125
1226
|
-webkit-backdrop-filter: blur(6px);
|
|
1126
1227
|
backdrop-filter: blur(6px);
|
|
1127
|
-
-webkit-mask-image: linear-gradient(to left, #000
|
|
1128
|
-
mask-image: linear-gradient(to left, #000
|
|
1228
|
+
-webkit-mask-image: linear-gradient(to left, #000, rgb(0 0 0 / 0.99) 10%, rgb(0 0 0 / 0.96) 20%, rgb(0 0 0 / 0.9) 30%, rgb(0 0 0 / 0.8) 40%, rgb(0 0 0 / 0.66) 50%, rgb(0 0 0 / 0.5) 60%, rgb(0 0 0 / 0.33) 70%, rgb(0 0 0 / 0.17) 80%, rgb(0 0 0 / 0.05) 90%, transparent);
|
|
1229
|
+
mask-image: linear-gradient(to left, #000, rgb(0 0 0 / 0.99) 10%, rgb(0 0 0 / 0.96) 20%, rgb(0 0 0 / 0.9) 30%, rgb(0 0 0 / 0.8) 40%, rgb(0 0 0 / 0.66) 50%, rgb(0 0 0 / 0.5) 60%, rgb(0 0 0 / 0.33) 70%, rgb(0 0 0 / 0.17) 80%, rgb(0 0 0 / 0.05) 90%, transparent);
|
|
1129
1230
|
}
|
|
1130
1231
|
}
|
|
1131
1232
|
|
|
1233
|
+
/* THE GLASS SIDEBAR (Studio.sidebar_style = :glass, components/_sidebar_panel glass: true).
|
|
1234
|
+
A rounded panel floats off the page's edge with the page blurred through it.
|
|
1235
|
+
|
|
1236
|
+
TWO BOXES. The aside keeps the drawer's geometry: its top and height from --nav-bottom,
|
|
1237
|
+
data-pin-edge's scroll animation, and Alpine's translate-x-full slide, which now carries
|
|
1238
|
+
the inset off screen with it. It is a clear frame that takes no clicks, so a click in
|
|
1239
|
+
the inset reaches the page and closes the panel like any outside click. The shell inside
|
|
1240
|
+
holds the fill and the blur. That keeps the containing block a backdrop-filter makes off
|
|
1241
|
+
the element Alpine, Turbo's snapshot and a view transition move.
|
|
1242
|
+
|
|
1243
|
+
THE FILL IS THE FALLBACK. Here it is nearly solid, so a browser with no backdrop-filter,
|
|
1244
|
+
or a reader who asks for less transparency, still gets readable text. The @supports rule
|
|
1245
|
+
thins it where the browser says it has the blur.
|
|
1246
|
+
|
|
1247
|
+
CONTRAST. The worst page behind the panel is the opposite of its theme: a white image
|
|
1248
|
+
under dark glass, a black one under light. A browser can claim the blur and not paint it, so
|
|
1249
|
+
the thin fill alone holds resting soft ink at 4.5:1 over both, and brightness() only adds
|
|
1250
|
+
to that. A thinner fill looks more like glass and fails over a white page.
|
|
1251
|
+
|
|
1252
|
+
IT HANGS FROM THE NAVBAR. The frame has no top padding, so no page shows between the
|
|
1253
|
+
navbar and the panel, and the shell's top is square with no top border: the navbar is
|
|
1254
|
+
its top edge. The sides and the bottom keep the inset and the round corners. The frame
|
|
1255
|
+
still takes its top from --nav-bottom and data-pin-edge, so it follows every navbar state.
|
|
1256
|
+
|
|
1257
|
+
SPACING IS FIVE TOKENS, on the shell: --glass-row-height, --glass-row-gap between rows,
|
|
1258
|
+
--glass-icon-size, --glass-icon-gap between icon and label, and --glass-section-gap,
|
|
1259
|
+
which sets the space under a section and above its header and from which the title and
|
|
1260
|
+
Log out padding derive. A row's no-jump description needs no rule of its own: the 18px
|
|
1261
|
+
label and 14px description take 32px, and with the lift at 7px a 44px row leaves 6px above
|
|
1262
|
+
and below them.
|
|
1263
|
+
|
|
1264
|
+
THE PHONE gets a narrower inset, the full width and a lighter blur, which costs less
|
|
1265
|
+
through the slide. */
|
|
1266
|
+
[data-sidebar-glass] {
|
|
1267
|
+
--sidebar-glass-inset: 0.75rem;
|
|
1268
|
+
width: 19.5rem;
|
|
1269
|
+
padding: 0 var(--sidebar-glass-inset) var(--sidebar-glass-inset);
|
|
1270
|
+
pointer-events: none;
|
|
1271
|
+
}
|
|
1272
|
+
[data-sidebar-glass-shell] {
|
|
1273
|
+
--glass-row-height: 2.75rem;
|
|
1274
|
+
--glass-row-gap: 0.125rem;
|
|
1275
|
+
--glass-icon-size: 1.375rem;
|
|
1276
|
+
--glass-icon-gap: 0.625rem;
|
|
1277
|
+
--glass-section-gap: 1rem;
|
|
1278
|
+
--sidebar-row-lift: 0.4375rem;
|
|
1279
|
+
--glass-fill: rgb(250 249 253 / 0.97);
|
|
1280
|
+
--glass-thin-fill: rgb(255 255 255 / 0.72);
|
|
1281
|
+
--glass-filter: blur(24px) saturate(150%) brightness(1.15);
|
|
1282
|
+
--glass-ink: #1d1832;
|
|
1283
|
+
--glass-soft-ink: #474260;
|
|
1284
|
+
--glass-hover: rgb(29 24 50 / 0.08);
|
|
1285
|
+
--glass-line: rgb(29 24 50 / 0.14);
|
|
1286
|
+
--glass-shadow: 0 24px 60px rgb(29 24 50 / 0.24);
|
|
1287
|
+
pointer-events: auto;
|
|
1288
|
+
overflow: hidden;
|
|
1289
|
+
min-height: 0;
|
|
1290
|
+
border: 1px solid var(--glass-line);
|
|
1291
|
+
border-top: 0;
|
|
1292
|
+
border-radius: 0 0 1.125rem 1.125rem;
|
|
1293
|
+
background-color: var(--glass-fill);
|
|
1294
|
+
box-shadow: var(--glass-shadow);
|
|
1295
|
+
color: var(--glass-ink);
|
|
1296
|
+
font-size: 0.84375rem;
|
|
1297
|
+
}
|
|
1298
|
+
.dark [data-sidebar-glass-shell] {
|
|
1299
|
+
--glass-fill: rgb(36 30 62 / 0.97);
|
|
1300
|
+
--glass-thin-fill: rgb(36 30 62 / 0.72);
|
|
1301
|
+
--glass-filter: blur(24px) saturate(150%) brightness(0.8);
|
|
1302
|
+
--glass-ink: #f1effa;
|
|
1303
|
+
--glass-soft-ink: #dedaed; /* 4.5:1 on the thin fill alone over white, if a blur never paints */
|
|
1304
|
+
--glass-hover: rgb(255 255 255 / 0.1);
|
|
1305
|
+
--glass-line: rgb(255 255 255 / 0.14);
|
|
1306
|
+
--glass-shadow: 0 24px 60px rgb(0 0 0 / 0.5);
|
|
1307
|
+
}
|
|
1308
|
+
/* The less-transparency rule turns the blur off, rather than this rule asking for that
|
|
1309
|
+
setting's absence: a browser that does not know the media feature answers false to a
|
|
1310
|
+
not query as well, which left Safari and Firefox on the solid fill. */
|
|
1311
|
+
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
|
1312
|
+
[data-sidebar-glass-shell] {
|
|
1313
|
+
background-color: var(--glass-thin-fill);
|
|
1314
|
+
-webkit-backdrop-filter: var(--glass-filter);
|
|
1315
|
+
backdrop-filter: var(--glass-filter);
|
|
1316
|
+
}
|
|
1317
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
1318
|
+
[data-sidebar-glass-shell] {
|
|
1319
|
+
background-color: var(--glass-fill);
|
|
1320
|
+
-webkit-backdrop-filter: none;
|
|
1321
|
+
backdrop-filter: none;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
@media (max-width: 47.99rem) {
|
|
1326
|
+
[data-sidebar-glass] {
|
|
1327
|
+
--sidebar-glass-inset: 0.5rem;
|
|
1328
|
+
width: 100%;
|
|
1329
|
+
}
|
|
1330
|
+
[data-sidebar-glass-shell] { --glass-filter: blur(16px) saturate(150%); }
|
|
1331
|
+
.dark [data-sidebar-glass-shell] { --glass-filter: blur(16px) saturate(150%) brightness(0.8); }
|
|
1332
|
+
}
|
|
1333
|
+
[data-sidebar-glass-head] {
|
|
1334
|
+
padding: calc(var(--glass-section-gap) + 0.25rem) 0.875rem calc(var(--glass-section-gap) * 0.75) 1.125rem;
|
|
1335
|
+
border-bottom: 0;
|
|
1336
|
+
}
|
|
1337
|
+
[data-sidebar-glass-head] [data-link-sidebar-title],
|
|
1338
|
+
[data-sidebar-glass-head] [data-sidebar-panel-lead] > * {
|
|
1339
|
+
color: var(--glass-ink);
|
|
1340
|
+
font-size: 0.9375rem;
|
|
1341
|
+
font-weight: 600;
|
|
1342
|
+
}
|
|
1343
|
+
[data-sidebar-glass-head] [data-link-sidebar-close] { color: var(--glass-soft-ink); }
|
|
1344
|
+
[data-sidebar-glass-head] [data-link-sidebar-close]:hover {
|
|
1345
|
+
color: var(--glass-ink);
|
|
1346
|
+
background-color: var(--glass-hover);
|
|
1347
|
+
}
|
|
1348
|
+
[data-sidebar-glass-shell] nav { padding-block: 0; scrollbar-width: thin; scrollbar-color: var(--glass-line) transparent; }
|
|
1349
|
+
[data-sidebar-glass-shell] nav > [role="group"] { padding: 0 0.625rem var(--glass-section-gap); }
|
|
1350
|
+
[data-sidebar-glass-shell] [data-sidebar-group-header] {
|
|
1351
|
+
padding: calc(var(--glass-section-gap) * 0.5) 0.5rem 0.375rem;
|
|
1352
|
+
color: var(--glass-soft-ink);
|
|
1353
|
+
font-size: 0.6875rem;
|
|
1354
|
+
font-weight: 600;
|
|
1355
|
+
letter-spacing: normal;
|
|
1356
|
+
text-transform: none;
|
|
1357
|
+
}
|
|
1358
|
+
[data-sidebar-glass-shell] nav > [role="group"] > :is(a, [aria-disabled]),
|
|
1359
|
+
[data-sidebar-glass-foot] > a {
|
|
1360
|
+
height: var(--glass-row-height);
|
|
1361
|
+
gap: var(--glass-icon-gap);
|
|
1362
|
+
padding: 0 0.5rem 0 0.375rem;
|
|
1363
|
+
border-radius: 0.625rem;
|
|
1364
|
+
color: var(--glass-ink);
|
|
1365
|
+
font-size: inherit;
|
|
1366
|
+
font-weight: 500;
|
|
1367
|
+
line-height: 1.125rem;
|
|
1368
|
+
}
|
|
1369
|
+
[data-sidebar-glass-shell] nav > [role="group"] > :is(a, [aria-disabled]):hover,
|
|
1370
|
+
[data-sidebar-glass-foot] > a:hover {
|
|
1371
|
+
color: var(--glass-ink);
|
|
1372
|
+
background-color: var(--glass-hover);
|
|
1373
|
+
}
|
|
1374
|
+
[data-sidebar-glass-shell] [data-sidebar-row-desc] {
|
|
1375
|
+
color: var(--glass-soft-ink);
|
|
1376
|
+
font-size: 0.6875rem;
|
|
1377
|
+
line-height: 0.875rem;
|
|
1378
|
+
}
|
|
1379
|
+
[data-sidebar-glass-shell] nav > [role="group"] > a[aria-current="page"],
|
|
1380
|
+
[data-sidebar-glass-shell] nav > [role="group"] > a[aria-current="page"]:hover {
|
|
1381
|
+
/* A step down the primary's own scale: white on the hub's primary is 3.1:1, and this
|
|
1382
|
+
mix holds 4.5:1 and still reads as the brand's violet. */
|
|
1383
|
+
background-color: color-mix(in srgb, var(--color-primary-600) 65%, var(--color-primary-700));
|
|
1384
|
+
box-shadow: 0 4px 14px rgb(var(--color-primary-rgb) / 0.4);
|
|
1385
|
+
color: #fff;
|
|
1386
|
+
}
|
|
1387
|
+
/* The glow above outranks the row's focus ring, which is a box-shadow. This ring is an
|
|
1388
|
+
outline set inside the edge, so the row's own fill shows on both sides of it: at the
|
|
1389
|
+
edge, white would melt into the light theme's panel. */
|
|
1390
|
+
[data-sidebar-glass-shell] nav > [role="group"] > a[aria-current="page"]:focus-visible {
|
|
1391
|
+
outline: 2px solid #fff;
|
|
1392
|
+
outline-offset: -4px;
|
|
1393
|
+
}
|
|
1394
|
+
[data-sidebar-glass-shell] a[aria-current="page"] [data-sidebar-row-desc] { color: rgb(255 255 255 / 0.9); }
|
|
1395
|
+
[data-sidebar-glass-shell] nav > [role="group"] > a + a { margin-top: var(--glass-row-gap); }
|
|
1396
|
+
/* The fixed column: a line icon sits in the 24px box the engine's emoji and portrait rows
|
|
1397
|
+
already take (EMOJI_SWAP_CSS), so every label starts at one x. A line icon, a portrait
|
|
1398
|
+
and an emoji all draw at --glass-icon-size. The swap's box is 1.25em tall and clips, so
|
|
1399
|
+
the font size is what keeps an emoji or a portrait whole. A host's portrait class asks
|
|
1400
|
+
for 20px, and the helper's svg for 18. */
|
|
1401
|
+
[data-sidebar-glass-shell] [data-sidebar-row-icon] {
|
|
1402
|
+
display: inline-flex;
|
|
1403
|
+
align-items: center;
|
|
1404
|
+
justify-content: center;
|
|
1405
|
+
width: 1.5rem;
|
|
1406
|
+
height: var(--glass-icon-size);
|
|
1407
|
+
}
|
|
1408
|
+
[data-sidebar-glass-shell] [data-sidebar-row-icon] svg,
|
|
1409
|
+
[data-sidebar-glass-shell] nav > [role="group"] > a > :is(.studio-emoji-box, .studio-emoji-swap) img {
|
|
1410
|
+
width: var(--glass-icon-size);
|
|
1411
|
+
height: var(--glass-icon-size);
|
|
1412
|
+
}
|
|
1413
|
+
[data-sidebar-glass-shell] nav > [role="group"] > a > :is(.studio-emoji-box, .studio-emoji-swap) {
|
|
1414
|
+
font-size: calc(var(--glass-icon-size) / 1.25);
|
|
1415
|
+
}
|
|
1416
|
+
[data-sidebar-glass-foot] {
|
|
1417
|
+
padding: calc(var(--glass-section-gap) * 0.625) 0.625rem;
|
|
1418
|
+
border-top: 1px solid var(--glass-line);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/* THE GLASS SURFACES (Studio.surface_style = :glass, on the host's root as
|
|
1422
|
+
html[data-surface-style="glass"]). A card turns translucent over a soft ambient page.
|
|
1423
|
+
|
|
1424
|
+
THE AMBIENT PAGE IS WHAT MAKES IT READ. On a flat page a translucent card looks like a
|
|
1425
|
+
flat tinted one, so body::before lays low glows behind everything. It is one fixed layer
|
|
1426
|
+
behind the content, so a scroll does not repaint it.
|
|
1427
|
+
|
|
1428
|
+
TWO TIERS. A dense card (--surface-glass-card) gets fill, hairline, top highlight and
|
|
1429
|
+
soft shadow, with no backdrop-filter: a board holds a hundred of them. A card a host
|
|
1430
|
+
marks with .surface-glass-blur, or a page's few big panels, adds the real blur. The
|
|
1431
|
+
fill is the same in both, so a browser with no backdrop-filter draws the first tier.
|
|
1432
|
+
|
|
1433
|
+
WHAT IT REACHES: .card, .card-hover, .empty-state, and a hand-rolled panel spelled
|
|
1434
|
+
.bg-surface.rounded-xl.border. NOT a menu, popover or modal: anything .absolute, .fixed,
|
|
1435
|
+
.sticky or .shadow-2xl, or inside a .fixed scrim or a dialog, stays solid, because it
|
|
1436
|
+
floats over text and has no blur to protect it. Under prefers-reduced-transparency
|
|
1437
|
+
every surface goes solid. */
|
|
1438
|
+
html[data-surface-style="glass"] {
|
|
1439
|
+
--surface-glass-fill: color-mix(in srgb, var(--color-surface) 62%, transparent);
|
|
1440
|
+
--surface-glass-fill-hover: color-mix(in srgb, var(--color-surface) 80%, transparent);
|
|
1441
|
+
--surface-glass-line: rgb(29 24 50 / 0.1);
|
|
1442
|
+
--surface-glass-highlight: inset 0 1px 0 rgb(255 255 255 / 0.7);
|
|
1443
|
+
--surface-glass-shadow: 0 6px 20px rgb(60 50 120 / 0.08);
|
|
1444
|
+
--surface-glass-filter: blur(16px) saturate(140%);
|
|
1445
|
+
--surface-glass-ambient:
|
|
1446
|
+
radial-gradient(60vw 55vh at 10% 5%, rgb(var(--color-primary-rgb) / 0.17), transparent 70%),
|
|
1447
|
+
radial-gradient(50vw 60vh at 92% 30%, rgb(56 130 246 / 0.1), transparent 70%),
|
|
1448
|
+
radial-gradient(70vw 55vh at 45% 100%, rgb(var(--color-primary-rgb) / 0.14), transparent 70%);
|
|
1449
|
+
}
|
|
1450
|
+
html.dark[data-surface-style="glass"] {
|
|
1451
|
+
--surface-glass-fill: color-mix(in srgb, var(--color-surface) 52%, transparent);
|
|
1452
|
+
--surface-glass-fill-hover: color-mix(in srgb, var(--color-surface) 72%, transparent);
|
|
1453
|
+
--surface-glass-line: rgb(255 255 255 / 0.1);
|
|
1454
|
+
--surface-glass-highlight: inset 0 1px 0 rgb(255 255 255 / 0.08);
|
|
1455
|
+
--surface-glass-shadow: 0 8px 24px rgb(0 0 0 / 0.22);
|
|
1456
|
+
--surface-glass-ambient:
|
|
1457
|
+
radial-gradient(60vw 55vh at 10% 5%, rgb(var(--color-primary-rgb) / 0.2), transparent 70%),
|
|
1458
|
+
radial-gradient(50vw 60vh at 92% 30%, rgb(56 130 246 / 0.1), transparent 70%),
|
|
1459
|
+
radial-gradient(70vw 55vh at 45% 100%, rgb(var(--color-primary-rgb) / 0.14), transparent 70%);
|
|
1460
|
+
}
|
|
1461
|
+
html[data-surface-style="glass"] body::before {
|
|
1462
|
+
content: "";
|
|
1463
|
+
position: fixed;
|
|
1464
|
+
inset: 0;
|
|
1465
|
+
z-index: -1;
|
|
1466
|
+
pointer-events: none;
|
|
1467
|
+
background-image: var(--surface-glass-ambient);
|
|
1468
|
+
}
|
|
1469
|
+
/* A CARD KEEPS WHAT IT SAYS. Glass sets no border and no tone. The fill of .card, .card-hover and
|
|
1470
|
+
.empty-state is a token its own utility reads, so an authored bg-*, border-* or focus-within:*
|
|
1471
|
+
on the element wins as it always did. A hand-rolled panel has .bg-surface, so it has no tone to
|
|
1472
|
+
lose, and only its fill changes. The shadow list carries the element's own ring and shadow. */
|
|
1473
|
+
html[data-surface-style="glass"] { --surface-card-fill: var(--surface-glass-fill); }
|
|
1474
|
+
html[data-surface-style="glass"] :is(.card, .card-hover, .empty-state):is(.absolute, .fixed, .sticky, .shadow-2xl, .fixed *, [role="dialog"] *) { --surface-card-fill: var(--color-surface); }
|
|
1475
|
+
html[data-surface-style="glass"] :is(.card-hover, a.card):not(.absolute, .fixed, .sticky, .shadow-2xl, .fixed *, [role="dialog"] *):hover { --surface-card-fill: var(--surface-glass-fill-hover); }
|
|
1476
|
+
html[data-surface-style="glass"] .bg-surface.rounded-xl.border:not(.absolute, .fixed, .sticky, .shadow-2xl, .fixed *, [role="dialog"] *) { background-color: var(--surface-glass-fill); }
|
|
1477
|
+
html[data-surface-style="glass"] :is(.card, .card-hover, .empty-state, .bg-surface.rounded-xl.border):not(.absolute, .fixed, .sticky, .shadow-2xl, .fixed *, [role="dialog"] *) {
|
|
1478
|
+
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
|
|
1479
|
+
var(--surface-glass-highlight), var(--surface-glass-shadow), var(--tw-shadow, 0 0 #0000);
|
|
1480
|
+
}
|
|
1481
|
+
/* A backdrop-filter starts a stacking context and a containing block, so a tooltip inside
|
|
1482
|
+
a blurred card must lift the card above its neighbours. */
|
|
1483
|
+
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
|
1484
|
+
html[data-surface-style="glass"] .surface-glass-blur {
|
|
1485
|
+
-webkit-backdrop-filter: var(--surface-glass-filter);
|
|
1486
|
+
backdrop-filter: var(--surface-glass-filter);
|
|
1487
|
+
}
|
|
1488
|
+
html[data-surface-style="glass"] .surface-glass-blur:is(:hover, :focus-within) { position: relative; z-index: 2; }
|
|
1489
|
+
/* The navbar is a blurred bar already. Thinner, the ambient page runs under it and no
|
|
1490
|
+
hard edge marks where the page begins. */
|
|
1491
|
+
html[data-surface-style="glass"] .nav-shell {
|
|
1492
|
+
background-color: color-mix(in srgb, var(--color-page) 62%, transparent);
|
|
1493
|
+
-webkit-backdrop-filter: blur(18px) saturate(140%);
|
|
1494
|
+
backdrop-filter: blur(18px) saturate(140%);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
/* THE STICKY PAGE HEADER UNDER GLASS. Stuck, the shared rule fills it with surface grey.
|
|
1499
|
+
Here it is a bar like the sidebar: square against the navbar, round at the bottom, a
|
|
1500
|
+
hairline edge and the same top light. Opaque page color without a blur; thin with one.
|
|
1501
|
+
A failed page-header controller takes the same bar, as it takes the grey one today. */
|
|
1502
|
+
html[data-surface-style="glass"] .page-header:is([data-stuck], [data-studio-controller-failed~="page-header"] + .page-header) {
|
|
1503
|
+
background-color: var(--color-page);
|
|
1504
|
+
border-top-color: transparent;
|
|
1505
|
+
border-bottom-color: var(--surface-glass-line);
|
|
1506
|
+
border-radius: 0 0 0.875rem 0.875rem;
|
|
1507
|
+
box-shadow: var(--surface-glass-highlight), var(--surface-glass-shadow);
|
|
1508
|
+
}
|
|
1509
|
+
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
|
1510
|
+
html[data-surface-style="glass"] .page-header:is([data-stuck], [data-studio-controller-failed~="page-header"] + .page-header) {
|
|
1511
|
+
background-color: color-mix(in srgb, var(--color-page) 62%, transparent);
|
|
1512
|
+
-webkit-backdrop-filter: blur(18px) saturate(140%);
|
|
1513
|
+
backdrop-filter: blur(18px) saturate(140%);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
/* LIT FROM ABOVE, under the same opt-in: one light for buttons, badges and inputs, from the
|
|
1518
|
+
direction of the cards' top highlight. Four shadows: top light, bottom shade, contact, and
|
|
1519
|
+
a glow in the button's own color.
|
|
1520
|
+
|
|
1521
|
+
THE GRADIENT KEEPS ITS CHROMA. Both stops are built in oklch from the token, a brighter
|
|
1522
|
+
and an equally saturated top over a deep saturated bottom; mixing toward black or grey is
|
|
1523
|
+
what mutes a button. White text is held to 4.5:1 against the midpoint behind it, so the
|
|
1524
|
+
top stop reads lower, as it does in the rule this follows.
|
|
1525
|
+
|
|
1526
|
+
WHO GETS WHAT. Every full-size filled button: gradient, top, bottom, contact and a glow in
|
|
1527
|
+
its own color, on the same 0.15 lightness range. A .btn-sm has no glow, since a toolbar or
|
|
1528
|
+
a board of them would be noise. Neutral,
|
|
1529
|
+
outline and google, which includes a resting tab: top, bottom, contact, softer. Badges: a
|
|
1530
|
+
small top light and contact, no press. Inputs read pressed in. Disabled is flat.
|
|
1531
|
+
|
|
1532
|
+
THE PRESS flips the shadow to inset. Only a full-size button also moves 1px: translate
|
|
1533
|
+
takes no layout, but a toolbar row of small buttons should not jitter, and reduced motion
|
|
1534
|
+
never moves.
|
|
1535
|
+
|
|
1536
|
+
FOCUS IS AN OUTLINE on .btn, which no box-shadow here touches. Each list names the ring
|
|
1537
|
+
variables on the element itself: declared once on the root they would resolve there, to no
|
|
1538
|
+
ring. A button that spells its own bg-* or border-* keeps it.
|
|
1539
|
+
|
|
1540
|
+
EDGE TO EDGE. An inset box-shadow paints inside the padding box, so a 1px border leaves a
|
|
1541
|
+
strip where the light and the shade stop short. The inset shadows live on ::after, over
|
|
1542
|
+
the whole border box and on the same radius. The clear 1px border stays as a spacer, so
|
|
1543
|
+
every size, including a hand-sized .btn px-3 py-1, keeps its box and a selected tab is as
|
|
1544
|
+
tall as the neutral tabs beside it. Neutral paints its hairline there too. An outline
|
|
1545
|
+
button keeps its real border.
|
|
1546
|
+
|
|
1547
|
+
data-lit-state forces a state, for the specimens on the style guide. */
|
|
1548
|
+
html[data-surface-style="glass"] {
|
|
1549
|
+
--lit-top: inset 0 1px 0 rgb(255 255 255 / 0.35);
|
|
1550
|
+
--lit-bottom: inset 0 -2px 0 rgb(0 0 0 / 0.2);
|
|
1551
|
+
--lit-contact: 0 1px 2px rgb(0 0 0 / 0.4);
|
|
1552
|
+
--lit-glow: 0 8px 24px -6px;
|
|
1553
|
+
--lit-glow-alpha: 0.6;
|
|
1554
|
+
--lit-soft-top: inset 0 1px 0 rgb(255 255 255 / 0.8);
|
|
1555
|
+
--lit-soft-bottom: inset 0 -1px 0 rgb(29 24 50 / 0.08);
|
|
1556
|
+
--lit-pressed: inset 0 2px 4px rgb(0 0 0 / 0.27);
|
|
1557
|
+
--lit-soft-pressed: inset 0 2px 3px rgb(29 24 50 / 0.16);
|
|
1558
|
+
--lit-badge: inset 0 1px 0 rgb(255 255 255 / 0.5), 0 1px 1px rgb(29 24 50 / 0.12);
|
|
1559
|
+
--lit-input: inset 0 1px 2px rgb(29 24 50 / 0.14), inset 0 -1px 0 rgb(255 255 255 / 0.6);
|
|
1560
|
+
}
|
|
1561
|
+
html.dark[data-surface-style="glass"] {
|
|
1562
|
+
--lit-bottom: inset 0 -2px 0 rgb(0 0 0 / 0.3);
|
|
1563
|
+
--lit-contact: 0 1px 2px rgb(0 0 0 / 0.65);
|
|
1564
|
+
--lit-glow: 0 12px 28px -10px;
|
|
1565
|
+
--lit-glow-alpha: 0.8;
|
|
1566
|
+
--lit-soft-top: inset 0 1px 0 rgb(255 255 255 / 0.12);
|
|
1567
|
+
--lit-soft-bottom: inset 0 -1px 0 rgb(0 0 0 / 0.25);
|
|
1568
|
+
--lit-soft-pressed: inset 0 2px 3px rgb(0 0 0 / 0.4);
|
|
1569
|
+
--lit-badge: inset 0 1px 0 rgb(255 255 255 / 0.14), 0 1px 1px rgb(0 0 0 / 0.35);
|
|
1570
|
+
--lit-input: inset 0 1px 2px rgb(0 0 0 / 0.4), inset 0 -1px 0 rgb(255 255 255 / 0.06);
|
|
1571
|
+
}
|
|
1572
|
+
/* Stops: the app's primary is often too light for white, so it steps down its own lightness;
|
|
1573
|
+
the semantic fills already hold 4.5:1, so the midpoint stays on the fill. */
|
|
1574
|
+
html[data-surface-style="glass"] .btn-primary {
|
|
1575
|
+
--lit-top-stop: oklch(from var(--color-cta) calc(l - 0.03) calc(c + 0.03) h / var(--lit-fill-alpha, 1));
|
|
1576
|
+
--lit-low-stop: oklch(from var(--color-cta) calc(l - 0.18) calc(c + 0.05) h / var(--lit-fill-alpha, 1));
|
|
1577
|
+
}
|
|
1578
|
+
html[data-surface-style="glass"] :is(.btn-secondary, .btn-danger, .btn-warning, .btn-success) {
|
|
1579
|
+
--lit-top-stop: oklch(from var(--lit-fill) calc(l + 0.075) calc(c + 0.03) h / var(--lit-fill-alpha, 1));
|
|
1580
|
+
--lit-low-stop: oklch(from var(--lit-fill) calc(l - 0.075) calc(c + 0.03) h / var(--lit-fill-alpha, 1));
|
|
1581
|
+
}
|
|
1582
|
+
html[data-surface-style="glass"] .btn-secondary { --lit-fill: var(--btn-secondary-bg, var(--color-success)); }
|
|
1583
|
+
html[data-surface-style="glass"] .btn-danger { --lit-fill: var(--color-danger-fill, var(--color-danger)); }
|
|
1584
|
+
html[data-surface-style="glass"] .btn-warning { --lit-fill: var(--color-warning-fill, var(--color-warning)); }
|
|
1585
|
+
html[data-surface-style="glass"] .btn-success { --lit-fill: var(--color-success); }
|
|
1586
|
+
/* The spacer holds in every state, so a submit that disables its button moves nothing. */
|
|
1587
|
+
html[data-surface-style="glass"] :is(.btn-primary, .btn-secondary, .btn-danger, .btn-warning, .btn-success):not([class*=" bg-"], [class^="bg-"]) { border: 1px solid transparent; }
|
|
1588
|
+
html[data-surface-style="glass"] :is(.btn-primary, .btn-secondary, .btn-danger, .btn-warning, .btn-success):not(:disabled, [aria-disabled="true"], [class*=" bg-"], [class^="bg-"]) {
|
|
1589
|
+
background-origin: border-box;
|
|
1590
|
+
background-repeat: no-repeat;
|
|
1591
|
+
background-image: linear-gradient(var(--lit-top-stop), var(--lit-low-stop));
|
|
1592
|
+
--lit-inset: var(--lit-top), var(--lit-bottom);
|
|
1593
|
+
--lit-inset-pressed: var(--lit-pressed);
|
|
1594
|
+
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--lit-contact);
|
|
1595
|
+
}
|
|
1596
|
+
html[data-surface-style="glass"] :is(.btn-primary, .btn-secondary, .btn-danger, .btn-warning, .btn-success):not(.btn-sm, :disabled, [aria-disabled="true"]) {
|
|
1597
|
+
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--lit-contact),
|
|
1598
|
+
var(--lit-glow) oklch(from var(--lit-glow-stop, var(--lit-low-stop)) l c h / var(--lit-glow-alpha));
|
|
1599
|
+
}
|
|
1600
|
+
/* On a dark page a deep green, red or orange glow disappears, so these glow from the top
|
|
1601
|
+
stop. The violet is already bright enough at its low stop. */
|
|
1602
|
+
html.dark[data-surface-style="glass"] :is(.btn-secondary, .btn-danger, .btn-warning, .btn-success) { --lit-glow-stop: var(--lit-top-stop); }
|
|
1603
|
+
/* A TOUCH OF TRANSPARENCY on a full-size filled button, dark theme only: a photo behind it
|
|
1604
|
+
shows subtly, and white text holds 4.5:1 at the midpoint. An outer shadow is not painted
|
|
1605
|
+
under its own box, so the glow never brightens the fill. On a light page the backdrop would
|
|
1606
|
+
lift the fill and cost contrast, so it stays solid there. Without a blur, or without relative
|
|
1607
|
+
oklch (the gradient would drop out and leave white text on nothing), it stays solid. */
|
|
1608
|
+
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) and (color: oklch(from red l c h)) {
|
|
1609
|
+
html.dark[data-surface-style="glass"] :is(.btn-primary, .btn-secondary, .btn-danger, .btn-warning, .btn-success):not(.btn-sm, :disabled, [aria-disabled="true"], [class*=" bg-"], [class^="bg-"]) {
|
|
1610
|
+
--lit-fill-alpha: 0.94;
|
|
1611
|
+
background-color: transparent;
|
|
1612
|
+
-webkit-backdrop-filter: blur(6px);
|
|
1613
|
+
backdrop-filter: blur(6px);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
html[data-surface-style="glass"] .btn-primary:not(:disabled, [aria-disabled="true"]):is(:hover, [data-lit-state="hover"]) {
|
|
1617
|
+
filter: brightness(0.94);
|
|
1618
|
+
}
|
|
1619
|
+
html[data-surface-style="glass"] :is(.btn-neutral, .btn-google):not(:disabled, [aria-disabled="true"], [class*=" border-"]) {
|
|
1620
|
+
border-color: transparent;
|
|
1621
|
+
}
|
|
1622
|
+
html[data-surface-style="glass"] :is(.btn-neutral, .btn-google):not(:disabled, [aria-disabled="true"]) {
|
|
1623
|
+
--lit-inset: inset 0 0 0 1px var(--surface-glass-line), var(--lit-soft-top), var(--lit-soft-bottom);
|
|
1624
|
+
--lit-inset-pressed: inset 0 0 0 1px var(--surface-glass-line), var(--lit-soft-pressed);
|
|
1625
|
+
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--lit-contact);
|
|
1626
|
+
}
|
|
1627
|
+
html[data-surface-style="glass"] .btn-outline:not(:disabled, [aria-disabled="true"]) {
|
|
1628
|
+
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--lit-soft-top), var(--lit-soft-bottom), var(--lit-contact);
|
|
1629
|
+
}
|
|
1630
|
+
html[data-surface-style="glass"] .btn-outline:not(:disabled, [aria-disabled="true"]):is(:active, [data-lit-state="pressed"]) {
|
|
1631
|
+
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--lit-soft-pressed);
|
|
1632
|
+
}
|
|
1633
|
+
html[data-surface-style="glass"] :is(.btn-primary, .btn-secondary, .btn-danger, .btn-warning, .btn-success, .btn-neutral, .btn-google):not(:disabled, [aria-disabled="true"], .absolute, .fixed, .sticky) { position: relative; }
|
|
1634
|
+
html[data-surface-style="glass"] :is(.btn-primary, .btn-secondary, .btn-danger, .btn-warning, .btn-success, .btn-neutral, .btn-google):not(:disabled, [aria-disabled="true"])::after {
|
|
1635
|
+
content: "";
|
|
1636
|
+
position: absolute;
|
|
1637
|
+
inset: -1px;
|
|
1638
|
+
border-radius: inherit;
|
|
1639
|
+
pointer-events: none;
|
|
1640
|
+
box-shadow: var(--lit-inset);
|
|
1641
|
+
}
|
|
1642
|
+
html[data-surface-style="glass"] :is(.btn-primary, .btn-secondary, .btn-danger, .btn-warning, .btn-success, .btn-neutral, .btn-google):not(:disabled, [aria-disabled="true"]):is(:active, [data-lit-state="pressed"]) {
|
|
1643
|
+
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000);
|
|
1644
|
+
}
|
|
1645
|
+
html[data-surface-style="glass"] :is(.btn-primary, .btn-secondary, .btn-danger, .btn-warning, .btn-success, .btn-neutral, .btn-google):not(:disabled, [aria-disabled="true"]):is(:active, [data-lit-state="pressed"])::after {
|
|
1646
|
+
box-shadow: var(--lit-inset-pressed);
|
|
1647
|
+
}
|
|
1648
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
1649
|
+
html[data-surface-style="glass"] .btn:not(.btn-sm, :disabled, [aria-disabled="true"]):is(:active, [data-lit-state="pressed"]) { translate: 0 1px; }
|
|
1650
|
+
}
|
|
1651
|
+
html[data-surface-style="glass"] .btn[data-lit-state="focus"] {
|
|
1652
|
+
outline: 2px solid color-mix(in srgb, var(--color-cta) 70%, transparent);
|
|
1653
|
+
outline-offset: 2px;
|
|
1654
|
+
}
|
|
1655
|
+
html[data-surface-style="glass"] .badge { box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--lit-badge); }
|
|
1656
|
+
html[data-surface-style="glass"] .input-field { box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--lit-input); }
|
|
1657
|
+
|
|
1658
|
+
/* LESS TRANSPARENCY: every surface above goes solid and drops its blur. Last, to win on order;
|
|
1659
|
+
both spellings of the root, since html.dark outranks a bare html[...] on specificity. */
|
|
1660
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
1661
|
+
html[data-surface-style="glass"], html.dark[data-surface-style="glass"] { --surface-glass-fill: var(--color-surface); --surface-glass-fill-hover: var(--color-surface-alt); }
|
|
1662
|
+
html[data-surface-style="glass"] :is(.surface-glass-blur, .nav-shell, .page-header:is([data-stuck], [data-studio-controller-failed~="page-header"] + .page-header)),
|
|
1663
|
+
html.dark[data-surface-style="glass"] :is(.btn-primary, .btn-secondary, .btn-danger, .btn-warning, .btn-success):not(.btn-sm, :disabled, [aria-disabled="true"]) { --lit-fill-alpha: 1; -webkit-backdrop-filter: none; backdrop-filter: none; }
|
|
1664
|
+
html[data-surface-style="glass"] :is(.nav-shell, .page-header:is([data-stuck], [data-studio-controller-failed~="page-header"] + .page-header)) { background-color: var(--color-page); }
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1132
1667
|
/* THE SIDEBAR ROW'S DESCRIPTION (components/_link_sidebar, _geo_signpost). At rest
|
|
1133
1668
|
a row with a description is one line, the height of any other row. On hover or
|
|
1134
1669
|
keyboard focus the description fades in under the label, and no layout changes:
|
|
@@ -1157,23 +1692,23 @@ html[data-page-transition="unfold-top"]::view-transition-new(root) { animation-n
|
|
|
1157
1692
|
[data-sidebar-row-label] { transition: transform 140ms ease-in; }
|
|
1158
1693
|
[data-sidebar-row-described]:focus-visible [data-sidebar-row-label],
|
|
1159
1694
|
[data-sidebar-row-described][aria-disabled="true"]:focus [data-sidebar-row-label] {
|
|
1160
|
-
transform: translateY(-0.5rem);
|
|
1695
|
+
transform: translateY(calc(-1 * var(--sidebar-row-lift, 0.5rem)));
|
|
1161
1696
|
transition: transform 200ms ease-out 150ms;
|
|
1162
1697
|
}
|
|
1163
1698
|
[data-sidebar-row-described]:focus-visible [data-sidebar-row-desc],
|
|
1164
1699
|
[data-sidebar-row-described][aria-disabled="true"]:focus [data-sidebar-row-desc] {
|
|
1165
1700
|
opacity: 1;
|
|
1166
|
-
transform: translateY(-0.5rem);
|
|
1701
|
+
transform: translateY(calc(-1 * var(--sidebar-row-lift, 0.5rem)));
|
|
1167
1702
|
transition: opacity 200ms ease-out 150ms, transform 200ms ease-out 150ms;
|
|
1168
1703
|
}
|
|
1169
1704
|
@media (hover: hover) {
|
|
1170
1705
|
[data-sidebar-row-described]:hover [data-sidebar-row-label] {
|
|
1171
|
-
transform: translateY(-0.5rem);
|
|
1706
|
+
transform: translateY(calc(-1 * var(--sidebar-row-lift, 0.5rem)));
|
|
1172
1707
|
transition: transform 200ms ease-out 150ms;
|
|
1173
1708
|
}
|
|
1174
1709
|
[data-sidebar-row-described]:hover [data-sidebar-row-desc] {
|
|
1175
1710
|
opacity: 1;
|
|
1176
|
-
transform: translateY(-0.5rem);
|
|
1711
|
+
transform: translateY(calc(-1 * var(--sidebar-row-lift, 0.5rem)));
|
|
1177
1712
|
transition: opacity 200ms ease-out 150ms, transform 200ms ease-out 150ms;
|
|
1178
1713
|
}
|
|
1179
1714
|
}
|
|
@@ -1186,7 +1721,7 @@ html[data-page-transition="unfold-top"]::view-transition-new(root) { animation-n
|
|
|
1186
1721
|
[data-sidebar-row-described]:hover [data-sidebar-row-desc],
|
|
1187
1722
|
[data-sidebar-row-described]:focus-visible [data-sidebar-row-desc],
|
|
1188
1723
|
[data-sidebar-row-described][aria-disabled="true"]:focus [data-sidebar-row-desc] {
|
|
1189
|
-
transform: translateY(-0.5rem);
|
|
1724
|
+
transform: translateY(calc(-1 * var(--sidebar-row-lift, 0.5rem)));
|
|
1190
1725
|
transition: opacity 200ms ease-out 150ms;
|
|
1191
1726
|
}
|
|
1192
1727
|
}
|