@marianmeres/stuic 3.119.0 → 3.121.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,70 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ import type { THC } from "../Thc/Thc.svelte";
4
+ import type { FloatPlacement, FloatPoint } from "./float-utils.js";
5
+ export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "title" | "draggable" | "class"> {
6
+ /** Header title (rendered via `Thc`). Stays visible when minimized. */
7
+ title?: THC;
8
+ /** Initial left position in px. Overrides the `placement` x. */
9
+ x?: number;
10
+ /** Initial top position in px. Overrides the `placement` y. */
11
+ y?: number;
12
+ /** Initial placement preset, used for any axis `x`/`y` does not pin. */
13
+ placement?: FloatPlacement;
14
+ /** Edge gap (px) used when resolving `placement`. Number or `{x,y}`. */
15
+ offset?: number | Partial<FloatPoint>;
16
+ /** Panel width: a number (px) or any CSS length. Defaults to a token. */
17
+ width?: number | string;
18
+ /** Initial collapsed state. Runtime changes go through the methods below. */
19
+ minimized?: boolean;
20
+ /** Enable drag-repositioning by the header. Default `true`. */
21
+ draggable?: boolean;
22
+ /** Show the close `×` and enable `close()` / Escape. Default `false`. */
23
+ closable?: boolean;
24
+ /** Called by `close()`, the `×` button, and Escape (when closable). */
25
+ onClose?: () => void;
26
+ /** Close on Escape when `closable`. Default `true`. */
27
+ closeOnEscape?: boolean;
28
+ /** Raise above sibling Floats on pointerdown. Default `true`. */
29
+ bringToFrontOnClick?: boolean;
30
+ /**
31
+ * Opt-in persistence key. When set, `{x, y, minimized}` is remembered
32
+ * across reloads in localStorage under `stuic-float-<storageKey>`.
33
+ */
34
+ storageKey?: string;
35
+ /** Minimum gap kept from every viewport edge while clamping. Default `0`. */
36
+ margin?: number;
37
+ /** Accessible labels for the built-in buttons. */
38
+ minimizeLabel?: string;
39
+ restoreLabel?: string;
40
+ closeLabel?: string;
41
+ /** Leading header slot (e.g. a grip/icon). Drag still works over it. */
42
+ icon?: Snippet;
43
+ /** Header actions slot, placed left of the minimize/close buttons. */
44
+ actions?: Snippet;
45
+ /** Body content (hidden when minimized). */
46
+ children?: Snippet;
47
+ /** Root element classes (merged). */
48
+ class?: string;
49
+ classHeader?: string;
50
+ classTitle?: string;
51
+ classActions?: string;
52
+ classBody?: string;
53
+ /** Drop all default styling (still positioned/draggable). */
54
+ unstyled?: boolean;
55
+ /** Bindable root element reference. */
56
+ el?: HTMLDivElement;
57
+ }
58
+ declare const Float: import("svelte").Component<Props, {
59
+ moveTo: (nx: number, ny: number) => void;
60
+ moveToPlacement: (a: FloatPlacement, o?: number | Partial<FloatPoint>) => void;
61
+ getPosition: () => FloatPoint;
62
+ minimize: () => void;
63
+ expand: () => void;
64
+ toggleMinimize: () => void;
65
+ isMinimized: () => boolean;
66
+ close: () => void;
67
+ bringToFront: () => void;
68
+ }, "el">;
69
+ type Float = ReturnType<typeof Float>;
70
+ export default Float;
@@ -0,0 +1,152 @@
1
+ # Float
2
+
3
+ A non-modal, draggable, collapsible floating panel — the reusable "container" half of a
4
+ dev/inspector tweak panel (dat.GUI / Tweakpane style). It has a header (optional leading
5
+ icon, a `THC` title, an actions slot, and minimize/close buttons) and an arbitrary body.
6
+
7
+ - **Positioned by params**: numeric `x`/`y` **or** a named `placement` preset (corners / edges / center).
8
+ - **`position: fixed`** relative to the viewport, with drag **clamped** so it never leaves the screen.
9
+ - **Draggable** by the whole header (buttons excepted).
10
+ - **Minimizable** to just the title bar (header button, double-click header, or methods).
11
+ - **Imperative control** via a `bind:this` ref (mirrors `Modal`/`ModalDialog`).
12
+ - Optional: leading icon, header actions, **localStorage persistence**, and **bring-to-front** stacking.
13
+
14
+ Resizing is intentionally out of scope (v1 is drag + minimize).
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ | --------------------------------------------------------------------- | ------------------ | ------------- | ------------------------------------------------------------------------ |
20
+ | `title` | `THC` | - | Header title (text/html/component/snippet). Stays visible when minimized |
21
+ | `x` / `y` | `number` | - | Initial position in px. Overrides the corresponding `placement` axis |
22
+ | `placement` | `FloatPlacement` | `"top-right"` | Initial preset for any axis `x`/`y` does not pin |
23
+ | `offset` | `number \| {x,y}` | `16` | Edge gap used when resolving `placement` |
24
+ | `width` | `number \| string` | token (320px) | Panel width (number = px, or any CSS length) |
25
+ | `minimized` | `boolean` | `false` | Initial collapsed state (initial-only; runtime via methods) |
26
+ | `draggable` | `boolean` | `true` | Enable header drag-repositioning |
27
+ | `closable` | `boolean` | `false` | Show the close `×` and enable `close()` / Escape |
28
+ | `onClose` | `() => void` | - | Fired by `close()`, the `×` button, and Escape |
29
+ | `closeOnEscape` | `boolean` | `true` | Close on Escape when `closable` |
30
+ | `bringToFrontOnClick` | `boolean` | `true` | Raise above sibling Floats on pointerdown |
31
+ | `storageKey` | `string` | - | Persist `{x, y, minimized}` in `localStorage` (`stuic-float-<key>`) |
32
+ | `margin` | `number` | `0` | Minimum gap kept from every viewport edge while clamping |
33
+ | `minimizeLabel` | `string` | `"Minimize"` | Accessible label for the minimize button |
34
+ | `restoreLabel` | `string` | `"Restore"` | Accessible label when minimized |
35
+ | `closeLabel` | `string` | `"Close"` | Accessible label for the close button |
36
+ | `class` / `classHeader` / `classTitle` / `classActions` / `classBody` | `string` | - | Merged class overrides for each part |
37
+ | `unstyled` | `boolean` | `false` | Drop default styling (still positioned/draggable) |
38
+ | `el` | `HTMLDivElement` | - | Root element reference (bindable) |
39
+
40
+ ## Snippets
41
+
42
+ | Snippet | Description |
43
+ | ---------- | -------------------------------------------------------------------- |
44
+ | `icon` | Leading header content (e.g. a grip/glyph). Drag still works over it |
45
+ | `actions` | Header actions, placed left of the minimize/close buttons |
46
+ | `children` | Body content (hidden when minimized) |
47
+
48
+ ## Methods
49
+
50
+ Accessed via a `bind:this` reference.
51
+
52
+ | Method | Description |
53
+ | -------------------------- | ------------------------------------------------------ |
54
+ | `moveTo(x, y)` | Move to an absolute position (clamped to the viewport) |
55
+ | `moveToPlacement(a, off?)` | Move to a named placement preset |
56
+ | `getPosition()` | `{ x, y }` of the current top-left |
57
+ | `minimize()` / `expand()` | Collapse to / expand from the title bar |
58
+ | `toggleMinimize()` | Toggle collapsed state |
59
+ | `isMinimized()` | Whether currently minimized |
60
+ | `close()` | Fire `onClose` (consumer unmounts) |
61
+ | `bringToFront()` | Raise above sibling Floats |
62
+
63
+ ## Data attributes
64
+
65
+ For CSS targeting: `data-minimized`, `data-dragging`, `data-draggable` (each `"true"`/`"false"`).
66
+
67
+ ## Usage
68
+
69
+ ### Basic tweak panel
70
+
71
+ ```svelte
72
+ <script lang="ts">
73
+ import { Float, Switch } from "stuic";
74
+
75
+ let fov = $state(50);
76
+ let wireframe = $state(false);
77
+ </script>
78
+
79
+ <Float title="Scene settings" placement="top-right" width={280}>
80
+ <label>FOV: {fov}<input type="range" min="10" max="120" bind:value={fov} /></label>
81
+ <Switch bind:checked={wireframe} label="Wireframe" />
82
+ </Float>
83
+ ```
84
+
85
+ ### Imperative control
86
+
87
+ ```svelte
88
+ <script lang="ts">
89
+ let panel: Float = $state()!;
90
+ </script>
91
+
92
+ <button onclick={() => panel.moveToPlacement("bottom-left")}>Dock bottom-left</button>
93
+ <button onclick={() => panel.minimize()}>Minimize</button>
94
+
95
+ <Float bind:this={panel} title="Inspector" x={24} y={24}>…</Float>
96
+ ```
97
+
98
+ ### Persisted position + leading icon + actions
99
+
100
+ ```svelte
101
+ <Float title="Layers" placement="left" storageKey="layers-panel">
102
+ {#snippet icon()}<MyGrip />{/snippet}
103
+ {#snippet actions()}
104
+ <button data-no-drag class="stuic-float-btn" onclick={reset}>↺</button>
105
+ {/snippet}
106
+
107
+ </Float>
108
+ ```
109
+
110
+ `storageKey` remembers `{x, y, minimized}` across reloads. Mark any custom interactive
111
+ controls in the header with `data-no-drag` so they don't start a drag.
112
+
113
+ ### Closable
114
+
115
+ ```svelte
116
+ {#if open}
117
+ <Float title="Hints" closable onClose={() => (open = false)} placement="bottom-right">
118
+
119
+ </Float>
120
+ {/if}
121
+ ```
122
+
123
+ ## CSS Variables
124
+
125
+ Structural tokens (radius/shadow/border-width/transition) resolve via the fallback pattern,
126
+ so global or per-instance overrides work.
127
+
128
+ | Variable | Default | Description |
129
+ | ------------------------------- | ---------------------------------- | -------------------------------------- |
130
+ | `--stuic-float-width` | `320px` | Default panel width |
131
+ | `--stuic-float-z` | `50` | Stacking floor (order is added on top) |
132
+ | `--stuic-float-bg` | `--stuic-color-background` | Panel background |
133
+ | `--stuic-float-text` | `--stuic-color-foreground` | Panel text |
134
+ | `--stuic-float-border-color` | `--stuic-color-border` | Border / header divider color |
135
+ | `--stuic-float-header-bg` | `--stuic-color-surface` | Header background |
136
+ | `--stuic-float-header-text` | `--stuic-color-surface-foreground` | Header text |
137
+ | `--stuic-float-body-max-height` | `70vh` | Body max height before it scrolls |
138
+ | `--stuic-float-btn-bg-hover` | `--stuic-color-muted` | Header button hover background |
139
+ | `--stuic-float-radius` | `--stuic-radius-container` | Corner radius (fallback) |
140
+ | `--stuic-float-shadow` | `--stuic-shadow-overlay` | Drop shadow (fallback) |
141
+
142
+ ```svelte
143
+ <Float style="--stuic-float-header-bg: var(--stuic-color-muted);" title="Custom">…</Float>
144
+ ```
145
+
146
+ ## Notes
147
+
148
+ - **Non-modal**: no backdrop, no focus trap, non-blocking. `role="dialog"` + `aria-modal="false"`.
149
+ - **No portal**: rendered in place with `position: fixed`. A `transform`ed ancestor would
150
+ break `fixed` positioning — mount Float near the top of the tree if that applies.
151
+ - Built on the reusable [`draggable`](../../actions/draggable.svelte.ts) action and the pure
152
+ [`float-utils`](./float-utils.ts) helpers (`resolvePlacement`, `clampToViewport`).
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Returns the next stacking order (a strictly increasing integer).
3
+ *
4
+ * Used both when a `Float` mounts (so later panels start above earlier ones)
5
+ * and on `bringToFront()`.
6
+ *
7
+ * @returns The next order value (>= 1).
8
+ */
9
+ export declare function nextFloatOrder(): number;
10
+ /**
11
+ * Current highest issued order. Mostly useful for tests / debugging.
12
+ *
13
+ * @returns The last value returned by {@link nextFloatOrder} (0 if none yet).
14
+ */
15
+ export declare function currentFloatOrder(): number;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Tiny module-level stacking counter shared by all `Float` instances.
3
+ *
4
+ * Each call returns a strictly increasing integer used as the panel's
5
+ * `z-index` *order* offset (added on top of the `--stuic-float-z` floor in CSS).
6
+ * The most recently raised panel therefore always wins. A plain module
7
+ * singleton is intentional — every import shares the same counter.
8
+ */
9
+ let order = 0;
10
+ /**
11
+ * Returns the next stacking order (a strictly increasing integer).
12
+ *
13
+ * Used both when a `Float` mounts (so later panels start above earlier ones)
14
+ * and on `bringToFront()`.
15
+ *
16
+ * @returns The next order value (>= 1).
17
+ */
18
+ export function nextFloatOrder() {
19
+ return ++order;
20
+ }
21
+ /**
22
+ * Current highest issued order. Mostly useful for tests / debugging.
23
+ *
24
+ * @returns The last value returned by {@link nextFloatOrder} (0 if none yet).
25
+ */
26
+ export function currentFloatOrder() {
27
+ return order;
28
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Pure (DOM-free) positioning helpers for the `Float` component.
3
+ *
4
+ * Kept separate from `Float.svelte` so the math can be unit-tested in the fast
5
+ * node test project (`*.test.ts`) without a browser.
6
+ */
7
+ /** A 2D size. */
8
+ export interface FloatSize {
9
+ width: number;
10
+ height: number;
11
+ }
12
+ /** A 2D point (top-left corner, in px). */
13
+ export interface FloatPoint {
14
+ x: number;
15
+ y: number;
16
+ }
17
+ /**
18
+ * Named placement preset, resolved against the viewport (with `Float`'s
19
+ * `offset` applied as a gap from the corresponding edges).
20
+ *
21
+ * - Single axis names (`top`, `left`, ...) center the unspecified axis.
22
+ * - `center` centers both axes.
23
+ */
24
+ export type FloatPlacement = "top-left" | "top" | "top-right" | "left" | "center" | "right" | "bottom-left" | "bottom" | "bottom-right";
25
+ /** All valid placements, handy for demos / iteration. */
26
+ export declare const FLOAT_PLACEMENTS: FloatPlacement[];
27
+ /**
28
+ * Normalizes a scalar-or-`{x,y}` offset into an `{x,y}` pair.
29
+ *
30
+ * @param offset - A single number (applied to both axes) or an explicit pair.
31
+ * @returns The resolved `{x,y}` offset.
32
+ */
33
+ export declare function normalizeOffset(offset: number | Partial<FloatPoint> | undefined, fallback?: number): FloatPoint;
34
+ /**
35
+ * Resolves a named {@link FloatPlacement} to an absolute top-left position so the
36
+ * element sits inside the viewport with `offset` as the edge gap.
37
+ *
38
+ * The returned position is guaranteed to be within `[0, viewport - size]` on
39
+ * each axis (it never needs further clamping).
40
+ *
41
+ * @param placement - The placement preset.
42
+ * @param size - The measured element size.
43
+ * @param viewport - The available viewport size.
44
+ * @param offset - Edge gap (number or `{x,y}`).
45
+ * @returns The resolved top-left `{x,y}`.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * resolvePlacement("top-right", { width: 200, height: 100 },
50
+ * { width: 1000, height: 800 }, { x: 16, y: 16 });
51
+ * // -> { x: 784, y: 16 }
52
+ * ```
53
+ */
54
+ export declare function resolvePlacement(placement: FloatPlacement, size: FloatSize, viewport: FloatSize, offset?: number | Partial<FloatPoint>): FloatPoint;
55
+ /**
56
+ * Clamps a top-left position so the whole element stays inside the viewport
57
+ * (never even partially off-screen), honoring an optional edge `margin`.
58
+ *
59
+ * When the element is larger than the viewport on an axis, it is pinned to the
60
+ * leading edge (`margin`) on that axis.
61
+ *
62
+ * @param pos - The desired top-left position.
63
+ * @param size - The measured element size.
64
+ * @param viewport - The available viewport size.
65
+ * @param margin - Minimum gap kept from every edge (default `0`).
66
+ * @returns The clamped `{x,y}`.
67
+ */
68
+ export declare function clampToViewport(pos: FloatPoint, size: FloatSize, viewport: FloatSize, margin?: number): FloatPoint;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Pure (DOM-free) positioning helpers for the `Float` component.
3
+ *
4
+ * Kept separate from `Float.svelte` so the math can be unit-tested in the fast
5
+ * node test project (`*.test.ts`) without a browser.
6
+ */
7
+ /** All valid placements, handy for demos / iteration. */
8
+ export const FLOAT_PLACEMENTS = [
9
+ "top-left",
10
+ "top",
11
+ "top-right",
12
+ "left",
13
+ "center",
14
+ "right",
15
+ "bottom-left",
16
+ "bottom",
17
+ "bottom-right",
18
+ ];
19
+ /**
20
+ * Normalizes a scalar-or-`{x,y}` offset into an `{x,y}` pair.
21
+ *
22
+ * @param offset - A single number (applied to both axes) or an explicit pair.
23
+ * @returns The resolved `{x,y}` offset.
24
+ */
25
+ export function normalizeOffset(offset, fallback = 0) {
26
+ if (offset == null)
27
+ return { x: fallback, y: fallback };
28
+ if (typeof offset === "number")
29
+ return { x: offset, y: offset };
30
+ return { x: offset.x ?? fallback, y: offset.y ?? fallback };
31
+ }
32
+ /**
33
+ * Resolves a named {@link FloatPlacement} to an absolute top-left position so the
34
+ * element sits inside the viewport with `offset` as the edge gap.
35
+ *
36
+ * The returned position is guaranteed to be within `[0, viewport - size]` on
37
+ * each axis (it never needs further clamping).
38
+ *
39
+ * @param placement - The placement preset.
40
+ * @param size - The measured element size.
41
+ * @param viewport - The available viewport size.
42
+ * @param offset - Edge gap (number or `{x,y}`).
43
+ * @returns The resolved top-left `{x,y}`.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * resolvePlacement("top-right", { width: 200, height: 100 },
48
+ * { width: 1000, height: 800 }, { x: 16, y: 16 });
49
+ * // -> { x: 784, y: 16 }
50
+ * ```
51
+ */
52
+ export function resolvePlacement(placement, size, viewport, offset = 0) {
53
+ const off = normalizeOffset(offset);
54
+ const maxX = Math.max(0, viewport.width - size.width);
55
+ const maxY = Math.max(0, viewport.height - size.height);
56
+ // horizontal/vertical buckets
57
+ let horiz = "center";
58
+ let vert = "center";
59
+ for (const part of placement.split("-")) {
60
+ if (part === "left" || part === "right")
61
+ horiz = part;
62
+ else if (part === "top" || part === "bottom")
63
+ vert = part;
64
+ // "center" leaves the unspecified axis centered
65
+ }
66
+ const x = horiz === "left" ? off.x : horiz === "right" ? maxX - off.x : maxX / 2;
67
+ const y = vert === "top" ? off.y : vert === "bottom" ? maxY - off.y : maxY / 2;
68
+ // guard against offsets larger than the available space
69
+ return {
70
+ x: Math.min(Math.max(0, x), maxX),
71
+ y: Math.min(Math.max(0, y), maxY),
72
+ };
73
+ }
74
+ /**
75
+ * Clamps a top-left position so the whole element stays inside the viewport
76
+ * (never even partially off-screen), honoring an optional edge `margin`.
77
+ *
78
+ * When the element is larger than the viewport on an axis, it is pinned to the
79
+ * leading edge (`margin`) on that axis.
80
+ *
81
+ * @param pos - The desired top-left position.
82
+ * @param size - The measured element size.
83
+ * @param viewport - The available viewport size.
84
+ * @param margin - Minimum gap kept from every edge (default `0`).
85
+ * @returns The clamped `{x,y}`.
86
+ */
87
+ export function clampToViewport(pos, size, viewport, margin = 0) {
88
+ const maxX = Math.max(margin, viewport.width - size.width - margin);
89
+ const maxY = Math.max(margin, viewport.height - size.height - margin);
90
+ return {
91
+ x: Math.min(Math.max(margin, pos.x), maxX),
92
+ y: Math.min(Math.max(margin, pos.y), maxY),
93
+ };
94
+ }
@@ -0,0 +1,182 @@
1
+ /* ============================================================================
2
+ FLOAT COMPONENT TOKENS
3
+ Override globally: :root { --stuic-float-width: 360px; }
4
+ Override locally: <Float style="--stuic-float-header-bg: red;" />
5
+ Structural tokens (radius/shadow/border-width/transition) are resolved via the
6
+ fallback pattern at the usage site, so per-instance overrides keep working.
7
+ ============================================================================ */
8
+
9
+ :root {
10
+ /* Surface */
11
+ --stuic-float-bg: var(--stuic-color-background);
12
+ --stuic-float-text: var(--stuic-color-foreground);
13
+ --stuic-float-border-color: var(--stuic-color-border);
14
+
15
+ /* Stacking: floor + per-instance order offset (set inline by the component) */
16
+ --stuic-float-z: 50;
17
+
18
+ /* Sizing */
19
+ --stuic-float-width: 320px;
20
+ --stuic-float-body-max-height: 70vh;
21
+
22
+ /* Header */
23
+ --stuic-float-header-bg: var(--stuic-color-surface, var(--stuic-color-muted));
24
+ --stuic-float-header-text: var(
25
+ --stuic-color-surface-foreground,
26
+ var(--stuic-color-foreground)
27
+ );
28
+ --stuic-float-header-padding-x: 0.5rem;
29
+ --stuic-float-header-padding-y: 0.25rem;
30
+ --stuic-float-header-gap: 0.5rem;
31
+ --stuic-float-header-font-weight: var(--font-weight-medium);
32
+
33
+ /* Body */
34
+ --stuic-float-body-padding-x: 0.75rem;
35
+ --stuic-float-body-padding-y: 0.75rem;
36
+
37
+ /* Header buttons */
38
+ --stuic-float-btn-size: 1.75rem;
39
+ --stuic-float-btn-color: var(--stuic-color-muted-foreground);
40
+ --stuic-float-btn-color-hover: var(--stuic-color-foreground);
41
+ --stuic-float-btn-bg-hover: var(--stuic-color-muted);
42
+
43
+ /* Focus ring */
44
+ --stuic-float-ring-width: 2px;
45
+ --stuic-float-ring-color: var(--stuic-color-ring);
46
+ }
47
+
48
+ @layer components {
49
+ /* ============================================================================
50
+ ROOT
51
+ ============================================================================ */
52
+
53
+ .stuic-float {
54
+ position: fixed;
55
+ display: flex;
56
+ flex-direction: column;
57
+ width: var(--stuic-float-width);
58
+ max-width: 100vw;
59
+ z-index: calc(var(--stuic-float-z) + var(--stuic-float-z-order, 0));
60
+
61
+ background: var(--stuic-float-bg);
62
+ color: var(--stuic-float-text);
63
+ border: var(--stuic-float-border-width, var(--stuic-border-width)) solid
64
+ var(--stuic-float-border-color);
65
+ border-radius: var(--stuic-float-radius, var(--stuic-radius-container));
66
+ box-shadow: var(--stuic-float-shadow, var(--stuic-shadow-overlay));
67
+ overflow: hidden; /* clip header/body to the rounded corners */
68
+ }
69
+
70
+ /* ============================================================================
71
+ HEADER (drag handle)
72
+ ============================================================================ */
73
+
74
+ .stuic-float-header {
75
+ display: flex;
76
+ align-items: center;
77
+ gap: var(--stuic-float-header-gap);
78
+ padding: var(--stuic-float-header-padding-y) var(--stuic-float-header-padding-x);
79
+ background: var(--stuic-float-header-bg);
80
+ color: var(--stuic-float-header-text);
81
+ border-bottom: var(--stuic-float-border-width, var(--stuic-border-width)) solid
82
+ var(--stuic-float-border-color);
83
+ user-select: none;
84
+ touch-action: none; /* let the drag own the gesture on touch devices */
85
+ }
86
+
87
+ .stuic-float[data-draggable="true"] > .stuic-float-header {
88
+ cursor: grab;
89
+ }
90
+
91
+ .stuic-float[data-dragging="true"] > .stuic-float-header {
92
+ cursor: grabbing;
93
+ }
94
+
95
+ .stuic-float-icon {
96
+ display: inline-flex;
97
+ align-items: center;
98
+ flex-shrink: 0;
99
+ }
100
+
101
+ .stuic-float-title {
102
+ flex: 1 1 auto;
103
+ min-width: 0;
104
+ overflow: hidden;
105
+ text-overflow: ellipsis;
106
+ white-space: nowrap;
107
+ font-weight: var(--stuic-float-header-font-weight);
108
+ }
109
+
110
+ .stuic-float-actions {
111
+ display: flex;
112
+ align-items: center;
113
+ gap: 0.125rem;
114
+ flex-shrink: 0;
115
+ }
116
+
117
+ /* ============================================================================
118
+ HEADER BUTTONS
119
+ ============================================================================ */
120
+
121
+ .stuic-float-btn {
122
+ display: inline-flex;
123
+ align-items: center;
124
+ justify-content: center;
125
+ width: var(--stuic-float-btn-size);
126
+ height: var(--stuic-float-btn-size);
127
+ border: none;
128
+ border-radius: var(--stuic-float-btn-radius, var(--stuic-radius));
129
+ background: transparent;
130
+ color: var(--stuic-float-btn-color);
131
+ cursor: pointer;
132
+ transition:
133
+ background var(--stuic-float-transition, var(--stuic-transition)),
134
+ color var(--stuic-float-transition, var(--stuic-transition));
135
+ }
136
+
137
+ .stuic-float-btn:hover {
138
+ background: var(--stuic-float-btn-bg-hover);
139
+ color: var(--stuic-float-btn-color-hover);
140
+ }
141
+
142
+ .stuic-float-btn:focus-visible {
143
+ outline: var(--stuic-float-ring-width) solid var(--stuic-float-ring-color);
144
+ outline-offset: -2px;
145
+ }
146
+
147
+ .stuic-float-chevron {
148
+ display: inline-flex;
149
+ transition: transform var(--stuic-float-transition, var(--stuic-transition));
150
+ }
151
+
152
+ /* expanded -> chevron points up ("collapse"); minimized -> points down ("expand") */
153
+ .stuic-float[data-minimized="false"] .stuic-float-chevron {
154
+ transform: rotate(180deg);
155
+ }
156
+
157
+ /* ============================================================================
158
+ BODY (grid-based collapse, mirrors Accordion)
159
+ ============================================================================ */
160
+
161
+ .stuic-float-body-wrap {
162
+ display: grid;
163
+ grid-template-rows: 1fr;
164
+ transition: grid-template-rows var(--stuic-float-transition, var(--stuic-transition));
165
+ }
166
+
167
+ .stuic-float[data-minimized="true"] > .stuic-float-body-wrap {
168
+ grid-template-rows: 0fr;
169
+ }
170
+
171
+ .stuic-float-body-inner {
172
+ overflow: hidden;
173
+ min-height: 0;
174
+ }
175
+
176
+ .stuic-float-body {
177
+ max-height: var(--stuic-float-body-max-height);
178
+ overflow: auto;
179
+ scrollbar-width: thin;
180
+ padding: var(--stuic-float-body-padding-y) var(--stuic-float-body-padding-x);
181
+ }
182
+ }
@@ -0,0 +1,2 @@
1
+ export { default as Float, type Props as FloatProps } from "./Float.svelte";
2
+ export { type FloatPlacement, type FloatPoint, type FloatSize, FLOAT_PLACEMENTS, resolvePlacement, clampToViewport, normalizeOffset, } from "./float-utils.js";
@@ -0,0 +1,2 @@
1
+ export { default as Float } from "./Float.svelte";
2
+ export { FLOAT_PLACEMENTS, resolvePlacement, clampToViewport, normalizeOffset, } from "./float-utils.js";
package/dist/index.css CHANGED
@@ -76,6 +76,7 @@ In practice:
76
76
  @import "./components/DismissibleMessage/index.css";
77
77
  @import "./components/DropdownMenu/index.css";
78
78
  @import "./components/EmailVerifyForm/index.css";
79
+ @import "./components/Float/index.css";
79
80
  @import "./components/H/index.css";
80
81
  @import "./components/Header/index.css";
81
82
  @import "./components/ImageCycler/index.css";
package/dist/index.d.ts CHANGED
@@ -44,6 +44,7 @@ export * from "./components/DismissibleMessage/index.js";
44
44
  export * from "./components/Drawer/index.js";
45
45
  export * from "./components/DropdownMenu/index.js";
46
46
  export * from "./components/EmailVerifyForm/index.js";
47
+ export * from "./components/Float/index.js";
47
48
  export * from "./components/H/index.js";
48
49
  export * from "./components/Header/index.js";
49
50
  export * from "./components/ImageCycler/index.js";
package/dist/index.js CHANGED
@@ -45,6 +45,7 @@ export * from "./components/DismissibleMessage/index.js";
45
45
  export * from "./components/Drawer/index.js";
46
46
  export * from "./components/DropdownMenu/index.js";
47
47
  export * from "./components/EmailVerifyForm/index.js";
48
+ export * from "./components/Float/index.js";
48
49
  export * from "./components/H/index.js";
49
50
  export * from "./components/Header/index.js";
50
51
  export * from "./components/ImageCycler/index.js";