project initiate.
This commit is contained in:
		
							
								
								
									
										81
									
								
								src/Layout.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/Layout.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| @layer pages { | ||||
|   .layout { | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     display: flex; | ||||
|     padding-block-start: calc(var(--spacing)); | ||||
|     padding-block-end: calc(var(--spacing) * 4); | ||||
|     padding-inline: calc(var(--spacing) * 4); | ||||
|     margin: 0; | ||||
|     flex-direction: column; | ||||
|     align-items: stretch; | ||||
|     gap: calc(var(--spacing) * 3); | ||||
|     header { | ||||
|       display: flex; | ||||
|       flex-direction: row; | ||||
|       align-items: flex-start; | ||||
|       height: calc(var(--spacing) * 6); | ||||
|       z-index: 10; | ||||
|       &.mac_titlebar { | ||||
|         padding-left: calc(var(--spacing) * 14); | ||||
|       } | ||||
|       h1 { | ||||
|         line-height: 1em; | ||||
|         font-style: italic; | ||||
|       } | ||||
|     } | ||||
|     section { | ||||
|       flex: 1; | ||||
|       display: flex; | ||||
|       flex-direction: row; | ||||
|       align-items: stretch; | ||||
|       gap: calc(var(--spacing) * 3); | ||||
|       menu { | ||||
|         flex: 1; | ||||
|         padding: calc(var(--spacing) * 4) 0; | ||||
|         max-width: calc(var(--spacing) * 12); | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         align-items: stretch; | ||||
|         gap: calc(var(--spacing) * 4); | ||||
|       } | ||||
|       .main_content { | ||||
|         flex: 1; | ||||
|         border-radius: calc(var(--border-radius) * 2); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   .window_move_handler { | ||||
|     position: absolute; | ||||
|     top: 0; | ||||
|     left: 0; | ||||
|     right: 0; | ||||
|     height: calc(var(--spacing) * 8); | ||||
|     z-index: 300; | ||||
|   } | ||||
|   .route_link { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     align-items: stretch; | ||||
|     gap: calc(var(--spacing)); | ||||
|     div { | ||||
|       text-align: center; | ||||
|       &.filled { | ||||
|         color: var(--color-dark-on-surface); | ||||
|         background-color: transparent; | ||||
|         border-radius: calc(var(--border-radius) * 2); | ||||
|         padding-block: calc(var(--spacing)); | ||||
|       } | ||||
|     } | ||||
|     &.inactive { | ||||
|       color: var(--color-dark-on-surface-variant); | ||||
|     } | ||||
|     &.active { | ||||
|       color: var(--color-dark-on-surface); | ||||
|       div.filled { | ||||
|         color: var(--color-dark-on-secondary-container); | ||||
|         background-color: var(--color-dark-secondary-container); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										70
									
								
								src/Layout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/Layout.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| import { Icon } from '@iconify/react/dist/iconify.js'; | ||||
| import { platform } from '@tauri-apps/plugin-os'; | ||||
| import cx from 'clsx'; | ||||
| import { FC, ReactNode } from 'react'; | ||||
| import { createPortal } from 'react-dom'; | ||||
| import { NavLink, Outlet } from 'react-router-dom'; | ||||
| import styles from './Layout.module.css'; | ||||
| import { defaultIconProps } from './icons/shared-props'; | ||||
| import StateBar from './page-components/state-bar/StateBar'; | ||||
|  | ||||
| type FunctionLinkProps = { | ||||
|   name: string; | ||||
|   url: string; | ||||
|   end?: boolean; | ||||
|   children?: ReactNode; | ||||
| }; | ||||
|  | ||||
| const FunctionLink: FC<FunctionLinkProps> = ({ name, url, end, children }) => { | ||||
|   return ( | ||||
|     <NavLink | ||||
|       to={url} | ||||
|       className={({ isActive }) => | ||||
|         cx(styles.route_link, isActive ? styles.active : styles.inactive) | ||||
|       } | ||||
|       end={end}> | ||||
|       <div className={styles.filled}>{children}</div> | ||||
|       <div>{name}</div> | ||||
|     </NavLink> | ||||
|   ); | ||||
| }; | ||||
| const Layout: FC = () => { | ||||
|   const os = platform(); | ||||
|  | ||||
|   return ( | ||||
|     <main className={styles.layout}> | ||||
|       <header className={cx({ [styles.mac_titlebar]: os === 'macos' })}> | ||||
|         <h1>ESTIM Remote</h1> | ||||
|         <StateBar /> | ||||
|       </header> | ||||
|       <section> | ||||
|         <menu> | ||||
|           <FunctionLink name="Device" url="/" end> | ||||
|             <Icon icon="material-symbols-light:device-unknown" {...defaultIconProps} /> | ||||
|           </FunctionLink> | ||||
|           <FunctionLink name="Play" url="/play"> | ||||
|             <Icon icon="material-symbols-light:motion-play" {...defaultIconProps} /> | ||||
|           </FunctionLink> | ||||
|           <FunctionLink name="Pattern Library" url="/library"> | ||||
|             <Icon icon="material-symbols-light:menu-book" {...defaultIconProps} /> | ||||
|           </FunctionLink> | ||||
|           <FunctionLink name="Pattern Editor" url="/pattern-editor"> | ||||
|             <Icon icon="material-symbols-light:movie-edit" {...defaultIconProps} /> | ||||
|           </FunctionLink> | ||||
|           <FunctionLink name="Settings" url="/settings"> | ||||
|             <Icon icon="material-symbols-light:settings" {...defaultIconProps} /> | ||||
|           </FunctionLink> | ||||
|         </menu> | ||||
|         <div className={styles.main_content}> | ||||
|           <Outlet /> | ||||
|         </div> | ||||
|       </section> | ||||
|       {createPortal( | ||||
|         <div data-tauri-drag-region className={styles.window_move_handler} />, | ||||
|         document.body, | ||||
|       )} | ||||
|     </main> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default Layout; | ||||
							
								
								
									
										203
									
								
								src/components.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								src/components.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,203 @@ | ||||
| @layer base { | ||||
|   :root { | ||||
|     --button-text: var(--color-dark-on-primary); | ||||
|     --button-surface: var(--color-dark-primary); | ||||
|     --button-outline: var(--color-dark-outline); | ||||
|   } | ||||
|  | ||||
|   :where(ul, menu) { | ||||
|     margin: 0; | ||||
|     padding: 0; | ||||
|   } | ||||
|  | ||||
|   :where(a) { | ||||
|     text-decoration: none; | ||||
|     color: var(--color-dark-on-surface); | ||||
|  | ||||
|     &:hover { | ||||
|       color: var(--color-dark-primary); | ||||
|     } | ||||
|  | ||||
|     &:active { | ||||
|       color: var(--color-dark-tertiary); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   :is(h1, h2, h3, h4, h5, h6) { | ||||
|     font-weight: bold; | ||||
|     line-height: 1.2em; | ||||
|   } | ||||
|  | ||||
|   h1 { | ||||
|     font-size: 2.6em; | ||||
|   } | ||||
|  | ||||
|   h2 { | ||||
|     font-size: 2em; | ||||
|   } | ||||
|  | ||||
|   h3 { | ||||
|     font-size: 1.8em; | ||||
|   } | ||||
|  | ||||
|   h4 { | ||||
|     font-size: 1.5em; | ||||
|   } | ||||
|  | ||||
|   h5 { | ||||
|     font-size: 1.2em; | ||||
|   } | ||||
|  | ||||
|   h6 { | ||||
|     font-size: 1em; | ||||
|   } | ||||
|  | ||||
|   .center { | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
|   } | ||||
|  | ||||
|   .spacer { | ||||
|     flex: 1 1; | ||||
|   } | ||||
|  | ||||
|   .workspace { | ||||
|     height: 100%; | ||||
|     width: 100%; | ||||
|     overflow: hidden; | ||||
|     display: flex; | ||||
|     align-items: stretch; | ||||
|     gap: calc(var(--spacing) * 6); | ||||
|     padding: calc(var(--spacing) * 2) calc(var(--spacing) * 2); | ||||
|     &.veritcal { | ||||
|       flex-direction: column; | ||||
|     } | ||||
|     &.horizontal { | ||||
|       flex-direction: row; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   :where(button, .button) { | ||||
|     border: none; | ||||
|     border-radius: calc(var(--border-radius) * 2); | ||||
|     padding: calc(var(--spacing) * 1) calc(var(--spacing) * 3); | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
|     gap: calc(var(--spacing) * 2); | ||||
|     font-size: calc(var(--font-size) * 1.2); | ||||
|     line-height: 1.3em; | ||||
|     color: var(--button-text); | ||||
|     background-color: var(--button-surface); | ||||
|     box-shadow: var(--elevation-dark-0); | ||||
|     &.smaller { | ||||
|       font-size: calc(var(--font-size) * 0.8); | ||||
|     } | ||||
|     &.small { | ||||
|       font-size: calc(var(--font-size) * 1); | ||||
|     } | ||||
|     &.large { | ||||
|       font-size: calc(var(--font-size) * 1.4); | ||||
|     } | ||||
|     &.larger { | ||||
|       font-size: calc(var(--font-size) * 1.6); | ||||
|     } | ||||
|     &:hover:not(:disabled) { | ||||
|       --button-text: var(--color-dark-on-primary); | ||||
|       box-shadow: var(--elevation-dark-1-ambient), var(--elevation-dark-1-umbra); | ||||
|     } | ||||
|     &:active:not(:disabled) { | ||||
|       color: color-mix(in oklch, var(--button-text) 12%, transparent); | ||||
|     } | ||||
|     &.tonal:not(:disabled) { | ||||
|       --button-text: var(--color-dark-on-primary-container); | ||||
|       --button-surface: var(--color-dark-primary-container); | ||||
|     } | ||||
|     &.danger:not(:disabled) { | ||||
|       --button-text: var(--color-dark-on-error); | ||||
|       --button-surface: var(--color-dark-error); | ||||
|     } | ||||
|     &.warn:not(:disabled) { | ||||
|       --button-text: var(--color-dark-on-warning); | ||||
|       --button-surface: var(--color-dark-warning); | ||||
|     } | ||||
|     &.success:not(:disabled) { | ||||
|       --button-text: var(--color-dark-on-success); | ||||
|       --button-surface: var(--color-dark-success); | ||||
|     } | ||||
|     &.info:not(:disabled) { | ||||
|       --button-text: var(--color-dark-on-info); | ||||
|       --button-surface: var(--color-dark-info); | ||||
|     } | ||||
|     &:disabled { | ||||
|       --button-text: color-mix(in oklch, var(--color-dark-on-surface) 38%, transparent); | ||||
|       --button-surface: color-mix(in oklch, var(--color-dark-on-surface) 12%, transparent); | ||||
|       --button-outline: color-mix(in oklch, var(--color-dark-on-surface) 12%, transparent); | ||||
|       cursor: not-allowed; | ||||
|     } | ||||
|     &.outline { | ||||
|       --button-text: var(--color-dark-primary); | ||||
|       --button-surface: transparent; | ||||
|       border: 1px solid var(--button-outline); | ||||
|       &:hover:not(:disabled) { | ||||
|         --button-text: var(--color-dark-primary); | ||||
|         --button-surface: color-mix(in oklch, var(--button-text) 8%, transparent); | ||||
|         box-shadow: var(--elevation-dark-0); | ||||
|       } | ||||
|       &:active:not(:disabled) { | ||||
|         --button-text: var(--color-dark-primary); | ||||
|         --button-surface: color-mix(in oklch, var(--button-text) 10%, transparent); | ||||
|         box-shadow: var(--elevation-dark-0); | ||||
|       } | ||||
|       &.danger:not(:disabled) { | ||||
|         --button-text: var(--color-dark-error); | ||||
|       } | ||||
|       &.warn:not(:disabled) { | ||||
|         --button-text: var(--color-dark-warning); | ||||
|       } | ||||
|       &.success:not(:disabled) { | ||||
|         --button-text: var(--color-dark-success); | ||||
|       } | ||||
|       &.info:not(:disabled) { | ||||
|         --button-text: var(--color-dark-info); | ||||
|       } | ||||
|       &:disabled { | ||||
|         --button-text: color-mix(in oklch, var(--color-dark-on-surface) 38%, transparent); | ||||
|       } | ||||
|     } | ||||
|     &.text { | ||||
|       --button-text: --color-dark-primary; | ||||
|       --button-surface: transparent; | ||||
|       border: none; | ||||
|       &:hover:not(:disabled) { | ||||
|         --button-surface: color-mix(in oklch, var(--color-dark-primary) 8%, transparent); | ||||
|       } | ||||
|       &:active:not(:disabled) { | ||||
|         --button-surface: color-mix(in oklch, var(--color-dark-primary) 10%, transparent); | ||||
|       } | ||||
|       &.danger:not(:disabled) { | ||||
|         --button-text: var(--color-dark-error); | ||||
|         --button-surface: transparent; | ||||
|       } | ||||
|       &.warn:not(:disabled) { | ||||
|         --button-text: var(--color-dark-warning); | ||||
|         --button-surface: transparent; | ||||
|       } | ||||
|       &.success:not(:disabled) { | ||||
|         --button-text: var(--color-dark-success); | ||||
|         --button-surface: transparent; | ||||
|       } | ||||
|       &.info:not(:disabled) { | ||||
|         --button-text: var(--color-dark-info); | ||||
|         --button-surface: transparent; | ||||
|       } | ||||
|       &:disabled { | ||||
|         --button-text: color-mix(in oklch, var(--color-dark-on-surface) 38%, transparent); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										43
									
								
								src/components/ScrollArea.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/components/ScrollArea.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| @layer components { | ||||
|   .scroll_area { | ||||
|     display: grid; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     overflow: hidden; | ||||
|     grid-template-columns: auto calc(var(--spacing) * 3); | ||||
|     grid-template-rows: auto calc(var(--spacing) * 3); | ||||
|   } | ||||
|   .content { | ||||
|     grid-column: 1; | ||||
|     grid-row: 1; | ||||
|     overflow: hidden; | ||||
|   } | ||||
|   .v_scrollbar { | ||||
|     grid-column: 2; | ||||
|     grid-row: 1; | ||||
|     overflow: hidden; | ||||
|     position: relative; | ||||
|     .v_thumb { | ||||
|       width: 100%; | ||||
|       aspect-ratio: 1 / 3; | ||||
|       position: absolute; | ||||
|       border-radius: calc(var(--border-radius) * 2); | ||||
|       background-color: oklch(from var(--color-dark-primary) l c h / 70%); | ||||
|       cursor: pointer; | ||||
|     } | ||||
|   } | ||||
|   .h_scrollbar { | ||||
|     grid-column: 1; | ||||
|     grid-row: 2; | ||||
|     overflow: hidden; | ||||
|     position: relative; | ||||
|     .h_thumb { | ||||
|       height: 100%; | ||||
|       aspect-ratio: 3 / 1; | ||||
|       position: absolute; | ||||
|       border-radius: calc(var(--border-radius) * 2); | ||||
|       background-color: oklch(from var(--color-dark-primary) l c h / 70%); | ||||
|       cursor: pointer; | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										195
									
								
								src/components/ScrollArea.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								src/components/ScrollArea.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,195 @@ | ||||
| import { clamp } from 'lodash-es'; | ||||
| import { MouseEvent, RefObject, useEffect, useRef, useState, WheelEvent } from 'react'; | ||||
| import styles from './ScrollArea.module.css'; | ||||
|  | ||||
| type ScrollBarProps = { | ||||
|   containerRef: RefObject<HTMLDivElement> | null; | ||||
| }; | ||||
|  | ||||
| function VerticalScrollBar({ containerRef }: ScrollBarProps) { | ||||
|   const [thumbPos, setThumbPos] = useState(0); | ||||
|   const trackRef = useRef<HTMLDivElement | null>(null); | ||||
|   const thumbRef = useRef<HTMLDivElement | null>(null); | ||||
|   const handleMouseDown = (evt: MouseEvent) => { | ||||
|     evt.preventDefault(); | ||||
|     //@ts-expect-error TS2769 | ||||
|     document.addEventListener('mousemove', handleMouseMove); | ||||
|     //@ts-expect-error TS2769 | ||||
|     document.addEventListener('mouseup', handleMouseUp); | ||||
|   }; | ||||
|   const handleMouseMove = (evt: MouseEvent<HTMLDivElement>) => { | ||||
|     evt.preventDefault(); | ||||
|     const container = containerRef?.current; | ||||
|     const scrollbar = trackRef.current; | ||||
|     const thumb = thumbRef.current; | ||||
|  | ||||
|     if (container && scrollbar && thumb) { | ||||
|       const trackRect = scrollbar.getBoundingClientRect(); | ||||
|       const thumbRect = thumb.getBoundingClientRect(); | ||||
|       const offsetY = evt.clientY - trackRect.top - thumbRect.height / 2; | ||||
|       const thumbPosition = clamp(offsetY, 0, trackRect.height - thumbRect.height); | ||||
|       setThumbPos(thumbPosition); | ||||
|  | ||||
|       const scrollPercentage = thumbPosition / (trackRect.height - thumbRect.height); | ||||
|       container.scrollTop = scrollPercentage * (container.scrollHeight - container.clientHeight); | ||||
|     } | ||||
|   }; | ||||
|   const handleMouseUp = (evt: MouseEvent) => { | ||||
|     evt.preventDefault(); | ||||
|     //@ts-expect-error TS2769 | ||||
|     document.removeEventListener('mousemove', handleMouseMove); | ||||
|     //@ts-expect-error TS2769 | ||||
|     document.removeEventListener('mouseup', handleMouseUp); | ||||
|   }; | ||||
|  | ||||
|   const updateThumbPosition = () => { | ||||
|     const container = containerRef?.current; | ||||
|     const scrollbar = trackRef.current; | ||||
|     const scrollPercentage = | ||||
|       container.scrollTop / (container.scrollHeight - container.clientHeight); | ||||
|     const thumbPosition = | ||||
|       scrollPercentage * (scrollbar.clientHeight - thumbRef.current!.clientHeight); | ||||
|     setThumbPos(thumbPosition); | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const container = containerRef?.current; | ||||
|     const scrollbar = trackRef.current; | ||||
|     if (container && scrollbar) { | ||||
|       container.addEventListener('scroll', updateThumbPosition); | ||||
|       return () => { | ||||
|         container.removeEventListener('scroll', updateThumbPosition); | ||||
|       }; | ||||
|     } | ||||
|   }, []); | ||||
|  | ||||
|   return ( | ||||
|     <div className={styles.v_scrollbar} ref={trackRef}> | ||||
|       <div | ||||
|         className={styles.v_thumb} | ||||
|         ref={thumbRef} | ||||
|         style={{ top: thumbPos }} | ||||
|         onMouseDown={handleMouseDown} | ||||
|       /> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function HorizontalScrollBar({ containerRef }: ScrollBarProps) { | ||||
|   const [thumbPos, setThumbPos] = useState(0); | ||||
|   const trackRef = useRef<HTMLDivElement | null>(null); | ||||
|   const thumbRef = useRef<HTMLDivElement | null>(null); | ||||
|   const handleMouseDown = (evt: MouseEvent) => { | ||||
|     evt.preventDefault(); | ||||
|     //@ts-expect-error TS2769 | ||||
|     document.addEventListener('mousemove', handleMouseMove); | ||||
|     //@ts-expect-error TS2769 | ||||
|     document.addEventListener('mouseup', handleMouseUp); | ||||
|   }; | ||||
|   const handleMouseMove = (evt: MouseEvent) => { | ||||
|     evt.preventDefault(); | ||||
|     const container = containerRef?.current; | ||||
|     const scrollbar = trackRef.current; | ||||
|     const thumb = thumbRef.current; | ||||
|  | ||||
|     if (container && scrollbar && thumb) { | ||||
|       const trackRect = scrollbar.getBoundingClientRect(); | ||||
|       const thumbRect = thumb.getBoundingClientRect(); | ||||
|       const offsetX = evt.clientX - trackRect.left - thumbRect.width / 2; | ||||
|       const thumbPosition = clamp(offsetX, 0, trackRect.width - thumbRect.width); | ||||
|       setThumbPos(thumbPosition); | ||||
|  | ||||
|       const scrollPercentage = thumbPosition / (trackRect.width - thumbRect.width); | ||||
|       container.scrollLeft = scrollPercentage * (container.scrollWidth - container.clientWidth); | ||||
|     } | ||||
|   }; | ||||
|   const handleMouseUp = (evt: MouseEvent) => { | ||||
|     evt.preventDefault(); | ||||
|     //@ts-expect-error TS2769 | ||||
|     document.removeEventListener('mousemove', handleMouseMove); | ||||
|     //@ts-expect-error TS2769 | ||||
|     document.removeEventListener('mouseup', handleMouseUp); | ||||
|   }; | ||||
|  | ||||
|   const updateThumbPosition = () => { | ||||
|     const container = containerRef?.current; | ||||
|     const scrollbar = trackRef.current; | ||||
|     const scrollPercentage = container.scrollLeft / (container.scrollWidth - container.clientWidth); | ||||
|     const thumbPosition = | ||||
|       scrollPercentage * (scrollbar.clientWidth - thumbRef.current!.clientWidth); | ||||
|     setThumbPos(thumbPosition); | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const container = containerRef?.current; | ||||
|     const scrollbar = trackRef.current; | ||||
|     if (container && scrollbar) { | ||||
|       container.addEventListener('scroll', updateThumbPosition); | ||||
|       return () => { | ||||
|         container.removeEventListener('scroll', updateThumbPosition); | ||||
|       }; | ||||
|     } | ||||
|   }, []); | ||||
|  | ||||
|   return ( | ||||
|     <div className={styles.h_scrollbar} ref={trackRef}> | ||||
|       <div | ||||
|         className={styles.h_thumb} | ||||
|         ref={thumbRef} | ||||
|         style={{ left: thumbPos }} | ||||
|         onMouseDown={(e) => handleMouseDown(e)} | ||||
|       /> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| type ScrollAreaProps = { | ||||
|   children?: React.ReactNode; | ||||
|   enableX?: boolean; | ||||
|   enableY?: boolean; | ||||
|   normalizedScroll?: boolean; | ||||
| }; | ||||
|  | ||||
| export function ScrollArea({ | ||||
|   children, | ||||
|   enableX = false, | ||||
|   enableY = false, | ||||
|   normalizedScroll = false, | ||||
| }: ScrollAreaProps) { | ||||
|   const scrollContainerRef = useRef<HTMLDivElement | null>(null); | ||||
|   const [xScrollNeeded, setXScrollNeeded] = useState(false); | ||||
|   const [yScrollNeeded, setYScrollNeeded] = useState(false); | ||||
|   const handleWheel = (evt: WheelEvent<HTMLDivElement>) => { | ||||
|     const container = scrollContainerRef?.current; | ||||
|     if (enableY && container) { | ||||
|       const delta = evt.deltaY; | ||||
|       const normalizedDelta = normalizedScroll ? clamp(delta, -1, 1) * 30 : delta; | ||||
|       const newScrollTop = container.scrollTop + normalizedDelta; | ||||
|       container.scrollTop = clamp(newScrollTop, 0, container.scrollHeight - container.clientHeight); | ||||
|     } | ||||
|     if (enableX && container) { | ||||
|       const delta = evt.deltaX; | ||||
|       const normalizedDelta = normalizedScroll ? clamp(delta, -1, 1) * 30 : delta; | ||||
|       const newScrollLeft = container.scrollLeft + normalizedDelta; | ||||
|       container.scrollLeft = clamp(newScrollLeft, 0, container.scrollWidth - container.clientWidth); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const container = scrollContainerRef.current; | ||||
|     if (container) { | ||||
|       setXScrollNeeded(container.scrollWidth > container.clientWidth); | ||||
|       setYScrollNeeded(container.scrollHeight > container.clientHeight); | ||||
|     } | ||||
|   }, [children]); | ||||
|  | ||||
|   return ( | ||||
|     <div className={styles.scroll_area}> | ||||
|       <div className={styles.content} ref={scrollContainerRef} onWheel={(e) => handleWheel(e)}> | ||||
|         {children} | ||||
|       </div> | ||||
|       {enableY && yScrollNeeded && <VerticalScrollBar containerRef={scrollContainerRef} />} | ||||
|       {enableX && xScrollNeeded && <HorizontalScrollBar containerRef={scrollContainerRef} />} | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										96
									
								
								src/context/EstimContext.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								src/context/EstimContext.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | ||||
| import { invoke } from '@tauri-apps/api/core'; | ||||
| import { Event, listen, UnlistenFn } from '@tauri-apps/api/event'; | ||||
| import { message } from '@tauri-apps/plugin-dialog'; | ||||
| import { atom, PrimitiveAtom, useSetAtom } from 'jotai'; | ||||
| import { atomFamily } from 'jotai/utils'; | ||||
| import { FC, ReactNode, useCallback, useEffect, useRef } from 'react'; | ||||
|  | ||||
| export type Channels = 'a' | 'b'; | ||||
| type ChannelState = { | ||||
|   playing: boolean; | ||||
|   playMode: 'shuffle' | 'repeat' | 'repeat-one'; | ||||
|   strength: number; | ||||
|   maxStrength: number; | ||||
|   boosting: boolean; | ||||
|   boostLevel: number; | ||||
|   maxBoostLevel: number; | ||||
| }; | ||||
| type DeviceState = { | ||||
|   rssi: number | null; | ||||
|   battery: number | null; | ||||
| }; | ||||
| type BluetoothState = { | ||||
|   ready: boolean | null; | ||||
|   searching: boolean | null; | ||||
|   connected: string | null; | ||||
| }; | ||||
|  | ||||
| export const BleState = atom<BluetoothState>({ | ||||
|   ready: null, | ||||
|   searching: null, | ||||
|   connected: null, | ||||
| }); | ||||
| export const DeviceState = atom<DeviceState>({ | ||||
|   rssi: null, | ||||
|   battery: null, | ||||
| }); | ||||
| const Channels: Record<Channels, PrimitiveAtom<ChannelState>> = { | ||||
|   a: atom<ChannelState>({ | ||||
|     playing: false, | ||||
|     playMode: 'repeat-one', | ||||
|     strength: 0, | ||||
|     maxStrength: 100, | ||||
|     boosting: false, | ||||
|     boostLevel: 0, | ||||
|     maxBoostLevel: 100, | ||||
|   }), | ||||
|   b: atom<ChannelState>({ | ||||
|     playing: false, | ||||
|     playMode: 'repeat-one', | ||||
|     strength: 0, | ||||
|     maxStrength: 100, | ||||
|     boosting: false, | ||||
|     boostLevel: 0, | ||||
|     maxBoostLevel: 100, | ||||
|   }), | ||||
| }; | ||||
| export const ChannelState = atomFamily((channel: Channels) => Channels[channel]); | ||||
|  | ||||
| const EstimWatchProvider: FC<{ children?: ReactNode }> = ({ children }) => { | ||||
|   const unlisten = useRef<UnlistenFn | null>(null); | ||||
|   const setBleState = useSetAtom(BleState); | ||||
|   const handleAppStateRefresh = useCallback(async (event: Event<unknown>) => { | ||||
|     try { | ||||
|       const newState = await invoke('refresh_application_state'); | ||||
|       setBleState({ | ||||
|         ready: newState.central.is_ready, | ||||
|         searching: newState.central.is_scanning, | ||||
|         connected: newState.central.connected, | ||||
|       }); | ||||
|     } catch (e) { | ||||
|       console.error('[Answer refresh state]', e); | ||||
|     } | ||||
|   }, []); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     (async function () { | ||||
|       try { | ||||
|         unlisten.current = await listen('app_state_updated', handleAppStateRefresh); | ||||
|         await invoke('activate_central_adapter'); | ||||
|       } catch (e) { | ||||
|         console.error('[Activate Adapter]', e); | ||||
|         await message('Fail to activate Bluetooth adapter.', { | ||||
|           title: 'Bluetooth Error', | ||||
|           kind: 'error', | ||||
|         }); | ||||
|       } | ||||
|     })(); | ||||
|  | ||||
|     return () => { | ||||
|       unlisten.current?.(); | ||||
|     }; | ||||
|   }, []); | ||||
|   return <>{children}</>; | ||||
| }; | ||||
|  | ||||
| export default EstimWatchProvider; | ||||
							
								
								
									
										42
									
								
								src/icons/IconBattery.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/icons/IconBattery.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| import { Icon } from '@iconify/react/dist/iconify.js'; | ||||
| import { FC, useMemo } from 'react'; | ||||
| import { SharedIconProps } from './shared-props'; | ||||
|  | ||||
| type IconBatteryProps = SharedIconProps & { | ||||
|   level?: number | null; | ||||
| }; | ||||
|  | ||||
| const IconBattery: FC<IconBatteryProps> = ({ | ||||
|   height = 24, | ||||
|   aspect = 1, | ||||
|   stroke = 0.5, | ||||
|   level = null, | ||||
| }) => { | ||||
|   const width = useMemo(() => height * aspect, [height, aspect]); | ||||
|   const batteryIcon = useMemo(() => { | ||||
|     if (level !== null && level >= 90) { | ||||
|       return 'material-symbols-light:battery-full'; | ||||
|     } else if (level !== null && level >= 80) { | ||||
|       return 'material-symbols-light:battery-6-bar'; | ||||
|     } else if (level !== null && level >= 70) { | ||||
|       return 'material-symbols-light:battery-5-bar'; | ||||
|     } else if (level !== null && level >= 50) { | ||||
|       return 'material-symbols-light:battery-4-bar'; | ||||
|     } else if (level !== null && level >= 30) { | ||||
|       return 'material-symbols-light:battery-3-bar'; | ||||
|     } else if (level !== null && level >= 20) { | ||||
|       return 'material-symbols-light:battery-2-bar'; | ||||
|     } else if (level !== null && level >= 10) { | ||||
|       return 'material-symbols-light:battery-1-bar'; | ||||
|     } else if (level !== null && level >= 0) { | ||||
|       return 'material-symbols-light:battery-0-bar'; | ||||
|     } else { | ||||
|       return 'material-symbols-light:battery-error'; | ||||
|     } | ||||
|   }, [level]); | ||||
|   console.debug('[icon battery]', level, batteryIcon); | ||||
|  | ||||
|   return <Icon icon={batteryIcon} width={width} height={height} stroke={stroke} />; | ||||
| }; | ||||
|  | ||||
| export default IconBattery; | ||||
							
								
								
									
										35
									
								
								src/icons/IconBluetooth.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/icons/IconBluetooth.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| import { Icon } from '@iconify/react/dist/iconify.js'; | ||||
| import { FC, useMemo } from 'react'; | ||||
| import { SharedIconProps } from './shared-props'; | ||||
|  | ||||
| type IconBluetoothProps = SharedIconProps & { | ||||
|   ready?: boolean | null; | ||||
|   searching?: boolean | null; | ||||
|   connected?: boolean | null; | ||||
| }; | ||||
|  | ||||
| const IconBluetooth: FC<IconBluetoothProps> = ({ | ||||
|   height = 24, | ||||
|   aspect = 1, | ||||
|   stroke = 0.5, | ||||
|   ready = false, | ||||
|   searching = false, | ||||
|   connected = false, | ||||
| }) => { | ||||
|   const width = useMemo(() => height * aspect, [height, aspect]); | ||||
|   const bleIcon = useMemo(() => { | ||||
|     if (ready && !searching && !connected) { | ||||
|       return 'material-symbols-light:bluetooth'; | ||||
|     } else if (ready && searching) { | ||||
|       return 'material-symbols-light:bluetooth-searching'; | ||||
|     } else if (ready && !searching && connected) { | ||||
|       return 'material-symbols-light:bluetooth-connected'; | ||||
|     } else { | ||||
|       return 'material-symbols-light:bluetooth-disabled'; | ||||
|     } | ||||
|   }, [ready, searching, connected]); | ||||
|  | ||||
|   return <Icon icon={bleIcon} width={width} height={height} stroke={stroke} />; | ||||
| }; | ||||
|  | ||||
| export default IconBluetooth; | ||||
							
								
								
									
										30
									
								
								src/icons/IconRssi.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/icons/IconRssi.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| import { Icon } from '@iconify/react/dist/iconify.js'; | ||||
| import { FC, useMemo } from 'react'; | ||||
| import { SharedIconProps } from './shared-props'; | ||||
|  | ||||
| type IconRssiProps = SharedIconProps & { | ||||
|   level?: number | null; | ||||
| }; | ||||
|  | ||||
| const IconRssi: FC<IconRssiProps> = ({ height = 24, aspect = 1, stroke = 0.5, level = null }) => { | ||||
|   const width = useMemo(() => height * aspect, [height, aspect]); | ||||
|   const rssiIcon = useMemo(() => { | ||||
|     if (level === null) { | ||||
|       return 'material-symbols-light:signal-cellular-nodata'; | ||||
|     } else if (level <= -80) { | ||||
|       return 'material-symbols-light:signal-cellular-4-bar'; | ||||
|     } else if (level <= -70) { | ||||
|       return 'material-symbols-light:signal-cellular-3-bar'; | ||||
|     } else if (level <= -60) { | ||||
|       return 'material-symbols-light:signal-cellular-2-bar'; | ||||
|     } else if (level <= -50) { | ||||
|       return 'material-symbols-light:signal-cellular-1-bar'; | ||||
|     } else { | ||||
|       return 'material-symbols-light:signal-cellular-null'; | ||||
|     } | ||||
|   }, [level]); | ||||
|  | ||||
|   return <Icon icon={rssiIcon} width={width} height={height} stroke={stroke} />; | ||||
| }; | ||||
|  | ||||
| export default IconRssi; | ||||
							
								
								
									
										17
									
								
								src/icons/shared-props.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/icons/shared-props.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import { IconProps } from '@iconify/react/dist/iconify.js'; | ||||
|  | ||||
| export type SharedIconProps = { | ||||
|   height?: number | null; | ||||
|   aspect?: number | null; | ||||
|   stroke?: number | null; | ||||
| }; | ||||
|  | ||||
| export const defaultIconProps: Partial<IconProps> = { | ||||
|   height: 24, | ||||
|   stroke: 0.5, | ||||
| }; | ||||
|  | ||||
| export const smallIconProps: Partial<IconProps> = { | ||||
|   height: 16, | ||||
|   stroke: 0.5, | ||||
| }; | ||||
							
								
								
									
										5
									
								
								src/index.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/index.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| @layer theme, base, components, utilities, pages; | ||||
| @import 'sanitize.css' layer(base); | ||||
| @import 'sanitize.css/forms.css' layer(base); | ||||
| @import './theme.css'; | ||||
| @import './components.css'; | ||||
							
								
								
									
										31
									
								
								src/main.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/main.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| // Load global styles | ||||
| import './index.css'; | ||||
| // Load foundations | ||||
| import React from 'react'; | ||||
| import ReactDOM from 'react-dom/client'; | ||||
| import { BrowserRouter, Route, Routes } from 'react-router-dom'; | ||||
| import Layout from './Layout'; | ||||
| import EstimWatchProvider from './context/EstimContext'; | ||||
| import Device from './pages/Device'; | ||||
| import PatternEditor from './pages/PatternEditor'; | ||||
| import PatternLibrary from './pages/PatternLibrary'; | ||||
| import PlayControl from './pages/Play'; | ||||
| import Settings from './pages/Settings'; | ||||
|  | ||||
| ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( | ||||
|   <React.StrictMode> | ||||
|     <EstimWatchProvider> | ||||
|       <BrowserRouter> | ||||
|         <Routes> | ||||
|           <Route path="/" element={<Layout />}> | ||||
|             <Route index element={<Device />} /> | ||||
|             <Route path="/play" element={<PlayControl />} /> | ||||
|             <Route path="/library" element={<PatternLibrary />} /> | ||||
|             <Route path="/pattern-editor" element={<PatternEditor />} /> | ||||
|             <Route path="/settings" element={<Settings />} /> | ||||
|           </Route> | ||||
|         </Routes> | ||||
|       </BrowserRouter> | ||||
|     </EstimWatchProvider> | ||||
|   </React.StrictMode>, | ||||
| ); | ||||
							
								
								
									
										11
									
								
								src/page-components/device/BleControl.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/page-components/device/BleControl.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| @layer pages { | ||||
|   .ble_control { | ||||
|     height: calc(var(--spacing) * 12); | ||||
|     padding-inline: calc(var(--spacing) * 2); | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     justify-content: flex-end; | ||||
|     align-items: center; | ||||
|     gap: calc(var(--spacing) * 2); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										17
									
								
								src/page-components/device/BleControl.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/page-components/device/BleControl.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import { useAtomValue } from 'jotai'; | ||||
| import { FC } from 'react'; | ||||
| import { BleState } from '../../context/EstimContext'; | ||||
| import styles from './BleControl.module.css'; | ||||
|  | ||||
| const BleControl: FC = () => { | ||||
|   const bleState = useAtomValue(BleState); | ||||
|  | ||||
|   return ( | ||||
|     <div className={styles.ble_control}> | ||||
|       <button disabled={!bleState.ready || bleState.searching}>Scan</button> | ||||
|       <button disabled={!bleState.ready || !bleState.connected}>Disconnect</button> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default BleControl; | ||||
							
								
								
									
										8
									
								
								src/page-components/device/DeviceDetail.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/page-components/device/DeviceDetail.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| @layer pages { | ||||
|   .device_detail { | ||||
|     flex: 2; | ||||
|     border-radius: calc(var(--border-radius) * 2); | ||||
|     padding: calc(var(--spacing) * 2); | ||||
|     background-color: var(--color-dark-surface-container); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										8
									
								
								src/page-components/device/DeviceDetail.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/page-components/device/DeviceDetail.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| import { FC } from 'react'; | ||||
| import styles from './DeviceDetail.module.css'; | ||||
|  | ||||
| const DeviceDetail: FC = () => { | ||||
|   return <div className={styles.device_detail}></div>; | ||||
| }; | ||||
|  | ||||
| export default DeviceDetail; | ||||
							
								
								
									
										8
									
								
								src/page-components/device/DeviceList.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/page-components/device/DeviceList.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| @layer pages { | ||||
|   .devices { | ||||
|     flex: 1; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     align-items: stretch; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										13
									
								
								src/page-components/device/DeviceList.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/page-components/device/DeviceList.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import { FC } from 'react'; | ||||
| import { ScrollArea } from '../../components/ScrollArea'; | ||||
| import styles from './DeviceList.module.css'; | ||||
|  | ||||
| const DeviceList: FC = () => { | ||||
|   return ( | ||||
|     <div className={styles.devices}> | ||||
|       <ScrollArea enableY></ScrollArea> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default DeviceList; | ||||
							
								
								
									
										11
									
								
								src/page-components/play-control/ChannelHost.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/page-components/play-control/ChannelHost.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| @layer pages { | ||||
|   .channel_host { | ||||
|     flex: 1; | ||||
|     padding: calc(var(--spacing) * 2) calc(var(--spacing) * 3); | ||||
|     border-radius: calc(var(--border-radius) * 2); | ||||
|     background-color: var(--color-dark-surface-container); | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     gap: calc(var(--spacing)); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										17
									
								
								src/page-components/play-control/ChannelHost.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/page-components/play-control/ChannelHost.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import { FC } from 'react'; | ||||
| import { Channels } from '../../context/EstimContext'; | ||||
| import styles from './ChannelHost.module.css'; | ||||
|  | ||||
| type ChannelHostProps = { | ||||
|   channel: Channels; | ||||
| }; | ||||
|  | ||||
| const ChannelHost: FC<ChannelHostProps> = ({ channel }) => { | ||||
|   return ( | ||||
|     <div className={styles.channel_host}> | ||||
|       <h3>Channel {channel.toUpperCase()}</h3> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default ChannelHost; | ||||
							
								
								
									
										30
									
								
								src/page-components/state-bar/BleState.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/page-components/state-bar/BleState.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| import { useAtomValue } from 'jotai'; | ||||
| import { FC, useMemo } from 'react'; | ||||
| import { BleState } from '../../context/EstimContext'; | ||||
| import IconBluetooth from '../../icons/IconBluetooth'; | ||||
|  | ||||
| const BleStates: FC = () => { | ||||
|   const ble = useAtomValue(BleState); | ||||
|   const bleIcon = useMemo(() => { | ||||
|     if (ble.ready && !ble.searching && !ble.connected) { | ||||
|       return 'material-symbols-light:bluetooth'; | ||||
|     } else if (ble.ready && ble.searching) { | ||||
|       return 'material-symbols-light:bluetooth-searching'; | ||||
|     } else if (ble.ready && !ble.searching && ble.connected) { | ||||
|       return 'material-symbols-light:bluetooth-connected'; | ||||
|     } else { | ||||
|       return 'material-symbols-light:bluetooth-disabled'; | ||||
|     } | ||||
|   }, [ble]); | ||||
|  | ||||
|   return ( | ||||
|     <IconBluetooth | ||||
|       height={16} | ||||
|       ready={ble.ready} | ||||
|       searching={ble.searching} | ||||
|       connected={ble.connected?.length > 0} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default BleStates; | ||||
							
								
								
									
										10
									
								
								src/page-components/state-bar/ChannelStates.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/page-components/state-bar/ChannelStates.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| @layer pages { | ||||
|   .channel_state { | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     justify-content: flex-start; | ||||
|     align-items: center; | ||||
|     gap: calc(var(--spacing)); | ||||
|     font-size: calc(var(--font-size) * 1.4); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										35
									
								
								src/page-components/state-bar/ChannelStates.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/page-components/state-bar/ChannelStates.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| import { Icon } from '@iconify/react/dist/iconify.js'; | ||||
| import { useAtomValue } from 'jotai'; | ||||
| import { FC } from 'react'; | ||||
| import { Channels, ChannelState } from '../../context/EstimContext'; | ||||
| import { smallIconProps } from '../../icons/shared-props'; | ||||
| import styles from './ChannelStates.module.css'; | ||||
|  | ||||
| const ChannelStates: FC<{ channel: Channels }> = ({ channel }) => { | ||||
|   const chState = useAtomValue(ChannelState(channel)); | ||||
|  | ||||
|   return ( | ||||
|     <div className={styles.channel_state}> | ||||
|       <span>Ch {channel.toUpperCase()}</span> | ||||
|       {chState.playing ? ( | ||||
|         <Icon icon="material-symbols-light:electric-bolt" {...smallIconProps} /> | ||||
|       ) : ( | ||||
|         <Icon icon="material-symbols-light:stop" {...smallIconProps} /> | ||||
|       )} | ||||
|       <span>{chState.strength}</span> | ||||
|       <Icon icon="material-symbols-light:arrow-upload-progress" {...smallIconProps} /> | ||||
|       <span>{chState.boostLevel}</span> | ||||
|       {chState.playMode === 'shuffle' && ( | ||||
|         <Icon icon="material-symbols-light:shuffle" {...smallIconProps} /> | ||||
|       )} | ||||
|       {chState.playMode === 'repeat' && ( | ||||
|         <Icon icon="material-symbols-light:repeat" {...smallIconProps} /> | ||||
|       )} | ||||
|       {chState.playMode === 'repeat-one' && ( | ||||
|         <Icon icon="material-symbols-light:repeat-one" {...smallIconProps} /> | ||||
|       )} | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default ChannelStates; | ||||
							
								
								
									
										18
									
								
								src/page-components/state-bar/DeviceStates.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/page-components/state-bar/DeviceStates.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import { useAtomValue } from 'jotai'; | ||||
| import { FC } from 'react'; | ||||
| import { DeviceState } from '../../context/EstimContext'; | ||||
| import IconBattery from '../../icons/IconBattery'; | ||||
| import IconRssi from '../../icons/IconRssi'; | ||||
|  | ||||
| const DeviceStates: FC = () => { | ||||
|   const deviceState = useAtomValue(DeviceState); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <IconRssi height={16} level={deviceState.rssi} /> | ||||
|       <IconBattery height={16} level={deviceState.battery} /> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default DeviceStates; | ||||
							
								
								
									
										13
									
								
								src/page-components/state-bar/StateBar.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/page-components/state-bar/StateBar.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| @layer pages { | ||||
|   .state_bar { | ||||
|     flex: 1; | ||||
|     padding-block-start: calc(var(--spacing)); | ||||
|     padding-inline: calc(var(--spacing) * 2); | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     justify-content: flex-end; | ||||
|     align-items: center; | ||||
|     gap: calc(var(--spacing) * 4); | ||||
|     font-size: calc(var(--font-size) * 1.6); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										18
									
								
								src/page-components/state-bar/StateBar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/page-components/state-bar/StateBar.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import { FC } from 'react'; | ||||
| import BleStates from './BleState'; | ||||
| import ChannelStates from './ChannelStates'; | ||||
| import DeviceStates from './DeviceStates'; | ||||
| import styles from './StateBar.module.css'; | ||||
|  | ||||
| const StateBar: FC = () => { | ||||
|   return ( | ||||
|     <div className={styles.state_bar}> | ||||
|       <BleStates /> | ||||
|       <ChannelStates channel="a" /> | ||||
|       <ChannelStates channel="b" /> | ||||
|       <DeviceStates /> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default StateBar; | ||||
							
								
								
									
										11
									
								
								src/pages/Device.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/pages/Device.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| @layer pages { | ||||
|   .device_list { | ||||
|     flex: 1; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     align-items: stretch; | ||||
|     gap: calc(var(--spacing) * 2); | ||||
|     border-radius: calc(var(--border-radius) * 2); | ||||
|     background-color: var(--color-dark-surface-container); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										19
									
								
								src/pages/Device.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/pages/Device.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| import { FC } from 'react'; | ||||
| import BleControl from '../page-components/device/BleControl'; | ||||
| import DeviceDetail from '../page-components/device/DeviceDetail'; | ||||
| import DeviceList from '../page-components/device/DeviceList'; | ||||
| import styles from './Device.module.css'; | ||||
|  | ||||
| const Device: FC = () => { | ||||
|   return ( | ||||
|     <div className="workspace horizontal"> | ||||
|       <div className={styles.device_list}> | ||||
|         <BleControl /> | ||||
|         <DeviceList /> | ||||
|       </div> | ||||
|       <DeviceDetail /> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default Device; | ||||
							
								
								
									
										7
									
								
								src/pages/PatternEditor.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/pages/PatternEditor.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| import { FC } from 'react'; | ||||
|  | ||||
| const PatternEditor: FC = () => { | ||||
|   return <div className="workspace"></div>; | ||||
| }; | ||||
|  | ||||
| export default PatternEditor; | ||||
							
								
								
									
										7
									
								
								src/pages/PatternLibrary.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/pages/PatternLibrary.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| import { FC } from 'react'; | ||||
|  | ||||
| const PatternLibrary: FC = () => { | ||||
|   return <div className="workspace"></div>; | ||||
| }; | ||||
|  | ||||
| export default PatternLibrary; | ||||
							
								
								
									
										10
									
								
								src/pages/Play.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/pages/Play.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| @layer pages { | ||||
|   .play_control { | ||||
|     width: 100%; | ||||
|     overflow: hidden; | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     align-items: stretch; | ||||
|     gap: calc(var(--spacing) * 2); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										13
									
								
								src/pages/Play.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/pages/Play.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import { FC } from 'react'; | ||||
| import ChannelHost from '../page-components/play-control/ChannelHost'; | ||||
|  | ||||
| const PlayControl: FC = () => { | ||||
|   return ( | ||||
|     <div className="workspace horizontal"> | ||||
|       <ChannelHost channel="a" /> | ||||
|       <ChannelHost channel="b" /> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default PlayControl; | ||||
							
								
								
									
										7
									
								
								src/pages/Settings.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/pages/Settings.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| import { FC } from 'react'; | ||||
|  | ||||
| const Settings: FC = () => { | ||||
|   return <div className="workspace vertical"></div>; | ||||
| }; | ||||
|  | ||||
| export default Settings; | ||||
							
								
								
									
										75
									
								
								src/theme.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/theme.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| @import './variables.css' layer(theme); | ||||
|  | ||||
| @layer base { | ||||
|   :root { | ||||
|     font-family: var(--font-family); | ||||
|     font-size: var(--font-size); | ||||
|     line-height: var(--line-height); | ||||
|     font-weight: 400; | ||||
|  | ||||
|     width: 100vw; | ||||
|     height: 100vh; | ||||
|     overflow: hidden; | ||||
|     user-select: none; | ||||
|  | ||||
|     font-synthesis: none; | ||||
|     text-rendering: optimizeLegibility; | ||||
|     -webkit-font-smoothing: antialiased; | ||||
|     -moz-osx-font-smoothing: grayscale; | ||||
|     -webkit-text-size-adjust: 100%; | ||||
|   } | ||||
|  | ||||
|   :where(html, body) { | ||||
|     color-scheme: dark; | ||||
|     width: 100vw; | ||||
|     height: 100vh; | ||||
|     overflow: hidden; | ||||
|     z-index: 10; | ||||
|   } | ||||
|  | ||||
|   body { | ||||
|     position: relative; | ||||
|     color: var(--color-dark-on-surface); | ||||
|     background-color: var(--color-dark-surface); | ||||
|   } | ||||
|  | ||||
|   :where(h1, h2, h3, h4, h5, h6, p, div, span) { | ||||
|     margin: 0; | ||||
|     padding: 0; | ||||
|   } | ||||
|  | ||||
|   :where(menu) { | ||||
|     margin: 0; | ||||
|   } | ||||
|  | ||||
|   #root { | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     overflow: hidden; | ||||
|     z-index: 15; | ||||
|   } | ||||
|  | ||||
|   .evelation-0 { | ||||
|     box-shadow: var(--elevation-dark-0); | ||||
|   } | ||||
|  | ||||
|   .evelation-1 { | ||||
|     box-shadow: var(--elevation-dark-1-ambient), --var(--elevation-dark-1-umbra); | ||||
|   } | ||||
|  | ||||
|   .evelation-2 { | ||||
|     box-shadow: var(--elevation-dark-2-ambient), var(--elevation-dark-2-umbra); | ||||
|   } | ||||
|  | ||||
|   .evelation-3 { | ||||
|     box-shadow: var(--elevation-dark-3-ambient), var(--elevation-dark-3-umbra); | ||||
|   } | ||||
|  | ||||
|   .evelation-4 { | ||||
|     box-shadow: var(--elevation-dark-4-ambient), var(--elevation-dark-4-umbra); | ||||
|   } | ||||
|  | ||||
|   .evelation-5 { | ||||
|     box-shadow: var(--elevation-dark-5-ambient), var(--elevation-dark-5-umbra); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										271
									
								
								src/variables.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										271
									
								
								src/variables.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,271 @@ | ||||
| @layer theme { | ||||
|   :root { | ||||
|     --font-family: 'Inter', system-ui, Avenir, Helvetica, Arial, sans-serif; | ||||
|  | ||||
|     --color-white: #ffffff; | ||||
|     --color-black: #000000; | ||||
|     --color-light-primary: #744c1f; | ||||
|     --color-light-on-primary: #fff5c0; | ||||
|     --color-light-primary-container: #95693b; | ||||
|     --color-light-on-primary-container: #fff5c0; | ||||
|     --color-light-primary-fixed: #95693b; | ||||
|     --color-light-primary-fixed-dim: #7a5124; | ||||
|     --color-light-on-primary-fixed: #fff5c0; | ||||
|     --color-light-on-primary-fixed-variant: #fff5c0; | ||||
|     --color-light-inverse-primary: #6f481b; | ||||
|     --color-light-secondary: #67513b; | ||||
|     --color-light-on-secondary: #fffae0; | ||||
|     --color-light-secondary-container: #866e57; | ||||
|     --color-light-on-secondary-container: #fffae0; | ||||
|     --color-light-secondary-fixed: #866e57; | ||||
|     --color-light-secondary-fixed-dim: #6c5540; | ||||
|     --color-light-on-secondary-fixed: #fffae0; | ||||
|     --color-light-on-secondary-fixed-variant: #fffae0; | ||||
|     --color-light-inverse-secondary: #dec1a8; | ||||
|     --color-light-tertiary: #5c561a; | ||||
|     --color-light-on-tertiary: #ffffbb; | ||||
|     --color-light-tertiary-container: #7b7436; | ||||
|     --color-light-on-tertiary-container: #ffffbb; | ||||
|     --color-light-tertiary-fixed: #7b7436; | ||||
|     --color-light-tertiary-fixed-dim: #615b1f; | ||||
|     --color-light-on-tertiary-fixed: #ffffbb; | ||||
|     --color-light-on-tertiary-fixed-variant: #ffffbb; | ||||
|     --color-light-inverse-tertiary: #d3c886; | ||||
|     --color-light-error: #b20000; | ||||
|     --color-light-on-error: #ffc89b; | ||||
|     --color-light-error-container: #d92f18; | ||||
|     --color-light-on-error-container: #ffc89b; | ||||
|     --color-light-error-fixed: #ffab80; | ||||
|     --color-light-error-fixed-dim: #ff8e66; | ||||
|     --color-light-on-error-fixed: #640000; | ||||
|     --color-light-on-error-fixed-variant: #9f0000; | ||||
|     --color-light-inverse-error: #ff8e66; | ||||
|     --color-light-surface: #fff8f1; | ||||
|     --color-light-surface-dim: #bfb7b1; | ||||
|     --color-light-surface-bright: #fff8f1; | ||||
|     --color-light-surface-variant: #efe0d4; | ||||
|     --color-light-surface-container: #e9e1db; | ||||
|     --color-light-surface-container-lowest: #fffef7; | ||||
|     --color-light-surface-container-low: #f7efe9; | ||||
|     --color-light-surface-container-high: #dbd3cd; | ||||
|     --color-light-surface-container-highest: #cdc5bf; | ||||
|     --color-light-on-surface: #090000; | ||||
|     --color-light-on-surface-variant: #41362d; | ||||
|     --color-light-inverse-surface: #f7efe9; | ||||
|     --color-light-inverse-on-surface: #090000; | ||||
|     --color-light-outline: #5f5349; | ||||
|     --color-light-outline-variant: #7d7065; | ||||
|     --color-light-scrim: #090000; | ||||
|     --color-light-shadow: #090000; | ||||
|     --color-light-success: #1c6300; | ||||
|     --color-light-on-success: #d6ff6a; | ||||
|     --color-light-success-container: #3f8200; | ||||
|     --color-light-on-success-container: #d6ff6a; | ||||
|     --color-light-success-fixed: #3f8200; | ||||
|     --color-light-success-fixed-dim: #226800; | ||||
|     --color-light-on-success-fixed: #d6ff6a; | ||||
|     --color-light-on-success-fixed-variant: #d6ff6a; | ||||
|     --color-light-inverse-success: #053f00; | ||||
|     --color-light-defensive: #7131b4; | ||||
|     --color-light-on-defensive: #ffdeff; | ||||
|     --color-light-defensive-container: #9350d6; | ||||
|     --color-light-on-defensive-container: #ffdeff; | ||||
|     --color-light-defensive-fixed: #9350d6; | ||||
|     --color-light-defensive-fixed-dim: #7737ba; | ||||
|     --color-light-on-defensive-fixed: #ffdeff; | ||||
|     --color-light-on-defensive-fixed-variant: #ffdeff; | ||||
|     --color-light-inverse-defensive: #47038c; | ||||
|     --color-light-gentle: #7c4b00; | ||||
|     --color-light-on-gentle: #fff25f; | ||||
|     --color-light-gentle-container: #9e6700; | ||||
|     --color-light-on-gentle-container: #fff25f; | ||||
|     --color-light-gentle-fixed: #9e6700; | ||||
|     --color-light-gentle-fixed-dim: #814f00; | ||||
|     --color-light-on-gentle-fixed: #fff25f; | ||||
|     --color-light-on-gentle-fixed-variant: #fff25f; | ||||
|     --color-light-inverse-gentle: #562900; | ||||
|     --color-light-aggressive: #b6002d; | ||||
|     --color-light-on-aggressive: #ffc3cc; | ||||
|     --color-light-aggressive-container: #dc2247; | ||||
|     --color-light-on-aggressive-container: #ffc3cc; | ||||
|     --color-light-aggressive-fixed: #dc2247; | ||||
|     --color-light-aggressive-fixed-dim: #bd0031; | ||||
|     --color-light-on-aggressive-fixed: #ffc3cc; | ||||
|     --color-light-on-aggressive-fixed-variant: #ffc3cc; | ||||
|     --color-light-inverse-aggressive: #88000e; | ||||
|     --color-light-info: #225694; | ||||
|     --color-light-on-info: #ddffff; | ||||
|     --color-light-info-container: #4973b4; | ||||
|     --color-light-on-info-container: #ddffff; | ||||
|     --color-light-info-fixed: #4973b4; | ||||
|     --color-light-info-fixed-dim: #2a5a99; | ||||
|     --color-light-on-info-fixed: #ddffff; | ||||
|     --color-light-on-info-fixed-variant: #ddffff; | ||||
|     --color-light-inverse-info: #00346e; | ||||
|     --color-light-warning: #834600; | ||||
|     --color-light-on-warning: #ffed93; | ||||
|     --color-light-warning-container: #a66204; | ||||
|     --color-light-on-warning-container: #ffed93; | ||||
|     --color-light-warning-fixed: #a66204; | ||||
|     --color-light-warning-fixed-dim: #884a00; | ||||
|     --color-light-on-warning-fixed: #ffed93; | ||||
|     --color-light-on-warning-fixed-variant: #ffed93; | ||||
|     --color-light-inverse-warning: #5b2300; | ||||
|     --color-dark-primary: #ffe6b1; | ||||
|     --color-dark-on-primary: #280000; | ||||
|     --color-dark-primary-container: #ecb986; | ||||
|     --color-dark-on-primary-container: #2b0000; | ||||
|     --color-dark-primary-fixed: #ffd8a5; | ||||
|     --color-dark-primary-fixed-dim: #f1bd8a; | ||||
|     --color-dark-on-primary-fixed: #280000; | ||||
|     --color-dark-on-primary-fixed-variant: #2d0600; | ||||
|     --color-dark-inverse-primary: #e8b482; | ||||
|     --color-dark-secondary: #ffebd1; | ||||
|     --color-dark-on-secondary: #1a0000; | ||||
|     --color-dark-secondary-container: #dabea5; | ||||
|     --color-dark-on-secondary-container: #1f0500; | ||||
|     --color-dark-secondary-fixed: #fbddc4; | ||||
|     --color-dark-secondary-fixed-dim: #dec1a8; | ||||
|     --color-dark-on-secondary-fixed: #1a0000; | ||||
|     --color-dark-on-secondary-fixed-variant: #220c00; | ||||
|     --color-dark-inverse-secondary: #715a44; | ||||
|     --color-dark-tertiary: #fef1ad; | ||||
|     --color-dark-on-tertiary: #150100; | ||||
|     --color-dark-tertiary-container: #cfc482; | ||||
|     --color-dark-on-tertiary-container: #1b0c00; | ||||
|     --color-dark-tertiary-fixed: #f0e4a0; | ||||
|     --color-dark-tertiary-fixed-dim: #d3c886; | ||||
|     --color-dark-on-tertiary-fixed: #150100; | ||||
|     --color-dark-on-tertiary-fixed-variant: #1e1200; | ||||
|     --color-dark-inverse-tertiary: #666023; | ||||
|     --color-dark-error: #ffb88d; | ||||
|     --color-dark-on-error: #4e0000; | ||||
|     --color-dark-error-container: #ff8a63; | ||||
|     --color-dark-on-error-container: #540000; | ||||
|     --color-dark-error-fixed: #ffab80; | ||||
|     --color-dark-error-fixed-dim: #ff8e66; | ||||
|     --color-dark-on-error-fixed: #640000; | ||||
|     --color-dark-on-error-fixed-variant: #9f0000; | ||||
|     --color-dark-inverse-error: #bf0902; | ||||
|     --color-dark-surface: #18120c; | ||||
|     --color-dark-surface-dim: #18120c; | ||||
|     --color-dark-surface-bright: #554f4a; | ||||
|     --color-dark-surface-variant: #50453b; | ||||
|     --color-dark-surface-container: #352f2a; | ||||
|     --color-dark-surface-container-lowest: #090000; | ||||
|     --color-dark-surface-container-low: #241f1a; | ||||
|     --color-dark-surface-container-high: #403a35; | ||||
|     --color-dark-surface-container-highest: #4c4640; | ||||
|     --color-dark-on-surface: #fffef7; | ||||
|     --color-dark-on-surface-variant: #fffdf0; | ||||
|     --color-dark-inverse-surface: #352f2a; | ||||
|     --color-dark-inverse-on-surface: #fffef7; | ||||
|     --color-dark-outline: #fcede1; | ||||
|     --color-dark-outline-variant: #cec0b4; | ||||
|     --color-dark-scrim: #090000; | ||||
|     --color-dark-shadow: #090000; | ||||
|     --color-dark-defensive: #ffcfff; | ||||
|     --color-dark-on-defensive: #050055; | ||||
|     --color-dark-defensive-container: #eca2ff; | ||||
|     --color-dark-on-defensive-container: #0d005d; | ||||
|     --color-dark-defensive-fixed: #ffc1ff; | ||||
|     --color-dark-defensive-fixed-dim: #f0a5ff; | ||||
|     --color-dark-on-defensive-fixed: #050055; | ||||
|     --color-dark-on-defensive-fixed-variant: #140062; | ||||
|     --color-dark-inverse-defensive: #ffcbff; | ||||
|     --color-dark-info: #cef0ff; | ||||
|     --color-dark-on-info: #00013a; | ||||
|     --color-dark-info-container: #9fc3ff; | ||||
|     --color-dark-on-info-container: #000d41; | ||||
|     --color-dark-info-fixed: #c0e2ff; | ||||
|     --color-dark-info-fixed-dim: #a3c6ff; | ||||
|     --color-dark-on-info-fixed: #00013a; | ||||
|     --color-dark-on-info-fixed-variant: #001346; | ||||
|     --color-dark-inverse-info: #caecff; | ||||
|     --color-dark-gentle: #ffe350; | ||||
|     --color-dark-on-gentle: #370000; | ||||
|     --color-dark-gentle-container: #fcb61a; | ||||
|     --color-dark-on-gentle-container: #390000; | ||||
|     --color-dark-gentle-fixed: #ffd642; | ||||
|     --color-dark-gentle-fixed-dim: #ffba20; | ||||
|     --color-dark-on-gentle-fixed: #370000; | ||||
|     --color-dark-on-gentle-fixed-variant: #3b0000; | ||||
|     --color-dark-inverse-gentle: #ffdf4c; | ||||
|     --color-dark-success: #c7ff5b; | ||||
|     --color-dark-on-success: #001600; | ||||
|     --color-dark-success-container: #97d529; | ||||
|     --color-dark-on-success-container: #001c00; | ||||
|     --color-dark-success-fixed: #b9f54d; | ||||
|     --color-dark-success-fixed-dim: #9cd92e; | ||||
|     --color-dark-on-success-fixed: #001600; | ||||
|     --color-dark-on-success-fixed-variant: #001f00; | ||||
|     --color-dark-inverse-success: #c2ff57; | ||||
|     --color-dark-warning: #ffde85; | ||||
|     --color-dark-on-warning: #350000; | ||||
|     --color-dark-warning-container: #ffb15b; | ||||
|     --color-dark-on-warning-container: #380000; | ||||
|     --color-dark-warning-fixed: #ffd179; | ||||
|     --color-dark-warning-fixed-dim: #ffb55e; | ||||
|     --color-dark-on-warning-fixed: #350000; | ||||
|     --color-dark-on-warning-fixed-variant: #3a0000; | ||||
|     --color-dark-inverse-warning: #ffda82; | ||||
|     --color-dark-aggressive: #ffb3bd; | ||||
|     --color-dark-on-aggressive: #4d0000; | ||||
|     --color-dark-aggressive-container: #ff8492; | ||||
|     --color-dark-on-aggressive-container: #540000; | ||||
|     --color-dark-aggressive-fixed: #ffa6b0; | ||||
|     --color-dark-aggressive-fixed-dim: #ff8896; | ||||
|     --color-dark-on-aggressive-fixed: #4d0000; | ||||
|     --color-dark-on-aggressive-fixed-variant: #590000; | ||||
|     --color-dark-inverse-aggressive: #ffafba; | ||||
|  | ||||
|     --spacing: 4px; | ||||
|     --border-radius: 2px; | ||||
|     --font-size: 10px; | ||||
|     --line-height: 1.2em; | ||||
|  | ||||
|     --elevation-light-0: none; | ||||
|     --elevation-light-1-ambient: 0 1px 3px 1px | ||||
|       color-mix(in oklch, var(--color-light-shadow) 15%, transparent); | ||||
|     --elevation-light-1-umbra: 0 1px 2px 0px | ||||
|       color-mix(in oklch, var(--color-light-shadow) 30%, transparent); | ||||
|     --elevation-light-2-ambient: 0 2px 6px 2px | ||||
|       color-mix(in oklch, var(--color-light-shadow) 15%, transparent); | ||||
|     --elevation-light-2-umbra: 0 1px 2px 0px | ||||
|       color-mix(in oklch, var(--color-light-shadow) 30%, transparent); | ||||
|     --elevation-light-3-ambient: 0 4px 8px 3px | ||||
|       color-mix(in oklch, var(--color-light-shadow) 15%, transparent); | ||||
|     --elevation-light-3-umbra: 0 1px 3px 0px | ||||
|       color-mix(in oklch, var(--color-light-shadow) 30%, transparent); | ||||
|     --elevation-light-4-ambient: 0 6px 10px 4px | ||||
|       color-mix(in oklch, var(--color-light-shadow) 15%, transparent); | ||||
|     --elevation-light-4-umbra: 0 2px 3px 0px | ||||
|       color-mix(in oklch, var(--color-light-shadow) 30%, transparent); | ||||
|     --elevation-light-5-ambient: 0 8px 12px 6px | ||||
|       color-mix(in oklch, var(--color-light-shadow) 15%, transparent); | ||||
|     --elevation-light-5-umbra: 0 4px 4px 0px | ||||
|       color-mix(in oklch, var(--color-light-shadow) 30%, transparent); | ||||
|  | ||||
|     --elevation-dark-0: none; | ||||
|     --elevation-dark-1-ambient: 0 1px 3px 1px | ||||
|       color-mix(in oklch, var(--color-dark-shadow) 15%, transparent); | ||||
|     --elevation-dark-1-umbra: 0 1px 2px 0px | ||||
|       color-mix(in oklch, var(--color-dark-shadow) 30%, transparent); | ||||
|     --elevation-dark-2-ambient: 0 2px 6px 2px | ||||
|       color-mix(in oklch, var(--color-dark-shadow) 15%, transparent); | ||||
|     --elevation-dark-2-umbra: 0 1px 2px 0px | ||||
|       color-mix(in oklch, var(--color-dark-shadow) 30%, transparent); | ||||
|     --elevation-dark-3-ambient: 0 4px 8px 3px | ||||
|       color-mix(in oklch, var(--color-dark-shadow) 15%, transparent); | ||||
|     --elevation-dark-3-umbra: 0 1px 3px 0px | ||||
|       color-mix(in oklch, var(--color-dark-shadow) 30%, transparent); | ||||
|     --elevation-dark-4-ambient: 0 6px 10px 4px | ||||
|       color-mix(in oklch, var(--color-dark-shadow) 15%, transparent); | ||||
|     --elevation-dark-4-umbra: 0 2px 3px 0px | ||||
|       color-mix(in oklch, var(--color-dark-shadow) 30%, transparent); | ||||
|     --elevation-dark-5-ambient: 0 8px 12px 6px | ||||
|       color-mix(in oklch, var(--color-dark-shadow) 15%, transparent); | ||||
|     --elevation-dark-5-umbra: 0 4px 4px 0px | ||||
|       color-mix(in oklch, var(--color-dark-shadow) 30%, transparent); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										1
									
								
								src/vite-env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/vite-env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| /// <reference types="vite/client" /> | ||||
		Reference in New Issue
	
	Block a user