From 2320a082c68f234017e8f28c6846fac455ca810b Mon Sep 17 00:00:00 2001 From: Victor Barroso Lino Date: Mon, 20 Apr 2026 22:22:09 -0300 Subject: [PATCH 1/3] feat(dl-item-tooltip): match in-game tooltip styling --- .../dl-item-tooltip/dl-item-tooltip.css | 1058 ++++++++++++++--- .../dl-item-tooltip/dl-item-tooltip.tsx | 492 +++++--- 2 files changed, 1213 insertions(+), 337 deletions(-) diff --git a/packages/core/src/components/dl-item-tooltip/dl-item-tooltip.css b/packages/core/src/components/dl-item-tooltip/dl-item-tooltip.css index be53818..95e6dca 100644 --- a/packages/core/src/components/dl-item-tooltip/dl-item-tooltip.css +++ b/packages/core/src/components/dl-item-tooltip/dl-item-tooltip.css @@ -1,8 +1,8 @@ :host { display: block; width: 450px; - font-family: var(--dl-font-family); - color: var(--dl-text-primary); + font-family: var(--dl-font-family, 'Retail Demo', sans-serif); + color: var(--dl-text-tooltip, #ffefd7); line-height: 1.4; text-align: left; } @@ -15,17 +15,51 @@ /* ─── Tooltip shell ─── */ .tooltip { - border-radius: 8px; - overflow: hidden; - box-shadow: 0 0 20px rgba(0, 0, 0, 0.38); + width: 450px; font-size: 18px; + --tooltip-off-white: #ffefd7; + --tooltip-muted: rgba(255, 255, 255, 0.5); + --tooltip-black: #10130d; + --courageBrightColor: #ec981a; + --courageColor: #9e630c; + --fortitudeBrightColor: #7cbb1e; + --fortitudeColor: #659818; + --spiritBrightColor: #ce91ff; + --spiritColor: #8b56b4; + --armorColor: #00ff9a; + --offWhite: #ffefd7; + --tooltip-courage-bright: var(--courageBrightColor); + --tooltip-fortitude-bright: var(--fortitudeBrightColor); + --tooltip-spirit-bright: var(--spiritBrightColor); + --tooltip-armor: var(--armorColor); + --tooltip-filter-courage: brightness(0) saturate(100%) invert(62%) sepia(82%) saturate(611%) hue-rotate(357deg) brightness(101%) contrast(91%); + --tooltip-filter-fortitude: brightness(0) saturate(100%) invert(84%) sepia(52%) saturate(895%) hue-rotate(91deg) brightness(109%) contrast(102%); + --tooltip-filter-spirit: brightness(0) saturate(100%) invert(74%) sepia(36%) saturate(1115%) hue-rotate(219deg) brightness(103%) contrast(97%); + --tooltip-filter-off-white: brightness(0) saturate(100%) invert(97%) sepia(12%) saturate(600%) hue-rotate(0deg) brightness(107%); + --tooltip-heal-text: #99f051; + --tooltip-stamina-text: #a9f0a1; + --tooltip-debuff: #9af052; +} + +.tooltip-shadow { + display: flex; + flex-direction: column; + overflow: hidden; + border-radius: 8px; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.66), inset 0 0 0 1px rgba(0, 0, 0, 0.66); +} + +.tooltip-main { + width: 100%; } /* ─── Header ─── */ .header-container { display: flex; - flex-direction: row; + align-items: center; + width: 100%; min-height: 96px; + padding: 0 20px; background-size: cover; background-position: center; background-repeat: no-repeat; @@ -33,306 +67,455 @@ } .mod-name-container { - align-self: center; - color: var(--dl-text-primary); - padding: 20px; - z-index: 100; + width: 100%; + min-width: 0; } .mod-name { + display: block; + max-width: 100%; + overflow: hidden; + color: var(--tooltip-off-white); font-size: 28px; font-weight: 700; - color: var(--dl-text-tooltip)!important; - max-width: 320px; - overflow: hidden; + line-height: 1.08; text-overflow: ellipsis; - text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.19); + text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.12); + white-space: nowrap; } .mod-cost { display: flex; align-items: center; - gap: 0.4rem; + gap: 5px; margin-top: 3px; + color: #99ffd6; font-size: 20px; font-weight: 700; - color: #99ffd6; line-height: 1; } .soul-icon { - height: 26px; - width: auto; display: block; + width: 16px; + height: 22px; + object-fit: contain; filter: brightness(0) saturate(100%) invert(92%) sepia(21%) saturate(695%) hue-rotate(84deg) brightness(100%) contrast(103%); } /* ─── Properties body ─── */ .properties-container { + width: 100%; + margin-top: -1px; + background-color: #0f0f1a; background-size: cover; background-position: center; background-repeat: no-repeat; - background-color: #0f0f1a; padding: 0; } /* ─── Sections ─── */ .section { - padding: 10px 15px; + width: 100%; + padding: 8px 14px 12px; } -.section-type-label { - text-transform: capitalize; +.section + .section { + padding-top: 0; } /* ─── Innate section (no label, just stats) ─── */ .innate-section { - border-bottom: 1px solid rgba(255, 255, 255, 0.08); + padding-top: 8px; } /* ─── Active/Passive label ─── */ .ability-type-label { - align-items: center; - background-color: rgba(16, 19, 13, 0.56); display: flex; + align-items: center; justify-content: space-between; + width: calc(100% + 28px); height: 32px; + margin: 0 -14px 8px; + padding-left: 20px; + background-color: rgba(16, 19, 13, 0.48); + color: var(--tooltip-off-white); font-size: 18px; font-weight: 700; - padding: 8px 0 8px 15px; - margin: 0 -15px 8px; - width: calc(100% + 30px); + line-height: 32px; + text-transform: capitalize; } -.ability-type-label.active { - color: var(--dl-text-primary); +.ability-type-label.passive { + background-color: rgba(16, 19, 13, 0.37); } -.ability-type-label.passive { +.ability-type-label.passive .ability-type-text { font-style: italic; font-weight: 600; + opacity: 0.7; } -.ability-cooldown { +.ability-timing-group { display: flex; - align-items: baseline; - gap: 4px; - font-family: 'Retail Demo', sans-serif; + align-items: stretch; + height: 100%; + margin-left: auto; +} + +.ability-timing { + display: flex; + align-items: center; + gap: 5px; + height: 100%; + padding: 0 20px; + background-color: rgba(0, 0, 0, 0.76); + color: var(--tooltip-off-white); font-size: 16px; - font-weight: 700; font-style: normal; - color: var(--dl-text-tooltip); - background-color: #000000b8; - padding: 0 1rem; - height: 30px; - line-height: 30px; + font-weight: 700; + line-height: 32px; } -.ability-cooldown-icon { - width: 16px; - height: 16px; +.ability-timing-icon { + width: 22px; + height: 22px; object-fit: contain; + filter: none; +} + +.ability-timing.cooldown .ability-timing-icon, +.ability-timing.charge-up .ability-timing-icon { + filter: none; } /* ─── Info/description text inside sections ─── */ .mod-info-label { - align-self: flex-start; - color: #a9a9a9; - font-size: 16px; + width: 100%; + color: #cdcdcd; + font-size: 18px; font-weight: 400; - line-height: 138%; - margin: 15px 0; + line-height: 135%; } -.mod-info-label .highlight { - color: #f8f8f8; - font-weight: 600; +/* ─── Stats block (important stats + regular props) ─── */ +.applied-attributes-container + .applied-attributes-container { + margin-top: 12px; } -.mod-info-label .diminish { - color: rgba(191, 187, 176, 0.9); - font-size: 15px; - font-style: italic; - font-weight: 600; +.has-description .stats-block { + margin-top: 15px; } -/* ─── Stats block (important stats + regular props) ─── */ -.stats-block { - background: rgba(0, 0, 0, 0.15); - border-radius: 4px; - margin: 10px 0; - padding: 0; +.innate-section .has-description .stats-block { + margin-top: 10px; } -.stats-block-props { +.stats-block { display: flex; - flex-wrap: wrap; - gap: 2px 16px; - padding: 8px 10px; - background: rgba(0, 0, 0, 0.2); - margin-top: 4px; + flex-direction: row; + gap: 4px; + width: 100%; + min-height: 80px; + overflow: hidden; + border-radius: 5px; } -/* Block property items (no icons, compact) */ -.block-prop-item { - display: flex; - align-items: center; - gap: 5px; - font-size: 15px; +/* When 1 important stat: side-by-side layout */ +.stats-block-stacked { + flex-direction: column; } -.block-prop-item .attribute-value { - font-size: 16px; - font-weight: 700; - color: var(--dl-text-tooltip); +.stats-block-no-important { + min-height: 40px; } -.block-prop-item .attribute-name { - font-size: 15px; - font-weight: 500; - color: rgba(200, 200, 210, 0.65); +.no-applied-stats .important-stats-wrapper { + flex: 1 1 100%; + width: 100%; } /* ─── Important stat boxes ─── */ .important-stats-wrapper { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); + display: flex; + flex: 0 0 130px; + flex-direction: row; + gap: 4px; + min-height: 80px; +} + +.stats-block-stacked .important-stats-wrapper { + flex: none; width: 100%; - gap: 8px; + min-height: 70px; +} + +.important-stats-wrapper.count-4, +.important-stats-wrapper.count-5, +.important-stats-wrapper.count-6 { + flex-wrap: wrap; } .important-stat-box { - background: rgba(0, 0, 0, 0.2); - padding: 8px 8px; - text-align: center; + position: relative; display: flex; - flex-direction: column; + flex: 1 1 0; align-items: center; justify-content: center; + min-width: 0; + min-height: 80px; + padding: 5px; + background-color: rgba(0, 0, 0, 0.25); + text-align: center; +} + +.important-stats-wrapper.count-4 .important-stat-box, +.important-stats-wrapper.count-5 .important-stat-box, +.important-stats-wrapper.count-6 .important-stat-box { + flex-basis: calc(50% - 2px); min-height: 70px; } -.important-stat-box.status-effect .important-stat-value { - color: #6dc04b; - font-size: 18px; +.important-stat-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1px; + width: 100%; + min-width: 0; + line-height: 1.1; } .important-stat-icon-value { display: flex; align-items: center; justify-content: center; - gap: 4px; + width: 100%; + min-width: 0; + margin-top: -2px; +} + +.important-stat-icon-value.hide-important-stat-icon .important-stat-icon { + display: none; } .important-stat-icon { flex-shrink: 0; width: 20px; height: 20px; + margin-right: 3px; object-fit: contain; - margin-right: 4px; } .important-stat-value { - font-size: 20px; + min-width: 0; + color: var(--tooltip-off-white); + font-size: 22px; font-weight: 700; - color: #fff; + white-space: nowrap; } -.important-stat-label { - font-size: 13px; - font-weight: 600; - line-height: 16px; - margin-top: 2px; +.important-stat-labels { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 3px; + width: 100%; + min-width: 0; +} + +.important-stat-type { + display: -webkit-box; + width: 100%; + max-height: 40px; overflow: hidden; + color: var(--tooltip-off-white); + font-size: 16px; + font-weight: 500; + line-height: 1.15; text-align: center; - text-overflow: ellipsis; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; } -.important-stat-conditional { +.important-stat-label { + display: -webkit-box; + width: 100%; + max-height: 40px; + overflow: hidden; + color: var(--tooltip-off-white); + font-size: 16px; font-style: italic; - font-weight: 500; - margin-top: 2px; - opacity: 0.4; + font-weight: 600; + line-height: 1.15; + opacity: 0.5; text-align: center; - font-size: 14px; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; } -/* ─── Standard attribute rows ─── */ -.attribute-line-item { +.important-stat-box.status-effect .important-stat-value { + color: #6dc04b; + font-size: 20px; +} + +.prop_bullet_damage .important-stat-type, +.prop_damage .important-stat-type { + color: #ec981a; + font-weight: 600; +} + +.prop_tech_damage .important-stat-type, +.prop_tech_power .important-stat-type, +.prop_spirit .important-stat-type { + color: #ce91ff; + font-weight: 600; +} + +.prop_healing .important-stat-type, +.prop_health .important-stat-type { + color: #7bba1d; + font-weight: 600; +} + +.stats-block-props { display: flex; - align-items: center; - gap: 6px; - font-size: 18px; - margin: 2px 0 4px; + flex: 1; + flex-direction: column; + justify-content: center; + gap: 1px; + min-width: 0; + min-height: 80px; + padding: 10px 15px; + background-color: rgba(0, 0, 0, 0.25); +} + +.stats-block-props.empty { + display: none; +} + +.stats-block-no-important .stats-block-props { width: 100%; + min-height: 0; + gap: 5px; } -.attribute-line-item:last-child { - margin-bottom: 0; +.stats-block-stacked .stats-block-props { + flex-direction: row; + flex-wrap: wrap; + gap: 1px 4px; + width: 100%; + min-height: 0; } -.prop-icon { - display: none; +/* Block property items (no icons, compact) */ +.stats-block-stacked .block-prop-item { + width: calc(50% - 2px); + padding: 2px 0; +} + +.block-prop-item { + display: block; + min-width: 0; + color: var(--tooltip-off-white); + font-size: 18px; + line-height: 1.3; + white-space: nowrap; } +/* ─── Standard attribute rows ─── */ .attribute-value { - font-size: 19px; font-weight: 700; - color: var(--dl-text-primary); white-space: nowrap; } -.attribute-value.elevated { - color: var(--slot-color); +.attribute-name { + margin-left: 5px; + color: #ffffff; + font-size: 18px; + font-weight: 500; + opacity: 0.7; + white-space: nowrap; } -.attribute-value.negative { - color: #ff6a6a; +.attribute-name.elevated { + color: #ffffff; + font-weight: 700; + opacity: 1; } -.attribute-name { - font-size: 18px; - font-weight: 500; - color: rgba(200, 200, 210, 0.75); +.full-property-value { + color: var(--tooltip-off-white); + font-weight: 700; + white-space: nowrap; } -.conditional { - font-size: 14px; - color: #e8c56a; - padding: 2px 0 2px 26px; - font-style: italic; +.prefix-value, +.postfix-value { + color: rgba(255, 255, 255, 0.5); + font-weight: 700; +} + +.postfix-value.shrink, +.stats-block-props .postfix-value { + font-size: 16px; +} + +.property-value { + color: var(--tooltip-off-white); + font-weight: 700; +} + +.full-property-value.is-negative .prefix-value, +.full-property-value.is-negative .postfix-value, +.full-property-value.is-negative .property-value { + color: #ff6a6a; + opacity: 1; } /* ─── Inline content from API HTML ─── */ -.highlight { +.highlight, +.mod-info-label .highlight { color: #f8f8f8; font-weight: 600; } .highlight_weapon, -.highlight_courage { - color: #ec9719; +.highlight_courage, +.mod-info-label .highlight_weapon, +.mod-info-label .highlight_courage { + color: var(--tooltip-courage-bright); font-weight: 700; } .highlight_spirit, -.highlight_tech { - color: #ce90ff; +.highlight_tech, +.mod-info-label .highlight_spirit, +.mod-info-label .highlight_tech { + color: var(--tooltip-spirit-bright); font-weight: 700; } .highlight_armor, -.highlight_fortitude { - color: #7bba1d; +.highlight_fortitude, +.mod-info-label .highlight_armor, +.mod-info-label .highlight_fortitude { + color: var(--tooltip-fortitude-bright); font-weight: 700; } -.is-negative { +.is-negative, +.isNegative, +.mod-info-label .is-negative, +.mod-info-label .isNegative { color: #ff6a6a; font-weight: 700; } -.diminish { +.diminish, +.mod-info-label .diminish { color: rgba(191, 187, 176, 0.9); font-size: 15px; font-style: italic; @@ -340,8 +523,8 @@ } .strike { - text-decoration: line-through; color: rgba(200, 200, 210, 0.5); + text-decoration: line-through; } .bold { @@ -355,14 +538,13 @@ /* Inline SVGs and images from API HTML */ svg, .mod-info-label img, -.section img:not(.prop-icon):not(.important-stat-icon), -.conditional img { +.section img:not(.important-stat-icon):not(.ability-timing-icon) { + position: relative; + top: 2px; display: inline-block; - vertical-align: middle; width: 16px; height: 16px; - top: 2px; - position: relative; + vertical-align: middle; } svg { @@ -373,56 +555,598 @@ svg { font-weight: 700; } +.mod-info-label .InlineKey, +.mod-info-label .keybind { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 24px; + height: 24px; + margin: 0 3px; + padding: 0 6px; + border-radius: 4px; + background-color: #c6b6a2; + color: var(--tooltip-black); + font-size: 17px; + font-style: normal; + font-weight: 800; + line-height: 24px; + text-transform: uppercase; + vertical-align: -3px; +} + +.mod-info-label .inline-attribute-label.Spirit, +.mod-info-label .inline-attribute-label.SpiritDamage, +.mod-info-label .inline-attribute-label.BonusSpiritDamage, +.mod-info-label .inline-attribute-label.SpiritDPS, +.mod-info-label .inline-attribute-label.SpiritResist, +.mod-info-label .InlineAttributeName.Spirit, +.mod-info-label .InlineAttributeName.SpiritDamage, +.mod-info-label .InlineAttributeName.BonusSpiritDamage, +.mod-info-label .InlineAttributeName.SpiritDPS, +.mod-info-label .InlineAttributeName.SpiritResist, +.mod-info-label .Spirit, +.mod-info-label .SpiritDamage, +.mod-info-label .BonusSpiritDamage, +.mod-info-label .SpiritDPS, +.mod-info-label .SpiritResist, +.mod-info-label [style*="spiritBrightColor"] { + color: var(--tooltip-spirit-bright) !important; +} + +.mod-info-label .inline-attribute-label.Courage, +.mod-info-label .inline-attribute-label.MeleeDamage, +.mod-info-label .inline-attribute-label.WeaponDamage, +.mod-info-label .inline-attribute-label.BonusWeaponDamage, +.mod-info-label .inline-attribute-label.BulletDamage, +.mod-info-label .inline-attribute-label.BulletResist, +.mod-info-label .InlineAttributeName.Courage, +.mod-info-label .InlineAttributeName.MeleeDamage, +.mod-info-label .InlineAttributeName.WeaponDamage, +.mod-info-label .InlineAttributeName.BonusWeaponDamage, +.mod-info-label .InlineAttributeName.BulletDamage, +.mod-info-label .InlineAttributeName.BulletResist, +.mod-info-label .Courage, +.mod-info-label .MeleeDamage, +.mod-info-label .WeaponDamage, +.mod-info-label .BonusWeaponDamage, +.mod-info-label .BulletDamage, +.mod-info-label .BulletResist, +.mod-info-label [style*="courageBrightColor"] { + color: var(--tooltip-courage-bright) !important; +} + +.mod-info-label .inline-attribute-label.Fortitude, +.mod-info-label .inline-attribute-label.Heals, +.mod-info-label .inline-attribute-label.Healing, +.mod-info-label .inline-attribute-label.Heal, +.mod-info-label .inline-attribute-label.Regen, +.mod-info-label .inline-attribute-label.MaxHealth, +.mod-info-label .InlineAttributeName.Fortitude, +.mod-info-label .InlineAttributeName.Heals, +.mod-info-label .InlineAttributeName.Healing, +.mod-info-label .InlineAttributeName.Heal, +.mod-info-label .InlineAttributeName.Regen, +.mod-info-label .InlineAttributeName.MaxHealth, +.mod-info-label .Fortitude, +.mod-info-label .Heals, +.mod-info-label .Healing, +.mod-info-label .Heal, +.mod-info-label .Regen, +.mod-info-label .MaxHealth, +.mod-info-label [style*="fortitudeBrightColor"] { + color: var(--tooltip-fortitude-bright) !important; +} + +.mod-info-label .inline-attribute-label.Slow, +.mod-info-label .inline-attribute-label.Slowing, +.mod-info-label .inline-attribute-label.SlowResistance, +.mod-info-label .inline-attribute-label.Debuff, +.mod-info-label .inline-attribute-label.ReducedFireRate, +.mod-info-label .inline-attribute-label.BonusMoveSpeed, +.mod-info-label .inline-attribute-label.MoveSpeed, +.mod-info-label .inline-attribute-label.FireRate, +.mod-info-label .inline-attribute-label.BonusFireRate, +.mod-info-label .inline-attribute-label.BonusSprintSpeed, +.mod-info-label .InlineAttributeName.Slow, +.mod-info-label .InlineAttributeName.Slowing, +.mod-info-label .InlineAttributeName.SlowResistance, +.mod-info-label .InlineAttributeName.Debuff, +.mod-info-label .InlineAttributeName.ReducedFireRate, +.mod-info-label .InlineAttributeName.BonusMoveSpeed, +.mod-info-label .InlineAttributeName.MoveSpeed, +.mod-info-label .InlineAttributeName.FireRate, +.mod-info-label .InlineAttributeName.BonusFireRate, +.mod-info-label .InlineAttributeName.BonusSprintSpeed { + color: var(--tooltip-off-white) !important; +} + +.mod-info-label .inline-attribute-label.CombatBarrier, +.mod-info-label .inline-attribute-label.DamageAmp, +.mod-info-label .inline-attribute-label.Immobilize, +.mod-info-label .inline-attribute-label.KnockBack, +.mod-info-label .inline-attribute-label.KnockUp, +.mod-info-label .inline-attribute-label.Pull, +.mod-info-label .inline-attribute-label.Pulls, +.mod-info-label .inline-attribute-label.Pulling, +.mod-info-label .inline-attribute-label.Drag, +.mod-info-label .inline-attribute-label.Stun, +.mod-info-label .InlineAttributeName.CombatBarrier, +.mod-info-label .InlineAttributeName.DamageAmp, +.mod-info-label .InlineAttributeName.Immobilize, +.mod-info-label .InlineAttributeName.KnockBack, +.mod-info-label .InlineAttributeName.KnockUp, +.mod-info-label .InlineAttributeName.Pull, +.mod-info-label .InlineAttributeName.Pulls, +.mod-info-label .InlineAttributeName.Pulling, +.mod-info-label .InlineAttributeName.Drag, +.mod-info-label .InlineAttributeName.Stun { + color: var(--tooltip-off-white) !important; +} + +.mod-info-label .inline-attribute-label.Heals, +.mod-info-label .inline-attribute-label.Healing, +.mod-info-label .inline-attribute-label.Heal, +.mod-info-label .inline-attribute-label.Regen, +.mod-info-label .inline-attribute-label.MaxHealth, +.mod-info-label .InlineAttributeName.Heals, +.mod-info-label .InlineAttributeName.Healing, +.mod-info-label .InlineAttributeName.Heal, +.mod-info-label .InlineAttributeName.Regen, +.mod-info-label .InlineAttributeName.MaxHealth { + color: var(--tooltip-heal-text) !important; +} + +.mod-info-label .inline-attribute-label.StaminaRegenPerSecond, +.mod-info-label .InlineAttributeName.StaminaRegenPerSecond { + color: var(--tooltip-stamina-text) !important; +} + +.mod-info-label .inline-attribute-label.PureDamage, +.mod-info-label .InlineAttributeName.PureDamage { + color: #ffffff !important; +} + +.mod-info-label .inline-attribute-label.Silence, +.mod-info-label .InlineAttributeName.Silence { + color: var(--tooltip-spirit-bright) !important; +} + +.mod-info-label .InlineAttributeIcon, +.mod-info-label .inline-attribute-icon { + display: inline-block; + width: 20px; + height: 20px; + vertical-align: sub; + background-position: center; + background-repeat: no-repeat; + background-size: contain; + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + -webkit-mask-position: center; + -webkit-mask-repeat: no-repeat; + -webkit-mask-size: contain; +} + +.mod-info-label .InlineAttributeIcon.Spirit, +.mod-info-label .InlineAttributeIcon.SpiritIcon, +.mod-info-label .inline-attribute-icon.Spirit, +.mod-info-label .inline-attribute-icon.SpiritIcon { + background-color: var(--tooltip-spirit-bright); + mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/icons/spirit.svg"); + -webkit-mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/icons/spirit.svg"); +} + +.mod-info-label .InlineAttributeIcon.Courage, +.mod-info-label .inline-attribute-icon.Courage { + background-color: var(--tooltip-courage-bright); + mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/images/hud/core/icon_damage_melee_psd.png"); + -webkit-mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/images/hud/core/icon_damage_melee_psd.png"); +} + +.mod-info-label .InlineAttributeIcon.Fortitude, +.mod-info-label .inline-attribute-icon.Fortitude { + background-color: var(--tooltip-fortitude-bright); + mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/icons/health.svg"); + -webkit-mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/icons/health.svg"); +} + +.mod-info-label .InlineAttributeIcon.SpiritDamage, +.mod-info-label .InlineAttributeIcon.BonusSpiritDamage, +.mod-info-label .InlineAttributeIcon.SpiritDPS, +.mod-info-label .inline-attribute-icon.SpiritDamage, +.mod-info-label .inline-attribute-icon.BonusSpiritDamage, +.mod-info-label .inline-attribute-icon.SpiritDPS { + background-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/icons/damage_magic_color.svg"); +} + +.mod-info-label .InlineAttributeIcon.BulletResist, +.mod-info-label .inline-attribute-icon.BulletResist { + background-color: var(--tooltip-courage-bright); + mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/images/upgrades/property_bullet_armor_psd.png"); + -webkit-mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/images/upgrades/property_bullet_armor_psd.png"); +} + +.mod-info-label .InlineAttributeIcon.SpiritResist, +.mod-info-label .inline-attribute-icon.SpiritResist { + background-color: var(--tooltip-spirit-bright); + mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/images/upgrades/property_tech_armor_psd.png"); + -webkit-mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/images/upgrades/property_tech_armor_psd.png"); +} + +.mod-info-label .InlineAttributeIcon.MeleeDamage, +.mod-info-label .inline-attribute-icon.MeleeDamage { + background-color: var(--tooltip-courage-bright); + mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/images/hud/core/icon_damage_melee_psd.png"); + -webkit-mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/images/hud/core/icon_damage_melee_psd.png"); +} + +.mod-info-label .InlineAttributeIcon.Slow, +.mod-info-label .InlineAttributeIcon.Slowing, +.mod-info-label .InlineAttributeIcon.SlowResistance, +.mod-info-label .inline-attribute-icon.Slow, +.mod-info-label .inline-attribute-icon.Slowing, +.mod-info-label .inline-attribute-icon.SlowResistance { + background-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/images/upgrades/property_slow_large_psd.png"); +} + +.mod-info-label .InlineAttributeIcon.WeaponDamage, +.mod-info-label .InlineAttributeIcon.BonusWeaponDamage, +.mod-info-label .InlineAttributeIcon.BulletDamage, +.mod-info-label .inline-attribute-icon.WeaponDamage, +.mod-info-label .inline-attribute-icon.BonusWeaponDamage, +.mod-info-label .inline-attribute-icon.BulletDamage { + background-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/icons/damage_bullet_color.svg"); +} + +.mod-info-label .InlineAttributeIcon.ReducedFireRate, +.mod-info-label .InlineAttributeIcon.BonusFireRate, +.mod-info-label .InlineAttributeIcon.FireRate, +.mod-info-label .inline-attribute-icon.ReducedFireRate, +.mod-info-label .inline-attribute-icon.BonusFireRate, +.mod-info-label .inline-attribute-icon.FireRate { + background-color: var(--tooltip-courage-bright); + mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/icons/fire_rate.svg"); + -webkit-mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/icons/fire_rate.svg"); +} + +.mod-info-label .InlineAttributeIcon.BonusMoveSpeed, +.mod-info-label .InlineAttributeIcon.MoveSpeed, +.mod-info-label .InlineAttributeIcon.BonusSprintSpeed, +.mod-info-label .inline-attribute-icon.BonusMoveSpeed, +.mod-info-label .inline-attribute-icon.MoveSpeed, +.mod-info-label .inline-attribute-icon.BonusSprintSpeed { + background-color: var(--tooltip-armor); + mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/icons/move_speed.svg"); + -webkit-mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/icons/move_speed.svg"); +} + +.mod-info-label .InlineAttributeIcon.Heals, +.mod-info-label .InlineAttributeIcon.Healing, +.mod-info-label .InlineAttributeIcon.Heal, +.mod-info-label .InlineAttributeIcon.Regen, +.mod-info-label .InlineAttributeIcon.MaxHealth, +.mod-info-label .inline-attribute-icon.Heals, +.mod-info-label .inline-attribute-icon.Healing, +.mod-info-label .inline-attribute-icon.Heal, +.mod-info-label .inline-attribute-icon.Regen, +.mod-info-label .inline-attribute-icon.MaxHealth { + background-color: var(--tooltip-armor); + mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/icons/heal.svg"); + -webkit-mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/icons/heal.svg"); +} + +.mod-info-label .InlineAttributeIcon.CombatBarrier, +.mod-info-label .inline-attribute-icon.CombatBarrier { + background-color: var(--tooltip-off-white); + mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/images/upgrades/property_bullet_armor_psd.png"); + -webkit-mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/images/upgrades/property_bullet_armor_psd.png"); +} + +.mod-info-label .InlineAttributeIcon.DamageAmp, +.mod-info-label .InlineAttributeIcon.PureDamage, +.mod-info-label .inline-attribute-icon.DamageAmp, +.mod-info-label .inline-attribute-icon.PureDamage { + background-color: var(--tooltip-off-white); + mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/images/damage_psd.png"); + -webkit-mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/images/damage_psd.png"); +} + +.mod-info-label .InlineAttributeIcon.Debuff, +.mod-info-label .inline-attribute-icon.Debuff { + background-color: var(--tooltip-debuff); + mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/icons/condition_toxic.svg"); + -webkit-mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/icons/condition_toxic.svg"); +} + +.mod-info-label .InlineAttributeIcon.Stun, +.mod-info-label .inline-attribute-icon.Stun { + background-color: var(--tooltip-off-white); + mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/icons/condition_stun.svg"); + -webkit-mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/icons/condition_stun.svg"); +} + +.mod-info-label .InlineAttributeIcon.Immobilize, +.mod-info-label .inline-attribute-icon.Immobilize { + background-color: var(--tooltip-off-white); + mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/icons/condition_immobilize.svg"); + -webkit-mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/icons/condition_immobilize.svg"); +} + +.mod-info-label .InlineAttributeIcon.KnockBack, +.mod-info-label .InlineAttributeIcon.KnockUp, +.mod-info-label .InlineAttributeIcon.Pull, +.mod-info-label .InlineAttributeIcon.Pulls, +.mod-info-label .InlineAttributeIcon.Pulling, +.mod-info-label .InlineAttributeIcon.Drag, +.mod-info-label .inline-attribute-icon.KnockBack, +.mod-info-label .inline-attribute-icon.KnockUp, +.mod-info-label .inline-attribute-icon.Pull, +.mod-info-label .inline-attribute-icon.Pulls, +.mod-info-label .inline-attribute-icon.Pulling, +.mod-info-label .inline-attribute-icon.Drag { + background-color: var(--tooltip-off-white); + mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/icons/condition_knockdown.svg"); + -webkit-mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/icons/condition_knockdown.svg"); +} + +.mod-info-label .InlineAttributeIcon.Silence, +.mod-info-label .inline-attribute-icon.Silence { + background-color: var(--tooltip-spirit-bright); + mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/icons/condition_silence.svg"); + -webkit-mask-image: url("https://assets-bucket.deadlock-api.com/assets-api-res/icons/condition_silence.svg"); +} + +.mod-info-label svg[fill="spiritBrightColor"], +.mod-info-label svg [fill="spiritBrightColor"] { + fill: var(--tooltip-spirit-bright) !important; +} + +.mod-info-label svg[stroke="spiritBrightColor"], +.mod-info-label svg [stroke="spiritBrightColor"] { + stroke: var(--tooltip-spirit-bright) !important; +} + +.mod-info-label svg[fill="courageBrightColor"], +.mod-info-label svg [fill="courageBrightColor"] { + fill: var(--tooltip-courage-bright) !important; +} + +.mod-info-label svg[stroke="courageBrightColor"], +.mod-info-label svg [stroke="courageBrightColor"] { + stroke: var(--tooltip-courage-bright) !important; +} + +.mod-info-label svg[fill="fortitudeBrightColor"], +.mod-info-label svg [fill="fortitudeBrightColor"] { + fill: var(--tooltip-fortitude-bright) !important; +} + +.mod-info-label svg[stroke="fortitudeBrightColor"], +.mod-info-label svg [stroke="fortitudeBrightColor"] { + stroke: var(--tooltip-fortitude-bright) !important; +} + +.mod-info-label svg[fill="armorColor"], +.mod-info-label svg [fill="armorColor"] { + fill: var(--tooltip-armor) !important; +} + +.mod-info-label svg[stroke="armorColor"], +.mod-info-label svg [stroke="armorColor"] { + stroke: var(--tooltip-armor) !important; +} + +.mod-info-label svg[fill="offWhite"], +.mod-info-label svg [fill="offWhite"] { + fill: var(--tooltip-off-white) !important; +} + +.mod-info-label svg[stroke="offWhite"], +.mod-info-label svg [stroke="offWhite"] { + stroke: var(--tooltip-off-white) !important; +} + +.mod-info-label svg[fill="white"], +.mod-info-label svg [fill="white"] { + fill: #ffffff !important; +} + +.mod-info-label svg[stroke="white"], +.mod-info-label svg [stroke="white"] { + stroke: #ffffff !important; +} + .inline-attribute { display: inline-block; + width: 20px; height: 20px; margin-right: 5px; vertical-align: middle; - width: 20px; +} + +.mod-info-label img.inline-attribute { + object-fit: contain; + filter: var(--tooltip-filter-off-white); +} + +.mod-info-label img.inline-attribute.Spirit, +.mod-info-label img.inline-attribute.SpiritIcon, +.mod-info-label img.inline-attribute.SpiritDamage, +.mod-info-label img.inline-attribute.BonusSpiritDamage, +.mod-info-label img.inline-attribute.SpiritDPS, +.mod-info-label img.inline-attribute.SpiritResist, +.mod-info-label img.inline-attribute.Silence { + filter: var(--tooltip-filter-spirit); +} + +.mod-info-label img.inline-attribute.Courage, +.mod-info-label img.inline-attribute.MeleeDamage, +.mod-info-label img.inline-attribute.WeaponDamage, +.mod-info-label img.inline-attribute.BonusWeaponDamage, +.mod-info-label img.inline-attribute.BulletDamage, +.mod-info-label img.inline-attribute.BulletResist, +.mod-info-label img.inline-attribute.ReducedFireRate, +.mod-info-label img.inline-attribute.BonusFireRate, +.mod-info-label img.inline-attribute.FireRate { + filter: var(--tooltip-filter-courage); +} + +.mod-info-label img.inline-attribute.Fortitude, +.mod-info-label img.inline-attribute.Heals, +.mod-info-label img.inline-attribute.Healing, +.mod-info-label img.inline-attribute.Heal, +.mod-info-label img.inline-attribute.Regen, +.mod-info-label img.inline-attribute.MaxHealth, +.mod-info-label img.inline-attribute.BonusMoveSpeed, +.mod-info-label img.inline-attribute.MoveSpeed, +.mod-info-label img.inline-attribute.BonusSprintSpeed, +.mod-info-label img.inline-attribute.StaminaRegenPerSecond, +.mod-info-label img.inline-attribute.Debuff { + filter: var(--tooltip-filter-fortitude); +} + +.mod-info-label img.inline-attribute.Slow, +.mod-info-label img.inline-attribute.Slowing, +.mod-info-label img.inline-attribute.SlowResistance, +.mod-info-label img.inline-attribute.CombatBarrier, +.mod-info-label img.inline-attribute.DamageAmp, +.mod-info-label img.inline-attribute.Immobilize, +.mod-info-label img.inline-attribute.KnockBack, +.mod-info-label img.inline-attribute.KnockUp, +.mod-info-label img.inline-attribute.Pull, +.mod-info-label img.inline-attribute.Pulls, +.mod-info-label img.inline-attribute.Pulling, +.mod-info-label img.inline-attribute.Drag, +.mod-info-label img.inline-attribute.Stun, +.mod-info-label img.inline-attribute.PureDamage { + filter: var(--tooltip-filter-off-white); +} + +.mod-info-label img.inline-attribute.DamageAmp { + content: url("https://assets-bucket.deadlock-api.com/assets-api-res/images/damage_psd.png"); +} + +.mod-info-label img.inline-attribute.Immobilize { + content: url("https://assets-bucket.deadlock-api.com/assets-api-res/icons/condition_immobilize.svg"); +} + +.prop_duration .important-stat-icon, +.prop_slow .important-stat-icon, +.prop_stun .important-stat-icon, +.prop_silence .important-stat-icon, +.prop_tech_power .important-stat-icon, +.prop_spirit .important-stat-icon { + filter: var(--tooltip-filter-spirit); +} + +.prop_cooldown .important-stat-icon, +.prop_charge_cooldown .important-stat-icon { + filter: none; +} + +.prop_fire_rate .important-stat-icon, +.prop_clipsize .important-stat-icon, +.prop_reload_speed .important-stat-icon, +.prop_melee_damage .important-stat-icon { + filter: var(--tooltip-filter-courage); +} + +/* These API SVGs already include their own palette. */ +.prop_bullet_armor_up .important-stat-icon, +.prop_bullet_armor_down .important-stat-icon, +.prop_bullet_damage .important-stat-icon { + filter: none; +} + +.prop_health .important-stat-icon, +.prop_healing .important-stat-icon, +.prop_move_speed .important-stat-icon, +.prop_combat_barrier .important-stat-icon { + filter: var(--tooltip-filter-fortitude); +} + +.prop_damage .important-stat-icon { + filter: brightness(0) saturate(100%) invert(41%) sepia(90%) saturate(1254%) hue-rotate(347deg) brightness(103%) contrast(107%); +} + +.prop_souls .important-stat-icon { + filter: brightness(0) saturate(100%) invert(94%) sepia(44%) saturate(481%) hue-rotate(88deg) brightness(104%) contrast(101%); +} + +.prop_distance .important-stat-icon, +.prop_cast .important-stat-icon { + filter: brightness(0) saturate(100%) invert(97%) sepia(12%) saturate(600%) hue-rotate(0deg) brightness(107%); +} + +/* These API SVGs already include their own palette. */ +.prop_range .important-stat-icon, +.prop_tech_damage .important-stat-icon, +.prop_tech_armor_up .important-stat-icon, +.prop_tech_armor_down .important-stat-icon { + filter: none; } /* ─── Component items ─── */ -.component-items-section { - padding: 8px 15px; - background-color: rgba(0, 0, 0, 0.3); +.component-items-shell { + display: flex; + flex-direction: column; + width: 100%; + margin-top: -1px; + border-radius: 0 0 8px 8px; + background-color: #0f0f1a; + background-size: cover; + background-position: center; + background-repeat: no-repeat; } -.component-items-section + .component-items-section { - padding-top: 4px; +.component-items-section { + display: flex; + flex-direction: column; + width: 100%; + background-color: rgba(0, 0, 0, 0.3); } .component-items-label { - font-size: 12px; + width: 100%; + padding: 4px 10px 2px; + color: rgba(255, 255, 255, 0.52); + font-size: 14px; font-weight: 700; - color: rgba(200, 200, 210, 0.6); - text-transform: uppercase; - letter-spacing: 0.5px; - margin-bottom: 4px; + line-height: 1.2; text-align: left; + text-transform: uppercase; } .component-items-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); - gap: 8px; + display: flex; + flex-flow: row wrap; + gap: 5px 8px; + width: 100%; + padding: 0 5px 5px 10px; } .component-item { display: flex; align-items: center; gap: 6px; + min-width: 0; } .component-item-icon { - width: 32px; - height: 32px; - object-fit: contain; flex-shrink: 0; + width: 30px; + height: 30px; + object-fit: contain; + opacity: 0.9; } .component-item-name { + overflow: hidden; + color: rgba(255, 239, 215, 0.75); font-size: 14px; - font-weight: 600; - color: #c8bba8; + font-weight: 700; + text-overflow: ellipsis; white-space: nowrap; } diff --git a/packages/core/src/components/dl-item-tooltip/dl-item-tooltip.tsx b/packages/core/src/components/dl-item-tooltip/dl-item-tooltip.tsx index f2ecdf4..e96e0fc 100644 --- a/packages/core/src/components/dl-item-tooltip/dl-item-tooltip.tsx +++ b/packages/core/src/components/dl-item-tooltip/dl-item-tooltip.tsx @@ -1,6 +1,7 @@ import { Component, Prop, State, Watch, h } from '@stencil/core'; +import type { VNode } from '@stencil/core'; import { Item, ItemProperty, ItemClassName, Language, TooltipSection } from '../../types'; -import { formatPropertyValue, isPropertyVisible, formatCost, getSlotColor } from '../../utils/format'; +import { isPropertyVisible, getSlotColor } from '../../utils/format'; import { tooltipHeaderBg, tooltipBodyBg, soulIcon } from '../../utils/assets'; import { fetchItem, fetchItems } from '../../api/client'; import { configState, onConfigChange } from '../../store/config-store'; @@ -11,6 +12,11 @@ export interface ComponentItemInfo { image?: string; } +interface SectionTiming { + key: string; + prop: ItemProperty; +} + @Component({ tag: 'dl-item-tooltip', styleUrl: 'dl-item-tooltip.css', @@ -63,10 +69,17 @@ export class DlItemTooltip { return this.itemId ?? this.itemClassName; } + private get displayName(): string { + // nameOverride prop (from dl-item-card) takes highest priority, + // then internally resolved name override, then item name + return this.nameOverride ?? this._nameOverride ?? this.item?.name ?? ''; + } + // ─── Lifecycle ──────────────────────────────────────────────────────────── connectedCallback() { injectFonts(); + if (this.itemKey && !this.itemData) { this.fetchItemData(); } else if (this.itemData) { @@ -74,6 +87,7 @@ export class DlItemTooltip { this.resolveParentItems(); this.resolveNameOverride(); } + this._unsubLanguage = onConfigChange('language', () => { if (this.itemKey && !this.itemData) { this.fetchItemData(); @@ -103,8 +117,10 @@ export class DlItemTooltip { private async fetchItemData() { const key = this.itemKey; if (!key) return; + this._loading = true; this._error = undefined; + try { this._item = await fetchItem(key, configState.language); this.resolveComponentItems(); @@ -124,8 +140,8 @@ export class DlItemTooltip { try { const allItems = await fetchItems(configState.language); const byClassName = new Map(allItems.map(i => [i.class_name, i])); - const resolved: ComponentItemInfo[] = []; + for (const cn of item.component_items) { const comp = byClassName.get(cn); if (!comp) continue; @@ -134,6 +150,7 @@ export class DlItemTooltip { image: comp.shop_image_webp || comp.shop_image || comp.image_webp || comp.image || undefined, }); } + this._componentItems = resolved; } catch { // silently fail @@ -146,8 +163,8 @@ export class DlItemTooltip { try { const allItems = await fetchItems(configState.language); - const parents: ComponentItemInfo[] = []; + for (const other of allItems) { if (other.component_items?.includes(item.class_name)) { parents.push({ @@ -156,6 +173,7 @@ export class DlItemTooltip { }); } } + this._parentItems = parents.length > 0 ? parents : undefined; } catch { // silently fail @@ -168,21 +186,17 @@ export class DlItemTooltip { this._nameOverride = undefined; return; } + try { const items = await fetchItems(this.itemNameLanguage); const match = items.find(i => i.class_name === item.class_name); this._nameOverride = match?.name; } catch { // silently fail — fall back to default name + this._nameOverride = undefined; } } - private get displayName(): string { - // nameOverride prop (from dl-item-card) takes highest priority, - // then internally resolved name override, then item name - return this.nameOverride ?? this._nameOverride ?? this.item?.name ?? ''; - } - // ─── Rendering helpers (unchanged) ─────────────────────────────────────── private static STATUS_EFFECT_LABELS: Record = { @@ -193,7 +207,93 @@ export class DlItemTooltip { StatusEffectInfiniteClip: { label: 'Infinite Clip', sublabel: '' }, }; - private renderImportantProp(key: string) { + private static DEFAULT_BINDING_KEYS: Record = { + AbilityMelee: 'Q', + Ability1: '1', + Ability2: '2', + Ability3: '3', + Ability4: '4', + Item1: 'Z', + Item2: 'X', + Item3: 'C', + Item4: 'V', + Mantle: 'SPACE', + Roll: 'SHIFT', + Crouch: 'CTRL', + MoveDown: 'CTRL', + OpenHeroSheet: 'B', + PurchaseQuickbuy: 'G', + Scoreboard: 'TAB', + Reload: 'R', + ReplayDeath: 'R', + ExtraInfo: 'ALT', + AltCast: 'MOUSE3', + MoveForward: 'W', + MoveBackwards: 'S', + MoveLeft: 'A', + MoveRight: 'D', + Attack: 'MOUSE1', + ADS: 'MOUSE2', + HeldItem: 'F', + Zipline: 'SPACE', + PushToTalk: 'T', + }; + + private getFormattedParts(prop: ItemProperty) { + const value = prop.value === null || prop.value === undefined ? '' : String(prop.value); + const numericValue = Number.parseFloat(value); + const sign = Number.isFinite(numericValue) && numericValue >= 0 ? '+' : ''; + const prefix = prop.prefix?.replace('{s:sign}', sign) ?? ''; + const postfix = prop.postfix ?? ''; + const trimmedPostfix = postfix.trim(); + const suffix = trimmedPostfix && value.endsWith(trimmedPostfix) ? '' : postfix; + + return { prefix, value, suffix }; + } + + private static escapeHtml(value: string): string { + return value.replace(/[&<>"']/g, char => ({ + '&': '&', + '<': '<', + '>': '>', + '"': '"', + '\'': ''', + })[char] ?? char); + } + + private formatDescriptionHtml(html: string): string { + return html.replace( + /\{g:citadel_(?:binding|keybind):(?:'([^']+)'|"([^"]+)"|([^}]+))\}/g, + (_match, singleQuoted?: string, doubleQuoted?: string, bare?: string) => { + const binding = (singleQuoted ?? doubleQuoted ?? bare ?? '').trim().replace(/^Default\./, ''); + const key = DlItemTooltip.DEFAULT_BINDING_KEYS[binding] ?? binding; + + return `${DlItemTooltip.escapeHtml(key)}`; + }, + ); + } + + private renderFormattedValue(prop: ItemProperty, shrinkPostfix = false): VNode { + const { prefix, value, suffix } = this.getFormattedParts(prop); + + return ( + + {prefix && {prefix}} + {value} + {suffix && {suffix}} + + ); + } + + private isTimingKey(key: string, prop?: ItemProperty | null): boolean { + void prop; + return key === 'AbilityCooldown' + || key === 'ProcCooldown' + || key === 'AbilityChargeUpTime' + || key === 'AbilityCooldownBetweenCharge'; + } + + private renderImportantProp(key: string): VNode | null { const item = this.item; if (!item?.properties) return null; @@ -201,168 +301,218 @@ export class DlItemTooltip { if (!prop || !isPropertyVisible(prop)) { const statusEffect = DlItemTooltip.STATUS_EFFECT_LABELS[key]; - if (statusEffect) { - return ( -
+ if (!statusEffect) return null; + + return ( +
+
{statusEffect.label}
{statusEffect.sublabel &&
{statusEffect.sublabel}
}
- ); - } - return null; +
+ ); } - const value = formatPropertyValue(prop); - return ( -
-
- {prop.icon && } -
{value}
+
+
+
+ {prop.icon && } +
{this.renderFormattedValue(prop, true)}
+
+
+
{prop.label ?? key}
+ {(prop.conditional || prop.usage_flags?.includes('ConditionallyApplied')) && ( +
Conditional
+ )} +
-
{prop.label ?? key}
- {(prop.conditional || prop.usage_flags?.includes('ConditionallyApplied')) && ( -
Conditional
- )}
); } - private renderProperty(key: string, elevated: boolean) { + private renderBlockProperty(key: string, elevated: boolean): VNode | null { const item = this.item; if (!item?.properties) return null; const prop = item.properties[key]; if (!prop || !isPropertyVisible(prop)) return null; - const value = formatPropertyValue(prop); - const isNegative = prop.negative_attribute === true; - - return [ -
- {prop.icon && } - - {value} - - {prop.label ?? key} -
, - prop.conditional &&
, - ]; - } - - private renderBlockProperty(key: string, elevated: boolean) { - const item = this.item; - if (!item?.properties) return null; - - const prop = item.properties[key]; - if (!prop || !isPropertyVisible(prop)) return null; - - const value = formatPropertyValue(prop); - const isNegative = prop.negative_attribute === true; - return ( -
- - {value} - - {prop.label ?? key} +
+ {this.renderFormattedValue(prop)} + {prop.label ?? key}
); } - private renderSectionContent(section: TooltipSection, excludeKey?: string) { + private renderSectionContent(section: TooltipSection, excludedKeys = new Set()) { + const itemProperties = this.item?.properties; + return section.section_attributes.map(attr => { const importantKeys = new Set(attr.important_properties ?? []); + const isExcluded = (key: string) => excludedKeys.has(key) || this.isTimingKey(key, itemProperties?.[key]); + + const importantList = (attr.important_properties ?? []).filter(key => !isExcluded(key)); const regularProps = [ - ...(attr.properties ?? []), ...(attr.elevated_properties ?? []), - ].filter(k => !importantKeys.has(k) && k !== excludeKey); + ...(attr.properties ?? []), + ].filter(key => !importantKeys.has(key) && !isExcluded(key)); const elevatedSet = new Set(attr.elevated_properties ?? []); - const importantList = attr.important_properties ?? []; - const hasImportant = importantList.length > 0; - - return [ - attr.loc_string && ( -
- ), - - hasImportant ? ( -
-
- {importantList.map(k => this.renderImportantProp(k))} + const importantNodes: VNode[] = []; + const regularNodes: VNode[] = []; + + importantList.forEach(key => { + const node = this.renderImportantProp(key); + if (node) importantNodes.push(node); + }); + + regularProps.forEach(key => { + const node = this.renderBlockProperty(key, elevatedSet.has(key)); + if (node) regularNodes.push(node); + }); + + const hasImportant = importantNodes.length > 0; + const hasRegular = regularNodes.length > 0; + const hasDescription = !!attr.loc_string; + const descriptionHtml = attr.loc_string ? this.formatDescriptionHtml(attr.loc_string) : ''; + + return ( +
= 2, + [`important-count-${importantNodes.length}`]: hasImportant, + 'no-applied-stats': !hasRegular, + }} + > + {descriptionHtml &&
} + + {(hasImportant || hasRegular) && ( +
3 || (importantNodes.length >= 2 && hasRegular), + 'stats-block-no-important': !hasImportant, + }} + > + {hasImportant && ( +
+ {importantNodes} +
+ )} + {hasRegular && ( +
+ {regularNodes} +
+ )} + {!hasRegular && hasImportant &&
}
- {regularProps.length > 0 && ( -
- {regularProps.map(propKey => this.renderBlockProperty(propKey, elevatedSet.has(propKey)))} -
- )} -
- ) : ( - regularProps.map(propKey => this.renderProperty(propKey, elevatedSet.has(propKey))) - ), - ]; + )} +
+ ); }); } - private renderInnateSection(section: TooltipSection) { - return ( -
- {this.renderSectionContent(section)} -
- ); - } + private findSectionTimings(section: TooltipSection): { cooldown?: SectionTiming; chargeUp?: SectionTiming } { + const item = this.item; + if (!item?.properties) return {}; - private isCooldownKey(key: string, prop: ItemProperty): boolean { - return prop.css_class === 'cooldown' - || key === 'AbilityCooldown' - || key === 'ProcCooldown' - || key === 'AbilityChargeUpTime'; - } + const props = item.properties; + const timings: { cooldown?: SectionTiming; chargeUp?: SectionTiming } = {}; - private findSectionCooldown(section: TooltipSection) { - const item = this.item; - if (!item?.properties) return null; + const addTiming = (key: string) => { + const prop = props[key]; + if (!prop || !isPropertyVisible(prop)) return; - for (const attr of section.section_attributes) { - const allProps = [...(attr.properties ?? []), ...(attr.elevated_properties ?? [])]; - for (const key of allProps) { - const prop = item.properties[key]; - if (prop && this.isCooldownKey(key, prop) && isPropertyVisible(prop)) { - return { key, prop }; + if (key === 'AbilityChargeUpTime' || key === 'AbilityCooldownBetweenCharge') { + timings.chargeUp = { key, prop }; + return; + } + + if (key === 'ProcCooldown' || key === 'AbilityCooldown') { + if (!timings.cooldown || key === 'ProcCooldown') { + timings.cooldown = { key, prop }; } } + }; + + for (const attr of section.section_attributes) { + [ + ...(attr.important_properties ?? []), + ...(attr.elevated_properties ?? []), + ...(attr.properties ?? []), + ].forEach(addTiming); } - const cooldown = item.properties['AbilityCooldown']; - if (cooldown && isPropertyVisible(cooldown)) { - return { key: 'AbilityCooldown', prop: cooldown }; + if (!timings.cooldown && section.section_type === 'active') { + addTiming('AbilityCooldown'); } - return null; + return timings; + } + + private renderTimingPill(timing: SectionTiming, kind: 'cooldown' | 'charge-up'): VNode { + const fallbackIcon = kind === 'cooldown' ? this.item?.properties?.['AbilityCooldown']?.icon : undefined; + const icon = timing.prop.icon || fallbackIcon; + + return ( + + {icon && } + {this.renderFormattedValue(timing.prop, true)} + + ); + } + + private renderInnateSection(section: TooltipSection) { + return ( +
+ {this.renderSectionContent(section)} +
+ ); } private renderAbilitySection(section: TooltipSection) { const sectionType = section.section_type ?? 'passive'; - const cooldown = this.findSectionCooldown(section); + const timings = this.findSectionTimings(section); + const excludedKeys = new Set(); + + if (timings.cooldown) excludedKeys.add(timings.cooldown.key); + if (timings.chargeUp) excludedKeys.add(timings.chargeUp.key); return ( -
+
- {sectionType} - {cooldown && ( - - {(cooldown.prop.icon || this.item?.properties?.['AbilityCooldown']?.icon) && ( - - )} - {formatPropertyValue(cooldown.prop)} + {sectionType} + {(timings.cooldown || timings.chargeUp) && ( + + {timings.cooldown && this.renderTimingPill(timings.cooldown, 'cooldown')} + {timings.chargeUp && this.renderTimingPill(timings.chargeUp, 'charge-up')} )}
- {this.renderSectionContent(section, cooldown?.key)} + {this.renderSectionContent(section, excludedKeys)} +
+ ); + } + + private renderComponentGroup(label: string, items?: ComponentItemInfo[]): VNode | null { + if (!items || items.length === 0) return null; + + return ( +
+
{label}
+
+ {items.map(item => ( +
+ {item.image && } + {item.name} +
+ ))} +
); } @@ -373,9 +523,13 @@ export class DlItemTooltip { // Loading state (only shown in standalone mode) if (this._loading) { return ( -
-
-
+
+
+
+
+
+
+
); } @@ -383,10 +537,14 @@ export class DlItemTooltip { // Error state (only shown in standalone mode) if (this._error) { return ( -
-
-
-
{this._error}
+
+
+
+
+
+
{this._error}
+
+
@@ -400,56 +558,50 @@ export class DlItemTooltip { const slotColor = getSlotColor(slot); const headerBg = tooltipHeaderBg(slot); const bodyBg = tooltipBodyBg(slot); - const sections = item.tooltip_sections ?? []; - const resolvedComponentItems = this.componentItemsData ?? this._componentItems; const resolvedParentItems = this.parentItemsData ?? this._parentItems; + const hasComponents = !!resolvedComponentItems?.length; + const hasParents = !!resolvedParentItems?.length; return ( -
- {/* ── Header ── */} -
-
-
{this.displayName}
- {item.cost != null && item.cost > 0 && ( -
- Souls - {formatCost(item.cost)} -
- )} -
-
- - {/* ── Properties body ── */} -
- {sections.map(s => s.section_type === 'innate' ? this.renderInnateSection(s) : this.renderAbilitySection(s))} - - {resolvedComponentItems && resolvedComponentItems.length > 0 && ( -
-
Upgrades from:
-
- {resolvedComponentItems.map(comp => ( -
- {comp.image && } - {comp.name} +
+
+
+ {/* ── Header ── */} +
+
+
{this.displayName}
+ {item.cost != null && item.cost > 0 && ( +
+ Souls + {String(item.cost)}
- ))} + )}
- )} - {resolvedParentItems && resolvedParentItems.length > 0 && ( -
-
Upgrades to:
-
- {resolvedParentItems.map(parent => ( -
- {parent.image && } - {parent.name} -
- ))} -
+ {/* ── Properties body ── */} +
+ {sections.map(section => ( + section.section_type === 'innate' + ? this.renderInnateSection(section) + : this.renderAbilitySection(section) + ))} +
+
+ + {(hasComponents || hasParents) && ( +
+ {this.renderComponentGroup('Component:', resolvedComponentItems)} + {this.renderComponentGroup('Component of:', resolvedParentItems)}
)}
From bd73c98d60e9b304fcee48f08678289e3e55ffbb Mon Sep 17 00:00:00 2001 From: Victor Barroso Lino Date: Tue, 21 Apr 2026 15:37:46 -0300 Subject: [PATCH 2/3] fix(dl-item-tooltip): remove plain stat block background --- .../core/src/components/dl-item-tooltip/dl-item-tooltip.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/src/components/dl-item-tooltip/dl-item-tooltip.css b/packages/core/src/components/dl-item-tooltip/dl-item-tooltip.css index 95e6dca..52d5af8 100644 --- a/packages/core/src/components/dl-item-tooltip/dl-item-tooltip.css +++ b/packages/core/src/components/dl-item-tooltip/dl-item-tooltip.css @@ -400,6 +400,8 @@ width: 100%; min-height: 0; gap: 5px; + padding: 0; + background-color: transparent; } .stats-block-stacked .stats-block-props { From 0c3cd76a96ec740a17f5965bde04d2b525521e56 Mon Sep 17 00:00:00 2001 From: Victor Barroso Lino Date: Tue, 21 Apr 2026 16:27:26 -0300 Subject: [PATCH 3/3] fix(dl-item-tooltip): sharpen in-game stat panels --- .../core/src/components/dl-item-tooltip/dl-item-tooltip.css | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/core/src/components/dl-item-tooltip/dl-item-tooltip.css b/packages/core/src/components/dl-item-tooltip/dl-item-tooltip.css index 52d5af8..9ce825b 100644 --- a/packages/core/src/components/dl-item-tooltip/dl-item-tooltip.css +++ b/packages/core/src/components/dl-item-tooltip/dl-item-tooltip.css @@ -218,7 +218,6 @@ width: 100%; min-height: 80px; overflow: hidden; - border-radius: 5px; } /* When 1 important stat: side-by-side layout */ @@ -566,7 +565,6 @@ svg { height: 24px; margin: 0 3px; padding: 0 6px; - border-radius: 4px; background-color: #c6b6a2; color: var(--tooltip-black); font-size: 17px; @@ -1096,7 +1094,6 @@ svg { flex-direction: column; width: 100%; margin-top: -1px; - border-radius: 0 0 8px 8px; background-color: #0f0f1a; background-size: cover; background-position: center;