@streamscloud/kit 0.22.0 → 0.23.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.
@@ -0,0 +1,46 @@
1
+ <script lang="ts">import { containFit } from './contain-fit';
2
+ const { basis = 'box', allowUpscale = false, alignInline = 'center', alignBlock = 'center', children } = $props();
3
+ </script>
4
+
5
+ <div class="fit-box">
6
+ <div class="fit-box__scaler" class:fit-box__scaler--natural={basis === 'content'} use:containFit={{ allowUpscale, alignInline, alignBlock }}>
7
+ {@render children()}
8
+ </div>
9
+ </div>
10
+
11
+ <!--
12
+ @component
13
+ Scales its content as one unit to fit the box like `object-fit: contain` — proportions preserved, bounded by both the box's width and
14
+ height, no crop, no scroll. A generic fit primitive for **cards and media content** (a card, an image/video, a fixed-size widget), not a
15
+ page-layout tool.
16
+
17
+ How it works: the box is `FitBox`'s **parent**, which must have a **definite size** (fixed / `%` / grid-track / aspect-ratio+width — a
18
+ content-driven `height: auto` parent collapses and nothing shows). The content is measured, then transform-scaled by
19
+ `min(boxW / contentW, boxH / contentH)` and positioned; a `ResizeObserver` re-fits on any box or content change.
20
+
21
+ Pick the mode with `basis`:
22
+ - `basis="box"` (default) — fluid, width-driven content (cards, text, single-aspect media). Fills the box width, scales down to fit the
23
+ height. Never enlarges.
24
+ - `basis="content"` — intrinsic content (`<img>` / `<video>`, fixed-size widgets). Keeps the content's natural size and fits both axes;
25
+ set `allowUpscale` to also enlarge it to fill.
26
+
27
+ `alignInline` / `alignBlock` place the content along an axis when fitting leaves free space there. Requires client-side JS (scales via
28
+ `transform`, so `position: fixed` descendants are relative to the box); content with a hard fixed width wider than the box is clipped
29
+ rather than contained.
30
+ -->
31
+
32
+ <style>.fit-box {
33
+ position: relative;
34
+ inline-size: 100%;
35
+ block-size: 100%;
36
+ overflow: hidden;
37
+ }
38
+ .fit-box__scaler {
39
+ position: absolute;
40
+ inline-size: 100%;
41
+ max-inline-size: 100%;
42
+ visibility: hidden;
43
+ }
44
+ .fit-box__scaler--natural {
45
+ inline-size: auto;
46
+ }</style>
@@ -0,0 +1,42 @@
1
+ import type { FitAlign, FitBasis } from './types';
2
+ import type { Snippet } from 'svelte';
3
+ type Props = {
4
+ /**
5
+ * What the content's width is based on before it is scaled to fit:
6
+ * - `'box'` — content is laid out at the **box width** first (use for fluid, width-driven content: a card, a text block, single-aspect
7
+ * media). Height follows from that width; the content is then scaled **down** only if too tall. Never enlarges.
8
+ * - `'content'` — content keeps its **own natural size** (use for intrinsic content: `<img>` / `<video>`, a fixed-size widget). It is
9
+ * scaled to fit **both** axes and may be enlarged when `allowUpscale` is set.
10
+ * @default 'box'
11
+ */
12
+ basis?: FitBasis;
13
+ /** Allow scaling content up past its natural size to fill the box. Takes effect only with `basis="content"` (`'box'` never enlarges). @default false */
14
+ allowUpscale?: boolean;
15
+ /** Inline-axis (horizontal) placement of the fitted content, visible only when free width remains after fitting. @default 'center' */
16
+ alignInline?: FitAlign;
17
+ /** Block-axis (vertical) placement of the fitted content, visible only when free height remains after fitting. @default 'center' */
18
+ alignBlock?: FitAlign;
19
+ children: Snippet;
20
+ };
21
+ /**
22
+ * Scales its content as one unit to fit the box like `object-fit: contain` — proportions preserved, bounded by both the box's width and
23
+ * height, no crop, no scroll. A generic fit primitive for **cards and media content** (a card, an image/video, a fixed-size widget), not a
24
+ * page-layout tool.
25
+ *
26
+ * How it works: the box is `FitBox`'s **parent**, which must have a **definite size** (fixed / `%` / grid-track / aspect-ratio+width — a
27
+ * content-driven `height: auto` parent collapses and nothing shows). The content is measured, then transform-scaled by
28
+ * `min(boxW / contentW, boxH / contentH)` and positioned; a `ResizeObserver` re-fits on any box or content change.
29
+ *
30
+ * Pick the mode with `basis`:
31
+ * - `basis="box"` (default) — fluid, width-driven content (cards, text, single-aspect media). Fills the box width, scales down to fit the
32
+ * height. Never enlarges.
33
+ * - `basis="content"` — intrinsic content (`<img>` / `<video>`, fixed-size widgets). Keeps the content's natural size and fits both axes;
34
+ * set `allowUpscale` to also enlarge it to fill.
35
+ *
36
+ * `alignInline` / `alignBlock` place the content along an axis when fitting leaves free space there. Requires client-side JS (scales via
37
+ * `transform`, so `position: fixed` descendants are relative to the box); content with a hard fixed width wider than the box is clipped
38
+ * rather than contained.
39
+ */
40
+ declare const Cmp: import("svelte").Component<Props, {}, "">;
41
+ type Cmp = ReturnType<typeof Cmp>;
42
+ export default Cmp;
@@ -0,0 +1,8 @@
1
+ import type { FitAlign } from './types';
2
+ import type { Action } from 'svelte/action';
3
+ export type ContainFitParams = {
4
+ allowUpscale?: boolean;
5
+ alignInline?: FitAlign;
6
+ alignBlock?: FitAlign;
7
+ };
8
+ export declare const containFit: Action<HTMLElement, ContainFitParams | undefined>;
@@ -0,0 +1,47 @@
1
+ const offset = (align, free) => (align === 'start' ? 0 : align === 'end' ? free : free / 2);
2
+ export const containFit = (node, params = {}) => {
3
+ const container = node.parentElement;
4
+ let allowUpscale = params.allowUpscale ?? false;
5
+ let alignInline = params.alignInline ?? 'center';
6
+ let alignBlock = params.alignBlock ?? 'center';
7
+ const apply = () => {
8
+ if (!container) {
9
+ return;
10
+ }
11
+ const boxWidth = container.clientWidth;
12
+ const boxHeight = container.clientHeight;
13
+ const naturalWidth = node.offsetWidth;
14
+ const naturalHeight = node.offsetHeight;
15
+ if (!boxWidth || !boxHeight || !naturalWidth || !naturalHeight) {
16
+ return;
17
+ }
18
+ let scale = Math.min(boxWidth / naturalWidth, boxHeight / naturalHeight);
19
+ if (!allowUpscale) {
20
+ scale = Math.min(scale, 1);
21
+ }
22
+ const left = offset(alignInline, boxWidth - naturalWidth * scale);
23
+ const top = offset(alignBlock, boxHeight - naturalHeight * scale);
24
+ node.style.transformOrigin = '0 0';
25
+ node.style.transform = `scale(${scale})`;
26
+ node.style.left = `${left}px`;
27
+ node.style.top = `${top}px`;
28
+ node.style.visibility = 'visible';
29
+ };
30
+ const observer = new ResizeObserver(apply);
31
+ if (container) {
32
+ observer.observe(container);
33
+ }
34
+ observer.observe(node);
35
+ apply();
36
+ return {
37
+ update(next = {}) {
38
+ allowUpscale = next.allowUpscale ?? false;
39
+ alignInline = next.alignInline ?? 'center';
40
+ alignBlock = next.alignBlock ?? 'center';
41
+ apply();
42
+ },
43
+ destroy() {
44
+ observer.disconnect();
45
+ }
46
+ };
47
+ };
@@ -0,0 +1,2 @@
1
+ export { default as FitBox } from './cmp.fit-box.svelte';
2
+ export type { FitAlign, FitBasis } from './types';
@@ -0,0 +1 @@
1
+ export { default as FitBox } from './cmp.fit-box.svelte';
@@ -0,0 +1,2 @@
1
+ export type FitAlign = 'start' | 'center' | 'end';
2
+ export type FitBasis = 'box' | 'content';
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/kit",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "author": "StreamsCloud",
5
5
  "repository": {
6
6
  "type": "git",
@@ -195,6 +195,10 @@
195
195
  "types": "./dist/ui/file-uploader/index.d.ts",
196
196
  "svelte": "./dist/ui/file-uploader/index.js"
197
197
  },
198
+ "./ui/fit-box": {
199
+ "types": "./dist/ui/fit-box/index.d.ts",
200
+ "svelte": "./dist/ui/fit-box/index.js"
201
+ },
198
202
  "./ui/form-field": {
199
203
  "types": "./dist/ui/form-field/index.d.ts",
200
204
  "svelte": "./dist/ui/form-field/index.js"