Compare commits

...

21 Commits

Author SHA1 Message Date
Vixalie
0aba9bb15e adjust PulseCard operating style. 2025-03-21 17:23:31 +08:00
Vixalie
cc3b762880 adjust Pulse List layout. 2025-03-21 17:00:34 +08:00
Vixalie
346566b147 add PulseCard component. 2025-03-21 16:59:08 +08:00
Vixalie
4cdc73ca90 add extra adjust sequence logic. 2025-03-21 16:58:35 +08:00
Vixalie
9222e60b58 remove unused import. 2025-03-21 16:35:56 +08:00
Vixalie
770efe269a fix pulse frequency shifting serializing. 2025-03-21 16:35:27 +08:00
Vixalie
65d2e739ef fix atom chain. 2025-03-21 16:27:49 +08:00
Vixalie
3253b8b98e refactor Pattern and Pulse methods. 2025-03-21 14:25:03 +08:00
Vixalie
c6f0b2a8fc refactor StatusBar font size. 2025-03-20 11:29:13 +08:00
Vixalie
440caf4de0 refactor UI font size. 2025-03-20 11:20:14 +08:00
Vixalie
5b30d4c2bc move DndContext defination position. 2025-03-13 22:31:24 +08:00
Vixalie
fe62564f6e add @dnd-kit support. 2025-03-13 22:01:21 +08:00
Vixalie
cbaf999692 add pulse manipulate function in Pattern. 2025-03-13 21:46:17 +08:00
Vixalie
b3ddf3710e add pulse operation. 2025-03-13 21:45:46 +08:00
Vixalie
cc91868c63 add Pattern Preview canvas coordinates drawing. 2025-03-12 22:39:18 +08:00
Vixalie
3296eba4b2 add navigate to editor function to pattern detail. 2025-03-11 22:49:14 +08:00
Vixalie
13d32fb0e0 basicly complete pattern detail presentation. 2025-03-11 22:37:57 +08:00
Vixalie
abee609e43 fix empty pattern duration calculation. 2025-03-11 22:37:37 +08:00
Vixalie
03fe09d1ce add hr component styles. 2025-03-11 22:13:51 +08:00
Vixalie
3c7b3c76b9 extract PatternPreview component styles. 2025-03-11 17:26:27 +08:00
Vixalie
2ec95eb590 mark some future processes. 2025-03-11 17:06:56 +08:00
33 changed files with 843 additions and 219 deletions

48
deno.lock generated
View File

@@ -1,6 +1,9 @@
{ {
"version": "4", "version": "4",
"specifiers": { "specifiers": {
"npm:@dnd-kit/core@^6.3.1": "6.3.1_react@19.0.0_react-dom@19.0.0__react@19.0.0",
"npm:@dnd-kit/modifiers@9": "9.0.0_@dnd-kit+core@6.3.1__react@19.0.0__react-dom@19.0.0___react@19.0.0_react@19.0.0_react-dom@19.0.0__react@19.0.0",
"npm:@dnd-kit/sortable@10": "10.0.0_@dnd-kit+core@6.3.1__react@19.0.0__react-dom@19.0.0___react@19.0.0_react@19.0.0_react-dom@19.0.0__react@19.0.0",
"npm:@eslint/js@^9.19.0": "9.21.0", "npm:@eslint/js@^9.19.0": "9.21.0",
"npm:@iconify/react@^5.2.0": "5.2.0_react@19.0.0", "npm:@iconify/react@^5.2.0": "5.2.0_react@19.0.0",
"npm:@tauri-apps/api@2": "2.2.0", "npm:@tauri-apps/api@2": "2.2.0",
@@ -182,6 +185,48 @@
"@babel/helper-validator-identifier" "@babel/helper-validator-identifier"
] ]
}, },
"@dnd-kit/accessibility@3.1.1_react@19.0.0": {
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
"dependencies": [
"react",
"tslib"
]
},
"@dnd-kit/core@6.3.1_react@19.0.0_react-dom@19.0.0__react@19.0.0": {
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"dependencies": [
"@dnd-kit/accessibility",
"@dnd-kit/utilities",
"react",
"react-dom",
"tslib"
]
},
"@dnd-kit/modifiers@9.0.0_@dnd-kit+core@6.3.1__react@19.0.0__react-dom@19.0.0___react@19.0.0_react@19.0.0_react-dom@19.0.0__react@19.0.0": {
"integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==",
"dependencies": [
"@dnd-kit/core",
"@dnd-kit/utilities",
"react",
"tslib"
]
},
"@dnd-kit/sortable@10.0.0_@dnd-kit+core@6.3.1__react@19.0.0__react-dom@19.0.0___react@19.0.0_react@19.0.0_react-dom@19.0.0__react@19.0.0": {
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
"dependencies": [
"@dnd-kit/core",
"@dnd-kit/utilities",
"react",
"tslib"
]
},
"@dnd-kit/utilities@3.2.2_react@19.0.0": {
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"dependencies": [
"react",
"tslib"
]
},
"@esbuild/aix-ppc64@0.25.0": { "@esbuild/aix-ppc64@0.25.0": {
"integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==" "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ=="
}, },
@@ -1612,6 +1657,9 @@
"workspace": { "workspace": {
"packageJson": { "packageJson": {
"dependencies": [ "dependencies": [
"npm:@dnd-kit/core@^6.3.1",
"npm:@dnd-kit/modifiers@9",
"npm:@dnd-kit/sortable@10",
"npm:@eslint/js@^9.19.0", "npm:@eslint/js@^9.19.0",
"npm:@iconify/react@^5.2.0", "npm:@iconify/react@^5.2.0",
"npm:@tauri-apps/api@2", "npm:@tauri-apps/api@2",

View File

@@ -11,6 +11,9 @@
"tauri:dev": "tauri dev" "tauri:dev": "tauri dev"
}, },
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@iconify/react": "^5.2.0", "@iconify/react": "^5.2.0",
"@tauri-apps/plugin-dialog": "~2.2.0", "@tauri-apps/plugin-dialog": "~2.2.0",
"@tauri-apps/plugin-notification": "~2.2.1", "@tauri-apps/plugin-notification": "~2.2.1",

View File

@@ -94,6 +94,7 @@ impl ConfigDb {
.collect::<Vec<u8>>(); .collect::<Vec<u8>>();
db.remove(key) db.remove(key)
.map_err(|e| anyhow::anyhow!("Unable to remove pattern: {}", e))?; .map_err(|e| anyhow::anyhow!("Unable to remove pattern: {}", e))?;
// todo: need to remove requested pattern in all playlists.
db.flush_async() db.flush_async()
.await .await
.map_err(|e| anyhow::anyhow!("Unable to save db: {}", e))?; .map_err(|e| anyhow::anyhow!("Unable to save db: {}", e))?;

View File

@@ -1,9 +1,11 @@
use rand::Rng; use rand::Rng;
use serde::{Deserialize, Serialize}; use serde::Deserialize;
use serde_repr::{Deserialize_repr, Serialize_repr};
use crate::fraction::Fraction; use crate::fraction::Fraction;
#[derive(Debug, Copy, Clone, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, Serialize_repr, Deserialize_repr)]
#[repr(u32)]
pub enum FrequencyShifting { pub enum FrequencyShifting {
Linear, Linear,
Quadratic, Quadratic,

View File

@@ -69,6 +69,11 @@
border-radius: calc(var(--border-radius) * 2); border-radius: calc(var(--border-radius) * 2);
padding-block: calc(var(--spacing)); padding-block: calc(var(--spacing));
} }
&.name {
font-size: var(--label-small-font-size);
line-height: var(--label-small-line-height);
font-weight: var(--label-small-font-weight);
}
} }
&.inactive { &.inactive {
color: var(--color-on-surface-variant); color: var(--color-on-surface-variant);

View File

@@ -24,7 +24,7 @@ const FunctionLink: FC<FunctionLinkProps> = ({ name, url, end, children }) => {
} }
end={end}> end={end}>
<div className={styles.filled}>{children}</div> <div className={styles.filled}>{children}</div>
<div>{name}</div> <div className={styles.name}>{name}</div>
</NavLink> </NavLink>
); );
}; };

