Module:InfoboxNeue
Jump to navigation
Jump to search
Template loop detected: Module:InfoboxNeue/doc
This module is used by modules to build infobox.
Components
Quick facts:
InfoboxNeue
Indicator
Indicator message
Title
Subtitle
Item label
Item data
Item label
Item data
Item label
Item data
Section title
Item label
Item data
Item label
Item data
Item label
Item data
Image
infobox:renderImage( 'Pico at New Babbage 1 1.jpg' )
Indicator
Parameter | Description | Type | Status |
---|---|---|---|
data |
Data of the indicator | string | required |
desc |
Description of the indicator | string | optional |
class |
HTML classes to be added to the indicator | string | optional |
infobox:renderIndicator( {
data = 'Indicator',
desc = 'Indicator message',
} )
Header
Parameter | Description | Type | Status |
---|---|---|---|
title |
Title of the infobox | string | required |
subtitle |
Subtitle of the infobox | string | optional |
infobox:renderHeader( {
title = 'Title',
subtitle = 'Subtitle'
} )
Message
This is a shortcut way to create a message wrapped in a section.
Parameter | Description | Type | Status |
---|---|---|---|
title |
Title of the message | string | required |
desc |
Description of the message | string | optional |
infobox:renderMessage( {
title = 'Message title',
desc = 'Message description'
} )
Item
Parameter | Description | Type | Status |
---|---|---|---|
data |
Data of the item | string | required |
label |
Label of the item | string | optional |
desc |
Description of the item | string | optional |
row |
Whether to display the item in a row | boolean | optional |
spacebetween |
Whether to put space between elements in the item | boolean | optional |
colspan |
Number of columns that the item spans | int | optional |
infobox:renderItem( {
label = 'Item label',
data = 'Item data'
} )
Section
This is used to wrap items into a section.
Parameter | Description | Type | Status |
---|---|---|---|
content |
Content of the section | string | required |
title |
Title of the section | string | optional |
subtitle |
Subtitle of the section | string | optional |
col |
Number of columns in the section | int | optional |
class |
HTML classes to be added to the section | string | optional |
infobox:renderSection( {
title = 'Section title',
content = table.concat( sectionTable ),
col = 3
} )
Layout
Row
Quick facts:
InfoboxNeue
Row layout
This is an example of the row layout.
Bacon
Good
Pancetta
Great
Prosciutto
Wonderful
-- Create items
sectionTable = {
infobox:renderItem( {
label = 'Bacon',
data = 'Good',
row = true,
spacebetween = true
} ),
infobox:renderItem( {
label = 'Pancetta',
data = 'Great',
row = true,
spacebetween = true
} ),
infobox:renderItem( {
label = 'Prosciutto',
data = 'Wonderful',
row = true,
spacebetween = true
} )
}
-- Create section with items
infobox:renderSection( {
title = 'Row layout',
subtitle = 'This is an example of the row layout.',
content = table.concat( sectionTable )
} )
List
Quick facts:
InfoboxNeue
List layout
This is an example of the list layout.
Bacon is good
Bacon ipsum dolor amet burgdoggen boudin spare ribs pork pork chop drumstick beef. Jowl turkey pork, kevin shankle shank shoulder.
Pancetta is great
Kevin pig fatback, alcatra pancetta sirloin venison tri-tip shankle kielbasa meatloaf spare ribs beef. Corned beef salami kielbasa tenderloin swine spare ribs andouille.
Prosciutto is wonderful
Venison chicken meatloaf, ground round swine short ribs shankle short loin tenderloin jerky capicola. Prosciutto venison sirloin beef brisket pancetta.
-- Create items
sectionTable = {
infobox:renderItem( {
data = 'Bacon is good',
desc = 'Bacon ipsum dolor amet burgdoggen boudin spare ribs pork pork chop drumstick beef. Jowl turkey pork, kevin shankle shank shoulder. ',
} ),
infobox:renderItem( {
data = 'Pancetta is great',
desc = 'Kevin pig fatback, alcatra pancetta sirloin venison tri-tip shankle kielbasa meatloaf spare ribs beef. Corned beef salami kielbasa tenderloin swine spare ribs andouille.',
} ),
infobox:renderItem( {
data = 'Prosciutto is wonderful',
desc = 'Venison chicken meatloaf, ground round swine short ribs shankle short loin tenderloin jerky capicola. Prosciutto venison sirloin beef brisket pancetta.',
} )
}
-- Create section with items
infobox:renderSection( {
title = 'List layout',
subtitle = 'This is an example of the list layout.',
content = table.concat( sectionTable )
} )
Grid
Quick facts:
InfoboxNeue
2 col grid layout
This is an example of the two column grid layout.
Bacon
Good
Pancetta
Great
Prosciutto
Wonderful
Capicola
Delightful
3 col grid layout
This is an example of the three column grid layout.
Bacon
Good
Pancetta
Great
Prosciutto
Wonderful
Capicola
Delightful
4 col grid layout
This is an example of the four column grid layout.
Bacon
Good
Pancetta
Great
Prosciutto
Wonderful
Capicola
Delightful
-- Create items
sectionTable = {
infobox:renderItem( {
label = 'Bacon',
data = 'Good'
} ),
infobox:renderItem( {
label = 'Pancetta',
data = 'Great'
} ),
infobox:renderItem( {
label = 'Prosciutto',
data = 'Wonderful'
} ),
infobox:renderItem( {
label = 'Capicola',
data = 'Delightful'
} )
}
-- Create section with items
infobox:renderSection( {
title = '2 col grid layout',
subtitle = 'This is an example of the two column grid layout.',
content = table.concat( sectionTable ),
col = 2
} )
infobox:renderSection( {
title = '3 col grid layout',
subtitle = 'This is an example of the three column grid layout.',
content = table.concat( sectionTable ),
col = 3
} )
infobox:renderSection( {
title = '4 col grid layout',
subtitle = 'This is an example of the four column grid layout.',
content = table.concat( sectionTable ),
col = 4
} )
local InfoboxNeue = {}
local metatable = {}
local methodtable = {}
local libraryUtil = require( 'libraryUtil' )
local checkType = libraryUtil.checkType
local checkTypeMulti = libraryUtil.checkTypeMulti
local i18n = require( 'Module:i18n' ):new()
metatable.__index = methodtable
metatable.__tostring = function( self )
return tostring( self:renderInfobox() )
end
--- Wrapper function for Module:i18n.translate
---
--- @param key string The translation key
--- @return string If the key was not found, the key is returned
local function t( key )
return i18n:translate( key )
end
--- Helper function to restore underscore from space
--- so that it does not screw up the external link wikitext syntax
--- For some reason SMW property converts underscore into space
--- mw.uri.encode can't be used on full URL
local function restoreUnderscore( s )
return s:gsub( ' ', '%%5F' )
end
--- Helper function to format string to number with separators
--- It is usually use to re-format raw number from SMW into more readable format
local function formatNumber( s )
local lang = mw.getContentLanguage()
if s == nil then
return
end
if type( s ) ~= 'number' then
s = tonumber( s )
end
if type( s ) == 'number' then
return lang:formatNum( s )
end
return s
end
--- Put table values into a comma-separated list
---
--- @param data table
--- @return string
function methodtable.tableToCommaList( data )
if type( data ) == 'table' then
return table.concat( data, ', ' )
else
return data
end
end
--- Show range if value1 and value2 are different
---
--- @param s1 string|nil
--- @param s2 string|nil
--- @return string|nil
function methodtable.formatRange( s1, s2, formatNum )
if s1 == nil and s2 == nil then
return
end
formatNum = formatNum or false;
if formatNum then
if s1 then
s1 = formatNumber( s1 )
end
if s2 then
s2 = formatNumber( s2 )
end
end
if s1 and s2 and s1 ~= s2 then
return s1 .. ' – ' .. s2
end
return s1 or s2
end
--- Append unit to the value if exists
---
--- @param s string
--- @param unit string
--- @return string|nil
function methodtable.addUnitIfExists( s, unit )
if s == nil then
return
end
return s .. ' ' .. unit
end
--- Shortcut to return the HTML of the infobox message component as string
---
--- @param data table {title, desc)
--- @return string html
function methodtable.renderMessage( self, data, noInsert )
checkType( 'Module:InfoboxNeue.renderMessage', 1, self, 'table' )
checkType( 'Module:InfoboxNeue.renderMessage', 2, data, 'table' )
checkType( 'Module:InfoboxNeue.renderMessage', 3, noInsert, 'boolean', true )
noInsert = noInsert or false
local item = self:renderSection( { content = self:renderItem( { data = data.title, desc = data.desc } ) }, noInsert )
if not noInsert then
table.insert( self.entries, item )
end
return item
end
--- Return the HTML of the infobox image component as string
---
--- @param filename string
--- @return string html
function methodtable.renderImage( self, filename )
checkType( 'Module:InfoboxNeue.renderImage', 1, self, 'table' )
local hasPlaceholderImage = false
if type( filename ) ~= 'string' and self.config.displayPlaceholder == true then
hasPlaceholderImage = true
filename = self.config.placeholderImage
-- Add tracking category for infoboxes using placeholder image
table.insert( self.categories,
string.format( '[[Category:%s]]', t( 'category_infobox_using_placeholder_image' ) )
)
end
if type( filename ) ~= 'string' then
return ''
end
local parts = mw.text.split( filename, ':', true )
if #parts > 1 then
table.remove( parts, 1 )
filename = table.concat( parts, ':' )
end
local html = mw.html.create( 'div' )
:addClass( 'infobox__image' )
:wikitext( mw.ustring.format( '[[File:%s|400px]]', filename ) )
if hasPlaceholderImage == true then
local icon = mw.html.create( 'span' ):addClass( 'citizen-ui-icon mw-ui-icon-wikimedia-upload' )
-- TODO: Point the Upload link to a specific file name
html:tag( 'div' ):addClass( 'infobox__image-upload' )
:wikitext( mw.ustring.format( '[[%s|%s]]', 'Special:UploadWizard', tostring( icon ) .. t( 'label_upload_image' ) ) )
end
local item = tostring( html )
table.insert( self.entries, item )
return item
end
--- Return the HTML of the infobox indicator component as string
---
--- @param data table {data, desc, class)
--- @return string html
function methodtable.renderIndicator( self, data )
checkType( 'Module:InfoboxNeue.renderIndicator', 1, self, 'table' )
checkType( 'Module:InfoboxNeue.renderIndicator', 2, data, 'table' )
if data == nil or data[ 'data' ] == nil or data[ 'data' ] == '' then return '' end
local html = mw.html.create( 'div' ):addClass( 'infobox__indicator' )
html:wikitext(
self:renderItem(
{
[ 'data' ] = data[ 'data' ],
[ 'desc' ] = data[ 'desc' ] or nil,
row = true,
spacebetween = true
}
)
)
if data[ 'class' ] then html:addClass( data[ 'class' ] ) end
local item = tostring( html )
table.insert( self.entries, item )
return item
end
--- Return the HTML of the infobox header component as string
---
--- @param data table {title, subtitle, badge)
--- @return string html
function methodtable.renderHeader( self, data )
checkType( 'Module:InfoboxNeue.renderHeader', 1, self, 'table' )
checkTypeMulti( 'Module:InfoboxNeue.renderHeader', 2, data, { 'table', 'string' } )
if type( data ) == 'string' then
data = {
title = data
}
end
if data == nil or data[ 'title' ] == nil then return '' end
local html = mw.html.create( 'div' ):addClass( 'infobox__header' )
if data[ 'badge' ] then
html:tag( 'div' )
:addClass( 'infobox__item infobox__badge' )
:wikitext( data[ 'badge' ] )
end
local titleItem = mw.html.create( 'div' ):addClass( 'infobox__item' )
titleItem:tag( 'div' )
:addClass( 'infobox__title' )
:wikitext( data[ 'title' ] )
if data[ 'subtitle' ] then
titleItem:tag( 'div' )
-- Subtitle is always data
:addClass( 'infobox__subtitle infobox__data' )
:wikitext( data[ 'subtitle' ] )
end
html:node( titleItem )
local item = tostring( html )
table.insert( self.entries, item )
return item
end
--- Wrap the HTML into an infobox section
---
--- @param data table {title, subtitle, content, border, col, class}
--- @param noInsert boolean whether to insert this section into the internal table table
--- @return string html
function methodtable.renderSection( self, data, noInsert )
checkType( 'Module:InfoboxNeue.renderSection', 1, self, 'table' )
checkType( 'Module:InfoboxNeue.renderSection', 2, data, 'table' )
checkType( 'Module:InfoboxNeue.renderSection', 3, noInsert, 'boolean', true )
noInsert = noInsert or false
if type( data.content ) == 'table' then
data.content = table.concat( data.content )
end
if data == nil or data[ 'content' ] == nil or data[ 'content' ] == '' then return '' end
local html = mw.html.create( 'div' ):addClass( 'infobox__section' )
if data[ 'title' ] then
local header = html:tag( 'div' ):addClass( 'infobox__sectionHeader' )
header:tag( 'div' )
:addClass( 'infobox__sectionTitle' )
:wikitext( data[ 'title' ] )
if data[ 'subtitle' ] then
header:tag( 'div' )
:addClass( 'infobox__sectionSubtitle' )
:wikitext( data[ 'subtitle' ] )
end
end
local content = html:tag( 'div' )
content:addClass( 'infobox__sectionContent')
:wikitext( data[ 'content' ] )
if data[ 'border' ] == false then html:addClass( 'infobox__section--noborder' ) end
if data[ 'col' ] then content:addClass( 'infobox__grid--cols-' .. data[ 'col' ] ) end
if data[ 'class' ] then html:addClass( data[ 'class' ] ) end
local item = tostring( html )
if not noInsert then
table.insert( self.entries, item )
end
return item
end
--- Return the HTML of the infobox link button component as string
---
--- @param data table {label, link, page}
--- @return string html
function methodtable.renderLinkButton( self, data )
checkType( 'Module:InfoboxNeue.renderLinkButton', 1, self, 'table' )
checkType( 'Module:InfoboxNeue.renderLinkButton', 2, data, 'table' )
if data == nil or data[ 'label' ] == nil or ( data[ 'link' ] == nil and data[ 'page' ] == nil ) then return '' end
--- Render multiple linkButton when link is a table
if type( data[ 'link' ] ) == 'table' then
local htmls = {}
for i, url in ipairs( data[ 'link' ] ) do
table.insert( htmls,
self:renderLinkButton( {
label = mw.ustring.format( '%s %d', data[ 'label' ], i ),
link = url
} )
)
end
return table.concat( htmls )
end
local html = mw.html.create( 'div' ):addClass( 'infobox__linkButton' )
if data[ 'link' ] then
html:wikitext( mw.ustring.format( '[%s %s]', restoreUnderscore( data[ 'link' ] ), data[ 'label' ] ) )
elseif data[ 'page' ] then
html:wikitext( mw.ustring.format( '[[%s|%s]]', data[ 'page' ], data[ 'label' ] ) )
end
return tostring( html )
end
--- Return the HTML of the infobox footer component as string
---
--- @param data table {content, button}
--- @return string html
function methodtable.renderFooter( self, data )
checkType( 'Module:InfoboxNeue.renderFooter', 1, self, 'table' )
checkType( 'Module:InfoboxNeue.renderFooter', 2, data, 'table' )
if data == nil then return '' end
-- Checks if an input is of type 'table' or 'string' and if it is not empty
local function isNonEmpty( input )
return ( type( input ) == 'table' and next( input ) ~= nil ) or ( type( input ) == 'string' and #input > 0 )
end
local hasContent = isNonEmpty( data[ 'content' ] )
local hasButton = isNonEmpty( data[ 'button' ] ) and isNonEmpty( data[ 'button' ][ 'content' ] ) and isNonEmpty( data[ 'button' ][ 'label' ] )
if not hasContent and not hasButton then return '' end
local html = mw.html.create( 'div' ):addClass( 'infobox__footer' )
if hasContent then
local content = data[ 'content' ]
if type( content ) == 'table' then content = table.concat( content ) end
html:addClass( 'infobox__footer--has-content')
html:tag( 'div' )
:addClass( 'infobox__section' )
:wikitext( content )
end
if hasButton then
html:addClass( 'infobox__footer--has-button')
local buttonData = data[ 'button' ];
local button = html:tag( 'div' ):addClass( 'infobox__button' )
local label = button:tag( 'div' ):addClass( 'infobox__buttonLabel' )
if buttonData[ 'icon' ] ~= nil then
label:wikitext( mw.ustring.format( '[[File:%s|16px|link=]]%s', buttonData[ 'icon' ], buttonData[ 'label' ] ) )
else
label:wikitext( buttonData[ 'label' ] )
end
if buttonData[ 'type' ] == 'link' then
button:tag( 'div' )
:addClass( 'infobox__buttonLink' )
:wikitext( buttonData[ 'content' ] )
elseif buttonData[ 'type' ] == 'popup' then
button:tag( 'div' )
:addClass( 'infobox__buttonCard' )
:wikitext( buttonData[ 'content' ] )
end
end
local item = tostring( html )
table.insert( self.entries, item )
return item
end
--- Return the HTML of the infobox footer button component as string
---
--- @param data table {icon, label, type, content}
--- @return string html
function methodtable.renderFooterButton( self, data )
checkType( 'Module:InfoboxNeue.renderFooterButton', 1, self, 'table' )
checkType( 'Module:InfoboxNeue.renderFooterButton', 2, data, 'table' )
if data == nil then return '' end
return self:renderFooter( { button = data } )
end
--- Return the HTML of the infobox item component as string
---
--- @param data table {label, data, desc, tooltip, icon, row, spacebetween, colspan)
--- @param content string|number|nil optional
--- @return string html
function methodtable.renderItem( self, data, content )
checkType( 'Module:InfoboxNeue.renderItem', 1, self, 'table' )
checkTypeMulti( 'Module:InfoboxNeue.renderItem', 2, data, { 'table', 'string' } )
checkTypeMulti( 'Module:InfoboxNeue.renderItem', 3, content, { 'string', 'number', 'nil' } )
-- The arguments are not passed as a table
-- Allows to call this as box:renderItem( 'Label', 'Data' )
if content ~= nil then
data = {
label = data,
data = content
}
end
if data == nil or data[ 'data' ] == nil or data[ 'data' ] == '' then return '' end
if self.config.removeEmpty == true and data[ 'data' ] == self.config.emptyString then
return ''
end
local html = mw.html.create( 'div' ):addClass( 'infobox__item' )
if data[ 'tooltip' ] then html:attr( 'title', data[ 'tooltip' ] ) end
if data[ 'row' ] == true then html:addClass( 'infobox__grid--row' ) end
if data[ 'spacebetween' ] == true then html:addClass( 'infobox__grid--space-between' ) end
if data[ 'colspan' ] then html:addClass( 'infobox__grid--col-span-' .. data[ 'colspan' ] ) end
local textWrapper = html
if data[ 'link' ] then
html:addClass( 'infobox__itemButton' )
html:tag( 'div' )
:addClass( 'infobox__itemButtonLink' )
:wikitext( mw.ustring.format( '[%s]', data[ 'link' ] ) )
elseif data[ 'page' ] then
html:addClass( 'infobox__itemButton' )
html:tag( 'div' )
:addClass( 'infobox__itemButtonLink' )
:wikitext( mw.ustring.format( '[[%s]]', data[ 'link' ] ) )
end
if data[ 'icon' ] then
html:addClass( 'infobox__item--hasIcon' )
html:tag( 'div' )
:addClass( 'infobox__icon' )
:wikitext( mw.ustring.format( '[[File:%s|16px|link=]]', data[ 'icon' ] ) )
-- Create wrapper for text to align with icon
textWrapper = html:tag( 'div' ):addClass( 'infobox__text' )
end
local dataOrder = { 'label', 'data', 'desc' }
for _, key in ipairs( dataOrder ) do
if data[ key ] then
if type( data[ key ] ) == 'table' then
data[ key ] = table.concat( data[ key ], ', ' )
end
textWrapper:tag( 'div' )
:addClass( 'infobox__' .. key )
:wikitext( data[ key ] )
end
end
-- Add arrow indicator as affordnance
if data[ 'link' ] or data[ 'page' ] then
html:tag( 'div' ):addClass( 'infobox__itemButtonArrow citizen-ui-icon mw-ui-icon-wikimedia-collapse' )
end
return tostring( html )
end
--- Wrap the infobox HTML
---
--- @param innerHtml string inner html of the infobox
--- @param snippetText string text used in snippet in mobile view
--- @return string html infobox html with templatestyles
function methodtable.renderInfobox( self, innerHtml, snippetText )
checkType( 'Module:InfoboxNeue.renderInfobox', 1, self, 'table' )
checkTypeMulti( 'Module:InfoboxNeue.renderInfobox', 2, innerHtml, { 'table', 'string', 'nil' } )
checkType( 'Module:InfoboxNeue.renderInfobox', 3, snippetText, 'string', true )
innerHtml = innerHtml or self.entries
if type( innerHtml ) == 'table' then
innerHtml = table.concat( self.entries )
end
local function renderSnippet()
if snippetText == nil then snippetText = mw.title.getCurrentTitle().text end
local html = mw.html.create( 'div' )
html
:addClass( 'infobox__snippet mw-collapsible-toggle' )
:attr( 'role', 'button' )
:attr( 'aria-owns', 'infobox__content' )
:tag( 'div' )
:addClass( 'citizen-ui-icon mw-ui-icon-wikimedia-collapse' )
:done()
:tag( 'div' )
:addClass( 'infobox__data' )
:wikitext( mw.ustring.format( '%s:', t( 'label_quick_facts' ) ) )
:done()
:tag( 'div' )
:addClass( 'infobox__desc' )
:wikitext( snippetText )
return tostring( html )
end
local html = mw.html.create( 'div' )
html
:addClass( 'infobox floatright mw-collapsible' )
:wikitext( renderSnippet() )
:tag( 'div' )
:addClass( 'infobox__content mw-collapsible-content' )
:attr( 'id', 'infobox__content' )
:wikitext( innerHtml )
return tostring( html ) .. mw.getCurrentFrame():extensionTag{
name = 'templatestyles', args = { src = 'Module:InfoboxNeue/styles.css' }
} .. table.concat( self.categories )
end
--- Just an accessor for the class method
function methodtable.showDescIfDiff( s1, s2 )
return InfoboxNeue.showDescIfDiff( s1, s2 )
end
--- Format text to show comparison as desc text if two strings are different
---
--- @param s1 string|nil base
--- @param s2 string|nil comparsion
--- @return string|nil html
function InfoboxNeue.showDescIfDiff( s1, s2 )
if s1 == nil or s2 == nil or s1 == s2 then return s1 end
return mw.ustring.format( '%s <span class="infobox__desc">(%s)</span>', s1, s2 )
end
--- New Instance
---
--- @return table InfoboxNeue
function InfoboxNeue.new( self, config )
local baseConfig = {
-- Flag to discard empty rows
removeEmpty = false,
-- Optional string which is valued as empty
emptyString = nil,
-- Display a placeholder image if addImage does not find an image
displayPlaceholder = true,
-- Placeholder Image
placeholderImage = 'Platzhalter.webp',
}
for k, v in pairs( config or {} ) do
baseConfig[ k ] = v
end
local instance = {
categories = {},
config = baseConfig,
entries = {}
}
setmetatable( instance, metatable )
return instance
end
--- Create an Infobox from args
---
--- @param frame table
--- @return string
function InfoboxNeue.fromArgs( frame )
local instance = InfoboxNeue:new()
local args = require( 'Module:Arguments' ).getArgs( frame )
local sections = {
{ content = {}, col = args[ 'col' ] or 2 }
}
local sectionMap = { default = 1 }
local currentSection
if args[ 'image' ] then
instance:renderImage( args[ 'image' ] )
end
if args[ 'indicator' ] then
instance:renderIndicator( {
data = args[ 'indicator' ],
desc = args[ 'indicatorDesc' ],
class = args[ 'indicatorClass' ]
} )
end
if args[ 'title' ] then
instance:renderHeader( {
title = args[ 'title' ],
subtitle = args[ 'subtitle' ],
} )
end
for i = 1, 50, 1 do
if args[ 'section' .. i ] then
currentSection = args[ 'section' .. i ]
table.insert( sections, {
title = currentSection,
subtitle = args[ 'section-subtitle' .. i ],
col = args[ 'section-col' .. i ] or args[ 'col' ] or 2,
content = {}
} )
sectionMap[ currentSection ] = #sections
end
if args[ 'label' .. i ] and args[ 'content' .. i ] then
table.insert( sections[ sectionMap[ ( currentSection or 'default' ) ] ].content, instance:renderItem( args[ 'label' .. i ], args[ 'content' .. i ] ) )
end
end
for _, section in ipairs( sections ) do
instance:renderSection( {
title = section.title,
subtitle = section.subtitle,
col = section.col,
content = section.content,
} )
end
return instance:renderInfobox( nil, args[ 'snippet' ] )
end
return InfoboxNeue