Compare commits

...

10 Commits

Author SHA1 Message Date
Vixalie
d2a854490f add save pattern command. 2025-03-10 15:52:07 +08:00
Vixalie
d465951f31 modify pattern name input placeholder. 2025-03-10 11:03:13 +08:00
Vixalie
55b10f5a5c continue correct button styles. 2025-03-10 08:43:10 +08:00
Vixalie
edf4163e38 prepare Pattern overview part layout. 2025-03-09 22:53:58 +08:00
Vixalie
09cba205a4 adjust Switch component styles. 2025-03-09 22:53:19 +08:00
Vixalie
eea7446346 fix button styles. 2025-03-09 22:53:01 +08:00
Vixalie
429d6451c4 fix button styles. 2025-03-09 22:51:45 +08:00
Vixalie
128a45ad77 prepare empty Pattern attribute part. 2025-03-09 22:15:24 +08:00
Vixalie
66a85f29f9 prepare Pattern Editor page header part. 2025-03-09 22:15:01 +08:00
Vixalie
d1d8def602 prepare switch component. 2025-03-09 22:14:20 +08:00
13 changed files with 254 additions and 12 deletions

View File

@ -143,3 +143,17 @@ pub async fn list_patterns(
.map_err(|e| errors::AppError::StorageFailure(e.to_string()))?;
Ok(patterns)
}
#[tauri::command]
pub async fn save_pattern(
app_state: State<'_, Arc<RwLock<AppState>>>,
pattern: Pattern,
) -> Result<(), errors::AppError> {
let state = app_state.read().await;
state
.db
.store_pattern(&pattern)
.await
.map_err(|e| errors::AppError::StorageFailure(e.to_string()))?;
Ok(())
}

View File

