Module:ProtoFlux: Difference between revisions

From Resonite Wiki
More shared css, fix multi outputs
No edit summary
Line 5: Line 5:
local ProtofluxColor = require("Module:ProtoFlux_Type_Color")
local ProtofluxColor = require("Module:ProtoFlux_Type_Color")


local css = {
local fieldHandlers = {}
root = "color: rgb(224, 224, 224); background-color: rgb(18, 20, 28); box-shadow: 0 2px 4px 2px #0005; "
.. "width: 256px; display: flex; flex-direction: column; align-items: stretch;",
node_title = "padding: 10px 0px 10px 0px; text-align: center; font-weight: bold; background-color: rgb(26, 41, 54); font-size: 18pt; flex-grow: 1;",
node_footer = "text-align: center; padding: 10px; font-size: 18pt; color: rgb(64,64,64); font-weight: bold;",
connector_row = "display: flex; min-height: 70px;",
connector_row_labels = "flex-grow: 2; overflow: visible; display: flex; flex-direction: column; justify-content: space-between; order: 2;",
multi_button = "font-size: 1.75rem; width: 1em; height: 1em; background-color: #2c2f35; border-radius: 2em; "
.. "display:flex; align-items:center; justify-content: center;",
multi_bar = function(self, is_input, multi)
return string.format(
"background-color: white; opacity: 0.5; width: 6px; height: %spx; position: absolute; bottom: 40px; %s: 13px;",
70 * (multi - 2) + 50,
is_input and "left" or "right"
)
end,
label = "text-overflow: ellipsis; border-bottom: 4px solid #11151d; border-top: 4px solid #11151d; ",
label_multi = "font-weight: bold; font-size: 1.4rem; line-height: 1;",
input_label = function(self, color, is_multi)
if is_multi then color = nil end
return string.format(
self.label
.."text-align: left; overflow: %s; padding-left: 4px; border-right: 20px solid #11151d; "
.."background-color: %s;%s",
color and "hidden" or "visible",
color and ColorToCss(MultA(color, 0.6)) or "transparent",
is_multi and " "..self.label_multi or ""
)
end,
output_label = function(self, color, is_multi)
if is_multi then color = nil end
return string.format(
self.label
.."text-align: right; overflow: %s; padding-right: 4px; border-left: 20px solid #11151d; "
.."background-color: %s;%s",
color and "hidden" or "visible",
color and ColorToCss(MultA(color, 0.6)) or "transparent",
is_multi and " "..self.label_multi or ""
)
end,
attachment_point = function(self, color, order)
return string.format(
"width: 30px; background-color: black; fill: %s; stroke: %s; order: %s;",
ColorToCss(MultA(color, 0.15)),
ColorToCss(color),
order
)
end,
global = function(self, color)
return string.format(
"display: flex; min-height: 70px; flex-direction: column; border-left: 10px solid %s;",
ColorToCss(color)
)
end,
global_input = "border-radius: 16px; text-align: center; display: flex; flex-direction: column; justify-content: center;"
}


--- Method that generates the HTML output
--- Method that generates the HTML output
Line 66: Line 11:
function p.GenerateUI( frame )
function p.GenerateUI( frame )
-- Parse the JSON input, returning an empty array if no argument was passed
-- Parse the JSON input, returning an empty array if no argument was passed
local inputs = mw.text.jsonDecode(frame.args.Inputs or '[]')
local fields = mw.text.jsonDecode(frame.args.Fields or '[]')
local outputs = mw.text.jsonDecode(frame.args.Outputs or '[]')
local globals = mw.text.jsonDecode(frame.args.Globals or '[]')
-- Create an HTML div element to contain our node UI
-- Create an HTML div element to contain our component UI
local protofluxContainer = mw.html.create( 'div' )
local componentContainer = mw.html.create( 'div' )
protofluxContainer
componentContainer
    :attr('class', frame.args.Inline and '' or 'floatright')
    :attr('class', frame.args.Inline and '' or 'floatright')
