mirror of
https://github.com/kbenestad/invoice.git
synced 2026-06-18 08:04:32 +00:00
827 lines
46 KiB
HTML
827 lines
46 KiB
HTML
<!-- @dsCard group="Brand" name="Forgejo theme preview" subtitle="Repo page mock — light + dark" viewport="1280x900" -->
|
|
<!doctype html>
|
|
<html lang="en" data-theme="light">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>kb / utils · code.kbenestad</title>
|
|
<meta name="viewport" content="width=1280">
|
|
<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=Schibsted+Grotesk:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
<style>
|
|
/* ───────────────────────────────────────────────────────────────
|
|
kBenestad → Forgejo · preview palette
|
|
Single file: this mirrors what theme-kbenestad-{light,dark}.css
|
|
produce on a real Forgejo install, scoped to [data-theme].
|
|
─────────────────────────────────────────────────────────────── */
|
|
:root[data-theme="light"] {
|
|
--bg: #f8f9fb;
|
|
--box: #ffffff;
|
|
--box-header: #f8f9fb;
|
|
--border: #e7eaef;
|
|
--border-soft: #eef0f4;
|
|
--text: #14181e;
|
|
--text-soft: #3a434f;
|
|
--text-mute: #6b7785;
|
|
--text-faint: #97a0ac;
|
|
--accent: #2f6fed;
|
|
--accent-hover: #1f57cf;
|
|
--accent-soft: #eef3fe;
|
|
--accent-ring: rgba(47,111,237,.18);
|
|
--code-bg: #f1f3f6;
|
|
--green: #1f9d5f;
|
|
--green-soft: #d7f0e1;
|
|
--hover: #f1f3f6;
|
|
--shadow: 0 1px 0 rgba(20,24,30,.02);
|
|
}
|
|
:root[data-theme="dark"] {
|
|
--bg: #0d1117;
|
|
--box: #161b22;
|
|
--box-header: #1c232c;
|
|
--border: #232a33;
|
|
--border-soft: #1c232c;
|
|
--text: #eef1f5;
|
|
--text-soft: #c2cad3;
|
|
--text-mute: #8b95a1;
|
|
--text-faint: #6f7986;
|
|
--accent: #2f6fed;
|
|
--accent-hover: #4f82ec;
|
|
--accent-soft: #16233f;
|
|
--accent-ring: rgba(86,133,233,.28);
|
|
--code-bg: #0f141a;
|
|
--green: #3bb97a;
|
|
--green-soft: #13301f;
|
|
--hover: #1c232c;
|
|
--shadow: 0 1px 0 rgba(0,0,0,.25);
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
html, body { margin: 0; padding: 0; }
|
|
body {
|
|
font-family: "Schibsted Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
color: var(--text);
|
|
background: var(--bg);
|
|
letter-spacing: -.005em;
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
min-height: 100vh;
|
|
}
|
|
code, pre, .mono { font-family: "JetBrains Mono", ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace; }
|
|
|
|
a { color: var(--accent); text-decoration: none; }
|
|
a:hover { text-decoration: underline; }
|
|
|
|
.container { max-width: 1216px; margin: 0 auto; padding: 0 32px; }
|
|
|
|
/* ── Top navbar ─────────────────────────────────────────────── */
|
|
.nav {
|
|
height: 56px;
|
|
background: var(--box);
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
.nav-inner { width: 100%; display: flex; align-items: center; gap: 24px; }
|
|
.brand {
|
|
display: flex; align-items: center; gap: 10px;
|
|
color: var(--text); font-weight: 700; font-size: 15px;
|
|
letter-spacing: -.01em;
|
|
}
|
|
.brand-mark {
|
|
position: relative; width: 22px; height: 22px;
|
|
}
|
|
.brand-mark span {
|
|
position: absolute; width: 14px; height: 14px;
|
|
border-radius: 4px; background: var(--accent);
|
|
}
|
|
.brand-mark span:nth-child(1) { left: 0; top: 0; opacity: .55; }
|
|
.brand-mark span:nth-child(2) { right: 0; bottom: 0; background: var(--text); }
|
|
.brand-dot { color: var(--text-mute); font-weight: 500; }
|
|
.nav-links { display: flex; gap: 4px; flex: 1; }
|
|
.nav-links a {
|
|
color: var(--text-soft); font-weight: 500; font-size: 14px;
|
|
padding: 6px 10px; border-radius: 6px;
|
|
}
|
|
.nav-links a:hover { background: var(--hover); text-decoration: none; color: var(--text); }
|
|
.nav-right { display: flex; align-items: center; gap: 12px; }
|
|
.search {
|
|
display: flex; align-items: center; gap: 8px;
|
|
height: 32px; padding: 0 12px;
|
|
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
|
|
color: var(--text-mute); font-size: 13px; min-width: 240px;
|
|
}
|
|
.search .kbd {
|
|
margin-left: auto; padding: 1px 6px;
|
|
border: 1px solid var(--border); border-radius: 4px;
|
|
font-family: "JetBrains Mono", monospace; font-size: 11px;
|
|
color: var(--text-faint); background: var(--box);
|
|
}
|
|
.icon-btn {
|
|
width: 32px; height: 32px; display: grid; place-items: center;
|
|
border-radius: 6px; color: var(--text-soft); cursor: pointer;
|
|
background: transparent; border: 0;
|
|
}
|
|
.icon-btn:hover { background: var(--hover); color: var(--text); }
|
|
.avatar {
|
|
width: 28px; height: 28px; border-radius: 99px;
|
|
background: linear-gradient(135deg, #2f6fed, #6b5fd2);
|
|
display: grid; place-items: center;
|
|
color: #fff; font-weight: 700; font-size: 11px;
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
/* ── Theme toggle (preview chrome, not part of Forgejo) ─────── */
|
|
.theme-toggle {
|
|
position: fixed; top: 14px; right: 18px; z-index: 50;
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
padding: 6px 10px; height: 30px;
|
|
background: var(--box); color: var(--text-soft);
|
|
border: 1px solid var(--border); border-radius: 99px;
|
|
font: 600 12px/1 "Schibsted Grotesk", system-ui;
|
|
cursor: pointer; box-shadow: 0 2px 12px rgba(20,24,30,.10);
|
|
}
|
|
.theme-toggle:hover { color: var(--text); }
|
|
.theme-toggle svg { width: 14px; height: 14px; }
|
|
|
|
/* ── Repo header ─────────────────────────────────────────────── */
|
|
.repo-header { padding: 24px 0 0; }
|
|
.repo-title { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
.repo-title .repo-icon { color: var(--text-mute); }
|
|
.repo-title h1 {
|
|
margin: 0; font-size: 20px; font-weight: 500;
|
|
display: flex; align-items: center; gap: 6px;
|
|
}
|
|
.repo-title h1 .owner { color: var(--accent); font-weight: 500; }
|
|
.repo-title h1 .slash { color: var(--text-faint); font-weight: 400; }
|
|
.repo-title h1 .name { color: var(--accent); font-weight: 700; }
|
|
.badge {
|
|
font-size: 11px; font-weight: 600; padding: 2px 8px;
|
|
border: 1px solid var(--border); border-radius: 99px;
|
|
color: var(--text-soft);
|
|
text-transform: none;
|
|
}
|
|
.repo-actions { margin-left: auto; display: flex; gap: 8px; }
|
|
.btn {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
height: 30px; padding: 0 12px;
|
|
background: var(--box); color: var(--text); font-weight: 500; font-size: 13px;
|
|
border: 1px solid var(--border); border-radius: 6px; cursor: pointer;
|
|
}
|
|
.btn:hover { background: var(--hover); }
|
|
.btn .count {
|
|
margin-left: 4px; padding: 1px 7px;
|
|
background: var(--bg); border: 1px solid var(--border);
|
|
border-radius: 99px; font-size: 11px; color: var(--text-soft);
|
|
}
|
|
.btn-primary { background: var(--accent); color: #fff; border-color: var(--accent); font-weight: 600; }
|
|
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
.btn-primary .count {
|
|
background: rgba(255,255,255,.18); border-color: transparent; color: rgba(255,255,255,.9);
|
|
}
|
|
.btn svg { width: 14px; height: 14px; }
|
|
|
|
/* ── Tabs ────────────────────────────────────────────────────── */
|
|
.tabs {
|
|
margin-top: 16px;
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex; gap: 4px;
|
|
}
|
|
.tab {
|
|
position: relative; display: inline-flex; align-items: center; gap: 8px;
|
|
padding: 10px 14px; color: var(--text-soft); font-size: 14px; font-weight: 500;
|
|
border-radius: 6px 6px 0 0; cursor: pointer; margin-bottom: -1px;
|
|
}
|
|
.tab:hover { background: var(--hover); color: var(--text); text-decoration: none; }
|
|
.tab svg { width: 14px; height: 14px; color: var(--text-mute); }
|
|
.tab.active { color: var(--accent); font-weight: 600; }
|
|
.tab.active svg { color: var(--accent); }
|
|
.tab.active::after {
|
|
content: ""; position: absolute; left: 8px; right: 8px; bottom: -1px; height: 2px;
|
|
background: var(--accent); border-radius: 2px;
|
|
}
|
|
.tab .count {
|
|
background: var(--accent-soft); border: 1px solid var(--accent-ring);
|
|
border-radius: 99px; padding: 0 7px; font-size: 11px; font-weight: 600;
|
|
color: var(--accent);
|
|
}
|
|
.tab.active .count {
|
|
background: var(--accent); border-color: var(--accent); color: #fff;
|
|
}
|
|
|
|
/* ── Main grid ──────────────────────────────────────────────── */
|
|
.main {
|
|
display: grid; grid-template-columns: 1fr 296px; gap: 28px;
|
|
margin: 24px 0 64px;
|
|
}
|
|
|
|
/* ── Subbar (branch + code button) ──────────────────────────── */
|
|
.subbar {
|
|
display: flex; align-items: center; gap: 10px; margin-bottom: 16px;
|
|
}
|
|
.branch-select {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
height: 30px; padding: 0 10px;
|
|
border: 1px solid var(--border); border-radius: 6px;
|
|
background: var(--box); color: var(--text); font-weight: 500; font-size: 13px;
|
|
cursor: pointer;
|
|
}
|
|
.branch-select svg { width: 14px; height: 14px; color: var(--text-mute); }
|
|
.ref-counts {
|
|
display: flex; gap: 14px; color: var(--text-soft); font-size: 13px;
|
|
}
|
|
.ref-counts a { color: var(--text-soft); }
|
|
.ref-counts strong { color: var(--text); font-weight: 600; }
|
|
.subbar .spacer { flex: 1; }
|
|
.go-file {
|
|
display: inline-flex; align-items: center; white-space: nowrap;
|
|
height: 30px; padding: 0 12px;
|
|
border: 1px solid var(--border); background: var(--box);
|
|
border-radius: 6px; color: var(--text-soft); font-size: 13px;
|
|
font-family: "JetBrains Mono", monospace;
|
|
}
|
|
|
|
/* ── Box (cards / tables) ───────────────────────────────────── */
|
|
.box {
|
|
background: var(--box);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
box-shadow: var(--shadow);
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* ── File table ─────────────────────────────────────────────── */
|
|
.commit-row {
|
|
display: flex; align-items: center; gap: 10px;
|
|
padding: 10px 16px;
|
|
background: var(--box-header);
|
|
border-bottom: 1px solid var(--border);
|
|
font-size: 13px;
|
|
}
|
|
.commit-row .avatar { width: 22px; height: 22px; font-size: 10px; }
|
|
.commit-row .author { font-weight: 600; color: var(--text); }
|
|
.commit-row .msg { color: var(--text-soft); }
|
|
.commit-row .sha {
|
|
margin-left: auto; color: var(--text-mute);
|
|
font-family: "JetBrains Mono", monospace; font-size: 12px;
|
|
}
|
|
.commit-row .when { color: var(--text-mute); font-size: 12px; }
|
|
|
|
.file-row {
|
|
display: grid; grid-template-columns: 24px 1fr 2fr auto;
|
|
align-items: center; gap: 12px;
|
|
padding: 10px 16px;
|
|
border-top: 1px solid var(--border-soft);
|
|
font-size: 14px;
|
|
}
|
|
.file-row:first-of-type { border-top: 0; }
|
|
.file-row:hover { background: var(--hover); }
|
|
.file-row .icon { color: var(--text-mute); display: grid; place-items: center; }
|
|
.file-row .icon.folder { color: var(--accent); }
|
|
.file-row .name { color: var(--text); font-weight: 500; }
|
|
.file-row .name a { color: inherit; }
|
|
.file-row .msg {
|
|
color: var(--text-mute); font-size: 13px;
|
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
}
|
|
.file-row .msg a { color: var(--text-mute); }
|
|
.file-row .msg a:hover { color: var(--accent); }
|
|
.file-row .when {
|
|
color: var(--text-mute); font-size: 12px; white-space: nowrap;
|
|
}
|
|
|
|
/* ── README card ─────────────────────────────────────────────── */
|
|
.readme { margin-top: 20px; }
|
|
.readme-header {
|
|
display: flex; align-items: center; gap: 8px;
|
|
padding: 10px 16px;
|
|
background: var(--box-header);
|
|
border-bottom: 1px solid var(--border);
|
|
font-size: 13px; font-weight: 600; color: var(--text-soft);
|
|
}
|
|
.readme-header svg { width: 14px; height: 14px; color: var(--text-mute); }
|
|
.readme-body { padding: 28px 36px 36px; }
|
|
.readme-body h1 {
|
|
margin: 0 0 4px; font-size: 28px; font-weight: 700; letter-spacing: -.02em;
|
|
padding-bottom: 10px; border-bottom: 1px solid var(--border);
|
|
}
|
|
.readme-body h2 {
|
|
margin: 28px 0 10px; font-size: 19px; font-weight: 600; letter-spacing: -.01em;
|
|
padding-bottom: 6px; border-bottom: 1px solid var(--border);
|
|
}
|
|
.readme-body p { margin: 0 0 12px; color: var(--text-soft); }
|
|
.readme-body ul { padding-left: 22px; margin: 0 0 12px; color: var(--text-soft); }
|
|
.readme-body ul li { margin: 4px 0; }
|
|
.readme-body ul li code { color: var(--text); }
|
|
.readme-body code {
|
|
background: var(--code-bg); padding: 1px 6px; border-radius: 4px;
|
|
font-size: 12.5px; color: var(--text);
|
|
}
|
|
.readme-body pre {
|
|
background: var(--code-bg); padding: 14px 16px;
|
|
border-radius: 8px; margin: 0 0 16px;
|
|
overflow-x: auto; font-size: 13px;
|
|
color: var(--text);
|
|
}
|
|
.readme-body pre code { background: transparent; padding: 0; font-size: 13px; }
|
|
.readme-body .tag { color: var(--accent); }
|
|
.readme-body .com { color: var(--text-faint); }
|
|
|
|
/* ── Sidebar ────────────────────────────────────────────────── */
|
|
.side-section { margin-bottom: 24px; }
|
|
.side-section h3 {
|
|
margin: 0 0 12px; font-size: 13px; font-weight: 600;
|
|
color: var(--text); letter-spacing: -.005em;
|
|
display: flex; align-items: center; gap: 8px;
|
|
}
|
|
.side-section h3 .edit {
|
|
margin-left: auto; color: var(--text-mute); cursor: pointer;
|
|
}
|
|
.side-section .desc { color: var(--text-soft); margin: 0 0 12px; }
|
|
.side-meta { list-style: none; padding: 0; margin: 0; display: grid; gap: 8px; }
|
|
.side-meta li {
|
|
display: flex; align-items: center; gap: 8px;
|
|
color: var(--text-soft); font-size: 13px; white-space: nowrap;
|
|
}
|
|
.side-meta li svg { width: 14px; height: 14px; color: var(--text-mute); flex: 0 0 14px; }
|
|
.side-meta li a { color: var(--text-soft); }
|
|
.side-meta li a:hover { color: var(--accent); }
|
|
.topics { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
.topic {
|
|
display: inline-block;
|
|
padding: 2px 10px;
|
|
background: var(--accent-soft);
|
|
color: var(--accent);
|
|
border-radius: 99px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
border: 1px solid var(--accent-ring);
|
|
}
|
|
.topic:hover { background: var(--accent); color: #fff; text-decoration: none; }
|
|
.divider { height: 1px; background: var(--border); margin: 20px 0; border: 0; }
|
|
|
|
.release {
|
|
display: flex; gap: 10px; align-items: flex-start;
|
|
}
|
|
.release .tag-icon {
|
|
width: 28px; height: 28px;
|
|
background: var(--green-soft); color: var(--green);
|
|
border-radius: 6px; display: grid; place-items: center;
|
|
flex: 0 0 28px;
|
|
}
|
|
.release-meta { flex: 1; min-width: 0; }
|
|
.release-title {
|
|
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
|
font-size: 14px; font-weight: 600; color: var(--text);
|
|
}
|
|
.pill {
|
|
font-size: 10px; font-weight: 700; padding: 1px 7px;
|
|
background: var(--green); color: #fff;
|
|
border-radius: 99px; text-transform: uppercase; letter-spacing: .03em;
|
|
}
|
|
.release-when { font-size: 12px; color: var(--text-mute); margin-top: 2px; }
|
|
|
|
.lang-bar {
|
|
display: flex; height: 8px; border-radius: 99px; overflow: hidden;
|
|
background: var(--border); margin-bottom: 12px;
|
|
}
|
|
.lang-key {
|
|
display: grid; grid-template-columns: 1fr 1fr; gap: 6px 12px;
|
|
font-size: 12px; color: var(--text-soft);
|
|
}
|
|
.lang-key .dot {
|
|
display: inline-block; width: 10px; height: 10px;
|
|
border-radius: 99px; margin-right: 6px; vertical-align: -1px;
|
|
}
|
|
.lang-key .pct { color: var(--text-mute); margin-left: 2px; }
|
|
|
|
/* ── Footer ─────────────────────────────────────────────────── */
|
|
.footer {
|
|
border-top: 1px solid var(--border);
|
|
color: var(--text-mute); font-size: 12px;
|
|
padding: 16px 0 32px;
|
|
display: flex; gap: 18px;
|
|
}
|
|
.footer a { color: var(--text-mute); }
|
|
.footer a:hover { color: var(--accent); }
|
|
.footer .spacer { flex: 1; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- preview chrome: theme toggle ───────────────────────────────── -->
|
|
<button class="theme-toggle" id="themeToggle" aria-label="Toggle theme">
|
|
<svg viewBox="0 0 16 16" fill="currentColor" id="themeIcon" aria-hidden="true">
|
|
<path d="M8 12.5a4.5 4.5 0 1 1 0-9 4.5 4.5 0 0 1 0 9zM8 0a.5.5 0 0 1 .5.5v1.5a.5.5 0 0 1-1 0V.5A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5V15a.5.5 0 0 1-1 0v-1.5A.5.5 0 0 1 8 13zM3.05 2.343a.5.5 0 0 1 .707 0l1.06 1.06a.5.5 0 1 1-.707.708L3.05 3.05a.5.5 0 0 1 0-.707zm8.486 8.486a.5.5 0 0 1 .707 0l1.06 1.06a.5.5 0 1 1-.707.708l-1.06-1.06a.5.5 0 0 1 0-.708zM0 8a.5.5 0 0 1 .5-.5H2a.5.5 0 0 1 0 1H.5A.5.5 0 0 1 0 8zm13 0a.5.5 0 0 1 .5-.5H15a.5.5 0 0 1 0 1h-1.5A.5.5 0 0 1 13 8zM2.343 12.95a.5.5 0 0 1 0-.707l1.06-1.06a.5.5 0 1 1 .708.707l-1.06 1.06a.5.5 0 0 1-.708 0zm8.486-8.486a.5.5 0 0 1 0-.707l1.06-1.06a.5.5 0 1 1 .708.707l-1.06 1.06a.5.5 0 0 1-.708 0z"/>
|
|
</svg>
|
|
<span id="themeLabel">Light</span>
|
|
</button>
|
|
|
|
<!-- ── Nav ─────────────────────────────────────────────────────── -->
|
|
<nav class="nav">
|
|
<div class="container nav-inner">
|
|
<a class="brand" href="#">
|
|
<span class="brand-mark" aria-hidden="true"><span></span><span></span></span>
|
|
<span>code<span class="brand-dot">.kbenestad</span></span>
|
|
</a>
|
|
<div class="nav-links">
|
|
<a href="#">Dashboard</a>
|
|
<a href="#">Issues</a>
|
|
<a href="#">Pull Requests</a>
|
|
<a href="#">Milestones</a>
|
|
<a href="#">Explore</a>
|
|
</div>
|
|
<div class="nav-right">
|
|
<div class="search">
|
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M10.68 11.74a6 6 0 1 1 1.06-1.06l3.04 3.04a.75.75 0 1 1-1.06 1.06l-3.04-3.04zM11.5 7a4.5 4.5 0 1 0-9 0 4.5 4.5 0 0 0 9 0z"/></svg>
|
|
<span>Search…</span>
|
|
<span class="kbd">⌘K</span>
|
|
</div>
|
|
<button class="icon-btn" aria-label="New">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8.75 1a.75.75 0 0 0-1.5 0v6.25H1a.75.75 0 0 0 0 1.5h6.25V15a.75.75 0 0 0 1.5 0V8.75H15a.75.75 0 0 0 0-1.5H8.75V1z"/></svg>
|
|
</button>
|
|
<button class="icon-btn" aria-label="Notifications">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 1.5a.75.75 0 0 1 .75.75v.554a4.752 4.752 0 0 1 3.5 4.586v1.79l1.058 2.116A.75.75 0 0 1 12.63 12.4H10.2a2.25 2.25 0 0 1-4.4 0H3.37a.75.75 0 0 1-.67-1.085l1.05-2.1V7.39A4.752 4.752 0 0 1 7.25 2.804v-.554A.75.75 0 0 1 8 1.5zM7.2 12.4a.75.75 0 0 0 1.6 0H7.2z"/></svg>
|
|
</button>
|
|
<div class="avatar" title="kb">KB</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="container">
|
|
|
|
<!-- ── Repo header ───────────────────────────────────────────── -->
|
|
<header class="repo-header">
|
|
<div class="repo-title">
|
|
<svg class="repo-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5zm10.5-1H4.5a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8zM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2z"/></svg>
|
|
<h1>
|
|
<span class="owner">kb</span><span class="slash">/</span><span class="name">utils</span>
|
|
</h1>
|
|
<span class="badge">Public</span>
|
|
<div class="repo-actions">
|
|
<button class="btn">
|
|
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 2c1.981 0 3.671.992 4.933 2.078 1.27 1.091 2.187 2.345 2.637 3.023a1.62 1.62 0 0 1 0 1.798c-.45.678-1.367 1.932-2.637 3.023C11.67 13.008 9.981 14 8 14c-1.981 0-3.67-.992-4.933-2.078C1.797 10.83.88 9.576.43 8.898a1.62 1.62 0 0 1 0-1.798c.45-.678 1.367-1.932 2.637-3.023C4.33 2.992 6.019 2 8 2zM1.679 7.932a.12.12 0 0 0 0 .136c.411.622 1.241 1.75 2.366 2.717C5.176 11.758 6.527 12.5 8 12.5c1.473 0 2.825-.742 3.955-1.715 1.124-.967 1.954-2.096 2.366-2.717a.12.12 0 0 0 0-.136c-.412-.621-1.242-1.75-2.366-2.717C10.824 4.242 9.473 3.5 8 3.5c-1.473 0-2.825.742-3.955 1.715-1.124.967-1.954 2.096-2.366 2.717zM8 10a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></svg>
|
|
Watch<span class="count">7</span>
|
|
</button>
|
|
<button class="btn">
|
|
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.75.75 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25z"/></svg>
|
|
Star<span class="count">142</span>
|
|
</button>
|
|
<button class="btn">
|
|
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0zM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0z"/></svg>
|
|
Fork<span class="count">23</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Tabs ───────────────────────────────────────────────── -->
|
|
<div class="tabs">
|
|
<a class="tab active" href="#">
|
|
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M4.72 3.22a.75.75 0 0 1 1.06 1.06L2.06 8l3.72 3.72a.75.75 0 1 1-1.06 1.06L.47 8.53a.75.75 0 0 1 0-1.06zm6.56 0a.75.75 0 0 0-1.06 1.06L13.94 8l-3.72 3.72a.75.75 0 1 0 1.06 1.06l4.25-4.25a.75.75 0 0 0 0-1.06z"/></svg>
|
|
Code
|
|
</a>
|
|
<a class="tab" href="#">
|
|
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z"/><path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0zM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0z"/></svg>
|
|
Issues <span class="count">12</span>
|
|
</a>
|
|
<a class="tab" href="#">
|
|
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354z"/></svg>
|
|
Pull Requests <span class="count">3</span>
|
|
</a>
|
|
<a class="tab" href="#">
|
|
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25z"/></svg>
|
|
Actions
|
|
</a>
|
|
<a class="tab" href="#">
|
|
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="m8.878.392 5.25 3.045a2 2 0 0 1 .872 1.617v7.642a2 2 0 0 1-.872 1.617l-5.25 3.045a2 2 0 0 1-2.066 0L1.872 14.32a2 2 0 0 1-.872-1.617V5.054a2 2 0 0 1 .872-1.617L7.122.392a2 2 0 0 1 2.066 0z"/></svg>
|
|
Packages
|
|
</a>
|
|
<a class="tab" href="#">
|
|
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M1 7.775V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.752 1.752 0 0 1 1 7.775zM6 5a1 1 0 1 0-2 0 1 1 0 0 0 2 0z"/></svg>
|
|
Releases <span class="count">4</span>
|
|
</a>
|
|
<a class="tab" href="#">
|
|
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M0 1.75A.75.75 0 0 1 .75 1h4.253c1.227 0 2.317.59 3 1.501A3.743 3.743 0 0 1 11.006 1h4.245a.75.75 0 0 1 .75.75v10.5a.75.75 0 0 1-.75.75h-4.507a2.25 2.25 0 0 0-1.591.659l-.622.621a.75.75 0 0 1-1.06 0l-.622-.621A2.25 2.25 0 0 0 5.258 13H.75a.75.75 0 0 1-.75-.75z"/></svg>
|
|
Wiki
|
|
</a>
|
|
<a class="tab" href="#">
|
|
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0a8.2 8.2 0 0 1 .701.031C9.444.095 9.99.645 10.16 1.29l.288 1.107c.018.066.079.158.212.224.231.114.454.243.668.386.123.082.233.09.299.071l1.103-.303c.644-.176 1.392.021 1.82.63.27.385.506.792.704 1.218.315.675.111 1.422-.364 1.891l-.814.806c-.049.048-.098.147-.088.294.016.257.016.515 0 .772-.01.147.038.246.088.294l.814.806c.475.469.679 1.216.364 1.891a7.977 7.977 0 0 1-.704 1.217c-.428.61-1.176.807-1.82.63l-1.102-.302c-.067-.019-.177-.011-.3.071a5.909 5.909 0 0 1-.668.386c-.133.066-.194.158-.211.224l-.29 1.106c-.168.646-.715 1.196-1.458 1.26a8.006 8.006 0 0 1-1.402 0c-.743-.064-1.289-.614-1.458-1.26l-.289-1.106c-.018-.066-.079-.158-.212-.224a5.738 5.738 0 0 1-.668-.386c-.123-.082-.233-.09-.299-.071l-1.103.303c-.644.176-1.392-.021-1.82-.63a8.12 8.12 0 0 1-.704-1.218c-.315-.675-.111-1.422.363-1.891l.815-.806c.05-.048.098-.147.088-.294a6.214 6.214 0 0 1 0-.772c.01-.147-.038-.246-.088-.294l-.815-.806C.635 6.045.431 5.298.746 4.623a7.92 7.92 0 0 1 .704-1.217c.428-.61 1.176-.807 1.82-.63l1.102.302c.067.019.177.011.3-.071.214-.143.437-.272.668-.386.133-.066.194-.158.211-.224l.29-1.106C5.91.645 6.457.095 7.2.031A8.2 8.2 0 0 1 8 0zm0 5a3 3 0 1 0 0 6 3 3 0 0 0 0-6z"/></svg>
|
|
Settings
|
|
</a>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- ── Main content + sidebar ────────────────────────────────── -->
|
|
<div class="main">
|
|
|
|
<section>
|
|
<!-- subbar ------------------------------------------------- -->
|
|
<div class="subbar">
|
|
<button class="branch-select">
|
|
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.49 2.49 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25z"/></svg>
|
|
<strong>main</strong>
|
|
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" style="opacity:.7"><path d="M12.78 6.22a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L3.22 7.28a.75.75 0 0 1 1.06-1.06L8 9.94l3.72-3.72a.75.75 0 0 1 1.06 0z"/></svg>
|
|
</button>
|
|
<div class="ref-counts">
|
|
<a href="#"><strong>12</strong> Branches</a>
|
|
<a href="#"><strong>8</strong> Tags</a>
|
|
</div>
|
|
<div class="spacer"></div>
|
|
<span class="go-file">Go to file</span>
|
|
<button class="btn">
|
|
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8.75 1a.75.75 0 0 0-1.5 0v6.25H1a.75.75 0 0 0 0 1.5h6.25V15a.75.75 0 0 0 1.5 0V8.75H15a.75.75 0 0 0 0-1.5H8.75V1z"/></svg>
|
|
Add file
|
|
</button>
|
|
<button class="btn btn-primary">
|
|
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M4.72 3.22a.75.75 0 0 1 1.06 1.06L2.06 8l3.72 3.72a.75.75 0 1 1-1.06 1.06L.47 8.53a.75.75 0 0 1 0-1.06zm6.56 0a.75.75 0 0 0-1.06 1.06L13.94 8l-3.72 3.72a.75.75 0 1 0 1.06 1.06l4.25-4.25a.75.75 0 0 0 0-1.06z"/></svg>
|
|
Code
|
|
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" style="margin-left:2px;width:12px;height:12px"><path d="M12.78 6.22a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L3.22 7.28a.75.75 0 0 1 1.06-1.06L8 9.94l3.72-3.72a.75.75 0 0 1 1.06 0z"/></svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- file table -------------------------------------------- -->
|
|
<div class="box">
|
|
<div class="commit-row">
|
|
<div class="avatar" style="background:linear-gradient(135deg,#1f9d5f,#2f6fed);">KB</div>
|
|
<span class="author">kb</span>
|
|
<span class="msg">Tighten retry backoff jitter; cap at 30s</span>
|
|
<span class="sha">a8f3c0e</span>
|
|
<span class="when">· 2 days ago</span>
|
|
</div>
|
|
|
|
<div class="file-row">
|
|
<span class="icon folder">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3h-6.5a.25.25 0 0 1-.2-.1l-.9-1.2A1.75 1.75 0 0 0 5.25 1z"/></svg>
|
|
</span>
|
|
<span class="name"><a href="#">.forgejo</a></span>
|
|
<span class="msg"><a href="#">ci: pin runner image to v2</a></span>
|
|
<span class="when">3 weeks ago</span>
|
|
</div>
|
|
<div class="file-row">
|
|
<span class="icon folder">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3h-6.5a.25.25 0 0 1-.2-.1l-.9-1.2A1.75 1.75 0 0 0 5.25 1z"/></svg>
|
|
</span>
|
|
<span class="name"><a href="#">cmd</a></span>
|
|
<span class="msg"><a href="#">cmd/kbu: drop deprecated --raw flag</a></span>
|
|
<span class="when">last month</span>
|
|
</div>
|
|
<div class="file-row">
|
|
<span class="icon folder">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3h-6.5a.25.25 0 0 1-.2-.1l-.9-1.2A1.75 1.75 0 0 0 5.25 1z"/></svg>
|
|
</span>
|
|
<span class="name"><a href="#">internal</a></span>
|
|
<span class="msg"><a href="#">internal/retry: jitter cap 30s</a></span>
|
|
<span class="when">2 days ago</span>
|
|
</div>
|
|
<div class="file-row">
|
|
<span class="icon folder">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3h-6.5a.25.25 0 0 1-.2-.1l-.9-1.2A1.75 1.75 0 0 0 5.25 1z"/></svg>
|
|
</span>
|
|
<span class="name"><a href="#">pkg</a></span>
|
|
<span class="msg"><a href="#">pkg/slug: handle combining marks correctly</a></span>
|
|
<span class="when">last week</span>
|
|
</div>
|
|
<div class="file-row">
|
|
<span class="icon folder">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3h-6.5a.25.25 0 0 1-.2-.1l-.9-1.2A1.75 1.75 0 0 0 5.25 1z"/></svg>
|
|
</span>
|
|
<span class="name"><a href="#">testdata</a></span>
|
|
<span class="msg"><a href="#">golden: refresh fixtures for v2.4</a></span>
|
|
<span class="when">2 weeks ago</span>
|
|
</div>
|
|
<div class="file-row">
|
|
<span class="icon">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25z"/></svg>
|
|
</span>
|
|
<span class="name"><a href="#">.editorconfig</a></span>
|
|
<span class="msg"><a href="#">chore: standardize 2-space yaml</a></span>
|
|
<span class="when">3 months ago</span>
|
|
</div>
|
|
<div class="file-row">
|
|
<span class="icon">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25z"/></svg>
|
|
</span>
|
|
<span class="name"><a href="#">.gitignore</a></span>
|
|
<span class="msg"><a href="#">ignore local kbpkg cache dirs</a></span>
|
|
<span class="when">6 months ago</span>
|
|
</div>
|
|
<div class="file-row">
|
|
<span class="icon">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25z"/></svg>
|
|
</span>
|
|
<span class="name"><a href="#">LICENSE</a></span>
|
|
<span class="msg"><a href="#">Initial commit</a></span>
|
|
<span class="when">2 years ago</span>
|
|
</div>
|
|
<div class="file-row">
|
|
<span class="icon">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25z"/></svg>
|
|
</span>
|
|
<span class="name"><a href="#">Makefile</a></span>
|
|
<span class="msg"><a href="#">make: split test:unit / test:race</a></span>
|
|
<span class="when">last month</span>
|
|
</div>
|
|
<div class="file-row">
|
|
<span class="icon">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25z"/></svg>
|
|
</span>
|
|
<span class="name"><a href="#">README.md</a></span>
|
|
<span class="msg"><a href="#">docs: usage examples for retry helper</a></span>
|
|
<span class="when">2 days ago</span>
|
|
</div>
|
|
<div class="file-row">
|
|
<span class="icon">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25z"/></svg>
|
|
</span>
|
|
<span class="name"><a href="#">go.mod</a></span>
|
|
<span class="msg"><a href="#">deps: go 1.22; bump x/sync</a></span>
|
|
<span class="when">last month</span>
|
|
</div>
|
|
<div class="file-row">
|
|
<span class="icon">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25z"/></svg>
|
|
</span>
|
|
<span class="name"><a href="#">go.sum</a></span>
|
|
<span class="msg"><a href="#">deps: go 1.22; bump x/sync</a></span>
|
|
<span class="when">last month</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- README ------------------------------------------------ -->
|
|
<div class="box readme">
|
|
<div class="readme-header">
|
|
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M0 1.75A.75.75 0 0 1 .75 1h4.253c1.227 0 2.317.59 3 1.501A3.744 3.744 0 0 1 11.006 1h4.245a.75.75 0 0 1 .75.75v10.5a.75.75 0 0 1-.75.75h-4.507a2.25 2.25 0 0 0-1.591.659l-.622.621a.75.75 0 0 1-1.06 0l-.622-.621A2.25 2.25 0 0 0 5.258 13H.75a.75.75 0 0 1-.75-.75z"/></svg>
|
|
README.md
|
|
</div>
|
|
<div class="readme-body">
|
|
<h1>utils</h1>
|
|
<p>A small, opinionated bundle of Go helpers used across <code>kb/*</code> services —
|
|
retry/backoff, slug, env loading, structured errors. Zero non-stdlib runtime
|
|
dependencies; everything else is dev-only.</p>
|
|
|
|
<h2>Install</h2>
|
|
<pre><code><span class="com"># Go module</span>
|
|
go get code.kbenestad.net/kb/utils@v2.4.0
|
|
|
|
<span class="com"># Or via kbpkg, our internal package manager</span>
|
|
kbpkg install kb/utils
|
|
</code></pre>
|
|
|
|
<h2>Usage</h2>
|
|
<pre><code><span class="tag">package</span> main
|
|
|
|
<span class="tag">import</span> (
|
|
"context"
|
|
"code.kbenestad.net/kb/utils/retry"
|
|
)
|
|
|
|
<span class="tag">func</span> main() {
|
|
ctx := context.Background()
|
|
_ = retry.Do(ctx, retry.Default, <span class="tag">func</span>() <span class="tag">error</span> {
|
|
<span class="com">// network call here</span>
|
|
<span class="tag">return</span> nil
|
|
})
|
|
}
|
|
</code></pre>
|
|
|
|
<h2>What's inside</h2>
|
|
<ul>
|
|
<li><code>retry</code> — context-aware exponential backoff with full jitter (cap 30s).</li>
|
|
<li><code>slug</code> — Unicode-correct slugification; handles combining marks.</li>
|
|
<li><code>envx</code> — typed env loading with defaults and required-key checks.</li>
|
|
<li><code>errs</code> — structured error wrapping that survives JSON round-trips.</li>
|
|
<li><code>iox</code> — small io helpers (limited readers, atomic file writes).</li>
|
|
</ul>
|
|
|
|
<h2>Stability</h2>
|
|
<p>Public API follows semver. Anything under <code>internal/</code> is fair game and
|
|
will change without notice.</p>
|
|
</div>
|
|
</div>
|
|
|
|
</section>
|
|
|
|
<!-- ── Sidebar ───────────────────────────────────────────── -->
|
|
<aside>
|
|
<section class="side-section">
|
|
<h3>About <span class="edit" title="Edit">
|
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M.18 13.86A1.75 1.75 0 0 0 0 14.629V15.5a.5.5 0 0 0 .5.5h.871a1.75 1.75 0 0 0 .77-.18l9.55-4.775a.5.5 0 0 0 .224-.224l1.286-2.572a.5.5 0 0 0-.09-.567L9.318.18a.5.5 0 0 0-.567-.09L6.18 1.378a.5.5 0 0 0-.224.224L1.18 11.151a1.75 1.75 0 0 0-.18.77z"/></svg>
|
|
</span></h3>
|
|
<p class="desc">Small Go helpers shared across kBenestad services — retry, slug, env, structured errors.</p>
|
|
<div class="topics">
|
|
<a class="topic" href="#">go</a>
|
|
<a class="topic" href="#">utilities</a>
|
|
<a class="topic" href="#">retry</a>
|
|
<a class="topic" href="#">backoff</a>
|
|
<a class="topic" href="#">kbenestad</a>
|
|
</div>
|
|
<ul class="side-meta" style="margin-top:14px">
|
|
<li>
|
|
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M0 1.75A.75.75 0 0 1 .75 1h4.253c1.227 0 2.317.59 3 1.501A3.744 3.744 0 0 1 11.006 1h4.245a.75.75 0 0 1 .75.75v10.5a.75.75 0 0 1-.75.75h-4.507a2.25 2.25 0 0 0-1.591.659l-.622.621a.75.75 0 0 1-1.06 0l-.622-.621A2.25 2.25 0 0 0 5.258 13H.75a.75.75 0 0 1-.75-.75z"/></svg>
|
|
Readme
|
|
</li>
|
|
<li>
|
|
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8.75.75V2h.985c.304 0 .603.08.867.231l1.29.736c.038.022.08.033.124.033h2.234a.75.75 0 0 1 0 1.5h-.427l2.111 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.006.005-.01.01-.045.04c-.21.176-.441.327-.686.45C13.928 10.86 13.144 11 12.5 11s-1.428-.14-2.072-.46a4.07 4.07 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L11.628 4.5h-.539c-.166 0-.331-.027-.49-.078L9.293 4.94c-.279.156-.594.236-.913.234V13h2.25a.75.75 0 0 1 0 1.5h-6a.75.75 0 0 1 0-1.5h2.25V5.173l-1-.572a.51.51 0 0 0-.123-.033H4.91l2.111 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.016.015-.045.04c-.21.176-.441.327-.686.45C5.428 10.86 4.644 11 4 11s-1.428-.14-2.072-.46a4.07 4.07 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L3.128 4.5H2.75a.75.75 0 0 1 0-1.5h2.234a.249.249 0 0 0 .125-.033l1.288-.737c.265-.15.564-.23.868-.23h.985V.75a.75.75 0 0 1 1.5 0z"/></svg>
|
|
<a href="#">MIT License</a>
|
|
</li>
|
|
<li>
|
|
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0zm0 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13zm.75 3.25v3.5h2.5a.75.75 0 0 1 0 1.5h-3.25a.75.75 0 0 1-.75-.75V4.75a.75.75 0 0 1 1.5 0z"/></svg>
|
|
<a href="#">Activity</a>
|
|
</li>
|
|
<li>
|
|
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 2c1.981 0 3.671.992 4.933 2.078 1.27 1.091 2.187 2.345 2.637 3.023a1.62 1.62 0 0 1 0 1.798c-.45.678-1.367 1.932-2.637 3.023C11.67 13.008 9.981 14 8 14c-1.981 0-3.67-.992-4.933-2.078C1.797 10.83.88 9.576.43 8.898a1.62 1.62 0 0 1 0-1.798c.45-.678 1.367-1.932 2.637-3.023C4.33 2.992 6.019 2 8 2zM8 12.5c1.473 0 2.825-.742 3.955-1.715 1.124-.967 1.954-2.096 2.366-2.717a.12.12 0 0 0 0-.136c-.412-.621-1.242-1.75-2.366-2.717C10.824 4.242 9.473 3.5 8 3.5c-1.473 0-2.825.742-3.955 1.715-1.124.967-1.954 2.096-2.366 2.717a.12.12 0 0 0 0 .136c.411.622 1.241 1.75 2.366 2.717C5.176 11.758 6.527 12.5 8 12.5zM8 6a2 2 0 1 1 0 4 2 2 0 0 1 0-4z"/></svg>
|
|
<a href="#">7 watching</a>
|
|
</li>
|
|
<li>
|
|
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.75.75 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25z"/></svg>
|
|
<a href="#">142 stars</a>
|
|
</li>
|
|
<li>
|
|
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0z"/></svg>
|
|
<a href="#">23 forks</a>
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
|
|
<hr class="divider">
|
|
|
|
<section class="side-section">
|
|
<h3>Releases <span style="color:var(--text-mute);font-weight:500;margin-left:auto">4</span></h3>
|
|
<div class="release">
|
|
<div class="tag-icon">
|
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M1 7.775V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.752 1.752 0 0 1 1 7.775zM6 5a1 1 0 1 0-2 0 1 1 0 0 0 2 0z"/></svg>
|
|
</div>
|
|
<div class="release-meta">
|
|
<div class="release-title">
|
|
<a href="#">v2.4.0</a>
|
|
<span class="pill">Latest</span>
|
|
</div>
|
|
<div class="release-when">2 days ago</div>
|
|
</div>
|
|
</div>
|
|
<p style="margin:12px 0 0;color:var(--text-mute);font-size:13px">
|
|
<a href="#">+ 3 releases</a>
|
|
</p>
|
|
</section>
|
|
|
|
<hr class="divider">
|
|
|
|
<section class="side-section">
|
|
<h3>Languages</h3>
|
|
<div class="lang-bar" aria-label="Language breakdown">
|
|
<span style="width:78%;background:#00ADD8"></span>
|
|
<span style="width:14%;background:var(--text-mute)"></span>
|
|
<span style="width:8%;background:var(--green)"></span>
|
|
</div>
|
|
<div class="lang-key">
|
|
<span><span class="dot" style="background:#00ADD8"></span>Go<span class="pct"> 78.2%</span></span>
|
|
<span><span class="dot" style="background:var(--text-mute)"></span>Shell<span class="pct"> 13.7%</span></span>
|
|
<span><span class="dot" style="background:var(--green)"></span>Makefile<span class="pct"> 8.1%</span></span>
|
|
</div>
|
|
</section>
|
|
</aside>
|
|
</div>
|
|
|
|
<!-- ── Footer ─────────────────────────────────────────────────── -->
|
|
<footer class="footer">
|
|
<span>Powered by Forgejo · kBenestad theme</span>
|
|
<span class="spacer"></span>
|
|
<a href="#">English</a>
|
|
<a href="#">Licenses</a>
|
|
<a href="#">API</a>
|
|
<a href="#">Source</a>
|
|
</footer>
|
|
</div>
|
|
|
|
<script>
|
|
// Theme toggle ↔ localStorage. Order: stored → OS → light.
|
|
(function () {
|
|
var root = document.documentElement;
|
|
var btn = document.getElementById('themeToggle');
|
|
var lbl = document.getElementById('themeLabel');
|
|
var ico = document.getElementById('themeIcon');
|
|
|
|
var SUN = '<path d="M8 12.5a4.5 4.5 0 1 1 0-9 4.5 4.5 0 0 1 0 9zM8 0a.5.5 0 0 1 .5.5v1.5a.5.5 0 0 1-1 0V.5A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5V15a.5.5 0 0 1-1 0v-1.5A.5.5 0 0 1 8 13zM3.05 2.343a.5.5 0 0 1 .707 0l1.06 1.06a.5.5 0 1 1-.707.708L3.05 3.05a.5.5 0 0 1 0-.707zm8.486 8.486a.5.5 0 0 1 .707 0l1.06 1.06a.5.5 0 1 1-.707.708l-1.06-1.06a.5.5 0 0 1 0-.708zM0 8a.5.5 0 0 1 .5-.5H2a.5.5 0 0 1 0 1H.5A.5.5 0 0 1 0 8zm13 0a.5.5 0 0 1 .5-.5H15a.5.5 0 0 1 0 1h-1.5A.5.5 0 0 1 13 8zM2.343 12.95a.5.5 0 0 1 0-.707l1.06-1.06a.5.5 0 1 1 .708.707l-1.06 1.06a.5.5 0 0 1-.708 0zm8.486-8.486a.5.5 0 0 1 0-.707l1.06-1.06a.5.5 0 1 1 .708.707l-1.06 1.06a.5.5 0 0 1-.708 0z"/>';
|
|
var MOON = '<path d="M6 .278a.77.77 0 0 1 .08.858 7.2 7.2 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.78.78 0 0 1 .81.316.73.73 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.78.78 0 0 1 6 .278z"/>';
|
|
|
|
function apply(t) {
|
|
root.setAttribute('data-theme', t);
|
|
lbl.textContent = t === 'dark' ? 'Dark' : 'Light';
|
|
ico.innerHTML = t === 'dark' ? MOON : SUN;
|
|
}
|
|
|
|
var stored = null;
|
|
try { stored = localStorage.getItem('kb-theme'); } catch (e) {}
|
|
var initial = stored || (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
|
apply(initial);
|
|
|
|
btn.addEventListener('click', function () {
|
|
var next = root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
|
apply(next);
|
|
try { localStorage.setItem('kb-theme', next); } catch (e) {}
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|