Skip to content

Backport DataList, DataListItem and other improvements from svelte5 to main#3

Open
Copilot wants to merge 8 commits intomainfrom
copilot/backport-datalist-dataitem
Open

Backport DataList, DataListItem and other improvements from svelte5 to main#3
Copilot wants to merge 8 commits intomainfrom
copilot/backport-datalist-dataitem

Conversation

Copy link
Copy Markdown

Copilot AI commented Feb 28, 2026

Backports components and improvements from the svelte5 branch to main (svelte3/4), converting Svelte 5 syntax ($props, $derived, $state, snippets) to Svelte 3/4 equivalents (export let, $:, slots).

New components

  • DataList — Container <ul> with CSS variable theming for icon sizes, action layouts, borders, selection colors, and container-query responsive stacking
  • DataListItem — Grid-based <li> row with start/end named slots, interactive and selectable modes, keyboard activation, and nested control click isolation
  • Form / Fieldset — Styled wrappers integrating with FormProvider for cascading form layout defaults
  • FormProvider — Svelte store-based context provider for form defaults (layout, fullWidth, globalInputStyles, multiline)
  • Option — Rich HTML <option> wrapper using data-html attribute for custom dropdown rendering in Select
  • types.ts — CSS variable prop interfaces (BaseStyleProps, ContainerStyleProps, etc.)

Enhanced existing components

  • FormItem — Layout modes (side/above/below), multiline prop, context defaults from FormProvider, grid CSS for above/below layouts
  • Select — Reads data-html from options, change event bubbles, focus styles on dropdown buttons

SCSS / theme fixes

  • _color.scss — Re-defines --fg/--bg so nested components inherit correctly
  • _box.scss — New box-size-props mixin
  • _typography.scss — Heading colors use explicit --heading-fg/--heading-bg fallbacks
  • _affordances.scss — Hover brightness 1.41.05
  • Dark theme secondary colors fixed for dark backgrounds

Usage

<DataList maxWidth="800px" iconSize="3rem" iconBorderRadius="50%">
  <DataListItem selectable bind:checked={selected}>
    <img slot="start" src="avatar.jpg" alt="" />
    <h4>Title</h4>
    <p>Description text</p>
    <span slot="end">
      <Button>Action</Button>
    </span>
  </DataListItem>
</DataList>

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 3 commits February 28, 2026 02:57
Co-authored-by: thinkle-iacs <15049315+thinkle-iacs@users.noreply.github.com>
New components (Svelte 3/4 syntax):
- FormProvider: context provider for form defaults
- Form: styled form wrapper with FormProvider
- Fieldset: styled fieldset with legend support
- Option: rich HTML option wrapper for Select

Improvements backported:
- FormItem: layout modes (above/below/side), multiline, context defaults
- SCSS: box-size-props mixin, color inheritance, heading colors, hover brightness
- Select: data-html support, event bubbling fix, focus styles
- Types: CSS variable prop type definitions
- Dark theme: secondary color fix
- Affordances: scrollbar-track-bg variable

Co-authored-by: thinkle-iacs <15049315+thinkle-iacs@users.noreply.github.com>
Use get() from svelte/store instead of subscribe/unsubscribe pattern
for synchronous store value reading.

Co-authored-by: thinkle-iacs <15049315+thinkle-iacs@users.noreply.github.com>
Copilot AI changed the title [WIP] Backport DataList and DataItem components to svelte3/4 Backport new components and improvements from svelte5 to main (svelte3/4) Feb 28, 2026
@thinkle-iacs thinkle-iacs marked this pull request as ready for review February 28, 2026 10:55
Copilot AI review requested due to automatic review settings February 28, 2026 10:55
@thinkle-iacs
Copy link
Copy Markdown
Collaborator

Hey -- I fixed a package issue in this branch for you (existing problem).

Bigger issue: you didn't backport DataList and DataListItem. Please add those!

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Backports UI components and styling improvements from the svelte5 branch to the Svelte 3/4 codepath, adding form-related primitives and updating theming/CSS variable behavior.

