Skip to content

Commit 91c451f

Browse files
committed
eq size layout controls
1 parent 3dc16ab commit 91c451f

File tree

2 files changed

+160
-139
lines changed

2 files changed

+160
-139
lines changed

site/app/live-editor.tsx

Lines changed: 159 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,26 @@ const CODE_QUERY_KEY = 'c'
1717
// Note: 'default' theme is used internally for SSR/hydration plain text styling and doesn't appear in this list
1818
const SYNTAX_THEMES = ['nord', 'vscode', 'solarized', 'minimal', 'monokai', 'base16']
1919

20+
// Theme color representatives (2 colors per theme)
21+
const THEME_COLORS: Record<string, [string, string]> = {
22+
nord: ['#fabd2f', '#fb4934'], // Gruvbox: yellow and red
23+
vscode: ['#569cd6', '#ce9178'], // VS Code: blue and orange
24+
solarized: ['#c678dd', '#98c379'], // One Dark: purple and green
25+
minimal: ['#b0b0b0', '#808080'], // Minimal: light gray and medium gray
26+
monokai: ['#f92672', '#a6e22e'], // Monokai: pink and green
27+
base16: ['#bb9af7', '#9ece6a'], // Tokyo Night: purple and green
28+
}
29+
30+
function ThemeColorBlocks({ theme }: { theme: string }) {
31+
const colors = THEME_COLORS[theme] || ['#000000', '#000000']
32+
return (
33+
<span className="flex items-center justify-center gap-1">
34+
<span className="w-3 h-3 rounded-sm" style={{ backgroundColor: colors[0] }} />
35+
<span className="w-3 h-3 rounded-sm" style={{ backgroundColor: colors[1] }} />
36+
</span>
37+
)
38+
}
39+
2040
function ControlButton({
2141
id,
2242
checked,
@@ -33,7 +53,7 @@ function ControlButton({
3353
return (
3454
<button className="control-button">
3555
<input id={id} type="checkbox" checked={checked} onChange={(event) => onChange?.(event.target.checked)} />
36-
<label className="controls-manager-label" data-checked={checked} htmlFor={id}>
56+
<label className="controls-manager-label whitespace-nowrap" data-checked={checked} htmlFor={id}>
3757
<span className="mr-2">{prefix ? prefix : checked ? '●' : '○'}</span>
3858
{` ${propName}`}
3959
</label>
@@ -424,85 +444,99 @@ function DropdownMenu({
424444
} & React.HTMLAttributes<HTMLDivElement>) {
425445
const nodeRef = useRef<HTMLDivElement>(null)
426446
const [isOpen, setIsOpen] = useState(false)
447+
const openTimeRef = useRef(0)
427448

428449
// Close the dropdown when clicking outside
429450
useEffect(() => {
430-
const handleClickOutside = (event: MouseEvent) => {
451+
const handlePointerDownOutside = (event: PointerEvent) => {
452+
// Prevent closing immediately after opening (mobile touch issue)
453+
if (Date.now() - openTimeRef.current < 100) {
454+
return
455+
}
456+
431457
const dropdown = nodeRef.current
432458
if (dropdown && !dropdown.contains(event.target as Node)) {
433459
setIsOpen(false)
434460
}
435461
}
436-
document.addEventListener('mousedown', handleClickOutside)
462+
document.addEventListener('pointerdown', handlePointerDownOutside)
437463
return () => {
438-
document.removeEventListener('mousedown', handleClickOutside)
464+
document.removeEventListener('pointerdown', handlePointerDownOutside)
439465
}
440466
}, [])
441467

468+
const arrowRef = useRef<SVGSVGElement>(null)
469+
470+
const handlePointerDown = (e: React.PointerEvent<HTMLButtonElement>) => {
471+
const arrowElement = arrowRef.current
472+
if (!arrowElement) {
473+
const newState = !isOpen
474+
setIsOpen(newState)
475+
if (newState) {
476+
openTimeRef.current = Date.now()
477+
}
478+
return
479+
}
480+
481+
const arrowRect = arrowElement.getBoundingClientRect()
482+
const arrowLeft = arrowRect.left
483+
const clientX = e.clientX
484+
485+
// Check if pointer is on the arrow (right side) - use a wider touch target
486+
const touchPadding = e.pointerType === 'touch' ? 12 : 8 // Extra padding for touch
487+
if (clientX >= arrowLeft - touchPadding) {
488+
// Pointer on arrow area - toggle dropdown
489+
e.preventDefault()
490+
const newState = !isOpen
491+
setIsOpen(newState)
492+
if (newState) {
493+
openTimeRef.current = Date.now()
494+
}
495+
} else if (onNext) {
496+
// Pointer on left side (not arrow) - switch theme
497+
e.preventDefault()
498+
setIsOpen(false)
499+
onNext()
500+
}
501+
}
502+
442503
return (
443504
<div {...props} ref={nodeRef} className={cx('dropdown-menu relative', props.className)}>
444-
<span className={cx('dropdown-menu-button-group', !!onNext && 'dropdown-menu-button-group--merged')}>
445-
<button ref={buttonRef} className="dropdown-menu-button flex-1" onClick={() => setIsOpen(!isOpen)}>
446-
<span className="flex items-center gap-1">
447-
{buttonText}
448-
{/* dropdown indicator */}
449-
<svg
450-
width="14"
451-
height="14"
452-
xmlns="http://www.w3.org/2000/svg"
453-
viewBox="0 0 24 24"
454-
fill="none"
455-
stroke="currentColor"
456-
strokeWidth="2"
457-
className={`ml-auto transition-transform duration-200 ease-out ${isOpen ? 'rotate-180' : ''}`}
458-
>
459-
<path d="M6 9l6 6 6-6" strokeLinecap="round" strokeLinejoin="round" />
460-
</svg>
461-
</span>
462-
</button>
463-
{!!onNext && (
464-
<>
465-
{/* divider */}
466-
<span className="dropdown-menu-divider" />
467-
{/* A button to go to the next */}
468-
469-
<button
470-
className="dropdown-menu-button--next"
471-
onClick={() => {
472-
setIsOpen(false)
473-
onNext()
474-
}}
475-
title="Next theme"
476-
>
477-
{/* next/forward arrow icon */}
478-
<svg
479-
width="12"
480-
height="12"
481-
xmlns="http://www.w3.org/2000/svg"
482-
viewBox="0 0 24 24"
483-
fill="none"
484-
stroke="currentColor"
485-
strokeWidth="2"
486-
className="dropdown-menu-next-icon"
487-
>
488-
<path d="M9 18l6-6-6-6" strokeLinecap="round" strokeLinejoin="round" />
489-
</svg>
490-
</button>
491-
</>
492-
)}
493-
</span>
505+
<button
506+
ref={buttonRef}
507+
className="dropdown-menu-button relative w-full"
508+
onPointerDown={handlePointerDown}
509+
>
510+
<span className="flex items-center gap-1 min-w-0 w-full relative">
511+
<span className="flex-1 flex items-center justify-center">{buttonText}</span>
512+
{/* dropdown indicator */}
513+
<svg
514+
ref={arrowRef}
515+
width="14"
516+
height="14"
517+
xmlns="http://www.w3.org/2000/svg"
518+
viewBox="0 0 24 24"
519+
fill="none"
520+
stroke="currentColor"
521+
strokeWidth="2"
522+
className={`flex-shrink-0 transition-transform duration-200 ease-out pointer-events-none ${isOpen ? 'rotate-180' : ''}`}
523+
>
524+
<path d="M6 9l6 6 6-6" strokeLinecap="round" strokeLinejoin="round" />
525+
</svg>
526+
</span>
527+
</button>
494528
{isOpen && (
495529
<ul className="dropdown-menu-list absolute top-full left-0 w-full min-w-max">
496530
{items.map((item, index) => (
497531
<li
498532
key={index}
499-
className="dropdown-menu-list-item"
533+
className="dropdown-menu-list-item flex items-center gap-2"
500534
onClick={() => {
501535
setIsOpen(false)
502536
onChange(item.text)
503537
}}
504538
>
505-
{item.text}
539+
<ThemeColorBlocks theme={item.text} />
506540
</li>
507541
))}
508542
</ul>
@@ -560,90 +594,77 @@ export function LiveEditor({
560594
return (
561595
<div>
562596
<div className="editor-layout">
563-
<div className="controls flex flex-row md:flex-nowrap mt-8 md:mt-8 mb-4 md:mb-4 items-start md:items-center justify-start md:justify-center">
597+
<div className="controls flex flex-row md:flex-nowrap mt-8 md:mt-8 mb-4 md:mb-4 items-start md:items-center justify-start md:justify-center w-full">
564598
{/* Left controls */}
565-
<div className="inline-flex flex-col gap-y-2 flex-wrap p-4 rounded-lg bg-[var(--app-editor-bg-color)] controls-left-panel">
566-
<div className="controls-manager">
567-
<ControlButton id="control-control" checked={controls} onChange={setControls} text="controls" />
568-
<ControlButton
569-
id="control-line-numbers"
570-
checked={lineNumbers}
571-
onChange={setLineNumbers}
572-
text="line no."
573-
/>
574-
{/* control of theme: light/dark */}
575-
<ControlButton
576-
id="control-theme"
577-
checked={theme === 'dark'}
578-
onChange={(checked) => setTheme(checked ? 'dark' : 'light')}
579-
text={theme === 'dark' ? 'dark' : 'light'}
580-
prefix={<span>{theme === 'dark' ? '🌙' : '☀️'}</span>}
581-
/>
582-
</div>
583-
{/* row */}
584-
<div className="flex flex-wrap gap-2">
585-
<RangeSelector
586-
text="indent"
587-
className="range-control"
588-
value={lineNumbersWidth}
589-
min={2}
590-
max={3}
591-
step={0.1}
592-
onChange={setLineNumbersWidth}
593-
displayValue={false}
594-
/>
595-
{/* selector highlight styling theme */}
596-
<DropdownMenu
597-
className="dropdown-menu-highlight w-36"
598-
buttonRef={themeButtonRef}
599-
buttonText={
600-
<>
601-
<span className="mr-2">🎨</span>
602-
<span>{highlightTheme}</span>
603-
</>
599+
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 p-4 rounded-lg bg-[var(--app-editor-bg-color)] controls-left-panel flex-1">
600+
<ControlButton id="control-control" checked={controls} onChange={setControls} text="controls" />
601+
<ControlButton
602+
id="control-line-numbers"
603+
checked={lineNumbers}
604+
onChange={setLineNumbers}
605+
text="line no."
606+
/>
607+
<ControlButton
608+
id="control-theme"
609+
checked={theme === 'dark'}
610+
onChange={(checked) => setTheme(checked ? 'dark' : 'light')}
611+
text={theme === 'dark' ? 'dark' : 'light'}
612+
prefix={<span>{theme === 'dark' ? '🌙' : '☀️'}</span>}
613+
/>
614+
<RangeSelector
615+
text="indent"
616+
className="range-control"
617+
value={lineNumbersWidth}
618+
min={2}
619+
max={3}
620+
step={0.1}
621+
onChange={setLineNumbersWidth}
622+
displayValue={false}
623+
/>
624+
<DropdownMenu
625+
className="dropdown-menu-highlight"
626+
buttonRef={themeButtonRef}
627+
buttonText={<ThemeColorBlocks theme={highlightTheme} />}
628+
items={SYNTAX_THEMES.map((theme) => ({ text: theme }))}
629+
onChange={(text) => {
630+
setHighlightTheme(text)
631+
}}
632+
onNext={() => {
633+
const nextIndex = SYNTAX_THEMES.indexOf(highlightTheme) + 1
634+
const nextTheme = SYNTAX_THEMES[nextIndex % SYNTAX_THEMES.length]
635+
setHighlightTheme(nextTheme)
636+
}}
637+
/>
638+
<ControlButton
639+
id="control-format"
640+
checked
641+
onChange={async () => {
642+
setFormat(!_f)
643+
setIsFormatting(true)
644+
try {
645+
const [prettierPluginBabel, prettierPluginEstree] = await Promise.all([
646+
import('prettier/plugins/babel'),
647+
import('prettier/plugins/estree'),
648+
])
649+
const formattedCode = await prettierFormat(code, {
650+
parser: 'babel',
651+
plugins: [prettierPluginBabel.default, prettierPluginEstree.default],
652+
printWidth: 120,
653+
singleQuote: true,
654+
trailingComma: 'es5',
655+
semi: false,
656+
tabWidth: 2,
657+
})
658+
setCode(formattedCode)
659+
} catch (error) {
660+
console.error('Formatting error:', error)
661+
} finally {
662+
setTimeout(() => setIsFormatting(false), 600)
604663
}
605-
items={SYNTAX_THEMES.map((theme) => ({ text: theme }))}
606-
onChange={(text) => {
607-
setHighlightTheme(text)
608-
}}
609-
onNext={() => {
610-
const nextIndex = SYNTAX_THEMES.indexOf(highlightTheme) + 1
611-
const nextTheme = SYNTAX_THEMES[nextIndex % SYNTAX_THEMES.length]
612-
setHighlightTheme(nextTheme)
613-
}}
614-
/>
615-
616-
<ControlButton
617-
id="control-format"
618-
checked
619-
onChange={async () => {
620-
setFormat(!_f)
621-
setIsFormatting(true)
622-
try {
623-
const [prettierPluginBabel, prettierPluginEstree] = await Promise.all([
624-
import('prettier/plugins/babel'),
625-
import('prettier/plugins/estree'),
626-
])
627-
const formattedCode = await prettierFormat(code, {
628-
parser: 'babel',
629-
plugins: [prettierPluginBabel.default, prettierPluginEstree.default],
630-
printWidth: 120,
631-
singleQuote: true,
632-
trailingComma: 'es5',
633-
semi: false,
634-
tabWidth: 2,
635-
})
636-
setCode(formattedCode)
637-
} catch (error) {
638-
console.error('Formatting error:', error)
639-
} finally {
640-
setTimeout(() => setIsFormatting(false), 600)
641-
}
642-
}}
643-
text="format"
644-
prefix={<FormatIcon width={14} height={14} stroke="currentColor" strokeWidth="2" className={isFormatting ? 'mop-animate' : ''} />}
645-
/>
646-
</div>
664+
}}
665+
text="format"
666+
prefix={<FormatIcon width={14} height={14} stroke="currentColor" strokeWidth="2" className={isFormatting ? 'mop-animate' : ''} />}
667+
/>
647668
</div>
648669
{/* Right side screenshot button */}
649670
<div className="inline-flex items-center self-stretch gap-2 flex-shrink-0">

site/app/styles.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,14 +464,14 @@ input[type='radio'] {
464464
position: relative;
465465
display: inline-block;
466466
padding: 0.2rem 0.8rem;
467-
padding-right: 0.2rem;
468467
border-radius: 88px;
469468
background-color: var(--control-bg-color);
470469
color: var(--control-color);
471470
font-size: 14px;
472471
font-weight: 600;
473472
cursor: pointer;
474473
transition: background-color 0.2s ease-in-out;
474+
line-height: 1.5;
475475
}
476476
/* virtual divider of two merged buttons */
477477
.dropdown-menu .dropdown-menu-divider {

0 commit comments

Comments
 (0)