Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/mosaic-button-icon-color.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
62 changes: 10 additions & 52 deletions packages/swingset/src/stories/button.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -145,79 +145,37 @@ export function Shapes(props: Record<string, unknown>) {
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon
name='plus'
size='sm'
/>
</Button>
<Button
{...knobsAsProps(props)}
shape='square'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon name='plus' />
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon
name='plus'
size='sm'
/>
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon name='plus' />
</Button>
</div>
);
Expand Down
110 changes: 110 additions & 0 deletions packages/ui/src/mosaic/components/button/button.styles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,6 +36,18 @@ const primaryActive = `color-mix(in oklab, ${colorVars['--cl-color-primary']}, $
const negativeHover = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-foreground']} 12%)`;
const negativeActive = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-foreground']} 18%)`;

// An icon rests a step below its label and comes up to it on hover, so the label leads and the icon
// reads as supporting. Each is an opaque faded form of the cell's own text color, not an alpha of
// it: a translucent icon would pick up whatever sits behind the button and drift per surface.
//
// The achromatic foregrounds (`primary`, `neutral-foreground`) share the house faded gray. The two
// that carry hue fade toward the tint that belongs to them, and the light-on-fill pairs fade toward
// their own fill, which is the only backdrop they can ever sit on.
const iconFadedNeutral = colorVars['--cl-color-neutral-faded'];
const iconFadedNegative = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-faded']} 50%)`;
const iconFadedOnPrimary = `color-mix(in oklab, ${colorVars['--cl-color-primary-foreground']}, ${colorVars['--cl-color-primary']} 40%)`;
const iconFadedOnNegative = `color-mix(in oklab, ${colorVars['--cl-color-negative-foreground']}, ${colorVars['--cl-color-negative']} 40%)`;

