@@ -3,6 +3,9 @@ import { Teleport, computed, inject, nextTick, onMounted, onUnmounted, ref, watc
import { useRoute , useRouter } from 'vue-router'
import Sortable from 'sortablejs'
import * as htmlToImage from 'html-to-image'
import SvgIcon from '../components/SvgIcon.vue'
import addColumnRightIcon from '../assets/icons/add_column_right.svg'
import addRowBelowIcon from '../assets/icons/add_row_below.svg'
import { api } from '../lib/api'
import { toApiUrl } from '../lib/runtime'
import { useAuthStore } from '../stores/auth'
@@ -19,11 +22,12 @@ const tierListId = computed(() => route.params.tierListId)
const previewMode = computed ( ( ) => route . query . preview === '1' )
const gameName = ref ( '' )
const columns = ref ( [ { id : 'col-1' , name : '' } ] )
const groups = ref ( [
{ id : 'gS' , name : 'S' , itemIds : [ ] } ,
{ id : 'gA' , name : 'A' , itemIds : [ ] } ,
{ id : 'gB' , name : 'B' , itemIds : [ ] } ,
{ id : 'gC' , name : 'C' , itemIds : [ ] } ,
{ id : 'gS' , name : 'S' , itemIds : [ ] , cells : [ [ ] ] } ,
{ id : 'gA' , name : 'A' , itemIds : [ ] , cells : [ [ ] ] } ,
{ id : 'gB' , name : 'B' , itemIds : [ ] , cells : [ [ ] ] } ,
{ id : 'gC' , name : 'C' , itemIds : [ ] , cells : [ [ ] ] } ,
] )
const pool = ref ( [ ] )
@@ -47,7 +51,9 @@ const templateRequestDraftDescription = ref('')
const templateRequestSaveToMyTierList = ref ( true )
const isDeleteModalOpen = ref ( false )
const isGroupDeleteModalOpen = ref ( false )
const isColumnDeleteModalOpen = ref ( false )
const pendingRemoveGroupId = ref ( '' )
const pendingRemoveColumnIndex = ref ( - 1 )
const ownerId = ref ( '' )
const authorName = ref ( '' )
const authorAccountName = ref ( '' )
@@ -154,38 +160,107 @@ function setIconSize(nextSize) {
iconSize . value = nextSize
}
function removeItemFromGroup ( groupId , itemId ) {
if ( ! canEdit . value || ! groupId || ! itemId ) return
function getGroupCellIds ( group , columnIndex ) {
return Array . isArray ( group ? . cells ? . [ columnIndex ] ) ? group . cells [ columnIndex ] : [ ]
}
function syncGroupItemIds ( group ) {
group . itemIds = ( group . cells || [ ] ) . flat ( )
}
function normalizeLoadedColumns ( rawGroups ) {
const fromGroup = Array . isArray ( rawGroups ) ? rawGroups . find ( ( group ) => Array . isArray ( group ? . columnNames ) && group . columnNames . length ) : null
const rawColumns = Array . isArray ( fromGroup ? . columnNames ) ? fromGroup . columnNames : [ ]
const cellCount = Math . max ( 1 , ... ( Array . isArray ( rawGroups ) ? rawGroups . map ( ( group ) => ( Array . isArray ( group ? . cells ) ? group . cells . length : 0 ) ) : [ 0 ] ) )
const size = Math . max ( rawColumns . length || 0 , cellCount )
return Array . from ( { length : size || 1 } , ( _ , index ) => ( {
id : rawColumns [ index ] ? . id || ` col- ${ index + 1 } ` ,
name : typeof rawColumns [ index ] ? . name === 'string' ? rawColumns [ index ] . name . slice ( 0 , 16 ) : '' ,
} ) )
}
function normalizeLoadedGroups ( rawGroups , nextColumns = columns . value ) {
if ( ! Array . isArray ( rawGroups ) || ! rawGroups . length ) {
return [
{ id : 'gS' , name : 'S' , itemIds : [ ] , cells : nextColumns . map ( ( ) => [ ] ) } ,
{ id : 'gA' , name : 'A' , itemIds : [ ] , cells : nextColumns . map ( ( ) => [ ] ) } ,
{ id : 'gB' , name : 'B' , itemIds : [ ] , cells : nextColumns . map ( ( ) => [ ] ) } ,
{ id : 'gC' , name : 'C' , itemIds : [ ] , cells : nextColumns . map ( ( ) => [ ] ) } ,
]
}
return rawGroups . map ( ( group , index ) => {
const cells = Array . from ( { length : nextColumns . length } , ( _ , cellIndex ) => {
if ( Array . isArray ( group ? . cells ? . [ cellIndex ] ) ) return [ ... group . cells [ cellIndex ] ]
if ( cellIndex === 0 && Array . isArray ( group ? . itemIds ) ) return [ ... group . itemIds ]
return [ ]
} )
return {
id : typeof group ? . id === 'string' && group . id ? group . id : ` g- ${ index + 1 } ` ,
name : typeof group ? . name === 'string' && group . name ? group . name . slice ( 0 , 16 ) : 'Tier' ,
itemIds : cells . flat ( ) ,
cells ,
}
} )
}
function buildGroupPayload ( ) {
return groups . value . map ( ( group ) => ( {
id : group . id ,
name : group . name ,
itemIds : ( group . cells || [ ] ) . flat ( ) ,
cells : ( group . cells || [ ] ) . map ( ( cell ) => [ ... cell ] ) ,
columnNames : columns . value . map ( ( column ) => ( { id : column . id , name : column . name || '' } ) ) ,
} ) )
}
function removeItemFromGroup ( groupId , columnIndex , itemId ) {
if ( ! canEdit . value || ! groupId || columnIndex == null || ! itemId ) return
const targetGroup = groups . value . find ( ( group ) => group . id === groupId )
if ( ! targetGroup ) return
if ( ! targetGroup . itemIds . includes ( itemId ) ) return
targetGroup . itemIds = targetGroup . itemIds . filter ( ( id ) => id !== itemId )
const nextCells = [ ... targetGroup . cells ]
nextCells [ columnIndex ] = getGroupCellIds ( targetGroup , columnIndex ) . filter ( ( id ) => id !== itemId )
targetGroup . cells = nextCells
syncGroupItemIds ( targetGroup )
pool . value = [ itemId , ... pool . value . filter ( ( id ) => id !== itemId ) ]
}
function setGroupDropEl ( groupId , el ) {
function setGroupDropEl ( groupId , columnIndex , el ) {
const key = ` ${ groupId } :: ${ columnIndex } `
if ( ! el ) {
delete groupDropEls . value [ groupId ]
delete groupDropEls . value [ key ]
return
}
groupDropEls . value [ groupId ] = el
groupDropEls . value [ key ] = el
}
function getListByContainer ( containerEl ) {
if ( ! containerEl ) return { type : null , groupId : null }
if ( ! containerEl ) return { type : null , groupId : null , columnIndex : null }
const t = containerEl . getAttribute ( 'data-list-type' )
if ( t === 'pool' ) return { type : 'pool' , groupId : null }
if ( t === 'group' ) return { type : 'group' , groupId : containerEl . getAttribute ( 'data-group-id' ) }
return { type : null , groupId : null }
if ( t === 'pool' ) return { type : 'pool' , groupId : null , columnIndex : null }
if ( t === 'group' ) {
return {
type : 'group' ,
groupId : containerEl . getAttribute ( 'data-group-id' ) ,
columnIndex : Number ( containerEl . getAttribute ( 'data-column-index' ) ) ,
}
}
return { type : null , groupId : null , columnIndex : null }
}
function normalizeSort ( containerEl ) {
const ids = Array . from ( containerEl . querySelectorAll ( '[data-item-id]' ) ) . map ( ( n ) => n . getAttribute ( 'data-item-id' ) )
const meta = getListByContainer ( containerEl )
if ( meta . type === 'pool' ) pool . value = ids
if ( meta . type === 'pool' ) {
pool . value = ids
return
}
if ( meta . type === 'group' ) {
const g = groups . value . find ( ( x ) => x . id === meta . groupId )
if ( g ) g . itemIds = ids
if ( ! g || ! Number . isInteger ( meta . columnIndex ) ) return
const nextCells = [ ... g . cells ]
nextCells [ meta . columnIndex ] = ids
g . cells = nextCells
syncGroupItemIds ( g )
}
}
@@ -224,7 +299,7 @@ async function initSortables() {
onAdd : ( ) => normalizeSort ( poolEl . value ) ,
} )
dropSortables . value = Object . entries ( groupDropEls . value ) . map ( ( [ gid , el ] ) =>
dropSortables . value = Object . entries ( groupDropEls . value ) . map ( ( [ , el ] ) =>
Sortable . create ( el , {
group : 'tier-items' ,
animation : 160 ,
@@ -257,32 +332,87 @@ async function syncSortables() {
}
}
function createGroupName ( ) {
function createGroupName ( index = groups . value . length ) {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
const index = groups . value . length
if ( index < alphabet . length ) return alphabet [ index ]
return ` Tier ${ index + 1 } `
}
function createColumnName ( index = columns . value . length ) {
return ` 열 ${ index + 1 } `
}
async function addGroup ( ) {
groups . value = [
... groups . value ,
{
id : ` g- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 16 ) . slice ( 2 , 8 ) } ` ,
id : ` g- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 16 ) . slice ( 2 , 8 ) } ` ,
name : createGroupName ( ) ,
itemIds : [ ] ,
cells : columns . value . map ( ( ) => [ ] ) ,
} ,
]
await syncSortables ( )
}
async function addColumn ( ) {
columns . value = [
... columns . value ,
{ id : ` col- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 16 ) . slice ( 2 , 8 ) } ` , name : createColumnName ( ) } ,
]
groups . value = groups . value . map ( ( group ) => ( {
... group ,
cells : [ ... group . cells , [ ] ] ,
itemIds : [ ... group . itemIds ] ,
} ) )
await syncSortables ( )
}
async function removeColumn ( columnIndex ) {
if ( ! canEdit . value || columns . value . length <= 1 ) return
const nextColumns = columns . value . filter ( ( _ , index ) => index !== columnIndex )
groups . value = groups . value . map ( ( group ) => {
const nextCells = group . cells . filter ( ( _ , index ) => index !== columnIndex )
const removed = Array . isArray ( group . cells [ columnIndex ] ) ? group . cells [ columnIndex ] : [ ]
if ( nextCells [ 0 ] && removed . length ) nextCells [ 0 ] = [ ... removed , ... nextCells [ 0 ] ]
const nextGroup = { ... group , cells : nextCells }
syncGroupItemIds ( nextGroup )
return nextGroup
} )
Object . keys ( groupDropEls . value ) . forEach ( ( key ) => {
if ( key . endsWith ( ` :: ${ columnIndex } ` ) ) delete groupDropEls . value [ key ]
} )
columns . value = nextColumns
await syncSortables ( )
}
function openColumnDeleteModal ( columnIndex ) {
if ( ! canEdit . value || columns . value . length <= 1 ) return
pendingRemoveColumnIndex . value = columnIndex
isColumnDeleteModalOpen . value = true
}
function closeColumnDeleteModal ( ) {
isColumnDeleteModalOpen . value = false
pendingRemoveColumnIndex . value = - 1
}
async function confirmRemoveColumn ( ) {
const columnIndex = pendingRemoveColumnIndex . value
closeColumnDeleteModal ( )
if ( columnIndex < 0 ) return
await removeColumn ( columnIndex )
}
async function performRemoveGroup ( groupId ) {
if ( groups . value . length <= 1 ) return
const target = groups . value . find ( ( group ) => group . id === groupId )
if ( ! target ) return
pool . value = [ ... target . itemIds , ... pool . value ]
groups . value = groups . value . filter ( ( group ) => group . id !== groupId )
delete groupDropEls . value [ groupId ]
Object . keys ( groupDropEls . value ) . forEach ( ( key ) => {
if ( key . startsWith ( ` ${ groupId } :: ` ) ) delete groupDropEls . value [ key ]
} )
await syncSortables ( )
}
@@ -469,10 +599,14 @@ async function uploadPendingCustomItems() {
}
itemsById . value = nextItemsById
pool . value = pool . value . map ( ( currentId ) => ( currentId === item . id ? uploaded . id : currentId ) )
groups . value = groups . value . map ( ( group ) => ( {
... group ,
itemIds : group . itemIds . map ( ( currentId ) => ( currentId === item . id ? uploaded . id : currentId ) ) ,
} ) )
groups . value = groups . value . map ( ( group ) => {
const nextGroup = {
... group ,
cells : group . cells . map ( ( cell ) => cell . map ( ( currentId ) => ( currentId === item . id ? uploaded . id : currentId ) ) ) ,
}
syncGroupItemIds ( nextGroup )
return nextGroup
} )
}
}
@@ -501,7 +635,7 @@ function buildPayload(existingId) {
sourceTierListId : sourceTierListId . value || '' ,
sourceSnapshotTitle : sourceSnapshotTitle . value || '' ,
sourceSnapshotAuthor : sourceSnapshotAuthor . value || '' ,
groups : groups . value . map ( ( g ) => ( { id : g . id , name : g . name , itemIds : g . itemIds } ) ) ,
groups : buildGroupPayload ( ) ,
pool : Object . values ( itemsById . value ) ,
}
}
@@ -633,11 +767,7 @@ async function requestTemplate(type) {
isPublic : ! ! isPublic . value ,
showCharacterNames : ! ! showCharacterNames . value ,
saveToMyTierList : ! ! templateRequestSaveToMyTierList . value ,
groups : groups . value . map ( ( group ) => ( {
id : group . id ,
name : group . name ,
itemIds : [ ... group . itemIds ] ,
} ) ) ,
groups : buildGroupPayload ( ) ,
boardItems : Object . values ( itemsById . value ) ,
} )
@@ -727,12 +857,13 @@ onMounted(() => {
sourceSnapshotAuthor . value = t . sourceSnapshotAuthor || ''
favoriteCount . value = Number ( t . favoriteCount || 0 )
isFavorited . value = ! ! t . isFavorited
group s. value = t . groups
column s. value = normalizeLoadedColumns ( t. groups )
groups . value = normalizeLoadedGroups ( t . groups , columns . value )
const map = { }
; ( t . pool || [ ] ) . forEach ( ( it ) => ( map [ it . id ] = it ) )
itemsById . value = map
const grouped = new Set ( )
groups . value . forEach ( ( g ) => g . itemIds . forEach ( ( id ) => grouped . add ( id ) ) )
groups . value . forEach ( ( group ) => group . itemIds . forEach ( ( id ) => grouped . add ( id ) ) )
pool . value = Object . keys ( itemsById . value ) . filter ( ( id ) => ! grouped . has ( id ) )
} catch ( e ) {
error . value = '티어표를 불러오지 못했어요.'
@@ -757,13 +888,23 @@ onUnmounted(() => {
< div class = "previewOnly__sheet" >
< div class = "previewOnly__title" > { { effectiveTitle } } < / div >
< div v-if = "description" class="previewOnly__description" > {{ description }} < / div >
< div v-if = "columns.length > 1" class="previewOnly__columns" >
< div class = "previewOnly__columnsSpacer" aria -hidden = " true " > < / div >
< div class = "previewOnly__columnsGrid" : style = "{ '--column-count': columns.length }" >
< div v-for = "(column, columnIndex) in columns" :key="column.id" class="previewOnly__columnHeader" > {{ column.name | | ' 열 ' + ( columnIndex + 1 ) }} < / div >
< / div >
< / div >
< div class = "previewOnly__rows" >
< div v-for = "g in groups" :key="g.id" class="previewOnly__row" >
< div class = "previewOnly__label" > { { g . name } } < / div >
< div class = "previewOnly__drop" >
< div v-for = "id in g.itemId s" :key="id" class="previewOnly__cell " >
< img :src = "resolveItemSrc(itemsById[id])" class = "thumb" : alt = "itemsById[id]?.label || id" / >
< div v-i f = "showCharacterNames" class="itemNameOverlay" > {{ itemsById [ id ] ? .label | | id }} < / div >
< div class = "previewOnly__dropGrid" : style = "{ '--column-count': columns.length } " >
< div v-for = "(column, columnIndex) in column s" :key="column. id" class="previewOnly__dropColumn " >
< div class = "previewOnly__drop" >
< div v-for = "id in getGroupCellIds(g, columnIndex)" :key="id" class="previewOnly__cell" >
< img :src = "resolveItemSrc(itemsById[id])" class = "thumb" : alt = "itemsById[id]?.label || id" / >
< div v-if = "showCharacterNames" class="itemNameOverlay" > {{ itemsById [ id ] ? .label | | id }} < / div >
< / div >
< / div >
< / div >
< / div >
< / div >
@@ -886,13 +1027,26 @@ onUnmounted(() => {
< div v-if = "isGroupDeleteModalOpen" class="modalOverlay" @click.self="closeGroupDeleteModal" >
< div class = "modalCard" role = "dialog" aria -modal = " true " aria -labelledby = " deleteGroupTitle " >
< div id = "deleteGroupTitle" class = "modalCard__title" > 티어 라인 삭제 < / div >
< div id = "deleteGroupTitle" class = "modalCard__title" > 티어 행 삭제 < / div >
< div class = "modalCard__desc" >
이 라인 을 삭제하면 현재 들어 있는 아이템은 모두 아래 아이템 영역으로 이동합니다 . 삭제 후에도 아이템 자체는 유지돼요 .
이 행 을 삭제하면 현재 들어 있는 아이템은 모두 아래 아이템 영역으로 이동합니다 . 삭제 후에도 아이템 자체는 유지돼요 .
< / div >
< div class = "modalCard__actions" >
< button class = "btn btn--ghost" @click ="closeGroupDeleteModal" > 취소 < / button >
< button class = "btn btn--danger" @click ="confirmRemoveGroup" > 라인 삭제 < / button >
< button class = "btn btn--danger" @click ="confirmRemoveGroup" > 행 삭제 < / button >
< / div >
< / div >
< / div >
< div v-if = "isColumnDeleteModalOpen" class="modalOverlay" @click.self="closeColumnDeleteModal" >
< div class = "modalCard" role = "dialog" aria -modal = " true " aria -labelledby = " deleteColumnTitle " >
< div id = "deleteColumnTitle" class = "modalCard__title" > 티어 열 삭제 < / div >
< div class = "modalCard__desc" >
이 열을 삭제하면 현재 들어 있는 아이템은 모두 첫 번째 열로 이동합니다 . 삭제 후에도 아이템 자체는 유지돼요 .
< / div >
< div class = "modalCard__actions" >
< button class = "btn btn--ghost" @click ="closeColumnDeleteModal" > 취소 < / button >
< button class = "btn btn--danger" @click ="confirmRemoveColumn" > 열 삭제 < / button >
< / div >
< / div >
< / div >
@@ -904,7 +1058,7 @@ onUnmounted(() => {
< div class = "editorMain__title" > { { gameName || gameId } } < / div >
< div class = "editorMain__subtitle" >
< template v-if = "canEdit" >
그룹 이름/ 순서 변경과 아이템 드래그& 드롭이 가능합니다 .
행 / 열 이름과 순서를 바꾸고 아이템을 드래그해서 배치할 수 있어요 .
< / template >
< template v-else >
공개된 티어표를 보는 중입니다. 로그인한 작성자만 수정할 수 있어요.
@@ -921,7 +1075,12 @@ onUnmounted(() => {
< div ref = "boardEl" class = "board" >
< div v-if = "canEdit && !isExporting" class="boardTools" >
< div class = "boardTools__left" >
< button class = "btn btn--ghost" @click ="addGroup" > 티어 추가 < / button >
< button class = "boardActionIcon" type = "button" title = "행 추가" aria -label = " 행 추가 " @click ="addGroup" >
< SvgIcon :src = "addRowBelowIcon" :size = "18" / >
< / button >
< button class = "boardActionIcon" type = "button" title = "열 추가" aria -label = " 열 추가 " @click ="addColumn" >
< SvgIcon :src = "addColumnRightIcon" :size = "18" / >
< / button >
< / div >
< div class = "boardTools__right" >
< span class = "boardTools__label" > 아이콘 크기 < / span >
@@ -941,6 +1100,22 @@ onUnmounted(() => {
< div ref = "exportBoardEl" class = "exportBoard" : class = "{ 'exportBoard--active': isExporting }" >
< div v-if = "isExporting" class="exportBoard__title" > {{ effectiveTitle }} < / div >
< div v-if = "isExporting && description" class="exportBoard__description" > {{ description }} < / div >
< div v-if = "columns.length > 1" class="boardColumnsHeader" :class="{ 'boardColumnsHeader--export': isExporting }" >
< div class = "boardColumnsHeader__spacer" aria -hidden = " true " > < / div >
< div class = "boardColumnsHeader__grid" : style = "{ '--column-count': columns.length }" >
< div v-for = "(column, columnIndex) in columns" :key="column.id" class="boardColumnsHeader__cell" >
< template v-if = "isExporting" >
< div class = "boardColumnsHeader__name" > { { column . name || '열 ' + ( columnIndex + 1 ) } } < / div >
< / template >
< template v-else >
< div class = "columnHeader" >
< input v-model = "column.name" class="columnName" maxlength="16" placeholder="열 이름" / >
< button class = "columnRemoveText" type = "button" title = "열 삭제" aria -label = " 열 삭제 " : disabled = "columns.length <= 1" @click ="openColumnDeleteModal(columnIndex)" > × < / button >
< / div >
< / template >
< / div >
< / div >
< / div >
< div ref = "groupListEl" class = "rows" >
< div v-for = "g in groups" :key="g.id" class="row" >
< div class = "row__label" >
@@ -948,40 +1123,46 @@ onUnmounted(() => {
< div class = "row__exportName" > { { g . name } } < / div >
< / template >
< template v-else >
< span class = "grab" title = "드래그로 순서 변경" data -group -handle > ↕ < / span >
< input v-model = "g.name" class="groupName" maxlength="16" :readonly="!canEdit" / >
< button
v-if = "canEdit"
class = "rowRemoveText"
type = "button"
title = "티어 라인 삭제"
title = "행 삭제"
aria -label = " 행 삭제 "
: disabled = "groups.length <= 1"
@click ="openGroupDeleteModal(g.id)"
>
열 삭제
< / button >
< span class = "grab" title = "드래그로 순서 변경" data -group -handle > ↕ < / span >
< input v-model = "g.name" class="groupName" maxlength="16" :readonly="!canEdit" / >
< / template >
< / div >
< div
class = "row__drop"
:data-list-type = "'group'"
:data-group-id = "g.id"
: ref = "(el) => setGroupDropEl(g.id, el)"
>
< div v-if = "!isExporting" class="row__empty" v-show="g.itemIds.length === 0" > 여기로 드래그해서 배치 < / div >
< div v-for = "id in g.itemIds" :key="id" class="cell" :data-item-id="id" >
< img :src = "resolveItemSrc(itemsById[id])" class = "thumb" : alt = "itemsById[id]?.label || id" / >
< div v-if = "showCharacterNames" class="itemNameOverlay" > {{ itemsById [ id ] ? .label | | id }} < / div >
< button
v-if = "canEdit && !isExporting"
class = "cellRemoveBtn"
type = "button"
title = "아이템 빼내기"
@pointerdown.stop
@click.stop ="removeItemFromGroup(g.id, id)"
>
×
< / button >
< / template >
< / div >
< div class = "row__content" : style = "{ '--column-count': columns.length }" >
< div v-for = "(column, columnIndex) in columns" :key="column.id" class="row__column" >
< div
class = "row__drop"
:data-list-type = "'group'"
:data-group-id = "g.id"
:data-column-index = "columnIndex"
: ref = "(el) => setGroupDropEl(g.id, columnIndex, el)"
>
< div v-if = "!isExporting" class="row__empty" v-show="getGroupCellIds(g, columnIndex).length === 0" > 여기로 드래그해서 배치 < / div >
< div v-for = "id in getGroupCellIds(g, columnIndex)" :key="id" class="cell" :data-item-id="id" >
< img :src = "resolveItemSrc(itemsById[id])" class = "thumb" : alt = "itemsById[id]?.label || id" / >
< div v-if = "showCharacterNames" class="itemNameOverlay" > {{ itemsById [ id ] ? .label | | id }} < / div >
< button
v-if = "canEdit && !isExporting"
class = "cellRemoveBtn"
type = "button"
title = "아이템 빼내기"
@pointerdown.stop
@click.stop ="removeItemFromGroup(g.id, columnIndex, id)"
>
×
< / button >
< / div >
< / div >
< / div >
< / div >
< / div >
@@ -1210,15 +1391,42 @@ onUnmounted(() => {
line - height : 1.6 ;
opacity : 0.76 ;
}
. previewOnly _ _columns {
display : grid ;
grid - template - columns : 132 px 1 fr ;
gap : 10 px ;
margin - bottom : 10 px ;
}
. previewOnly _ _columnsGrid {
display : grid ;
grid - template - columns : repeat ( var ( -- column - count , 1 ) , minmax ( 0 , 1 fr ) ) ;
gap : 10 px ;
}
. previewOnly _ _columnHeader {
min - height : 20 px ;
font - size : 12 px ;
font - weight : 800 ;
text - align : center ;
opacity : 0.72 ;
}
. previewOnly _ _rows {
display : grid ;
gap : 10 px ;
}
. previewOnly _ _row {
display : grid ;
grid - template - columns : 180 px 1 fr ;
grid - template - columns : 132 px 1 fr ;
gap : 10 px ;
}
. previewOnly _ _dropGrid {
display : grid ;
grid - template - columns : repeat ( var ( -- column - count , 1 ) , minmax ( 0 , 1 fr ) ) ;
gap : 10 px ;
}
. previewOnly _ _dropColumn {
display : grid ;
gap : 8 px ;
}
. previewOnly _ _label {
display : grid ;
place - items : center ;
@@ -1526,6 +1734,51 @@ onUnmounted(() => {
. boardTools _ _left {
margin - right : auto ;
}
. boardActionIcon {
width : 40 px ;
height : 40 px ;
display : inline - flex ;
align - items : center ;
justify - content : center ;
border - radius : 12 px ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.12 ) ;
background : rgba ( 255 , 255 , 255 , 0.04 ) ;
color : rgba ( 255 , 255 , 255 , 0.9 ) ;
cursor : pointer ;
transition : background 160 ms ease , border - color 160 ms ease , color 160 ms ease ;
}
. boardActionIcon : hover {
background : rgba ( 96 , 165 , 250 , 0.12 ) ;
border - color : rgba ( 96 , 165 , 250 , 0.28 ) ;
color : rgba ( 255 , 255 , 255 , 0.98 ) ;
}
. boardColumnsHeader {
display : grid ;
grid - template - columns : 132 px 1 fr ;
gap : 10 px ;
margin - bottom : 10 px ;
}
. boardColumnsHeader _ _grid {
display : grid ;
grid - template - columns : repeat ( var ( -- column - count , 1 ) , minmax ( 0 , 1 fr ) ) ;
gap : 10 px ;
}
. boardColumnsHeader _ _cell {
min - width : 0 ;
position : relative ;
}
. boardColumnsHeader _ _name {
min - height : 38 px ;
display : flex ;
align - items : center ;
justify - content : center ;
padding : 0 12 px ;
text - align : center ;
font - size : 12 px ;
line - height : 1.2 ;
font - weight : 800 ;
opacity : 0.74 ;
}
. boardTools _ _label {
font - size : 13 px ;
opacity : 0.76 ;
@@ -1592,7 +1845,7 @@ onUnmounted(() => {
}
. row {
display : grid ;
grid - template - columns : 180 px 1 fr ;
grid - template - columns : 132 px 1 fr ;
gap : 10 px ;
align - items : stretch ;
}
@@ -1602,24 +1855,86 @@ onUnmounted(() => {
background : rgba ( 255 , 255 , 255 , 0.08 ) ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.12 ) ;
display : flex ;
gap : 8 px ;
align - items : center ;
justify - content : center ;
padding : 10 px 12 px 30 px ;
padding : 14 px 28 px ;
font - weight : 900 ;
overflow : hidden ;
}
. grab {
cursor : grab ;
opacity : 0.85 ;
width : 26 px ;
height : 26 px ;
. row _ _content {
display : grid ;
grid - template - columns : repeat ( var ( -- column - count , 1 ) , minmax ( 0 , 1 fr ) ) ;
gap : 10 px ;
}
. row _ _column {
display : grid ;
gap : 8 px ;
min - width : 0 ;
}
. columnHeader {
position : relative ;
display : flex ;
align - items : center ;
min - height : 38 px ;
padding : 0 28 px ;
}
. columnName {
width : 100 % ;
border : 0 ;
border - bottom : 1 px solid rgba ( 255 , 255 , 255 , 0.12 ) ;
background : transparent ;
color : rgba ( 255 , 255 , 255 , 0.88 ) ;
padding : 4 px 0 ;
text - align : center ;
font - size : 12 px ;
line - height : 1.2 ;
font - weight : 800 ;
outline : none ;
}
. columnName : : placeholder {
color : rgba ( 255 , 255 , 255 , 0.34 ) ;
}
. columnRemoveText {
position : absolute ;
top : 50 % ;
right : 0 ;
transform : translateY ( - 50 % ) ;
width : 20 px ;
height : 20 px ;
display : grid ;
place - items : center ;
border - radius : 10 px ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.14 ) ;
padding : 0 ;
border : 0 ;
border - radius : 999 px ;
background : transparent ;
color : rgba ( 255 , 255 , 255 , 0.56 ) ;
font - size : 16 px ;
line - height : 1 ;
font - weight : 800 ;
cursor : pointer ;
}
. columnRemoveText : hover {
color : rgba ( 255 , 255 , 255 , 0.92 ) ;
background : rgba ( 255 , 255 , 255 , 0.06 ) ;
}
. columnRemoveText : disabled {
opacity : 0.32 ;
cursor : not - allowed ;
}
. grab {
position : absolute ;
top : 10 px ;
left : 10 px ;
cursor : grab ;
opacity : 0.72 ;
width : 22 px ;
height : 22 px ;
display : grid ;
place - items : center ;
border - radius : 8 px ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ) ;
background : rgba ( 0 , 0 , 0 , 0.16 ) ;
flex : 0 0 auto ;
font - size : 12 px ;
}
. groupName {
width : 100 % ;
@@ -1627,7 +1942,7 @@ onUnmounted(() => {
background : rgba ( 0 , 0 , 0 , 0.18 ) ;
color : rgba ( 255 , 255 , 255 , 0.92 ) ;
border - radius : 10 px ;
padding : 6 px 8 px ;
padding : 8 px 10 px ;
font - weight : 900 ;
text - align : center ;
outline : none ;
@@ -1635,19 +1950,25 @@ onUnmounted(() => {
}
. rowRemoveText {
position : absolute ;
right : 12 px ;
bottom : 10 px ;
top : 10 px ;
right : 10 px ;
width : 20 px ;
height : 20 px ;
display : grid ;
place - items : center ;
padding : 0 ;
border : 0 ;
border - radius : 999 px ;
background : transparent ;
color : rgba ( 255 , 255 , 255 , 0.6 ) ;
cursor : pointer ;
font - size : 12 px ;
font - size : 16 px ;
line - height : 1 ;
font - weight : 800 ;
}
. rowRemoveText : hover {
color : rgba ( 255 , 255 , 255 , 0.9 ) ;
color : rgba ( 255 , 255 , 255 , 0.92 ) ;
background : rgba ( 255 , 255 , 255 , 0.06 ) ;
}
. rowRemoveText : disabled {
opacity : 0.32 ;
@@ -1689,17 +2010,21 @@ onUnmounted(() => {
. itemNameOverlay {
position : absolute ;
inset : auto 0 0 0 ;
padd ing : 16 px 8 px 6 px ;
m in- height : 2 6px ;
padding : 18 px 8 px 6 px ;
border - radius : 0 0 10 px 10 px ;
background : linear - gradient ( 180 deg , rgba ( 7 , 10 , 18 , 0 ) , rgba ( 7 , 10 , 18 , 0.92 ) ) ;
color : rgba ( 255 , 255 , 255 , 0.96 ) ;
font - size : 11 px ;
line - height : 1.25 ;
line - height : 1.2 ;
font - weight : 800 ;
text - align : center ;
white - space : nowrap ;
overflow : hidden ;
text - overflow : ellipsis ;
display : flex ;
align - items : flex - end ;
justify - content : center ;
pointer - events : none ;
}
. cellRemoveBtn {
@@ -2043,6 +2368,9 @@ onUnmounted(() => {
. editorCanvas {
grid - template - columns : 1 fr ;
}
. row _ _content {
grid - template - columns : 1 fr ;
}
. row {
grid - template - columns : 150 px 1 fr ;
}
@@ -2074,7 +2402,18 @@ onUnmounted(() => {
. previewOnly {
padding : 14 px ;
}
. previewOnly _ _row {
. previewOnly _ _columns ,
. previewOnly _ _row ,
. boardColumnsHeader ,
. row {
grid - template - columns : 1 fr ;
}
. previewOnly _ _columnsSpacer ,
. boardColumnsHeader _ _spacer {
display : none ;
}
. previewOnly _ _dropGrid ,
. boardColumnsHeader _ _grid {
grid - template - columns : 1 fr ;
}
. pool {