contain-css-svelte 1.1.8 → 1.1.10

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Tom Hinkle
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,58 +1,138 @@
1
- # create-svelte
1
+ # ContainCSS for Svelte
2
2
 
3
- Everything you need to build a Svelte library, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
3
+ **A small Svelte component library that gets out of your way.** Every component is
4
+ styled through CSS custom properties, sized by container queries, and built on
5
+ native HTML elements — so you can theme it from the outside, drop it into any
6
+ layout, and keep the accessibility you get for free from `<dialog>`, `<details>`,
7
+ and `popover`.
4
8
 
5
- Read more about creating a library [in the docs](https://kit.svelte.dev/docs/packaging).
9
+ ### 📖 [Browse the docs & live demos ](https://thinkle.github.io/svelte-contain-css/)
6
10
 
7
- ## Creating a project
11
+ [![npm](https://img.shields.io/npm/v/contain-css-svelte.svg)](https://www.npmjs.com/package/contain-css-svelte)
8
12
 
9
- If you're seeing this, you've probably already done this step. Congrats!
13
+ ---
10
14
 
11
- ```bash
12
- # create a new project in the current directory
13
- npm create svelte@latest
15
+ ## Which version do I want?
14
16
 
15
- # create a new project in my-app
16
- npm create svelte@latest my-app
17
- ```
17
+ | | Branch | npm version | Svelte | Docs |
18
+ | --- | --- | --- | --- | --- |
19
+ | **👉 Start here** | [`svelte5`](https://github.com/thinkle/svelte-contain-css/tree/svelte5) | `1.x` | Svelte 5 | [svelte5 docs](https://thinkle.github.io/svelte-contain-css/svelte5/) |
20
+ | Legacy | [`main`](https://github.com/thinkle/svelte-contain-css/tree/main) | `0.0.x` | Svelte 3 & 4 | [legacy docs](https://thinkle.github.io/svelte-contain-css/legacy/) |
18
21
 
19
- ## Developing
22
+ **`svelte5` is the active development branch** and where all new work lands.
23
+ `main` is frozen for existing Svelte 4 and earlier projects; it gets fixes
24
+ backported when they're worth it, but no new features. If you're starting
25
+ something new, use `svelte5` / `1.x`.
20
26
 
21
- Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
27
+ ## Install
22
28
 
23
29
  ```bash
24
- npm run dev
30
+ npm install contain-css-svelte
31
+ ```
25
32
 
26
- # or start the server and open the app in a new browser tab
27
- npm run dev -- --open
33
+ Import the default variables once, at the root of your app:
34
+
35
+ ```svelte
36
+ <!-- +layout.svelte or App.svelte -->
37
+ <script>
38
+ import "contain-css-svelte/vars/defaults.css";
39
+ </script>
28
40
  ```
29
41
 
30
- Everything inside `src/lib` is part of your library, everything inside `src/routes` can be used as a showcase or preview app.
42
+ Then use components anywhere:
43
+
44
+ ```svelte
45
+ <script>
46
+ import { Container, Button, Card } from "contain-css-svelte";
47
+ </script>
31
48
 
32
- ## Building
49
+ <Container>
50
+ <Card>
51
+ <h2>Hello</h2>
52
+ <Button primary>Do the thing</Button>
53
+ </Card>
54
+ </Container>
55
+ ```
33
56
 
34
- To build your library:
57
+ ## The idea
35
58
 
36
- ```bash
37
- npm run package
59
+ **1. Style props are CSS variables, not inline styles.**
60
+
61
+ ```svelte
62
+ <Button bg="green" fg="white" height="64px">Wow</Button>
63
+ <!-- becomes style="--button-bg: green; --button-fg: white; --button-height: 64px" -->
38
64
  ```
39
65
 
40
- To create a production version of your showcase app:
66
+ Because they're variables rather than hard-coded styles, the cascade still
67
+ works — which means you can theme a whole subtree from above:
41
68
 
42
- ```bash
43
- npm run build
69
+ ```svelte
70
+ <div style="--primary-bg: darkblue; --primary-fg: white; --border-radius: 0">
71
+ <Button primary>Themed</Button>
72
+ <Button primary>Also themed</Button>
73
+ </div>
74
+ ```
75
+
76
+ ...or in plain CSS, with no build step and no wrapper components:
77
+
78
+ ```css
79
+ .my-app {
80
+ --bg: #1a1a1a;
81
+ --fg: #fff;
82
+ --primary-bg: #4a90d9;
83
+ --font-family: "Inter", sans-serif;
84
+ }
44
85
  ```
45
86
 
46
- You can preview the production build with `npm run preview`.
87
+ **2. Components respond to their container, not the viewport.** A `Card` in a
88
+ narrow sidebar lays itself out like a narrow card, whether or not the window is
89
+ wide. No breakpoint bookkeeping.
90
+
91
+ **3. Native elements underneath.** `Dialog` is a real `<dialog>`, `Accordion` is
92
+ `<details>`, `Tooltip` uses the `popover` API. Focus trapping, escape-to-close,
93
+ and screen-reader semantics come from the platform.
94
+
95
+ ## What's in the box
96
+
97
+ - **Controls** — `Button`, `ButtonLink`, `MiniButton`, `Input`, `Select`,
98
+ `Checkbox`, `RadioButton`, `Toggle`, `Slider`
99
+ - **Layout** — `Container`, `Page`, `Row`/`Column`/`Columns`, `Stack`, `Inline`,
100
+ `GridLayout`, `SplitPane`, `Sidebar`, `Bar`, `TabBar`, `Table`, `Tile`,
101
+ `DataList`, `Hero`, `Accordion`, `Form`/`FormItem`/`Fieldset`
102
+ - **Overlays** — `Dialog`, `Tooltip`, `DropdownMenu`
103
+ - **Misc** — `Card`, `Code`, `Progress`, `Tag`, `TextLayout`, `ResponsiveText`
47
104
 
48
- > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
105
+ Plus a set of ready-made themes (`light`, `dark`, `material`, `bootstrap`,
106
+ `retro`, `purple`, `forest`, `canyon`, and several typography-only themes) under
107
+ `contain-css-svelte/themes/`.
49
108
 
50
- ## Publishing
109
+ Every component and every CSS variable is documented with a live, editable demo
110
+ in [the docs site](https://thinkle.github.io/svelte-contain-css/svelte5/).
51
111
 
52
- Go into the `package.json` and give your package the desired name through the `"name"` option. Also consider adding a `"license"` field and point it to a `LICENSE` file which you can create from a template (one popular option is the [MIT license](https://opensource.org/license/mit/)).
112
+ ## Using this with an AI assistant
53
113
 
54
- To publish your library to [npm](https://www.npmjs.com):
114
+ [`AGENTS-EXTERNAL.md`](AGENTS-EXTERNAL.md) is a complete reference written for
115
+ coding agents — component list, prop-to-variable conventions, and the full CSS
116
+ variable table. Point your assistant at it (or drop it in your project) and it
117
+ will write idiomatic ContainCSS code.
118
+
119
+ ## Developing the library
55
120
 
56
121
  ```bash
57
- npm publish
122
+ npm install
123
+ npm run dev # docs site + live demos at localhost:5173
124
+ npm run check # svelte-check
125
+ npm run test:e2e # playwright
126
+ npm run package # build dist/
58
127
  ```
128
+
129
+ The docs site lives in `src/routes`; the library itself is `src/lib`. Pushing to
130
+ `svelte5` or `main` rebuilds and publishes both doc versions to GitHub Pages via
131
+ [`.github/workflows/deploy-docs.yml`](.github/workflows/deploy-docs.yml).
132
+
133
+ See [AGENTS-INTERNAL.md](AGENTS-INTERNAL.md) for library-development conventions
134
+ and [BACKPORT.md](BACKPORT.md) for moving fixes to the legacy branch.
135
+
136
+ ## License
137
+
138
+ MIT © [Tom Hinkle](https://tomhinkle.net)
package/dist/index.d.ts CHANGED
@@ -23,6 +23,7 @@ import Form from "./layout/Form.svelte";
23
23
  import FormItem from "./layout/FormItem.svelte";
24
24
  import FormProvider from "./layout/FormProvider.svelte";
25
25
  import GridLayout from "./layout/GridLayout.svelte";
26
+ import GridRow from "./layout/GridRow.svelte";
26
27
  import Hero from "./layout/Hero.svelte";
27
28
  import Inline from "./layout/Inline.svelte";
28
29
  import MenuList from "./layout/MenuList.svelte";
@@ -48,7 +49,7 @@ export { Tag };
48
49
  export { Hero };
49
50
  export { TabItem };
50
51
  export { Button, ButtonLink, Checkbox, Input, Slider, Toggle, RadioButton, MiniButton, Select, Option, };
51
- export { Bar, ColumnContainer, Container, GridLayout, Inline, MenuList, Page, ResponsiveText, RowContainer, Stack, TabBar, };
52
+ export { Bar, ColumnContainer, Container, GridLayout, GridRow, Inline, MenuList, Page, ResponsiveText, RowContainer, Stack, TabBar, };
52
53
  export { TextLayout, Code, Accordion };
53
54
  export { Card, Tile, Form, FormItem, FormProvider, Fieldset };
54
55
  export { DataList, DataListItem };
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ import Form from "./layout/Form.svelte";
24
24
  import FormItem from "./layout/FormItem.svelte";
25
25
  import FormProvider from "./layout/FormProvider.svelte";
26
26
  import GridLayout from "./layout/GridLayout.svelte";
27
+ import GridRow from "./layout/GridRow.svelte";
27
28
  import Hero from "./layout/Hero.svelte";
28
29
  import Inline from "./layout/Inline.svelte";
29
30
  import MenuList from "./layout/MenuList.svelte";
@@ -48,7 +49,7 @@ export { Tag };
48
49
  export { Hero };
49
50
  export { TabItem };
50
51
  export { Button, ButtonLink, Checkbox, Input, Slider, Toggle, RadioButton, MiniButton, Select, Option, };
51
- export { Bar, ColumnContainer, Container, GridLayout, Inline, MenuList, Page, ResponsiveText, RowContainer, Stack, TabBar, };
52
+ export { Bar, ColumnContainer, Container, GridLayout, GridRow, Inline, MenuList, Page, ResponsiveText, RowContainer, Stack, TabBar, };
52
53
  export { TextLayout, Code, Accordion };
53
54
  export { Card, Tile, Form, FormItem, FormProvider, Fieldset };
54
55
  export { DataList, DataListItem };
@@ -11,7 +11,8 @@ export {};
11
11
  {@render children?.()}
12
12
  </div>
13
13
 
14
- <style>/* Warning: because we define a fallback
14
+ <style>@charset "UTF-8";
15
+ /* Warning: because we define a fallback
15
16
  media query, the media query can override the container
16
17
  if we stack a bunch of these in a row and aren't thoughtful about the order.
17
18
  Put min-width queries *after* max-width queries so that smaller
@@ -20,10 +21,58 @@ export {};
20
21
  /* Convenience groupings */
21
22
  .grid-layout {
22
23
  display: grid;
23
- grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--grid-layout-item-width, var(--item-width, 250px))), 1fr));
24
+ /* Tracks sized to the item, not `1fr`.
25
+
26
+ `1fr` splits the leftover width between the tracks, which turns it into
27
+ gutter *between* the items while the edges of the run stay tight -- three
28
+ cards in a 900px container end up sprung apart and pushed left. That is
29
+ rarely what anyone means by declaring an item width: they mean "lay these
30
+ out at this size", and the leftover belongs at the edges.
31
+
32
+ Sizing the track to the item gives exactly the `gap` that was asked for
33
+ between items, and hands the remainder to `justify-content: center` below,
34
+ which centres the whole run. That declaration has been here all along and
35
+ could never fire, because `1fr` tracks always consume the full width.
36
+
37
+ `min(100%, …)` keeps a track from outgrowing a narrow screen.
38
+
39
+ `auto-fit`, not `auto-fill`, and the difference is the whole ballgame for
40
+ centring. Both lay down as many tracks as fit the container; `auto-fill`
41
+ keeps the empty ones at full width, so three cards in a wide container
42
+ leave `justify-content` no slack to work with and pack against the left.
43
+ `auto-fit` collapses the tracks nothing landed in, so the run shrinks to
44
+ the items and centres. Three 200px items in a 1400px container: 72px left
45
+ and 696px right under `auto-fill`, 384px either side under `auto-fit`.
46
+
47
+ One caveat worth knowing, because it is not a bug we can fix here: an item
48
+ spanning `1 / -1` -- a `GridRow`, a heading -- occupies every track, so
49
+ there is nothing left for `auto-fit` to collapse and the run goes back to
50
+ full width and left-packed. A grid with full-width rows in it is a
51
+ full-width grid. Headings kept outside the grid, one grid per section,
52
+ keep the centring. */
53
+ grid-template-columns: repeat(auto-fit, min(100%, var(--grid-layout-item-width, var(--item-width, 250px))));
24
54
  gap: var(--grid-layout-gap, var(--gap, 8px));
25
- justify-content: var(--grid-justify-content, center);
26
- place-content: var(--grid-place-content, center);
55
+ /* Where the run of tracks sits in the grid box. Both axes as longhands,
56
+ because `place-content` is a shorthand and was declared after
57
+ `justify-content` here -- so it quietly overwrote it and
58
+ `--grid-justify-content` never did anything at all. Only
59
+ `--grid-place-content` worked, and it moves both axes at once.
60
+
61
+ Named `--grid-layout-*` like everything else, with the old spellings kept
62
+ as fallbacks so anything already setting them keeps working -- and
63
+ `--grid-justify-content` starts working for the first time. */
64
+ justify-content: var(--grid-layout-justify-content, var(--justify-content, var(--grid-justify-content, var(--grid-place-content, center))));
65
+ align-content: var(--grid-layout-align-content, var(--align-content, var(--grid-place-content, center)));
66
+ /* Where an item sits *inside* its track -- a separate question from where
67
+ the tracks sit in the grid, which is `justify-content` above.
68
+
69
+ Mostly moot now that a track is sized to the item: there is usually no
70
+ room inside a track to sit anywhere. It still matters when a child is
71
+ narrower than the track it was handed, which happens whenever a component
72
+ carries its own width -- a `Tile` with a `--tile-width` smaller than the
73
+ grid's `--item-width`. The default stays `stretch` so anything that can
74
+ use its whole track still gets it. */
75
+ justify-items: var(--grid-layout-justify-items, var(--justify-items, stretch));
27
76
  box-sizing: border-box;
28
77
  --_padding: var(--grid-layout-padding, var(--padding, 4px));
29
78
  padding: var(--_padding);
@@ -38,10 +87,56 @@ export {};
38
87
  border-radius: var(--grid-layout-border-radius, var(--border-radius, none));
39
88
  }
40
89
 
90
+ /* Escape hatch for the things in a grid that are not items in the run: a
91
+ section heading, or a note explaining why the grid is empty. Both want the
92
+ whole row rather than one item-width column with tiles beside them.
93
+
94
+ Declared here so it travels with the component, and matched as a descendant
95
+ rather than a child on purpose -- a component handed a CSS custom property
96
+ is wrapped in a `<svelte-css-wrapper>`, so it is a grandchild of the grid
97
+ even though it is still the grid item. `grid-column` is inert on anything
98
+ that is not a grid item, so the looser match costs nothing. */
99
+ .grid-layout :global(.grid-full-row) {
100
+ grid-column: 1/-1;
101
+ /* Spanning is only half of it. How an item fills the tracks it spans is
102
+ `justify-items` on the grid, which a tile grid sets to `center` -- and a
103
+ centred block with no width of its own shrinks to fit its content, so the
104
+ row it spans renders as a short centred blob rather than a full-width
105
+ heading. Saying `stretch` here makes the class mean the same thing in
106
+ every grid, whatever the container prefers for ordinary items.
107
+
108
+ A consumer spanning their own item -- `grid-column: span 2` -- owns this
109
+ decision themselves and will want `justify-self` alongside it. */
110
+ justify-self: var(--grid-row-justify, var(--justify, stretch));
111
+ /* And the typography has to be let go of too, which is the part that took a
112
+ while to see. A heading inside a typographic container gets a readable
113
+ measure and auto side margins -- `max-width: var(--line-width)`, 42rem by
114
+ default, with `margin-inline: auto` -- so it sits as a centred column of
115
+ text. Sensible for prose, wrong for a band that is supposed to run the
116
+ width of a grid.
117
+
118
+ Worse, an auto inline margin makes `justify-self: stretch` do nothing at
119
+ all: the spec only stretches a box whose inline margins are both
120
+ non-auto, so the heading fell back to shrink-to-fit. "Sections" measured
121
+ 74px in a 1064px row, and computed style reports the *used* margin as
122
+ `0px`, so it does not look like margins are the culprit until you set them
123
+ and watch the width jump. */
124
+ max-width: none;
125
+ margin-inline: 0;
126
+ }
127
+
41
128
  .card-grid {
42
129
  --item-width: var(--card-width, 400px);
43
130
  }
44
131
 
45
132
  .tile-grid {
46
- --item-width: var(--tile-width, 200px);
133
+ /* Has to be the width a Tile actually defaults to, not a round number near
134
+ it. Tile says `calc(var(--space-lg) * 24)`, which is 192px; this said
135
+ 200px, so every tile sat 4px inside its own track and a full-width row
136
+ did not line up with the tiles under it. */
137
+ --item-width: var(--tile-width, calc(var(--space-lg) * 24));
138
+ /* The track now matches `--item-width`, so this only bites when a Tile is
139
+ narrower than the track it was handed -- a `--tile-width` set smaller than
140
+ the grid's item width, say. */
141
+ --justify-items: center;
47
142
  }</style>
@@ -0,0 +1,23 @@
1
+ <script lang="ts">import { injectVars } from "../util";
2
+ let { justify = null, center = false, children, ...restProps } = $props();
3
+ const resolvedJustify = $derived(justify ?? (center ? "center" : null));
4
+ const inlineStyle = $derived(restProps.style);
5
+ const elementProps = $derived.by(() => {
6
+ const { style: _, ...rest } = restProps;
7
+ return rest;
8
+ });
9
+ const style = $derived(injectVars({ justify: resolvedJustify, ...elementProps }, "grid-row", ["justify"]) +
10
+ (inlineStyle ?? ""));
11
+ </script>
12
+
13
+ <div class="grid-full-row grid-row" {...elementProps} {style}>
14
+ {@render children?.()}
15
+ </div>
16
+
17
+ <style>.grid-row {
18
+ /* Spanning, stretching and the margin reset all live on `.grid-full-row`,
19
+ in GridLayout. What this adds is releasing the measure for the content:
20
+ without it a heading in here is clamped to `--line-width` and centred by
21
+ its own auto margins, so the band spans and the words inside it do not. */
22
+ --line-width: none;
23
+ }</style>
@@ -0,0 +1,24 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ /**
4
+ * A full-width band inside a GridLayout: a section heading, an explanation, a
5
+ * note saying why the grid is empty. Anything that belongs *in* the run of
6
+ * items without being one of them.
7
+ *
8
+ * Carries `.grid-full-row`, which is the same thing as a class for when you
9
+ * already have an element and would rather not nest a div inside it. The
10
+ * difference is `--line-width`: a heading or paragraph placed inside this
11
+ * component is meant to run the width of the band, so the readable measure
12
+ * that typographic containers impose is lifted for its contents too. Put the
13
+ * class on your own heading instead and the heading keeps that measure.
14
+ */
15
+ type Props = {
16
+ /** How the row sits in the span it covers: start, center, end, stretch. */
17
+ justify?: string | null;
18
+ /** Shorthand for `justify="center"`, as `Stack` takes `center`. */
19
+ center?: boolean;
20
+ children?: Snippet;
21
+ } & HTMLAttributes<HTMLDivElement>;
22
+ declare const GridRow: import("svelte").Component<Props, {}, "">;
23
+ type GridRow = ReturnType<typeof GridRow>;
24
+ export default GridRow;
@@ -1,28 +1,54 @@
1
- <script lang="ts">let { checked = $bindable(false), ...props } = $props();
1
+ <script lang="ts">import { injectVars } from "../util";
2
+ let { checked = $bindable(false), ...props } = $props();
3
+ /* Props to CSS variables, the way Stack, Card, Tag and the rest do it, so a
4
+ Tile can be aimed with `<Tile center>` rather than only a raw variable. */
5
+ const styleProps = $derived.by(() => {
6
+ const { justify = null, align = null, center = false, bg = null, fg = null, padding = null, width = null, height = null, } = props;
7
+ return {
8
+ bg,
9
+ fg,
10
+ padding,
11
+ width,
12
+ height,
13
+ justify: justify ?? (center ? "center" : null),
14
+ align: align ?? (center ? "center" : null),
15
+ };
16
+ });
17
+ const style = $derived(injectVars(styleProps, "tile", [
18
+ "bg",
19
+ "fg",
20
+ "padding",
21
+ "width",
22
+ "height",
23
+ "justify",
24
+ "align",
25
+ ]) + (props.style ?? ""));
26
+ /** Everything that is ours rather than the element's. */
27
+ function stripOwnProps(value) {
28
+ const { selectable: _selectable, interactive: _interactive, children: _children, justify: _justify, align: _align, center: _center, bg: _bg, fg: _fg, padding: _padding, width: _width, height: _height, style: _style, ...rest } = value;
29
+ return rest;
30
+ }
2
31
  function getSelectableInputProps(value) {
3
- const { selectable: _selectable, interactive: _interactive, children: _children, ...inputProps } = value;
4
- return inputProps;
32
+ return stripOwnProps(value);
5
33
  }
6
34
  function getInteractiveButtonProps(value) {
7
- const { selectable: _selectable, interactive: _interactive, children: _children, ...buttonProps } = value;
8
- return buttonProps;
35
+ return stripOwnProps(value);
9
36
  }
10
- export {};
11
37
  </script>
12
38
 
13
39
  {#if props.selectable}
14
- <label class="tile">
40
+ <label class="tile" {style}>
15
41
  <div class="checkbox">
16
42
  <input type="checkbox" bind:checked {...getSelectableInputProps(props)} />
17
43
  </div>
18
44
  {@render props.children?.()}
19
45
  </label>
20
46
  {:else if props.interactive}
21
- <button class="tile" {...getInteractiveButtonProps(props)}>
47
+ <button class="tile" {style} {...getInteractiveButtonProps(props)}>
22
48
  {@render props.children?.()}
23
49
  </button>
24
50
  {:else}
25
- <div class="tile">
51
+ <div class="tile" {style} {...stripOwnProps(props as RenderProps)}>
26
52
  {@render props.children?.()}
27
53
  </div>
28
54
  {/if}
@@ -1,6 +1,18 @@
1
1
  import type { Snippet } from "svelte";
2
2
  import type { HTMLAttributes, HTMLButtonAttributes, HTMLInputAttributes } from "svelte/elements";
3
- type BaseProps = {
3
+ import type { BaseStyleProps } from "../types";
4
+ type BaseProps = BaseStyleProps & {
5
+ /** Distribution along the tile's own axis, which is vertical. */
6
+ justify?: string | null;
7
+ /** Cross-axis alignment, which is horizontal. Centred already. */
8
+ align?: string | null;
9
+ /**
10
+ * Shorthand for centring the tile's contents. Sets both axes, unlike
11
+ * `Stack`'s `center`, because a Tile already centres horizontally -- setting
12
+ * only the cross axis here would be a no-op and `<Tile center>` would look
13
+ * broken.
14
+ */
15
+ center?: boolean;
4
16
  children?: Snippet;
5
17
  };
6
18
  type StaticTileProps = BaseProps & HTMLAttributes<HTMLDivElement> & {
@@ -1,4 +1,4 @@
1
- <script lang="ts">"use strict";
1
+ <script lang="ts">import { tick } from "svelte";
2
2
  let tooltipDiv = $state();
3
3
  let targetDiv = $state();
4
4
  let tooltipMeasurementDiv = $state();
@@ -7,7 +7,29 @@ let { tooltipText = "", vertical = "bottom", horizontal = "right", children, too
7
7
  let renderedVertical = $state(vertical);
8
8
  // svelte-ignore state_referenced_locally
9
9
  let renderedHorizontal = $state(horizontal);
10
- function showPopover() {
10
+ /**
11
+ * Tooltip content mounts on first show, not on mount. A page with many
12
+ * tooltips (e.g. a grid of cells each carrying a rich tooltip snippet)
13
+ * would otherwise build every tooltip twice — popover + measurement copy —
14
+ * before the user hovers anything. Once shown, content stays mounted.
15
+ */
16
+ let hasRendered = $state(false);
17
+ /** Guards against the pointer/focus leaving while content mounts. */
18
+ let wantsShow = false;
19
+ function hidePopover() {
20
+ wantsShow = false;
21
+ tooltipDiv?.togglePopover(false);
22
+ }
23
+ async function showPopover() {
24
+ wantsShow = true;
25
+ if (!hasRendered) {
26
+ hasRendered = true;
27
+ // Wait for the content to exist before measuring it — positioning below
28
+ // reads the measurement element's height/width to decide flip direction.
29
+ await tick();
30
+ if (!wantsShow)
31
+ return;
32
+ }
11
33
  // Get the position of the target element
12
34
  // with respect to our screen
13
35
  if (!targetDiv?.children[0])
@@ -75,9 +97,9 @@ function showPopover() {
75
97
  <div
76
98
  class="tooltip-wrapper"
77
99
  onmouseenter={() => showPopover()}
78
- onmouseleave={() => (tooltipDiv ? tooltipDiv.togglePopover(false) : null)}
100
+ onmouseleave={() => hidePopover()}
79
101
  onfocusin={() => showPopover()}
80
- onfocusout={() => (tooltipDiv ? tooltipDiv.togglePopover(false) : null)}
102
+ onfocusout={() => hidePopover()}
81
103
  bind:this={targetDiv}
82
104
  >
83
105
  {@render children?.()}
@@ -90,13 +112,17 @@ function showPopover() {
90
112
  class:left={renderedHorizontal === "left"}
91
113
  class:right={renderedHorizontal === "right"}
92
114
  >
93
- {#if tooltip}{@render tooltip()}{:else}
94
- {tooltipText}
115
+ {#if hasRendered}
116
+ {#if tooltip}{@render tooltip()}{:else}
117
+ {tooltipText}
118
+ {/if}
95
119
  {/if}
96
120
  </div>
97
121
  <div class="tooltip invisible measure" bind:this={tooltipMeasurementDiv}>
98
- {#if tooltip}{@render tooltip()}{:else}
99
- {tooltipText}
122
+ {#if hasRendered}
123
+ {#if tooltip}{@render tooltip()}{:else}
124
+ {tooltipText}
125
+ {/if}
100
126
  {/if}
101
127
  </div>
102
128
  </div>
@@ -105,9 +131,9 @@ function showPopover() {
105
131
  <span
106
132
  class="tooltip-wrapper"
107
133
  onmouseenter={() => showPopover()}
108
- onmouseleave={() => (tooltipDiv ? tooltipDiv.togglePopover(false) : null)}
134
+ onmouseleave={() => hidePopover()}
109
135
  onfocusin={() => showPopover()}
110
- onfocusout={() => (tooltipDiv ? tooltipDiv.togglePopover(false) : null)}
136
+ onfocusout={() => hidePopover()}
111
137
  bind:this={targetDiv}
112
138
  >
113
139
  {@render children?.()}
@@ -120,13 +146,17 @@ function showPopover() {
120
146
  class:left={renderedHorizontal === "left"}
121
147
  class:right={renderedHorizontal === "right"}
122
148
  >
123
- {#if tooltip}{@render tooltip()}{:else}
124
- {tooltipText}
149
+ {#if hasRendered}
150
+ {#if tooltip}{@render tooltip()}{:else}
151
+ {tooltipText}
152
+ {/if}
125
153
  {/if}
126
154
  </span>
127
155
  <span class="tooltip invisible measure" bind:this={tooltipMeasurementDiv}>
128
- {#if tooltip}{@render tooltip()}{:else}
129
- {tooltipText}
156
+ {#if hasRendered}
157
+ {#if tooltip}{@render tooltip()}{:else}
158
+ {tooltipText}
159
+ {/if}
130
160
  {/if}
131
161
  </span>
132
162
  </span>
@@ -1,4 +1,5 @@
1
- <script lang="ts">import ReviewThemeSwitcher from "./ReviewThemeSwitcher.svelte";
1
+ <script lang="ts">import { base } from "$app/paths";
2
+ import ReviewThemeSwitcher from "./ReviewThemeSwitcher.svelte";
2
3
  import { reviewAppScenarios } from "./scenarios";
3
4
  let { data } = $props();
4
5
  const scenario = $derived(reviewAppScenarios[data.scenario]);
@@ -10,8 +11,8 @@ const scenario = $derived(reviewAppScenarios[data.scenario]);
10
11
 
11
12
  <div class="review-page-header">
12
13
  <div class="review-page-links">
13
- <a href={`/svelte-contain-css/review/${data.theme}/`}>{data.themeLabel} routes</a>
14
- <a href={`/svelte-contain-css/review/${data.theme}/app/`}>App scenarios</a>
14
+ <a href={`${base}/review/${data.theme}/`}>{data.themeLabel} routes</a>
15
+ <a href={`${base}/review/${data.theme}/app/`}>App scenarios</a>
15
16
  </div>
16
17
 
17
18
  <ReviewThemeSwitcher theme={data.theme} />
@@ -1,4 +1,5 @@
1
- <script lang="ts">import ReviewIndexShell from "./ReviewIndexShell.svelte";
1
+ <script lang="ts">import { base } from "$app/paths";
2
+ import ReviewIndexShell from "./ReviewIndexShell.svelte";
2
3
  import { reviewThemeLabels } from "./themes";
3
4
  </script>
4
5
 
@@ -13,7 +14,7 @@ import { reviewThemeLabels } from "./themes";
13
14
  <ul>
14
15
  {#each Object.entries(reviewThemeLabels) as [theme, label]}
15
16
  <li>
16
- <a href={`/svelte-contain-css/review/${theme}/`}>{label}</a>
17
+ <a href={`${base}/review/${theme}/`}>{label}</a>
17
18
  </li>
18
19
  {/each}
19
20
  </ul>
@@ -1,4 +1,5 @@
1
- <script lang="ts">import ReviewIndexShell from "./ReviewIndexShell.svelte";
1
+ <script lang="ts">import { base } from "$app/paths";
2
+ import ReviewIndexShell from "./ReviewIndexShell.svelte";
2
3
  import ReviewThemeSwitcher from "./ReviewThemeSwitcher.svelte";
3
4
  import { reviewAppScenarios } from "./scenarios";
4
5
  let { theme, themeLabel } = $props();
@@ -17,7 +18,7 @@ let { theme, themeLabel } = $props();
17
18
  <ul>
18
19
  {#each Object.entries(reviewAppScenarios) as [scenario, entry]}
19
20
  <li>
20
- <a href={`/svelte-contain-css/review/${theme}/app/${scenario}`}>
21
+ <a href={`${base}/review/${theme}/app/${scenario}`}>
21
22
  {entry.title}
22
23
  </a>
23
24
  </li>
@@ -1,4 +1,5 @@
1
- <script lang="ts">import ReviewIndexShell from "./ReviewIndexShell.svelte";
1
+ <script lang="ts">import { base } from "$app/paths";
2
+ import ReviewIndexShell from "./ReviewIndexShell.svelte";
2
3
  import ReviewThemeSwitcher from "./ReviewThemeSwitcher.svelte";
3
4
  import { reviewAppScenarios } from "./scenarios";
4
5
  let { theme, themeLabel } = $props();
@@ -18,7 +19,7 @@ let { theme, themeLabel } = $props();
18
19
  <h2>Groups</h2>
19
20
  <ul>
20
21
  <li>
21
- <a href={`/svelte-contain-css/review/${theme}/app/`}>App scenarios</a>
22
+ <a href={`${base}/review/${theme}/app/`}>App scenarios</a>
22
23
  </li>
23
24
  </ul>
24
25
  </section>
@@ -28,7 +29,7 @@ let { theme, themeLabel } = $props();
28
29
  <ul>
29
30
  {#each Object.entries(reviewAppScenarios) as [scenario, entry]}
30
31
  <li>
31
- <a href={`/svelte-contain-css/review/${theme}/app/${scenario}`}>
32
+ <a href={`${base}/review/${theme}/app/${scenario}`}>
32
33
  {entry.title}
33
34
  </a>
34
35
  </li>
@@ -1,4 +1,5 @@
1
- <script lang="ts">import { page } from "$app/state";
1
+ <script lang="ts">import { base } from "$app/paths";
2
+ import { page } from "$app/state";
2
3
  import { reviewThemeLabels, reviewThemeNames, } from "./themes";
3
4
  let { theme } = $props();
4
5
  const themeLinks = $derived.by(() => {
@@ -16,7 +17,7 @@ const themeLinks = $derived.by(() => {
16
17
  </script>
17
18
 
18
19
  <nav class="review-theme-switcher" aria-label="Review theme switcher">
19
- <a class="utility-link" href="/svelte-contain-css/review/">All Themes</a>
20
+ <a class="utility-link" href={`${base}/review/`}>All Themes</a>
20
21
 
21
22
  {#each themeLinks as item}
22
23
  <a
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "contain-css-svelte",
3
- "version": "1.1.8",
3
+ "version": "1.1.10",
4
+ "license": "MIT",
4
5
  "scripts": {
5
6
  "capture:review": "node scripts/capture-review.mjs",
6
7
  "capture:review:matrix": "node scripts/capture-review-matrix.mjs",