View File

@@ -23,33 +23,40 @@
} }
} }
:is(h1, h2, h3, h4, h5, h6) {
font-weight: bold;
line-height: 1.2em;
}
h1 { h1 {
font-size: 2.6em; font-size: 2rem;
line-height: 2.5rem;
font-weight: 400;
} }
h2 { h2 {
font-size: 2em; font-size: 1.75rem;
line-height: 2.25rem;
font-weight: 400;
} }
h3 { h3 {
font-size: 1.8em; font-size: 1.5rem;
line-height: 2rem;
font-weight: 400;
} }
h4 { h4 {
font-size: 1.5em; font-size: 1.375rem;
line-height: 1.75rem;
font-weight: 500;
} }
h5 { h5 {
font-size: 1.2em; font-size: 1rem;
line-height: 1.5rem;
font-weight: 500;
} }
h6 { h6 {
font-size: 1em; font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
} }
.center { .center {
@@ -80,6 +87,29 @@
} }
} }
hr {
margin: 0;
padding: 0;
border-width: 0;
border-top-width: 1px;
border-color: var(--color-outline-variant);
border-style: solid;
width: 100%;
align-self: stretch;
&.dashed {
border-style: dashed;
}
&.dotted {
border-style: dotted;
}
&.vertical {
border-top-width: 0;
border-left-width: 1px;
width: auto;
height: 100%;
}
}
:where(button, .button) { :where(button, .button) {
border: none; border: none;
border-radius: calc(var(--border-radius) * 2); border-radius: calc(var(--border-radius) * 2);
@@ -89,23 +119,22 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: calc(var(--spacing) * 2); gap: calc(var(--spacing) * 2);
font-size: calc(var(--font-size) * 1.2); font-size: var(--label-medium-font-size);
line-height: 1.3em; line-height: var(--label-medium-line-height);
font-weight: var(--label-medium-font-weight);
color: var(--button-text); color: var(--button-text);
background-color: var(--button-surface); background-color: var(--button-surface);
box-shadow: var(--elevation-0); box-shadow: var(--elevation-0);
cursor: pointer; cursor: pointer;
&.smaller {
font-size: calc(var(--font-size) * 0.8);
}
&.small { &.small {
font-size: calc(var(--font-size) * 1); font-size: var(--label-small-font-size);
line-height: var(--label-small-line-height);
font-weight: var(--label-small-font-weight);
} }
&.large { &.large {
font-size: calc(var(--font-size) * 1.4); font-size: var(--label-large-font-size);
} line-height: var(--label-large-line-height);
&.larger { font-weight: var(--label-large-font-weight);
font-size: calc(var(--font-size) * 1.6);
} }
&:hover:not(:disabled) { &:hover:not(:disabled) {
color: var(--button-surface); color: var(--button-surface);
@@ -433,7 +462,9 @@
border-top-left-radius: calc(var(--border-radius) * 2); border-top-left-radius: calc(var(--border-radius) * 2);
border-top-right-radius: calc(var(--border-radius) * 2); border-top-right-radius: calc(var(--border-radius) * 2);
padding: calc(var(--spacing)) calc(var(--spacing) * 2); padding: calc(var(--spacing)) calc(var(--spacing) * 2);
line-height: 1.5em; font-size: var(--body-small-font-size);
line-height: var(--body-small-line-height);
font-weight: var(--body-small-font-weight);
color: var(--color-on-surface); color: var(--color-on-surface);
background-color: var(--color-surface-container-highest); background-color: var(--color-surface-container-highest);
&.error { &.error {

View File

@@ -1,15 +1,11 @@
@layer components { @layer components {
.pattern_preview { .pattern_preview {
flex-basis: 140px; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
gap: calc(var(--spacing) * 2); gap: calc(var(--spacing) * 2);
color: var(--color-on-surface);
background-color: var(--color-surface-container);
padding: calc(var(--spacing)) calc(var(--spacing) * 2);
border-radius: calc(var(--border-radius) * 2);
.canvas_wrapper { .canvas_wrapper {
flex: 1 0; flex: 1 0;
canvas { canvas {

View File

@@ -1,17 +1,72 @@
import { FC } from 'react'; import { getDefaultStore } from 'jotai';
import { FC, useEffect, useRef, useState } from 'react';
import { useMeasure } from 'react-use';
import { ErrorColorAtom, PrimaryColorAtom } from '../context/ThemeColors';
import styles from './PatternPreview.module.css'; import styles from './PatternPreview.module.css';
interface RawPusle {
tickOrder: number;
x: number;
y: number;
z: number;
frequencyLevel: number;
}
const canvasSafeBound = { const canvasSafeBound = {
coordinate: { pt: 5.5, pr: 5.5, pb: 5.5, pl: 5.5 }, coordinate: { pt: 5.5, pr: 5.5, pb: 5.5, pl: 5.5 },
chart: { pt: 5.5, pr: 5.5, pb: 5.5, pl: 5.5 }, chart: { pt: 5.5, pr: 5.5, pb: 5.5, pl: 5.5 },
}; };
function drawCoordinates(ctx: CanvasRenderingContext2D, width: number, height: width) {
const colorStore = getDefaultStore();
ctx.clearRect(0, 0, width, height);
ctx.beginPath();
// Draw x axis
ctx.moveTo(canvasSafeBound.coordinate.pl, height - canvasSafeBound.coordinate.pb);
ctx.lineTo(width - canvasSafeBound.coordinate.pr, height - canvasSafeBound.coordinate.pt);
ctx.strokeStyle = colorStore.get(PrimaryColorAtom);
ctx.stroke();
ctx.beginPath();
// Draw aggressive mark line
const tickUnit = (height - canvasSafeBound.coordinate.pt - canvasSafeBound.coordinate.pb) / 31;
const highlightY = height - canvasSafeBound.coordinate.pb - 20 * tickUnit;
ctx.moveTo(canvasSafeBound.coordinate.pl, highlightY);
ctx.lineTo(width - canvasSafeBound.coordinate.pr, highlightY);
ctx.strokeStyle = colorStore.get(ErrorColorAtom);
ctx.stroke();
}
const PatternPreview: FC = () => { const PatternPreview: FC = () => {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [conatienrRef, { width, height }] = useMeasure();
const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const canvas = canvasRef.current;
if (canvas) {
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio ?? 1;
setCanvasSize({ width: (width ?? 0) * dpr, height: (height ?? 0) * dpr });
canvas.width = (width ?? 0) * dpr;
canvas.height = (height ?? 0) * dpr;
}
}, [width, height]);
useEffect(() => {
const canvas = canvasRef.current;
if (canvas) {
const ctx = canvas.getContext('2d');
drawCoordinates(ctx, canvasSize.width, canvasSize.height);
}
}, [canvasSize]);
return ( return (
<div className={styles.pattern_preview}> <div className={styles.pattern_preview}>
<h4>Pattern Preview</h4> <h5>Pattern Preview</h5>
<div> <div className={styles.canvas_wrapper} ref={conatienrRef}>
<canvas /> <canvas ref={canvasRef} />
</div> </div>
</div> </div>
); );

View File

@@ -1,125 +1,9 @@
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import dayjs from 'dayjs'; import { atom, useAtomValue, useSetAtom } from 'jotai';
import { atom, useSetAtom } from 'jotai';
import { atomWithRefresh } from 'jotai/utils'; import { atomWithRefresh } from 'jotai/utils';
import { get, reduce } from 'lodash-es'; import { useCallback } from 'react';
import { v4 } from 'uuid';
import { NotificationType, ToastDuration, useNotification } from '../components/Notifications'; import { NotificationType, ToastDuration, useNotification } from '../components/Notifications';
import { Pattern, Pulse, totalDuration } from './pattern-model';
export enum FrequencyShifting {
/**
* Change frequency undergoes a linear transformation from previous pulse to current one.
*/
Linear,
/**
* Change frequency undergoes a quadratic transformation from previous pulse to current one.
*/
Quadratic,
/**
* Change frequency undergoes a cubic transformation from previous pulse to current one.
*/
Cubic,
/**
* Change frequency with quick fade in and fade out.
*/
Ease,
/**
* Change frequency with spiking within range from previous pulse and current one.
*/
Pulsating,
/**
* Based on frequency of previous pulse, take the twice of frequency of current pulse as the peak frequency. Follow the maximum frequency limitation.
*/
Spiking,
/**
* Randomize frequency within range from previous frequency and current one.
*/
Randomize,
/**
* Randomize frequency within minium and maximum frequency.
*/
Maniac,
/**
* Synchronize changes of frequency with pulse width changes.
*/
Synchronized,
}
export interface ControlPoint {
x: number;
y: number;
}
export class Pulse {
order: number;
id: number;
offset: number;
width: number;
maniac: boolean;
frequency: number;
frequencyShifting: FrequencyShifting;
controlPoint1: ControlPoint;
controlPoint2: ControlPoint;
constructor(order: number, width: number, frequency: number) {
this.id = v4();
this.order = order;
this.offset = 0;
this.width = width;
this.maniac = false;
this.frequency = frequency;
this.frequencyShifting = FrequencyShifting.Linear;
this.controlPoint1 = { x: 0, y: 0 };
this.controlPoint2 = { x: 0, y: 0 };
}
equals(other: Pulse): boolean {
return this.id === other.id;
}
}
export class Pattern {
id: string;
name: string;
createdAt: number;
lastModifiedAt: number | null;
smoothRepeat: boolean;
pulses: Pulse[];
constructor() {
this.id = v4();
this.name = '';
this.createdAt = dayjs().valueOf();
this.lastModifiedAt = null;
this.smoothRepeat = true;
this.pulses = [];
}
}
export function createNewPulse(pattern: Pattern): Pulse {
const maxOrder = reduce(pattern.pulses, (former, pulse) => Math.max(former, pulse.order), 0);
if (pattern.smoothRepeat) {
return new Pulse(
maxOrder + 1,
get(pattern.pulses, '[0].width', 0),
get(pattern.pulses, '[0].frequency', 1),
);
} else {
return new Pulse(
maxOrder + 1,
get(pattern.pulses, '[-1].width', 0),
get(pattern.pulses, '[-1].frequency', 1),
);
}
}
export function totalDuration(pattern: Pattern): number {
return reduce(
pattern.pulses,
(former, pulse) => former + pulse.offset,
pattern.smoothRepeat ? 100 : 0,
);
}
export const SearchKeywordAtom = atom<string | null>(null); export const SearchKeywordAtom = atom<string | null>(null);
export const PatternsAtom = atomWithRefresh(async (get) => { export const PatternsAtom = atomWithRefresh(async (get) => {
@@ -133,7 +17,7 @@ export const PatternsAtom = atomWithRefresh(async (get) => {
return []; return [];
}); });
export const SelectedPatternIdAtom = atom<string | null>(null); export const SelectedPatternIdAtom = atom<string | null>(null);
export const CurrentPatternAtom = atom<Pattern | null>(async (get) => { export const CurrentPatternAtom = atomWithRefresh<Pattern | null>(async (get) => {
try { try {
const patternId = get(SelectedPatternIdAtom); const patternId = get(SelectedPatternIdAtom);
if (patternId === null) { if (patternId === null) {
@@ -146,35 +30,26 @@ export const CurrentPatternAtom = atom<Pattern | null>(async (get) => {
} }
return null; return null;
}); });
export const PulsesInCurrentPatternAtom = atom(
(get) => get(CurrentPatternAtom)?.pulses ?? [],
(get, set, pulse: Pulse) => {
const currentPulses = get(CurrentPatternAtom)?.pulses ?? [];
const newPulses = currentPulses.map((p) => (p.id === pulse.id ? pulse : p));
if (!newPulses.some((p) => p.id === pulse.id)) {
newPulses.push(pulse);
}
newPulses.sort((a, b) => a.order - b.order);
const currentPattern = get(CurrentPatternAtom);
if (currentPattern) {
set(CurrentPatternAtom, {
...currentPattern,
pulses: newPulses,
});
}
},
);
export const CurrentPatternDuration = atom((get) => { export const CurrentPatternDuration = atom((get) => {
const currentPattern = get(CurrentPatternAtom); const currentPattern = get(CurrentPatternAtom);
if (!currentPattern) return 0; if (!currentPattern) return 0;
return totalDuration(currentPattern); return totalDuration(currentPattern);
}); });
export const SelectedPulseIdAtom = atom<string | null>(null);
export const SelectedPulseAtom = atom<Pulse | null>(async (get) => {
const pattern = await get(CurrentPatternAtom);
const selectedPulseId = get(SelectedPulseIdAtom);
console.debug('[refresh selected pulse]', selectedPulseId, pattern);
return pattern?.pulses?.find((pulse) => pulse.id === selectedPulseId) ?? null;
});
export function useSavePattern() { export function useSavePattern() {
const refreshPatterns = useSetAtom(PatternsAtom); const refreshPatterns = useSetAtom(PatternsAtom);
const selectedPatternId = useAtomValue(SelectedPatternIdAtom);
const { showToast } = useNotification(); const { showToast } = useNotification();
const savePattern = async (pattern: Pattern) => { const savePattern = useCallback(
async (pattern: Pattern) => {
try { try {
await invoke('save_pattern', { pattern }); await invoke('save_pattern', { pattern });
refreshPatterns(); refreshPatterns();
@@ -189,7 +64,9 @@ export function useSavePattern() {
); );
} }
return false; return false;
}; },
[selectedPatternId],
);
return savePattern; return savePattern;
} }

View File

@@ -0,0 +1,23 @@
import { atomWithRefresh } from 'jotai/utils';
export const PrimaryColorAtom = atomWithRefresh(() =>
window.getComputedStyle(document.documentElement).getPropertyValue('--color-primary').trim(),
);
export const SecondaryColorAtom = atomWithRefresh(() =>
window.getComputedStyle(document.documentElement).getPropertyValue('--color-secondary').trim(),
);
export const TeritaryColorAtom = atomWithRefresh(() =>
window.getComputedStyle(document.documentElement).getPropertyValue('--color-tertiary').trim(),
);
export const ErrorColorAtom = atomWithRefresh(() =>
window.getComputedStyle(document.documentElement).getPropertyValue('--color-error').trim(),
);
export const GentleColorAtom = atomWithRefresh(() =>
window.getComputedStyle(document.documentElement).getPropertyValue('--color-gentle').trim(),
);
export const AggressiveColorAtom = atomWithRefresh(() =>
window.getComputedStyle(document.documentElement).getPropertyValue('--color-aggressive').trim(),
);
export const DefensiveColorAtom = atomWithRefresh(() =>
window.getComputedStyle(document.documentElement).getPropertyValue('--color-defensive').trim(),
);

View File

@@ -0,0 +1,190 @@
import dayjs from 'dayjs';
import { get, reduce } from 'lodash-es';
import { v4 } from 'uuid';
export enum FrequencyShifting {
/**
* Change frequency undergoes a linear transformation from previous pulse to current one.
*/
Linear = 0,
/**
* Change frequency undergoes a quadratic transformation from previous pulse to current one.
*/
Quadratic = 1,
/**
* Change frequency undergoes a cubic transformation from previous pulse to current one.
*/
Cubic = 2,
/**
* Change frequency with quick fade in and fade out.
*/
Ease = 3,
/**
* Change frequency with spiking within range from previous pulse and current one.
*/
Pulsating = 4,
/**
* Based on frequency of previous pulse, take the twice of frequency of current pulse as the peak frequency. Follow the maximum frequency limitation.
*/
Spiking = 5,
/**
* Randomize frequency within range from previous frequency and current one.
*/
Randomize = 6,
/**
* Randomize frequency within minium and maximum frequency.
*/
Maniac = 7,
/**
* Synchronize changes of frequency with pulse width changes.
*/
Synchronized = 8,
}
export interface ControlPoint {
x: number;
y: number;
}
export interface Pulse {
order: number;
id: number;
offset: number;
width: number;
maniac: boolean;
frequency: number;
frequencyShifting: FrequencyShifting;
controlPoint1: ControlPoint;
controlPoint2: ControlPoint;
}
export function createPulse(order: number, width: number, frequency: number) {
return {
id: v4(),
order,
offset: 0,
width,
maniac: false,
frequency,
frequencyShifting: FrequencyShifting.Linear,
controlPoint1: { x: 0, y: 0 },
controlPoint2: { x: 0, y: 0 },
} as Pulse;
}
export interface Pattern {
id: string;
name: string;
createdAt: number;
lastModifiedAt: number | null;
smoothRepeat: boolean;
pulses: Pulse[];
}
export function createPattern() {
return {
id: v4(),
name: '',
createdAt: dayjs().valueOf(),
lastModifiedAt: null,
smoothRepeat: true,
pulses: [],
} as Pattern;
}
export function movePulseUp(pattern: Pattern, pulseId: string, step: number) {
const index = pattern.pulses.findIndex((pulse) => pulse.id === pulseId);
if (index === -1 || index - step < 0) return;
const targetIndex = index - step;
const targetPulse = pattern.pulses[targetIndex];
const currentPulse = pattern.pulses[index];
// Swap the pulses
pattern.pulses[targetIndex] = currentPulse;
pattern.pulses[index] = targetPulse;
// If the target pulse's order is 1, swap their offsets
if (targetPulse.order === 1) {
const tempOffset = currentPulse.offset;
currentPulse.offset = targetPulse.offset;
targetPulse.offset = tempOffset;
}
// Swap their order
const tempOrder = currentPulse.order;
currentPulse.order = targetPulse.order;
targetPulse.order = tempOrder;
// Sort pulses by order
pattern.pulses.sort((a, b) => a.order - b.order);
}
export function movePulseDown(pattern: Pattern, pulseId: string, step: number) {
const index = pattern.pulses.findIndex((pulse) => pulse.id === pulseId);
if (index === -1 || index + step >= pattern.pulses.length) return;
const targetIndex = index + step;
const targetPulse = pattern.pulses[targetIndex];
const currentPulse = pattern.pulses[index];
// Swap the pulses
pattern.pulses[targetIndex] = currentPulse;
pattern.pulses[index] = targetPulse;
// If the current pulse's order is 1, swap their offsets
if (currentPulse.order === 1) {
const tempOffset = currentPulse.offset;
currentPulse.offset = targetPulse.offset;
targetPulse.offset = tempOffset;
}
// Swap their order
const tempOrder = currentPulse.order;
currentPulse.order = targetPulse.order;
targetPulse.order = tempOrder;
// Sort pulses by order
pattern.pulses.sort((a, b) => a.order - b.order);
}
export function addPulse(pattern: Pattern): Pulse {
const maxOrder = reduce(pattern.pulses, (former, pulse) => Math.max(former, pulse.order), 0);
const newPulse = createPulse(
maxOrder + 1,
pattern.smoothRepeat
? get(pattern.pulses, '[0].width', 0)
: get(pattern.pulses, '[-1].width', 0),
pattern.smoothRepeat
? get(pattern.pulses, '[0].frequency', 1)
: get(pattern.pulses, '[-1].frequency', 1),
);
pattern.pulses.push(newPulse);
return newPulse;
}
export function updatePulse(pattern: Pattern, pulseId: string, pulse: Pulse) {
const index = pattern.pulses.findIndex((p) => p.id === pulseId);
if (index !== -1) {
const { id, order, ...rest } = pulse;
pattern.pulses[index] = { ...pattern.pulses[index], ...rest };
}
}
export function deletePulse(pattern: Pattern, pulseId: string) {
pattern.pulses = pattern.pulses.filter((pulse) => pulse.id !== pulseId);
pattern.pulses.sort((a, b) => a.order - b.order);
pattern.pulses.forEach((pulse, index) => {
pulse.order = index + 1;
});
}
export function durationRemains(pattern: Pattern): number {
return 20 * 1000 - totalDuration(pattern) - (pattern.smoothRepeat ? 100 : 0);
}
export function totalDuration(pattern: Pattern): number {
return reduce(
pattern.pulses,
(former, pulse) => former + pulse.offset,
pattern.smoothRepeat && pattern.pulses.length > 1 ? 100 : 0,
);
}

View File

@@ -12,6 +12,6 @@ export const defaultIconProps: Partial<IconProps> = {
}; };
export const smallIconProps: Partial<IconProps> = { export const smallIconProps: Partial<IconProps> = {
height: 16, height: 14,
stroke: 0.5, stroke: 0.5,
}; };

View File

@@ -31,9 +31,11 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: calc(var(--spacing) * 1); gap: calc(var(--spacing) * 1);
font-size: var(--body-medium-font-size);
line-height: var(--body-medium-line-height);
font-weight: var(--body-medium-font-weight);
.device_name { .device_name {
flex: 1; flex: 1;
font-size: calc(var(--font-size) * 1.3);
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }

View File

@@ -18,7 +18,7 @@ const PatternHeader: FC = () => {
return ( return (
<div className={styles.pattern_header}> <div className={styles.pattern_header}>
<EditableContent <EditableContent
as="h3" as="h4"
placeholder="Pattern Name" placeholder="Pattern Name"
value={currentPattern?.name ?? null} value={currentPattern?.name ?? null}
additionalClassName={styles.content} additionalClassName={styles.content}

View File

@@ -5,7 +5,7 @@
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
gap: calc(var(--spacing)); gap: calc(var(--spacing));
padding: calc(var(--spacing) * 2) calc(var(--spacing)); padding: calc(var(--spacing) * 2) calc(var(--spacing) * 2);
border-radius: calc(var(--border-radius) * 2); border-radius: calc(var(--border-radius) * 2);
color: var(--color-on-surface); color: var(--color-on-surface);
background-color: var(--color-surface-container); background-color: var(--color-surface-container);
@@ -15,5 +15,8 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: calc(var(--spacing) * 2); gap: calc(var(--spacing) * 2);
font-size: var(--body-small-font-size);
line-height: var(--body-small-line-height);
font-weight: var(--body-small-font-weight);
} }
} }

View File

@@ -0,0 +1,53 @@
@layer pages {
.pulse_card {
display: flex;
flex-direction: row;
align-items: center;
gap: calc(var(--spacing) * 2);
color: var(--color-on-surface);
background-color: var(--color-surface);
border-radius: calc(var(--border-radius) * 2);
padding: 0;
.order {
padding: calc(var(--spacing) * 2) calc(var(--spacing) * 3);
background-color: var(--color-secondary-container);
color: var(--color-on-secondary-container);
border-top-left-radius: calc(var(--border-radius) * 2);
border-bottom-left-radius: calc(var(--border-radius) * 2);
}
.offset {
padding: calc(var(--spacing) * 2) calc(var(--spacing) * 3);
}
&.selected {
background-color: color-mix(in oklch, var(--color-on-surface) 18%, transparent);
.order {
background-color: color-mix(
in oklch,
var(--color-secondary-container) 88%,
var(--color-white)
);
}
}
&:hover {
background-color: color-mix(in oklch, var(--color-on-surface) 8%, transparent);
box-shadow: var(--elevation-2-ambient), var(--elevation-2-umbra);
.order {
background-color: color-mix(
in oklch,
var(--color-secondary-container) 60%,
var(--color-white)
);
}
}
&:active {
background-color: color-mix(in oklch, var(--color-on-surface) 18%, transparent);
.order {
background-color: color-mix(
in oklch,
var(--color-secondary-container) 68%,
var(--color-white)
);
}
}
}
}

View File

@@ -0,0 +1,34 @@
import cx from 'clsx';
import { useAtom } from 'jotai';
import { FC, useCallback } from 'react';
import { Pulse } from '../../context/pattern-model';
import { SelectedPulseIdAtom } from '../../context/Patterns';
import styles from './PulseCard.module.css';
type PulseCardProps = {
pulse: Pulse;
};
const PulseCard: FC<PulseCardProps> = ({ pulse }) => {
const [selected, setSelected] = useAtom(SelectedPulseIdAtom);
const handleSelect = useCallback(() => {
if (selected === pulse.id) {
setSelected(null);
} else {
setSelected(pulse.id);
}
}, [pulse, selected]);
return (
<div
className={cx(styles.pulse_card, selected === pulse.id && styles.selected)}
onClick={handleSelect}>
<div className={styles.order}>{pulse.order.toString().padStart(3, '0')}</div>
<div className="spacer" />
<div className={styles.offset}>+ {pulse.offset} ms</div>
</div>
);
};
export default PulseCard;

View File

@@ -5,6 +5,18 @@
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
gap: calc(var(--spacing) * 2); gap: calc(var(--spacing) * 2);
.attribute_unit {
display: flex;
flex-direction: row;
align-items: center;
gap: calc(var(--spacing));
font-size: var(--body-small-font-size);
line-height: var(--body-small-line-height);
font-weight: var(--body-small-font-weight);
label {
font-weight: 500;
}
}
} }
.pulses { .pulses {
min-width: 0; min-width: 0;
@@ -17,4 +29,10 @@
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
} }
.pulse_cards {
display: flex;
flex-direction: column;
align-items: stretch;
gap: calc(var(--spacing) * 2);
}
} }

View File

@@ -1,19 +1,91 @@
import { Icon } from '@iconify/react/dist/iconify.js'; import { Icon } from '@iconify/react/dist/iconify.js';
import { FC } from 'react'; import { useAtom, useAtomValue } from 'jotai';
import { max } from 'lodash-es';
import { FC, useCallback, useMemo } from 'react';
import { ScrollArea } from '../../components/ScrollArea'; import { ScrollArea } from '../../components/ScrollArea';
import { addPulse, deletePulse } from '../../context/pattern-model';
import {
CurrentPatternAtom,
CurrentPatternDuration,
SelectedPulseAtom,
useSavePattern,
} from '../../context/Patterns';
import PulseCard from './PulseCard';
import styles from './PulseList.module.css'; import styles from './PulseList.module.css';
const PulseList: FC = () => { const PulseList: FC = () => {
const [pattern, refreshPattern] = useAtom(CurrentPatternAtom);
const duration = useAtomValue(CurrentPatternDuration);
const selectedPulse = useAtomValue(SelectedPulseAtom);
const maxPulseOrder = useMemo(
() => max(pattern?.pulses.map((pulse) => pulse.order) ?? []),
[pattern],
);
const savePattern = useSavePattern();
const handleAddPulseAction = useCallback(async () => {
if (!pattern) return;
addPulse(pattern);
await savePattern(pattern);
refreshPattern();
}, [pattern]);
const handleDeletePulseAction = useCallback(async () => {
if (!pattern || !selectedPulse) return;
deletePulse(pattern, selectedPulse.id);
await savePattern(pattern);
refreshPattern();
}, [pattern, selectedPulse]);
return ( return (
<> <>
<div className={styles.pulse_tools}> <div className={styles.pulse_tools}>
<button className="text"> <div className={styles.attribute_unit}>
<span>{pattern?.pulses.length ?? 0}</span>
<label>Key Pulses</label>
</div>
<div className={styles.attribute_unit}>
<label>Total Duration</label>
<span>{duration.toFixed(2)} s</span>
</div>
</div>
<div className={styles.pulse_tools}>
<button className="text" disabled>
<Icon icon="material-symbols-light:play-arrow-outline" />
<span>Test Run</span>
</button>
<button className="text" onClick={handleAddPulseAction}>
<Icon icon="material-symbols-light:add" /> <Icon icon="material-symbols-light:add" />
<span>Add Pulse</span> <span>Add Pulse</span>
</button> </button>
<button className="text" onClick={handleDeletePulseAction} disabled={!selectedPulse}>
<Icon icon="material-symbols-light:delete-forever-outline" />
<span>Delete Selected</span>
</button>
</div>
<div className={styles.pulse_tools}>
<button className="text" disabled={!selectedPulse || selectedPulse.order === 1}>
<Icon icon="material-symbols-light:arrow-upward" />
<span>Move Up</span>
</button>
<button className="text" disabled={!selectedPulse || selectedPulse.order === maxPulseOrder}>
<Icon icon="material-symbols-light:arrow-downward" />
<span>Move Down</span>
</button>
</div> </div>
<div className={styles.pulses}> <div className={styles.pulses}>
<ScrollArea enableY></ScrollArea> <ScrollArea enableY>
<div className={styles.pulse_cards}>
{pattern?.pulses.length === 0 ? (
<div className="empty_prompt">No key pulses.</div>
) : (
<>
{pattern?.pulses.map((pulse) => (
<PulseCard key={pulse.id} pulse={pulse} />
))}
</>
)}
</div>
</ScrollArea>
</div> </div>
</> </>
); );

View File

@@ -3,15 +3,58 @@
flex: 2; flex: 2;
border-radius: calc(var(--border-radius) * 2); border-radius: calc(var(--border-radius) * 2);
background-color: var(--color-surface-container); background-color: var(--color-surface-container);
padding: calc(var(--spacing) * 2) calc(var(--spacing) * 2);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
gap: calc(var(--spacing) * 2); gap: calc(var(--spacing) * 2);
.control_panel {
display: flex;
flex-direction: column;
gap: calc(var(--spacing) * 2);
.button_row {
display: flex;
flex-direction: row;
align-items: center;
gap: calc(var(--spacing) * 2);
}
}
.detail_panel {
flex: 1;
display: flex;
flex-direction: column;
gap: calc(var(--spacing) * 2);
.detail_row {
display: flex;
flex-direction: row;
align-items: center;
gap: calc(var(--spacing) * 2);
.detail_unit {
display: flex;
flex-direction: row;
align-items: center;
gap: calc(var(--spacing));
label {
min-width: 8em;
flex: 0 0 8em;
text-align: right;
}
.content {
flex: 1 1;
}
}
}
}
.preview_panel {
min-height: 140px;
flex: 0 0;
}
} }
.empty_promption { .empty_promption {
flex: 2; flex: 2;
border-radius: calc(var(--border-radius) * 2); border-radius: calc(var(--border-radius) * 2);
background-color: var(--color-surface-container); background-color: var(--color-surface-container);
padding: calc(var(--spacing) * 2) calc(var(--spacing) * 2);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;

View File

@@ -1,6 +1,13 @@
import { useAtomValue } from 'jotai'; import { invoke } from '@tauri-apps/api/core';
import { FC } from 'react'; import { ask } from '@tauri-apps/plugin-dialog';
import { CurrentPatternAtom, Pattern } from '../../context/Patterns'; import dayjs from 'dayjs';
import { useAtomValue, useSetAtom } from 'jotai';
import { FC, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { NotificationType, useNotification } from '../../components/Notifications';
import PatternPreview from '../../components/PatternPreview';
import { CurrentPatternAtom, PatternsAtom, SelectedPatternIdAtom } from '../../context/Patterns';
import { Pattern, totalDuration } from '../../context/pattern-model';
import styles from './PatternDetail.module.css'; import styles from './PatternDetail.module.css';
const EmptyPromption: FC = () => { const EmptyPromption: FC = () => {
@@ -15,7 +22,85 @@ const EmptyPromption: FC = () => {
}; };
const Detail: FC<{ pattern: Pattern }> = ({ pattern }) => { const Detail: FC<{ pattern: Pattern }> = ({ pattern }) => {
return <div className={styles.pattern_detail}></div>; const { showToast } = useNotification();
const navigate = useNavigate();
const refreshPatterns = useSetAtom(PatternsAtom);
const resetSelected = useSetAtom(SelectedPatternIdAtom);
const patternDuration = useMemo(() => totalDuration(pattern), [pattern]);
const createTime = useMemo(
() => dayjs(pattern.createdAt).format('YYYY-MM-DD HH:mm:ss'),
[pattern],
);
const handleDeleteAction = useCallback(async () => {
try {
const answer = await ask(
`The pattern ${pattern.name} will be deleted, and cannot be revoked. Are you sure?`,
{
title: 'Confirm action',
kind: 'warning',
},
);
if (answer) {
await invoke('remove_pattern', { patternId: pattern.id });
showToast(NotificationType.SUCCESS, 'Pattern deleted.');
refreshPatterns();
resetSelected(null);
}
} catch (e) {
console.error('[delete pattern]', e);
showToast(NotificationType.ERROR, 'Failed to delete pattern. Please try again.');
}
}, [pattern]);
return (
<div className={styles.pattern_detail}>
<div className={styles.control_panel}>
<div className={styles.button_row}>
<button className="tonal" onClick={() => navigate('/pattern-editor/edit')}>
Edit Pattern
</button>
<button className="tonal danger" onClick={handleDeleteAction}>
Delete Pattern
</button>
</div>
<div className={styles.button_row}>
<button className="tonal warn">Test Run</button>
</div>
<div className={styles.button_row}>
<button className="tonal secondary">Add to Channel A Playlist</button>
<button className="tonal secondary">Add to Channel B Playlist</button>
</div>
</div>
<hr className="dotted" />
<div className={styles.detail_panel}>
<div className={styles.detail_row}>
<div className={styles.detail_unit}>
<label>Created At</label>
<div className={styles.content}>{createTime}</div>
</div>
</div>
<div className={styles.detail_row}>
<div className={styles.detail_unit}>
<label>Duration</label>
<div className={styles.content}>{(patternDuration / 1000).toFixed(2)} s</div>
</div>
</div>
<div className={styles.detail_row}>
<div className={styles.detail_unit}>
<label>&nbsp;</label>
<div className={styles.content}>
{pattern.pulses.length} key frame{pattern.pulses.length > 1 && 's'}
</div>
</div>
</div>
</div>
<hr className="dotted" />
<div className={styles.preview_panel}>
<PatternPreview />
</div>
</div>
);
}; };
const PatternDetail: FC = () => { const PatternDetail: FC = () => {

View File

@@ -5,13 +5,8 @@ import { FC, useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useDebounce } from 'react-use'; import { useDebounce } from 'react-use';
import { ScrollArea } from '../../components/ScrollArea'; import { ScrollArea } from '../../components/ScrollArea';
import { import { PatternsAtom, SearchKeywordAtom, SelectedPatternIdAtom } from '../../context/Patterns';
Pattern, import { Pattern, totalDuration } from '../../context/pattern-model';
PatternsAtom,
SearchKeywordAtom,
SelectedPatternIdAtom,
totalDuration,
} from '../../context/Patterns';
import styles from './Patterns.module.css'; import styles from './Patterns.module.css';
const PatternCard: FC<{ pattern: Pattern }> = ({ pattern }) => { const PatternCard: FC<{ pattern: Pattern }> = ({ pattern }) => {

View File

@@ -2,6 +2,7 @@ import { useAtomValue } from 'jotai';
import { FC, useMemo } from 'react'; import { FC, useMemo } from 'react';
import { BleState } from '../../context/EstimContext'; import { BleState } from '../../context/EstimContext';
import IconBluetooth from '../../icons/IconBluetooth'; import IconBluetooth from '../../icons/IconBluetooth';
import { smallIconProps } from '../../icons/shared-props';
const BleStates: FC = () => { const BleStates: FC = () => {
const ble = useAtomValue(BleState); const ble = useAtomValue(BleState);
@@ -19,7 +20,7 @@ const BleStates: FC = () => {
return ( return (
<IconBluetooth <IconBluetooth
height={16} {...smallIconProps}
ready={ble.ready} ready={ble.ready}
searching={ble.searching} searching={ble.searching}
connected={ble.connected?.length > 0} connected={ble.connected?.length > 0}

View File

@@ -5,6 +5,8 @@
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
gap: calc(var(--spacing)); gap: calc(var(--spacing));
font-size: calc(var(--font-size) * 1.4); font-size: var(--label-medium-font-size);
line-height: var(--label-medium-line-height);
font-weight: var(--label-medium-font-weight);
} }
} }

View File

@@ -3,14 +3,15 @@ import { FC } from 'react';
import { DeviceState } from '../../context/EstimContext'; import { DeviceState } from '../../context/EstimContext';
import IconBattery from '../../icons/IconBattery'; import IconBattery from '../../icons/IconBattery';
import IconRssi from '../../icons/IconRssi'; import IconRssi from '../../icons/IconRssi';
import { smallIconProps } from '../../icons/shared-props';
const DeviceStates: FC = () => { const DeviceStates: FC = () => {
const deviceState = useAtomValue(DeviceState); const deviceState = useAtomValue(DeviceState);
return ( return (
<> <>
<IconRssi height={16} level={deviceState?.rssi} /> <IconRssi {...smallIconProps} level={deviceState?.rssi} />
<IconBattery height={16} level={deviceState?.battery} /> <IconBattery {...smallIconProps} level={deviceState?.battery} />
</> </>
); );
}; };

View File

@@ -8,6 +8,8 @@
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
gap: calc(var(--spacing) * 4); gap: calc(var(--spacing) * 4);
font-size: calc(var(--font-size) * 1.6); font-size: var(--label-small-font-size);
line-height: var(--label-small-line-height);
font-weight: var(--label-small-font-weight);
} }
} }

View File

@@ -3,7 +3,7 @@ import { useSetAtom } from 'jotai';
import { FC, useActionState } from 'react'; import { FC, useActionState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { NotificationType, ToastDuration, useNotification } from '../components/Notifications'; import { NotificationType, ToastDuration, useNotification } from '../components/Notifications';
import { Pattern, SelectedPatternIdAtom, useSavePattern } from '../context/Patterns'; import { SelectedPatternIdAtom, useSavePattern } from '../context/Patterns';
import styles from './CreatePattern.module.css'; import styles from './CreatePattern.module.css';
const CreatePattern: FC = () => { const CreatePattern: FC = () => {

View File

@@ -7,4 +7,11 @@
align-items: stretch; align-items: stretch;
gap: calc(var(--spacing) * 4); gap: calc(var(--spacing) * 4);
} }
.pattern_preview {
flex-basis: 140px;
color: var(--color-on-surface);
background-color: var(--color-surface-container);
padding: calc(var(--spacing)) calc(var(--spacing) * 2);
border-radius: calc(var(--border-radius) * 2);
}
} }

View File

@@ -13,8 +13,10 @@ const PatternEditor: FC = () => {
<PatternOverview /> <PatternOverview />
<PulseAttributes /> <PulseAttributes />
</div> </div>
<div className={styles.pattern_preview}>
<PatternPreview /> <PatternPreview />
</div> </div>
</div>
); );
}; };

View File

@@ -3,9 +3,9 @@
@layer base { @layer base {
:root { :root {
font-family: var(--font-family); font-family: var(--font-family);
font-size: var(--font-size); font-size: var(--root-font-size);
line-height: var(--line-height); line-height: var(--root-line-height);
font-weight: 400; font-weight: var(--root-font-weight);
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;

View File

@@ -14,7 +14,9 @@
.empty_prompt { .empty_prompt {
padding-block: calc(var(--spacing) * 1); padding-block: calc(var(--spacing) * 1);
text-align: center; text-align: center;
font-size: calc(var(--font-size) * 0.8); font-size: var(--body-extra-small-font-size);
line-height: var(--body-extra-small-line-height);
font-weight: var(--body-extra-small-font-weight);
color: var(--color-on-surface-variant); color: var(--color-on-surface-variant);
} }
} }

View File

@@ -1,7 +1,5 @@
@layer theme { @layer theme {
:root { :root {
--font-family: 'Inter', system-ui, Avenir, Helvetica, Arial, sans-serif;
--color-white: #ffffff; --color-white: #ffffff;
--color-black: #000000; --color-black: #000000;
--color-primary: light-dark(#744c1f, #ffe6b1); --color-primary: light-dark(#744c1f, #ffe6b1);
@@ -333,6 +331,79 @@
--font-size: 10px; --font-size: 10px;
--line-height: 1.2em; --line-height: 1.2em;
--font-family: 'Inter', system-ui, Avenir, Helvetica, Arial, sans-serif;
--root-font-size: 16px;
--root-line-height: 1.2;
--root-font-weight: 400;
--display-large-font-size: 3.5625rem;
--display-large-line-height: 1.1228;
--display-large-font-weight: 400;
--display-medium-font-size: 2.8125rem;
--display-medium-line-height: 1.1556;
--display-medium-font-weight: 400;
--display-small-font-size: 2.25rem;
--display-small-line-height: 1.2222;
--display-small-font-weight: 400;
--headline-large-font-size: 2rem;
--headline-large-line-height: 1.25;
--headline-large-font-weight: 400;
--headline-medium-font-size: 1.75rem;
--headline-medium-line-height: 1.2857;
--headline-medium-font-weight: 400;
--headline-small-font-size: 1.5rem;
--headline-small-line-height: 1.3333;
--headline-small-font-weight: 400;
--title-large-font-size: 1.375rem;
--title-large-line-height: 1.2727;
--title-large-font-weight: 400;
--title-medium-font-size: 1rem;
--title-medium-line-height: 1.5;
--title-medium-font-weight: 400;
--title-small-font-size: 0.875rem;
--title-small-line-height: 1.4286;
--title-small-font-weight: 500;
--body-large-font-size: 1rem;
--body-large-line-height: 1.5;
--body-large-font-weight: 400;
--body-medium-font-size: 0.875rem;
--body-medium-line-height: 1.4286;
--body-medium-font-weight: 400;
--body-small-font-size: 0.75rem;
--body-small-line-height: 1.3333;
--body-small-font-weight: 400;
--body-extra-small-font-size: 0.625rem;
--body-extra-small-line-height: 1.2;
--body-extra-small-font-weight: 400;
--body-extrime-small-font-size: 0.5rem;
--body-extrime-small-line-height: 1;
--body-extrime-small-font-weight: 400;
--label-large-font-size: 0.875rem;
--label-large-line-height: 1.4286;
--label-large-font-weight: 500;
--label-medium-font-size: 0.75rem;
--label-medium-line-height: 1.3333;
--label-medium-font-weight: 500;
--label-small-font-size: 0.6875rem;
--label-small-line-height: 1.4545;
--label-small-font-weight: 400;
--elevation-0: none; --elevation-0: none;
--elevation-1-ambient: 0 1px 3px 1px color-mix(in oklch, var(--color-shadow) 15%, transparent); --elevation-1-ambient: 0 1px 3px 1px color-mix(in oklch, var(--color-shadow) 15%, transparent);
--elevation-1-umbra: 0 1px 2px 0px color-mix(in oklch, var(--color-shadow) 30%, transparent); --elevation-1-umbra: 0 1px 2px 0px color-mix(in oklch, var(--color-shadow) 30%, transparent);