hypnagaga/src/components/Framer/Resizer/index.svelte
hobbes7878 f6e4081024
framer
2025-04-18 11:25:38 +01:00

245 lines
5.6 KiB
Svelte

<script lang="ts">
import { faDesktop, faMobileAlt } from '@fortawesome/free-solid-svg-icons';
// @ts-ignore Temporary
import Fa from 'svelte-fa/src/fa.svelte';
import { width } from '../stores.js';
interface Props {
breakpoints?: number[];
maxFrameWidth?: number;
minFrameWidth?: number;
}
let {
breakpoints = [330, 510, 660, 930, 1200],
maxFrameWidth = 1200,
minFrameWidth = 320,
}: Props = $props();
let container: HTMLElement | undefined = $state();
const sliderWidth = 90;
let windowInnerWidth = $state(1200);
let minWidth = $derived(minFrameWidth);
let maxWidth = $derived(Math.min(windowInnerWidth - 70, maxFrameWidth));
let pixelRange = $derived(maxWidth - minWidth);
$effect(() => {
if ($width > maxWidth) width.set(maxWidth);
});
// svelte-ignore state_referenced_locally
let offset = $state(($width - minWidth) / pixelRange);
let sliding = $state(false);
let isFocused = $state(false);
const roundToNearestFive = (d: number) => Math.ceil(d / 5) * 5;
const getPx = () => Math.round(pixelRange * offset + minWidth);
let pixelLabel: null | number = $state(null);
const move = (e: MouseEvent) => {
if (!sliding || !container) return;
const { left } = container.getBoundingClientRect();
offset = Math.min(Math.max(0, e.pageX - left), sliderWidth) / sliderWidth;
pixelLabel = roundToNearestFive(getPx());
};
const handleKeyDown = (e: KeyboardEvent) => {
if (!isFocused) return;
const { keyCode } = e;
const pixelWidth = sliderWidth / pixelRange;
// right
if (keyCode === 39) {
offset = Math.min(1, offset + pixelWidth / sliderWidth);
// left
} else if (keyCode === 37) {
offset = Math.max(0, offset - pixelWidth / sliderWidth);
}
width.set(getPx());
};
const start = (e: MouseEvent) => {
sliding = true;
move(e);
};
const end = () => {
sliding = false;
pixelLabel = null;
width.set(roundToNearestFive(getPx()));
};
const onFocus = () => {
isFocused = true;
};
const onBlur = () => {
isFocused = false;
};
const increment = () => {
const availableBreakpoints = breakpoints
.filter((b) => b <= maxWidth)
.filter((b) => b > $width);
if (availableBreakpoints.length === 0) {
width.set(maxWidth);
} else {
width.set(availableBreakpoints[0]);
}
};
const decrement = () => {
const availableBreakpoints = breakpoints.filter((b) => b < $width);
if (availableBreakpoints.length === 0) {
width.set(minWidth);
} else {
width.set(availableBreakpoints.slice(-1)[0]);
}
};
</script>
<svelte:window
onmousemove={move}
onmouseup={end}
onkeydown={handleKeyDown}
bind:innerWidth={windowInnerWidth}
/>
<div id="resizer">
<div class="slider">
<div class="label" style={`opacity: ${sliding || isFocused ? 1 : 0};`}>
{pixelLabel || $width}px
</div>
<button
class="icon left"
disabled={$width === minWidth}
onclick={decrement}
onfocus={onFocus}
onmouseover={onFocus}
onmouseleave={onBlur}
>
<Fa icon={faMobileAlt} fw />
</button>
<div class="slider-container" bind:this={container}>
<div class="track"></div>
<div
class="handle"
tabindex="0"
role="button"
style="left: calc({offset * 100}% - 5px);"
onmousedown={start}
onfocus={onFocus}
onblur={onBlur}
></div>
</div>
<button
class="icon right"
disabled={$width === maxWidth}
onclick={increment}
onfocus={onFocus}
onmouseover={onFocus}
onmouseleave={onBlur}
>
<Fa icon={faDesktop} fw />
</button>
</div>
</div>
<style lang="scss">
#resizer {
width: 250px;
position: fixed;
bottom: 0px;
right: 0px;
padding: 15px;
.slider {
width: 100%;
display: flex;
justify-content: flex-end;
align-items: center;
& > div,
button {
display: inline-block;
}
}
div.label {
font-family: monospace;
font-size: 13px;
line-height: 13px;
text-align: center;
transition: opacity 0.2s;
color: grey;
background-color: rgba(255, 255, 255, 0.8);
padding: 4px 8px;
border-radius: 4px;
margin-right: 5px;
}
button.icon {
font-size: 14px;
line-height: 14px;
color: #bbb;
cursor: pointer;
background-color: transparent;
border: 0;
&:active,
&:focus {
outline: none;
}
&:hover {
color: #999;
}
&:active {
transform: translate(1px, 1px);
}
&[disabled] {
color: #ccc;
cursor: default;
&:hover {
color: #ccc;
}
&:active {
transform: translate(0px, 0px);
}
}
&.left {
text-align: right;
padding-right: 3px;
}
&.right {
padding-left: 6px;
text-align: left;
}
}
div.slider-container {
width: 90px;
height: 20px;
position: relative;
div.track {
height: 4px;
width: 100%;
position: absolute;
border-radius: 2px;
top: calc(50% - 2px);
background-color: lightgrey;
}
}
}
.handle {
z-index: 30;
width: 10px;
height: 20px;
cursor: ew-resize;
background: #bbb;
user-select: none;
position: absolute;
border-radius: 4px;
border: 1px solid grey;
top: calc(50% - 10px);
box-shadow:
0 10px 20px rgba(0, 0, 0, 0.19),
0 6px 6px rgba(0, 0, 0, 0.23);
&:active,
&:focus {
outline: none;
}
}
</style>