Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 470
feat(ui): add Mosaic hover, motionSafe, and alpha CSS utilities#8817
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
d49ea74ab96a8e6abf088da6991aFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| --- | ||
| --- | ||
alexcarpenter marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
This file was deleted.
Uh oh!
There was an error while loading. Please reload this page.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import { describe, expect, it } from 'vitest'; | ||
| import { defaultMosaicVariables, resolveVariables } from '../variables'; | ||
| import { alpha, hover, motionSafe } from '../utils'; | ||
| describe('hover', () => { | ||
| it('wraps styles in @media (hover: hover) and &:hover', () => { | ||
| expect(hover({ color: 'red' })).toEqual({ | ||
| '@media (hover: hover)': { | ||
| '&:hover': { color: 'red' }, | ||
| }, | ||
| }); | ||
| }); | ||
| it('passes complex style objects through unchanged', () => { | ||
| const styles = { backgroundColor: 'blue', opacity: 0.8, transform: 'scale(1.05)' }; | ||
| expect(hover(styles)).toEqual({ | ||
| '@media (hover: hover)': { | ||
| '&:hover': styles, | ||
| }, | ||
| }); | ||
| }); | ||
| }); | ||
| describe('motionSafe', () => { | ||
| it('wraps styles in @media (prefers-reduced-motion: no-preference)', () => { | ||
| expect(motionSafe({ transition: 'transform 200ms ease' })).toEqual({ | ||
| '@media (prefers-reduced-motion: no-preference)': { transition: 'transform 200ms ease' }, | ||
| }); | ||
| }); | ||
| }); | ||
| describe('alpha', () => { | ||
| it('produces a color-mix expression', () => { | ||
| expect(alpha('#000', 50)).toBe('color-mix(in oklab, #000 50%, transparent)'); | ||
| }); | ||
| it('accepts any CSS color string', () => { | ||
| expect(alpha('oklch(58.5% .233 277.117)', 80)).toBe( | ||
| 'color-mix(in oklab, oklch(58.5% .233 277.117) 80%, transparent)', | ||
| ); | ||
| }); | ||
| it('matches theme.alpha output for the same resolved color', () => { | ||
| const theme = resolveVariables(defaultMosaicVariables); | ||
| const resolvedColor = defaultMosaicVariables.color.primary; | ||
| expect(theme.alpha('primary', 60)).toBe(alpha(resolvedColor, 60)); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import type { StyleRule } from './cva'; | ||
| /** Wraps styles in `@media (hover: hover)` so they only apply on hover-capable devices. */ | ||
| export function hover(styles: StyleRule): StyleRule { | ||
| return { | ||
| '@media (hover: hover)': { | ||
| '&:hover': styles, | ||
| }, | ||
| }; | ||
| } | ||
| /** Wraps styles in `@media (prefers-reduced-motion: no-preference)` so animations only run when the user has not requested reduced motion. */ | ||
| export function motionSafe(styles: StyleRule): StyleRule { | ||
| return { | ||
| '@media (prefers-reduced-motion: no-preference)': styles, | ||
| }; | ||
| } | ||
| /** Applies opacity to any CSS color via `color-mix`. */ | ||
| export function alpha(color: string, opacity: number): string { | ||
| return `color-mix(in oklab, ${color} ${opacity}%, transparent)`; | ||
| } | ||
Comment on lines
+20
to
+22
| ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does the changeset just need an empty entry?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, since these are don't have any user facing changes atm we can use an empty changeset.