/
45case
/
ptable
Обзор
Документация
Войти
/
45case
/
ptable
Код
Запросы
3
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
dev
src/entities/table/ui/TableLegend.vue
655 строк
19 KB
Olya
Отредактирована надпись и блоки электронов
24 июл 2026, 12:53
24 июл 2026, 12:53
67e6930
Код
Авторство
О чём код?
<script setup lang="ts"> import { computed, ref, watch } from "vue"; import CategoryLegend from "./CategoryLegend.vue"; import PropertyKey from "./PropertyKey.vue"; import type { ElementPhaseAtStp } from "../../element/model/elements"; import type { ElementCategory } from "../../element/model/category"; import type { PropertyScaleKind } from "../../element/lib/propertyColorScale"; import type { CategoryLegendGroupKey, CategoryLegendHover } from "../model/categoryLegend"; import { areLegendFiltersEqual, type LegendFilter, stateLegendItems } from "../model/legend"; import { blockLegendItems, type ElectronBlock, } from "@/entities/electrons"; import ConfigBlocks from "@/entities/electrons/ui/ConfigBlocks.vue"; type LeftLegendMode = "phases" | "blocks" | "property" | "none"; const props = defineProps<{ categoryHover: CategoryLegendHover | null; isWide?: boolean; /** Home: phases / property scale / none. Electrons: s/p/d/f blocks. */ leftLegend?: LeftLegendMode; /** When false, hides Metals/Nonmetals category legend (e.g. Atomic mass property). */ showCategoryLegend?: boolean; phaseHover: LegendFilter | null; selectedPhases: ReadonlySet<ElementPhaseAtStp>; selectedCategories: ReadonlySet<ElementCategory>; selectedGroups: ReadonlySet<CategoryLegendGroupKey>; hoveredBlock?: ElectronBlock | null; selectedBlocks?: ReadonlySet<ElectronBlock>; /** Selected element for orbital ConfigBlocks (Electrons page). */ selectedAtomicNumber?: number | null; /** Swipe transition when switching Properties ↔ Electrons. */ legendSwipe?: string; /** PropertyKey (numeric properties). */ propertyStartColor?: string; propertyEndColor?: string; propertyUnknownColor?: string; propertyZeroColor?: string; propertyScale?: PropertyScaleKind; showPropertyZero?: boolean; disablePropertyLogarithmic?: boolean; /** Melt/boil: zero stop position in legend gradient (0–100). */ propertyZeroStopPercent?: number; }>(); const emit = defineEmits<{ "hover-legend": [hover: CategoryLegendHover | null]; "preview-phase": [filter: LegendFilter | null]; "toggle-phase": [phase: ElementPhaseAtStp]; "toggle-category": [category: ElementCategory]; "toggle-group": [group: CategoryLegendGroupKey]; "hover-block": [block: ElectronBlock | null]; "toggle-block": [block: ElectronBlock]; "update:propertyStartColor": [value: string]; "update:propertyEndColor": [value: string]; "update:propertyUnknownColor": [value: string]; "update:propertyZeroColor": [value: string]; "update:propertyScale": [value: PropertyScaleKind]; }>(); const showBlocks = () => props.leftLegend === "blocks"; const showPropertyKey = () => props.leftLegend === "property"; const showPhases = () => props.leftLegend === "phases" || props.leftLegend == null; const showCategories = () => props.showCategoryLegend !== false && !showBlocks() && !showPropertyKey(); const showCategoriesOnly = () => showCategories() && !showPhases() && !showBlocks(); /** Remount + swipe when left legend mode changes (phases ↔ Color/Scale ↔ blocks). */ const legendModeKey = computed<LeftLegendMode>(() => props.leftLegend ?? "phases"); const LEGEND_SWIPE_RANK: Record<LeftLegendMode, number> = { none: 0, phases: 1, property: 2, blocks: 3, }; const legendTransitionName = ref(props.legendSwipe || "aside-swipe-left"); watch( legendModeKey, (next, prev) => { if (prev == null || next === prev) { return; } const nextRank = LEGEND_SWIPE_RANK[next]; const prevRank = LEGEND_SWIPE_RANK[prev]; legendTransitionName.value = nextRank >= prevRank ? "aside-swipe-left" : "aside-swipe-right"; }, { flush: "sync" }, ); watch( () => props.legendSwipe, (name) => { if (name) { legendTransitionName.value = name; } }, { flush: "sync" }, ); function previewPhase(filter: LegendFilter | null) { emit("preview-phase", filter); } function isSelectedPhase(phase: ElementPhaseAtStp) { return props.selectedPhases.has(phase); } function isActivePhase(filter: LegendFilter) { if (filter.kind !== "phase") { return false; } if (isSelectedPhase(filter.key)) { return true; } return props.selectedPhases.size === 0 && areLegendFiltersEqual(props.phaseHover, filter); } function isSelectedBlock(block: ElectronBlock) { return props.selectedBlocks?.has(block) ?? false; } function isActiveBlock(block: ElectronBlock) { if (isSelectedBlock(block)) { return true; } const selected = props.selectedBlocks; if (selected && selected.size > 0) { return false; } return props.hoveredBlock === block; } </script> <template> <aside class="table-legend" :class="{ 'table-legend--wide': isWide, 'table-legend--phases-only': showPhases() && !showCategories(), 'table-legend--categories-only': showCategoriesOnly(), 'table-legend--property': showPropertyKey(), 'table-legend--blocks': showBlocks(), }" aria-label="Легенда таблицы" > <div class="table-legend__swap-host"> <Transition :name="legendTransitionName"> <div :key="legendModeKey" class="table-legend__swap"> <div v-if="showBlocks()" class="table-legend__states" @mouseleave="emit('hover-block', null)" > <button v-for="item in blockLegendItems" :key="item.block" type="button" class="table-legend__state table-legend__block" :data-block="item.block" :data-active="isActiveBlock(item.block) || undefined" :data-selected="isSelectedBlock(item.block) || undefined" :aria-pressed="isSelectedBlock(item.block)" @click="emit('toggle-block', item.block)" @mouseenter="emit('hover-block', item.block)" @focus="emit('hover-block', item.block)" @blur="emit('hover-block', null)" > <span class="table-legend__state-sample table-legend__block-sample" :data-block="item.block" > {{ item.block }} </span> <span class="table-legend__state-label table-legend__block-label" :data-block="item.block" > {{ item.label }} </span> </button> </div> <PropertyKey v-else-if="showPropertyKey()" :start-color="propertyStartColor ?? '#ffffff'" :end-color="propertyEndColor ?? '#f00000'" :unknown-color="propertyUnknownColor ?? '#e6e6e6'" :zero-color="propertyZeroColor" :show-zero="showPropertyZero" :zero-stop-percent="propertyZeroStopPercent" :disable-logarithmic="disablePropertyLogarithmic" :scale="propertyScale ?? 'lin'" @update:start-color="emit('update:propertyStartColor', $event)" @update:end-color="emit('update:propertyEndColor', $event)" @update:unknown-color="emit('update:propertyUnknownColor', $event)" @update:zero-color="emit('update:propertyZeroColor', $event)" @update:scale="emit('update:propertyScale', $event)" /> <div v-else-if="showPhases()" class="table-legend__states" > <button v-for="item in stateLegendItems" :key="item.key" type="button" class="table-legend__state" :data-active="isActivePhase(item.filter) || undefined" :data-selected="isSelectedPhase(item.key) || undefined" :aria-pressed="isSelectedPhase(item.key)" @click="emit('toggle-phase', item.key)" @mouseenter="previewPhase(item.filter)" @mouseleave="previewPhase(null)" > <span class="table-legend__state-sample" :data-state="item.key">{{ item.sample }}</span> <span class="table-legend__state-label" :data-state="item.key">{{ item.label }}</span> </button> </div> <ConfigBlocks v-if="showBlocks()" :atomic-number="selectedAtomicNumber ?? 1" :is-wide="isWide" /> <CategoryLegend v-if="showCategories()" :category-hover="categoryHover" :is-wide="isWide" :selected-categories="selectedCategories" :selected-groups="selectedGroups" @hover-legend="emit('hover-legend', $event)" @toggle-category="emit('toggle-category', $event)" @toggle-group="emit('toggle-group', $event)" /> </div> </Transition> </div> </aside> </template> <style scoped> .table-legend { --legend-state-surface: color-mix(in oklab, var(--table-surface) 92%, var(--color-surface)); --legend-selection-border: var(--table-selected-line); --legend-selection-surface: var(--table-selected-surface); display: grid; height: 100%; min-width: 0; grid-template-columns: minmax(0, 1fr); align-items: stretch; padding: 0; } .table-legend__swap-host { position: relative; display: grid; min-width: 0; width: 100%; height: 100%; overflow: hidden; } .table-legend__swap-host > * { grid-area: 1 / 1; min-width: 0; min-height: 0; width: 100%; } .table-legend__swap { display: grid; height: 100%; min-width: 0; width: 100%; grid-template-columns: auto minmax(0, 1fr); gap: var(--table-legend-split, clamp(0.78rem, calc(var(--table-cell-size) * 0.26), 1.35rem)); align-items: stretch; } .aside-swipe-left-enter-active, .aside-swipe-left-leave-active, .aside-swipe-right-enter-active, .aside-swipe-right-leave-active { transition: transform 0.38s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.3s ease; } /* Leave overlays the enter panel so host height stays stable (no post-swipe settle jump). */ .aside-swipe-left-leave-active, .aside-swipe-right-leave-active { position: absolute; inset: 0; z-index: 1; pointer-events: none; } .aside-swipe-left-enter-from { transform: translateX(108%); opacity: 0.35; } .aside-swipe-left-leave-to { transform: translateX(-42%); opacity: 0; } .aside-swipe-right-enter-from { transform: translateX(-108%); opacity: 0.35; } .aside-swipe-right-leave-to { transform: translateX(42%); opacity: 0; } .table-legend--phases-only { grid-template-columns: max-content; width: max-content; } .table-legend--phases-only .table-legend__swap { grid-template-columns: max-content; width: max-content; } .table-legend--categories-only .table-legend__swap { grid-template-columns: minmax(0, 1fr); } .table-legend--property { grid-template-columns: minmax(0, 1fr); width: 100%; max-width: 100%; height: 100%; min-height: 0; align-items: stretch; pointer-events: auto; /* Keep overlapping «Цвет»/«Масштаб» titles inside the legend cell. */ padding-top: 0.15rem; box-sizing: border-box; } .table-legend--property .table-legend__swap { grid-template-columns: minmax(0, 1fr); min-height: 0; overflow: hidden; } .table-legend--property :deep(.property-key) { width: 100%; height: 100%; min-height: 0; pointer-events: auto; } .table-legend__states { display: grid; width: max-content; max-width: 100%; grid-template-rows: repeat(4, minmax(0, 1fr)); gap: clamp(0.12rem, calc(var(--table-cell-size) * 0.024), 0.18rem); align-content: stretch; padding-top: clamp(0.1rem, calc(var(--table-cell-size) * 0.02), 0.22rem); } .table-legend__state { display: grid; min-width: 0; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: clamp(0.2rem, calc(var(--table-cell-size) * 0.04), 0.32rem); padding: 0; border: 0; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; } .table-legend__state-sample { display: inline-flex; align-items: center; justify-content: center; width: clamp(1.42rem, calc(var(--table-cell-size) * 0.32), 1.66rem); height: clamp(1.42rem, calc(var(--table-cell-size) * 0.32), 1.66rem); border: 1px solid var(--legend-state-border, var(--table-line)); border-radius: 0.18rem; background: var(--legend-state-surface); font-family: var(--table-font-ui); font-size: clamp(0.74rem, calc(var(--table-cell-size) * 0.148), 0.9rem); font-weight: 700; letter-spacing: -0.04em; transition: border-color 180ms ease, background-color 180ms ease, box-shadow 180ms ease; } .table-legend__state-label { font-family: var(--table-font-ui); font-size: clamp(0.72rem, calc(var(--table-cell-size) * 0.14), 0.86rem); font-weight: 520; line-height: 1.04; letter-spacing: -0.02em; white-space: nowrap; transition: color 180ms ease; } .table-legend__block-label { font-size: clamp(0.78rem, calc(var(--table-cell-size) * 0.155), 0.98rem); font-weight: 600; line-height: 1.1; } .table-legend__swap > :deep(.config-blocks) { min-width: 0; max-width: 100%; width: 100%; height: 100%; } .table-legend__state-sample[data-state="solid"] { color: var(--table-ink-strong); } .table-legend__state-label[data-state="solid"] { color: var(--table-ink-strong); } .table-legend__state-sample[data-state="liquid"], .table-legend__state-label[data-state="liquid"] { color: var(--table-state-liquid); } .table-legend__state-sample[data-state="gas"], .table-legend__state-label[data-state="gas"] { color: var(--table-state-gas); } .table-legend__state-sample[data-state="unknown"], .table-legend__state-label[data-state="unknown"] { color: var(--table-state-unknown); } .table-legend__block-sample[data-block="s"], .table-legend__block-label[data-block="s"] { color: color-mix(in oklab, var(--block-s) 35%, var(--table-ink-strong)); } .table-legend__block-sample[data-block="p"], .table-legend__block-label[data-block="p"] { color: color-mix(in oklab, var(--block-p) 35%, var(--table-ink-strong)); } .table-legend__block-sample[data-block="d"], .table-legend__block-label[data-block="d"] { color: color-mix(in oklab, var(--block-d) 35%, var(--table-ink-strong)); } .table-legend__block-sample[data-block="f"], .table-legend__block-label[data-block="f"] { color: color-mix(in oklab, var(--block-f) 35%, var(--table-ink-strong)); } .table-legend__block-sample[data-block="s"] { background: var(--block-s); color: #1b2430; } .table-legend__block-sample[data-block="p"] { background: var(--block-p); color: #1b2430; } .table-legend__block-sample[data-block="d"] { background: var(--block-d); color: #1b2430; } .table-legend__block-sample[data-block="f"] { background: var(--block-f); color: #1b2430; } .table-legend__state:hover .table-legend__state-sample, .table-legend__state[data-active="true"] .table-legend__state-sample { border-color: var(--legend-selection-border); background: color-mix(in oklab, var(--legend-selection-surface) 58%, var(--legend-state-surface)); } .table-legend__block:hover .table-legend__block-sample[data-block="s"], .table-legend__block[data-active="true"] .table-legend__block-sample[data-block="s"] { background: var(--block-s); border-color: var(--legend-selection-border); } .table-legend__block:hover .table-legend__block-sample[data-block="p"], .table-legend__block[data-active="true"] .table-legend__block-sample[data-block="p"] { background: var(--block-p); border-color: var(--legend-selection-border); } .table-legend__block:hover .table-legend__block-sample[data-block="d"], .table-legend__block[data-active="true"] .table-legend__block-sample[data-block="d"] { background: var(--block-d); border-color: var(--legend-selection-border); } .table-legend__block:hover .table-legend__block-sample[data-block="f"], .table-legend__block[data-active="true"] .table-legend__block-sample[data-block="f"] { background: var(--block-f); border-color: var(--legend-selection-border); } .table-legend__state[data-selected="true"] .table-legend__state-sample { box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--legend-selection-border) 72%, transparent); } .table-legend__state:hover .table-legend__state-label, .table-legend__state[data-active="true"] .table-legend__state-label { color: color-mix(in oklab, currentColor 90%, var(--table-ink-strong)); } @media (max-width: 1180px) { .table-legend { grid-template-columns: 1fr; gap: 0.6rem; } .table-legend__states { grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-rows: none; padding-top: 0; } } @media (max-width: 860px) { .table-legend__states { grid-template-columns: repeat(2, minmax(0, 1fr)); } } .table-legend--wide { /* Single column: columns live inside .table-legend__swap (not on this wrapper). */ height: auto; min-width: 0; width: 100%; padding: 0; font-size: calc(var(--table-wide-legend-font, 0.78rem) * 1.3); grid-template-columns: minmax(0, 1fr); gap: 0; align-items: stretch; } .table-legend--wide .table-legend__swap-host { /* Avoid height:100% under height:auto — causes a second layout pass / settle jump. */ height: auto; } .table-legend--wide .table-legend__swap { height: auto; gap: var(--table-wide-legend-split, 0.85rem); align-items: end; } .table-legend--wide.table-legend--phases-only { grid-template-columns: max-content; width: max-content; } .table-legend--wide.table-legend--categories-only { grid-template-columns: minmax(0, 1fr); } .table-legend--wide.table-legend--property { grid-template-columns: minmax(0, 1fr); width: 100%; max-width: 100%; height: 100%; align-items: stretch; } .table-legend--wide.table-legend--property .table-legend__swap-host, .table-legend--wide.table-legend--property .table-legend__swap { height: 100%; } .table-legend--wide.table-legend--blocks { grid-template-columns: minmax(0, 1fr); width: 100%; max-width: 100%; height: 100%; min-height: 0; align-items: stretch; } .table-legend--wide.table-legend--blocks .table-legend__swap-host, .table-legend--wide.table-legend--blocks .table-legend__swap { height: 100%; min-height: 0; } .table-legend--wide.table-legend--blocks .table-legend__swap { align-items: stretch; } .table-legend--wide.table-legend--blocks .table-legend__states { align-content: start; align-self: stretch; } .table-legend--wide .table-legend__states { grid-template-rows: repeat(4, auto); align-content: end; gap: clamp(0.55rem, calc(var(--table-cell-size) * 0.2), 1rem); padding-top: 0; } .table-legend--wide .table-legend__state { gap: clamp(0.16rem, calc(var(--table-cell-size) * 0.045), 0.24rem); } .table-legend--wide .table-legend__state-sample { width: clamp(1.2rem, calc(var(--table-cell-size) * 0.42), 1.55rem); height: clamp(1.2rem, calc(var(--table-cell-size) * 0.42), 1.55rem); font-size: clamp(0.62rem, calc(var(--table-cell-size) * 0.2), 0.82rem); } .table-legend--wide .table-legend__state-label { font-size: clamp(0.58rem, calc(var(--table-cell-size) * 0.18), 0.78rem); } .table-legend--wide .table-legend__block-label { font-size: clamp(0.68rem, calc(var(--table-cell-size) * 0.22), 0.9rem); } </style>