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:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user