diff --git a/CHANGELOG.md b/CHANGELOG.md index 918cd7a..0edbb64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Makefile b/Makefile index 8e2d060..5907251 100644 --- a/Makefile +++ b/Makefile @@ -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/ diff --git a/README.md b/README.md index 51428ea..d2c38b8 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,18 @@ # md-to-html -Сервис конвертации Markdown в самодостаточный HTML. Полностью офлайн, без обращений к внешним API. +Сервис конвертации Markdown в самодостаточный HTML. Конвертация выполняется локально, без внешних API. -Текущая версия: `0.2.1` (Go + goldmark + templUI) +![Превью интерфейса](screen.png) + +Текущая версия: `0.2.2` (Go + goldmark + templUI) ## Возможности - GFM + footnote + emoji + подсветка кода через chroma. -- Якоря в заголовках с 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 diff --git a/VERSION b/VERSION index 0c62199..ee1372d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.1 +0.2.2 diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 820daa4..e502f3f 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -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) { diff --git a/internal/ui/home.templ b/internal/ui/home.templ index 27469e3..f0f854c 100644 --- a/internal/ui/home.templ +++ b/internal/ui/home.templ @@ -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") { -
-
-
-
- Go migration - goldmark + templUI + @Layout("Markdown → standalone HTML") { +
+
+

Markdown → standalone HTML

+

+ Загрузите .md файл или вставьте Markdown-текст. Результат — готовый самодостаточный HTML со встроенными стилями. +

+
+ +
+ + +
+
+
+ + +
-
-

- Markdown → HTML без внешних зависимостей в результирующем документе. -

-

- Загрузите `.md`-файл или вставьте текст вручную. Сервис отдаст автономный HTML, одноразовое превью и отдельную ссылку на скачивание. -

-
-
-
-
-
Самодостаточный HTML
-

Результат открывается локально без CDN и без сетевых вызовов.

-
-
-
Одноразовые ссылки
-

Preview и download живут до первого открытия или максимум один час.

-
-
-
Русский интерфейс
-

Форма ориентирована на быстрый ручной прогон документации и заметок.

-
-
-
-
- @card.Card(card.Props{Class: "section-card overflow-hidden"}) { - @card.Header(card.HeaderProps{Class: "space-y-2 border-b border-border/70 pb-6"}) { -
Конвертация
- @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"}) { - -
-
Источник
-
- - -
-
-
- + +
+
+ + + - + + +
+ +
+

+ Конвертация использует публичный GitHub API +

+
+ + +
+
+
+ + +
+ +
+
+
+ API + + POST /convert + +
+ +
+ +
$ curl -X POST http://localhost:8000/convert \
+  -H 'Content-Type: application/json' \
+  -d '{"markdown":"# Hello"}'
+ } } diff --git a/internal/ui/home_templ.go b/internal/ui/home_templ.go index ed576d6..d47911a 100644 --- a/internal/ui/home_templ.go +++ b/internal/ui/home_templ.go @@ -8,11 +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/ui/components/button" - "github.com/fserg/md-to-html/internal/ui/components/card" -) - func Home() 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 @@ -46,162 +41,77 @@ func Home() templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
Go migration goldmark + templUI

Markdown → HTML без внешних зависимостей в результирующем документе.

Загрузите `.md`-файл или вставьте текст вручную. Сервис отдаст автономный HTML, одноразовое превью и отдельную ссылку на скачивание.

Самодостаточный HTML

Результат открывается локально без CDN и без сетевых вызовов.

Одноразовые ссылки

Preview и download живут до первого открытия или максимум один час.

Русский интерфейс

Форма ориентирована на быстрый ручной прогон документации и заметок.

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

Markdown → standalone HTML

Загрузите .md файл или вставьте Markdown-текст. Результат — готовый самодостаточный HTML со встроенными стилями.

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "Загрузить файл
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = FileIcon("size-4").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
README.md
3.4 KB · изменён только что
0 символов

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = InfoIcon("size-3.5").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "Поддерживается CommonMark + GFM

Конвертация использует публичный GitHub API

API POST /convert
$ curl -X POST http://localhost:8000/convert \\ -H 'Content-Type: application/json' \\ -d '{\"markdown\":\"# Hello\"}'
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } return nil }) - templ_7745c5c3_Err = Layout("Markdown → HTML").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = Layout("Markdown → standalone HTML").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/internal/ui/icons.templ b/internal/ui/icons.templ new file mode 100644 index 0000000..82c8373 --- /dev/null +++ b/internal/ui/icons.templ @@ -0,0 +1,75 @@ +package ui + +templ UploadIcon(class string) { + +} + +templ FileIcon(class string) { + +} + +templ AlignLeftIcon(class string) { + +} + +templ ArrowRightIcon(class string) { + +} + +templ InfoIcon(class string) { + +} + +templ CloseIcon(class string) { + +} + +templ CheckIcon(class string) { + +} + +templ DownloadIcon(class string) { + +} + +templ CopyIcon(class string) { + +} + +templ ExternalLinkIcon(class string) { + +} diff --git a/internal/ui/icons_templ.go b/internal/ui/icons_templ.go new file mode 100644 index 0000000..46975bc --- /dev/null +++ b/internal/ui/icons_templ.go @@ -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, " ") + 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, " ") + 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, " ") + 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, " ") + 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, " ") + 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, " ") + 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, "") + 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, " ") + 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, " ") + 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, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/internal/ui/layout.templ b/internal/ui/layout.templ index 91c214f..08f9473 100644 --- a/internal/ui/layout.templ +++ b/internal/ui/layout.templ @@ -1,28 +1,23 @@ package ui -import "github.com/fserg/md-to-html/internal/version" - templ Layout(title string) { - - - { title } + + + { title } · md-to-html - + + + + - -
-
-
- { children... } -
-
- -
+ + { children... } } diff --git a/internal/ui/layout_templ.go b/internal/ui/layout_templ.go index f5104f2..13673c0 100644 --- a/internal/ui/layout_templ.go +++ b/internal/ui/layout_templ.go @@ -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, "") + 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, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " · md-to-html") 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, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/internal/ui/result.templ b/internal/ui/result.templ index 20c0e41..0a24f7a 100644 --- a/internal/ui/result.templ +++ b/internal/ui/result.templ @@ -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"}) { -
- @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 - } - Файл: { filename } -
-

