goldenChat base source add

This commit is contained in:
aidev
2026-05-23 15:11:48 +09:00
commit a4ea7762b5
2081 changed files with 1155760 additions and 0 deletions
+134
View File
@@ -0,0 +1,134 @@
/**
* 시간대 선택 — 하단 바 클릭·설정 화면 공용
*/
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { TIMEZONE_OPTIONS, formatUnixClock, getTimezoneAbbr, normalizeTimezone } from '../utils/timezone';
interface Props {
value: string;
onChange: (tz: string) => void;
/** compact: 하단 바용 버튼 스타일 */
variant?: 'bar' | 'select';
/** 하단 바: 마지막 봉 시각 표시 (unix 초) */
displayUnix?: number;
className?: string;
}
const TimezonePicker: React.FC<Props> = ({ value, onChange, variant = 'bar', displayUnix, className = '' }) => {
const [open, setOpen] = useState(false);
const [menuPos, setMenuPos] = useState<{ bottom: number; right: number } | null>(null);
const rootRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLButtonElement>(null);
const menuRef = useRef<HTMLUListElement>(null);
const tz = normalizeTimezone(value);
const updateMenuPos = useCallback(() => {
const btn = triggerRef.current;
if (!btn) return;
const r = btn.getBoundingClientRect();
setMenuPos({
bottom: window.innerHeight - r.top + 4,
right: Math.max(8, window.innerWidth - r.right),
});
}, []);
useLayoutEffect(() => {
if (!open || variant !== 'bar') return;
updateMenuPos();
const onResize = () => updateMenuPos();
window.addEventListener('resize', onResize);
window.addEventListener('scroll', onResize, true);
return () => {
window.removeEventListener('resize', onResize);
window.removeEventListener('scroll', onResize, true);
};
}, [open, variant, updateMenuPos]);
useEffect(() => {
if (!open) return;
const onDoc = (e: MouseEvent) => {
const t = e.target as Node;
if (rootRef.current?.contains(t) || menuRef.current?.contains(t)) return;
setOpen(false);
};
const id = window.setTimeout(() => {
document.addEventListener('mousedown', onDoc);
}, 0);
return () => {
window.clearTimeout(id);
document.removeEventListener('mousedown', onDoc);
};
}, [open]);
if (variant === 'select') {
return (
<select
className={`stg-select ${className}`}
value={tz}
onChange={e => onChange(e.target.value)}
>
{TIMEZONE_OPTIONS.map(o => (
<option key={o.id} value={o.id}>{o.label}</option>
))}
</select>
);
}
const menu = open && variant === 'bar' && menuPos && (
<ul
ref={menuRef}
className="tz-picker-menu tz-picker-menu--portal"
role="listbox"
style={{ bottom: menuPos.bottom, right: menuPos.right }}
>
{TIMEZONE_OPTIONS.map(o => (
<li key={o.id}>
<button
type="button"
role="option"
aria-selected={o.id === tz}
className={`tz-picker-item${o.id === tz ? ' active' : ''}`}
onClick={() => {
onChange(o.id);
setOpen(false);
}}
>
<span className="tz-picker-item-label">{o.label}</span>
<span className="tz-picker-item-abbr">{o.abbr}</span>
</button>
</li>
))}
</ul>
);
return (
<div className={`tz-picker ${className}`} ref={rootRef}>
<button
ref={triggerRef}
type="button"
className="tv-time-display tz-picker-trigger"
onClick={() => {
if (open) {
setOpen(false);
return;
}
updateMenuPos();
setOpen(true);
}}
title="시간대 변경"
aria-expanded={open}
aria-haspopup="listbox"
>
{displayUnix != null && displayUnix > 0 && (
<span className="tz-picker-clock">{formatUnixClock(displayUnix, tz, true)}</span>
)}
<span className="tz-picker-abbr">{getTimezoneAbbr(tz)}</span>
<span className="tz-picker-caret"></span>
</button>
{menu && createPortal(menu, document.body)}
</div>
);
};
export default TimezonePicker;