Drag-and-drop UX rewrite: - The old "highlight target row + margin-grow animation" approach was driven by per-row dragenter/dragleave events. Those fire in a noisy enter/leave/enter cascade as the pointer crosses sub-elements and as the row itself grows under the pointer, which is why the gap was pulsing open/closed. - New approach: a single container-level dragover handler. On dragstart the source row is briefly cloned into a translucent "placeholder" element (dashed outline, 45% opacity, pointer-events:none) inserted in the source's slot; the original is then hidden (display:none) right after dragstart so the browser can still capture it as the drag image. As the cursor moves over the container we compute which sibling's midpoint the pointer just crossed and insertBefore the placeholder accordingly. The list length stays constant the whole time, so there is no growing/shrinking gap to fight with — what the user sees is the dragged item itself shown semi-transparently at the exact drop slot. On drop, splice the array using the placeholder's index among the non-source children, then re-render. - The bindContainerDnd helper handles both lists; image grid uses vertical Y math (same midpoint rule as the track list since cards flow row-by-row in the auto-fill grid). attachDraggable now only sets up dragstart/dragend/contextmenu per row; no more dragenter/dragleave. Image grid: - Image cards now have a caption below the thumbnail. When the same URL appears in the music list, the music entry's title/artist are borrowed via captionForImage(url); otherwise "(제목 없음)" muted text. Layout changed from a square aspect-ratio card to a flex column: .imgWrap holds the square thumbnail, .cardCaption sits underneath with single-line title + smaller muted artist line. CSS cleanup: - Drop the old .dropAbove margin-grow rules and .dragOver border rule on .trackRow/.imageCard. Replaced with .dragPlaceholder + .hiddenWhileDragging. - .imageCard no longer uses aspect-ratio on itself; aspect lives on .imgWrap so caption can extend the card vertically.
548 lines
13 KiB
CSS
548 lines
13 KiB
CSS
:root {
|
|
color-scheme: dark;
|
|
--bg: #0d1117;
|
|
--bg-alt: #161b22;
|
|
--bg-card: #1f242c;
|
|
--border: #30363d;
|
|
--text: #e6edf3;
|
|
--text-muted: #8b949e;
|
|
--accent: #2f81f7;
|
|
--accent-hover: #1f6feb;
|
|
--danger: #f85149;
|
|
--success: #3fb950;
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
html, body { margin: 0; padding: 0; }
|
|
|
|
body.siteBody {
|
|
font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
min-height: 100vh;
|
|
}
|
|
|
|
body.siteBody.centerLayout {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.pageWrap {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 32px 24px 80px;
|
|
}
|
|
|
|
.topNav {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 16px 32px;
|
|
background: var(--bg-alt);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.navBrand {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
text-decoration: none;
|
|
color: inherit;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.navLogo { font-size: 22px; }
|
|
.navTitle { font-size: 16px; }
|
|
|
|
.navUser { position: relative; }
|
|
|
|
.navUserButton {
|
|
background: transparent;
|
|
border: 1px solid var(--border);
|
|
color: var(--text);
|
|
padding: 8px 14px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.navUserButton:hover { background: var(--bg-card); }
|
|
|
|
.navUserMenu {
|
|
position: absolute;
|
|
right: 0;
|
|
top: calc(100% + 6px);
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
padding: 8px;
|
|
min-width: 160px;
|
|
box-shadow: 0 12px 24px rgba(0,0,0,0.4);
|
|
z-index: 10;
|
|
}
|
|
|
|
.navUserMenu form { margin: 0; }
|
|
|
|
.dangerLink {
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--danger);
|
|
cursor: pointer;
|
|
padding: 6px 8px;
|
|
font-size: 14px;
|
|
width: 100%;
|
|
text-align: left;
|
|
}
|
|
|
|
.dangerLink:hover { background: rgba(248, 81, 73, 0.1); border-radius: 6px; }
|
|
|
|
.hero h1 { margin: 0 0 8px; font-size: 30px; }
|
|
.hero p { color: var(--text-muted); margin: 0 0 32px; }
|
|
|
|
.muted { color: var(--text-muted); font-size: 13px; }
|
|
|
|
.cardRow {
|
|
display: flex;
|
|
gap: 16px;
|
|
flex-wrap: nowrap;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.horizontalScroll {
|
|
overflow-x: auto;
|
|
padding-bottom: 12px;
|
|
}
|
|
|
|
.packCard {
|
|
flex: 0 0 280px;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 14px;
|
|
padding: 18px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
position: relative;
|
|
}
|
|
|
|
.packCard h2 { margin: 0; font-size: 18px; }
|
|
|
|
.metaList {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 8px 0 0;
|
|
font-size: 13px;
|
|
color: var(--text-muted);
|
|
display: grid;
|
|
gap: 4px;
|
|
}
|
|
|
|
.cardLink {
|
|
text-decoration: none;
|
|
color: inherit;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.cardLink[data-disabled="true"] { pointer-events: none; opacity: 0.85; }
|
|
|
|
.cardCheckbox {
|
|
position: absolute;
|
|
top: 12px;
|
|
right: 12px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
background: rgba(0,0,0,0.4);
|
|
padding: 4px 8px;
|
|
border-radius: 8px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.dashboardHeader {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 24px;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
}
|
|
|
|
.dashboardHeader > div { min-width: 0; }
|
|
.dashboardHeader h1 { margin: 0; font-size: 24px; }
|
|
|
|
.dashboardActions {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
margin-left: auto;
|
|
align-items: center;
|
|
}
|
|
|
|
.inlineForm { margin: 0; }
|
|
|
|
.deleteConfirmRow {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 8px;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.primaryButton {
|
|
background: var(--accent);
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 18px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
text-decoration: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.primaryButton:hover { background: var(--accent-hover); }
|
|
|
|
.secondaryButton {
|
|
background: var(--bg-card);
|
|
color: var(--text);
|
|
border: 1px solid var(--border);
|
|
padding: 10px 18px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
text-decoration: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.secondaryButton:hover { border-color: var(--accent); }
|
|
|
|
.dangerButton {
|
|
background: var(--danger);
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 18px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
text-decoration: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.dangerButton:hover { background: #d73a48; }
|
|
|
|
.ghostLink {
|
|
color: var(--text-muted);
|
|
text-decoration: none;
|
|
border: 1px solid var(--border);
|
|
padding: 8px 14px;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.ghostLink:hover { color: var(--text); border-color: var(--accent); }
|
|
|
|
.editorHeader {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.eyebrow {
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.16em;
|
|
font-size: 11px;
|
|
color: var(--accent);
|
|
margin: 0 0 4px;
|
|
}
|
|
|
|
.editorHeader h1 { margin: 0; font-size: 24px; }
|
|
|
|
.editorForm {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 24px;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 14px;
|
|
padding: 24px;
|
|
}
|
|
|
|
.gridTwo {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 16px;
|
|
}
|
|
|
|
.gridTwo > .fullSpan { grid-column: span 2; }
|
|
|
|
.editorForm label {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.editorForm input,
|
|
.editorForm select,
|
|
.editorForm textarea {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
border: 1px solid var(--border);
|
|
padding: 10px 12px;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.editorForm input:focus,
|
|
.editorForm select:focus,
|
|
.editorForm textarea:focus {
|
|
outline: none;
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.dynamicListFieldset {
|
|
border: 1px dashed var(--border);
|
|
border-radius: 12px;
|
|
padding: 16px;
|
|
}
|
|
|
|
.dynamicListFieldset legend { padding: 0 8px; color: var(--text-muted); }
|
|
|
|
.dynamicList {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.dynamicRow {
|
|
display: grid;
|
|
grid-template-columns: 1fr 2fr auto;
|
|
gap: 8px;
|
|
align-items: center;
|
|
}
|
|
|
|
.loginCard {
|
|
width: 360px;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 16px;
|
|
padding: 32px;
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.45);
|
|
}
|
|
|
|
.loginCard h1 { margin: 0 0 16px; font-size: 22px; }
|
|
|
|
.loginForm {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14px;
|
|
}
|
|
|
|
.loginForm label {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.loginForm input {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
border: 1px solid var(--border);
|
|
padding: 10px 12px;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.errorBanner {
|
|
background: rgba(248, 81, 73, 0.15);
|
|
color: var(--danger);
|
|
border: 1px solid rgba(248, 81, 73, 0.4);
|
|
padding: 10px 12px;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
margin: 0 0 14px;
|
|
}
|
|
|
|
/* ── /op/list, /op/list/:pack, /op/datapack ────────────── */
|
|
|
|
.tabBar { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 20px; }
|
|
.tabBtn {
|
|
background: transparent; border: none; color: var(--text-muted);
|
|
padding: 10px 18px; cursor: pointer; font-size: 14px;
|
|
border-bottom: 2px solid transparent;
|
|
}
|
|
.tabBtn:hover { color: var(--text); }
|
|
.tabBtn.active { color: var(--text); border-bottom-color: var(--accent); }
|
|
|
|
.tabPanel { display: block; }
|
|
.tabPanel[hidden] { display: none !important; }
|
|
|
|
.listActionsRow { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; flex-wrap: wrap; }
|
|
.statusText { font-size: 13px; color: var(--text-muted); margin-left: 8px; }
|
|
.statusText.error { color: var(--danger); }
|
|
|
|
.playlistRow { display: flex; gap: 8px; margin-bottom: 16px; }
|
|
.textInput {
|
|
flex: 1; background: var(--bg); color: var(--text);
|
|
border: 1px solid var(--border); padding: 10px 12px; border-radius: 8px;
|
|
font-size: 14px;
|
|
}
|
|
.textInput:focus { outline: none; border-color: var(--accent); }
|
|
|
|
/* 음악 행 */
|
|
.trackList { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
.trackRow {
|
|
display: grid;
|
|
grid-template-columns: 36px 80px 1fr auto;
|
|
gap: 12px; align-items: center;
|
|
padding: 8px 12px; background: var(--bg-card);
|
|
border: 1px solid var(--border); border-radius: 8px;
|
|
cursor: grab; user-select: none;
|
|
}
|
|
.rowNum { color: var(--text-muted); font-size: 14px; text-align: center; }
|
|
.rowThumb { width: 80px; height: 45px; object-fit: cover; border-radius: 4px; background: #000; }
|
|
.rowMeta { min-width: 0; }
|
|
.rowTitle {
|
|
font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
outline: none; border-radius: 4px; padding: 2px 4px; margin: -2px -4px;
|
|
}
|
|
.rowSub {
|
|
font-size: 12px; color: var(--text-muted); margin-top: 2px;
|
|
outline: none; border-radius: 4px; padding: 2px 4px;
|
|
}
|
|
.rowTitle[contenteditable="true"]:hover,
|
|
.rowSub[contenteditable="true"]:hover { background: rgba(255,255,255,0.04); }
|
|
.rowTitle[contenteditable="true"]:focus,
|
|
.rowSub[contenteditable="true"]:focus {
|
|
background: var(--bg);
|
|
box-shadow: 0 0 0 1px var(--accent);
|
|
white-space: normal; cursor: text;
|
|
}
|
|
.rowTitle[contenteditable="true"]:empty::before,
|
|
.rowSub[contenteditable="true"]:empty::before {
|
|
content: attr(data-placeholder);
|
|
color: var(--text-muted);
|
|
opacity: 0.6;
|
|
}
|
|
.rowDur { color: var(--text-muted); font-size: 13px; }
|
|
|
|
/* 드래그 시스템 공통: 원본은 잠시 숨기고, 같은 모양의 placeholder 가 들어갈 자리에서 반투명하게 보임 */
|
|
.hiddenWhileDragging { display: none !important; }
|
|
.dragPlaceholder {
|
|
opacity: 0.45;
|
|
pointer-events: none;
|
|
outline: 2px dashed var(--accent);
|
|
outline-offset: -2px;
|
|
background: rgba(47, 129, 247, 0.08);
|
|
}
|
|
.dragPlaceholder * { pointer-events: none !important; }
|
|
|
|
/* 사진 그리드 */
|
|
.imageGrid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
gap: 12px;
|
|
}
|
|
.imageCard {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border); border-radius: 10px;
|
|
overflow: hidden; cursor: grab; user-select: none;
|
|
display: flex; flex-direction: column;
|
|
}
|
|
.imageCard .imgWrap {
|
|
position: relative; aspect-ratio: 1 / 1; overflow: hidden;
|
|
}
|
|
.imageCard img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
.cardNum {
|
|
position: absolute; top: 6px; left: 6px;
|
|
background: rgba(0,0,0,0.7); color: #fff;
|
|
padding: 2px 8px; border-radius: 999px;
|
|
font-size: 12px; font-weight: 600;
|
|
}
|
|
.cardCaption {
|
|
padding: 8px 10px;
|
|
border-top: 1px solid var(--border);
|
|
background: var(--bg-card);
|
|
}
|
|
.cardTitle {
|
|
font-size: 13px; color: var(--text);
|
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
}
|
|
.cardSub {
|
|
font-size: 11px; color: var(--text-muted);
|
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
margin-top: 2px;
|
|
}
|
|
.cardTitle .muted { color: var(--text-muted); }
|
|
|
|
/* 컨텍스트 메뉴 */
|
|
.ctxMenu {
|
|
position: absolute; z-index: 200;
|
|
background: var(--bg-card); border: 1px solid var(--border);
|
|
border-radius: 8px; padding: 4px; min-width: 120px;
|
|
box-shadow: 0 12px 24px rgba(0,0,0,0.5);
|
|
}
|
|
.ctxMenu button {
|
|
display: block; width: 100%; text-align: left;
|
|
background: transparent; border: none; color: var(--text);
|
|
padding: 8px 12px; cursor: pointer; font-size: 13px; border-radius: 4px;
|
|
}
|
|
.ctxMenu button:hover { background: var(--bg); }
|
|
|
|
/* 모달 (음악퀴즈 인스톨러의 modalOverlay 와 호환) */
|
|
.modalOverlay {
|
|
position: fixed; inset: 0; background: rgba(0,0,0,0.6);
|
|
display: flex; align-items: center; justify-content: center; z-index: 1000;
|
|
}
|
|
.modalOverlay[hidden] { display: none; }
|
|
.modalCard {
|
|
background: var(--bg-alt); border: 1px solid var(--border);
|
|
border-radius: 12px; width: min(560px, 92vw); max-height: 86vh;
|
|
display: grid; grid-template-rows: auto 1fr auto; overflow: hidden;
|
|
}
|
|
.modalCard > header {
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
padding: 12px 16px; border-bottom: 1px solid var(--border);
|
|
}
|
|
.modalCard > header h3 { margin: 0; font-size: 16px; }
|
|
.modalCard > footer { padding: 12px 16px; border-top: 1px solid var(--border); }
|
|
.modalClose { background: transparent; border: none; color: var(--text-muted); font-size: 22px; cursor: pointer; }
|
|
.modalClose:hover { color: var(--text); }
|
|
.modalBody { padding: 16px; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; }
|
|
.modalBody label { display: flex; flex-direction: column; gap: 6px; font-size: 13px; color: var(--text-muted); }
|
|
|
|
/* 토글 버튼 (segmented) */
|
|
.segmentedRow { display: flex; gap: 4px; }
|
|
.segBtn {
|
|
background: var(--bg-card); border: 1px solid var(--border); color: var(--text-muted);
|
|
padding: 8px 14px; border-radius: 8px; cursor: pointer; font-size: 13px;
|
|
}
|
|
.segBtn.active { border-color: var(--accent); color: var(--text); background: rgba(47,129,247,0.15); }
|
|
|
|
/* 데이터팩 페이지 */
|
|
.dpControls { display: flex; gap: 12px; align-items: center; margin-bottom: 12px; }
|
|
.dpActions { display: flex; gap: 8px; align-items: center; margin: 12px 0; }
|
|
.codeBlock {
|
|
background: var(--bg-card); border: 1px solid var(--border);
|
|
border-radius: 10px; padding: 14px 16px; overflow-x: auto;
|
|
font-family: 'Consolas','SFMono-Regular',monospace; font-size: 13px;
|
|
white-space: pre-wrap; word-break: break-word;
|
|
max-height: 60vh; overflow-y: auto;
|
|
}
|
|
.packCard.pickable { cursor: pointer; }
|
|
.packCard.pickable:hover { border-color: var(--accent); }
|