workstations and light theme

This commit is contained in:
2026-04-16 16:33:48 +03:00
parent 555e920c18
commit bbfaf5ccc5
37 changed files with 1015 additions and 106 deletions
+5 -1
View File
@@ -2,6 +2,10 @@
{ {
"description": "A personal page for projects, stack notes, and developer work.", "description": "A personal page for projects, stack notes, and developer work.",
"key": "web", "key": "web",
"links": {
"homepage": "https://scuroneko.dev",
"sources": "https://git.scuroneko.dev/ScuroNeko/scuroneko.dev"
},
"name": "ScuroNeko.dev", "name": "ScuroNeko.dev",
"stack": ["html", "css", "js"], "stack": ["html", "css", "js"],
"status": "active", "status": "active",
@@ -16,6 +20,6 @@
"name": "Laniakea", "name": "Laniakea",
"stack": ["go", "tg"], "stack": ["go", "tg"],
"status": "active", "status": "active",
"tone": "purple" "tone": "rose"
} }
] ]
+32
View File
@@ -0,0 +1,32 @@
{
"$id": "https://scuroneko.dev/schemas/links.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"items": {
"additionalProperties": false,
"properties": {
"key": {
"minLength": 1,
"type": "string"
},
"label": {
"minLength": 1,
"type": "string"
},
"name": {
"minLength": 1,
"type": "string"
},
"tone": {
"$ref": "./shared.schema.json#/$defs/tone"
},
"url": {
"format": "uri",
"type": "string"
}
},
"required": ["key", "name", "label", "url", "tone"],
"type": "object"
},
"title": "Links",
"type": "array"
}
+47
View File
@@ -0,0 +1,47 @@
{
"$id": "https://scuroneko.dev/schemas/projects.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"items": {
"additionalProperties": false,
"properties": {
"description": {
"minLength": 1,
"type": "string"
},
"key": {
"minLength": 1,
"type": "string"
},
"links": {
"additionalProperties": {
"format": "uri",
"type": "string"
},
"type": "object"
},
"name": {
"minLength": 1,
"type": "string"
},
"stack": {
"items": {
"minLength": 1,
"type": "string"
},
"type": "array",
"uniqueItems": true
},
"status": {
"enum": ["active", "archive", "wip", "paused"],
"type": "string"
},
"tone": {
"$ref": "./shared.schema.json#/$defs/tone"
}
},
"required": ["key", "name", "description", "stack", "status", "tone"],
"type": "object"
},
"title": "Projects",
"type": "array"
}
+23
View File
@@ -0,0 +1,23 @@
{
"$defs": {
"tone": {
"enum": [
"blue",
"gold",
"mint",
"purple",
"red",
"cyan",
"green",
"pink",
"teal",
"lime",
"rose"
],
"type": "string"
}
},
"$id": "https://scuroneko.dev/schemas/shared.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Shared schema definitions"
}
+39
View File
@@ -0,0 +1,39 @@
{
"$id": "https://scuroneko.dev/schemas/stack.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"items": {
"additionalProperties": false,
"properties": {
"hide": {
"type": "boolean"
},
"key": {
"minLength": 1,
"type": "string"
},
"name": {
"minLength": 1,
"type": "string"
},
"tags": {
"items": {
"minLength": 1,
"type": "string"
},
"type": "array",
"uniqueItems": true
},
"tone": {
"$ref": "./shared.schema.json#/$defs/tone"
},
"years": {
"minimum": 0,
"type": "number"
}
},
"required": ["key", "name", "tone", "years"],
"type": "object"
},
"title": "Stack",
"type": "array"
}
+87
View File
@@ -0,0 +1,87 @@
{
"$id": "https://scuroneko.dev/schemas/workstations.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"items": {
"additionalProperties": false,
"properties": {
"actual": {
"type": "boolean"
},
"devices": {
"items": {
"additionalProperties": false,
"properties": {
"key": {
"minLength": 1,
"type": "string"
},
"name": {
"minLength": 1,
"type": "string"
}
},
"required": ["key", "name"],
"type": "object"
},
"type": "array"
},
"key": {
"minLength": 1,
"type": "string"
},
"name": {
"minLength": 1,
"type": "string"
},
"specs": {
"properties": {
"cpu": {
"minLength": 1,
"type": "string"
},
"gpu": {
"minLength": 1,
"type": "string"
},
"os": {
"type": "string"
},
"ram": {
"minLength": 1,
"type": "string"
},
"storage": {
"items": {
"additionalProperties": false,
"properties": {
"capacity": {
"minLength": 1,
"type": "string"
},
"name": {
"minLength": 1,
"type": "string"
},
"type": {
"minLength": 1,
"type": "string"
}
},
"required": ["name", "capacity"],
"type": "object"
},
"type": "array"
}
},
"type": "object"
},
"tone": {
"$ref": "./shared.schema.json#/$defs/tone"
}
},
"required": ["key", "name", "tone"],
"type": "object"
},
"title": "Workstations",
"type": "array"
}
+21
View File
@@ -45,6 +45,20 @@
"tone": "blue", "tone": "blue",
"years": 2 "years": 2
}, },
{
"key": "jetbrains",
"name": "JetBrains IDE",
"tags": ["tools"],
"tone": "purple",
"years": 10
},
{
"key": "vscode",
"name": "Visual Studio Code",
"tags": ["tools"],
"tone": "purple",
"years": 7
},
{ {
"hide": true, "hide": true,
"key": "html", "key": "html",
@@ -65,5 +79,12 @@
"name": "JavaScript", "name": "JavaScript",
"tone": "gold", "tone": "gold",
"years": 7 "years": 7
},
{
"hide": true,
"key": "sass",
"name": "Sass",
"tone": "rose",
"years": 5
} }
] ]
+110
View File
@@ -0,0 +1,110 @@
[
{
"actual": true,
"devices": [
{
"key": "monitor1",
"name": "AOC 24G2SPU"
},
{
"key": "monitor2",
"name": "AOC 24E1Q"
},
{
"key": "keyboard",
"name": "Ardor Wakizashi"
},
{
"key": "mouse",
"name": "Ardor Immortaliry PRO"
},
{
"key": "microphone",
"name": "Fifine AM8"
}
],
"key": "pc",
"name": "PC",
"specs": {
"cpu": "AMD Ryzen 7 7700",
"gpu": "NVIDIA GeForce RTX 5070",
"os": "Windows 11 Pro",
"ram": "32GB DDR5",
"storage": [
{
"capacity": "1TB",
"name": "Adata XPG GAMMIX S11 Pro",
"type": "M2 PCI-E 3.0"
},
{
"capacity": "1TB",
"name": "WD Blue",
"type": "SATA HDD"
}
]
},
"tone": "rose"
},
{
"actual": true,
"key": "notebook",
"name": "Notebook",
"specs": {
"cpu": "Intel Core i3-8470U",
"gpu": "Intel UHD Graphics 620",
"os": "Windows 10 LTSC 2021",
"ram": "16GB DDR4",
"storage": [
{
"capacity": "256GB",
"name": "WD Blue SN550",
"type": "M2 PCI-E 3.0"
},
{
"capacity": "256GB",
"name": "Neo Forza NFS12",
"type": "SATA SSD"
}
]
},
"tone": "blue"
},
{
"actual": true,
"key": "vps",
"name": "VPS",
"specs": {
"cpu": "2 vCore (AMD EPYC 7763)",
"gpu": "N/A",
"os": "Arch Linux",
"ram": "4GB",
"storage": [
{
"capacity": "60GB",
"name": "NVMe SSD",
"type": "NVMe"
}
]
},
"tone": "green"
},
{
"actual": false,
"key": "pc",
"name": "PC",
"specs": {
"cpu": "Intel Core i7-6700",
"gpu": "AMD Radeon RX580",
"os": "Windows 11 Pro",
"ram": "16GB DDR4",
"storage": [
{
"capacity": "256GB",
"name": "Samsung 850 EVO",
"type": "SATA SSD"
}
]
},
"tone": "teal"
}
]