Changes:

  • Add new layout/components: FormProvider, Form, Fieldset, and control Option (for rich select options), plus exported TS style-prop interfaces.
  • Enhance FormItem with layout modes (side|above|below), context-driven defaults, and grid-based layout styles.
  • Update theming/styling utilities and SCSS (dark theme secondary colors, affordances vars, typography heading vars, color/box mixins) and improve Select rich option rendering + bubbled change events.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/lib/vars/themes/dark.css Adjust dark theme secondary fg/bg variables for better contrast.
src/lib/vars/affordances.css Add scrollbar track background custom property.
src/lib/util.ts Broaden injectVars prop typing.
src/lib/types.ts Add exported TS interfaces for common CSS-variable props.
src/lib/sass/_typography.scss Update heading color/background variable fallback behavior.
src/lib/sass/_color.scss Modify color-props mixin to re-define --fg/--bg for nesting.
src/lib/sass/_box.scss Add box-size-props mixin for width/height/min/max controls.
src/lib/sass/_affordances.scss Reduce default hover brightness.
src/lib/layout/FormProvider.svelte New context provider for form defaults (layout/fullWidth/etc.).
src/lib/layout/FormItem.svelte Add context defaults + new layout modes and grid CSS.
src/lib/layout/Form.svelte New styled <form> wrapper with FormProvider integration.
src/lib/layout/Fieldset.svelte New styled <fieldset> wrapper with legend slot and FormProvider integration.
src/lib/index.ts Export new components and style-prop types.
src/lib/controls/Select.svelte Support rich option HTML via data-html, bubble change events, add focus styles.
src/lib/controls/Option.svelte New helper component to populate <option data-html> from slotted markup.
package-lock.json Update lockfile (version bump and Svelte version alignment).
Comments suppressed due to low confidence (1)

src/lib/util.ts:13

  • Changing injectVars to accept { [key: string]: any } hides type errors and can generate invalid CSS when non-string values are passed (e.g. booleans become true/false, objects become [object Object]). Consider narrowing this to a safe union (string/number/null/undefined) and/or coercing/ignoring unsupported types before interpolating into the style string to avoid producing invalid custom property values.
