/* ============================================================================ * TDS Asset (Mobile, 2026-03) ─ Task #158 * ---------------------------------------------------------------------------- * - 루트 namespace `Asset` 글로벌 + Frame 베이스 + 5 Wrapped + 5 Content + frameShape 프리셋 11종 * - Stepper.AssetFrame(#149) 와는 의도적으로 분리: Stepper 안 좌측 마커 전용 vs 화면 어디서나 쓰는 범용 Asset. * - Toast.Icon(#154) 와도 분리: Toast 내부 24×24 고정 아이콘 vs 자유 크기 Asset.Icon. * - 모든 색·라운드·간격은 var(--tds-…) 토큰 사용. 하드코딩 없음. * - window.adaptive (#157 에서 IIFE 로 초기화됨) 를 backgroundColor 인자에 그대로 사용 가능. * ========================================================================= */ (function ensureAdaptive() { // AgreementV4(#157) 가 먼저 로드되어 있으면 그대로 사용. // 단독 데모 등으로 Asset 만 먼저 마운트되는 경우를 대비해 가드. if (typeof window === "undefined") return; if (window.adaptive && typeof window.adaptive === "object") return; var hues = ["red", "orange", "yellow", "green", "teal", "blue", "purple", "grey"]; var steps = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900]; var out = { white: "#ffffff", black: "#000000", transparent: "transparent" }; hues.forEach(function (h) { steps.forEach(function (s) { out[h + s] = "var(--tds-" + h + "-" + s + ")"; }); }); window.adaptive = out; })(); /* ---------------------------------------------------------------------------- * 1. Frame 프리셋 (`Asset.frameShape`) * - radius 는 number(px) 또는 string('50%' / 'var(--tds-radius-…)') * - 호출자가 직접 {width,height,radius} 를 줘도 동작 (FrameShapeType) * ------------------------------------------------------------------------- */ const __ASSET_FRAME_SHAPES = Object.freeze({ // Square — 정사각, 작은 라운드 SquareSmall: Object.freeze({ width: 36, height: 36, radius: 8 }), SquareMedium: Object.freeze({ width: 48, height: 48, radius: 10 }), SquareLarge: Object.freeze({ width: 64, height: 64, radius: 14 }), // Rectangle — 가로 긴 직사각 RectangleMedium: Object.freeze({ width: 64, height: 48, radius: 10 }), RectangleLarge: Object.freeze({ width: 96, height: 64, radius: 14 }), // Circle — 정원 CircleSmall: Object.freeze({ width: 36, height: 36, radius: "50%" }), CircleMedium: Object.freeze({ width: 48, height: 48, radius: "50%" }), CircleLarge: Object.freeze({ width: 64, height: 64, radius: "50%" }), // Card — 세로 긴 카드 CardSmall: Object.freeze({ width: 80, height: 96, radius: 12 }), CardMedium: Object.freeze({ width: 104, height: 120, radius: 14 }), CardLarge: Object.freeze({ width: 128, height: 144, radius: 16 }), }); const __ASSET_ACC_POSITIONS = ["top-left", "top-right", "bottom-left", "bottom-right"]; const __ASSET_ACC_MASKINGS = ["circle", "none"]; const __ASSET_SCALE_TYPES = ["fit", "crop"]; /* ------- helpers ---------------------------------------------------------- */ function __assetSizeToCss(v) { // number → "Npx", string → 그대로, 그 외 → undefined if (typeof v === "number" && Number.isFinite(v)) return v + "px"; if (typeof v === "string" && v.length > 0) return v; return undefined; } function __assetClampScale(v) { if (typeof v !== "number" || !Number.isFinite(v)) return 1; if (v <= 0) return 1; if (v > 1) return 1; return v; } function __assetWarn(msg) { if (typeof console !== "undefined" && console.warn) console.warn("[Asset] " + msg); } function __assetEnum(value, allowed, fallback, propName) { if (allowed.indexOf(value) === -1) { __assetWarn(propName + ' 는 ' + JSON.stringify(allowed) + " 중 하나여야 해요. 받은 값: " + JSON.stringify(value) + " — '" + fallback + "' 로 폴백."); return fallback; } return value; } /* ============================================================================ * 2. Asset.Frame ─ 베이스 컨테이너 * props: content*, shape*, backgroundColor, color, acc, accPosition, accMasking, overlap * ========================================================================= */ function AssetFrame(props) { const { content, shape, backgroundColor, color, acc, accPosition = "bottom-right", accMasking = "none", overlap, id, className, style, ...rest } = props || {}; if (content == null) __assetWarn("Asset.Frame 에 content 가 없어요. 빈 프레임이 그려져요."); if (shape == null || typeof shape !== "object") __assetWarn("Asset.Frame 에 shape 가 없어요. width·height 가 자동 부여되지 않아 크기가 0 일 수 있어요."); const safeShape = (shape && typeof shape === "object") ? shape : {}; const accPos = __assetEnum(accPosition, __ASSET_ACC_POSITIONS, "bottom-right", "accPosition"); const accMask = __assetEnum(accMasking, __ASSET_ACC_MASKINGS, "none", "accMasking"); // 인라인 style 매핑 (CSS 변수로 흘려서 ::before/::after 도 사용) const cssVars = {}; const w = __assetSizeToCss(safeShape.width); const h = __assetSizeToCss(safeShape.height); const r = __assetSizeToCss(safeShape.radius); if (w) cssVars["--tds-asset-w"] = w; if (h) cssVars["--tds-asset-h"] = h; if (r) cssVars["--tds-asset-r"] = r; if (backgroundColor) cssVars["--tds-asset-bg"] = backgroundColor; if (color) cssVars["--tds-asset-color"] = color; // 액세서리 사이즈/위치 (acc.width/height/x/y 모두 옵셔널) if (acc != null) { var accCfg = (safeShape && safeShape.acc && typeof safeShape.acc === "object") ? safeShape.acc : {}; var aw = __assetSizeToCss(accCfg.width); var ah = __assetSizeToCss(accCfg.height); var ax = __assetSizeToCss(accCfg.x); var ay = __assetSizeToCss(accCfg.y); if (aw) cssVars["--tds-asset-acc-w"] = aw; if (ah) cssVars["--tds-asset-acc-h"] = ah; if (ax) cssVars["--tds-asset-acc-x"] = ax; if (ay) cssVars["--tds-asset-acc-y"] = ay; } // overlap — 배경에 겹침 효과 (border 색 = overlap.color, ::before pseudo 로 표현) // FrameOverlapShapeType 의 x/y/blur/spread 는 shape.overlap 에서 옴 (옵셔널). if (overlap && typeof overlap === "object" && overlap.color) { cssVars["--tds-asset-overlap-color"] = overlap.color; var ovlCfg = (safeShape && safeShape.overlap && typeof safeShape.overlap === "object") ? safeShape.overlap : {}; var ox = __assetSizeToCss(ovlCfg.x) || "6px"; var oy = __assetSizeToCss(ovlCfg.y) || "6px"; var ob = __assetSizeToCss(ovlCfg.blur) || "0px"; var os = __assetSizeToCss(ovlCfg.spread) || "0px"; cssVars["--tds-asset-overlap-x"] = ox; cssVars["--tds-asset-overlap-y"] = oy; cssVars["--tds-asset-overlap-blur"] = ob; cssVars["--tds-asset-overlap-spread"] = os; } const classes = [ "tds-asset-frame", overlap && overlap.color ? "tds-asset-frame--has-overlap" : "", acc != null ? "tds-asset-frame--has-acc" : "", acc != null ? "tds-asset-frame--acc-" + accPos : "", acc != null && accMask === "circle" ? "tds-asset-frame--acc-mask-circle" : "", className || "", ].filter(Boolean).join(" "); return (
); } /* ============================================================================ * 3. Content 컴포넌트 5종 * ========================================================================= */ /* ---- ContentImage -------------------------------------------------------- */ function AssetContentImage(props) { const { src, alt, scaleType = "fit", scale, style, className, ...rest } = props || {}; if (!src) __assetWarn("Asset.ContentImage 에 src 가 없어요."); const t = __assetEnum(scaleType, __ASSET_SCALE_TYPES, "fit", "scaleType"); const s = scale != null ? __assetClampScale(scale) : 1; const inlineStyle = Object.assign( { objectFit: t === "crop" ? "cover" : "contain", width: s === 1 ? "100%" : (s * 100) + "%", height: s === 1 ? "100%" : (s * 100) + "%", }, style || {} ); return ( {alt ); } /* ---- ContentIcon --------------------------------------------------------- */ function AssetContentIcon(props) { const { name, color, size, style, className, ...rest } = props || {}; if (!name) __assetWarn("Asset.ContentIcon 에 name 이 없어요."); // window.IOSIcon 가 있으면 그쪽으로 위임 (우리 앱 표준 아이콘 라이브러리) const IOSIcon = (typeof window !== "undefined") ? window.IOSIcon : null; const inlineStyle = Object.assign( color ? { color: color } : {}, style || {} ); if (IOSIcon) { return ( ); } // 폴백: 단순 동그라미 (IOSIcon 미로드) return (