// Interactive states are gated on `:enabled`: the `disabled` attribute blocks activation but
// not matching, and the button stays hit-testable so `cursor: not-allowed` renders and a
// wrapping tooltip still gets the pointer. Disabled keeps its resting fill and only dims.
Expand DownExpand Up@@ -149,9 +161,22 @@ export const styles = stylex.create({
// variant × color, one entry per cell of the design matrix, keyed `<variant>-<color>` so the
// component can index directly. Each cell is self-contained so it reads — and tunes — against
// the spec without tracing shared parts.
//
// `--_cl-icon-color` lives per cell rather than once in `base`: StyleX resolves a property to the
// last style that declares it, so a cell setting it would drop `base`'s hover branch wholesale
// rather than merge with it. `Icon` reads the var (`icon.styles.ts`) — StyleX can't emit a
// descendant rule, so the value crosses the element boundary as a custom property.
export const variants = stylex.create({
// The pressed state stays outside the hover media query so no-hover devices still get one.
'filled-primary': {
'--_cl-icon-color': {
default: iconFadedOnPrimary,
':enabled[data-open]': colorVars['--cl-color-primary-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary-foreground'],
},
},
backgroundColor: {
default: colorVars['--cl-color-primary'],
':enabled:active': primaryActive,
Expand All@@ -164,6 +189,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-primary-foreground'],
},
'filled-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-neutral-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
backgroundColor: {
default: neutralStep0,
':enabled:active': neutralStep2,
Expand All@@ -176,6 +209,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-neutral-foreground'],
},
'filled-negative': {
'--_cl-icon-color': {
default: iconFadedOnNegative,
':enabled[data-open]': colorVars['--cl-color-negative-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative-foreground'],
},
},
backgroundColor: {
default: colorVars['--cl-color-negative'],
':enabled:active': negativeActive,
Expand All@@ -192,6 +233,14 @@ export const variants = stylex.create({
// rises underneath it. Keeps the border opaque so it can't alpha-fade against an incoming
// fill, and leaves it independently themeable.
'outline-primary': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-primary'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary'],
},
},
borderColor: colorVars['--cl-color-border'],
backgroundColor: {
default: 'transparent',
Expand All@@ -205,6 +254,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-primary'],
},
'outline-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-neutral-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
borderColor: colorVars['--cl-color-border'],
backgroundColor: {
default: 'transparent',
Expand All@@ -218,6 +275,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-neutral-foreground'],
},
'outline-negative': {
'--_cl-icon-color': {
default: iconFadedNegative,
':enabled[data-open]': colorVars['--cl-color-negative'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative'],
},
},
borderColor: colorVars['--cl-color-border'],
backgroundColor: {
default: 'transparent',
Expand All@@ -232,6 +297,14 @@ export const variants = stylex.create({
},

'ghost-primary': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-primary'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary'],
},
},
backgroundColor: {
default: 'transparent',
':enabled:active': neutralStep1,
Expand All@@ -244,6 +317,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-primary'],
},
'ghost-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-neutral-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
backgroundColor: {
default: 'transparent',
':enabled:active': neutralStep1,
Expand All@@ -258,6 +339,14 @@ export const variants = stylex.create({
// The one ghost that tints instead of graying, so its pressed step walks its own faded
// fill toward the negative it carries rather than joining the gray ramp.
'ghost-negative': {
'--_cl-icon-color': {
default: iconFadedNegative,
':enabled[data-open]': colorVars['--cl-color-negative'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative'],
},
},
backgroundColor: {
default: 'transparent',
':enabled:active': `color-mix(in oklab, ${colorVars['--cl-color-negative-faded']}, ${colorVars['--cl-color-negative']} 8%)`,
Expand All@@ -273,6 +362,13 @@ export const variants = stylex.create({
// link opts out of the box the size axis sets — it reads as text, not a control. Per-side
// zeros for the same reason `shapeSquare` uses them.
'link-primary': {
'--_cl-icon-color': {
default: iconFadedNeutral,
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary'],
},
},
backgroundColor: 'transparent',
color: colorVars['--cl-color-primary'],
paddingInlineEnd: 0,
Expand All@@ -282,6 +378,13 @@ export const variants = stylex.create({
height: 'auto',
},
'link-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
backgroundColor: 'transparent',
color: colorVars['--cl-color-neutral-foreground'],
paddingInlineEnd: 0,
Expand All@@ -291,6 +394,13 @@ export const variants = stylex.create({
height: 'auto',
},
'link-negative': {
'--_cl-icon-color': {
default: iconFadedNegative,
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative'],
},
},
backgroundColor: 'transparent',
color: colorVars['--cl-color-negative'],
paddingInlineEnd: 0,
Expand Down
14 changes: 12 additions & 2 deletions packages/ui/src/mosaic/components/icon/icon.styles.ts
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
import * as stylex from '@stylexjs/stylex';

import { space } from '../../tokens.stylex';
import { durationVars, space } from '../../tokens.stylex';

export const styles = stylex.create({
base: { display: 'inline-block', flexShrink: 0 },
base: {
// A container that wants the icon at a different strength than its label writes
// `--_cl-icon-color` (see `button.styles.ts`); everywhere else the icon inherits as before.
color: 'var(--_cl-icon-color, currentColor)',
display: 'inline-block',
flexShrink: 0,
// Transitions don't inherit, so the container's own color transition doesn't animate this.
transitionDuration: durationVars['--cl-duration-fast'],
transitionProperty: 'color',
transitionTimingFunction: 'linear',
},
});

export const sizes = stylex.create({
Expand Down
5 changes: 0 additions & 5 deletions packages/ui/src/mosaic/components/menu/menu.styles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,9 +107,4 @@ export const styles = stylex.create({
backgroundColor: colorVars['--cl-color-border'],
blockSize: '1px',
},

triggerIcon: {
height: space['4'],
width: space['4'],
},
});
6 changes: 2 additions & 4 deletions packages/ui/src/mosaic/components/menu/menu.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,13 +10,11 @@ import { Menu as Primitive } from '@clerk/headless/menu';
import * as stylex from '@stylexjs/stylex';
import React from 'react';

import { iconRegistry } from '../../icons/registry';
import { mergeStyleProps, themeProps } from '../../props';
import { Button } from '../button';
import { Icon } from '../icon';
import { styles } from './menu.styles';

const EllipsisIcon = iconRegistry.ellipsis;

export type { MenuProps, MenuSeparatorProps, MenuTriggerProps };

/**
Expand DownExpand Up@@ -44,7 +42,7 @@ export const MenuTrigger = React.forwardRef<HTMLButtonElement, MenuTriggerProps>
{...mergeStyleProps(themeProps('menu-trigger'), className, style)}
{...rest}
>
{children ?? <EllipsisIcon {...stylex.props(styles.triggerIcon)} />}
{children ?? <Icon name='ellipsis' />}
</Primitive.Trigger>
);
});
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/mosaic-button-icon-color.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
62 changes: 10 additions & 52 deletions packages/swingset/src/stories/button.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -145,79 +145,37 @@ export function Shapes(props: Record<string, unknown>) {
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon
name='plus'
size='sm'
/>
</Button>
<Button
{...knobsAsProps(props)}
shape='square'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon name='plus' />
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon
name='plus'
size='sm'
/>
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon name='plus' />
</Button>
</div>
);
Expand Down
110 changes: 110 additions & 0 deletions packages/ui/src/mosaic/components/button/button.styles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,6 +36,18 @@ const primaryActive = `color-mix(in oklab, ${colorVars['--cl-color-primary']}, $
const negativeHover = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-foreground']} 12%)`;
const negativeActive = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-foreground']} 18%)`;

// An icon rests a step below its label and comes up to it on hover, so the label leads and the icon
// reads as supporting. Each is an opaque faded form of the cell's own text color, not an alpha of
// it: a translucent icon would pick up whatever sits behind the button and drift per surface.
//
// The achromatic foregrounds (`primary`, `neutral-foreground`) share the house faded gray. The two
// that carry hue fade toward the tint that belongs to them, and the light-on-fill pairs fade toward
// their own fill, which is the only backdrop they can ever sit on.
const iconFadedNeutral = colorVars['--cl-color-neutral-faded'];
const iconFadedNegative = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-faded']} 50%)`;
const iconFadedOnPrimary = `color-mix(in oklab, ${colorVars['--cl-color-primary-foreground']}, ${colorVars['--cl-color-primary']} 40%)`;
const iconFadedOnNegative = `color-mix(in oklab, ${colorVars['--cl-color-negative-foreground']}, ${colorVars['--cl-color-negative']} 40%)`;

// Interactive states are gated on `:enabled`: the `disabled` attribute blocks activation but
// not matching, and the button stays hit-testable so `cursor: not-allowed` renders and a
// wrapping tooltip still gets the pointer. Disabled keeps its resting fill and only dims.
Expand DownExpand Up@@ -149,9 +161,22 @@ export const styles = stylex.create({
// variant × color, one entry per cell of the design matrix, keyed `<variant>-<color>` so the
// component can index directly. Each cell is self-contained so it reads — and tunes — against
// the spec without tracing shared parts.
//
// `--_cl-icon-color` lives per cell rather than once in `base`: StyleX resolves a property to the
// last style that declares it, so a cell setting it would drop `base`'s hover branch wholesale
// rather than merge with it. `Icon` reads the var (`icon.styles.ts`) — StyleX can't emit a
// descendant rule, so the value crosses the element boundary as a custom property.
export const variants = stylex.create({
// The pressed state stays outside the hover media query so no-hover devices still get one.
'filled-primary': {
'--_cl-icon-color': {
default: iconFadedOnPrimary,
':enabled[data-open]': colorVars['--cl-color-primary-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary-foreground'],
},
},
backgroundColor: {
default: colorVars['--cl-color-primary'],
':enabled:active': primaryActive,
Expand All@@ -164,6 +189,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-primary-foreground'],
},
'filled-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-neutral-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
backgroundColor: {
default: neutralStep0,
':enabled:active': neutralStep2,
Expand All@@ -176,6 +209,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-neutral-foreground'],
},
'filled-negative': {
'--_cl-icon-color': {
default: iconFadedOnNegative,
':enabled[data-open]': colorVars['--cl-color-negative-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative-foreground'],
},
},
backgroundColor: {
default: colorVars['--cl-color-negative'],
':enabled:active': negativeActive,
Expand All@@ -192,6 +233,14 @@ export const variants = stylex.create({
// rises underneath it. Keeps the border opaque so it can't alpha-fade against an incoming
// fill, and leaves it independently themeable.
'outline-primary': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-primary'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary'],
},
},
borderColor: colorVars['--cl-color-border'],
backgroundColor: {
default: 'transparent',
Expand All@@ -205,6 +254,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-primary'],
},
'outline-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-neutral-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
borderColor: colorVars['--cl-color-border'],
backgroundColor: {
default: 'transparent',
Expand All@@ -218,6 +275,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-neutral-foreground'],
},
'outline-negative': {
'--_cl-icon-color': {
default: iconFadedNegative,
':enabled[data-open]': colorVars['--cl-color-negative'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative'],
},
},
borderColor: colorVars['--cl-color-border'],
backgroundColor: {
default: 'transparent',
Expand All@@ -232,6 +297,14 @@ export const variants = stylex.create({
},

'ghost-primary': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-primary'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary'],
},
},
backgroundColor: {
default: 'transparent',
':enabled:active': neutralStep1,
Expand All@@ -244,6 +317,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-primary'],
},
'ghost-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-neutral-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
backgroundColor: {
default: 'transparent',
':enabled:active': neutralStep1,
Expand All@@ -258,6 +339,14 @@ export const variants = stylex.create({
// The one ghost that tints instead of graying, so its pressed step walks its own faded
// fill toward the negative it carries rather than joining the gray ramp.
'ghost-negative': {
'--_cl-icon-color': {
default: iconFadedNegative,
':enabled[data-open]': colorVars['--cl-color-negative'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative'],
},
},
backgroundColor: {
default: 'transparent',
':enabled:active': `color-mix(in oklab, ${colorVars['--cl-color-negative-faded']}, ${colorVars['--cl-color-negative']} 8%)`,
Expand All@@ -273,6 +362,13 @@ export const variants = stylex.create({
// link opts out of the box the size axis sets — it reads as text, not a control. Per-side
// zeros for the same reason `shapeSquare` uses them.
'link-primary': {
'--_cl-icon-color': {
default: iconFadedNeutral,
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary'],
},
},
backgroundColor: 'transparent',
color: colorVars['--cl-color-primary'],
paddingInlineEnd: 0,
Expand All@@ -282,6 +378,13 @@ export const variants = stylex.create({
height: 'auto',
},
'link-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
backgroundColor: 'transparent',
color: colorVars['--cl-color-neutral-foreground'],
paddingInlineEnd: 0,
Expand All@@ -291,6 +394,13 @@ export const variants = stylex.create({
height: 'auto',
},
'link-negative': {
'--_cl-icon-color': {
default: iconFadedNegative,
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative'],
},
},
backgroundColor: 'transparent',
color: colorVars['--cl-color-negative'],
paddingInlineEnd: 0,
Expand Down
14 changes: 12 additions & 2 deletions packages/ui/src/mosaic/components/icon/icon.styles.ts
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
import * as stylex from '@stylexjs/stylex';

import { space } from '../../tokens.stylex';
import { durationVars, space } from '../../tokens.stylex';

export const styles = stylex.create({
base: { display: 'inline-block', flexShrink: 0 },
base: {
// A container that wants the icon at a different strength than its label writes
// `--_cl-icon-color` (see `button.styles.ts`); everywhere else the icon inherits as before.
color: 'var(--_cl-icon-color, currentColor)',
display: 'inline-block',
flexShrink: 0,
// Transitions don't inherit, so the container's own color transition doesn't animate this.
transitionDuration: durationVars['--cl-duration-fast'],
transitionProperty: 'color',
transitionTimingFunction: 'linear',
},
});

export const sizes = stylex.create({
Expand Down
5 changes: 0 additions & 5 deletions packages/ui/src/mosaic/components/menu/menu.styles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,9 +107,4 @@ export const styles = stylex.create({
backgroundColor: colorVars['--cl-color-border'],
blockSize: '1px',
},

triggerIcon: {
height: space['4'],
width: space['4'],
},
});
6 changes: 2 additions & 4 deletions packages/ui/src/mosaic/components/menu/menu.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,13 +10,11 @@ import { Menu as Primitive } from '@clerk/headless/menu';
import * as stylex from '@stylexjs/stylex';
import React from 'react';

import { iconRegistry } from '../../icons/registry';
import { mergeStyleProps, themeProps } from '../../props';
import { Button } from '../button';
import { Icon } from '../icon';
import { styles } from './menu.styles';

const EllipsisIcon = iconRegistry.ellipsis;

export type { MenuProps, MenuSeparatorProps, MenuTriggerProps };

/**
Expand DownExpand Up@@ -44,7 +42,7 @@ export const MenuTrigger = React.forwardRef<HTMLButtonElement, MenuTriggerProps>
{...mergeStyleProps(themeProps('menu-trigger'), className, style)}
{...rest}
>
{children ?? <EllipsisIcon {...stylex.props(styles.triggerIcon)} />}
{children ?? <Icon name='ellipsis' />}
</Primitive.Trigger>
);
});
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/mosaic-button-icon-color.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
62 changes: 10 additions & 52 deletions packages/swingset/src/stories/button.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -145,79 +145,37 @@ export function Shapes(props: Record<string, unknown>) {
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon
name='plus'
size='sm'
/>
</Button>
<Button
{...knobsAsProps(props)}
shape='square'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon name='plus' />
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon
name='plus'
size='sm'
/>
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon name='plus' />
</Button>
</div>
);
Expand Down
110 changes: 110 additions & 0 deletions packages/ui/src/mosaic/components/button/button.styles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,6 +36,18 @@ const primaryActive = `color-mix(in oklab, ${colorVars['--cl-color-primary']}, $
const negativeHover = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-foreground']} 12%)`;
const negativeActive = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-foreground']} 18%)`;

// An icon rests a step below its label and comes up to it on hover, so the label leads and the icon
// reads as supporting. Each is an opaque faded form of the cell's own text color, not an alpha of
// it: a translucent icon would pick up whatever sits behind the button and drift per surface.
//
// The achromatic foregrounds (`primary`, `neutral-foreground`) share the house faded gray. The two
// that carry hue fade toward the tint that belongs to them, and the light-on-fill pairs fade toward
// their own fill, which is the only backdrop they can ever sit on.
const iconFadedNeutral = colorVars['--cl-color-neutral-faded'];
const iconFadedNegative = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-faded']} 50%)`;
const iconFadedOnPrimary = `color-mix(in oklab, ${colorVars['--cl-color-primary-foreground']}, ${colorVars['--cl-color-primary']} 40%)`;
const iconFadedOnNegative = `color-mix(in oklab, ${colorVars['--cl-color-negative-foreground']}, ${colorVars['--cl-color-negative']} 40%)`;

// Interactive states are gated on `:enabled`: the `disabled` attribute blocks activation but
// not matching, and the button stays hit-testable so `cursor: not-allowed` renders and a
// wrapping tooltip still gets the pointer. Disabled keeps its resting fill and only dims.
Expand DownExpand Up@@ -149,9 +161,22 @@ export const styles = stylex.create({
// variant × color, one entry per cell of the design matrix, keyed `<variant>-<color>` so the
// component can index directly. Each cell is self-contained so it reads — and tunes — against
// the spec without tracing shared parts.
//
// `--_cl-icon-color` lives per cell rather than once in `base`: StyleX resolves a property to the
// last style that declares it, so a cell setting it would drop `base`'s hover branch wholesale
// rather than merge with it. `Icon` reads the var (`icon.styles.ts`) — StyleX can't emit a
// descendant rule, so the value crosses the element boundary as a custom property.
export const variants = stylex.create({
// The pressed state stays outside the hover media query so no-hover devices still get one.
'filled-primary': {
'--_cl-icon-color': {
default: iconFadedOnPrimary,
':enabled[data-open]': colorVars['--cl-color-primary-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary-foreground'],
},
},
backgroundColor: {
default: colorVars['--cl-color-primary'],
':enabled:active': primaryActive,
Expand All@@ -164,6 +189,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-primary-foreground'],
},
'filled-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-neutral-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
backgroundColor: {
default: neutralStep0,
':enabled:active': neutralStep2,
Expand All@@ -176,6 +209,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-neutral-foreground'],
},
'filled-negative': {
'--_cl-icon-color': {
default: iconFadedOnNegative,
':enabled[data-open]': colorVars['--cl-color-negative-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative-foreground'],
},
},
backgroundColor: {
default: colorVars['--cl-color-negative'],
':enabled:active': negativeActive,
Expand All@@ -192,6 +233,14 @@ export const variants = stylex.create({
// rises underneath it. Keeps the border opaque so it can't alpha-fade against an incoming
// fill, and leaves it independently themeable.
'outline-primary': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-primary'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary'],
},
},
borderColor: colorVars['--cl-color-border'],
backgroundColor: {
default: 'transparent',
Expand All@@ -205,6 +254,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-primary'],
},
'outline-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-neutral-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
borderColor: colorVars['--cl-color-border'],
backgroundColor: {
default: 'transparent',
Expand All@@ -218,6 +275,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-neutral-foreground'],
},
'outline-negative': {
'--_cl-icon-color': {
default: iconFadedNegative,
':enabled[data-open]': colorVars['--cl-color-negative'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative'],
},
},
borderColor: colorVars['--cl-color-border'],
backgroundColor: {
default: 'transparent',
Expand All@@ -232,6 +297,14 @@ export const variants = stylex.create({
},

'ghost-primary': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-primary'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary'],
},
},
backgroundColor: {
default: 'transparent',
':enabled:active': neutralStep1,
Expand All@@ -244,6 +317,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-primary'],
},
'ghost-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-neutral-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
backgroundColor: {
default: 'transparent',
':enabled:active': neutralStep1,
Expand All@@ -258,6 +339,14 @@ export const variants = stylex.create({
// The one ghost that tints instead of graying, so its pressed step walks its own faded
// fill toward the negative it carries rather than joining the gray ramp.
'ghost-negative': {
'--_cl-icon-color': {
default: iconFadedNegative,
':enabled[data-open]': colorVars['--cl-color-negative'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative'],
},
},
backgroundColor: {
default: 'transparent',
':enabled:active': `color-mix(in oklab, ${colorVars['--cl-color-negative-faded']}, ${colorVars['--cl-color-negative']} 8%)`,
Expand All@@ -273,6 +362,13 @@ export const variants = stylex.create({
// link opts out of the box the size axis sets — it reads as text, not a control. Per-side
// zeros for the same reason `shapeSquare` uses them.
'link-primary': {
'--_cl-icon-color': {
default: iconFadedNeutral,
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary'],
},
},
backgroundColor: 'transparent',
color: colorVars['--cl-color-primary'],
paddingInlineEnd: 0,
Expand All@@ -282,6 +378,13 @@ export const variants = stylex.create({
height: 'auto',
},
'link-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
backgroundColor: 'transparent',
color: colorVars['--cl-color-neutral-foreground'],
paddingInlineEnd: 0,
Expand All@@ -291,6 +394,13 @@ export const variants = stylex.create({
height: 'auto',
},
'link-negative': {
'--_cl-icon-color': {
default: iconFadedNegative,
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative'],
},
},
backgroundColor: 'transparent',
color: colorVars['--cl-color-negative'],
paddingInlineEnd: 0,
Expand Down
14 changes: 12 additions & 2 deletions packages/ui/src/mosaic/components/icon/icon.styles.ts
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
import * as stylex from '@stylexjs/stylex';

import { space } from '../../tokens.stylex';
import { durationVars, space } from '../../tokens.stylex';

export const styles = stylex.create({
base: { display: 'inline-block', flexShrink: 0 },
base: {
// A container that wants the icon at a different strength than its label writes
// `--_cl-icon-color` (see `button.styles.ts`); everywhere else the icon inherits as before.
color: 'var(--_cl-icon-color, currentColor)',
display: 'inline-block',
flexShrink: 0,
// Transitions don't inherit, so the container's own color transition doesn't animate this.
transitionDuration: durationVars['--cl-duration-fast'],
transitionProperty: 'color',
transitionTimingFunction: 'linear',
},
});

export const sizes = stylex.create({
Expand Down
5 changes: 0 additions & 5 deletions packages/ui/src/mosaic/components/menu/menu.styles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,9 +107,4 @@ export const styles = stylex.create({
backgroundColor: colorVars['--cl-color-border'],
blockSize: '1px',
},

triggerIcon: {
height: space['4'],
width: space['4'],
},
});
6 changes: 2 additions & 4 deletions packages/ui/src/mosaic/components/menu/menu.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,13 +10,11 @@ import { Menu as Primitive } from '@clerk/headless/menu';
import * as stylex from '@stylexjs/stylex';
import React from 'react';

import { iconRegistry } from '../../icons/registry';
import { mergeStyleProps, themeProps } from '../../props';
import { Button } from '../button';
import { Icon } from '../icon';
import { styles } from './menu.styles';

const EllipsisIcon = iconRegistry.ellipsis;

export type { MenuProps, MenuSeparatorProps, MenuTriggerProps };

/**
Expand DownExpand Up@@ -44,7 +42,7 @@ export const MenuTrigger = React.forwardRef<HTMLButtonElement, MenuTriggerProps>
{...mergeStyleProps(themeProps('menu-trigger'), className, style)}
{...rest}
>
{children ?? <EllipsisIcon {...stylex.props(styles.triggerIcon)} />}
{children ?? <Icon name='ellipsis' />}
</Primitive.Trigger>
);
});
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/mosaic-button-icon-color.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
62 changes: 10 additions & 52 deletions packages/swingset/src/stories/button.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -145,79 +145,37 @@ export function Shapes(props: Record<string, unknown>) {
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon
name='plus'
size='sm'
/>
</Button>
<Button
{...knobsAsProps(props)}
shape='square'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon name='plus' />
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon
name='plus'
size='sm'
/>
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon name='plus' />
</Button>
</div>
);
Expand Down
110 changes: 110 additions & 0 deletions packages/ui/src/mosaic/components/button/button.styles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,6 +36,18 @@ const primaryActive = `color-mix(in oklab, ${colorVars['--cl-color-primary']}, $
const negativeHover = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-foreground']} 12%)`;
const negativeActive = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-foreground']} 18%)`;

// An icon rests a step below its label and comes up to it on hover, so the label leads and the icon
// reads as supporting. Each is an opaque faded form of the cell's own text color, not an alpha of
// it: a translucent icon would pick up whatever sits behind the button and drift per surface.
//
// The achromatic foregrounds (`primary`, `neutral-foreground`) share the house faded gray. The two
// that carry hue fade toward the tint that belongs to them, and the light-on-fill pairs fade toward
// their own fill, which is the only backdrop they can ever sit on.
const iconFadedNeutral = colorVars['--cl-color-neutral-faded'];
const iconFadedNegative = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-faded']} 50%)`;
const iconFadedOnPrimary = `color-mix(in oklab, ${colorVars['--cl-color-primary-foreground']}, ${colorVars['--cl-color-primary']} 40%)`;
const iconFadedOnNegative = `color-mix(in oklab, ${colorVars['--cl-color-negative-foreground']}, ${colorVars['--cl-color-negative']} 40%)`;

// Interactive states are gated on `:enabled`: the `disabled` attribute blocks activation but
// not matching, and the button stays hit-testable so `cursor: not-allowed` renders and a
// wrapping tooltip still gets the pointer. Disabled keeps its resting fill and only dims.
Expand DownExpand Up@@ -149,9 +161,22 @@ export const styles = stylex.create({
// variant × color, one entry per cell of the design matrix, keyed `<variant>-<color>` so the
// component can index directly. Each cell is self-contained so it reads — and tunes — against
// the spec without tracing shared parts.
//
// `--_cl-icon-color` lives per cell rather than once in `base`: StyleX resolves a property to the
// last style that declares it, so a cell setting it would drop `base`'s hover branch wholesale
// rather than merge with it. `Icon` reads the var (`icon.styles.ts`) — StyleX can't emit a
// descendant rule, so the value crosses the element boundary as a custom property.
export const variants = stylex.create({
// The pressed state stays outside the hover media query so no-hover devices still get one.
'filled-primary': {
'--_cl-icon-color': {
default: iconFadedOnPrimary,
':enabled[data-open]': colorVars['--cl-color-primary-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary-foreground'],
},
},
backgroundColor: {
default: colorVars['--cl-color-primary'],
':enabled:active': primaryActive,
Expand All@@ -164,6 +189,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-primary-foreground'],
},
'filled-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-neutral-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
backgroundColor: {
default: neutralStep0,
':enabled:active': neutralStep2,
Expand All@@ -176,6 +209,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-neutral-foreground'],
},
'filled-negative': {
'--_cl-icon-color': {
default: iconFadedOnNegative,
':enabled[data-open]': colorVars['--cl-color-negative-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative-foreground'],
},
},
backgroundColor: {
default: colorVars['--cl-color-negative'],
':enabled:active': negativeActive,
Expand All@@ -192,6 +233,14 @@ export const variants = stylex.create({
// rises underneath it. Keeps the border opaque so it can't alpha-fade against an incoming
// fill, and leaves it independently themeable.
'outline-primary': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-primary'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary'],
},
},
borderColor: colorVars['--cl-color-border'],
backgroundColor: {
default: 'transparent',
Expand All@@ -205,6 +254,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-primary'],
},
'outline-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-neutral-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
borderColor: colorVars['--cl-color-border'],
backgroundColor: {
default: 'transparent',
Expand All@@ -218,6 +275,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-neutral-foreground'],
},
'outline-negative': {
'--_cl-icon-color': {
default: iconFadedNegative,
':enabled[data-open]': colorVars['--cl-color-negative'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative'],
},
},
borderColor: colorVars['--cl-color-border'],
backgroundColor: {
default: 'transparent',
Expand All@@ -232,6 +297,14 @@ export const variants = stylex.create({
},

'ghost-primary': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-primary'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary'],
},
},
backgroundColor: {
default: 'transparent',
':enabled:active': neutralStep1,
Expand All@@ -244,6 +317,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-primary'],
},
'ghost-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-neutral-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
backgroundColor: {
default: 'transparent',
':enabled:active': neutralStep1,
Expand All@@ -258,6 +339,14 @@ export const variants = stylex.create({
// The one ghost that tints instead of graying, so its pressed step walks its own faded
// fill toward the negative it carries rather than joining the gray ramp.
'ghost-negative': {
'--_cl-icon-color': {
default: iconFadedNegative,
':enabled[data-open]': colorVars['--cl-color-negative'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative'],
},
},
backgroundColor: {
default: 'transparent',
':enabled:active': `color-mix(in oklab, ${colorVars['--cl-color-negative-faded']}, ${colorVars['--cl-color-negative']} 8%)`,
Expand All@@ -273,6 +362,13 @@ export const variants = stylex.create({
// link opts out of the box the size axis sets — it reads as text, not a control. Per-side
// zeros for the same reason `shapeSquare` uses them.
'link-primary': {
'--_cl-icon-color': {
default: iconFadedNeutral,
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary'],
},
},
backgroundColor: 'transparent',
color: colorVars['--cl-color-primary'],
paddingInlineEnd: 0,
Expand All@@ -282,6 +378,13 @@ export const variants = stylex.create({
height: 'auto',
},
'link-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
backgroundColor: 'transparent',
color: colorVars['--cl-color-neutral-foreground'],
paddingInlineEnd: 0,
Expand All@@ -291,6 +394,13 @@ export const variants = stylex.create({
height: 'auto',
},
'link-negative': {
'--_cl-icon-color': {
default: iconFadedNegative,
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative'],
},
},
backgroundColor: 'transparent',
color: colorVars['--cl-color-negative'],
paddingInlineEnd: 0,
Expand Down
14 changes: 12 additions & 2 deletions packages/ui/src/mosaic/components/icon/icon.styles.ts
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
import * as stylex from '@stylexjs/stylex';

import { space } from '../../tokens.stylex';
import { durationVars, space } from '../../tokens.stylex';

export const styles = stylex.create({
base: { display: 'inline-block', flexShrink: 0 },
base: {
// A container that wants the icon at a different strength than its label writes
// `--_cl-icon-color` (see `button.styles.ts`); everywhere else the icon inherits as before.
color: 'var(--_cl-icon-color, currentColor)',
display: 'inline-block',
flexShrink: 0,
// Transitions don't inherit, so the container's own color transition doesn't animate this.
transitionDuration: durationVars['--cl-duration-fast'],
transitionProperty: 'color',
transitionTimingFunction: 'linear',
},
});

export const sizes = stylex.create({
Expand Down
5 changes: 0 additions & 5 deletions packages/ui/src/mosaic/components/menu/menu.styles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,9 +107,4 @@ export const styles = stylex.create({
backgroundColor: colorVars['--cl-color-border'],
blockSize: '1px',
},

triggerIcon: {
height: space['4'],
width: space['4'],
},
});
6 changes: 2 additions & 4 deletions packages/ui/src/mosaic/components/menu/menu.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,13 +10,11 @@ import { Menu as Primitive } from '@clerk/headless/menu';
import * as stylex from '@stylexjs/stylex';
import React from 'react';

import { iconRegistry } from '../../icons/registry';
import { mergeStyleProps, themeProps } from '../../props';
import { Button } from '../button';
import { Icon } from '../icon';
import { styles } from './menu.styles';

const EllipsisIcon = iconRegistry.ellipsis;

export type { MenuProps, MenuSeparatorProps, MenuTriggerProps };

/**
Expand DownExpand Up@@ -44,7 +42,7 @@ export const MenuTrigger = React.forwardRef<HTMLButtonElement, MenuTriggerProps>
{...mergeStyleProps(themeProps('menu-trigger'), className, style)}
{...rest}
>
{children ?? <EllipsisIcon {...stylex.props(styles.triggerIcon)} />}
{children ?? <Icon name='ellipsis' />}
</Primitive.Trigger>
);
});
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/mosaic-button-icon-color.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
62 changes: 10 additions & 52 deletions packages/swingset/src/stories/button.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -145,79 +145,37 @@ export function Shapes(props: Record<string, unknown>) {
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon
name='plus'
size='sm'
/>
</Button>
<Button
{...knobsAsProps(props)}
shape='square'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon name='plus' />
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon
name='plus'
size='sm'
/>
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon name='plus' />
</Button>
</div>
);
Expand Down
110 changes: 110 additions & 0 deletions packages/ui/src/mosaic/components/button/button.styles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,6 +36,18 @@ const primaryActive = `color-mix(in oklab, ${colorVars['--cl-color-primary']}, $
const negativeHover = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-foreground']} 12%)`;
const negativeActive = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-foreground']} 18%)`;

// An icon rests a step below its label and comes up to it on hover, so the label leads and the icon
// reads as supporting. Each is an opaque faded form of the cell's own text color, not an alpha of
// it: a translucent icon would pick up whatever sits behind the button and drift per surface.
//
// The achromatic foregrounds (`primary`, `neutral-foreground`) share the house faded gray. The two
// that carry hue fade toward the tint that belongs to them, and the light-on-fill pairs fade toward
// their own fill, which is the only backdrop they can ever sit on.
const iconFadedNeutral = colorVars['--cl-color-neutral-faded'];
const iconFadedNegative = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-faded']} 50%)`;
const iconFadedOnPrimary = `color-mix(in oklab, ${colorVars['--cl-color-primary-foreground']}, ${colorVars['--cl-color-primary']} 40%)`;
const iconFadedOnNegative = `color-mix(in oklab, ${colorVars['--cl-color-negative-foreground']}, ${colorVars['--cl-color-negative']} 40%)`;

// Interactive states are gated on `:enabled`: the `disabled` attribute blocks activation but
// not matching, and the button stays hit-testable so `cursor: not-allowed` renders and a
// wrapping tooltip still gets the pointer. Disabled keeps its resting fill and only dims.
Expand DownExpand Up@@ -149,9 +161,22 @@ export const styles = stylex.create({
// variant × color, one entry per cell of the design matrix, keyed `<variant>-<color>` so the
// component can index directly. Each cell is self-contained so it reads — and tunes — against
// the spec without tracing shared parts.
//
// `--_cl-icon-color` lives per cell rather than once in `base`: StyleX resolves a property to the
// last style that declares it, so a cell setting it would drop `base`'s hover branch wholesale
// rather than merge with it. `Icon` reads the var (`icon.styles.ts`) — StyleX can't emit a
// descendant rule, so the value crosses the element boundary as a custom property.
export const variants = stylex.create({
// The pressed state stays outside the hover media query so no-hover devices still get one.
'filled-primary': {
'--_cl-icon-color': {
default: iconFadedOnPrimary,
':enabled[data-open]': colorVars['--cl-color-primary-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary-foreground'],
},
},
backgroundColor: {
default: colorVars['--cl-color-primary'],
':enabled:active': primaryActive,
Expand All@@ -164,6 +189,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-primary-foreground'],
},
'filled-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-neutral-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
backgroundColor: {
default: neutralStep0,
':enabled:active': neutralStep2,
Expand All@@ -176,6 +209,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-neutral-foreground'],
},
'filled-negative': {
'--_cl-icon-color': {
default: iconFadedOnNegative,
':enabled[data-open]': colorVars['--cl-color-negative-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative-foreground'],
},
},
backgroundColor: {
default: colorVars['--cl-color-negative'],
':enabled:active': negativeActive,
Expand All@@ -192,6 +233,14 @@ export const variants = stylex.create({
// rises underneath it. Keeps the border opaque so it can't alpha-fade against an incoming
// fill, and leaves it independently themeable.
'outline-primary': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-primary'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary'],
},
},
borderColor: colorVars['--cl-color-border'],
backgroundColor: {
default: 'transparent',
Expand All@@ -205,6 +254,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-primary'],
},
'outline-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-neutral-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
borderColor: colorVars['--cl-color-border'],
backgroundColor: {
default: 'transparent',
Expand All@@ -218,6 +275,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-neutral-foreground'],
},
'outline-negative': {
'--_cl-icon-color': {
default: iconFadedNegative,
':enabled[data-open]': colorVars['--cl-color-negative'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative'],
},
},
borderColor: colorVars['--cl-color-border'],
backgroundColor: {
default: 'transparent',
Expand All@@ -232,6 +297,14 @@ export const variants = stylex.create({
},

'ghost-primary': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-primary'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary'],
},
},
backgroundColor: {
default: 'transparent',
':enabled:active': neutralStep1,
Expand All@@ -244,6 +317,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-primary'],
},
'ghost-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-neutral-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
backgroundColor: {
default: 'transparent',
':enabled:active': neutralStep1,
Expand All@@ -258,6 +339,14 @@ export const variants = stylex.create({
// The one ghost that tints instead of graying, so its pressed step walks its own faded
// fill toward the negative it carries rather than joining the gray ramp.
'ghost-negative': {
'--_cl-icon-color': {
default: iconFadedNegative,
':enabled[data-open]': colorVars['--cl-color-negative'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative'],
},
},
backgroundColor: {
default: 'transparent',
':enabled:active': `color-mix(in oklab, ${colorVars['--cl-color-negative-faded']}, ${colorVars['--cl-color-negative']} 8%)`,
Expand All@@ -273,6 +362,13 @@ export const variants = stylex.create({
// link opts out of the box the size axis sets — it reads as text, not a control. Per-side
// zeros for the same reason `shapeSquare` uses them.
'link-primary': {
'--_cl-icon-color': {
default: iconFadedNeutral,
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary'],
},
},
backgroundColor: 'transparent',
color: colorVars['--cl-color-primary'],
paddingInlineEnd: 0,
Expand All@@ -282,6 +378,13 @@ export const variants = stylex.create({
height: 'auto',
},
'link-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
backgroundColor: 'transparent',
color: colorVars['--cl-color-neutral-foreground'],
paddingInlineEnd: 0,
Expand All@@ -291,6 +394,13 @@ export const variants = stylex.create({
height: 'auto',
},
'link-negative': {
'--_cl-icon-color': {
default: iconFadedNegative,
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative'],
},
},
backgroundColor: 'transparent',
color: colorVars['--cl-color-negative'],
paddingInlineEnd: 0,
Expand Down
14 changes: 12 additions & 2 deletions packages/ui/src/mosaic/components/icon/icon.styles.ts
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
import * as stylex from '@stylexjs/stylex';

import { space } from '../../tokens.stylex';
import { durationVars, space } from '../../tokens.stylex';

export const styles = stylex.create({
base: { display: 'inline-block', flexShrink: 0 },
base: {
// A container that wants the icon at a different strength than its label writes
// `--_cl-icon-color` (see `button.styles.ts`); everywhere else the icon inherits as before.
color: 'var(--_cl-icon-color, currentColor)',
display: 'inline-block',
flexShrink: 0,
// Transitions don't inherit, so the container's own color transition doesn't animate this.
transitionDuration: durationVars['--cl-duration-fast'],
transitionProperty: 'color',
transitionTimingFunction: 'linear',
},
});

export const sizes = stylex.create({
Expand Down
5 changes: 0 additions & 5 deletions packages/ui/src/mosaic/components/menu/menu.styles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,9 +107,4 @@ export const styles = stylex.create({
backgroundColor: colorVars['--cl-color-border'],
blockSize: '1px',
},

triggerIcon: {
height: space['4'],
width: space['4'],
},
});
6 changes: 2 additions & 4 deletions packages/ui/src/mosaic/components/menu/menu.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,13 +10,11 @@ import { Menu as Primitive } from '@clerk/headless/menu';
import * as stylex from '@stylexjs/stylex';
import React from 'react';

import { iconRegistry } from '../../icons/registry';
import { mergeStyleProps, themeProps } from '../../props';
import { Button } from '../button';
import { Icon } from '../icon';
import { styles } from './menu.styles';

const EllipsisIcon = iconRegistry.ellipsis;

export type { MenuProps, MenuSeparatorProps, MenuTriggerProps };

/**
Expand DownExpand Up@@ -44,7 +42,7 @@ export const MenuTrigger = React.forwardRef<HTMLButtonElement, MenuTriggerProps>
{...mergeStyleProps(themeProps('menu-trigger'), className, style)}
{...rest}
>
{children ?? <EllipsisIcon {...stylex.props(styles.triggerIcon)} />}
{children ?? <Icon name='ellipsis' />}
</Primitive.Trigger>
);
});
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/mosaic-button-icon-color.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
62 changes: 10 additions & 52 deletions packages/swingset/src/stories/button.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -145,79 +145,37 @@ export function Shapes(props: Record<string, unknown>) {
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon
name='plus'
size='sm'
/>
</Button>
<Button
{...knobsAsProps(props)}
shape='square'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon name='plus' />
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon
name='plus'
size='sm'
/>
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon name='plus' />
</Button>
</div>
);
Expand Down
110 changes: 110 additions & 0 deletions packages/ui/src/mosaic/components/button/button.styles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,6 +36,18 @@ const primaryActive = `color-mix(in oklab, ${colorVars['--cl-color-primary']}, $
const negativeHover = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-foreground']} 12%)`;
const negativeActive = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-foreground']} 18%)`;

// An icon rests a step below its label and comes up to it on hover, so the label leads and the icon
// reads as supporting. Each is an opaque faded form of the cell's own text color, not an alpha of
// it: a translucent icon would pick up whatever sits behind the button and drift per surface.
//
// The achromatic foregrounds (`primary`, `neutral-foreground`) share the house faded gray. The two
// that carry hue fade toward the tint that belongs to them, and the light-on-fill pairs fade toward
// their own fill, which is the only backdrop they can ever sit on.
const iconFadedNeutral = colorVars['--cl-color-neutral-faded'];
const iconFadedNegative = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-faded']} 50%)`;
const iconFadedOnPrimary = `color-mix(in oklab, ${colorVars['--cl-color-primary-foreground']}, ${colorVars['--cl-color-primary']} 40%)`;
const iconFadedOnNegative = `color-mix(in oklab, ${colorVars['--cl-color-negative-foreground']}, ${colorVars['--cl-color-negative']} 40%)`;

// Interactive states are gated on `:enabled`: the `disabled` attribute blocks activation but
// not matching, and the button stays hit-testable so `cursor: not-allowed` renders and a
// wrapping tooltip still gets the pointer. Disabled keeps its resting fill and only dims.
Expand DownExpand Up@@ -149,9 +161,22 @@ export const styles = stylex.create({
// variant × color, one entry per cell of the design matrix, keyed `<variant>-<color>` so the
// component can index directly. Each cell is self-contained so it reads — and tunes — against
// the spec without tracing shared parts.
//
// `--_cl-icon-color` lives per cell rather than once in `base`: StyleX resolves a property to the
// last style that declares it, so a cell setting it would drop `base`'s hover branch wholesale
// rather than merge with it. `Icon` reads the var (`icon.styles.ts`) — StyleX can't emit a
// descendant rule, so the value crosses the element boundary as a custom property.
export const variants = stylex.create({
// The pressed state stays outside the hover media query so no-hover devices still get one.
'filled-primary': {
'--_cl-icon-color': {
default: iconFadedOnPrimary,
':enabled[data-open]': colorVars['--cl-color-primary-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary-foreground'],
},
},
backgroundColor: {
default: colorVars['--cl-color-primary'],
':enabled:active': primaryActive,
Expand All@@ -164,6 +189,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-primary-foreground'],
},
'filled-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-neutral-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
backgroundColor: {
default: neutralStep0,
':enabled:active': neutralStep2,
Expand All@@ -176,6 +209,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-neutral-foreground'],
},
'filled-negative': {
'--_cl-icon-color': {
default: iconFadedOnNegative,
':enabled[data-open]': colorVars['--cl-color-negative-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative-foreground'],
},
},
backgroundColor: {
default: colorVars['--cl-color-negative'],
':enabled:active': negativeActive,
Expand All@@ -192,6 +233,14 @@ export const variants = stylex.create({
// rises underneath it. Keeps the border opaque so it can't alpha-fade against an incoming
// fill, and leaves it independently themeable.
'outline-primary': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-primary'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary'],
},
},
borderColor: colorVars['--cl-color-border'],
backgroundColor: {
default: 'transparent',
Expand All@@ -205,6 +254,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-primary'],
},
'outline-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-neutral-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
borderColor: colorVars['--cl-color-border'],
backgroundColor: {
default: 'transparent',
Expand All@@ -218,6 +275,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-neutral-foreground'],
},
'outline-negative': {
'--_cl-icon-color': {
default: iconFadedNegative,
':enabled[data-open]': colorVars['--cl-color-negative'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative'],
},
},
borderColor: colorVars['--cl-color-border'],
backgroundColor: {
default: 'transparent',
Expand All@@ -232,6 +297,14 @@ export const variants = stylex.create({
},

'ghost-primary': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-primary'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary'],
},
},
backgroundColor: {
default: 'transparent',
':enabled:active': neutralStep1,
Expand All@@ -244,6 +317,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-primary'],
},
'ghost-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-neutral-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
backgroundColor: {
default: 'transparent',
':enabled:active': neutralStep1,
Expand All@@ -258,6 +339,14 @@ export const variants = stylex.create({
// The one ghost that tints instead of graying, so its pressed step walks its own faded
// fill toward the negative it carries rather than joining the gray ramp.
'ghost-negative': {
'--_cl-icon-color': {
default: iconFadedNegative,
':enabled[data-open]': colorVars['--cl-color-negative'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative'],
},
},
backgroundColor: {
default: 'transparent',
':enabled:active': `color-mix(in oklab, ${colorVars['--cl-color-negative-faded']}, ${colorVars['--cl-color-negative']} 8%)`,
Expand All@@ -273,6 +362,13 @@ export const variants = stylex.create({
// link opts out of the box the size axis sets — it reads as text, not a control. Per-side
// zeros for the same reason `shapeSquare` uses them.
'link-primary': {
'--_cl-icon-color': {
default: iconFadedNeutral,
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary'],
},
},
backgroundColor: 'transparent',
color: colorVars['--cl-color-primary'],
paddingInlineEnd: 0,
Expand All@@ -282,6 +378,13 @@ export const variants = stylex.create({
height: 'auto',
},
'link-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
backgroundColor: 'transparent',
color: colorVars['--cl-color-neutral-foreground'],
paddingInlineEnd: 0,
Expand All@@ -291,6 +394,13 @@ export const variants = stylex.create({
height: 'auto',
},
'link-negative': {
'--_cl-icon-color': {
default: iconFadedNegative,
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative'],
},
},
backgroundColor: 'transparent',
color: colorVars['--cl-color-negative'],
paddingInlineEnd: 0,
Expand Down
14 changes: 12 additions & 2 deletions packages/ui/src/mosaic/components/icon/icon.styles.ts
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
import * as stylex from '@stylexjs/stylex';

import { space } from '../../tokens.stylex';
import { durationVars, space } from '../../tokens.stylex';

export const styles = stylex.create({
base: { display: 'inline-block', flexShrink: 0 },
base: {
// A container that wants the icon at a different strength than its label writes
// `--_cl-icon-color` (see `button.styles.ts`); everywhere else the icon inherits as before.
color: 'var(--_cl-icon-color, currentColor)',
display: 'inline-block',
flexShrink: 0,
// Transitions don't inherit, so the container's own color transition doesn't animate this.
transitionDuration: durationVars['--cl-duration-fast'],
transitionProperty: 'color',
transitionTimingFunction: 'linear',
},
});

export const sizes = stylex.create({
Expand Down
5 changes: 0 additions & 5 deletions packages/ui/src/mosaic/components/menu/menu.styles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,9 +107,4 @@ export const styles = stylex.create({
backgroundColor: colorVars['--cl-color-border'],
blockSize: '1px',
},

triggerIcon: {
height: space['4'],
width: space['4'],
},
});
6 changes: 2 additions & 4 deletions packages/ui/src/mosaic/components/menu/menu.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,13 +10,11 @@ import { Menu as Primitive } from '@clerk/headless/menu';
import * as stylex from '@stylexjs/stylex';
import React from 'react';

import { iconRegistry } from '../../icons/registry';
import { mergeStyleProps, themeProps } from '../../props';
import { Button } from '../button';
import { Icon } from '../icon';
import { styles } from './menu.styles';

const EllipsisIcon = iconRegistry.ellipsis;

export type { MenuProps, MenuSeparatorProps, MenuTriggerProps };

/**
Expand DownExpand Up@@ -44,7 +42,7 @@ export const MenuTrigger = React.forwardRef<HTMLButtonElement, MenuTriggerProps>
{...mergeStyleProps(themeProps('menu-trigger'), className, style)}
{...rest}
>
{children ?? <EllipsisIcon {...stylex.props(styles.triggerIcon)} />}
{children ?? <Icon name='ellipsis' />}
</Primitive.Trigger>
);
});
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/mosaic-button-icon-color.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
62 changes: 10 additions & 52 deletions packages/swingset/src/stories/button.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -145,79 +145,37 @@ export function Shapes(props: Record<string, unknown>) {
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon
name='plus'
size='sm'
/>
</Button>
<Button
{...knobsAsProps(props)}
shape='square'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon name='plus' />
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon
name='plus'
size='sm'
/>
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon name='plus' />
</Button>
</div>
);
Expand Down
110 changes: 110 additions & 0 deletions packages/ui/src/mosaic/components/button/button.styles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,6 +36,18 @@ const primaryActive = `color-mix(in oklab, ${colorVars['--cl-color-primary']}, $
const negativeHover = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-foreground']} 12%)`;
const negativeActive = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-foreground']} 18%)`;

// An icon rests a step below its label and comes up to it on hover, so the label leads and the icon
// reads as supporting. Each is an opaque faded form of the cell's own text color, not an alpha of
// it: a translucent icon would pick up whatever sits behind the button and drift per surface.
//
// The achromatic foregrounds (`primary`, `neutral-foreground`) share the house faded gray. The two
// that carry hue fade toward the tint that belongs to them, and the light-on-fill pairs fade toward
// their own fill, which is the only backdrop they can ever sit on.
const iconFadedNeutral = colorVars['--cl-color-neutral-faded'];
const iconFadedNegative = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-faded']} 50%)`;
const iconFadedOnPrimary = `color-mix(in oklab, ${colorVars['--cl-color-primary-foreground']}, ${colorVars['--cl-color-primary']} 40%)`;
const iconFadedOnNegative = `color-mix(in oklab, ${colorVars['--cl-color-negative-foreground']}, ${colorVars['--cl-color-negative']} 40%)`;

// Interactive states are gated on `:enabled`: the `disabled` attribute blocks activation but
// not matching, and the button stays hit-testable so `cursor: not-allowed` renders and a
// wrapping tooltip still gets the pointer. Disabled keeps its resting fill and only dims.
Expand DownExpand Up@@ -149,9 +161,22 @@ export const styles = stylex.create({
// variant × color, one entry per cell of the design matrix, keyed `<variant>-<color>` so the
// component can index directly. Each cell is self-contained so it reads — and tunes — against
// the spec without tracing shared parts.
//
// `--_cl-icon-color` lives per cell rather than once in `base`: StyleX resolves a property to the
// last style that declares it, so a cell setting it would drop `base`'s hover branch wholesale
// rather than merge with it. `Icon` reads the var (`icon.styles.ts`) — StyleX can't emit a
// descendant rule, so the value crosses the element boundary as a custom property.
export const variants = stylex.create({
// The pressed state stays outside the hover media query so no-hover devices still get one.
'filled-primary': {
'--_cl-icon-color': {
default: iconFadedOnPrimary,
':enabled[data-open]': colorVars['--cl-color-primary-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary-foreground'],
},
},
backgroundColor: {
default: colorVars['--cl-color-primary'],
':enabled:active': primaryActive,
Expand All@@ -164,6 +189,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-primary-foreground'],
},
'filled-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-neutral-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
backgroundColor: {
default: neutralStep0,
':enabled:active': neutralStep2,
Expand All@@ -176,6 +209,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-neutral-foreground'],
},
'filled-negative': {
'--_cl-icon-color': {
default: iconFadedOnNegative,
':enabled[data-open]': colorVars['--cl-color-negative-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative-foreground'],
},
},
backgroundColor: {
default: colorVars['--cl-color-negative'],
':enabled:active': negativeActive,
Expand All@@ -192,6 +233,14 @@ export const variants = stylex.create({
// rises underneath it. Keeps the border opaque so it can't alpha-fade against an incoming
// fill, and leaves it independently themeable.
'outline-primary': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-primary'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary'],
},
},
borderColor: colorVars['--cl-color-border'],
backgroundColor: {
default: 'transparent',
Expand All@@ -205,6 +254,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-primary'],
},
'outline-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-neutral-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
borderColor: colorVars['--cl-color-border'],
backgroundColor: {
default: 'transparent',
Expand All@@ -218,6 +275,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-neutral-foreground'],
},
'outline-negative': {
'--_cl-icon-color': {
default: iconFadedNegative,
':enabled[data-open]': colorVars['--cl-color-negative'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative'],
},
},
borderColor: colorVars['--cl-color-border'],
backgroundColor: {
default: 'transparent',
Expand All@@ -232,6 +297,14 @@ export const variants = stylex.create({
},

'ghost-primary': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-primary'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary'],
},
},
backgroundColor: {
default: 'transparent',
':enabled:active': neutralStep1,
Expand All@@ -244,6 +317,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-primary'],
},
'ghost-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-neutral-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
backgroundColor: {
default: 'transparent',
':enabled:active': neutralStep1,
Expand All@@ -258,6 +339,14 @@ export const variants = stylex.create({
// The one ghost that tints instead of graying, so its pressed step walks its own faded
// fill toward the negative it carries rather than joining the gray ramp.
'ghost-negative': {
'--_cl-icon-color': {
default: iconFadedNegative,
':enabled[data-open]': colorVars['--cl-color-negative'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative'],
},
},
backgroundColor: {
default: 'transparent',
':enabled:active': `color-mix(in oklab, ${colorVars['--cl-color-negative-faded']}, ${colorVars['--cl-color-negative']} 8%)`,
Expand All@@ -273,6 +362,13 @@ export const variants = stylex.create({
// link opts out of the box the size axis sets — it reads as text, not a control. Per-side
// zeros for the same reason `shapeSquare` uses them.
'link-primary': {
'--_cl-icon-color': {
default: iconFadedNeutral,
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary'],
},
},
backgroundColor: 'transparent',
color: colorVars['--cl-color-primary'],
paddingInlineEnd: 0,
Expand All@@ -282,6 +378,13 @@ export const variants = stylex.create({
height: 'auto',
},
'link-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
backgroundColor: 'transparent',
color: colorVars['--cl-color-neutral-foreground'],
paddingInlineEnd: 0,
Expand All@@ -291,6 +394,13 @@ export const variants = stylex.create({
height: 'auto',
},
'link-negative': {
'--_cl-icon-color': {
default: iconFadedNegative,
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative'],
},
},
backgroundColor: 'transparent',
color: colorVars['--cl-color-negative'],
paddingInlineEnd: 0,
Expand Down
14 changes: 12 additions & 2 deletions packages/ui/src/mosaic/components/icon/icon.styles.ts
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
import * as stylex from '@stylexjs/stylex';

import { space } from '../../tokens.stylex';
import { durationVars, space } from '../../tokens.stylex';

export const styles = stylex.create({
base: { display: 'inline-block', flexShrink: 0 },
base: {
// A container that wants the icon at a different strength than its label writes
// `--_cl-icon-color` (see `button.styles.ts`); everywhere else the icon inherits as before.
color: 'var(--_cl-icon-color, currentColor)',
display: 'inline-block',
flexShrink: 0,
// Transitions don't inherit, so the container's own color transition doesn't animate this.
transitionDuration: durationVars['--cl-duration-fast'],
transitionProperty: 'color',
transitionTimingFunction: 'linear',
},
});

export const sizes = stylex.create({
Expand Down
5 changes: 0 additions & 5 deletions packages/ui/src/mosaic/components/menu/menu.styles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,9 +107,4 @@ export const styles = stylex.create({
backgroundColor: colorVars['--cl-color-border'],
blockSize: '1px',
},

triggerIcon: {
height: space['4'],
width: space['4'],
},
});
6 changes: 2 additions & 4 deletions packages/ui/src/mosaic/components/menu/menu.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,13 +10,11 @@ import { Menu as Primitive } from '@clerk/headless/menu';
import * as stylex from '@stylexjs/stylex';
import React from 'react';

import { iconRegistry } from '../../icons/registry';
import { mergeStyleProps, themeProps } from '../../props';
import { Button } from '../button';
import { Icon } from '../icon';
import { styles } from './menu.styles';

const EllipsisIcon = iconRegistry.ellipsis;

export type { MenuProps, MenuSeparatorProps, MenuTriggerProps };

/**
Expand DownExpand Up@@ -44,7 +42,7 @@ export const MenuTrigger = React.forwardRef<HTMLButtonElement, MenuTriggerProps>
{...mergeStyleProps(themeProps('menu-trigger'), className, style)}
{...rest}
>
{children ?? <EllipsisIcon {...stylex.props(styles.triggerIcon)} />}
{children ?? <Icon name='ellipsis' />}
</Primitive.Trigger>
);
});
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/mosaic-button-icon-color.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
62 changes: 10 additions & 52 deletions packages/swingset/src/stories/button.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -145,79 +145,37 @@ export function Shapes(props: Record<string, unknown>) {
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon
name='plus'
size='sm'
/>
</Button>
<Button
{...knobsAsProps(props)}
shape='square'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon name='plus' />
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon
name='plus'
size='sm'
/>
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
<Icon name='plus' />
</Button>
</div>
);
Expand Down
110 changes: 110 additions & 0 deletions packages/ui/src/mosaic/components/button/button.styles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,6 +36,18 @@ const primaryActive = `color-mix(in oklab, ${colorVars['--cl-color-primary']}, $
const negativeHover = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-foreground']} 12%)`;
const negativeActive = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-foreground']} 18%)`;

// An icon rests a step below its label and comes up to it on hover, so the label leads and the icon
// reads as supporting. Each is an opaque faded form of the cell's own text color, not an alpha of
// it: a translucent icon would pick up whatever sits behind the button and drift per surface.
//
// The achromatic foregrounds (`primary`, `neutral-foreground`) share the house faded gray. The two
// that carry hue fade toward the tint that belongs to them, and the light-on-fill pairs fade toward
// their own fill, which is the only backdrop they can ever sit on.
const iconFadedNeutral = colorVars['--cl-color-neutral-faded'];
const iconFadedNegative = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-faded']} 50%)`;
const iconFadedOnPrimary = `color-mix(in oklab, ${colorVars['--cl-color-primary-foreground']}, ${colorVars['--cl-color-primary']} 40%)`;
const iconFadedOnNegative = `color-mix(in oklab, ${colorVars['--cl-color-negative-foreground']}, ${colorVars['--cl-color-negative']} 40%)`;

// Interactive states are gated on `:enabled`: the `disabled` attribute blocks activation but
// not matching, and the button stays hit-testable so `cursor: not-allowed` renders and a
// wrapping tooltip still gets the pointer. Disabled keeps its resting fill and only dims.
Expand DownExpand Up@@ -149,9 +161,22 @@ export const styles = stylex.create({
// variant × color, one entry per cell of the design matrix, keyed `<variant>-<color>` so the
// component can index directly. Each cell is self-contained so it reads — and tunes — against
// the spec without tracing shared parts.
//
// `--_cl-icon-color` lives per cell rather than once in `base`: StyleX resolves a property to the
// last style that declares it, so a cell setting it would drop `base`'s hover branch wholesale
// rather than merge with it. `Icon` reads the var (`icon.styles.ts`) — StyleX can't emit a
// descendant rule, so the value crosses the element boundary as a custom property.
export const variants = stylex.create({
// The pressed state stays outside the hover media query so no-hover devices still get one.
'filled-primary': {
'--_cl-icon-color': {
default: iconFadedOnPrimary,
':enabled[data-open]': colorVars['--cl-color-primary-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary-foreground'],
},
},
backgroundColor: {
default: colorVars['--cl-color-primary'],
':enabled:active': primaryActive,
Expand All@@ -164,6 +189,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-primary-foreground'],
},
'filled-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-neutral-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
backgroundColor: {
default: neutralStep0,
':enabled:active': neutralStep2,
Expand All@@ -176,6 +209,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-neutral-foreground'],
},
'filled-negative': {
'--_cl-icon-color': {
default: iconFadedOnNegative,
':enabled[data-open]': colorVars['--cl-color-negative-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative-foreground'],
},
},
backgroundColor: {
default: colorVars['--cl-color-negative'],
':enabled:active': negativeActive,
Expand All@@ -192,6 +233,14 @@ export const variants = stylex.create({
// rises underneath it. Keeps the border opaque so it can't alpha-fade against an incoming
// fill, and leaves it independently themeable.
'outline-primary': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-primary'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary'],
},
},
borderColor: colorVars['--cl-color-border'],
backgroundColor: {
default: 'transparent',
Expand All@@ -205,6 +254,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-primary'],
},
'outline-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-neutral-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
borderColor: colorVars['--cl-color-border'],
backgroundColor: {
default: 'transparent',
Expand All@@ -218,6 +275,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-neutral-foreground'],
},
'outline-negative': {
'--_cl-icon-color': {
default: iconFadedNegative,
':enabled[data-open]': colorVars['--cl-color-negative'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative'],
},
},
borderColor: colorVars['--cl-color-border'],
backgroundColor: {
default: 'transparent',
Expand All@@ -232,6 +297,14 @@ export const variants = stylex.create({
},

'ghost-primary': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-primary'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary'],
},
},
backgroundColor: {
default: 'transparent',
':enabled:active': neutralStep1,
Expand All@@ -244,6 +317,14 @@ export const variants = stylex.create({
color: colorVars['--cl-color-primary'],
},
'ghost-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
':enabled[data-open]': colorVars['--cl-color-neutral-foreground'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
backgroundColor: {
default: 'transparent',
':enabled:active': neutralStep1,
Expand All@@ -258,6 +339,14 @@ export const variants = stylex.create({
// The one ghost that tints instead of graying, so its pressed step walks its own faded
// fill toward the negative it carries rather than joining the gray ramp.
'ghost-negative': {
'--_cl-icon-color': {
default: iconFadedNegative,
':enabled[data-open]': colorVars['--cl-color-negative'],
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative'],
},
},
backgroundColor: {
default: 'transparent',
':enabled:active': `color-mix(in oklab, ${colorVars['--cl-color-negative-faded']}, ${colorVars['--cl-color-negative']} 8%)`,
Expand All@@ -273,6 +362,13 @@ export const variants = stylex.create({
// link opts out of the box the size axis sets — it reads as text, not a control. Per-side
// zeros for the same reason `shapeSquare` uses them.
'link-primary': {
'--_cl-icon-color': {
default: iconFadedNeutral,
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-primary'],
},
},
backgroundColor: 'transparent',
color: colorVars['--cl-color-primary'],
paddingInlineEnd: 0,
Expand All@@ -282,6 +378,13 @@ export const variants = stylex.create({
height: 'auto',
},
'link-neutral': {
'--_cl-icon-color': {
default: iconFadedNeutral,
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-neutral-foreground'],
},
},
backgroundColor: 'transparent',
color: colorVars['--cl-color-neutral-foreground'],
paddingInlineEnd: 0,
Expand All@@ -291,6 +394,13 @@ export const variants = stylex.create({
height: 'auto',
},
'link-negative': {
'--_cl-icon-color': {
default: iconFadedNegative,
'@media (hover: hover)': {
default: null,
':enabled:hover': colorVars['--cl-color-negative'],
},
},
backgroundColor: 'transparent',
color: colorVars['--cl-color-negative'],
paddingInlineEnd: 0,
Expand Down
14 changes: 12 additions & 2 deletions packages/ui/src/mosaic/components/icon/icon.styles.ts
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
import * as stylex from '@stylexjs/stylex';

import { space } from '../../tokens.stylex';
import { durationVars, space } from '../../tokens.stylex';

export const styles = stylex.create({
base: { display: 'inline-block', flexShrink: 0 },
base: {
// A container that wants the icon at a different strength than its label writes
// `--_cl-icon-color` (see `button.styles.ts`); everywhere else the icon inherits as before.
color: 'var(--_cl-icon-color, currentColor)',
display: 'inline-block',
flexShrink: 0,
// Transitions don't inherit, so the container's own color transition doesn't animate this.
transitionDuration: durationVars['--cl-duration-fast'],
transitionProperty: 'color',
transitionTimingFunction: 'linear',
},
});

export const sizes = stylex.create({
Expand Down
5 changes: 0 additions & 5 deletions packages/ui/src/mosaic/components/menu/menu.styles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,9 +107,4 @@ export const styles = stylex.create({
backgroundColor: colorVars['--cl-color-border'],
blockSize: '1px',
},

triggerIcon: {
height: space['4'],
width: space['4'],
},
});
6 changes: 2 additions & 4 deletions packages/ui/src/mosaic/components/menu/menu.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,13 +10,11 @@ import { Menu as Primitive } from '@clerk/headless/menu';
import * as stylex from '@stylexjs/stylex';
import React from 'react';

import { iconRegistry } from '../../icons/registry';
import { mergeStyleProps, themeProps } from '../../props';
import { Button } from '../button';
import { Icon } from '../icon';
import { styles } from './menu.styles';

const EllipsisIcon = iconRegistry.ellipsis;

export type { MenuProps, MenuSeparatorProps, MenuTriggerProps };

/**
Expand DownExpand Up@@ -44,7 +42,7 @@ export const MenuTrigger = React.forwardRef<HTMLButtonElement, MenuTriggerProps>
{...mergeStyleProps(themeProps('menu-trigger'), className, style)}
{...rest}
>
{children ?? <EllipsisIcon {...stylex.props(styles.triggerIcon)} />}
{children ?? <Icon name='ellipsis' />}
</Primitive.Trigger>
);
});
Expand Down
Loading