@ -65,7 +65,8 @@ pub fn run() {
cmd::activate_central_adapter,
cmd::start_scan_devices,
cmd::stop_scan_devices,
cmd::list_patterns
cmd::list_patterns,
cmd::save_pattern
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@ -227,10 +227,12 @@
--button-text: var(--color-info);
}
&:hover {
color: var(--button-text);
background-color: color-mix(in oklch, var(--button-text) 8%, transparent);
box-shadow: var(--elevation-0);
}
&:active {
color: var(--button-text);
background-color: color-mix(in oklch, var(--button-text) 18%, transparent);
}
}
@ -260,11 +262,13 @@
--button-text: var(--color-info);
}
&:hover {
--button-surface: color-mix(in oklch, var(--button-text) 8%, transparent);
color: var(--button-text);
background-color: color-mix(in oklch, var(--button-text) 8%, transparent);
box-shadow: var(--elevation-0);
}
&:active {
--button-surface: color-mix(in oklch, var(--button-text) 18%, transparent);
color: var(--button-text);
background-color: color-mix(in oklch, var(--button-text) 18%, transparent);
}
}
&.text:disabled {
@ -274,6 +278,7 @@
&.icon:not(:disabled) {
--button-text: var(--color-on-surface-variant);
--button-surface: transparent;
padding: calc(var(--spacing) * 1);
&.selected {
--button-text: var(--color-primary);
&.secondary {
@ -296,20 +301,24 @@
}
}
&:hover {
--button-surface: color-mix(in oklch, var(--button-text) 8%, transparent);
color: var(--button-text);
background-color: color-mix(in oklch, var(--button-text) 8%, transparent);
box-shadow: var(--elevation-0);
}
&:active {
--button-surface: color-mix(in oklch, var(--button-text) 18%, transparent);
color: var(--button-text);
background-color: color-mix(in oklch, var(--button-text) 18%, transparent);
}
}
&.icon:disabled {
--button-text: color-mix(in oklch, var(--color-on-surface) 38%, transparent);
--button-surface: transparent;
padding: calc(var(--spacing) * 1);
}
&.filled_icon:not(:disabled) {
--button-text: var(--color-on-primary);
--button-surface: var(--color-primary);
padding: calc(var(--spacing) * 1);
&.secondary {
--button-text: var(--color-on-secondary);
--button-surface: var(--color-secondary);
@ -342,10 +351,12 @@
&.filled_icon:disabled {
--button-text: color-mix(in oklch, var(--color-on-surface) 38%, transparent);
--button-surface: color-mix(in oklch, var(--color-on-surface) 12%, transparent);
padding: calc(var(--spacing) * 1);
}
&.tonal_icon:not(:disabled) {
--button-text: var(--color-on-primary-container);
--button-surface: var(--color-primary-container);
padding: calc(var(--spacing) * 1);
&.secondary {
--button-text: var(--color-on-secondary-container);
--button-surface: var(--color-secondary-container);
@ -360,52 +371,59 @@
}
&.warn {
--button-text: var(--color-on-warning-container);
--button-surface: var(--color-warning);
--button-surface: var(--color-warning-container);
}
&.success {
--button-text: var(--color-on-success-container);
--button-surface: var(--color-success-container);
}
&.info {
--button-text: var(--color-on-info);
--button-surface: var(--color-info);
--button-text: var(--color-on-info-container);
--button-surface: var(--color-info-container);
}
&.unselected {
--button-text: var(--color-on-surface-variant);
--button-surface: var(--color-surface-container-highest);
}
&:hover {
--button-surface: color-mix(in oklch, var(--button-text) 8%, transparent);
color: var(--button-surface);
background-color: color-mix(in oklch, var(--button-surface) 8%, transparent);
box-shadow: var(--elevation-0);
}
&:active {
--button-surface: color-mix(in oklch, var(--button-text) 18%, transparent);
color: var(--button-surface);
background-color: color-mix(in oklch, var(--button-surface) 18%, transparent);
}
}
&.tonal_icon:disabled {
--button-text: color-mix(in oklch, var(--color-on-surface) 38%, transparent);
--button-surface: color-mix(in oklch, var(--color-on-surface) 12%, transparent);
padding: calc(var(--spacing) * 1);
}
&.outlined_icon:not(:disabled) {
--button-text: var(--color-on-surface-variant);
--button-surface: transparent;
border: 1px solid var(--button-outline);
padding: calc(var(--spacing) * 1);
&.selected {
--button-text: var(--color-inverse-on-surface);
--button-surface: var(--color-inverse-surface);
}
&:hover {
--button-surface: color-mix(in oklch, var(--button-text) 8%, transparent);
color: var(--button-text);
background-color: color-mix(in oklch, var(--button-text) 8%, transparent);
box-shadow: var(--elevation-0);
}
&:active {
--button-surface: color-mix(in oklch, var(--button-text) 18%, transparent);
color: var(--button-text);
background-color: color-mix(in oklch, var(--button-text) 18%, transparent);
}
}
&.outlined_icon:disabled {
--button-text: color-mix(in oklch, var(--color-on-surface) 38%, transparent);
--button-surface: color-mix(in oklch, var(--color-on-surface) 12%, transparent);
border: 1px solid color-mix(in oklch, var(--color-on-surface) 12%, transparent);
padding: calc(var(--spacing) * 1);
}
}

View File

@ -0,0 +1,47 @@
@layer components {
.switch {
display: inline-flex;
align-items: center;
gap: 1em;
cursor: pointer;
&[aria-disabled='true'] {
cursor: not-allowed;
}
}
.switch_handle {
position: relative;
width: calc(var(--spacing) * 9);
height: calc(var(--spacing) * 5);
border: 1px solid var(--color-primary);
border-radius: calc(var(--border-radius) * 2);
background-color: color-mix(in oklch, var(--color-surface-container-high) 38%, transparent);
&::before {
content: '';
position: absolute;
transform: translate(2px, 3px);
width: calc(var(--spacing) * 3);
height: calc(var(--spacing) * 3);
border-radius: calc(var(--border-radius) * 2);
background-color: var(--color-primary);
transition: transform 0.2s ease-in-out;
}
&.checked {
background-color: color-mix(in oklch, var(--color-primary) 38%, transparent);
&::before {
background-color: var(--color-primary);
transform: translate(calc(var(--spacing) * 5 - 2px), 3px);
}
}
[aria-disabled='true'] & {
--disabled-color: var(--color-on-surface);
background-color: color-mix(in oklch, var(--disabled-color) 8%, transparent);
border-color: var(--disabled-color);
&.checked {
background-color: color-mix(in oklch, var(--disabled-color) 20%, transparent);
}
&::before {
background-color: color-mix(in oklch, var(--disabled-color) 38%, transparent);
}
}
}
}

40
src/components/Switch.tsx Normal file
View File

@ -0,0 +1,40 @@
import cx from 'clsx';
import { FC, useCallback, useEffect, useState } from 'react';
import styles from './Switch.module.css';
type SwitchProps = {
name?: string;
checked?: boolean;
disabled?: boolean;
onChange?: (checked: boolean) => void;
};
const Switch: FC<SwitchProps> = ({ name, checked = false, disabled = false, onChange }) => {
const [isChecked, setChecked] = useState(checked);
const handleChange = useCallback(() => {
if (!disabled) {
setChecked((prev) => !prev);
onChange?.(!isChecked);
}
}, [disabled, onChange, isChecked]);
useEffect(() => {
if (checked !== isChecked) {
setChecked(checked);
}
}, [checked]);
return (
<div aria-disabled={disabled} className={styles.switch}>
<div
className={cx(styles.switch_handle, isChecked && styles.checked)}
onClick={handleChange}
/>
{name !== undefined && (
<input type="hidden" name={name} value={isChecked ? 'true' : 'false'} />
)}
</div>
);
};
export default Switch;

View File

@ -9,5 +9,8 @@
border-radius: calc(var(--border-radius) * 2);
color: var(--color-light-on-surface);
background-color: var(--color-surface-container);
.content {
max-width: 300px;
}
}
}

