feat(proj): init
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
||||
|
||||
export type AppButtonStyle = "filled" | "tinted" | "gray" | "plain" | "destructive";
|
||||
export type AppButtonSize = "large" | "medium" | "small";
|
||||
|
||||
type Props = ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
label?: ReactNode;
|
||||
children?: ReactNode;
|
||||
buttonStyle?: AppButtonStyle;
|
||||
size?: AppButtonSize;
|
||||
expanded?: boolean;
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
export function AppButton({
|
||||
label,
|
||||
children,
|
||||
buttonStyle = "filled",
|
||||
size = "large",
|
||||
expanded = true,
|
||||
loading = false,
|
||||
className = "",
|
||||
disabled,
|
||||
type = "button",
|
||||
...rest
|
||||
}: Props) {
|
||||
const classes = [
|
||||
"app-btn",
|
||||
`app-btn--${buttonStyle}`,
|
||||
`app-btn--${size}`,
|
||||
expanded ? "app-btn--expanded" : "",
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
return (
|
||||
<button type={type} className={classes} disabled={disabled || loading} {...rest}>
|
||||
{loading ? <span className="app-spinner" aria-hidden="true" /> : (label ?? children)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { AppButton } from "./AppButton";
|
||||
import { AppText } from "./AppText";
|
||||
|
||||
export function AppSpinner() {
|
||||
return <span className="app-spinner" role="status" aria-label="Загрузка" />;
|
||||
}
|
||||
|
||||
export function AppSkeleton({ width = "100%", height = 12 }: { width?: string | number; height?: number }) {
|
||||
return <span className="app-skeleton" style={{ width, height }} />;
|
||||
}
|
||||
|
||||
export function AppProgress({
|
||||
spent,
|
||||
total,
|
||||
label,
|
||||
}: {
|
||||
spent: number;
|
||||
total: number;
|
||||
label?: string;
|
||||
}) {
|
||||
const pct = total > 0 ? Math.min(100, Math.max(0, (spent / total) * 100)) : 0;
|
||||
const tone = pct >= 100 ? "is-danger" : pct >= 85 ? "is-warn" : "";
|
||||
return (
|
||||
<div className="app-progress">
|
||||
<div
|
||||
className="app-progress__track"
|
||||
role="progressbar"
|
||||
aria-valuenow={Math.round(pct)}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-label={label ?? "Потрачено от бюджета"}
|
||||
>
|
||||
<span className={`app-progress__bar ${tone}`.trim()} style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AppEmptyView({
|
||||
title = "Пусто",
|
||||
message,
|
||||
action,
|
||||
}: {
|
||||
title?: string;
|
||||
message?: ReactNode;
|
||||
action?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="app-empty">
|
||||
<AppText variant="headline">{title}</AppText>
|
||||
{message ? <AppText variant="subhead">{message}</AppText> : null}
|
||||
{action}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AppErrorView({
|
||||
message,
|
||||
onRetry,
|
||||
}: {
|
||||
message: string;
|
||||
onRetry?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="app-error">
|
||||
<AppText variant="headline">Не удалось загрузить</AppText>
|
||||
<AppText variant="subhead">{message}</AppText>
|
||||
{onRetry ? (
|
||||
<AppButton label="Повторить" buttonStyle="tinted" size="medium" expanded={false} onClick={onRetry} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AppChip({
|
||||
label,
|
||||
selected = false,
|
||||
onClick,
|
||||
disabled,
|
||||
}: {
|
||||
label: string;
|
||||
selected?: boolean;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<button type="button" className={`app-chip${selected ? " is-on" : ""}`} onClick={onClick} disabled={disabled}>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function AppAvatar({ initials }: { initials: string }) {
|
||||
return <span className="app-avatar">{initials}</span>;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { AppSectionHeader } from "./AppText";
|
||||
|
||||
type SectionProps = {
|
||||
header?: string;
|
||||
footer?: ReactNode;
|
||||
children: ReactNode;
|
||||
flush?: boolean;
|
||||
};
|
||||
|
||||
export function AppListSection({ header, footer, children, flush = false }: SectionProps) {
|
||||
return (
|
||||
<section>
|
||||
{header ? <AppSectionHeader>{header}</AppSectionHeader> : null}
|
||||
<div className={`app-list${flush ? " app-list--flush" : ""}`}>{children}</div>
|
||||
{footer ? <p className="app-section-footer">{footer}</p> : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
type TileProps = {
|
||||
title: ReactNode;
|
||||
subtitle?: ReactNode;
|
||||
value?: ReactNode;
|
||||
leading?: ReactNode;
|
||||
trailing?: ReactNode;
|
||||
chevron?: boolean;
|
||||
destructive?: boolean;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
};
|
||||
|
||||
export function AppListTile({
|
||||
title,
|
||||
subtitle,
|
||||
value,
|
||||
leading,
|
||||
trailing,
|
||||
chevron = false,
|
||||
destructive = false,
|
||||
onClick,
|
||||
href,
|
||||
}: TileProps) {
|
||||
const interactive = Boolean(onClick || href);
|
||||
const className = `app-tile${destructive ? " app-tile--destructive" : ""}`;
|
||||
const body = (
|
||||
<>
|
||||
{leading}
|
||||
<div className="app-tile__body">
|
||||
<p className="app-tile__title">{title}</p>
|
||||
{subtitle ? <p className="app-tile__subtitle">{subtitle}</p> : null}
|
||||
</div>
|
||||
{trailing ?? (value != null ? <p className="app-tile__value">{value}</p> : null)}
|
||||
{chevron ? <span className="app-tile__chevron" aria-hidden="true">›</span> : null}
|
||||
</>
|
||||
);
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<a className={className} href={href}>
|
||||
{body}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (interactive) {
|
||||
return (
|
||||
<button type="button" className={className} onClick={onClick}>
|
||||
{body}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className={className}>{body}</div>;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
type Option<T extends string> = { value: T; label: string };
|
||||
|
||||
type Props<T extends string> = {
|
||||
value: T;
|
||||
options: Option<T>[];
|
||||
onChange: (value: T) => void;
|
||||
ariaLabel?: string;
|
||||
};
|
||||
|
||||
export function AppSegmented<T extends string>({
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
ariaLabel,
|
||||
}: Props<T>) {
|
||||
return (
|
||||
<div className="app-segmented" role="tablist" aria-label={ariaLabel}>
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={option.value === value}
|
||||
className={`app-segmented__item${option.value === value ? " is-on" : ""}`}
|
||||
onClick={() => onChange(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { ElementType, ReactNode } from "react";
|
||||
|
||||
export type AppTextVariant =
|
||||
| "largeTitle"
|
||||
| "title1"
|
||||
| "title2"
|
||||
| "title3"
|
||||
| "headline"
|
||||
| "body"
|
||||
| "callout"
|
||||
| "subhead"
|
||||
| "footnote"
|
||||
| "caption";
|
||||
|
||||
export type AppTextTone = "label" | "secondary" | "tertiary" | "accent" | "danger";
|
||||
|
||||
const DEFAULT_TONE: Record<AppTextVariant, AppTextTone> = {
|
||||
largeTitle: "label",
|
||||
title1: "label",
|
||||
title2: "label",
|
||||
title3: "label",
|
||||
headline: "label",
|
||||
body: "label",
|
||||
callout: "secondary",
|
||||
subhead: "secondary",
|
||||
footnote: "secondary",
|
||||
caption: "tertiary",
|
||||
};
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
variant?: AppTextVariant;
|
||||
tone?: AppTextTone;
|
||||
as?: ElementType;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function AppText({
|
||||
children,
|
||||
variant = "body",
|
||||
tone,
|
||||
as: Tag = "p",
|
||||
className = "",
|
||||
}: Props) {
|
||||
const color = tone ?? DEFAULT_TONE[variant];
|
||||
return (
|
||||
<Tag className={`app-text app-text--${variant} app-text--${color} ${className}`.trim()}>
|
||||
{children}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
export function AppSectionHeader({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<span className="app-section-header">
|
||||
{typeof children === "string" ? children.toUpperCase() : children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { InputHTMLAttributes, ReactNode, SelectHTMLAttributes } from "react";
|
||||
import { AppSectionHeader } from "./AppText";
|
||||
|
||||
type FieldProps = {
|
||||
label?: string;
|
||||
hint?: ReactNode;
|
||||
error?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function AppTextField({
|
||||
label,
|
||||
id,
|
||||
hint,
|
||||
error,
|
||||
className = "",
|
||||
...rest
|
||||
}: InputHTMLAttributes<HTMLInputElement> & FieldProps & { id: string }) {
|
||||
return (
|
||||
<div className={`app-field ${error ? "app-field--error" : ""} ${className}`.trim()}>
|
||||
{label ? (
|
||||
<label htmlFor={id}>
|
||||
<AppSectionHeader>{label}</AppSectionHeader>
|
||||
</label>
|
||||
) : null}
|
||||
<input id={id} className="app-field__control" {...rest} />
|
||||
{error ? <p className="app-field__error">{error}</p> : hint}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AppSelect({
|
||||
label,
|
||||
id,
|
||||
hint,
|
||||
error,
|
||||
className = "",
|
||||
children,
|
||||
...rest
|
||||
}: SelectHTMLAttributes<HTMLSelectElement> & FieldProps & { id: string }) {
|
||||
return (
|
||||
<div className={`app-field ${error ? "app-field--error" : ""} ${className}`.trim()}>
|
||||
{label ? (
|
||||
<label htmlFor={id}>
|
||||
<AppSectionHeader>{label}</AppSectionHeader>
|
||||
</label>
|
||||
) : null}
|
||||
<select id={id} className="app-field__control" {...rest}>
|
||||
{children}
|
||||
</select>
|
||||
{error ? <p className="app-field__error">{error}</p> : hint}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export { AppText, AppSectionHeader } from "./AppText";
|
||||
export { AppButton } from "./AppButton";
|
||||
export type { AppButtonSize, AppButtonStyle } from "./AppButton";
|
||||
export { AppTextField, AppSelect } from "./AppTextField";
|
||||
export { AppListSection, AppListTile } from "./AppList";
|
||||
export { AppSegmented } from "./AppSegmented";
|
||||
export {
|
||||
AppSpinner,
|
||||
AppSkeleton,
|
||||
AppProgress,
|
||||
AppEmptyView,
|
||||
AppErrorView,
|
||||
AppChip,
|
||||
AppAvatar,
|
||||
} from "./AppFeedback";
|
||||
@@ -0,0 +1,346 @@
|
||||
/* iOS kit — mirrors mobile/lib/ui */
|
||||
|
||||
.app-text {
|
||||
margin: 0;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.app-text--largeTitle {
|
||||
font-size: var(--text-large-title);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.37px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.app-text--title1 {
|
||||
font-size: var(--text-title1);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.36px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.app-text--title2 {
|
||||
font-size: var(--text-title2);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.35px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.app-text--title3 {
|
||||
font-size: var(--text-title3);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.38px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.app-text--headline {
|
||||
font-size: var(--text-headline);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.41px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.app-text--body {
|
||||
font-size: var(--text-body);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.41px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.app-text--callout {
|
||||
font-size: var(--text-callout);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.32px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.app-text--subhead {
|
||||
font-size: var(--text-subhead);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.24px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.app-text--footnote {
|
||||
font-size: var(--text-footnote);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.08px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.app-text--caption {
|
||||
font-size: var(--text-caption1);
|
||||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.app-text--label { color: var(--label); }
|
||||
.app-text--secondary { color: var(--secondary-label); }
|
||||
.app-text--tertiary { color: var(--tertiary-label); }
|
||||
.app-text--accent { color: var(--accent); }
|
||||
.app-text--danger { color: var(--system-red); }
|
||||
|
||||
.app-section-header {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: var(--space-4) var(--gutter) var(--space-2);
|
||||
font-size: var(--text-footnote);
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.4px;
|
||||
text-transform: uppercase;
|
||||
color: var(--secondary-label);
|
||||
}
|
||||
|
||||
.app-section-footer {
|
||||
margin: 0;
|
||||
padding: var(--space-2) var(--gutter) 0;
|
||||
font-size: var(--text-footnote);
|
||||
color: var(--secondary-label);
|
||||
}
|
||||
|
||||
.app-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.41px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: filter 140ms ease, opacity 140ms ease;
|
||||
}
|
||||
|
||||
.app-btn:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.app-btn--expanded { width: 100%; }
|
||||
.app-btn--large {
|
||||
height: var(--control-height);
|
||||
padding: 0 var(--space-4);
|
||||
border-radius: var(--radius-lg);
|
||||
font-size: var(--text-headline);
|
||||
}
|
||||
.app-btn--medium {
|
||||
height: var(--control-height-md);
|
||||
padding: 0 var(--space-4);
|
||||
border-radius: var(--radius-lg);
|
||||
font-size: var(--text-body);
|
||||
}
|
||||
.app-btn--small {
|
||||
height: var(--control-height-sm);
|
||||
padding: 0 var(--space-3);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--text-subhead);
|
||||
}
|
||||
|
||||
.app-btn--filled { background: var(--accent); color: #fff; }
|
||||
.app-btn--tinted { background: rgb(18 136 90 / 15%); color: var(--accent); }
|
||||
.app-btn--gray { background: var(--system-gray5); color: var(--label); }
|
||||
.app-btn--plain { background: transparent; color: var(--accent); }
|
||||
.app-btn--destructive { background: rgb(255 59 48 / 15%); color: var(--system-red); }
|
||||
|
||||
.app-btn:hover:not(:disabled) { filter: brightness(1.04); }
|
||||
|
||||
.app-field { display: grid; gap: var(--space-2); }
|
||||
.app-field .app-section-header {
|
||||
padding: 0;
|
||||
}
|
||||
.app-field__control {
|
||||
width: 100%;
|
||||
height: var(--control-height-md);
|
||||
border: 0;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--grouped-surface);
|
||||
color: var(--label);
|
||||
padding: 0 var(--space-3);
|
||||
font-size: var(--text-body);
|
||||
letter-spacing: -0.41px;
|
||||
}
|
||||
.app-field__control:disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
.app-field--error .app-field__control {
|
||||
box-shadow: inset 0 0 0 1px var(--system-red);
|
||||
}
|
||||
.app-field__error {
|
||||
margin: 0;
|
||||
font-size: var(--text-footnote);
|
||||
color: var(--system-red);
|
||||
}
|
||||
|
||||
.app-list {
|
||||
margin: 0 var(--gutter);
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
background: var(--grouped-surface);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-list--flush {
|
||||
margin-inline: 0;
|
||||
}
|
||||
|
||||
.app-tile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
width: 100%;
|
||||
min-height: var(--row-min-height);
|
||||
padding: 10px var(--gutter);
|
||||
border: 0;
|
||||
background: var(--grouped-surface);
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
cursor: default;
|
||||
}
|
||||
button.app-tile,
|
||||
a.app-tile {
|
||||
cursor: pointer;
|
||||
}
|
||||
button.app-tile:hover,
|
||||
a.app-tile:hover {
|
||||
background: var(--system-gray5);
|
||||
}
|
||||
.app-tile + .app-tile {
|
||||
box-shadow: inset 0 var(--hairline) 0 var(--separator);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
.app-tile__body { flex: 1; min-width: 0; }
|
||||
.app-tile__title {
|
||||
margin: 0;
|
||||
font-size: var(--text-body);
|
||||
letter-spacing: -0.41px;
|
||||
color: var(--label);
|
||||
}
|
||||
.app-tile--destructive .app-tile__title { color: var(--system-red); }
|
||||
.app-tile__subtitle {
|
||||
margin: 2px 0 0;
|
||||
font-size: var(--text-footnote);
|
||||
color: var(--secondary-label);
|
||||
}
|
||||
.app-tile__value {
|
||||
margin: 0;
|
||||
font-size: var(--text-body);
|
||||
color: var(--secondary-label);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.app-tile__chevron {
|
||||
color: var(--tertiary-label);
|
||||
font-size: 17px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.app-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 28px;
|
||||
padding: 0 12px;
|
||||
border: 0;
|
||||
border-radius: var(--radius-capsule);
|
||||
background: var(--system-gray5);
|
||||
color: var(--label);
|
||||
font-size: var(--text-subhead);
|
||||
cursor: pointer;
|
||||
}
|
||||
.app-chip.is-on {
|
||||
background: rgb(18 136 90 / 15%);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.app-avatar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: var(--radius-capsule);
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
font-size: var(--text-subhead);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.app-progress__track {
|
||||
height: 4px;
|
||||
border-radius: var(--radius-capsule);
|
||||
background: var(--system-gray5);
|
||||
overflow: hidden;
|
||||
}
|
||||
.app-progress__bar {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
border-radius: inherit;
|
||||
}
|
||||
.app-progress__bar.is-warn { background: var(--system-orange); }
|
||||
.app-progress__bar.is-danger { background: var(--system-red); }
|
||||
|
||||
.app-spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--system-gray3);
|
||||
border-top-color: var(--secondary-label);
|
||||
border-radius: 50%;
|
||||
animation: app-spin 700ms linear infinite;
|
||||
}
|
||||
@keyframes app-spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.app-skeleton {
|
||||
display: block;
|
||||
height: 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--system-gray5);
|
||||
}
|
||||
|
||||
.app-empty,
|
||||
.app-error {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-7) var(--gutter);
|
||||
text-align: center;
|
||||
color: var(--secondary-label);
|
||||
}
|
||||
|
||||
.app-segmented {
|
||||
display: flex;
|
||||
padding: 2px;
|
||||
border-radius: 9px;
|
||||
background: var(--system-gray5);
|
||||
}
|
||||
.app-segmented__item {
|
||||
flex: 1;
|
||||
min-height: 32px;
|
||||
border: 0;
|
||||
border-radius: 7px;
|
||||
background: transparent;
|
||||
color: var(--label);
|
||||
font-size: var(--text-subhead);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
.app-segmented__item.is-on {
|
||||
background: var(--grouped-surface);
|
||||
box-shadow: 0 1px 3px rgb(0 0 0 / 12%);
|
||||
}
|
||||
|
||||
.app-toast {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 24px;
|
||||
transform: translateX(-50%);
|
||||
z-index: var(--z-toast);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
min-height: 44px;
|
||||
padding: 0 16px;
|
||||
border-radius: var(--radius-capsule);
|
||||
background: rgb(28 28 30 / 88%);
|
||||
color: #fff;
|
||||
font-size: var(--text-subhead);
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.app-btn--tinted { background: rgb(60 214 140 / 18%); }
|
||||
}
|
||||
Reference in New Issue
Block a user