This commit is contained in:
Shahrad Elahi 2024-05-29 16:23:25 +03:30
parent 9ee534f2bb
commit ae787625b9
No known key found for this signature in database
170 changed files with 2434 additions and 2643 deletions

View File

@ -1,5 +1,5 @@
---
'wireadmin': patch
"wireadmin": patch
---
fix: Improve password hashing method and env loader

View File

@ -0,0 +1,5 @@
---
"wireadmin": major
---
BREAKING: `UI_PASSWORD` has been removed. Please use `ADMIN_PASSWORD` instead.

View File

@ -1,5 +1,5 @@
---
'wireadmin': patch
"wireadmin": patch
---
fix: tor config generation when container restarts

View File

@ -9,4 +9,4 @@ Dockerfile
*.md
tests/
*.log
tmp/
tmp/

View File

@ -1,7 +1,8 @@
.DS_Store
node_modules
/build
web
build
dist
.svelte-kit
/package
.env
.env.*

View File

@ -1,9 +1,11 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"useTabs": false,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": true,
"trailingComma": "all",
"trailingComma": "es5",
"endOfLine": "lf",
"printWidth": 100,
"overrides": [
{
@ -12,6 +14,19 @@
"parser": "markdown",
"printWidth": 79
}
},
{
"files": "*.svelte",
"options": {
"parser": "svelte",
"plugins": ["prettier-plugin-svelte"]
}
}
],
"importOrder": ["<THIRD_PARTY_MODULES>", "", "^types$", "^@lib/(.*)$", "^@/(.*)$", "", "^[./]"],
"plugins": [
"@ianvs/prettier-plugin-sort-imports",
"prettier-plugin-tailwindcss",
"prettier-plugin-sh"
]
}

View File