View File

@ -0,0 +1,19 @@
import { Icon } from '@iconify/react/dist/iconify.js';
import { FC } from 'react';
import EditableContent from '../../components/EditableContent';
import styles from './PatternHeader.module.css';
const PatternHeader: FC = () => {
return (
<div className={styles.pattern_header}>
<EditableContent as="h3" placeholder="Pattern Name" additionalClassName={styles.content} />
<div className="spacer" />
<button className="tonal">
<Icon icon="material-symbols-light:close" />
Close Edit
</button>
</div>
);
};
export default PatternHeader;

View File

@ -0,0 +1,19 @@
@layer pages {
.pattern_overview {
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
gap: calc(var(--spacing));
padding: calc(var(--spacing) * 2) calc(var(--spacing));
border-radius: calc(var(--border-radius) * 2);
color: var(--color-on-surface);
background-color: var(--color-surface-container);
}
.attribute_row {
display: flex;
flex-direction: row;
align-items: center;
gap: calc(var(--spacing) * 2);
}
}

View File

@ -0,0 +1,18 @@
import { FC } from 'react';
import Switch from '../../components/Switch';
import styles from './PatternOverview.module.css';
import PulseList from './PulseList';
const PatternOverview: FC = () => {
return (
<div className={styles.pattern_overview}>
<div className={styles.attribute_row}>
<label>Smooth Repeat</label>
<Switch />
</div>
<PulseList />
</div>
);
};
export default PatternOverview;

View File

@ -0,0 +1,13 @@
@layer pages {
.pulse_attributes {
flex: 2;
display: flex;
flex-direction: column;
align-items: stretch;
gap: calc(var(--spacing) * 2);
padding: calc(var(--spacing) * 2) calc(var(--spacing));
border-radius: calc(var(--border-radius) * 2);
color: var(--color-on-surface);
background-color: var(--color-surface-container);
}
}

View File

@ -0,0 +1,8 @@
import { FC } from 'react';
import styles from './PulseAttributes.module.css';
const PulseAttributes: FC = () => {
return <div className={styles.pulse_attributes}></div>;
};
export default PulseAttributes;

View File

@ -0,0 +1,20 @@
@layer page {
.pulse_tools {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: calc(var(--spacing) * 2);
}
.pulses {
min-width: 0;
min-height: 0;
overflow: hidden;
flex: 1;
padding-inline: calc(var(--spacing) * 2);
padding-block-end: calc(var(--spacing));
display: flex;
flex-direction: column;
align-items: stretch;
}
}

View File

@ -0,0 +1,22 @@
import { Icon } from '@iconify/react/dist/iconify.js';
import { FC } from 'react';
import { ScrollArea } from '../../components/ScrollArea';
import styles from './PulseList.module.css';
const PulseList: FC = () => {
return (
<>
<div className={styles.pulse_tools}>
<button className="text">
<Icon icon="material-symbols-light:add" />
<span>Add Pulse</span>
</button>
</div>
<div className={styles.pulses}>
<ScrollArea enableY></ScrollArea>
</div>
</>
);
};
export default PulseList;