@lumiastream/ui 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ function styleInject(css, { insertAt } = {}) {
21
21
  }
22
22
 
23
23
  // src/lumia-ui.css
24
- styleInject(".ls-color-picker,\n.ls-color-picker *,\n.mui-multi-select,\n.mui-multi-select *,\n.ls-multi-select-value,\n.ls-multi-select-value *,\n.ls-variable-picker,\n.ls-variable-picker *,\n.ls-variable-token,\n.ls-variable-token *,\n.ls-font-picker,\n.ls-font-picker *,\n.mui-ls-input,\n.mui-ls-input * {\n box-sizing: border-box;\n}\n.ls-color-picker button,\n.mui-multi-select button {\n font: inherit;\n letter-spacing: inherit;\n color: inherit;\n margin: 0;\n}\n.ls-color-picker__preview {\n appearance: none;\n -webkit-appearance: none;\n background-color: transparent;\n background-image: none;\n cursor: pointer;\n text-transform: uppercase;\n user-select: none;\n}\n.mui-ls-button.Mui-disabled {\n color: var(--neutralLight2, var(--white2, #cac9d5));\n opacity: 0.45;\n background-color: var(--separator, #0a0821);\n cursor: not-allowed;\n}\n.mui-ls-button {\n border-radius: var(--radius, 1rem);\n padding: 0.375rem 1.5rem;\n text-transform: uppercase;\n}\n.mui-ls-button.MuiButton-sizeSmall {\n padding: 0.25rem 1rem;\n}\n.mui-ls-button.MuiButton-sizeLarge {\n padding: 0.625rem 2rem;\n}\n.mui-ls-button--extended {\n padding-inline: 3rem;\n}\n.mui-ls-button.MuiButton-containedPrimary,\n.mui-ls-button.Mui-disabled.MuiButton-containedPrimary {\n background: var(--primary, #ff4076);\n color: var(--alwayswhite, #fff) !important;\n}\n.mui-ls-button.MuiButton-containedSecondary {\n background-color: var(--secondary, #535395);\n color: var(--alwayswhite, #fff) !important;\n}\n.mui-ls-button.Mui-disabled.MuiButton-containedSecondary {\n background-color: var(--secondary, #535395);\n color: var(--neutralLight2, var(--white2, #cac9d5));\n opacity: 0.4;\n}\n.mui-ls-button.MuiButton-outlinedPrimary {\n color: var(--primary, #ff4076);\n border-color: var(--primary, #ff4076);\n}\n.mui-ls-button.MuiButton-outlinedPrimary:hover {\n border-color: var(--primary, #ff4076);\n background-color: color-mix(in srgb, var(--primary, #ff4076) 12%, transparent);\n}\n.mui-ls-button.MuiButton-outlinedSecondary {\n color: var(--neutralLight1, var(--white, #fff));\n border-color: var(--secondary, #535395);\n}\n.mui-ls-button.MuiButton-outlinedSecondary.Mui-disabled {\n color: var(--neutralLight2, var(--white2, #cac9d5));\n border-color: var(--neutralDark4, var(--cardborder, #393853));\n background-color: var(--transwhite, rgba(255, 255, 255, 0.05));\n opacity: 0.5;\n}\n.mui-ls-button.MuiButton-outlinedSecondary:hover {\n border-color: var(--secondary, #535395);\n background-color: var(--transwhite, rgba(255, 255, 255, 0.05));\n}\n.mui-ls-button.MuiButton-containedError {\n color: var(--alwayswhite, #fff) !important;\n}\n.mui-ls-button.MuiButton-outlinedError {\n color: var(--error, #fd5454);\n border-color: var(--error, #fd5454);\n}\n.mui-ls-button.MuiButton-outlinedError:hover {\n border-color: var(--error, #fd5454);\n background-color: color-mix(in srgb, var(--error, #fd5454) 12%, transparent);\n}\n.mui-ls-button--neutral.mui-ls-button--contained {\n background-color: var(--neutralDark4, var(--cardborder, #393853));\n color: var(--neutralLight1, var(--white, #fff)) !important;\n}\n.mui-ls-button--neutral.mui-ls-button--contained:hover {\n background-color: color-mix(in srgb, var(--neutralDark4, var(--cardborder, #393853)) 82%, var(--neutralLight1, #fff));\n}\n.mui-ls-button--neutral.mui-ls-button--outlined {\n color: var(--neutralLight1, var(--white, #fff));\n border-color: var(--neutralDark4, var(--cardborder, #393853));\n background-color: transparent;\n}\n.mui-ls-button--neutral.mui-ls-button--outlined:hover {\n border-color: var(--neutralLight3, var(--grey, #9392a1));\n background-color: var(--transwhite, rgba(255, 255, 255, 0.05));\n}\n.mui-ls-button--neutral.mui-ls-button--text {\n color: var(--neutralLight1, var(--white, #fff));\n}\n.mui-ls-button--neutral.mui-ls-button--text:hover {\n background-color: var(--transwhite, rgba(255, 255, 255, 0.05));\n}\n.mui-ls-button svg {\n max-height: 24px;\n max-width: 24px;\n}\n.mui-ls-input,\n.ls-font-picker,\n.ls-color-picker__input {\n width: 100%;\n}\n.mui-ls-input,\n.mui-ls-select-form-control,\n.mui-multi-select,\n.ls-font-picker,\n.mui-ls-slider-input-root {\n --ls-control-height: 55px;\n}\n.mui-ls-input .MuiOutlinedInput-root,\n.mui-ls-input .MuiInputBase-root,\n.mui-ls-input .MuiOutlinedInput-notchedOutline,\n.mui-ls-select.MuiOutlinedInput-root,\n.mui-ls-select .MuiOutlinedInput-notchedOutline,\n.mui-multi-select .mui-ls-select.MuiOutlinedInput-root,\n.mui-multi-select .mui-ls-select .MuiOutlinedInput-notchedOutline,\n.ls-font-picker .MuiOutlinedInput-root,\n.ls-font-picker .MuiInputBase-root,\n.ls-font-picker .MuiOutlinedInput-notchedOutline,\n.ls-color-picker__input .MuiOutlinedInput-root,\n.ls-color-picker__input .MuiInputBase-root,\n.ls-color-picker__input .MuiOutlinedInput-notchedOutline {\n border-radius: var(--radius) !important;\n}\n.mui-ls-input .MuiInputBase-root,\n.mui-ls-select.MuiInputBase-root,\n.mui-multi-select .mui-ls-select.MuiInputBase-root,\n.ls-font-picker .MuiInputBase-root {\n min-height: var(--ls-control-height);\n}\n.mui-ls-input.small .MuiInputBase-root {\n min-height: 25px;\n}\n.mui-ls-input.medium .MuiInputBase-root {\n min-height: 35px;\n}\n.mui-ls-input.noMinHeight .MuiInputBase-root {\n min-height: unset;\n}\n.mui-ls-date-picker input[type=datetime-local]::-webkit-calendar-picker-indicator {\n cursor: pointer;\n opacity: 0.9;\n filter: invert(1);\n}\n.mui-ls-select .MuiSelect-select,\n.mui-multi-select .MuiSelect-select {\n display: flex;\n min-height: calc(var(--ls-control-height) - 2px);\n align-items: center;\n box-sizing: border-box;\n border-radius: var(--radius);\n}\n.mui-multi-select .MuiSelect-select {\n min-width: 0;\n}\n.MuiSvgIcon-root svg {\n color: var(--neutralLight1, var(--white, #fff)) !important;\n}\n.mui-ls-input .MuiSvgIcon-root svg path {\n fill: currentcolor;\n stroke: currentcolor;\n}\n.mui-ls-input input::placeholder,\n.mui-ls-input textarea::placeholder {\n color: var(--neutralLight3, var(--grey, #9392a1));\n opacity: 1;\n}\n.mui-ls-input .Mui-focused .MuiOutlinedInput-notchedOutline,\n.mui-ls-input .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline {\n border-color: var(--primary, #ff4076) !important;\n}\n.ls-variable-autocomplete-popper .MuiAutocomplete-paper,\n.ls-variable-autocomplete-paper {\n border: 1px solid var(--neutralDark4, var(--cardborder, #393853)) !important;\n border-radius: var(--radius, 1rem) !important;\n background: var(--neutralDark2, var(--cardbackground, #211f45)) !important;\n color: var(--neutralLight1, var(--white, #fff)) !important;\n box-shadow: 0 16px 32px rgba(0, 0, 0, 0.32) !important;\n overflow: hidden !important;\n}\n.ls-variable-autocomplete-popper .MuiAutocomplete-listbox,\n.ls-variable-autocomplete-listbox {\n padding: 0.25rem 0 !important;\n background: var(--neutralDark2, var(--cardbackground, #211f45)) !important;\n color: var(--neutralLight1, var(--white, #fff)) !important;\n}\n.ls-variable-autocomplete-popper .MuiAutocomplete-option,\n.ls-variable-autocomplete-listbox .MuiAutocomplete-option {\n display: flex !important;\n min-height: 44px !important;\n align-items: center !important;\n padding: 0.625rem 1rem !important;\n color: var(--neutralLight1, var(--white, #fff)) !important;\n font-size: 1rem !important;\n line-height: 1.35 !important;\n cursor: pointer !important;\n transition: background-color 120ms ease, color 120ms ease !important;\n}\n.ls-variable-autocomplete-popper .MuiAutocomplete-option > *,\n.ls-variable-autocomplete-listbox .MuiAutocomplete-option > * {\n min-width: 0;\n}\n.ls-variable-autocomplete-popper .MuiAutocomplete-option:hover,\n.ls-variable-autocomplete-popper .MuiAutocomplete-option.Mui-focused,\n.ls-variable-autocomplete-popper .MuiAutocomplete-option[data-focus=true],\n.ls-variable-autocomplete-popper .MuiAutocomplete-option[aria-selected=true],\n.ls-variable-autocomplete-listbox .MuiAutocomplete-option:hover,\n.ls-variable-autocomplete-listbox .MuiAutocomplete-option.Mui-focused,\n.ls-variable-autocomplete-listbox .MuiAutocomplete-option[data-focus=true],\n.ls-variable-autocomplete-listbox .MuiAutocomplete-option[aria-selected=true] {\n background: var( --cardHover, var(--transwhite, rgba(255, 255, 255, 0.05)) ) !important;\n color: var(--neutralLight1, var(--white, #fff)) !important;\n}\n.ls-variable-option {\n gap: 0.75rem;\n}\n.ls-variable-option__thumb {\n width: 40px;\n height: 40px;\n flex: 0 0 40px;\n border-radius: var(--radius, 1rem);\n object-fit: cover;\n}\n.ls-variable-option__thumb--placeholder {\n background: var(--neutralDark4, var(--cardborder, #393853));\n}\n.ls-variable-option__body {\n min-width: 0;\n}\n.ls-variable-option__label {\n color: var(--neutralLight1, var(--white, #fff));\n font-weight: 500;\n line-height: 1.25;\n}\n.ls-variable-option__helper {\n margin-top: 0.125rem;\n color: var(--neutralLight3, var(--grey, #9392a1));\n font-size: 0.875rem;\n line-height: 1.25;\n}\n.ls-variable-autocomplete-popup-indicator,\n.ls-variable-autocomplete-popup-indicator.MuiIconButton-root,\n.ls-variable-autocomplete-popup-indicator svg {\n color: var(--neutralLight1, var(--white, #fff)) !important;\n}\n.ls-variable-autocomplete-popup-indicator svg path {\n fill: currentcolor !important;\n stroke: currentcolor !important;\n}\n.ls-variable-input-adornment,\n.ls-variable-input-adornment.MuiInputAdornment-root {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: 24px;\n width: 24px;\n min-width: 24px;\n margin-left: 0.35rem;\n border-radius: var(--radius, 1rem);\n background: var(--primary, #ff4076) !important;\n color: var(--alwayswhite, var(--white, #fff)) !important;\n font-weight: 800;\n line-height: 1;\n letter-spacing: 0;\n cursor: pointer;\n user-select: none;\n}\n.ls-variable-input-adornment svg,\n.ls-variable-input-adornment path {\n color: currentcolor !important;\n fill: currentcolor !important;\n stroke: currentcolor !important;\n}\n.ls-variable-input-adornment:hover,\n.ls-variable-input-adornment.MuiInputAdornment-root:hover {\n filter: brightness(1.08);\n}\n.ls-variable-input-adornment:active,\n.ls-variable-input-adornment.MuiInputAdornment-root:active {\n filter: brightness(0.96);\n}\n.mui-ls-slider-input-root {\n width: 100%;\n}\n.mui-ls-slider-input-fieldset {\n position: relative;\n min-width: 0;\n width: 100%;\n min-height: var(--ls-control-height);\n margin: 0;\n border: 2px solid var(--neutralDark4, var(--cardborder, #393853));\n border-radius: var(--radius, 1rem);\n padding: 0 0.875rem 0.5rem 0.875rem;\n background: transparent;\n color: var(--neutralLight1, var(--white, #fff));\n}\n.mui-ls-slider-input-fieldset:hover {\n border-color: var(--primary, #ff4076);\n}\n.mui-ls-slider-input-legend {\n display: block;\n width: auto;\n max-width: calc(100% - 1rem);\n margin: 0;\n padding: 0 0.25rem;\n color: var(--neutralLight1, var(--white, #fff));\n font-size: 0.875rem;\n line-height: 1.1;\n white-space: nowrap;\n}\n.mui-ls-slider-input-row {\n display: flex;\n min-width: 0;\n min-height: calc(var(--ls-control-height) - 14px);\n align-items: center;\n gap: 1rem;\n}\n.mui-ls-slider-input-row--slider {\n flex-wrap: nowrap;\n}\n.mui-ls-slider-input-row--input {\n justify-content: stretch;\n}\n.mui-ls-slider-input-value.mui-ls-input {\n margin: 0;\n}\n.mui-ls-slider-input-value.mui-ls-input .MuiFormControl-root,\n.mui-ls-slider-input-value.mui-ls-input .MuiTextField-root {\n margin: 0;\n}\n.mui-ls-slider-input-value.mui-ls-input .MuiInputBase-root {\n min-height: 36px;\n}\n.mui-ls-slider-input-value.mui-ls-input .MuiOutlinedInput-notchedOutline {\n border-color: var(--neutralDark4, var(--cardborder, #393853));\n}\n.mui-ls-slider-input-helper {\n display: block;\n margin-top: 0.25rem;\n color: var(--neutralLight3, var(--grey, #9392a1));\n font-size: 0.75rem;\n line-height: 1.25;\n}\n.ls-multi-select-value {\n display: flex;\n width: 100%;\n min-width: 0;\n flex-wrap: nowrap;\n align-items: center;\n justify-content: space-between;\n gap: 0.5rem;\n overflow: hidden;\n white-space: nowrap;\n}\n.ls-multi-select-value__label {\n min-width: 0;\n flex: 1 1 0%;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.ls-multi-select-value__count {\n flex-shrink: 0;\n white-space: nowrap;\n}\n.ls-color-picker__preview {\n position: relative;\n display: inline-block;\n width: 1.5rem;\n height: 1.5rem;\n flex-shrink: 0;\n padding: 0;\n border: 1px solid color-mix(in srgb, #fff 25%, transparent);\n border-radius: var(--radius, 1rem);\n background-size: cover;\n background-position: center;\n background-repeat: no-repeat;\n box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);\n color: transparent;\n line-height: 0;\n}\n.ls-color-picker__preview:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n.ls-color-picker__preview:focus {\n outline: none;\n}\n.ls-color-picker__preview:focus-visible {\n box-shadow:\n 0 0 0 2px var(--primary, #ff4076),\n 0 0 0 4px var(--neutralDark1, #1b1a28),\n 0 0 0 1px rgba(0, 0, 0, 0.25);\n}\n.ls-color-picker__gradient circle,\n.ls-color-picker__gradient polyline {\n stroke: #fff;\n}\n.ls-variable-picker {\n --variable-custom-color: var(--customVariables, #69ffd2);\n --variable-function-color: var( --functionVariables, var(--semanticPurple, #b489ff) );\n --variable-normal-color: var(--normalVariables, var(--semanticBlue, #65b8ff));\n display: flex;\n width: 92vw;\n max-width: 1100px;\n min-height: 50vh;\n max-height: 55vh;\n flex-direction: column;\n overflow: hidden;\n padding: 1rem;\n}\n.ls-variable-picker__field {\n margin: 0 0 0.75rem;\n border: 1px solid color-mix(in srgb, var(--primary, #ff4076) 42%, var(--neutralDark4, #393853));\n border-radius: var(--radius, 1rem);\n padding: 0.5rem;\n}\n.ls-variable-picker__legend {\n padding-inline: 0.375rem;\n color: var(--primary, #ff4076);\n font-size: 0.75rem;\n font-weight: 700;\n}\n.ls-variable-picker__intro {\n margin-bottom: 1rem;\n color: var(--neutralLight2, #cac9d5);\n}\n.ls-variable-picker__header,\n.ls-variable-picker__row {\n display: grid;\n grid-template-columns: minmax(300px, 1.5fr) 1fr 1fr;\n gap: 2rem;\n}\n.ls-variable-picker__header {\n margin-bottom: 0.25rem;\n padding-inline: 0.25rem;\n padding-right: 1.7rem;\n color: var(--neutralLight3, #9392a1);\n}\n.ls-variable-picker__list {\n height: 100%;\n overflow: auto;\n padding-right: 1rem;\n}\n.ls-variable-picker__row {\n cursor: pointer;\n align-items: start;\n border-bottom: 1px solid color-mix(in srgb, var(--neutralDark4, #393853) 70%, transparent);\n padding: 1rem 0.25rem;\n}\n.ls-variable-picker__row--no-border {\n border-bottom: 0;\n}\n.ls-variable-picker__token-cell {\n display: flex;\n min-width: 0;\n align-items: center;\n gap: 0.375rem;\n}\n.ls-variable-picker__expand-toggle,\n.ls-variable-picker__expand-spacer {\n width: 24px;\n height: 24px;\n flex: 0 0 24px;\n}\n.ls-variable-picker__expand-toggle {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: 1px solid color-mix(in srgb, var(--neutralDark4, #393853) 75%, transparent);\n border-radius: var(--radius, 1rem);\n padding: 0;\n background: var(--neutralDark3, var(--containerbackground, #1f1f3a));\n color: var(--neutralLight2, #cac9d5);\n transition:\n background-color 140ms ease,\n border-color 140ms ease,\n color 140ms ease,\n transform 140ms ease;\n}\n.ls-variable-picker__expand-toggle:hover {\n border-color: var(--primary, #ff4076);\n background: color-mix(in srgb, var(--primary, #ff4076) 16%, var(--neutralDark3, #1f1f3a));\n color: var(--neutralLight1, #fff);\n}\n.ls-variable-picker__expand-toggle svg {\n transform: rotate(-90deg);\n transition: transform 140ms ease;\n}\n.ls-variable-picker__expand-toggle--expanded svg {\n transform: rotate(0deg);\n}\n.ls-variable-picker__row:active {\n opacity: 0.8;\n}\n.ls-variable-picker__description {\n max-width: 600px;\n overflow-wrap: break-word;\n line-height: 1.25;\n word-break: break-all;\n}\n.ls-variable-picker__value {\n min-width: 0;\n}\n.ls-variable-picker__value-input {\n width: 100%;\n min-width: 0;\n cursor: text;\n overflow: hidden;\n border: 0;\n background: transparent;\n color: var(--neutralLight1, var(--white, #fff));\n caret-color: transparent;\n font: inherit;\n font-size: 1rem;\n line-height: 1.25;\n outline: 0;\n padding: 0;\n text-overflow: ellipsis;\n}\n.ls-variable-picker__value-input:focus {\n outline: 0;\n}\n.ls-variable-picker__value-input::selection {\n background: color-mix(in srgb, var(--primary, #ff4076) 45%, transparent);\n color: var(--alwayswhite, var(--white, #fff));\n}\n.ls-variable-token {\n display: flex;\n min-width: 0;\n align-items: center;\n}\n.ls-variable-token--suggested {\n color: var(--suggestedVariables, #ffb469);\n}\n.ls-variable-token--custom {\n color: var(--customVariables, #69ffd2);\n}\n.ls-variable-token--function {\n color: var(--functionVariables, var(--semanticPurple, #b489ff));\n}\n.ls-variable-token--system {\n color: var(--normalVariables, var(--semanticBlue, #65b8ff));\n}\n.ls-variable-token__name {\n min-width: 0;\n word-break: break-all;\n}\n.ls-variable-token__badge {\n display: inline-flex;\n margin-left: 0.5rem;\n flex-shrink: 0;\n align-items: center;\n border: 1px solid currentcolor;\n border-radius: var(--radius, 1rem);\n padding-inline: 0.25rem;\n padding-block: 0;\n font-size: 10px;\n font-weight: 600;\n line-height: 1;\n text-transform: uppercase;\n}\n");
24
+ styleInject(".ls-color-picker,\n.ls-color-picker *,\n.mui-multi-select,\n.mui-multi-select *,\n.ls-multi-select-value,\n.ls-multi-select-value *,\n.ls-variable-picker,\n.ls-variable-picker *,\n.ls-variable-token,\n.ls-variable-token *,\n.ls-font-picker,\n.ls-font-picker *,\n.mui-ls-input,\n.mui-ls-input * {\n box-sizing: border-box;\n}\n.ls-color-picker button,\n.mui-multi-select button {\n font: inherit;\n letter-spacing: inherit;\n color: inherit;\n margin: 0;\n}\n.ls-color-picker__preview {\n appearance: none;\n -webkit-appearance: none;\n background-color: transparent;\n background-image: none;\n cursor: pointer;\n text-transform: uppercase;\n user-select: none;\n}\n.mui-ls-button.Mui-disabled {\n color: var(--neutralLight2, var(--white2, #cac9d5));\n opacity: 0.45;\n background-color: var(--separator, #0a0821);\n cursor: not-allowed;\n}\n.mui-ls-button {\n border-radius: var(--radius, 1rem);\n padding: 0.375rem 1.5rem;\n text-transform: uppercase;\n}\n.mui-ls-button.MuiButton-sizeSmall {\n padding: 0.25rem 1rem;\n}\n.mui-ls-button.MuiButton-sizeLarge {\n padding: 0.625rem 2rem;\n}\n.mui-ls-button--extended {\n padding-inline: 3rem;\n}\n.mui-ls-button.MuiButton-containedPrimary,\n.mui-ls-button.Mui-disabled.MuiButton-containedPrimary {\n background: var(--primary, #ff4076);\n color: var(--alwayswhite, #fff) !important;\n}\n.mui-ls-button.MuiButton-containedSecondary {\n background-color: var(--secondary, #535395);\n color: var(--alwayswhite, #fff) !important;\n}\n.mui-ls-button.Mui-disabled.MuiButton-containedSecondary {\n background-color: var(--secondary, #535395);\n color: var(--neutralLight2, var(--white2, #cac9d5));\n opacity: 0.4;\n}\n.mui-ls-button.MuiButton-outlinedPrimary {\n color: var(--primary, #ff4076);\n border-color: var(--primary, #ff4076);\n}\n.mui-ls-button.MuiButton-outlinedPrimary:hover {\n border-color: var(--primary, #ff4076);\n background-color: color-mix(in srgb, var(--primary, #ff4076) 12%, transparent);\n}\n.mui-ls-button.MuiButton-outlinedSecondary {\n color: var(--neutralLight1, var(--white, #fff));\n border-color: var(--secondary, #535395);\n}\n.mui-ls-button.MuiButton-outlinedSecondary.Mui-disabled {\n color: var(--neutralLight2, var(--white2, #cac9d5));\n border-color: var(--neutralDark4, var(--cardborder, #393853));\n background-color: var(--transwhite, rgba(255, 255, 255, 0.05));\n opacity: 0.5;\n}\n.mui-ls-button.MuiButton-outlinedSecondary:hover {\n border-color: var(--secondary, #535395);\n background-color: var(--transwhite, rgba(255, 255, 255, 0.05));\n}\n.mui-ls-button.MuiButton-containedError {\n color: var(--alwayswhite, #fff) !important;\n}\n.mui-ls-button.MuiButton-outlinedError {\n color: var(--error, #fd5454);\n border-color: var(--error, #fd5454);\n}\n.mui-ls-button.MuiButton-outlinedError:hover {\n border-color: var(--error, #fd5454);\n background-color: color-mix(in srgb, var(--error, #fd5454) 12%, transparent);\n}\n.mui-ls-button--neutral.mui-ls-button--contained {\n background-color: var(--neutralDark4, var(--cardborder, #393853));\n color: var(--neutralLight1, var(--white, #fff)) !important;\n}\n.mui-ls-button--neutral.mui-ls-button--contained:hover {\n background-color: color-mix(in srgb, var(--neutralDark4, var(--cardborder, #393853)) 82%, var(--neutralLight1, #fff));\n}\n.mui-ls-button--neutral.mui-ls-button--outlined {\n color: var(--neutralLight1, var(--white, #fff));\n border-color: var(--neutralDark4, var(--cardborder, #393853));\n background-color: transparent;\n}\n.mui-ls-button--neutral.mui-ls-button--outlined:hover {\n border-color: var(--neutralLight3, var(--grey, #9392a1));\n background-color: var(--transwhite, rgba(255, 255, 255, 0.05));\n}\n.mui-ls-button--neutral.mui-ls-button--text {\n color: var(--neutralLight1, var(--white, #fff));\n}\n.mui-ls-button--neutral.mui-ls-button--text:hover {\n background-color: var(--transwhite, rgba(255, 255, 255, 0.05));\n}\n.mui-ls-button svg {\n max-height: 24px;\n max-width: 24px;\n}\n.mui-ls-input,\n.ls-font-picker,\n.ls-color-picker__input {\n width: 100%;\n}\n.mui-ls-input,\n.mui-ls-select-form-control,\n.mui-multi-select,\n.ls-font-picker,\n.mui-ls-slider-input-root {\n --ls-control-height: 55px;\n}\n.mui-ls-input .MuiOutlinedInput-root,\n.mui-ls-input .MuiInputBase-root,\n.mui-ls-input .MuiOutlinedInput-notchedOutline,\n.mui-ls-select.MuiOutlinedInput-root,\n.mui-ls-select .MuiOutlinedInput-notchedOutline,\n.mui-multi-select .mui-ls-select.MuiOutlinedInput-root,\n.mui-multi-select .mui-ls-select .MuiOutlinedInput-notchedOutline,\n.ls-font-picker .MuiOutlinedInput-root,\n.ls-font-picker .MuiInputBase-root,\n.ls-font-picker .MuiOutlinedInput-notchedOutline,\n.ls-color-picker__input .MuiOutlinedInput-root,\n.ls-color-picker__input .MuiInputBase-root,\n.ls-color-picker__input .MuiOutlinedInput-notchedOutline {\n border-radius: var(--radius) !important;\n}\n.mui-ls-input .MuiInputBase-root,\n.mui-ls-select.MuiInputBase-root,\n.mui-multi-select .mui-ls-select.MuiInputBase-root,\n.ls-font-picker .MuiInputBase-root {\n min-height: var(--ls-control-height);\n}\n.mui-ls-input.small .MuiInputBase-root {\n min-height: 25px;\n}\n.mui-ls-input.medium .MuiInputBase-root {\n min-height: 35px;\n}\n.mui-ls-input.noMinHeight .MuiInputBase-root {\n min-height: unset;\n}\n.mui-ls-date-picker input[type=datetime-local]::-webkit-calendar-picker-indicator {\n cursor: pointer;\n opacity: 0.9;\n filter: invert(1);\n}\n.mui-ls-select .MuiSelect-select,\n.mui-multi-select .MuiSelect-select {\n display: flex;\n min-height: calc(var(--ls-control-height) - 2px);\n align-items: center;\n box-sizing: border-box;\n border-radius: var(--radius);\n}\n.mui-multi-select .MuiSelect-select {\n min-width: 0;\n}\n.MuiSvgIcon-root svg {\n color: var(--neutralLight1, var(--white, #fff)) !important;\n}\n.mui-ls-input .MuiSvgIcon-root svg path {\n fill: currentcolor;\n stroke: currentcolor;\n}\n.mui-ls-input input::placeholder,\n.mui-ls-input textarea::placeholder {\n color: var(--neutralLight3, var(--grey, #9392a1));\n opacity: 0.55;\n font-style: italic;\n}\n.mui-ls-input .MuiInputBase-input,\n.mui-ls-input .MuiInputAdornment-root,\n.mui-ls-input .MuiSvgIcon-root,\n.mui-ls-input .MuiFormHelperText-root {\n color: var(--neutralLight1, var(--white, #fff));\n opacity: 1;\n}\n.mui-ls-input .Mui-focused .MuiOutlinedInput-notchedOutline,\n.mui-ls-input .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline {\n border-color: var(--primary, #ff4076) !important;\n}\n.ls-variable-autocomplete-popper .MuiAutocomplete-paper,\n.ls-variable-autocomplete-paper {\n border: 1px solid var(--neutralDark4, var(--cardborder, #393853)) !important;\n border-radius: var(--radius, 1rem) !important;\n background: var(--neutralDark2, var(--cardbackground, #211f45)) !important;\n color: var(--neutralLight1, var(--white, #fff)) !important;\n box-shadow: 0 16px 32px rgba(0, 0, 0, 0.32) !important;\n overflow: hidden !important;\n}\n.ls-variable-autocomplete-popper .MuiAutocomplete-listbox,\n.ls-variable-autocomplete-listbox {\n padding: 0.25rem 0 !important;\n background: var(--neutralDark2, var(--cardbackground, #211f45)) !important;\n color: var(--neutralLight1, var(--white, #fff)) !important;\n}\n.ls-variable-autocomplete-popper .MuiAutocomplete-option,\n.ls-variable-autocomplete-listbox .MuiAutocomplete-option {\n display: flex !important;\n min-height: 44px !important;\n align-items: center !important;\n padding: 0.625rem 1rem !important;\n color: var(--neutralLight1, var(--white, #fff)) !important;\n font-size: 1rem !important;\n line-height: 1.35 !important;\n cursor: pointer !important;\n transition: background-color 120ms ease, color 120ms ease !important;\n}\n.ls-variable-autocomplete-popper .MuiAutocomplete-option > *,\n.ls-variable-autocomplete-listbox .MuiAutocomplete-option > * {\n min-width: 0;\n}\n.ls-variable-autocomplete-popper .MuiAutocomplete-option:hover,\n.ls-variable-autocomplete-popper .MuiAutocomplete-option.Mui-focused,\n.ls-variable-autocomplete-popper .MuiAutocomplete-option[data-focus=true],\n.ls-variable-autocomplete-popper .MuiAutocomplete-option[aria-selected=true],\n.ls-variable-autocomplete-listbox .MuiAutocomplete-option:hover,\n.ls-variable-autocomplete-listbox .MuiAutocomplete-option.Mui-focused,\n.ls-variable-autocomplete-listbox .MuiAutocomplete-option[data-focus=true],\n.ls-variable-autocomplete-listbox .MuiAutocomplete-option[aria-selected=true] {\n background: var( --cardHover, var(--transwhite, rgba(255, 255, 255, 0.05)) ) !important;\n color: var(--neutralLight1, var(--white, #fff)) !important;\n}\n.ls-variable-option {\n gap: 0.75rem;\n}\n.ls-variable-option__thumb {\n width: 40px;\n height: 40px;\n flex: 0 0 40px;\n border-radius: var(--radius, 1rem);\n object-fit: cover;\n}\n.ls-variable-option__thumb--placeholder {\n background: var(--neutralDark4, var(--cardborder, #393853));\n}\n.ls-variable-option__body {\n min-width: 0;\n}\n.ls-variable-option__label {\n color: var(--neutralLight1, var(--white, #fff));\n font-weight: 500;\n line-height: 1.25;\n}\n.ls-variable-option__helper {\n margin-top: 0.125rem;\n color: var(--neutralLight3, var(--grey, #9392a1));\n font-size: 0.875rem;\n line-height: 1.25;\n}\n.ls-variable-autocomplete-popup-indicator,\n.ls-variable-autocomplete-popup-indicator.MuiIconButton-root,\n.ls-variable-autocomplete-popup-indicator svg {\n color: var(--neutralLight1, var(--white, #fff)) !important;\n}\n.ls-variable-autocomplete-popup-indicator svg path {\n fill: currentcolor !important;\n stroke: currentcolor !important;\n}\n.ls-variable-input-adornment,\n.ls-variable-input-adornment.MuiInputAdornment-root {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: 24px;\n width: 24px;\n min-width: 24px;\n margin-left: 0.35rem;\n border-radius: var(--radius, 1rem);\n background: var(--primary, #ff4076) !important;\n color: var(--alwayswhite, var(--white, #fff)) !important;\n font-weight: 800;\n line-height: 1;\n letter-spacing: 0;\n cursor: pointer;\n user-select: none;\n}\n.ls-variable-input-adornment svg,\n.ls-variable-input-adornment path {\n color: currentcolor !important;\n fill: currentcolor !important;\n stroke: currentcolor !important;\n}\n.ls-variable-input-adornment:hover,\n.ls-variable-input-adornment.MuiInputAdornment-root:hover {\n filter: brightness(1.08);\n}\n.ls-variable-input-adornment:active,\n.ls-variable-input-adornment.MuiInputAdornment-root:active {\n filter: brightness(0.96);\n}\n.mui-ls-slider-input-root {\n width: 100%;\n}\n.mui-ls-slider-input-fieldset {\n position: relative;\n min-width: 0;\n width: 100%;\n min-height: var(--ls-control-height);\n margin: 0;\n border: 2px solid var(--neutralDark4, var(--cardborder, #393853));\n border-radius: var(--radius, 1rem);\n padding: 0 0.875rem 0.5rem 0.875rem;\n background: transparent;\n color: var(--neutralLight1, var(--white, #fff));\n}\n.mui-ls-slider-input-fieldset:hover {\n border-color: var(--primary, #ff4076);\n}\n.mui-ls-slider-input-legend {\n display: block;\n width: auto;\n max-width: calc(100% - 1rem);\n margin: 0;\n padding: 0 0.25rem;\n color: var(--neutralLight1, var(--white, #fff));\n font-size: 0.875rem;\n line-height: 1.1;\n white-space: nowrap;\n}\n.mui-ls-slider-input-row {\n display: flex;\n min-width: 0;\n min-height: calc(var(--ls-control-height) - 14px);\n align-items: center;\n gap: 1rem;\n}\n.mui-ls-slider-input-row--slider {\n flex-wrap: nowrap;\n}\n.mui-ls-slider-input-row--input {\n justify-content: stretch;\n}\n.mui-ls-slider-input-value.mui-ls-input {\n margin: 0;\n}\n.mui-ls-slider-input-value.mui-ls-input .MuiFormControl-root,\n.mui-ls-slider-input-value.mui-ls-input .MuiTextField-root {\n margin: 0;\n}\n.mui-ls-slider-input-value.mui-ls-input .MuiInputBase-root {\n min-height: 36px;\n}\n.mui-ls-slider-input-value.mui-ls-input .MuiOutlinedInput-notchedOutline {\n border-color: var(--neutralDark4, var(--cardborder, #393853));\n}\n.mui-ls-slider-input-helper {\n display: block;\n margin-top: 0.25rem;\n color: var(--neutralLight3, var(--grey, #9392a1));\n font-size: 0.75rem;\n line-height: 1.25;\n}\n.ls-multi-select-value {\n display: flex;\n width: 100%;\n min-width: 0;\n flex-wrap: nowrap;\n align-items: center;\n justify-content: space-between;\n gap: 0.5rem;\n overflow: hidden;\n white-space: nowrap;\n}\n.ls-multi-select-value__label {\n min-width: 0;\n flex: 1 1 0%;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.ls-multi-select-value__count {\n flex-shrink: 0;\n white-space: nowrap;\n}\n.ls-color-picker__preview {\n position: relative;\n display: inline-block;\n width: 1.5rem;\n height: 1.5rem;\n flex-shrink: 0;\n padding: 0;\n border: 1px solid color-mix(in srgb, #fff 25%, transparent);\n border-radius: var(--radius, 1rem);\n background-size: cover;\n background-position: center;\n background-repeat: no-repeat;\n box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);\n color: transparent;\n line-height: 0;\n}\n.ls-color-picker__preview:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n.ls-color-picker__preview:focus {\n outline: none;\n}\n.ls-color-picker__preview:focus-visible {\n box-shadow:\n 0 0 0 2px var(--primary, #ff4076),\n 0 0 0 4px var(--neutralDark1, #1b1a28),\n 0 0 0 1px rgba(0, 0, 0, 0.25);\n}\n.ls-color-picker__gradient circle,\n.ls-color-picker__gradient polyline {\n stroke: #fff;\n}\n.ls-variable-picker {\n --variable-custom-color: var(--customVariables, #69ffd2);\n --variable-function-color: var( --functionVariables, var(--semanticPurple, #b489ff) );\n --variable-normal-color: var(--normalVariables, var(--semanticBlue, #65b8ff));\n display: flex;\n width: 92vw;\n max-width: 1100px;\n min-height: 50vh;\n max-height: 55vh;\n flex-direction: column;\n overflow: hidden;\n padding: 1rem;\n}\n.ls-variable-picker__field {\n margin: 0 0 0.75rem;\n border: 1px solid color-mix(in srgb, var(--primary, #ff4076) 42%, var(--neutralDark4, #393853));\n border-radius: var(--radius, 1rem);\n padding: 0.5rem;\n}\n.ls-variable-picker__legend {\n padding-inline: 0.375rem;\n color: var(--primary, #ff4076);\n font-size: 0.75rem;\n font-weight: 700;\n}\n.ls-variable-picker__intro {\n margin-bottom: 1rem;\n color: var(--neutralLight2, #cac9d5);\n}\n.ls-variable-picker__header,\n.ls-variable-picker__row {\n display: grid;\n grid-template-columns: minmax(300px, 1.5fr) 1fr 1fr;\n gap: 2rem;\n}\n.ls-variable-picker__header {\n margin-bottom: 0.25rem;\n padding-inline: 0.25rem;\n padding-right: 1.7rem;\n color: var(--neutralLight3, #9392a1);\n}\n.ls-variable-picker__list {\n height: 100%;\n overflow: auto;\n padding-right: 1rem;\n}\n.ls-variable-picker__row {\n cursor: pointer;\n align-items: start;\n border-bottom: 1px solid color-mix(in srgb, var(--neutralDark4, #393853) 70%, transparent);\n padding: 1rem 0.25rem;\n}\n.ls-variable-picker__row--no-border {\n border-bottom: 0;\n}\n.ls-variable-picker__token-cell {\n display: flex;\n min-width: 0;\n align-items: center;\n gap: 0.375rem;\n}\n.ls-variable-picker__expand-toggle,\n.ls-variable-picker__expand-spacer {\n width: 24px;\n height: 24px;\n flex: 0 0 24px;\n}\n.ls-variable-picker__expand-toggle {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: 1px solid color-mix(in srgb, var(--neutralDark4, #393853) 75%, transparent);\n border-radius: var(--radius, 1rem);\n padding: 0;\n background: var(--neutralDark3, var(--containerbackground, #1f1f3a));\n color: var(--neutralLight2, #cac9d5);\n transition:\n background-color 140ms ease,\n border-color 140ms ease,\n color 140ms ease,\n transform 140ms ease;\n}\n.ls-variable-picker__expand-toggle:hover {\n border-color: var(--primary, #ff4076);\n background: color-mix(in srgb, var(--primary, #ff4076) 16%, var(--neutralDark3, #1f1f3a));\n color: var(--neutralLight1, #fff);\n}\n.ls-variable-picker__expand-toggle svg {\n transform: rotate(-90deg);\n transition: transform 140ms ease;\n}\n.ls-variable-picker__expand-toggle--expanded svg {\n transform: rotate(0deg);\n}\n.ls-variable-picker__row:active {\n opacity: 0.8;\n}\n.ls-variable-picker__description {\n max-width: 600px;\n overflow-wrap: break-word;\n line-height: 1.25;\n word-break: break-all;\n}\n.ls-variable-picker__value {\n min-width: 0;\n}\n.ls-variable-picker__value-input {\n width: 100%;\n min-width: 0;\n cursor: text;\n overflow: hidden;\n border: 0;\n background: transparent;\n color: var(--neutralLight1, var(--white, #fff));\n caret-color: transparent;\n font: inherit;\n font-size: 1rem;\n line-height: 1.25;\n outline: 0;\n padding: 0;\n text-overflow: ellipsis;\n}\n.ls-variable-picker__value-input:focus {\n outline: 0;\n}\n.ls-variable-picker__value-input::selection {\n background: color-mix(in srgb, var(--primary, #ff4076) 45%, transparent);\n color: var(--alwayswhite, var(--white, #fff));\n}\n.ls-variable-token {\n display: flex;\n min-width: 0;\n align-items: center;\n}\n.ls-variable-token--suggested {\n color: var(--suggestedVariables, #ffb469);\n}\n.ls-variable-token--custom {\n color: var(--customVariables, #69ffd2);\n}\n.ls-variable-token--function {\n color: var(--functionVariables, var(--semanticPurple, #b489ff));\n}\n.ls-variable-token--system {\n color: var(--normalVariables, var(--semanticBlue, #65b8ff));\n}\n.ls-variable-token__name {\n min-width: 0;\n word-break: break-all;\n}\n.ls-variable-token__badge {\n display: inline-flex;\n margin-left: 0.5rem;\n flex-shrink: 0;\n align-items: center;\n border: 1px solid currentcolor;\n border-radius: var(--radius, 1rem);\n padding-inline: 0.25rem;\n padding-block: 0;\n font-size: 10px;\n font-weight: 600;\n line-height: 1;\n text-transform: uppercase;\n}\n");
25
25
 
