Add emoji confetti bursts and component playground.

Support app.relay.emoji_confetti on jumbo emoji clicks with canvas physics,
per-emoji particle profiles, and a fixed overlay that does not affect scroll.
Also add a Vite playground for live component previews and theme hover preview.
This commit is contained in:
2026-07-29 20:40:36 +10:00
parent ea7642f0bb
commit 0da4f0d6cb
48 changed files with 3689 additions and 46 deletions

View File

@@ -8,6 +8,7 @@ import React, {
} from 'react';
import dayjs from 'dayjs';
import { as, Box, Button, Chip, color, config, Header, IconButton, Input, Menu, MenuItem, PopOut, RectCords, Scroll, Switch, Text, toRem } from 'folds';
import { useSetAtom } from 'jotai';
import { Icon, Icons } from '../../../components/icons';
import { HexColorPicker } from 'react-colorful';
import { isKeyHotkey } from 'is-hotkey';
@@ -24,6 +25,7 @@ import {
LightTheme,
Theme,
ThemeKind,
themePreviewIdAtom,
useSystemThemeKind,
useThemeNames,
useThemes,
@@ -41,28 +43,49 @@ type ThemeSelectorProps = {
onSelect: (theme: Theme) => void;
};
const ThemeSelector = as<'div', ThemeSelectorProps>(
({ themeNames, themes, selected, onSelect, ...props }, ref) => (
<Menu {...props} ref={ref}>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
{themes.map((theme) => (
<MenuItem
key={theme.id}
size="300"
variant={theme.id === selected.id ? 'Primary' : 'Surface'}
radii="300"
onClick={() => onSelect(theme)}
>
<Text size="T300">{themeNames[theme.id] ?? theme.id}</Text>
</MenuItem>
))}
</Box>
</Menu>
)
({ themeNames, themes, selected, onSelect, ...props }, ref) => {
const setPreviewId = useSetAtom(themePreviewIdAtom);
useEffect(
() => () => {
setPreviewId(undefined);
},
[setPreviewId]
);
const clearPreview = () => setPreviewId(undefined);
const previewTheme = (theme: Theme) => setPreviewId(theme.id);
return (
<Menu
{...props}
ref={ref}
onMouseLeave={clearPreview}
>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
{themes.map((theme) => (
<MenuItem
key={theme.id}
size="300"
variant={theme.id === selected.id ? 'Primary' : 'Surface'}
radii="300"
onMouseEnter={() => previewTheme(theme)}
onFocus={() => previewTheme(theme)}
onClick={() => onSelect(theme)}
>
<Text size="T300">{themeNames[theme.id] ?? theme.id}</Text>
</MenuItem>
))}
</Box>
</Menu>
);
}
);
function SelectTheme({ disabled }: { disabled?: boolean }) {
const themes = useThemes();
const themeNames = useThemeNames();
const setPreviewId = useSetAtom(themePreviewIdAtom);
const [themeId, setThemeId] = useSetting(settingsAtom, 'themeId');
const [menuCords, setMenuCords] = useState<RectCords>();
const selectedTheme = themes.find((theme) => theme.id === themeId) ?? LightTheme;
@@ -72,10 +95,16 @@ function SelectTheme({ disabled }: { disabled?: boolean }) {
};
const handleThemeSelect = (theme: Theme) => {
setPreviewId(undefined);
setThemeId(theme.id);
setMenuCords(undefined);
};
const closeMenu = () => {
setPreviewId(undefined);
setMenuCords(undefined);
};
return (
<>
<Button
@@ -99,7 +128,7 @@ function SelectTheme({ disabled }: { disabled?: boolean }) {
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => setMenuCords(undefined),
onDeactivate: closeMenu,
clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) =>
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
@@ -125,6 +154,7 @@ function SystemThemePreferences() {
const themeKind = useSystemThemeKind();
const themeNames = useThemeNames();
const themes = useThemes();
const setPreviewId = useSetAtom(themePreviewIdAtom);
const [lightThemeId, setLightThemeId] = useSetting(settingsAtom, 'lightThemeId');
const [darkThemeId, setDarkThemeId] = useSetting(settingsAtom, 'darkThemeId');
@@ -145,15 +175,27 @@ function SystemThemePreferences() {
};
const handleLightThemeSelect = (theme: Theme) => {
setPreviewId(undefined);
setLightThemeId(theme.id);
setLTCords(undefined);
};
const handleDarkThemeSelect = (theme: Theme) => {
setPreviewId(undefined);
setDarkThemeId(theme.id);
setDTCords(undefined);
};
const closeLightMenu = () => {
setPreviewId(undefined);
setLTCords(undefined);
};
const closeDarkMenu = () => {
setPreviewId(undefined);
setDTCords(undefined);
};
return (
<Box wrap="Wrap" gap="400">
<SettingTile
@@ -179,7 +221,7 @@ function SystemThemePreferences() {
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => setLTCords(undefined),
onDeactivate: closeLightMenu,
clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) =>
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
@@ -220,7 +262,7 @@ function SystemThemePreferences() {
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => setDTCords(undefined),
onDeactivate: closeDarkMenu,
clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) =>
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',