diff --git a/lua/spec/snapshots/standings_legend.png b/lua/spec/snapshots/standings_legend.png new file mode 100644 index 00000000000..ec5ed640687 Binary files /dev/null and b/lua/spec/snapshots/standings_legend.png differ diff --git a/lua/spec/standings_legend_spec.lua b/lua/spec/standings_legend_spec.lua new file mode 100644 index 00000000000..402369c831e --- /dev/null +++ b/lua/spec/standings_legend_spec.lua @@ -0,0 +1,21 @@ +--- Triple Comment to Enable our LLS Plugin +insulate('Widget/Legend', function() + it('integration', function() + local LegendComponent = require('Module:Widget/Legend') + + GoldenTest('standings_legend', tostring(LegendComponent{ + color = { + byeup = 'Lorem ipsum', + seedup = 'Lorem ipsum', + up = 'Lorem ipsum', + stayup = 'Lorem ipsum', + stay = 'Lorem ipsum', + staydown = 'Lorem ipsum', + down = 'Lorem ipsum', + }, + points = {'Lorem ipsum'}, + showMinimum = true, + showNumberSection = true, + })) + end) +end) diff --git a/lua/wikis/commons/Widget/Legend.lua b/lua/wikis/commons/Widget/Legend.lua new file mode 100644 index 00000000000..1d31ecc54bd --- /dev/null +++ b/lua/wikis/commons/Widget/Legend.lua @@ -0,0 +1,185 @@ +--- +-- @Liquipedia +-- page=Module:Widget/Legend +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Lua = require('Module:Lua') + +local Array = Lua.import('Module:Array') +local Json = Lua.import('Module:Json') +local Logic = Lua.import('Module:Logic') + +local Component = Lua.import('Module:Widget/Component') +local ChevronToggle = Lua.import('Module:Widget/GeneralCollapsible/ChevronToggle') +local GeneralCollapsible = Lua.import('Module:Widget/GeneralCollapsible/Default') +local Html = Lua.import('Module:Widget/Html') +local Icon = Lua.import('Module:Widget/Image/Icon/Fontawesome') +local LabelWidget = Lua.import('Module:Widget/Basic/Label') +local WidgetUtil = Lua.import('Module:Widget/Util') + +local LABEL_COLORS = {'byeup', 'seedup', 'up', 'stayup', 'stay', 'staydown', 'down'} + +---@class LegendComponent +local LegendComponent = {} + +--TODO: pass defaultProps directly to Component.component (see #7476) +LegendComponent.defaultProps = { + title = 'Legend', + showConfirmed = true, + showUndecided = true, +} + +---@param props table +---@return Widget +function LegendComponent.render(props) + return GeneralCollapsible{ + shouldCollapse = Logic.readBool(props.shouldCollapse), + collapseAreaClasses = {}, + classes = {'legend'}, + titleWidget = LegendComponent._createHeader(props), + children = WidgetUtil.collect( + LegendComponent._createColorSection(props), + LegendComponent._createPointsSection(props), + LegendComponent._createNumberSection(props) + ) + } +end + +---@private +---@return VNode +function LegendComponent._createHeader(props) + return Html.Div{ + classes = {'legend-header'}, + attributes = {['data-collapsible-click-region'] = 'true'}, + children = { + Html.Div{children = { + Icon{iconName = 'general-info'}, + Html.Span{children = {Logic.emptyOr(props.title, LegendComponent.defaultProps.title)}} + }}, + ChevronToggle{} + } + } +end + +---@private +---@return VNode? +function LegendComponent._createColorSection(props) + local sectionData = Json.parseIfString(props.color) + if Logic.isEmpty(sectionData) then + return + end + local labels = Array.map(LABEL_COLORS, function (labelColor) + local labelText = sectionData[labelColor] + if Logic.isEmpty(labelText) then + return + end + return Html.Div{ + classes = {'legend-item'}, + children = { + Html.Span{ + classes = {labelColor .. '-text'}, + children = {'●'} + }, + Html.Span{children = labelText} + } + } + end) + + if Logic.isEmpty(labels) then + return + end + + return Html.Div{ + classes = {'legend-section'}, + children = labels + } +end + +---@private +---@return VNode? +function LegendComponent._createPointsSection(props) + local sectionData = Json.parseIfString(props.points) + if not sectionData then + return + end + local pointsText = sectionData[1] or sectionData.points + if Logic.isEmpty(pointsText) then + return + end + return Html.Div{ + classes = {'legend-section'}, + children = Html.Div{ + classes = {'legend-item'}, + children = { + Html.Span{ + css = {['font-weight'] = 'bold'}, + children = {'Pts'} + }, + Html.Span{children = pointsText} + } + } + } +end + +---@private +---@return VNode? +function LegendComponent._createNumberSection(props) + if not Logic.readBool(props.showNumberSection) then + return + end + + local labels = WidgetUtil.collect( + Logic.nilOr( + Logic.readBoolOrNil(props.showConfirmed), + LegendComponent.defaultProps.showConfirmed + ) and Html.Div{ + classes = {'legend-item'}, + children = { + LabelWidget{ + labelScheme = 'placement', + labelType = 'legend-confirmed', + children = 1 + }, + Html.Span{children = {'Placement confirmed'}} + } + } or nil, + Logic.readBool(props.showMinimum) and Html.Div{ + classes = {'legend-item'}, + children = { + LabelWidget{ + labelScheme = 'placement', + labelType = 'legend-minimum', + children = 1 + }, + Html.Span{children = {'Minimum placement reached'}} + } + } or nil, + Logic.nilOr( + Logic.readBoolOrNil(props.showUndecided), + LegendComponent.defaultProps.showUndecided + ) and Html.Div{ + classes = {'legend-item'}, + children = { + LabelWidget{ + labelScheme = 'placement', + labelType = 'legend-undecided', + children = 1 + }, + Html.Span{children = {'Placement undecided'}} + } + } or nil + ) + + if Logic.isEmpty(labels) then + return + end + + return Html.Div{ + classes = {'legend-section'}, + children = labels + } +end + +return Component.component(LegendComponent.render) diff --git a/stylesheets/Main.scss b/stylesheets/Main.scss index 6b492cf170f..8dda35a0607 100644 --- a/stylesheets/Main.scss +++ b/stylesheets/Main.scss @@ -29,6 +29,7 @@ @use "commons/Jquery"; @use "commons/Dialog"; @use "commons/Label"; +@use "commons/Legend"; @use "commons/Mainpage"; @use "commons/Matchseries"; @use "commons/MatchTable"; diff --git a/stylesheets/commons/Colours.scss b/stylesheets/commons/Colours.scss index ff7329e0417..e4557b50923 100644 --- a/stylesheets/commons/Colours.scss +++ b/stylesheets/commons/Colours.scss @@ -775,3 +775,31 @@ ul.nav-tabs li.game-wiiu, --position-staydown-color: #f2a288; --position-down-color: #fc6868; } + +.byeup-text { + color: var( --position-byeup-color ); +} + +.seedup-text { + color: var( --position-seedup-color ); +} + +.up-text { + color: var( --position-up-color ); +} + +.stayup-text { + color: var( --position-stayup-color ); +} + +.stay-text { + color: var( --position-stay-color ); +} + +.staydown-text { + color: var( --position-staydown-color ); +} + +.down-text { + color: var( --position-down-color ); +} diff --git a/stylesheets/commons/Label.scss b/stylesheets/commons/Label.scss index 7d20ee36f66..c3764f97f94 100644 --- a/stylesheets/commons/Label.scss +++ b/stylesheets/commons/Label.scss @@ -119,7 +119,7 @@ --placement-text-color: var( --clr-secondary-9 ); } - &:not( [ data-placement-type ] ) { + &:not( [ data-placement-type ] ):not( [ data-label-type|="legend" ] ) { --placement-solid-color: var( --clr-on-surface-light-primary-4 ); --placement-text-color: var( --clr-secondary-25 ); @@ -132,7 +132,43 @@ &[ data-label-type="placement-minimum" ] { color: var( --placement-solid-color ); background-color: hsl( from var( --placement-solid-color ) h s l / 0.08 ); - box-shadow: 0 0 0 calc( var( --label-scale ) * 0.0125rem ) var( --placement-solid-color ) inset; + box-shadow: 0 0 0 calc( var( --label-scale ) * 0.0625rem ) var( --placement-solid-color ) inset; + } + + &[ data-label-type|="legend" ] { + --label-scale: 0.75; + } + + &[ data-label-type="legend-confirmed" ] { + --placement-solid-color: var( --clr-secondary-25 ); + --placement-text-color: var( --clr-secondary-100 ); + + .theme--dark & { + --placement-solid-color: var( --clr-secondary-90 ); + --placement-text-color: var( --clr-secondary-25 ); + } + } + + &[ data-label-type="legend-minimum" ] { + color: var( --clr-secondary-25 ); + background-color: var( --clr-on-surface-light-primary-12 ); + box-shadow: 0 0 0 0.0625rem var( --clr-secondary-25 ) inset; + + .theme--dark & { + color: var( --clr-secondary-100 ); + background-color: var( --clr-on-surface-dark-primary-12 ); + box-shadow: 0 0 0 0.0625rem var( --clr-secondary-80 ) inset; + } + } + + &[ data-label-type="legend-undecided" ] { + --placement-solid-color: var( --clr-on-surface-light-primary-12 ); + --placement-text-color: var( --clr-secondary-25 ); + + .theme--dark & { + --placement-solid-color: var( --clr-on-surface-dark-primary-12 ); + --placement-text-color: var( --clr-secondary-100 ); + } } &[ data-placement-type="byeup" ] { diff --git a/stylesheets/commons/Legend.scss b/stylesheets/commons/Legend.scss new file mode 100644 index 00000000000..e6ac5236354 --- /dev/null +++ b/stylesheets/commons/Legend.scss @@ -0,0 +1,92 @@ +.legend { + display: flex; + flex-direction: column; + gap: 0.5rem; + min-width: 16.5rem; + width: fit-content; + max-width: 100%; + border: 1px solid var( --clr-on-surface-light-primary-12 ); + border-radius: 0.5rem; + padding: 0.5rem; + background-color: var( --clr-primary-100 ); + font-size: 0.75rem; + + .theme--dark & { + border-color: var( --clr-on-surface-dark-primary-8 ); + background-color: var( --clr-on-surface-dark-primary-8 ); + } + + @media ( max-width: 435px ) { + width: 100% !important; + } + + &-header { + display: grid; + grid-template-columns: min-content auto min-content; + align-items: center; + align-content: center; + gap: 0.25rem; + font-weight: bold; + color: var( --clr-secondary-16 ); + + .theme--dark & { + color: var( --clr-secondary-90 ); + } + + > :first-child { + grid-column: 1 / -2; + display: grid; + grid-template-columns: subgrid; + margin-left: 0.5rem; + align-items: center; + } + + .general-collapsible-default-toggle { + min-width: unset; + + .general-collapsible-expand-button, + .general-collapsible-expand-button:visited, + .general-collapsible-collapse-button, + .general-collapsible-collapse-button:visited { + outline: unset; + color: var( --clr-secondary-7 ); + + .theme--dark & { + color: var( --clr-secondary-90 ); + } + } + } + } + + &-section { + display: grid; + grid-template-columns: min-content auto; + column-gap: 0.25rem; + + & + &::before { + display: block; + grid-column: 1 / -1; + content: ""; + width: 100%; + border: 0; + border-top: 0.125rem solid var( --clr-on-surface-light-primary-8 ); + padding: 0; + position: relative; + top: -0.25rem; + + .theme--dark & { + border-color: var( --clr-on-surface-dark-primary-8 ); + } + } + } + + &-item { + display: grid; + grid-column: 1 / -1; + grid-template-columns: subgrid; + } + + &:not( .collapsed ) .should-collapse { + display: contents; + } +}