26
26
  // src/components/LSButton/LSButton.tsx
27
27
  import Button from "@mui/material/Button";
@@ -3133,7 +3133,7 @@ var resolveChatterProfileUrlWithResolvers = async ({
3133
3133
  };
3134
3134
 
3135
3135
  // src/se-import/index.ts
3136
- import { nanoid as nanoid10 } from "nanoid";
3136
+ import { nanoid as nanoid8 } from "nanoid";
3137
3137
 
3138
3138
  // src/se-import/translate.ts
3139
3139
  var SE_TO_LUMIA_PLACEHOLDER = {
@@ -4198,12 +4198,26 @@ function mapImage(widget, ctx) {
4198
4198
  );
4199
4199
  const innerWidth = widget.image?.css?.width;
4200
4200
  const innerHeight = widget.image?.css?.height;
4201
+ const outerWidthExplicit = isExplicitPxSize(widget.css?.width);
4202
+ const outerHeightExplicit = isExplicitPxSize(widget.css?.height);
4201
4203
  const bounds = unit.layer.bounds;
4202
- if (innerWidth === "auto") bounds.autoWidth = true;
4203
- if (innerHeight === "auto") bounds.autoHeight = true;
4204
+ if (innerWidth === "auto" && !outerWidthExplicit) bounds.autoWidth = true;
4205
+ if (innerHeight === "auto" && !outerHeightExplicit) bounds.autoHeight = true;
4204
4206
  return unit;
4205
4207
  }
4208
+ function isExplicitPxSize(value) {
4209
+ if (value == null) return false;
4210
+ if (typeof value === "number") return Number.isFinite(value) && value > 0;
4211
+ if (typeof value !== "string") return false;
4212
+ const s = value.trim().toLowerCase();
4213
+ if (s === "" || s === "auto") return false;
4214
+ if (s.endsWith("%")) return false;
4215
+ const parsed = parseFloat(s);
4216
+ return Number.isFinite(parsed) && parsed > 0;
4217
+ }
4206
4218
  function mapVideo(widget, ctx) {
4219
+ const innerWidth = widget.video?.css?.width;
4220
+ const isInnerAutoWidth = innerWidth === "auto";
4207
4221
  const unit = buildUnit(
4208
4222
  widget,
4209
4223
  "video",
@@ -4212,12 +4226,19 @@ function mapVideo(widget, ctx) {
4212
4226
  src: widget.video?.src ?? "",
4213
4227
  volume: widget.video?.volume ?? 0.5,
4214
4228
  loop: true,
4215
- muted: false
4229
+ muted: false,
4230
+ ...isInnerAutoWidth ? { objectFit: "contain" } : {}
4216
4231
  }
4217
4232
  },
4218
4233
  ctx
4219
4234
  );
4220
- unit.layer.bounds.autoHeight = true;
4235
+ const bounds = unit.layer.bounds;
4236
+ if (isInnerAutoWidth) {
4237
+ bounds.autoWidth = true;
4238
+ bounds.autoHeight = true;
4239
+ } else {
4240
+ bounds.autoHeight = true;
4241
+ }
4221
4242
  return unit;
4222
4243
  }
