Add initial project
This commit is contained in:
1 parent
b3ad230739
commit
a79d639017
6 files changed
+1084
No files matched your search
@@ -0,0 +1,7 @@
|
||||
"NO ASSHOLE-WARE LICENSE"
|
||||
<daniel.stock@tooloop.de> wrote this file. As long as you retain this notice you
|
||||
can do whatever you want with this stuff. If we meet some day, and you think
|
||||
this stuff is worth it, you can buy me a beer.
|
||||
|
||||
Be nice to other people. Respect women. Don't be a dick. Don't support fascists.
|
||||
If you can't do that, don't use this code.
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Snappy</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
/* Transitional from https://modernfontstacks.com/ */
|
||||
font-family: Charter, 'Bitstream Charter', 'Sitka Text', Cambria, serif;
|
||||
font-weight: normal;
|
||||
font-size: 1.25rem;
|
||||
background-color: #f8f9fa;
|
||||
color: #343a40;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background-color: #212529;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 2rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.snappy-carousel {
|
||||
counter-reset: snappy-slide;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.snappy-slide {
|
||||
counter-increment: snappy-slide;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 33%;
|
||||
aspect-ratio: 3/2;
|
||||
max-height: 400px;
|
||||
|
||||
background-color: #f1f3f5;
|
||||
color: #343a40;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background-color: #495057;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
}
|
||||
|
||||
.snappy-slide:nth-child(even) {
|
||||
background-color: #e9ecef;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background-color: #343a40;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
}
|
||||
|
||||
.snappy-slide:before {
|
||||
content: counter(snappy-slide);
|
||||
}
|
||||
|
||||
.snappy-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.snappy-button {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
background-color: #f1f3f5;
|
||||
color: #339af0;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background-color: #dee2e6;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background-color: #495057;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background-color: #868e96;
|
||||
color: #1971c2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.snappy-pagination {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.snappy-pagination-dot {
|
||||
width: 0.66rem !important;
|
||||
height: 0.66rem !important;
|
||||
border-radius: 0.33rem;
|
||||
background-color: #dee2e6 !important;
|
||||
margin: 0 0.25rem;
|
||||
|
||||
transition-property: width, background;
|
||||
transition-duration: .8s;
|
||||
transition-timing-function: cubic-bezier(0.25, 1, 0.5, 1);
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.snappy-pagination-dot:hover {
|
||||
background-color: #adb5bd !important;
|
||||
}
|
||||
|
||||
.snappy-pagination-dot.is-active {
|
||||
background-color: #1971c2 !important;
|
||||
width: 2rem !important;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="dist/snappy.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<h1>Snappy</h1>
|
||||
|
||||
<!--
|
||||
Options
|
||||
-------
|
||||
|
||||
https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Scroll_snap
|
||||
|
||||
data-direction="x"
|
||||
data-direction="y"
|
||||
|
||||
data-gap="var(--size-1)"
|
||||
data-gap="10px"
|
||||
data-gap="1rem"
|
||||
|
||||
data-snap-stop="always"
|
||||
data-snap-stop="normal"
|
||||
|
||||
data-snap-align="center"
|
||||
data-snap-align="start"
|
||||
data-snap-align="end"
|
||||
-->
|
||||
<div id="demo-carousel" class="snappy-carousel" data-gap="1rem" data-snap-align="start">
|
||||
<div class="snappy-slide"></div>
|
||||
<div class="snappy-slide"></div>
|
||||
<div class="snappy-slide"></div>
|
||||
<div class="snappy-slide"></div>
|
||||
<div class="snappy-slide"></div>
|
||||
<div class="snappy-slide"></div>
|
||||
<div class="snappy-slide"></div>
|
||||
<div class="snappy-slide"></div>
|
||||
</div>
|
||||
|
||||
<div class="snappy-controls">
|
||||
<button class="snappy-button snappy-prev" data-carousel="demo-carousel">❮</button>
|
||||
<button class="snappy-button snappy-next" data-carousel="demo-carousel">❯</button>
|
||||
<div class="snappy-pagination" data-carousel="demo-carousel"></div>
|
||||
</div>
|
||||
|
||||
<!-- Javascript is only needed for pagination and buttons -->
|
||||
<script src="dist/snappy.js" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Generated
+499
@@ -0,0 +1,499 @@
|
||||
{
|
||||
"name": "snappy-carousel",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "snappy-carousel",
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
|
||||
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
|
||||
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
|
||||
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
|
||||
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
|
||||
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
|
||||
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
|
||||
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
|
||||
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
|
||||
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.12",
|
||||
"@esbuild/android-arm": "0.25.12",
|
||||
"@esbuild/android-arm64": "0.25.12",
|
||||
"@esbuild/android-x64": "0.25.12",
|
||||
"@esbuild/darwin-arm64": "0.25.12",
|
||||
"@esbuild/darwin-x64": "0.25.12",
|
||||
"@esbuild/freebsd-arm64": "0.25.12",
|
||||
"@esbuild/freebsd-x64": "0.25.12",
|
||||
"@esbuild/linux-arm": "0.25.12",
|
||||
"@esbuild/linux-arm64": "0.25.12",
|
||||
"@esbuild/linux-ia32": "0.25.12",
|
||||
"@esbuild/linux-loong64": "0.25.12",
|
||||
"@esbuild/linux-mips64el": "0.25.12",
|
||||
"@esbuild/linux-ppc64": "0.25.12",
|
||||
"@esbuild/linux-riscv64": "0.25.12",
|
||||
"@esbuild/linux-s390x": "0.25.12",
|
||||
"@esbuild/linux-x64": "0.25.12",
|
||||
"@esbuild/netbsd-arm64": "0.25.12",
|
||||
"@esbuild/netbsd-x64": "0.25.12",
|
||||
"@esbuild/openbsd-arm64": "0.25.12",
|
||||
"@esbuild/openbsd-x64": "0.25.12",
|
||||
"@esbuild/openharmony-arm64": "0.25.12",
|
||||
"@esbuild/sunos-x64": "0.25.12",
|
||||
"@esbuild/win32-arm64": "0.25.12",
|
||||
"@esbuild/win32-ia32": "0.25.12",
|
||||
"@esbuild/win32-x64": "0.25.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "snappy-carousel",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "esbuild src/snappy.js src/snappy.css --outdir=dist --minify",
|
||||
"watch": "esbuild src/snappy.js src/snappy.css --outdir=dist --minify --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
.snappy-carousel {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
user-select: none;
|
||||
cursor: grab;
|
||||
scroll-behavior: smooth;
|
||||
|
||||
&[data-direction="y"] {
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&.is-dragging {
|
||||
scroll-snap-type: none;
|
||||
scroll-behavior: unset;
|
||||
}
|
||||
|
||||
scroll-snap-type: x mandatory;
|
||||
|
||||
&[data-direction="y"] {
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scroll-snap-type: y mandatory;
|
||||
}
|
||||
|
||||
gap: var(--carousel-gap, 0);
|
||||
gap: attr(data-gap type(*), 0);
|
||||
|
||||
.snappy-slide {
|
||||
flex-shrink: 0;
|
||||
scroll-snap-align: center;
|
||||
scroll-snap-stop: normal;
|
||||
}
|
||||
|
||||
&[data-snap-align="start"] .snappy-slide {
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
&[data-snap-align="end"] .snappy-slide {
|
||||
scroll-snap-align: end;
|
||||
}
|
||||
|
||||
&[data-snap-stop="always"] .snappy-slide {
|
||||
scroll-snap-stop: always;
|
||||
}
|
||||
|
||||
}
|
||||
.snappy-pagination-dot {
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
background-color: silver;
|
||||
}
|
||||
+334
@@ -0,0 +1,334 @@
|
||||
/* use strict; */
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.snappy = new Snappy();
|
||||
});
|
||||
|
||||
class Snappy {
|
||||
carousels = [];
|
||||
|
||||
constructor() {
|
||||
document.querySelectorAll('.snappy-carousel').forEach(element => {
|
||||
this.carousels.push(new Carousel(element));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Carousel {
|
||||
// DOM references
|
||||
element = null;
|
||||
prevButton = null;
|
||||
nextButton = null;
|
||||
pagination = null;
|
||||
slides = [];
|
||||
paginationDots = [];
|
||||
|
||||
// Bahavioral constants
|
||||
inertia = 0.90; // Decay rate (0.9 = slower stop, 0.99 = longer slide)
|
||||
|
||||
// State variables
|
||||
isDragging = false;
|
||||
direction = 'x';
|
||||
startPosition = 0;
|
||||
startScrollPosition = 0;
|
||||
velocity = 0;
|
||||
lastPosition;
|
||||
lastTime;
|
||||
activeSlideIndex = 0;
|
||||
|
||||
constructor(element) {
|
||||
this.element = element;
|
||||
this.direction = this.element.dataset.direction === 'y' ? 'y' : 'x';
|
||||
|
||||
const gap = this.element.dataset.gap;
|
||||
if (gap) this.element.style.setProperty('--carousel-gap', gap);
|
||||
|
||||
this.slides = this.element.querySelectorAll('.snappy-slide');
|
||||
this.prevButton = document.querySelector(`.snappy-prev[data-carousel="${this.element.id}"]`);
|
||||
this.nextButton = document.querySelector(`.snappy-next[data-carousel="${this.element.id}"]`);
|
||||
this.pagination = document.querySelector(`.snappy-pagination[data-carousel="${this.element.id}"]`);
|
||||
|
||||
this.element.addEventListener('mousedown', this.startDrag.bind(this));
|
||||
this.element.addEventListener('mousemove', this.drag.bind(this));
|
||||
this.element.addEventListener('mouseup', this.endDrag.bind(this));
|
||||
this.element.addEventListener('mouseleave', this.endDrag.bind(this));
|
||||
this.element.addEventListener('touchstart', this.reset.bind(this));
|
||||
this.element.addEventListener('wheel', this.reset.bind(this));
|
||||
this.element.addEventListener('scroll', this.updateActiveSlide.bind(this));
|
||||
|
||||
if (this.prevButton) {
|
||||
this.prevButton.addEventListener('click', () => this.prev());
|
||||
}
|
||||
|
||||
if (this.nextButton) {
|
||||
this.nextButton.addEventListener('click', () => this.next());
|
||||
}
|
||||
|
||||
if (this.pagination) {
|
||||
this.createPagination();
|
||||
}
|
||||
|
||||
this.updateActiveSlide();
|
||||
}
|
||||
|
||||
startDrag(e) {
|
||||
this.isDragging = true;
|
||||
this.element.classList.add('is-dragging'); // Disable scroll snapping
|
||||
this.velocity = 0;
|
||||
this.lastPosition = undefined;
|
||||
this.lastTime = undefined;
|
||||
this.startPosition = this.getPointerPosition(e);
|
||||
this.startScrollPosition = this.getScrollPosition();
|
||||
e.preventDefault(); // Prevent text selection during drag
|
||||
}
|
||||
|
||||
drag(e) {
|
||||
if (!this.isDragging) return;
|
||||
e.preventDefault();
|
||||
const pointerPosition = this.getPointerPosition(e);
|
||||
const currentTime = Date.now();
|
||||
|
||||
// Calculate velocity (pixels per millisecond)
|
||||
if (this.lastTime !== undefined && currentTime > this.lastTime) {
|
||||
this.velocity = (pointerPosition - this.lastPosition) / (currentTime - this.lastTime);
|
||||
}
|
||||
|
||||
this.lastPosition = pointerPosition;
|
||||
this.lastTime = currentTime;
|
||||
|
||||
const distance = pointerPosition - this.startPosition;
|
||||
requestAnimationFrame(() => {
|
||||
this.setScrollPosition(this.startScrollPosition - distance);
|
||||
});
|
||||
}
|
||||
|
||||
endDrag() {
|
||||
if (!this.isDragging) return;
|
||||
this.isDragging = false;
|
||||
|
||||
// If the drag velocity is low at release, let the native scroll snap take over
|
||||
if (Math.abs(this.velocity) < 0.05) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
const target = this.calculateDragTarget();
|
||||
const element = this.element;
|
||||
const distance = target - this.getScrollPosition();
|
||||
|
||||
this.velocity = -distance * (1 - this.inertia) / 10;
|
||||
|
||||
const animateInertia = () => {
|
||||
if (this.isDragging) return; // Stop the animation if the user starts dragging again
|
||||
|
||||
const current = this.getScrollPosition();
|
||||
const remaining = target - current;
|
||||
|
||||
if (Math.abs(remaining) <= 0.1) {
|
||||
this.finishDrag(target);
|
||||
return;
|
||||
}
|
||||
|
||||
const delta = -this.velocity * 10;
|
||||
const next = Math.abs(delta) >= Math.abs(remaining)
|
||||
? target
|
||||
: current + delta;
|
||||
|
||||
this.setScrollPosition(next);
|
||||
|
||||
// Stop if the browser cannot represent a smaller scroll-position change.
|
||||
if (this.getScrollPosition() === current || this.getScrollPosition() === target) {
|
||||
this.finishDrag(target);
|
||||
return;
|
||||
}
|
||||
|
||||
this.velocity *= this.inertia;
|
||||
requestAnimationFrame(animateInertia);
|
||||
};
|
||||
|
||||
animateInertia();
|
||||
}
|
||||
|
||||
finishDrag(target) {
|
||||
this.setScrollPosition(target);
|
||||
this.reset();
|
||||
}
|
||||
|
||||
getPointerPosition(event) {
|
||||
return this.direction === 'y' ? event.clientY : event.clientX;
|
||||
}
|
||||
|
||||
getScrollPosition() {
|
||||
return this.direction === 'y' ? this.element.scrollTop : this.element.scrollLeft;
|
||||
}
|
||||
|
||||
setScrollPosition(position) {
|
||||
if (this.direction === 'y') {
|
||||
this.element.scrollTop = position;
|
||||
} else {
|
||||
this.element.scrollLeft = position;
|
||||
}
|
||||
}
|
||||
|
||||
calculateDragTarget() {
|
||||
const element = this.element;
|
||||
const isVertical = this.direction === 'y';
|
||||
const maxScrollPosition = isVertical
|
||||
? element.scrollHeight - element.clientHeight
|
||||
: element.scrollWidth - element.clientWidth;
|
||||
const clamp = value => Math.max(0, Math.min(value, maxScrollPosition));
|
||||
const cssPixels = value => Number.parseFloat(value) || 0;
|
||||
const startEdge = isVertical ? 'top' : 'left';
|
||||
const endEdge = isVertical ? 'bottom' : 'right';
|
||||
const paddingStart = isVertical ? 'scrollPaddingTop' : 'scrollPaddingLeft';
|
||||
const paddingEnd = isVertical ? 'scrollPaddingBottom' : 'scrollPaddingRight';
|
||||
const marginStart = isVertical ? 'scrollMarginTop' : 'scrollMarginLeft';
|
||||
const marginEnd = isVertical ? 'scrollMarginBottom' : 'scrollMarginRight';
|
||||
|
||||
// Project the final scroll position based on current velocity and inertia
|
||||
const projectedStop = clamp(
|
||||
this.getScrollPosition() - this.velocity * 10 / (1 - this.inertia)
|
||||
);
|
||||
|
||||
if (!this.slides.length) return clamp(projectedStop);
|
||||
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const elementStyle = getComputedStyle(element);
|
||||
const snapportStartEdge =
|
||||
elementRect[startEdge] + (isVertical ? element.clientTop : element.clientLeft);
|
||||
const clientExtent = isVertical ? element.clientHeight : element.clientWidth;
|
||||
const snapportStart =
|
||||
snapportStartEdge + cssPixels(elementStyle[paddingStart]);
|
||||
const snapportEnd =
|
||||
snapportStartEdge + clientExtent - cssPixels(elementStyle[paddingEnd]);
|
||||
|
||||
const targets = [...this.slides].map(slide => {
|
||||
const slideRect = slide.getBoundingClientRect();
|
||||
const slideStyle = getComputedStyle(slide);
|
||||
const snapAreaStart =
|
||||
slideRect[startEdge] - cssPixels(slideStyle[marginStart]);
|
||||
const snapAreaEnd =
|
||||
slideRect[endEdge] + cssPixels(slideStyle[marginEnd]);
|
||||
const alignments = slideStyle.scrollSnapAlign.trim().split(/\s+/);
|
||||
const align = isVertical ? alignments[0] : alignments[1] ?? alignments[0];
|
||||
const scrollPosition = this.getScrollPosition();
|
||||
|
||||
let target;
|
||||
|
||||
if (align === 'start') {
|
||||
target = scrollPosition + snapAreaStart - snapportStart;
|
||||
} else if (align === 'end') {
|
||||
target = scrollPosition + snapAreaEnd - snapportEnd;
|
||||
} else {
|
||||
const snapAreaCenter = (snapAreaStart + snapAreaEnd) / 2;
|
||||
const snapportCenter = (snapportStart + snapportEnd) / 2;
|
||||
target = scrollPosition + snapAreaCenter - snapportCenter;
|
||||
}
|
||||
|
||||
return clamp(target);
|
||||
});
|
||||
|
||||
return targets.reduce((nearest, target) =>
|
||||
Math.abs(target - projectedStop) < Math.abs(nearest - projectedStop)
|
||||
? target
|
||||
: nearest
|
||||
);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.isDragging = false;
|
||||
this.element.classList.remove('is-dragging');
|
||||
this.velocity = 0
|
||||
}
|
||||
|
||||
createPagination() {
|
||||
this.slides.forEach((slide, index) => {
|
||||
// Populate the pagination container with dots for each slide
|
||||
let div = document.createElement('div');
|
||||
div.classList.add('snappy-pagination-dot');
|
||||
div.addEventListener('click', () => this.goToSlide(index));
|
||||
this.pagination.appendChild(div);
|
||||
|
||||
// Store a reference to the pagination dots in the carousel object for later use
|
||||
if (!this.paginationDots) this.paginationDots = [];
|
||||
this.paginationDots.push(div);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
getActiveSlideIndex() {
|
||||
if (!this.slides.length) return -1;
|
||||
|
||||
const isVertical = this.direction === 'y';
|
||||
const startEdge = isVertical ? 'top' : 'left';
|
||||
const endEdge = isVertical ? 'bottom' : 'right';
|
||||
const paddingStart = isVertical ? 'scrollPaddingTop' : 'scrollPaddingLeft';
|
||||
const paddingEnd = isVertical ? 'scrollPaddingBottom' : 'scrollPaddingRight';
|
||||
const marginStart = isVertical ? 'scrollMarginTop' : 'scrollMarginLeft';
|
||||
const marginEnd = isVertical ? 'scrollMarginBottom' : 'scrollMarginRight';
|
||||
const cssPixels = value => Number.parseFloat(value) || 0;
|
||||
const containerRect = this.element.getBoundingClientRect();
|
||||
const containerStyle = getComputedStyle(this.element);
|
||||
const snapportStartEdge = containerRect[startEdge] +
|
||||
(isVertical ? this.element.clientTop : this.element.clientLeft);
|
||||
const clientExtent = isVertical ? this.element.clientHeight : this.element.clientWidth;
|
||||
const snapportStart = snapportStartEdge + cssPixels(containerStyle[paddingStart]);
|
||||
const snapportEnd = snapportStartEdge + clientExtent - cssPixels(containerStyle[paddingEnd]);
|
||||
const snapportCenter = (snapportStart + snapportEnd) / 2;
|
||||
|
||||
let activeIndex = 0;
|
||||
let closestDistance = Infinity;
|
||||
|
||||
this.slides.forEach((slide, index) => {
|
||||
const rect = slide.getBoundingClientRect();
|
||||
const slideStyle = getComputedStyle(slide);
|
||||
const snapAreaStart = rect[startEdge] - cssPixels(slideStyle[marginStart]);
|
||||
const snapAreaEnd = rect[endEdge] + cssPixels(slideStyle[marginEnd]);
|
||||
const alignments = slideStyle.scrollSnapAlign.trim().split(/\s+/);
|
||||
const align = isVertical ? alignments[0] : alignments[1] ?? alignments[0];
|
||||
|
||||
let distance;
|
||||
|
||||
if (align === 'start') {
|
||||
distance = Math.abs(snapAreaStart - snapportStart);
|
||||
} else if (align === 'end') {
|
||||
distance = Math.abs(snapAreaEnd - snapportEnd);
|
||||
} else {
|
||||
const snapAreaCenter = (snapAreaStart + snapAreaEnd) / 2;
|
||||
distance = Math.abs(snapAreaCenter - snapportCenter);
|
||||
}
|
||||
|
||||
if (distance < closestDistance) {
|
||||
activeIndex = index;
|
||||
closestDistance = distance;
|
||||
}
|
||||
});
|
||||
|
||||
return activeIndex;
|
||||
}
|
||||
|
||||
updateActiveSlide() {
|
||||
this.activeSlideIndex = this.getActiveSlideIndex();
|
||||
|
||||
this.slides.forEach((slide, index) => {
|
||||
slide.classList.toggle('is-active', index === this.activeSlideIndex);
|
||||
});
|
||||
|
||||
this.paginationDots.forEach((dot, index) => {
|
||||
dot.classList.toggle('is-active', index === this.activeSlideIndex);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
goToSlide(index) {
|
||||
this.activeSlideIndex = Math.max(0, Math.min(index, this.slides.length - 1));
|
||||
this.slides[this.activeSlideIndex].scrollIntoView();
|
||||
}
|
||||
|
||||
next() {
|
||||
this.goToSlide(this.activeSlideIndex + 1);
|
||||
}
|
||||
prev() {
|
||||
this.goToSlide(this.activeSlideIndex - 1);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user