vaalboks 0.1.0__tar.gz
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.
- vaalboks-0.1.0/LICENSE +21 -0
- vaalboks-0.1.0/PKG-INFO +140 -0
- vaalboks-0.1.0/README.md +115 -0
- vaalboks-0.1.0/pyproject.toml +65 -0
- vaalboks-0.1.0/setup.cfg +4 -0
- vaalboks-0.1.0/share/__init__.py +0 -0
- vaalboks-0.1.0/share/admin.py +1 -0
- vaalboks-0.1.0/share/apps.py +5 -0
- vaalboks-0.1.0/share/middleware.py +37 -0
- vaalboks-0.1.0/share/migrations/__init__.py +0 -0
- vaalboks-0.1.0/share/models.py +1 -0
- vaalboks-0.1.0/share/static/share/app.css +103 -0
- vaalboks-0.1.0/share/static/share/app.js +233 -0
- vaalboks-0.1.0/share/static/share/favicon.ico +0 -0
- vaalboks-0.1.0/share/static/share/favicon.svg +13 -0
- vaalboks-0.1.0/share/static/share/htmx.min.js +1873 -0
- vaalboks-0.1.0/share/static/share/logo.svg +20 -0
- vaalboks-0.1.0/share/templates/share/_file_list.html +27 -0
- vaalboks-0.1.0/share/templates/share/index.html +44 -0
- vaalboks-0.1.0/share/tests.py +44 -0
- vaalboks-0.1.0/share/urls.py +12 -0
- vaalboks-0.1.0/share/views.py +128 -0
- vaalboks-0.1.0/vaalboks/__init__.py +0 -0
- vaalboks-0.1.0/vaalboks/asgi.py +16 -0
- vaalboks-0.1.0/vaalboks/cli.py +150 -0
- vaalboks-0.1.0/vaalboks/gunicorn.py +31 -0
- vaalboks-0.1.0/vaalboks/settings.py +158 -0
- vaalboks-0.1.0/vaalboks/urls.py +24 -0
- vaalboks-0.1.0/vaalboks/wsgi.py +16 -0
- vaalboks-0.1.0/vaalboks.egg-info/PKG-INFO +140 -0
- vaalboks-0.1.0/vaalboks.egg-info/SOURCES.txt +33 -0
- vaalboks-0.1.0/vaalboks.egg-info/dependency_links.txt +1 -0
- vaalboks-0.1.0/vaalboks.egg-info/entry_points.txt +2 -0
- vaalboks-0.1.0/vaalboks.egg-info/requires.txt +7 -0
- vaalboks-0.1.0/vaalboks.egg-info/top_level.txt +2 -0
vaalboks-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vaalboks contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
vaalboks-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vaalboks
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Simple local-network file sharing over HTTPS
|
|
5
|
+
Author: vaalboks contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: django,file-sharing,lan,self-hosted
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Framework :: Django
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
|
|
14
|
+
Requires-Python: >=3.14
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: cryptography>=46.0.0
|
|
18
|
+
Requires-Dist: django>=6.1.1
|
|
19
|
+
Requires-Dist: gunicorn>=26.2.0
|
|
20
|
+
Requires-Dist: uvicorn>=0.52.4
|
|
21
|
+
Requires-Dist: uvicorn-worker>=0.4.0
|
|
22
|
+
Requires-Dist: whitenoise>=6.12.0
|
|
23
|
+
Requires-Dist: zstandard>=0.25.0
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# vaalboks
|
|
27
|
+
|
|
28
|
+
<p align="center">
|
|
29
|
+
<img src="share/static/share/logo.svg" alt="vaalboks logo" width="520">
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
Vaalboks is a small file-sharing server for a local network. Drop files or
|
|
33
|
+
folders in the browser, then download them from another device on the same
|
|
34
|
+
network.
|
|
35
|
+
|
|
36
|
+
The name is Afrikaans for “dull box” — a straightforward tool for moving files
|
|
37
|
+
between computers without a separate hosted service.
|
|
38
|
+
|
|
39
|
+
## Stack
|
|
40
|
+
|
|
41
|
+
- Python 3.14, Django 6.1, uv
|
|
42
|
+
- Frontend: plain HTML/CSS/JS + htmx 4.0
|
|
43
|
+
- Uploads use XHR for live progress and speed tracking; downloads use streaming
|
|
44
|
+
`fetch`
|
|
45
|
+
|
|
46
|
+
## Run
|
|
47
|
+
|
|
48
|
+
After installing the package, start the server with:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
uvx vaalboks
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This starts HTTPS on `0.0.0.0:8443` with two workers. Runtime data is stored
|
|
55
|
+
in `./vaalboks-data`, and a self-signed certificate is generated there on
|
|
56
|
+
first launch. Migrations and static-file collection run automatically. Use
|
|
57
|
+
`--http` for plain HTTP on port 8123. The CLI uses Gunicorn on Linux and macOS
|
|
58
|
+
and Uvicorn on Windows.
|
|
59
|
+
|
|
60
|
+
When running from this checkout:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
uv run python manage.py runserver 0.0.0.0:8123
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Then open `http://<your-LAN-IP>:8123/` from any device on the network.
|
|
67
|
+
(Find your IP with `ipconfig getifaddr en0`.)
|
|
68
|
+
|
|
69
|
+
## Use as a Django app
|
|
70
|
+
|
|
71
|
+
The file-sharing interface can also be mounted in an existing Django project.
|
|
72
|
+
Install `vaalboks`, add `share` to `INSTALLED_APPS`, configure the directory
|
|
73
|
+
used for uploads, and include the app URLs:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# settings.py
|
|
77
|
+
INSTALLED_APPS = [
|
|
78
|
+
# ...
|
|
79
|
+
"share",
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
VAALBOKS_SHARED_ROOT = BASE_DIR / "shared"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
# urls.py
|
|
87
|
+
from django.urls import include, path
|
|
88
|
+
|
|
89
|
+
urlpatterns = [
|
|
90
|
+
path("share/", include("share.urls")),
|
|
91
|
+
]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The app does not require the bundled `vaalboks` settings, middleware, or
|
|
95
|
+
server. The host project remains responsible for Django middleware, static
|
|
96
|
+
files, CSRF, and deployment configuration.
|
|
97
|
+
|
|
98
|
+
## Direct Gunicorn HTTPS + zstd
|
|
99
|
+
|
|
100
|
+
This section applies to Linux and macOS. Windows users should use the `vaalboks`
|
|
101
|
+
command, which selects Uvicorn automatically.
|
|
102
|
+
|
|
103
|
+
Gunicorn can terminate HTTPS, and Django compresses eligible text responses
|
|
104
|
+
with zstd:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
uv run gunicorn vaalboks.asgi:application \
|
|
108
|
+
--worker-class uvicorn_worker.UvicornWorker \
|
|
109
|
+
--workers 2 \
|
|
110
|
+
--bind 0.0.0.0:8443 \
|
|
111
|
+
--certfile certs/vaalboks-cert.pem \
|
|
112
|
+
--keyfile certs/vaalboks-key.pem
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Open `https://<your-LAN-IP>:8443/` and trust the self-signed certificate on
|
|
116
|
+
each device that will connect. This setup provides HTTPS and zstd compression.
|
|
117
|
+
|
|
118
|
+
The CLI also accepts `--host`, `--port`, `--workers`, `--data-dir`,
|
|
119
|
+
`--certfile`, and `--keyfile`.
|
|
120
|
+
|
|
121
|
+
## Publishing
|
|
122
|
+
|
|
123
|
+
Build artifacts locally with:
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
uv build
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The repository includes GitHub Actions for quality checks and publishing on a
|
|
130
|
+
published GitHub release. Configure PyPI trusted publishing for the GitHub
|
|
131
|
+
repository before creating the first release; no long-lived PyPI token is
|
|
132
|
+
required.
|
|
133
|
+
|
|
134
|
+
## How it works
|
|
135
|
+
|
|
136
|
+
- `GET /` — single page with drop zone and live file listing
|
|
137
|
+
- `GET /api/files/` — htmx partial that re-renders the listing after uploads
|
|
138
|
+
- `POST /api/upload/` — multipart upload; folders are traversed client-side
|
|
139
|
+
(`webkitGetAsEntry`) and each file is sent with its relative path
|
|
140
|
+
- `GET /files/<path>` — download a shared file (path-traversal protected)
|
vaalboks-0.1.0/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# vaalboks
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="share/static/share/logo.svg" alt="vaalboks logo" width="520">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
Vaalboks is a small file-sharing server for a local network. Drop files or
|
|
8
|
+
folders in the browser, then download them from another device on the same
|
|
9
|
+
network.
|
|
10
|
+
|
|
11
|
+
The name is Afrikaans for “dull box” — a straightforward tool for moving files
|
|
12
|
+
between computers without a separate hosted service.
|
|
13
|
+
|
|
14
|
+
## Stack
|
|
15
|
+
|
|
16
|
+
- Python 3.14, Django 6.1, uv
|
|
17
|
+
- Frontend: plain HTML/CSS/JS + htmx 4.0
|
|
18
|
+
- Uploads use XHR for live progress and speed tracking; downloads use streaming
|
|
19
|
+
`fetch`
|
|
20
|
+
|
|
21
|
+
## Run
|
|
22
|
+
|
|
23
|
+
After installing the package, start the server with:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
uvx vaalboks
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This starts HTTPS on `0.0.0.0:8443` with two workers. Runtime data is stored
|
|
30
|
+
in `./vaalboks-data`, and a self-signed certificate is generated there on
|
|
31
|
+
first launch. Migrations and static-file collection run automatically. Use
|
|
32
|
+
`--http` for plain HTTP on port 8123. The CLI uses Gunicorn on Linux and macOS
|
|
33
|
+
and Uvicorn on Windows.
|
|
34
|
+
|
|
35
|
+
When running from this checkout:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
uv run python manage.py runserver 0.0.0.0:8123
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Then open `http://<your-LAN-IP>:8123/` from any device on the network.
|
|
42
|
+
(Find your IP with `ipconfig getifaddr en0`.)
|
|
43
|
+
|
|
44
|
+
## Use as a Django app
|
|
45
|
+
|
|
46
|
+
The file-sharing interface can also be mounted in an existing Django project.
|
|
47
|
+
Install `vaalboks`, add `share` to `INSTALLED_APPS`, configure the directory
|
|
48
|
+
used for uploads, and include the app URLs:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# settings.py
|
|
52
|
+
INSTALLED_APPS = [
|
|
53
|
+
# ...
|
|
54
|
+
"share",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
VAALBOKS_SHARED_ROOT = BASE_DIR / "shared"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
# urls.py
|
|
62
|
+
from django.urls import include, path
|
|
63
|
+
|
|
64
|
+
urlpatterns = [
|
|
65
|
+
path("share/", include("share.urls")),
|
|
66
|
+
]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The app does not require the bundled `vaalboks` settings, middleware, or
|
|
70
|
+
server. The host project remains responsible for Django middleware, static
|
|
71
|
+
files, CSRF, and deployment configuration.
|
|
72
|
+
|
|
73
|
+
## Direct Gunicorn HTTPS + zstd
|
|
74
|
+
|
|
75
|
+
This section applies to Linux and macOS. Windows users should use the `vaalboks`
|
|
76
|
+
command, which selects Uvicorn automatically.
|
|
77
|
+
|
|
78
|
+
Gunicorn can terminate HTTPS, and Django compresses eligible text responses
|
|
79
|
+
with zstd:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
uv run gunicorn vaalboks.asgi:application \
|
|
83
|
+
--worker-class uvicorn_worker.UvicornWorker \
|
|
84
|
+
--workers 2 \
|
|
85
|
+
--bind 0.0.0.0:8443 \
|
|
86
|
+
--certfile certs/vaalboks-cert.pem \
|
|
87
|
+
--keyfile certs/vaalboks-key.pem
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Open `https://<your-LAN-IP>:8443/` and trust the self-signed certificate on
|
|
91
|
+
each device that will connect. This setup provides HTTPS and zstd compression.
|
|
92
|
+
|
|
93
|
+
The CLI also accepts `--host`, `--port`, `--workers`, `--data-dir`,
|
|
94
|
+
`--certfile`, and `--keyfile`.
|
|
95
|
+
|
|
96
|
+
## Publishing
|
|
97
|
+
|
|
98
|
+
Build artifacts locally with:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
uv build
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The repository includes GitHub Actions for quality checks and publishing on a
|
|
105
|
+
published GitHub release. Configure PyPI trusted publishing for the GitHub
|
|
106
|
+
repository before creating the first release; no long-lived PyPI token is
|
|
107
|
+
required.
|
|
108
|
+
|
|
109
|
+
## How it works
|
|
110
|
+
|
|
111
|
+
- `GET /` — single page with drop zone and live file listing
|
|
112
|
+
- `GET /api/files/` — htmx partial that re-renders the listing after uploads
|
|
113
|
+
- `POST /api/upload/` — multipart upload; folders are traversed client-side
|
|
114
|
+
(`webkitGetAsEntry`) and each file is sent with its relative path
|
|
115
|
+
- `GET /files/<path>` — download a shared file (path-traversal protected)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "vaalboks"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Simple local-network file sharing over HTTPS"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.14"
|
|
9
|
+
authors = [{ name = "vaalboks contributors" }]
|
|
10
|
+
keywords = ["django", "file-sharing", "lan", "self-hosted"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Environment :: Console",
|
|
14
|
+
"Framework :: Django",
|
|
15
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
16
|
+
"Programming Language :: Python :: 3.14",
|
|
17
|
+
"Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"cryptography>=46.0.0",
|
|
21
|
+
"django>=6.1.1",
|
|
22
|
+
"gunicorn>=26.2.0",
|
|
23
|
+
"uvicorn>=0.52.4",
|
|
24
|
+
"uvicorn-worker>=0.4.0",
|
|
25
|
+
"whitenoise>=6.12.0",
|
|
26
|
+
"zstandard>=0.25.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
vaalboks = "vaalboks.cli:main"
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["setuptools>=80"]
|
|
34
|
+
build-backend = "setuptools.build_meta"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
include = ["vaalboks", "vaalboks.*", "share", "share.*"]
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.package-data]
|
|
40
|
+
share = ["templates/**/*.html", "static/**/*"]
|
|
41
|
+
|
|
42
|
+
[tool.ruff]
|
|
43
|
+
target-version = "py314"
|
|
44
|
+
line-length = 100
|
|
45
|
+
extend-exclude = ["shared", "vaalboks-data", "build", "certs"]
|
|
46
|
+
|
|
47
|
+
#[tool.ruff.lint]
|
|
48
|
+
#select = ["E", "F", "I", "B", "UP", "RUF"]
|
|
49
|
+
|
|
50
|
+
[tool.ruff.format]
|
|
51
|
+
quote-style = "double"
|
|
52
|
+
indent-style = "space"
|
|
53
|
+
|
|
54
|
+
[tool.ty.environment]
|
|
55
|
+
python-version = "3.14"
|
|
56
|
+
|
|
57
|
+
[tool.ty.src]
|
|
58
|
+
include = ["manage.py", "vaalboks", "share"]
|
|
59
|
+
exclude = ["shared/**", "vaalboks-data/**", "build/**"]
|
|
60
|
+
|
|
61
|
+
[dependency-groups]
|
|
62
|
+
dev = [
|
|
63
|
+
"ruff>=0.16.6",
|
|
64
|
+
"ty>=0.0.78",
|
|
65
|
+
]
|
vaalboks-0.1.0/setup.cfg
ADDED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Register your models here.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import zstandard
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ZstdMiddleware:
|
|
5
|
+
"""Compress eligible non-streaming responses when the client supports zstd."""
|
|
6
|
+
|
|
7
|
+
min_size = 1024
|
|
8
|
+
|
|
9
|
+
def __init__(self, get_response):
|
|
10
|
+
self.get_response = get_response
|
|
11
|
+
self.compressor = zstandard.ZstdCompressor(level=3)
|
|
12
|
+
|
|
13
|
+
def __call__(self, request):
|
|
14
|
+
response = self.get_response(request)
|
|
15
|
+
accepts_zstd = "zstd" in request.headers.get("Accept-Encoding", "").lower()
|
|
16
|
+
content_type = response.headers.get("Content-Type", "").lower()
|
|
17
|
+
|
|
18
|
+
if (
|
|
19
|
+
accepts_zstd
|
|
20
|
+
and not response.streaming
|
|
21
|
+
and response.status_code not in (204, 304)
|
|
22
|
+
and "Content-Encoding" not in response.headers
|
|
23
|
+
and (
|
|
24
|
+
content_type.startswith("text/")
|
|
25
|
+
or "javascript" in content_type
|
|
26
|
+
or "json" in content_type
|
|
27
|
+
or "xml" in content_type
|
|
28
|
+
)
|
|
29
|
+
):
|
|
30
|
+
body = b"".join(response)
|
|
31
|
+
if len(body) >= self.min_size:
|
|
32
|
+
response.content = self.compressor.compress(body)
|
|
33
|
+
response.headers["Content-Encoding"] = "zstd"
|
|
34
|
+
response.headers.pop("Content-Length", None)
|
|
35
|
+
response.headers["Vary"] = "Accept-Encoding"
|
|
36
|
+
|
|
37
|
+
return response
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Create your models here.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light dark;
|
|
3
|
+
--bg: light-dark(#f6f7f9, #14161a);
|
|
4
|
+
--panel: light-dark(#ffffff, #1d2026);
|
|
5
|
+
--border: light-dark(#e2e5ea, #30343d);
|
|
6
|
+
--accent: #4f7cff;
|
|
7
|
+
--text: light-dark(#1b1e24, #e8eaee);
|
|
8
|
+
--muted: light-dark(#6b7280, #9aa1ad);
|
|
9
|
+
}
|
|
10
|
+
* { box-sizing: border-box; }
|
|
11
|
+
body {
|
|
12
|
+
margin: 0;
|
|
13
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
14
|
+
background: var(--bg);
|
|
15
|
+
color: var(--text);
|
|
16
|
+
display: flex;
|
|
17
|
+
justify-content: center;
|
|
18
|
+
padding: 2rem 1rem;
|
|
19
|
+
}
|
|
20
|
+
main { width: min(720px, 100%); }
|
|
21
|
+
.brand-logo {
|
|
22
|
+
display: block;
|
|
23
|
+
width: min(100%, 360px);
|
|
24
|
+
height: auto;
|
|
25
|
+
margin: 0 auto 1.25rem;
|
|
26
|
+
}
|
|
27
|
+
h1 { font-size: 1.05rem; margin: 0 0 1.25rem; }
|
|
28
|
+
h1 .subtitle { color: var(--muted); font-weight: 400; }
|
|
29
|
+
#dropzone {
|
|
30
|
+
border: 2px dashed var(--border);
|
|
31
|
+
border-radius: 12px;
|
|
32
|
+
background: var(--panel);
|
|
33
|
+
padding: 3rem 1.5rem;
|
|
34
|
+
text-align: center;
|
|
35
|
+
color: var(--muted);
|
|
36
|
+
transition: border-color .15s, background .15s;
|
|
37
|
+
cursor: pointer;
|
|
38
|
+
}
|
|
39
|
+
#dropzone.dragover {
|
|
40
|
+
border-color: var(--accent);
|
|
41
|
+
background: color-mix(in srgb, var(--accent) 8%, var(--panel));
|
|
42
|
+
color: var(--text);
|
|
43
|
+
}
|
|
44
|
+
#dropzone p { margin: .35rem 0; }
|
|
45
|
+
#dropzone .big { font-size: 1.05rem; color: var(--text); }
|
|
46
|
+
#progress-wrap { margin-top: 1rem; display: none; }
|
|
47
|
+
#progress-wrap.active { display: block; }
|
|
48
|
+
progress { width: 100%; height: 8px; }
|
|
49
|
+
#progress-label { font-size: .85rem; color: var(--muted); margin-top: .3rem; }
|
|
50
|
+
#stats {
|
|
51
|
+
margin-top: 1rem;
|
|
52
|
+
display: flex;
|
|
53
|
+
gap: 1.25rem;
|
|
54
|
+
font-size: .82rem;
|
|
55
|
+
color: var(--muted);
|
|
56
|
+
font-variant-numeric: tabular-nums;
|
|
57
|
+
}
|
|
58
|
+
#stats b { color: var(--text); font-weight: 600; }
|
|
59
|
+
#file-list {
|
|
60
|
+
margin-top: 1.5rem;
|
|
61
|
+
background: var(--panel);
|
|
62
|
+
border: 1px solid var(--border);
|
|
63
|
+
border-radius: 12px;
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
}
|
|
66
|
+
#file-list:empty::after {
|
|
67
|
+
content: "Nothing shared yet — drop some files above.";
|
|
68
|
+
display: block;
|
|
69
|
+
padding: 1.5rem;
|
|
70
|
+
color: var(--muted);
|
|
71
|
+
text-align: center;
|
|
72
|
+
}
|
|
73
|
+
.row {
|
|
74
|
+
display: flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
gap: .6rem;
|
|
77
|
+
padding: .55rem 1rem;
|
|
78
|
+
border-top: 1px solid var(--border);
|
|
79
|
+
font-size: .92rem;
|
|
80
|
+
}
|
|
81
|
+
.row:first-child { border-top: none; }
|
|
82
|
+
.folder-toggle {
|
|
83
|
+
width: 1.1em;
|
|
84
|
+
padding: 0;
|
|
85
|
+
border: 0;
|
|
86
|
+
background: none;
|
|
87
|
+
color: var(--muted);
|
|
88
|
+
font: inherit;
|
|
89
|
+
font-size: 1.3rem;
|
|
90
|
+
line-height: 1;
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
transition: transform .15s;
|
|
93
|
+
}
|
|
94
|
+
.folder-toggle[aria-expanded="true"] { transform: rotate(90deg); }
|
|
95
|
+
.folder-toggle.empty { visibility: hidden; cursor: default; }
|
|
96
|
+
.children { display: none; padding-left: 1.4rem; }
|
|
97
|
+
.children.open { display: block; }
|
|
98
|
+
.row .icon { width: 1.1em; text-align: center; }
|
|
99
|
+
.row .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
100
|
+
.row .size { margin-left: auto; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
101
|
+
.row a { color: var(--accent); text-decoration: none; }
|
|
102
|
+
.row a:hover { text-decoration: underline; }
|
|
103
|
+
.row.dir { color: var(--muted); }
|