4223
4244
  var LUMIA_SNOW_WEBM = "https://storage.lumiastream.com/overlays/global/seasonal/snow.webm";
@@ -4276,118 +4297,157 @@ function mapReadout(widget, fallbackVar, ctx) {
4276
4297
 
4277
4298
  // src/se-import/mappers/alert.ts
4278
4299
  import { LumiaAlertConfigs } from "@lumiastream/lumia-types";
4300
+ import { nanoid as nanoid2 } from "nanoid";
4301
+
4302
+ // src/se-import/import-utils.ts
4303
+ function pxToNumber(raw, fallback) {
4304
+ if (typeof raw === "number" && Number.isFinite(raw)) return raw;
4305
+ if (typeof raw !== "string") return fallback;
4306
+ const trimmed = raw.trim();
4307
+ if (!trimmed) return fallback;
4308
+ if (trimmed.endsWith("px")) {
4309
+ const n2 = parseFloat(trimmed.slice(0, -2));
4310
+ return Number.isFinite(n2) ? n2 : fallback;
4311
+ }
4312
+ const n = parseFloat(trimmed);
4313
+ return Number.isFinite(n) ? n : fallback;
4314
+ }
4315
+ function parseDurationMs(raw, fallbackMs) {
4316
+ if (typeof raw === "number" && Number.isFinite(raw)) return raw;
4317
+ if (typeof raw !== "string") return fallbackMs;
4318
+ const trimmed = raw.trim();
4319
+ if (!trimmed) return fallbackMs;
4320
+ if (trimmed.endsWith("ms")) {
4321
+ const n2 = parseFloat(trimmed.slice(0, -2));
4322
+ return Number.isFinite(n2) ? n2 : fallbackMs;
4323
+ }
4324
+ if (trimmed.endsWith("s")) {
4325
+ const n2 = parseFloat(trimmed.slice(0, -1));
4326
+ return Number.isFinite(n2) ? n2 * 1e3 : fallbackMs;
4327
+ }
4328
+ const n = parseFloat(trimmed);
4329
+ return Number.isFinite(n) ? n : fallbackMs;
4330
+ }
4331
+ function buildImportDescription(head, sourceUrl) {
4332
+ if (typeof sourceUrl === "string" && sourceUrl.trim().length > 0) {
4333
+ return `${head}
4334
+ Source: ${sourceUrl.trim()}`;
4335
+ }
4336
+ return head;
4337
+ }
4279
4338
 
4280
4339
  // src/se-import/mappers/provider-alerts.ts
4340
+ import { LumiaAlertValues } from "@lumiastream/lumia-types";
4281
4341
  var ALERT_BOX_KEYS = {
4282
4342
  twitch: {
4283
- follower: ["twitch-follower"],
4284
- subscriber: ["twitch-subscriber"],
4285
- tip: ["lumiastream-donation", "streamelements-donation"],
4286
- cheer: ["twitch-bits"],
4287
- raid: ["twitch-raid"],
4288
- merch: ["fourthwall-shopOrder"],
4289
- purchase: ["fourthwall-shopOrder"],
4290
- charityCampaignDonation: ["twitch-charityDonation"]
4343
+ follower: [LumiaAlertValues.TWITCH_FOLLOWER],
4344
+ subscriber: [LumiaAlertValues.TWITCH_SUBSCRIBER],
4345
+ tip: [LumiaAlertValues.LUMIASTREAM_DONATION, LumiaAlertValues.STREAMELEMENTS_DONATION],
4346
+ cheer: [LumiaAlertValues.TWITCH_BITS],
4347
+ raid: [LumiaAlertValues.TWITCH_RAID],
4348
+ merch: [LumiaAlertValues.FOURTHWALL_SHOPORDER],
4349
+ purchase: [LumiaAlertValues.FOURTHWALL_SHOPORDER],
4350
+ charityCampaignDonation: [LumiaAlertValues.TWITCH_CHARITY_DONATION]
4291
4351
  },
4292
4352
  kick: {
4293
- follower: ["kick-follower"],
4294
- subscriber: ["kick-subscriber"],
4295
- tip: ["lumiastream-donation", "streamelements-donation"],
4296
- cheer: ["kick-kicks"],
4297
- raid: ["kick-host"],
4298
- host: ["kick-host"],
4299
- merch: ["fourthwall-shopOrder"],
4300
- purchase: ["fourthwall-shopOrder"]
4353
+ follower: [LumiaAlertValues.KICK_FOLLOWER],
4354
+ subscriber: [LumiaAlertValues.KICK_SUBSCRIBER],
4355
+ tip: [LumiaAlertValues.LUMIASTREAM_DONATION, LumiaAlertValues.STREAMELEMENTS_DONATION],
4356
+ cheer: [LumiaAlertValues.KICK_KICKS],
4357
+ raid: [LumiaAlertValues.KICK_HOST],
4358
+ host: [LumiaAlertValues.KICK_HOST],
4359
+ merch: [LumiaAlertValues.FOURTHWALL_SHOPORDER],
4360
+ purchase: [LumiaAlertValues.FOURTHWALL_SHOPORDER]
4301
4361
  },
4302
4362
  youtube: {
4303
- follower: ["youtube-subscriber"],
4304
- subscriber: ["youtube-member"],
4305
- tip: ["lumiastream-donation", "streamelements-donation"],
4306
- cheer: ["youtube-superchat"],
4307
- merch: ["fourthwall-shopOrder"],
4308
- purchase: ["fourthwall-shopOrder"]
4363
+ follower: [LumiaAlertValues.YOUTUBE_SUBSCRIBER],
4364
+ subscriber: [LumiaAlertValues.YOUTUBE_MEMBER],
4365
+ tip: [LumiaAlertValues.LUMIASTREAM_DONATION, LumiaAlertValues.STREAMELEMENTS_DONATION],
4366
+ cheer: [LumiaAlertValues.YOUTUBE_SUPERCHAT],
4367
+ merch: [LumiaAlertValues.FOURTHWALL_SHOPORDER],
4368
+ purchase: [LumiaAlertValues.FOURTHWALL_SHOPORDER]
4309
4369
  }
4310
4370
  };
4311
4371
  var KAPPAGEN_KEYS = {
4312
4372
  twitch: {
4313
- follower: "twitch-follower",
4314
- subscriber: "twitch-subscriber",
4315
- cheer: "twitch-bits",
4316
- raid: "twitch-raid",
4317
- tip: "lumiastream-donation"
4373
+ follower: LumiaAlertValues.TWITCH_FOLLOWER,
4374
+ subscriber: LumiaAlertValues.TWITCH_SUBSCRIBER,
4375
+ cheer: LumiaAlertValues.TWITCH_BITS,
4376
+ raid: LumiaAlertValues.TWITCH_RAID,
4377
+ tip: LumiaAlertValues.LUMIASTREAM_DONATION
4318
4378
  },
4319
4379
  kick: {
4320
- follower: "kick-follower",
4321
- subscriber: "kick-subscriber",
4322
- cheer: "kick-kicks",
4323
- raid: "kick-host",
4324
- tip: "lumiastream-donation"
4380
+ follower: LumiaAlertValues.KICK_FOLLOWER,
4381
+ subscriber: LumiaAlertValues.KICK_SUBSCRIBER,
4382
+ cheer: LumiaAlertValues.KICK_KICKS,
4383
+ raid: LumiaAlertValues.KICK_HOST,
4384
+ tip: LumiaAlertValues.LUMIASTREAM_DONATION
4325
4385
  },
4326
4386
  youtube: {
4327
- follower: "youtube-subscriber",
4328
- subscriber: "youtube-member",
4329
- cheer: "youtube-superchat",
4330
- tip: "lumiastream-donation"
4387
+ follower: LumiaAlertValues.YOUTUBE_SUBSCRIBER,
4388
+ subscriber: LumiaAlertValues.YOUTUBE_MEMBER,
4389
+ cheer: LumiaAlertValues.YOUTUBE_SUPERCHAT,
4390
+ tip: LumiaAlertValues.LUMIASTREAM_DONATION
4331
4391
  }
4332
4392
  };
4333
4393
  var STREAMBOSS_LISTENER_KEYS = {
4334
4394
  twitch: {
4335
- "follower-latest": ["twitch-follower"],
4336
- "subscriber-latest": ["twitch-subscriber"],
4337
- "tip-latest": ["lumiastream-donation", "streamelements-donation"],
4338
- "cheer-latest": ["twitch-bits"],
4339
- "raid-latest": ["twitch-raid"]
4395
+ "follower-latest": [LumiaAlertValues.TWITCH_FOLLOWER],
4396
+ "subscriber-latest": [LumiaAlertValues.TWITCH_SUBSCRIBER],
4397
+ "tip-latest": [LumiaAlertValues.LUMIASTREAM_DONATION, LumiaAlertValues.STREAMELEMENTS_DONATION],
4398
+ "cheer-latest": [LumiaAlertValues.TWITCH_BITS],
4399
+ "raid-latest": [LumiaAlertValues.TWITCH_RAID]
4340
4400
  },
4341
4401
  kick: {
4342
- "follower-latest": ["kick-follower"],
4343
- "subscriber-latest": ["kick-subscriber"],
4344
- "tip-latest": ["lumiastream-donation", "streamelements-donation"],
4345
- "cheer-latest": ["kick-kicks"],
4346
- "raid-latest": ["kick-host"]
4402
+ "follower-latest": [LumiaAlertValues.KICK_FOLLOWER],
4403
+ "subscriber-latest": [LumiaAlertValues.KICK_SUBSCRIBER],
4404
+ "tip-latest": [LumiaAlertValues.LUMIASTREAM_DONATION, LumiaAlertValues.STREAMELEMENTS_DONATION],
4405
+ "cheer-latest": [LumiaAlertValues.KICK_KICKS],
4406
+ "raid-latest": [LumiaAlertValues.KICK_HOST]
4347
4407
  },
4348
4408
  youtube: {
4349
- "follower-latest": ["youtube-subscriber"],
4350
- "subscriber-latest": ["youtube-member"],
4351
- "tip-latest": ["lumiastream-donation", "streamelements-donation"],
4352
- "cheer-latest": ["youtube-superchat"]
4409
+ "follower-latest": [LumiaAlertValues.YOUTUBE_SUBSCRIBER],
4410
+ "subscriber-latest": [LumiaAlertValues.YOUTUBE_MEMBER],
4411
+ "tip-latest": [LumiaAlertValues.LUMIASTREAM_DONATION, LumiaAlertValues.STREAMELEMENTS_DONATION],
4412
+ "cheer-latest": [LumiaAlertValues.YOUTUBE_SUPERCHAT]
4353
4413
  }
4354
4414
  };
4355
4415
  var CURATED_ALERTS = {
4356
4416
  twitch: [
4357
- "twitch-follower",
4358
- "twitch-subscriber",
4359
- "twitch-giftSubscription",
4360
- "twitch-bits",
4361
- "twitch-bitsCombo",
4362
- "twitch-raid",
4363
- "twitch-points",
4364
- "twitch-redemption",
4365
- "twitch-charityDonation",
4366
- "twitch-hypetrainStarted",
4367
- "twitch-clip",
4368
- "twitch-shoutoutReceive",
4369
- "twitch-watchStreak",
4370
- "twitch-firstChatter",
4371
- "twitch-extension"
4417
+ LumiaAlertValues.TWITCH_FOLLOWER,
4418
+ LumiaAlertValues.TWITCH_SUBSCRIBER,
4419
+ LumiaAlertValues.TWITCH_GIFT_SUBSCRIPTION,
4420
+ LumiaAlertValues.TWITCH_BITS,
4421
+ LumiaAlertValues.TWITCH_BITS_COMBO,
4422
+ LumiaAlertValues.TWITCH_RAID,
4423
+ LumiaAlertValues.TWITCH_POINTS,
4424
+ LumiaAlertValues.TWITCH_REDEMPTION,
4425
+ LumiaAlertValues.TWITCH_CHARITY_DONATION,
4426
+ LumiaAlertValues.TWITCH_HYPETRAIN_STARTED,
4427
+ LumiaAlertValues.TWITCH_CLIP,
4428
+ LumiaAlertValues.TWITCH_SHOUTOUT_RECEIVE,
4429
+ LumiaAlertValues.TWITCH_WATCH_STREAK,
4430
+ LumiaAlertValues.TWITCH_FIRST_CHATTER,
4431
+ LumiaAlertValues.TWITCH_EXTENSION
4372
4432
  ],
4373
4433
  kick: [
4374
- "kick-follower",
4375
- "kick-subscriber",
4376
- "kick-subscriptionGift",
4377
- "kick-kicks",
4378
- "kick-host",
4379
- "kick-points",
4380
- "kick-firstChatter"
4434
+ LumiaAlertValues.KICK_FOLLOWER,
4435
+ LumiaAlertValues.KICK_SUBSCRIBER,
4436
+ LumiaAlertValues.KICK_GIFT_SUBSCRIPTION,
4437
+ LumiaAlertValues.KICK_KICKS,
4438
+ LumiaAlertValues.KICK_HOST,
4439
+ LumiaAlertValues.KICK_POINTS,
4440
+ LumiaAlertValues.KICK_FIRST_CHATTER
4381
4441
  ],
4382
4442
  youtube: [
4383
- "youtube-subscriber",
4384
- "youtube-member",
4385
- "youtube-giftMembers",
4386
- "youtube-superchat",
4387
- "youtube-supersticker",
4388
- "youtube-gifts",
4389
- "youtube-like",
4390
- "youtube-firstChatter"
4443
+ LumiaAlertValues.YOUTUBE_SUBSCRIBER,
4444
+ LumiaAlertValues.YOUTUBE_MEMBER,
4445
+ LumiaAlertValues.YOUTUBE_GIFT_MEMBERS,
4446
+ LumiaAlertValues.YOUTUBE_SUPERCHAT,
4447
+ LumiaAlertValues.YOUTUBE_SUPERSTICKER,
4448
+ LumiaAlertValues.YOUTUBE_GIFTS,
4449
+ LumiaAlertValues.YOUTUBE_LIKE,
4450
+ LumiaAlertValues.YOUTUBE_FIRST_CHATTER
4391
4451
  ]
4392
4452
  };
4393
4453
  function curatedAlertsFor(provider = "twitch") {
@@ -4455,8 +4515,12 @@ function buildAlertEvent(seEvent, seEventType) {
4455
4515
  const shadowEnabled = text.enableShadow !== false;
4456
4516
  const importedTextShadow = css["text-shadow"] ?? "rgba(0, 0, 0, 0.8) 1px 1px 1px";
4457
4517
  const textShadow = shadowEnabled ? importedTextShadow : "none";
4458
- const marginTopRaw = Number(css["margin-top"]);
4459
- const pushTextUp = seLayoutUsesPushText(seEvent.layout) && Number.isFinite(marginTopRaw) ? -marginTopRaw : 0;
4518
+ const allowPush = seLayoutUsesPushText(seEvent.layout);
4519
+ const pushTextUp = allowPush ? pxToNumber(css["margin-top"], 0) : 0;
4520
+ const pushTextLeft = allowPush ? pxToNumber(css["margin-left"], 0) : 0;
4521
+ const usesColumnLayout = !seEvent.layout || seEvent.layout === "column";
4522
+ const usesRowLayout = seEvent.layout === "row";
4523
+ const tightLayout = allowPush && (usesColumnLayout || usesRowLayout);
4460
4524
  const variations = (seEvent.variations ?? []).map((variation) => buildAlertVariation(variation, seEventType, seEvent)).filter((v) => v != null);
4461
4525
  const defaultDuration = (seEventType && SE_DEFAULT_DURATION_BY_EVENT[seEventType]) ?? 8;
4462
4526
  return {
@@ -4483,7 +4547,7 @@ function buildAlertEvent(seEvent, seEventType) {
4483
4547
  showBrandIcon: true
4484
4548
  },
4485
4549
  settings: { duration: (seEvent.duration ?? defaultDuration) * 1e3 },
4486
- layout: { value: translateLayout(seEvent.layout), css: {}, customCss: "", pushTextUp, pushTextLeft: 0 },
4550
+ layout: { value: translateLayout(seEvent.layout), css: {}, customCss: "", pushTextUp, pushTextLeft, tight: tightLayout },
4487
4551
  text: {
4488
4552
  css: {
4489
4553
  fontFamily: css["font-family"] ?? "Roboto",
@@ -4492,6 +4556,10 @@ function buildAlertEvent(seEvent, seEventType) {
4492
4556
  fontWeight: css["font-weight"] ?? "bold",
4493
4557
  textShadow,
4494
4558
  textAlign: css["text-align"] ?? "center"
4559
+ // SE's per-event `margin-top` / `margin-left` are routed to
4560
+ // `layout.pushTextUp` / `layout.pushTextLeft` above (not
4561
+ // stamped on `text.css`) so they land in the alert editor's
4562
+ // dedicated sliders. See the long comment near `pushTextUp`.
4495
4563
  },
4496
4564
  messageCss,
4497
4565
  content: { value: translateSeText(text.message ?? "") }
@@ -4561,6 +4629,15 @@ function buildAlertVariation(v, seEventType, parentSettings) {
4561
4629
  const mergedSettings = deepMergeSeSettings(parentSettings ? parentInheritable : void 0, v.settings);
4562
4630
  const base = buildAlertEvent(mergedSettings, seEventType);
4563
4631
  return {
4632
+ // Stable per-variation ID. Lumia's alert editor (Settings.tsx) auto-
4633
+ // stamps a nanoid on first open for any variation missing an id, but
4634
+ // that leaves a window where the imported overlay's variations have
4635
+ // no stable key — list components rendering them key on
4636
+ // `variation.id || variation.name || idx` and adjacent variations
4637
+ // with the same name collide. Stamping at import time gives every
4638
+ // variation a stable identity from save #1 without waiting for the
4639
+ // editor's first mount.
4640
+ id: nanoid2(),
4564
4641
  name: v.name ?? "Variation",
4565
4642
  on: v.enabled !== false,
4566
4643
  randomChance: v.chance ?? 100,
@@ -4644,7 +4721,7 @@ function mapSingleAlert(widget, lumiaAlertKey, ctx) {
4644
4721
  css: { ...topCss, ...innerCss }
4645
4722
  };
4646
4723
  }
4647
- const built = buildAlertEvent(merged);
4724
+ const built = buildAlertEvent(merged, void 0);
4648
4725
  const events = {
4649
4726
  [lumiaAlertKey]: built
4650
4727
  };
@@ -4790,7 +4867,7 @@ function mapGoal(widget, ctx) {
4790
4867
  }
4791
4868
 
4792
4869
  // src/se-import/mappers/custom.ts
4793
- import { nanoid as nanoid2 } from "nanoid";
4870
+ import { nanoid as nanoid3 } from "nanoid";
4794
4871
  var SOURCE_TO_LUMIA_FIELD_TYPE = {
4795
4872
  text: "input",
4796
4873
  number: "number",
@@ -4880,7 +4957,7 @@ function mapCustom(widget, ctx) {
4880
4957
  // imports — codeId is the scoping key for variables, storage, and
4881
4958
  // event listeners. Mint a fresh nanoid so every imported widget
4882
4959
  // has its own scope.
4883
- codeId: nanoid2(),
4960
+ codeId: nanoid3(),
4884
4961
  html: v.html ?? "",
4885
4962
  css: v.css ?? "",
4886
4963
  js: v.js ?? "",
@@ -5017,6 +5094,63 @@ var TIPJAR_DEFAULT_MODULE = {
5017
5094
  version: 1,
5018
5095
  content: TIPJAR_DEFAULT_CONTENT_FIELDS
5019
5096
  };
5097
+ var EVENTLIST_DEFAULT_CONTENT_FIELDS = {
5098
+ version: 2,
5099
+ eventListType: {},
5100
+ theme: "basic",
5101
+ themeConfig: {},
5102
+ borderRadius: "10px",
5103
+ condensedText: true,
5104
+ src: "",
5105
+ // Category blacklist — events whose Lumia type appears here are
5106
+ // hidden. SL `show_<type>: false` maps to inserting the equivalent
5107
+ // Lumia category here.
5108
+ filters: [],
5109
+ sites: [],
5110
+ maxItemsToShow: 20,
5111
+ removeAfter: 0,
5112
+ reverse: false,
5113
+ horizontal: false,
5114
+ animations: {
5115
+ enterAnimation: "fadeIn",
5116
+ exitAnimation: "fadeOut",
5117
+ enterAnimationDuration: 1e3,
5118
+ exitAnimationDuration: 1e3,
5119
+ enterAnimationDelay: 0,
5120
+ exitAnimationDelay: 0
5121
+ },
5122
+ showDetailedText: true,
5123
+ hideAlertMessage: false,
5124
+ showAlertIcon: true,
5125
+ showSiteIcon: false,
5126
+ showUserAvatar: false,
5127
+ fadeOutAfterDelay: false,
5128
+ fadeOutDelayTime: 5,
5129
+ image: "",
5130
+ itemGap: 16,
5131
+ audio: {
5132
+ enter: "",
5133
+ volume: 1
5134
+ }
5135
+ };
5136
+ var EVENTLIST_DEFAULT_MODULE = {
5137
+ version: 2,
5138
+ content: EVENTLIST_DEFAULT_CONTENT_FIELDS,
5139
+ css: {
5140
+ borderRadius: "0px",
5141
+ borderStyle: "solid",
5142
+ borderWidth: "0px",
5143
+ borderColor: "transparent",
5144
+ fontSize: 16,
5145
+ lineHeight: 1.2,
5146
+ textAlign: "left",
5147
+ fontFamily: "Roboto",
5148
+ fontWeight: "normal",
5149
+ color: "#ffffff",
5150
+ textShadow: "",
5151
+ background: "transparent"
5152
+ }
5153
+ };
5020
5154
 
5021
5155
  // src/se-import/mappers/misc.ts
5022
5156
  function mapChatboxTheme(seTheme) {
@@ -6966,11 +7100,6 @@ function rewriteAssetURLs(overlay, mapping) {
6966
7100
  };
6967
7101
  }
6968
7102
  var MAX_MIRROR_ASSET_BYTES = 100 * 1024 * 1024;
6969
- function looksLikeCorsError2(err) {
6970
- if (!(err instanceof TypeError)) return false;
6971
- const msg = (err.message ?? "").toLowerCase();
6972
- return msg.includes("failed to fetch") || msg.includes("networkerror") || msg.includes("network error") || msg.includes("cors") || msg.includes("access-control") || err.message.length > 0;
6973
- }
6974
7103
  async function mirrorOneAsset(url, upload, signal, proxyAssetFetch) {
6975
7104
  const blob = await fetchAssetBlob(url, signal, proxyAssetFetch);
6976
7105
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
@@ -7002,7 +7131,7 @@ async function fetchAssetBlob(url, signal, proxyAssetFetch) {
7002
7131
  return res.blob();
7003
7132
  }
7004
7133
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
7005
- if (proxyAssetFetch && looksLikeCorsError2(directErr)) {
7134
+ if (proxyAssetFetch && looksLikeCorsError(directErr)) {
7006
7135
  try {
7007
7136
  return await proxyAssetFetch(url);
7008
7137
  } catch (proxyErr) {
@@ -7053,7 +7182,7 @@ function guessMime(name) {
7053
7182
  }
7054
7183
 
7055
7184
  // src/se-import/transplant.ts
7056
- import { nanoid as nanoid3 } from "nanoid";
7185
+ import { nanoid as nanoid4 } from "nanoid";
7057
7186
  function cloneModuleForTransplant(srcModule, newId) {
7058
7187
  const clonedContent = srcModule.content ? structuredClone(srcModule.content) : srcModule.content;
7059
7188
  if (srcModule.settings?.type === "custom" && clonedContent) {
@@ -7071,7 +7200,7 @@ function transplantLayer(source, sourceLayerId) {
7071
7200
  const srcLayer = layers.find((l) => l.id === sourceLayerId);
7072
7201
  const srcModule = modules[sourceLayerId];
7073
7202
  if (!srcLayer || !srcModule) return null;
7074
- const rootNewId = nanoid3();
7203
+ const rootNewId = nanoid4();
7075
7204
  if (srcLayer.type !== "group") {
7076
7205
  const layer = { ...srcLayer, id: rootNewId };
7077
7206
  const module = cloneModuleForTransplant(srcModule, rootNewId);
@@ -7089,7 +7218,7 @@ function transplantLayer(source, sourceLayerId) {
7089
7218
  if (seen.has(child.id)) continue;
7090
7219
  seen.add(child.id);
7091
7220
  descendants.push(child);
7092
- idRemap.set(child.id, nanoid3());
7221
+ idRemap.set(child.id, nanoid4());
7093
7222
  if (child.type === "group") queue.push(child.id);
7094
7223
  }
7095
7224
  }
@@ -7593,7 +7722,74 @@ function readSLProvider(response) {
7593
7722
  }
7594
7723
 
7595
7724
  // src/sl-import/mappers/alert-box.ts
7596
- import { nanoid as nanoid4 } from "nanoid";
7725
+ import { nanoid as nanoid6 } from "nanoid";
7726
+ import { LumiaAlertValues as LumiaAlertValues2 } from "@lumiastream/lumia-types";
7727
+
7728
+ // src/se-import/import-envelope.ts
7729
+ import { nanoid as nanoid5 } from "nanoid";
7730
+ function buildModuleEnvelope(opts) {
7731
+ const id = opts.id ?? nanoid5();
7732
+ const locked = opts.locked ?? false;
7733
+ const visible = opts.visible ?? true;
7734
+ const layer = {
7735
+ id,
7736
+ ...opts.parentGroupId !== void 0 ? { group: opts.parentGroupId } : {},
7737
+ ...opts.type_layer ? { type: opts.type_layer } : {},
7738
+ state: {
7739
+ visible,
7740
+ locked,
7741
+ ...opts.expanded !== void 0 ? { expanded: opts.expanded } : {}
7742
+ },
7743
+ bounds: {
7744
+ x: opts.bounds.x,
7745
+ y: opts.bounds.y,
7746
+ width: opts.bounds.width,
7747
+ height: opts.bounds.height,
7748
+ scale: [1, 1],
7749
+ ...opts.zIndex !== void 0 ? { zIndex: opts.zIndex } : {}
7750
+ }
7751
+ };
7752
+ const module = {
7753
+ id,
7754
+ version: opts.version ?? 1,
7755
+ loaded: true,
7756
+ settings: { type: opts.type, title: opts.title, locked },
7757
+ lights: opts.lights ?? [],
7758
+ css: opts.css ?? {},
7759
+ content: opts.content ?? {},
7760
+ variables: opts.variables ?? {},
7761
+ events: opts.events ?? {},
7762
+ ...opts.alert !== void 0 ? { alert: opts.alert } : {}
7763
+ };
7764
+ return { id, layer, module };
7765
+ }
7766
+ var IMPORT_META_KEY = "importMeta";
7767
+ function stampImportMetaMerge(module, meta) {
7768
+ const existing = module.content?.[IMPORT_META_KEY] ?? {};
7769
+ module.content = {
7770
+ ...module.content ?? {},
7771
+ [IMPORT_META_KEY]: {
7772
+ importedAt: (/* @__PURE__ */ new Date()).toISOString(),
7773
+ ...existing,
7774
+ ...meta
7775
+ }
7776
+ };
7777
+ }
7778
+ function customHtmlReviewItem(moduleId, importSourceType, settings, reason) {
7779
+ return {
7780
+ moduleId,
7781
+ // ReviewItem's `seWidget` is typed against SEWidget but the review
7782
+ // UI just reads `.type` and `.settings`. Cast through unknown is
7783
+ // the only place we touch the shape mismatch; widening the
7784
+ // ReviewItem type is a future cleanup.
7785
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7786
+ seWidget: { type: importSourceType, settings },
7787
+ status: "template",
7788
+ reason
7789
+ };
7790
+ }
7791
+
7792
+ // src/sl-import/mappers/alert-box.ts
7597
7793
  var SL_EVENT_ROUTING = {
7598
7794
  // Platform-native — provider routes (Twitch / Kick / YouTube).
7599
7795
  follow: { pickProviderKeys: "follower" },
@@ -7605,47 +7801,35 @@ var SL_EVENT_ROUTING = {
7605
7801
  // `subscriber` here is YouTube subscribe (free); `sponsor` is YouTube
7606
7802
  // channel membership; `fanfunding` is YouTube superchat. SL ships these
7607
7803
  // even on Twitch-primary accounts so we always map them.
7608
- subscriber: { keys: ["youtube-subscriber"] },
7609
- sponsor: { keys: ["youtube-member"] },
7610
- fanfunding: { keys: ["youtube-superchat"] },
7611
- super_sticker: { keys: ["youtube-supersticker"] },
7804
+ subscriber: { keys: [LumiaAlertValues2.YOUTUBE_SUBSCRIBER] },
7805
+ sponsor: { keys: [LumiaAlertValues2.YOUTUBE_MEMBER] },
7806
+ fanfunding: { keys: [LumiaAlertValues2.YOUTUBE_SUPERCHAT] },
7807
+ super_sticker: { keys: [LumiaAlertValues2.YOUTUBE_SUPERSTICKER] },
7612
7808
  // Donation / tip channels — Lumia ships dedicated alert keys for each
7613
7809
  // integration. `donation` itself fans out to `streamlabs-donation` AND
7614
7810
  // `lumiastream-donation` so the imported alert fires regardless of which
7615
7811
  // channel the donation actually came in on.
7616
- donation: { keys: ["streamlabs-donation", "lumiastream-donation"] },
7617
- eldonation: { keys: ["extralife-donation"] },
7618
- tiltifydonation: { keys: ["tiltify-campaignDonation"] },
7619
- donordrivedonation: { keys: ["donordrive-donation"] },
7620
- justgivingdonation: { keys: ["streamlabs-donation"] },
7621
- pledge: { keys: ["patreon-campaignPledge"] },
7812
+ donation: { keys: [LumiaAlertValues2.STREAMLABS_DONATION, LumiaAlertValues2.LUMIASTREAM_DONATION] },
7813
+ eldonation: { keys: [LumiaAlertValues2.EXTRALIFE_DONATION] },
7814
+ tiltifydonation: { keys: [LumiaAlertValues2.TILTIFY_DONATION] },
7815
+ donordrivedonation: { keys: [LumiaAlertValues2.DONORDRIVE_DONATION] },
7816
+ justgivingdonation: { keys: [LumiaAlertValues2.STREAMLABS_DONATION] },
7817
+ pledge: { keys: [LumiaAlertValues2.PATREON_PLEDGE] },
7622
7818
  // Charity / merch / loyalty / prime gifting — SL-flavored channels with
7623
7819
  // matching Lumia keys.
7624
- streamlabscharitydonation: { keys: ["streamlabs-charity"] },
7625
- twitchcharitydonation: { keys: ["twitch-charityDonation"] },
7626
- merch: { keys: ["streamlabs-merch"] },
7627
- loyalty_store_redemption: { keys: ["streamlabs-redemption"] },
7628
- prime_sub_gift: { keys: ["streamlabs-primegift"] },
7629
- treat: { keys: ["treatstream-treat"] },
7820
+ streamlabscharitydonation: { keys: [LumiaAlertValues2.STREAMLABS_CHARITY] },
7821
+ twitchcharitydonation: { keys: [LumiaAlertValues2.TWITCH_CHARITY_DONATION] },
7822
+ merch: { keys: [LumiaAlertValues2.STREAMLABS_MERCH] },
7823
+ loyalty_store_redemption: { keys: [LumiaAlertValues2.STREAMLABS_REDEMPTION] },
7824
+ prime_sub_gift: { keys: [LumiaAlertValues2.STREAMLABS_PRIMEGIFT] },
7825
+ treat: { keys: [LumiaAlertValues2.TREATSTREAM_TREAT] },
7630
7826
  // TikTok-flavored (jewel gift is TikTok-ish in concept).
7631
- jewel_gift: { keys: ["tiktok-gift"] }
7827
+ jewel_gift: { keys: [LumiaAlertValues2.TIKTOK_GIFT] }
7632
7828
  };
7633
7829
  function getField(settings, prefix, field) {
7634
7830
  const key = `${prefix}_${field}`;
7635
7831
  return settings[key];
7636
7832
  }
7637
- function pxToNumber(raw, fallback) {
7638
- if (typeof raw === "number" && Number.isFinite(raw)) return raw;
7639
- if (typeof raw !== "string") return fallback;
7640
- const trimmed = raw.trim();
7641
- if (!trimmed) return fallback;
7642
- if (trimmed.endsWith("px")) {
7643
- const n2 = parseFloat(trimmed.slice(0, -2));
7644
- return Number.isFinite(n2) ? n2 : fallback;
7645
- }
7646
- const n = parseFloat(trimmed);
7647
- return Number.isFinite(n) ? n : fallback;
7648
- }
7649
7833
  function translateLayout2(slLayout) {
7650
7834
  switch (slLayout) {
7651
7835
  case "above":
@@ -7675,13 +7859,7 @@ function buildBaseAlertEvent(settings, prefix, defaultMessage) {
7675
7859
  const fontWeight = getField(settings, prefix, "font_weight") ?? 600;
7676
7860
  const fontColor = getField(settings, prefix, "font_color") ?? "#FFFFFF";
7677
7861
  const highlightColor = getField(settings, prefix, "font_color2") ?? "#32C3A6";
7678
- const messageFontSize = pxToNumber(
7679
- getField(settings, prefix, "message_font_size"),
7680
- 20
7681
- );
7682
- const messageFontFamily = getField(settings, prefix, "message_font") ?? fontFamily;
7683
- const messageFontWeight = getField(settings, prefix, "message_font_weight") ?? 400;
7684
- const messageFontColor = getField(settings, prefix, "message_font_color") ?? "#FFFFFF";
7862
+ const secondaryMessage = readSecondaryMessageConfig(settings, prefix, fontFamily);
7685
7863
  const imageHref = absolutizeSLUrl(getField(settings, prefix, "image_href"));
7686
7864
  const soundHref = absolutizeSLUrl(getField(settings, prefix, "sound_href"));
7687
7865
  const soundVolume = getField(settings, prefix, "sound_volume") ?? 50;
@@ -7700,7 +7878,7 @@ function buildBaseAlertEvent(settings, prefix, defaultMessage) {
7700
7878
  accentColor: null,
7701
7879
  highlightColor,
7702
7880
  firstMessageTemplate: translateSeText(messageTemplate),
7703
- secondMessageTemplate: "",
7881
+ secondMessageTemplate: secondaryMessage?.template ?? "",
7704
7882
  thirdMessageTemplate: "",
7705
7883
  showAvatar: false,
7706
7884
  showAmount: true,
@@ -7723,12 +7901,10 @@ function buildBaseAlertEvent(settings, prefix, defaultMessage) {
7723
7901
  textShadow: "rgba(0, 0, 0, 0.8) 1px 1px 1px",
7724
7902
  textAlign: "center"
7725
7903
  },
7726
- messageCss: {
7727
- fontFamily: messageFontFamily,
7728
- fontSize: messageFontSize,
7729
- color: messageFontColor,
7730
- fontWeight: messageFontWeight
7731
- },
7904
+ // `messageCss` only matters when secondMessageTemplate is set;
7905
+ // leave it empty when SL didn't ship a secondary-message block
7906
+ // so Lumia's renderer falls back to its own default styling.
7907
+ messageCss: secondaryMessage?.css ?? {},
7732
7908
  content: { value: translateSeText(messageTemplate) }
7733
7909
  },
7734
7910
  media: {
@@ -7763,14 +7939,39 @@ function buildBaseAlertEvent(settings, prefix, defaultMessage) {
7763
7939
  tts: {
7764
7940
  enabled: getField(settings, prefix, "tts_enabled") ?? false,
7765
7941
  voice: getField(settings, prefix, "tts_language") ?? "Brian",
7766
- volume: clampVolume(
7767
- getField(settings, prefix, "tts_volume") ?? 75
7768
- ),
7942
+ volume: clampVolume(getField(settings, prefix, "tts_volume") ?? 75),
7769
7943
  minAmount: getField(settings, prefix, "tts_min_amount") ?? 0
7770
7944
  },
7771
7945
  variations: []
7772
7946
  };
7773
7947
  }
7948
+ var SECONDARY_MESSAGE_PREFIX_OVERRIDE = {
7949
+ sub: "resub"
7950
+ };
7951
+ function readSecondaryMessageConfig(settings, prefix, primaryFontFamily) {
7952
+ const secondaryPrefix = SECONDARY_MESSAGE_PREFIX_OVERRIDE[prefix] ?? prefix;
7953
+ const showKey = `show_${secondaryPrefix}_message`;
7954
+ const enabled = settings[showKey];
7955
+ if (enabled !== true) return null;
7956
+ const fontFamily = getField(settings, secondaryPrefix, "message_font") ?? primaryFontFamily;
7957
+ const fontSize = pxToNumber(getField(settings, secondaryPrefix, "message_font_size"), 20);
7958
+ const fontWeight = getField(settings, secondaryPrefix, "message_font_weight") ?? 400;
7959
+ const fontColor = getField(settings, secondaryPrefix, "message_font_color") ?? "#FFFFFF";
7960
+ return {
7961
+ // `{message}` is the SE/SL chat-message token (mapped to Lumia's
7962
+ // `{{message}}` in translate.ts). It resolves at render time from
7963
+ // the alert payload's `message` field — populated for every event
7964
+ // where SL ships a secondary-message line (donations, resubs,
7965
+ // cheers, charity, merch, etc.).
7966
+ template: translateSeText("{message}"),
7967
+ css: {
7968
+ fontFamily,
7969
+ fontSize,
7970
+ color: fontColor,
7971
+ fontWeight
7972
+ }
7973
+ };
7974
+ }
7774
7975
  function clampVolume(slVolume) {
7775
7976
  if (typeof slVolume !== "number" || !Number.isFinite(slVolume)) return 0.5;
7776
7977
  return Math.max(0, Math.min(1, slVolume / 100));
@@ -7780,11 +7981,20 @@ function translateSLVariationCondition(v) {
7780
7981
  const numericData = typeof data === "number" ? data : Number(data);
7781
7982
  switch (v.condition) {
7782
7983
  case "RANDOM":
7783
- return { conditionType: "RANDOM", condition: Number.isFinite(numericData) ? numericData : 100 };
7984
+ return {
7985
+ conditionType: "RANDOM",
7986
+ condition: Number.isFinite(numericData) ? numericData : 100
7987
+ };
7784
7988
  case "MIN_BITS_USED":
7785
- return { conditionType: "GREATER_NUMBER", condition: Number.isFinite(numericData) ? numericData : 0 };
7989
+ return {
7990
+ conditionType: "GREATER_NUMBER",
7991
+ condition: Number.isFinite(numericData) ? numericData : 0
7992
+ };
7786
7993
  case "EXACT_BITS_USED":
7787
- return { conditionType: "EQUAL_NUMBER", condition: Number.isFinite(numericData) ? numericData : 0 };
7994
+ return {
7995
+ conditionType: "EQUAL_NUMBER",
7996
+ condition: Number.isFinite(numericData) ? numericData : 0
7997
+ };
7788
7998
  case "MIN_MONTHS_SUBSCRIBED":
7789
7999
  return {
7790
8000
  conditionType: "SUBSCRIBED_MONTHS_GREATER",
@@ -7849,6 +8059,14 @@ function buildVariationEvent(v, parentBase) {
7849
8059
  if (s.hideAnimation) inherited.animations.alertExitAnimation = s.hideAnimation;
7850
8060
  if (s.layout) inherited.layout.value = translateLayout2(s.layout);
7851
8061
  return {
8062
+ // Reuse SL's own `uuid` when present so re-importing the same SL
8063
+ // alert-box keeps the variation identity stable across import
8064
+ // runs. SL always emits a uuid in their config (every fixture we've
8065
+ // seen has one); fall back to nanoid for the edge case of a
8066
+ // hand-crafted / older payload that omits it. Matches the SE
8067
+ // alert mapper's variation-id pattern — see Settings.tsx auto-fill
8068
+ // note there for why a stable id matters from save #1.
8069
+ id: typeof v.uuid === "string" && v.uuid.length > 0 ? v.uuid : nanoid6(),
7852
8070
  name: v.name ?? "Variation",
7853
8071
  on: v.paused !== true,
7854
8072
  randomChance: 100,
@@ -7914,11 +8132,7 @@ function buildSLAlertBox(settings, options = {}) {
7914
8132
  for (const [prefix, routing] of Object.entries(SL_EVENT_ROUTING)) {
7915
8133
  const lumiaKeys = routing.pickProviderKeys ? alertBoxKeysFor(routing.pickProviderKeys, provider) : routing.keys ?? [];
7916
8134
  if (lumiaKeys.length === 0) continue;
7917
- const built = buildBaseAlertEvent(
7918
- settings,
7919
- prefix,
7920
- DEFAULT_MESSAGES[prefix] ?? "{name}"
7921
- );
8135
+ const built = buildBaseAlertEvent(settings, prefix, DEFAULT_MESSAGES[prefix] ?? "{name}");
7922
8136
  if (!built) continue;
7923
8137
  const variationKey = VARIATION_KEY_FOR_PREFIX[prefix];
7924
8138
  const variations = variationKey ? settings[variationKey] : void 0;
@@ -7935,53 +8149,32 @@ function buildSLAlertBox(settings, options = {}) {
7935
8149
  channelsImported.push(prefix);
7936
8150
  if (firstEvent == null) firstEvent = built;
7937
8151
  }
7938
- const id = nanoid4();
7939
- const layer = {
7940
- id,
7941
- state: { visible: true, locked: false },
7942
- bounds: { x: 100, y: 100, width: 700, height: 600, scale: [1, 1] }
7943
- };
7944
8152
  const baseAlert = firstEvent ? stripEventOnlyFields(firstEvent) : {};
7945
- const module = {
7946
- id,
7947
- version: 1,
7948
- loaded: true,
7949
- settings: { type: "alert", title: "Streamlabs Alerts", locked: false },
7950
- lights: [],
7951
- css: {},
7952
- content: {},
7953
- variables: {},
7954
- events: {},
8153
+ const { layer, module } = buildModuleEnvelope({
8154
+ type: "alert",
8155
+ title: "Streamlabs Alerts",
8156
+ bounds: { x: 100, y: 100, width: 700, height: 600 },
7955
8157
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
7956
8158
  alert: { ...baseAlert, events }
7957
- };
8159
+ });
7958
8160
  return { layer, module, channelsImported };
7959
8161
  }
7960
8162
  function stripEventOnlyFields(event) {
7961
- const { on: _on, variations: _variations, matchEmptyCondition: _mec, ...base } = event;
8163
+ const {
8164
+ on: _on,
8165
+ variations: _variations,
8166
+ matchEmptyCondition: _mec,
8167
+ ...base
8168
+ } = event;
7962
8169
  return base;
7963
8170
  }
7964
8171
 
7965
8172
  // src/sl-import/mappers/tip-jar.ts
7966
- import { nanoid as nanoid5 } from "nanoid";
7967
- function pxToNumber2(raw, fallback) {
7968
- if (typeof raw === "number" && Number.isFinite(raw)) return raw;
7969
- if (typeof raw !== "string") return fallback;
7970
- const trimmed = raw.trim();
7971
- if (!trimmed) return fallback;
7972
- if (trimmed.endsWith("px")) {
7973
- const n2 = parseFloat(trimmed.slice(0, -2));
7974
- return Number.isFinite(n2) ? n2 : fallback;
7975
- }
7976
- const n = parseFloat(trimmed);
7977
- return Number.isFinite(n) ? n : fallback;
7978
- }
7979
8173
  function buildSLTipJar(settings) {
7980
- const id = nanoid5();
7981
8174
  const backgroundColor = settings.background?.color ?? "transparent";
7982
8175
  const textColor = settings.text?.color ?? "#FFFFFF";
7983
8176
  const fontFamily = settings.text?.font ?? "Open Sans";
7984
- const fontSize = pxToNumber2(settings.text?.size, 32);
8177
+ const fontSize = pxToNumber(settings.text?.size, 32);
7985
8178
  const types = settings.types ?? {};
7986
8179
  const tipsConfig = types.tips ?? {};
7987
8180
  const slEvents = {
@@ -7997,17 +8190,11 @@ function buildSLTipJar(settings) {
7997
8190
  ...TIPJAR_DEFAULT_MODULE.content.display,
7998
8191
  showEventMessages: settings.text?.show !== false
7999
8192
  };
8000
- const layer = {
8001
- id,
8002
- state: { visible: true, locked: false },
8003
- bounds: { x: 100, y: 100, width: 600, height: 700, scale: [1, 1] }
8004
- };
8005
- const module = {
8006
- id,
8193
+ const { layer, module } = buildModuleEnvelope({
8194
+ type: "tipjar",
8195
+ title: "Streamlabs Tip Jar",
8196
+ bounds: { x: 100, y: 100, width: 600, height: 700 },
8007
8197
  version: TIPJAR_DEFAULT_MODULE.version,
8008
- loaded: true,
8009
- settings: { type: "tipjar", title: "Streamlabs Tip Jar", locked: false },
8010
- lights: [],
8011
8198
  css: {
8012
8199
  fontFamily,
8013
8200
  fontSize,
@@ -8028,12 +8215,7 @@ function buildSLTipJar(settings) {
8028
8215
  source: "streamlabs",
8029
8216
  widgetType: "tip_jar",
8030
8217
  importedAt: (/* @__PURE__ */ new Date()).toISOString(),
8031
- // SL jar type doesn't have a 1:1 mapping to Lumia's preset
8032
- // vocabulary (cup-01, cup-02, …). Stashed for review.
8033
8218
  slJarType: settings.jar?.type ?? null,
8034
- // SL tier coin tiers — Lumia's sprite tiers are per-event and
8035
- // configured in the sprite editor; the user re-creates these
8036
- // manually. Stored here so they're not lost.
8037
8219
  slTipTiers: Array.isArray(tipsConfig.tiers) ? tipsConfig.tiers.map((t) => ({
8038
8220
  minimumAmount: t.minimum_amount,
8039
8221
  imageSrc: t.image_src ?? null
@@ -8041,15 +8223,12 @@ function buildSLTipJar(settings) {
8041
8223
  slMinimumTipAmount: tipsConfig.minimum_amount ?? null,
8042
8224
  slHadCustomHtml: settings.custom_html_enabled === true
8043
8225
  }
8044
- },
8045
- variables: {},
8046
- events: {}
8047
- };
8226
+ }
8227
+ });
8048
8228
  return { layer, module };
8049
8229
  }
8050
8230
 
8051
8231
  // src/sl-import/mappers/goal.ts
8052
- import { nanoid as nanoid6 } from "nanoid";
8053
8232
  var GOAL_SOURCE_MAP = {
8054
8233
  follower_goal: "follower",
8055
8234
  sub_goal: "subscriber",
@@ -8062,69 +8241,262 @@ var GOAL_TITLE_MAP = {
8062
8241
  donation_goal: "Donation Goal",
8063
8242
  bit_goal: "Bits Goal"
8064
8243
  };
8065
- function pxToNumber3(raw, fallback) {
8066
- if (typeof raw === "number" && Number.isFinite(raw)) return raw;
8067
- if (typeof raw !== "string") return fallback;
8068
- const trimmed = raw.trim();
8069
- if (!trimmed) return fallback;
8070
- if (trimmed.endsWith("px")) {
8071
- const n2 = parseFloat(trimmed.slice(0, -2));
8072
- return Number.isFinite(n2) ? n2 : fallback;
8073
- }
8074
- const n = parseFloat(trimmed);
8075
- return Number.isFinite(n) ? n : fallback;
8076
- }
8244
+ var GOAL_CURRENT_TEMPLATE = {
8245
+ follower_goal: "{{twitch_total_follower_count}}",
8246
+ sub_goal: "{{twitch_total_subscriber_count}}",
8247
+ donation_goal: "{{total_donation_amount}}",
8248
+ bit_goal: "{{twitch_total_bits_count}}"
8249
+ };
8077
8250
  function buildSLGoal(settings, widgetType) {
8078
- const id = nanoid6();
8079
8251
  const goalSource = GOAL_SOURCE_MAP[widgetType] ?? "follower";
8080
8252
  const defaultTitle = GOAL_TITLE_MAP[widgetType] ?? "Goal";
8253
+ const currentTemplate = GOAL_CURRENT_TEMPLATE[widgetType] ?? "";
8254
+ const requiresReview = settings.custom_enabled === true;
8081
8255
  const title = typeof settings.title === "string" ? settings.title : defaultTitle;
8082
8256
  const goalAmount = typeof settings.goal === "number" ? settings.goal : 100;
8083
8257
  const currentAmount = typeof settings.current === "number" ? settings.current : 0;
8084
8258
  const startingAmount = typeof settings.starting_amount === "number" ? settings.starting_amount : 0;
8085
- const endsAt = typeof settings.ends_at === "string" ? settings.ends_at : null;
8259
+ const endsAt = typeof settings.ends_at === "string" ? settings.ends_at : "";
8086
8260
  const barColor = settings.bar_color ?? "#32C3A6";
8087
- const barBackgroundColor = settings.bar_background_color ?? "#181818";
8261
+ const barBgColor = settings.bar_bg_color ?? "#181818";
8262
+ const widgetBgColor = settings.background_color ?? "transparent";
8088
8263
  const textColor = settings.text_color ?? "#FFFFFF";
8089
8264
  const barTextColor = settings.bar_text_color ?? textColor;
8090
8265
  const fontFamily = settings.font ?? "Open Sans";
8091
- const layer = {
8092
- id,
8093
- state: { visible: true, locked: false },
8094
- bounds: { x: 100, y: 100, width: 740, height: 90, scale: [1, 1] }
8095
- };
8096
- const module = {
8097
- id,
8098
- version: 1,
8099
- loaded: true,
8100
- settings: { type: "goal", title, locked: false },
8101
- lights: [],
8266
+ const fontSize = pxToNumber(settings.font_size, 24);
8267
+ const barThickness = pxToNumber(settings.bar_thickness, 48);
8268
+ const layerHeight = Math.max(60, barThickness + 32);
8269
+ const { layer, module } = buildModuleEnvelope({
8270
+ type: "goal",
8271
+ title,
8272
+ bounds: { x: 100, y: 100, width: 740, height: layerHeight },
8102
8273
  css: {
8103
8274
  fontFamily,
8104
8275
  color: textColor,
8105
- background: barBackgroundColor
8276
+ background: widgetBgColor,
8277
+ fontSize
8106
8278
  },
8279
+ // Use Lumia's canonical goal field names — `targetGoal` (string),
8280
+ // `current` (template string), `display`, `filledColor` /
8281
+ // `unfilledColor`. The prior schema used invented field names
8282
+ // (`goalAmount`, `barColor`) that the renderer ignored.
8107
8283
  content: {
8108
- title,
8109
- goalSource,
8110
- goalAmount,
8111
- currentAmount,
8112
- startingAmount,
8113
- endsAt,
8114
- barColor,
8115
- barBackgroundColor,
8116
- barTextColor,
8117
- fontFamily,
8118
- fontSize: pxToNumber3(settings.font_size, 24)
8284
+ version: 1,
8285
+ type: "bar",
8286
+ label: title,
8287
+ subLabel: "",
8288
+ display: "{{current}} / {{goal}}",
8289
+ current: currentAmount > 0 ? String(currentAmount) : currentTemplate,
8290
+ targetGoal: String(goalAmount),
8291
+ image: "",
8292
+ audio: {
8293
+ decrement: "",
8294
+ increment: "",
8295
+ goal: "",
8296
+ goalFailed: "",
8297
+ volume: 1
8298
+ },
8299
+ endDate: endsAt,
8300
+ goalAnimation: "",
8301
+ goalAnimationLoop: true,
8302
+ unfilledColor: barBgColor,
8303
+ filledColor: barColor,
8304
+ alignItemsToEdges: false,
8305
+ border: "solid 1px transparent",
8306
+ borderRadius: "40px",
8307
+ highlightColor: "inherit",
8308
+ importMeta: {
8309
+ source: "streamlabs",
8310
+ widgetType,
8311
+ importedAt: (/* @__PURE__ */ new Date()).toISOString(),
8312
+ goalSource,
8313
+ slLayout: settings.layout ?? null,
8314
+ slBarTextColor: barTextColor,
8315
+ slBarThickness: barThickness,
8316
+ slStartingAmount: startingAmount,
8317
+ slHadCustomHtml: settings.custom_enabled === true
8318
+ }
8319
+ }
8320
+ });
8321
+ return { layer, module, requiresReview };
8322
+ }
8323
+
8324
+ // src/sl-import/mappers/chat-box.ts
8325
+ function buildSitesArray(settings) {
8326
+ const defaultSites = [...CHATBOX_DEFAULT_MODULE.content.sites];
8327
+ const drop = /* @__PURE__ */ new Set();
8328
+ if (settings.show_bttv_emotes === false) drop.add("bttv");
8329
+ if (settings.show_franker_emotes === false) drop.add("ffz");
8330
+ if (settings.show_7tv_emotes === false) drop.add("seventv");
8331
+ if (settings.twitch_enabled === false) drop.add("twitch");
8332
+ if (settings.youtube_enabled === false) drop.add("youtube");
8333
+ if (settings.facebook_enabled === false) drop.add("facebook");
8334
+ if (settings.tiktok_enabled === false) drop.add("tiktok");
8335
+ if (settings.kick_enabled === false) drop.add("kick");
8336
+ if (drop.size === 0) return defaultSites;
8337
+ return defaultSites.filter((s) => !drop.has(s));
8338
+ }
8339
+ function buildSLChatBox(settings) {
8340
+ const requiresReview = settings.custom_enabled === true;
8341
+ const backgroundColor = settings.background_color ?? "transparent";
8342
+ const textColor = settings.text_color ?? "#FFFFFF";
8343
+ const textSize = pxToNumber(settings.text_size, 16);
8344
+ const fadeOutAfterDelay = settings.always_show_messages !== true;
8345
+ const fadeOutDelayTime = fadeOutAfterDelay ? Math.max(1, Math.round((settings.message_hide_delay ?? 15e3) / 1e3)) : 0;
8346
+ const sites = buildSitesArray(settings);
8347
+ const { layer, module } = buildModuleEnvelope({
8348
+ type: "chatbox",
8349
+ title: "Streamlabs Chat Box",
8350
+ bounds: { x: 100, y: 100, width: 375, height: 470 },
8351
+ version: CHATBOX_DEFAULT_MODULE.version,
8352
+ css: {
8353
+ background: backgroundColor,
8354
+ color: textColor,
8355
+ fontSize: textSize
8119
8356
  },
8120
- variables: {},
8121
- events: {}
8122
- };
8123
- return { layer, module };
8357
+ content: {
8358
+ // Spread canonical defaults FIRST so the renderer's dereferences
8359
+ // (chatboxStreamingSite.<platform>.themeConfig, animations,
8360
+ // chatboxStreamingSite.<platform>.message, etc.) all exist.
8361
+ ...CHATBOX_DEFAULT_MODULE.content,
8362
+ // Then override the bits SL controls.
8363
+ sites,
8364
+ fadeOutAfterDelay,
8365
+ fadeOutDelayTime,
8366
+ // SL keeps history forever by default — `always_show_messages`
8367
+ // implies the streamer wants a permanent scrollback. Lumia
8368
+ // exposes that as `permanent: true`.
8369
+ permanent: settings.always_show_messages === true,
8370
+ // Preserve the SL-specific provenance for the review step.
8371
+ importMeta: {
8372
+ source: "streamlabs",
8373
+ widgetType: "chat_box",
8374
+ importedAt: (/* @__PURE__ */ new Date()).toISOString(),
8375
+ slTheme: settings.theme ?? null,
8376
+ slHadCustomHtml: settings.custom_enabled === true,
8377
+ slHidesCommonBots: settings.hide_common_chat_bots === true,
8378
+ slHidesCommands: settings.hide_commands === true,
8379
+ slMutedChatters: settings.muted_chatters ?? "",
8380
+ slMessageShowDelayMs: settings.message_show_delay ?? 0
8381
+ }
8382
+ }
8383
+ });
8384
+ return { layer, module, requiresReview };
8385
+ }
8386
+
8387
+ // src/sl-import/mappers/event-list.ts
8388
+ var SL_SHOW_FLAG_TO_LUMIA_FILTER = {
8389
+ show_follows: "follower",
8390
+ show_subscriptions: "subscribers",
8391
+ show_resubs: "subscribers",
8392
+ show_sub_gifts: "gifts",
8393
+ show_subscribers: "subscribers",
8394
+ show_sponsors: "subscribers",
8395
+ // YouTube channel members
8396
+ show_sponsor_gifts: "gifts",
8397
+ show_bits: "bits",
8398
+ show_donations: "donation",
8399
+ show_pledges: "donation",
8400
+ // Patreon
8401
+ show_eldonations: "donation",
8402
+ show_tiltifydonations: "donation",
8403
+ show_donordrivedonations: "donation",
8404
+ show_justgivingdonations: "donation",
8405
+ show_streamlabscharitydonations: "donation",
8406
+ show_twitchcharitydonations: "donation",
8407
+ show_twitch_charity: "donation",
8408
+ show_treats: "donation",
8409
+ // TreatStream
8410
+ show_merch: "purchases",
8411
+ show_raids: "raids",
8412
+ show_hosts: "raids",
8413
+ show_redemptions: "redemption",
8414
+ show_fanfundings: "superchats",
8415
+ // YouTube Super Chat
8416
+ show_super_stickers: "superstickers",
8417
+ show_jewel_gifts: "stars"
8418
+ // TikTok-flavored, closest analogue
8419
+ };
8420
+ function buildFilters(settings) {
8421
+ const drop = /* @__PURE__ */ new Set();
8422
+ for (const [slFlag, lumiaCategory] of Object.entries(SL_SHOW_FLAG_TO_LUMIA_FILTER)) {
8423
+ if (settings[slFlag] === false) {
8424
+ drop.add(lumiaCategory);
8425
+ }
8426
+ }
8427
+ return Array.from(drop);
8428
+ }
8429
+ function buildSLEventList(settings) {
8430
+ const requiresReview = settings.custom_enabled === true;
8431
+ const backgroundColor = settings.background_color ?? "transparent";
8432
+ const textColor = settings.text_color ?? "#FFFFFF";
8433
+ const textSize = pxToNumber(settings.text_size, 16);
8434
+ const fontFamily = settings.font_family ?? "Roboto";
8435
+ const maxEvents = typeof settings.max_events === "number" && settings.max_events > 0 ? settings.max_events : EVENTLIST_DEFAULT_MODULE.content.maxItemsToShow;
8436
+ const enterAnimation = settings.show_animation ?? "fadeIn";
8437
+ const exitAnimation = settings.hide_animation ?? "fadeOut";
8438
+ const animationDuration = typeof settings.animation_speed === "number" ? settings.animation_speed : 1e3;
8439
+ const filters = buildFilters(settings);
8440
+ const { layer, module } = buildModuleEnvelope({
8441
+ type: "eventlist",
8442
+ title: "Streamlabs Event List",
8443
+ bounds: { x: 100, y: 100, width: 375, height: 470 },
8444
+ version: EVENTLIST_DEFAULT_MODULE.version,
8445
+ css: {
8446
+ ...EVENTLIST_DEFAULT_MODULE.css,
8447
+ background: backgroundColor,
8448
+ color: textColor,
8449
+ fontSize: textSize,
8450
+ fontFamily
8451
+ },
8452
+ content: {
8453
+ // Spread canonical defaults FIRST so the renderer's dereferences
8454
+ // don't crash on missing nested objects.
8455
+ ...EVENTLIST_DEFAULT_MODULE.content,
8456
+ maxItemsToShow: maxEvents,
8457
+ // SL's `flip_y` corresponds to Lumia's `reverse` (newest item
8458
+ // at the bottom vs the top). SL's `flip_x` flips the alignment
8459
+ // to right; Lumia covers that via css.textAlign but doesn't
8460
+ // have a per-module horizontal flip — stash as importMeta for
8461
+ // the review step.
8462
+ reverse: settings.flip_y === true,
8463
+ // SL's `keep_history: false` means events disappear after the
8464
+ // fade time; map to `fadeOutAfterDelay: true` with the fade
8465
+ // time SL configured.
8466
+ fadeOutAfterDelay: settings.keep_history === false,
8467
+ fadeOutDelayTime: typeof settings.fade_time === "number" && settings.fade_time > 0 ? Math.max(1, Math.round(settings.fade_time / 1e3)) : EVENTLIST_DEFAULT_MODULE.content.fadeOutDelayTime,
8468
+ animations: {
8469
+ ...EVENTLIST_DEFAULT_MODULE.content.animations,
8470
+ enterAnimation,
8471
+ exitAnimation,
8472
+ enterAnimationDuration: animationDuration,
8473
+ exitAnimationDuration: animationDuration
8474
+ },
8475
+ filters,
8476
+ importMeta: {
8477
+ source: "streamlabs",
8478
+ widgetType: "event_list",
8479
+ importedAt: (/* @__PURE__ */ new Date()).toISOString(),
8480
+ slTheme: settings.theme ?? null,
8481
+ slThemeColor: settings.theme_color ?? null,
8482
+ slFlipX: settings.flip_x === true,
8483
+ slHadCustomHtml: settings.custom_enabled === true,
8484
+ // Per-event-type minimums SL filters by — Lumia's eventlist
8485
+ // has no equivalent thresholding; preserve so the streamer
8486
+ // can recreate via the alert module's variation conditions.
8487
+ slHostViewerMinimum: settings.host_viewer_minimum ?? 0,
8488
+ slBitsMinimum: settings.bits_minimum ?? 0,
8489
+ slRaidRaiderMinimum: settings.raid_raider_minimum ?? 0
8490
+ }
8491
+ }
8492
+ });
8493
+ return { layer, module, requiresReview };
8124
8494
  }
8125
8495
 
8126
8496
  // src/sl-import/sl-dispatch.ts
8127
- var IMPORT_META_KEY = "importMeta";
8497
+ function buildSLDescription(widgetType, sourceUrl) {
8498
+ return buildImportDescription(`Imported from Streamlabs ${widgetType}`, sourceUrl);
8499
+ }
8128
8500
  var GOAL_WIDGET_TYPES = /* @__PURE__ */ new Set([
8129
8501
  "follower_goal",
8130
8502
  "sub_goal",
@@ -8132,8 +8504,6 @@ var GOAL_WIDGET_TYPES = /* @__PURE__ */ new Set([
8132
8504
  "bit_goal"
8133
8505
  ]);
8134
8506
  var PLACEHOLDER_WIDGET_TYPES = /* @__PURE__ */ new Set([
8135
- "chat_box",
8136
- "event_list",
8137
8507
  "donation_ticker",
8138
8508
  "stream_boss",
8139
8509
  "credits",
@@ -8143,8 +8513,6 @@ var PLACEHOLDER_WIDGET_TYPES = /* @__PURE__ */ new Set([
8143
8513
  "streamlabels"
8144
8514
  ]);
8145
8515
  var PLACEHOLDER_REASONS = {
8146
- chat_box: "Lumia has a native chatbox module \u2014 set it up directly rather than importing the SL config.",
8147
- event_list: "Lumia has a native eventlist module \u2014 set it up directly rather than importing the SL config.",
8148
8516
  donation_ticker: "No native Lumia equivalent yet \u2014 imported as a placeholder text layer.",
8149
8517
  stream_boss: "Lumia has a native streamboss module \u2014 set it up directly rather than importing the SL config.",
8150
8518
  credits: "Lumia has a native credits module \u2014 set it up directly rather than importing the SL config.",
@@ -8153,19 +8521,14 @@ var PLACEHOLDER_REASONS = {
8153
8521
  sub_train: "No native Lumia equivalent yet \u2014 imported as a placeholder text layer.",
8154
8522
  streamlabels: "Use Lumia template variables (e.g. {{twitch_last_follower}}) inside a Text module instead of importing the SL streamlabels config."
8155
8523
  };
8524
+ var CUSTOM_HTML_REVIEW_REASON = "The Streamlabs widget used custom HTML/CSS/JS that Lumia cannot host. The native Lumia module is the right starting point \u2014 recreate the styling there. The original SL settings are stashed under module.content.importMeta for reference.";
8156
8525
  function stampImportMeta(module, widgetType, role, extra = {}) {
8157
- const existing = module.content?.[IMPORT_META_KEY] ?? {};
8158
- module.content = {
8159
- ...module.content ?? {},
8160
- [IMPORT_META_KEY]: {
8161
- source: "streamlabs",
8162
- widgetType,
8163
- role,
8164
- importedAt: (/* @__PURE__ */ new Date()).toISOString(),
8165
- ...existing,
8166
- ...extra
8167
- }
8168
- };
8526
+ stampImportMetaMerge(module, {
8527
+ source: "streamlabs",
8528
+ widgetType,
8529
+ role,
8530
+ ...extra
8531
+ });
8169
8532
  }
8170
8533
  function buildPlaceholder(widgetType) {
8171
8534
  const id = nanoid7();
@@ -8197,7 +8560,7 @@ function buildPlaceholder(widgetType) {
8197
8560
  };
8198
8561
  return { layer, module };
8199
8562
  }
8200
- function importStreamlabsWidget(response, widgetType) {
8563
+ function importStreamlabsWidget(response, widgetType, options = {}) {
8201
8564
  if (!isSLWidgetConfigResponse(response)) {
8202
8565
  throw new Error(
8203
8566
  "Streamlabs response is missing the expected `settings` envelope. The widget may be private or the response format may have changed."
@@ -8247,9 +8610,61 @@ function importStreamlabsWidget(response, widgetType) {
8247
8610
  coverage.mappings.push({
8248
8611
  seType: `streamlabs:${widgetType}`,
8249
8612
  lumiaType: "goal",
8250
- status: "direct",
8613
+ status: built.requiresReview ? "partial" : "direct",
8251
8614
  count: 1
8252
8615
  });
8616
+ if (built.requiresReview) {
8617
+ reviewItems.push(
8618
+ customHtmlReviewItem(
8619
+ built.module.id,
8620
+ `streamlabs-${widgetType}`,
8621
+ response.settings,
8622
+ CUSTOM_HTML_REVIEW_REASON
8623
+ )
8624
+ );
8625
+ }
8626
+ } else if (widgetType === "chat_box") {
8627
+ const built = buildSLChatBox(response.settings);
8628
+ stampImportMeta(built.module, widgetType, "chatbox");
8629
+ layers.push(built.layer);
8630
+ modules[built.layer.id] = built.module;
8631
+ coverage.mappings.push({
8632
+ seType: "streamlabs:chat_box",
8633
+ lumiaType: "chatbox",
8634
+ status: built.requiresReview ? "partial" : "direct",
8635
+ count: 1
8636
+ });
8637
+ if (built.requiresReview) {
8638
+ reviewItems.push(
8639
+ customHtmlReviewItem(
8640
+ built.module.id,
8641
+ "streamlabs-chat_box",
8642
+ response.settings,
8643
+ CUSTOM_HTML_REVIEW_REASON
8644
+ )
8645
+ );
8646
+ }
8647
+ } else if (widgetType === "event_list") {
8648
+ const built = buildSLEventList(response.settings);
8649
+ stampImportMeta(built.module, widgetType, "eventlist");
8650
+ layers.push(built.layer);
8651
+ modules[built.layer.id] = built.module;
8652
+ coverage.mappings.push({
8653
+ seType: "streamlabs:event_list",
8654
+ lumiaType: "eventlist",
8655
+ status: built.requiresReview ? "partial" : "direct",
8656
+ count: 1
8657
+ });
8658
+ if (built.requiresReview) {
8659
+ reviewItems.push(
8660
+ customHtmlReviewItem(
8661
+ built.module.id,
8662
+ "streamlabs-event_list",
8663
+ response.settings,
8664
+ CUSTOM_HTML_REVIEW_REASON
8665
+ )
8666
+ );
8667
+ }
8253
8668
  } else if (PLACEHOLDER_WIDGET_TYPES.has(widgetType)) {
8254
8669
  const placeholder = buildPlaceholder(widgetType);
8255
8670
  const reason = PLACEHOLDER_REASONS[widgetType] ?? "No native Lumia mapping yet.";
@@ -8298,7 +8713,7 @@ function importStreamlabsWidget(response, widgetType) {
8298
8713
  uuid: "",
8299
8714
  listen_id: "",
8300
8715
  name: overlayName,
8301
- description: `Imported from Streamlabs ${widgetType}`,
8716
+ description: buildSLDescription(widgetType, options.sourceUrl),
8302
8717
  settings: {
8303
8718
  metadata: { width: 1920, height: 1080 },
8304
8719
  layers,
@@ -9860,7 +10275,12 @@ function SEImportWizard({
9860
10275
  );
9861
10276
  }
9862
10277
  importedOverlays.push({
9863
- result: importSEBootstrap(bootstrap),
10278
+ result: importSEBootstrap(bootstrap, {
10279
+ // JWT-flow imports don't come from a pasted URL; synthesize
10280
+ // the SE dashboard URL so the description still carries a
10281
+ // useful traceback. Same shape SE editor links use.
10282
+ sourceUrl: `https://streamelements.com/dashboard/overlays/${overlayId}/editor`
10283
+ }),
9864
10284
  summary
9865
10285
  });
9866
10286
  }
@@ -9905,7 +10325,9 @@ function SEImportWizard({
9905
10325
  "StreamElements returned a payload that doesn't look like an overlay bootstrap."
9906
10326
  );
9907
10327
  }
9908
- const imported = importSEBootstrap(bootstrap);
10328
+ const imported = importSEBootstrap(bootstrap, {
10329
+ sourceUrl: `https://streamelements.com/dashboard/overlays/${overlayId}/editor`
10330
+ });
9909
10331
  setBatchImports([{ result: imported, summary }]);
9910
10332
  setResult(imported);
9911
10333
  setAssetUrls(findSEAssetURLs(imported.overlay));
@@ -9993,7 +10415,9 @@ function SEImportWizard({
9993
10415
  "Streamlabs returned a response that does not look like a widget config payload."
9994
10416
  );
9995
10417
  }
9996
- const imported = importStreamlabsWidget(raw, slParts.widgetType);
10418
+ const imported = importStreamlabsWidget(raw, slParts.widgetType, {
10419
+ sourceUrl: url
10420
+ });
9997
10421
  setBatchImports([]);
9998
10422
  setResult(imported);
9999
10423
  setAssetUrls(findSEAssetURLs(imported.overlay));
@@ -10034,7 +10458,7 @@ function SEImportWizard({
10034
10458
  "StreamElements returned a response that does not look like an Element widget payload."
10035
10459
  );
10036
10460
  }
10037
- const imported = importElementOverlay(raw);
10461
+ const imported = importElementOverlay(raw, { sourceUrl: url });
10038
10462
  setBatchImports([]);
10039
10463
  setResult(imported);
10040
10464
  setAssetUrls(findSEAssetURLs(imported.overlay));
@@ -10082,7 +10506,7 @@ function SEImportWizard({
10082
10506
  ...parts,
10083
10507
  proxyFetch: bindings.proxyFetch
10084
10508
  });
10085
- const imported = importSEBootstrap(bootstrap);
10509
+ const imported = importSEBootstrap(bootstrap, { sourceUrl: url });
10086
10510
  setBatchImports([]);
10087
10511
  setResult(imported);
10088
10512
  setAssetUrls(findSEAssetURLs(imported.overlay));
@@ -10741,7 +11165,7 @@ function extractErrorMessage(err) {
10741
11165
  }
10742
11166
 
10743
11167
  // src/se-import/element-dispatch.ts
10744
- import { nanoid as nanoid9 } from "nanoid";
11168
+ import { LumiaAlertValues as LumiaAlertValues3 } from "@lumiastream/lumia-types";
10745
11169
 
10746
11170
  // src/se-import/element-types.ts
10747
11171
  function isSEElementResponse(value) {
@@ -10985,34 +11409,8 @@ function translateAnimationName(name, fallback) {
10985
11409
  if (PINNED_ANIMATION_MAP[name]) return PINNED_ANIMATION_MAP[name];
10986
11410
  return name.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
10987
11411
  }
10988
- function parseAnimationDurationMs(raw, fallbackMs) {
10989
- if (typeof raw === "number" && Number.isFinite(raw)) return raw;
10990
- if (typeof raw !== "string") return fallbackMs;
10991
- const trimmed = raw.trim();
10992
- if (!trimmed) return fallbackMs;
10993
- if (trimmed.endsWith("ms")) {
10994
- const n2 = parseFloat(trimmed.slice(0, -2));
10995
- return Number.isFinite(n2) ? n2 : fallbackMs;
10996
- }
10997
- if (trimmed.endsWith("s")) {
10998
- const n2 = parseFloat(trimmed.slice(0, -1));
10999
- return Number.isFinite(n2) ? n2 * 1e3 : fallbackMs;
11000
- }
11001
- const n = parseFloat(trimmed);
11002
- return Number.isFinite(n) ? n : fallbackMs;
11003
- }
11004
- function parsePx2(raw) {
11005
- if (typeof raw === "number" && Number.isFinite(raw)) return raw;
11006
- if (typeof raw !== "string") return NaN;
11007
- const trimmed = raw.trim();
11008
- if (!trimmed) return NaN;
11009
- if (trimmed.endsWith("px")) {
11010
- const n2 = parseFloat(trimmed.slice(0, -2));
11011
- return Number.isFinite(n2) ? n2 : NaN;
11012
- }
11013
- const n = parseFloat(trimmed);
11014
- return Number.isFinite(n) ? n : NaN;
11015
- }
11412
+ var parseAnimationDurationMs = parseDurationMs;
11413
+ var parsePx2 = (raw) => pxToNumber(raw, NaN);
11016
11414
  function findFieldByType(fields, type) {
11017
11415
  if (!fields) return void 0;
11018
11416
  for (const f of Object.values(fields)) {
@@ -11088,6 +11486,47 @@ function buildAnimations(title) {
11088
11486
  const visibleDurationMs = enterDelay + enterDuration + staticDuration + exitDuration;
11089
11487
  return { animations, visibleDurationMs: visibleDurationMs || DEFAULT_DURATION_MS };
11090
11488
  }
11489
+ function resolveFieldBoundsInCanvas(style, canvas, textContent) {
11490
+ if (!style) return null;
11491
+ const x = parsePx2(style.left);
11492
+ const y = parsePx2(style.top);
11493
+ const hasXY = Number.isFinite(x) || Number.isFinite(y);
11494
+ if (!hasXY && style.width == null && style.height == null) return null;
11495
+ const sx = Number.isFinite(x) ? x : 0;
11496
+ const sy = Number.isFinite(y) ? y : 0;
11497
+ let width = parsePx2(style.width);
11498
+ let height = parsePx2(style.height);
11499
+ if (!Number.isFinite(width)) width = Math.max(1, canvas.width - sx);
11500
+ if (!Number.isFinite(height)) {
11501
+ if (typeof textContent === "string") {
11502
+ const lineCount = (textContent.match(/\n/g)?.length ?? 0) + 1;
11503
+ const lhPx = parsePx2(style.lineHeight) || parsePx2(style["line-height"]);
11504
+ const fsPx = parsePx2(style.fontSize);
11505
+ let effectiveLh = lhPx;
11506
+ if (!Number.isFinite(effectiveLh) || effectiveLh < 8) {
11507
+ effectiveLh = Number.isFinite(fsPx) ? fsPx * 1.3 : 24 * 1.3;
11508
+ }
11509
+ height = Math.max(1, lineCount * effectiveLh);
11510
+ } else {
11511
+ height = Math.max(1, canvas.height - sy);
11512
+ }
11513
+ }
11514
+ return { x: sx, y: sy, width, height };
11515
+ }
11516
+ function unionBounds(boxes) {
11517
+ if (boxes.length === 0) return null;
11518
+ let minX = Infinity;
11519
+ let minY = Infinity;
11520
+ let maxX = -Infinity;
11521
+ let maxY = -Infinity;
11522
+ for (const b of boxes) {
11523
+ minX = Math.min(minX, b.x);
11524
+ minY = Math.min(minY, b.y);
11525
+ maxX = Math.max(maxX, b.x + b.width);
11526
+ maxY = Math.max(maxY, b.y + b.height);
11527
+ }
11528
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
11529
+ }
11091
11530
  function buildElementAlertEvent(state, options = {}) {
11092
11531
  const compositeFields = state.settings?.compositeFields;
11093
11532
  const titleField = findFieldByType(compositeFields, "text");
@@ -11105,6 +11544,31 @@ function buildElementAlertEvent(state, options = {}) {
11105
11544
  const titleStyle = titleField?.style;
11106
11545
  const highlightColor = titleField?.highlightedStyle?.color ?? null;
11107
11546
  const { animations, visibleDurationMs } = buildAnimations(titleField);
11547
+ const canvas = options.canvas ?? { width: 1920, height: 1080 };
11548
+ const mediaBounds = resolveFieldBoundsInCanvas(mainAsset?.style, canvas);
11549
+ const textBounds = resolveFieldBoundsInCanvas(titleStyle, canvas, rawMessage);
11550
+ const collected = [];
11551
+ if (mediaBounds) collected.push(mediaBounds);
11552
+ if (textBounds) collected.push(textBounds);
11553
+ let layerBounds = unionBounds(collected);
11554
+ if (!layerBounds) {
11555
+ layerBounds = {
11556
+ x: Math.round(canvas.width * 0.6),
11557
+ y: Math.round(canvas.height * 0.15),
11558
+ width: 400,
11559
+ height: 300
11560
+ };
11561
+ }
11562
+ let pushTextUp = 0;
11563
+ let pushTextLeft = 0;
11564
+ if (textBounds) {
11565
+ const desiredTextTopInLayer = textBounds.y - layerBounds.y;
11566
+ const naturalTextTopInLayer = Math.max(0, (layerBounds.height - textBounds.height) / 2);
11567
+ pushTextUp = Math.round(desiredTextTopInLayer - naturalTextTopInLayer);
11568
+ const desiredTextLeftInLayer = textBounds.x - layerBounds.x;
11569
+ const naturalTextLeftInLayer = Math.max(0, (layerBounds.width - textBounds.width) / 2);
11570
+ pushTextLeft = Math.round(desiredTextLeftInLayer - naturalTextLeftInLayer);
11571
+ }
11108
11572
  const event = {
11109
11573
  on: true,
11110
11574
  // `'custom'` lowercase is load-bearing in Lumia's AlertBoxHandler —
@@ -11123,7 +11587,11 @@ function buildElementAlertEvent(state, options = {}) {
11123
11587
  showBrandIcon: true
11124
11588
  },
11125
11589
  settings: { duration: visibleDurationMs },
11126
- layout: { value: "column", css: {}, customCss: "", pushTextUp: 0, pushTextLeft: 0 },
11590
+ // `textOver` matches SE Element's text-on-video render pattern.
11591
+ // `imageOver` / `column` / `row` would all stack the text outside
11592
+ // the media, which is the bug this layout fixes (text rendered
11593
+ // below the alert graphic instead of overlaid on it).
11594
+ layout: { value: "textOver", css: {}, customCss: "", pushTextUp, pushTextLeft },
11127
11595
  text: {
11128
11596
  css: titleCss(titleStyle),
11129
11597
  messageCss: {},
@@ -11145,44 +11613,13 @@ function buildElementAlertEvent(state, options = {}) {
11145
11613
  },
11146
11614
  variations: []
11147
11615
  };
11148
- const style = mainAsset?.style;
11149
- let bounds;
11150
- if (style) {
11151
- const x = parsePx2(style.left);
11152
- const y = parsePx2(style.top);
11153
- const width = parsePx2(style.width);
11154
- const height = parsePx2(style.height);
11155
- if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(width)) {
11156
- bounds = {
11157
- x,
11158
- y,
11159
- width,
11160
- // Height isn't always present on video Main Assets (the video
11161
- // aspect-ratios itself from the source). Fall back to a
11162
- // reasonable default that matches SE's preview row.
11163
- height: Number.isFinite(height) ? height : Math.round(width * 0.75)
11164
- };
11165
- }
11166
- }
11167
- return { event, bounds };
11616
+ return { event, bounds: layerBounds };
11168
11617
  }
11169
11618
 
11170
11619
  // src/se-import/mappers/element-scene.ts
11171
- import { nanoid as nanoid8 } from "nanoid";
11172
11620
  var TEXT_FALLBACK = { width: 400, height: 50 };
11173
11621
  var IMAGE_FALLBACK = { width: 300, height: 100 };
11174
- function parsePx3(raw) {
11175
- if (typeof raw === "number" && Number.isFinite(raw)) return raw;
11176
- if (typeof raw !== "string") return NaN;
11177
- const trimmed = raw.trim();
11178
- if (!trimmed) return NaN;
11179
- if (trimmed.endsWith("px")) {
11180
- const n2 = parseFloat(trimmed.slice(0, -2));
11181
- return Number.isFinite(n2) ? n2 : NaN;
11182
- }
11183
- const n = parseFloat(trimmed);
11184
- return Number.isFinite(n) ? n : NaN;
11185
- }
11622
+ var parsePx3 = (raw) => pxToNumber(raw, NaN);
11186
11623
  function styleToBounds(style, type, canvas) {
11187
11624
  const x = Number.isFinite(parsePx3(style?.left)) ? parsePx3(style?.left) : 0;
11188
11625
  const y = Number.isFinite(parsePx3(style?.top)) ? parsePx3(style?.top) : 0;
@@ -11218,33 +11655,17 @@ function styleToBounds(style, type, canvas) {
11218
11655
  }
11219
11656
  return { x, y, width, height };
11220
11657
  }
11221
- function buildLayer(id, groupId, bounds, zIndex) {
11222
- return {
11223
- id,
11224
- group: groupId,
11225
- state: { visible: true, locked: false },
11226
- bounds: {
11227
- x: bounds.x,
11228
- y: bounds.y,
11229
- width: bounds.width,
11230
- height: bounds.height,
11231
- scale: [1, 1],
11232
- zIndex
11233
- }
11234
- };
11235
- }
11236
- function buildModule(id, type, title, content, css = {}) {
11237
- return {
11238
- id,
11239
- version: 1,
11240
- loaded: true,
11241
- settings: { type, title, locked: false },
11242
- lights: [],
11243
- css,
11658
+ function buildChild(groupId, zIndex, bounds, type, title, content, css = {}) {
11659
+ const { layer, module } = buildModuleEnvelope({
11660
+ type,
11661
+ title,
11662
+ bounds,
11244
11663
  content,
11245
- variables: {},
11246
- events: {}
11247
- };
11664
+ css,
11665
+ parentGroupId: groupId,
11666
+ zIndex
11667
+ });
11668
+ return { layer, module };
11248
11669
  }
11249
11670
  function textStyleToCss(style) {
11250
11671
  const s = style ?? {};
@@ -11259,80 +11680,69 @@ function textStyleToCss(style) {
11259
11680
  return out;
11260
11681
  }
11261
11682
  function mapCompositeField(name, field, groupId, zIndex, provider, canvas) {
11262
- const id = nanoid8();
11263
11683
  switch (field.type) {
11264
11684
  case "video": {
11265
- const bounds = styleToBounds(field.style, "video", canvas);
11266
- const layer = buildLayer(id, groupId, bounds, zIndex);
11267
11685
  const src = pickFieldMediaHref(field.media, "video/");
11268
11686
  const volume = typeof field.volume === "number" ? field.volume : 1;
11269
- const module = buildModule(id, "video", name, {
11270
- src,
11271
- volume,
11272
- loop: true,
11273
- autoplay: true,
11274
- muted: volume === 0
11275
- });
11276
- return { layer, module };
11687
+ return buildChild(
11688
+ groupId,
11689
+ zIndex,
11690
+ styleToBounds(field.style, "video", canvas),
11691
+ "video",
11692
+ name,
11693
+ { src, volume, loop: true, autoplay: true, muted: volume === 0 }
11694
+ );
11277
11695
  }
11278
11696
  case "image": {
11279
- const bounds = styleToBounds(field.style, "image", canvas);
11280
- const layer = buildLayer(id, groupId, bounds, zIndex);
11281
11697
  const src = pickFieldMediaHref(field.media, "image/");
11282
- const module = buildModule(id, "image", name, { src });
11283
- return { layer, module };
11698
+ return buildChild(
11699
+ groupId,
11700
+ zIndex,
11701
+ styleToBounds(field.style, "image", canvas),
11702
+ "image",
11703
+ name,
11704
+ { src }
11705
+ );
11284
11706
  }
11285
11707
  case "text": {
11286
- const bounds = styleToBounds(field.style, "text", canvas);
11287
- const layer = buildLayer(id, groupId, bounds, zIndex);
11288
11708
  const raw = field.text?.[0]?.content ?? "";
11289
11709
  const value = substituteElementTokens(raw, { provider });
11290
- const module = buildModule(
11291
- id,
11710
+ return buildChild(
11711
+ groupId,
11712
+ zIndex,
11713
+ styleToBounds(field.style, "text", canvas),
11292
11714
  "text",
11293
11715
  name,
11294
11716
  { value, highlightColor: "inherit" },
11295
11717
  textStyleToCss(field.style)
11296
11718
  );
11297
- return { layer, module };
11298
11719
  }
11299
11720
  case "audio": {
11300
- const layer = buildLayer(id, groupId, { x: 0, y: 0, width: 1, height: 1 }, zIndex);
11301
11721
  const src = pickFieldMediaHref(field.media, "audio/");
11302
11722
  const volume = typeof field.volume === "number" ? field.volume : 0.5;
11303
- const module = buildModule(id, "audio", name, { src, volume, loop: false });
11304
- return { layer, module };
11723
+ return buildChild(
11724
+ groupId,
11725
+ zIndex,
11726
+ { x: 0, y: 0, width: 1, height: 1 },
11727
+ "audio",
11728
+ name,
11729
+ { src, volume, loop: false }
11730
+ );
11305
11731
  }
11306
11732
  default:
11307
11733
  return null;
11308
11734
  }
11309
11735
  }
11310
11736
  function buildElementScene(state, options) {
11311
- const groupId = nanoid8();
11312
11737
  const groupTitle = typeof state.displayName === "string" && state.displayName || state.referenceId || "Scene";
11313
- const groupLayer = {
11314
- id: groupId,
11738
+ const { id: groupId, layer: groupLayer, module: groupModule } = buildModuleEnvelope({
11315
11739
  type: "group",
11316
- group: null,
11317
- bounds: {
11318
- x: 0,
11319
- y: 0,
11320
- width: options.canvas.width,
11321
- height: options.canvas.height,
11322
- scale: [1, 1]
11323
- },
11324
- state: { visible: true, locked: false, expanded: false }
11325
- };
11326
- const groupModule = {
11327
- id: groupId,
11328
- loaded: true,
11329
- settings: { type: "group", title: groupTitle },
11330
- lights: [],
11331
- css: {},
11332
- content: {},
11333
- variables: {},
11334
- events: {}
11335
- };
11740
+ title: groupTitle,
11741
+ bounds: { x: 0, y: 0, width: options.canvas.width, height: options.canvas.height },
11742
+ type_layer: "group",
11743
+ parentGroupId: null,
11744
+ expanded: false
11745
+ });
11336
11746
  const children = [];
11337
11747
  const fields = state.settings?.compositeFields ?? {};
11338
11748
  let zIndex = 1;
@@ -11351,7 +11761,12 @@ function buildElementScene(state, options) {
11351
11761
  }
11352
11762
 
11353
11763
  // src/se-import/element-dispatch.ts
11354
- var IMPORT_META_KEY2 = "importMeta";
11764
+ function buildElementDescription(widgetInstanceId, sourceUrl) {
11765
+ return buildImportDescription(
11766
+ `Imported from StreamElements Element widget ${widgetInstanceId}`,
11767
+ sourceUrl
11768
+ );
11769
+ }
11355
11770
  function elementAlertKindToLumiaKeys(kind, provider) {
11356
11771
  switch (kind) {
11357
11772
  case "follower":
@@ -11370,9 +11785,9 @@ function elementAlertKindToLumiaKeys(kind, provider) {
11370
11785
  return alertBoxKeysFor("charityCampaignDonation", provider);
11371
11786
  case "subscriberGift":
11372
11787
  case "communityGiftPurchase": {
11373
- if (provider === "twitch") return ["twitch-giftSubscription"];
11374
- if (provider === "kick") return ["kick-subscriptionGift"];
11375
- if (provider === "youtube") return ["youtube-giftMembers"];
11788
+ if (provider === "twitch") return [LumiaAlertValues3.TWITCH_GIFT_SUBSCRIPTION];
11789
+ if (provider === "kick") return [LumiaAlertValues3.KICK_GIFT_SUBSCRIPTION];
11790
+ if (provider === "youtube") return [LumiaAlertValues3.YOUTUBE_GIFT_MEMBERS];
11376
11791
  return alertBoxKeysFor("subscriber", provider);
11377
11792
  }
11378
11793
  default:
@@ -11387,7 +11802,7 @@ function buildCombinedAlertModule(alertStates, provider, canvas) {
11387
11802
  for (const state of alertStates) {
11388
11803
  const cls = classifyConfigState(state);
11389
11804
  if (cls.kind !== "alert") continue;
11390
- const { event, bounds: bounds2 } = buildElementAlertEvent(state, { provider });
11805
+ const { event, bounds: bounds2 } = buildElementAlertEvent(state, { provider, canvas });
11391
11806
  const keys = elementAlertKindToLumiaKeys(cls.alertKind, provider);
11392
11807
  if (keys.length === 0) continue;
11393
11808
  for (const key of keys) {
@@ -11399,61 +11814,42 @@ function buildCombinedAlertModule(alertStates, provider, canvas) {
11399
11814
  }
11400
11815
  }
11401
11816
  if (Object.keys(events).length === 0) return null;
11402
- const id = nanoid9();
11403
11817
  const bounds = firstBounds ?? {
11404
11818
  x: Math.round(canvas.width * 0.6),
11405
11819
  y: Math.round(canvas.height * 0.15),
11406
11820
  width: 400,
11407
11821
  height: 300
11408
11822
  };
11409
- const layer = {
11410
- id,
11411
- state: { visible: true, locked: false },
11412
- bounds: {
11413
- x: bounds.x,
11414
- y: bounds.y,
11415
- width: bounds.width,
11416
- height: bounds.height,
11417
- scale: [1, 1]
11418
- }
11419
- };
11420
11823
  const baseAlert = firstEvent ? stripEventOnlyFields2(firstEvent) : {};
11421
- const module = {
11422
- id,
11423
- version: 1,
11424
- loaded: true,
11425
- settings: { type: "alert", title: "Alerts (Element import)", locked: false },
11426
- lights: [],
11427
- css: {},
11428
- content: {},
11429
- variables: {},
11430
- events: {},
11824
+ const { layer, module } = buildModuleEnvelope({
11825
+ type: "alert",
11826
+ title: "Alerts (Element import)",
11827
+ bounds,
11431
11828
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
11432
11829
  alert: { ...baseAlert, events }
11433
- };
11830
+ });
11434
11831
  return { layer, module };
11435
11832
  }
11436
11833
  function stripEventOnlyFields2(event) {
11437
- const { on: _on, variations: _variations, matchEmptyCondition: _mec, ...base } = event;
11834
+ const {
11835
+ on: _on,
11836
+ variations: _variations,
11837
+ matchEmptyCondition: _mec,
11838
+ ...base
11839
+ } = event;
11438
11840
  return base;
11439
11841
  }
11440
11842
  function stampImportMeta2(module, state, role) {
11441
- module.content = {
11442
- ...module.content ?? {},
11443
- [IMPORT_META_KEY2]: {
11444
- source: "streamelements-element",
11445
- role,
11446
- referenceId: state?.referenceId,
11447
- categoryId: state?.categoryId,
11448
- displayName: state?.displayName,
11449
- importedAt: (/* @__PURE__ */ new Date()).toISOString()
11450
- }
11451
- };
11843
+ stampImportMetaMerge(module, {
11844
+ source: "streamelements-element",
11845
+ role,
11846
+ referenceId: state?.referenceId,
11847
+ categoryId: state?.categoryId,
11848
+ displayName: state?.displayName
11849
+ });
11452
11850
  }
11453
- function importElementOverlay(response) {
11454
- const configData = parseElementConfigData(
11455
- response.widgetInstanceConfigVersion.configData
11456
- );
11851
+ function importElementOverlay(response, options = {}) {
11852
+ const configData = parseElementConfigData(response.widgetInstanceConfigVersion.configData);
11457
11853
  if (!configData) {
11458
11854
  throw new Error(
11459
11855
  "StreamElements Element response carries an invalid configData payload. The widget may be corrupted or the response format may have changed."
@@ -11523,7 +11919,10 @@ function importElementOverlay(response) {
11523
11919
  uuid: "",
11524
11920
  listen_id: "",
11525
11921
  name: response.widgetInstance.displayName ? `[SE Element] ${response.widgetInstance.displayName}` : `Imported from StreamElements Element (${response.widgetInstance.widgetInstanceId})`,
11526
- description: `Imported from StreamElements Element widget ${response.widgetInstance.widgetInstanceId}`,
11922
+ description: buildElementDescription(
11923
+ response.widgetInstance.widgetInstanceId,
11924
+ options.sourceUrl
11925
+ ),
11527
11926
  settings: {
11528
11927
  metadata: { width: canvas.width, height: canvas.height },
11529
11928
  layers,
@@ -11537,7 +11936,7 @@ function importElementOverlay(response) {
11537
11936
  }
11538
11937
 
11539
11938
  // src/se-import/index.ts
11540
- var IMPORT_META_KEY3 = "importMeta";
11939
+ var IMPORT_META_KEY2 = "importMeta";
11541
11940
  var REVIEW_REASONS = {
11542
11941
  "se-widget-bit-boss": "No native Lumia equivalent \u2014 bit boss fight bar with HP/damage states.",
11543
11942
  // `se-widget-hype-cup` intentionally omitted — it now routes to the native
@@ -11567,6 +11966,10 @@ function reasonFor(seType, status, flaggedOff) {
11567
11966
  if (flaggedOff && FLAG_OFF_REASONS[seType]) return FLAG_OFF_REASONS[seType];
11568
11967
  return REVIEW_REASONS[seType] ?? (status === "template" ? "Imported as a generic template stub." : "No native mapping yet \u2014 imported as a labelled text placeholder.");
11569
11968
  }
11969
+ function buildSEBootstrapDescription(overlayId, sourceUrl) {
11970
+ const head = overlayId ? `Imported from StreamElements overlay ${overlayId}` : "Imported from StreamElements";
11971
+ return buildImportDescription(head, sourceUrl);
11972
+ }
11570
11973
  function extractSEOverlayId(input) {
11571
11974
  const trimmed = input.trim();
11572
11975
  const m = trimmed.match(/[0-9a-f]{24}/i);
@@ -11621,7 +12024,7 @@ function isSEBootstrap(value) {
11621
12024
  const o = v.overlay;
11622
12025
  return Array.isArray(o.widgets);
11623
12026
  }
11624
- function importSEBootstrap(bootstrap) {
12027
+ function importSEBootstrap(bootstrap, options = {}) {
11625
12028
  const seOverlay = bootstrap.overlay;
11626
12029
  const widgets = seOverlay?.widgets ?? [];
11627
12030
  const coverage = {
@@ -11644,7 +12047,7 @@ function importSEBootstrap(bootstrap) {
11644
12047
  if (w.type !== "se-widget-group") continue;
11645
12048
  const uid = w.variables?.uid;
11646
12049
  if (typeof uid !== "string") continue;
11647
- const lumiaGroupId = nanoid10();
12050
+ const lumiaGroupId = nanoid8();
11648
12051
  seUidToLumiaGroupId[uid] = lumiaGroupId;
11649
12052
  groupWidgetByUid[uid] = w;
11650
12053
  }
@@ -11683,7 +12086,7 @@ function importSEBootstrap(bootstrap) {
11683
12086
  lights: [],
11684
12087
  css: {},
11685
12088
  content: {
11686
- [IMPORT_META_KEY3]: {
12089
+ [IMPORT_META_KEY2]: {
11687
12090
  source: "streamelements",
11688
12091
  widgetType: w.type,
11689
12092
  importedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -11721,7 +12124,7 @@ function importSEBootstrap(bootstrap) {
11721
12124
  if (result.status === "placeholder" || result.status === "template") {
11722
12125
  module.content = {
11723
12126
  ...module.content ?? {},
11724
- [IMPORT_META_KEY3]: {
12127
+ [IMPORT_META_KEY2]: {
11725
12128
  source: "streamelements",
11726
12129
  widgetType: w.type,
11727
12130
  importedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -11761,7 +12164,7 @@ function importSEBootstrap(bootstrap) {
11761
12164
  uuid: "",
11762
12165
  listen_id: "",
11763
12166
  name: seOverlay?.name ? `[SE] ${seOverlay.name}` : `Imported from StreamElements (${seOverlay?._id ?? ""})`,
11764
- description: seOverlay?._id ? `Imported from StreamElements overlay ${seOverlay._id}` : "Imported from StreamElements",
12167
+ description: buildSEBootstrapDescription(seOverlay?._id, options.sourceUrl),
11765
12168
  settings: {
11766
12169
  metadata: {
11767
12170
  width: seOverlay?.settings?.width ?? 1920,
@@ -11809,7 +12212,7 @@ function applyReviewAction(result, moduleId, action, payload) {
11809
12212
  bounds: existingLayer.bounds,
11810
12213
  state: existingLayer.state
11811
12214
  };
11812
- const importMeta2 = existing2.content?.[IMPORT_META_KEY3];
12215
+ const importMeta2 = existing2.content?.[IMPORT_META_KEY2];
11813
12216
  const mergedLights = (existing2.lights?.length ?? 0) > 0 ? existing2.lights : transplant.module.lights;
11814
12217
  const mergedEvents = existing2.events && Object.keys(existing2.events).length > 0 ? { ...transplant.module.events, ...existing2.events } : transplant.module.events;
11815
12218
  const mergedVariables = existing2.variables && Object.keys(existing2.variables).length > 0 ? { ...transplant.module.variables, ...existing2.variables } : transplant.module.variables;
@@ -11821,7 +12224,7 @@ function applyReviewAction(result, moduleId, action, payload) {
11821
12224
  variables: mergedVariables,
11822
12225
  content: {
11823
12226
  ...transplant.module.content ?? {},
11824
- [IMPORT_META_KEY3]: importMeta2
12227
+ [IMPORT_META_KEY2]: importMeta2
11825
12228
  }
11826
12229
  };
11827
12230
  const rootOldNewId = transplant.layer.id;
@@ -11849,7 +12252,7 @@ function applyReviewAction(result, moduleId, action, payload) {
11849
12252
  const existing = modules[moduleId];
11850
12253
  const generated = payload;
11851
12254
  if (!existing || !generated) return result;
11852
- const importMeta = existing.content?.[IMPORT_META_KEY3];
12255
+ const importMeta = existing.content?.[IMPORT_META_KEY2];
11853
12256
  const replaced = {
11854
12257
  ...existing,
11855
12258
  settings: { ...existing.settings ?? {}, type: "custom" },
@@ -11863,7 +12266,7 @@ function applyReviewAction(result, moduleId, action, payload) {
11863
12266
  data: parseLooseJson(generated.data) ?? {},
11864
12267
  configs: parseLooseJson(generated.configs) ?? [],
11865
12268
  flavor: "ai-generated",
11866
- [IMPORT_META_KEY3]: importMeta
12269
+ [IMPORT_META_KEY2]: importMeta
11867
12270
  }
11868
12271
  };
11869
12272
  modules[moduleId] = replaced;