:cssText(css.root)
:cssText('color: rgb(224, 224, 224); background-color: rgb(18, 20, 28); width: 512px; display: flex; flex-direction: column; align-items: stretch; gap: 6px; padding: 8px;')
:tag('div') -- HTML div to contain the node title
:tag('div') -- HTML div to contain the component title
:cssText(css.node_title)
:cssText('color: #f8f770; /* Resonite Yellow */ border-radius: 8px; background-color: #2b2f35; /* Resonite Mid */ text-align:center; font-size: 1.25rem; font-weight: bold; flex-grow: 1;')
:wikitext(frame.args.Name)
:wikitext(frame.args.Name)
:done() -- Close node title div
:done() -- Close node title div
local processedInputs = {}
local processedOutputs = {}
for i=1,#inputs do
local multi = tonumber(inputs[i] and inputs[i].Multi or 1)
for j=1,multi do
inputs[i].MultiIndex = j
table.insert(processedInputs, { Name=inputs[i].Name, Type=inputs[i].Type, Multi=multi, MultiIndex=j })
end
end
for i=1,#outputs do
local multi = tonumber(outputs[i] and outputs[i].Multi or 1)
for j=1,multi do
table.insert(processedOutputs, { Name=outputs[i].Name, Type=outputs[i].Type, Multi=multi, MultiIndex=j })
end
end
-- Calculate the larger number of rows required (either for inputs or outputs, if the node is asymmetric)
local maxRows = math.max(#processedInputs, #processedOutputs)
-- Iterate over each row, and populate it with the inputs/outputs. If the node is asymmetric,
-- Iterate over each row, and populate it with the inputs/outputs. If the node is asymmetric,
-- the value passed for either input or output might be nil.
-- the value passed for either input or output might be nil.
for i=1,#fields do
for i=1,maxRows do
CreateField(componentContainer, fields[i])
CreateConnectorRow(protofluxContainer, processedInputs[i], processedOutputs[i])
end
-- Iterate over each global value in the node, and create a row. These elements take up the entire width
-- of the node, and so don't need to be balanced in any way.
for i=1,#globals do
CreateGlobalsRow(protofluxContainer, globals[i])
end
end
protofluxContainer
:tag('div') -- HTML div to contain the node category footer
:cssText(css.node_footer)
:wikitext(frame.args.Category)
:done() -- Close category footer div
-- Return the HTML generated above to the wiki page this script is invoked from.
-- Return the HTML generated above to the wiki page this script is invoked from.
return tostring(protofluxContainer) .. '[[Category:ProtoFlux:All]]'
return tostring(componentContainer) .. '[[Category:Components:All]]'
end
end


--- Creates a text which should be shown when hovering over a connector
--- Creates a new Component field row in the output
-- @param Input Table containing a Name (optional) and Type for the input on this row.
-- @param Container The Scribunto HTML node we'll be creating this row inside of. Should be a container of some sort.
function connector_hover_text(connector)
-- @param Field Table containing a Name, FieldType, and Type for the field on this row.
if connector.Name == nil then
function CreateField(Container, Field)
return ("<%s>"):format(connector.Type)
local fieldContentContainer = Container
end
:tag('div') -- HTML div to contain the field. We specify a min-height of 40px for consistency.
return ("%s <%s>"):format(connector.Name, connector.Type)
:cssText('display: flex;')
:tag('div') -- HTML div to contain the field
:cssText( 'flex-grow: 2; overflow: hidden; display: flex; flex-direction: row;')
fieldHandlers[Field.FieldType](fieldContentContainer, Field)
end
end


--- Creates a new ProtoFlux connector row in the output
function fieldHandlers.Sync(Container, Field)
-- @param container The Scribunto HTML node we'll be creating this row inside of. Should be a container of some sort.
Container
-- @param input Table containing a Name and Type for the input on this row. Can be nil if no input is to be placed on this row.
:tag('div') -- HTML div to contain the field label
-- @param output Table containing a Name and Type for the output on this row. Can be nil if no output is to be placed on this row.
:attr('title', Field and (Field.Name .. ' ' .. Field.FieldType .. '<' .. Field.Type .. '>') or '') -- Add a basic mouseover description
function CreateConnectorRow(container, input, output)
:cssText('text-align: left; width:30%; overflow: hidden; text-overflow: ellipsis; padding-left: 4px;')
local connector_row = container
:wikitext( (Field and Field.Name or '') .. ':' )
:tag('div') -- HTML div to contain the connector row. We specify a min-height of 70px for consistency.
:done() -- Close field label div
:cssText(css.connector_row)
if Field.Type == 'Bool' then
 
CreateUICheckbox(Container, '')
local connector_row_labels = connector_row
elseif Field.Type == 'Int' or Field.Type == 'String' then
:tag('div') -- HTML div to contain the input and output labels in the connector row
CreateUIInputBox(Container, '0')
:cssText(css.connector_row_labels)
elseif Field.Type == 'Float' or Field.Type == 'Double' then
 
Container
if input then
:tag('div')
local color, is_impulse = GetColor(input)
:cssText('display:flex; flex-direction: row; align-items:center; flex-grow:1; gap: 4px;')
local hover_text = connector_hover_text(input)
:tag('div')
 
:cssText('height: 0.5rem; border-radius: 8px; background: rgb(43, 46, 54); flex-grow:5;')
if input.Multi and input.MultiIndex > 1 then
local label = connector_row_labels
:tag('div') -- HTML div to contain the input label
:attr('title', hover_text) -- Add a basic mouseover description
:cssText(css:input_label())
if input.Multi == input.MultiIndex then
label
:tag('div')
:tag('div')
:cssText('position:relative; top:2rem; display: flex; gap: 4px;')
:cssText('height:1em; width: 1em; position:relative; top: -0.25rem; border-radius: 1em; background-color:white;')
:tag('div')
:done()
:cssText(css.multi_button)
:done()
:wikitext('+')
:tag('div')
:done()
:cssText('height: 1.5rem; border: 2px solid rgb(43, 46, 54); border-radius: 8px; background: rgb(30, 33, 38); flex-grow:1; display:flex; justify-content: center; align-items:center;')
:tag('div')
:wikitext('0')
:cssText(css.multi_button)
elseif Field.Type == "TextHorizontalAlignment" then
:wikitext('-')
CreateUIForwardBackwards(Container, "Left")
:done()
elseif Field.Type == "TextVerticalAlignment" then
:tag('div')
CreateUIForwardBackwards(Container, "Top")
:cssText(css:multi_bar(true, input.Multi))
elseif Field.Type == "Elements.Assets.AlignmentMode" then
end
CreateUIForwardBackwards(Container, "Geometric")
else
elseif Field.Type == "Alignment" then
connector_row_labels
CreateUIForwardBackwards(Container, "TopLeft")
:tag('div') -- HTML div to contain the input label
elseif Field.Type == "ColorX" then
:attr('title', hover_text) -- Add a basic mouseover description
local c = Container
:cssText(css:input_label(color, input.Multi > 1))
:tag('div')
:wikitext(input.Name or '')
:cssText('display:flex; flex-direction: column; align-items:stretch; flex-grow:1; gap: 4px;')
:done() -- Close input label div
CreateUIColor(c)
end


-- Create the input (left) attachement point
local c2 = c:tag('div')
local segments = GetTypeSegments(input)
:cssText('display:flex; flex-direction: row; align-items: stretch; flex-grow: 1; gap: 4px;')
CreateConnectorAttachmentPoint(connector_row, hover_text, color, segments, is_impulse, true)
else -- if input
connector_row:tag("div"):cssText("width: 30px; order: 1;")
connector_row_labels:tag("div")
end -- if input
 
if output then
local color, is_impulse = GetColor(output)
local hover_text = connector_hover_text(output)
 
if output.Multi and output.MultiIndex > 1 then
local label = connector_row_labels
:tag('div') -- HTML div to contain the output label
:attr('title', hover_text) -- Add a basic mouseover description
:cssText(css:output_label())
if output.Multi == output.MultiIndex then
label
:tag('div')
:tag('div')
:cssText('position:relative; display: flex; justify-content: flex-end; gap: 4px;')
:cssText('display:flex; justify-content: center; align-items:center;')
:tag('div')
:wikitext('Profile:')
:cssText(css.multi_button)
:done()
:wikitext('+')
CreateUIForwardBackwards(c2, "sRGB")
:done()
else
:tag('div')
Container
:cssText(css.multi_button)
:wikitext( Field.FieldType .. '<' .. Field.Type .. '>')
:wikitext('-')
end
:done()
:tag('div')
:cssText(css:multi_bar(false, output.Multi))
end
else
connector_row_labels
:tag('div') -- HTML div to contain the output label
:attr('title', hover_text) -- Add a basic mouseover description
:cssText(css:output_label(color, output.Multi > 1))
:wikitext(output.Name or '')
:done() -- Close output label div
end
 
-- Create the output (right) attachment point
local segments = GetTypeSegments(output)
CreateConnectorAttachmentPoint(connector_row, hover_text, color, segments, is_impulse, false);
else -- if output
connector_row:tag("div"):cssText("width: 30px; order: 3;")
end -- if output
end
end


--- Creates a new ProtoFlux global input field row in the output
function CreateUIColor(Container)
-- @param container The Scribunto HTML node we'll be creating this row inside of. Should be a container of some sort.
local c = Container
-- @param global Table containing a Name and Type for the global field on this row.
:tag('div')
function CreateGlobalsRow(container, global)
:cssText('display:flex; height: 1.5rem; flex-direction: row; align-items:stretch; flex-grow:1; gap: 4px;')
local color = GetTypeColor(global)
CreateUIBasicText(c, 'R')
local hover_text = connector_hover_text(global)
CreateUIInputBox(c, '0')
 
CreateUIBasicText(c, 'G')
container
CreateUIInputBox(c, '0')
:tag( 'div' )
CreateUIBasicText(c, 'B')
:attr('title', hover_text) -- Add a basic mouseover description
CreateUIInputBox(c, '0')
:cssText(css:global(color))
CreateUIBasicText(c, 'A')
:tag( 'div' )
CreateUIInputBox(c, '0')
:cssText('display: flex; flex-direction: row; align-items: center; flex-grow:1; overflow: hidden;')
c:tag('div')
:tag( 'span' )
:cssText('display:flex; height: 1.5rem; flex-direction: row; align-items:stretch; flex-grow:1;')
:cssText('text-align: center; overflow: hidden; text-overflow: ellipsis; font-size: 14pt; font-weight: bold; flex-grow:1;')
:tag('div')
:wikitext(global.Name)
:cssText('background-color: black; flex-grow: 1;')
:done()
:done()
:tag('div')
:cssText('background-color: white; flex-grow: 1;')
:done()
:done()
:tag( 'div' )
:done()
:attr('style', 'display: flex; gap: 10px; flex-grow: 1;')
end
:tag('div')
function CreateUIBasicText(Container, Text)
:cssText('background-color: #777; font-style: italic; flex-grow: 3; '..css.global_input)
Container
:tag( 'span' )
:wikitext(Text)
:wikitext('null')
:done()
:done()
return Container;
:done()
:tag( 'div' )
:cssText('background-color: #333; flex-grow: 1; '..css.global_input)
:tag ( 'span' )
:wikitext('∅') 
end
end


--- Creates a new input or output attachement point for a connector row.
function CreateUIForwardBackwards(Container, Text)
-- @param container The Scribunto HTML node we'll be creating this row inside of. Should be a container of some sort.
local c = Container
--  @param hover_text Text to show when hovering, possibly nil
:tag('div')
-- @param color The color of this attachment point (table with r, g, b, optionally a).
:cssText('display:flex; height: 1.5rem; flex-direction: row; align-items:stretch; flex-grow:1; gap: 4px;')
-- @param segments The number of segments for the type, used for vectors or matrices.
CreateUISmallButton(c, '<<')
-- @param is_impulse True if this is an impulse, false if it is a data type.
:tag('div')
-- @param is_input True if this is an input side attachment point, false if it is an output side attachment point.
:cssText('border-radius: 8px; background: rgb(8, 8, 10); flex-grow:1; display:flex; justify-content: center; align-items:center;')
function CreateConnectorAttachmentPoint(container, hover_text, color, segments, is_impulse, is_input)
:wikitext(Text)
local connector_type = nil
:done()
if is_impulse then connector_type = "InputArrow"
CreateUISmallButton(c, '>>')
elseif is_input then connector_type = "InputBox"
return Container
else connector_type = "OutputBox"
end
 
container
:tag( 'div' )
:attr('title', hover_text or "") -- Add a basic mouseover description
:cssText(css:attachment_point(color, is_input and 1 or 3))
:wikitext(mw.getCurrentFrame():expandTemplate({title = 'ProtoFluxConnector', args = {Type=connector_type, Segments=segments}}))
end
end


--- Returns a CSS color for the data type or impulse and whether the type is an impulse
function CreateUICheckbox(Container, Checked)
-- @param connector Table containing a Name (Optional) and Type (Required) for the input/output.
local c = Container
function GetColor(connector)
:tag('div')
if connector == nil then error("connector is nil") end
:cssText('background-color: #2b2f35; /* Resonite Mid */ border-radius: 8px; width: 1.5rem; height: 1.5rem; ')
if connector == nil or connector.Type == nil then error("Missing Type") end
if Checked then
local color = ProtofluxColor.get_impulse_color(connector.Type)
c:wikiText('[[File:Checkmark.png|checkmark|24px]]')
if color ~= nil then
return color, true
end
end
return GetTypeColor(connector), false
end
function CreateUISmallButton(Container, Text)
Container
:tag('div')
:cssText('display:flex; flex-direction: column; align-items:center; justify-content: center;')
:tag('div')
:cssText('width: 1.5rem; height: 1.5rem; background-color: rgb(43, 46, 54); border-radius: 8px; flex-grow:0; display:flex; justify-content: center; align-items:center;')
:wikitext(Text)
:done()
return Container
end
function CreateUIButton(Container, Text)
Container
:tag('div')
:cssText('display:flex; flex-direction: column; align-items:stretch; justify-content: center;')
:tag('div')
:cssText('height: 1.5rem; background-color: rgb(43, 46, 54); border-radius: 8px; flex-grow:0; display:flex; justify-content: center; align-items:center;')
:wikitext(Text)
:done()
return Container
end
end


--- Returns a CSS color for the data type
function CreateUIInputBox(Container, Text)
-- @param connector Table containing a Name (Optional) and Type (Required) for the input/output or global.
Container
function GetTypeColor(connector)
:tag('div')
if connector == nil then error("connector is nil") end
:cssText('height: 1.5em; border: 2px solid rgb(43, 46, 54); border-radius: 8px; background: rgb(30, 33, 38); flex-grow:1; display:flex; justify-content: center; align-items:center;')
if connector == nil or connector.Type == nil then error("Missing Type") end
:wikitext(Text)
local color = ProtofluxColor.get_type_color(connector.Type)
:done()
return MultRGB(color, 1.5)
end
end


function MultRGB(color, fact)
function CreateUIRefInputBox(Container, Text, Italics)
return {
Container
r=color.r*fact,
:tag('div')
g=color.g*fact,
:cssText('height: 1.5em; background: rgb(43, 46, 54); border-radius: 8px; flex-grow:1; display:flex; justify-content: center; align-items:center;' .. (Italics and 'font-style:italic;' or ''))
b=color.b*fact,
:wikitext(Text)
a=color.a,
:done()
}
return Container
end
end


function MultA(color, fact)
function CreateUISyncRefEditor(Container, Text)
return {
local c = Container
r=color.r,
:tag('div')
g=color.g,
:cssText('display:flex; flex-direction: row; align-items:stretch; flex-grow:1; gap: 4px;')
b=color.b,
a=(color.a or 1)*fact,
CreateUISmallButton(c, '⤴')
}
CreateUISmallButton(c, '↑')
CreateUIRefInputBox(c, Text or 'null', true)
CreateUISmallButton(c, '∅')
end
end


function ColorToCss(color)
function fieldHandlers.SyncRef(Container,Field)
return ("rgba(%s,%s,%s,%s)"):format(color.r*255, color.g*255, color.b*255, color.a or 1)
Container
:tag('div') -- HTML div to contain the field label
:attr('title', Field and (Field.Name .. ' ' .. Field.FieldType .. '<' .. Field.Type .. '>') or '') -- Add a basic mouseover description
:cssText('text-align: left; width:30%; overflow: hidden; text-overflow: ellipsis; padding-left: 4px;')
:wikitext( (Field and Field.Name or '') .. ':' )
:done() -- Close field label div
CreateUISyncRefEditor(Container, 'null')
end
end


local type_segments = {
function fieldHandlers.SyncAssetList(Container,Field)
enabled = {
local c = Container
bool=true,
:tag('div')
byte=true,
:cssText('display: flex; flex-direction: column; flex-grow: 1; gap: 4px;')
sbyte=true,
:tag('div') -- HTML div to contain the field label
ushort=true,
:attr('title', Field and (Field.Name .. ' ' .. Field.FieldType .. '<' .. Field.Type .. '>') or '') -- Add a basic mouseover description
short=true,
:cssText('text-align: center;')
uint=true,
:wikitext( (Field and Field.Name or '') .. '(list):' )
int=true,
:done() -- Close field label div
ulong=true,
CreateUISyncRefEditor(c, 'null')
long=true,
CreateUIButton(c, 'Add')
float=true,
end
double=true,
decimal=true,
},
suffix = {
["2"]=2,
["3"]=3,
["4"]=4,
--["2x2"]=2,
--["3x3"]=3,
--["4x4"]=4,
},
}


function fieldHandlers.AssetRef(Container,Field)
Container
:tag('div') -- HTML div to contain the field label
:attr('title', Field and (Field.Name .. ' ' .. Field.FieldType .. '<' .. Field.Type .. '>') or '') -- Add a basic mouseover description
:cssText('text-align: left; width:30%; overflow: hidden; text-overflow: ellipsis; padding-left: 4px;')
:wikitext( (Field and Field.Name or '') .. ':' )
:done() -- Close field label div
CreateUISyncRefEditor(Container, 'null')
end


--- Returns a CSS color for the data type
--- Returns an RGBA value representing the type color within Resonite
-- @param connector Table containing a Name (Optional) and Type (Required) for the input/output.
-- @param Connector Table containing a Name (Optional) and Type (Required) for the input/output attachment point.
function GetTypeSegments(connector)
-- @param Alpha The alpha to use in the RGBA value.  
function GetTypeColor(Connector, Alpha)
if connector == nil then error("connector is nil") end
if connector == nil then error("connector is nil") end
if connector == nil or connector.Type == nil then error("Missing Type") end
if connector == nil or connector.Type == nil then error("Missing Type") end
local base = connector.Type:match("^%a+")
return ProtofluxColor.get_type_color(connector.Type)
if not type_segments.enabled[base] then return 1 end
local suffix = connector.Type:sub(#base+1)
return type_segments.suffix[suffix] or 1
end
end


return p
return p

Revision as of 15:10, 11 February 2024

This is the Lua module for generating ProtoFlux UI elements on pages such as To String (ProtoFlux)

Please note, due to caching, updates made to this module will not immediately take effect on pages using it. If you need to see changes right away, use the editor view of a given page, which will update immediately.

Parameters

Name

Name of this ProtoFlux node. This should be the same as the name you see at the top of a given node in Resonite.

Category

Category for this ProtoFlux node. This is not the full category path, but only the direct parent - it should be the same as what appears at the bottom of a given node in Resonite.

Inline

Controls whether this node is displayed inline, or floats to the right. If this parameter is set, the node will draw inline, regardless of the value passed to it.

If you need content to flow below the element, use <div style="clear:right;"></div>, add the class .floatnone to an element, or use other elements with a similar CSS style tag.

Inputs

A string containing a JSON-encoded array of Name/Type information for each input on this node. Example input might be similar to the following, which adds two inputs of type String, one named Input1 and one named Input2

[{"Name":"Input1", "Type":"String"}, {"Name":"Input2", "Type":"String"}]

Outputs

A string containing a JSON-encoded array of Name/Type information for each output on this node. Example input might be similar to the following, which adds two outputs of type bool, one named Output1 and one named Output2

[{"Name":"Output1", "Type":"bool"}, {"Name":"Output2", "Type":"bool"}]

Globals

A string containing a JSON-encoded array of Name/Type information for each global on this node. Example input might be similar to the following, which adds two inputs of type User, one named Global1 and one named Global2

[{"Name":"Global1", "Type":"User"}, {"Name":"Global2", "Type":"User"}]

Custom

Either true or false (not case sensitive, defaults to false), if true the page is not added to ProtoFlux:All. This is mainly used for custom nodes.

Custom=false

Example Usage

If we combine all of the above examples together, we end up with this template invocation:

{{#Invoke:ProtoFlux|GenerateUI
|Name=Example Name
|Category=Example Category
|Inputs=
[
{"Name": "Input1", "Type": "Call"},
{"Name": "Input2", "Type": "String"}
]
|Outputs=
[
{"Name": "Output1", "Type": "bool"},
{"Name": "Output2", "Type": "bool"}
]
|Globals=
[
{"Name": "Global1", "Type": "User"},
{"Name": "Global2", "Type": "User"}
]
|}}

which results in the following:

Example Name

-- Package definition to return to Scribunto - this allows us to define methods that can be called
-- when this module is targeted.
local p = {}

local ProtofluxColor = require("Module:ProtoFlux_Type_Color")

local fieldHandlers = {}

--- Method that generates the HTML output
--	@param frame A Scribunto frame instance. frame.args contains the parameters passed into the module call
function p.GenerateUI( frame )
	-- Parse the JSON input, returning an empty array if no argument was passed
	local fields = mw.text.jsonDecode(frame.args.Fields or '[]')
	
	-- Create an HTML div element to contain our component UI
	local componentContainer = mw.html.create( 'div' )
	componentContainer
	    :attr('class', frame.args.Inline and '' or 'floatright')
		:cssText('color: rgb(224, 224, 224); background-color: rgb(18, 20, 28); width: 512px; display: flex; flex-direction: column; align-items: stretch; gap: 6px; padding: 8px;')
		:tag('div') -- HTML div to contain the component title
			:cssText('color: #f8f770; /* Resonite Yellow */ border-radius: 8px; background-color: #2b2f35; /* Resonite Mid */ text-align:center; font-size: 1.25rem; font-weight: bold; flex-grow: 1;')
			:wikitext(frame.args.Name)
			:done() -- Close node title div
	
	-- Iterate over each row, and populate it with the inputs/outputs. If the node is asymmetric,
	-- the value passed for either input or output might be nil.
	for i=1,#fields do
		CreateField(componentContainer, fields[i])
	end
			
	-- Return the HTML generated above to the wiki page this script is invoked from.
	return tostring(componentContainer) .. '[[Category:Components:All]]'
end

--- Creates a new Component field row in the output
-- @param Container The Scribunto HTML node we'll be creating this row inside of. Should be a container of some sort.
-- @param Field Table containing a Name, FieldType, and Type for the field on this row.
function CreateField(Container, Field)
	local fieldContentContainer = Container
		:tag('div') -- HTML div to contain the field. We specify a min-height of 40px for consistency.
			:cssText('display: flex;')
			:tag('div') -- HTML div to contain the field
				:cssText( 'flex-grow: 2; overflow: hidden; display: flex; flex-direction: row;')
	fieldHandlers[Field.FieldType](fieldContentContainer, Field)
end

function fieldHandlers.Sync(Container, Field)
	Container
		:tag('div') -- HTML div to contain the field label
					:attr('title', Field and (Field.Name .. ' ' .. Field.FieldType .. '<' .. Field.Type .. '>') or '') -- Add a basic mouseover description
					:cssText('text-align: left; width:30%; overflow: hidden; text-overflow: ellipsis; padding-left: 4px;')
					:wikitext( (Field and Field.Name or '') .. ':' )
					:done() -- Close field label div
	if Field.Type == 'Bool' then
		CreateUICheckbox(Container, '')
	elseif Field.Type == 'Int' or Field.Type == 'String' then
		CreateUIInputBox(Container, '0')
	elseif Field.Type == 'Float' or Field.Type == 'Double' then
		Container
			:tag('div')
				:cssText('display:flex; flex-direction: row; align-items:center; flex-grow:1; gap: 4px;')
				:tag('div')
					:cssText('height: 0.5rem; border-radius: 8px; background: rgb(43, 46, 54); flex-grow:5;')
					:tag('div')
						:cssText('height:1em; width: 1em; position:relative; top: -0.25rem; border-radius: 1em; background-color:white;')
						:done()
					:done()
				:tag('div')
					:cssText('height: 1.5rem; border: 2px solid rgb(43, 46, 54); border-radius: 8px; background: rgb(30, 33, 38); flex-grow:1; display:flex; justify-content: center; align-items:center;')
					:wikitext('0')
	elseif Field.Type == "TextHorizontalAlignment" then
		CreateUIForwardBackwards(Container, "Left")
	elseif Field.Type == "TextVerticalAlignment" then
		CreateUIForwardBackwards(Container, "Top")
	elseif Field.Type == "Elements.Assets.AlignmentMode" then
		CreateUIForwardBackwards(Container, "Geometric")
	elseif Field.Type == "Alignment" then
		CreateUIForwardBackwards(Container, "TopLeft")
	elseif Field.Type == "ColorX" then
		local c = Container
			:tag('div')
				:cssText('display:flex; flex-direction: column; align-items:stretch; flex-grow:1; gap: 4px;')
	CreateUIColor(c)

	local c2 = c:tag('div')
					:cssText('display:flex; flex-direction: row; align-items: stretch; flex-grow: 1; gap: 4px;')
					:tag('div')
						:cssText('display:flex; justify-content: center; align-items:center;')
						:wikitext('Profile:')
						:done()
	CreateUIForwardBackwards(c2, "sRGB")
	else
		Container
			:wikitext( Field.FieldType .. '<' .. Field.Type .. '>')
	end
end

function CreateUIColor(Container)
	local c = Container
		:tag('div')
			:cssText('display:flex; height: 1.5rem; flex-direction: row; align-items:stretch; flex-grow:1; gap: 4px;')
	CreateUIBasicText(c, 'R')
	CreateUIInputBox(c, '0')
	CreateUIBasicText(c, 'G')
	CreateUIInputBox(c, '0')
	CreateUIBasicText(c, 'B')
	CreateUIInputBox(c, '0')
	CreateUIBasicText(c, 'A')
	CreateUIInputBox(c, '0')
	c:tag('div')
		:cssText('display:flex; height: 1.5rem; flex-direction: row; align-items:stretch; flex-grow:1;')
			:tag('div')
				:cssText('background-color: black; flex-grow: 1;')
				:done()
			:tag('div')
				:cssText('background-color: white; flex-grow: 1;')
				:done()
		:done()
end
function CreateUIBasicText(Container, Text)
	Container
		:wikitext(Text)
		:done()
	return Container;
end

function CreateUIForwardBackwards(Container, Text)
	local c = Container
		:tag('div')
			:cssText('display:flex; height: 1.5rem; flex-direction: row; align-items:stretch; flex-grow:1; gap: 4px;')
	CreateUISmallButton(c, '<<')
		:tag('div')
			:cssText('border-radius: 8px; background: rgb(8, 8, 10); flex-grow:1; display:flex; justify-content: center; align-items:center;')
			:wikitext(Text)
			:done()
	CreateUISmallButton(c, '>>')
	return Container
end

function CreateUICheckbox(Container, Checked)
	local c = Container
		:tag('div')
			:cssText('background-color: #2b2f35; /* Resonite Mid */ border-radius: 8px; width: 1.5rem; height: 1.5rem; ')
	if Checked then
		c:wikiText('[[File:Checkmark.png|checkmark|24px]]')
	end
end
function CreateUISmallButton(Container, Text)
	Container
		:tag('div')
			:cssText('display:flex; flex-direction: column; align-items:center; justify-content: center;')
			:tag('div')
				:cssText('width: 1.5rem; height: 1.5rem; background-color: rgb(43, 46, 54); border-radius: 8px; flex-grow:0; display:flex; justify-content: center; align-items:center;')
				:wikitext(Text)
				:done()
	return Container
end
function CreateUIButton(Container, Text)
	Container
		:tag('div')
			:cssText('display:flex; flex-direction: column; align-items:stretch; justify-content: center;')
			:tag('div')
				:cssText('height: 1.5rem; background-color: rgb(43, 46, 54); border-radius: 8px; flex-grow:0; display:flex; justify-content: center; align-items:center;')
				:wikitext(Text)
				:done()
	return Container
end

function CreateUIInputBox(Container, Text)
	Container
		:tag('div')
			:cssText('height: 1.5em; border: 2px solid rgb(43, 46, 54); border-radius: 8px; background: rgb(30, 33, 38); flex-grow:1; display:flex; justify-content: center; align-items:center;')
			:wikitext(Text)
			:done()
end

function CreateUIRefInputBox(Container, Text, Italics)
	Container
		:tag('div')
			:cssText('height: 1.5em; background: rgb(43, 46, 54); border-radius: 8px; flex-grow:1; display:flex; justify-content: center; align-items:center;' ..  (Italics and 'font-style:italic;' or ''))
			:wikitext(Text)
			:done()
	return Container
end

function CreateUISyncRefEditor(Container, Text)
	local c = Container
	:tag('div')
		:cssText('display:flex; flex-direction: row; align-items:stretch; flex-grow:1; gap: 4px;')
	
	CreateUISmallButton(c, '⤴')
	CreateUISmallButton(c, '↑')
	CreateUIRefInputBox(c, Text or 'null', true)
	CreateUISmallButton(c, '∅')
end

function fieldHandlers.SyncRef(Container,Field)
	Container
		:tag('div') -- HTML div to contain the field label
					:attr('title', Field and (Field.Name .. ' ' .. Field.FieldType .. '<' .. Field.Type .. '>') or '') -- Add a basic mouseover description
					:cssText('text-align: left; width:30%; overflow: hidden; text-overflow: ellipsis; padding-left: 4px;')
					:wikitext( (Field and Field.Name or '') .. ':' )
					:done() -- Close field label div
	CreateUISyncRefEditor(Container, 'null')
end

function fieldHandlers.SyncAssetList(Container,Field)
	local c = Container
		:tag('div')
			:cssText('display: flex; flex-direction: column; flex-grow: 1; gap: 4px;')
			:tag('div') -- HTML div to contain the field label
				:attr('title', Field and (Field.Name .. ' ' .. Field.FieldType .. '<' .. Field.Type .. '>') or '') -- Add a basic mouseover description
				:cssText('text-align: center;')
				:wikitext( (Field and Field.Name or '') .. '(list):' )
				:done() -- Close field label div
	CreateUISyncRefEditor(c, 'null')
	CreateUIButton(c, 'Add')
end

function fieldHandlers.AssetRef(Container,Field)
	Container
		:tag('div') -- HTML div to contain the field label
					:attr('title', Field and (Field.Name .. ' ' .. Field.FieldType .. '<' .. Field.Type .. '>') or '') -- Add a basic mouseover description
					:cssText('text-align: left; width:30%; overflow: hidden; text-overflow: ellipsis; padding-left: 4px;')
					:wikitext( (Field and Field.Name or '') .. ':' )
					:done() -- Close field label div
	CreateUISyncRefEditor(Container, 'null')
end

--- Returns an RGBA value representing the type color within Resonite
-- @param Connector Table containing a Name (Optional) and Type (Required) for the input/output attachment point.
-- @param Alpha The alpha to use in the RGBA value. 
function GetTypeColor(Connector, Alpha)
	if connector == nil then error("connector is nil") end
	if connector == nil or connector.Type == nil then error("Missing Type") end
	return ProtofluxColor.get_type_color(connector.Type)
end

return p