Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5bb488ccd0 | |||
| 256d5c9e6d |
@@ -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.
|
||||
|
||||
## [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
|
||||
|
||||
@@ -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/
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
# md-to-html
|
||||
|
||||
Сервис конвертации Markdown в самодостаточный HTML. Полностью офлайн, без обращений к внешним API.
|
||||
Сервис конвертации Markdown в самодостаточный HTML. Конвертация выполняется локально, без внешних API.
|
||||
|
||||
Текущая версия: `0.2.1` (Go + goldmark + templUI)
|
||||

|
||||
|
||||
Текущая версия: `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
@@ -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 | — | 66ca056 | `v0.2.0` tag remains failed in remote history; phase continues via patch release `v0.2.1` after fixing lowercase GHCR tags in `release.yml`. |
|
||||
| 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` — не начата
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+346
-133
@@ -1,158 +1,371 @@
|
||||
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>
|
||||
@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>
|
||||
</header>
|
||||
|
||||
<form
|
||||
id="convert-form"
|
||||
class="space-y-6"
|
||||
hx-post="/ui/convert"
|
||||
hx-target="#result"
|
||||
hx-swap="outerHTML"
|
||||
hx-encoding="multipart/form-data"
|
||||
onreset="window.setTimeout(window.mdToHTMLResetForm, 0)"
|
||||
>
|
||||
<input id="source-field" type="hidden" name="source" value="file"/>
|
||||
|
||||
<section class="overflow-hidden rounded-xl border border-border bg-background shadow-xs">
|
||||
<div class="border-b border-border px-4 py-4">
|
||||
<div class="inline-flex items-center gap-1 rounded-lg bg-muted p-1" role="tablist" aria-label="Источник markdown">
|
||||
<button
|
||||
id="tab-file"
|
||||
type="button"
|
||||
value="file"
|
||||
class="tabs-trigger"
|
||||
data-state="active"
|
||||
onclick="window.mdToHTMLSetSource('file')"
|
||||
>
|
||||
@FileIcon("size-3.5")
|
||||
<span>Загрузить файл</span>
|
||||
</button>
|
||||
<button
|
||||
id="tab-text"
|
||||
type="button"
|
||||
value="text"
|
||||
class="tabs-trigger"
|
||||
onclick="window.mdToHTMLSetSource('text')"
|
||||
>
|
||||
@AlignLeftIcon("size-3.5")
|
||||
<span>Вставить текст</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div 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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-4 sm:grid-cols-3">
|
||||
<div class="section-card p-4">
|
||||
<div class="text-sm font-semibold text-foreground">Самодостаточный HTML</div>
|
||||
<p class="mt-2 text-sm leading-6 text-muted-foreground">Результат открывается локально без CDN и без сетевых вызовов.</p>
|
||||
</div>
|
||||
<div class="section-card p-4">
|
||||
<div class="text-sm font-semibold text-foreground">Одноразовые ссылки</div>
|
||||
<p class="mt-2 text-sm leading-6 text-muted-foreground">Preview и download живут до первого открытия или максимум один час.</p>
|
||||
</div>
|
||||
<div class="section-card p-4">
|
||||
<div class="text-sm font-semibold text-foreground">Русский интерфейс</div>
|
||||
<p class="mt-2 text-sm leading-6 text-muted-foreground">Форма ориентирована на быстрый ручной прогон документации и заметок.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
@card.Card(card.Props{Class: "section-card overflow-hidden"}) {
|
||||
@card.Header(card.HeaderProps{Class: "space-y-2 border-b border-border/70 pb-6"}) {
|
||||
<div class="text-sm font-semibold uppercase tracking-[0.18em] text-muted-foreground">Конвертация</div>
|
||||
@card.Title(card.TitleProps{Class: "text-2xl font-semibold tracking-tight text-foreground"}) {
|
||||
Выберите источник Markdown
|
||||
}
|
||||
@card.Description(card.DescriptionProps{Class: "max-w-xl text-sm leading-6 text-muted-foreground"}) {
|
||||
Форма отправляется через HTMX на `POST /ui/convert`, а результат подменяется прямо в блоке ниже.
|
||||
}
|
||||
}
|
||||
@card.Content(card.ContentProps{Class: "space-y-5"}) {
|
||||
<form
|
||||
id="convert-form"
|
||||
hx-post="/ui/convert"
|
||||
hx-target="#result"
|
||||
hx-swap="innerHTML"
|
||||
hx-encoding="multipart/form-data"
|
||||
class="space-y-5"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<div class="field-label">Источник</div>
|
||||
<div class="grid grid-cols-2 gap-2 rounded-[1.35rem] border border-border/80 bg-muted/55 p-2">
|
||||
<label
|
||||
class="source-tab source-tab-active"
|
||||
data-source-tab="file"
|
||||
data-active-classes="source-tab source-tab-active"
|
||||
data-inactive-classes="source-tab"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="source"
|
||||
value="file"
|
||||
class="sr-only"
|
||||
checked
|
||||
onchange="window.mdToHTMLSwitchSource(this.value)"
|
||||
/>
|
||||
Файл
|
||||
</label>
|
||||
<label
|
||||
class="source-tab"
|
||||
data-source-tab="text"
|
||||
data-active-classes="source-tab source-tab-active"
|
||||
data-inactive-classes="source-tab"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="source"
|
||||
value="text"
|
||||
class="sr-only"
|
||||
onchange="window.mdToHTMLSwitchSource(this.value)"
|
||||
/>
|
||||
Текст
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="source-file" class="source-panel space-y-3">
|
||||
<label class="field-label" for="markdown-file">Markdown-файл</label>
|
||||
|
||||
<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 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 id="source-text" class="source-panel hidden space-y-3">
|
||||
<label class="field-label" for="markdown-text">Markdown-текст</label>
|
||||
</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="# Привет, мир - списки - таблицы - код"
|
||||
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="# Мой заголовок Здесь будет текст..."
|
||||
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,
|
||||
}) {
|
||||
<span>Конвертировать</span>
|
||||
}
|
||||
<span class="field-hint">Лимиты тела запроса и markdown берутся из server config.</span>
|
||||
</div>
|
||||
</form>
|
||||
<div id="result" class="min-h-[4rem]"></div>
|
||||
}
|
||||
}
|
||||
<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-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>
|
||||
@ArrowRightIcon("size-4")
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<div id="result" class="mt-6"></div>
|
||||
|
||||
<section class="mt-6 overflow-hidden rounded-xl border border-border bg-background shadow-xs">
|
||||
<div class="flex items-center justify-between border-b border-border px-5 py-3.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium text-foreground">API</span>
|
||||
<span class="inline-flex items-center rounded-md border border-border bg-muted px-1.5 py-px text-[10px] font-medium text-foreground font-mono">
|
||||
POST /convert
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="focus-ring inline-flex size-7 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted hover:text-foreground"
|
||||
aria-label="Скопировать curl"
|
||||
data-copy-target="api-curl"
|
||||
data-copy-label="curl"
|
||||
onclick="window.mdToHTMLCopyButton(this)"
|
||||
>
|
||||
@CopyIcon("size-3.5")
|
||||
</button>
|
||||
</div>
|
||||
<textarea id="api-curl" class="sr-only" readonly>curl -X POST http://localhost:8000/convert \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"markdown":"# Hello"}'</textarea>
|
||||
<pre class="overflow-x-auto px-5 py-4 font-mono text-[12px] leading-relaxed text-foreground"><span class="text-muted-foreground">$</span> curl -X POST http://localhost:8000/convert \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"markdown":"# Hello"}'</pre>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.mdToHTMLSwitchSource = function(value) {
|
||||
const filePanel = document.getElementById("source-file");
|
||||
const textPanel = document.getElementById("source-text");
|
||||
if (!filePanel || !textPanel) {
|
||||
return;
|
||||
(() => {
|
||||
function byId(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
const showFile = value === "file";
|
||||
filePanel.classList.toggle("hidden", !showFile);
|
||||
textPanel.classList.toggle("hidden", showFile);
|
||||
function formatBytes(bytes) {
|
||||
if (bytes < 1024) {
|
||||
return bytes + " B";
|
||||
}
|
||||
return (bytes / 1024).toFixed(1) + " KB";
|
||||
}
|
||||
|
||||
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 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 helper = document.createElement("textarea");
|
||||
helper.value = value;
|
||||
helper.setAttribute("readonly", "readonly");
|
||||
helper.style.position = "absolute";
|
||||
helper.style.left = "-9999px";
|
||||
document.body.appendChild(helper);
|
||||
helper.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(helper);
|
||||
}
|
||||
|
||||
function bindDropzone() {
|
||||
const dropzone = byId("markdown-dropzone");
|
||||
const input = byId("markdown-file");
|
||||
if (!dropzone || !input || dropzone.dataset.bound === "true") {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeClasses = ["border-foreground/35", "bg-muted/60"];
|
||||
dropzone.dataset.bound = "true";
|
||||
|
||||
dropzone.addEventListener("dragover", (event) => {
|
||||
event.preventDefault();
|
||||
activeClasses.forEach((className) => dropzone.classList.add(className));
|
||||
});
|
||||
dropzone.addEventListener("dragleave", () => {
|
||||
activeClasses.forEach((className) => dropzone.classList.remove(className));
|
||||
});
|
||||
dropzone.addEventListener("drop", (event) => {
|
||||
event.preventDefault();
|
||||
activeClasses.forEach((className) => dropzone.classList.remove(className));
|
||||
if (!event.dataTransfer || !event.dataTransfer.files.length) {
|
||||
return;
|
||||
}
|
||||
input.files = event.dataTransfer.files;
|
||||
window.mdToHTMLHandleFileChange(input);
|
||||
});
|
||||
}
|
||||
|
||||
window.mdToHTMLSetSource = function(source) {
|
||||
const sourceField = byId("source-field");
|
||||
const filePanel = byId("panel-file");
|
||||
const textPanel = byId("panel-text");
|
||||
const fileTab = byId("tab-file");
|
||||
const textTab = byId("tab-text");
|
||||
if (!sourceField || !filePanel || !textPanel || !fileTab || !textTab) {
|
||||
return;
|
||||
}
|
||||
|
||||
const showFile = source === "file";
|
||||
sourceField.value = source;
|
||||
filePanel.classList.toggle("hidden", !showFile);
|
||||
filePanel.classList.toggle("flex", showFile);
|
||||
textPanel.classList.toggle("hidden", showFile);
|
||||
textPanel.classList.toggle("flex", !showFile);
|
||||
fileTab.setAttribute("data-state", showFile ? "active" : "inactive");
|
||||
textTab.setAttribute("data-state", showFile ? "inactive" : "active");
|
||||
};
|
||||
|
||||
window.mdToHTMLHandleFileChange = function(input) {
|
||||
const file = input && input.files && input.files[0];
|
||||
const summary = byId("selected-file");
|
||||
const fileName = byId("selected-file-name");
|
||||
const fileMeta = byId("selected-file-meta");
|
||||
if (!summary || !fileName || !fileMeta) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file) {
|
||||
summary.classList.add("hidden");
|
||||
summary.classList.remove("flex");
|
||||
return;
|
||||
}
|
||||
|
||||
fileName.textContent = file.name;
|
||||
fileMeta.textContent = formatBytes(file.size) + " · " + formatRelativeTime(file.lastModified);
|
||||
summary.classList.remove("hidden");
|
||||
summary.classList.add("flex");
|
||||
};
|
||||
|
||||
window.mdToHTMLClearFile = function() {
|
||||
const input = byId("markdown-file");
|
||||
const summary = byId("selected-file");
|
||||
if (input) {
|
||||
input.value = "";
|
||||
}
|
||||
if (summary) {
|
||||
summary.classList.add("hidden");
|
||||
summary.classList.remove("flex");
|
||||
}
|
||||
};
|
||||
|
||||
window.mdToHTMLUpdateCharCount = function(textarea) {
|
||||
const counter = byId("markdown-char-count");
|
||||
if (!counter || !textarea) {
|
||||
return;
|
||||
}
|
||||
const count = textarea.value.length;
|
||||
counter.textContent = count + " символов";
|
||||
};
|
||||
|
||||
window.mdToHTMLResetForm = function() {
|
||||
window.mdToHTMLSetSource("file");
|
||||
window.mdToHTMLClearFile();
|
||||
const textarea = byId("markdown-text");
|
||||
if (textarea) {
|
||||
window.mdToHTMLUpdateCharCount(textarea);
|
||||
}
|
||||
const result = byId("result");
|
||||
if (result) {
|
||||
result.innerHTML = "";
|
||||
}
|
||||
};
|
||||
|
||||
window.mdToHTMLCopyButton = async function(button) {
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
const targetID = button.dataset.copyTarget;
|
||||
const target = targetID ? byId(targetID) : null;
|
||||
const value = target ? target.value || target.textContent || "" : "";
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await copyText(value);
|
||||
flashCopyState(button);
|
||||
} catch (_) {
|
||||
// noop
|
||||
}
|
||||
};
|
||||
|
||||
function init() {
|
||||
bindDropzone();
|
||||
window.mdToHTMLSetSource("file");
|
||||
const textarea = byId("markdown-text");
|
||||
if (textarea) {
|
||||
window.mdToHTMLUpdateCharCount(textarea);
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init, { once: true });
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
}
|
||||
|
||||
+60
-150
File diff suppressed because one or more lines are too long
@@ -0,0 +1,75 @@
|
||||
package ui
|
||||
|
||||
templ UploadIcon(class string) {
|
||||
<svg class={ class } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="17 8 12 3 7 8"></polyline>
|
||||
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ FileIcon(class string) {
|
||||
<svg class={ class } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<path d="M14 2v6h6"></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ AlignLeftIcon(class string) {
|
||||
<svg class={ class } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M4 7h16"></path>
|
||||
<path d="M4 12h10"></path>
|
||||
<path d="M4 17h16"></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ ArrowRightIcon(class string) {
|
||||
<svg class={ class } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M5 12h14"></path>
|
||||
<path d="m12 5 7 7-7 7"></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ InfoIcon(class string) {
|
||||
<svg class={ class } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<path d="M12 16v-4"></path>
|
||||
<path d="M12 8h.01"></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ CloseIcon(class string) {
|
||||
<svg class={ class } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M18 6 6 18"></path>
|
||||
<path d="m6 6 12 12"></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ CheckIcon(class string) {
|
||||
<svg class={ class } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M20 6 9 17l-5-5"></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ DownloadIcon(class string) {
|
||||
<svg class={ class } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="7 10 12 15 17 10"></polyline>
|
||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ CopyIcon(class string) {
|
||||
<svg class={ class } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect>
|
||||
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ ExternalLinkIcon(class string) {
|
||||
<svg class={ class } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
|
||||
<polyline points="15 3 21 3 21 9"></polyline>
|
||||
<line x1="10" y1="14" x2="21" y2="3"></line>
|
||||
</svg>
|
||||
}
|
||||
@@ -0,0 +1,481 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1001
|
||||
package ui
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func UploadIcon(class string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var2 = []any{class}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<svg class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/icons.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"></path> <polyline points=\"17 8 12 3 7 8\"></polyline> <line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"15\"></line></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func FileIcon(class string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var4 == nil {
|
||||
templ_7745c5c3_Var4 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var5 = []any{class}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<svg class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var5).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/icons.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"></path> <path d=\"M14 2v6h6\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func AlignLeftIcon(class string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var7 == nil {
|
||||
templ_7745c5c3_Var7 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var8 = []any{class}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<svg class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var8).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/icons.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M4 7h16\"></path> <path d=\"M4 12h10\"></path> <path d=\"M4 17h16\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ArrowRightIcon(class string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var10 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var10 == nil {
|
||||
templ_7745c5c3_Var10 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var11 = []any{class}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<svg class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var11).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/icons.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M5 12h14\"></path> <path d=\"m12 5 7 7-7 7\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func InfoIcon(class string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var13 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var13 == nil {
|
||||
templ_7745c5c3_Var13 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var14 = []any{class}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<svg class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var14).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/icons.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"></circle> <path d=\"M12 16v-4\"></path> <path d=\"M12 8h.01\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func CloseIcon(class string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var16 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var16 == nil {
|
||||
templ_7745c5c3_Var16 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var17 = []any{class}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<svg class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var17).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/icons.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M18 6 6 18\"></path> <path d=\"m6 6 12 12\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func CheckIcon(class string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var19 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var19 == nil {
|
||||
templ_7745c5c3_Var19 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var20 = []any{class}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<svg class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var20).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/icons.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func DownloadIcon(class string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var22 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var22 == nil {
|
||||
templ_7745c5c3_Var22 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var23 = []any{class}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var23...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<svg class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var24 string
|
||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var23).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/icons.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"></path> <polyline points=\"7 10 12 15 17 10\"></polyline> <line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"></line></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func CopyIcon(class string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var25 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var25 == nil {
|
||||
templ_7745c5c3_Var25 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var26 = []any{class}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var26...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<svg class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var27 string
|
||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var26).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/icons.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"></rect> <path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ExternalLinkIcon(class string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var28 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var28 == nil {
|
||||
templ_7745c5c3_Var28 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var29 = []any{class}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var29...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<svg class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var30 string
|
||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var29).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/icons.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"></path> <polyline points=\"15 3 21 3 21 9\"></polyline> <line x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\"></line></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
+12
-17
@@ -1,28 +1,23 @@
|
||||
package ui
|
||||
|
||||
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">
|
||||
{ children... }
|
||||
</div>
|
||||
</div>
|
||||
<footer class="mt-6 text-center text-sm text-muted-foreground">
|
||||
Markdown → HTML · v{ version.Version }
|
||||
</footer>
|
||||
</div>
|
||||
<body class="min-h-screen bg-[#fafafa] font-sans text-foreground antialiased">
|
||||
{ children... }
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+62
-47
@@ -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>
|
||||
</div>
|
||||
<p class="text-sm leading-6 text-muted-foreground">
|
||||
Ссылки одноразовые: после первого успешного открытия соответствующий UUID удаляется из preview-store.
|
||||
</p>
|
||||
<details class="group overflow-hidden rounded-[1.25rem] border border-border bg-card/80">
|
||||
<summary class="cursor-pointer list-none px-4 py-3 text-sm font-semibold text-foreground">
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<span class="inline-flex size-6 items-center justify-center rounded-full bg-muted text-xs text-muted-foreground">i</span>
|
||||
Inline-превью в изолированном iframe
|
||||
</span>
|
||||
</summary>
|
||||
<div class="border-t border-border/70 px-4 pb-4 pt-3">
|
||||
<iframe
|
||||
class="result-frame"
|
||||
sandbox=""
|
||||
referrerpolicy="no-referrer"
|
||||
srcdoc={ fullHTML }
|
||||
></iframe>
|
||||
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>
|
||||
</details>
|
||||
}
|
||||
}
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-sm font-medium text-foreground">Готово — { filename }</div>
|
||||
<div class="text-xs text-muted-foreground font-mono">
|
||||
{ formatResultMeta(sizeBytes, lineCount, elapsedMs) }
|
||||
</div>
|
||||
</div>
|
||||
<span class="inline-flex items-center rounded-md border border-border bg-muted px-2 py-0.5 text-[11px] font-medium text-foreground font-mono">
|
||||
standalone
|
||||
</span>
|
||||
</div>
|
||||
<textarea id={ "result-html-" + previewID } class="sr-only" readonly>{ fullHTML }</textarea>
|
||||
<a href={ "/preview/" + previewID } target="_blank" rel="noreferrer" class="sr-only">Открыть превью</a>
|
||||
<iframe class="hidden" sandbox="" referrerpolicy="no-referrer" srcdoc={ fullHTML }></iframe>
|
||||
<div class="flex flex-wrap items-center gap-2 px-5 py-5">
|
||||
<a
|
||||
href={ "/download/" + downloadID }
|
||||
class="focus-ring inline-flex h-9 items-center justify-center gap-2 rounded-md bg-primary px-3.5 text-sm font-medium text-primary-foreground transition hover:bg-primary/90"
|
||||
>
|
||||
@DownloadIcon("size-4")
|
||||
<span>Скачать HTML</span>
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="focus-ring inline-flex h-9 items-center justify-center gap-2 rounded-md border border-border bg-background px-3.5 text-sm font-medium text-foreground transition hover:bg-muted"
|
||||
data-copy-target={ "result-html-" + previewID }
|
||||
onclick="window.mdToHTMLCopyButton(this)"
|
||||
>
|
||||
@CopyIcon("size-4")
|
||||
<span>Скопировать</span>
|
||||
</button>
|
||||
<a
|
||||
href={ "/preview/" + previewID }
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="focus-ring inline-flex h-9 items-center justify-center gap-2 rounded-md border border-border bg-background px-3.5 text-sm font-medium text-foreground transition hover:bg-muted"
|
||||
>
|
||||
@ExternalLinkIcon("size-4")
|
||||
<span>Открыть в новой вкладке</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ Error(msg string) {
|
||||
<div class="rounded-[1.25rem] border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
|
||||
{ msg }
|
||||
<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)
|
||||
}
|
||||
|
||||
+170
-134
@@ -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\">")
|
||||
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, "Открыть превью")
|
||||
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)
|
||||
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
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "Скачать HTML")
|
||||
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)
|
||||
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\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(filename)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/ui/result.templ`, Line: 27, Col: 110}
|
||||
}
|
||||
_, 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=\"")
|
||||
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}
|
||||
}
|
||||
_, 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>")
|
||||
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)
|
||||
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, 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_Err = CheckIcon("size-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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
|
||||
}
|
||||
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}
|
||||
}
|
||||
_, 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, 3, "</div><div class=\"text-xs text-muted-foreground font-mono\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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
|
||||
}
|
||||
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_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, 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: 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, 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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/release-build.sh
|
||||
./scripts/release-build.sh --all
|
||||
|
||||
Options:
|
||||
--all Build all release targets used in CI:
|
||||
linux/amd64, linux/arm64, darwin/arm64
|
||||
EOF
|
||||
}
|
||||
|
||||
mode="current"
|
||||
if [[ $# -gt 1 ]]; then
|
||||
usage
|
||||
exit 2
|
||||
fi
|
||||
if [[ $# -eq 1 ]]; then
|
||||
case "$1" in
|
||||
--all)
|
||||
mode="all"
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_root="$(cd "${script_dir}/.." && pwd)"
|
||||
|
||||
cd "${repo_root}"
|
||||
|
||||
version="$(tr -d '[:space:]' < VERSION)"
|
||||
if [[ -z "${version}" ]]; then
|
||||
echo "VERSION is empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ldflags="-s -w -X github.com/fserg/md-to-html/internal/version.Version=${version}"
|
||||
dist_dir="${repo_root}/dist"
|
||||
mkdir -p "${dist_dir}" "${repo_root}/web/static/dist"
|
||||
|
||||
echo "==> Generating templ code"
|
||||
go run github.com/a-h/templ/cmd/templ@v0.3.1001 generate ./...
|
||||
|
||||
echo "==> Building Tailwind bundle"
|
||||
npx tailwindcss -c tailwind.config.js -i web/static/src/app.css -o web/static/dist/app.css --minify
|
||||
|
||||
echo "==> Running tests"
|
||||
go test ./...
|
||||
|
||||
targets=()
|
||||
if [[ "${mode}" == "all" ]]; then
|
||||
targets+=("linux amd64")
|
||||
targets+=("linux arm64")
|
||||
targets+=("darwin arm64")
|
||||
else
|
||||
current_goos="$(go env GOOS)"
|
||||
current_goarch="$(go env GOARCH)"
|
||||
targets+=("${current_goos} ${current_goarch}")
|
||||
fi
|
||||
|
||||
artifacts=()
|
||||
for target in "${targets[@]}"; do
|
||||
read -r goos goarch <<<"${target}"
|
||||
output="${dist_dir}/md-to-html-${goos}-${goarch}"
|
||||
echo "==> Building ${goos}/${goarch}"
|
||||
CGO_ENABLED=0 GOOS="${goos}" GOARCH="${goarch}" \
|
||||
go build -trimpath -ldflags="${ldflags}" -o "${output}" ./cmd/md-to-html
|
||||
artifacts+=("${output}")
|
||||
done
|
||||
|
||||
checksum_file="${dist_dir}/SHA256SUMS"
|
||||
(
|
||||
cd "${dist_dir}"
|
||||
shasum -a 256 "${artifacts[@]##${dist_dir}/}" > "${checksum_file}"
|
||||
)
|
||||
|
||||
echo
|
||||
echo "Artifacts:"
|
||||
for artifact in "${artifacts[@]}"; do
|
||||
echo " ${artifact}"
|
||||
done
|
||||
echo " ${checksum_file}"
|
||||
|
||||
if [[ "${mode}" == "current" ]]; then
|
||||
echo
|
||||
echo "Run to verify:"
|
||||
echo " ${artifacts[0]} serve"
|
||||
fi
|
||||
+19
-19
@@ -7,29 +7,29 @@ module.exports = {
|
||||
theme: {
|
||||
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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user