contain-css-svelte 1.1.20 → 1.2.0

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.
Files changed (94) hide show
  1. package/dist/Card.svelte +15 -6
  2. package/dist/Card.svelte.d.ts +4 -5
  3. package/dist/controls/Button.svelte +19 -5
  4. package/dist/controls/Button.svelte.d.ts +4 -4
  5. package/dist/controls/ButtonLink.svelte +17 -14
  6. package/dist/controls/ButtonLink.svelte.d.ts +4 -4
  7. package/dist/controls/Checkbox.svelte +15 -8
  8. package/dist/controls/Checkbox.svelte.d.ts +4 -4
  9. package/dist/controls/CircleButton.svelte +14 -7
  10. package/dist/controls/CircleButton.svelte.d.ts +4 -4
  11. package/dist/controls/Input.svelte +14 -3
  12. package/dist/controls/Input.svelte.d.ts +4 -3
  13. package/dist/controls/Option.svelte +4 -3
  14. package/dist/controls/Option.svelte.d.ts +3 -2
  15. package/dist/controls/RadioButton.svelte +14 -7
  16. package/dist/controls/RadioButton.svelte.d.ts +4 -4
  17. package/dist/controls/Select.svelte +15 -3
  18. package/dist/controls/Select.svelte.d.ts +4 -3
  19. package/dist/controls/Slider.svelte +5 -11
  20. package/dist/controls/Slider.svelte.d.ts +3 -3
  21. package/dist/controls/TabItem.svelte +24 -5
  22. package/dist/controls/TabItem.svelte.d.ts +4 -4
  23. package/dist/controls/Toggle.svelte +21 -5
  24. package/dist/controls/Toggle.svelte.d.ts +4 -4
  25. package/dist/dropdowns/DropdownMenu.svelte +18 -6
  26. package/dist/dropdowns/DropdownMenu.svelte.d.ts +4 -4
  27. package/dist/layout/Accordion.svelte +17 -2
  28. package/dist/layout/Accordion.svelte.d.ts +5 -3
  29. package/dist/layout/Bar.svelte +13 -7
  30. package/dist/layout/Bar.svelte.d.ts +4 -4
  31. package/dist/layout/Column.svelte.d.ts +3 -3
  32. package/dist/layout/ColumnContainer.svelte +12 -9
  33. package/dist/layout/ColumnContainer.svelte.d.ts +4 -4
  34. package/dist/layout/Columns.svelte +4 -3
  35. package/dist/layout/Columns.svelte.d.ts +7 -3
  36. package/dist/layout/Container.svelte +23 -8
  37. package/dist/layout/Container.svelte.d.ts +7 -5
  38. package/dist/layout/DataList.svelte +15 -7
  39. package/dist/layout/DataList.svelte.d.ts +5 -4
  40. package/dist/layout/DataListItem.svelte +14 -8
  41. package/dist/layout/DataListItem.svelte.d.ts +4 -4
  42. package/dist/layout/Fieldset.svelte +16 -8
  43. package/dist/layout/Fieldset.svelte.d.ts +6 -3
  44. package/dist/layout/Form.svelte +17 -8
  45. package/dist/layout/Form.svelte.d.ts +8 -6
  46. package/dist/layout/FormItem.svelte +12 -7
  47. package/dist/layout/FormItem.svelte.d.ts +4 -3
  48. package/dist/layout/FormProvider.svelte +3 -0
  49. package/dist/layout/GridLayout.svelte +14 -9
  50. package/dist/layout/GridLayout.svelte.d.ts +4 -4
  51. package/dist/layout/GridRow.svelte +6 -10
  52. package/dist/layout/GridRow.svelte.d.ts +3 -2
  53. package/dist/layout/Hero.svelte +14 -8
  54. package/dist/layout/Hero.svelte.d.ts +4 -4
  55. package/dist/layout/Inline.svelte +16 -15
  56. package/dist/layout/Inline.svelte.d.ts +4 -4
  57. package/dist/layout/MenuList.svelte +13 -6
  58. package/dist/layout/MenuList.svelte.d.ts +4 -4
  59. package/dist/layout/Page.svelte +14 -8
  60. package/dist/layout/Page.svelte.d.ts +11 -3
  61. package/dist/layout/ResponsiveText.svelte +4 -0
  62. package/dist/layout/Row.svelte.d.ts +3 -3
  63. package/dist/layout/RowContainer.svelte +12 -9
  64. package/dist/layout/RowContainer.svelte.d.ts +4 -4
  65. package/dist/layout/Sidebar.svelte +17 -6
  66. package/dist/layout/Sidebar.svelte.d.ts +12 -4
  67. package/dist/layout/SplitPane.svelte +8 -14
  68. package/dist/layout/SplitPane.svelte.d.ts +16 -2
  69. package/dist/layout/Stack.svelte +16 -15
  70. package/dist/layout/Stack.svelte.d.ts +4 -4
  71. package/dist/layout/TabBar.svelte +12 -2
  72. package/dist/layout/TabBar.svelte.d.ts +17 -10
  73. package/dist/layout/Table.svelte +5 -1
  74. package/dist/layout/Tile.svelte +40 -27
  75. package/dist/layout/Tile.svelte.d.ts +32 -10
  76. package/dist/misc/Code.svelte +2 -0
  77. package/dist/misc/Progress.svelte +25 -8
  78. package/dist/misc/Progress.svelte.d.ts +15 -8
  79. package/dist/misc/Tag.svelte +97 -44
  80. package/dist/misc/Tag.svelte.d.ts +17 -5
  81. package/dist/overlays/Dialog.svelte +18 -5
  82. package/dist/overlays/Dialog.svelte.d.ts +5 -3
  83. package/dist/overlays/Tooltip.svelte +21 -3
  84. package/dist/overlays/Tooltip.svelte.d.ts +5 -3
  85. package/dist/styleProps.d.ts +67 -0
  86. package/dist/styleProps.js +76 -0
  87. package/dist/types.d.ts +40 -1
  88. package/dist/types.js +1 -1
  89. package/dist/typography/Text.svelte +4 -11
  90. package/dist/typography/TextLayout.svelte +4 -7
  91. package/dist/typography/TextLayout.svelte.d.ts +4 -3
  92. package/dist/util.d.ts +60 -3
  93. package/dist/util.js +101 -11
  94. package/package.json +1 -1