- Ссылки одноразовые: после первого успешного открытия соответствующий UUID удаляется из preview-store. -

-
- - - i - Inline-превью в изолированном iframe - - -
- +templ Result(previewID, downloadID, fullHTML, filename string, sizeBytes int, lineCount int, elapsedMs int) { +
+
+
+
+ @CheckIcon("size-4")
-
- } - } +
+
Готово — { filename }
+
+ { formatResultMeta(sizeBytes, lineCount, elapsedMs) } +
+
+ + standalone + + + + Открыть превью + +
+ + @DownloadIcon("size-4") + Скачать HTML + + + + @ExternalLinkIcon("size-4") + Открыть в новой вкладке + +
+ + } templ Error(msg string) { -
- { msg } +
+
+ { msg } +
} + +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) +} diff --git a/internal/ui/result_templ.go b/internal/ui/result_templ.go index d2d6458..a28888a 100644 --- a/internal/ui/result_templ.go +++ b/internal/ui/result_templ.go @@ -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, "
") - 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, "Файл: ") - 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, "

Ссылки одноразовые: после первого успешного открытия соответствующий UUID удаляется из preview-store.

i Inline-превью в изолированном iframe
") - 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, "
") + 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, "
Готово — ") + 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, "
") + 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, "
standalone
Открыть превью
") 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, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") 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, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") 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 diff --git a/screen.png b/screen.png new file mode 100644 index 0000000..f7e192f Binary files /dev/null and b/screen.png differ diff --git a/scripts/release-build.sh b/scripts/release-build.sh new file mode 100755 index 0000000..996b4d4 --- /dev/null +++ b/scripts/release-build.sh @@ -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 diff --git a/tailwind.config.js b/tailwind.config.js index dc17457..3054f1a 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -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"], }, }, }, diff --git a/web/static/src/app.css b/web/static/src/app.css index 43f4f2a..f710b06 100644 --- a/web/static/src/app.css +++ b/web/static/src/app.css @@ -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; } }