@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.
- package/dist/actions/draggable.svelte.d.ts +62 -0
- package/dist/actions/draggable.svelte.js +93 -0
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/components/AssetsPreview/_internal/AssetsPreviewContent.svelte +22 -15
- package/dist/components/Float/Float.fixture.svelte +26 -0
- package/dist/components/Float/Float.fixture.svelte.d.ts +4 -0
- package/dist/components/Float/Float.svelte +369 -0
- package/dist/components/Float/Float.svelte.d.ts +70 -0
- package/dist/components/Float/README.md +152 -0
- package/dist/components/Float/float-stack.d.ts +15 -0
- package/dist/components/Float/float-stack.js +28 -0
- package/dist/components/Float/float-utils.d.ts +68 -0
- package/dist/components/Float/float-utils.js +94 -0
- package/dist/components/Float/index.css +182 -0
- package/dist/components/Float/index.d.ts +2 -0
- package/dist/components/Float/index.js +2 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +11 -11
|
@@ -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
|
+
}
|
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";
|