@@ -1,8 +1,21 @@
1
1
  <script lang="ts">import CircleButton from "../controls/CircleButton.svelte";
2
- import { copyCSSVariables, injectVars } from "../util";
3
- let { open = true, modal = true, dismissible = false, children, onClose, onclose: oncloseProp = null, ...restProps } = $props();
2
+ import { copyCSSVariables, splitProps } from "../util";
3
+ import { COLOR_VARS, PADDING_VARS, RADIUS_VARS, TYPOGRAPHY_CONTAINER_VARS, } from "../styleProps";
4
+ let { open = true, modal = true, dismissible = false, children, onClose, onclose: oncloseProp = null, class: className, ...restProps } = $props();
4
5
  let onclose = $derived(oncloseProp || onClose);
5
- const style = $derived(injectVars(restProps, "dialog", []));
6
+ /* The <section> is the variable carrier that copyCSSVariables reads through,
7
+ so it takes the style while the <dialog> takes the attributes -- a
8
+ caller's `aria-*` and `data-*` belong on the dialog itself. */
9
+ /* The shorthands this component's own CSS backs, one group per mixin
10
+ it includes. The Props type is derived from this same array, so what
11
+ the component accepts and what it emits cannot drift apart. */
12
+ const DIALOG_VARS = [
13
+ ...COLOR_VARS,
14
+ ...PADDING_VARS,
15
+ ...RADIUS_VARS,
16
+ ...TYPOGRAPHY_CONTAINER_VARS,
17
+ ];
18
+ const el = $derived(splitProps(restProps, "dialog", DIALOG_VARS));
6
19
  // Handle backdrop click to close modal (click outside behavior)
7
20
  // Clicks on the dialog element itself (the backdrop) close the dialog
