Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Build and Push Docker Image

on:
push:
branches: [ feature/font-picker, main ]
workflow_dispatch:

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=raw,value=latest,enable={{is_default_branch}}
type=raw,value=font-picker

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
/playground
/.idea
/glance*.yml
.DS_Store
config/
14 changes: 14 additions & 0 deletions internal/glance/glance.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,15 @@ func newApplication(c *config) (*application, error) {

for key, properties := range config.Theme.Presets.Items() {
properties.Key = key
properties.Font = config.Theme.Font
if err := properties.init(); err != nil {
return nil, fmt.Errorf("initializing preset theme %s: %v", key, err)
}
}
}

config.Theme.Key = "default"
config.Theme.Font = config.Theme.Font
if err := config.Theme.init(); err != nil {
return nil, fmt.Errorf("initializing default theme: %v", err)
}
Expand Down Expand Up @@ -300,6 +302,17 @@ func (a *application) populateTemplateRequestData(data *templateRequestData, r *
}
}

// Apply font override from cookie if present; when present, recompile CSS on a copy
if fontCookie, err := r.Cookie("font"); err == nil {
propertiesCopy := *theme
propertiesCopy.Font = fontCookie.Value
if err := propertiesCopy.init(); err == nil {
data.Theme = &propertiesCopy
return
}
// if init fails, fall back to original theme
}

data.Theme = theme
}

Expand Down Expand Up @@ -443,6 +456,7 @@ func (a *application) server() (func() error, func() error) {

if !a.Config.Theme.DisablePicker {
mux.HandleFunc("POST /api/set-theme/{key}", a.handleThemeChangeRequest)
mux.HandleFunc("POST /api/set-font/{key}", a.handleFontChangeRequest)
}

mux.HandleFunc("/api/widgets/{widget}/{path...}", a.handleWidgetRequest)
Expand Down
8 changes: 8 additions & 0 deletions internal/glance/static/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
src: url('../fonts/JetBrainsMono-Regular.woff2') format('woff2');
}

@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('../fonts/InterVariable.woff2') format('woff2');
}

