main_laptop_1 -> main byeonghap (4 hwangyeong 585 commits) #12
@@ -214,12 +214,19 @@ function storageFor(key: string): Storage {
|
||||
return key.startsWith("aislo:pref:") ? window.localStorage : window.sessionStorage;
|
||||
}
|
||||
|
||||
/** 위와 같은 판단을 **키만 들고 저장소를 직접 만지는 화면 공용 부품**에 내준다
|
||||
* (`ui_template_resizer`·`ui_template_overlay`·`ui_template_overlay_drag`).
|
||||
* 그 부품들은 등록표에서 받은 키로 높이·열림·자리를 저장하는데, 여기를 안 거치면
|
||||
* 취향만 세션에 남아 탭을 새로 열 때마다 초기화된다. */
|
||||
export function storageOf(key: string): Storage {
|
||||
return storageFor(key);
|
||||
/**
|
||||
* **키만 아는 자리**를 위한 읽기·쓰기 창구 — 등록표 이름 대신 완성된 키를 든 곳이 쓴다
|
||||
* (`ui_template_resizer`·`_overlay`·`_overlay_drag`, 조정창 저장소).
|
||||
*
|
||||
* 저장소를 직접 고르게 두면 두 가지가 샜다 — 취향이 세션에 남아 탭마다 초기화됐고
|
||||
* (2026-09-07), 계정 올려보내기도 안 걸렸다. **저장소 선택과 올려보내기를 여기 한 곳**에 둔다.
|
||||
*/
|
||||
export function readByKey(key: string): string | null {
|
||||
return readRaw(key);
|
||||
}
|
||||
|
||||
export function writeByKey(key: string, value: string | null): void {
|
||||
writeRaw(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,6 +254,72 @@ function liftPrefsToLocalStorage(): void {
|
||||
|
||||
liftPrefsToLocalStorage();
|
||||
|
||||
/* ── 계정에 붙는 화면 취향 (2026-09-07 사용자 승인) ─────────────────────────────
|
||||
* 브라우저에 남긴 값 위에 **계정 값을 얹는다** — PC 를 바꿔도 같은 배치가 되게.
|
||||
* 규칙 셋:
|
||||
* · 서버가 없거나 못 읽으면 **로컬 값으로 그대로 돈다**(계획서 원문).
|
||||
* · 서버에 있는 키만 덮는다 — 서버가 비었다고 로컬을 지우지 않는다.
|
||||
* · 취향이 바뀌면 잠시 모아 한 번에 올린다. **설계값은 여기 오지 않는다** —
|
||||
* 자동저장 금지(CLAUDE.md 5장)는 설계값 규칙이고, 화면 배치는 [저장] 단추가 없다.
|
||||
* ------------------------------------------------------------------------- */
|
||||
const PREFS_ENDPOINT = "/api/dashboard/me/ui-prefs";
|
||||
const PREFS_PUSH_DELAY_MS = 1500;
|
||||
let applyingServerPrefs = false;
|
||||
let prefsPushTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function localPrefs(): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
try {
|
||||
for (const key of Object.keys(window.localStorage)) {
|
||||
if (!key.startsWith("aislo:pref:")) continue;
|
||||
const value = window.localStorage.getItem(key);
|
||||
if (value !== null) out[key] = value;
|
||||
}
|
||||
} catch {
|
||||
/* 저장소가 막힌 브라우저 — 올릴 것이 없다. */
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 취향이 바뀌면 잠시 모아 한 번에 올린다. 실패는 조용히 넘긴다(다음 변경 때 다시 올라간다). */
|
||||
function pushUiPrefs(): void {
|
||||
if (applyingServerPrefs) return;
|
||||
if (prefsPushTimer) clearTimeout(prefsPushTimer);
|
||||
prefsPushTimer = setTimeout(() => {
|
||||
prefsPushTimer = null;
|
||||
void fetch(PREFS_ENDPOINT, {
|
||||
method: "PUT",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ prefs: localPrefs() }),
|
||||
}).catch(() => undefined);
|
||||
}, PREFS_PUSH_DELAY_MS);
|
||||
}
|
||||
|
||||
/** 계정에 저장된 취향을 받아 로컬에 얹는다 — 로그인 뒤 한 번. */
|
||||
export async function syncUiPrefs(): Promise<void> {
|
||||
let stored: Record<string, unknown> = {};
|
||||
try {
|
||||
const response = await fetch(PREFS_ENDPOINT, { credentials: "include" });
|
||||
if (!response.ok) return;
|
||||
const body = (await response.json()) as { prefs?: Record<string, unknown> };
|
||||
stored = body.prefs ?? {};
|
||||
} catch {
|
||||
return; // 서버가 없으면 로컬 값으로 그대로 돈다.
|
||||
}
|
||||
applyingServerPrefs = true;
|
||||
try {
|
||||
for (const [key, value] of Object.entries(stored)) {
|
||||
if (!key.startsWith("aislo:pref:") || typeof value !== "string") continue;
|
||||
window.localStorage.setItem(key, value);
|
||||
}
|
||||
} catch {
|
||||
/* 저장소가 막힌 브라우저 — 이번 세션은 화면 메모리로 돈다. */
|
||||
} finally {
|
||||
applyingServerPrefs = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 저장소 접근은 막힌 브라우저(사생활 보호)에서 던진다 — 전부 조용히 넘긴다. */
|
||||
function readRaw(key: string): string | null {
|
||||
try {
|
||||
@@ -264,6 +337,7 @@ function writeRaw(key: string, value: string | null): void {
|
||||
} catch {
|
||||
/* 무시 — 값은 화면 메모리에 남는다. */
|
||||
}
|
||||
if (key.startsWith("aislo:pref:")) pushUiPrefs();
|
||||
}
|
||||
|
||||
/** 옛 키에 남은 값을 새 키로 한 번 옮긴다(옮기고 나면 옛 키는 지운다). */
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { syncUiPrefs } from "../A00_Common/b_page_state";
|
||||
|
||||
export interface ApiResult {
|
||||
status: string;
|
||||
@@ -42,10 +43,14 @@ export function verifyLogin(email: string, otpCode: string): Promise<ApiResult>
|
||||
에서는 즉시 버려 다음 조회가 서버를 다시 본다. */
|
||||
const SESSION_CACHE_MS = 5000;
|
||||
let sessionCache: { at: number; value: Promise<SessionUser | null> } | null = null;
|
||||
/** 계정 취향 받아오기는 한 번만 — 세션 조회는 화면마다 여러 번 불린다. */
|
||||
let uiPrefsSynced = false;
|
||||
|
||||
/** 세션 캐시를 버린다 — 로그인·로그아웃처럼 상태가 바뀌는 자리에서 부른다. */
|
||||
export function clearSessionCache(): void {
|
||||
sessionCache = null;
|
||||
// 로그인·로그아웃이면 다음 세션에서 취향을 다시 받아 온다(다른 계정일 수 있다).
|
||||
uiPrefsSynced = false;
|
||||
}
|
||||
|
||||
function sessionOnce(): Promise<SessionUser | null> {
|
||||
@@ -56,6 +61,12 @@ function sessionOnce(): Promise<SessionUser | null> {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/session`, { credentials: "include" });
|
||||
if (!response.ok) return null;
|
||||
const data = (await response.json()) as { status: string; user?: SessionUser };
|
||||
// 로그인이 확인된 첫 순간에 계정 취향을 한 번 받아 온다 — PC 를 바꿔도 같은 배치가
|
||||
// 되게(2026-09-07). 실패해도 화면은 브라우저에 남은 값으로 그대로 돈다.
|
||||
if (data.user && !uiPrefsSynced) {
|
||||
uiPrefsSynced = true;
|
||||
void syncUiPrefs();
|
||||
}
|
||||
return data.user ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -520,3 +520,38 @@ async def get_system_resources(days: int = 30) -> dict[str, Any]:
|
||||
(bucket_seconds, bucket_seconds, since, bucket_seconds),
|
||||
)
|
||||
return {"current": current, "history": list(await cursor.fetchall()), "stats": current}
|
||||
|
||||
|
||||
async def get_user_ui_prefs(user_id: int) -> dict[str, Any]:
|
||||
"""계정에 붙은 화면 취향(배치·표시). 없으면 빈 묶음.
|
||||
|
||||
설계값이 아니라 **패널 높이·접힘 같은 배치 값**만 담는다(표 `user_ui_prefs` 주석 참조).
|
||||
읽기 실패는 비치명 — 화면은 브라우저에 남은 값으로 그대로 돈다.
|
||||
"""
|
||||
pool = get_db_pool()
|
||||
async with pool.acquire() as connection, connection.cursor() as cursor:
|
||||
await cursor.execute("SELECT prefs FROM user_ui_prefs WHERE user_id = %s", (user_id,))
|
||||
row = await cursor.fetchone()
|
||||
if not row or not row[0]:
|
||||
return {}
|
||||
raw = row[0]
|
||||
if isinstance(raw, (bytes, bytearray)):
|
||||
raw = raw.decode("utf-8")
|
||||
if isinstance(raw, str):
|
||||
try:
|
||||
raw = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
return raw if isinstance(raw, dict) else {}
|
||||
|
||||
|
||||
async def save_user_ui_prefs(user_id: int, prefs: dict[str, Any]) -> None:
|
||||
"""화면 취향을 통째로 덮어쓴다 — 사용자당 한 줄."""
|
||||
pool = get_db_pool()
|
||||
async with pool.acquire() as connection, connection.cursor() as cursor:
|
||||
await cursor.execute(
|
||||
"""INSERT INTO user_ui_prefs (user_id, prefs) VALUES (%s, %s)
|
||||
ON DUPLICATE KEY UPDATE prefs = VALUES(prefs)""",
|
||||
(user_id, json.dumps(prefs, ensure_ascii=False)),
|
||||
)
|
||||
await connection.commit()
|
||||
|
||||
@@ -34,11 +34,13 @@ from .B01_Dashboard_Repository import (
|
||||
get_project,
|
||||
get_system_resources,
|
||||
get_user_admin_target,
|
||||
get_user_ui_prefs,
|
||||
list_all_projects,
|
||||
list_all_users,
|
||||
list_audit_logs,
|
||||
list_company_projects,
|
||||
list_user_projects,
|
||||
save_user_ui_prefs,
|
||||
soft_delete_project,
|
||||
soft_delete_user,
|
||||
update_admin_user,
|
||||
@@ -85,6 +87,7 @@ from .B01_Dashboard_Schema import (
|
||||
InviteMemberRequest,
|
||||
JoinCompanyRequest,
|
||||
ProcessJoinRequest,
|
||||
UiPrefsRequest,
|
||||
UpdateCompanyAssetRequest,
|
||||
UpdateCompanyLogoRequest,
|
||||
UpdateProjectRequest,
|
||||
@@ -169,6 +172,24 @@ async def patch_dashboard_me(
|
||||
return {"status": "success", "user": user}
|
||||
|
||||
|
||||
# ── 화면 취향 (2026-09-07 사용자 승인) ───────────────────────────────────────
|
||||
# 패널 높이·접힘 같은 **배치 값**만 계정에 붙인다. 설계값은 여기 오지 않는다 — 초안·결과는
|
||||
# 브라우저에 두고 [저장]·[확정]에서만 정본으로 간다(CLAUDE.md 5장).
|
||||
# 브라우저는 이 값을 **로컬 사본 위에 병합**하고, 서버가 없거나 못 읽으면 로컬 값으로 돈다.
|
||||
@router.get("/me/ui-prefs")
|
||||
async def read_ui_prefs(session: dict[str, Any] = Depends(verify_session)):
|
||||
return {"status": "success", "prefs": await get_user_ui_prefs(int(session["user_id"]))}
|
||||
|
||||
|
||||
@router.put("/me/ui-prefs")
|
||||
async def write_ui_prefs(
|
||||
payload: UiPrefsRequest,
|
||||
session: dict[str, Any] = Depends(verify_session),
|
||||
):
|
||||
await save_user_ui_prefs(int(session["user_id"]), payload.prefs)
|
||||
return {"status": "success"}
|
||||
|
||||
|
||||
@router.get("/user/projects")
|
||||
async def user_projects(session: dict[str, Any] = Depends(verify_session)):
|
||||
# 회사에 속하면 회사 프로젝트 전체를 본다 (2026-09-06 사용자 지시) — 수정 권한은 따로다.
|
||||
|
||||
@@ -12,6 +12,16 @@ class UpdateUserRequest(BaseModel):
|
||||
phone: str | None = Field(default=None, max_length=50)
|
||||
|
||||
|
||||
class UiPrefsRequest(BaseModel):
|
||||
"""화면 취향 묶음 — 배치·표시 값만. 키는 브라우저 등록표(`b_page_state.ts`)가 정본이다.
|
||||
|
||||
값은 문자열 하나(높이는 `"438"`, 접힘은 `"true"`)라 저장소에 넣던 모양 그대로다.
|
||||
설계값이 섞여 들어오지 않게 **문자열만** 받는다.
|
||||
"""
|
||||
|
||||
prefs: dict[str, str] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class CreateCompanyRequest(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=255)
|
||||
business_registration_number: str = Field(min_length=1, max_length=20)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { writeCrossDesignChoice } from "./B06_Section_Cross_Design_Session";
|
||||
import { CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_frontend";
|
||||
import { leaveForDashboard } from "../A00_Common/b_missing_data_guard";
|
||||
import { stateKey, storageOf } from "../A00_Common/b_page_state";
|
||||
import { readByKey, stateKey, writeByKey } from "../A00_Common/b_page_state";
|
||||
import { navigateTo } from "../A00_Common/router";
|
||||
import { createButton, createInputField, showToast } from "@ui/ui_template_elements";
|
||||
import type { StructureInstance, StructureType } from "../B05_Profile/B05_Profile_Api_Structures";
|
||||
@@ -529,7 +529,7 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
const width = crossHalfWidth();
|
||||
if (!key || width === undefined) return;
|
||||
try {
|
||||
storageOf(key).setItem(key, String(width));
|
||||
writeByKey(key, String(width));
|
||||
} catch {
|
||||
/* 무시 */
|
||||
}
|
||||
@@ -708,7 +708,7 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
// 세션에 보관된 표시 반폭이 있으면 그것이 우선한다(사용자가 마지막으로 지정한 값).
|
||||
const sessionDisplayKey = displaySessionKey();
|
||||
if (sessionDisplayKey) {
|
||||
const sessionDisplay = Number(storageOf(sessionDisplayKey).getItem(sessionDisplayKey));
|
||||
const sessionDisplay = Number(readByKey(sessionDisplayKey));
|
||||
if (Number.isFinite(sessionDisplay) && sessionDisplay > 0)
|
||||
crossHalfWidthField.input.value = sessionDisplay.toFixed(1);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import { flushPendingPipes } from "../B05_Profile/B05_Profile_Api_Pipes_Draft";
|
||||
import { flushPendingStructures } from "../B05_Profile/B05_Profile_Api_Structures";
|
||||
import { flushUphillOverrides } from "../B05_Profile/B05_Profile_Api_Fetch";
|
||||
import { buildCrossPatches, type CrossPatchSources } from "./B06_Section_UI_Page_Patches";
|
||||
import { readState, storageOf } from "../A00_Common/b_page_state";
|
||||
import { readByKey, readState, writeByKey } from "../A00_Common/b_page_state";
|
||||
import {
|
||||
applyStructureAreaRows,
|
||||
STRUCTURE_AREA_KEYS,
|
||||
@@ -92,7 +92,7 @@ export function createRockBoundaryStore(options: {
|
||||
const storageKey = sessionKey();
|
||||
if (!storageKey) return;
|
||||
try {
|
||||
storageOf(storageKey).setItem(storageKey, JSON.stringify(Object.fromEntries(offsets)));
|
||||
writeByKey(storageKey, JSON.stringify(Object.fromEntries(offsets)));
|
||||
} catch {
|
||||
/* 세션 저장 실패는 무시(용량 초과 등) — 값은 메모리에 유지된다. */
|
||||
}
|
||||
@@ -105,7 +105,7 @@ export function createRockBoundaryStore(options: {
|
||||
const storageKey = sessionKey();
|
||||
if (!storageKey) return;
|
||||
try {
|
||||
const raw = storageOf(storageKey).getItem(storageKey);
|
||||
const raw = readByKey(storageKey);
|
||||
if (!raw) return;
|
||||
const parsed = JSON.parse(raw) as Record<string, number>;
|
||||
Object.entries(parsed).forEach(([chainage, offset]) => {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
-- 018_user_ui_prefs.sql
|
||||
-- 화면 취향을 계정에 붙인다 (2026-09-07 사용자 승인)
|
||||
--
|
||||
-- 패널 높이·접힘, 유토곡선 열림·범례 같은 **배치 값**이다. 설계값은 담지 않는다 —
|
||||
-- 설계 초안·계산 결과는 지금처럼 브라우저 세션에 두고 [저장]·[확정]에서만 정본으로 간다.
|
||||
--
|
||||
-- 왜 필요한가 — 취향이 브라우저에만 있어 **PC 를 바꾸면 처음부터 다시 맞춰야 했다**.
|
||||
-- 사용자가 노트북·데스크톱 두 대를 오가며 쓴다. (탭을 새로 열 때마다 초기화되던 것은
|
||||
-- 앞서 `localStorage` 로 옮겨 해결했고, 이 표는 그 위에 「다른 PC 에서도 같은 배치」를 얹는다.)
|
||||
--
|
||||
-- 왜 칸을 나누지 않고 JSON 한 칸인가 — 취향은 화면이 늘 때마다 늘어난다. 칸으로 나누면
|
||||
-- 그때마다 마이그레이션이 또 필요하다. 값이 작고(수십 바이트) 검색 대상이 아니라
|
||||
-- 한 칸에 담는 편이 값싸다. 키 이름은 브라우저 등록표(`A00_Common/b_page_state.ts`)가 정본이다.
|
||||
|
||||
USE aislo_db;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_ui_prefs (
|
||||
user_id INT NOT NULL COMMENT 'users.id — 사용자당 한 줄',
|
||||
prefs JSON NOT NULL COMMENT '화면 취향 묶음 {키: 값}. 키는 브라우저 등록표의 pref 이름',
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (user_id),
|
||||
CONSTRAINT fk_user_ui_prefs_user FOREIGN KEY (user_id)
|
||||
REFERENCES users (id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='사용자별 화면 취향 (배치·표시). 설계값은 담지 않는다';
|
||||
@@ -5,7 +5,7 @@ import { makePanelDraggable } from "./ui_template_overlay_drag";
|
||||
// 워크플로 상태는 공용 창구 하나로 받는다 — 화면마다 따로 부르면 진입에서 같은 답을
|
||||
// 두 번 받는다(2026-09-06 실측). 그 창구가 짧은 시간 동안 캐시한다.
|
||||
import { fetchWorkflowState } from "../A00_Common/b_workflow_nav";
|
||||
import { storageOf } from "../A00_Common/b_page_state";
|
||||
import { readByKey, writeByKey } from "../A00_Common/b_page_state";
|
||||
|
||||
const TITLE_OVERLAY_STATE_KEY = "frd_workflow_title_overlay_open";
|
||||
const PROGRESS_OVERLAY_STATE_KEY = "frd_workflow_progress_overlay_open";
|
||||
@@ -85,7 +85,7 @@ export function createWorkflowPanelHandle(
|
||||
}
|
||||
|
||||
function readOpenState(key: string): boolean {
|
||||
return storageOf(key).getItem(key) !== "false";
|
||||
return readByKey(key) !== "false";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -192,7 +192,7 @@ function createPanel(
|
||||
toggle.setAttribute("aria-label", toggle.title);
|
||||
toggle.setAttribute("aria-expanded", String(isOpen));
|
||||
}
|
||||
storageOf(storageKey).setItem(storageKey, String(isOpen));
|
||||
writeByKey(storageKey, String(isOpen));
|
||||
// 펼친 뒤에는 아래 공간이 모자랄 수 있다 — 열리는 방향을 다시 잡는다.
|
||||
if (dragHandle) requestAnimationFrame(() => dragHandle?.refresh());
|
||||
onOpenChange?.(isOpen);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
* 자리는 세션에만 남긴다(5장 데이터 3층 — 화면 조작값은 캐시 몫).
|
||||
*/
|
||||
|
||||
import { storageOf } from "../A00_Common/b_page_state";
|
||||
import { readByKey, writeByKey } from "../A00_Common/b_page_state";
|
||||
|
||||
const DRAG_THRESHOLD_PX = 4;
|
||||
/** 상단 공용 헤더 높이(--spacing-64) — 그 위로는 못 올라간다. */
|
||||
@@ -30,7 +30,7 @@ interface PanelPosition {
|
||||
}
|
||||
|
||||
function readPosition(key: string): PanelPosition | null {
|
||||
const raw = storageOf(key).getItem(key);
|
||||
const raw = readByKey(key);
|
||||
if (!raw) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as Partial<PanelPosition>;
|
||||
@@ -94,7 +94,7 @@ export function makePanelDraggable(
|
||||
root.style.bottom = "auto";
|
||||
root.style.top = `${next.top}px`;
|
||||
}
|
||||
storageOf(storageKey).setItem(storageKey, JSON.stringify(next));
|
||||
writeByKey(storageKey, JSON.stringify(next));
|
||||
}
|
||||
|
||||
function swallowNextClick(): void {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import "./ui_template_resizer.css";
|
||||
import { storageOf } from "../A00_Common/b_page_state";
|
||||
import { readByKey, writeByKey } from "../A00_Common/b_page_state";
|
||||
|
||||
/* =============================================================================
|
||||
* ui_template_resizer.ts
|
||||
@@ -62,20 +62,20 @@ export function createPanelResizer(options: PanelResizerOptions): PanelResizer {
|
||||
function apply(size: number, persist: boolean): void {
|
||||
const next = clamp(size);
|
||||
target.style.setProperty(cssVar, `${Math.round(next)}px`);
|
||||
if (persist && storageKey) storageOf(storageKey).setItem(storageKey, String(Math.round(next)));
|
||||
if (persist && storageKey) writeByKey(storageKey, String(Math.round(next)));
|
||||
onResize?.(next);
|
||||
}
|
||||
|
||||
function restore(): void {
|
||||
if (!storageKey) return;
|
||||
const saved = Number(storageOf(storageKey).getItem(storageKey));
|
||||
const saved = Number(readByKey(storageKey));
|
||||
// 저장한 뒤 창 크기가 바뀌었을 수 있으니 복원할 때도 상·하한을 다시 씌운다.
|
||||
if (Number.isFinite(saved) && saved > 0) apply(saved, false);
|
||||
}
|
||||
|
||||
function reset(): void {
|
||||
target.style.removeProperty(cssVar);
|
||||
if (storageKey) storageOf(storageKey).removeItem(storageKey);
|
||||
if (storageKey) writeByKey(storageKey, null);
|
||||
onResize?.(axis === "vertical" ? target.clientHeight : target.clientWidth);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user