Before

Width:  |  Height:  |  Size: 208 KiB

After

Width:  |  Height:  |  Size: 208 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 822 B

After

Width:  |  Height:  |  Size: 822 B

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before

Width:  |  Height:  |  Size: 327 B

After

Width:  |  Height:  |  Size: 327 B

+1
View File
@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>JetBrains</title><path d="M2.345 23.997A2.347 2.347 0 0 1 0 21.652V10.988C0 9.665.535 8.37 1.473 7.433l5.965-5.961A5.01 5.01 0 0 1 10.989 0h10.666A2.347 2.347 0 0 1 24 2.345v10.664a5.056 5.056 0 0 1-1.473 3.554l-5.965 5.965A5.017 5.017 0 0 1 13.007 24v-.003H2.345Zm8.969-6.854H5.486v1.371h5.828v-1.371ZM3.963 6.514h13.523v13.519l4.257-4.257a3.936 3.936 0 0 0 1.146-2.767V2.345c0-.678-.552-1.234-1.234-1.234H10.989a3.897 3.897 0 0 0-2.767 1.145L3.963 6.514Zm-.192.192L2.256 8.22a3.944 3.944 0 0 0-1.145 2.768v10.664c0 .678.552 1.234 1.234 1.234h10.666a3.9 3.9 0 0 0 2.767-1.146l1.512-1.511H3.771V6.706Z"/></svg>

After

Width:  |  Height:  |  Size: 688 B

View File

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 974 B

View File

Before

Width:  |  Height:  |  Size: 527 B

After

Width:  |  Height:  |  Size: 527 B

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

