Skip to content

Commit 9f4751e

Browse files
authored
feat(scroll): add scroll-driven animation utilities (#41)
1 parent 2cade1e commit 9f4751e

File tree

10 files changed

+435
-24
lines changed

10 files changed

+435
-24
lines changed

apps/docs/src/components/layout/BaseLayout.astro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const isHomePage = Astro.url.pathname === '/' || Astro.url.pathname === '';
6363
<link rel="preload" href={`${base}/fonts/jetbrains-mono-latin-400-normal.woff2`} as="font" type="font/woff2" crossorigin />
6464
</head>
6565
<body>
66+
<div s-scroll-progress aria-hidden="true"></div>
6667
<slot />
6768
{!isThemePage && !isHomePage && <ThemeCustomizer />}
6869
</body>

apps/docs/src/styles/docs.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ html {
6161

6262
.docs-header {
6363
position: sticky;
64-
top: 0;
64+
top: 3px; /* Make room for scroll progress bar */
6565
z-index: 100;
6666
height: var(--docs-header-height);
6767
display: flex;

apps/docs/tsconfig.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
"extends": "astro/tsconfigs/strict",
33
"compilerOptions": {
44
"strictNullChecks": true,
5-
"plugins": [
6-
{ "name": "@astrojs/ts-plugin" }
7-
]
5+
"plugins": [{ "name": "@astrojs/ts-plugin" }]
86
}
97
}

packages/core/scripts/build.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ function buildIndividualLayers(): void {
103103
{ name: 'flex', path: 'utils/flex.css' },
104104
{ name: 'typography', path: 'utils/typography.css' },
105105
{ name: 'visibility', path: 'utils/visibility.css' },
106+
{ name: 'scroll', path: 'utils/scroll.css' },
106107
];
107108

108109
// Create subdirectories

packages/core/src/components/card.css

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@
33
*
44
* Flexible card component with header, body, footer sections.
55
* Uses semantic attribute selectors and supports container queries.
6+
* Progressively enhanced with @scope for proper isolation of nested cards.
67
*
78
* Usage:
89
* <article s-card>Basic card content</article>
910
* <article s-card s-surface="raised">Elevated card</article>
1011
* <article s-card s-interactive>Clickable card</article>
12+
*
13+
* Browser Support:
14+
* - All browsers: Standard attribute selectors
15+
* - Chrome 118+, Safari 17.4+: @scope for nested card isolation
1116
*/
1217

1318
/* Base card - zero specificity for easy overrides */
@@ -85,6 +90,13 @@
8590
color: var(--s-text-secondary);
8691
}
8792

93+
/* Card actions (base fallback for browsers without @scope) */
94+
[s-card-actions] {
95+
display: flex;
96+
gap: var(--s-space-2);
97+
margin-top: var(--s-space-4);
98+
}
99+
88100
/* =============================================================================
89101
CARD VARIANTS
90102
============================================================================= */
@@ -262,3 +274,46 @@
262274
}
263275
}
264276
}
277+
278+
/* =============================================================================
279+
CSS @SCOPE - Nested Card Isolation (Progressive Enhancement)
280+
281+
When cards are nested inside other cards, this ensures that parent card
282+
styles (like borders and separators) don't bleed into child cards.
283+
Uses "donut scope" to exclude nested [s-card] elements.
284+
============================================================================= */
285+
286+
@supports (selector(:scope)) {
287+
@scope ([s-card]) to ([s-card]) {
288+
/**
289+
* Scoped card sections - these styles only apply to direct card sections,
290+
* not sections inside nested cards.
291+
*/
292+
293+
/* Header with bottom border - only for this card's header */
294+
[s-card-header] {
295+
border-bottom: 1px solid var(--_card-border, var(--s-border-muted));
296+
}
297+
298+
/* Footer with top border - only for this card's footer */
299+
[s-card-footer] {
300+
border-top: 1px solid var(--_card-border, var(--s-border-muted));
301+
}
302+
303+
/* Typography inherits from scoped card context */
304+
[s-card-title] {
305+
color: inherit;
306+
}
307+
308+
[s-card-subtitle] {
309+
color: var(--s-text-secondary);
310+
}
311+
312+
/* Actions container scoped to this card */
313+
[s-card-actions] {
314+
display: flex;
315+
gap: var(--s-space-2);
316+
margin-top: var(--s-space-4);
317+
}
318+
}
319+
}

packages/core/src/components/input.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,18 @@ textarea[s-input] {
9898
resize: vertical;
9999
}
100100