@ -1,95 +1,65 @@
ARG ALPINE_VERSION=3.19
ARG NODE_VERSION=20
ARG VERSION=0.0.0-canary
FROM --platform=$BUILDPLATFORM chriswayg/tor-alpine:latest as tor
FROM --platform=$BUILDPLATFORM node:${NODE_VERSION}-alpine${ALPINE_VERSION} as base
LABEL Maintainer="Shahrad Elahi <https://github.com/shahradelahi>"
WORKDIR /app
FROM --platform=$BUILDPLATFORM shahradel/torproxy:latest as tor
FROM --platform=$BUILDPLATFORM node:${NODE_VERSION}-alpine${ALPINE_VERSION} as node
ENV TZ=UTC
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN apk update \
&& apk upgrade \
&& rm -rf /var/cache/apk/*
COPY --from=tor /usr/local/bin/obfs4proxy /usr/local/bin/obfs4proxy
COPY --from=tor /usr/local/bin/meek-server /usr/local/bin/meek-server
# Install required packages
FROM node as base
RUN apk add -U --no-cache \
iproute2 iptables net-tools \
screen curl bash \
iptables net-tools \
screen bash \
wireguard-tools \
tor &&\
# NPM packages
npm install -g @litehex/node-checksum@0.2 &&\
# Clear APK cache
rm -rf /var/cache/apk/*
tor \
&& rm -rf /var/cache/apk/*
FROM base AS build
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
COPY web .
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile \
&& NODE_ENV=production pnpm build \
&& pnpm prune --prod \
&& mv node_modules /tmp/node_modules \
&& mv build /tmp/build \
&& rm -rf ./*
FROM base
WORKDIR /app
COPY --from=tor /usr/local/bin/lyrebird /usr/local/bin/lyrebird
COPY /config/torrc.template /etc/tor/torrc.template
# Copy user scripts
COPY /bin /usr/local/bin
RUN chmod -R +x /usr/local/bin
COPY web/package.json web/pnpm-lock.yaml ./
# Base env
ENV PROTOCOL_HEADER=x-forwarded-proto
ENV HOST_HEADER=x-forwarded-host
FROM base AS build
# Setup Pnpm - Pnpm only used for build stage
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
COPY web .
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile \
# build
&& mkdir -p /data \
&& echo gA== > /data/storage.b64 \
&& NODE_ENV=production pnpm run build \
# Omit devDependencies
&& pnpm prune --prod \
# Move the goods to a temporary location
&& mv node_modules /tmp/node_modules \
&& mv build /tmp/build \
# Remove everything else
&& rm -rf ./*
FROM base AS release
ENV NODE_ENV=production
ENV LOG_LEVEL=error
# Copy the goods from the build stage
COPY --from=build /tmp/package.json package.json
COPY --from=build /tmp/node_modules node_modules
COPY --from=build /tmp/build build
# Fix permissions
RUN mkdir -p /data && chmod 700 /data
RUN mkdir -p /etc/torrc.d && chmod -R 400 /etc/torrc.d
RUN mkdir -p /var/vlogs && touch /var/vlogs/web && chmod -R 600 /var/vlogs
ENV NODE_ENV=production
ENV LOG_LEVEL=error
RUN mkdir -p /etc/tor/torrc.d && chmod -R 400 /etc/tor/torrc.d
RUN mkdir -p /var/log/wireadmin && touch /var/log/wireadmin/web.log
# Setup entrypoint
COPY docker-entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
# Healthcheck
HEALTHCHECK --interval=60s --timeout=3s --start-period=20s --retries=3 \
CMD curl -f http://127.0.0.1:3000/api/health || exit 1
# Volumes
VOLUME ["/etc/torrc.d", "/data", "/var/vlogs"]
# Overwrite package version
RUN node -e "const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('/app/package.json')); pkg.version = process.env.VERSION; fs.writeFileSync('/app/package.json', JSON.stringify(pkg, null, 2));"
VOLUME ["/etc/tor", "/var/lib/tor", "/data"]
# Run the app
EXPOSE 3000/tcp
CMD [ "npm", "run", "start" ]
CMD [ "node", "./build/index.js" ]

View File

@ -1,57 +1,48 @@
ARG ALPINE_VERSION=3.19
ARG NODE_VERSION=20
ARG VERSION=0.0.0-dev
FROM --platform=$BUILDPLATFORM chriswayg/tor-alpine:latest as tor
FROM --platform=$BUILDPLATFORM node:${NODE_VERSION}-alpine${ALPINE_VERSION} as base
LABEL Maintainer="Shahrad Elahi <https://github.com/shahradelahi>"
WORKDIR /app
FROM --platform=$BUILDPLATFORM shahradel/torproxy:latest as tor
FROM --platform=$BUILDPLATFORM node:${NODE_VERSION}-alpine${ALPINE_VERSION} as node
ENV TZ=UTC
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN apk update \
&& apk upgrade \
&& rm -rf /var/cache/apk/*
COPY --from=tor /usr/local/bin/obfs4proxy /usr/local/bin/obfs4proxy
COPY --from=tor /usr/local/bin/meek-server /usr/local/bin/meek-server
# Install required packages
FROM node as base
RUN apk add -U --no-cache \
iproute2 iptables net-tools \
screen vim curl bash \
iptables net-tools \
screen logrotate vim bash \
wireguard-tools \
tor &&\
# NPM packages
npm install -g @litehex/node-checksum@0.2 &&\
# Clear APK cache
rm -rf /var/cache/apk/*
# Copy Tor Configs
COPY /config/torrc.template /etc/tor/torrc.template
COPY /config/obfs4-bridges.conf /etc/torrc.d/obfs4-bridges.conf
# Copy user scripts
COPY /bin /usr/local/bin
RUN chmod -R +x /usr/local/bin
FROM base
WORKDIR /app
# Setup Pnpm
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
# Base env
ENV NODE_ENV=development
ENV LOG_LEVEL=debug
ENV PROTOCOL_HEADER=x-forwarded-proto
ENV HOST_HEADER=x-forwarded-host
COPY --from=tor /usr/local/bin/lyrebird /usr/local/bin/lyrebird
FROM base AS runner
ENV NODE_ENV=development
ENV LOG_LEVEL=debug
# Copy Tor Configs
COPY /config/torrc.template /etc/tor/torrc.template
COPY /config/obfs4-bridges.conf /etc/tor/torrc.d/obfs4-bridges.conf
# Fix permissions
RUN mkdir -p /data && chmod 700 /data
RUN mkdir -p /etc/torrc.d && chmod -R 400 /etc/torrc.d
RUN mkdir -p /var/vlogs && touch /var/vlogs/web && chmod -R 600 /var/vlogs
RUN mkdir -p /etc/tor/torrc.d && chmod -R 400 /etc/tor/torrc.d
RUN mkdir -p /var/log/wireadmin && touch /var/log/wireadmin/web.log
RUN echo '* * * * * /usr/bin/env logrotate /etc/logrotate.d/rotator' > /etc/crontabs/root
# Setup entrypoint
COPY docker-entrypoint.sh /entrypoint.sh
@ -59,7 +50,7 @@ RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
# Volumes
VOLUME ["/etc/torrc.d", "/data", "/var/vlogs"]
VOLUME ["/etc/tor", "/var/lib/tor", "/data"]
# Run the app
EXPOSE 5173/tcp

138
README.md
View File

@ -1,6 +1,6 @@
# WireGuard (Easy Admin UI)
[![CI](https://github.com/shahradelahi/wireadmin/actions/workflows/ci.yml/badge.svg)](https://github.com/shahradelahi/wireadmin/actions/workflows/ci.yml)
[![CI](https://github.com/wireadmin/wireadmin/actions/workflows/ci.yml/badge.svg)](https://github.com/wireadmin/wireadmin/actions/workflows/ci.yml)
[![GPL-3.0 Licensed](https://img.shields.io/badge/License-GPL3.0-blue.svg?style=flat)](https://opensource.org/licenses/GPL-3.0)
![Screenshot](assets/screenshot-1.png)
@ -9,6 +9,21 @@
| :----------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------: |
| <img src="assets/screenshot-2.png" alt="screenshot" style="width:100%;max-height:300px;"/> | <img src="assets/screenshot-4.png" alt="screenshot" style="width:100%;max-height:300px;"/> | <img src="assets/screenshot-3.png" alt="screenshot" style="width:100%;max-height:300px;"/> |
---
- [Features](#features)
- [Build locally](#build-locally)
- [Image](#image)
- [Ports](#ports)
- [Usage](#usage)
- [Docker Compose](#docker-compose)
- [Command line](#command-line)
- [Persistent Data](#persistent-data)
- [Environment variables](#environment-variables)
- [Upgrade](#upgrade)
- [Contributing](#contributing)
- [License](#license)
## Features
- Simple and friendly UI
@ -20,61 +35,44 @@
- Easily download the client configurations.
- Automatic Light/Dark Mode
## Installation
## Image
### 1. Prerequisites
| Registry | Image |
| ------------------------------------------------------------------------------------------------------- | ----------------------------- |
| [GitHub Container Registry](https://github.com/users/shahradelahi/packages/container/package/cfw-proxy) | `ghcr.io/wireadmin/wireadmin` |
- [Docker Engine](https://docs.docker.com/engine/install/)
## Ports
### 2. Docker Image
- `3000`: WebUI
#### Build from source (recommended)
And for any additional ports of WireGuard instance, should be exposed through Docker.
## Usage
### Docker Compose
Docker compose is the recommended way to run this image. You can use the following
[docker compose template](docker-compose.yml), then run the container:
```bash
git clone https://github.com/shahradelahi/wireadmin
docker buildx build --tag litehex/wireadmin ./wireadmin
docker compose up -d
docker compose logs -f
```
#### Pull from Docker Hub
```bash
docker pull litehex/wireadmin # OR ghcr.io/shahradelahi/wireadmin
```
### 3. Persistent Data
WireAdmin store configurations at `/data`. It's important to mount a volume at this location to ensure that
your data is not lost during container restarts or updates.
#### Create a docker volume
```bash
docker volume create wireadmin-data --driver local
```
### 4. Run WireAdmin
When creating each server, ensure that you add the port exposure through Docker. In the below command, the port `51820`
is added for the WireGuard server.
> 💡 The port `3000` is for the WebUI, and can be changed with `PORT` environment variable, but for security
> reasons, it's recommended to NOT expose **_any kind of WebUI_** to the public. It's up to you to remove it after
> configuring
> the Servers/Peers.
### Command line
```shell
docker run --detach \
--name wireadmin \
-e WG_HOST=<YOUR_SERVER_IP> \
-e UI_PASSWORD=<ADMIN_PASSWORD> \
-p "3000:3000/tcp" \
-p "51820:51820/udp" \
-v "wireadmin-data:/data" \
--cap-add=NET_ADMIN \
--cap-add=SYS_MODULE \
--sysctl="net.ipv4.conf.all.src_valid_mark=1" \
--sysctl="net.ipv4.ip_forward=1" \
litehex/wireadmin
$ docker run -d \
--name wireadmin \
-e WG_HOST= \
ADMIN_PASSWORD= \
"3000:3000/tcp" \
-p "51820:51820/udp" \
--cap-add=NET_ADMIN \
--cap-add=SYS_MODULE \
--sysctl="net.ipv4.conf.all.src_valid_mark=1" \
--sysctl="net.ipv4.ip_forward=1" \
ghcr.io/wireadmin/wireadmin < YOUR_SERVER_IP > -e < ADMIN_PASSWORD > -p
```
> 💡 Replace `<YOUR_SERVER_IP>` with the IP address of your server.
@ -83,24 +81,46 @@ docker run --detach \
The Web UI will now be available on `http://0.0.0.0:3000`.
## Options
### Persistent Data
WireAdmin store configurations at `/data`. It's important to mount a volume at this location to ensure that
your data is not lost during container restarts or updates.
To create a docker volume, use the following command:
```bash
docker volume create wireadmin-data --driver local
```
### Environment variables
These options can be configured by setting environment variables using `-e KEY="VALUE"` in the `docker run` command.
| Option | Description | Default | Optional |
| ----------------- | ------------------------------------------------------------------------------- | ------------------- | -------- |
| `WG_HOST` | The public IP address of the WireGuard server. | - | |
| `UI_PASSWORD` | The password for the admin UI. | `insecure-password` | |
| `HOST` | The hostname for the WebUI. | `127.0.0.1` | ✔️ |
| `PORT` | The port for the WebUI. | `3000` | ✔️ |
| `TOR_USE_BRIDGES` | Set this to `1` and then mount the bridges file at `/etc/torrc.d/bridges.conf`. | - | ✔️ |
| `TOR_*` | The `Torrc` proxy configuration. (e.g. `SocksPort` as `TOR_SOCKS_PORT="9050"`) | - | ✔️ |
| Option | Description | Default | Optional |
| ----------------- | ----------------------------------------------------------------------------------- | ------------------- | -------- |
| `WG_HOST` | The public IP address of the WireGuard server. | - | |
| `ADMIN_PASSWORD` | The password for the admin UI. | `insecure-password` | |
| `HOST` | The hostname for the WebUI. | `127.0.0.1` | ✔️ |
| `PORT` | The port for the WebUI. | `3000` | ✔️ |
| `TOR_USE_BRIDGES` | Set this to `1` and then mount the bridges file at `/etc/tor/torrc.d/bridges.conf`. | - | ✔️ |
| `TOR_*` | The `Torrc` proxy configuration. (e.g. `SocksPort` as `TOR_SOCKS_PORT="9050"`) | - | ✔️ |
## Reporting
## Upgrade
For bug reports, and feature requests, please create an issue
on [GitHub](https://github.com/shahradelahi/wireadmin/issues).
Recreate the container whenever I push an update:
```bash
docker compose pull
docker compose up -d
```
## Contributing
Want to contribute? Awesome! To show your support is to star the project, or to raise issues
on [GitHub](https://github.com/shahradelahi/docker-cfw-proxy).
Thanks again for your support, it is much appreciated! 🙏
## License
[GPL-3.0](LICENSE) © [Shahrad Elahi](https://github.com/shahradelahi)
[GPL-3.0](/LICENSE) © [Shahrad Elahi](https://github.com/shahradelahi)

View File

@ -1,16 +0,0 @@
#!/bin/bash
# This script is used for getting last 100 lines of a screen session
SCREEN_NAME="$1"
LIMIT="${2:-100}"
SCRIPT=$(basename "$0")
if [ -z "$SCREEN_NAME" ]; then
echo "Usage: ${SCRIPT} <screen_name>"
exit 1
fi
screen -S "$SCREEN_NAME" -X hardcopy /tmp/screen-hardcopy
tail -n "${LIMIT}" /tmp/screen-hardcopy
rm /tmp/screen-hardcopy

View File

@ -12,4 +12,11 @@ Bridge obfs4 31.18.117.18:9899 13BD8D1786AB84231D2630840142E81B0DDDAD19 cert=E31
Bridge obfs4 86.88.234.28:50001 DE6145637D189CEBF7B052DFC111A511B2BE8072 cert=FXAneGUETzpaw5oxNqO1Wi3EWLBSgbeIN0Z8GVFxromPutq6JkduMpzzvbQpyfYcGYjyJw iat-mode=0
Bridge obfs4 65.108.214.170:23909 8ABD0C0130A37EB3F686F883BCE6D5E59F66C228 cert=mJZdHhaAk6VzaOjQA1UWGkVbDbGqLRuNSuBSk0evlfKRKVzb2EmNio2N0ja+JG1to8KWYw iat-mode=0
Bridge obfs4 92.243.27.238:46311 2E5DC5F2632535630E87883262F967DA376700E2 cert=1BAr2DKmCPxel2DTMXKyOQgoxHM2q6SqJ0tDrZdlyCCrBXhJhsGCICWWZpBEuVB6bdMVWA iat-mode=0
Bridge obfs4 129.213.132.232:50913 5F163F907B3CFCCA66639EE297C2CD27006F7235 cert=ojqfACdTxWZNEPZwfEbAbDMMumnxzwoRAVMRwjkVl5RDH1h1j38YALzRhFFVpzsu7ZthQw iat-mode=0
Bridge obfs4 79.215.99.47:9531 DC1A7B010A348F3A6BE0750D38428D1EAD976D69 cert=TX3XOj1SX3fAB9yoA4dCx8Geu325i564gwIBgnAMyhP6NBdd9dW90gJpWQXKL/VC2BlTNQ iat-mode=0
Bridge obfs4 167.235.71.161:25754 EED9A10892988E28ADCFDDF19AB4F8868C51892D cert=6q19P7O+Zcai7mCxDCVIjiQnrufsMO4X5Ky88dcNBI2H5+LUqNMIcr3kNV3Cd7sKcgUSeg iat-mode=0
Bridge obfs4 65.21.6.66:15751 4D0BEE93BABCFBCD837BB33344850B78FFECD9FF cert=29a0bbjME3mTxC5wcafYAS4v43DVyOtSQWx374De7R38ARiVQZZ3fORSwgGCtDMCFZyxcw iat-mode=0
Bridge obfs4 185.177.207.205:11205 084113B9A27A8087C26236EF67A16784DF58D7F0 cert=pzuLxMv5n+7nRqX2czUQGh8JZBCMEVUHlkciocGRpX2IsPlTqd1YyXFQxRwfsYEFuuBdBQ iat-mode=2
Bridge obfs4 51.75.74.245:8356 18C27C9850967FD4BF4188963C1AEBEC40807823 cert=y6cQEx4d/25KALeqJA+2uB+6rmzoD9KZ0FrQGNwxb10yVj3mDjHtOneqcqhRT+BADhCTYg iat-mode=0
Bridge obfs4 91.134.100.128:51106 ABB9F62BEC331EE5DE7B3C3BEA014F8910E0C6BD cert=bC5k/PWVu06cSPhSm6mrQDBevReEpdtpokmDibpK0MBxRaVnn0S3O6YvEi4BDUeasn71bA iat-mode=0
Bridge obfs4 51.83.252.216:45918 C2B7E51665111C9BE43894E90B9A65DD8A25490D cert=oQgHCdMhvfF44gwHJssSHXltUE4r8gddEQeZ4iy17XHZMP+ql2QTG9LziiEqNfNCqFDBSw iat-mode=0

View File

@ -1,5 +1,9 @@
##### Auto-Generated by the WireAdmin. Do not edit. #####
AutomapHostsOnResolve 1
VirtualAddrNetwork 10.192.0.0/10
DNSPort {{INET_ADDRESS}}:53530
TransPort {{INET_ADDRESS}}:59040
ClientTransportPlugin obfs4 exec /usr/local/bin/obfs4proxy managed
User tor
DataDirectory /var/lib/tor
TransPort {{INET_ADDRESS}}:59040 IsolateClientAddr IsolateClientProtocol IsolateDestAddr IsolateDestPort
ClientTransportPlugin meek_lite,obfs2,obfs3,obfs4,scramblesuit exec /usr/local/bin/lyrebird
%include /etc/tor/torrc.d/*.conf

34
docker-bake.hcl Normal file
View File

@ -0,0 +1,34 @@
variable "DEFAULT_TAG" {
default = "wireadmin:local"
}
// Special target: https://github.com/docker/metadata-action#bake-definition
target "docker-metadata-action" {
tags = ["${DEFAULT_TAG}"]
}
// Default target if none specified
group "default" {
targets = ["image-local"]
}
target "image" {
inherits = ["docker-metadata-action"]
}
target "image-local" {
inherits = ["image"]
output = ["type=docker"]
}
target "image-all" {
inherits = ["image"]
platforms = [
"linux/amd64",
"linux/arm/v6",
"linux/arm/v7",
"linux/arm64",
"linux/386",
"linux/s390x"
]
}

View File

@ -1,13 +1,15 @@
version: '3.8'
services:
wireadmin:
image: wireadmin
build:
context: .
dockerfile: Dockerfile-Dev
volumes:
- ./web/:/app/
ports:
- '5173:5173'
environment:
- WG_HOST=192.168.1.102
- UI_PASSWORD=password
- WG_HOST=192.168.88.252
- ADMIN_PASSWORD=password
extra_hosts:
- 'host.docker.internal:host-gateway'

12
docker-compose.yml Normal file → Executable file
View File

@ -1,4 +1,3 @@
version: '3.8'
services:
wireadmin:
environment:
@ -7,13 +6,14 @@ services:
- WG_HOST=localhost
# ⚠️ Required:
# You can use `openssl rand -base64 8` to generate a secure password
- UI_PASSWORD=super-secret-password
- ADMIN_PASSWORD=super-secret-password
image: wireadmin
image: wireadmin/wireadmin
container_name: wireadmin
restart: unless-stopped
volumes:
- persist-data:/data
- wireadmin-data:/data
- tor-data:/var/lib/tor
ports:
- '51820:51820/udp'
- '3000:3000/tcp'
@ -25,5 +25,5 @@ services:
- net.ipv4.conf.all.src_valid_mark=1
volumes:
persist-data:
driver: local
wireadmin-data:
tor-data:

View File

@ -7,9 +7,7 @@ TOR_CONFIG="/etc/tor/torrc"
TOR_CONFIG_TEMPLATE="${TOR_CONFIG}.template"
log() {
local level=$1
local message=$2
echo "[$(date +"%Y-%m-%d %H:%M:%S")] [$level] $message"
echo "[$(date +"%Y-%m-%d %H:%M:%S")] [$1] $2"
}
to_camel_case() {
@ -31,19 +29,12 @@ generate_tor_config() {
key=$(echo "$line" | awk '{print $1}')
value=$(echo "$line" | awk '{print $2}')
key=$(to_camel_case "$key")
echo "$key $value" >>"${TOR_CONFIG}"
echo "$key $value" >> "${TOR_CONFIG}"
done
# Removing duplicated tor options
awk -F= '!a[tolower($1)]++' "${TOR_CONFIG}" >"/tmp/$(basename "${TOR_CONFIG}")" &&
mv "/tmp/$(basename "${TOR_CONFIG}")" "${TOR_CONFIG}"
# Checking if there is /etc/torrc.d folder and if there are use globbing to include all files
local torrc_files=$(find /etc/torrc.d -type f -name "*.conf")
if [ -n "${torrc_files}" ]; then
log "notice" "Found torrc.d folder with configuration files"
echo "%include /etc/torrc.d/*.conf" >>"${TOR_CONFIG}"
fi
awk -F= '!a[tolower($1)]++' "${TOR_CONFIG}" > "/tmp/$(basename "${TOR_CONFIG}")" \
&& mv "/tmp/$(basename "${TOR_CONFIG}")" "${TOR_CONFIG}"
# Remove comment line with single Hash
sed -i '/^#\([^#]\)/d' "${TOR_CONFIG}"
@ -63,54 +54,50 @@ echo "| |/ |/ / / / / __/ ___ / /_/ / / / / / / / / / /"
echo "|__/|__/_/_/ \___/_/ |_\__,_/_/ /_/ /_/_/_/ /_/ "
echo " "
mkdir -p /var/vlogs
touch "$ENV_FILE"
chmod 400 "$ENV_FILE"
touch "${ENV_FILE}"
chmod 400 "${ENV_FILE}"
if ! grep -q "AUTH_SECRET" "${ENV_FILE}"; then
tee -a "${ENV_FILE}" &>/dev/null <<EOF
AUTH_SECRET=$(openssl rand -base64 32)
EOF
fi
# Checking if there is `UI_PASSWORD` environment variable
# if there was, converting it to sha256 and storing it to
# the .env
if [ -n "$UI_PASSWORD" ]; then
sed -i '/^HASHED_PASSWORD/d' "${ENV_FILE}"
tee -a "${ENV_FILE}" &>/dev/null <<EOF
HASHED_PASSWORD=$(checksum hash -a sha256 -C "${UI_PASSWORD}")
EOF
unset UI_PASSWORD
else
log "error" "no password set for the UI"
exit 1
fi
if [ -z "$WG_HOST" ]; then
log "error" "the WG_HOST environment variable is not set"
exit 1
if [ -z "$ADMIN_PASSWORD" ]; then
log warn "No ADMIN_PASSWORD provided, using default password"
fi
# Remove duplicated envs
awk -F= '!a[$1]++' "${ENV_FILE}" >"/tmp/$(basename "${ENV_FILE}")" &&
mv "/tmp/$(basename "${ENV_FILE}")" "${ENV_FILE}"
awk -F= '!a[$1]++' "$ENV_FILE" > "/tmp/$(basename "$ENV_FILE")" \
&& mv "/tmp/$(basename "$ENV_FILE")" "$ENV_FILE"
if [ -z "$WG_HOST" ]; then
log "error" "the WG_HOST environment variable is not set"
exit 1
fi
# Generate Tor configuration
generate_tor_config
tee "/etc/logrotate.d/rotator" &> /dev/null << EOF
/var/log/wireadmin/*.log {
size 512K
rotate 3
missingok
notifempty
create 0640 root adm
copytruncate
}
EOF
# Start Tor on the background
screen -dmS "tor" tor -f "${TOR_CONFIG}"
crond
_TOR_UID=$(id -u tor)
mkdir -p /var/lib/tor && chown -R "$_TOR_UID:$_TOR_UID" /var/lib/tor
screen -L -Logfile /var/log/wireadmin/tor.log -dmS tor bash -c "screen -S tor -X wrap off; tor -f $TOR_CONFIG"
sleep 1
echo -e "\n======================== Versions ========================"
echo -e "Alpine Version: \c" && cat /etc/alpine-release
echo -e "WireGuard Version: \c" && wg -v | head -n 1 | awk '{print $1,$2}'
echo -e "Tor Version: \c" && tor --version | head -n 1
echo -e "Obfs4proxy Version: \c" && obfs4proxy -version
echo -e "Alpine: \c" && cat /etc/alpine-release
echo -e "WireGuard: \c" && wg -v | head -n 1 | awk '{print $2}'
echo -e "Tor: \c" && tor --version | head -n 1 | cut -d ' ' -f3
echo -e "Lyrebird: \c" && lyrebird -version
echo -e "\n========================= Torrc ========================="
cat "${TOR_CONFIG}"
grep -v "^#" "$TOR_CONFIG"
echo -e "========================================================\n"
sleep 1

View File

@ -5,20 +5,21 @@
"private": true,
"packageManager": "pnpm@8.15.0",
"scripts": {
"dev": "pnpm docker:drop && docker compose -f docker-compose.yml -f docker-compose.dev.yml up",
"dev:image": "docker buildx build --tag wireadmin -f Dockerfile-Dev .",
"build": "pnpm docker:build",
"start": "pnpm docker:drop && docker compose -f docker-compose.yml up",
"docker:build": "docker buildx build --tag wireadmin .",
"docker:drop": "docker compose rm -fsv",
"format": "prettier --write . && pnpm --if-present -r format",
"format:check": "prettier --check . && pnpm --if-present -r format:check"
"dev": "docker compose -f docker-compose.yml -f docker-compose.dev.yml up --force-recreate",
"dev:image": "docker buildx build --network host --tag wireadmin -f Dockerfile-Dev .",
"build": "docker buildx build --tag wireadmin .",
"start": "docker compose -f docker-compose.yml up --force-recreate",
"format": "prettier --write .",
"format:check": "prettier --check . "
},
"keywords": [],
"license": "GPL-3.0",
"devDependencies": {
"dependencies": {
"@changesets/cli": "^2.27.1",
"@ianvs/prettier-plugin-sort-imports": "^4.2.1",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.2"
"prettier-plugin-sh": "^0.14.0",
"prettier-plugin-svelte": "^3.2.2",
"prettier-plugin-tailwindcss": "^0.5.14"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +0,0 @@
{
"$schema": "https://json.schemastore.org/mocharc.json",
"require": ["tsx", "chai/register-expect", "mocha.setup.js"],
"timeout": 10000
}

View File

@ -12,4 +12,4 @@ static
pnpm-lock.yaml
package-lock.json
yarn.lock
tsconfig.json
tsconfig.json

View File

@ -1,17 +0,0 @@
{
"useTabs": false,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte",
"plugins": ["prettier-plugin-svelte"]
}
}
]
}

View File

@ -2,12 +2,12 @@
"$schema": "https://shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": {
"config": "tailwind.config.js",
"config": "tailwind.config.ts",
"css": "src/app.css",
"baseColor": "gray"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils"
"components": "@lib/components",
"utils": "@lib/utils"
}
}

View File

@ -1 +0,0 @@
process.env.NODE_ENV = 'test';

View File

@ -1,17 +1,13 @@
{
"name": "web",
"version": "0.0.0-dev",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test": "mocha",
"check:format": "prettier --check .",
"format": "prettier --write .",
"start": "node ./build/index.js"
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"packageManager": "pnpm@8.15.0",
"engines": {
@ -19,50 +15,45 @@
},
"devDependencies": {
"@sveltejs/adapter-node": "^5.0.1",
"@sveltejs/kit": "^2.5.5",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@types/chai": "^4.3.14",
"@sveltejs/kit": "^2.5.10",
"@sveltejs/vite-plugin-svelte": "^3.1.0",
"@types/jsonwebtoken": "^9.0.6",
"@types/mocha": "^10.0.6",
"@types/node": "^20.12.2",
"@types/node": "^20.12.12",
"@types/qrcode": "^1.5.5",
"autoprefixer": "^10.4.19",
"chai": "^5.1.0",
"mocha": "^10.4.0",
"postcss": "^8.4.38",
"postcss-load-config": "^5.0.3",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.2",
"svelte": "^4.2.12",
"svelte-check": "^3.6.8",
"svelte-preprocess": "^5.1.3",
"sveltekit-superforms": "^2.12.2",
"postcss-load-config": "^5.1.0",
"svelte": "^4.2.17",
"svelte-check": "^3.7.1",
"svelte-preprocess": "^5.1.4",
"sveltekit-superforms": "^2.13.1",
"tailwindcss": "^3.4.3",
"tslib": "^2.6.2",
"tsx": "^4.7.1",
"typescript": "^5.4.3",
"vite": "^5.2.7",
"zod": "^3.22.4"
"tsx": "^4.10.5",
"typescript": "^5.4.5",
"vite": "^5.2.11",
"zod": "^3.23.8"
},
"dependencies": {
"@litehex/storage-box": "^0.2.2-canary.0",
"@t3-oss/env-core": "0.7.3",
"bits-ui": "^0.18.0",
"clsx": "^2.1.0",
"bits-ui": "^0.21.9",
"clsx": "^2.1.1",
"deepmerge": "^4.3.1",
"dotenv": "^16.4.4",
"execa": "^8.0.1",
"dotenv": "^16.4.5",
"execa": "^9.1.0",
"formsnap": "^1.0.0",
"jsonwebtoken": "^9.0.2",
"lucide-svelte": "^0.330.0",
"lucide-svelte": "^0.379.0",
"mode-watcher": "^0.3.0",
"node-netkit": "0.1.0-canary.2",
"pino": "^8.18.0",
"pino-pretty": "^10.3.1",
"node-netkit": "0.1.0-canary.3",
"p-safe": "^1.0.0",
"pino": "^9.1.0",
"pino-pretty": "^11.0.0",
"pretty-bytes": "^6.1.1",
"qrcode": "^1.5.3",
"storage-box": "^1.0.0-canary.4",
"svelte-french-toast": "^1.2.0",
"tailwind-merge": "^2.2.1",
"tailwind-variants": "^0.2.0"
"tailwind-merge": "^2.3.0",
"tailwind-variants": "^0.2.1"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -74,6 +74,9 @@
}
body {
@apply bg-background text-foreground;
font-feature-settings:
'rlig' 1,
'calt' 1;
}
}

View File

@ -3,7 +3,6 @@
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
<link rel="stylesheet" href="%sveltekit.assets%/fontawesome/all.min.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>

View File

@ -1,25 +1,25 @@
import { type Handle, redirect } from '@sveltejs/kit';
import { verifyToken } from '$lib/auth';
import { AUTH_COOKIE } from '$lib/constants';
import 'dotenv/config';
import { redirect, type Handle } from '@sveltejs/kit';
import { verifyToken } from '@lib/auth';
import { AUTH_COOKIE } from '@lib/constants';
import logger from '@lib/logger';
export const handle: Handle = async ({ event, resolve }) => {
if (!AUTH_EXCEPTION.includes(event.url.pathname)) {
const token = event.cookies.get(AUTH_COOKIE);
const token_valid = await verifyToken(token ?? '');
logger.debug(`-> ${event.request.method} ${event.url.pathname}`);
const is_login_page = event.url.pathname === '/login';
if (!token_valid && !is_login_page) {
// return redirect;
throw redirect(303, '/login');
}
const token = event.cookies.get(AUTH_COOKIE);
const token_valid = await verifyToken(token ?? '');
if (token_valid && is_login_page) {
throw redirect(303, '/');
}
const is_login_page = event.url.pathname === '/login';
if (!token_valid && !is_login_page) {
throw redirect(303, '/login');
}
if (token_valid && is_login_page) {
throw redirect(303, '/');
}
return resolve(event);
};
const AUTH_EXCEPTION = ['/api/health'];

View File

@ -1,34 +1,42 @@
import jwt from 'jsonwebtoken';
import { client } from '$lib/storage';
import { env } from '$lib/env';
export async function generateToken(): Promise<string> {
import { WG_AUTH_PATH } from '@lib/constants';
import { env } from '@lib/env';
import { storage } from '@lib/storage';
import { sha256 } from '@lib/utils/hash';
interface GenerateTokenParams {
expiresIn: number;
}
export async function generateToken(params: GenerateTokenParams): Promise<string> {
const now = Math.floor(Date.now() / 1000);
const oneHour = 60 * 60;
const token = jwt.sign(
{
ok: true,
iat: now,
exp: now + oneHour,
exp: now + params.expiresIn,
},
env.AUTH_SECRET,
env.AUTH_SECRET
);
client.setex(token, '1', oneHour);
await storage.lpushex(WG_AUTH_PATH, sha256(token), params.expiresIn);
return token;
}
export async function verifyToken(token: string): Promise<boolean> {
try {
const decode = jwt.verify(token, env.AUTH_SECRET);
if (!decode) return false;
if (!token || !(await storage.lexists(WG_AUTH_PATH, sha256(token)))) return false;
const exists = client.exists(token);
return exists;
return !!jwt.verify(token, env.AUTH_SECRET);
} catch (e) {
return false;
}
}
export async function revokeToken(token: string): Promise<void> {
client.del(token);
if (!token) return;
const index = await storage
.lgetall(WG_AUTH_PATH)
.then((l) => l.findIndex((t) => t === sha256(token)));
await storage.ldel(WG_AUTH_PATH, index);
}

View File

@ -1,5 +1,7 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
import { ClipboardCopyIcon } from 'lucide-svelte';
import { Button } from '@lib/components/ui/button';
export let showInHover: boolean = false;
export let rootClass: string | undefined = undefined;
@ -12,20 +14,23 @@
};
</script>
<div class={cn('group flex items-center', rootClass)}>
<div class={cn('group flex items-center gap-3', rootClass)}>
<slot />
<i
<Button
aria-roledescription="Copy to clipboard"
role="button"
tabindex="0"
class={cn(
'ml-2 mb-0.5 far fa-copy cursor-pointer text-gray-400/80 hover:text-primary',
showInHover && 'group-hover:opacity-100 opacity-0',
className,
)}
size="none"
variant="ghost"
on:click={handleCopy}
on:keydown={(e) => {
if (e.key === 'Enter') handleCopy();
}}
/>
>
<ClipboardCopyIcon
class={cn(
'h-4 w-4 cursor-pointer text-gray-400/80 hover:text-primary',
showInHover && 'group-hover:opacity-100 opacity-0',
className
)}
/>
</Button>
</div>

View File

@ -1,7 +1,9 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
import { createEventDispatcher } from 'svelte';
import type { ZodEffects, ZodString } from 'zod';
import { SquarePenIcon } from 'lucide-svelte';
import { Button } from '@lib/components/ui/button';
export let editMode: boolean = false;
export let schema: ZodString | ZodEffects<any>;
@ -40,7 +42,7 @@
editMode ? 'block' : 'hidden',
'w-full ring-2 ring-neutral-800 ring-offset-2 rounded transition-colors duration-200 ease-in-out outline-transparent',
inputClass,
error && 'ring-red-500 rounded',
error && 'ring-red-500 rounded'
)}
{value}
on:keydown={(e) => {
@ -60,14 +62,16 @@
}}
/>
<i
class="fal fa-pen-to-square text-sm opacity-0 group-hover:opacity-100 text-neutral-400 hover:text-primary cursor-pointer"
role="button"
tabindex="0"
<Button
class="opacity-0 group-hover:opacity-100 text-gray-400/80 group-hover:text-primary"
aria-roledescription="Edit"
size="none"
variant="ghost"
on:click={handleEnterEditMode}
on:keydown={(e) => {
if (e.key === 'Enter') handleEnterEditMode();
}}
/>
>
<SquarePenIcon class="h-4 w-4" />
</Button>
</div>

View File

@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = HTMLAttributes<HTMLDivElement>;

View File

@ -1,7 +1,7 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { mode } from 'mode-watcher';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = HTMLAttributes<SVGImageElement> & {
borderColor?: string;

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
import { EmptyDescription, EmptySimpleImage, type Props } from '.';
type $$Props = Props;

View File

@ -1,7 +1,8 @@
import Root from './empty.svelte';
import type { HTMLAttributes } from 'svelte/elements';
import Description from './empty-description.svelte';
import SimpleImage from './empty-simple-img.svelte';
import type { HTMLAttributes } from 'svelte/elements';
import Root from './empty.svelte';
interface Props extends HTMLAttributes<HTMLDivElement> {
description?: string | null;

View File

@ -0,0 +1,20 @@
<script lang="ts">
import { cn } from '@lib/utils.js';
export let name: string | undefined = undefined;
export let color = 'currentColor';
export let size: number | string = 24;
export let strokeWidth: number | string = 2;
export let absoluteStrokeWidth: boolean = false;
</script>
<svg
{...$$restProps}
width={size}
height={size}
stroke={color}
stroke-width={absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth}
class={cn('lucide-icon', 'lucide', name ? `lucide-${name}` : '', $$props.class)}
>
<slot />
</svg>

View File

@ -0,0 +1,21 @@
import type { SVGAttributes } from 'svelte/elements';
import Root from './icon.svelte';
import Onion from './onion-icon.svelte';
interface Props extends SVGAttributes<SVGSVGElement> {
color?: string;
size?: number | string;
strokeWidth?: number | string;
absoluteStrokeWidth?: boolean;
class?: string;
}
export {
Root,
Onion,
type Props,
//
Root as Icon,
Onion as OnionIcon,
};

View File

@ -0,0 +1,15 @@
<script lang="ts">
import Icon from '@lib/components/iconset/icon.svelte';
import type { Props } from '@lib/components/iconset';
type $$Props = Props;
</script>
<Icon {...$$restProps} viewBox="0 0 179.51151 205.15602" fill="currentColor">
<g transform="translate(-17.934981,-39.932967)">
<path
d="m 104.88587,47.145484 -1.60279,-2.804867 q -2.00347,-4.407649 -6.811816,-4.407649 -7.212516,0.801391 -8.414602,8.013907 -2.804867,14.024336 -11.620164,24.041719 -8.815297,9.616688 -20.435462,18.431985 0,0 0,0 0,0 0,0 -14.425031,10.818771 -26.045196,25.644501 -11.219469,14.42503 -12.02086,39.26814 0.400696,24.04172 15.226423,40.47023 14.825727,16.02781 37.66536,23.64102 l -4.407648,12.82225 q -2.003477,8.8153 6.010429,12.42156 8.815298,2.00348 12.421555,-6.01043 l 4.808344,-14.42503 q 4.006954,0.80139 8.414602,1.20208 v 10.01739 q 0.801391,8.81529 9.616685,9.61668 8.8153,-0.80139 9.61669,-9.61668 V 225.4549 q 4.40765,-0.40069 8.4146,-1.20208 l 4.80835,14.42503 q 3.60625,8.01391 12.42155,6.01043 8.01391,-3.60626 6.01043,-12.42156 l -4.40765,-12.82225 q 22.83964,-7.61321 37.66536,-23.64102 14.82573,-16.42851 15.22643,-40.47023 -0.8014,-24.84311 -12.02086,-39.26814 -11.62017,-14.82573 -26.0452,-25.644501 0,0 0,0 0,0 0,0 -11.62016,-8.815297 -20.43546,-18.431985 -8.8153,-10.017383 -11.21947,-24.041719 -1.60278,-7.212516 -8.8153,-8.013907 -4.80834,0 -6.81182,4.407649 l -1.60278,2.804867 q -2.80487,3.606258 -5.60973,0 z M 56.802427,114.4623 v -0.4007 q 0.400695,-0.40069 0.80139,-0.80139 0,0 0,0 1.602782,-2.40417 4.407649,-2.80486 1.202086,0 2.404172,0.40069 0.801391,0 2.003477,0.80139 2.404172,1.60278 2.804867,4.40765 0,1.20209 -0.400695,2.40417 0,0.80139 -0.400696,1.20209 v 0.40069 q -0.400695,0.4007 -0.80139,1.20209 -0.400696,1.60278 -2.003477,4.40765 -2.404172,6.01043 -5.209039,15.22642 -2.804868,9.61669 -3.606258,19.63407 -0.400696,10.01738 2.404172,18.03129 1.602781,6.01043 -4.006953,8.4146 -5.609735,1.60278 -8.013907,-3.60626 -4.006953,-11.62016 -3.205563,-23.64102 1.202086,-12.02086 4.006954,-22.03824 3.205562,-10.41808 6.01043,-16.82921 1.202086,-3.20556 2.003476,-4.80834 0.400695,-0.4007 0.400695,-0.80139 0,-0.4007 0.400696,-0.80139 z M 101.6803,89.218493 q 0.4007,-0.801391 0.4007,-1.202086 0.80139,-1.202086 2.00348,-2.003477 2.00347,-1.602781 4.80834,-1.202086 1.20208,0.400696 2.00348,0.801391 1.20208,0.801391 2.00347,2.003477 0.80139,1.202086 1.20209,2.804867 0,0.801391 0,2.003477 -0.4007,0.80139 -0.4007,1.202086 v 0 l -0.40069,0.80139 q 0,0 0,0.400696 -0.4007,0.80139 -0.80139,1.602781 0,0 0,0.400695 -1.20209,2.804867 -2.40418,7.212516 -3.60625,10.01738 -7.21251,25.6445 -4.006954,15.62712 -5.20904,32.05563 -0.801391,16.8292 3.20556,30.05215 1.60278,5.60973 -4.006951,8.0139 -5.609734,1.20209 -8.013906,-4.00695 -4.808344,-16.02781 -3.606258,-34.4598 1.202086,-18.43198 5.209039,-34.86049 3.606258,-16.02781 7.212516,-26.846589 2.003476,-4.808343 2.80487,-8.013906 0.80139,-1.202086 1.20208,-2.003477 v -0.400695 -0.400695 z m 31.65493,14.825727 q 0.4007,-6.01043 6.41113,-6.411125 0,0 0,0 0,0 0,0 0,0 0,0 0,0 0.40069,0 3.60626,0 5.60974,3.205565 l -0.4007,0.40069 v 0 q 0.4007,-0.40069 0.4007,-0.40069 v 0 0 0 0.40069 l 0.40069,0.4007 q 0.4007,0.40069 0.80139,1.60278 1.20209,2.00348 2.80487,5.60973 3.60626,7.61322 8.01391,19.63407 4.40765,12.02086 6.41112,26.0452 2.00348,14.42503 -1.60278,28.04867 -2.40417,5.60974 -8.0139,4.40765 -5.60974,-2.00347 -4.40765,-7.61321 2.80486,-10.81877 1.20208,-23.24033 -1.60278,-12.02086 -5.60973,-23.64102 -4.00695,-11.21947 -7.61321,-18.03129 -1.60278,-3.60626 -2.40417,-5.60974 v 0 q -2.40418,-1.60278 -2.40418,-4.80834 z"
aria-label="Onion"
/>
</g>
</Icon>

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
export let content: string;
export let maxLength: number = Math.floor(content.length / 2);

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { cn } from '$lib/utils';
import PageFooter from '$lib/components/page/PageFooter.svelte';
import PageHeader from '$lib/components/page/PageHeader.svelte';
import { cn } from '@lib/utils';
import PageFooter from '@lib/components/page/PageFooter.svelte';
import PageHeader from '@lib/components/page/PageHeader.svelte';
export let rootClass: string | undefined = undefined;
let className: string | undefined = undefined;

View File

@ -1,5 +1,5 @@
<script>
import DotDivider from '$lib/components/DotDivider.svelte';
import DotDivider from '@lib/components/DotDivider.svelte';
</script>
<footer class={'flex items-center justify-center'}>
@ -12,7 +12,7 @@
</a>
<DotDivider className="font-bold text-gray-400" />
<a
href={'https://github.com/shahradelahi/wireadmin'}
href={'https://github.com/wireadmin/wireadmin'}
title={'Github'}
class={'px-2 font-medium text-gray-400/80 hover:text-gray-500 text-xs'}
>

View File

@ -3,8 +3,8 @@
import Moon from 'lucide-svelte/icons/moon';
import { toggleMode } from 'mode-watcher';
import { Button } from '$lib/components/ui/button';
import { cn } from '$lib/utils';
import { Button } from '@lib/components/ui/button';
import { LogOutIcon } from 'lucide-svelte';
export let showLogout: boolean = false;
</script>
@ -18,12 +18,12 @@
<div class={'flex items-center gap-x-3'}>
<a
href={'https://github.com/shahradelahi/wireadmin'}
href={'https://github.com/wireadmin/wireadmin'}
title={'Giv me a star on Github'}
class="hidden md:block"
>
<img
src={'https://img.shields.io/github/stars/shahradelahi/wireadmin.svg?style=social&label=Star'}
src={'https://img.shields.io/github/stars/wireadmin/wireadmin.svg?style=social&label=Star'}
alt={'Gimme a Star'}
/>
</a>
@ -41,17 +41,8 @@
{#if showLogout}
<a href="/logout" rel="external" title="Logout">
<Button variant="ghost" class="group text-sm/2 gap-x-2 font-medium">
<i
class={cn(
'far fa-arrow-right-from-arc text-sm mr-0.5',
'text-neutral-500 group-hover:text-neutral-800',
'dark:text-neutral-400 dark:group-hover:text-neutral-100',
)}
></i>
<span
class="text-neutral-700 hover:text-neutral-800 dark:text-neutral-100 dark:hover:text-neutral-100"
>Logout</span
>
<LogOutIcon class={'w-4 h-4 mr-0.5'} />
Logout
</Button>
</a>
{/if}

View File

@ -6,11 +6,11 @@
DialogHeader,
DialogTitle,
DialogTrigger,
} from '$lib/components/ui/dialog';
import { Skeleton } from '$lib/components/ui/skeleton';
} from '@lib/components/ui/dialog';
import { Skeleton } from '@lib/components/ui/skeleton';
import { Dialog as DialogPrimitive } from 'bits-ui';
import { Button } from '$lib/components/ui/button';
import type { SafeReturn } from '$lib/typings';
import { Button } from '@lib/components/ui/button';
import type { SafeReturn } from '@lib/typings';
const DialogClose = DialogPrimitive.Close;

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
import { badgeVariants, type Variant } from '.';
let className: string | undefined | null = undefined;

View File

@ -1,4 +1,5 @@
import { tv, type VariantProps } from 'tailwind-variants';
export { default as Badge } from './badge.svelte';
export const badgeVariants = tv({

View File

@ -0,0 +1,24 @@
<script lang="ts">
import Ellipsis from 'lucide-svelte/icons/ellipsis';
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '@lib/utils.js';
type $$Props = HTMLAttributes<HTMLSpanElement> & {
el?: HTMLSpanElement;
};
export let el: $$Props['el'] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<span
bind:this={el}
role="presentation"
aria-hidden="true"
class={cn('flex h-9 w-9 items-center justify-center', className)}
{...$$restProps}
>
<Ellipsis class="h-4 w-4" />
<span class="sr-only">More</span>
</span>

View File

@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLLiAttributes } from 'svelte/elements';
import { cn } from '@lib/utils.js';
type $$Props = HTMLLiAttributes & {
el?: HTMLLIElement;
};
export let el: $$Props['el'] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<li bind:this={el} class={cn('inline-flex items-center gap-1.5', className)}>
<slot />
</li>

View File

@ -0,0 +1,31 @@
<script lang="ts">
import type { HTMLAnchorAttributes } from 'svelte/elements';
import { cn } from '@lib/utils.js';
type $$Props = HTMLAnchorAttributes & {
el?: HTMLAnchorElement;
asChild?: boolean;
};
export let href: $$Props['href'] = undefined;
export let el: $$Props['el'] = undefined;
export let asChild: $$Props['asChild'] = false;
let className: $$Props['class'] = undefined;
export { className as class };
let attrs: Record<string, unknown>;
$: attrs = {
class: cn('transition-colors hover:text-foreground', className),
href,
...$$restProps,
};
</script>
{#if asChild}
<slot {attrs} />
{:else}
<a bind:this={el} {...attrs} {href}>
<slot {attrs} />
</a>
{/if}

View File

@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLOlAttributes } from 'svelte/elements';
import { cn } from '@lib/utils.js';
type $$Props = HTMLOlAttributes & {
el?: HTMLOListElement;
};
export let el: $$Props['el'] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<ol
bind:this={el}
class={cn(
'flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5',
className
)}
{...$$restProps}
>
<slot />
</ol>

View File

@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '@lib/utils.js';
type $$Props = HTMLAttributes<HTMLSpanElement> & {
el?: HTMLSpanElement;
};
export let el: $$Props['el'] = undefined;
export let className: $$Props['class'] = undefined;
export { className as class };
</script>
<span
bind:this={el}
role="link"
aria-disabled="true"
aria-current="page"
class={cn('font-normal text-foreground', className)}
{...$$restProps}
>
<slot />
</span>

View File

@ -0,0 +1,25 @@
<script lang="ts">
import type { HTMLLiAttributes } from 'svelte/elements';
import ChevronRight from 'lucide-svelte/icons/chevron-right';
import { cn } from '@lib/utils.js';
type $$Props = HTMLLiAttributes & {
el?: HTMLLIElement;
};
export let el: $$Props['el'] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<li
role="presentation"
aria-hidden="true"
class={cn('[&>svg]:size-3.5', className)}
bind:this={el}
{...$$restProps}
>
<slot>
<ChevronRight />
</slot>
</li>

View File

@ -0,0 +1,15 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
type $$Props = HTMLAttributes<HTMLElement> & {
el?: HTMLElement;
};
export let el: $$Props['el'] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<nav class={className} bind:this={el} aria-label="breadcrumb" {...$$restProps}>
<slot />
</nav>

View File

@ -0,0 +1,25 @@
import Ellipsis from './breadcrumb-ellipsis.svelte';
import Item from './breadcrumb-item.svelte';
import Link from './breadcrumb-link.svelte';
import List from './breadcrumb-list.svelte';
import Page from './breadcrumb-page.svelte';
import Separator from './breadcrumb-separator.svelte';
import Root from './breadcrumb.svelte';
export {
Root,
Ellipsis,
Item,
Separator,
Link,
List,
Page,
//
Root as Breadcrumb,
Ellipsis as BreadcrumbEllipsis,
Item as BreadcrumbItem,
Separator as BreadcrumbSeparator,
Link as BreadcrumbLink,
List as BreadcrumbList,
Page as BreadcrumbPage,
};

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { Button as ButtonPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
import { type Events, type Props, buttonVariants } from '.';
import { type Events, type Props, buttonVariants } from './index';
import { cn } from '@lib/utils';
type $$Props = Props;
type $$Events = Events;

View File

@ -1,5 +1,6 @@
import { type VariantProps, tv } from 'tailwind-variants';
import type { Button as ButtonPrimitive } from 'bits-ui';
import { tv, type VariantProps } from 'tailwind-variants';
import Root from './button.svelte';
const buttonVariants = tv({
@ -8,7 +9,6 @@ const buttonVariants = tv({
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
success: 'bg-green-500 text-white hover:bg-green-500/90 hover:text-gray-50',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
@ -19,6 +19,7 @@ const buttonVariants = tv({
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
none: '',
},
},
defaultVariants: {

View File

@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = HTMLAttributes<HTMLDivElement>;

View File

@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = HTMLAttributes<HTMLParagraphElement>;

View File

@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = HTMLAttributes<HTMLDivElement>;

View File

@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = HTMLAttributes<HTMLDivElement>;

View File

@ -1,7 +1,7 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import type { HeadingLevel } from './index.js';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
tag?: HeadingLevel;

View File

@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = HTMLAttributes<HTMLDivElement>;

View File

@ -1,9 +1,9 @@
import Root from './card.svelte';
import Content from './card-content.svelte';
import Description from './card-description.svelte';
import Footer from './card-footer.svelte';
import Header from './card-header.svelte';
import Title from './card-title.svelte';
import Root from './card.svelte';
export {
Root,

View File

@ -2,7 +2,7 @@
import { Checkbox as CheckboxPrimitive } from 'bits-ui';
import Check from 'lucide-svelte/icons/check';
import Minus from 'lucide-svelte/icons/minus';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = CheckboxPrimitive.Props;
type $$Events = CheckboxPrimitive.Events;
@ -15,7 +15,7 @@
<CheckboxPrimitive.Root
class={cn(
'peer box-content h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:cursor-not-allowed data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[disabled=true]:opacity-50',
className,
className
)}
bind:checked
{...$$restProps}

View File

@ -1,4 +1,5 @@
import Root from './checkbox.svelte';
export {
Root,
//

View File

@ -1,4 +1,5 @@
import { Collapsible as CollapsiblePrimitive } from 'bits-ui';
import Content from './collapsible-content.svelte';
const Root = CollapsiblePrimitive.Root;

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import * as Dialog from '.';
import { cn, flyAndScale } from '$lib/utils';
import { cn, flyAndScale } from '@lib/utils';
import { X } from 'lucide-svelte';
type $$Props = DialogPrimitive.ContentProps;
@ -21,7 +21,7 @@
{transitionConfig}
class={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg sm:rounded-lg md:w-full',
className,
className
)}
{...$$restProps}
>

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = DialogPrimitive.DescriptionProps;

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
import type { HTMLAttributes } from 'svelte/elements';
type $$Props = HTMLAttributes<HTMLDivElement>;

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
import type { HTMLAttributes } from 'svelte/elements';
type $$Props = HTMLAttributes<HTMLDivElement>;

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
import { fade } from 'svelte/transition';
type $$Props = DialogPrimitive.OverlayProps;

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = DialogPrimitive.TitleProps;

View File

@ -1,17 +1,17 @@
import { Dialog as DialogPrimitive } from 'bits-ui';
import Content from './dialog-content.svelte';
import Description from './dialog-description.svelte';
import Footer from './dialog-footer.svelte';
import Header from './dialog-header.svelte';
import Overlay from './dialog-overlay.svelte';
import Portal from './dialog-portal.svelte';
import Title from './dialog-title.svelte';
const Root = DialogPrimitive.Root;
const Trigger = DialogPrimitive.Trigger;
const Close = DialogPrimitive.Close;
import Title from './dialog-title.svelte';
import Portal from './dialog-portal.svelte';
import Footer from './dialog-footer.svelte';
import Header from './dialog-header.svelte';
import Overlay from './dialog-overlay.svelte';
import Content from './dialog-content.svelte';
import Description from './dialog-description.svelte';
export {
Root,
Title,

View File

@ -1,5 +1,5 @@
<script lang="ts">
import * as Button from '$lib/components/ui/button/index.js';
import * as Button from '@lib/components/ui/button/index.js';
type $$Props = Button.Props;
type $$Events = Button.Events;

View File

@ -1,27 +0,0 @@
<script lang="ts">
import { getFormField } from 'formsnap';
import type { Checkbox as CheckboxPrimitive } from 'bits-ui';
import { Checkbox } from '$lib/components/ui/checkbox';
type $$Props = CheckboxPrimitive.Props;
type $$Events = CheckboxPrimitive.Events;
export let onCheckedChange: $$Props['onCheckedChange'] = undefined;
const { name, setValue, attrStore, value } = getFormField();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { name: nameAttr, value: valueAttr, ...rest } = $attrStore;
</script>
<Checkbox
{...rest}
checked={typeof $value === 'boolean' ? $value : false}
onCheckedChange={(v) => {
onCheckedChange?.(v);
setValue(v);
}}
{...$$restProps}
on:click
on:keydown
/>
<input hidden {name} value={$value} />

View File

@ -1,7 +1,7 @@
<script lang="ts">
import * as FormPrimitive from 'formsnap';
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils.js';
type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: string | undefined | null = undefined;

View File

@ -1,6 +1,5 @@
<script lang="ts" context="module">
import type { FormPathLeaves, SuperForm } from 'sveltekit-superforms';
type T = Record<string, unknown>;
type U = FormPathLeaves<T>;
</script>
@ -8,7 +7,7 @@
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPathLeaves<T>">
import type { HTMLAttributes } from 'svelte/elements';
import * as FormPrimitive from 'formsnap';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils.js';
type $$Props = FormPrimitive.ElementFieldProps<T, U> & HTMLAttributes<HTMLElement>;

View File

@ -1,6 +1,6 @@
<script lang="ts">
import * as FormPrimitive from 'formsnap';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils.js';
type $$Props = FormPrimitive.FieldErrorsProps & {
errorClasses?: string | undefined | null;

View File

@ -1,6 +1,5 @@
<script lang="ts" context="module">
import type { FormPath, SuperForm } from 'sveltekit-superforms';
type T = Record<string, unknown>;
type U = FormPath<T>;
</script>
@ -8,7 +7,7 @@
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import type { HTMLAttributes } from 'svelte/elements';
import * as FormPrimitive from 'formsnap';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils.js';
type $$Props = FormPrimitive.FieldProps<T, U> & HTMLAttributes<HTMLElement>;

View File

@ -1,13 +1,12 @@
<script lang="ts" context="module">
import type { FormPath, SuperForm } from 'sveltekit-superforms';
type T = Record<string, unknown>;
type U = FormPath<T>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import * as FormPrimitive from 'formsnap';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils.js';
type $$Props = FormPrimitive.FieldsetProps<T, U>;

View File

@ -1,28 +0,0 @@
<script lang="ts">
import { getFormField } from 'formsnap';
import type { HTMLInputAttributes } from 'svelte/elements';
import { Input, type InputEvents } from '$lib/components/ui/input';
type $$Props = HTMLInputAttributes;
type $$Events = InputEvents;
const { attrStore, value } = getFormField();
</script>
<Input
{...$attrStore}
bind:value={$value}
{...$$restProps}
on:blur
on:change
on:click
on:focus
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input
/>

View File

@ -1,12 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils';
import type { HTMLAttributes } from 'svelte/elements';
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<div class={cn('space-y-2', className)} {...$$restProps}>
<slot />
</div>

View File

@ -1,8 +1,8 @@
<script lang="ts">
import type { Label as LabelPrimitive } from 'bits-ui';
import { getFormControl } from 'formsnap';
import { cn } from '$lib/utils';
import { Label } from '$lib/components/ui/label';
import { cn } from '@lib/utils.js';
import { Label } from '@lib/components/ui/label/index.js';
type $$Props = LabelPrimitive.Props;

View File

@ -1,6 +1,6 @@
<script lang="ts">
import * as FormPrimitive from 'formsnap';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils.js';
type $$Props = FormPrimitive.LegendProps;

View File

@ -1,24 +0,0 @@
<script lang="ts">
import { Form as FormPrimitive } from 'formsnap';
import { buttonVariants } from '$lib/components/ui/button';
import { cn } from '$lib/utils';
import { ChevronDown } from 'lucide-svelte';
import type { HTMLSelectAttributes } from 'svelte/elements';
type $$Props = HTMLSelectAttributes;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<FormPrimitive.Select
class={cn(
buttonVariants({ variant: 'outline' }),
'appearance-none bg-transparent font-normal',
className,
)}
{...$$restProps}
>
<slot />
</FormPrimitive.Select>
<ChevronDown class="absolute right-3 top-2.5 h-4 w-4 opacity-50" />

View File

@ -1,22 +0,0 @@
<script lang="ts">
import { getFormField } from 'formsnap';
import type { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
import * as RadioGroup from '$lib/components/ui/radio-group';
type $$Props = RadioGroupPrimitive.Props;
const { attrStore, setValue, name, value } = getFormField();
export let onValueChange: $$Props['onValueChange'] = undefined;
</script>
<RadioGroup.Root
{...$attrStore}
onValueChange={(v) => {
onValueChange?.(v);
setValue(v);
}}
{...$$restProps}
>
<slot />
<input hidden {name} value={$value} />
</RadioGroup.Root>

View File

@ -1,17 +0,0 @@
<script lang="ts">
import * as Select from '$lib/components/ui/select';
import type { Select as SelectPrimitive } from 'bits-ui';
import { getFormField } from 'formsnap';
type $$Props = SelectPrimitive.TriggerProps & {
placeholder?: string;
};
type $$Events = SelectPrimitive.TriggerEvents;
const { attrStore } = getFormField();
export let placeholder = '';
</script>
<Select.Trigger {...$$restProps} {...$attrStore} on:click on:keydown>
<Select.Value {placeholder} />
<slot />
</Select.Trigger>

View File

@ -1,20 +0,0 @@
<script lang="ts">
import * as Select from '$lib/components/ui/select';
import { getFormField } from 'formsnap';
import type { Select as SelectPrimitive } from 'bits-ui';
type $$Props = SelectPrimitive.Props;
const { setValue, name, value } = getFormField();
export let onSelectedChange: $$Props['onSelectedChange'] = undefined;
</script>
<Select.Root
onSelectedChange={(v) => {
onSelectedChange?.(v);
setValue(v ? v.value : undefined);
}}
{...$$restProps}
>
<slot />
<input hidden {name} value={$value} />
</Select.Root>

View File

@ -1,25 +0,0 @@
<script lang="ts">
import { getFormField } from 'formsnap';
import type { Switch as SwitchPrimitive } from 'bits-ui';
import { Switch } from '$lib/components/ui/switch';
type $$Props = SwitchPrimitive.Props;
type $$Events = SwitchPrimitive.Events;
export let onCheckedChange: $$Props['onCheckedChange'] = undefined;
const { name, setValue, attrStore, value } = getFormField();
</script>
<Switch
{...$attrStore}
checked={typeof $value === 'boolean' ? $value : false}
onCheckedChange={(v) => {
onCheckedChange?.(v);
setValue(v);
}}
{...$$restProps}
on:click
on:keydown
/>
<input hidden {name} value={$value} />

View File

@ -1,29 +0,0 @@
<script lang="ts">
import { getFormField } from 'formsnap';
import type { HTMLTextareaAttributes } from 'svelte/elements';
import type { TextareaGetFormField } from './index';
import { Textarea, type TextareaEvents } from '$lib/components/ui/textarea';
type $$Props = HTMLTextareaAttributes;
type $$Events = TextareaEvents;
const { attrStore, value } = getFormField() as TextareaGetFormField;
</script>
<Textarea
{...$attrStore}
bind:value={$value}
{...$$restProps}
on:blur
on:change
on:click
on:focus
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input
/>

View File

@ -1,14 +0,0 @@
<script lang="ts">
import { Form as FormPrimitive } from 'formsnap';
import { cn } from '$lib/utils';
import type { HTMLAttributes } from 'svelte/elements';
type $$Props = HTMLAttributes<HTMLParagraphElement>;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<FormPrimitive.Validation
class={cn('text-sm font-medium text-destructive', className)}
{...$$restProps}
/>

View File

@ -1,12 +1,13 @@
import * as FormPrimitive from 'formsnap';
import Button from './form-button.svelte';
import Description from './form-description.svelte';
import Label from './form-label.svelte';
import ElementField from './form-element-field.svelte';
import FieldErrors from './form-field-errors.svelte';
import Field from './form-field.svelte';
import Fieldset from './form-fieldset.svelte';
import Label from './form-label.svelte';
import Legend from './form-legend.svelte';
import ElementField from './form-element-field.svelte';
import Button from './form-button.svelte';
const Control = FormPrimitive.Control;

View File

@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLInputAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
import type { InputEvents } from '.';
type $$Props = HTMLInputAttributes;
@ -14,7 +14,7 @@
<input
class={cn(
'flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-foreground file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className,
className
)}
bind:value
on:blur

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { Label as LabelPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils.js';
type $$Props = LabelPrimitive.Props;
type $$Events = LabelPrimitive.Events;
@ -12,7 +12,7 @@
<LabelPrimitive.Root
class={cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
className,
className
)}
{...$$restProps}
on:mousedown

View File

@ -1,7 +1,8 @@
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
import Root from './radio-group.svelte';
import Item from './radio-group-item.svelte';
import Root from './radio-group.svelte';
const Input = RadioGroupPrimitive.Input;
export {

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
import { Circle } from 'lucide-svelte';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = RadioGroupPrimitive.ItemProps;
type $$Events = RadioGroupPrimitive.ItemEvents;
@ -15,7 +15,7 @@
{value}
class={cn(
'aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className,
className
)}
{...$$restProps}
on:click

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = RadioGroupPrimitive.Props;

View File

@ -1,10 +1,10 @@
import { Select as SelectPrimitive } from 'bits-ui';
import Label from './select-label.svelte';
import Item from './select-item.svelte';
import Content from './select-content.svelte';
import Trigger from './select-trigger.svelte';
import Item from './select-item.svelte';
import Label from './select-label.svelte';
import Separator from './select-separator.svelte';
import Trigger from './select-trigger.svelte';
const Root = SelectPrimitive.Root;
const Group = SelectPrimitive.Group;

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui';
import { scale } from 'svelte/transition';
import { cn, flyAndScale } from '$lib/utils';
import { cn, flyAndScale } from '@lib/utils';
type $$Props = SelectPrimitive.ContentProps;
type $$Events = SelectPrimitive.ContentEvents;
@ -28,7 +28,7 @@
{sideOffset}
class={cn(
'relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md outline-none',
className,
className
)}
{...$$restProps}
on:keydown

View File

@ -1,7 +1,7 @@
<script lang="ts">
import Check from 'lucide-svelte/icons/check';
import { Select as SelectPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = SelectPrimitive.ItemProps;
type $$Events = SelectPrimitive.ItemEvents;
@ -19,7 +19,7 @@
{label}
class={cn(
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50',
className,
className
)}
{...$$restProps}
on:click

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = SelectPrimitive.LabelProps;

Some files were not shown because too many files have changed in this diff Show More