export function injectVars(
  props: { [key: string]: any },
  prefix: string,
  varList: string[]
) {
  let cssVars = "";
  varList.forEach((v) => {
    if (props[v]) {
      const cssVarName = toKebabCase(v);
      cssVars += `--${prefix}-${cssVarName}: ${props[v]};`;
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +38 to +44
<option data-html={htmlContent} {value} {...$$restProps}>
{textContent}
</option>

<div bind:this={template} style="display: none;" class="option-template hidden">
<slot />
</div>
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Option currently renders both an <option> and a hidden <div>. When used inside a native <select>, the extra <div> is invalid HTML (a <select>'s content model only allows <option>, <optgroup>, and optionally <script>/<template>), and browsers may move/drop the element, breaking measurement/extraction and potentially the select itself. Consider using a <template> element for the hidden markup (allowed inside <select>) or extracting the HTML in another way that doesn't introduce non-option children.

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +9
--fg: #{fn.var-no-fallbacks(--fg, $prefixes...)};
--bg: #{fn.var-no-fallbacks(--bg, $prefixes...)};
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

color-props now assigns --fg/--bg using fn.var-no-fallbacks(--fg/--bg, ...), which expands to a value that ultimately references var(--fg, unset) / var(--bg, unset). Setting a custom property in terms of itself creates a cyclic dependency and makes the variable invalid at computed-value time, which can break downstream var(--fg)/var(--bg) usage. To achieve inheritance for nested components, set --fg/--bg from prefixed variables without referencing the same property name (e.g. fall back to inherit/unset, or use a helper that doesn't include the base --fg/--bg in the chain).

Suggested change
--fg: #{fn.var-no-fallbacks(--fg, $prefixes...)};
--bg: #{fn.var-no-fallbacks(--bg, $prefixes...)};
--fg: #{fn.vars(fg, $prefixes...)};
--bg: #{fn.vars(bg, $prefixes...)};

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +16
export let padding: string | undefined = undefined;
export let border: string | boolean | undefined = undefined;
export let borderRadius: string | undefined = undefined;
export let margin: string | undefined = undefined;
export let maxWidth: string | undefined = undefined;
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

border is typed as string | boolean | undefined and then fed into injectVars as a CSS custom property. If a consumer passes border={true}, injectVars will emit --form-border: true; which is not a valid CSS border value and can cause border: var(--form-border, ...) to compute unexpectedly. Prefer making border a string CSS value only, or use a separate boolean like bordered/showBorder to switch to a default border style.

Copilot uses AI. Check for mistakes.
export let bg: string | undefined = undefined;
export let fg: string | undefined = undefined;
export let padding: string | undefined = undefined;
export let border: string | boolean | undefined = undefined;
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

border is typed as string | boolean | undefined and then passed through injectVars. Passing border={true} would emit a --fieldset-border: true; custom property, which is not a valid border value and may lead to unexpected computed styles. Consider restricting border to a string CSS value, or splitting the API into a boolean flag for default border vs. a string override.

Suggested change
export let border: string | boolean | undefined = undefined;
export let border: string | undefined = undefined;

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +33
// Update store reactively when props or parent change
$: {
let parentVal = undefined;
if (parentStore && typeof parentStore.subscribe === 'function') {
parentVal = get(parentStore);
}
store.set({
layout: layout ?? parentVal?.layout,
fullWidth: fullWidth ?? parentVal?.fullWidth,
globalInputStyles: globalInputStyles ?? parentVal?.globalInputStyles,
multiline: multiline ?? parentVal?.multiline,
});
}
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FormProvider reads the parent defaults store via get(parentStore) inside a reactive block, but get() does not establish a reactive subscription. As a result, updates to a parent FormProvider's store won't propagate to this provider (nested providers won't update when parent values change). Use a store subscription (e.g. $parentStore in a $: statement) or derived() to recompute when the parent store updates, and then store.set(...) from those reactive values.

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +17
const contextStore = getContext("formItemDefaults");

$: contextDefaults = contextStore && typeof contextStore.subscribe === 'function'
? get(contextStore)
: contextStore;
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contextDefaults is computed with get(contextStore), which won't update when the context store emits new values. That means changing layout/fullWidth/globalInputStyles/multiline on the surrounding FormProvider after initial render won't update this FormItem. Subscribe reactively to the store (e.g. use $contextStore in a $: block) instead of calling get() inside a reactive statement.

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +51
const richHtml = optionEl.dataset.html ?? optionEl.innerHTML;
options.push({
value: optionEl.value,
html: optionEl.innerHTML,
html: richHtml.trim(),
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here richHtml is populated from optionEl.dataset.html ?? optionEl.innerHTML and then stored in options[idx].html, which is later rendered via {@html ...} in this component. If any option label or data-html attribute is derived from attacker-controlled data (for example, user input or external API responses), this flow will turn what Svelte normally treats as safe text into executable HTML/JS in the custom dropdown, enabling DOM XSS. To mitigate this, avoid feeding untrusted values into dataset.html/option contents for this component or introduce a sanitization/whitelisting step before assigning to richHtml and using it with @html.

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +39
$: textContent = template ? template.textContent : "";
$: htmlContent = template ? template.innerHTML : "";
</script>

<option data-html={htmlContent} {value} {...$$restProps}>
{textContent}
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here htmlContent is taken from template.innerHTML and written into the data-html attribute of the <option>, which is later consumed by Select.svelte and rendered with {@html}. This means that slot content containing </> (including attacker-controlled strings that Svelte would normally escape as text) can be turned back into live HTML/JS in the custom dropdown, enabling DOM XSS if an attacker controls the option label. To mitigate this, avoid feeding untrusted slot content into this component or ensure the extracted HTML is sanitized/whitelisted before exposing it via data-html for use with {@html}.

Copilot uses AI. Check for mistakes.
New components converted from Svelte 5 to Svelte 3/4 syntax:
- DataList: container for data list rows with CSS variable theming
- DataListItem: individual row with start/end slots, interactive and
  selectable modes, responsive grid layout

Co-authored-by: thinkle-iacs <15049315+thinkle-iacs@users.noreply.github.com>
Copilot AI changed the title Backport new components and improvements from svelte5 to main (svelte3/4) Backport DataList, DataListItem and other improvements from svelte5 to main Feb 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants