Add SEO baseline + rebuild www/ (mobile toc/prefs polish, responsive viewport)

SEO: robots.txt, sitemap.xml, canonical, OG/Twitter tags, JSON-LD
(ProfessionalService) on / and /folders/, plus a reused screenshot as
og-image.png. prepare-www.mjs now copies robots/sitemap/og-image into
the build. Also carries pending mobile layout tweaks (toc/prefs bar,
viewport-fit=cover) that were already staged in the working tree.
This commit is contained in:
2026-07-08 10:58:46 -04:00
parent 6a571de240
commit 6949bee6e6
22 changed files with 507 additions and 94 deletions
+30 -4
View File
@@ -7,7 +7,7 @@ Homepage design concepts for Levkin Inc.
| **Spec** | `/spec/` | Company story as RFC-style documentation | | **Spec** | `/spec/` | Company story as RFC-style documentation |
| **Folder** | `/stack-folder/` | Manila folders L0L7, sticky tab stack, site previews | | **Folder** | `/stack-folder/` | Manila folders L0L7, sticky tab stack, site previews |
Open `/` to pick a direction. **Production** (`levkin.ca`): `/` → spec, `/folders/` → stack. Dev picker at `/` only when running `npm run dev` (not deployed).
## Develop ## Develop
@@ -16,7 +16,7 @@ npm install
npm run dev npm run dev
``` ```
Vite default: `http://localhost:5173` Dev server: `http://localhost:5175` (5173 is often taken by other apps)
| Page | URL | | Page | URL |
|------|-----| |------|-----|
@@ -28,9 +28,22 @@ Vite default: `http://localhost:5173`
```bash ```bash
npm run build npm run build
# dist/ # dist/ — Vite multi-page output
npm run build:www
# www/ — production layout for nginx (commit www/ before deploy)
``` ```
### Deploy (pve10 LXC 220)
```bash
npm run build:www
git add www/ && git commit -m "Rebuild www" && git push origin main
ssh root@10.0.10.10 'pct exec 220 -- bash -c "cd /var/www/levkin && git pull origin main"'
```
Requires DNS: `levkin.ca` and `www.levkin.ca` A → your home IP (same as other `*.levkin.ca` sites). Caddy on VM 106 proxies to `10.0.10.60:80`.
## Folder stack (`/stack-folder/`) ## Folder stack (`/stack-folder/`)
Eight layers with labeled tabs. Scroll pins earlier tabs on a shared rail; L7 joins last. Scroll stops when all tabs align. Eight layers with labeled tabs. Scroll pins earlier tabs on a shared rail; L7 joins last. Scroll stops when all tabs align.
@@ -52,9 +65,22 @@ Writes PNGs used in `stack-folder/previews/` (spec, auto, caseware, iliadobkin,
```bash ```bash
npm run dev npm run dev
STACK_URL=http://localhost:5173/stack-folder/ npm run test:folder STACK_URL=http://localhost:5175/stack-folder/ npm run test:folder
``` ```
Playwright runs **desktop** (tab alignment at max scroll) and **mobile** (bottom tab rail, no horizontal overflow, L3 jump).
### Test on a phone
Dev server listens on your LAN (`host: true` in Vite config):
```bash
npm run dev
# open http://<your-lan-ip>:5175/stack-folder/ on the phone (same WiFi)
```
Production: [levkin.ca/folders/](https://levkin.ca/folders/) — on viewports ≤720px the side rail becomes a **bottom tab bar** (L0L7) with safe-area padding for iOS.
## Related sites ## Related sites
- [auto.levkin.ca](https://auto.levkin.ca) - [auto.levkin.ca](https://auto.levkin.ca)
Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://levkin.ca/sitemap.xml
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://levkin.ca/</loc>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://levkin.ca/folders/</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
</urlset>
+1 -1
View File
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'url';
const root = join(dirname(fileURLToPath(import.meta.url)), '..'); const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const out = join(root, 'stack-folder/previews'); const out = join(root, 'stack-folder/previews');
const base = process.env.PREVIEW_BASE || 'http://localhost:5177'; const base = process.env.PREVIEW_BASE || 'http://localhost:5175';
mkdirSync(out, { recursive: true }); mkdirSync(out, { recursive: true });
+3
View File
@@ -37,6 +37,9 @@ await mkdir(join(www, 'folders'), { recursive: true });
await cp(join(dist, 'assets'), join(www, 'assets'), { recursive: true }); await cp(join(dist, 'assets'), join(www, 'assets'), { recursive: true });
await cp(join(root, 'public', 'favicon.svg'), join(www, 'favicon.svg')); await cp(join(root, 'public', 'favicon.svg'), join(www, 'favicon.svg'));
await cp(join(root, 'public', 'robots.txt'), join(www, 'robots.txt'));
await cp(join(root, 'public', 'sitemap.xml'), join(www, 'sitemap.xml'));
await cp(join(root, 'public', 'og-image.png'), join(www, 'og-image.png'));
const specHtml = await readFile(join(dist, 'spec', 'index.html'), 'utf8'); const specHtml = await readFile(join(dist, 'spec', 'index.html'), 'utf8');
await writeFile( await writeFile(
+111 -51
View File
@@ -1,64 +1,124 @@
/** /**
* Tab rail alignment tests for /stack-folder/. * Tab rail alignment tests for /stack-folder/ (desktop + mobile).
* Run: STACK_URL=http://localhost:5173/stack-folder/ npm run test:folder * Run: STACK_URL=http://localhost:5175/stack-folder/ npm run test:folder
*/ */
import { chromium } from 'playwright'; import { chromium, devices } from 'playwright';
const URL = process.env.STACK_URL || 'http://localhost:5173/stack-folder/'; const URL = process.env.STACK_URL || 'http://localhost:5175/stack-folder/';
const VERBOSE = process.env.VERBOSE === '1'; const VERBOSE = process.env.VERBOSE === '1';
const browser = await chromium.launch(); async function runViewport(name, contextOptions) {
const page = await browser.newPage({ viewport: { width: 1400, height: 900 } }); const browser = await chromium.launch();
await page.goto(URL, { waitUntil: 'networkidle' }); const page = await browser.newPage(contextOptions);
await page.goto(URL, { waitUntil: 'networkidle' });
async function tabTops() { async function tabTops() {
return page.evaluate(() => { return page.evaluate(() => {
const tabs = [...document.querySelectorAll('.mount .tab')]; const tabs = [...document.querySelectorAll('.mount .tab')];
const stick = const stick =
parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--stack-stick')) * 16; parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--stack-stick')) * 16;
return { return {
scrollY: window.scrollY, scrollY: window.scrollY,
isFolded: document.querySelector('.mount')?.classList.contains('is-folded'), isFolded: document.querySelector('.mount')?.classList.contains('is-folded'),
stick, stick,
tabs: tabs.map((t) => ({ tabs: tabs.map((t) => ({
code: t.querySelector('.tab-code')?.textContent, code: t.querySelector('.tab-code')?.textContent,
top: Math.round(t.getBoundingClientRect().top), top: Math.round(t.getBoundingClientRect().top),
left: Math.round(t.getBoundingClientRect().left), left: Math.round(t.getBoundingClientRect().left),
})), })),
}; };
});
}
if (VERBOSE) console.log(`${name} initial`, await tabTops());
await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight));
await page.waitForTimeout(600);
if (VERBOSE) console.log(`${name} max scroll`, await tabTops());
const railVisible = await page.evaluate(() => {
const rail = document.querySelector('.tab-rail');
if (!rail) return false;
return getComputedStyle(rail).display !== 'none';
}); });
if (name === 'mobile' && !railVisible) {
await browser.close();
return [`${name}: mobile tab rail not visible`];
}
const foldIssues = await page.evaluate(({ isMobile }) => {
const tabs = [...document.querySelectorAll('.mount .tab')];
const tops = tabs.map((t) => t.getBoundingClientRect().top);
const min = Math.min(...tops);
const max = Math.max(...tops);
const issues = [];
const spreadLimit = isMobile ? 40 : 30;
if (document.documentElement.scrollWidth > window.innerWidth + 1) {
issues.push('horizontal overflow');
}
if (max - min > spreadLimit) {
issues.push(`tab row spread ${Math.round(max - min)}px (want ≤${spreadLimit})`);
}
if (!document.querySelector('.mount')?.classList.contains('is-folded')) {
issues.push('mount not folded at max scroll');
}
return issues;
}, { isMobile: name === 'mobile' });
if (foldIssues.length) {
await browser.close();
return foldIssues.map((issue) => `${name}: ${issue}`);
}
if (name === 'mobile') {
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(200);
await page.locator('.tab-rail .rail-tab[data-goto="3"]').tap();
await page.waitForTimeout(800);
if (VERBOSE) console.log(`${name} rail goto L3`, await tabTops());
} else {
await page.click('[data-goto="7"]');
await page.waitForTimeout(800);
if (VERBOSE) console.log(`${name} goto L7`, await tabTops());
}
const fail = await page.evaluate(({ isMobile }) => {
const issues = [];
if (isMobile) {
const rail = document.querySelector('.tab-rail .rail-tab[data-goto="3"]');
const railBox = rail?.getBoundingClientRect();
if (!railBox || railBox.height < 40) issues.push('rail tap target too small');
if (window.scrollY < 100) issues.push('rail L3 tap did not scroll');
} else {
const tabs = [...document.querySelectorAll('.mount .tab')];
const tops = tabs.map((t) => t.getBoundingClientRect().top);
const l7 = tabs.find((t) => t.querySelector('.tab-code')?.textContent === 'L7');
const l0 = tabs.find((t) => t.querySelector('.tab-code')?.textContent === 'L0');
if (l7 && l0 && Math.abs(l7.getBoundingClientRect().top - l0.getBoundingClientRect().top) > 30) {
issues.push('L7 not aligned with L0');
}
}
return issues;
}, { isMobile: name === 'mobile' });
await browser.close();
return fail.map((issue) => `${name}: ${issue}`);
} }
if (VERBOSE) console.log('initial', await tabTops()); const desktop = { viewport: { width: 1400, height: 900 } };
const mobile = { ...devices['iPhone 13'], hasTouch: true };
await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight)); const results = [
await page.waitForTimeout(600); ...(await runViewport('desktop', desktop)),
if (VERBOSE) console.log('max scroll', await tabTops()); ...(await runViewport('mobile', mobile)),
];
await page.click('[data-goto="7"]'); if (results.length) {
await page.waitForTimeout(800); console.error('FAIL:', results.join('; '));
if (VERBOSE) console.log('goto L7', await tabTops());
const fail = await page.evaluate(() => {
const tabs = [...document.querySelectorAll('.mount .tab')];
const tops = tabs.map((t) => t.getBoundingClientRect().top);
const min = Math.min(...tops);
const max = Math.max(...tops);
const l7 = tabs.find((t) => t.querySelector('.tab-code')?.textContent === 'L7');
const l0 = tabs.find((t) => t.querySelector('.tab-code')?.textContent === 'L0');
const issues = [];
if (max - min > 30) issues.push(`tab row spread ${Math.round(max - min)}px (want ≤30)`);
if (l7 && l0 && Math.abs(l7.getBoundingClientRect().top - l0.getBoundingClientRect().top) > 30)
issues.push('L7 not aligned with L0');
if (!document.querySelector('.mount')?.classList.contains('is-folded'))
issues.push('mount not folded at max scroll');
return issues;
});
await browser.close();
if (fail.length) {
console.error('FAIL:', fail.join('; '));
process.exit(1); process.exit(1);
} }
console.log('PASS: stack-folder tab rail'); console.log('PASS: stack-folder (desktop + mobile)');
+38 -7
View File
@@ -2,10 +2,37 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Levkin — Company Specification</title> <title>Levkin — Company Specification</title>
<meta name="description" content="Levkin Inc.: software development and automation for production systems." /> <meta name="description" content="Levkin Inc.: software development and automation for production systems." />
<meta property="og:title" content="Levkin — Company Specification" />
<meta property="og:description" content="Levkin Inc.: software development and automation for production systems." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://levkin.ca/" />
<meta property="og:site_name" content="Levkin Inc." />
<meta property="og:image" content="https://levkin.ca/og-image.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Levkin — Company Specification" />
<meta name="twitter:description" content="Levkin Inc.: software development and automation for production systems." />
<meta name="twitter:image" content="https://levkin.ca/og-image.png" />
<link rel="canonical" href="https://levkin.ca/" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" /> <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "ProfessionalService",
"name": "Levkin Inc.",
"url": "https://levkin.ca/",
"description": "Software development and automation for production systems.",
"email": "ilia@levkine.ca",
"areaServed": ["North America", "Europe"],
"sameAs": [
"https://auto.levkin.ca/",
"https://caseware.levkin.ca/",
"https://iliadobkin.com/"
]
}
</script>
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Literata:opsz,wght@7..72,400;7..72,600&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Literata:opsz,wght@7..72,400;7..72,600&display=swap" rel="stylesheet" />
@@ -24,21 +51,24 @@
<a class="skip-link" href="#main">Skip to specification</a> <a class="skip-link" href="#main">Skip to specification</a>
<aside class="toc" aria-labelledby="toc-heading"> <aside class="toc" aria-labelledby="toc-heading">
<a href="/folders/" class="back">← folders</a> <div class="toc-head">
<p class="toc-doc-id">LK-SPEC-1.0</p> <a href="/folders/" class="back">← folders</a>
<p class="toc-doc-id">LK-SPEC-1.0</p>
</div>
<h2 id="toc-heading" class="toc-title">Table of contents</h2> <h2 id="toc-heading" class="toc-title">Table of contents</h2>
<nav aria-label="Specification sections"> <nav aria-label="Specification sections">
<a href="#preamble"><span class="toc-num"></span> Preamble</a> <a href="#preamble"><span class="toc-num"></span> Preamble</a>
<a href="#scope"><span class="toc-num">1</span> Scope</a> <a href="#scope"><span class="toc-num">1</span> Scope</a>
<a href="#services"><span class="toc-num">2</span> Services</a> <a href="#services"><span class="toc-num">2</span> Services</a>
<a href="#working"><span class="toc-num">3</span> Terms of engagement</a> <a href="#working"><span class="toc-num">3</span> <span class="toc-long">Terms of engagement</span><span class="toc-short">Terms</span></a>
<a href="#links"><span class="toc-num">4</span> References</a> <a href="#links"><span class="toc-num">4</span> References</a>
<a href="#contact"><span class="toc-num">5</span> Contact</a> <a href="#contact"><span class="toc-num">5</span> Contact</a>
</nav> </nav>
<section class="prefs" aria-labelledby="prefs-heading"> <section class="prefs" aria-labelledby="prefs-heading">
<h2 id="prefs-heading" class="prefs-heading">Display</h2> <h2 id="prefs-heading" class="prefs-heading">Display</h2>
<fieldset class="theme-field"> <div class="prefs-bar">
<fieldset class="theme-field">
<legend class="theme-legend">Theme</legend> <legend class="theme-legend">Theme</legend>
<div class="theme-options" role="radiogroup" aria-label="Color theme"> <div class="theme-options" role="radiogroup" aria-label="Color theme">
<label class="theme-option"> <label class="theme-option">
@@ -54,8 +84,8 @@
<span>Dark</span> <span>Dark</span>
</label> </label>
</div> </div>
</fieldset> </fieldset>
<div class="font-controls"> <div class="font-controls">
<span class="font-label" id="font-controls-label">Text size</span> <span class="font-label" id="font-controls-label">Text size</span>
<div class="font-buttons" role="group" aria-labelledby="font-controls-label"> <div class="font-buttons" role="group" aria-labelledby="font-controls-label">
<button type="button" class="font-btn" data-action="decrease" aria-label="Decrease text size">A</button> <button type="button" class="font-btn" data-action="decrease" aria-label="Decrease text size">A</button>
@@ -63,6 +93,7 @@
<button type="button" class="font-btn" data-action="increase" aria-label="Increase text size">A+</button> <button type="button" class="font-btn" data-action="increase" aria-label="Increase text size">A+</button>
</div> </div>
<span class="font-scale-readout" id="font-scale-readout" aria-live="polite">100%</span> <span class="font-scale-readout" id="font-scale-readout" aria-live="polite">100%</span>
</div>
</div> </div>
</section> </section>
+180 -14
View File
@@ -228,13 +228,17 @@ a:hover { color: var(--link-hover); }
text-align: right; text-align: right;
} }
.toc-short {
display: none;
}
.toc nav a:hover, .toc nav a:hover,
.toc nav a:hover .toc-num { color: var(--link); } .toc nav a:hover .toc-num { color: var(--link); }
.toc nav a.is-active { .toc nav a.is-active {
font-weight: 600; font-weight: 600;
background: var(--nav-active); background: var(--nav-active);
outline: 1px solid var(--rule); box-shadow: inset 0 0 0 1px var(--rule);
} }
.toc nav a.is-active .toc-num { color: var(--ink); } .toc nav a.is-active .toc-num { color: var(--ink); }
@@ -256,23 +260,41 @@ a:hover { color: var(--link-hover); }
.theme-field { border: 0; margin: 0 0 0.55rem; padding: 0; } .theme-field { border: 0; margin: 0 0 0.55rem; padding: 0; }
.theme-legend { font-size: 0.58rem; color: var(--muted); margin-bottom: 0.25rem; } .theme-legend { font-size: 0.58rem; color: var(--muted); margin-bottom: 0.25rem; }
.theme-options { display: flex; gap: 0.15rem; } .theme-options { display: flex; gap: 0.15rem; }
.theme-option { flex: 1; cursor: pointer; } .theme-option { flex: 1; cursor: pointer; position: relative; }
.theme-option input { position: absolute; opacity: 0; width: 0; height: 0; } .theme-option input {
position: absolute;
inset: 0;
opacity: 0;
width: 100%;
height: 100%;
margin: 0;
cursor: pointer;
}
.theme-option span { .theme-option span,
.font-btn {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 2.25rem; min-height: 2.25rem;
min-width: 2.25rem;
border: 1px solid var(--rule); border: 1px solid var(--rule);
background: var(--sheet); background: var(--sheet);
color: var(--ink); color: var(--ink);
} }
.theme-option span {
width: 100%;
}
.theme-option input:checked + span { .theme-option input:checked + span {
font-weight: 600; font-weight: 600;
background: var(--nav-active); background: var(--nav-active);
outline: 1px solid var(--rule-strong); box-shadow: inset 0 0 0 1px var(--rule-strong);
}
.prefs-bar {
display: contents;
} }
.font-controls { .font-controls {
@@ -288,11 +310,6 @@ a:hover { color: var(--link-hover); }
.font-btn { .font-btn {
font-family: var(--mono); font-family: var(--mono);
font-size: 0.68rem; font-size: 0.68rem;
min-width: 2.25rem;
min-height: 2.25rem;
border: 1px solid var(--rule);
background: var(--sheet);
color: var(--ink);
cursor: pointer; cursor: pointer;
} }
@@ -617,19 +634,168 @@ a:hover { color: var(--link-hover); }
.toc { .toc {
position: static; position: static;
height: auto; height: auto;
padding: 0.65rem 0.75rem 0.55rem;
border-right: none; border-right: none;
border-bottom: 1px solid var(--rule); border-bottom: 1px solid var(--rule);
} }
.toc-head {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 0.35rem 0.65rem;
margin-bottom: 0.45rem;
}
.back {
margin-bottom: 0;
}
.toc-doc-id {
margin-bottom: 0;
letter-spacing: 0.08em;
}
.toc-title {
display: none;
}
.toc-short {
display: inline;
}
.toc-long {
display: none;
}
.toc nav { .toc nav {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: nowrap;
gap: 0.2rem; gap: 0;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
border: 1px solid var(--rule);
background: var(--sheet);
}
.toc nav::-webkit-scrollbar {
display: none;
} }
.toc nav a { .toc nav a {
min-height: auto; display: inline-flex;
padding: 0.3rem 0.5rem; align-items: center;
gap: 0.25rem;
flex: 0 0 auto;
min-height: 2.25rem;
padding: 0.35rem 0.55rem;
border: none;
border-right: 1px solid var(--rule);
border-radius: 0;
background: transparent;
white-space: nowrap;
font-size: 0.58rem;
}
.toc nav a:last-child {
border-right: none;
}
.toc nav a.is-active {
box-shadow: none;
}
.toc-num {
text-align: left;
}
.prefs {
margin-top: 0;
padding-top: 0.45rem;
border-top: none;
}
.prefs-bar {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.35rem;
width: 100%;
}
.prefs-heading,
.theme-legend,
.font-label {
display: none;
}
.theme-field,
.font-controls {
display: flex;
min-width: 0;
margin: 0;
border: 1px solid var(--rule);
background: var(--sheet);
}
.theme-options {
display: flex;
flex: 1;
width: 100%;
gap: 0;
}
.theme-option {
flex: 1;
min-width: 0;
}
.theme-option span,
.font-btn {
width: 100%;
height: 2.25rem;
min-width: 0;
min-height: 2.25rem;
padding: 0 0.15rem;
border: none;
border-radius: 0;
font-size: 0.56rem;
box-shadow: none;
}
.theme-option:not(:first-child) span,
.font-btn + .font-btn {
border-left: 1px solid var(--rule);
}
.theme-option input:checked + span {
box-shadow: none;
}
.font-controls {
flex-wrap: nowrap;
gap: 0;
}
.font-buttons {
display: flex;
flex: 1;
min-width: 0;
gap: 0;
}
.font-btn {
flex: 1;
}
.font-scale-readout {
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 2.35rem;
border-left: 1px solid var(--rule);
font-size: 0.55rem;
color: var(--muted);
} }
.meta { display: none; } .meta { display: none; }
+45 -2
View File
@@ -681,8 +681,51 @@ a.site-preview:hover {
} }
@media (max-width: 720px) { @media (max-width: 720px) {
.tab-rail { display: none; } .site-header {
.mount { padding-right: 1rem; } font-size: 0.58rem;
line-height: 1.35;
padding: 0.45rem 0.65rem;
}
.tab-rail {
top: auto;
bottom: 0;
left: 0;
right: 0;
flex-direction: row;
flex-wrap: nowrap;
gap: 0.35rem;
padding: 0.45rem 0.5rem calc(0.45rem + env(safe-area-inset-bottom, 0px));
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
border: none;
border-top: 1px solid #4a4844;
}
.tab-rail::-webkit-scrollbar {
display: none;
}
.rail-tab {
flex: 0 0 auto;
min-height: 2.75rem;
min-width: 2.5rem;
padding: 0.4rem 0.55rem;
font-size: 0.62rem;
}
.mount {
padding-right: 1rem;
padding-bottom: calc(3.75rem + env(safe-area-inset-bottom, 0px));
--folder-body-h: min(28rem, calc(100dvh - var(--stack-nav) - var(--stack-tab-h) - 5.5rem));
}
.tab {
min-height: 2.75rem;
padding-top: 0.5rem;
}
:root { --tab-offset: 1.75rem; } :root { --tab-offset: 1.75rem; }
} }
+8 -1
View File
@@ -2,9 +2,16 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Levkin — Company Files</title> <title>Levkin — Company Files</title>
<meta name="description" content="Levkin Inc. — company specification, filed by section." /> <meta name="description" content="Levkin Inc. — company specification, filed by section." />
<meta property="og:title" content="Levkin — Company Files" />
<meta property="og:description" content="Levkin Inc. — company specification, filed by section." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://levkin.ca/folders/" />
<meta property="og:site_name" content="Levkin Inc." />
<meta name="twitter:card" content="summary" />
<link rel="canonical" href="https://levkin.ca/folders/" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" /> <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+5
View File
@@ -2,6 +2,11 @@ import { resolve } from 'path';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
export default defineConfig({ export default defineConfig({
server: {
host: true,
port: 5175,
strictPort: true,
},
build: { build: {
rollupOptions: { rollupOptions: {
input: { input: {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+10 -3
View File
@@ -2,16 +2,23 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Levkin — Company Files</title> <title>Levkin — Company Files</title>
<meta name="description" content="Levkin Inc. — company specification, filed by section." /> <meta name="description" content="Levkin Inc. — company specification, filed by section." />
<meta property="og:title" content="Levkin — Company Files" />
<meta property="og:description" content="Levkin Inc. — company specification, filed by section." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://levkin.ca/folders/" />
<meta property="og:site_name" content="Levkin Inc." />
<meta name="twitter:card" content="summary" />
<link rel="canonical" href="https://levkin.ca/folders/" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" /> <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Instrument+Sans:wght@400;500;600&family=Literata:opsz,wght@7..72,400;7..72,600&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Instrument+Sans:wght@400;500;600&family=Literata:opsz,wght@7..72,400;7..72,600&display=swap" rel="stylesheet" />
<script type="module" crossorigin src="/assets/stackFolder-4cxThntN.js"></script> <script type="module" crossorigin src="/assets/stackFolder-_Qi-mHWa.js"></script>
<link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-B5Qt9EMX.js"> <link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-B5Qt9EMX.js">
<link rel="stylesheet" crossorigin href="/assets/stackFolder-oqVQaMMI.css"> <link rel="stylesheet" crossorigin href="/assets/stackFolder-DTsVZwGE.css">
<script defer src="https://stats.levkin.ca/script.js" data-website-id="2a3bc7b2-b827-4770-8b31-c94016667734"></script> <script defer src="https://stats.levkin.ca/script.js" data-website-id="2a3bc7b2-b827-4770-8b31-c94016667734"></script>
</head> </head>
<body> <body>
+40 -9
View File
@@ -2,10 +2,37 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Levkin — Company Specification</title> <title>Levkin — Company Specification</title>
<meta name="description" content="Levkin Inc.: software development and automation for production systems." /> <meta name="description" content="Levkin Inc.: software development and automation for production systems." />
<meta property="og:title" content="Levkin — Company Specification" />
<meta property="og:description" content="Levkin Inc.: software development and automation for production systems." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://levkin.ca/" />
<meta property="og:site_name" content="Levkin Inc." />
<meta property="og:image" content="https://levkin.ca/og-image.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Levkin — Company Specification" />
<meta name="twitter:description" content="Levkin Inc.: software development and automation for production systems." />
<meta name="twitter:image" content="https://levkin.ca/og-image.png" />
<link rel="canonical" href="https://levkin.ca/" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" /> <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "ProfessionalService",
"name": "Levkin Inc.",
"url": "https://levkin.ca/",
"description": "Software development and automation for production systems.",
"email": "ilia@levkine.ca",
"areaServed": ["North America", "Europe"],
"sameAs": [
"https://auto.levkin.ca/",
"https://caseware.levkin.ca/",
"https://iliadobkin.com/"
]
}
</script>
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Literata:opsz,wght@7..72,400;7..72,600&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Literata:opsz,wght@7..72,400;7..72,600&display=swap" rel="stylesheet" />
@@ -18,30 +45,33 @@
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
})(); })();
</script> </script>
<script type="module" crossorigin src="/assets/spec-BY4A7Wkv.js"></script> <script type="module" crossorigin src="/assets/spec-B2lX7K2V.js"></script>
<link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-B5Qt9EMX.js"> <link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-B5Qt9EMX.js">
<link rel="stylesheet" crossorigin href="/assets/spec-BoGenpAF.css"> <link rel="stylesheet" crossorigin href="/assets/spec-Cx4F7IDd.css">
<script defer src="https://stats.levkin.ca/script.js" data-website-id="2a3bc7b2-b827-4770-8b31-c94016667734"></script> <script defer src="https://stats.levkin.ca/script.js" data-website-id="2a3bc7b2-b827-4770-8b31-c94016667734"></script>
</head> </head>
<body> <body>
<a class="skip-link" href="#main">Skip to specification</a> <a class="skip-link" href="#main">Skip to specification</a>
<aside class="toc" aria-labelledby="toc-heading"> <aside class="toc" aria-labelledby="toc-heading">
<a href="/folders/" class="back">← folders</a> <div class="toc-head">
<p class="toc-doc-id">LK-SPEC-1.0</p> <a href="/folders/" class="back">← folders</a>
<p class="toc-doc-id">LK-SPEC-1.0</p>
</div>
<h2 id="toc-heading" class="toc-title">Table of contents</h2> <h2 id="toc-heading" class="toc-title">Table of contents</h2>
<nav aria-label="Specification sections"> <nav aria-label="Specification sections">
<a href="#preamble"><span class="toc-num"></span> Preamble</a> <a href="#preamble"><span class="toc-num"></span> Preamble</a>
<a href="#scope"><span class="toc-num">1</span> Scope</a> <a href="#scope"><span class="toc-num">1</span> Scope</a>
<a href="#services"><span class="toc-num">2</span> Services</a> <a href="#services"><span class="toc-num">2</span> Services</a>
<a href="#working"><span class="toc-num">3</span> Terms of engagement</a> <a href="#working"><span class="toc-num">3</span> <span class="toc-long">Terms of engagement</span><span class="toc-short">Terms</span></a>
<a href="#links"><span class="toc-num">4</span> References</a> <a href="#links"><span class="toc-num">4</span> References</a>
<a href="#contact"><span class="toc-num">5</span> Contact</a> <a href="#contact"><span class="toc-num">5</span> Contact</a>
</nav> </nav>
<section class="prefs" aria-labelledby="prefs-heading"> <section class="prefs" aria-labelledby="prefs-heading">
<h2 id="prefs-heading" class="prefs-heading">Display</h2> <h2 id="prefs-heading" class="prefs-heading">Display</h2>
<fieldset class="theme-field"> <div class="prefs-bar">
<fieldset class="theme-field">
<legend class="theme-legend">Theme</legend> <legend class="theme-legend">Theme</legend>
<div class="theme-options" role="radiogroup" aria-label="Color theme"> <div class="theme-options" role="radiogroup" aria-label="Color theme">
<label class="theme-option"> <label class="theme-option">
@@ -57,8 +87,8 @@
<span>Dark</span> <span>Dark</span>
</label> </label>
</div> </div>
</fieldset> </fieldset>
<div class="font-controls"> <div class="font-controls">
<span class="font-label" id="font-controls-label">Text size</span> <span class="font-label" id="font-controls-label">Text size</span>
<div class="font-buttons" role="group" aria-labelledby="font-controls-label"> <div class="font-buttons" role="group" aria-labelledby="font-controls-label">
<button type="button" class="font-btn" data-action="decrease" aria-label="Decrease text size">A</button> <button type="button" class="font-btn" data-action="decrease" aria-label="Decrease text size">A</button>
@@ -66,6 +96,7 @@
<button type="button" class="font-btn" data-action="increase" aria-label="Increase text size">A+</button> <button type="button" class="font-btn" data-action="increase" aria-label="Increase text size">A+</button>
</div> </div>
<span class="font-scale-readout" id="font-scale-readout" aria-live="polite">100%</span> <span class="font-scale-readout" id="font-scale-readout" aria-live="polite">100%</span>
</div>
</div> </div>
</section> </section>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://levkin.ca/sitemap.xml
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://levkin.ca/</loc>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://levkin.ca/folders/</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
</urlset>