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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/components/KeycodeSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/**
* KeycodeSelector Component
*
* A modal dialog for selecting behaviors and configuring parameters.
* A sheet-style dialog for selecting behaviors and configuring parameters.
* Behavior-first approach: select behavior, then configure parameters.
* Supports various parameter types with dedicated UI selectors.
*
* Features:
* - Close on select: Automatically close the dialog after selecting the last parameter
* - Close on select: Automatically close the sheet after selecting the last parameter
* (setting is persisted in localStorage)
*/
import { useState, useMemo, useCallback, useEffect } from "react";
Expand Down Expand Up @@ -598,8 +598,8 @@ export function KeycodeSelector({
return (
<Dialog.Root open={open} onOpenChange={handleOpenChange}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50" />
<Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full tablet:w-[90vw] max-w-4xl h-full tablet:h-[85vh] bg-[var(--color-surface)] rounded-none tablet:rounded-xl border border-[var(--color-border)] shadow-2xl z-50 flex flex-col overflow-hidden">
<Dialog.Overlay className="fixed inset-0 bg-black/50 backdrop-blur-sm tablet:bg-transparent tablet:backdrop-blur-0 z-50" />
<Dialog.Content className="keycode-selector-sheet fixed inset-x-0 bottom-0 w-full h-full tablet:h-[50vh] bg-[var(--color-surface)] rounded-none tablet:rounded-t-xl border border-[var(--color-border)] shadow-2xl z-50 flex flex-col overflow-hidden">
{/* Header with Cancel Button */}
<div className="flex items-center justify-between p-4 border-b border-[var(--color-border)]">
<Dialog.Title className="text-lg font-medium text-[var(--color-text)] flex items-center gap-2">
Expand Down
2 changes: 2 additions & 0 deletions src/components/PhysicalKey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ interface PhysicalKeyProps {
}

export function PhysicalKey({
keyPosition,
attrs,
isModified,
displayName,
Expand Down Expand Up @@ -102,6 +103,7 @@ export function PhysicalKey({
// Key content
const keyContent = (
<div
data-key-position={keyPosition}
className={`
absolute rounded-lg border cursor-pointer transition-all duration-150
flex flex-col items-center justify-center p-1.5 overflow-hidden
Expand Down
16 changes: 16 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,22 @@
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
}

.keycode-selector-sheet[data-state="open"] {
animation: keycode-selector-sheet-in 180ms ease-out;
}

@keyframes keycode-selector-sheet-in {
from {
opacity: 0.96;
transform: translateY(100%);
}

to {
opacity: 1;
transform: translateY(0);
}
}

.glass-panel {
background: linear-gradient(
135deg,
Expand Down
59 changes: 57 additions & 2 deletions src/pages/KeymapPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { useState, useContext, useCallback, useMemo, useEffect } from "react";
import {
useState,
useContext,
useCallback,
useMemo,
useEffect,
useRef,
} from "react";
import {
IconKeyboard,
IconDeviceFloppy,
Expand Down Expand Up @@ -34,6 +41,7 @@ export function KeymapPage() {
const physicalLayoutModules = usePhysicalLayoutModules();
const sensorRotate = useRuntimeSensorRotate();
const inputStream = useInputStream();
const pageRef = useRef<HTMLDivElement>(null);

// Local UI state
const [selectedLayerIndex, setSelectedLayerIndex] = useState(0);
Expand Down Expand Up @@ -69,6 +77,43 @@ export function KeymapPage() {
return currentLayer.bindings[selectedKeyPosition] ?? null;
}, [selectedKeyPosition, currentLayer]);

const scrollSelectedKeyIntoPreview = useCallback((keyPosition: number) => {
const pageElement = pageRef.current;
if (!pageElement) return;

const selectedKeyElement = pageElement.querySelector<HTMLElement>(
`[data-key-position="${keyPosition}"]`,
);
if (!selectedKeyElement) return;

const isWideViewport =
typeof window.matchMedia === "function"
? window.matchMedia("(min-width: 768px)").matches
: window.innerWidth >= 768;
if (!isWideViewport) return;

const pageRect = pageElement.getBoundingClientRect();
const keyRect = selectedKeyElement.getBoundingClientRect();
const sheetTop = window.innerHeight * 0.5;
const visibleTop = Math.max(pageRect.top, 0);
const visibleBottom = Math.min(pageRect.bottom, sheetTop);

if (visibleBottom <= visibleTop) return;

const targetCenterY = visibleTop + (visibleBottom - visibleTop) / 2;
const keyCenterY = keyRect.top + keyRect.height / 2;
const nextScrollTop = pageElement.scrollTop + keyCenterY - targetCenterY;

if (typeof pageElement.scrollTo === "function") {
pageElement.scrollTo({
top: Math.max(0, nextScrollTop),
behavior: "smooth",
});
} else {
pageElement.scrollTop = Math.max(0, nextScrollTop);
}
}, []);

// Handle key click
const handleKeyClick = useCallback((keyPosition: number) => {
setSelectedKeyPosition(keyPosition);
Expand Down Expand Up @@ -196,8 +241,18 @@ export function KeymapPage() {
setSelectedLayerIndex(inputStream.activeLayerIndex);
}, [inputStream.activeLayerIndex, keymap.keymap?.layers]);

useEffect(() => {
if (!showKeycodeSelector || selectedKeyPosition === null) return;

const frame = window.requestAnimationFrame(() => {
scrollSelectedKeyIntoPreview(selectedKeyPosition);
});

return () => window.cancelAnimationFrame(frame);
}, [scrollSelectedKeyIntoPreview, selectedKeyPosition, showKeycodeSelector]);

return (
<div className="p-6 h-full overflow-auto">
<div ref={pageRef} className="p-6 h-full overflow-auto">
<div className="max-w-6xl mx-auto">
{/* Header */}
<div className="flex flex-col tablet:flex-row tablet:items-center gap-3 mb-4">
Expand Down
Loading