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:
@@ -7,7 +7,7 @@ Homepage design concepts for Levkin Inc.
|
||||
| **Spec** | `/spec/` | Company story as RFC-style documentation |
|
||||
| **Folder** | `/stack-folder/` | Manila folders L0–L7, 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
|
||||
|
||||
@@ -16,7 +16,7 @@ npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Vite default: `http://localhost:5173`
|
||||
Dev server: `http://localhost:5175` (5173 is often taken by other apps)
|
||||
|
||||
| Page | URL |
|
||||
|------|-----|
|
||||
@@ -28,9 +28,22 @@ Vite default: `http://localhost:5173`
|
||||
|
||||
```bash
|
||||
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/`)
|
||||
|
||||
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
|
||||
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 Wi‑Fi)
|
||||
```
|
||||
|
||||
Production: [levkin.ca/folders/](https://levkin.ca/folders/) — on viewports ≤720px the side rail becomes a **bottom tab bar** (L0–L7) with safe-area padding for iOS.
|
||||
|
||||
## Related sites
|
||||
|
||||
- [auto.levkin.ca](https://auto.levkin.ca)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://levkin.ca/sitemap.xml
|
||||
@@ -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>
|
||||
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'url';
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
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 });
|
||||
|
||||
|
||||
@@ -37,6 +37,9 @@ await mkdir(join(www, 'folders'), { 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', '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');
|
||||
await writeFile(
|
||||
|
||||
+111
-51
@@ -1,64 +1,124 @@
|
||||
/**
|
||||
* Tab rail alignment tests for /stack-folder/.
|
||||
* Run: STACK_URL=http://localhost:5173/stack-folder/ npm run test:folder
|
||||
* Tab rail alignment tests for /stack-folder/ (desktop + mobile).
|
||||
* 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 browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 1400, height: 900 } });
|
||||
await page.goto(URL, { waitUntil: 'networkidle' });
|
||||
async function runViewport(name, contextOptions) {
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage(contextOptions);
|
||||
await page.goto(URL, { waitUntil: 'networkidle' });
|
||||
|
||||
async function tabTops() {
|
||||
return page.evaluate(() => {
|
||||
const tabs = [...document.querySelectorAll('.mount .tab')];
|
||||
const stick =
|
||||
parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--stack-stick')) * 16;
|
||||
return {
|
||||
scrollY: window.scrollY,
|
||||
isFolded: document.querySelector('.mount')?.classList.contains('is-folded'),
|
||||
stick,
|
||||
tabs: tabs.map((t) => ({
|
||||
code: t.querySelector('.tab-code')?.textContent,
|
||||
top: Math.round(t.getBoundingClientRect().top),
|
||||
left: Math.round(t.getBoundingClientRect().left),
|
||||
})),
|
||||
};
|
||||
async function tabTops() {
|
||||
return page.evaluate(() => {
|
||||
const tabs = [...document.querySelectorAll('.mount .tab')];
|
||||
const stick =
|
||||
parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--stack-stick')) * 16;
|
||||
return {
|
||||
scrollY: window.scrollY,
|
||||
isFolded: document.querySelector('.mount')?.classList.contains('is-folded'),
|
||||
stick,
|
||||
tabs: tabs.map((t) => ({
|
||||
code: t.querySelector('.tab-code')?.textContent,
|
||||
top: Math.round(t.getBoundingClientRect().top),
|
||||
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));
|
||||
await page.waitForTimeout(600);
|
||||
if (VERBOSE) console.log('max scroll', await tabTops());
|
||||
const results = [
|
||||
...(await runViewport('desktop', desktop)),
|
||||
...(await runViewport('mobile', mobile)),
|
||||
];
|
||||
|
||||
await page.click('[data-goto="7"]');
|
||||
await page.waitForTimeout(800);
|
||||
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('; '));
|
||||
if (results.length) {
|
||||
console.error('FAIL:', results.join('; '));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('PASS: stack-folder tab rail');
|
||||
console.log('PASS: stack-folder (desktop + mobile)');
|
||||
|
||||
+38
-7
@@ -2,10 +2,37 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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>
|
||||
<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" />
|
||||
<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.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" />
|
||||
@@ -24,21 +51,24 @@
|
||||
<a class="skip-link" href="#main">Skip to specification</a>
|
||||
|
||||
<aside class="toc" aria-labelledby="toc-heading">
|
||||
<a href="/folders/" class="back">← folders</a>
|
||||
<p class="toc-doc-id">LK-SPEC-1.0</p>
|
||||
<div class="toc-head">
|
||||
<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>
|
||||
<nav aria-label="Specification sections">
|
||||
<a href="#preamble"><span class="toc-num">—</span> Preamble</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="#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="#contact"><span class="toc-num">5</span> Contact</a>
|
||||
</nav>
|
||||
|
||||
<section class="prefs" aria-labelledby="prefs-heading">
|
||||
<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>
|
||||
<div class="theme-options" role="radiogroup" aria-label="Color theme">
|
||||
<label class="theme-option">
|
||||
@@ -54,8 +84,8 @@
|
||||
<span>Dark</span>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="font-controls">
|
||||
</fieldset>
|
||||
<div class="font-controls">
|
||||
<span class="font-label" id="font-controls-label">Text size</span>
|
||||
<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>
|
||||
@@ -63,6 +93,7 @@
|
||||
<button type="button" class="font-btn" data-action="increase" aria-label="Increase text size">A+</button>
|
||||
</div>
|
||||
<span class="font-scale-readout" id="font-scale-readout" aria-live="polite">100%</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
+180
-14
@@ -228,13 +228,17 @@ a:hover { color: var(--link-hover); }
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.toc-short {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toc nav a:hover,
|
||||
.toc nav a:hover .toc-num { color: var(--link); }
|
||||
|
||||
.toc nav a.is-active {
|
||||
font-weight: 600;
|
||||
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); }
|
||||
@@ -256,23 +260,41 @@ a:hover { color: var(--link-hover); }
|
||||
.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-options { display: flex; gap: 0.15rem; }
|
||||
.theme-option { flex: 1; cursor: pointer; }
|
||||
.theme-option input { position: absolute; opacity: 0; width: 0; height: 0; }
|
||||
.theme-option { flex: 1; cursor: pointer; position: relative; }
|
||||
.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;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 2.25rem;
|
||||
min-width: 2.25rem;
|
||||
border: 1px solid var(--rule);
|
||||
background: var(--sheet);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.theme-option span {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.theme-option input:checked + span {
|
||||
font-weight: 600;
|
||||
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 {
|
||||
@@ -288,11 +310,6 @@ a:hover { color: var(--link-hover); }
|
||||
.font-btn {
|
||||
font-family: var(--mono);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -617,19 +634,168 @@ a:hover { color: var(--link-hover); }
|
||||
.toc {
|
||||
position: static;
|
||||
height: auto;
|
||||
padding: 0.65rem 0.75rem 0.55rem;
|
||||
border-right: none;
|
||||
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 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.2rem;
|
||||
flex-wrap: nowrap;
|
||||
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 {
|
||||
min-height: auto;
|
||||
padding: 0.3rem 0.5rem;
|
||||
display: inline-flex;
|
||||
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; }
|
||||
|
||||
+45
-2
@@ -681,8 +681,51 @@ a.site-preview:hover {
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.tab-rail { display: none; }
|
||||
.mount { padding-right: 1rem; }
|
||||
.site-header {
|
||||
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; }
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,16 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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>
|
||||
<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="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
|
||||
@@ -2,6 +2,11 @@ import { resolve } from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
host: true,
|
||||
port: 5175,
|
||||
strictPort: true,
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
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
@@ -2,16 +2,23 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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>
|
||||
<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="preconnect" href="https://fonts.googleapis.com" />
|
||||
<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" />
|
||||
<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="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>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
+40
-9
@@ -2,10 +2,37 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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>
|
||||
<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" />
|
||||
<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.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" />
|
||||
@@ -18,30 +45,33 @@
|
||||
} catch (e) { /* ignore */ }
|
||||
})();
|
||||
</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="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>
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#main">Skip to specification</a>
|
||||
|
||||
<aside class="toc" aria-labelledby="toc-heading">
|
||||
<a href="/folders/" class="back">← folders</a>
|
||||
<p class="toc-doc-id">LK-SPEC-1.0</p>
|
||||
<div class="toc-head">
|
||||
<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>
|
||||
<nav aria-label="Specification sections">
|
||||
<a href="#preamble"><span class="toc-num">—</span> Preamble</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="#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="#contact"><span class="toc-num">5</span> Contact</a>
|
||||
</nav>
|
||||
|
||||
<section class="prefs" aria-labelledby="prefs-heading">
|
||||
<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>
|
||||
<div class="theme-options" role="radiogroup" aria-label="Color theme">
|
||||
<label class="theme-option">
|
||||
@@ -57,8 +87,8 @@
|
||||
<span>Dark</span>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="font-controls">
|
||||
</fieldset>
|
||||
<div class="font-controls">
|
||||
<span class="font-label" id="font-controls-label">Text size</span>
|
||||
<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>
|
||||
@@ -66,6 +96,7 @@
|
||||
<button type="button" class="font-btn" data-action="increase" aria-label="Increase text size">A+</button>
|
||||
</div>
|
||||
<span class="font-scale-readout" id="font-scale-readout" aria-live="polite">100%</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://levkin.ca/sitemap.xml
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user