+1
View File
@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Sass</title><path d="M12 0c6.627 0 12 5.373 12 12s-5.373 12-12 12S0 18.627 0 12 5.373 0 12 0zM9.615 15.998c.175.645.156 1.248-.024 1.792l-.065.18c-.024.061-.052.12-.078.176-.14.29-.326.56-.555.81-.698.759-1.672 1.047-2.09.805-.45-.262-.226-1.335.584-2.19.871-.918 2.12-1.509 2.12-1.509v-.003l.108-.061zm9.911-10.861c-.542-2.133-4.077-2.834-7.422-1.645-1.989.707-4.144 1.818-5.693 3.267C4.568 8.48 4.275 9.98 4.396 10.607c.427 2.211 3.457 3.657 4.703 4.73v.006c-.367.18-3.056 1.529-3.686 2.925-.675 1.47.105 2.521.615 2.655 1.575.436 3.195-.36 4.065-1.649.84-1.261.766-2.881.404-3.676.496-.135 1.08-.195 1.83-.104 2.101.24 2.521 1.56 2.43 2.1-.09.539-.523.854-.674.944-.15.091-.195.12-.181.181.015.09.091.09.21.075.165-.03 1.096-.45 1.141-1.471.045-1.29-1.186-2.729-3.375-2.7-.9.016-1.471.091-1.875.256-.03-.045-.061-.075-.105-.105-1.35-1.455-3.855-2.475-3.75-4.41.03-.705.285-2.564 4.8-4.814 3.705-1.846 6.661-1.335 7.171-.21.733 1.604-1.576 4.59-5.431 5.024-1.47.165-2.235-.404-2.431-.615-.209-.225-.239-.24-.314-.194-.12.06-.045.255 0 .375.12.3.585.825 1.396 1.095.704.225 2.43.359 4.5-.45 2.324-.899 4.139-3.405 3.614-5.505l.073.067z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 742 B

After

Width:  |  Height:  |  Size: 742 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg role="img" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<title>Visual Studio Code</title>
<path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M90.767 127.126a7.968 7.968 0 0 0 6.35-.244l26.353-12.681a8 8 0 0 0 4.53-7.209V21.009a8 8 0 0 0-4.53-7.21L97.117 1.12a7.97 7.97 0 0 0-9.093 1.548l-50.45 46.026L15.6 32.013a5.328 5.328 0 0 0-6.807.302l-7.048 6.411a5.335 5.335 0 0 0-.006 7.888L20.796 64 1.74 81.387a5.336 5.336 0 0 0 .006 7.887l7.048 6.411a5.327 5.327 0 0 0 6.807.303l21.974-16.68 50.45 46.025a7.96 7.96 0 0 0 2.743 1.793Zm5.252-92.183L57.74 64l38.28 29.058V34.943Z"/>
</svg>

After

Width:  |  Height:  |  Size: 622 B

