milestone 23 s3: convert features/pause/PauseWidget.tsx to stylex
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import * as stylex from "@stylexjs/stylex";
|
||||||
import { pauseMutation, pauseQuery } from "@/lib/queries";
|
import { pauseMutation, pauseQuery } from "@/lib/queries";
|
||||||
import InlineError from "@/lib/InlineError";
|
import InlineError from "@/lib/InlineError";
|
||||||
import { buttonClass, insetFocusRing } from "@/ui/classes";
|
import { styles as shared } from "@/ui/styles";
|
||||||
|
import { colors } from "@/ui/tokens.stylex";
|
||||||
|
|
||||||
const DURATIONS = [
|
const DURATIONS = [
|
||||||
{ label: "60 seconds", seconds: 60 },
|
{ label: "60 seconds", seconds: 60 },
|
||||||
@@ -11,6 +13,72 @@ const DURATIONS = [
|
|||||||
{ label: "Indefinitely", seconds: null },
|
{ label: "Indefinitely", seconds: null },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
const styles = stylex.create({
|
||||||
|
/**
|
||||||
|
* Disabled text darkens in light scheme and lightens in dark, the opposite
|
||||||
|
* direction from `textMuted`, so the token cannot express it.
|
||||||
|
*/
|
||||||
|
trigger: {
|
||||||
|
color: {
|
||||||
|
default: null,
|
||||||
|
":disabled": "oklch(70.5% 0.015 286.067)",
|
||||||
|
"@media (prefers-color-scheme: dark)": { default: null, ":disabled": "oklch(44.2% 0.017 285.786)" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pausedRow: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "flex-end",
|
||||||
|
},
|
||||||
|
pausedControls: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "0.5rem",
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Amber as standalone text on the app ground, not inside a warning banner, so
|
||||||
|
* the `warn*` tokens — tuned against `warnSurface` — do not apply here.
|
||||||
|
*/
|
||||||
|
pausedLabel: {
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
lineHeight: "1.25rem",
|
||||||
|
color: {
|
||||||
|
default: "oklch(55.5% 0.163 48.998)",
|
||||||
|
"@media (prefers-color-scheme: dark)": "oklch(82.8% 0.189 84.429)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
anchor: {
|
||||||
|
position: "relative",
|
||||||
|
},
|
||||||
|
menu: {
|
||||||
|
position: "absolute",
|
||||||
|
right: 0,
|
||||||
|
top: "100%",
|
||||||
|
zIndex: 10,
|
||||||
|
marginTop: "0.25rem",
|
||||||
|
display: "flex",
|
||||||
|
width: "9rem",
|
||||||
|
flexDirection: "column",
|
||||||
|
borderRadius: "0.25rem",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderStyle: "solid",
|
||||||
|
borderColor: colors.border,
|
||||||
|
backgroundColor: colors.surfaceRaised,
|
||||||
|
paddingBlock: "0.25rem",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
|
||||||
|
},
|
||||||
|
menuItem: {
|
||||||
|
borderStyle: "none",
|
||||||
|
backgroundColor: { default: "transparent", ":hover": colors.surfaceHover },
|
||||||
|
color: "inherit",
|
||||||
|
paddingInline: "0.75rem",
|
||||||
|
paddingBlock: "0.375rem",
|
||||||
|
textAlign: "left",
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
lineHeight: "1.25rem",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export function formatRemaining(totalSeconds: number): string {
|
export function formatRemaining(totalSeconds: number): string {
|
||||||
const clamped = Math.max(0, totalSeconds);
|
const clamped = Math.max(0, totalSeconds);
|
||||||
const hours = Math.floor(clamped / 3600);
|
const hours = Math.floor(clamped / 3600);
|
||||||
@@ -35,8 +103,6 @@ function useNowSeconds(active: boolean): number {
|
|||||||
return now;
|
return now;
|
||||||
}
|
}
|
||||||
|
|
||||||
const triggerButtonClass = `${buttonClass} disabled:text-zinc-400 dark:disabled:text-zinc-600`;
|
|
||||||
|
|
||||||
export default function PauseWidget() {
|
export default function PauseWidget() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { data } = useQuery({
|
const { data } = useQuery({
|
||||||
@@ -52,7 +118,7 @@ export default function PauseWidget() {
|
|||||||
|
|
||||||
if (data === undefined) {
|
if (data === undefined) {
|
||||||
return (
|
return (
|
||||||
<button type="button" disabled className={triggerButtonClass}>
|
<button type="button" disabled {...stylex.props(shared.button, styles.trigger, shared.focusRing)}>
|
||||||
Pause
|
Pause
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
@@ -60,16 +126,16 @@ export default function PauseWidget() {
|
|||||||
|
|
||||||
if (data.paused) {
|
if (data.paused) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-end">
|
<div {...stylex.props(styles.pausedRow)}>
|
||||||
<div className="flex items-center gap-2">
|
<div {...stylex.props(styles.pausedControls)}>
|
||||||
<span className="text-sm text-amber-700 dark:text-amber-400">
|
<span {...stylex.props(styles.pausedLabel)}>
|
||||||
{data.until === null ? "Paused" : `Paused ${formatRemaining(data.until - now)}`}
|
{data.until === null ? "Paused" : `Paused ${formatRemaining(data.until - now)}`}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => mutation.mutate({ paused: false })}
|
onClick={() => mutation.mutate({ paused: false })}
|
||||||
disabled={mutation.isPending}
|
disabled={mutation.isPending}
|
||||||
className={triggerButtonClass}
|
{...stylex.props(shared.button, styles.trigger, shared.focusRing)}
|
||||||
>
|
>
|
||||||
Resume
|
Resume
|
||||||
</button>
|
</button>
|
||||||
@@ -81,7 +147,7 @@ export default function PauseWidget() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative"
|
{...stylex.props(styles.anchor)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Escape") setMenuOpen(false);
|
if (e.key === "Escape") setMenuOpen(false);
|
||||||
}}
|
}}
|
||||||
@@ -92,15 +158,12 @@ export default function PauseWidget() {
|
|||||||
aria-controls="pause-menu"
|
aria-controls="pause-menu"
|
||||||
onClick={() => setMenuOpen((open) => !open)}
|
onClick={() => setMenuOpen((open) => !open)}
|
||||||
disabled={mutation.isPending}
|
disabled={mutation.isPending}
|
||||||
className={triggerButtonClass}
|
{...stylex.props(shared.button, styles.trigger, shared.focusRing)}
|
||||||
>
|
>
|
||||||
Pause
|
Pause
|
||||||
</button>
|
</button>
|
||||||
{menuOpen && (
|
{menuOpen && (
|
||||||
<div
|
<div id="pause-menu" {...stylex.props(styles.menu)}>
|
||||||
id="pause-menu"
|
|
||||||
className="absolute right-0 top-full z-10 mt-1 flex w-36 flex-col rounded border border-zinc-200 bg-white py-1 shadow dark:border-zinc-800 dark:bg-zinc-900"
|
|
||||||
>
|
|
||||||
{DURATIONS.map(({ label, seconds }) => (
|
{DURATIONS.map(({ label, seconds }) => (
|
||||||
<button
|
<button
|
||||||
key={label}
|
key={label}
|
||||||
@@ -111,7 +174,7 @@ export default function PauseWidget() {
|
|||||||
seconds === null ? { paused: true } : { paused: true, duration_seconds: seconds },
|
seconds === null ? { paused: true } : { paused: true, duration_seconds: seconds },
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
className={`px-3 py-1.5 text-left text-sm hover:bg-zinc-100 ${insetFocusRing} dark:hover:bg-zinc-800`}
|
{...stylex.props(styles.menuItem, shared.insetFocusRing)}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ import { colors } from "./tokens.stylex";
|
|||||||
const FOCUS = ":focus-visible";
|
const FOCUS = ":focus-visible";
|
||||||
const DISABLED = ":disabled";
|
const DISABLED = ":disabled";
|
||||||
|
|
||||||
|
/** The half-fade loop a placeholder runs while its data is in flight. */
|
||||||
|
const pulseFrames = stylex.keyframes({
|
||||||
|
"50%": { opacity: 0.5 },
|
||||||
|
});
|
||||||
|
|
||||||
export const styles = stylex.create({
|
export const styles = stylex.create({
|
||||||
/** The ring drawn outside the element. */
|
/** The ring drawn outside the element. */
|
||||||
focusRing: {
|
focusRing: {
|
||||||
@@ -151,6 +156,13 @@ export const styles = stylex.create({
|
|||||||
color: colors.dangerText,
|
color: colors.dangerText,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Marks an element as waiting on data. Carries no colour of its own. */
|
||||||
|
pulse: {
|
||||||
|
animationName: pulseFrames,
|
||||||
|
animationDuration: "2s",
|
||||||
|
animationTimingFunction: "cubic-bezier(0.4, 0, 0.6, 1)",
|
||||||
|
animationIterationCount: "infinite",
|
||||||
|
},
|
||||||
/** Digits of equal width, so a column of counts does not jitter as it updates. */
|
/** Digits of equal width, so a column of counts does not jitter as it updates. */
|
||||||
tabularNums: {
|
tabularNums: {
|
||||||
fontVariantNumeric: "tabular-nums",
|
fontVariantNumeric: "tabular-nums",
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export const colors = stylex.defineVars({
|
|||||||
surface: { default: "oklch(98.5% 0 none)", [DARK]: "oklch(14.1% 0.005 285.823)" },
|
surface: { default: "oklch(98.5% 0 none)", [DARK]: "oklch(14.1% 0.005 285.823)" },
|
||||||
/** Cards, dialogs and inputs sitting on top of the ground. */
|
/** Cards, dialogs and inputs sitting on top of the ground. */
|
||||||
surfaceRaised: { default: "#fff", [DARK]: "oklch(21% 0.006 285.885)" },
|
surfaceRaised: { default: "#fff", [DARK]: "oklch(21% 0.006 285.885)" },
|
||||||
|
/** The fill under a pointer on a nav item or a menu row. */
|
||||||
|
surfaceHover: { default: "oklch(96.7% 0.001 286.375)", [DARK]: "oklch(27.4% 0.006 286.033)" },
|
||||||
/** Hairlines between rows and around cards. */
|
/** Hairlines between rows and around cards. */
|
||||||
border: { default: "oklch(92% 0.004 286.32)", [DARK]: "oklch(27.4% 0.006 286.033)" },
|
border: { default: "oklch(92% 0.004 286.32)", [DARK]: "oklch(27.4% 0.006 286.033)" },
|
||||||
/** Control outlines, which need more contrast than a row divider. */
|
/** Control outlines, which need more contrast than a row divider. */
|
||||||
|
|||||||
Reference in New Issue
Block a user