diff --git a/apps/web/src/components/AskMentoPanel.vue b/apps/web/src/components/AskMentoPanel.vue index cf9099a..7d9392e 100644 --- a/apps/web/src/components/AskMentoPanel.vue +++ b/apps/web/src/components/AskMentoPanel.vue @@ -90,7 +90,7 @@ form { gap: 0.45rem; background: #fff; border: 1px solid #e2e8f0; - border-radius: 14px; + border-radius: var(--radius); padding: 0.35rem 0.4rem 0.35rem 0.7rem; } @@ -110,7 +110,7 @@ input { button { border: 0; - border-radius: 10px; + border-radius: var(--radius); background: #14b8a6; color: #fff; font: inherit; @@ -126,7 +126,7 @@ button:disabled { .answer { background: #f0fdfa; - border-radius: 12px; + border-radius: var(--radius); padding: 0.7rem 0.85rem; } diff --git a/apps/web/src/components/CustomFieldsSection.vue b/apps/web/src/components/CustomFieldsSection.vue index 0da833d..6cdca03 100644 --- a/apps/web/src/components/CustomFieldsSection.vue +++ b/apps/web/src/components/CustomFieldsSection.vue @@ -2,6 +2,7 @@ import { onMounted, ref, watch } from 'vue'; import { api } from '@/lib/api'; import JalaliDateInput from '@/components/JalaliDateInput.vue'; +import { UiField, UiInput, UiSelect } from '@/components/ui'; import type { CustomFieldDefinitionDto, CustomFieldEntity, @@ -58,27 +59,28 @@ onMounted(load); diff --git a/apps/web/src/components/EntityTimeline.vue b/apps/web/src/components/EntityTimeline.vue index 6fad0f3..f3d3d1e 100644 --- a/apps/web/src/components/EntityTimeline.vue +++ b/apps/web/src/components/EntityTimeline.vue @@ -289,7 +289,7 @@ onMounted(load); .composer input { width: 100%; border: 1px solid var(--line); - border-radius: 12px; + border-radius: var(--radius); padding: 0.7rem 0.85rem; background: #fff; } @@ -316,7 +316,7 @@ onMounted(load); gap: 0.75rem; align-items: flex-start; border: 1px solid var(--line); - border-radius: 12px; + border-radius: var(--radius); padding: 0.75rem 0.85rem; background: #fbfcfb; } diff --git a/apps/web/src/components/JalaliDateInput.vue b/apps/web/src/components/JalaliDateInput.vue index 8733049..cef3806 100644 --- a/apps/web/src/components/JalaliDateInput.vue +++ b/apps/web/src/components/JalaliDateInput.vue @@ -675,7 +675,7 @@ onBeforeUnmount(() => { justify-content: flex-start; gap: 0.8rem; min-height: 52px; - border-radius: 16px; + border-radius: var(--radius); padding: 0.45rem 0.85rem 0.45rem 0.45rem; background: linear-gradient(180deg, #ffffff 0%, #f8fffd 100%); } @@ -698,7 +698,7 @@ onBeforeUnmount(() => { left: 50%; bottom: 50%; transform-origin: center bottom; - border-radius: 99px; + border-radius: var(--radius); background: #0f766e; } @@ -726,7 +726,7 @@ onBeforeUnmount(() => { } .clock-text { - font-weight: 800; + font-weight: 600; font-variant-numeric: tabular-nums; letter-spacing: 0.02em; font-size: 1.05rem; @@ -768,7 +768,7 @@ onBeforeUnmount(() => { max-height: calc(100vh - 16px); overflow: auto; background: #fff; - border-radius: 16px; + border-radius: var(--radius); box-shadow: 0 18px 48px rgba(15, 23, 42, 0.18); padding: 0.75rem 0.8rem 0.7rem; font-family: IRANSans, Tahoma, sans-serif; @@ -797,7 +797,7 @@ onBeforeUnmount(() => { width: 32px; height: 32px; border: 0; - border-radius: 8px; + border-radius: var(--radius); background: #f1f5f9; color: #0f766e; display: grid; @@ -826,7 +826,7 @@ onBeforeUnmount(() => { .day { height: 34px; border: 0; - border-radius: 9px; + border-radius: var(--radius); background: none; color: #0f172a; font: inherit; @@ -881,12 +881,12 @@ onBeforeUnmount(() => { min-width: 5.1rem; height: 4.1rem; border: 0; - border-radius: 14px; + border-radius: var(--radius); background: #f1f5f9; color: #0f172a; font: inherit; font-size: 2.35rem; - font-weight: 800; + font-weight: 600; font-variant-numeric: tabular-nums; letter-spacing: 0.02em; cursor: pointer; @@ -900,7 +900,7 @@ onBeforeUnmount(() => { .md-colon { font-size: 2.2rem; - font-weight: 800; + font-weight: 600; color: #0f172a; line-height: 1; padding-bottom: 0.15rem; @@ -1005,7 +1005,7 @@ onBeforeUnmount(() => { margin: 0; text-align: center; font-size: 1.2rem; - font-weight: 800; + font-weight: 600; color: #0f766e; letter-spacing: 0.04em; } @@ -1033,7 +1033,7 @@ onBeforeUnmount(() => { height: 220px; overflow: auto; border: 1px solid #e2e8f0; - border-radius: 12px; + border-radius: var(--radius); background: #f8fafc; padding: 0.25rem; scrollbar-width: thin; @@ -1044,7 +1044,7 @@ onBeforeUnmount(() => { width: 100%; height: 36px; border: 0; - border-radius: 8px; + border-radius: var(--radius); background: transparent; color: #334155; font: inherit; @@ -1087,9 +1087,9 @@ onBeforeUnmount(() => { margin-inline-start: auto; background: #14b8a6; color: #fff; - border-radius: 10px; + border-radius: var(--radius); padding: 0.4rem 0.8rem; - font-weight: 800; + font-weight: 600; font-size: 0.8rem; } diff --git a/apps/web/src/components/MentoMemoryPanel.vue b/apps/web/src/components/MentoMemoryPanel.vue index 8ae678d..3a60c49 100644 --- a/apps/web/src/components/MentoMemoryPanel.vue +++ b/apps/web/src/components/MentoMemoryPanel.vue @@ -203,7 +203,7 @@ h2 { textarea { border: 1px solid var(--line); - border-radius: 12px; + border-radius: var(--radius); padding: 0.75rem 0.9rem; background: #fff; font: inherit; @@ -221,7 +221,7 @@ textarea { .items li { border: 1px solid var(--line); - border-radius: 12px; + border-radius: var(--radius); padding: 0.75rem 0.85rem; background: #fff; } diff --git a/apps/web/src/components/NoteAiSuggestionPanel.vue b/apps/web/src/components/NoteAiSuggestionPanel.vue index 1c07e7c..bb07b73 100644 --- a/apps/web/src/components/NoteAiSuggestionPanel.vue +++ b/apps/web/src/components/NoteAiSuggestionPanel.vue @@ -231,7 +231,7 @@ function formatPayloadSummary(value: NoteAiPayloadDto | null) { margin-top: 0.75rem; padding: 0.75rem 0.85rem; border: 1px dashed var(--accent); - border-radius: 12px; + border-radius: var(--radius); background: rgba(255, 255, 255, 0.65); display: grid; gap: 0.6rem; @@ -281,7 +281,7 @@ header { input[type='text'], textarea { border: 1px solid var(--line); - border-radius: 10px; + border-radius: var(--radius); padding: 0.55rem 0.7rem; font: inherit; width: 100%; diff --git a/apps/web/src/components/QuickCreate.vue b/apps/web/src/components/QuickCreate.vue index 73ef15f..fb7a866 100644 --- a/apps/web/src/components/QuickCreate.vue +++ b/apps/web/src/components/QuickCreate.vue @@ -5,8 +5,9 @@ import type { RouteLocationRaw } from 'vue-router'; import UiIcon from '@/components/UiIcon.vue'; const FAB_SIZE = 36; -const FAB_MARGIN = 20; -const POS_KEY = 'mento.quickCreateFab'; +const FAB_MARGIN_BOTTOM = 24; +const FAB_MARGIN_LEFT = 24; +const POS_KEY = 'mento.quickCreateFab_v2'; type FabPos = { left: number; top: number }; @@ -67,7 +68,10 @@ const router = useRouter(); const hidden = computed(() => route.name === 'person-detail'); const isQuickCreateOpen = ref(false); const ready = ref(false); -const fabPos = ref({ left: FAB_MARGIN, top: FAB_MARGIN }); +const fabPos = ref({ + left: FAB_MARGIN_LEFT, + top: typeof window !== 'undefined' ? window.innerHeight - FAB_MARGIN_BOTTOM - FAB_SIZE : 600, +}); const viewport = ref({ w: 1280, h: 800 }); const dragging = ref(false); let dragMoved = false; @@ -75,10 +79,10 @@ let dragOffsetX = 0; let dragOffsetY = 0; function defaultPos(): FabPos { - const bottomGap = viewport.value.w <= 900 ? FAB_MARGIN + 64 : FAB_MARGIN; + const extra = viewport.value.w <= 900 ? 64 : 0; return { - left: FAB_MARGIN, - top: Math.max(FAB_MARGIN, viewport.value.h - bottomGap - FAB_SIZE), + left: FAB_MARGIN_LEFT, + top: Math.max(8, viewport.value.h - FAB_MARGIN_BOTTOM - extra - FAB_SIZE), }; } @@ -171,7 +175,7 @@ const fabStyle = computed(() => ({ const panelStyle = computed(() => { const gap = 12; const edge = 16; - const width = Math.min(390, viewport.value.w - 32); + const width = Math.min(300, viewport.value.w - 32); let left = fabPos.value.left; left = Math.min(left, viewport.value.w - width - edge); left = Math.max(edge, left); @@ -278,7 +282,7 @@ onBeforeUnmount(() => { @click="go(item.to)" > - + {{ item.title }} @@ -303,7 +307,7 @@ onBeforeUnmount(() => { @pointercancel="onFabPointerUp" > @@ -328,25 +332,26 @@ onBeforeUnmount(() => { .panel { position: fixed; z-index: 92; - width: 390px; - max-height: min(540px, calc(100dvh - 80px)); + width: 300px; + max-height: min(480px, calc(100dvh - 80px)); box-sizing: border-box; display: flex; flex-direction: column; background: #fff; - border-radius: 28px; - padding: 16px 16px 12px; - box-shadow: 0 18px 48px rgba(15, 23, 42, 0.18); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 12px 12px 10px; + box-shadow: var(--shadow-lg); overflow: hidden; } .panel h2 { flex-shrink: 0; - margin: 0 0 12px; + margin: 0 0 10px; text-align: right; - font-size: 20px; - font-weight: 800; - color: #111827; + font-size: var(--fs-md); + font-weight: 700; + color: var(--ink); } .items { @@ -354,31 +359,37 @@ onBeforeUnmount(() => { overflow-y: auto; display: flex; flex-direction: column; - gap: 8px; + gap: 6px; padding-left: 2px; overscroll-behavior: contain; } .item { width: 100%; - min-height: 68px; + min-height: 46px; display: flex; align-items: center; - gap: 14px; - padding: 10px 14px; - border: 1px solid #eef1f2; - border-radius: 16px; + gap: 10px; + padding: 6px 10px; + border: 1px solid var(--line-light); + border-radius: var(--radius); background: #fff; - box-shadow: 0 4px 14px rgba(15, 23, 42, 0.04); + box-shadow: var(--shadow-xs); font: inherit; text-align: right; cursor: pointer; + transition: background var(--transition-fast), border-color var(--transition-fast); +} + +.item:hover { + background: var(--surface-hover); + border-color: var(--line-strong); } .item-ico { - width: 52px; - height: 52px; - border-radius: 14px; + width: 34px; + height: 34px; + border-radius: var(--radius); display: grid; place-items: center; color: #fff; @@ -390,7 +401,7 @@ onBeforeUnmount(() => { } .item-ico.company { - background: #22d3ee; + background: #0891b2; } .item-ico.deal { @@ -398,7 +409,7 @@ onBeforeUnmount(() => { } .item-ico.activity { - background: #f97316; + background: #ea580c; } .item-ico.note { @@ -407,33 +418,34 @@ onBeforeUnmount(() => { .copy { display: grid; - gap: 4px; + gap: 2px; min-width: 0; text-align: right; } .copy strong { - font-size: 16px; + font-size: var(--fs-sm); font-weight: 600; - color: #111827; + color: var(--ink); + line-height: 1.2; } .copy small { - font-size: 13px; - color: #94a3b8; - line-height: 1.5; + font-size: var(--fs-xs); + color: var(--muted); + line-height: 1.3; } .fab { position: fixed; z-index: 93; box-sizing: border-box; - width: 36px; - height: 36px; + width: var(--control-h); + height: var(--control-h); padding: 0; margin: 0; border: 0; - border-radius: 10px; + border-radius: var(--radius); overflow: hidden; background: linear-gradient(145deg, #0f766e 0%, #14b8a6 100%); color: #fff; @@ -442,7 +454,7 @@ onBeforeUnmount(() => { line-height: 0; direction: ltr; box-shadow: - 0 8px 22px rgba(13, 148, 136, 0.4), + 0 4px 14px rgba(13, 148, 136, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.22); cursor: grab; touch-action: none; @@ -463,29 +475,28 @@ onBeforeUnmount(() => { .fab :deep(.ico) { display: block; - width: 28px; - height: 28px; - max-width: 28px; - max-height: 28px; + width: 20px; + height: 20px; + max-width: 20px; + max-height: 20px; stroke-width: 1.7; - filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.15)); } .fab:hover { filter: brightness(1.06); box-shadow: - 0 10px 26px rgba(13, 148, 136, 0.48), + 0 6px 18px rgba(13, 148, 136, 0.42), inset 0 1px 0 rgba(255, 255, 255, 0.25); } .fab.open { background: linear-gradient(145deg, #115e59 0%, #0d9488 100%); - box-shadow: 0 10px 28px rgba(15, 23, 42, 0.28); + box-shadow: 0 6px 20px rgba(15, 23, 42, 0.25); } .fab.dragging { cursor: grabbing; transform: scale(1.05); - box-shadow: 0 12px 28px rgba(13, 148, 136, 0.5); + box-shadow: 0 8px 24px rgba(13, 148, 136, 0.45); } diff --git a/apps/web/src/components/UiIcon.vue b/apps/web/src/components/UiIcon.vue index e161b32..a9d50b0 100644 --- a/apps/web/src/components/UiIcon.vue +++ b/apps/web/src/components/UiIcon.vue @@ -64,7 +64,13 @@ defineProps<{ | 'logout' | 'pulse' | 'sliders' - | 'camera'; + | 'camera' + | 'arrow-right' + | 'arrow-left' + | 'chevron-right' + | 'chevron-left' + | 'chevrons-right' + | 'chevrons-left'; size?: number; }>(); @@ -218,9 +224,24 @@ defineProps<{ - @@ -26,18 +38,52 @@ withDefaults( display: inline-flex; align-items: center; justify-content: center; - gap: 0.35rem; - min-height: var(--control-h); - padding: 0 0.95rem; + gap: var(--sp-2); + min-height: var(--control-h-md); + height: var(--control-h-md); + padding: 0 0.85rem; border: 1px solid transparent; border-radius: var(--radius); background: var(--accent); - color: #fff; + color: var(--accent-contrast); font: inherit; - font-weight: 700; - font-size: 0.88rem; + font-weight: var(--fw-semibold); + font-size: var(--fs-sm); white-space: nowrap; cursor: pointer; + box-sizing: border-box; + transition: background var(--transition-fast), border-color var(--transition-fast), + box-shadow var(--transition-fast), opacity var(--transition-fast); + user-select: none; +} + +/* Sizes */ +.ui-btn.sm { + min-height: var(--control-h-sm); + height: var(--control-h-sm); + padding: 0 0.65rem; + font-size: var(--fs-xs); + border-radius: var(--radius-sm); +} + +.ui-btn.md { + min-height: var(--control-h-md); + height: var(--control-h-md); + padding: 0 0.85rem; + font-size: var(--fs-sm); + border-radius: var(--radius); +} + +.ui-btn.lg { + min-height: var(--control-h-lg); + height: var(--control-h-lg); + padding: 0 1.15rem; + font-size: var(--fs-base); + border-radius: var(--radius-md); +} + +.ui-btn:hover:not(:disabled) { + background: var(--accent-hover); } .ui-btn:disabled { @@ -49,28 +95,125 @@ withDefaults( width: 100%; } +/* Variants */ .ui-btn.ghost { - background: #fff; + background: transparent; color: var(--ink); border-color: var(--line); } +.ui-btn.ghost:hover:not(:disabled) { + background: var(--surface-subtle); + border-color: var(--line-strong); +} + +.ui-btn.outline { + background: transparent; + color: var(--accent); + border-color: var(--accent); +} + +.ui-btn.outline:hover:not(:disabled) { + background: var(--accent-subtle); +} + +.ui-btn.secondary { + background: var(--surface-subtle); + color: var(--ink-secondary); + border-color: var(--line); +} + +.ui-btn.secondary:hover:not(:disabled) { + background: var(--line); + color: var(--ink); +} + +.ui-btn.danger { + background: var(--danger); + color: #fff; + border-color: var(--danger); +} + +.ui-btn.danger:hover:not(:disabled) { + background: var(--danger-hover); +} + +.ui-btn.success { + background: var(--success); + color: #fff; + border-color: var(--success); +} + +.ui-btn.success:hover:not(:disabled) { + background: var(--success-deep); +} + .ui-btn.dashed { background: #fff; - border: 1.5px dashed currentColor; - font-weight: 700; + color: var(--muted); + border: 1.5px dashed var(--line-strong); +} + +.ui-btn.dashed:hover:not(:disabled) { + color: var(--accent); + border-color: var(--accent); + background: var(--accent-subtle); +} + +.ui-btn.link { + background: transparent; + border-color: transparent; + color: var(--accent); + padding: 0 var(--sp-1); + min-height: auto; + font-weight: var(--fw-semibold); +} + +.ui-btn.link:hover:not(:disabled) { + background: transparent; + text-decoration: underline; } .ui-btn.icon { - width: var(--control-h); + width: var(--control-h-md); padding: 0; background: #fff; color: var(--accent-deep); border-color: var(--accent); } +.ui-btn.icon.sm { + width: var(--control-h-sm); + min-height: var(--control-h-sm); +} + +.ui-btn.icon.lg { + width: var(--control-h-lg); + min-height: var(--control-h-lg); +} + +.ui-btn.icon:hover:not(:disabled) { + background: var(--accent-subtle); +} + .ui-btn:focus-visible { outline: 2px solid var(--accent-soft); outline-offset: 1px; } + +/* Spinner */ +.spinner { + width: 14px; + height: 14px; + border: 2px solid currentColor; + border-right-color: transparent; + border-radius: 50%; + animation: ui-spin 0.6s linear infinite; +} + +@keyframes ui-spin { + to { + transform: rotate(360deg); + } +} diff --git a/apps/web/src/components/ui/UiCard.vue b/apps/web/src/components/ui/UiCard.vue index 0423d0b..a6b5915 100644 --- a/apps/web/src/components/ui/UiCard.vue +++ b/apps/web/src/components/ui/UiCard.vue @@ -1,14 +1,119 @@ + + diff --git a/apps/web/src/components/ui/UiChart.vue b/apps/web/src/components/ui/UiChart.vue index 2fc080a..97c5994 100644 --- a/apps/web/src/components/ui/UiChart.vue +++ b/apps/web/src/components/ui/UiChart.vue @@ -1,54 +1,114 @@ - + + diff --git a/apps/web/src/components/ui/UiEmptyState.vue b/apps/web/src/components/ui/UiEmptyState.vue new file mode 100644 index 0000000..2acff19 --- /dev/null +++ b/apps/web/src/components/ui/UiEmptyState.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/apps/web/src/components/ui/UiField.vue b/apps/web/src/components/ui/UiField.vue new file mode 100644 index 0000000..e758cfb --- /dev/null +++ b/apps/web/src/components/ui/UiField.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/apps/web/src/components/ui/UiFilterBar.vue b/apps/web/src/components/ui/UiFilterBar.vue index acb7a68..8963879 100644 --- a/apps/web/src/components/ui/UiFilterBar.vue +++ b/apps/web/src/components/ui/UiFilterBar.vue @@ -21,7 +21,7 @@ const open = ref(false);
- + فیلترها @@ -54,9 +54,10 @@ const open = ref(false); display: grid; grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr)); gap: 0.65rem; - padding: var(--pad-modal); + padding: var(--sp-4); background: #fff; border: 1px solid var(--line); border-radius: var(--radius); + box-shadow: var(--shadow-sm); } diff --git a/apps/web/src/components/ui/UiInput.vue b/apps/web/src/components/ui/UiInput.vue index b2e05a6..03c6d68 100644 --- a/apps/web/src/components/ui/UiInput.vue +++ b/apps/web/src/components/ui/UiInput.vue @@ -4,48 +4,122 @@ withDefaults( type?: string; placeholder?: string; disabled?: boolean; + readonly?: boolean; required?: boolean; - maxlength?: number; - minlength?: number; + error?: boolean | string; + size?: 'sm' | 'md' | 'lg'; + maxlength?: number | string; + minlength?: number | string; + id?: string; + autocomplete?: string; }>(), - { type: 'text' }, + { + type: 'text', + size: 'md', + disabled: false, + readonly: false, + required: false, + }, ); -const model = defineModel({ default: '' }); +const model = defineModel({ default: '' }); diff --git a/apps/web/src/components/ui/UiModal.vue b/apps/web/src/components/ui/UiModal.vue index 57b02f0..1c32e33 100644 --- a/apps/web/src/components/ui/UiModal.vue +++ b/apps/web/src/components/ui/UiModal.vue @@ -61,7 +61,7 @@ function onBackdrop(event: MouseEvent) { z-index: 80; display: grid; place-items: center; - padding: 1.25rem; + padding: 16px; background: rgba(15, 23, 42, 0.35); } @@ -72,55 +72,122 @@ function onBackdrop(event: MouseEvent) { flex-direction: column; background: #fff; border: 1px solid var(--line); - border-radius: var(--radius); + border-radius: var(--radius, 10px); box-shadow: 0 18px 48px rgba(15, 23, 42, 0.16); overflow: hidden; } -.head, -.body, -.foot { - padding: var(--pad-modal); -} - .head { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; - padding-bottom: 0; + padding: 16px; + border-bottom: 1px solid var(--line); + flex-shrink: 0; + background: #fff; + z-index: 10; } .head h2 { margin: 0; - font-size: 1.05rem; - font-weight: 800; + font-size: 16px; + font-weight: 500; + color: var(--ink); + line-height: 1.4; } .close { - width: var(--control-h); - height: var(--control-h); + width: 36px; + height: 36px; border: 1px solid var(--line); - border-radius: var(--radius); + border-radius: var(--radius, 10px); background: #fff; color: var(--muted); display: grid; place-items: center; cursor: pointer; + transition: all var(--transition-fast); +} + +.close:hover { + background: var(--surface-subtle); + color: var(--ink); + border-color: var(--line-strong); } .body { - overflow: auto; + flex: 1 1 auto; min-height: 0; - padding-top: 1rem; + overflow-y: auto; + display: flex; + flex-direction: column; + padding: 16px; + box-sizing: border-box; +} + +.body :deep(.crm-page.embedded), +.body :deep(.page.embedded), +.body :deep(.product-form-page.embedded), +.body :deep(.company-form-page.embedded) { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 100%; +} + +.body :deep(.page.embedded > .form), +.body :deep(.page.embedded > form), +.body :deep(.crm-page.embedded > .form), +.body :deep(.crm-page.embedded > form), +.body :deep(.crm-page.embedded > .crm-form) { + flex: 1 1 auto; + min-height: 0; +} + +.body :deep(.embed-actions), +.body :deep(.embedded-actions), +.body :deep(.form-bottom-actions), +.body :deep(.form-footer-actions) { + order: 99; + flex-shrink: 0; + position: sticky; + bottom: -16px; + margin-top: auto; + margin-inline: -16px; + margin-bottom: -16px; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 12px; + padding: 16px; + background: #fff; + border-top: 1px solid var(--line); + z-index: 10; + box-shadow: 0 -4px 14px rgba(15, 23, 42, 0.05); +} + +.body :deep(.embed-actions .btn), +.body :deep(.embedded-actions .btn), +.body :deep(.embed-actions .ui-btn), +.body :deep(.embedded-actions .ui-btn), +.body :deep(.form-bottom-actions .btn), +.body :deep(.form-bottom-actions .ui-btn), +.body :deep(.form-footer-actions .btn), +.body :deep(.form-footer-actions .ui-btn) { + border-radius: var(--radius, 10px); } .foot { display: flex; + align-items: center; justify-content: flex-start; - gap: 0.5rem; - padding-top: 0; - border-top: 1px solid #f1f5f9; + gap: 12px; + padding: 16px; + border-top: 1px solid var(--line); + flex-shrink: 0; + background: #fff; + z-index: 10; } @media (max-width: 900px) { @@ -138,62 +205,15 @@ function onBackdrop(event: MouseEvent) { } .head { - flex-shrink: 0; - padding: 0.75rem 1rem 0; + padding: 16px; } .body { - flex: 1; - min-height: 0; - overflow: auto; - display: flex; - flex-direction: column; - padding: 0.75rem 1rem; - } - - .body:has(.page.embedded) { - overflow: hidden; - padding-bottom: 0; - } - - .body :deep(.page.embedded) { - display: flex; - flex-direction: column; - flex: 1; - min-height: 0; - gap: 0.75rem; - } - - .body :deep(.page.embedded > .form), - .body :deep(.page.embedded > form) { - flex: 1 1 auto; - min-height: 0; - overflow: auto; - } - - .body :deep(.embed-actions) { - order: 20; - flex: 0 0 auto; - position: sticky; - bottom: 0; - margin-top: auto; - margin-inline: -1rem; - display: flex; - gap: 0.5rem; - padding: 0.75rem 1rem calc(0.75rem + env(safe-area-inset-bottom)); - background: #fff; - border-top: 1px solid #f1f5f9; - z-index: 3; - } - - .body :deep(.embed-actions .btn) { - flex: 1; - min-width: 0; + padding: 16px; } .foot { - flex-shrink: 0; - padding: 0.75rem 1rem calc(0.75rem + env(safe-area-inset-bottom)); + padding: 16px calc(16px + env(safe-area-inset-bottom)); } } diff --git a/apps/web/src/components/ui/UiPageHeader.vue b/apps/web/src/components/ui/UiPageHeader.vue new file mode 100644 index 0000000..4b0a157 --- /dev/null +++ b/apps/web/src/components/ui/UiPageHeader.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/apps/web/src/components/ui/UiPagination.vue b/apps/web/src/components/ui/UiPagination.vue new file mode 100644 index 0000000..92f3c10 --- /dev/null +++ b/apps/web/src/components/ui/UiPagination.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/apps/web/src/components/ui/UiSearch.vue b/apps/web/src/components/ui/UiSearch.vue index 2307cf1..78b0988 100644 --- a/apps/web/src/components/ui/UiSearch.vue +++ b/apps/web/src/components/ui/UiSearch.vue @@ -29,12 +29,15 @@ const model = defineModel({ default: '' }); align-items: center; gap: 0.45rem; min-width: 0; + height: var(--control-h); min-height: var(--control-h); padding: 0 0.75rem; - background: #fff; + background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); color: var(--muted); + box-sizing: border-box; + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); } .ui-search input { @@ -45,11 +48,14 @@ const model = defineModel({ default: '' }); background: transparent; color: var(--ink); font: inherit; + font-size: var(--fs-sm); + padding: 0; } .ui-search:focus-within { - outline: 2px solid var(--accent-soft); + outline: none; border-color: var(--accent); + box-shadow: 0 0 0 2px var(--accent-soft); } .sr { diff --git a/apps/web/src/components/ui/UiSelect.vue b/apps/web/src/components/ui/UiSelect.vue index 6d54614..44613b6 100644 --- a/apps/web/src/components/ui/UiSelect.vue +++ b/apps/web/src/components/ui/UiSelect.vue @@ -1,19 +1,30 @@ @@ -22,20 +33,45 @@ const model = defineModel({ default: '' }); position: relative; display: block; min-width: 0; + width: 100%; } .ui-select select { appearance: none; width: 100%; - min-height: var(--control-h); - padding: 0 2rem 0 0.85rem; + min-height: var(--control-h-md); + padding: 0 2rem 0 var(--sp-3); border: 1px solid var(--line); border-radius: var(--radius); - background: #fff; + background: var(--surface); color: var(--ink); font: inherit; - font-weight: 600; + font-weight: var(--fw-medium); + font-size: var(--fs-base); cursor: pointer; + box-sizing: border-box; + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); +} + +.ui-select.sm select { + min-height: var(--control-h-sm); + padding: 0 1.75rem 0 var(--sp-2); + border-radius: var(--radius-sm); + font-size: var(--fs-xs); +} + +.ui-select.md select { + min-height: var(--control-h-md); + padding: 0 2rem 0 var(--sp-3); + border-radius: var(--radius); + font-size: var(--fs-base); +} + +.ui-select.lg select { + min-height: var(--control-h-lg); + padding: 0 2.25rem 0 var(--sp-4); + border-radius: var(--radius-md); + font-size: var(--fs-md); } .ui-select select:focus { @@ -43,6 +79,21 @@ const model = defineModel({ default: '' }); border-color: var(--accent); } +.ui-select.hasError select { + border-color: var(--danger); +} + +.ui-select.hasError select:focus { + outline: 2px solid var(--danger-soft); + border-color: var(--danger); +} + +.ui-select.disabled select { + background: var(--surface-subtle); + opacity: 0.65; + cursor: not-allowed; +} + .chev { position: absolute; left: 0.65rem; diff --git a/apps/web/src/components/ui/UiTextarea.vue b/apps/web/src/components/ui/UiTextarea.vue new file mode 100644 index 0000000..f839a6b --- /dev/null +++ b/apps/web/src/components/ui/UiTextarea.vue @@ -0,0 +1,81 @@ + + +