Private
Public Access
1
0

feat: Fluent UI Outlook Lite + connections mockup

This commit is contained in:
2026-04-14 18:52:25 +00:00
parent 1199eff6c3
commit dfa4010406
34820 changed files with 1003813 additions and 205 deletions

View File

@@ -0,0 +1,360 @@
'use client';
import { makeResetStyles, makeStyles, mergeClasses, shorthands } from '@griffel/react';
import { tokens, typographyStyles } from '@fluentui/react-theme';
export const spinButtonClassNames = {
root: 'fui-SpinButton',
input: 'fui-SpinButton__input',
incrementButton: 'fui-SpinButton__incrementButton',
decrementButton: 'fui-SpinButton__decrementButton'
};
const spinButtonExtraClassNames = {
buttonActive: 'fui-SpinButton__button_active'
};
const fieldHeights = {
small: '24px',
medium: '32px'
};
const useRootClassName = makeResetStyles({
display: 'inline-grid',
gridTemplateColumns: `1fr 24px`,
gridTemplateRows: '1fr 1fr',
columnGap: tokens.spacingHorizontalXS,
rowGap: 0,
position: 'relative',
isolation: 'isolate',
verticalAlign: 'middle',
backgroundColor: tokens.colorNeutralBackground1,
minHeight: fieldHeights.medium,
padding: `0 0 0 ${tokens.spacingHorizontalMNudge}`,
borderRadius: tokens.borderRadiusMedium,
// Apply border styles on the ::before pseudo element.
// We cannot use ::after since that is used for selection.
// Using the pseudo element allows us to place the border
// above content in the component which ensures the buttons
// line up visually with the border as expected. Without this
// there is a bit of a gap which can become very noticeable
// at high zoom or when OS zoom levels are not divisible by 2
// (e.g., 150% on Windows in Firefox)
// This is most noticeable on the "outline" appearance which is
// also the default so it feels worth the extra ceremony to get right.
'::before': {
content: '""',
boxSizing: 'border-box',
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
pointerEvents: 'none',
zIndex: 10,
border: `1px solid ${tokens.colorNeutralStroke1}`,
borderBottomColor: tokens.colorNeutralStrokeAccessible,
borderRadius: tokens.borderRadiusMedium
},
'::after': {
boxSizing: 'border-box',
content: '""',
position: 'absolute',
right: 0,
bottom: 0,
left: 0,
zIndex: 20,
// Maintaining the correct corner radius:
// Use the whole border-radius as the height and only put radii on the bottom corners.
// (Otherwise the radius would be automatically reduced to fit available space.)
// max() ensures the focus border still shows up even if someone sets tokens.borderRadiusMedium to 0.
height: `max(2px, ${tokens.borderRadiusMedium})`,
borderBottomLeftRadius: tokens.borderRadiusMedium,
borderBottomRightRadius: tokens.borderRadiusMedium,
// Flat 2px border:
// By default borderBottom will cause little "horns" on the ends. The clipPath trims them off.
// (This could be done without trimming using `background: linear-gradient(...)`, but using
// borderBottom makes it easier for people to override the color if needed.)
borderBottom: `2px solid ${tokens.colorCompoundBrandStroke}`,
clipPath: 'inset(calc(100% - 2px) 0 0 0)',
// Animation for focus OUT
transform: 'scaleX(0)',
transitionProperty: 'transform',
transitionDuration: tokens.durationUltraFast,
transitionDelay: tokens.curveAccelerateMid,
'@media screen and (prefers-reduced-motion: reduce)': {
transitionDuration: '0.01ms',
transitionDelay: '0.01ms'
}
},
':focus-within::after': {
// Animation for focus IN
transform: 'scaleX(1)',
transitionProperty: 'transform',
transitionDuration: tokens.durationNormal,
transitionDelay: tokens.curveDecelerateMid,
'@media screen and (prefers-reduced-motion: reduce)': {
transitionDuration: '0.01ms',
transitionDelay: '0.01ms'
}
},
':focus-within:active::after': {
// This is if the user clicks the field again while it's already focused
borderBottomColor: tokens.colorCompoundBrandStrokePressed
},
':focus-within': {
outline: '2px solid transparent'
}
});
const useRootStyles = makeStyles({
small: {
minHeight: fieldHeights.small,
...typographyStyles.caption1,
paddingLeft: tokens.spacingHorizontalS
},
medium: {
},
outline: {
},
outlineInteractive: {
':hover::before': {
...shorthands.borderColor(tokens.colorNeutralStroke1Hover),
borderBottomColor: tokens.colorNeutralStrokeAccessibleHover
},
// DO NOT add a space between the selectors! It changes the behavior of make-styles.
':active,:focus-within': {
'::before': {
...shorthands.borderColor(tokens.colorNeutralStroke1Pressed),
borderBottomColor: tokens.colorNeutralStrokeAccessiblePressed
}
}
},
underline: {
'::before': {
...shorthands.borderWidth(0, 0, '1px', 0),
borderRadius: tokens.borderRadiusNone
}
},
underlineInteractive: {
':hover::before': {
borderBottomColor: tokens.colorNeutralStrokeAccessibleHover
},
// DO NOT add a space between the selectors! It changes the behavior of make-styles.
':active,:focus-within': {
'::before': {
borderBottomColor: tokens.colorNeutralStrokeAccessiblePressed
}
},
'::after': {
borderRadius: tokens.borderRadiusNone
}
},
filled: {
'::before': {
border: `1px solid ${tokens.colorTransparentStroke}`
}
},
'filled-darker': {
backgroundColor: tokens.colorNeutralBackground3
},
'filled-lighter': {
backgroundColor: tokens.colorNeutralBackground1
},
filledInteractive: {
// DO NOT add a space between the selectors! It changes the behavior of make-styles.
':hover,:focus-within': {
'::before': {
// also handles pressed border color (:active)
...shorthands.borderColor(tokens.colorTransparentStrokeInteractive)
}
}
},
invalid: {
':not(:focus-within),:hover:not(:focus-within)': {
'::before': {
...shorthands.borderColor(tokens.colorPaletteRedBorder2)
}
}
},
disabled: {
cursor: 'not-allowed',
backgroundColor: tokens.colorTransparentBackground,
'::before': {
...shorthands.borderColor(tokens.colorNeutralStrokeDisabled),
'@media (forced-colors: active)': {
...shorthands.borderColor('GrayText')
}
}
}
});
const useInputClassName = makeResetStyles({
gridColumnStart: '1',
gridColumnEnd: '2',
gridRowStart: '1',
gridRowEnd: '3',
outlineStyle: 'none',
border: '0',
padding: '0',
color: tokens.colorNeutralForeground1,
// Use literal "transparent" (not from the theme) to always let the color from the root show through
backgroundColor: 'transparent',
fontFamily: 'inherit',
fontSize: 'inherit',
fontWeight: 'inherit',
lineHeight: 'inherit',
width: '100%',
'::placeholder': {
color: tokens.colorNeutralForeground4,
opacity: 1
}
});
const useInputStyles = makeStyles({
disabled: {
color: tokens.colorNeutralForegroundDisabled,
cursor: 'not-allowed',
backgroundColor: tokens.colorTransparentBackground,
'::placeholder': {
color: tokens.colorNeutralForegroundDisabled
}
}
});
const useBaseButtonClassName = makeResetStyles({
display: 'inline-flex',
width: '24px',
alignItems: 'center',
justifyContent: 'center',
border: '0',
position: 'absolute',
outlineStyle: 'none',
height: '16px',
// Use literal "transparent" (not from the theme) to always let the color from the root show through
backgroundColor: 'transparent',
color: tokens.colorNeutralForeground3,
// common button layout
gridColumnStart: '2',
borderRadius: '0',
padding: '0 5px 0 5px',
':active': {
outlineStyle: 'none'
},
':enabled': {
':hover': {
cursor: 'pointer',
color: tokens.colorNeutralForeground3Hover,
backgroundColor: tokens.colorSubtleBackgroundHover
},
':active': {
color: tokens.colorNeutralForeground3Pressed,
backgroundColor: tokens.colorSubtleBackgroundPressed
},
[`&.${spinButtonExtraClassNames.buttonActive}`]: {
color: tokens.colorNeutralForeground3Pressed,
backgroundColor: tokens.colorSubtleBackgroundPressed
}
},
':disabled': {
cursor: 'not-allowed',
color: tokens.colorNeutralForegroundDisabled
}
});
const useButtonStyles = makeStyles({
increment: {
gridRowStart: '1',
borderTopRightRadius: tokens.borderRadiusMedium,
paddingTop: '4px',
paddingBottom: '1px'
},
decrement: {
gridRowStart: '2',
borderBottomRightRadius: tokens.borderRadiusMedium,
paddingTop: '1px',
paddingBottom: '4px'
},
// Padding values numbers don't align with design specs
// but visually the padding aligns.
// The icons are set in a 16x16px square but the artwork is inset from that
// so these padding values are computed by hand.
// Additionally the design uses fractional values so these are
// rounded to the nearest integer.
incrementButtonSmall: {
padding: '3px 6px 0px 4px',
height: '12px'
},
decrementButtonSmall: {
padding: '0px 6px 3px 4px',
height: '12px'
},
outline: {
},
underline: {
backgroundColor: 'transparent',
color: tokens.colorNeutralForeground3,
':enabled': {
':hover': {
color: tokens.colorNeutralForeground3Hover,
backgroundColor: tokens.colorSubtleBackgroundHover
},
':active': {
color: tokens.colorNeutralForeground3Pressed,
backgroundColor: tokens.colorSubtleBackgroundPressed
},
[`&.${spinButtonExtraClassNames.buttonActive}`]: {
color: tokens.colorNeutralForeground3Pressed,
backgroundColor: tokens.colorSubtleBackgroundPressed
}
},
':disabled': {
color: tokens.colorNeutralForegroundDisabled
}
},
'filled-darker': {
backgroundColor: 'transparent',
color: tokens.colorNeutralForeground3,
':enabled': {
':hover': {
color: tokens.colorNeutralForeground3Hover,
backgroundColor: tokens.colorNeutralBackground3Hover
},
':active': {
color: tokens.colorNeutralForeground3Pressed,
backgroundColor: tokens.colorNeutralBackground3Pressed
},
[`&.${spinButtonExtraClassNames.buttonActive}`]: {
color: tokens.colorNeutralForeground3Pressed,
backgroundColor: tokens.colorNeutralBackground3Pressed
}
},
':disabled': {
color: tokens.colorNeutralForegroundDisabled
}
},
'filled-lighter': {
backgroundColor: 'transparent',
color: tokens.colorNeutralForeground3,
':enabled': {
':hover': {
color: tokens.colorNeutralForeground3Hover,
backgroundColor: tokens.colorNeutralBackground1Hover
},
[`:active,&.${spinButtonExtraClassNames.buttonActive}`]: {
color: tokens.colorNeutralForeground3Pressed,
backgroundColor: tokens.colorNeutralBackground1Pressed
}
},
':disabled': {
color: tokens.colorNeutralForegroundDisabled
}
}
});
/**
* Apply styling to the SpinButton slots based on the state
*/ export const useSpinButtonStyles_unstable = (state)=>{
'use no memo';
const { appearance, spinState, size } = state;
const disabled = state.input.disabled;
const invalid = `${state.input['aria-invalid']}` === 'true';
const filled = appearance.startsWith('filled');
const rootStyles = useRootStyles();
const buttonStyles = useButtonStyles();
const inputStyles = useInputStyles();
state.root.className = mergeClasses(spinButtonClassNames.root, useRootClassName(), rootStyles[size], rootStyles[appearance], filled && rootStyles.filled, !disabled && appearance === 'outline' && rootStyles.outlineInteractive, !disabled && appearance === 'underline' && rootStyles.underlineInteractive, !disabled && filled && rootStyles.filledInteractive, !disabled && invalid && rootStyles.invalid, disabled && rootStyles.disabled, state.root.className);
state.incrementButton.className = mergeClasses(spinButtonClassNames.incrementButton, spinState === 'up' && `${spinButtonExtraClassNames.buttonActive}`, useBaseButtonClassName(), buttonStyles.increment, buttonStyles[appearance], size === 'small' && buttonStyles.incrementButtonSmall, state.incrementButton.className);
state.decrementButton.className = mergeClasses(spinButtonClassNames.decrementButton, spinState === 'down' && `${spinButtonExtraClassNames.buttonActive}`, useBaseButtonClassName(), buttonStyles.decrement, buttonStyles[appearance], size === 'small' && buttonStyles.decrementButtonSmall, state.decrementButton.className);
state.input.className = mergeClasses(spinButtonClassNames.input, useInputClassName(), disabled && inputStyles.disabled, state.input.className);
return state;
};