hidden shit, and online status bars, search settings

This commit is contained in:
2026-07-12 07:49:17 +10:00
parent 5374c10c61
commit 02d96a9758
31 changed files with 1323 additions and 265 deletions

View File

@@ -0,0 +1,18 @@
import { keyframes, style } from '@vanilla-extract/css';
import { color, config } from 'folds';
const focusPulse = keyframes({
'0%': {
outlineColor: color.Primary.Main,
},
'100%': {
outlineColor: 'transparent',
},
});
export const SettingTileFocus = style({
borderRadius: config.radii.R400,
outline: `${config.borderWidth.B300} solid ${color.Primary.Main}`,
outlineOffset: config.space.S100,
animation: `${focusPulse} 1.2s ease-out forwards`,
});

View File

@@ -1,6 +1,9 @@
import React, { ReactNode } from 'react';
import React, { ReactNode, useEffect, useRef, useState } from 'react';
import { Box, Text } from 'folds';
import classNames from 'classnames';
import { BreakWord } from '../../styles/Text.css';
import { settingAnchorId, useSettingsFocus } from './SettingsFocus';
import { SettingTileFocus } from './SettingTile.css';
type SettingTileProps = {
title?: ReactNode;
@@ -8,10 +11,65 @@ type SettingTileProps = {
before?: ReactNode;
after?: ReactNode;
children?: ReactNode;
/** Override auto-generated scroll anchor. */
anchorId?: string;
};
export function SettingTile({ title, description, before, after, children }: SettingTileProps) {
export function SettingTile({
title,
description,
before,
after,
children,
anchorId: anchorIdProp,
}: SettingTileProps) {
const ref = useRef<HTMLDivElement>(null);
const { anchorId: focusAnchorId, setAnchorId } = useSettingsFocus();
const [highlighted, setHighlighted] = useState(false);
const autoAnchorId = typeof title === 'string' ? settingAnchorId(title) : undefined;
const anchorId = anchorIdProp ?? autoAnchorId;
useEffect(() => {
if (!anchorId || !focusAnchorId || anchorId !== focusAnchorId) return;
let cancelled = false;
let attempts = 0;
const run = () => {
if (cancelled) return;
const node = ref.current;
if (node) {
node.scrollIntoView({ behavior: 'smooth', block: 'center' });
setHighlighted(true);
setAnchorId(undefined);
return;
}
if (attempts < 40) {
attempts += 1;
window.requestAnimationFrame(run);
}
};
const timer = window.setTimeout(run, 50);
return () => {
cancelled = true;
window.clearTimeout(timer);
};
}, [anchorId, focusAnchorId, setAnchorId]);
useEffect(() => {
if (!highlighted) return;
const timer = window.setTimeout(() => setHighlighted(false), 1400);
return () => window.clearTimeout(timer);
}, [highlighted]);
return (
<Box alignItems="Center" gap="300">
<Box
ref={ref}
id={anchorId}
alignItems="Center"
gap="300"
className={classNames(highlighted && SettingTileFocus)}
>
{before && <Box shrink="No">{before}</Box>}
<Box grow="Yes" direction="Column" gap="100">
{title && (

View File

@@ -0,0 +1,32 @@
import { createContext, useContext } from 'react';
export type SettingsFocus = {
/** Stable anchor id for a setting tile (from settingAnchorId). */
anchorId?: string;
setAnchorId: (anchorId: string | undefined) => void;
};
const SettingsFocusContext = createContext<SettingsFocus | null>(null);
export const SettingsFocusProvider = SettingsFocusContext.Provider;
export const useSettingsFocus = (): SettingsFocus => {
const ctx = useContext(SettingsFocusContext);
if (!ctx) {
return {
anchorId: undefined,
setAnchorId: () => undefined,
};
}
return ctx;
};
/** Stable DOM id for a settings tile title. */
export function settingAnchorId(title: string): string {
const slug = title
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
return `settings-item-${slug || 'untitled'}`;
}

View File

@@ -1 +1,2 @@
export * from './SettingTile';
export * from './SettingsFocus';