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

This commit is contained in:
Sergey Filkin
2026-04-18 14:42:16 +03:00
parent 256d5c9e6d
commit 5bb488ccd0
17 changed files with 1431 additions and 589 deletions
+8
View File
@@ -4,6 +4,14 @@ 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 ## [0.2.1] - 2026-04-18
### Fixed ### Fixed
+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 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/
+47 -8
View File
@@ -1,16 +1,18 @@
# md-to-html # md-to-html
Сервис конвертации Markdown в самодостаточный HTML. Полностью офлайн, без обращений к внешним API. Сервис конвертации Markdown в самодостаточный HTML. Конвертация выполняется локально, без внешних API.
Текущая версия: `0.2.1` (Go + goldmark + templUI) ![Превью интерфейса](screen.png)
Текущая версия: `0.2.2` (Go + goldmark + templUI)
## Возможности ## Возможности
- GFM + footnote + emoji + подсветка кода через chroma. - GFM + footnote + emoji + подсветка кода через chroma.
- Якоря в заголовках с ASCII-транслитом: `## Установка``#ustanovka`. - Web UI на `http://localhost:8080/` с загрузкой файла или вставкой текста, HTMX-обновлением результата и одноразовыми ссылками на preview/download.
- CLI: `md-to-html cli file.md`. - CLI: `md-to-html cli file.md`.
- HTTP API: `POST /convert` совместим с `v0.1.x`. - HTTP API: `POST /convert`, совместим с `v0.1.x`.
- Web UI на `http://localhost:8080/` с inline-preview в sandbox iframe и одноразовыми ссылками на preview/download. - Якоря в заголовках с ASCII-транслитом: `## Установка``#ustanovka`.
## Запуск через Docker ## Запуск через Docker
@@ -18,12 +20,22 @@
docker run --rm -p 8080:8080 ghcr.io/fserg/md-to-html:latest 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 ```bash
go install github.com/a-h/templ/cmd/templ@v0.3.1001 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 tailwind
make build make build
./bin/md-to-html serve ./bin/md-to-html serve
@@ -35,6 +47,33 @@ make build
make dev 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 ## CLI
```bash ```bash
+1 -1
View File
@@ -1 +1 @@
0.2.1 0.2.2
+8 -1
View File
@@ -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) {
+329 -116
View File
@@ -1,158 +1,371 @@
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>
</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, одноразовое превью и отдельную ссылку на скачивание.
</p> </p>
</div> </header>
</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 <form
id="convert-form" id="convert-form"
class="space-y-6"
hx-post="/ui/convert" hx-post="/ui/convert"
hx-target="#result" hx-target="#result"
hx-swap="innerHTML" hx-swap="outerHTML"
hx-encoding="multipart/form-data" hx-encoding="multipart/form-data"
class="space-y-5" onreset="window.setTimeout(window.mdToHTMLResetForm, 0)"
> >
<div class="space-y-2"> <input id="source-field" type="hidden" name="source" value="file"/>
<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"> <section class="overflow-hidden rounded-xl border border-border bg-background shadow-xs">
<label <div class="border-b border-border px-4 py-4">
class="source-tab source-tab-active" <div class="inline-flex items-center gap-1 rounded-lg bg-muted p-1" role="tablist" aria-label="Источник markdown">
data-source-tab="file" <button
data-active-classes="source-tab source-tab-active" id="tab-file"
data-inactive-classes="source-tab" type="button"
>
<input
type="radio"
name="source"
value="file" value="file"
class="sr-only" class="tabs-trigger"
checked data-state="active"
onchange="window.mdToHTMLSwitchSource(this.value)" onclick="window.mdToHTMLSetSource('file')"
/>
Файл
</label>
<label
class="source-tab"
data-source-tab="text"
data-active-classes="source-tab source-tab-active"
data-inactive-classes="source-tab"
> >
<input @FileIcon("size-3.5")
type="radio" <span>Загрузить файл</span>
name="source" </button>
<button
id="tab-text"
type="button"
value="text" value="text"
class="sr-only" class="tabs-trigger"
onchange="window.mdToHTMLSwitchSource(this.value)" onclick="window.mdToHTMLSetSource('text')"
/> >
Текст @AlignLeftIcon("size-3.5")
</label> <span>Вставить текст</span>
</button>
</div> </div>
</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 <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>
<div id="source-text" class="source-panel hidden space-y-3"> <div class="text-sm font-medium text-foreground">Перетащите .md файл сюда</div>
<label class="field-label" for="markdown-text">Markdown-текст</label> <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 <textarea
id="markdown-text" id="markdown-text"
class="surface-textarea"
name="markdown_text" name="markdown_text"
rows="14" rows="10"
placeholder="# Привет, мир&#10;&#10;- списки&#10;- таблицы&#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> ></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>
}) {
<div class="flex flex-wrap items-center justify-between gap-3 border-t border-border px-4 py-4">
<p class="text-xs text-muted-foreground">
Конвертация использует публичный <code class="font-mono text-foreground">GitHub API</code>
</p>
<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>Конвертировать</span>
} @ArrowRightIcon("size-4")
<span class="field-hint">Лимиты тела запроса и markdown берутся из server config.</span> </button>
</div> </div>
</div>
</section>
</form> </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> </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) { }
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; return;
} }
const showFile = value === "file"; const helper = document.createElement("textarea");
filePanel.classList.toggle("hidden", !showFile); helper.value = value;
textPanel.classList.toggle("hidden", showFile); 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) => { function bindDropzone() {
const tabValue = tab.getAttribute("data-source-tab"); const dropzone = byId("markdown-dropzone");
const active = tabValue === value; const input = byId("markdown-file");
tab.className = active if (!dropzone || !input || dropzone.dataset.bound === "true") {
? tab.getAttribute("data-active-classes") return;
: tab.getAttribute("data-inactive-classes"); }
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>
} }
} }
+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 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">
<div class="hero-panel">
<div class="relative px-5 py-6 sm:px-8 sm:py-8 lg:px-10 lg:py-10">
{ children... } { 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>
} }
+4 -19
View File
@@ -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
} }
+59 -44
View File
@@ -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> </div>
<p class="text-sm leading-6 text-muted-foreground"> <div class="min-w-0 flex-1">
Ссылки одноразовые: после первого успешного открытия соответствующий UUID удаляется из preview-store. <div class="truncate text-sm font-medium text-foreground">Готово — { filename }</div>
</p> <div class="text-xs text-muted-foreground font-mono">
<details class="group overflow-hidden rounded-[1.25rem] border border-border bg-card/80"> { formatResultMeta(sizeBytes, lineCount, elapsedMs) }
<summary class="cursor-pointer list-none px-4 py-3 text-sm font-semibold text-foreground"> </div>
<span class="inline-flex items-center gap-2"> </div>
<span class="inline-flex size-6 items-center justify-center rounded-full bg-muted text-xs text-muted-foreground">i</span> <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">
Inline-превью в изолированном iframe standalone
</span> </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> <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">
<div class="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800 shadow-xs">
{ msg } { 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)
} }
+128 -92
View File
@@ -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
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\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Var4 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_Err = CheckIcon("size-4").Render(ctx, templ_7745c5c3_Buffer)
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, "Открыть превью")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
return nil 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\">Готово — ")
})
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)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Var5 := 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)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
} }
}() _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "Скачать HTML")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
return nil templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div><div class=\"text-xs text-muted-foreground font-mono\">")
})
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)
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, 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var6 string 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_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(filename)
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
}
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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
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, 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var7 string var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fullHTML) templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fullHTML)
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.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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
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, 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_7745c5c3_Err return templ_7745c5c3_Err
} }
return nil var templ_7745c5c3_Var8 templ.SafeURL
}) templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs("/download/" + downloadID)
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 {
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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
return nil 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 {
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) 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 { 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
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: { 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
View File
@@ -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;
} }
} }