:root {
font-size: 10px;

Expand Down
33 changes: 32 additions & 1 deletion internal/glance/static/css/site.css
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ ul {

body {
font-size: 1.3rem;
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-family, 'JetBrains Mono', monospace);
font-variant-ligatures: none;
line-height: 1.6;
color: var(--color-text-base);
Expand Down Expand Up @@ -370,6 +370,37 @@ kbd:active {
border-color: var(--color-text-base);
}

/* Apply the same rounded border style to font presets */
.font-choices {
display: flex;
align-items: center;
gap: 1.4rem; /* allow space for ::before inset (-.4rem) on adjacent buttons */
}
.font-choices .theme-preset {
position: relative;
}
/* Keep font picker labels static regardless of current selection */
.font-choices .font-preset[data-key="sans-serif"] {
font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif !important;
}
.font-choices .font-preset[data-key="monospace"] {
font-family: 'JetBrains Mono', monospace !important;
}
.font-choices .theme-preset::before {
content: '';
position: absolute;
inset: -.4rem;
border-radius: .7rem;
border: 2px solid transparent;
transition: border-color .3s;
}
.font-choices .theme-preset:hover::before {
border-color: var(--color-text-subdue);
}
.font-choices .theme-preset.current::before {
border-color: var(--color-text-base);
}

.theme-preset-light {
gap: 0.3rem;
height: 1.8rem;
Expand Down
Binary file added internal/glance/static/fonts/InterVariable.woff2
Binary file not shown.
56 changes: 56 additions & 0 deletions internal/glance/static/js/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,7 @@ async function changeTheme(key, onChanged) {

const response = await fetch(`${pageData.baseURL}/api/set-theme/${key}`, {
method: "POST",
credentials: "same-origin",
});

if (response.status != 200) {
Expand All @@ -691,6 +692,29 @@ async function changeTheme(key, onChanged) {
setTimeout(() => { tempStyle.remove(); }, 10);
}

async function changeFont(key, onChanged) {
const themeStyleElem = find("#theme-style");

const response = await fetch(`${pageData.baseURL}/api/set-font/${key}`, {
method: "POST",
credentials: "same-origin",
});

if (response.status != 200) {
alert("Failed to set font: " + response.statusText);
return;
}
const newThemeStyle = await response.text();

const tempStyle = elem("style")
.html("* { transition: none !important; }")
.appendTo(document.head);

themeStyleElem.html(newThemeStyle);
typeof onChanged == "function" && onChanged();
setTimeout(() => { tempStyle.remove(); }, 10);
}

function initThemePicker() {
const themeChoicesInMobileNav = find(".mobile-navigation .theme-choices");
if (!themeChoicesInMobileNav) return;
Expand Down Expand Up @@ -743,8 +767,40 @@ function initThemePicker() {
})
}

function initFontPicker() {
const presetElems = findAll(".font-choices .font-preset");
if (presetElems.length === 0) return;

let isLoading = false;

const setCurrent = (key) => {
presetElems.forEach((e) => {
if (e.dataset.key === key) e.classList.add("current");
else e.classList.remove("current");
});
};

if (pageData.font) setCurrent(pageData.font);

presetElems.forEach((presetElement) => {
const fontKey = presetElement.dataset.key;
if (!fontKey) return;

presetElement.addEventListener("click", () => {
if (isLoading) return;
isLoading = true;
changeFont(fontKey, function() {
isLoading = false;
pageData.font = fontKey;
setCurrent(fontKey);
});
});
});
}

async function setupPage() {
initThemePicker();
initFontPicker();

const pageElement = document.getElementById("page");
const pageContentElement = document.getElementById("page-content");
Expand Down
1 change: 1 addition & 0 deletions internal/glance/templates/document.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
/*{{ if .Page }}*/slug: "{{ .Page.Slug }}",/*{{ end }}*/
baseURL: "{{ .App.Config.Server.BaseURL }}",
theme: "{{ .Request.Theme.Key }}",
font: "{{ .Request.Theme.Font }}",
};
</script>
<title>{{ block "document-title" . }}{{ end }}</title>
Expand Down
10 changes: 10 additions & 0 deletions internal/glance/templates/page.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
</div>
<div data-popover-html>
<div class="theme-choices"></div>
<div class="margin-block-10"></div>
<div class="font-choices">
<button type="button" class="theme-preset font-preset" data-key="sans-serif" title="Sans-serif" style="font-family: &quot;Inter&quot;, system-ui, -apple-system, &quot;Segoe UI&quot;, Roboto, Arial, sans-serif;">sans-serif</button>
<button type="button" class="theme-preset font-preset" data-key="monospace" title="Monospace" style="font-family: 'JetBrains Mono', monospace;">monospace</button>
</div>
</div>
</div>
{{ end }}
Expand Down Expand Up @@ -77,6 +82,11 @@
{{ $preset.PreviewHTML }}
{{ end }}
</div>
<div class="margin-block-10"></div>
<div class="font-choices">
<button type="button" class="theme-preset font-preset" data-key="sans-serif" title="Sans-serif" style="font-family: &quot;Inter&quot;, system-ui, -apple-system, &quot;Segoe UI&quot;, Roboto, Arial, sans-serif;">sans-serif</button>
<button type="button" class="theme-preset font-preset" data-key="monospace" title="Monospace" style="font-family: 'JetBrains Mono', monospace;">monospace</button>
</div>
</div>

<div class="size-h3 pointer-events-none select-none">Change theme</div>
Expand Down
9 changes: 9 additions & 0 deletions internal/glance/templates/theme-style.gotmpl
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
:root {
{{- if .Font }}
{{- if eq .Font "sans-serif" }}
--font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
{{- else if eq .Font "monospace" }}
--font-family: 'JetBrains Mono', monospace;
{{- else }}
--font-family: {{ .Font }};
{{- end }}
{{- end }}
{{ if .BackgroundColor }}
--bgh: {{ .BackgroundColor.H }};
--bgs: {{ .BackgroundColor.S }}%;
Expand Down
40 changes: 40 additions & 0 deletions internal/glance/theme.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,45 @@ func (a *application) handleThemeChangeRequest(w http.ResponseWriter, r *http.Re
w.Write([]byte(properties.CSS))
}

func (a *application) handleFontChangeRequest(w http.ResponseWriter, r *http.Request) {
fontKey := r.PathValue("key")
if fontKey != "sans-serif" && fontKey != "monospace" {
w.WriteHeader(http.StatusNotFound)
return
}

// Determine current theme properties (respect theme cookie)
properties := &a.Config.Theme.themeProperties
if !a.Config.Theme.DisablePicker {
if selectedTheme, err := r.Cookie("theme"); err == nil {
if preset, exists := a.Config.Theme.Presets.Get(selectedTheme.Value); exists {
properties = preset
}
}
}

// Apply font
propertiesCopy := *properties // copy to avoid mutating shared theme state
propertiesCopy.Font = fontKey
if err := propertiesCopy.init(); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}

http.SetCookie(w, &http.Cookie{
Name: "font",
Value: fontKey,
Path: a.Config.Server.BaseURL + "/",
SameSite: http.SameSiteLaxMode,
Expires: time.Now().Add(2 * 365 * 24 * time.Hour),
})

w.Header().Set("Content-Type", "text/css")
w.Header().Set("X-Scheme", ternary(propertiesCopy.Light, "light", "dark"))
w.Write([]byte(propertiesCopy.CSS))
}

type themeProperties struct {
BackgroundColor *hslColorField `yaml:"background-color"`
PrimaryColor *hslColorField `yaml:"primary-color"`
Expand All @@ -46,6 +85,7 @@ type themeProperties struct {
Light bool `yaml:"light"`
ContrastMultiplier float32 `yaml:"contrast-multiplier"`
TextSaturationMultiplier float32 `yaml:"text-saturation-multiplier"`
Font string `yaml:"font"`

Key string `yaml:"-"`
CSS template.CSS `yaml:"-"`
Expand Down