9 Commits

Author SHA1 Message Date
Sergey Filkin 937f014ce0 Use GitHub markdown CSS for generated HTML
Docker / build (push) Successful in 46s
2026-04-30 20:01:59 +03:00
Sergey Filkin e6ff59c75b Fix code block styling and add deploy webhook
Docker / build (push) Successful in 39s
2026-04-30 19:52:19 +03:00
Sergey Filkin 79a4bcb890 Use Gitea-compatible artifact action
Docker / build (push) Successful in 1m30s
2026-04-30 19:18:14 +03:00
Sergey Filkin 62f1ea5d36 Add Gitea Docker build workflow
Docker / build (push) Has been cancelled
2026-04-30 16:21:56 +03:00
Sergey Filkin 5bb488ccd0 Release v0.2.2
release / release (push) Has been cancelled
build / test (push) Successful in 11m1s
build / cross-compile (amd64, linux) (push) Failing after 5m43s
build / cross-compile (arm64, darwin) (push) Failing after 5m23s
build / cross-compile (arm64, linux) (push) Failing after 5m23s
2026-04-18 14:42:16 +03:00
Sergey Filkin 256d5c9e6d chore(progress): complete phase7 2026-04-18 13:43:58 +03:00
Sergey Filkin a90519807c phase7: release v0.2.1
release / release (push) Has been cancelled
2026-04-18 13:32:31 +03:00
Sergey Filkin 4cd85e3515 chore(progress): resume phase7 2026-04-18 13:30:37 +03:00
Sergey Filkin 9531730283 chore(progress): block phase7 2026-04-18 12:59:29 +03:00
38 changed files with 23189 additions and 4979 deletions
+87
View File
@@ -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 }}"
+1 -1
View File
@@ -101,7 +101,7 @@ jobs:
-o "bin/md-to-html-${GOOS}-${GOARCH}" \
./cmd/md-to-html
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v3
with:
name: md-to-html-${{ matrix.goos }}-${{ matrix.goarch }}
path: bin/md-to-html-${{ matrix.goos }}-${{ matrix.goarch }}
+8 -2
View File
@@ -55,6 +55,12 @@ jobs:
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
@@ -66,8 +72,8 @@ jobs:
push: true
platforms: linux/amd64,linux/arm64
tags: |
ghcr.io/${{ github.repository }}:${{ github.ref_name }}
ghcr.io/${{ github.repository }}:latest
${{ steps.image.outputs.name }}:${{ github.ref_name }}
${{ steps.image.outputs.name }}:latest
- name: Create GitHub Release
env:
+14
View File
@@ -4,6 +4,20 @@ 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.
## [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
+7 -1
View File
@@ -3,7 +3,7 @@ LDFLAGS := -X github.com/fserg/md-to-html/internal/version.Version=$(VERSION)
GOBIN := $(shell go env GOPATH)/bin
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:
go build -ldflags "$(LDFLAGS)" -o bin/md-to-html ./cmd/md-to-html
@@ -31,6 +31,12 @@ dev:
docker:
@echo "docker target will be implemented in phase 6"
release:
./scripts/release-build.sh
release-all:
./scripts/release-build.sh --all
clean:
rm -rf bin/ tmp/ web/static/dist/
+47 -8
View File
@@ -1,16 +1,18 @@
# md-to-html
Сервис конвертации Markdown в самодостаточный HTML. Полностью офлайн, без обращений к внешним API.
Сервис конвертации Markdown в самодостаточный HTML. Конвертация выполняется локально, без внешних API.
Текущая версия: `0.2.0` (Go + goldmark + templUI)
![Превью интерфейса](screen.png)
Текущая версия: `0.2.2` (Go + goldmark + templUI)
## Возможности
- GFM + footnote + emoji + подсветка кода через chroma.
- Якоря в заголовках с ASCII-транслитом: `## Установка``#ustanovka`.
- Web UI на `http://localhost:8080/` с загрузкой файла или вставкой текста, HTMX-обновлением результата и одноразовыми ссылками на preview/download.
- CLI: `md-to-html cli file.md`.
- HTTP API: `POST /convert` совместим с `v0.1.x`.
- Web UI на `http://localhost:8080/` с inline-preview в sandbox iframe и одноразовыми ссылками на preview/download.
- HTTP API: `POST /convert`, совместим с `v0.1.x`.
- Якоря в заголовках с ASCII-транслитом: `## Установка``#ustanovka`.
## Запуск через Docker
@@ -18,12 +20,22 @@
docker run --rm -p 8080:8080 ghcr.io/fserg/md-to-html:latest
```
## Локальная разработка
Требования: Go 1.23+, `templ` CLI, Node.js для dev-режима Tailwind или standalone `tailwindcss`.
## Быстрый старт
```bash
go install github.com/a-h/templ/cmd/templ@v0.3.1001
npm install
make build
./bin/md-to-html serve
```
## Локальная разработка
Требования: Go 1.24+, Node.js, `templ` CLI.
```bash
go install github.com/a-h/templ/cmd/templ@v0.3.1001
npm install
make tailwind
make build
./bin/md-to-html serve
@@ -35,6 +47,33 @@ make build
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
+1 -1
View File
@@ -1 +1 @@
0.2.0
0.2.2
+1 -1
View File
@@ -16,7 +16,7 @@
| 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 | |
| 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) | 🔄 in_progress | 2026-04-18 | — | — | |
| 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` — не начата
File diff suppressed because it is too large Load Diff
+1272 -256
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1272 -256
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1272 -256
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1272 -256
View File
File diff suppressed because it is too large Load Diff
+1272 -256
View File
File diff suppressed because it is too large Load Diff
+8 -1
View File
@@ -11,6 +11,7 @@ import (
"net/http"
"path/filepath"
"strings"
"time"
"github.com/fserg/md-to-html/internal/converter"
"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) {
startedAt := time.Now()
r.Body = http.MaxBytesReader(w, r.Body, s.cfg.MaxRequestBytes)
if err := r.ParseMultipartForm(s.cfg.MaxRequestBytes); err != nil {
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)
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.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) {
+326 -116
View File
@@ -1,158 +1,368 @@
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() {
@Layout("Markdown → HTML") {
<div class="panel-grid">
<section class="space-y-6">
<div class="space-y-4">
<div class="eyebrow">
<span>Go migration</span>
<span>goldmark + templUI</span>
</div>
<div class="space-y-3">
<h1 class="max-w-3xl text-4xl font-semibold leading-tight tracking-tight text-foreground sm:text-5xl">
Markdown → HTML без внешних зависимостей в результирующем документе.
</h1>
<p class="max-w-2xl text-base leading-7 text-muted-foreground sm:text-lg">
Загрузите `.md`-файл или вставьте текст вручную. Сервис отдаст автономный HTML, одноразовое превью и отдельную ссылку на скачивание.
@Layout("Markdown → standalone HTML") {
<div class="mx-auto max-w-3xl px-6 py-10">
<header class="mb-8">
<h1 class="text-2xl font-semibold tracking-tight text-foreground sm:text-[2rem]">Markdown → standalone HTML</h1>
<p class="mt-1.5 max-w-prose text-sm leading-6 text-muted-foreground">
Загрузите .md файл или вставьте Markdown-текст. Результат — готовый самодостаточный HTML со встроенными стилями.
</p>
</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"}) {
</header>
<form
id="convert-form"
class="space-y-6"
hx-post="/ui/convert"
hx-target="#result"
hx-swap="innerHTML"
hx-swap="outerHTML"
hx-encoding="multipart/form-data"
class="space-y-5"
onreset="window.setTimeout(window.mdToHTMLResetForm, 0)"
>
<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"
<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="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"
class="tabs-trigger"
data-state="active"
onclick="window.mdToHTMLSetSource('file')"
>
<input
type="radio"
name="source"
@FileIcon("size-3.5")
<span>Загрузить файл</span>
</button>
<button
id="tab-text"
type="button"
value="text"
class="sr-only"
onchange="window.mdToHTMLSwitchSource(this.value)"
/>
Текст
</label>
class="tabs-trigger"
onclick="window.mdToHTMLSetSource('text')"
>
@AlignLeftIcon("size-3.5")
<span>Вставить текст</span>
</button>
</div>
</div>
<div id="source-file" class="source-panel space-y-3">
<label class="field-label" for="markdown-file">Markdown-файл</label>
<div class="p-6">
<div id="panel-file" class="flex flex-col gap-4">
<label
id="markdown-dropzone"
for="markdown-file"
class="dropzone group block cursor-pointer rounded-lg border-2 border-dashed border-border p-10 text-center transition hover:border-foreground/25"
>
<input
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"
name="markdown_file"
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 id="source-text" class="source-panel hidden space-y-3">
<label class="field-label" for="markdown-text">Markdown-текст</label>
<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="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
id="markdown-text"
class="surface-textarea"
name="markdown_text"
rows="14"
placeholder="# Привет, мир&#10;&#10;- списки&#10;- таблицы&#10;- код"
rows="10"
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="# Мой заголовок&#10;&#10;Здесь будет текст..."
oninput="window.mdToHTMLUpdateCharCount(this)"
></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 class="flex flex-wrap items-center gap-3">
@button.Button(button.Props{
Type: button.TypeSubmit,
Class: "rounded-2xl bg-primary px-5 py-3 text-sm font-semibold text-primary-foreground hover:bg-primary/90",
Variant: button.VariantDefault,
Size: button.SizeDefault,
}) {
<p class="flex items-center gap-2 text-[11px] text-muted-foreground">
@InfoIcon("size-3.5")
<span>Поддерживается CommonMark + GFM</span>
</p>
</div>
</div>
<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">
<button
type="reset"
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"
>
Сбросить
</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>
}
<span class="field-hint">Лимиты тела запроса и markdown берутся из server config.</span>
@ArrowRightIcon("size-4")
</button>
</div>
</div>
</section>
</form>
<div id="result" class="min-h-[4rem]"></div>
}
}
<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 '&#123;"markdown":"# Hello"&#125;'</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 '&#123;"markdown":"# Hello"&#125;'</pre>
</section>
</div>
<script>
window.mdToHTMLSwitchSource = function(value) {
const filePanel = document.getElementById("source-file");
const textPanel = document.getElementById("source-text");
if (!filePanel || !textPanel) {
(() => {
function byId(id) {
return document.getElementById(id);
}
function formatBytes(bytes) {
if (bytes < 1024) {
return bytes + " B";
}
return (bytes / 1024).toFixed(1) + " KB";
}
function formatRelativeTime(lastModified) {
const delta = Date.now() - lastModified;
const minute = 60 * 1000;
const hour = 60 * minute;
const day = 24 * hour;
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 showFile = value === "file";
filePanel.classList.toggle("hidden", !showFile);
textPanel.classList.toggle("hidden", showFile);
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);
}
document.querySelectorAll("[data-source-tab]").forEach((tab) => {
const tabValue = tab.getAttribute("data-source-tab");
const active = tabValue === value;
tab.className = active
? tab.getAttribute("data-active-classes")
: tab.getAttribute("data-inactive-classes");
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>
}
}
+58 -148
View File
File diff suppressed because one or more lines are too long
+75
View File
@@ -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>
}
+481
View File
@@ -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
+11 -16
View File
@@ -1,28 +1,23 @@
package ui
import "github.com/fserg/md-to-html/internal/version"
templ Layout(title string) {
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{ title }</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>{ title } · md-to-html</title>
<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>
<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">
<body class="min-h-screen bg-[#fafafa] font-sans text-foreground antialiased">
{ children... }
</div>
</div>
<footer class="mt-6 text-center text-sm text-muted-foreground">
Markdown → HTML · v{ version.Version }
</footer>
</div>
</body>
</html>
}
+4 -19
View File
@@ -8,8 +8,6 @@ package ui
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "github.com/fserg/md-to-html/internal/version"
func Layout(title 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
@@ -31,20 +29,20 @@ func Layout(title string) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
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 {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
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))
if templ_7745c5c3_Err != nil {
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 {
return templ_7745c5c3_Err
}
@@ -52,20 +50,7 @@ func Layout(title string) templ.Component {
if templ_7745c5c3_Err != nil {
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")
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>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
+59 -44
View File
@@ -1,56 +1,71 @@
package ui
import (
"github.com/fserg/md-to-html/internal/ui/components/button"
"github.com/fserg/md-to-html/internal/ui/components/card"
)
import "fmt"
templ Result(previewID, downloadID string, fullHTML string, filename string) {
@card.Card(card.Props{Class: "section-card border-primary/20 bg-background/90"}) {
@card.Content(card.ContentProps{Class: "space-y-4"}) {
<div class="flex flex-wrap items-center gap-3">
@button.Button(button.Props{
Href: "/preview/" + previewID,
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>
templ Result(previewID, downloadID, fullHTML, filename string, sizeBytes int, lineCount int, elapsedMs int) {
<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">
@CheckIcon("size-4")
</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
<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>
</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>
</details>
}
}
<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) {
<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">
<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>
}
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)
}
+128 -92
View File
@@ -8,12 +8,9 @@ package ui
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"github.com/fserg/md-to-html/internal/ui/components/button"
"github.com/fserg/md-to-html/internal/ui/components/card"
)
import "fmt"
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) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
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
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := 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_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_Var3 := 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_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_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"flex flex-wrap items-center gap-3\">")
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\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var4 := 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_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_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "Открыть превью")
templ_7745c5c3_Err = CheckIcon("size-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = button.Button(button.Props{
Href: "/preview/" + previewID,
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,
}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var4), templ_7745c5c3_Buffer)
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
}
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_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
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(filename)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/result.templ`, Line: 13, Col: 90}
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "Скачать HTML")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = 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,
}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var5), templ_7745c5c3_Buffer)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div><div class=\"text-xs text-muted-foreground font-mono\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<span class=\"text-sm text-muted-foreground\">Файл: <span class=\"font-medium text-foreground\">")
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(formatResultMeta(sizeBytes, lineCount, elapsedMs))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/result.templ`, Line: 15, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
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, 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=\"")
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
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs("result-html-" + previewID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/result.templ`, Line: 22, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" class=\"sr-only\" readonly>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fullHTML)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/result.templ`, Line: 22, Col: 82}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</textarea> <a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 templ.SafeURL
templ_7745c5c3_Var6, 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: 23, Col: 36}
}
_, 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, 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=\"")
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=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fullHTML)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/result.templ`, Line: 44, Col: 23}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/result.templ`, Line: 24, Col: 83}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\"></iframe></div></details>")
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 {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = card.Content(card.ContentProps{Class: "space-y-4"}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var3), templ_7745c5c3_Buffer)
var templ_7745c5c3_Var8 templ.SafeURL
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs("/download/" + downloadID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/result.templ`, Line: 27, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
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, 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 {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = DownloadIcon("size-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
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 {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs("result-html-" + previewID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/result.templ`, Line: 36, Col: 50}
}
_, 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, 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 {
return templ_7745c5c3_Err
}
@@ -177,25 +204,25 @@ func Error(msg string) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var8 := templ.GetChildren(ctx)
if templ_7745c5c3_Var8 == nil {
templ_7745c5c3_Var8 = templ.NopComponent
templ_7745c5c3_Var11 := templ.GetChildren(ctx)
if templ_7745c5c3_Var11 == nil {
templ_7745c5c3_Var11 = templ.NopComponent
}
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 {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(msg)
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(msg)
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 {
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 {
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
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

+100
View File
@@ -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
View File
@@ -7,29 +7,29 @@ module.exports = {
theme: {
extend: {
colors: {
background: "#f5efe2",
foreground: "#221f1a",
card: "#fffdf8",
"card-foreground": "#221f1a",
primary: "#b85c38",
"primary-foreground": "#fffaf4",
secondary: "#ead7b0",
"secondary-foreground": "#3f3528",
muted: "#efe4d2",
"muted-foreground": "#6c6254",
accent: "#d0b38a",
"accent-foreground": "#2e2417",
border: "#d8c6ab",
ring: "#b85c38",
input: "#fffaf4",
destructive: "#b42318",
background: "#ffffff",
foreground: "#0a0a0a",
card: "#ffffff",
"card-foreground": "#0a0a0a",
primary: "#171717",
"primary-foreground": "#fafafa",
secondary: "#f5f5f5",
"secondary-foreground": "#171717",
muted: "#f5f5f5",
"muted-foreground": "#737373",
accent: "#f5f5f5",
"accent-foreground": "#171717",
border: "#e5e5e5",
ring: "#0a0a0a",
input: "#e5e5e5",
destructive: "#dc2626",
},
boxShadow: {
xs: "0 1px 2px rgba(34, 31, 26, 0.08)",
xs: "0 1px 2px 0 rgb(0 0 0 / 0.04)",
},
fontFamily: {
sans: ["IBM Plex Sans", "Avenir Next", "Segoe UI", "sans-serif"],
mono: ["IBM Plex Mono", "SFMono-Regular", "monospace"],
sans: ["Geist", "ui-sans-serif", "system-ui", "sans-serif"],
mono: ["Geist Mono", "ui-monospace", "Menlo", "monospace"],
},
},
},
+31 -59
View File
@@ -5,83 +5,55 @@
@layer base {
:root {
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 {
background:
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%);
@apply bg-[#fafafa];
}
body {
@apply min-h-screen bg-transparent font-sans text-foreground antialiased;
}
a {
@apply transition-colors;
@apply min-h-screen bg-transparent text-foreground antialiased;
}
::selection {
background: rgba(184, 92, 56, 0.18);
background: rgba(23, 23, 23, 0.14);
}
}
@layer components {
.app-shell {
@apply mx-auto max-w-6xl px-4 py-8 sm:px-6 lg:px-8;
.dropzone {
background-image: repeating-linear-gradient(
45deg,
rgba(245, 245, 245, 0.9) 0 10px,
rgba(255, 255, 255, 0.9) 10px 20px
);
}
.hero-panel {
@apply relative overflow-hidden rounded-[2rem] border border-border/70 bg-card/95 shadow-xl shadow-stone-900/5 backdrop-blur;
.tabs-trigger {
@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 {
content: "";
@apply absolute inset-x-0 top-0 h-40 bg-gradient-to-r from-secondary/80 via-card to-transparent;
.tabs-trigger[data-state="active"] {
@apply bg-background text-foreground shadow-xs;
}
.panel-grid {
@apply grid gap-6 lg:grid-cols-[minmax(0,1.1fr)_minmax(21rem,0.9fr)];
}
.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;
.focus-ring {
@apply outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background;
}
}
+1272 -256
View File
File diff suppressed because it is too large Load Diff