Compare commits
10 Commits
32d6e0c875
...
d2a854490f
Author | SHA1 | Date | |
---|---|---|---|
|
d2a854490f | ||
|
d465951f31 | ||
|
55b10f5a5c | ||
|
edf4163e38 | ||
|
09cba205a4 | ||
|
eea7446346 | ||
|
429d6451c4 | ||
|
128a45ad77 | ||
|
66a85f29f9 | ||
|
d1d8def602 |
|
@ -143,3 +143,17 @@ pub async fn list_patterns(
|
||||||
.map_err(|e| errors::AppError::StorageFailure(e.to_string()))?;
|
.map_err(|e| errors::AppError::StorageFailure(e.to_string()))?;
|
||||||
Ok(patterns)
|
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(())
|
||||||
|
}
|
||||||
|
|
|
@ -65,7 +65,8 @@ pub fn run() {
|
||||||
cmd::activate_central_adapter,
|
cmd::activate_central_adapter,
|
||||||
cmd::start_scan_devices,
|
cmd::start_scan_devices,
|
||||||
cmd::stop_scan_devices,
|
cmd::stop_scan_devices,
|
||||||
cmd::list_patterns
|
cmd::list_patterns,
|
||||||
|
cmd::save_pattern
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|
|
@ -227,10 +227,12 @@
|
||||||
--button-text: var(--color-info);
|
--button-text: var(--color-info);
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
|
color: var(--button-text);
|
||||||
background-color: color-mix(in oklch, var(--button-text) 8%, transparent);
|
background-color: color-mix(in oklch, var(--button-text) 8%, transparent);
|
||||||
box-shadow: var(--elevation-0);
|
box-shadow: var(--elevation-0);
|
||||||
}
|
}
|
||||||
&:active {
|
&:active {
|
||||||
|
color: var(--button-text);
|
||||||
background-color: color-mix(in oklch, var(--button-text) 18%, transparent);
|
background-color: color-mix(in oklch, var(--button-text) 18%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -260,11 +262,13 @@
|
||||||
--button-text: var(--color-info);
|
--button-text: var(--color-info);
|
||||||
}
|
}
|
||||||
&:hover {
|
&: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);
|
box-shadow: var(--elevation-0);
|
||||||
}
|
}
|
||||||
&:active {
|
&: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 {
|
&.text:disabled {
|
||||||
|
@ -274,6 +278,7 @@
|
||||||
&.icon:not(:disabled) {
|
&.icon:not(:disabled) {
|
||||||
--button-text: var(--color-on-surface-variant);
|
--button-text: var(--color-on-surface-variant);
|
||||||
--button-surface: transparent;
|
--button-surface: transparent;
|
||||||
|
padding: calc(var(--spacing) * 1);
|
||||||
&.selected {
|
&.selected {
|
||||||
--button-text: var(--color-primary);
|
--button-text: var(--color-primary);
|
||||||
&.secondary {
|
&.secondary {
|
||||||
|
@ -296,20 +301,24 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:hover {
|
&: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);
|
box-shadow: var(--elevation-0);
|
||||||
}
|
}
|
||||||
&:active {
|
&: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 {
|
&.icon:disabled {
|
||||||
--button-text: color-mix(in oklch, var(--color-on-surface) 38%, transparent);
|
--button-text: color-mix(in oklch, var(--color-on-surface) 38%, transparent);
|
||||||
--button-surface: transparent;
|
--button-surface: transparent;
|
||||||
|
padding: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
&.filled_icon:not(:disabled) {
|
&.filled_icon:not(:disabled) {
|
||||||
--button-text: var(--color-on-primary);
|
--button-text: var(--color-on-primary);
|
||||||
--button-surface: var(--color-primary);
|
--button-surface: var(--color-primary);
|
||||||
|
padding: calc(var(--spacing) * 1);
|
||||||
&.secondary {
|
&.secondary {
|
||||||
--button-text: var(--color-on-secondary);
|
--button-text: var(--color-on-secondary);
|
||||||
--button-surface: var(--color-secondary);
|
--button-surface: var(--color-secondary);
|
||||||
|
@ -342,10 +351,12 @@
|
||||||
&.filled_icon:disabled {
|
&.filled_icon:disabled {
|
||||||
--button-text: color-mix(in oklch, var(--color-on-surface) 38%, transparent);
|
--button-text: color-mix(in oklch, var(--color-on-surface) 38%, transparent);
|
||||||
--button-surface: color-mix(in oklch, var(--color-on-surface) 12%, transparent);
|
--button-surface: color-mix(in oklch, var(--color-on-surface) 12%, transparent);
|
||||||
|
padding: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
&.tonal_icon:not(:disabled) {
|
&.tonal_icon:not(:disabled) {
|
||||||
--button-text: var(--color-on-primary-container);
|
--button-text: var(--color-on-primary-container);
|
||||||
--button-surface: var(--color-primary-container);
|
--button-surface: var(--color-primary-container);
|
||||||
|
padding: calc(var(--spacing) * 1);
|
||||||
&.secondary {
|
&.secondary {
|
||||||
--button-text: var(--color-on-secondary-container);
|
--button-text: var(--color-on-secondary-container);
|
||||||
--button-surface: var(--color-secondary-container);
|
--button-surface: var(--color-secondary-container);
|
||||||
|
@ -360,52 +371,59 @@
|
||||||
}
|
}
|
||||||
&.warn {
|
&.warn {
|
||||||
--button-text: var(--color-on-warning-container);
|
--button-text: var(--color-on-warning-container);
|
||||||
--button-surface: var(--color-warning);
|
--button-surface: var(--color-warning-container);
|
||||||
}
|
}
|
||||||
&.success {
|
&.success {
|
||||||
--button-text: var(--color-on-success-container);
|
--button-text: var(--color-on-success-container);
|
||||||
--button-surface: var(--color-success-container);
|
--button-surface: var(--color-success-container);
|
||||||
}
|
}
|
||||||
&.info {
|
&.info {
|
||||||
--button-text: var(--color-on-info);
|
--button-text: var(--color-on-info-container);
|
||||||
--button-surface: var(--color-info);
|
--button-surface: var(--color-info-container);
|
||||||
}
|
}
|
||||||
&.unselected {
|
&.unselected {
|
||||||
--button-text: var(--color-on-surface-variant);
|
--button-text: var(--color-on-surface-variant);
|
||||||
--button-surface: var(--color-surface-container-highest);
|
--button-surface: var(--color-surface-container-highest);
|
||||||
}
|
}
|
||||||
&:hover {
|
&: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);
|
box-shadow: var(--elevation-0);
|
||||||
}
|
}
|
||||||
&:active {
|
&: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 {
|
&.tonal_icon:disabled {
|
||||||
--button-text: color-mix(in oklch, var(--color-on-surface) 38%, transparent);
|
--button-text: color-mix(in oklch, var(--color-on-surface) 38%, transparent);
|
||||||
--button-surface: color-mix(in oklch, var(--color-on-surface) 12%, transparent);
|
--button-surface: color-mix(in oklch, var(--color-on-surface) 12%, transparent);
|
||||||
|
padding: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
&.outlined_icon:not(:disabled) {
|
&.outlined_icon:not(:disabled) {
|
||||||
--button-text: var(--color-on-surface-variant);
|
--button-text: var(--color-on-surface-variant);
|
||||||
--button-surface: transparent;
|
--button-surface: transparent;
|
||||||
border: 1px solid var(--button-outline);
|
border: 1px solid var(--button-outline);
|
||||||
|
padding: calc(var(--spacing) * 1);
|
||||||
&.selected {
|
&.selected {
|
||||||
--button-text: var(--color-inverse-on-surface);
|
--button-text: var(--color-inverse-on-surface);
|
||||||
--button-surface: var(--color-inverse-surface);
|
--button-surface: var(--color-inverse-surface);
|
||||||
}
|
}
|
||||||
&:hover {
|
&: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);
|
box-shadow: var(--elevation-0);
|
||||||
}
|
}
|
||||||
&:active {
|
&: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 {
|
&.outlined_icon:disabled {
|
||||||
--button-text: color-mix(in oklch, var(--color-on-surface) 38%, transparent);
|
--button-text: color-mix(in oklch, var(--color-on-surface) 38%, transparent);
|
||||||
--button-surface: color-mix(in oklch, var(--color-on-surface) 12%, 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);
|
border: 1px solid color-mix(in oklch, var(--color-on-surface) 12%, transparent);
|
||||||
|
padding: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
47
src/components/Switch.module.css
Normal file
47
src/components/Switch.module.css
Normal 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
40
src/components/Switch.tsx
Normal 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;
|
|
@ -9,5 +9,8 @@
|
||||||
border-radius: calc(var(--border-radius) * 2);
|
border-radius: calc(var(--border-radius) * 2);
|
||||||
color: var(--color-light-on-surface);
|
color: var(--color-light-on-surface);
|
||||||
background-color: var(--color-surface-container);
|
background-color: var(--color-surface-container);
|
||||||
|
.content {
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
19
src/page-components/pattern-editor/PatternHeader.tsx
Normal file
19
src/page-components/pattern-editor/PatternHeader.tsx
Normal 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;
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
18
src/page-components/pattern-editor/PatternOverview.tsx
Normal file
18
src/page-components/pattern-editor/PatternOverview.tsx
Normal 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;
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
8
src/page-components/pattern-editor/PulseAttributes.tsx
Normal file
8
src/page-components/pattern-editor/PulseAttributes.tsx
Normal 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;
|
20
src/page-components/pattern-editor/PulseList.module.css
Normal file
20
src/page-components/pattern-editor/PulseList.module.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
22
src/page-components/pattern-editor/PulseList.tsx
Normal file
22
src/page-components/pattern-editor/PulseList.tsx
Normal 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;
|
Loading…
Reference in New Issue
Block a user