refactor: 공용 폼 모달 BaseModal + 전역 .form-* 필드 클래스 (Phase 1a)

- components/common/BaseModal.vue 신설: backdrop+헤더(제목/닫기)+본문/푸터 슬롯,
  Teleport·ESC·바깥클릭 닫기 내장. 전역 .modal* 와 충돌 없게 bm- 접두 클래스 사용
- relay.css 에 .form-field/-label/-row/-input/-textarea/-select(+wrap/chev) 표준 추가
- 일정·카테고리 모달을 BaseModal + .form-* 로 전환(중복 모달/필드 CSS 제거,
  푸터 버튼은 전역 .mbtn 사용)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 23:35:33 +09:00
parent 92ed6c332f
commit 5f8b2597bf
4 changed files with 466 additions and 548 deletions
+84
View File
@@ -569,6 +569,90 @@ body {
color: var(--text-3);
}
/* ============================================================
* 폼 필드 (폼 모달 공통 — BaseModal 본문 등에서 사용)
* ============================================================ */
.form-field {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-label {
font-size: 0.781rem;
font-weight: 600;
color: var(--text-2);
}
.form-row {
display: flex;
gap: 0.75rem;
}
.form-row .form-field {
flex: 1;
min-width: 0;
}
.form-input {
width: 100%;
height: 2.5rem;
padding: 0 0.75rem;
border: 1px solid var(--border-strong);
border-radius: var(--radius);
font-family: inherit;
font-size: 0.844rem;
color: var(--text);
background: #fff;
}
.form-input::placeholder {
color: var(--text-3);
}
.form-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-weak);
}
.form-textarea {
height: auto;
min-height: 5.75rem;
padding: 0.625rem 0.75rem;
line-height: 1.5;
resize: vertical;
}
/* 네이티브 select 래퍼 + 셰브론 */
.form-select-wrap {
position: relative;
}
.form-select {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 2.5rem;
padding: 0 2.125rem 0 0.75rem;
border: 1px solid var(--border-strong);
border-radius: var(--radius);
font-family: inherit;
font-size: 0.844rem;
color: var(--text);
background: #fff;
cursor: pointer;
}
.form-select:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-weak);
}
.form-select-chev {
position: absolute;
right: 0.625rem;
top: 50%;
transform: translateY(-50%);
display: grid;
color: var(--text-3);
pointer-events: none;
}
.form-select-chev svg {
width: 0.9375rem;
height: 0.9375rem;
}
/* ============================================================
* 더보기 버튼 (목록 페이지네이션 공통)
* ============================================================ */
@@ -0,0 +1,162 @@
<script setup lang="ts">
// 공용 폼 모달 셸 — backdrop + 헤더(제목/닫기) + 본문 슬롯 + 푸터 슬롯.
// Teleport(body) · ESC · 바깥(backdrop) 클릭 닫기 내장.
// 전역 .modal*(확인 다이얼로그 전용)과 충돌하지 않도록 bm- 접두 클래스를 쓴다.
// 폼 필드는 전역 .form-* 클래스(relay.css)를 본문 슬롯에서 사용한다.
import { onBeforeUnmount, onMounted } from 'vue'
withDefaults(
defineProps<{
title: string
// 너비: md(기본 33.75rem) | sm(27.5rem)
size?: 'md' | 'sm'
}>(),
{ size: 'md' },
)
const emit = defineEmits<{ (e: 'close'): void }>()
function onKey(e: KeyboardEvent): void {
if (e.key === 'Escape') emit('close')
}
onMounted(() => document.addEventListener('keydown', onKey))
onBeforeUnmount(() => document.removeEventListener('keydown', onKey))
</script>
<template>
<Teleport to="body">
<div
class="bm-back"
@mousedown.self="emit('close')"
>
<div
class="bm"
:class="{ 'bm-sm': size === 'sm' }"
role="dialog"
aria-modal="true"
>
<div class="bm-head">
<h3>{{ title }}</h3>
<button
type="button"
class="bm-x"
aria-label="닫기"
@click="emit('close')"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.2"
stroke-linecap="round"
><path d="M18 6 6 18M6 6l12 12" /></svg>
</button>
</div>
<div class="bm-body">
<slot />
</div>
<div
v-if="$slots.foot"
class="bm-foot"
>
<slot name="foot" />
</div>
</div>
</div>
</Teleport>
</template>
<style scoped>
.bm-back {
position: fixed;
inset: 0;
z-index: 100;
background: rgba(20, 24, 33, 0.5);
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
}
.bm {
width: 100%;
max-width: 33.75rem;
max-height: 90vh;
overflow-y: auto;
background: #fff;
border-radius: 0.875rem;
box-shadow: 0 24px 64px rgba(20, 24, 33, 0.32);
display: flex;
flex-direction: column;
}
.bm.bm-sm {
max-width: 27.5rem;
}
.bm-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.375rem 1.5rem 1rem;
position: sticky;
top: 0;
background: #fff;
border-radius: 0.875rem 0.875rem 0 0;
z-index: 1;
}
.bm-head h3 {
font-size: 1.0625rem;
font-weight: 700;
letter-spacing: -0.01875rem;
}
.bm-x {
width: 2rem;
height: 2rem;
border: none;
background: transparent;
color: var(--text-3);
border-radius: var(--radius-sm);
cursor: pointer;
display: grid;
place-items: center;
margin-right: -0.3125rem;
}
.bm-x:hover {
background: #f1f2f4;
color: var(--text);
}
.bm-x svg {
width: 1.125rem;
height: 1.125rem;
}
.bm-body {
padding: 0.5rem 1.5rem 0.75rem;
display: flex;
flex-direction: column;
gap: 1.125rem;
text-align: left;
}
.bm-foot {
display: flex;
justify-content: flex-end;
gap: 0.5625rem;
padding: 1rem 1.5rem;
border-top: 1px solid var(--border);
background: #fafbfc;
position: sticky;
bottom: 0;
border-radius: 0 0 0.875rem 0.875rem;
}
/* 푸터 버튼 — 전역 .mbtn(확인 다이얼로그용 flex:1) 대신 폼 모달용 우측정렬 컴팩트 */
.bm-foot :deep(.mbtn) {
flex: 0 0 auto;
height: 2.375rem;
padding: 0 1rem;
}
.bm-foot :deep(.mbtn:disabled) {
opacity: 0.45;
cursor: not-allowed;
}
.bm-foot :deep(.mbtn.primary:disabled),
.bm-foot :deep(.mbtn.primary:disabled:hover) {
background: var(--accent);
border-color: var(--accent);
}
</style>
@@ -1,6 +1,7 @@
<script setup lang="ts">
// 카테고리 생성/수정 모달 — 이름 + 색상(hue 슬라이더)
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { computed, ref } from 'vue'
import BaseModal from '@/components/common/BaseModal.vue'
import type {
ApiScheduleCategory,
ScheduleCategoryPayload,
@@ -57,181 +58,60 @@ function submit(): void {
if (!valid.value) return
emit('save', { label: name.value.trim(), color: color.value })
}
function onKey(ev: KeyboardEvent): void {
if (ev.key === 'Escape') emit('close')
}
onMounted(() => document.addEventListener('keydown', onKey))
onBeforeUnmount(() => document.removeEventListener('keydown', onKey))
</script>
<template>
<div
class="modal-back"
@mousedown.self="emit('close')"
<BaseModal
:title="isEdit ? '카테고리 편집' : '새 카테고리'"
size="sm"
@close="emit('close')"
>
<div class="modal modal-sm">
<div class="modal-head">
<h3>{{ isEdit ? '카테고리 편집' : '새 카테고리' }}</h3>
<button
class="modal-x"
@click="emit('close')"
<label class="form-field"><span class="form-label">이름</span>
<input
v-model="name"
class="form-input"
placeholder="카테고리 이름"
autofocus
@keydown.enter="submit"
>
</label>
<div class="form-field">
<span class="form-label">색상</span>
<div class="cat-color">
<span
class="cat-dot"
:style="{ background: color }"
/>
<input
v-model.number="hue"
type="range"
min="0"
max="359"
class="hue-slider"
:style="{ '--cur': color }"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.2"
stroke-linecap="round"
><path d="M18 6 6 18M6 6l12 12" /></svg>
</button>
</div>
<div class="modal-body">
<label class="f-field"><span class="f-label">이름</span>
<input
v-model="name"
class="f-input"
placeholder="카테고리 이름"
autofocus
@keydown.enter="submit"
>
</label>
<div class="f-field">
<span class="f-label">색상</span>
<div class="cat-color">
<span
class="cat-dot"
:style="{ background: color }"
/>
<input
v-model.number="hue"
type="range"
min="0"
max="359"
class="hue-slider"
:style="{ '--cur': color }"
>
</div>
</div>
</div>
<div class="modal-foot">
<button
class="mbtn"
@click="emit('close')"
>
취소
</button>
<button
class="mbtn primary"
:disabled="!valid"
@click="submit"
>
{{ isEdit ? '저장' : '추가' }}
</button>
</div>
</div>
</div>
<template #foot>
<button
class="mbtn"
@click="emit('close')"
>
취소
</button>
<button
class="mbtn primary"
:disabled="!valid"
@click="submit"
>
{{ isEdit ? '저장' : '추가' }}
</button>
</template>
</BaseModal>
</template>
<style scoped>
.modal-back {
position: fixed;
inset: 0;
z-index: 100;
background: rgba(20, 24, 33, 0.5);
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
}
.modal {
width: 100%;
max-width: 33.75rem;
background: #fff;
border-radius: 0.875rem;
box-shadow: 0 24px 64px rgba(20, 24, 33, 0.32);
display: flex;
flex-direction: column;
}
.modal.modal-sm {
max-width: 27.5rem;
}
.modal-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.375rem 1.5rem 1rem;
}
.modal-head h3 {
font-size: 1.0625rem;
font-weight: 700;
letter-spacing: -0.01875rem;
}
.modal-x {
width: 2rem;
height: 2rem;
border: none;
background: transparent;
color: var(--text-3);
border-radius: var(--radius-sm);
cursor: pointer;
display: grid;
place-items: center;
margin-right: -0.3125rem;
}
.modal-x:hover {
background: #f1f2f4;
color: var(--text);
}
.modal-x svg {
width: 1.125rem;
height: 1.125rem;
}
.modal-body {
padding: 0.5rem 1.5rem 0.75rem;
display: flex;
flex-direction: column;
gap: 1.125rem;
/* 전역 .modal-body(확인 다이얼로그용)의 가운데 정렬을 폼 모달에선 왼쪽으로 */
text-align: left;
}
.modal-foot {
display: flex;
justify-content: flex-end;
gap: 0.5625rem;
padding: 1rem 1.5rem;
border-top: 1px solid var(--border);
background: #fafbfc;
border-radius: 0 0 0.875rem 0.875rem;
}
.f-field {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.f-label {
font-size: 0.781rem;
font-weight: 600;
color: var(--text-2);
}
.f-input {
width: 100%;
height: 2.5rem;
padding: 0 0.75rem;
border: 1px solid var(--border-strong);
border-radius: var(--radius);
font-family: inherit;
font-size: 0.844rem;
color: var(--text);
background: #fff;
}
.f-input::placeholder {
color: var(--text-3);
}
.f-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-weak);
}
.cat-color {
display: flex;
align-items: center;
@@ -284,20 +164,4 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKey))
box-shadow: 0 1px 4px rgba(20, 24, 33, 0.4);
cursor: grab;
}
/* 푸터 버튼 — 전역 .mbtn(확인 다이얼로그용 flex:1) 대신 폼 모달용 우측정렬 컴팩트(멤버 초대 모달과 동일).
색/서체/테두리는 전역 .mbtn / .mbtn.primary 가 담당한다. */
.mbtn {
flex: 0 0 auto;
height: 2.375rem;
padding: 0 1rem;
}
.mbtn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.mbtn.primary:disabled,
.mbtn.primary:disabled:hover {
background: var(--accent);
border-color: var(--accent);
}
</style>
@@ -1,6 +1,7 @@
<script setup lang="ts">
// 일정 생성/수정 모달
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import BaseModal from '@/components/common/BaseModal.vue'
import { useClickOutside } from '@/composables/useClickOutside'
import ScheduleAvatar from './ScheduleAvatar.vue'
import type {
@@ -91,16 +92,9 @@ function togglePerson(id: string): void {
? attendeeIds.value.filter((x) => x !== id)
: [...attendeeIds.value, id]
}
// 참석자 멀티셀렉트 바깥클릭 닫기(ESC 는 모달 닫기에서 처리하므로 제외)
// 참석자 멀티셀렉트 바깥클릭 닫기(ESC 는 모달 닫기에서 처리)
useClickOutside(msOpen, () => (msOpen.value = false), [msRef], { esc: false })
// 모달 ESC = 닫기
function onKey(ev: KeyboardEvent): void {
if (ev.key === 'Escape') emit('close')
}
onMounted(() => document.addEventListener('keydown', onKey))
onBeforeUnmount(() => document.removeEventListener('keydown', onKey))
function submit(): void {
if (!valid.value) return
const time = endT.value ? `${startT.value} ${endT.value}` : startT.value
@@ -119,373 +113,201 @@ function submit(): void {
</script>
<template>
<div
class="modal-back"
@mousedown.self="emit('close')"
<BaseModal
:title="isEdit ? '일정 수정' : '새 일정'"
@close="emit('close')"
>
<div class="modal">
<div class="modal-head">
<h3>{{ isEdit ? '일정 수정' : '새 일정' }}</h3>
<button
class="modal-x"
@click="emit('close')"
<label class="form-field"><span class="form-label">제목</span>
<input
v-model="title"
class="form-input"
placeholder="일정 제목"
autofocus
>
</label>
<label class="form-field"><span class="form-label">유형</span>
<div class="form-select-wrap">
<select
v-model="categoryId"
class="form-select"
>
<option
v-for="c in ordered"
:key="c.id"
:value="c.id"
>
{{ c.label }}
</option>
</select>
<span class="form-select-chev">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.2"
stroke-width="2"
stroke-linecap="round"
><path d="M18 6 6 18M6 6l12 12" /></svg>
</button>
stroke-linejoin="round"
><path d="m6 9 6 6 6-6" /></svg>
</span>
</div>
<div class="modal-body">
<label class="f-field"><span class="f-label">제목</span>
<input
v-model="title"
class="f-input"
placeholder="일정 제목"
autofocus
</label>
<div class="form-row">
<label class="form-field"><span class="form-label">시작일</span>
<input
v-model="s"
type="date"
class="form-input"
>
</label>
<label class="form-field"><span class="form-label">종료일</span>
<input
v-model="e"
type="date"
class="form-input"
:min="s"
>
</label>
</div>
<div class="form-field">
<span class="form-label">시간</span>
<div class="time-row">
<input
v-model="startT"
type="time"
class="form-input time-input"
>
<span class="time-dash"></span>
<input
v-model="endT"
type="time"
class="form-input time-input"
>
</div>
</div>
<label class="form-field"><span class="form-label">장소</span>
<input
v-model="place"
class="form-input"
placeholder="장소 또는 화상"
>
</label>
<div class="form-field">
<span class="form-label">참석자</span>
<div
ref="msRef"
class="ms"
>
<button
type="button"
class="ms-field"
:class="{ empty: !allHands && attendeeIds.length === 0, open: msOpen }"
@click="msOpen = !msOpen"
>
<span class="ms-val">{{ msLabel }}</span>
<span class="chev">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><path d="m6 9 6 6 6-6" /></svg>
</span>
</button>
<div
v-if="msOpen"
class="ms-panel"
>
<button
type="button"
class="ms-item"
:class="{ on: allHands }"
@click="toggleAll"
>
</label>
<label class="f-field"><span class="f-label">유형</span>
<div class="f-select-wrap">
<select
v-model="categoryId"
class="f-select"
<span class="ms-all"></span>
<span class="ms-name"> 직원</span>
<span
v-if="allHands"
class="ms-check"
>
<option
v-for="c in ordered"
:key="c.id"
:value="c.id"
>
{{ c.label }}
</option>
</select>
<span class="f-select-chev">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-width="2.4"
stroke-linecap="round"
stroke-linejoin="round"
><path d="m6 9 6 6 6-6" /></svg>
><path d="M20 6 9 17l-5-5" /></svg>
</span>
</div>
</label>
<div class="f-row">
<label class="f-field"><span class="f-label">시작일</span>
<input
v-model="s"
type="date"
class="f-input"
>
</label>
<label class="f-field"><span class="f-label">종료일</span>
<input
v-model="e"
type="date"
class="f-input"
:min="s"
>
</label>
</div>
<div class="f-field">
<span class="f-label">시간</span>
<div class="time-row">
<input
v-model="startT"
type="time"
class="f-input time-input"
>
<span class="time-dash"></span>
<input
v-model="endT"
type="time"
class="f-input time-input"
>
</div>
</div>
<label class="f-field"><span class="f-label">장소</span>
<input
v-model="place"
class="f-input"
placeholder="장소 또는 화상"
</button>
<div class="ms-div" />
<button
v-for="p in people"
:key="p.id"
type="button"
class="ms-item"
:class="{ on: attendeeIds.includes(p.id) }"
@click="togglePerson(p.id)"
>
</label>
<div class="f-field">
<span class="f-label">참석자</span>
<ScheduleAvatar
:name="p.name"
:avatar-url="p.avatarUrl"
:size="20"
/>
<span class="ms-name">{{ p.name }}</span>
<span
v-if="attendeeIds.includes(p.id)"
class="ms-check"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.4"
stroke-linecap="round"
stroke-linejoin="round"
><path d="M20 6 9 17l-5-5" /></svg>
</span>
</button>
<div
ref="msRef"
class="ms"
v-if="people.length === 0"
class="ms-empty"
>
<button
type="button"
class="ms-field"
:class="{ empty: !allHands && attendeeIds.length === 0, open: msOpen }"
@click="msOpen = !msOpen"
>
<span class="ms-val">{{ msLabel }}</span>
<span class="chev">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><path d="m6 9 6 6 6-6" /></svg>
</span>
</button>
<div
v-if="msOpen"
class="ms-panel"
>
<button
type="button"
class="ms-item"
:class="{ on: allHands }"
@click="toggleAll"
>
<span class="ms-all"></span>
<span class="ms-name"> 직원</span>
<span
v-if="allHands"
class="ms-check"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.4"
stroke-linecap="round"
stroke-linejoin="round"
><path d="M20 6 9 17l-5-5" /></svg>
</span>
</button>
<div class="ms-div" />
<button
v-for="p in people"
:key="p.id"
type="button"
class="ms-item"
:class="{ on: attendeeIds.includes(p.id) }"
@click="togglePerson(p.id)"
>
<ScheduleAvatar
:name="p.name"
:avatar-url="p.avatarUrl"
:size="20"
/>
<span class="ms-name">{{ p.name }}</span>
<span
v-if="attendeeIds.includes(p.id)"
class="ms-check"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.4"
stroke-linecap="round"
stroke-linejoin="round"
><path d="M20 6 9 17l-5-5" /></svg>
</span>
</button>
<div
v-if="people.length === 0"
class="ms-empty"
>
등록된 사용자가 없습니다
</div>
</div>
등록된 사용자가 없습니다
</div>
</div>
<label class="f-field"><span class="f-label">설명</span>
<textarea
v-model="desc"
class="f-input f-textarea"
placeholder="메모"
rows="3"
/>
</label>
</div>
<div class="modal-foot">
<button
class="mbtn"
@click="emit('close')"
>
취소
</button>
<button
class="mbtn primary"
:disabled="!valid"
@click="submit"
>
{{ isEdit ? '저장' : '추가' }}
</button>
</div>
</div>
</div>
<label class="form-field"><span class="form-label">설명</span>
<textarea
v-model="desc"
class="form-input form-textarea"
placeholder="메모"
rows="3"
/>
</label>
<template #foot>
<button
class="mbtn"
@click="emit('close')"
>
취소
</button>
<button
class="mbtn primary"
:disabled="!valid"
@click="submit"
>
{{ isEdit ? '저장' : '추가' }}
</button>
</template>
</BaseModal>
</template>
<style scoped>
/* 사이트 공통 폼 모달 규격(멤버 초대 모달 + relay.css 토큰)에 맞춤 — rem 단위 */
.modal-back {
position: fixed;
inset: 0;
z-index: 100;
background: rgba(20, 24, 33, 0.5);
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
}
.modal {
width: 100%;
max-width: 33.75rem;
max-height: 90vh;
overflow-y: auto;
background: #fff;
border-radius: 0.875rem;
box-shadow: 0 24px 64px rgba(20, 24, 33, 0.32);
display: flex;
flex-direction: column;
}
.modal-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.375rem 1.5rem 1rem;
position: sticky;
top: 0;
background: #fff;
border-radius: 0.875rem 0.875rem 0 0;
}
.modal-head h3 {
font-size: 1.0625rem;
font-weight: 700;
letter-spacing: -0.01875rem;
}
.modal-x {
width: 2rem;
height: 2rem;
border: none;
background: transparent;
color: var(--text-3);
border-radius: var(--radius-sm);
cursor: pointer;
display: grid;
place-items: center;
margin-right: -0.3125rem;
}
.modal-x:hover {
background: #f1f2f4;
color: var(--text);
}
.modal-x svg {
width: 1.125rem;
height: 1.125rem;
}
.modal-body {
padding: 0.5rem 1.5rem 0.75rem;
display: flex;
flex-direction: column;
gap: 1.125rem;
/* 전역 .modal-body(확인 다이얼로그용)의 가운데 정렬을 폼 모달에선 왼쪽으로 */
text-align: left;
}
.modal-foot {
display: flex;
justify-content: flex-end;
gap: 0.5625rem;
padding: 1rem 1.5rem;
border-top: 1px solid var(--border);
background: #fafbfc;
position: sticky;
bottom: 0;
border-radius: 0 0 0.875rem 0.875rem;
}
.f-field {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.f-label {
font-size: 0.781rem;
font-weight: 600;
color: var(--text-2);
}
.f-input {
width: 100%;
height: 2.5rem;
padding: 0 0.75rem;
border: 1px solid var(--border-strong);
border-radius: var(--radius);
font-family: inherit;
font-size: 0.844rem;
color: var(--text);
background: #fff;
}
.f-input::placeholder {
color: var(--text-3);
}
.f-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-weak);
}
.f-textarea {
height: auto;
padding: 0.625rem 0.75rem;
resize: vertical;
line-height: 1.5;
min-height: 5.75rem;
}
.f-row {
display: flex;
gap: 0.75rem;
}
.f-row .f-field {
flex: 1;
min-width: 0;
}
.f-select-wrap {
position: relative;
}
.f-select {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 2.5rem;
padding: 0 2.125rem 0 0.75rem;
border: 1px solid var(--border-strong);
border-radius: var(--radius);
font-family: inherit;
font-size: 0.844rem;
color: var(--text);
background: #fff;
cursor: pointer;
}
.f-select:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-weak);
}
.f-select-chev {
position: absolute;
right: 0.625rem;
top: 50%;
transform: translateY(-50%);
display: grid;
color: var(--text-3);
pointer-events: none;
}
.f-select-chev svg {
width: 0.9375rem;
height: 0.9375rem;
}
/* 시간 입력 행 */
.time-row {
display: flex;
align-items: center;
@@ -499,6 +321,8 @@ function submit(): void {
.time-dash {
color: var(--text-3);
}
/* 참석자 멀티셀렉트 */
.ms {
position: relative;
}
@@ -594,7 +418,7 @@ function submit(): void {
height: 0.9375rem;
}
.ms-all {
/* 참석자 아바타(ScheduleAvatar :size="20" = 20px)와 동일 크기로 고정 */
/* 참석자 아바타(ScheduleAvatar :size="20" = 20px)와 동일 크기 */
width: 20px;
height: 20px;
border-radius: 50%;
@@ -617,20 +441,4 @@ function submit(): void {
color: var(--text-3);
text-align: center;
}
/* 푸터 버튼 — 전역 .mbtn(확인 다이얼로그용 flex:1) 대신 폼 모달용 우측정렬 컴팩트(멤버 초대 모달과 동일).
색/서체/테두리는 전역 .mbtn / .mbtn.primary 가 담당한다. */
.mbtn {
flex: 0 0 auto;
height: 2.375rem;
padding: 0 1rem;
}
.mbtn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.mbtn.primary:disabled,
.mbtn.primary:disabled:hover {
background: var(--accent);
border-color: var(--accent);
}
</style>