Compare commits
14 Commits
2894cf222b
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 937f014ce0 | |||
| e6ff59c75b | |||
| 79a4bcb890 | |||
| 62f1ea5d36 | |||
| 5bb488ccd0 | |||
| 256d5c9e6d | |||
| a90519807c | |||
| 4cd85e3515 | |||
| 9531730283 | |||
| 66ca05692b | |||
| 13ce2a5b4f | |||
| 5a6f278d8a | |||
| 4b55661aa4 | |||
| 08d12feaa9 |
@@ -0,0 +1,20 @@
|
|||||||
|
.git
|
||||||
|
.github
|
||||||
|
.review-sandboxes
|
||||||
|
.claude
|
||||||
|
.agents
|
||||||
|
.DS_Store
|
||||||
|
.venv
|
||||||
|
venv
|
||||||
|
__pycache__
|
||||||
|
*.py[cod]
|
||||||
|
archive
|
||||||
|
docs
|
||||||
|
tmp
|
||||||
|
bin
|
||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
web/static/dist
|
||||||
|
.air.log
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
name: Docker
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE_NAME: fsadmin/md-to-html
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Prepare tags
|
||||||
|
id: meta
|
||||||
|
shell: sh
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
if [ -z "${{ vars.REGISTRY }}" ]; then
|
||||||
|
echo "::error::Set the REGISTRY repository variable to your Gitea registry host."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
image="${{ vars.REGISTRY }}/${IMAGE_NAME}"
|
||||||
|
short_sha="$(printf '%s' "${GITHUB_SHA}" | cut -c1-12)"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "image=${image}"
|
||||||
|
echo "tags<<EOF"
|
||||||
|
echo "${image}:${short_sha}"
|
||||||
|
if [ "${GITHUB_REF}" = "refs/heads/main" ]; then
|
||||||
|
echo "${image}:latest"
|
||||||
|
fi
|
||||||
|
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
|
||||||
|
echo "${image}:${GITHUB_REF_NAME}"
|
||||||
|
fi
|
||||||
|
echo "EOF"
|
||||||
|
} >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
- name: Login to registry
|
||||||
|
run: |
|
||||||
|
printf '%s' "${{ secrets.REGISTRY_PASSWORD }}" \
|
||||||
|
| docker login "${{ vars.REGISTRY }}" \
|
||||||
|
--username "${{ secrets.REGISTRY_USERNAME }}" \
|
||||||
|
--password-stdin
|
||||||
|
|
||||||
|
- name: Build and push image
|
||||||
|
env:
|
||||||
|
DOCKER_BUILDKIT: "1"
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
tags=""
|
||||||
|
while IFS= read -r tag; do
|
||||||
|
tags="${tags} -t ${tag}"
|
||||||
|
done <<'EOF'
|
||||||
|
${{ steps.meta.outputs.tags }}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
docker build ${tags} .
|
||||||
|
|
||||||
|
while IFS= read -r tag; do
|
||||||
|
docker push "${tag}"
|
||||||
|
done <<'EOF'
|
||||||
|
${{ steps.meta.outputs.tags }}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Trigger Dokploy deployment
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
if [ -z "${{ secrets.DOKPLOY_WEBHOOK_URL }}" ]; then
|
||||||
|
echo "::error::Set the DOKPLOY_WEBHOOK_URL repository secret."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -fsS -X POST "${{ secrets.DOKPLOY_WEBHOOK_URL }}"
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
name: build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
TAILWIND_VERSION: v3.4.17
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Install templ
|
||||||
|
run: go install github.com/a-h/templ/cmd/templ@v0.3.1001
|
||||||
|
|
||||||
|
- name: Install tailwindcss standalone
|
||||||
|
run: |
|
||||||
|
curl -fsSL -o /usr/local/bin/tailwindcss \
|
||||||
|
"https://github.com/tailwindlabs/tailwindcss/releases/download/${TAILWIND_VERSION}/tailwindcss-linux-x64"
|
||||||
|
chmod +x /usr/local/bin/tailwindcss
|
||||||
|
|
||||||
|
- name: Build assets
|
||||||
|
run: |
|
||||||
|
mkdir -p web/static/dist
|
||||||
|
templ generate ./...
|
||||||
|
tailwindcss -c tailwind.config.js -i web/static/src/app.css -o web/static/dist/app.css --minify
|
||||||
|
|
||||||
|
- name: Vet
|
||||||
|
run: go vet ./...
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test -race ./...
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
mkdir -p bin
|
||||||
|
CGO_ENABLED=0 go build -trimpath \
|
||||||
|
-ldflags="-s -w -X github.com/fserg/md-to-html/internal/version.Version=$(cat VERSION)" \
|
||||||
|
-o bin/md-to-html ./cmd/md-to-html
|
||||||
|
|
||||||
|
cross-compile:
|
||||||
|
needs: test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
TAILWIND_VERSION: v3.4.17
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- goos: linux
|
||||||
|
goarch: amd64
|
||||||
|
- goos: linux
|
||||||
|
goarch: arm64
|
||||||
|
- goos: darwin
|
||||||
|
goarch: arm64
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
- name: Install templ
|
||||||
|
run: go install github.com/a-h/templ/cmd/templ@v0.3.1001
|
||||||
|
|
||||||
|
- name: Install tailwindcss standalone
|
||||||
|
run: |
|
||||||
|
curl -fsSL -o /usr/local/bin/tailwindcss \
|
||||||
|
"https://github.com/tailwindlabs/tailwindcss/releases/download/${TAILWIND_VERSION}/tailwindcss-linux-x64"
|
||||||
|
chmod +x /usr/local/bin/tailwindcss
|
||||||
|
|
||||||
|
- name: Build assets
|
||||||
|
run: |
|
||||||
|
mkdir -p web/static/dist
|
||||||
|
templ generate ./...
|
||||||
|
tailwindcss -c tailwind.config.js -i web/static/src/app.css -o web/static/dist/app.css --minify
|
||||||
|
|
||||||
|
- name: Cross-compile
|
||||||
|
env:
|
||||||
|
GOOS: ${{ matrix.goos }}
|
||||||
|
GOARCH: ${{ matrix.goarch }}
|
||||||
|
CGO_ENABLED: "0"
|
||||||
|
run: |
|
||||||
|
mkdir -p bin
|
||||||
|
go build -trimpath \
|
||||||
|
-ldflags="-s -w -X github.com/fserg/md-to-html/internal/version.Version=$(cat VERSION)" \
|
||||||
|
-o "bin/md-to-html-${GOOS}-${GOARCH}" \
|
||||||
|
./cmd/md-to-html
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: md-to-html-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||||
|
path: bin/md-to-html-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||||
|
retention-days: 7
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
name: release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
TAILWIND_VERSION: v3.4.17
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
- name: Install templ
|
||||||
|
run: go install github.com/a-h/templ/cmd/templ@v0.3.1001
|
||||||
|
|
||||||
|
- name: Install tailwindcss standalone
|
||||||
|
run: |
|
||||||
|
curl -fsSL -o /usr/local/bin/tailwindcss \
|
||||||
|
"https://github.com/tailwindlabs/tailwindcss/releases/download/${TAILWIND_VERSION}/tailwindcss-linux-x64"
|
||||||
|
chmod +x /usr/local/bin/tailwindcss
|
||||||
|
|
||||||
|
- name: Build assets
|
||||||
|
run: |
|
||||||
|
mkdir -p web/static/dist
|
||||||
|
templ generate ./...
|
||||||
|
tailwindcss -c tailwind.config.js -i web/static/src/app.css -o web/static/dist/app.css --minify
|
||||||
|
|
||||||
|
- name: Cross-compile binaries
|
||||||
|
run: |
|
||||||
|
mkdir -p dist
|
||||||
|
for target in "linux amd64" "linux arm64" "darwin arm64"; do
|
||||||
|
set -- $target
|
||||||
|
GOOS=$1 GOARCH=$2 CGO_ENABLED=0 go build -trimpath \
|
||||||
|
-ldflags="-s -w -X github.com/fserg/md-to-html/internal/version.Version=$(cat VERSION)" \
|
||||||
|
-o "dist/md-to-html-${1}-${2}" \
|
||||||
|
./cmd/md-to-html
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Compute image name
|
||||||
|
id: image
|
||||||
|
run: |
|
||||||
|
repo_lower=$(echo "${GITHUB_REPOSITORY}" | tr '[:upper:]' '[:lower:]')
|
||||||
|
echo "name=ghcr.io/${repo_lower}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
- uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: |
|
||||||
|
${{ steps.image.outputs.name }}:${{ github.ref_name }}
|
||||||
|
${{ steps.image.outputs.name }}:latest
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
gh release create "${{ github.ref_name }}" \
|
||||||
|
--title "${{ github.ref_name }}" \
|
||||||
|
--notes-file CHANGELOG.md \
|
||||||
|
dist/*
|
||||||
@@ -4,6 +4,42 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
The format is based on Keep a Changelog, and the project uses Semantic Versioning.
|
The format is based on Keep a Changelog, and the project uses Semantic Versioning.
|
||||||
|
|
||||||
|
## [0.2.2] - 2026-04-18
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Web UI redesigned into a single-column classic layout with tabbed file/text input, updated result card, and API snippet.
|
||||||
|
- Added a local release build script and Make targets for current-platform and cross-platform release artifacts.
|
||||||
|
- README updated with the current build, release, and run instructions.
|
||||||
|
|
||||||
|
## [0.2.1] - 2026-04-18
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- GitHub release workflow now lowercases the GHCR image name before publishing, which fixes releases for repositories with uppercase owner names.
|
||||||
|
|
||||||
|
## [0.2.0] - 2026-04-18
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **BREAKING**: project fully rewritten in Go (goldmark + templUI); Python implementation moved to `archive/`.
|
||||||
|
- **BREAKING**: heading anchors now use ASCII transliteration (`## Установка` → `id="ustanovka"`).
|
||||||
|
- **BREAKING**: heading HTML markup simplified; `<div class="markdown-heading">` is no longer emitted.
|
||||||
|
- Removed the GitHub Markdown API dependency; conversion now works fully offline.
|
||||||
|
- Replaced the two-process runtime (uvicorn + Streamlit) with a single binary.
|
||||||
|
- Preview and download links are now one-shot, UUID-backed, and expire after one hour.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Syntax highlighting via chroma with inline styles for self-contained HTML output.
|
||||||
|
- Footnote support in addition to baseline GFM features.
|
||||||
|
- Cross-platform release binaries for `linux/amd64`, `linux/arm64`, and `darwin/arm64`.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- `READY_CHECK_GITHUB` environment variable.
|
||||||
|
- Streamlit UI on dedicated port `:8501`.
|
||||||
|
|
||||||
## [0.1.2] - 2026-04-18
|
## [0.1.2] - 2026-04-18
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
|
|
||||||
|
FROM debian:bookworm-slim AS tailwind
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends ca-certificates curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
ARG TARGETARCH
|
||||||
|
ARG TAILWIND_VERSION=v3.4.17
|
||||||
|
|
||||||
|
RUN case "$TARGETARCH" in \
|
||||||
|
amd64) tailwind_arch='x64' ;; \
|
||||||
|
arm64) tailwind_arch='arm64' ;; \
|
||||||
|
*) echo "unsupported TARGETARCH: $TARGETARCH" >&2; exit 1 ;; \
|
||||||
|
esac \
|
||||||
|
&& curl -fsSL -o /usr/local/bin/tailwindcss \
|
||||||
|
"https://github.com/tailwindlabs/tailwindcss/releases/download/${TAILWIND_VERSION}/tailwindcss-linux-${tailwind_arch}" \
|
||||||
|
&& chmod +x /usr/local/bin/tailwindcss
|
||||||
|
|
||||||
|
COPY tailwind.config.js ./
|
||||||
|
COPY web/ ./web/
|
||||||
|
COPY internal/ui/ ./internal/ui/
|
||||||
|
|
||||||
|
RUN mkdir -p web/static/dist \
|
||||||
|
&& tailwindcss \
|
||||||
|
-c tailwind.config.js \
|
||||||
|
-i web/static/src/app.css \
|
||||||
|
-o web/static/dist/app.css \
|
||||||
|
--minify
|
||||||
|
|
||||||
|
FROM golang:1.24-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates git
|
||||||
|
RUN go install github.com/a-h/templ/cmd/templ@v0.3.1001
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
COPY --from=tailwind /src/web/static/dist/app.css ./web/static/dist/app.css
|
||||||
|
|
||||||
|
RUN templ generate ./... \
|
||||||
|
&& CGO_ENABLED=0 GOOS=linux go build \
|
||||||
|
-trimpath \
|
||||||
|
-ldflags="-s -w -X github.com/fserg/md-to-html/internal/version.Version=$(cat VERSION)" \
|
||||||
|
-o /out/md-to-html \
|
||||||
|
./cmd/md-to-html
|
||||||
|
|
||||||
|
FROM gcr.io/distroless/static-debian12:nonroot
|
||||||
|
|
||||||
|
COPY --from=build /out/md-to-html /md-to-html
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
USER nonroot
|
||||||
|
|
||||||
|
ENTRYPOINT ["/md-to-html", "serve"]
|
||||||
@@ -3,7 +3,7 @@ LDFLAGS := -X github.com/fserg/md-to-html/internal/version.Version=$(VERSION)
|
|||||||
GOBIN := $(shell go env GOPATH)/bin
|
GOBIN := $(shell go env GOPATH)/bin
|
||||||
TEMPL := $(GOBIN)/templ
|
TEMPL := $(GOBIN)/templ
|
||||||
|
|
||||||
.PHONY: build run test templ tailwind dev docker clean tools
|
.PHONY: build run test templ tailwind dev docker clean tools release release-all
|
||||||
|
|
||||||
build:
|
build:
|
||||||
go build -ldflags "$(LDFLAGS)" -o bin/md-to-html ./cmd/md-to-html
|
go build -ldflags "$(LDFLAGS)" -o bin/md-to-html ./cmd/md-to-html
|
||||||
@@ -31,6 +31,12 @@ dev:
|
|||||||
docker:
|
docker:
|
||||||
@echo "docker target will be implemented in phase 6"
|
@echo "docker target will be implemented in phase 6"
|
||||||
|
|
||||||
|
release:
|
||||||
|
./scripts/release-build.sh
|
||||||
|
|
||||||
|
release-all:
|
||||||
|
./scripts/release-build.sh --all
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf bin/ tmp/ web/static/dist/
|
rm -rf bin/ tmp/ web/static/dist/
|
||||||
|
|
||||||
|
|||||||
@@ -1,81 +1,129 @@
|
|||||||
# md-to-html
|
# md-to-html
|
||||||
|
|
||||||
Сервис конвертации Markdown в самодостаточный HTML (через GitHub API).
|
Сервис конвертации Markdown в самодостаточный HTML. Конвертация выполняется локально, без внешних API.
|
||||||
|
|
||||||
Текущая версия: `0.1.2`
|

|
||||||
|
|
||||||
Часто нужен адекватно (минималистично) выглядящий HTML из Markdown. HTML получем через открытый API GitHub, а стили просто захардкожены в шаблоне.
|
Текущая версия: `0.2.2` (Go + goldmark + templUI)
|
||||||
|
|
||||||

|
## Возможности
|
||||||
|
|
||||||
GITHUB_TOKEN не нужен, если не требуется массовая (поточная) конвертация. Но если нужно, то его можно передать через переменную окружения при запуске.
|
- GFM + footnote + emoji + подсветка кода через chroma.
|
||||||
|
- Web UI на `http://localhost:8080/` с загрузкой файла или вставкой текста, HTMX-обновлением результата и одноразовыми ссылками на preview/download.
|
||||||
|
- CLI: `md-to-html cli file.md`.
|
||||||
|
- HTTP API: `POST /convert`, совместим с `v0.1.x`.
|
||||||
|
- Якоря в заголовках с ASCII-транслитом: `## Установка` → `#ustanovka`.
|
||||||
|
|
||||||
Есть два интерфейса:
|
## Запуск через Docker
|
||||||
|
|
||||||
- FastAPI на `http://localhost:8000`
|
|
||||||
- Streamlit UI на `http://localhost:8501` с двумя режимами ввода: загрузка `.md` файла или вставка Markdown-текста из буфера обмена
|
|
||||||
|
|
||||||
## Локальный запуск
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv venv .venv
|
docker run --rm -p 8080:8080 ghcr.io/fserg/md-to-html:latest
|
||||||
source .venv/bin/activate
|
|
||||||
uv pip install -r requirements.txt
|
|
||||||
uvicorn app.api:app --reload
|
|
||||||
streamlit run app/streamlit_app.py
|
|
||||||
```
|
```
|
||||||
|
|
||||||
CLI сохранился:
|
## Быстрый старт
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 md_to_html.py /path/to/file.md
|
go install github.com/a-h/templ/cmd/templ@v0.3.1001
|
||||||
|
npm install
|
||||||
|
make build
|
||||||
|
./bin/md-to-html serve
|
||||||
```
|
```
|
||||||
|
|
||||||
## Docker
|
## Локальная разработка
|
||||||
|
|
||||||
|
Требования: Go 1.24+, Node.js, `templ` CLI.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -t md-to-html .
|
go install github.com/a-h/templ/cmd/templ@v0.3.1001
|
||||||
docker run --rm -p 8000:8000 -p 8501:8501 -e GITHUB_TOKEN=your_token md-to-html
|
npm install
|
||||||
|
make tailwind
|
||||||
|
make build
|
||||||
|
./bin/md-to-html serve
|
||||||
```
|
```
|
||||||
|
|
||||||
## API
|
Для live-reload:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Релизная сборка
|
||||||
|
|
||||||
|
Локальный release-билд для текущей платформы:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make release
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт:
|
||||||
|
- генерирует `templ`-код
|
||||||
|
- собирает Tailwind bundle
|
||||||
|
- прогоняет `go test ./...`
|
||||||
|
- собирает release-бинарь с версией из `VERSION`
|
||||||
|
- кладёт артефакты в `dist/`
|
||||||
|
|
||||||
|
Проверка готового release-билда:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./dist/md-to-html-$(go env GOOS)-$(go env GOARCH) serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Сборка всех release-таргетов как в CI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make release-all
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
md-to-html cli file.md
|
||||||
|
md-to-html cli file.md -o out.html
|
||||||
|
md-to-html cli --stdin < file.md
|
||||||
|
md-to-html cli - --title "Заголовок"
|
||||||
|
```
|
||||||
|
|
||||||
|
## HTTP API
|
||||||
|
|
||||||
`POST /convert`
|
`POST /convert`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:8000/convert \
|
curl -X POST http://localhost:8080/convert \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'content-type: application/json' \
|
||||||
-d '{"markdown":"# Hello"}'
|
-d '{"markdown":"# Привет"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
`GET /health`
|
Прочие эндпоинты:
|
||||||
|
|
||||||
```bash
|
- `GET /` — веб-интерфейс.
|
||||||
curl http://localhost:8000/health
|
- `GET /health`, `GET /version`, `GET /ready` — служебные эндпоинты.
|
||||||
```
|
- `GET /preview/{id}`, `GET /download/{id}` — одноразовые ссылки из веб-формы.
|
||||||
|
|
||||||
`GET /version`
|
## Env-переменные
|
||||||
|
|
||||||
```bash
|
| Переменная | По умолчанию | Назначение |
|
||||||
curl http://localhost:8000/version
|
|----------------------|--------------|------------|
|
||||||
```
|
| `ADDR` | `:8080` | Адрес прослушивания |
|
||||||
|
| `MAX_MARKDOWN_BYTES` | `1048576` | Лимит размера markdown |
|
||||||
|
| `MAX_REQUEST_BYTES` | `1200000` | Лимит размера HTTP-запроса |
|
||||||
|
| `PREVIEW_TTL` | `1h` | TTL одноразовых ссылок |
|
||||||
|
|
||||||
|
## Миграция с v0.1.x
|
||||||
|
|
||||||
|
- API-контракт `POST /convert` не изменился, существующие клиенты продолжают работать.
|
||||||
|
- Якоря заголовков теперь используют ASCII-транслит. Ссылки вида `#установка` нужно заменить на `#ustanovka`.
|
||||||
|
- HTML-разметка упрощена: больше нет `<div class="markdown-heading">`, поэтому ручные CSS-оверрайды нужно пересмотреть.
|
||||||
|
- Переменная окружения `READY_CHECK_GITHUB` удалена: сервис больше не зависит от внешнего Markdown API.
|
||||||
|
- UI работает на том же порту `8080`, отдельный UI-порт `:8501` больше не нужен.
|
||||||
|
|
||||||
|
Python-реализация сохранена в `archive/`.
|
||||||
|
|
||||||
## Релизы
|
## Релизы
|
||||||
|
|
||||||
Проект использует Semantic Versioning. Текущая версия хранится в файле `VERSION`, история изменений ведётся в `CHANGELOG.md`.
|
|
||||||
|
|
||||||
Чтобы выпустить релиз:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add VERSION CHANGELOG.md
|
git commit -am "Release vX.Y.Z"
|
||||||
git commit -m "Release v0.1.2"
|
git tag vX.Y.Z
|
||||||
git tag v0.1.2
|
|
||||||
git push origin main --tags
|
git push origin main --tags
|
||||||
gh release create v0.1.2 --notes-file CHANGELOG.md
|
|
||||||
```
|
```
|
||||||
|
|
||||||
После публикации релиза GitHub Actions автоматически собирает Docker-образ и публикует его в GitHub Container Registry:
|
GitHub Actions публикует Docker-образ для `linux/amd64` и `linux/arm64` в GHCR и прикладывает бинарники для `linux/amd64`, `linux/arm64` и `darwin/arm64` к GitHub Release.
|
||||||
|
|
||||||
```bash
|
|
||||||
docker pull ghcr.io/fserg/md-to-html:v0.1.2
|
|
||||||
```
|
|
||||||
|
|||||||
+2
-2
@@ -15,8 +15,8 @@
|
|||||||
| 3 | [HTTP-сервер](phases/phase-3-server.md) | ✅ done | 2026-04-18 | 2026-04-18 | 843d8dc | |
|
| 3 | [HTTP-сервер](phases/phase-3-server.md) | ✅ done | 2026-04-18 | 2026-04-18 | 843d8dc | |
|
||||||
| 4 | [UI на templUI](phases/phase-4-ui.md) | ✅ done | 2026-04-18 | 2026-04-18 | d6aef55 | |
|
| 4 | [UI на templUI](phases/phase-4-ui.md) | ✅ done | 2026-04-18 | 2026-04-18 | d6aef55 | |
|
||||||
| 5 | [CLI-подкоманда](phases/phase-5-cli.md) | ✅ done | 2026-04-18 | 2026-04-18 | 6aa19fe | |
|
| 5 | [CLI-подкоманда](phases/phase-5-cli.md) | ✅ done | 2026-04-18 | 2026-04-18 | 6aa19fe | |
|
||||||
| 6 | [Docker + CI](phases/phase-6-docker-ci.md) | ⏳ pending | — | — | — | |
|
| 6 | [Docker + CI](phases/phase-6-docker-ci.md) | ✅ done | 2026-04-18 | 2026-04-18 | 4b55661 | Tailwind standalone pinned to `v3.4.17`: `latest` did not emit `web/static/dist/app.css` for the current build pipeline. |
|
||||||
| 7 | [Документация + v0.2.0](phases/phase-7-docs.md) | ⏳ pending | — | — | — | |
|
| 7 | [Документация + v0.2.0](phases/phase-7-docs.md) | ✅ done | 2026-04-18 | 2026-04-18 | a905198 | `v0.2.0` remained as failed tag history after the initial GHCR naming bug; phase completed via patch release `v0.2.1` after lowercasing image tags in `release.yml`. |
|
||||||
|
|
||||||
Легенда статусов:
|
Легенда статусов:
|
||||||
- ⏳ `pending` — не начата
|
- ⏳ `pending` — не начата
|
||||||
|
|||||||
+1274
-258
File diff suppressed because it is too large
Load Diff
+1274
-258
File diff suppressed because it is too large
Load Diff
+1274
-258
File diff suppressed because it is too large
Load Diff
+1274
-258
File diff suppressed because it is too large
Load Diff
+1274
-258
File diff suppressed because it is too large
Load Diff
+1274
-258
File diff suppressed because it is too large
Load Diff
+1274
-258
File diff suppressed because it is too large
Load Diff
+1274
-258
File diff suppressed because it is too large
Load Diff
+1274
-258
File diff suppressed because it is too large
Load Diff
+1274
-258
File diff suppressed because it is too large
Load Diff
+1274
-258
File diff suppressed because it is too large
Load Diff
+1274
-258
File diff suppressed because it is too large
Load Diff
+1274
-258
File diff suppressed because it is too large
Load Diff
+1274
-258
File diff suppressed because it is too large
Load Diff
+1274
-258
File diff suppressed because it is too large
Load Diff
+1274
-258
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fserg/md-to-html/internal/converter"
|
"github.com/fserg/md-to-html/internal/converter"
|
||||||
"github.com/fserg/md-to-html/internal/ui"
|
"github.com/fserg/md-to-html/internal/ui"
|
||||||
@@ -79,6 +80,7 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleUIConvert(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleUIConvert(w http.ResponseWriter, r *http.Request) {
|
||||||
|
startedAt := time.Now()
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, s.cfg.MaxRequestBytes)
|
r.Body = http.MaxBytesReader(w, r.Body, s.cfg.MaxRequestBytes)
|
||||||
if err := r.ParseMultipartForm(s.cfg.MaxRequestBytes); err != nil {
|
if err := r.ParseMultipartForm(s.cfg.MaxRequestBytes); err != nil {
|
||||||
s.renderUIError(w, r, http.StatusRequestEntityTooLarge, "Слишком большой файл или ошибка формы")
|
s.renderUIError(w, r, http.StatusRequestEntityTooLarge, "Слишком большой файл или ошибка формы")
|
||||||
@@ -100,10 +102,15 @@ func (s *Server) handleUIConvert(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
previewID := s.store.Put(result.HTML, "text/html; charset=utf-8", filename)
|
previewID := s.store.Put(result.HTML, "text/html; charset=utf-8", filename)
|
||||||
downloadID := s.store.Put(result.HTML, "text/html; charset=utf-8", filename)
|
downloadID := s.store.Put(result.HTML, "text/html; charset=utf-8", filename)
|
||||||
|
lineCount := bytes.Count(result.HTML, []byte("\n")) + 1
|
||||||
|
elapsedMs := int(time.Since(startedAt).Milliseconds())
|
||||||
|
if elapsedMs < 1 {
|
||||||
|
elapsedMs = 1
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
_ = ui.Result(previewID, downloadID, string(result.HTML), filename).Render(r.Context(), w)
|
_ = ui.Result(previewID, downloadID, string(result.HTML), filename, len(result.HTML), lineCount, elapsedMs).Render(r.Context(), w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handlePreview(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handlePreview(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
+343
-133
@@ -1,158 +1,368 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/fserg/md-to-html/internal/ui/components/button"
|
|
||||||
"github.com/fserg/md-to-html/internal/ui/components/card"
|
|
||||||
)
|
|
||||||
|
|
||||||
templ Home() {
|
templ Home() {
|
||||||
@Layout("Markdown → HTML") {
|
@Layout("Markdown → standalone HTML") {
|
||||||
<div class="panel-grid">
|
<div class="mx-auto max-w-3xl px-6 py-10">
|
||||||
<section class="space-y-6">
|
<header class="mb-8">
|
||||||
<div class="space-y-4">
|
<h1 class="text-2xl font-semibold tracking-tight text-foreground sm:text-[2rem]">Markdown → standalone HTML</h1>
|
||||||
<div class="eyebrow">
|
<p class="mt-1.5 max-w-prose text-sm leading-6 text-muted-foreground">
|
||||||
<span>Go migration</span>
|
Загрузите .md файл или вставьте Markdown-текст. Результат — готовый самодостаточный HTML со встроенными стилями.
|
||||||
<span>goldmark + templUI</span>
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form
|
||||||
|
id="convert-form"
|
||||||
|
class="space-y-6"
|
||||||
|
hx-post="/ui/convert"
|
||||||
|
hx-target="#result"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-encoding="multipart/form-data"
|
||||||
|
onreset="window.setTimeout(window.mdToHTMLResetForm, 0)"
|
||||||
|
>
|
||||||
|
<input id="source-field" type="hidden" name="source" value="file"/>
|
||||||
|
|
||||||
|
<section class="overflow-hidden rounded-xl border border-border bg-background shadow-xs">
|
||||||
|
<div class="border-b border-border px-4 py-4">
|
||||||
|
<div class="inline-flex items-center gap-1 rounded-lg bg-muted p-1" role="tablist" aria-label="Источник markdown">
|
||||||
|
<button
|
||||||
|
id="tab-file"
|
||||||
|
type="button"
|
||||||
|
value="file"
|
||||||
|
class="tabs-trigger"
|
||||||
|
data-state="active"
|
||||||
|
onclick="window.mdToHTMLSetSource('file')"
|
||||||
|
>
|
||||||
|
@FileIcon("size-3.5")
|
||||||
|
<span>Загрузить файл</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="tab-text"
|
||||||
|
type="button"
|
||||||
|
value="text"
|
||||||
|
class="tabs-trigger"
|
||||||
|
onclick="window.mdToHTMLSetSource('text')"
|
||||||
|
>
|
||||||
|
@AlignLeftIcon("size-3.5")
|
||||||
|
<span>Вставить текст</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-3">
|
|
||||||
<h1 class="max-w-3xl text-4xl font-semibold leading-tight tracking-tight text-foreground sm:text-5xl">
|
<div class="p-6">
|
||||||
Markdown → HTML без внешних зависимостей в результирующем документе.
|
<div id="panel-file" class="flex flex-col gap-4">
|
||||||
</h1>
|
<label
|
||||||
<p class="max-w-2xl text-base leading-7 text-muted-foreground sm:text-lg">
|
id="markdown-dropzone"
|
||||||
Загрузите `.md`-файл или вставьте текст вручную. Сервис отдаст автономный HTML, одноразовое превью и отдельную ссылку на скачивание.
|
for="markdown-file"
|
||||||
</p>
|
class="dropzone group block cursor-pointer rounded-lg border-2 border-dashed border-border p-10 text-center transition hover:border-foreground/25"
|
||||||
</div>
|
>
|
||||||
</div>
|
|
||||||
<div class="grid gap-4 sm:grid-cols-3">
|
|
||||||
<div class="section-card p-4">
|
|
||||||
<div class="text-sm font-semibold text-foreground">Самодостаточный HTML</div>
|
|
||||||
<p class="mt-2 text-sm leading-6 text-muted-foreground">Результат открывается локально без CDN и без сетевых вызовов.</p>
|
|
||||||
</div>
|
|
||||||
<div class="section-card p-4">
|
|
||||||
<div class="text-sm font-semibold text-foreground">Одноразовые ссылки</div>
|
|
||||||
<p class="mt-2 text-sm leading-6 text-muted-foreground">Preview и download живут до первого открытия или максимум один час.</p>
|
|
||||||
</div>
|
|
||||||
<div class="section-card p-4">
|
|
||||||
<div class="text-sm font-semibold text-foreground">Русский интерфейс</div>
|
|
||||||
<p class="mt-2 text-sm leading-6 text-muted-foreground">Форма ориентирована на быстрый ручной прогон документации и заметок.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
@card.Card(card.Props{Class: "section-card overflow-hidden"}) {
|
|
||||||
@card.Header(card.HeaderProps{Class: "space-y-2 border-b border-border/70 pb-6"}) {
|
|
||||||
<div class="text-sm font-semibold uppercase tracking-[0.18em] text-muted-foreground">Конвертация</div>
|
|
||||||
@card.Title(card.TitleProps{Class: "text-2xl font-semibold tracking-tight text-foreground"}) {
|
|
||||||
Выберите источник Markdown
|
|
||||||
}
|
|
||||||
@card.Description(card.DescriptionProps{Class: "max-w-xl text-sm leading-6 text-muted-foreground"}) {
|
|
||||||
Форма отправляется через HTMX на `POST /ui/convert`, а результат подменяется прямо в блоке ниже.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@card.Content(card.ContentProps{Class: "space-y-5"}) {
|
|
||||||
<form
|
|
||||||
id="convert-form"
|
|
||||||
hx-post="/ui/convert"
|
|
||||||
hx-target="#result"
|
|
||||||
hx-swap="innerHTML"
|
|
||||||
hx-encoding="multipart/form-data"
|
|
||||||
class="space-y-5"
|
|
||||||
>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="field-label">Источник</div>
|
|
||||||
<div class="grid grid-cols-2 gap-2 rounded-[1.35rem] border border-border/80 bg-muted/55 p-2">
|
|
||||||
<label
|
|
||||||
class="source-tab source-tab-active"
|
|
||||||
data-source-tab="file"
|
|
||||||
data-active-classes="source-tab source-tab-active"
|
|
||||||
data-inactive-classes="source-tab"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="source"
|
|
||||||
value="file"
|
|
||||||
class="sr-only"
|
|
||||||
checked
|
|
||||||
onchange="window.mdToHTMLSwitchSource(this.value)"
|
|
||||||
/>
|
|
||||||
Файл
|
|
||||||
</label>
|
|
||||||
<label
|
|
||||||
class="source-tab"
|
|
||||||
data-source-tab="text"
|
|
||||||
data-active-classes="source-tab source-tab-active"
|
|
||||||
data-inactive-classes="source-tab"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="source"
|
|
||||||
value="text"
|
|
||||||
class="sr-only"
|
|
||||||
onchange="window.mdToHTMLSwitchSource(this.value)"
|
|
||||||
/>
|
|
||||||
Текст
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="source-file" class="source-panel space-y-3">
|
|
||||||
<label class="field-label" for="markdown-file">Markdown-файл</label>
|
|
||||||
<input
|
<input
|
||||||
id="markdown-file"
|
id="markdown-file"
|
||||||
class="surface-input file:mr-4 file:rounded-xl file:border-0 file:bg-primary file:px-4 file:py-2 file:text-sm file:font-semibold file:text-primary-foreground hover:file:bg-primary/90"
|
|
||||||
type="file"
|
type="file"
|
||||||
name="markdown_file"
|
name="markdown_file"
|
||||||
accept=".md,.markdown,.mdown,text/markdown"
|
accept=".md,.markdown,.mdown,text/markdown"
|
||||||
|
class="sr-only"
|
||||||
|
onchange="window.mdToHTMLHandleFileChange(this)"
|
||||||
/>
|
/>
|
||||||
<p class="field-hint">Используйте для загрузки существующего документа. Имя файла станет базой для имени HTML.</p>
|
<div class="mx-auto mb-3 grid size-10 place-items-center rounded-full bg-muted text-muted-foreground transition group-hover:bg-primary/5 group-hover:text-foreground">
|
||||||
|
@UploadIcon("size-5")
|
||||||
|
</div>
|
||||||
|
<div class="text-sm font-medium text-foreground">Перетащите .md файл сюда</div>
|
||||||
|
<div class="mt-1 text-xs text-muted-foreground">
|
||||||
|
или <span class="text-foreground underline underline-offset-2">выберите на диске</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 text-[11px] text-muted-foreground">Лимит: 200 MB · Тип: text/markdown</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div id="selected-file" class="hidden items-center gap-3 rounded-lg border border-border bg-muted/40 p-3">
|
||||||
|
<div class="grid size-9 shrink-0 place-items-center rounded-md border border-border bg-background text-muted-foreground">
|
||||||
|
@FileIcon("size-4")
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<div id="selected-file-name" class="truncate text-sm font-medium text-foreground">README.md</div>
|
||||||
|
<div id="selected-file-meta" class="text-xs text-muted-foreground font-mono">3.4 KB · изменён только что</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex size-7 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted hover:text-foreground"
|
||||||
|
aria-label="Удалить файл"
|
||||||
|
onclick="window.mdToHTMLClearFile()"
|
||||||
|
>
|
||||||
|
@CloseIcon("size-4")
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="source-text" class="source-panel hidden space-y-3">
|
</div>
|
||||||
<label class="field-label" for="markdown-text">Markdown-текст</label>
|
|
||||||
|
<div id="panel-text" class="hidden flex-col gap-3">
|
||||||
|
<label for="markdown-text" class="text-[13px] font-medium text-foreground">Markdown-текст</label>
|
||||||
|
<div class="relative">
|
||||||
<textarea
|
<textarea
|
||||||
id="markdown-text"
|
id="markdown-text"
|
||||||
class="surface-textarea"
|
|
||||||
name="markdown_text"
|
name="markdown_text"
|
||||||
rows="14"
|
rows="10"
|
||||||
placeholder="# Привет, мир - списки - таблицы - код"
|
class="focus-ring min-h-[16rem] w-full resize-y rounded-md border border-border bg-background px-3 py-2.5 font-mono text-sm leading-6 text-foreground placeholder:text-muted-foreground"
|
||||||
|
placeholder="# Мой заголовок Здесь будет текст..."
|
||||||
|
oninput="window.mdToHTMLUpdateCharCount(this)"
|
||||||
></textarea>
|
></textarea>
|
||||||
<p class="field-hint">Подходит для быстрых заметок и вставок без промежуточного файла.</p>
|
<span id="markdown-char-count" class="pointer-events-none absolute bottom-2.5 right-3 text-[10px] text-muted-foreground font-mono">
|
||||||
|
0 символов
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<p class="flex items-center gap-2 text-[11px] text-muted-foreground">
|
||||||
@button.Button(button.Props{
|
@InfoIcon("size-3.5")
|
||||||
Type: button.TypeSubmit,
|
<span>Поддерживается CommonMark + GFM</span>
|
||||||
Class: "rounded-2xl bg-primary px-5 py-3 text-sm font-semibold text-primary-foreground hover:bg-primary/90",
|
</p>
|
||||||
Variant: button.VariantDefault,
|
</div>
|
||||||
Size: button.SizeDefault,
|
</div>
|
||||||
}) {
|
|
||||||
<span>Конвертировать</span>
|
<div class="flex flex-wrap items-center justify-end gap-3 border-t border-border px-4 py-4">
|
||||||
}
|
<div class="flex items-center gap-2">
|
||||||
<span class="field-hint">Лимиты тела запроса и markdown берутся из server config.</span>
|
<button
|
||||||
</div>
|
type="reset"
|
||||||
</form>
|
class="focus-ring inline-flex h-9 items-center justify-center rounded-md border border-border bg-background px-3.5 text-sm font-medium text-foreground transition hover:bg-muted"
|
||||||
<div id="result" class="min-h-[4rem]"></div>
|
>
|
||||||
}
|
Сбросить
|
||||||
}
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="focus-ring inline-flex h-9 items-center justify-center gap-2 rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition hover:bg-primary/90"
|
||||||
|
>
|
||||||
|
<span>Конвертировать</span>
|
||||||
|
@ArrowRightIcon("size-4")
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="result" class="mt-6"></div>
|
||||||
|
|
||||||
|
<section class="mt-6 overflow-hidden rounded-xl border border-border bg-background shadow-xs">
|
||||||
|
<div class="flex items-center justify-between border-b border-border px-5 py-3.5">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm font-medium text-foreground">API</span>
|
||||||
|
<span class="inline-flex items-center rounded-md border border-border bg-muted px-1.5 py-px text-[10px] font-medium text-foreground font-mono">
|
||||||
|
POST /convert
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="focus-ring inline-flex size-7 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted hover:text-foreground"
|
||||||
|
aria-label="Скопировать curl"
|
||||||
|
data-copy-target="api-curl"
|
||||||
|
data-copy-label="curl"
|
||||||
|
onclick="window.mdToHTMLCopyButton(this)"
|
||||||
|
>
|
||||||
|
@CopyIcon("size-3.5")
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<textarea id="api-curl" class="sr-only" readonly>curl -X POST http://localhost:8000/convert \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"markdown":"# Hello"}'</textarea>
|
||||||
|
<pre class="overflow-x-auto px-5 py-4 font-mono text-[12px] leading-relaxed text-foreground"><span class="text-muted-foreground">$</span> curl -X POST http://localhost:8000/convert \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"markdown":"# Hello"}'</pre>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.mdToHTMLSwitchSource = function(value) {
|
(() => {
|
||||||
const filePanel = document.getElementById("source-file");
|
function byId(id) {
|
||||||
const textPanel = document.getElementById("source-text");
|
return document.getElementById(id);
|
||||||
if (!filePanel || !textPanel) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const showFile = value === "file";
|
function formatBytes(bytes) {
|
||||||
filePanel.classList.toggle("hidden", !showFile);
|
if (bytes < 1024) {
|
||||||
textPanel.classList.toggle("hidden", showFile);
|
return bytes + " B";
|
||||||
|
}
|
||||||
|
return (bytes / 1024).toFixed(1) + " KB";
|
||||||
|
}
|
||||||
|
|
||||||
document.querySelectorAll("[data-source-tab]").forEach((tab) => {
|
function formatRelativeTime(lastModified) {
|
||||||
const tabValue = tab.getAttribute("data-source-tab");
|
const delta = Date.now() - lastModified;
|
||||||
const active = tabValue === value;
|
const minute = 60 * 1000;
|
||||||
tab.className = active
|
const hour = 60 * minute;
|
||||||
? tab.getAttribute("data-active-classes")
|
const day = 24 * hour;
|
||||||
: tab.getAttribute("data-inactive-classes");
|
|
||||||
});
|
if (delta < minute) {
|
||||||
};
|
return "изменён только что";
|
||||||
|
}
|
||||||
|
if (delta < hour) {
|
||||||
|
const value = Math.max(1, Math.round(delta / minute));
|
||||||
|
return "изменён " + value + " мин назад";
|
||||||
|
}
|
||||||
|
if (delta < day) {
|
||||||
|
const value = Math.max(1, Math.round(delta / hour));
|
||||||
|
return "изменён " + value + " ч назад";
|
||||||
|
}
|
||||||
|
const value = Math.max(1, Math.round(delta / day));
|
||||||
|
return "изменён " + value + " дн назад";
|
||||||
|
}
|
||||||
|
|
||||||
|
function flashCopyState(button) {
|
||||||
|
if (!button) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const original = button.dataset.copyFlashOriginal || button.innerHTML;
|
||||||
|
button.dataset.copyFlashOriginal = original;
|
||||||
|
button.innerHTML = "Скопировано";
|
||||||
|
window.setTimeout(() => {
|
||||||
|
button.innerHTML = original;
|
||||||
|
}, 1400);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyText(value) {
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
await navigator.clipboard.writeText(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const helper = document.createElement("textarea");
|
||||||
|
helper.value = value;
|
||||||
|
helper.setAttribute("readonly", "readonly");
|
||||||
|
helper.style.position = "absolute";
|
||||||
|
helper.style.left = "-9999px";
|
||||||
|
document.body.appendChild(helper);
|
||||||
|
helper.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
document.body.removeChild(helper);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindDropzone() {
|
||||||
|
const dropzone = byId("markdown-dropzone");
|
||||||
|
const input = byId("markdown-file");
|
||||||
|
if (!dropzone || !input || dropzone.dataset.bound === "true") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeClasses = ["border-foreground/35", "bg-muted/60"];
|
||||||
|
dropzone.dataset.bound = "true";
|
||||||
|
|
||||||
|
dropzone.addEventListener("dragover", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
activeClasses.forEach((className) => dropzone.classList.add(className));
|
||||||
|
});
|
||||||
|
dropzone.addEventListener("dragleave", () => {
|
||||||
|
activeClasses.forEach((className) => dropzone.classList.remove(className));
|
||||||
|
});
|
||||||
|
dropzone.addEventListener("drop", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
activeClasses.forEach((className) => dropzone.classList.remove(className));
|
||||||
|
if (!event.dataTransfer || !event.dataTransfer.files.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
input.files = event.dataTransfer.files;
|
||||||
|
window.mdToHTMLHandleFileChange(input);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.mdToHTMLSetSource = function(source) {
|
||||||
|
const sourceField = byId("source-field");
|
||||||
|
const filePanel = byId("panel-file");
|
||||||
|
const textPanel = byId("panel-text");
|
||||||
|
const fileTab = byId("tab-file");
|
||||||
|
const textTab = byId("tab-text");
|
||||||
|
if (!sourceField || !filePanel || !textPanel || !fileTab || !textTab) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const showFile = source === "file";
|
||||||
|
sourceField.value = source;
|
||||||
|
filePanel.classList.toggle("hidden", !showFile);
|
||||||
|
filePanel.classList.toggle("flex", showFile);
|
||||||
|
textPanel.classList.toggle("hidden", showFile);
|
||||||
|
textPanel.classList.toggle("flex", !showFile);
|
||||||
|
fileTab.setAttribute("data-state", showFile ? "active" : "inactive");
|
||||||
|
textTab.setAttribute("data-state", showFile ? "inactive" : "active");
|
||||||
|
};
|
||||||
|
|
||||||
|
window.mdToHTMLHandleFileChange = function(input) {
|
||||||
|
const file = input && input.files && input.files[0];
|
||||||
|
const summary = byId("selected-file");
|
||||||
|
const fileName = byId("selected-file-name");
|
||||||
|
const fileMeta = byId("selected-file-meta");
|
||||||
|
if (!summary || !fileName || !fileMeta) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
summary.classList.add("hidden");
|
||||||
|
summary.classList.remove("flex");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName.textContent = file.name;
|
||||||
|
fileMeta.textContent = formatBytes(file.size) + " · " + formatRelativeTime(file.lastModified);
|
||||||
|
summary.classList.remove("hidden");
|
||||||
|
summary.classList.add("flex");
|
||||||
|
};
|
||||||
|
|
||||||
|
window.mdToHTMLClearFile = function() {
|
||||||
|
const input = byId("markdown-file");
|
||||||
|
const summary = byId("selected-file");
|
||||||
|
if (input) {
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
if (summary) {
|
||||||
|
summary.classList.add("hidden");
|
||||||
|
summary.classList.remove("flex");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.mdToHTMLUpdateCharCount = function(textarea) {
|
||||||
|
const counter = byId("markdown-char-count");
|
||||||
|
if (!counter || !textarea) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const count = textarea.value.length;
|
||||||
|
counter.textContent = count + " символов";
|
||||||
|
};
|
||||||
|
|
||||||
|
window.mdToHTMLResetForm = function() {
|
||||||
|
window.mdToHTMLSetSource("file");
|
||||||
|
window.mdToHTMLClearFile();
|
||||||
|
const textarea = byId("markdown-text");
|
||||||
|
if (textarea) {
|
||||||
|
window.mdToHTMLUpdateCharCount(textarea);
|
||||||
|
}
|
||||||
|
const result = byId("result");
|
||||||
|
if (result) {
|
||||||
|
result.innerHTML = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.mdToHTMLCopyButton = async function(button) {
|
||||||
|
if (!button) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const targetID = button.dataset.copyTarget;
|
||||||
|
const target = targetID ? byId(targetID) : null;
|
||||||
|
const value = target ? target.value || target.textContent || "" : "";
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await copyText(value);
|
||||||
|
flashCopyState(button);
|
||||||
|
} catch (_) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
bindDropzone();
|
||||||
|
window.mdToHTMLSetSource("file");
|
||||||
|
const textarea = byId("markdown-text");
|
||||||
|
if (textarea) {
|
||||||
|
window.mdToHTMLUpdateCharCount(textarea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", init, { once: true });
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+60
-150
File diff suppressed because one or more lines are too long
@@ -0,0 +1,75 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
templ UploadIcon(class string) {
|
||||||
|
<svg class={ class } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||||
|
<polyline points="17 8 12 3 7 8"></polyline>
|
||||||
|
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ FileIcon(class string) {
|
||||||
|
<svg class={ class } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||||
|
<path d="M14 2v6h6"></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ AlignLeftIcon(class string) {
|
||||||
|
<svg class={ class } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M4 7h16"></path>
|
||||||
|
<path d="M4 12h10"></path>
|
||||||
|
<path d="M4 17h16"></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ ArrowRightIcon(class string) {
|
||||||
|
<svg class={ class } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M5 12h14"></path>
|
||||||
|
<path d="m12 5 7 7-7 7"></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ InfoIcon(class string) {
|
||||||
|
<svg class={ class } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<path d="M12 16v-4"></path>
|
||||||
|
<path d="M12 8h.01"></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ CloseIcon(class string) {
|
||||||
|
<svg class={ class } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M18 6 6 18"></path>
|
||||||
|
<path d="m6 6 12 12"></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ CheckIcon(class string) {
|
||||||
|
<svg class={ class } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M20 6 9 17l-5-5"></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ DownloadIcon(class string) {
|
||||||
|
<svg class={ class } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||||
|
<polyline points="7 10 12 15 17 10"></polyline>
|
||||||
|
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ CopyIcon(class string) {
|
||||||
|
<svg class={ class } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect>
|
||||||
|
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ ExternalLinkIcon(class string) {
|
||||||
|
<svg class={ class } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
|
||||||
|
<polyline points="15 3 21 3 21 9"></polyline>
|
||||||
|
<line x1="10" y1="14" x2="21" y2="3"></line>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
@@ -0,0 +1,481 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.1001
|
||||||
|
package ui
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
func UploadIcon(class string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
var templ_7745c5c3_Var2 = []any{class}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<svg class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/icons.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"></path> <polyline points=\"17 8 12 3 7 8\"></polyline> <line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"15\"></line></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileIcon(class string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var4 == nil {
|
||||||
|
templ_7745c5c3_Var4 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
var templ_7745c5c3_Var5 = []any{class}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<svg class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var5).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/icons.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"></path> <path d=\"M14 2v6h6\"></path></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func AlignLeftIcon(class string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var7 == nil {
|
||||||
|
templ_7745c5c3_Var7 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
var templ_7745c5c3_Var8 = []any{class}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<svg class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 string
|
||||||
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var8).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/icons.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M4 7h16\"></path> <path d=\"M4 12h10\"></path> <path d=\"M4 17h16\"></path></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ArrowRightIcon(class string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var10 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var10 == nil {
|
||||||
|
templ_7745c5c3_Var10 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
var templ_7745c5c3_Var11 = []any{class}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<svg class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var12 string
|
||||||
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var11).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/icons.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M5 12h14\"></path> <path d=\"m12 5 7 7-7 7\"></path></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func InfoIcon(class string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var13 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var13 == nil {
|
||||||
|
templ_7745c5c3_Var13 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
var templ_7745c5c3_Var14 = []any{class}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<svg class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var15 string
|
||||||
|
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var14).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/icons.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"></circle> <path d=\"M12 16v-4\"></path> <path d=\"M12 8h.01\"></path></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func CloseIcon(class string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var16 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var16 == nil {
|
||||||
|
templ_7745c5c3_Var16 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
var templ_7745c5c3_Var17 = []any{class}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<svg class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var18 string
|
||||||
|
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var17).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/icons.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M18 6 6 18\"></path> <path d=\"m6 6 12 12\"></path></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckIcon(class string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var19 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var19 == nil {
|
||||||
|
templ_7745c5c3_Var19 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
var templ_7745c5c3_Var20 = []any{class}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<svg class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var21 string
|
||||||
|
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var20).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/icons.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"></path></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownloadIcon(class string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var22 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var22 == nil {
|
||||||
|
templ_7745c5c3_Var22 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
var templ_7745c5c3_Var23 = []any{class}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var23...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<svg class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var24 string
|
||||||
|
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var23).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/icons.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"></path> <polyline points=\"7 10 12 15 17 10\"></polyline> <line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"></line></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func CopyIcon(class string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var25 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var25 == nil {
|
||||||
|
templ_7745c5c3_Var25 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
var templ_7745c5c3_Var26 = []any{class}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var26...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<svg class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var27 string
|
||||||
|
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var26).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/icons.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"></rect> <path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"></path></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExternalLinkIcon(class string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var28 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var28 == nil {
|
||||||
|
templ_7745c5c3_Var28 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
var templ_7745c5c3_Var29 = []any{class}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var29...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<svg class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var30 string
|
||||||
|
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var29).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/icons.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"></path> <polyline points=\"15 3 21 3 21 9\"></polyline> <line x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\"></line></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
+12
-17
@@ -1,28 +1,23 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import "github.com/fserg/md-to-html/internal/version"
|
|
||||||
|
|
||||||
templ Layout(title string) {
|
templ Layout(title string) {
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ru">
|
<html lang="ru">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
<title>{ title }</title>
|
<title>{ title } · md-to-html</title>
|
||||||
<link rel="stylesheet" href="/static/dist/app.css"/>
|
<link rel="stylesheet" href="/static/dist/app.css"/>
|
||||||
<script src="/static/htmx.min.js"></script>
|
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500;600&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<script src="https://unpkg.com/htmx.org@2.0.3"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="min-h-screen bg-[#fafafa] font-sans text-foreground antialiased">
|
||||||
<div class="app-shell">
|
{ children... }
|
||||||
<div class="hero-panel">
|
|
||||||
<div class="relative px-5 py-6 sm:px-8 sm:py-8 lg:px-10 lg:py-10">
|
|
||||||
{ children... }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<footer class="mt-6 text-center text-sm text-muted-foreground">
|
|
||||||
Markdown → HTML · v{ version.Version }
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ package ui
|
|||||||
import "github.com/a-h/templ"
|
import "github.com/a-h/templ"
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
import "github.com/fserg/md-to-html/internal/version"
|
|
||||||
|
|
||||||
func Layout(title string) templ.Component {
|
func Layout(title string) templ.Component {
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
@@ -31,20 +29,20 @@ func Layout(title string) templ.Component {
|
|||||||
templ_7745c5c3_Var1 = templ.NopComponent
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"ru\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"ru\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var2 string
|
var templ_7745c5c3_Var2 string
|
||||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/layout.templ`, Line: 11, Col: 17}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/layout.templ`, Line: 9, Col: 17}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><link rel=\"stylesheet\" href=\"/static/dist/app.css\"><script src=\"/static/htmx.min.js\"></script></head><body><div class=\"app-shell\"><div class=\"hero-panel\"><div class=\"relative px-5 py-6 sm:px-8 sm:py-8 lg:px-10 lg:py-10\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " · md-to-html</title><link rel=\"stylesheet\" href=\"/static/dist/app.css\"><link rel=\"preconnect\" href=\"https://fonts.googleapis.com\"><link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin><link href=\"https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500;600&display=swap\" rel=\"stylesheet\"><script src=\"https://unpkg.com/htmx.org@2.0.3\"></script></head><body class=\"min-h-screen bg-[#fafafa] font-sans text-foreground antialiased\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -52,20 +50,7 @@ func Layout(title string) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div></div><footer class=\"mt-6 text-center text-sm text-muted-foreground\">Markdown → HTML · v")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</body></html>")
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var3 string
|
|
||||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(version.Version)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/layout.templ`, Line: 23, Col: 44}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</footer></div></body></html>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
+62
-47
@@ -1,56 +1,71 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import "fmt"
|
||||||
"github.com/fserg/md-to-html/internal/ui/components/button"
|
|
||||||
"github.com/fserg/md-to-html/internal/ui/components/card"
|
|
||||||
)
|
|
||||||
|
|
||||||
templ Result(previewID, downloadID string, fullHTML string, filename string) {
|
templ Result(previewID, downloadID, fullHTML, filename string, sizeBytes int, lineCount int, elapsedMs int) {
|
||||||
@card.Card(card.Props{Class: "section-card border-primary/20 bg-background/90"}) {
|
<div id="result" class="mt-6">
|
||||||
@card.Content(card.ContentProps{Class: "space-y-4"}) {
|
<section class="overflow-hidden rounded-xl border border-border bg-background shadow-xs">
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="flex items-center gap-3 border-b border-border px-5 py-4">
|
||||||
@button.Button(button.Props{
|
<div class="grid size-8 shrink-0 place-items-center rounded-md bg-emerald-50 text-emerald-600">
|
||||||
Href: "/preview/" + previewID,
|
@CheckIcon("size-4")
|
||||||
Target: "_blank",
|
|
||||||
Class: "rounded-2xl bg-primary px-4 py-2.5 text-sm font-semibold text-primary-foreground hover:bg-primary/90",
|
|
||||||
Variant: button.VariantDefault,
|
|
||||||
}) {
|
|
||||||
Открыть превью
|
|
||||||
}
|
|
||||||
@button.Button(button.Props{
|
|
||||||
Href: "/download/" + downloadID,
|
|
||||||
Class: "rounded-2xl border border-border bg-card px-4 py-2.5 text-sm font-semibold text-foreground hover:bg-muted/60",
|
|
||||||
Variant: button.VariantOutline,
|
|
||||||
}) {
|
|
||||||
Скачать HTML
|
|
||||||
}
|
|
||||||
<span class="text-sm text-muted-foreground">Файл: <span class="font-medium text-foreground">{ filename }</span></span>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm leading-6 text-muted-foreground">
|
|
||||||
Ссылки одноразовые: после первого успешного открытия соответствующий UUID удаляется из preview-store.
|
|
||||||
</p>
|
|
||||||
<details class="group overflow-hidden rounded-[1.25rem] border border-border bg-card/80">
|
|
||||||
<summary class="cursor-pointer list-none px-4 py-3 text-sm font-semibold text-foreground">
|
|
||||||
<span class="inline-flex items-center gap-2">
|
|
||||||
<span class="inline-flex size-6 items-center justify-center rounded-full bg-muted text-xs text-muted-foreground">i</span>
|
|
||||||
Inline-превью в изолированном iframe
|
|
||||||
</span>
|
|
||||||
</summary>
|
|
||||||
<div class="border-t border-border/70 px-4 pb-4 pt-3">
|
|
||||||
<iframe
|
|
||||||
class="result-frame"
|
|
||||||
sandbox=""
|
|
||||||
referrerpolicy="no-referrer"
|
|
||||||
srcdoc={ fullHTML }
|
|
||||||
></iframe>
|
|
||||||
</div>
|
</div>
|
||||||
</details>
|
<div class="min-w-0 flex-1">
|
||||||
}
|
<div class="truncate text-sm font-medium text-foreground">Готово — { filename }</div>
|
||||||
}
|
<div class="text-xs text-muted-foreground font-mono">
|
||||||
|
{ formatResultMeta(sizeBytes, lineCount, elapsedMs) }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="inline-flex items-center rounded-md border border-border bg-muted px-2 py-0.5 text-[11px] font-medium text-foreground font-mono">
|
||||||
|
standalone
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<textarea id={ "result-html-" + previewID } class="sr-only" readonly>{ fullHTML }</textarea>
|
||||||
|
<a href={ "/preview/" + previewID } target="_blank" rel="noreferrer" class="sr-only">Открыть превью</a>
|
||||||
|
<iframe class="hidden" sandbox="" referrerpolicy="no-referrer" srcdoc={ fullHTML }></iframe>
|
||||||
|
<div class="flex flex-wrap items-center gap-2 px-5 py-5">
|
||||||
|
<a
|
||||||
|
href={ "/download/" + downloadID }
|
||||||
|
class="focus-ring inline-flex h-9 items-center justify-center gap-2 rounded-md bg-primary px-3.5 text-sm font-medium text-primary-foreground transition hover:bg-primary/90"
|
||||||
|
>
|
||||||
|
@DownloadIcon("size-4")
|
||||||
|
<span>Скачать HTML</span>
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="focus-ring inline-flex h-9 items-center justify-center gap-2 rounded-md border border-border bg-background px-3.5 text-sm font-medium text-foreground transition hover:bg-muted"
|
||||||
|
data-copy-target={ "result-html-" + previewID }
|
||||||
|
onclick="window.mdToHTMLCopyButton(this)"
|
||||||
|
>
|
||||||
|
@CopyIcon("size-4")
|
||||||
|
<span>Скопировать</span>
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href={ "/preview/" + previewID }
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
class="focus-ring inline-flex h-9 items-center justify-center gap-2 rounded-md border border-border bg-background px-3.5 text-sm font-medium text-foreground transition hover:bg-muted"
|
||||||
|
>
|
||||||
|
@ExternalLinkIcon("size-4")
|
||||||
|
<span>Открыть в новой вкладке</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ Error(msg string) {
|
templ Error(msg string) {
|
||||||
<div class="rounded-[1.25rem] border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
|
<div id="result" class="mt-6">
|
||||||
{ msg }
|
<div class="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800 shadow-xs">
|
||||||
|
{ msg }
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func formatResultMeta(sizeBytes int, lineCount int, elapsedMs int) string {
|
||||||
|
kilobytes := float64(sizeBytes) / 1024
|
||||||
|
seconds := float64(elapsedMs) / 1000
|
||||||
|
if seconds < 0.1 {
|
||||||
|
seconds = 0.1
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.1f KB · %d строки · сгенерирован %.1f сек назад", kilobytes, lineCount, seconds)
|
||||||
|
}
|
||||||
|
|||||||
+170
-134
@@ -8,12 +8,9 @@ package ui
|
|||||||
import "github.com/a-h/templ"
|
import "github.com/a-h/templ"
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
import (
|
import "fmt"
|
||||||
"github.com/fserg/md-to-html/internal/ui/components/button"
|
|
||||||
"github.com/fserg/md-to-html/internal/ui/components/card"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Result(previewID, downloadID string, fullHTML string, filename string) templ.Component {
|
func Result(previewID, downloadID, fullHTML, filename string, sizeBytes int, lineCount int, elapsedMs int) templ.Component {
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
@@ -34,126 +31,156 @@ func Result(previewID, downloadID string, fullHTML string, filename string) temp
|
|||||||
templ_7745c5c3_Var1 = templ.NopComponent
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"result\" class=\"mt-6\"><section class=\"overflow-hidden rounded-xl border border-border bg-background shadow-xs\"><div class=\"flex items-center gap-3 border-b border-border px-5 py-4\"><div class=\"grid size-8 shrink-0 place-items-center rounded-md bg-emerald-50 text-emerald-600\">")
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
if templ_7745c5c3_Err != nil {
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
return templ_7745c5c3_Err
|
||||||
if !templ_7745c5c3_IsBuffer {
|
}
|
||||||
defer func() {
|
templ_7745c5c3_Err = CheckIcon("size-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
if templ_7745c5c3_Err != nil {
|
||||||
if templ_7745c5c3_Err == nil {
|
return templ_7745c5c3_Err
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
}
|
||||||
}
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div><div class=\"min-w-0 flex-1\"><div class=\"truncate text-sm font-medium text-foreground\">Готово — ")
|
||||||
}()
|
if templ_7745c5c3_Err != nil {
|
||||||
}
|
return templ_7745c5c3_Err
|
||||||
ctx = templ.InitializeContext(ctx)
|
}
|
||||||
templ_7745c5c3_Var3 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
var templ_7745c5c3_Var2 string
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(filename)
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
if templ_7745c5c3_Err != nil {
|
||||||
if !templ_7745c5c3_IsBuffer {
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/result.templ`, Line: 13, Col: 90}
|
||||||
defer func() {
|
}
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
if templ_7745c5c3_Err == nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}()
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div><div class=\"text-xs text-muted-foreground font-mono\">")
|
||||||
}
|
if templ_7745c5c3_Err != nil {
|
||||||
ctx = templ.InitializeContext(ctx)
|
return templ_7745c5c3_Err
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"flex flex-wrap items-center gap-3\">")
|
}
|
||||||
if templ_7745c5c3_Err != nil {
|
var templ_7745c5c3_Var3 string
|
||||||
return templ_7745c5c3_Err
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(formatResultMeta(sizeBytes, lineCount, elapsedMs))
|
||||||
}
|
if templ_7745c5c3_Err != nil {
|
||||||
templ_7745c5c3_Var4 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/result.templ`, Line: 15, Col: 57}
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
}
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
if !templ_7745c5c3_IsBuffer {
|
if templ_7745c5c3_Err != nil {
|
||||||
defer func() {
|
return templ_7745c5c3_Err
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
}
|
||||||
if templ_7745c5c3_Err == nil {
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div></div><span class=\"inline-flex items-center rounded-md border border-border bg-muted px-2 py-0.5 text-[11px] font-medium text-foreground font-mono\">standalone</span></div><textarea id=\"")
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
if templ_7745c5c3_Err != nil {
|
||||||
}
|
return templ_7745c5c3_Err
|
||||||
}()
|
}
|
||||||
}
|
var templ_7745c5c3_Var4 string
|
||||||
ctx = templ.InitializeContext(ctx)
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs("result-html-" + previewID)
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "Открыть превью")
|
if templ_7745c5c3_Err != nil {
|
||||||
if templ_7745c5c3_Err != nil {
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/result.templ`, Line: 22, Col: 44}
|
||||||
return templ_7745c5c3_Err
|
}
|
||||||
}
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
return nil
|
if templ_7745c5c3_Err != nil {
|
||||||
})
|
return templ_7745c5c3_Err
|
||||||
templ_7745c5c3_Err = button.Button(button.Props{
|
}
|
||||||
Href: "/preview/" + previewID,
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" class=\"sr-only\" readonly>")
|
||||||
Target: "_blank",
|
if templ_7745c5c3_Err != nil {
|
||||||
Class: "rounded-2xl bg-primary px-4 py-2.5 text-sm font-semibold text-primary-foreground hover:bg-primary/90",
|
return templ_7745c5c3_Err
|
||||||
Variant: button.VariantDefault,
|
}
|
||||||
}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var4), templ_7745c5c3_Buffer)
|
var templ_7745c5c3_Var5 string
|
||||||
if templ_7745c5c3_Err != nil {
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fullHTML)
|
||||||
return templ_7745c5c3_Err
|
if templ_7745c5c3_Err != nil {
|
||||||
}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/result.templ`, Line: 22, Col: 82}
|
||||||
templ_7745c5c3_Var5 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
}
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
if templ_7745c5c3_Err != nil {
|
||||||
if !templ_7745c5c3_IsBuffer {
|
return templ_7745c5c3_Err
|
||||||
defer func() {
|
}
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</textarea> <a href=\"")
|
||||||
if templ_7745c5c3_Err == nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}()
|
var templ_7745c5c3_Var6 templ.SafeURL
|
||||||
}
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinURLErrs("/preview/" + previewID)
|
||||||
ctx = templ.InitializeContext(ctx)
|
if templ_7745c5c3_Err != nil {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "Скачать HTML")
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/result.templ`, Line: 23, Col: 36}
|
||||||
if templ_7745c5c3_Err != nil {
|
}
|
||||||
return templ_7745c5c3_Err
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
}
|
if templ_7745c5c3_Err != nil {
|
||||||
return nil
|
return templ_7745c5c3_Err
|
||||||
})
|
}
|
||||||
templ_7745c5c3_Err = button.Button(button.Props{
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" target=\"_blank\" rel=\"noreferrer\" class=\"sr-only\">Открыть превью</a> <iframe class=\"hidden\" sandbox=\"\" referrerpolicy=\"no-referrer\" srcdoc=\"")
|
||||||
Href: "/download/" + downloadID,
|
if templ_7745c5c3_Err != nil {
|
||||||
Class: "rounded-2xl border border-border bg-card px-4 py-2.5 text-sm font-semibold text-foreground hover:bg-muted/60",
|
return templ_7745c5c3_Err
|
||||||
Variant: button.VariantOutline,
|
}
|
||||||
}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var5), templ_7745c5c3_Buffer)
|
var templ_7745c5c3_Var7 string
|
||||||
if templ_7745c5c3_Err != nil {
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fullHTML)
|
||||||
return templ_7745c5c3_Err
|
if templ_7745c5c3_Err != nil {
|
||||||
}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/result.templ`, Line: 24, Col: 83}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<span class=\"text-sm text-muted-foreground\">Файл: <span class=\"font-medium text-foreground\">")
|
}
|
||||||
if templ_7745c5c3_Err != nil {
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
return templ_7745c5c3_Err
|
if templ_7745c5c3_Err != nil {
|
||||||
}
|
return templ_7745c5c3_Err
|
||||||
var templ_7745c5c3_Var6 string
|
}
|
||||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(filename)
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\"></iframe><div class=\"flex flex-wrap items-center gap-2 px-5 py-5\"><a href=\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/result.templ`, Line: 27, Col: 110}
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
var templ_7745c5c3_Var8 templ.SafeURL
|
||||||
if templ_7745c5c3_Err != nil {
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs("/download/" + downloadID)
|
||||||
return templ_7745c5c3_Err
|
if templ_7745c5c3_Err != nil {
|
||||||
}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/result.templ`, Line: 27, Col: 37}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span></span></div><p class=\"text-sm leading-6 text-muted-foreground\">Ссылки одноразовые: после первого успешного открытия соответствующий UUID удаляется из preview-store.</p><details class=\"group overflow-hidden rounded-[1.25rem] border border-border bg-card/80\"><summary class=\"cursor-pointer list-none px-4 py-3 text-sm font-semibold text-foreground\"><span class=\"inline-flex items-center gap-2\"><span class=\"inline-flex size-6 items-center justify-center rounded-full bg-muted text-xs text-muted-foreground\">i</span> Inline-превью в изолированном iframe</span></summary><div class=\"border-t border-border/70 px-4 pb-4 pt-3\"><iframe class=\"result-frame\" sandbox=\"\" referrerpolicy=\"no-referrer\" srcdoc=\"")
|
}
|
||||||
if templ_7745c5c3_Err != nil {
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
return templ_7745c5c3_Err
|
if templ_7745c5c3_Err != nil {
|
||||||
}
|
return templ_7745c5c3_Err
|
||||||
var templ_7745c5c3_Var7 string
|
}
|
||||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fullHTML)
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" class=\"focus-ring inline-flex h-9 items-center justify-center gap-2 rounded-md bg-primary px-3.5 text-sm font-medium text-primary-foreground transition hover:bg-primary/90\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/result.templ`, Line: 44, Col: 23}
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
templ_7745c5c3_Err = DownloadIcon("size-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\"></iframe></div></details>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<span>Скачать HTML</span></a> <button type=\"button\" class=\"focus-ring inline-flex h-9 items-center justify-center gap-2 rounded-md border border-border bg-background px-3.5 text-sm font-medium text-foreground transition hover:bg-muted\" data-copy-target=\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
return nil
|
var templ_7745c5c3_Var9 string
|
||||||
})
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs("result-html-" + previewID)
|
||||||
templ_7745c5c3_Err = card.Content(card.ContentProps{Class: "space-y-4"}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var3), templ_7745c5c3_Buffer)
|
if templ_7745c5c3_Err != nil {
|
||||||
if templ_7745c5c3_Err != nil {
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/result.templ`, Line: 36, Col: 50}
|
||||||
return templ_7745c5c3_Err
|
}
|
||||||
}
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
return nil
|
if templ_7745c5c3_Err != nil {
|
||||||
})
|
return templ_7745c5c3_Err
|
||||||
templ_7745c5c3_Err = card.Card(card.Props{Class: "section-card border-primary/20 bg-background/90"}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" onclick=\"window.mdToHTMLCopyButton(this)\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = CopyIcon("size-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<span>Скопировать</span></button> <a href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var10 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinURLErrs("/preview/" + previewID)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/result.templ`, Line: 43, Col: 35}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" target=\"_blank\" rel=\"noreferrer\" class=\"focus-ring inline-flex h-9 items-center justify-center gap-2 rounded-md border border-border bg-background px-3.5 text-sm font-medium text-foreground transition hover:bg-muted\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = ExternalLinkIcon("size-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<span>Открыть в новой вкладке</span></a></div></section></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -177,25 +204,25 @@ func Error(msg string) templ.Component {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
ctx = templ.InitializeContext(ctx)
|
ctx = templ.InitializeContext(ctx)
|
||||||
templ_7745c5c3_Var8 := templ.GetChildren(ctx)
|
templ_7745c5c3_Var11 := templ.GetChildren(ctx)
|
||||||
if templ_7745c5c3_Var8 == nil {
|
if templ_7745c5c3_Var11 == nil {
|
||||||
templ_7745c5c3_Var8 = templ.NopComponent
|
templ_7745c5c3_Var11 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"rounded-[1.25rem] border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div id=\"result\" class=\"mt-6\"><div class=\"rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800 shadow-xs\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var9 string
|
var templ_7745c5c3_Var12 string
|
||||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(msg)
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(msg)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/result.templ`, Line: 54, Col: 7}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/result.templ`, Line: 59, Col: 8}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -203,4 +230,13 @@ func Error(msg string) templ.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func formatResultMeta(sizeBytes int, lineCount int, elapsedMs int) string {
|
||||||
|
kilobytes := float64(sizeBytes) / 1024
|
||||||
|
seconds := float64(elapsedMs) / 1000
|
||||||
|
if seconds < 0.1 {
|
||||||
|
seconds = 0.1
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.1f KB · %d строки · сгенерирован %.1f сек назад", kilobytes, lineCount, seconds)
|
||||||
|
}
|
||||||
|
|
||||||
var _ = templruntime.GeneratedTemplate
|
var _ = templruntime.GeneratedTemplate
|
||||||
|
|||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
Executable
+100
@@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/release-build.sh
|
||||||
|
./scripts/release-build.sh --all
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--all Build all release targets used in CI:
|
||||||
|
linux/amd64, linux/arm64, darwin/arm64
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
mode="current"
|
||||||
|
if [[ $# -gt 1 ]]; then
|
||||||
|
usage
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [[ $# -eq 1 ]]; then
|
||||||
|
case "$1" in
|
||||||
|
--all)
|
||||||
|
mode="all"
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
repo_root="$(cd "${script_dir}/.." && pwd)"
|
||||||
|
|
||||||
|
cd "${repo_root}"
|
||||||
|
|
||||||
|
version="$(tr -d '[:space:]' < VERSION)"
|
||||||
|
if [[ -z "${version}" ]]; then
|
||||||
|
echo "VERSION is empty" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ldflags="-s -w -X github.com/fserg/md-to-html/internal/version.Version=${version}"
|
||||||
|
dist_dir="${repo_root}/dist"
|
||||||
|
mkdir -p "${dist_dir}" "${repo_root}/web/static/dist"
|
||||||
|
|
||||||
|
echo "==> Generating templ code"
|
||||||
|
go run github.com/a-h/templ/cmd/templ@v0.3.1001 generate ./...
|
||||||
|
|
||||||
|
echo "==> Building Tailwind bundle"
|
||||||
|
npx tailwindcss -c tailwind.config.js -i web/static/src/app.css -o web/static/dist/app.css --minify
|
||||||
|
|
||||||
|
echo "==> Running tests"
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
targets=()
|
||||||
|
if [[ "${mode}" == "all" ]]; then
|
||||||
|
targets+=("linux amd64")
|
||||||
|
targets+=("linux arm64")
|
||||||
|
targets+=("darwin arm64")
|
||||||
|
else
|
||||||
|
current_goos="$(go env GOOS)"
|
||||||
|
current_goarch="$(go env GOARCH)"
|
||||||
|
targets+=("${current_goos} ${current_goarch}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
artifacts=()
|
||||||
|
for target in "${targets[@]}"; do
|
||||||
|
read -r goos goarch <<<"${target}"
|
||||||
|
output="${dist_dir}/md-to-html-${goos}-${goarch}"
|
||||||
|
echo "==> Building ${goos}/${goarch}"
|
||||||
|
CGO_ENABLED=0 GOOS="${goos}" GOARCH="${goarch}" \
|
||||||
|
go build -trimpath -ldflags="${ldflags}" -o "${output}" ./cmd/md-to-html
|
||||||
|
artifacts+=("${output}")
|
||||||
|
done
|
||||||
|
|
||||||
|
checksum_file="${dist_dir}/SHA256SUMS"
|
||||||
|
(
|
||||||
|
cd "${dist_dir}"
|
||||||
|
shasum -a 256 "${artifacts[@]##${dist_dir}/}" > "${checksum_file}"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Artifacts:"
|
||||||
|
for artifact in "${artifacts[@]}"; do
|
||||||
|
echo " ${artifact}"
|
||||||
|
done
|
||||||
|
echo " ${checksum_file}"
|
||||||
|
|
||||||
|
if [[ "${mode}" == "current" ]]; then
|
||||||
|
echo
|
||||||
|
echo "Run to verify:"
|
||||||
|
echo " ${artifacts[0]} serve"
|
||||||
|
fi
|
||||||
+19
-19
@@ -7,29 +7,29 @@ module.exports = {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
background: "#f5efe2",
|
background: "#ffffff",
|
||||||
foreground: "#221f1a",
|
foreground: "#0a0a0a",
|
||||||
card: "#fffdf8",
|
card: "#ffffff",
|
||||||
"card-foreground": "#221f1a",
|
"card-foreground": "#0a0a0a",
|
||||||
primary: "#b85c38",
|
primary: "#171717",
|
||||||
"primary-foreground": "#fffaf4",
|
"primary-foreground": "#fafafa",
|
||||||
secondary: "#ead7b0",
|
secondary: "#f5f5f5",
|
||||||
"secondary-foreground": "#3f3528",
|
"secondary-foreground": "#171717",
|
||||||
muted: "#efe4d2",
|
muted: "#f5f5f5",
|
||||||
"muted-foreground": "#6c6254",
|
"muted-foreground": "#737373",
|
||||||
accent: "#d0b38a",
|
accent: "#f5f5f5",
|
||||||
"accent-foreground": "#2e2417",
|
"accent-foreground": "#171717",
|
||||||
border: "#d8c6ab",
|
border: "#e5e5e5",
|
||||||
ring: "#b85c38",
|
ring: "#0a0a0a",
|
||||||
input: "#fffaf4",
|
input: "#e5e5e5",
|
||||||
destructive: "#b42318",
|
destructive: "#dc2626",
|
||||||
},
|
},
|
||||||
boxShadow: {
|
boxShadow: {
|
||||||
xs: "0 1px 2px rgba(34, 31, 26, 0.08)",
|
xs: "0 1px 2px 0 rgb(0 0 0 / 0.04)",
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ["IBM Plex Sans", "Avenir Next", "Segoe UI", "sans-serif"],
|
sans: ["Geist", "ui-sans-serif", "system-ui", "sans-serif"],
|
||||||
mono: ["IBM Plex Mono", "SFMono-Regular", "monospace"],
|
mono: ["Geist Mono", "ui-monospace", "Menlo", "monospace"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+31
-59
@@ -5,83 +5,55 @@
|
|||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
|
--color-background: #ffffff;
|
||||||
|
--color-foreground: #0a0a0a;
|
||||||
|
--color-muted: #f5f5f5;
|
||||||
|
--color-muted-foreground: #737373;
|
||||||
|
--color-border: #e5e5e5;
|
||||||
|
--color-input: #e5e5e5;
|
||||||
|
--color-ring: #0a0a0a;
|
||||||
|
--color-primary: #171717;
|
||||||
|
--color-primary-foreground: #fafafa;
|
||||||
|
--color-secondary: #f5f5f5;
|
||||||
|
--color-secondary-foreground: #171717;
|
||||||
|
--color-accent: #f5f5f5;
|
||||||
|
--color-accent-foreground: #171717;
|
||||||
|
--color-card: #ffffff;
|
||||||
|
--color-card-foreground: #0a0a0a;
|
||||||
|
--color-destructive: #dc2626;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
background:
|
@apply bg-[#fafafa];
|
||||||
radial-gradient(circle at top left, rgba(234, 215, 176, 0.55), transparent 34rem),
|
|
||||||
radial-gradient(circle at top right, rgba(184, 92, 56, 0.14), transparent 24rem),
|
|
||||||
linear-gradient(180deg, #fbf7ef 0%, #f3eadb 100%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply min-h-screen bg-transparent font-sans text-foreground antialiased;
|
@apply min-h-screen bg-transparent text-foreground antialiased;
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
@apply transition-colors;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
background: rgba(184, 92, 56, 0.18);
|
background: rgba(23, 23, 23, 0.14);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.app-shell {
|
.dropzone {
|
||||||
@apply mx-auto max-w-6xl px-4 py-8 sm:px-6 lg:px-8;
|
background-image: repeating-linear-gradient(
|
||||||
|
45deg,
|
||||||
|
rgba(245, 245, 245, 0.9) 0 10px,
|
||||||
|
rgba(255, 255, 255, 0.9) 10px 20px
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-panel {
|
.tabs-trigger {
|
||||||
@apply relative overflow-hidden rounded-[2rem] border border-border/70 bg-card/95 shadow-xl shadow-stone-900/5 backdrop-blur;
|
@apply inline-flex h-9 items-center justify-center gap-2 rounded-md px-3 text-xs font-medium text-muted-foreground transition;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-panel::before {
|
.tabs-trigger[data-state="active"] {
|
||||||
content: "";
|
@apply bg-background text-foreground shadow-xs;
|
||||||
@apply absolute inset-x-0 top-0 h-40 bg-gradient-to-r from-secondary/80 via-card to-transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-grid {
|
.focus-ring {
|
||||||
@apply grid gap-6 lg:grid-cols-[minmax(0,1.1fr)_minmax(21rem,0.9fr)];
|
@apply outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background;
|
||||||
}
|
|
||||||
|
|
||||||
.eyebrow {
|
|
||||||
@apply inline-flex items-center gap-2 rounded-full border border-border/80 bg-background/90 px-3 py-1 text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-card {
|
|
||||||
@apply rounded-[1.5rem] border border-border/80 bg-card/90 shadow-lg shadow-stone-900/5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-label {
|
|
||||||
@apply text-sm font-semibold text-foreground;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-hint {
|
|
||||||
@apply text-sm text-muted-foreground;
|
|
||||||
}
|
|
||||||
|
|
||||||
.surface-input {
|
|
||||||
@apply block w-full rounded-2xl border border-border bg-background/95 px-4 py-3 text-sm text-foreground shadow-xs outline-none transition focus:border-primary focus:ring-2 focus:ring-primary/20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.surface-textarea {
|
|
||||||
@apply surface-input min-h-[18rem] resize-y font-mono leading-6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.source-tab {
|
|
||||||
@apply inline-flex flex-1 cursor-pointer items-center justify-center rounded-2xl px-4 py-3 text-sm font-semibold text-muted-foreground transition;
|
|
||||||
}
|
|
||||||
|
|
||||||
.source-tab-active {
|
|
||||||
@apply bg-primary text-primary-foreground shadow-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
.source-panel {
|
|
||||||
@apply rounded-[1.5rem] border border-dashed border-border/80 bg-background/70 p-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-frame {
|
|
||||||
@apply mt-3 h-[36rem] w-full rounded-[1.25rem] border border-border bg-white shadow-inner;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1274
-258
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user