8
21
  function handleBackdropClick(event) {
@@ -52,9 +65,9 @@ let dialogElement;
52
65
  let ref;
53
66
  </script>
54
67
 
55
- <section {style}>
68
+ <section style={el.style}>
56
69
  <div class="variable-placeholder" bind:this={ref}></div>
57
- <dialog bind:this={dialogElement} {onclose} {...restProps}>
70
+ <dialog bind:this={dialogElement} {onclose} class={className} {...el.attrs}>
58
71
  <div class="close-bar">
59
72
  <div class="close-button">
60
73
  <CircleButton onclick={onclose}>&times;</CircleButton>
@@ -1,12 +1,14 @@
1
+ import { type StyleProps } from "../styleProps";
1
2
  import type { Snippet } from "svelte";
2
- type PropsType = {
3
+ import type { HTMLDialogAttributes } from "svelte/elements";
4
+ import type { ContainProps } from "../types";
5
+ declare const Dialog: import("svelte").Component<ContainProps<HTMLDialogAttributes, {
3
6
  open?: boolean;
4
7
  modal?: boolean;
5
8
  dismissible?: boolean;
6
9
  children?: Snippet;
7
10
  onclose?: (() => void) | null;
8
11
  onClose?: (() => void) | null;
9
- } & Record<string, unknown>;
10
- declare const Dialog: import("svelte").Component<PropsType, {}, "">;
12
+ }, StyleProps<readonly ["bg", "fg", "padding", "borderRadius", "fontFamily", "fontSize", "fontWeight", "lineHeight", "letterSpacing", "textAlign", "textTransform", "textDecoration", "fontVariant", "lineWidth"]>>, {}, "">;
11
13
  type Dialog = ReturnType<typeof Dialog>;
12
14
  export default Dialog;
@@ -1,8 +1,24 @@
1
1
  <script lang="ts">import { onDestroy, tick } from "svelte";
2
+ import { elementProps } from "../util";
3
+ import { COLOR_VARS, PADDING_VARS, RADIUS_VARS, TYPOGRAPHY_CONTAINER_VARS, } from "../styleProps";
2
4
  let tooltipDiv = $state();
3
5
  let targetDiv = $state();
4
6
  let tooltipMeasurementDiv = $state();
5
- let { tooltipText = "", vertical = "bottom", horizontal = "right", children, tooltip, block = false, } = $props();
7
+ let { tooltipText = "", vertical = "bottom", horizontal = "right", children, tooltip, block = false, class: className, ...restProps } = $props();
8
+ /* The wrapper is the element a caller is pointing at -- the tooltip itself
9
+ is a popover the component owns and positions. */
10
+ /* The shorthands this component's own CSS backs, one group per mixin
11
+ it includes. The Props type is derived from this same array, so what
12
+ the component accepts and what it emits cannot drift apart. */
13
+ const TOOLTIP_VARS = [
14
+ ...COLOR_VARS,
15
+ ...PADDING_VARS,
16
+ ...RADIUS_VARS,
17
+ ...TYPOGRAPHY_CONTAINER_VARS,
18
+ ];
19
+ /* The variables land on the wrapper and reach the popover by inheritance:
20
+ the tooltip is a descendant of it, and custom properties inherit. */
21
+ const el = $derived(elementProps(restProps, "tooltip", TOOLTIP_VARS));
6
22
  // svelte-ignore state_referenced_locally
7
23
  let renderedVertical = $state(vertical);
8
24
  // svelte-ignore state_referenced_locally
@@ -206,11 +222,12 @@ async function showPopover() {
206
222
  {#if block}
207
223
  <!-- svelte-ignore a11y_no_static_element_interactions -->
208
224
  <div
209
- class="tooltip-wrapper"
225
+ class={["tooltip-wrapper", className]}
210
226
  onmouseenter={() => showPopover()}
211
227
  onmouseleave={() => hidePopover()}
212
228
  onfocusin={() => showPopover()}
213
229
  onfocusout={() => hidePopover()}
230
+ {...el}
214
231
  >
215
232
  <div class="tooltip-target" bind:this={targetDiv}>
216
233
  {@render children?.()}
@@ -241,11 +258,12 @@ async function showPopover() {
241
258
  {:else}
242
259
  <!-- svelte-ignore a11y_no_static_element_interactions -->
243
260
  <span
244
- class="tooltip-wrapper"
261
+ class={["tooltip-wrapper", className]}
245
262
  onmouseenter={() => showPopover()}
246
263
  onmouseleave={() => hidePopover()}
247
264
  onfocusin={() => showPopover()}
248
265
  onfocusout={() => hidePopover()}
266
+ {...el}
249
267
  >
250
268
  <span class="tooltip-target" bind:this={targetDiv}>
251
269
  {@render children?.()}
@@ -1,11 +1,13 @@
1
- interface Props {
1
+ import type { HTMLAttributes } from "svelte/elements";
2
+ import type { ContainProps } from "../types";
3
+ import { type StyleProps } from "../styleProps";
4
+ declare const Tooltip: import("svelte").Component<ContainProps<HTMLAttributes<HTMLElement>, {
2
5
  tooltipText?: string;
3
6
  vertical?: string;
4
7
  horizontal?: string;
5
8
  children?: import("svelte").Snippet;
6
9
  tooltip?: import("svelte").Snippet;
7
10
  block?: boolean;
8
- }
9
- declare const Tooltip: import("svelte").Component<Props, {}, "">;
11
+ }, StyleProps<readonly ["bg", "fg", "padding", "borderRadius", "fontFamily", "fontSize", "fontWeight", "lineHeight", "letterSpacing", "textAlign", "textTransform", "textDecoration", "fontVariant", "lineWidth"]>>, {}, "">;
10
12
  type Tooltip = ReturnType<typeof Tooltip>;
11
13
  export default Tooltip;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The style-prop shorthands, grouped to mirror the SCSS mixins that make them
3
+ * readable.
4
+ *
5
+ * The problem this solves: which shorthands a component accepted used to be a
6
+ * hand-maintained list in each component, drifting from the mixins its CSS
7
+ * actually included. Tag exposed `fontSize` while the other 23 components
8
+ * whose CSS includes a typography mixin exposed none of it -- `--button-font-
9
+ * weight` resolved perfectly well, there was just no prop for it. In the other
10
+ * direction, FormItem injected three variables no stylesheet read at all.
11
+ *
12
+ * So each group below names one mixin, and a component's prop list is the
13
+ * union of the groups for the mixins its own CSS includes. "Which shorthands
14
+ * does this component take" stops being a decision and becomes a consequence.
15
+ *
16
+ * The prop types are derived from these same arrays via `StyleProps<>`, so the
17
+ * type and the runtime list cannot disagree -- adding a name to the array is
18
+ * what adds the prop.
19
+ *
20
+ * A shorthand is only safe when the element the component renders has no HTML
21
+ * attribute of that name: `ContainProps` resolves the clash by dropping the
22
+ * attribute, which is right for a deprecated or meaningless one and wrong for
23
+ * `<progress max>` or `<option label>`. That is why the size group is applied
24
+ * per component rather than being folded into the others, and why Progress's
25
+ * accessible-name prop is `progressLabel`.
26
+ */
27
+ /** Every style prop takes a CSS value, or null/undefined to leave it unset. */
28
+ export type StyleProps<T extends readonly string[]> = {
29
+ [K in T[number]]?: string | null;
30
+ };
31
+ /** `@include color-props(<prefix>, ...)` */
32
+ export declare const COLOR_VARS: readonly ["bg", "fg"];
33
+ /** `@include padding-props(...)`, and the box-props family that includes it */
34
+ export declare const PADDING_VARS: readonly ["padding"];
35
+ /** The border-radius the box-props family sets alongside padding */
36
+ export declare const RADIUS_VARS: readonly ["borderRadius"];
37
+ /** `@include margin-props(<prefix>, ...)` */
38
+ export declare const MARGIN_VARS: readonly ["marginBlock", "marginInline"];
39
+ /** `@include gap-props(<prefix>, ...)` */
40
+ export declare const GAP_VARS: readonly ["gap"];
41
+ /**
42
+ * `@include typography-props(...)` and its `-bare` / `-bold` variants.
43
+ *
44
+ * `fontSize` was the only one of these ever exposed as a prop, and only on
45
+ * Tag. The rest resolved in CSS the whole time.
46
+ */
47
+ export declare const TYPOGRAPHY_VARS: readonly ["fontFamily", "fontSize", "fontWeight", "lineHeight", "letterSpacing", "textAlign", "textTransform", "textDecoration", "fontVariant"];
48
+ /**
49
+ * `@include typography-container-props(...)`, which is typography-props plus
50
+ * the readable measure it imposes on prose children.
51
+ */
52
+ export declare const TYPOGRAPHY_CONTAINER_VARS: readonly ["fontFamily", "fontSize", "fontWeight", "lineHeight", "letterSpacing", "textAlign", "textTransform", "textDecoration", "fontVariant", "lineWidth"];
53
+ /**
54
+ * `@include box-size-props(<prefix>, ...)`.
55
+ *
56
+ * Deliberately NOT part of any other group: `width` and `height` are real HTML
57
+ * attributes on `<input>`, `<img>`, `<canvas>` and `<iframe>`, so a component
58
+ * rendering one of those must not shadow them. Applied per component.
59
+ */
60
+ export declare const SIZE_VARS: readonly ["width", "minWidth", "maxWidth", "height", "minHeight", "maxHeight"];
61
+ export type ColorStyleProps = StyleProps<typeof COLOR_VARS>;
62
+ export type PaddingStyleProps = StyleProps<typeof PADDING_VARS>;
63
+ export type RadiusStyleProps = StyleProps<typeof RADIUS_VARS>;
64
+ export type GapStyleProps = StyleProps<typeof GAP_VARS>;
65
+ export type TypographyStyleProps = StyleProps<typeof TYPOGRAPHY_VARS>;
66
+ export type TypographyContainerStyleProps = StyleProps<typeof TYPOGRAPHY_CONTAINER_VARS>;
67
+ export type SizeStyleProps = StyleProps<typeof SIZE_VARS>;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The style-prop shorthands, grouped to mirror the SCSS mixins that make them
3
+ * readable.
4
+ *
5
+ * The problem this solves: which shorthands a component accepted used to be a
6
+ * hand-maintained list in each component, drifting from the mixins its CSS
7
+ * actually included. Tag exposed `fontSize` while the other 23 components
8
+ * whose CSS includes a typography mixin exposed none of it -- `--button-font-
9
+ * weight` resolved perfectly well, there was just no prop for it. In the other
10
+ * direction, FormItem injected three variables no stylesheet read at all.
11
+ *
12
+ * So each group below names one mixin, and a component's prop list is the
13
+ * union of the groups for the mixins its own CSS includes. "Which shorthands
14
+ * does this component take" stops being a decision and becomes a consequence.
15
+ *
16
+ * The prop types are derived from these same arrays via `StyleProps<>`, so the
17
+ * type and the runtime list cannot disagree -- adding a name to the array is
18
+ * what adds the prop.
19
+ *
20
+ * A shorthand is only safe when the element the component renders has no HTML
21
+ * attribute of that name: `ContainProps` resolves the clash by dropping the
22
+ * attribute, which is right for a deprecated or meaningless one and wrong for
23
+ * `<progress max>` or `<option label>`. That is why the size group is applied
24
+ * per component rather than being folded into the others, and why Progress's
25
+ * accessible-name prop is `progressLabel`.
26
+ */
27
+ /** `@include color-props(<prefix>, ...)` */
28
+ export const COLOR_VARS = ["bg", "fg"];
29
+ /** `@include padding-props(...)`, and the box-props family that includes it */
30
+ export const PADDING_VARS = ["padding"];
31
+ /** The border-radius the box-props family sets alongside padding */
32
+ export const RADIUS_VARS = ["borderRadius"];
33
+ /** `@include margin-props(<prefix>, ...)` */
34
+ export const MARGIN_VARS = ["marginBlock", "marginInline"];
35
+ /** `@include gap-props(<prefix>, ...)` */
36
+ export const GAP_VARS = ["gap"];
37
+ /**
38
+ * `@include typography-props(...)` and its `-bare` / `-bold` variants.
39
+ *
40
+ * `fontSize` was the only one of these ever exposed as a prop, and only on
41
+ * Tag. The rest resolved in CSS the whole time.
42
+ */
43
+ export const TYPOGRAPHY_VARS = [
44
+ "fontFamily",
45
+ "fontSize",
46
+ "fontWeight",
47
+ "lineHeight",
48
+ "letterSpacing",
49
+ "textAlign",
50
+ "textTransform",
51
+ "textDecoration",
52
+ "fontVariant",
53
+ ];
54
+ /**
55
+ * `@include typography-container-props(...)`, which is typography-props plus
56
+ * the readable measure it imposes on prose children.
57
+ */
58
+ export const TYPOGRAPHY_CONTAINER_VARS = [
59
+ ...TYPOGRAPHY_VARS,
60
+ "lineWidth",
61
+ ];
62
+ /**
63
+ * `@include box-size-props(<prefix>, ...)`.
64
+ *
65
+ * Deliberately NOT part of any other group: `width` and `height` are real HTML
66
+ * attributes on `<input>`, `<img>`, `<canvas>` and `<iframe>`, so a component
67
+ * rendering one of those must not shadow them. Applied per component.
68
+ */
69
+ export const SIZE_VARS = [
70
+ "width",
71
+ "minWidth",
72
+ "maxWidth",
73
+ "height",
74
+ "minHeight",
75
+ "maxHeight",
76
+ ];
package/dist/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Common CSS variable props that can be passed to components.
3
- * These are converted to CSS custom properties via injectVars().
3
+ * These are converted to CSS custom properties via elementProps().
4
4
  */
5
5
  /** Base style props supported by most components */
6
6
  export interface BaseStyleProps {
@@ -88,3 +88,42 @@ export interface DropdownMenuStyleProps extends BaseStyleProps {
88
88
  triggerFg?: string | null;
89
89
  triggerPadding?: string | null;
90
90
  }
91
+ /**
92
+ * Props that let a caller set CSS custom properties directly on a component.
93
+ *
94
+ * NOTE: writing `<Tag --tag-bg="red">` in a template does NOT go through this
95
+ * type -- Svelte intercepts `--*` attributes on components itself and renders
96
+ * `<svelte-css-wrapper style="display: contents; --tag-bg: red;">` around the
97
+ * component. This type covers the remaining path, an object spread
98
+ * (`<Tag {...{"--tag-bg": "red"}}>`), which Svelte cannot detect statically and
99
+ * therefore passes through as an ordinary prop.
100
+ */
101
+ export type CssVarProps = {
102
+ [key: `--${string}`]: string | number | null | undefined;
103
+ };
104
+ /**
105
+ * The standard prop type for a Contain CSS component.
106
+ *
107
+ * Composes the three things every component's props are made of, and -- the
108
+ * reason this exists rather than a bare `&` chain -- removes the element
109
+ * attributes that the style props and the component's own props shadow.
110
+ * `width`, `height` and `align` are real HTML attributes, so intersecting
111
+ * `BaseStyleProps` with `HTMLInputAttributes` the old way produced
112
+ * `(string | null) & (string | number | undefined)` for `width`: a type neither
113
+ * side meant, which silently accepted and rejected the wrong values.
114
+ *
115
+ * @typeParam Attributes - the element's attribute type, e.g. `HTMLButtonAttributes`
116
+ * @typeParam Own - the component's own props (snippets, variant booleans, ...)
117
+ * @typeParam Style - the component's style props, e.g. `BaseStyleProps`
118
+ * @typeParam Owned - attributes the component controls and a caller must not
119
+ * set, e.g. `"type"` on Slider's range input. Spreading rest props last means
120
+ * a caller's value would otherwise win over the component's own.
121
+ *
122
+ * @example
123
+ * type Props = ContainProps<
124
+ * HTMLButtonAttributes,
125
+ * { primary?: boolean; children?: Snippet },
126
+ * BaseStyleProps
127
+ * >;
128
+ */
129
+ export type ContainProps<Attributes, Own = {}, Style = {}, Owned extends keyof Attributes | string = never> = Own & Style & Omit<Attributes, keyof Own | keyof Style | Owned> & CssVarProps;
package/dist/types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * Common CSS variable props that can be passed to components.
3
- * These are converted to CSS custom properties via injectVars().
3
+ * These are converted to CSS custom properties via elementProps().
4
4
  */
5
5
  export {};
@@ -1,5 +1,5 @@
1
- <script lang="ts">import { injectVars } from "../util";
2
- let { children, primary = false, secondary = false, info = false, success = false, warning = false, danger = false, muted = false, bold = false, italic = false, color, amount, style: inlineStyle, ...restProps } = $props();
1
+ <script lang="ts">import { elementProps } from "../util";
2
+ let { children, primary = false, secondary = false, info = false, success = false, warning = false, danger = false, muted = false, bold = false, italic = false, color, amount, class: className, ...restProps } = $props();
3
3
  // Choose one tone deterministically; explicit color takes precedence.
4
4
  const tone = $derived(danger
5
5
  ? "danger"
@@ -30,17 +30,10 @@ const foreground = $derived.by(() => {
30
30
  return mixed;
31
31
  return `var(--${tone}-text-fg, ${mixed})`;
32
32
  });
33
- const elementProps = $derived(Object.fromEntries(Object.entries(restProps).filter(([key]) => !key.startsWith("--"))));
34
- const style = $derived(injectVars(restProps, "text", []) + (inlineStyle ?? ""));
33
+ const el = $derived(elementProps(restProps, "text"));
35
34
  </script>
36
35
 
37
- <span
38
- {...elementProps}
39
- {style}
40
- style:color={foreground}
41
- class:bold
42
- class:italic
43
- >
36
+ <span class={className} class:bold class:italic style:color={foreground} {...el}>
44
37
  {@render children?.()}
45
38
  </span>
46
39
 
@@ -1,12 +1,9 @@
1
- <script lang="ts">import { injectVars } from "../util";
2
- let { id = "", marginBlock = null, marginInline = null, children } = $props();
3
- const style = $derived(injectVars({ marginBlock, marginInline }, "text-layout", [
4
- "marginBlock",
5
- "marginInline",
6
- ]));
1
+ <script lang="ts">import { elementProps } from "../util";
2
+ let { id = "", children, class: className, ...restProps } = $props();
3
+ const el = $derived(elementProps(restProps, "text-layout", ["marginBlock", "marginInline"]));
7
4
  </script>
8
5
 
9
- <div {id} {style}>
6
+ <div {id} class={className} {...el}>
10
7
  {@render children?.()}
11
8
  </div>
12
9
 
@@ -1,8 +1,9 @@
1
- import type { MarginStyleProps } from "../types";
2
- interface Props extends MarginStyleProps {
1
+ import type { HTMLAttributes } from "svelte/elements";
2
+ import type { ContainProps, MarginStyleProps } from "../types";
3
+ type Props = ContainProps<HTMLAttributes<HTMLDivElement>, {
3
4
  id?: string;
4
5
  children?: import("svelte").Snippet;
5
- }
6
+ }, MarginStyleProps>;
6
7
  declare const TextLayout: import("svelte").Component<Props, {}, "">;
7
8
  type TextLayout = ReturnType<typeof TextLayout>;
8
9
  export default TextLayout;
package/dist/util.d.ts CHANGED
@@ -1,4 +1,61 @@
1
- export declare function injectVars(props: {
2
- [key: string]: any;
3
- }, prefix: string, varList: string[]): string;
1
+ /**
2
+ * Split a component's rest props into a `style` string and the attributes that
3
+ * belong on an element, keeping the two separate.
4
+ *
5
+ * Use this for a component that renders a wrapper around a control (Toggle,
6
+ * RadioButton, Checkbox, FormItem): the CSS variables and the caller's `style`
7
+ * belong on the wrapper, because that is what the component's own CSS targets
8
+ * and what a caller expects to position, while `aria-*`, `name`, `required`
9
+ * and `data-*` belong on the control itself. {@link elementProps} is the
10
+ * single-element case and is what most components want.
11
+ *
12
+ * @returns `style` for the wrapper, `attrs` (never containing `style`) for the
13
+ * control.
14
+ */
15
+ export declare function splitProps(props: Record<string, any>, prefix: string, varList?: readonly string[], extraVars?: string): {
16
+ style: string;
17
+ attrs: Record<string, unknown>;
18
+ };
19
+ /**
20
+ * Split a component's rest props into the attributes that belong on its root
21
+ * element, with the component's CSS variables and the caller's own `style`
22
+ * merged into a single `style` attribute.
23
+ *
24
+ * This is the one place the library's attribute pass-through contract lives.
25
+ * It exists because spreading raw rest props gets three things wrong at once:
26
+ *
27
+ * 1. **Style props leaked as attributes.** `bg`, `fg`, `padding` and friends are
28
+ * read for their CSS variables but were then spread too, rendering invalid
29
+ * `<button bg="red">`. Names in `varList` are consumed here, never emitted.
30
+ * 2. **The caller's `style` clobbered the variables.** `<Tag bg="red" style="margin:0">`
31
+ * dropped `--tag-bg` entirely, because the spread's `style` overwrote the
32
+ * component's. Both are concatenated instead, caller last so their
33
+ * declarations win without erasing the variables.
34
+ * 3. **Custom properties leaked as attributes.** A spread `{"--tag-bg": "red"}`
35
+ * rendered a literal `--tag-bg="red"` attribute. They become declarations.
36
+ *
37
+ * `class` is deliberately NOT handled here. Merging it in the template as
38
+ * `class={["tag", className]}` keeps the component's own class literal in the
39
+ * markup, which is what lets the compiler's CSS pruning see that `.tag` is used.
40
+ *
41
+ * @param props - the component's rest props. Pass style props that were
42
+ * destructured out back in explicitly (`{ bg, fg, ...restProps }`) or their
43
+ * variables will not be emitted.
44
+ * @param prefix - CSS variable prefix, e.g. `"tag"` for `--tag-bg`
45
+ * @param varList - prop names to convert into `--prefix-name` declarations
46
+ * @param extraVars - variables the component computes itself, e.g.
47
+ * `--slider-percent`. Placed after the prop variables and before the
48
+ * caller's `style`, so a caller can still override it.
49
+ *
50
+ * @example
51
+ * const el = $derived(elementProps(restProps, "tag", ["bg", "fg", "padding"]));
52
+ * // <span class={["tag", className]} class:primary {...el}>
53
+ */
54
+ export declare function elementProps(props: Record<string, any>, prefix: string, varList?: readonly string[], extraVars?: string): Record<string, unknown>;
55
+ /**
56
+ * Build only the CSS variable string, for a component that has no element to
57
+ * spread attributes onto (Dialog, which portals its content and copies
58
+ * variables across).
59
+ */
60
+ export declare function injectVars(props: Record<string, any>, prefix: string, varList: readonly string[]): string;
4
61
  export declare function copyCSSVariables(origin: HTMLElement, target: HTMLElement, variableList?: string[]): void;
package/dist/util.js CHANGED
@@ -1,19 +1,109 @@
1
1
  import { cssProperties } from "./cssprops";
2
- export function injectVars(props, prefix, varList) {
2
+ /**
3
+ * Build the `--prefix-name` CSS variable declarations for a component.
4
+ *
5
+ * Kept separate from {@link elementProps} only so a component that composes its
6
+ * style string by hand (Dialog, which also copies variables across a portal)
7
+ * can reuse the naming rules. Prefer `elementProps` everywhere else.
8
+ */
9
+ function buildVars(props, prefix, varList) {
3
10
  let cssVars = "";
4
- varList.forEach((v) => {
5
- if (props[v]) {
6
- const cssVarName = toKebabCase(v);
7
- cssVars += `--${prefix}-${cssVarName}: ${props[v]};`;
8
- }
9
- });
10
- for (let prop in props) {
11
- if (prop[0] == "-" && prop[1] == "-") {
12
- cssVars += `${prop}: ${props[prop]};`;
13
- }
11
+ for (const name of varList) {
12
+ const value = props[name];
13
+ // `0` is a meaningful length, so it is NOT skipped -- the old `if (value)`
14
+ // test dropped it. `false` still is: Form and Fieldset type `border` as
15
+ // `string | boolean`, where `false` means "no border", not the declaration
16
+ // `--form-border: false`.
17
+ if (value == null || value === "" || value === false)
18
+ continue;
19
+ cssVars += `--${prefix}-${toKebabCase(name)}: ${value};`;
20
+ }
21
+ for (const key in props) {
22
+ // Custom properties arriving via an object spread -- see CssVarProps.
23
+ if (key.startsWith("--"))
24
+ cssVars += `${key}: ${props[key]};`;
14
25
  }
15
26
  return cssVars;
16
27
  }
28
+ /**
29
+ * Split a component's rest props into a `style` string and the attributes that
30
+ * belong on an element, keeping the two separate.
31
+ *
32
+ * Use this for a component that renders a wrapper around a control (Toggle,
33
+ * RadioButton, Checkbox, FormItem): the CSS variables and the caller's `style`
34
+ * belong on the wrapper, because that is what the component's own CSS targets
35
+ * and what a caller expects to position, while `aria-*`, `name`, `required`
36
+ * and `data-*` belong on the control itself. {@link elementProps} is the
37
+ * single-element case and is what most components want.
38
+ *
39
+ * @returns `style` for the wrapper, `attrs` (never containing `style`) for the
40
+ * control.
41
+ */
42
+ export function splitProps(props, prefix, varList = [], extraVars = "") {
43
+ const attrs = {};
44
+ for (const key in props) {
45
+ if (key.startsWith("--"))
46
+ continue; // becomes a declaration, not an attribute
47
+ if (key === "style")
48
+ continue; // returned separately
49
+ if (varList.includes(key))
50
+ continue; // a style prop, not an attribute
51
+ attrs[key] = props[key];
52
+ }
53
+ return {
54
+ style: buildVars(props, prefix, varList) + extraVars + (props.style ?? ""),
55
+ attrs,
56
+ };
57
+ }
58
+ /**
59
+ * Split a component's rest props into the attributes that belong on its root
60
+ * element, with the component's CSS variables and the caller's own `style`
61
+ * merged into a single `style` attribute.
62
+ *
63
+ * This is the one place the library's attribute pass-through contract lives.
64
+ * It exists because spreading raw rest props gets three things wrong at once:
65
+ *
66
+ * 1. **Style props leaked as attributes.** `bg`, `fg`, `padding` and friends are
67
+ * read for their CSS variables but were then spread too, rendering invalid
68
+ * `<button bg="red">`. Names in `varList` are consumed here, never emitted.
69
+ * 2. **The caller's `style` clobbered the variables.** `<Tag bg="red" style="margin:0">`
70
+ * dropped `--tag-bg` entirely, because the spread's `style` overwrote the
71
+ * component's. Both are concatenated instead, caller last so their
72
+ * declarations win without erasing the variables.
73
+ * 3. **Custom properties leaked as attributes.** A spread `{"--tag-bg": "red"}`
74
+ * rendered a literal `--tag-bg="red"` attribute. They become declarations.
75
+ *
76
+ * `class` is deliberately NOT handled here. Merging it in the template as
77
+ * `class={["tag", className]}` keeps the component's own class literal in the
78
+ * markup, which is what lets the compiler's CSS pruning see that `.tag` is used.
79
+ *
80
+ * @param props - the component's rest props. Pass style props that were
81
+ * destructured out back in explicitly (`{ bg, fg, ...restProps }`) or their
82
+ * variables will not be emitted.
83
+ * @param prefix - CSS variable prefix, e.g. `"tag"` for `--tag-bg`
84
+ * @param varList - prop names to convert into `--prefix-name` declarations
85
+ * @param extraVars - variables the component computes itself, e.g.
86
+ * `--slider-percent`. Placed after the prop variables and before the
87
+ * caller's `style`, so a caller can still override it.
88
+ *
89
+ * @example
90
+ * const el = $derived(elementProps(restProps, "tag", ["bg", "fg", "padding"]));
91
+ * // <span class={["tag", className]} class:primary {...el}>
92
+ */
93
+ export function elementProps(props, prefix, varList = [], extraVars = "") {
94
+ const { style, attrs } = splitProps(props, prefix, varList, extraVars);
95
+ if (style)
96
+ attrs.style = style;
97
+ return attrs;
98
+ }
99
+ /**
100
+ * Build only the CSS variable string, for a component that has no element to
101
+ * spread attributes onto (Dialog, which portals its content and copies
102
+ * variables across).
103
+ */
104
+ export function injectVars(props, prefix, varList) {
105
+ return buildVars(props, prefix, varList);
106
+ }
17
107
  function toKebabCase(str) {
18
108
  return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
19
109
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contain-css-svelte",
3
- "version": "1.1.20",
3
+ "version": "1.2.0",
4
4
  "license": "MIT",
5
5
  "scripts": {
6
6
  "capture:review": "node scripts/capture-review.mjs",