Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: npm

- name: Install dependencies
run: npm ci

- name: Test
run: npm run test
10 changes: 5 additions & 5 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Touchpoint Playground</title>
<script>
// Set the theme before first paint to avoid a flash of the wrong palette.
// Set the color mode before first paint to avoid a flash of the wrong palette.
(function () {
try {
var t = localStorage.getItem("lsTheme");
document.documentElement.dataset.theme =
t === "light" || t === "dark" ? t : "dark";
var m = localStorage.getItem("lsColorMode");
document.documentElement.dataset.colorMode =
m === "light" || m === "dark" ? m : "dark";
} catch (e) {
document.documentElement.dataset.theme = "dark";
document.documentElement.dataset.colorMode = "dark";
}
})();
</script>
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"prepublishOnly": "npm run build",
"lint": "eslint src/ --fix",
"format": "git ls-files | grep -E '\\.(js|jsx|ts|tsx|css|html)$' | xargs prettier --write",
"test": "typedoc --emit none && vitest run",
"format:check": "git ls-files | grep -E '\\.(js|jsx|ts|tsx|css|html)$' | xargs prettier --check",
"test": "npm run lint:check && npm run format:check && npm run build && typedoc --emit none && vitest run",
"tsc": "tsc",
"update-readme": "rm -rf docs/ && typedoc --excludeExternals --externalPattern src/components/ui/Icons.tsx && find docs -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} +"
},
Expand Down
5 changes: 4 additions & 1 deletion src/components/ErrorMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ export const ErrorMessage: FC<{
message: string;
}> = ({ message }) => {
return (
<div className="bg-error-secondary text-error-primary text-base p-3 rounded-inner flex items-center gap-2">
<div
role="alert"
className="bg-error-secondary text-error-primary text-base p-3 rounded-inner flex items-center gap-2"
>
<Warning className="w-4 h-4 flex-none" />
<p className="text-primary-80">{message}</p>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/FullscreenVoice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ export const FullscreenVoice: FC<Props> = ({
<IconButton
Icon={micEnabled ? Mic : MicOff}
label="Voice"
type={micEnabled ? "sound" : "ghost"}
type={micEnabled ? "subtle" : "ghost"}
onClick={() => {
setMicEnabled((prev) => !prev);
}}
Expand Down
3 changes: 1 addition & 2 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { type ColorMode } from "../interface";
import {
Close,
Settings,
Undo,
Restart,
Volume,
VolumeOff,
Expand Down Expand Up @@ -74,7 +73,7 @@ export const Header: FC<HeaderProps> = ({
className="ml-auto"
Icon={Settings}
label="Settings"
type={isSettingsOpen ? "sound" : iconButtonType}
type={isSettingsOpen ? "subtle" : iconButtonType}
onClick={enabled ? toggleSettings : undefined}
/>
) : null}
Expand Down
9 changes: 7 additions & 2 deletions src/components/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,10 @@ export const Input: FC<InputProps> = ({
)}
>
{uploadErrorMessage != null && (
<div className="flex items-center gap-1 px-2 py-1 mb-2 w-full bg-error-secondary rounded-inner">
<div
role="alert"
className="flex items-center gap-1 px-2 py-1 mb-2 w-full bg-error-secondary rounded-inner"
>
<Error size={16} className="flex-none" />
<span className="truncate">{uploadErrorMessage}</span>
</div>
Expand Down Expand Up @@ -275,6 +278,7 @@ export const Input: FC<InputProps> = ({
<>
<label
htmlFor="file-upload"
aria-label="Upload file"
className="p-3 w-10 h-10 flex-none block transition-colors rounded-full bg-primary-80 hover:bg-primary-80 text-secondary-80 cursor-pointer"
>
<Attachment />
Expand Down Expand Up @@ -305,6 +309,7 @@ export const Input: FC<InputProps> = ({
"disabled:text-primary-40",
)}
placeholder="Type something"
aria-label="Type something"
maxRows={10}
onFocus={() => {
setIsTextAreaInFocus(true);
Expand Down Expand Up @@ -339,7 +344,7 @@ export const Input: FC<InputProps> = ({
submit();
}
}
type="sound"
type="subtle"
Icon={Send}
/>
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/components/Messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -259,11 +259,12 @@ export const UserMessage: FC<{
)}
>
{files.map((file, index) => (
// TODO: style, add file name as alt text
// TODO: style
<img
className="rounded-inner h-20"
key={index}
src={URL.createObjectURL(file)}
alt={file.name}
/>
))}
</div>
Expand Down
65 changes: 3 additions & 62 deletions src/components/Theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export const toCustomProperties = (theme: Theme): CSSProperties => {

"--color-accent": theme.accent,
"--color-accent-20": theme.accent20,
"--color-on-accent": theme.onAccent,
"--color-background": theme.background,
"--color-overlay": theme.overlay,

Expand All @@ -45,7 +44,7 @@ export const toCustomProperties = (theme: Theme): CSSProperties => {
} as CSSProperties;
};

const customProperties: Theme = {
export const defaultTheme: Theme = {
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
innerBorderRadius: "20px",
Expand Down Expand Up @@ -73,10 +72,8 @@ const customProperties: Theme = {

// Accent defaults to black/white (matching primary) so that it stays
// understated out of the box, and setting a brand accent is clearly visible.
accent: "light-dark(rgba(0, 0, 0, 0.9), rgba(255, 255, 255, 0.95))",
accent: "light-dark(rgba(0, 0, 0, 1), rgba(255, 255, 255, 1))",
accent20: "light-dark(rgba(0, 0, 0, 0.2), rgba(255, 255, 255, 0.25))",
// The contrasting foreground on the default black/white accent (i.e. secondary).
onAccent: "light-dark(rgb(255, 255, 255), rgb(0, 2, 9))",
// Base surface fill (per Figma): light #F2F2F2 @ 90%, dark #1B1B21 @ 95%.
background: "light-dark(rgba(242, 242, 242, 0.9), rgba(27, 27, 33, 0.95))",
overlay: "light-dark(rgba(0, 2, 9, 0.4), rgba(0, 0, 0, 0.4))",
Expand All @@ -90,69 +87,13 @@ const customProperties: Theme = {
focus: "light-dark(rgba(0, 127, 217, 0.9), rgba(0, 149, 255, 0.7))",
};

/**
* Parses a solid CSS color (hex or rgb/rgba) into `[r, g, b]`. Returns null for
* anything mode-dependent or otherwise unresolvable (e.g. `light-dark(...)`,
* `var(...)`, named colors), where a single foreground can't be derived.
*/
const parseRgb = (color: string): [number, number, number] | null => {
const value = color.trim();
const hex = /^#([0-9a-f]{3,8})$/i.exec(value);
if (hex != null) {
let h = hex[1];
if (h.length === 3 || h.length === 4) {
h = h
.split("")
.map((c) => c + c)
.join("");
}
return [
parseInt(h.slice(0, 2), 16),
parseInt(h.slice(2, 4), 16),
parseInt(h.slice(4, 6), 16),
];
}
const rgb = /^rgba?\(([^)]+)\)$/i.exec(value);
if (rgb != null) {
const parts = rgb[1]
.split(/[,/\s]+/)
.filter(Boolean)
.slice(0, 3)
.map(Number);
if (parts.length === 3 && parts.every((n) => !isNaN(n))) {
return [parts[0], parts[1], parts[2]];
}
}
return null;
};

/**
* Derives a legible foreground color for content on top of `accent`. Colored
* accents keep a light foreground for a branded look; only near-white accents
* flip to dark. Falls back to secondary (light/dark) when the accent isn't a
* resolvable solid color.
*/
const deriveOnAccent = (accent: string): string => {
const rgb = parseRgb(accent);
if (rgb == null) {
return "light-dark(rgb(255, 255, 255), rgb(0, 2, 9))";
}
const [r, g, b] = rgb;
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance > 0.82 ? "rgb(0, 2, 9)" : "rgb(255, 255, 255)";
};

export const intelligentMerge = (theme: Partial<Theme>): Theme => {
const computed: Partial<Theme> = {};

if (theme.accent != null && theme.accent20 == null) {
computed.accent20 = `color-mix(in srgb, ${theme.accent} 20%, transparent)`;
}

if (theme.accent != null && theme.onAccent == null) {
computed.onAccent = deriveOnAccent(theme.accent);
}

if (theme.primary != null) {
if (theme.primary90 == null)
computed.primary90 = `rgb(from ${theme.primary} r g b / 0.9)`;
Expand Down Expand Up @@ -192,7 +133,7 @@ export const intelligentMerge = (theme: Partial<Theme>): Theme => {
computed.secondary1 = `rgb(from ${theme.secondary} r g b / 0.01)`;
}
return {
...customProperties,
...defaultTheme,
...computed,
...theme,
};
Expand Down
1 change: 1 addition & 0 deletions src/components/VoiceMini.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export const VoiceMini: FC<{
<img
className="w-10 h-10 block flex-none object-contain object-center"
src={brandIcon}
alt=""
role="presentation"
/>
) : null;
Expand Down
11 changes: 7 additions & 4 deletions src/components/ui/CustomCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,13 @@ export const CustomCard: FC<CustomCardProps> = ({
newTab,
}) => {
const containerClassName = clsx(
"block shrink-0 rounded-inner overflow-hidden",
"relative block shrink-0 rounded-inner bg-secondary-40 overflow-hidden",
"before:content-[''] before:absolute before:inset-0 before:bg-transparent",
"w-80 space-y-px",
selected ? "outline-solid outline-2 outline-accent" : "",
onClick != null || href != null ? "hover:bg-primary-5" : "",
"focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-focus",
"border border-solid",
selected ? "border-accent" : "border-primary-10",
onClick != null || href != null ? "hover:before:bg-primary-5" : "",
className,
);

Expand Down Expand Up @@ -174,7 +177,7 @@ export const CustomCardRow: FC<CustomCardRowProps> = ({
<div
className={clsx(
className,
"flex items-center relative justify-between gap-2 bg-primary-5 px-3 py-4",
"flex items-center relative justify-between gap-2 px-3 py-4",
)}
>
<div className="space-y-1 text-left">{left}</div>
Expand Down
1 change: 1 addition & 0 deletions src/components/ui/DateInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export const DateInput: FC<DateInputProps> = ({ onSubmit, className }) => {
: "placeholder-primary-40",
)}
placeholder="MM / DD / YYYY"
aria-label="Date"
onChange={(event) => {
const value = event.target.value;
setMaskedValue(value);
Expand Down
18 changes: 9 additions & 9 deletions src/components/ui/IconButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import { useAppRoot } from "../../utils/useAppRoot";
*
* - `main`: The primary icon button.
* - `ghost`: A transparent or less prominent icon button.
* - `sound`: An icon button that indicates an active state.
* - `subtle`: An icon button that indicates an active state.
* - `coverup`: An icon button used to cover up or mask something.
* @category Modality components
*/
export type IconButtonType = "main" | "ghost" | "sound" | "coverup" | "error";
export type IconButtonType = "main" | "ghost" | "subtle" | "coverup" | "error";

/**
* Props for the IconButton component
Expand Down Expand Up @@ -51,16 +51,16 @@ const mainClass =
"bg-primary-90 text-secondary-90 enabled:hover:before:bg-primary-90 enabled:active:before:bg-secondary-10 disabled:bg-primary-10 disabled:text-secondary-40";

const ghostClass =
"text-primary-80 enabled:hover:before:bg-primary-10 enabled:active:before:bg-secondary-10 disabled:text-primary-20";
"text-primary-60 enabled:hover:before:bg-primary-5 enabled:active:before:bg-secondary-10 disabled:text-primary-20";

const soundClass =
"bg-accent text-on-accent enabled:hover:before:bg-primary-40 enabled:active:before:bg-secondary-10 disabled:bg-accent-20";
const subtleClass =
"bg-primary-10 text-primary-80 enabled:hover:before:bg-primary-5 enabled:active:before:bg-primary-10 disabled:bg-primary-5 disabled:text-primary-20";

const coverupClass =
"bg-secondary-60 backdrop-blur-sm text-primary-80 enabled:hover:before:bg-primary-10 enabled:active:before:bg-secondary-10 disabled:bg-secondary-20 disabled:text-primary-20";
"bg-secondary-60 backdrop-blur-sm text-primary-80 enabled:hover:before:bg-primary-5 enabled:active:before:bg-primary-10 disabled:bg-secondary-20 disabled:text-primary-20";

const errorClass =
"bg-error-primary text-secondary enabled:hover:before:bg-primary-10 disabled:bg-secondary-20 disabled:text-primary-20";
"bg-error-primary text-secondary-90 enabled:hover:before:bg-primary-5 enabled:active:before:bg-primary-5 disabled:bg-secondary-20 disabled:text-primary-20";

// Used in <summary> tags
export const UnsemanticIconButton: FC<{
Expand All @@ -74,7 +74,7 @@ export const UnsemanticIconButton: FC<{
"block",
type === "main" ? mainClass : null,
type === "ghost" ? ghostClass : null,
type === "sound" ? soundClass : null,
type === "subtle" ? subtleClass : null,
type === "coverup" ? coverupClass : null,
type === "error" ? errorClass : null,
)}
Expand Down Expand Up @@ -155,7 +155,7 @@ export const IconButton: FC<IconButtonProps> = ({
baseClass,
type === "main" ? mainClass : null,
type === "ghost" ? ghostClass : null,
type === "sound" ? soundClass : null,
type === "subtle" ? subtleClass : null,
type === "coverup" ? coverupClass : null,
type === "error" ? errorClass : null,
className,
Expand Down
4 changes: 4 additions & 0 deletions src/components/ui/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ const iconSvgProps = (props: IconProps): SVGProps<SVGSVGElement> => ({
width: props.size != null ? `${props.size}px` : "100%",
height: props.size != null ? `${props.size}px` : "100%",
className: props.className,
// Icons are decorative: the accessible name always comes from the
// surrounding button or text, so keep them out of the accessibility tree.
"aria-hidden": true,
focusable: false,
});

export const Action: Icon = (props) => {
Expand Down
8 changes: 7 additions & 1 deletion src/components/ui/LaunchButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,18 @@ export const LaunchButton: FC<LaunchButtonProps> = (props) => {
)}
disabled={props.onClick == null}
onClick={props.onClick}
aria-label={props.label}
>
<span className="block flex-none w-8 h-8">
{props.iconUrl == null ? (
<Touchpoint />
) : (
<img src={props.iconUrl} className="w-8 h-8" />
<img
src={props.iconUrl}
alt=""
role="presentation"
className="w-8 h-8"
/>
)}
</span>
{(props.showLabel ?? false) ? <span>{props.label}</span> : null}
Expand Down
Loading
Loading