101+
/* Auto-sizing textarea - grows with content */
102+
textarea[s-input][s-autosize] {
103+
resize: none;
104+
105+
/* Progressive enhancement: field-sizing auto-grows with content */
106+
@supports (field-sizing: content) {
107+
field-sizing: content;
108+
min-height: 3lh; /* Minimum 3 lines using line-height units */
109+
max-height: 20lh; /* Maximum 20 lines before scrolling */
110+
}
111+
}
112+
101113
/* Select */
102114
select[s-input] {
103115
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");

packages/core/src/components/modal.css

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -61,49 +61,88 @@ dialog[s-modal]:not([open]) {
6161
color: var(--s-text-primary);
6262
}
6363

64-
/* Open state animation */
64+
/* Open state animation - using @starting-style for cleaner entry animations */
6565
dialog[s-modal][open] {
66-
animation: s-modal-scale-in var(--s-duration-200) var(--s-ease-out);
67-
}
68-
69-
/* Focus visible state for keyboard navigation */
70-
dialog[s-modal]:focus-visible {
71-
outline: 2px solid var(--s-focus-ring);
72-
outline-offset: 2px;
66+
opacity: 1;
67+
transform: scale(1) translateY(0);
68+
transition:
69+
opacity var(--s-duration-200) var(--s-ease-out),
70+
transform var(--s-duration-200) var(--s-ease-out),
71+
overlay var(--s-duration-200) var(--s-ease-out) allow-discrete,
72+
display var(--s-duration-200) var(--s-ease-out) allow-discrete;
7373
}
7474

75-
@keyframes s-modal-scale-in {
76-
from {
75+
/* Entry animation initial state - CSS @starting-style */
76+
@starting-style {
77+
dialog[s-modal][open] {
7778
opacity: 0;
7879
transform: scale(0.95) translateY(-10px);
7980
}
80-
to {
81-
opacity: 1;
82-
transform: scale(1) translateY(0);
81+
}
82+
83+
/* Fallback for browsers without @starting-style (using transition-behavior as proxy test) */
84+
@supports not (transition-behavior: allow-discrete) {
85+
dialog[s-modal][open] {
86+
animation: s-modal-scale-in var(--s-duration-200) var(--s-ease-out);
87+
}
88+
89+
@keyframes s-modal-scale-in {
90+
from {
91+
opacity: 0;
92+
transform: scale(0.95) translateY(-10px);
93+
}
94+
to {
95+
opacity: 1;
96+
transform: scale(1) translateY(0);
97+
}
8398
}
8499
}
85100

101+
/* Focus visible state for keyboard navigation */
102+
dialog[s-modal]:focus-visible {
103+
outline: 2px solid var(--s-focus-ring);
104+
outline-offset: 2px;
105+
}
106+
86107
/* =============================================================================
87108
BACKDROP STYLING
88109
============================================================================= */
89110

90111
dialog[s-modal]::backdrop {
91112
background: oklch(0 0 0 / 0.5);
92113
backdrop-filter: blur(4px);
93-
animation: s-modal-backdrop-in var(--s-duration-200) var(--s-ease-out);
114+
opacity: 1;
115+
transition:
116+
opacity var(--s-duration-200) var(--s-ease-out),
117+
overlay var(--s-duration-200) var(--s-ease-out) allow-discrete,
118+
display var(--s-duration-200) var(--s-ease-out) allow-discrete;
94119

95120
/* RCS: adaptive backdrop - tinted with brand hue */
96121
@supports (color: oklch(from red l c h)) {
97122
background: oklch(from var(--s-primary-500) 0.1 0.02 h / 0.5);
98123
}
99124
}
100125

101-
@keyframes s-modal-backdrop-in {
102-
from {
126+
/* Backdrop entry animation */
127+
@starting-style {
128+
dialog[s-modal]::backdrop {
103129
opacity: 0;
104130
}
105-
to {
106-
opacity: 1;
131+
}
132+
133+
/* Fallback for browsers without @starting-style (using transition-behavior as proxy test) */
134+
@supports not (transition-behavior: allow-discrete) {
135+
dialog[s-modal]::backdrop {
136+
animation: s-modal-backdrop-in var(--s-duration-200) var(--s-ease-out);
137+
}
138+
139+
@keyframes s-modal-backdrop-in {
140+
from {
141+
opacity: 0;
142+
}
143+
to {
144+
opacity: 1;
145+
}
107146
}
108147
}
109148

@@ -262,6 +301,18 @@ dialog[s-modal][s-position="right"] {
262301
ACCESSIBILITY & HIGH CONTRAST
263302
============================================================================= */
264303

304+
/* Respect reduced motion preferences */
305+
@media (prefers-reduced-motion: reduce) {
306+
dialog[s-modal][open] {
307+
transition: none;
308+
transform: none;
309+
}
310+
311+
dialog[s-modal]::backdrop {
312+
transition: none;
313+
}
314+
}
315+
265316
/* High contrast mode support */
266317
@media (forced-colors: active) {
267318
dialog[s-modal] {

packages/core/src/components/tooltip.css

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@
6565

6666
/* RCS: tethered tinting - derive border from tooltip background */
6767
@supports (color: oklch(from red l c h)) {
68-
border: 1px solid oklch(from var(--_tooltip-bg) max(calc(l - 0.1), 0.05) c h);
68+
border: 1px solid
69+
oklch(from var(--_tooltip-bg) max(calc(l - 0.1), 0.05) c h);
6970
}
7071

7172
/* Typography */
@@ -74,7 +75,10 @@
7475
line-height: var(--s-leading-tight);
7576
text-align: center;
7677
text-wrap: balance;
77-
min-width: min(100%, var(--_tooltip-max-width)); /* At least as wide as trigger, capped at max */
78+
min-width: min(
79+
100%,
80+
var(--_tooltip-max-width)
81+
); /* At least as wide as trigger, capped at max */
7882
max-width: var(--_tooltip-max-width);
7983

8084
/* Prevent tooltip from blocking interactions */

packages/core/src/index.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@
3333
@import "./utils/spacing.css" layer(shift.utils);
3434
@import "./utils/flex.css" layer(shift.utils);
3535
@import "./utils/typography.css" layer(shift.utils);
36+
@import "./utils/scroll.css" layer(shift.utils);

0 commit comments

Comments
 (0)