+64 -2
View File
@@ -3,11 +3,50 @@
<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" />
<meta
name="description"
content="ScuroNeko is a Go and JavaScript developer building self-hosted tools, small web systems, and developer-focused projects."
/>
<meta name="author" content="ScuroNeko" />
<meta
name="theme-color"
content="#1f1f28"
media="(prefers-color-scheme: dark)"
/>
<meta
name="theme-color"
content="#f2ecbc"
media="(prefers-color-scheme: light)"
/>
<meta name="color-scheme" content="dark light" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="ScuroNeko" />
<meta property="og:title" content="ScuroNeko" />
<meta
property="og:description"
content="Go and JavaScript developer building self-hosted tools, small web systems, and developer-focused projects."
/>
<meta property="og:url" content="https://scuroneko.dev/" />
<meta property="og:image" content="https://scuroneko.dev/favicon.png" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="ScuroNeko" />
<meta
name="twitter:description"
content="Go and JavaScript developer building self-hosted tools, small web systems, and developer-focused projects."
/>
<meta name="twitter:image" content="https://scuroneko.dev/favicon.png" />
<link rel="canonical" href="https://scuroneko.dev/" />
<link rel="stylesheet" href="./styles/main.css" /> <link rel="stylesheet" href="./styles/main.css" />
<link rel="stylesheet" href="./styles/hero.css" /> <link rel="stylesheet" href="./styles/hero.css" />
<link rel="stylesheet" href="./styles/stack.css" /> <link rel="stylesheet" href="./styles/stack.css" />
<link rel="stylesheet" href="./styles/projects.css" /> <link rel="stylesheet" href="./styles/projects.css" />
<link rel="stylesheet" href="./styles/workstations.css" />
<link rel="stylesheet" href="./styles/links.css" /> <link rel="stylesheet" href="./styles/links.css" />
<link rel="stylesheet" href="./styles/tones.css" />
<link rel="stylesheet" href="./styles/theme-light.css" />
<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 <link
@@ -18,11 +57,13 @@
rel="stylesheet" rel="stylesheet"
href="https://storage.scuroneko.dev/fonts/FantasqueSansMono.css" href="https://storage.scuroneko.dev/fonts/FantasqueSansMono.css"
/> />
<link rel="shortcut icon" href="./favicon.png" type="image/png" /> <link rel="shortcut icon" href="./icons/favicon-512.png" type="image/png" />
<link rel="manifest" href="./manifest.json" />
<script src="./js/data.js" defer></script> <script src="./js/data.js" defer></script>
<script src="./js/stack.js" defer></script> <script src="./js/stack.js" defer></script>
<script src="./js/projects.js" defer></script> <script src="./js/projects.js" defer></script>
<script src="./js/workstations.js" defer></script>
<script src="./js/links.js" defer></script> <script src="./js/links.js" defer></script>
<title>ScuroNeko</title> <title>ScuroNeko</title>
</head> </head>
@@ -38,6 +79,7 @@
<nav class="site-nav" aria-label="Page sections"> <nav class="site-nav" aria-label="Page sections">
<a href="#stack-section">Stack</a> <a href="#stack-section">Stack</a>
<a href="#projects-section">Projects</a> <a href="#projects-section">Projects</a>
<a href="#workstations-section">Workstations</a>
<a href="#links-section">Links</a> <a href="#links-section">Links</a>
</nav> </nav>
</div> </div>
@@ -51,6 +93,9 @@
<p class="block-subtitle"> <p class="block-subtitle">
Tools I reach for when the task needs to ship. Tools I reach for when the task needs to ship.
</p> </p>
<button class="stack-toggle" id="stack-toggle" type="button" hidden>
View all
</button>
<div class="stack-grid" id="stack"></div> <div class="stack-grid" id="stack"></div>
</section> </section>
@@ -63,9 +108,26 @@
<div class="project-grid" id="projects"></div> <div class="project-grid" id="projects"></div>
</section> </section>
<section class="block" id="links-section"> <section class="block" id="workstations-section">
<div class="section-heading"> <div class="section-heading">
<span class="section-index">03</span> <span class="section-index">03</span>
<h2>Workstations</h2>
</div>
<p class="block-subtitle">Machines, peripherals, and daily drivers.</p>
<button
class="workstation-toggle"
id="workstation-toggle"
type="button"
hidden
>
View all
</button>
<div class="workstation-grid" id="workstations"></div>
</section>
<section class="block" id="links-section">
<div class="section-heading">
<span class="section-index">04</span>
<h2>Links</h2> <h2>Links</h2>
</div> </div>
<p class="block-subtitle"> <p class="block-subtitle">
+7 -2
View File
@@ -1,5 +1,6 @@
let stackList = []; let stackList = [];
let projectsList = []; let projectsList = [];
let workstationsList = [];
let linksList = []; let linksList = [];
const fetchJson = async (path) => { const fetchJson = async (path) => {
@@ -13,7 +14,7 @@ const fetchJson = async (path) => {
}; };
const loadIcon = async (key) => { const loadIcon = async (key) => {
const response = await fetch(`./icons/${key}.svg`); const response = await fetch(`./img/icons/${key}.svg`);
if (!response.ok) { if (!response.ok) {
return key; return key;
@@ -25,5 +26,9 @@ const loadIcon = async (key) => {
const formatIndex = (index) => String(index + 1).padStart(2, "0"); const formatIndex = (index) => String(index + 1).padStart(2, "0");
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
loadStack().then(loadProjects).then(loadLinks).catch(console.error); loadStack()
.then(loadProjects)
.then(loadWorkstations)
.then(loadLinks)
.catch(console.error);
}); });
+1 -1
View File
@@ -21,7 +21,7 @@ const loadProjects = async () => {
const buildProjectCard = (project, index) => { const buildProjectCard = (project, index) => {
const card = document.createElement("article"); const card = document.createElement("article");
card.classList.add("project-card", `project-card--${project.tone ?? "mint"}`); card.classList.add("project-card", `tone-${project.tone ?? "mint"}`);
const header = document.createElement("div"); const header = document.createElement("div");
header.classList.add("project-card__header"); header.classList.add("project-card__header");
+20 -5
View File
@@ -1,3 +1,5 @@
let isStackExpanded = false;
const getExperienceLevel = (years) => { const getExperienceLevel = (years) => {
if (years >= 7) return "Core"; if (years >= 7) return "Core";
if (years >= 4) return "Strong"; if (years >= 4) return "Strong";
@@ -17,21 +19,34 @@ const loadStackForProject = (keys) => {
const loadStack = async () => { const loadStack = async () => {
const stackContainer = document.getElementById("stack"); const stackContainer = document.getElementById("stack");
const stackToggle = document.getElementById("stack-toggle");
if (!stackContainer) { if (!stackContainer) {
return; return;
} }
stackList = await fetchJson("./data/stack.json"); stackList = await fetchJson("./data/stack.json");
const stackCards = stackList.map(buildStackCard).filter(Boolean); renderStack(stackContainer);
if (stackToggle && stackList.some((stack) => stack.hide === true)) {
stackToggle.hidden = false;
stackToggle.addEventListener("click", () => {
isStackExpanded = !isStackExpanded;
stackToggle.textContent = isStackExpanded ? "Show less" : "View all";
stackToggle.setAttribute("aria-expanded", String(isStackExpanded));
renderStack(stackContainer);
});
}
};
const renderStack = (stackContainer) => {
const stackCards = stackList
.filter((stack) => isStackExpanded || stack.hide !== true)
.map(buildStackCard);
stackContainer.replaceChildren(...stackCards); stackContainer.replaceChildren(...stackCards);
}; };
const buildStackCard = (stack) => { const buildStackCard = (stack) => {
if (stack?.hide === true) {
return null;
}
const card = document.createElement("div"); const card = document.createElement("div");
card.classList.add("stack-card", `tone-${stack.tone ?? "mint"}`); card.classList.add("stack-card", `tone-${stack.tone ?? "mint"}`);
+144
View File
@@ -0,0 +1,144 @@
let areWorkstationsExpanded = false;
const loadWorkstations = async () => {
const workstationContainer = document.getElementById("workstations");
const workstationToggle = document.getElementById("workstation-toggle");
if (!workstationContainer) {
return;
}
workstationsList = await fetchJson("./data/workstations.json");
renderWorkstations(workstationContainer);
if (
workstationToggle &&
workstationsList.some((workstation) => workstation.actual === false)
) {
workstationToggle.hidden = false;
workstationToggle.addEventListener("click", () => {
areWorkstationsExpanded = !areWorkstationsExpanded;
workstationToggle.textContent = areWorkstationsExpanded
? "Show less"
: "View all";
workstationToggle.setAttribute(
"aria-expanded",
String(areWorkstationsExpanded),
);
renderWorkstations(workstationContainer);
});
}
};
const renderWorkstations = (workstationContainer) => {
const workstationCards = workstationsList
.filter(
(workstation) => areWorkstationsExpanded || workstation.actual !== false,
)
.map(buildWorkstationCard);
workstationContainer.replaceChildren(...workstationCards);
};
const buildWorkstationCard = (workstation, index) => {
const card = document.createElement("article");
card.classList.add("workstation-card", `tone-${workstation.tone ?? "mint"}`);
const header = document.createElement("div");
header.classList.add("workstation-card__header");
const indexBadge = document.createElement("span");
indexBadge.classList.add("workstation-card__index");
indexBadge.textContent = formatIndex(index);
const key = document.createElement("span");
key.classList.add("workstation-card__key");
key.textContent = workstation.key;
const status = document.createElement("span");
status.classList.add(
"workstation-card__status",
workstation.actual
? "workstation-card__status--actual"
: "workstation-card__status--archive",
);
status.textContent = workstation.actual ? "active" : "archive";
const title = document.createElement("h3");
title.classList.add("workstation-card__title");
title.textContent = workstation.name;
const specs = document.createElement("dl");
specs.classList.add("workstation-card__specs");
appendSpec(specs, "CPU", workstation.specs?.cpu);
appendSpec(specs, "GPU", workstation.specs?.gpu);
appendSpec(specs, "RAM", workstation.specs?.ram);
appendSpec(specs, "OS", workstation.specs?.os);
const storage = buildChipGroup(
"Storage",
workstation.specs?.storage?.map(formatStorage) ?? [],
);
const devices = buildChipGroup(
"Devices",
workstation.devices?.map((device) => device.name) ?? [],
);
header.append(indexBadge, key, status);
card.append(header, title, specs);
if (storage) {
card.append(storage);
}
if (devices) {
card.append(devices);
}
return card;
};
const appendSpec = (list, label, value) => {
if (!value) {
return;
}
const term = document.createElement("dt");
term.textContent = label;
const description = document.createElement("dd");
description.textContent = value;
list.append(term, description);
};
const buildChipGroup = (label, items) => {
if (items.length === 0) {
return null;
}
const group = document.createElement("div");
group.classList.add("workstation-card__group");
const title = document.createElement("p");
title.classList.add("workstation-card__group-title");
title.textContent = label;
const chips = document.createElement("div");
chips.classList.add("workstation-card__chips");
chips.append(
...items.map((item) => {
const chip = document.createElement("span");
chip.textContent = item;
return chip;
}),
);
group.append(title, chips);
return group;
};
const formatStorage = (storage) => {
const parts = [storage.capacity, storage.name, storage.type].filter(Boolean);
return parts.join(" · ");
};
+17
View File
@@ -0,0 +1,17 @@
{
"background_color": "#1f1f28",
"description": "Go and JavaScript developer building self-hosted tools, small web systems, and developer-focused projects.",
"display": "standalone",
"icons": [
{
"sizes": "512x512",
"src": "icons/favicon-512.png",
"type": "image/png"
}
],
"name": "ScuroNeko",
"scope": "/",
"short_name": "ScuroNeko",
"start_url": "/",
"theme_color": "#1f1f28"
}
+3 -2
View File
@@ -27,7 +27,7 @@
border-color: var(--card-accent); border-color: var(--card-accent);
background-color: color-mix(in srgb, var(--card-glow) 36%, var(--surface)); background-color: color-mix(in srgb, var(--card-glow) 36%, var(--surface));
color: var(--text); color: var(--text);
transform: translateY(-2px); transform: translateY(-5px);
} }
.link-card__index { .link-card__index {
@@ -64,8 +64,9 @@
.link-card__name { .link-card__name {
color: var(--text); color: var(--text);
font-family: "Barlow Condensed", sans-serif; font-family: "Barlow Condensed", sans-serif;
font-size: 1.25rem;
font-weight: 700; font-weight: 700;
font-size: 1.25rem;
line-height: 1;
} }
.link-card__label { .link-card__label {
+8 -42
View File
@@ -19,6 +19,9 @@
--glow-cyan: rgba(127, 180, 202, 0.2); --glow-cyan: rgba(127, 180, 202, 0.2);
--glow-green: rgba(152, 187, 108, 0.18); --glow-green: rgba(152, 187, 108, 0.18);
--glow-pink: rgba(210, 126, 153, 0.18); --glow-pink: rgba(210, 126, 153, 0.18);
--glow-teal: rgba(107, 196, 180, 0.18);
--glow-lime: rgba(184, 204, 100, 0.18);
--glow-rose: rgba(224, 112, 128, 0.2);
--block-size: 980px; --block-size: 980px;
@@ -64,15 +67,18 @@ a:hover {
} }
h1 { h1 {
font-size: 3.5rem; font-size: clamp(3.5rem, 7vw, 5rem);
line-height: 0.95;
} }
h2 { h2 {
font-size: 2rem; font-size: clamp(1.85rem, 3vw, 2.25rem);
line-height: 1;
} }
h3 { h3 {
font-size: 1.5rem; font-size: 1.5rem;
line-height: 1.1;
} }
h4 { h4 {
@@ -124,46 +130,6 @@ h6 {
max-width: 560px; max-width: 560px;
} }
.tone-blue {
--card-accent: var(--primary);
--card-glow: var(--glow-blue);
}
.tone-gold {
--card-accent: var(--warm);
--card-glow: var(--glow-gold);
}
.tone-mint {
--card-accent: var(--accent);
--card-glow: var(--glow-mint);
}
.tone-purple {
--card-accent: #b876d3;
--card-glow: var(--glow-purple);
}
.tone-red {
--card-accent: var(--danger);
--card-glow: var(--glow-red);
}
.tone-cyan {
--card-accent: #7fb4ca;
--card-glow: var(--glow-cyan);
}
.tone-green {
--card-accent: #98bb6c;
--card-glow: var(--glow-green);
}
.tone-pink {
--card-accent: #d27e99;
--card-glow: var(--glow-pink);
}
@media (max-width: 560px) { @media (max-width: 560px) {
body { body {
padding-top: 32px; padding-top: 32px;
+10 -50
View File
@@ -1,43 +1,3 @@
.project-card.project-card--blue {
--project-accent: var(--primary);
--project-glow: var(--glow-blue);
}
.project-card.project-card--gold {
--project-accent: var(--warm);
--project-glow: var(--glow-gold);
}
.project-card.project-card--mint {
--project-accent: var(--accent);
--project-glow: var(--glow-mint);
}
.project-card.project-card--purple {
--project-accent: #b876d3;
--project-glow: var(--glow-purple);
}
.project-card.project-card--red {
--project-accent: var(--danger);
--project-glow: var(--glow-red);
}
.project-card.project-card--cyan {
--project-accent: #7fb4ca;
--project-glow: var(--glow-cyan);
}
.project-card.project-card--green {
--project-accent: #98bb6c;
--project-glow: var(--glow-green);
}
.project-card.project-card--pink {
--project-accent: #d27e99;
--project-glow: var(--glow-pink);
}
.project-grid { .project-grid {
display: grid; display: grid;
gap: 18px; gap: 18px;
@@ -46,16 +6,14 @@
} }
.project-card { .project-card {
--project-accent: var(--accent);
--project-glow: var(--glow-mint);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 220px; min-height: 220px;
padding: 22px; padding: 22px;
border: 1px solid color-mix(in srgb, var(--project-accent) 36%, var(--border)); border: 1px solid color-mix(in srgb, var(--card-accent) 36%, var(--border));
border-radius: 8px; border-radius: 8px;
background: background:
linear-gradient(140deg, var(--project-glow), transparent 48%), linear-gradient(140deg, var(--card-glow), transparent 48%),
linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent), linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent),
var(--surface); var(--surface);
box-shadow: 0 18px 42px rgba(0, 0, 0, 0.2); box-shadow: 0 18px 42px rgba(0, 0, 0, 0.2);
@@ -66,9 +24,9 @@
} }
.project-card:hover { .project-card:hover {
border-color: var(--project-accent); border-color: var(--card-accent);
box-shadow: 0 24px 52px rgba(0, 0, 0, 0.28); box-shadow: 0 24px 52px rgba(0, 0, 0, 0.28);
transform: translateY(-2px); transform: translateY(-5px);
} }
.project-card__header { .project-card__header {
@@ -92,8 +50,8 @@
} }
.project-card__key { .project-card__key {
border: 1px solid color-mix(in srgb, var(--project-accent) 56%, var(--border)); border: 1px solid color-mix(in srgb, var(--card-accent) 56%, var(--border));
color: var(--project-accent); color: var(--card-accent);
text-transform: uppercase; text-transform: uppercase;
} }
@@ -107,7 +65,8 @@
.project-card__title { .project-card__title {
margin-top: 22px; margin-top: 22px;
color: var(--text); color: var(--text);
font-size: 1.75rem; font-size: clamp(1.6rem, 2.4vw, 1.9rem);
line-height: 1;
} }
.project-card__description { .project-card__description {
@@ -131,6 +90,7 @@
color: var(--card-accent); color: var(--card-accent);
font-size: 0.9rem; font-size: 0.9rem;
font-family: var(--fonts-monospace); font-family: var(--fonts-monospace);
font-style: italic;
} }
.project-card__links { .project-card__links {
@@ -144,7 +104,7 @@
.project-card__links a { .project-card__links a {
border-bottom: 1px solid currentColor; border-bottom: 1px solid currentColor;
color: var(--project-accent); color: var(--card-accent);
font-weight: 700; font-weight: 700;
text-decoration: none; text-decoration: none;
} }
+30 -1
View File
@@ -5,6 +5,32 @@
margin-top: 18px; margin-top: 18px;
} }
.stack-toggle {
margin-top: 14px;
min-height: 38px;
padding: 8px 16px;
border: 1px solid var(--border);
border-radius: 8px;
background-color: transparent;
color: var(--text);
cursor: pointer;
font: inherit;
transition:
background-color 160ms ease,
border-color 160ms ease,
color 160ms ease;
}
.stack-toggle:hover {
border-color: var(--accent);
background-color: var(--glow-mint);
color: var(--text);
}
.stack-toggle[hidden] {
display: none;
}
.stack-card { .stack-card {
position: relative; position: relative;
display: grid; display: grid;
@@ -30,7 +56,7 @@
.stack-card:hover { .stack-card:hover {
border-color: var(--card-accent); border-color: var(--card-accent);
box-shadow: 0 20px 44px rgba(0, 0, 0, 0.26); box-shadow: 0 20px 44px rgba(0, 0, 0, 0.26);
transform: translateY(-2px); transform: translateY(-5px);
} }
.stack-card__badge { .stack-card__badge {
@@ -70,6 +96,8 @@
.stack-card__title { .stack-card__title {
margin-top: 4px; margin-top: 4px;
color: var(--text); color: var(--text);
font-size: 1.5rem;
line-height: 1;
} }
.stack-card__footer { .stack-card__footer {
@@ -88,6 +116,7 @@
border: 1px solid rgba(220, 215, 186, 0.16); border: 1px solid rgba(220, 215, 186, 0.16);
padding: 4px 8px; padding: 4px 8px;
background-color: rgba(31, 31, 40, 0.4); background-color: rgba(31, 31, 40, 0.4);
font-style: italic;
} }
@media (max-width: 780px) { @media (max-width: 780px) {
+107
View File
@@ -0,0 +1,107 @@
@media (prefers-color-scheme: light) {
:root {
--bg: #f2ecbc;
--surface: #e5ddb0;
--border: #c8c093;
--text: #545464;
--text-soft: #43436c;
--text-muted: #77713f;
--primary: #2f5d95;
--accent: #4e8ca2;
--warm: #cc6d00;
--danger: #c84053;
--glow-blue: rgba(47, 93, 149, 0.14);
--glow-gold: rgba(204, 109, 0, 0.13);
--glow-mint: rgba(78, 140, 162, 0.12);
--glow-purple: rgba(118, 107, 144, 0.14);
--glow-red: rgba(200, 64, 83, 0.13);
--glow-cyan: rgba(58, 143, 183, 0.12);
--glow-green: rgba(111, 137, 78, 0.13);
--glow-pink: rgba(179, 91, 121, 0.12);
--glow-teal: rgba(90, 119, 133, 0.12);
--glow-lime: rgba(119, 113, 63, 0.12);
--glow-rose: rgba(215, 71, 75, 0.13);
}
body {
color-scheme: light;
}
.tone-purple {
--card-accent: #766b90;
}
.tone-cyan {
--card-accent: #3a8fb7;
}
.tone-green {
--card-accent: #6f894e;
}
.tone-pink {
--card-accent: #b35b79;
}
.tone-teal {
--card-accent: #5a7785;
}
.tone-lime {
--card-accent: #77713f;
}
.tone-rose {
--card-accent: #d7474b;
}
.stack-card,
.project-card,
.workstation-card {
background:
linear-gradient(135deg, var(--card-glow), transparent 46%),
linear-gradient(180deg, rgba(247, 241, 197, 0.52), transparent),
var(--surface);
box-shadow: 0 16px 36px rgba(67, 67, 108, 0.1);
}
.stack-card:hover,
.project-card:hover,
.workstation-card:hover {
box-shadow: 0 22px 48px rgba(67, 67, 108, 0.14);
}
.stack-card__badge,
.stack-card__footer span,
.project-card__status,
.project-card__stack span,
.workstation-card__status,
.workstation-card__specs dt,
.workstation-card__chips span,
.link-card {
background-color: rgba(242, 236, 188, 0.58);
border-color: color-mix(in srgb, var(--card-accent) 28%, var(--border));
}
.workstation-card__status--actual {
background-color: var(--card-accent);
color: var(--surface);
}
.workstation-card__status--archive {
background-color: rgba(31, 31, 40, 0.52);
color: var(--surface);
font-style: italic;
}
.link-card:hover {
background-color: color-mix(in srgb, var(--card-glow) 46%, var(--surface));
}
.site-pill,
.site-nav a,
.stack-toggle,
.workstation-toggle {
background-color: rgba(242, 236, 188, 0.42);
}
}
+54
View File
@@ -0,0 +1,54 @@
.tone-blue {
--card-accent: var(--primary);
--card-glow: var(--glow-blue);
}
.tone-gold {
--card-accent: var(--warm);
--card-glow: var(--glow-gold);
}
.tone-mint {
--card-accent: var(--accent);
--card-glow: var(--glow-mint);
}
.tone-purple {
--card-accent: #b876d3;
--card-glow: var(--glow-purple);
}
.tone-red {
--card-accent: var(--danger);
--card-glow: var(--glow-red);
}
.tone-cyan {
--card-accent: #7fb4ca;
--card-glow: var(--glow-cyan);
}
.tone-green {
--card-accent: #98bb6c;
--card-glow: var(--glow-green);
}
.tone-pink {
--card-accent: #d27e99;
--card-glow: var(--glow-pink);
}
.tone-teal {
--card-accent: #6bc4b4;
--card-glow: var(--glow-teal);
}
.tone-lime {
--card-accent: #b8cc64;
--card-glow: var(--glow-lime);
}
.tone-rose {
--card-accent: #e07080;
--card-glow: var(--glow-rose);
}
+180
View File
@@ -0,0 +1,180 @@
.workstation-grid {
display: grid;
gap: 18px;
grid-template-columns: repeat(2, minmax(0, 1fr));
margin-top: 18px;
}
.workstation-toggle {
margin-top: 14px;
min-height: 38px;
padding: 8px 16px;
border: 1px solid var(--border);
border-radius: 8px;
background-color: transparent;
color: var(--text);
cursor: pointer;
font: inherit;
transition:
background-color 160ms ease,
border-color 160ms ease,
color 160ms ease;
}
.workstation-toggle:hover {
border-color: var(--accent);
background-color: var(--glow-mint);
color: var(--text);
}
.workstation-toggle[hidden] {
display: none;
}
.workstation-card {
display: flex;
flex-direction: column;
min-height: 260px;
padding: 22px;
border: 1px solid color-mix(in srgb, var(--card-accent) 36%, var(--border));
border-radius: 8px;
background:
linear-gradient(140deg, var(--card-glow), transparent 48%),
linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent),
var(--surface);
box-shadow: 0 18px 42px rgba(0, 0, 0, 0.2);
transition:
border-color 160ms ease,
transform 160ms ease,
box-shadow 160ms ease;
}
.workstation-card:hover {
border-color: var(--card-accent);
box-shadow: 0 24px 52px rgba(0, 0, 0, 0.28);
transform: translateY(-5px);
}
.workstation-card__header {
display: flex;
align-items: center;
gap: 12px;
}
.workstation-card__index {
color: var(--text-soft);
font-family: var(--fonts-monospace);
font-size: 0.82rem;
}
.workstation-card__key,
.workstation-card__status {
border-radius: 8px;
padding: 4px 8px;
font-family: var(--fonts-monospace);
font-size: 0.82rem;
font-weight: 700;
}
.workstation-card__key {
border: 1px solid color-mix(in srgb, var(--card-accent) 56%, var(--border));
color: var(--card-accent);
text-transform: uppercase;
}
.workstation-card__status {
margin-left: auto;
background-color: rgba(31, 31, 40, 0.52);
color: var(--text);
}
.workstation-card__status--actual {
background-color: var(--card-accent);
color: var(--surface);
}
.workstation-card__status--archive {
background-color: rgba(31, 31, 40, 0.52);
color: var(--text);
font-style: italic;
}
.workstation-card__title {
margin-top: 22px;
color: var(--text);
font-size: clamp(1.6rem, 2.4vw, 1.9rem);
line-height: 1;
}
.workstation-card__specs {
display: grid;
grid-template-columns: 68px minmax(0, 1fr);
gap: 8px 12px;
margin-top: 18px;
font-family: var(--fonts-monospace);
}
.workstation-card__specs dt {
align-self: start;
border: 1px solid color-mix(in srgb, var(--card-accent) 48%, var(--border));
border-radius: 8px;
padding: 4px 7px;
background-color: rgba(31, 31, 40, 0.36);
color: var(--card-accent);
font-size: 0.72rem;
font-weight: 700;
line-height: 1;
text-align: center;
text-transform: uppercase;
}
.workstation-card__specs dd {
align-self: center;
min-width: 0;
color: var(--text);
font-weight: 400;
line-height: 1.4;
}
.workstation-card__group {
margin-top: 20px;
}
.workstation-card__group-title {
color: var(--card-accent);
font-family: var(--fonts-monospace);
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
}
.workstation-card__chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
.workstation-card__chips span {
border-radius: 8px;
border: 1px solid var(--card-glow);
padding: 5px 9px;
background-color: rgba(31, 31, 40, 0.42);
color: var(--card-accent);
font-family: var(--fonts-monospace);
font-size: 0.9rem;
}
@media (max-width: 780px) {
.workstation-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 560px) {
.workstation-card__specs {
grid-template-columns: 58px minmax(0, 1fr);
}
.workstation-card__specs dd {
margin-bottom: 0;
}
}