feat(i18n): translate all case studies and resume to English
- add .en.html overrides for 16 case pages + resume via apinex translator - translate remaining partials (projects-related, mini-projects-*, fab-stack-home-contact, fab-stack-resume, content-edit-fab-stack, cookie-notice) - enhance translate.mjs to protect <script>/<style> from translation - second pass: translate attribute values (aria-label/alt/title/placeholder/content/ data-hint-default) and visible text DeepL skipped in html mode - make logo language-aware (NM / НМ) via i18n
This commit is contained in:
1 parent
20ee81bfaf
commit
4b52cec00d
58 files changed
+14812
-6924
No files matched your search
@@ -8,6 +8,7 @@
|
||||
"skip.home": "Skip to main content",
|
||||
"skip.case": "Skip to case content",
|
||||
"logo.aria": "Home",
|
||||
"logo.text": "NM",
|
||||
"more.sections": "More sections",
|
||||
"back.home": "Home",
|
||||
"footer.credit": "Website design and development — Nikolay Mamadaev",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"skip.home": "К основному содержимому",
|
||||
"skip.case": "К содержимому кейса",
|
||||
"logo.aria": "На главную",
|
||||
"logo.text": "НМ",
|
||||
"more.sections": "Ещё разделы",
|
||||
"back.home": "На главную",
|
||||
"footer.credit": "Дизайн и разработка сайта — Николай Мамадаев",
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="dark" data-radius="medium" data-font="merriweather">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Case Study: AI Assistant—a Figma plugin with AI features."
|
||||
/>
|
||||
<title>AI Assistant — Figma Plugin — Case Study</title>
|
||||
{{> head-links}}
|
||||
{{> head-plyr}}
|
||||
<style>
|
||||
.case-article {
|
||||
max-width: 48rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.case-article h2 {
|
||||
margin: 2rem 0 0.75rem;
|
||||
font-size: clamp(1.15rem, 2vw, 1.35rem);
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article h3 {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article .section__prose {
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article a:not(.btn) {
|
||||
color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var t = JSON.parse(localStorage.getItem("siteThemeV1") || "{}");
|
||||
var d = document.documentElement;
|
||||
var m = t.mode;
|
||||
if (m !== "light" && m !== "dark" && m !== "system") m = "system";
|
||||
var eff =
|
||||
m === "system"
|
||||
? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: m;
|
||||
d.dataset.theme = eff;
|
||||
var h = typeof t.h === "number" ? t.h : typeof t.hue === "number" ? t.hue : null;
|
||||
var s = typeof t.s === "number" ? t.s : typeof t.sat === "number" ? t.sat : null;
|
||||
var l = typeof t.l === "number" ? t.l : typeof t.light === "number" ? t.light : null;
|
||||
if (h !== null) d.style.setProperty("--theme-h", String(h));
|
||||
if (s !== null) d.style.setProperty("--theme-s", String(s));
|
||||
if (l !== null) {
|
||||
var L = Number(l);
|
||||
if (Number.isFinite(L)) d.style.setProperty("--theme-l", String(Math.min(70, Math.max(20, L))));
|
||||
}
|
||||
var rad = t.radius;
|
||||
if (rad === "soft" || rad === "medium" || rad === "strong") {
|
||||
d.dataset.radius = rad;
|
||||
} else {
|
||||
d.dataset.radius = "medium";
|
||||
}
|
||||
var f = t.font;
|
||||
if (f === "onest") f = "geologica";
|
||||
if (f === "anonymous") f = "mono";
|
||||
if (f === "merriweather" || f === "mono" || f === "geologica") {
|
||||
d.dataset.font = f;
|
||||
} else {
|
||||
d.dataset.font = "merriweather";
|
||||
}
|
||||
var mb = t.meshBg;
|
||||
if (mb === "off") {
|
||||
d.dataset.meshBg = "off";
|
||||
} else {
|
||||
d.dataset.meshBg = "on";
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body data-current-case="ai-assistant">
|
||||
{{> header-case}}
|
||||
{{> fab-stack}}
|
||||
<main id="case-main" class="section section--tight">
|
||||
<div class="section__inner case-article">
|
||||
<p class="hero__eyebrow">Figma · Plugin</p>
|
||||
<h1 class="case-page__h1">AI Assistant</h1>
|
||||
<p class="case-page__sub">A Figma plugin with AI features</p>
|
||||
|
||||
<p class="section__prose">
|
||||
<!-- Здесь будет введение -->
|
||||
</p>
|
||||
|
||||
<h2 id="context">Context</h2>
|
||||
<p class="section__prose">
|
||||
<!-- Контекст проекта -->
|
||||
</p>
|
||||
|
||||
<h2 id="task">Task</h2>
|
||||
<p class="section__prose">
|
||||
<!-- Описание задачи -->
|
||||
</p>
|
||||
|
||||
<h2 id="solution">Solution</h2>
|
||||
<p class="section__prose">
|
||||
<!-- Что было сделано -->
|
||||
</p>
|
||||
|
||||
<h2 id="result">Result</h2>
|
||||
<p class="section__prose">
|
||||
<!-- Итоги -->
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{> mini-projects-related}}
|
||||
{{> contact-section}}
|
||||
{{> footer}}
|
||||
{{> cookie-notice}}
|
||||
{{> nav-more-floating}}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded',function(){
|
||||
var s=document.body.dataset.currentCase;
|
||||
if(!s)return;
|
||||
document.querySelectorAll('[data-related-slug="'+s+'"]').forEach(function(el){el.hidden=true;});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,422 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="dark" data-radius="medium" data-font="merriweather">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Case Study: The AI Server Interface in Primo RPA—OCR for Corporate Documents, Model Fine-Tuning, Skills, and UX/UI."
|
||||
/>
|
||||
<title>AI Server in Primo RPA — Case Study (Draft 2)</title>
|
||||
{{> head-links}}
|
||||
{{> head-plyr}}
|
||||
<style>
|
||||
.case-article {
|
||||
max-width: 48rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.case-article h2 {
|
||||
margin: 2rem 0 0.75rem;
|
||||
font-size: clamp(1.15rem, 2vw, 1.35rem);
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article h3 {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article .section__prose {
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article img,
|
||||
.case-article video {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 0.75rem 0 1rem;
|
||||
}
|
||||
.case-article ul,
|
||||
.case-article ol {
|
||||
margin: 0.5rem 0 1rem 1.25rem;
|
||||
padding: 0;
|
||||
}
|
||||
.case-article li {
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
.case-article a:not(.btn) {
|
||||
color: var(--accent);
|
||||
}
|
||||
.case-block {
|
||||
margin-top: 3rem;
|
||||
padding-top: 2.5rem;
|
||||
border-top: 1px solid color-mix(in srgb, var(--text) 14%, transparent);
|
||||
}
|
||||
.case-block:first-of-type {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var t = JSON.parse(localStorage.getItem("siteThemeV1") || "{}");
|
||||
var d = document.documentElement;
|
||||
var m = t.mode;
|
||||
if (m !== "light" && m !== "dark" && m !== "system") m = "system";
|
||||
var eff =
|
||||
m === "system"
|
||||
? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: m;
|
||||
d.dataset.theme = eff;
|
||||
var h = typeof t.h === "number" ? t.h : typeof t.hue === "number" ? t.hue : null;
|
||||
var s = typeof t.s === "number" ? t.s : typeof t.sat === "number" ? t.sat : null;
|
||||
var l = typeof t.l === "number" ? t.l : typeof t.light === "number" ? t.light : null;
|
||||
if (h !== null) d.style.setProperty("--theme-h", String(h));
|
||||
if (s !== null) d.style.setProperty("--theme-s", String(s));
|
||||
if (l !== null) {
|
||||
var L = Number(l);
|
||||
if (Number.isFinite(L)) d.style.setProperty("--theme-l", String(Math.min(70, Math.max(20, L))));
|
||||
}
|
||||
var rad = t.radius;
|
||||
if (rad === "soft" || rad === "medium" || rad === "strong") {
|
||||
d.dataset.radius = rad;
|
||||
} else {
|
||||
d.dataset.radius = "medium";
|
||||
}
|
||||
var f = t.font;
|
||||
if (f === "onest") f = "geologica";
|
||||
if (f === "anonymous") f = "mono";
|
||||
if (f === "merriweather" || f === "mono" || f === "geologica") {
|
||||
d.dataset.font = f;
|
||||
} else {
|
||||
d.dataset.font = "merriweather";
|
||||
}
|
||||
var mb = t.meshBg;
|
||||
if (mb === "off") {
|
||||
d.dataset.meshBg = "off";
|
||||
} else {
|
||||
d.dataset.meshBg = "on";
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
{{> header-case}}
|
||||
{{> fab-stack}}
|
||||
<main id="case-main" class="section section--tight">
|
||||
<div class="section__inner case-article">
|
||||
<p class="hero__eyebrow">Product Design · AI</p>
|
||||
<h1 class="case-page__h1">User Interface Development for the Primo RPA AI Server</h1>
|
||||
<p class="case-page__sub">
|
||||
Intelligent document<br /> processing platform
|
||||
and AI agent orchestration
|
||||
</p>
|
||||
|
||||
<div class="case-block" id="obzor">
|
||||
<h2>Overview</h2>
|
||||
<p class="section__prose">
|
||||
A service was launched within the company that significantly improved the workflow for text recognition models on documents: loading a dataset from company documents, field tagging, model training, and the subsequent use of the model in other products. This approach provided higher recognition quality and met security requirements—data remained within the company’s network. As work on the product progressed, functionality was added to fine-tune the base neural networks for the company’s specific tasks and to work with skills such as summarization, extraction, generation, and other scenarios.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
I was invited to join the project as a UX/UI designer; prior to that, the developers had been building the interface on their own. As a result of this work, the service’s barrier to entry was lowered—significantly more people could use the product without specialized training in AI and OCR.
|
||||
</p>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">✅</span>
|
||||
<div class="case-callout__content">
|
||||
<p>
|
||||
The main benefit for the business wasn’t just a “pretty picture,” but the ability to scale the solution: less reliance on a narrow circle of experts and an implementation team at every step.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="bylo-stalo">
|
||||
<h2>Before and After</h2>
|
||||
<p class="section__prose">
|
||||
The service was built using <a href="https://ant.design/components/overview" target="_blank" rel="noopener noreferrer">Ant Design</a> on top of React with the <a href="https://templates.iqonic.design/hope-ui/html/dist/" target="_blank" rel="noopener noreferrer">Hope UI</a> visual theme for Bootstrap. The company already had several web products and wanted to unify and refresh the user interface, so the development team relied on components from those systems.
|
||||
</p>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji" aria-hidden="true">⚠️</span>
|
||||
<div class="case-callout__content">
|
||||
<p>
|
||||
The main pain point was project configuration: many steps and small tasks, with three types of projects requiring different configuration scenarios. It took a long time to go from creating a project to actually using it, and assistance from the implementation team was often required. Confusing navigation and duplicated functionality reduced productivity and prevented us from reaching a wide audience.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
<p class="section__prose">
|
||||
I immersed myself in the product and, together with the team, analyzed all the functionality in detail.
|
||||
</p>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">✨</span>
|
||||
<div class="case-callout__content">
|
||||
<p>
|
||||
<strong>My experience with AI</strong> and web platforms with similar logic greatly accelerated the work: it was easier to stay in sync with the development team and not get lost in the domain terminology.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
<p class="section__prose">
|
||||
I mapped out user flows based on current scenarios: these showed the team where confusion arose in the interface and user journeys. Based on this, we redesigned the configuration processes themselves: we automated and hid some functions to reduce the risk of errors and eliminate unnecessary routine tasks.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
The new approach changed both the perception of the product and the setup process. The interface guides users through the setup steps without disrupting their usual workflows: a fully linear wizard isn’t feasible here—users still need to go back and refine settings to ensure high-quality results.
|
||||
</p>
|
||||
<h3>Questions for the team regarding the current status</h3>
|
||||
<p class="section__prose">The answers will help quantify the impact of the implementation with facts and figures.</p>
|
||||
<ul>
|
||||
<li>Has the number of support requests decreased?</li>
|
||||
<li>Are users able to complete project setup on their own from start to finish?</li>
|
||||
<li>Does the new interface allow users to create new types of projects based on existing patterns?</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="patterns">
|
||||
<h2>Interface Patterns</h2>
|
||||
<p class="section__prose">
|
||||
Below are the key areas of the product: how they support learning and model-working scenarios, and how they differ from the previous approach.
|
||||
</p>
|
||||
|
||||
<h3>Field Mapping</h3>
|
||||
<p class="section__prose">
|
||||
Mapping links document fields to the target data structure. It was important to make the relationship visual and reversible so that users don’t lose context when making edits.
|
||||
</p>
|
||||
<figure class="case-media" style="margin: 0.85rem 0 1.2rem">
|
||||
<div class="case-video-shell">
|
||||
<video
|
||||
controls
|
||||
preload="metadata"
|
||||
playsinline
|
||||
poster="{{assetPrefix}}assets/ai-platform-primo-rpa/fields-maping.webp"
|
||||
data-case-video-player
|
||||
style="display: block; width: 100%; height: auto; margin: 0"
|
||||
>
|
||||
<source src="{{assetPrefix}}assets/ai-platform-primo-rpa/fields-maping.webm" type="video/webm" />
|
||||
<source src="{{assetPrefix}}assets/ai-platform-primo-rpa/fields-maping.mp4" type="video/mp4" />
|
||||
Your browser does not support video.
|
||||
</video>
|
||||
</div>
|
||||
<figcaption>Field Mapping in the Interface</figcaption>
|
||||
</figure>
|
||||
|
||||
<h3>Dataset, Training, and Quality Control</h3>
|
||||
<p class="section__prose">
|
||||
The dataset editor, training launch, and quality screens form a single mental path: data preparation → training → result verification.
|
||||
</p>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji" aria-hidden="true">➡️</span>
|
||||
<div class="case-callout__content">
|
||||
<p>
|
||||
Below are “before and after” screenshots of the dataset editor—they show how we simplified the visual hierarchy and key actions on the screen.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/dataset-editor.webp"
|
||||
alt="Dataset Editor — Current Version of the Interface"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Dataset Editor—Current Version</figcaption>
|
||||
</figure>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/dataset-editor-old.webp"
|
||||
alt="Dataset Editor — Previous Version for Comparison"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Previous version of the editor</figcaption>
|
||||
</figure>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/dataset-lerning.webp"
|
||||
alt="Model Training Screen Using a Dataset"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Training on a dataset</figcaption>
|
||||
</figure>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/dataset-quality.webp"
|
||||
alt="Quality Control of the Model After Training"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Quality and result verification</figcaption>
|
||||
</figure>
|
||||
|
||||
<h3>Inference in the project</h3>
|
||||
<p class="section__prose">
|
||||
Special Focus: Running and Monitoring the Model Within the Project: The user must understand the status of the inference run and stay in sync with the rest of the settings.
|
||||
</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/in-project-inference.webp"
|
||||
alt="Model inference interface within the project"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Inference in the project</figcaption>
|
||||
</figure>
|
||||
<figure class="case-media" data-case-tall-media data-case-tall-width="wide">
|
||||
<div class="case-media-tall" data-case-tall-root>
|
||||
<div class="case-media-tall__clip" id="ai-case-tall-inference-full" data-case-tall-clip>
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/in-project-inference-full.webp"
|
||||
alt="Inference in the project — full-screen interface"
|
||||
width="1200"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<div class="case-media-tall__fog" aria-hidden="true"></div>
|
||||
</div>
|
||||
<div class="case-media-tall__actions">
|
||||
<button
|
||||
type="button"
|
||||
class="case-media-tall__btn btn btn--ghost btn--flat"
|
||||
data-case-tall-toggle
|
||||
aria-expanded="false"
|
||||
aria-controls="ai-case-tall-inference-full"
|
||||
><span class="case-media-tall__btn-chev" aria-hidden="true"></span>
|
||||
|
||||
<span data-case-tall-label-more>Expand</span>
|
||||
<span data-case-tall-label-less hidden>Collapse</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<figcaption>Full vertical screenshot of the inference screen</figcaption>
|
||||
</figure>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/in-project-inference-old.webp"
|
||||
alt="Previous version of the inference screen in the project"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Previous version of inference</figcaption>
|
||||
</figure>
|
||||
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">⭐</span>
|
||||
<div class="case-callout__content">
|
||||
<p>
|
||||
Full-screen mode and a separate import window isolate tasks from the main project context: you can focus on the process or, conversely, keep the panels handy—see the three layout options below.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<h3>Full-Screen Mode and Import</h3>
|
||||
<p class="section__prose">
|
||||
This isn’t about “long tasks,” but rather about processes in a separate context: some block work on the project until they’re finished, while others run in parallel in a separate window, allowing you to continue configuring project settings. We moved this functionality to a separate window because there were several entry points for these tasks, and a standard modal wasn’t enough—you had to complete a separate process elsewhere and then return to the project settings. This approach reduced the amount of development and testing required.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
Below are three full-screen canvas options: import in a separate window, panel mode, and the minimalist “zen” mode.
|
||||
</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/fullscreen_modal-import_dataset.webp"
|
||||
alt="A modal window for importing a dataset on a separate screen"
|
||||
width="1440"
|
||||
height="1024"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Import in a modal window</figcaption>
|
||||
</figure>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/full-screen-mode-with-panels.webp"
|
||||
alt="Full-screen mode with side panels"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Full-screen mode with panels</figcaption>
|
||||
</figure>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/full-screen-mode-zen.webp"
|
||||
alt="Full-screen mode without distracting panels"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Minimal mode without unnecessary chrome</figcaption>
|
||||
</figure>
|
||||
|
||||
<h3>Future Development</h3>
|
||||
<p class="section__prose">
|
||||
Several major items remained on the roadmap: scenarios related to the LLM (lists, statuses, actions), a step-by-step training wizard with progress tracking and undo functionality, and a full-featured OCR engine with region marking and on-page quality previews. These were not included in the minimum set for the interface’s first production release, but the established patterns should accommodate them without major rework.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="process">
|
||||
<h2>Process and Handoff</h2>
|
||||
<p class="section__prose">
|
||||
Work proceeded through regular synchronization with the development team: mockups, specifications, and a phased handoff to the front end. After I left the project, the team looked for the next designer and structured the onboarding process based on the existing design assets and layout guidelines.
|
||||
</p>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji" aria-hidden="true">💡</span>
|
||||
<div class="case-callout__content">
|
||||
<p>
|
||||
For the next designer, the most important thing is to rely on the user flow and project configuration decisions—that’s where the main complexity of the domain lies.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
<h3>Questions for the Next Designer</h3>
|
||||
<ul>
|
||||
<li>What turned out to be unclear during the first two weeks on the project?</li>
|
||||
<li>Which artifacts and handover guidelines proved most useful?</li>
|
||||
<li>What had to be redesigned and why.</li>
|
||||
<li>How would you describe the current state of UX in three sentences?</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="zaklyuchenie">
|
||||
<h2>Conclusion</h2>
|
||||
<p class="section__prose">
|
||||
The AI Server interface in Primo RPA was transformed from an “engineer-oriented developer interface” into a product with intuitive configuration logic and user guidance. The focus on user flows and routine automation reduced the implementation burden and paved the way for broader adoption within the company and among clients.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{> projects-related}}{{> projects-section}}
|
||||
{{> contact-section}}
|
||||
{{> footer}}
|
||||
{{> cookie-notice}}
|
||||
{{> nav-more-floating}}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,551 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="dark" data-radius="medium" data-font="merriweather">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Case Study: Redesign of the Primo RPA AI Server — Lowering the Barrier to Entry for Customers Without an Implementation Team."
|
||||
/>
|
||||
<title>AI Server in Primo RPA — Case Study</title>
|
||||
{{> head-links}}
|
||||
{{> head-plyr}}
|
||||
<style>
|
||||
.case-article {
|
||||
max-width: 48rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.case-article h2 {
|
||||
margin: 2rem 0 0.75rem;
|
||||
font-size: clamp(1.7rem, 3vw, 2rem);
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article h3 {
|
||||
font-size: 1.55rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article .section__prose {
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.7;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article img,
|
||||
.case-article video {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 0.75rem 0 1rem;
|
||||
}
|
||||
.case-article ul,
|
||||
.case-article ol {
|
||||
margin: 0.5rem 0 1rem 1.25rem;
|
||||
padding: 0;
|
||||
}
|
||||
.case-article li {
|
||||
margin-bottom: 0.35rem;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.case-article a:not(.btn) {
|
||||
color: var(--accent);
|
||||
}
|
||||
.case-meta {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 0.25rem 0.75rem;
|
||||
margin: 1.25rem 0 1.5rem;
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.6;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.case-meta dt {
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-meta dd {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var t = JSON.parse(localStorage.getItem("siteThemeV1") || "{}");
|
||||
var d = document.documentElement;
|
||||
var m = t.mode;
|
||||
if (m !== "light" && m !== "dark" && m !== "system") m = "system";
|
||||
var eff =
|
||||
m === "system"
|
||||
? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: m;
|
||||
d.dataset.theme = eff;
|
||||
var h = typeof t.h === "number" ? t.h : typeof t.hue === "number" ? t.hue : null;
|
||||
var s = typeof t.s === "number" ? t.s : typeof t.sat === "number" ? t.sat : null;
|
||||
var l = typeof t.l === "number" ? t.l : typeof t.light === "number" ? t.light : null;
|
||||
if (h !== null) d.style.setProperty("--theme-h", String(h));
|
||||
if (s !== null) d.style.setProperty("--theme-s", String(s));
|
||||
if (l !== null) {
|
||||
var L = Number(l);
|
||||
if (Number.isFinite(L)) d.style.setProperty("--theme-l", String(Math.min(70, Math.max(20, L))));
|
||||
}
|
||||
var rad = t.radius;
|
||||
if (rad === "soft" || rad === "medium" || rad === "strong") {
|
||||
d.dataset.radius = rad;
|
||||
} else {
|
||||
d.dataset.radius = "medium";
|
||||
}
|
||||
var f = t.font;
|
||||
if (f === "onest") f = "geologica";
|
||||
if (f === "anonymous") f = "mono";
|
||||
if (f === "merriweather" || f === "mono" || f === "geologica") {
|
||||
d.dataset.font = f;
|
||||
} else {
|
||||
d.dataset.font = "merriweather";
|
||||
}
|
||||
var mb = t.meshBg;
|
||||
if (mb === "off") {
|
||||
d.dataset.meshBg = "off";
|
||||
} else {
|
||||
d.dataset.meshBg = "on";
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
{{> header-case}}
|
||||
{{> fab-stack}}
|
||||
<main id="case-main" class="section section--tight">
|
||||
<div class="section__inner case-article">
|
||||
<p class="hero__eyebrow">Product Design · AI</p>
|
||||
<h1 class="case-page__h1">Simplified Primo RPA AI Server setup: customers configure the AI service without an implementation team</h1>
|
||||
<p class="case-page__sub">
|
||||
A platform for intelligent document<br />
|
||||
processing and AI agent orchestration. Requests to the implementation team have dropped by 40%, and the time required to set up the first project has been reduced from ~3 days to ~4 hours.
|
||||
</p>
|
||||
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/main-new.webp"
|
||||
alt="The updated dataset markup editor—the service's main screen following the redesign"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
</figure>
|
||||
|
||||
<dl class="case-meta">
|
||||
<dt>Product</dt>
|
||||
<dd>AI Server. Neural networks within the company’s internal network, configured via a web interface</dd>
|
||||
<dt>Role</dt>
|
||||
<dd>UX/UI Designer (the only one on the project)</dd>
|
||||
<dt>Objective</dt>
|
||||
<dd>Lower the barrier to entry so that the client can configure the service themselves</dd>
|
||||
<dt>Stack</dt>
|
||||
<dd>Ant Design + React, styled with Hope UI</dd>
|
||||
<dt>Format</dt>
|
||||
<dd>Redesign of a live product; releases continued uninterrupted</dd>
|
||||
</dl>
|
||||
|
||||
<div class="case-block" id="problem">
|
||||
<h2>Only experts could handle the configuration. This slowed down sales</h2>
|
||||
<p class="section__prose">
|
||||
The AI Server is deployed on the client’s internal servers. The product was actively tested, and the feedback revealed an unpleasant truth: only experts and the Primo RPA implementation team could configure it. Every new client consumed hours of technical support. The product was losing its appeal and didn’t scale well.
|
||||
</p>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">✅</span>
|
||||
<div class="case-callout__content">
|
||||
<p>
|
||||
Business goal: The client sets up AI Server on their own, without technical support or an implementation team. We measured the success of the redesign based on this criterion.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
<p class="section__prose">
|
||||
I started by getting to know the team and the configuration experts. I divided the scenarios into primary and secondary ones, created a visual representation of user behavior, and worked with the development team to identify areas where the interface could be simplified. I had already worked with AI and similar web platforms, so I quickly got up to speed on the subject matter and was able to speak the same language as the development team.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="constraints">
|
||||
<h2>Constraints</h2>
|
||||
<ul>
|
||||
<li>Customers were already using the product. It wasn’t possible to halt development and completely rebuild the interface.</li>
|
||||
<li>Changes had to be incorporated into the release plan; no one allocated time for a separate “redesign sprint.”</li>
|
||||
<li>Strict tech stack: Ant Design on top of React, styled after Hope UI. We only created new components as a last resort; first, we made the most of the existing library.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="navigation">
|
||||
<h2>Navigation was confusing: project sections were mixed in with the main menu</h2>
|
||||
<p class="section__prose">
|
||||
The service used a layout common to admin panels: a sidebar on the left, content on the right. Space was wasted, and when opening a project, navigation through its sections was intermixed with the main navigation. Users got lost.
|
||||
</p>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji" aria-hidden="true">💡</span>
|
||||
<div class="case-callout__content">
|
||||
<p>
|
||||
Having two navigation systems in a single sidebar forced users to figure out “where they were” every time. I organized them by level: the main navigation at the top, and project-specific navigation within each project.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/main-new.webp"
|
||||
alt="The AI Server home screen with a compact top navigation bar"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>The AI Server home screen with a compact top navigation bar—the list of projects takes up almost the entire width of the screen</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
I moved the main navigation to the top and condensed it. The content now has noticeably more space, while the layout remains familiar. Projects now have their own navigation that doesn’t overlap with the main navigation.
|
||||
</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/nav-project-nlp.webp"
|
||||
alt="Internal Navigation of the NLP Project"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Internal navigation for the NLP project—horizontal step-by-step items below the main menu</figcaption>
|
||||
</figure>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/nav-project-ocr.webp"
|
||||
alt="Internal Navigation for the OCR Project"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Internal navigation for the OCR project—the same pattern with its own set of sections</figcaption>
|
||||
</figure>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">✨</span>
|
||||
<div class="case-callout__content">
|
||||
<p>
|
||||
The arrow-shaped icons suggest a configuration order but do not enforce it: the sections remain standard tabs. A suggestion, not a requirement.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/nav-secondary-detail_view.webp"
|
||||
alt="Close-up of secondary navigation—arrow-shaped markers"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Close-up of secondary navigation—items in the form of arrows pointing from left to right</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="editor">
|
||||
<h2>Dataset Editor: the screen where users spend most of their time</h2>
|
||||
<p class="section__prose">
|
||||
This is where users spend most of their time. Annotating fields on hundreds of images is monotonous work, but the quality of the data used to train the model depends on it. The previous editor didn’t fit the overall style, looked cluttered, and made working with fields confusing.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
I broke the editor down into microscenarios, organized them by priority and interdependencies. Together with the team, we designed a smoother and more intuitive user experience, reserved space for future features, and aligned the design with the service’s overall style.
|
||||
</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/dataset-editor.webp"
|
||||
alt="Dataset Markup Editor — the document being marked up in the center, with field and tool panels on either side"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Dataset markup editor—the document being marked up is in the center, with field and tool panels on either side</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
The page became symmetrical. Almost all the space is devoted to the image being annotated, and actions are grouped by panels: the desired function can be found quickly.
|
||||
</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/dataset_editor-wireframe-default_mode.webp"
|
||||
alt="A schematic overview of the editor's layout—the central workspace and side panels"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Schematic layout of the editor—central workspace and side panels, default mode</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
For advanced users, I’ve added a “Full Screen” mode. The side panels collapse, and you work using keyboard shortcuts and the bottom panel. This is a lifesaver on small monitors.
|
||||
</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/full-screen-mode-with-panels.webp"
|
||||
alt="Full-screen editor mode with side panels open"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Full-screen editor mode with side panels expanded</figcaption>
|
||||
</figure>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/full-screen-mode-zen.webp"
|
||||
alt="Full-screen mode without panels—only the document being marked up and the bottom panel"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Full-screen mode without panels—only the document being edited and the bottom toolbar are visible on the screen</figcaption>
|
||||
</figure>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/dataset_editor-wireframe-zen_mode-both_panels.webp"
|
||||
alt="Four Full-Screen Mode Frame Options"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Four full-screen layout options: both panels, no panels, left panel only, right panel only</figcaption>
|
||||
</figure>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/dataset_editor-main_toolbar.webp"
|
||||
alt="Bottom toolbar — switch between images and quick markup actions"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Bottom toolbar—switch between images and quick markup actions</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="mapping">
|
||||
<h2>Dozens of fields on a single scan, without sacrificing document space</h2>
|
||||
<p class="section__prose">
|
||||
Related task: Configure unique field settings for the dataset. A large area is needed for the document scan, with dozens of fields, each having its own types and dependent settings.
|
||||
</p>
|
||||
<figure class="case-media" style="margin: 0.85rem 0 1.2rem">
|
||||
<div class="case-video-shell">
|
||||
<video
|
||||
controls
|
||||
preload="metadata"
|
||||
playsinline
|
||||
poster="{{assetPrefix}}assets/ai-platform-primo-rpa/fields-maping.webp"
|
||||
data-case-video-player
|
||||
style="display: block; width: 100%; height: auto; margin: 0"
|
||||
>
|
||||
<source src="{{assetPrefix}}assets/ai-platform-primo-rpa/fields-maping.webm" type="video/webm" />
|
||||
<source src="{{assetPrefix}}assets/ai-platform-primo-rpa/fields-maping.mp4" type="video/mp4" />
|
||||
Your browser does not support video.
|
||||
</video>
|
||||
</div>
|
||||
<figcaption>Field mapping prototype—the user selects a field on the document scan and configures its parameters in a compact panel</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
The interface turned out to be compact. The mechanics aren’t immediately obvious at first glance, but after a short learning curve, working with it becomes quick and comfortable.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="learning">
|
||||
<h2>Training cards: a pattern that has spread throughout the product</h2>
|
||||
<p class="section__prose">
|
||||
After annotating the dataset, the model undergoes fine-tuning. We analyzed all user actions and explored several page layouts. We settled on a card feed.
|
||||
</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/in_project-learn-overview.webp"
|
||||
alt="Model Training Section — Vertical Card Strip"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>The model training section is a vertical card feed, with a new training setup card at the top</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
At the beginning of the feed is a configuration card: the annotation results for the active dataset, parameters, and the option to start fine-tuning.
|
||||
</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/in_project-learn-card-new.webp"
|
||||
alt="Training Configuration Card — Dataset Quality Matrix and Run Parameters Block"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Training configuration card—dataset quality matrix and launch parameters section</figcaption>
|
||||
</figure>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">⭐</span>
|
||||
<div class="case-callout__content">
|
||||
<p>
|
||||
The annotation quality matrix shows the dataset’s readiness for training on a single screen. Previously, we had to manually review fields across all documents.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
<p class="section__prose">
|
||||
A separate card appears for each running process. You can start multiple processes with different parameters and compare which configuration performed better.
|
||||
</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/in_project-learn-card-learning_process.webp"
|
||||
alt="Active Learning Process Card — Status, Progress, and Metrics"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Card for a running training process—status, progress, and metrics organized into logical blocks</figcaption>
|
||||
</figure>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/in_project-learn-card-learning_process_wireframe.webp"
|
||||
alt="Learning Process Card Template"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>The framework of a training process card—a layout of information blocks and actions</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
Cards can be collapsed; the title and status remain visible in the header.
|
||||
</p>
|
||||
<figure class="case-media case-media--natural">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/in_project-learn-card-сollapsed.webp"
|
||||
alt="A collapsed training card—a single line showing the name and status of the process"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<figcaption>Collapsed training card—a single line showing the process name and status</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
Patterns from these cards were later applied to other sections of the product. It became easier to maintain a consistent style.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="processes">
|
||||
<h2>Long processes—in a separate tab</h2>
|
||||
<p class="section__prose">
|
||||
Some tasks run in parallel with other work. For such scenarios, a modal window opens on a separate page.
|
||||
</p>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji" aria-hidden="true">💡</span>
|
||||
<div class="case-callout__content">
|
||||
<p>
|
||||
The user keeps the process window open in a neighboring tab to monitor progress while continuing to work on the project in the available sections.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/fullscreen_modal-import_dataset.webp"
|
||||
alt="Importing a dataset in a modal window on a separate page"
|
||||
width="1440"
|
||||
height="1024"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Importing a dataset in a modal window on a separate page—a full-screen upload form</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
Meanwhile, a placeholder with a button to navigate to the process remains on the project page.
|
||||
</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/fullscreen_modal-in_project-import_dataset.webp"
|
||||
alt="Active Import Placeholder Within a Project"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Placeholder for active import within the project—progress indicator and "Go to Process" button</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="decisions">
|
||||
<h2>What I Decided Not to Do, and Why</h2>
|
||||
<ul>
|
||||
<li><strong>I chose not to disable buttons on individual pages</strong> during active processes. A modal on a separate page is technically simpler, and progress is visible in the adjacent tab.</li>
|
||||
<li><strong>I didn’t create new components</strong> except in extreme cases. The ready-made Ant Design library sped up development and kept the interface consistent.</li>
|
||||
<li><strong>I didn’t remove the editor’s side panels entirely</strong>; instead, I made them collapsible. Beginners need on-screen hints, while advanced users need as much space as possible for their document.</li>
|
||||
<li><strong>I didn’t set up a separate design system.</strong> The product already had a framework, so I passed links to the existing user flows in Figma to the development team.</li>
|
||||
</ul>
|
||||
<h3>Handoff to Development</h3>
|
||||
<p class="section__prose">
|
||||
Once a week, we discussed the design with the team: the task list, priorities, and deadlines aligned with release plans. I went over the details of the mockups one-on-one with the front-end developer and provided feedback after the layout was completed to ensure the result matched the intended behavior.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="results">
|
||||
<h2>Result</h2>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">📈</span>
|
||||
<div class="case-callout__content">
|
||||
<p>
|
||||
Setup for the first project: it used to take ~3 days, now it takes ~4 hours. 7 out of 10 customers complete the setup without technical support.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
<ul>
|
||||
<li>Requests to the implementation team during the setup phase decreased by <strong>40%.</strong> Clients complete the basic setup on their own.</li>
|
||||
<li><strong>7 out of 10</strong> new clients set up their first project without technical support. Before the redesign, it was 1 out of 10.</li>
|
||||
<li>Setting up the first project: from <strong>~3 days to ~4 hours</strong>.</li>
|
||||
<li><strong>Over 30 screens</strong> were redesigned: navigation, dataset editor, model training, and data import.</li>
|
||||
<li>We established design patterns that allow functionality to be expanded without major rework.</li>
|
||||
<li>The company hired a second designer. I brought her up to speed and oversaw the results; after she became familiar with the project, it was entirely transferred to her responsibility.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="conclusions">
|
||||
<h2>Conclusions</h2>
|
||||
<ul>
|
||||
<li>The rigid framework turned out to be an accelerator rather than a limitation: the ready-made component library allowed us to focus on scenarios rather than pixels.</li>
|
||||
<li>The most valuable insights came from configuration experts. Next time, I would involve the implementation team even earlier.</li>
|
||||
<li>Handing the project over to a new designer confirmed that the established patterns work and can be explained and maintained without the original author.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{> projects-related}}{{> projects-section}}
|
||||
{{> contact-section}}
|
||||
{{> footer}}
|
||||
{{> cookie-notice}}
|
||||
{{> nav-more-floating}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,593 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="dark" data-radius="medium" data-font="merriweather">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Case Study: The AI Server Interface in Primo RPA—OCR for Corporate Documents, Model Fine-Tuning, Skills, and UX/UI."
|
||||
/>
|
||||
<title>AI Server in Primo RPA — Case Study</title>
|
||||
{{> head-links}}
|
||||
{{> head-plyr}}
|
||||
<style>
|
||||
.case-article {
|
||||
max-width: 48rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.case-article h2 {
|
||||
margin: 2rem 0 0.75rem;
|
||||
font-size: clamp(1.15rem, 2vw, 1.35rem);
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article h3 {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article .section__prose {
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article img,
|
||||
.case-article video {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 0.75rem 0 1rem;
|
||||
}
|
||||
.case-article ul,
|
||||
.case-article ol {
|
||||
margin: 0.5rem 0 1rem 1.25rem;
|
||||
padding: 0;
|
||||
}
|
||||
.case-article li {
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
.case-article a:not(.btn) {
|
||||
color: var(--accent);
|
||||
}
|
||||
.case-block {
|
||||
margin-top: 3rem;
|
||||
padding-top: 2.5rem;
|
||||
border-top: 1px solid color-mix(in srgb, var(--text) 14%, transparent);
|
||||
}
|
||||
.case-block:first-of-type {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var t = JSON.parse(localStorage.getItem("siteThemeV1") || "{}");
|
||||
var d = document.documentElement;
|
||||
var m = t.mode;
|
||||
if (m !== "light" && m !== "dark" && m !== "system") m = "system";
|
||||
var eff =
|
||||
m === "system"
|
||||
? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: m;
|
||||
d.dataset.theme = eff;
|
||||
var h = typeof t.h === "number" ? t.h : typeof t.hue === "number" ? t.hue : null;
|
||||
var s = typeof t.s === "number" ? t.s : typeof t.sat === "number" ? t.sat : null;
|
||||
var l = typeof t.l === "number" ? t.l : typeof t.light === "number" ? t.light : null;
|
||||
if (h !== null) d.style.setProperty("--theme-h", String(h));
|
||||
if (s !== null) d.style.setProperty("--theme-s", String(s));
|
||||
if (l !== null) {
|
||||
var L = Number(l);
|
||||
if (Number.isFinite(L)) d.style.setProperty("--theme-l", String(Math.min(70, Math.max(20, L))));
|
||||
}
|
||||
var rad = t.radius;
|
||||
if (rad === "soft" || rad === "medium" || rad === "strong") {
|
||||
d.dataset.radius = rad;
|
||||
} else {
|
||||
d.dataset.radius = "medium";
|
||||
}
|
||||
var f = t.font;
|
||||
if (f === "onest") f = "geologica";
|
||||
if (f === "anonymous") f = "mono";
|
||||
if (f === "merriweather" || f === "mono" || f === "geologica") {
|
||||
d.dataset.font = f;
|
||||
} else {
|
||||
d.dataset.font = "merriweather";
|
||||
}
|
||||
var mb = t.meshBg;
|
||||
if (mb === "off") {
|
||||
d.dataset.meshBg = "off";
|
||||
} else {
|
||||
d.dataset.meshBg = "on";
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body data-current-case="ai-platform-primo-rpa">
|
||||
{{> header-case}}
|
||||
{{> fab-stack}}
|
||||
<main id="case-main" class="section section--tight">
|
||||
<div class="section__inner case-article">
|
||||
<p class="hero__eyebrow">Product Design · AI</p>
|
||||
<h1 class="case-page__h1">User Interface Development for the Primo RPA AI Server</h1>
|
||||
<p class="case-page__sub">
|
||||
Intelligent document<br /> processing platform
|
||||
and AI agent orchestration
|
||||
</p>
|
||||
|
||||
<div class="case-block" id="obzor">
|
||||
<h2>Overview</h2>
|
||||
<p class="section__prose">
|
||||
The AI Server product is deployed on clients’ internal servers. It enables the use of neural networks within the company’s internal network and is configured via a web interface.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
I was invited to join the project as a UX/UI designer. The product was in the active testing phase; feedback from customers made it clear that the configuration was too complex: only experts and the Primo RPA implementation team could handle it. The complex configuration reduced the product’s appeal and hindered scaling.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
My task was to simplify the configuration process and lower the barrier to entry, thereby reducing the workload on technical support and the implementation team—so that customers could configure the service themselves to meet their specific needs.
|
||||
</p>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">✅</span>
|
||||
<div class="case-callout__content">
|
||||
<p>
|
||||
The business set a goal to simplify AI Server configuration and make the product more accessible to customers. I was tasked with creating an interface that would allow users to complete the setup without constant assistance from support or the implementation team.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
<p class="section__prose">
|
||||
Below is an in-depth look at the product, examples of completed tasks, and interface patterns.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="pogruzhenie">
|
||||
<h2>Overview</h2>
|
||||
<p class="section__prose">
|
||||
I began by getting to know the team: who was responsible for the product, who was responsible for implementation, and how everything was currently organized. I spoke with configuration experts, categorized key and secondary scenarios, and compiled a visualization of user behavior. I worked closely with the development team to identify areas where the interface could be simplified and the setup process accelerated.
|
||||
</p>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">✨</span>
|
||||
<div class="case-callout__content">
|
||||
<p>
|
||||
My experience in AI and similar web platforms helped me quickly get a handle on the domain and speak the same language as the development team.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
<p class="section__prose">
|
||||
The product was already in use by customers, so we couldn’t halt development and completely rebuild the interface. I had to decide in what order and which parts to address in the redesign. I incorporated the most significant changes into the current development plan without disrupting the release schedule.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
I had to plan the interface changes with the framework in mind. At the time, it was
|
||||
Ant<a href="https://ant.design/components/overview" target="_blank" rel="noopener noreferrer"> Design</a> on top of React, styled after
|
||||
Hope<a href="https://templates.iqonic.design/hope-ui/html/dist/" target="_blank" rel="noopener noreferrer"> UI</a>.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
I only had to create new components as a last resort; the priority was to reuse the existing library. This is evident in the final screens: the interface is mostly built from ready-made blocks, rather than from scratch.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="zadachi">
|
||||
<h2>Completed Tasks</h2>
|
||||
<p class="section__prose">
|
||||
Interface elements that I redesigned or built almost from scratch. Below are individual tasks from the project that I wanted to showcase in my portfolio—not an overview of the entire product.
|
||||
</p>
|
||||
|
||||
<h3>Main navigation of the service</h3>
|
||||
<p class="section__prose">
|
||||
The service used a layout common for admin panels: a side menu on the left and content on the right. The screenshot shows that this page structure didn’t make effective use of the available screen space. When you open the project, the navigation for the project sections appears on the left—intermixed with the main navigation—which confuses the user. This situation needed to be fixed.
|
||||
</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/main.webp"
|
||||
alt="Previous main navigation page for the AI Server service"
|
||||
width="2520"
|
||||
height="1680"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Previous Navigation</figcaption>
|
||||
</figure>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/main-new.webp"
|
||||
alt="Updated Main Navigation for the AI Server Service"
|
||||
width="1920"
|
||||
height="1080"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Updated navigation</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
The updated navigation is now more compact and remains familiar to most users. There is significantly more space for content. As you’ll see later, projects will have their own separate navigation that won’t overlap with the main navigation. At the same time, the service’s overall aesthetic has improved.
|
||||
</p>
|
||||
<h3>Navigation Within a Specific Project</h3>
|
||||
<p class="section__prose">
|
||||
Users retain access to both the navigation within a project and the service’s main navigation. Different project types contain varying numbers of items, and this solution allows for their optimal placement.
|
||||
</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/nav-project-nlp.webp"
|
||||
alt="Navigation within the NLP project and the service's main navigation"
|
||||
width="1920"
|
||||
height="302"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>NLP Project</figcaption>
|
||||
</figure>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/nav-project-ocr.webp"
|
||||
alt="Navigation within an OCR project and the service's main navigation"
|
||||
width="1920"
|
||||
height="302"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>OCR project</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
I’d like to highlight the design of the navigation items. Visually, it subtly suggests that it’s more convenient to go through the setup step by step, but the sections still function as regular tabs: you can switch between them in any order.
|
||||
</p>
|
||||
<figure class="case-media">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/nav-secondary-detail_view.webp"
|
||||
alt="Secondary navigation for the project with arrow-shaped items"
|
||||
width="924"
|
||||
height="101"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Shape of navigation items</figcaption>
|
||||
</figure>
|
||||
<h3>Dataset Annotation Editor</h3>
|
||||
<p class="section__prose">
|
||||
This is the screen where users spend the most time. Annotating fields across hundreds of images is monotonous work, but the quality of the data used to train the model depends on it. The previous editor stood out sharply from the service’s overall style: it looked cluttered and was confusing in places, especially when working with fields.
|
||||
</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/dataset-editor-old.webp"
|
||||
alt="Dataset Editor — Previous Version for Comparison"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Previous version of the dataset editor</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
I broke down the editor into microscenarios, organized them by priority and how they relate to each other. The team and I discussed how to make the user experience smoother and more intuitive. In the new page structure, I reserved space for new functionality and incorporated the same design patterns used throughout the rest of the service. The editor’s design now aligns with the overall style.
|
||||
</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/dataset-editor.webp"
|
||||
alt="Dataset Editor — Current Version of the Interface"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Updated version of the dataset editor</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
The schematic sketch shows that the page has become symmetrical: almost all the space is devoted to the image being marked up. Actions are grouped by panels—making it easier to find the desired function.
|
||||
</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/dataset_editor-wireframe-default_mode.webp"
|
||||
alt="Dataset Editor Wireframe — Default Mode"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Frame: Default mode</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
Added a “Full Screen” mode. Advanced users usually hide unnecessary panels and work with markup using keyboard shortcuts and the bottom panel. The side panels can be collapsed when more space is needed for the image—this is especially important on smaller monitors.
|
||||
</p>
|
||||
<figure class="case-media case-media-gallery case-media-gallery--wide" style="--case-gallery-cols: 2">
|
||||
<div class="case-media-gallery__grid">
|
||||
<div class="case-media-gallery__item">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/full-screen-mode-with-panels.webp"
|
||||
alt="Full-screen mode with side panels"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/full-screen-mode-zen.webp"
|
||||
alt="Full-screen mode without distracting panels"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<figcaption>Editor view in “Full Screen” mode</figcaption>
|
||||
</figure>
|
||||
<figure class="case-media case-media-gallery case-media-gallery--wide" style="--case-gallery-cols: 2">
|
||||
<div class="case-media-gallery__grid">
|
||||
<div class="case-media-gallery__item">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/dataset_editor-wireframe-zen_mode-both_panels.webp"
|
||||
alt="Wireframe zen mode — both panels"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/dataset_editor-wireframe-zen_mode.webp"
|
||||
alt="Wireframe Zen Mode"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/dataset_editor-wireframe-zen_mode-left_panel.webp"
|
||||
alt="Wireframe zen mode — left panel"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/dataset_editor-wireframe-zen_mode-right_panel.webp"
|
||||
alt="Wireframe zen mode — right panel"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<figcaption>Layout options in "Full Screen" mode</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
The bottom toolbar lets you switch between images and perform quick actions.
|
||||
</p>
|
||||
<figure class="case-media case-media--content">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/dataset_editor-main_toolbar.webp"
|
||||
alt="The Dataset Editor's Main Toolbar"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Bottom toolbar</figcaption>
|
||||
</figure>
|
||||
|
||||
<h3>Model training configuration cards</h3>
|
||||
<p class="section__prose">
|
||||
After labeling the dataset, the model can be fine-tuned. As usual, our team broke down all user actions for this task. We created several layout options for this page; the one with a card feed turned out to be the most successful.
|
||||
</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/in_project-learn-overview.webp"
|
||||
alt="Overview of Training Configuration Cards in the Project"
|
||||
width="1920"
|
||||
height="740"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>View of the Model Training section</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
At the top of the feed, the user sees a training configuration card: it displays the annotation results for the active dataset, allows the user to set parameters, and initiates fine-tuning.
|
||||
</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/in_project-learn-card-new.webp"
|
||||
alt="Training Configuration Card"
|
||||
width="1306"
|
||||
height="1104"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Training Configuration Card</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
The card includes a separate section with an assessment of the dataset’s annotation quality and a section with parameters to configure before starting training.
|
||||
</p>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">⭐</span>
|
||||
<div class="case-callout__content">
|
||||
<p>
|
||||
The dataset quality assessment matrix turned out to be both intuitive and compact.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
<p class="section__prose">
|
||||
A separate card will appear below for each process that is started. You can run multiple processes with different parameters and then compare which configuration performed better.
|
||||
</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/in_project-learn-card-learning_process.webp"
|
||||
alt="Model Learning Process Card"
|
||||
width="1306"
|
||||
height="1066"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Training Process Card</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
Within the card, information and actions are organized into logical blocks. We considered various layout options and ultimately settled on a scheme that proved to be more user-friendly than the others.
|
||||
</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/in_project-learn-card-learning_process_wireframe.webp"
|
||||
alt="Learning Process Card Wireframe"
|
||||
width="1306"
|
||||
height="1066"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Wireframe of the Learning Process Card</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
The cards can be collapsed. For convenience, the name and status are displayed in the header.
|
||||
</p>
|
||||
<figure class="case-media case-media--content">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/in_project-learn-card-сollapsed.webp"
|
||||
alt="Collapsed Training Settings Card"
|
||||
width="1306"
|
||||
height="102"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Collapsed Card</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
After refining these cards, we identified patterns that were later used in other sections of the product. This makes it easier to maintain a consistent style across the interface.
|
||||
</p>
|
||||
|
||||
<h3>Field Parameter Configuration</h3>
|
||||
<p class="section__prose">
|
||||
On one of the pages in the project section, we needed to allow users to configure unique field parameters for a dataset. The editor required a large area for an image (a scanned document), and there could be dozens of fields, each with its own dependent parameters and types.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
The video shows the prototype of the finished solution in action.
|
||||
</p>
|
||||
<figure class="case-media" style="margin: 0.85rem 0 1.2rem">
|
||||
<div class="case-video-shell">
|
||||
<video
|
||||
controls
|
||||
preload="metadata"
|
||||
playsinline
|
||||
poster="{{assetPrefix}}assets/ai-platform-primo-rpa/fields-maping.webp"
|
||||
data-case-video-player
|
||||
style="display: block; width: 100%; height: auto; margin: 0"
|
||||
>
|
||||
<source src="{{assetPrefix}}assets/ai-platform-primo-rpa/fields-maping.webm" type="video/webm" />
|
||||
<source src="{{assetPrefix}}assets/ai-platform-primo-rpa/fields-maping.mp4" type="video/mp4" />
|
||||
Your browser does not support video.
|
||||
</video>
|
||||
</div>
|
||||
<figcaption>Field mapping in the interface</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
The interface turned out to be compact and clean. The mechanics aren’t immediately obvious, but after a brief learning curve, users find it comfortable to use.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
Developing the editor proved to be an interesting and challenging task for me: there were many drafts and different behavior options. In the end, we settled on a version that met all the requirements my team and I had defined.
|
||||
</p>
|
||||
|
||||
<h3>Modal windows on a separate screen</h3>
|
||||
<p class="section__prose">
|
||||
In the service, some tasks need to be performed alongside other work. For such scenarios, we implemented the following: a modal window opens on a separate page.
|
||||
</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/fullscreen_modal-import_dataset.webp"
|
||||
alt="A modal window for importing a dataset on a separate screen"
|
||||
width="1440"
|
||||
height="1024"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Importing a dataset in a modal window</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
A placeholder is displayed on the project page: the process is in progress, with a button next to it to navigate to it.
|
||||
</p>
|
||||
<figure class="case-media case-media--content">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/fullscreen_modal-in_project-import_dataset.webp"
|
||||
alt="Importing a dataset within a project"
|
||||
width="1920"
|
||||
height="740"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Importing a dataset within a project</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
These placeholders appear in several sections of the project—depending on whether you can work with that section while the task is active.
|
||||
</p>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">💡</span>
|
||||
<div class="case-callout__content">
|
||||
<p>
|
||||
The user can open the window showing the process in an adjacent tab and monitor its progress. In another tab or window, they can continue working on the project in the available sections.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
<p class="section__prose">
|
||||
This approach turned out to be technically simpler than selectively disabling buttons on individual pages.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="process">
|
||||
<h2>Handoff to Development</h2>
|
||||
<p class="section__prose">
|
||||
Once a week, the team met to discuss the design. The calls were long but productive. We created a weekly list of design tasks, adjusting priorities and deadlines to align with release plans.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
I created the designs in Figma and sent links to the finished user flows to the development team. We didn’t set up a separate design system or UI kit—the product already had a ready-made framework.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
From time to time, I would have one-on-one calls with the front-end developer to clarify details about the mockups or explain how the interface should behave. After the layout was completed, I provided feedback on revisions to ensure the result matched the intended behavior and appearance of the interface.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="smena-dizaynera">
|
||||
<h2>Change in Designers</h2>
|
||||
<p class="section__prose">
|
||||
I managed this project alongside others, so I had to balance my workload. The company hired a new designer—my task was to bring her up to speed and oversee the results of her work on each task. Once she was up to speed, responsibility for the project shifted entirely to her.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="zaklyuchenie">
|
||||
<h2>Result</h2>
|
||||
<p class="section__prose">
|
||||
The Primo RPA AI Server interface now has a consistent look and feel. It is user-friendly even for those who are not yet experts in AI. The interface is designed to accommodate future changes, allowing for the addition of new features without major overhauls.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{> projects-related}}{{> projects-section}}
|
||||
{{> contact-section}}
|
||||
{{> footer}}
|
||||
{{> cookie-notice}}
|
||||
{{> nav-more-floating}}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded',function(){
|
||||
var s=document.body.dataset.currentCase;
|
||||
if(!s)return;
|
||||
document.querySelectorAll('[data-related-slug="'+s+'"]').forEach(function(el){el.hidden=true;});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,529 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="dark" data-radius="medium" data-font="merriweather">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Case Study: Card Columns, a view plugin for Obsidian Bases. It displays notes as cards arranged in columns, with tag filtering using AND/OR."
|
||||
/>
|
||||
<title>Card Columns, a plugin for Obsidian Bases: a case study</title>
|
||||
{{> head-links}}
|
||||
{{> head-plyr}}
|
||||
<style>
|
||||
.case-article {
|
||||
max-width: 48rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.case-article h2 {
|
||||
margin: 2rem 0 0.75rem;
|
||||
font-size: clamp(1.15rem, 2vw, 1.35rem);
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article h3 {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article .section__prose {
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article img,
|
||||
.case-article video {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 0.75rem 0 1rem;
|
||||
}
|
||||
.case-article ul,
|
||||
.case-article ol {
|
||||
margin: 0.5rem 0 1rem 1.25rem;
|
||||
padding: 0;
|
||||
}
|
||||
.case-article li {
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
.case-article a:not(.btn) {
|
||||
color: var(--accent);
|
||||
}
|
||||
.case-article .case-brief {
|
||||
list-style: none;
|
||||
margin: 1.5rem 0 0;
|
||||
padding: 1.1rem 1.25rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
|
||||
gap: 0.85rem 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
border: 1px solid color-mix(in srgb, var(--text) 14%, transparent);
|
||||
border-radius: var(--radius-md, 12px);
|
||||
}
|
||||
.case-article .case-brief li {
|
||||
margin: 0;
|
||||
}
|
||||
.case-article .case-brief span {
|
||||
display: block;
|
||||
margin-bottom: 0.15rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.case-article .case-todo {
|
||||
margin: 1rem 0 1.25rem;
|
||||
padding: 0.9rem 1.1rem;
|
||||
border-left: 2px solid var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 8%, transparent);
|
||||
border-radius: 0 var(--radius-sm, 8px) var(--radius-sm, 8px) 0;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
.case-article .case-todo p {
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
.case-article .case-todo p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.case-article .case-todo__q {
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
}
|
||||
.case-todo__label {
|
||||
display: block;
|
||||
margin-bottom: 0.35rem;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.case-article details.case-details {
|
||||
margin: 0.75rem 0 1rem;
|
||||
padding: 0.6rem 0.9rem;
|
||||
border: 1px solid color-mix(in srgb, var(--text) 14%, transparent);
|
||||
border-radius: var(--radius-sm, 8px);
|
||||
}
|
||||
.case-article details.case-details > summary {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
.case-article details.case-details[open] > summary {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.case-block {
|
||||
margin-top: 3rem;
|
||||
padding-top: 2.5rem;
|
||||
border-top: 1px solid color-mix(in srgb, var(--text) 14%, transparent);
|
||||
}
|
||||
.case-block:first-of-type {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var t = JSON.parse(localStorage.getItem("siteThemeV1") || "{}");
|
||||
var d = document.documentElement;
|
||||
var m = t.mode;
|
||||
if (m !== "light" && m !== "dark" && m !== "system") m = "system";
|
||||
var eff =
|
||||
m === "system"
|
||||
? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: m;
|
||||
d.dataset.theme = eff;
|
||||
var h = typeof t.h === "number" ? t.h : typeof t.hue === "number" ? t.hue : null;
|
||||
var s = typeof t.s === "number" ? t.s : typeof t.sat === "number" ? t.sat : null;
|
||||
var l = typeof t.l === "number" ? t.l : typeof t.light === "number" ? t.light : null;
|
||||
if (h !== null) d.style.setProperty("--theme-h", String(h));
|
||||
if (s !== null) d.style.setProperty("--theme-s", String(s));
|
||||
if (l !== null) {
|
||||
var L = Number(l);
|
||||
if (Number.isFinite(L)) d.style.setProperty("--theme-l", String(Math.min(70, Math.max(20, L))));
|
||||
}
|
||||
var rad = t.radius;
|
||||
if (rad === "soft" || rad === "medium" || rad === "strong") {
|
||||
d.dataset.radius = rad;
|
||||
} else {
|
||||
d.dataset.radius = "medium";
|
||||
}
|
||||
var f = t.font;
|
||||
if (f === "onest") f = "geologica";
|
||||
if (f === "anonymous") f = "mono";
|
||||
if (f === "merriweather" || f === "mono" || f === "geologica") {
|
||||
d.dataset.font = f;
|
||||
} else {
|
||||
d.dataset.font = "merriweather";
|
||||
}
|
||||
var mb = t.meshBg;
|
||||
if (mb === "off") {
|
||||
d.dataset.meshBg = "off";
|
||||
} else {
|
||||
d.dataset.meshBg = "on";
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body data-current-case="card-columns">
|
||||
{{> header-case}}
|
||||
{{> fab-stack}}
|
||||
<main id="case-main" class="section section--tight">
|
||||
<div class="section__inner case-article">
|
||||
<p class="hero__eyebrow">Obsidian Bases · Plugin</p>
|
||||
<h1 class="case-page__h1">
|
||||
A Kanban view for Obsidian Bases<br />
|
||||
that leaves your notes untouched
|
||||
</h1>
|
||||
<p class="case-page__sub">
|
||||
Card Columns: Notes as cards in columns,<br />
|
||||
filtered by AND/OR tags with just two clicks
|
||||
</p>
|
||||
|
||||
<!-- МЕДИА: hero-спойлер, финальный экран плагина. Раскомментировать при вставке скрина.
|
||||
<figure class="case-media case-media--wide">
|
||||
<img src="../assets/card-columns-obsidian/hero.webp" alt="Card Columns: card columns on top of the .base file" width="1920" height="1080" loading="eager" decoding="async" data-image-viewer />
|
||||
<figcaption>"Columns" view on top of the note database</figcaption>
|
||||
</figure>
|
||||
-->
|
||||
|
||||
<ul class="case-brief">
|
||||
<li>Role: Product Design and Development</li>
|
||||
<li><span>Platform:</span> Obsidian Bases</li>
|
||||
<li><span>Status</span>: Working plugin, open source (MIT)</li>
|
||||
<li><span>Team: Solo</span>, side project</li>
|
||||
<li><span>Status:</span> Draft response, to be replaced</li>
|
||||
</ul>
|
||||
|
||||
<div class="case-todo">
|
||||
<span class="case-todo__label">Question for the author</span>
|
||||
<p class="case-todo__q">
|
||||
How long did it take from the initial idea to a working version? Weeks? A month of weekends?
|
||||
</p>
|
||||
<p>
|
||||
Draft answer: I put it together in three weeks in the evenings, then spent another month refining it while
|
||||
working on my own projects. Replace with the actual time.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="section__prose">
|
||||
Obsidian now has Bases, a separate file type that displays notes not as text, but
|
||||
in various formats: tables, cards, and trees. The standard card view didn’t work for me: it’s
|
||||
flat, with no grouping, and it’s hard to make sense of when there are hundreds of notes. I wanted to see them
|
||||
arranged in columns, like a Kanban board, but without the extra overhead. That’s how the Card Columns view plugin came about.
|
||||
</p>
|
||||
|
||||
<div class="case-block" id="kontekst">
|
||||
<h2>Context</h2>
|
||||
<p class="section__prose">
|
||||
A “Bases” is neither a note nor a separate database. It’s a query to the vault: you select
|
||||
notes using a filter, and the framework displays them in the chosen view. Obsidian itself has
|
||||
a built-in card view, but it’s one-dimensional: no grouping by properties, no
|
||||
proper filtering. I decided to create my own view that integrates into the
|
||||
Bases toggle alongside the default ones.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
The main limitation was in the framework itself: the plugin doesn’t store data, but rather renders what
|
||||
the Bases query returns (Sort, Filter, Properties, Search from the toolbar). This means that all the logic
|
||||
for data retrieval remains with Bases, and my task is solely to display and navigate the results.
|
||||
</p>
|
||||
<!-- МЕДИА: общий скрин/видео обзора — доска колонок поверх .base файла.
|
||||
<figure class="case-media case-media--wide">
|
||||
<img src="../assets/card-columns-obsidian/overview.webp" alt="View of "Card Columns" in the .base file: notes on the columns" width="1920" height="1080" loading="lazy" decoding="async" data-image-viewer />
|
||||
<figcaption>Species Overview</figcaption>
|
||||
</figure>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="pochemu-svoi">
|
||||
<h2>Why not use a ready-made solution?</h2>
|
||||
<p class="section__prose">
|
||||
Before writing any code, I checked what was already available. Three options, none of them worked:
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
<strong>The default Bases card view.</strong>
|
||||
It displays a continuous stream of cards without grouping.
|
||||
When there are over a hundred notes, it becomes a feed where you have to scroll and squint at the titles.
|
||||
I ruled that out first.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
<strong>Kanban plugins from the community.</strong>
|
||||
Most come with their own storage system:
|
||||
the board exists separately from the vault, and notes are duplicated. This results in a second database
|
||||
that needs to be synchronized. I didn’t need that.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
<strong>The neighboring bases-kanban.</strong>
|
||||
The closest in spirit: it hooks into Bases to create
|
||||
a Kanban board. But it writes to the notes: dragging a card changes the frontmatter. For a view plugin,
|
||||
this is a risk, which I’ll discuss below.
|
||||
</p>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">📌</span>
|
||||
<div class="case-callout__content">
|
||||
<p>
|
||||
The decision to forgo drag-and-drop isn’t a shortcoming—it’s a design choice. A view plugin should not
|
||||
write to user data. This decision shaped everything else.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="case-todo">
|
||||
<span class="case-todo__label">Question for the author</span>
|
||||
<p class="case-todo__q">
|
||||
Did you actually install and test other people’s plugins, or did you just go by the descriptions? Which
|
||||
ones did you look at specifically?
|
||||
</p>
|
||||
<p>
|
||||
Preliminary answer: I installed three of them, ran them for a week on my database, then uninstalled them. I looked at the source code for bases-kanban
|
||||
but didn’t install it. Replace with actual information; include the plugin names.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="zadacha">
|
||||
<h2>Task</h2>
|
||||
<p class="section__prose">
|
||||
Design a view for Bases that displays notes as cards arranged in columns and allows you to
|
||||
quickly select the ones you need. Strict constraint: the plugin does not write anything to the notes and does not store
|
||||
its own state, and sorting, filtering, properties, and search from the standard Bases toolbar must
|
||||
work as before.
|
||||
</p>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">📌</span>
|
||||
<div class="case-callout__content">
|
||||
<p>
|
||||
The main risk is creating a view that conflicts with the standard Bases toolbar. If that happens,
|
||||
the user loses the sorting and search capabilities—the very features that make Bases worth using. This could not
|
||||
be allowed to happen.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="reshenie">
|
||||
<h2>Solutions</h2>
|
||||
<p class="section__prose">
|
||||
The plugin adds a “Columns” view to Bases. Cards are built from the results of a Bases query;
|
||||
grouping is defined by a property, and filtering is done via clickable tags. Next, there are five options
|
||||
that determine the result. Each one includes something I decided against.
|
||||
</p>
|
||||
<!-- МЕДИА: скриншот вида с колонками и карточками внутри.
|
||||
<figure class="case-media case-media--wide">
|
||||
<img src="../assets/card-columns-obsidian/board.webp" alt="Columns of cards grouped by property" width="1920" height="1080" loading="lazy" decoding="async" data-image-viewer />
|
||||
<figcaption>View with columns</figcaption>
|
||||
</figure>
|
||||
-->
|
||||
|
||||
<h3>Grouping by any property</h3>
|
||||
<p class="section__prose">
|
||||
Columns are generated using the “Group by” option in the Bases toolbar, by tag, status, category, or any
|
||||
frontmatter property. Change the property in the query, and the board reloads automatically. This way, a single
|
||||
database covers different perspectives: projects by status, notes by tags, tasks by
|
||||
priority.
|
||||
</p>
|
||||
|
||||
<h3>Tag filter by AND and OR</h3>
|
||||
<p class="section__prose">
|
||||
This is exactly what the plugin was designed for. Below the toolbar, there are tag icons; when clicked, they
|
||||
filter cards across all columns at once:
|
||||
</p>
|
||||
<ul class="section__prose">
|
||||
<li>Left-click on a tag to show only that tag (resets the other filters).</li>
|
||||
<li>Left-click on the next tag to show notes with ANY of the selected tags (OR mode).</li>
|
||||
<li>Ctrl+Click or right-click to switch tags without resetting the selection.</li>
|
||||
<li>The OR/AND button: toggle between “any of” and “all of” for multiple tags.</li>
|
||||
<li>All: Clear all filters.</li>
|
||||
</ul>
|
||||
<p class="section__prose">
|
||||
The combination of “OR—add with a left-click + exact toggle via Ctrl” turned out to be intuitive:
|
||||
quick selection doesn’t disrupt what’s already selected. I immediately rejected the alternative—moving the filter to a modal window—
|
||||
because it requires an extra click and loses the context of what’s already been filtered.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
The choice here isn’t about the feature itself, but where it lives. I chose the pills below the toolbar instead of
|
||||
a separate filter window because filtering by tags is an action performed at every step of working
|
||||
with the database. A modal window turns it into an interrupted workflow: open, configure, close, check.
|
||||
The pill-shaped icons keep the result right in front of you, and the filter status is always visible.
|
||||
</p>
|
||||
<!-- МЕДИА: короткое видео фильтрации по тегам (OR / AND).
|
||||
<figure class="case-media" style="margin: 0.85rem 0 1.2rem">
|
||||
<div class="case-video-shell">
|
||||
<video controls preload="metadata" playsinline poster="../assets/card-columns-obsidian/filter-poster.webp" data-case-video-player style="display: block; width: 100%; height: auto; margin: 0">
|
||||
<source src="../assets/card-columns-obsidian/filter.webm" type="video/webm" />
|
||||
<source src="../assets/card-columns-obsidian/filter.mp4" type="video/mp4" />
|
||||
Your browser does not support video.
|
||||
</video>
|
||||
</div>
|
||||
<figcaption>Filtering by tags: OR and AND</figcaption>
|
||||
</figure>
|
||||
-->
|
||||
|
||||
<h3>Cards and Covers</h3>
|
||||
<p class="section__prose">
|
||||
Properties such as chips, text, tags, links, Booleans, and dates are displayed for each note. You can
|
||||
show a cover: from the first image in the note or from a separate property. You have
|
||||
full control over the cover, including aspect ratio (Auto, 1:1, 3:2, 4:3, 16:9), border, fit
|
||||
(cover/contain), orientation, and position on the card (above the title, below it, at the end).
|
||||
</p>
|
||||
|
||||
<h3>Solution: Settings have been moved out of the code</h3>
|
||||
<p class="section__prose">
|
||||
At first, the parameters were hardcoded for my database. This was a mistake: the view would stop working
|
||||
for someone else’s scenario. I moved everything to the gear menu, dividing it into four sections so that the view
|
||||
could adapt to different databases, rather than just my single use case. More than 15 parameters.
|
||||
</p>
|
||||
<details class="case-details">
|
||||
<summary>Show the full list of settings</summary>
|
||||
<p class="section__prose"><strong>General</strong></p>
|
||||
<ul class="section__prose">
|
||||
<li>Where to open the card: active panel, pop-up window, new tab, split to the right / down.</li>
|
||||
<li>Card width (px).</li>
|
||||
<li>Columns per group (1–6), grid within a column.</li>
|
||||
<li>Zebra columns (alternating background colors for easy scanning).</li>
|
||||
<li>Masonry layout (cards fill vertical gaps).</li>
|
||||
</ul>
|
||||
<p class="section__prose"><strong>Title</strong></p>
|
||||
<ul class="section__prose">
|
||||
<li>Title wrapping, bold title, font size (px).</li>
|
||||
</ul>
|
||||
<p class="section__prose"><strong>Properties</strong></p>
|
||||
<ul class="section__prose">
|
||||
<li>Chip layout: stack (vertical) or grid (compact in a row).</li>
|
||||
<li>Wrap multi-line values, font size.</li>
|
||||
<li>Date and date-time formats + locale (Moment.js).</li>
|
||||
</ul>
|
||||
<p class="section__prose"><strong>Cover</strong></p>
|
||||
<ul class="section__prose">
|
||||
<li>Source: none / first image in the post / cover property.</li>
|
||||
<li>Style (borderless / with border), aspect ratio, orientation, fit, position on the card.</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<div class="case-todo">
|
||||
<span class="case-todo__label">Question for the author</span>
|
||||
<p class="case-todo__q">
|
||||
Is it true about the hard-coded parameters? If not, replace it with the real reason for moving them
|
||||
to the settings: user requests, multiple databases with different structures, whatever.
|
||||
</p>
|
||||
<p>
|
||||
Preliminary answer: Yes, the first version was tailored to my database; the settings were added when
|
||||
I needed to make the plugin available externally. I’ll check and fix it.
|
||||
</p>
|
||||
</div>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">💡</span>
|
||||
<div class="case-callout__content">
|
||||
<p>
|
||||
I implemented the split preview just like in Obsidian itself: left-click reuses the panel, Ctrl+click opens
|
||||
a new tab within the split. Without this, on a wide screen, it’s inconvenient to
|
||||
view cards next to the board.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<h3>Works within the standard toolbar</h3>
|
||||
<p class="section__prose">
|
||||
The view hooks into the BasesView framework, so sorting, filtering, properties, and search from
|
||||
the Bases toolbar remain functional. The plugin only renders the query results; the data is not
|
||||
duplicated and does not depend on a separate storage.
|
||||
</p>
|
||||
<!-- МЕДИА: скриншот настроек (gear-меню) с секциями.
|
||||
<figure class="case-media case-media--content">
|
||||
<img src="../assets/card-columns-obsidian/settings.webp" alt="Plugin Settings Window: General, Title, Properties, Cover" width="1200" height="675" loading="lazy" decoding="async" data-image-viewer />
|
||||
<figcaption>View Settings</figcaption>
|
||||
</figure>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="rezultat">
|
||||
<h2>The Result and How I Tested It</h2>
|
||||
<p class="section__prose">
|
||||
This view solved my problem: a Bases query now reads like a board, rather than a flat stream
|
||||
of cards. Grouping by property + filtering by AND/OR tags—exactly what the default
|
||||
view was missing. Everything runs on the Bases query, so moving the vault and syncing don’t
|
||||
break anything.
|
||||
</p>
|
||||
|
||||
<h3>Metrics: number of steps to filter</h3>
|
||||
<p class="section__prose">
|
||||
The main task I used to measure success: filter notes by two tags and find the right
|
||||
card. In the standard card view, this involves scrolling through the feed and scanning the titles with your eyes. In
|
||||
Columns, two clicks on the pills and the column is right in front of you.
|
||||
</p>
|
||||
|
||||
<div class="case-todo">
|
||||
<span class="case-todo__label">Question for the author</span>
|
||||
<p class="case-todo__q">
|
||||
Can you run this test for real? Time 10 repetitions of the task: the default
|
||||
view versus Columns. Or count the exact number of steps.
|
||||
</p>
|
||||
<p>
|
||||
Preliminary answer: on a sample of 25 notes, filtering by two tags took about 40 seconds and
|
||||
required scrolling through the feed; in Columns, two clicks, about 5 seconds. These are rough estimates—
|
||||
replace them with actual measurements.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3>Test: Long-term usability in daily work</h3>
|
||||
<p class="section__prose">
|
||||
The second criterion is more subjective, but for a tool tailored to my needs, it’s more important: will I still be
|
||||
using the plugin a month from now? I am—it’s become an integral part of my work with the database and doesn’t require
|
||||
me to remember how to use it.
|
||||
</p>
|
||||
|
||||
<div class="case-todo">
|
||||
<span class="case-todo__label">Question for the author</span>
|
||||
<p class="case-todo__q">
|
||||
Are there any external indicators: installs, GitHub stars, reviews, or questions in the issue tracker? Any
|
||||
number confirming that you’re not the only one using the plugin.
|
||||
</p>
|
||||
<p>
|
||||
Preliminary answer: The plugin currently needs to be installed manually; it hasn’t been approved for the community repository yet, so
|
||||
there are only a handful of installs and no stars so far. If there’s actual data, include it here—
|
||||
it’s more convincing than any self-report.
|
||||
</p>
|
||||
</div>
|
||||
<p class="section__prose">
|
||||
What I’d do differently. I deliberately chose not to implement drag-and-drop of cards between columns,
|
||||
since that changes a property in the note, and for a view plugin, that poses a separate data integrity risk.
|
||||
But I see a demand for this among users, and I might add it as an optional mode. For
|
||||
now, since the plugin is maintained manually (it hasn’t been reviewed by the community directory yet), this is the only thing
|
||||
preventing users from using it right away.
|
||||
</p>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">✅</span>
|
||||
<div class="case-callout__content">
|
||||
<p>
|
||||
Main conclusion: a view for Bases only works when it respects
|
||||
the framework’s toolbar. I added a view, not a second storage system, and the notes
|
||||
remained regular notes.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{> mini-projects-related}}
|
||||
{{> contact-section}}
|
||||
{{> footer}}
|
||||
{{> cookie-notice}}
|
||||
{{> nav-more-floating}}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded',function(){
|
||||
var s=document.body.dataset.currentCase;
|
||||
if(!s)return;
|
||||
document.querySelectorAll('[data-related-slug="'+s+'"]').forEach(function(el){el.hidden=true;});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,891 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="dark" data-radius="medium" data-font="merriweather">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Design System Portfolio: colors, typography, grid, interface components."
|
||||
/>
|
||||
<title>Design System — Portfolio</title>
|
||||
{{> head-links}}
|
||||
<style>
|
||||
#main {
|
||||
padding-bottom: clamp(3.5rem, 8vw, 5.5rem);
|
||||
}
|
||||
|
||||
.case-article.ds-page {
|
||||
max-width: 52rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.case-article.ds-page .case-page__h1 {
|
||||
margin-bottom: 0.65rem;
|
||||
}
|
||||
|
||||
.case-article.ds-page .case-page__sub {
|
||||
margin-bottom: clamp(1.75rem, 4vw, 2.5rem);
|
||||
}
|
||||
|
||||
.case-article.ds-page .section__prose {
|
||||
margin-bottom: 1.15rem;
|
||||
line-height: 1.65;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.case-article.ds-page > h2 {
|
||||
margin: 0 0 1.15rem;
|
||||
padding-top: clamp(2.75rem, 6vw, 4rem);
|
||||
font-family: var(--font);
|
||||
font-size: clamp(1.7rem, 3vw, 2rem);
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
scroll-margin-top: 1.5rem;
|
||||
border-top: 1px solid color-mix(in srgb, var(--line) 90%, transparent);
|
||||
}
|
||||
|
||||
.case-article.ds-page > h2#colors {
|
||||
padding-top: clamp(1.5rem, 4vw, 2.25rem);
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.case-article.ds-page > h3 {
|
||||
margin: clamp(2rem, 4vw, 2.75rem) 0 0.85rem;
|
||||
font-family: var(--font);
|
||||
font-size: 1.55rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.case-article.ds-page > h2 + .section__prose,
|
||||
.case-article.ds-page > h2 + h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.case-article.ds-page > h2 + h3 {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.case-article.ds-page #components h3:first-of-type {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.case-article.ds-page a:not(.btn):not(.fab) {
|
||||
color: var(--accent);
|
||||
}
|
||||
.sg-toc {
|
||||
margin: clamp(1.5rem, 3vw, 2rem) 0 clamp(2.25rem, 5vw, 3.25rem);
|
||||
padding: 1.15rem 1.35rem;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--line);
|
||||
background: color-mix(in srgb, var(--surface) 72%, transparent);
|
||||
}
|
||||
.sg-toc ol {
|
||||
margin: 0;
|
||||
padding-left: 1.25rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.sg-toc li + li {
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
.sg-toc a {
|
||||
color: var(--accent-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
.sg-toc a:hover,
|
||||
.sg-toc a:focus-visible {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.sg-blocks-card {
|
||||
margin: 0 0 2.5rem;
|
||||
padding: 1.15rem 1.35rem;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--line);
|
||||
background: color-mix(in srgb, var(--surface) 72%, transparent);
|
||||
}
|
||||
|
||||
.sg-blocks-card p {
|
||||
margin: 0 0 0.85rem;
|
||||
line-height: 1.65;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.sg-blocks-card p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#blocks,
|
||||
#mini-projects {
|
||||
scroll-margin-top: calc(var(--header-offset, 4.25rem) + 0.75rem);
|
||||
}
|
||||
.sg-swatch-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(7.5rem, 1fr));
|
||||
gap: 0.75rem;
|
||||
margin: 1rem 0 1.75rem;
|
||||
}
|
||||
.sg-swatch {
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--line);
|
||||
overflow: hidden;
|
||||
}
|
||||
.sg-swatch__chip {
|
||||
height: 3.25rem;
|
||||
}
|
||||
.sg-swatch__name {
|
||||
display: block;
|
||||
padding: 0.35rem 0.45rem;
|
||||
font-size: 0.68rem;
|
||||
line-height: 1.25;
|
||||
font-family: var(--font);
|
||||
color: var(--text-muted);
|
||||
word-break: break-all;
|
||||
}
|
||||
.sg-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
margin: 1rem 0 2rem;
|
||||
}
|
||||
.sg-stack--row {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
.sg-label {
|
||||
margin: 0.35rem 0 0;
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font);
|
||||
}
|
||||
|
||||
.case-article.ds-page .sg-label {
|
||||
display: block;
|
||||
margin-bottom: 1.75rem;
|
||||
}
|
||||
|
||||
.sg-table-wrap {
|
||||
overflow-x: auto;
|
||||
margin: 1rem 0 2rem;
|
||||
}
|
||||
.sg-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
.sg-table th,
|
||||
.sg-table td {
|
||||
padding: 0.65rem 0.85rem;
|
||||
border: 1px solid var(--line);
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
.sg-table th {
|
||||
background: color-mix(in srgb, var(--surface) 80%, transparent);
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
.sg-table td {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.sg-table code {
|
||||
font-size: 0.82em;
|
||||
}
|
||||
.sg-table--type .sg-type-sample {
|
||||
color: var(--text);
|
||||
min-width: 11rem;
|
||||
}
|
||||
.sg-table--type .sg-type-sample > * {
|
||||
margin: 0;
|
||||
}
|
||||
.sg-table--type td:first-child {
|
||||
white-space: nowrap;
|
||||
vertical-align: top;
|
||||
}
|
||||
.sg-table--type tbody td:last-child {
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
.sg-layout-demo {
|
||||
margin: 1rem 0 2rem;
|
||||
padding: 1.1rem;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px dashed var(--line);
|
||||
background: color-mix(in srgb, var(--bg-elevated) 55%, transparent);
|
||||
}
|
||||
.sg-layout-demo__hero {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
@media (min-width: 860px) {
|
||||
.sg-layout-demo__hero {
|
||||
grid-template-columns: 1.15fr 0.85fr;
|
||||
}
|
||||
}
|
||||
.sg-layout-demo__block {
|
||||
min-height: 2.75rem;
|
||||
border-radius: var(--radius-sm);
|
||||
background: color-mix(in srgb, var(--accent) 18%, var(--surface));
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--line));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.sg-contact-demo {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.sg-contact-demo .contact-card {
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
/* В компонентах только 2 карточки — без 4-колоночной сетки главной */
|
||||
.sg-contact-components.contact-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@media (min-width: 560px) {
|
||||
.sg-contact-components.contact-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
.sg-portfolio-wrap {
|
||||
max-width: 100%;
|
||||
}
|
||||
.sg-portfolio-wrap .portfolio-card {
|
||||
pointer-events: none;
|
||||
}
|
||||
.sg-section-demo.section {
|
||||
margin: 0.75rem calc(-1 * max(var(--space), env(safe-area-inset-left, 0px))) 2rem;
|
||||
padding: 1.35rem max(var(--space), env(safe-area-inset-right, 0px)) 1.35rem
|
||||
max(var(--space), env(safe-area-inset-left, 0px));
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--surface);
|
||||
border: 1px solid color-mix(in srgb, var(--line) 85%, transparent);
|
||||
}
|
||||
|
||||
.sg-section-demo .section__inner {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sg-section-demo .section__title {
|
||||
margin: 0 0 0.65rem;
|
||||
padding: 0;
|
||||
border: none;
|
||||
font-size: clamp(1.02rem, 1.65vw, 1.28rem);
|
||||
}
|
||||
|
||||
.sg-section-demo .section__prose {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.case-article.ds-page pre {
|
||||
margin: 0.75rem 0 2rem;
|
||||
}
|
||||
|
||||
.case-article.ds-page ul.section__prose,
|
||||
.case-article.ds-page ol.section__prose {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.case-article.ds-page .skill-task-card,
|
||||
.case-article.ds-page .sg-portfolio-wrap {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.case-article.ds-page .contact-grid {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var t = JSON.parse(localStorage.getItem("siteThemeV1") || "{}");
|
||||
var d = document.documentElement;
|
||||
var m = t.mode;
|
||||
if (m !== "light" && m !== "dark" && m !== "system") m = "system";
|
||||
var eff =
|
||||
m === "system"
|
||||
? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: m;
|
||||
d.dataset.theme = eff;
|
||||
var h = typeof t.h === "number" ? t.h : typeof t.hue === "number" ? t.hue : null;
|
||||
var s = typeof t.s === "number" ? t.s : typeof t.sat === "number" ? t.sat : null;
|
||||
var l = typeof t.l === "number" ? t.l : typeof t.light === "number" ? t.light : null;
|
||||
if (h !== null) d.style.setProperty("--theme-h", String(h));
|
||||
if (s !== null) d.style.setProperty("--theme-s", String(s));
|
||||
if (l !== null) {
|
||||
var L = Number(l);
|
||||
if (Number.isFinite(L)) d.style.setProperty("--theme-l", String(Math.min(70, Math.max(20, L))));
|
||||
}
|
||||
var rad = t.radius;
|
||||
if (rad === "soft" || rad === "medium" || rad === "strong") {
|
||||
d.dataset.radius = rad;
|
||||
} else {
|
||||
d.dataset.radius = "medium";
|
||||
}
|
||||
var f = t.font;
|
||||
if (f === "onest") f = "geologica";
|
||||
if (f === "anonymous") f = "mono";
|
||||
if (f === "merriweather" || f === "mono" || f === "geologica") {
|
||||
d.dataset.font = f;
|
||||
} else {
|
||||
d.dataset.font = "merriweather";
|
||||
}
|
||||
var mb = t.meshBg;
|
||||
if (mb === "off") {
|
||||
d.dataset.meshBg = "off";
|
||||
} else {
|
||||
d.dataset.meshBg = "on";
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
{{> header-minimal}}
|
||||
{{> fab-stack-home-contact}}
|
||||
<main id="main" class="section section--tight">
|
||||
<div class="section__inner case-article ds-page">
|
||||
<p class="hero__eyebrow">Documentation · UI</p>
|
||||
<h1 class="case-page__h1">Design System</h1>
|
||||
<p class="case-page__sub">Tokens, typography, grid, components, and longgrid blocks</p>
|
||||
<p class="section__prose">
|
||||
Switch between themes, accents, and fonts using the floating panel on the right. When making global style changes (tokens,
|
||||
components, longgrid blocks), be sure to update this page as well—it’s the project’s live UI catalog.
|
||||
</p>
|
||||
|
||||
<nav class="sg-toc" aria-label="Table of Contents: Design System">
|
||||
<ol>
|
||||
<li><a href="#colors">Colors</a></li>
|
||||
<li><a href="#fonts">Fonts</a></li>
|
||||
<li><a href="#layout">Grid and Responsive Design</a></li>
|
||||
<li><a href="#components">Components</a></li>
|
||||
<li><a href="#fab">FAB Buttons</a></li>
|
||||
<li><a href="#blocks">Blocks</a></li>
|
||||
<li><a href="#mini-projects">Activity</a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<h2 id="colors">Colors</h2>
|
||||
<p class="section__prose">
|
||||
The accent and “cool/warm” tones of the entire UI are defined by three numbers in the <strong>HSL</strong> model (Hue,
|
||||
Saturation, Lightness) at <code><html></code>. From these,
|
||||
<code>--accent</code> and derived tokens; neutral backgrounds are slightly tinted with the same hue
|
||||
(<code>--neutral-accent-tint</code>). Light/dark theme — <code>data-theme</code>, mesh —
|
||||
<code>data-mesh-bg</code>. Can be changed via the FAB on the right.
|
||||
</p>
|
||||
<h3>What is <code>--theme-h</code>, <code>--theme-s</code>, <code>--theme-l</code></h3>
|
||||
<ul class="section__prose">
|
||||
<li>
|
||||
<code>--theme-h</code> — <strong>hue (Hue)</strong>, position on the color wheel 0–360°. A hue shift
|
||||
recolors buttons, links, the mesh gradient, and square accents (<code>--accent-square-2</code> …
|
||||
<code>4</code>), without having to manually adjust each hex value.
|
||||
</li>
|
||||
<li>
|
||||
<code>--theme-s</code> — <strong>Saturation</strong>: how “pure” the color is, rather than gray.
|
||||
</li>
|
||||
<li>
|
||||
<code>--theme-l</code> — Accent <strong>lightness</strong>: darker or lighter than the primary brand color
|
||||
(usually limited by the panel’s range to ensure buttons remain readable).
|
||||
</li>
|
||||
</ul>
|
||||
<p class="section__prose">
|
||||
In CSS, it looks like this:
|
||||
<code>--accent: hsl(var(--theme-h) calc(var(--theme-s) * 1%) calc(var(--theme-l) * 1%))</code>.
|
||||
Values are written in <code>localStorage</code> (<code>siteThemeV1</code>) and are inserted by an inline script into
|
||||
<code>head</code>to prevent a “foreign” color from flashing during loading.
|
||||
</p>
|
||||
<div class="sg-swatch-grid" aria-label="Token Palette">
|
||||
<div class="sg-swatch">
|
||||
<div class="sg-swatch__chip" style="background: var(--bg)"></div>
|
||||
<span class="sg-swatch__name">--bg</span>
|
||||
</div>
|
||||
<div class="sg-swatch">
|
||||
<div class="sg-swatch__chip" style="background: var(--bg-elevated)"></div>
|
||||
<span class="sg-swatch__name">--bg-elevated</span>
|
||||
</div>
|
||||
<div class="sg-swatch">
|
||||
<div class="sg-swatch__chip" style="background: var(--surface)"></div>
|
||||
<span class="sg-swatch__name">--surface</span>
|
||||
</div>
|
||||
<div class="sg-swatch">
|
||||
<div class="sg-swatch__chip" style="background: var(--text)"></div>
|
||||
<span class="sg-swatch__name">--text</span>
|
||||
</div>
|
||||
<div class="sg-swatch">
|
||||
<div class="sg-swatch__chip" style="background: var(--text-muted)"></div>
|
||||
<span class="sg-swatch__name">--text-muted</span>
|
||||
</div>
|
||||
<div class="sg-swatch">
|
||||
<div class="sg-swatch__chip" style="background: var(--line)"></div>
|
||||
<span class="sg-swatch__name">--line</span>
|
||||
</div>
|
||||
<div class="sg-swatch">
|
||||
<div class="sg-swatch__chip" style="background: var(--accent)"></div>
|
||||
<span class="sg-swatch__name">--accent</span>
|
||||
</div>
|
||||
<div class="sg-swatch">
|
||||
<div class="sg-swatch__chip" style="background: var(--accent-text)"></div>
|
||||
<span class="sg-swatch__name">--accent-text</span>
|
||||
</div>
|
||||
<div class="sg-swatch">
|
||||
<div class="sg-swatch__chip" style="background: var(--accent-soft)"></div>
|
||||
<span class="sg-swatch__name">--accent-soft</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="section__prose sg-label">
|
||||
Additionally: quadratic mesh shades — <code>--accent-square-2</code> … <code>4</code>; they depend on hue.
|
||||
</p>
|
||||
|
||||
<h2 id="fonts">Fonts</h2>
|
||||
<p class="section__prose">
|
||||
A pair of fonts — <code>data-font</code> on <code><html></code> (FAB “Typography”): sets
|
||||
<code>--font-accent</code> (headings) and <code>--font</code> (body text). Scale in tables: class, example, purpose.
|
||||
</p>
|
||||
<h3>Font families</h3>
|
||||
<div class="sg-table-wrap">
|
||||
<table class="sg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"><code>data-font</code></th>
|
||||
<th scope="col">Stack</th>
|
||||
<th scope="col">Where</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>merriweather</code></td>
|
||||
<td>Merriweather + Alice</td>
|
||||
<td>Default pair; Alice—body text, Merriweather—emphasized headings</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>mono</code></td>
|
||||
<td>Source Code Pro + PT Mono</td>
|
||||
<td>Monospace pair</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>geologica</code></td>
|
||||
<td>Geologica + PT Sans</td>
|
||||
<td>Sans-serif + neutral body text</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3>Typographic scale</h3>
|
||||
<div class="sg-table-wrap">
|
||||
<table class="sg-table sg-table--type" aria-label="Typographic scale">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Class</th>
|
||||
<th scope="col">Example</th>
|
||||
<th scope="col">Purpose</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>.hero__eyebrow</code></td>
|
||||
<td class="sg-type-sample"><p class="hero__eyebrow">Section caption</p></td>
|
||||
<td>Hero, case input</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>.hero__title</code></td>
|
||||
<td class="sg-type-sample">
|
||||
<p class="hero__title" style="font-size: clamp(1.5rem, 3.5vw, 2.15rem); line-height: 1.08">
|
||||
Display main heading
|
||||
</p>
|
||||
</td>
|
||||
<td>H1 for the main page; reduced on DS for the column</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>.section__title</code></td>
|
||||
<td class="sg-type-sample"><h2 class="section__title">Section</h2></td>
|
||||
<td>“Skills,” “Portfolio,” contacts; main font, no caps</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>.case-page__h1</code></td>
|
||||
<td class="sg-type-sample"><p class="case-page__h1">Page title</p></td>
|
||||
<td>H1 for case studies / Design System</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>.case-page__sub</code></td>
|
||||
<td class="sg-type-sample"><p class="case-page__sub">Lead text under the heading</p></td>
|
||||
<td>Subheading below H1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>.section__prose</code></td>
|
||||
<td class="sg-type-sample"><p class="section__prose">Main paragraph text.</p></td>
|
||||
<td>Prose, lists, and links in the article</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>h2</code> in <code>.case-article</code></td>
|
||||
<td class="sg-type-sample"><h2 style="margin: 0">H2 subsection</h2></td>
|
||||
<td>Long-form sections (such as “Colors”)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>h3</code> in <code>.case-article</code></td>
|
||||
<td class="sg-type-sample"><h3 style="margin: 0">H3 subsection</h3></td>
|
||||
<td>Subsections within H2; margins in <code>main.css</code>: 3.25rem top, 1.5rem bottom, after <code>figure</code> 4rem, <code>h3+h3</code> 3.5rem</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3>Rhythm and Code</h3>
|
||||
<p class="section__prose">Rhythm example:</p>
|
||||
<ul class="section__prose">
|
||||
<li>bulleted list;</li>
|
||||
<li><strong>emphasis</strong> and <a href="#fonts">link</a>;</li>
|
||||
</ul>
|
||||
<ol class="section__prose">
|
||||
<li>numbered item;</li>
|
||||
</ol>
|
||||
<p class="section__prose">Inline <code>code</code> and block:</p>
|
||||
<pre><code>--font: "Alice", system-ui, serif;
|
||||
--font-accent: "Merriweather", var(--font);</code></pre>
|
||||
<p class="section__prose sg-label">
|
||||
Headings use <code>clamp()</code> — scale relative to the viewport; see the “Grid” section for more details.
|
||||
</p>
|
||||
|
||||
<h2 id="layout">Grid and Responsive</h2>
|
||||
<p class="section__prose">
|
||||
There is no separate 12-column grid: a <strong>full-bleed</strong> layout with side margins
|
||||
<code>--space</code> and local CSS Grid within sections. The content column for DS and case studies —
|
||||
<code>.case-article</code> (max-width 48rem).
|
||||
</p>
|
||||
|
||||
<h3>Basic framework</h3>
|
||||
<ul class="section__prose">
|
||||
<li><code>--space: clamp(1rem, 4vw, 2.75rem)</code> — horizontal margins + <code>safe-area-inset</code>.</li>
|
||||
<li><code>--max: none</code> — no fixed 1200px “box”; width = viewport minus padding.</li>
|
||||
<li><code>.section</code> — vertical spacing via <code>clamp()</code>; <code>.section__inner</code> centers the block.</li>
|
||||
<li><code>.section--tight</code> — More compact vertically (case studies, this page).</li>
|
||||
</ul>
|
||||
|
||||
<h3>Breakpoints</h3>
|
||||
<div class="sg-table-wrap">
|
||||
<table class="sg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">From</th>
|
||||
<th scope="col">What Changes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>860px</code></td>
|
||||
<td><code>.hero__grid</code> → 2 columns <code>1.15fr 0.85fr</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>760px</code> / <code>1180px</code></td>
|
||||
<td><code>.projects-carousel</code>: <code>--projects-visible</code> 1 → 2 → 3 cards</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>720px</code></td>
|
||||
<td><code>.skills-category__grid</code> → 2 columns</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>1024px</code></td>
|
||||
<td>Skills: 6-column mosaic (<code>grid-column: span …</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>560px</code> / <code>1024px</code></td>
|
||||
<td><code>.contact-grid</code> → 2 → 4 columns</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>48rem</code> / <code>28rem</code></td>
|
||||
<td>Case gallery: ≤2 col → 1 col</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>640px</code></td>
|
||||
<td><code>case-media-gallery--wide</code> collapses to the width of a column</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3>Hero: one → two columns</h3>
|
||||
<p class="section__prose">Layout <code>.hero__grid</code> (compressed to 860px):</p>
|
||||
<div class="sg-layout-demo" aria-hidden="true">
|
||||
<div class="sg-layout-demo__hero">
|
||||
<div class="sg-layout-demo__block">Text</div>
|
||||
<div class="sg-layout-demo__block">Visual</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Contacts: 1 → 2 → 4</h3>
|
||||
<p class="section__prose">Demo <code>.contact-grid</code> (placeholders, no transitions):</p>
|
||||
<ul class="contact-grid sg-contact-demo" aria-hidden="true">
|
||||
<li><span class="contact-card contact-card--link contact-card--brand-tg"><span class="contact-card__corner" aria-hidden="true"></span><span class="contact-card__icon" aria-hidden="true"></span></span>
|
||||
|
||||
|
||||
|
||||
|
||||
<span class="contact-card contact-card--link contact-card--brand-tg"><span class="contact-card__body"><span class="contact-card__name">1 col</span>
|
||||
<span class="contact-card__hint">< 560px</span>
|
||||
|
||||
|
||||
</span></span></li>
|
||||
<li><span class="contact-card contact-card--link contact-card--brand-vk"><span class="contact-card__corner" aria-hidden="true"></span><span class="contact-card__icon" aria-hidden="true"></span></span>
|
||||
|
||||
|
||||
|
||||
|
||||
<span class="contact-card contact-card--link contact-card--brand-vk"><span class="contact-card__body"><span class="contact-card__name">2 col</span>
|
||||
≥<span class="contact-card__hint"> 560px</span>
|
||||
|
||||
|
||||
</span></span></li>
|
||||
<li><span class="contact-card contact-card--link contact-card--brand-ya"><span class="contact-card__corner" aria-hidden="true"></span><span class="contact-card__icon" aria-hidden="true"></span></span>
|
||||
|
||||
|
||||
|
||||
|
||||
<span class="contact-card contact-card--link contact-card--brand-ya"><span class="contact-card__body"><span class="contact-card__name">4 col</span>
|
||||
≥<span class="contact-card__hint"> 1024px</span>
|
||||
|
||||
|
||||
</span></span></li>
|
||||
<li><span class="contact-card contact-card--action contact-card--brand-email"><span class="contact-card__corner contact-card__corner--copy" aria-hidden="true"></span><span class="contact-card__icon contact-card__icon--accent" aria-hidden="true"></span></span>
|
||||
|
||||
|
||||
|
||||
|
||||
<span class="contact-card contact-card--action contact-card--brand-email"><span class="contact-card__body"><span class="contact-card__name">Grid</span>
|
||||
on<span class="contact-card__hint"> the homepage</span>
|
||||
|
||||
|
||||
</span></span></li>
|
||||
</ul>
|
||||
<p class="section__prose">
|
||||
Skills: mosaic layout on wide screens—see
|
||||
<a href="{{pagePrefix}}#skills">the homepage, #skills</a>. Portfolio: carousel on
|
||||
<code>100vw</code> with <code>--projects-card-width</code> and JS scrolling — on the homepage
|
||||
<a href="{{pagePrefix}}#projects">#projects</a>.
|
||||
</p>
|
||||
|
||||
<h3>Wide in case studies</h3>
|
||||
<p class="section__prose">
|
||||
<code>.case-media--wide</code>, <code>.case-media-gallery--wide</code> and <code>.case-callout</code> wider
|
||||
article columns: <code>min(100vw − 2×padding, content + 8rem)</code>, with margins <code>--space</code>. On a narrow screen (up to 640px), "wide" collapses to the width of the column. Images with <code>data-image-viewer</code> — the outline and hover effects
|
||||
are the same as <code>.case-video-shell</code> (live examples — case study element demo). The caption below the video is
|
||||
<code>figcaption</code> centered in the column.
|
||||
</p>
|
||||
|
||||
<h3>Typography and Motion</h3>
|
||||
<p class="section__prose">
|
||||
Fluid type: <code>clamp()</code> on <code>.hero__title</code>, <code>.section__title</code>,
|
||||
<code>.case-page__h1</code>. Rounded corners (<code>data-radius</code>) do not change the grid. For long screenshots and
|
||||
animations, the <code>prefers-reduced-motion: reduce</code>.
|
||||
</p>
|
||||
|
||||
<h2 id="components">Components</h2>
|
||||
<p class="section__prose">Live examples; the shell (header, FAB, footer, cookie) is already integrated on this page.</p>
|
||||
|
||||
<h3>Buttons</h3>
|
||||
<div class="sg-stack">
|
||||
<a class="btn btn--primary btn--flat" href="#components"><span class="btn__icon" aria-hidden="true"></span><span>Primary flat</span></a>
|
||||
<a class="btn btn--primary btn--raised" href="#components"><span>Primary raised</span></a>
|
||||
<a class="btn btn--secondary btn--flat" href="#components"><span>Secondary flat</span></a>
|
||||
<a class="btn btn--secondary btn--raised" href="#components"><span>Secondary raised</span></a>
|
||||
<button type="button" class="btn btn--ghost btn--flat">Ghost flat</button>
|
||||
</div>
|
||||
<p class="sg-label">.btn .btn--primary | secondary | ghost · .btn--flat | raised · .btn__icon</p>
|
||||
|
||||
<h3>FAB buttons</h3>
|
||||
<p class="section__prose">
|
||||
Floating buttons in the lower-right corner (<code>.fab-stack</code>). Icon-only and with a text label.
|
||||
</p>
|
||||
<div class="sg-stack sg-stack--row">
|
||||
<a class="fab fab--primary" href="#components" aria-label="FAB Demo"><span class="fab-primary__ico" aria-hidden="true"></span></a>
|
||||
<button type="button" class="fab fab--primary fab--label"><span class="fab-primary__ico" aria-hidden="true"></span><span class="fab__label">Download as PDF</span></button>
|
||||
<button type="button" class="fab fab--secondary" aria-label="Secondary FAB Demo"><span class="theme-panel-fab__icon theme-panel-fab__icon--open-hide" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
<p class="sg-label">.fab .fab--primary | secondary · .fab--label (with text) · .fab-primary__ico · .fab__label</p>
|
||||
|
||||
<h3>Tags</h3>
|
||||
<ul class="hero__tags" aria-label="Tag Demo">
|
||||
<li class="hero__tag">hero__tag</li>
|
||||
<li class="hero__tag">UX/UI</li>
|
||||
</ul>
|
||||
<p class="sg-label">.hero__tags / .hero__tag</p>
|
||||
<ul class="portfolio-card__tags" style="display: flex; flex-wrap: wrap; gap: 0.35rem; list-style: none; padding: 0; margin: 0.5rem 0">
|
||||
<li class="portfolio-card__tag">portfolio-card__tag</li>
|
||||
<li class="portfolio-card__tag">Web App</li>
|
||||
</ul>
|
||||
<p class="sg-label">.portfolio-card__tags / .portfolio-card__tag</p>
|
||||
|
||||
<h3>Portfolio Card</h3>
|
||||
<div class="sg-portfolio-wrap">
|
||||
<a class="portfolio-card" href="#components" tabindex="-1" aria-hidden="true">
|
||||
<div class="portfolio-card__body">
|
||||
<h3 class="portfolio-card__title">portfolio-card</h3>
|
||||
<p class="portfolio-card__summary">A brief two- or three-line description of the project for the Design System demo.</p>
|
||||
<ul class="portfolio-card__tags" aria-label="Tags">
|
||||
<li class="portfolio-card__tag">Demo</li>
|
||||
<li class="portfolio-card__tag">UI</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="portfolio-card__cover" aria-hidden="true">
|
||||
<img
|
||||
class="portfolio-card__image"
|
||||
src="{{assetPrefix}}assets/shared/placeholder.webp"
|
||||
alt=""
|
||||
width="400"
|
||||
height="225"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<p class="sg-label">.portfolio-card and child BEM classes</p>
|
||||
|
||||
<h3>Placeholder while images are loading</h3>
|
||||
<p class="section__prose">
|
||||
While <code>img</code> hasn't loaded yet, <code>initImageLoadPlaceholder</code> it
|
||||
<code>.is-img-loading</code> on the image and <code>.has-img-loading</code> the wrapper. The background layer is
|
||||
<code>::before</code>, a single glow from left to right— <code>::after</code> (<code>img-ph-shine-sweep</code>).
|
||||
Icons, SVGs, and small images up to 40px are skipped;
|
||||
disable selectively: <code>data-no-img-placeholder</code>. Below is a fixed demo (on the website, the classes are removed
|
||||
after <code>load</code>).
|
||||
</p>
|
||||
<div
|
||||
class="case-media case-media--content has-img-loading img-ph-demo"
|
||||
role="img"
|
||||
aria-label="Demo: Placeholder when loading an image"
|
||||
></div>
|
||||
<p class="sg-label">.has-img-loading · .is-img-loading · .img-ph-demo (placeholder demo)</p>
|
||||
|
||||
<h3>TUI Braille spinner</h3>
|
||||
<p class="section__prose">
|
||||
Loading large images in the viewer (<code>.case-image-viewer</code>) is indicated by a monospaced Braille spinner—just like in TUI. Pure CSS, no JS: 10 frames cycle through an animation. Reusable class: <code>steps()</code>an animation. Reusable class <code>.tui-spinner</code>
|
||||
(color — <code>--accent</code>, font—system mono).
|
||||
</p>
|
||||
<div class="sg-stack sg-stack--row sg-stack--center" style="gap: 1.5rem; padding: 1.25rem; background: color-mix(in srgb, var(--surface) 70%, transparent); border: 1px solid var(--line); border-radius: var(--radius-card, 14px);">
|
||||
<span class="tui-spinner" aria-label="Loading" role="status"><span>⠋</span><span>⠙</span><span>⠹</span><span>⠸</span><span>⠼</span><span>⠴</span><span>⠦</span><span>⠧</span><span>⠇</span><span>⠏</span></span>
|
||||
<code class="tui-spinner-seq" style="font-family: ui-monospace, 'Cascadia Code', Consolas, monospace; color: var(--text-muted); font-size: 0.95rem;">⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏</code>
|
||||
</div>
|
||||
<p class="sg-label">.tui-spinner · frames: ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏ · prefers-reduced-motion displays ⠋</p>
|
||||
|
||||
<h3>Skill card</h3>
|
||||
<article class="skill-task-card">
|
||||
<p class="skill-task-card__task">skill-task-card__task — the task description provided by the business.</p>
|
||||
<div class="skill-task-card__solutions">
|
||||
<p class="skill-task-card__solution">skill-task-card__solution — the first approach to the solution.</p>
|
||||
<p class="skill-task-card__solution">The second item in the solutions section.</p>
|
||||
</div>
|
||||
</article>
|
||||
<p class="sg-label">.skill-task-card__task · __solutions · __solution</p>
|
||||
|
||||
<h3>Callout</h3>
|
||||
<p class="section__prose">
|
||||
Longread accent block: wider than the column (like wide media), text <code>.case-callout__content</code> slightly
|
||||
larger than the main paragraph.
|
||||
</p>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">📌</span>
|
||||
<div class="case-callout__content">
|
||||
<p>case-callout — an important idea or conclusion. On the left, only colored emojis (📌 💡 ✨ ✅), not text symbols ✎ ✓ ◆; <code>case-callout__emoji--pulse</code> for a subtle animation.</p>
|
||||
</div>
|
||||
</aside>
|
||||
<p class="sg-label">.case-callout · .case-callout__emoji · .case-callout__emoji--pulse · .case-callout__content</p>
|
||||
|
||||
<h3>Contact Cards</h3>
|
||||
<p class="section__prose sg-label" style="margin-top: 0">
|
||||
On the home page, there are four cards in a grid (1→2→4 columns). Here are two examples—the grid is forced into two columns.
|
||||
</p>
|
||||
<ul class="contact-grid sg-contact-components">
|
||||
<li><a class="contact-card contact-card--link contact-card--brand-tg" href="https://t.me/mamadaevv" rel="noopener noreferrer"><span class="contact-card__corner" aria-hidden="true"></span></a>
|
||||
|
||||
|
||||
|
||||
<a class="contact-card contact-card--link contact-card--brand-tg" href="https://t.me/mamadaevv" rel="noopener noreferrer"><span class="contact-card__icon" aria-hidden="true"><img src="{{assetPrefix}}assets/shared/icons/tg.svg" alt="" width="24" height="24" loading="eager" decoding="async" /></span></a>
|
||||
|
||||
|
||||
<a class="contact-card contact-card--link contact-card--brand-tg" href="https://t.me/mamadaevv" rel="noopener noreferrer"><span class="contact-card__body"><span class="contact-card__name">contact-card--link</span>
|
||||
<span class="contact-card__hint">External link</span>
|
||||
|
||||
|
||||
</span></a></li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="contact-card contact-card--action contact-card--brand-email"
|
||||
data-contact-copy
|
||||
data-email-b64="bmlrQG1hbWFkYWV2di5ydQ=="
|
||||
data-hint-default="Copy the address"
|
||||
aria-label="Demo: Copy email"
|
||||
><span class="contact-card__corner contact-card__corner--copy" aria-hidden="true"></span><span class="contact-card__icon contact-card__icon--accent" aria-hidden="true"></span>
|
||||
|
||||
|
||||
|
||||
<span class="contact-card__body"><span class="contact-card__name">contact-card--action</span>
|
||||
<span class="contact-card__hint" data-contact-copy-hint>Copy address</span>
|
||||
|
||||
</span></button>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="sg-label">.contact-card--link · --action · --brand-*</p>
|
||||
|
||||
<h3>Section with a background</h3>
|
||||
<p class="section__prose">
|
||||
On the homepage, the strip <code>.section--surface</code> is visually presented using mesh; in other cases, the page background is
|
||||
solid—see the compact demo below with <code>background: var(--surface)</code>.
|
||||
</p>
|
||||
<section class="section section--surface section--tight sg-section-demo" aria-label="Section Demos">
|
||||
<div class="section__inner">
|
||||
<h2 class="section__title">section__title</h2>
|
||||
<p class="section__prose">Snippet: .section.section--surface + .section__inner</p>
|
||||
</div>
|
||||
</section>
|
||||
<p class="sg-label">.section · .section--surface · .section--tight · .section__inner</p>
|
||||
|
||||
<h3>Separators</h3>
|
||||
<p class="section__prose">
|
||||
Horizontal separators: a line or simply white space (indentation). Modifiers <code>--line</code>, <code>--air</code>, <code>--air-lg</code>.
|
||||
</p>
|
||||
<div class="sg-stack">
|
||||
<hr class="sep sep--line">
|
||||
<hr class="sep sep--air">
|
||||
<hr class="sep sep--air-lg">
|
||||
</div>
|
||||
<p class="sg-label">.sep · .sep--line · .sep--air · .sep--air-lg</p>
|
||||
|
||||
<h2 id="blocks">Blocks</h2>
|
||||
<div class="sg-blocks-card">
|
||||
<p>
|
||||
Longgrid case layout: media and galleries, link cards, videos, callouts, wide blocks, and other partials.
|
||||
A live example with all variations is available on a separate page; use it to copy sections when building a new case.
|
||||
</p>
|
||||
<p>
|
||||
<a class="btn btn--secondary btn--flat" href="../template-elements-demo/">Case element demo
|
||||
</a></p>
|
||||
</div>
|
||||
|
||||
<h2 id="mini-projects">Activity</h2>
|
||||
<div class="sg-blocks-card">
|
||||
<p>
|
||||
On the homepage in the section <code>#mini-projects</code> card <code>mp-card</code> section on the homepage in a masonry grid. Types, cells, and the live
|
||||
layout are on a separate page.
|
||||
</p>
|
||||
<p>
|
||||
<a class="btn btn--secondary btn--flat" href="../mini-projects-showcase/">Activity Card Catalog
|
||||
</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{> footer}}
|
||||
{{> cookie-notice}}
|
||||
{{> nav-more-floating}}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,382 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="dark" data-radius="medium" data-font="merriweather">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Case Study: Developing the Lunacy Interface in Icons8 — Scenarios, UI Iterations, and Screenshots of the Process."
|
||||
/>
|
||||
<title>Lunacy — Case Study</title>
|
||||
{{> head-links}}
|
||||
<style>
|
||||
.case-article {
|
||||
max-width: 48rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.case-article h2 {
|
||||
margin: 2rem 0 0.75rem;
|
||||
font-size: clamp(1.15rem, 2vw, 1.35rem);
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article h3 {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article .section__prose {
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article img,
|
||||
.case-article video {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 0.75rem 0 1rem;
|
||||
}
|
||||
.case-article ul,
|
||||
.case-article ol {
|
||||
margin: 0.5rem 0 1rem 1.25rem;
|
||||
padding: 0;
|
||||
}
|
||||
.case-article li {
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
.case-article a:not(.btn) {
|
||||
color: var(--accent);
|
||||
}
|
||||
.case-media-gallery__item img {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var t = JSON.parse(localStorage.getItem("siteThemeV1") || "{}");
|
||||
var d = document.documentElement;
|
||||
var m = t.mode;
|
||||
if (m !== "light" && m !== "dark" && m !== "system") m = "system";
|
||||
var eff =
|
||||
m === "system"
|
||||
? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: m;
|
||||
d.dataset.theme = eff;
|
||||
var h = typeof t.h === "number" ? t.h : typeof t.hue === "number" ? t.hue : null;
|
||||
var s = typeof t.s === "number" ? t.s : typeof t.sat === "number" ? t.sat : null;
|
||||
var l = typeof t.l === "number" ? t.l : typeof t.light === "number" ? t.light : null;
|
||||
if (h !== null) d.style.setProperty("--theme-h", String(h));
|
||||
if (s !== null) d.style.setProperty("--theme-s", String(s));
|
||||
if (l !== null) {
|
||||
var L = Number(l);
|
||||
if (Number.isFinite(L)) d.style.setProperty("--theme-l", String(Math.min(70, Math.max(20, L))));
|
||||
}
|
||||
var rad = t.radius;
|
||||
if (rad === "soft" || rad === "medium" || rad === "strong") {
|
||||
d.dataset.radius = rad;
|
||||
} else {
|
||||
d.dataset.radius = "medium";
|
||||
}
|
||||
var f = t.font;
|
||||
if (f === "onest") f = "geologica";
|
||||
if (f === "anonymous") f = "mono";
|
||||
if (f === "merriweather" || f === "mono" || f === "geologica") {
|
||||
d.dataset.font = f;
|
||||
} else {
|
||||
d.dataset.font = "merriweather";
|
||||
}
|
||||
var mb = t.meshBg;
|
||||
if (mb === "off") {
|
||||
d.dataset.meshBg = "off";
|
||||
} else {
|
||||
d.dataset.meshBg = "on";
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body data-current-case="lunacy">
|
||||
{{> header-case}}
|
||||
{{> fab-stack}}
|
||||
<main id="case-main" class="section section--tight">
|
||||
<div class="section__inner case-article" data-case-sequential-images>
|
||||
<p class="hero__eyebrow">Icons8 · desktop</p>
|
||||
<h1 class="case-page__h1">Lunacy Graphic Editor</h1>
|
||||
<p class="section__prose">
|
||||
Work on the Lunacy interface as part of the Icons8 team: user flow, prototypes of complex scenarios, refinement of functionality and
|
||||
visual logic. Below are screenshots from the process (the order of the screenshots is based on the filenames).
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
Product: <a href="https://icons8.com/lunacy" target="_blank" rel="noopener noreferrer">icons8.com/lunacy</a>
|
||||
</p>
|
||||
|
||||
<h2 id="obzor">Overview</h2>
|
||||
<p class="section__prose">General view of the application—the workspace with an open document, toolbars, and the canvas. This screen demonstrates Lunacy’s basic layout: the left panel shows layers, the right panel shows object properties, and the top bar displays the context bar with the current tool. This serves as the starting point for the rest of the materials on the page.</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
||||
alt="Lunacy App Review"
|
||||
data-src="{{assetPrefix}}assets/lunacy/Lunacy_app_overview.webp"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Overview of the Lunacy window.</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">Next are individual scenarios and interface iterations, presented in the order that makes the most sense in terms of context and file names.</p>
|
||||
|
||||
<h2 id="blob-holst">Blob: Settings on the Canvas</h2>
|
||||
<p class="section__prose">Working with a blob in the context of the canvas is a separate scenario, prior to the analysis of the panel across versions.</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
||||
alt="Blob Settings on Canvas"
|
||||
data-src="{{assetPrefix}}assets/lunacy/blob_settings_on_canvas.webp"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption><code>blob_settings_on_canvas</code></figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">The settings panel for these tasks evolved through a series of mockups from v1 to v9—they’re compiled in the grid below.</p>
|
||||
|
||||
<h2 id="compare">Components and Comparison</h2>
|
||||
<p class="section__prose">Comparison of components and the update indicator in the panel.</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
||||
alt="Component Comparison Dialog Box"
|
||||
data-src="{{assetPrefix}}assets/lunacy/compare_components_modal_window.webp"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption><code>compare_components_modal_window</code></figcaption>
|
||||
</figure>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
||||
alt="Component Update Indicator in the Panel"
|
||||
data-src="{{assetPrefix}}assets/lunacy/components_update_indicator_location_panel.webp"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption><code>components_update_indicator_location_panel</code></figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">Two related screens: searching for discrepancies and displaying update status next to the panel navigation.</p>
|
||||
|
||||
<h2 id="prototype">Prototype in the Panel</h2>
|
||||
<p class="section__prose">Creating a prototype on the canvas and panel states for prototype mode.</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
||||
alt="Modal Window: Create a Prototype on Canvas"
|
||||
data-src="{{assetPrefix}}assets/lunacy/modal_window_for_prototype_create_on_canvas.webp"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption><code>modal_window_for_prototype_create_on_canvas</code></figcaption>
|
||||
</figure>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
||||
alt="Modal Window: Prototype Panel States"
|
||||
data-src="{{assetPrefix}}assets/lunacy/modal_window_for_prototype_panel_states.webp"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption><code>modal_window_for_prototype_panel_states</code></figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">Both screenshots show how the prototype is linked to the canvas and to a set of dashboard states.</p>
|
||||
|
||||
<h2 id="shadow-pro">Shadow Pro: Settings</h2>
|
||||
<p class="section__prose">Shadow Pro block: panel, ideas, modals with auto-open and auto-close.</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
||||
alt="Shadow Pro: Settings Panel"
|
||||
data-src="{{assetPrefix}}assets/lunacy/shadow_pro_settings_panel.webp"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption><code>shadow_pro_settings_panel</code></figcaption>
|
||||
</figure>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
||||
alt="Shadow Pro: Ideas"
|
||||
data-src="{{assetPrefix}}assets/lunacy/shadow_pro_settings_ideas.webp"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption><code>shadow_pro_settings_ideas</code></figcaption>
|
||||
</figure>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
||||
alt="Shadow Pro: Mods, Auto Enabled"
|
||||
data-src="{{assetPrefix}}assets/lunacy/shadow_pro_settings_modal_auto_on.webp"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption><code>shadow_pro_settings_modal_auto_on</code></figcaption>
|
||||
</figure>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
||||
alt="Shadow Pro: Mod Mode, Auto Off"
|
||||
data-src="{{assetPrefix}}assets/lunacy/shadow_pro_settings_modal_auto-off.webp"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption><code>shadow_pro_settings_modal_auto-off</code></figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">Four consecutive frames show the development of a single subproject: from the panel and sketches to refining the behavior in modals.</p>
|
||||
|
||||
<h2 id="blob-panel-versions">Blob settings panel: versions v1–v9</h2>
|
||||
<p class="section__prose">
|
||||
Panel iterations by filename <code>blob_settings_panel_v1</code> … <code>v9</code>. Grid based on text width, three columns on a
|
||||
wide screen; as the window narrows, the number of columns decreases automatically.
|
||||
</p>
|
||||
<figure class="case-media case-media-gallery" style="--case-gallery-cols: 3">
|
||||
<div class="case-media-gallery__grid">
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="blob panel v1" data-src="{{assetPrefix}}assets/lunacy/blob_settings_panel_v1.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="blob panel v2" data-src="{{assetPrefix}}assets/lunacy/blob_settings_panel_v2.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="blob panel v3" data-src="{{assetPrefix}}assets/lunacy/blob_settings_panel_v3.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="blob panel v4" data-src="{{assetPrefix}}assets/lunacy/blob_settings_panel_v4.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="blob panel v5" data-src="{{assetPrefix}}assets/lunacy/blob_settings_panel_v5.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="blob panel v6" data-src="{{assetPrefix}}assets/lunacy/blob_settings_panel_v6.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="blob panel v7" data-src="{{assetPrefix}}assets/lunacy/blob_settings_panel_v7.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="blob panel v8" data-src="{{assetPrefix}}assets/lunacy/blob_settings_panel_v8.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="blob panel v9" data-src="{{assetPrefix}}assets/lunacy/blob_settings_panel_v9.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
</div>
|
||||
<figcaption>Nine versions in a row: read from left to right, top to bottom.</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">The grid is convenient for step-by-step comparisons; for details, clicking on a cell opens a full-screen view.</p>
|
||||
|
||||
<h2 id="slider-versions">Slider: versions v1–v18 and frame 14</h2>
|
||||
<p class="section__prose">
|
||||
Ordered by name: <code>slider_v1</code> … <code>slider_v13</code>, then <code>slider_14</code> (in the asset without a suffix <code>v</code>),
|
||||
then <code>slider_v15</code> … <code>slider_v18</code>.
|
||||
</p>
|
||||
<figure class="case-media case-media-gallery" style="--case-gallery-cols: 4">
|
||||
<div class="case-media-gallery__grid">
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="slider v1" data-src="{{assetPrefix}}assets/lunacy/slider_v1.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="slider v2" data-src="{{assetPrefix}}assets/lunacy/slider_v2.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="slider v3" data-src="{{assetPrefix}}assets/lunacy/slider_v3.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="slider v4" data-src="{{assetPrefix}}assets/lunacy/slider_v4.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="slider v5" data-src="{{assetPrefix}}assets/lunacy/slider_v5.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="slider v6" data-src="{{assetPrefix}}assets/lunacy/slider_v6.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="slider v7" data-src="{{assetPrefix}}assets/lunacy/slider_v7.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="slider v8" data-src="{{assetPrefix}}assets/lunacy/slider_v8.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="slider v9" data-src="{{assetPrefix}}assets/lunacy/slider_v9.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="slider v10" data-src="{{assetPrefix}}assets/lunacy/slider_v10.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="slider v11" data-src="{{assetPrefix}}assets/lunacy/slider_v11.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="slider v12" data-src="{{assetPrefix}}assets/lunacy/slider_v12.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="slider v13" data-src="{{assetPrefix}}assets/lunacy/slider_v13.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="slider 14" data-src="{{assetPrefix}}assets/lunacy/slider_14.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="slider v15" data-src="{{assetPrefix}}assets/lunacy/slider_v15.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="slider v16" data-src="{{assetPrefix}}assets/lunacy/slider_v16.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="slider v17" data-src="{{assetPrefix}}assets/lunacy/slider_v17.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="slider v18" data-src="{{assetPrefix}}assets/lunacy/slider_v18.webp" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
</div>
|
||||
<figcaption>Eighteen slider variants plus a separate frame <code>slider_14</code> between v13 and v15.</figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">This sequence is useful for analyzing the evolution of the control: from early layouts to final states.</p>
|
||||
|
||||
<h2 id="sliders-all">Sliders: general frame</h2>
|
||||
<p class="section__prose">A summary screen for sliders—following the version series.</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
||||
alt="All Sliders: Overview"
|
||||
data-src="{{assetPrefix}}assets/lunacy/sliders_all_set.webp"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption><code>sliders_all_set</code></figcaption>
|
||||
</figure>
|
||||
<p class="section__prose">This concludes the section on sliders: context—how multiple controls coexist on a single screen.</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{> projects-related}}{{> projects-section}}
|
||||
{{> contact-section}}
|
||||
{{> footer}}
|
||||
{{> cookie-notice}}
|
||||
{{> nav-more-floating}}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded',function(){
|
||||
var s=document.body.dataset.currentCase;
|
||||
if(!s)return;
|
||||
document.querySelectorAll('[data-related-slug="'+s+'"]').forEach(function(el){el.hidden=true;});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="dark" data-radius="medium" data-font="merriweather">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Case Study: Redesign and Development of the MANU Game Engine Interface—Visual System and Complex User Scenarios."
|
||||
/>
|
||||
<title>MANU Game Engine — Case Study</title>
|
||||
{{> head-links}}
|
||||
<style>
|
||||
.case-article {
|
||||
max-width: 48rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.case-article h2 {
|
||||
margin: 2rem 0 0.75rem;
|
||||
font-size: clamp(1.15rem, 2vw, 1.35rem);
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article .section__prose {
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article a:not(.btn) {
|
||||
color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var t = JSON.parse(localStorage.getItem("siteThemeV1") || "{}");
|
||||
var d = document.documentElement;
|
||||
var m = t.mode;
|
||||
if (m !== "light" && m !== "dark" && m !== "system") m = "system";
|
||||
var eff =
|
||||
m === "system"
|
||||
? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: m;
|
||||
d.dataset.theme = eff;
|
||||
var h = typeof t.h === "number" ? t.h : typeof t.hue === "number" ? t.hue : null;
|
||||
var s = typeof t.s === "number" ? t.s : typeof t.sat === "number" ? t.sat : null;
|
||||
var l = typeof t.l === "number" ? t.l : typeof t.light === "number" ? t.light : null;
|
||||
if (h !== null) d.style.setProperty("--theme-h", String(h));
|
||||
if (s !== null) d.style.setProperty("--theme-s", String(s));
|
||||
if (l !== null) {
|
||||
var L = Number(l);
|
||||
if (Number.isFinite(L)) d.style.setProperty("--theme-l", String(Math.min(70, Math.max(20, L))));
|
||||
}
|
||||
var rad = t.radius;
|
||||
if (rad === "soft" || rad === "medium" || rad === "strong") {
|
||||
d.dataset.radius = rad;
|
||||
} else {
|
||||
d.dataset.radius = "medium";
|
||||
}
|
||||
var f = t.font;
|
||||
if (f === "onest") f = "geologica";
|
||||
if (f === "anonymous") f = "mono";
|
||||
if (f === "merriweather" || f === "mono" || f === "geologica") {
|
||||
d.dataset.font = f;
|
||||
} else {
|
||||
d.dataset.font = "merriweather";
|
||||
}
|
||||
var mb = t.meshBg;
|
||||
if (mb === "off") {
|
||||
d.dataset.meshBg = "off";
|
||||
} else {
|
||||
d.dataset.meshBg = "on";
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body data-current-case="manu-game-maker">
|
||||
{{> header-case}}
|
||||
{{> fab-stack}}
|
||||
<main id="case-main" class="section section--tight">
|
||||
<div class="section__inner case-article">
|
||||
<p class="hero__eyebrow">Product Design · Desktop App</p>
|
||||
<h1 class="case-page__h1">MANU Game Engine</h1>
|
||||
<p class="case-page__sub">
|
||||
Interface redesign and development: a new operational concept, an updated visual system, and hundreds of carefully crafted user scenarios.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
The work focused on the desktop editor and associated panels: the goal was to maintain the integrity of the visual language while expanding functionality and ensuring that the usage scenarios of power users were preserved.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
A detailed case study will be posted on this page later; below is the cover image for portfolio navigation.
|
||||
</p>
|
||||
<h2>Cover</h2>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/manu-game-maker/cover.webp"
|
||||
alt="MANU Game Engine Interface — Case Cover"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Current preview for the portfolio card</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{> projects-related}}{{> projects-section}}
|
||||
{{> contact-section}}
|
||||
{{> footer}}
|
||||
{{> cookie-notice}}
|
||||
{{> nav-more-floating}}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded',function(){
|
||||
var s=document.body.dataset.currentCase;
|
||||
if(!s)return;
|
||||
document.querySelectorAll('[data-related-slug="'+s+'"]').forEach(function(el){el.hidden=true;});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,643 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="dark" data-radius="medium" data-font="merriweather">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta
|
||||
name="description"
|
||||
content=""Activity" Section Card Catalog: Types, Cell Widths, and a Live Masonry Grid."
|
||||
/>
|
||||
<title>Activity — Card Catalog</title>
|
||||
{{> head-links}}
|
||||
<style>
|
||||
#main {
|
||||
padding-bottom: clamp(3.5rem, 8vw, 5.5rem);
|
||||
}
|
||||
|
||||
.case-article.mp-showcase-page {
|
||||
max-width: 56rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.case-article.mp-showcase-page .case-page__sub {
|
||||
margin-bottom: clamp(1.5rem, 4vw, 2.25rem);
|
||||
}
|
||||
|
||||
.case-article.mp-showcase-page .section__prose {
|
||||
margin-bottom: 1.1rem;
|
||||
line-height: 1.65;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.case-article.mp-showcase-page > h2 {
|
||||
margin: 0 0 1rem;
|
||||
padding-top: clamp(2.5rem, 5vw, 3.5rem);
|
||||
font-family: var(--font);
|
||||
font-size: clamp(1.15rem, 2vw, 1.35rem);
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
scroll-margin-top: 1.5rem;
|
||||
border-top: 1px solid color-mix(in srgb, var(--line) 90%, transparent);
|
||||
}
|
||||
|
||||
.case-article.mp-showcase-page > h2#intro {
|
||||
padding-top: clamp(1.25rem, 3vw, 2rem);
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.mp-showcase-toc {
|
||||
margin: 0 0 1.75rem;
|
||||
padding: 1rem 1.15rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--surface);
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.mp-showcase-toc ol {
|
||||
margin: 0.35rem 0 0;
|
||||
padding-left: 1.2rem;
|
||||
}
|
||||
|
||||
.mp-showcase-toc a {
|
||||
color: var(--accent-text);
|
||||
}
|
||||
|
||||
.mp-showcase-specimens {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
margin: 1.25rem 0 0.5rem;
|
||||
}
|
||||
|
||||
.mp-showcase-specimen-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(14rem, 1fr) minmax(12rem, 1.15fr);
|
||||
gap: 1rem 1.75rem;
|
||||
align-items: start;
|
||||
padding-bottom: 2.25rem;
|
||||
margin-bottom: 2.25rem;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--line) 88%, transparent);
|
||||
}
|
||||
|
||||
.mp-showcase-specimen-row:last-child {
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
@media (max-width: 719px) {
|
||||
.mp-showcase-specimen-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.mp-showcase-specimen__card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.45rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mp-showcase-specimen__desc .section__prose {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mp-showcase-specimen__desc .section__prose strong {
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.mp-showcase-specimen__label {
|
||||
margin: 0;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.4;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.mp-showcase-specimen__card .mp-card {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
/* На главной тоже, но в узкой колонке демо center давал пустоту над заголовком */
|
||||
.mp-showcase-specimen__card .mp-card--horizontal .mp-card__body {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.mp-showcase-grid-section {
|
||||
padding-top: clamp(2.75rem, 6vw, 4.5rem);
|
||||
}
|
||||
|
||||
.mp-showcase-grid-section .section__title {
|
||||
margin-bottom: 0.85rem;
|
||||
}
|
||||
|
||||
.mp-showcase-grid-section .section__prose {
|
||||
margin-bottom: clamp(1.5rem, 3vw, 2.25rem);
|
||||
}
|
||||
|
||||
.mp-showcase-cells-table {
|
||||
width: 100%;
|
||||
margin: 1rem 0 0.5rem;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.mp-showcase-cells-table th,
|
||||
.mp-showcase-cells-table td {
|
||||
padding: 0.55rem 0.65rem;
|
||||
border: 1px solid var(--line);
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.mp-showcase-cells-table th {
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
background: color-mix(in srgb, var(--surface) 88%, var(--accent));
|
||||
}
|
||||
|
||||
.mp-showcase-cells-demo {
|
||||
margin: 1.35rem 0 1.65rem;
|
||||
padding: 1.1rem 1.15rem 1.2rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-md);
|
||||
background: color-mix(in srgb, var(--surface) 90%, var(--accent));
|
||||
}
|
||||
|
||||
.mp-showcase-cells-demo__variant {
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
.mp-showcase-cells-demo__variant:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mp-showcase-cells-demo__legend {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.85rem;
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mp-showcase-cells-demo__legend--6 {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
}
|
||||
|
||||
.mp-showcase-cells-demo__legend--4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
.mp-showcase-cells-demo__legend--2 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.mp-showcase-cells-demo__legend span {
|
||||
padding: 0.2rem 0;
|
||||
border-radius: var(--radius-sm);
|
||||
background: color-mix(in srgb, var(--line) 35%, transparent);
|
||||
}
|
||||
|
||||
.mp-showcase-cells-demo__row-label {
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
margin: 0 0 0.55rem;
|
||||
}
|
||||
|
||||
.mp-showcase-cells-demo__grid {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.55rem;
|
||||
}
|
||||
|
||||
.mp-showcase-cells-demo__grid--6 {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
}
|
||||
|
||||
.mp-showcase-cells-demo__grid--4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
.mp-showcase-cells-demo__grid--2 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.mp-showcase-cells-demo__block {
|
||||
min-height: 2.35rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 42%, var(--line));
|
||||
background: color-mix(in srgb, var(--accent) 14%, var(--surface));
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
color: var(--accent-text);
|
||||
}
|
||||
|
||||
.mp-showcase-cells-demo__block--1 {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.mp-showcase-cells-demo__block--2 {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.mp-showcase-cells-demo__block--3 {
|
||||
grid-column: span 3;
|
||||
}
|
||||
|
||||
.mp-showcase-cells-demo__block--4 {
|
||||
grid-column: span 4;
|
||||
}
|
||||
|
||||
.mp-showcase-cells-demo__caption {
|
||||
margin: 0.85rem 0 0;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.45;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.mp-showcase-cells-demo__row-label {
|
||||
margin: 0.65rem 0 0.35rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.mp-showcase-grid-section .mini-projects-grid {
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var raw = localStorage.getItem("siteThemeV1");
|
||||
if (!raw) return;
|
||||
var t = JSON.parse(raw);
|
||||
var d = document.documentElement;
|
||||
if (t.mode === "light" || t.mode === "dark") d.dataset.theme = t.mode;
|
||||
if (t.radius === "soft" || t.radius === "medium" || t.radius === "strong") d.dataset.radius = t.radius;
|
||||
var f = t.font;
|
||||
if (f === "onest") f = "geologica";
|
||||
if (f === "anonymous") f = "mono";
|
||||
if (f === "merriweather" || f === "mono" || f === "geologica") d.dataset.font = f;
|
||||
if (t.meshBg === "off") d.dataset.meshBg = "off";
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
{{> header-minimal}}
|
||||
{{> fab-stack-home-contact}}
|
||||
<main id="main">
|
||||
<section class="section section--tight">
|
||||
<div class="section__inner">
|
||||
<div class="case-article mp-showcase-page">
|
||||
<p class="hero__eyebrow">Design System · Activity</p>
|
||||
<h1 class="case-page__h1">Activity Cards</h1>
|
||||
<p class="case-page__sub">All card types and a live grid for selecting layouts on the homepage</p>
|
||||
<p class="section__prose">
|
||||
Layout and content page: the same classes and scripts are used here as in the section on the homepage. Change the font and theme in the panel on the right;
|
||||
narrow the window, and the grid will recalculate. The text in the card headers is just placeholder text; you can remove it when previewing.
|
||||
</p>
|
||||
|
||||
<nav class="mp-showcase-toc" aria-label="Catalog Table of Contents">
|
||||
<p><strong>Sections</strong></p>
|
||||
<ol>
|
||||
<li><a href="#types">Card Types</a></li>
|
||||
<li><a href="#cells">Width in Cells</a></li>
|
||||
<li><a href="#grid">Live Masonry Grid</a></li>
|
||||
<li><a href="#fixed-height">Fixed height</a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<h2 id="intro">How to Use</h2>
|
||||
<p class="section__prose">
|
||||
First, select <strong>a type</strong> (stack, horizontal, compact, decor), then <strong>data-mp-cells</strong> (1–4) and
|
||||
the order <code>li</code> in the list. Cards with <code>data-mp-extra</code> do not affect the height of the list until the “Show All” button is clicked.
|
||||
For more details, see <code>docs/mini-projects-cards.md</code>.
|
||||
</p>
|
||||
|
||||
<h2 id="types">Card Types</h2>
|
||||
<p class="section__prose">
|
||||
On the left is an example of a text card; on the right is the type name and an explanation of the classes.
|
||||
</p>
|
||||
{{> mini-projects-showcase-specimens}}
|
||||
|
||||
<h2 id="cells">Width in Cells</h2>
|
||||
<p class="section__prose">
|
||||
Attribute <code>data-mp-cells</code> sets <code>li.mini-projects-grid__item</code> specifies the desired width from 1 to 4 cells.
|
||||
The script will not allow the card to be wider than the current row.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
For responsive width, use <code>data-mp-cells-sm</code> (for 4 cells per row, ≥ 880 px) and
|
||||
<code>data-mp-cells-xs</code> (for 2 cells, ≥ 520 px). The script selects the value based on the current number of cells in a row.
|
||||
</p>
|
||||
<div class="mp-showcase-attrs-table">
|
||||
<table class="mp-showcase-cells-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">The</th>
|
||||
<th scope="col">When to use</th>
|
||||
<th scope="col">Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>data-mp-cells</code></td>
|
||||
<td>Wide screen (6 cells, ≥ 1180 px)</td>
|
||||
<td><code>data-mp-cells="3"</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>data-mp-cells-sm</code></td>
|
||||
<td>Medium screen (4 cells, ≥ 880 px)</td>
|
||||
<td><code>data-mp-cells-sm="2"</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>data-mp-cells-xs</code></td>
|
||||
<td>Narrow screen (2 cells, ≥ 520 px)</td>
|
||||
<td><code>data-mp-cells-xs="1"</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="section__prose" style="margin-top:0.55rem">
|
||||
<strong>Example:</strong> <code>data-mp-cells="3" data-mp-cells-sm="2"</code> — A card takes up 3 cells on a wide screen, 2 on a medium screen, and 1 on a narrow screen.
|
||||
</p>
|
||||
<figure class="mp-showcase-cells-demo" aria-label="Chart showing the width of the cards in the cells">
|
||||
<div class="mp-showcase-cells-demo__variant">
|
||||
<div class="mp-showcase-cells-demo__legend mp-showcase-cells-demo__legend--6" aria-hidden="true">
|
||||
<span>123456</span>
|
||||
</div>
|
||||
<p class="mp-showcase-cells-demo__row-label">≥ 1180 px — 6 cells</p>
|
||||
<div class="mp-showcase-cells-demo__grid mp-showcase-cells-demo__grid--6">
|
||||
<div class="mp-showcase-cells-demo__block mp-showcase-cells-demo__block--3">3</div>
|
||||
<div class="mp-showcase-cells-demo__block mp-showcase-cells-demo__block--3">3</div>
|
||||
<div class="mp-showcase-cells-demo__block mp-showcase-cells-demo__block--2">2</div>
|
||||
<div class="mp-showcase-cells-demo__block mp-showcase-cells-demo__block--2">2</div>
|
||||
<div class="mp-showcase-cells-demo__block mp-showcase-cells-demo__block--2">2</div>
|
||||
<div class="mp-showcase-cells-demo__block mp-showcase-cells-demo__block--1">1</div>
|
||||
<div class="mp-showcase-cells-demo__block mp-showcase-cells-demo__block--2">2</div>
|
||||
<div class="mp-showcase-cells-demo__block mp-showcase-cells-demo__block--3">3</div>
|
||||
<div class="mp-showcase-cells-demo__block mp-showcase-cells-demo__block--4">4</div>
|
||||
<div class="mp-showcase-cells-demo__block mp-showcase-cells-demo__block--2">2</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mp-showcase-cells-demo__variant">
|
||||
<div class="mp-showcase-cells-demo__legend mp-showcase-cells-demo__legend--4" aria-hidden="true">
|
||||
<span>1234</span>
|
||||
</div>
|
||||
<p class="mp-showcase-cells-demo__row-label">≥ 880 px — 4 cells</p>
|
||||
<div class="mp-showcase-cells-demo__grid mp-showcase-cells-demo__grid--4">
|
||||
<div class="mp-showcase-cells-demo__block mp-showcase-cells-demo__block--3">3</div>
|
||||
<div class="mp-showcase-cells-demo__block mp-showcase-cells-demo__block--1">1</div>
|
||||
<div class="mp-showcase-cells-demo__block mp-showcase-cells-demo__block--2">2</div>
|
||||
<div class="mp-showcase-cells-demo__block mp-showcase-cells-demo__block--2">2</div>
|
||||
<div class="mp-showcase-cells-demo__block mp-showcase-cells-demo__block--1">1</div>
|
||||
<div class="mp-showcase-cells-demo__block mp-showcase-cells-demo__block--2">2</div>
|
||||
<div class="mp-showcase-cells-demo__block mp-showcase-cells-demo__block--2">2</div>
|
||||
<div class="mp-showcase-cells-demo__block mp-showcase-cells-demo__block--4">4</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mp-showcase-cells-demo__variant">
|
||||
<div class="mp-showcase-cells-demo__legend mp-showcase-cells-demo__legend--2" aria-hidden="true">
|
||||
<span>12</span>
|
||||
</div>
|
||||
<p class="mp-showcase-cells-demo__row-label">≥ 520 px — 2 cells</p>
|
||||
<div class="mp-showcase-cells-demo__grid mp-showcase-cells-demo__grid--2">
|
||||
<div class="mp-showcase-cells-demo__block mp-showcase-cells-demo__block--1">1</div>
|
||||
<div class="mp-showcase-cells-demo__block mp-showcase-cells-demo__block--1">1</div>
|
||||
<div class="mp-showcase-cells-demo__block mp-showcase-cells-demo__block--2">2</div>
|
||||
<div class="mp-showcase-cells-demo__block mp-showcase-cells-demo__block--2">2</div>
|
||||
<div class="mp-showcase-cells-demo__block mp-showcase-cells-demo__block--1">1</div>
|
||||
<div class="mp-showcase-cells-demo__block mp-showcase-cells-demo__block--1">1</div>
|
||||
</div>
|
||||
</div>
|
||||
<figcaption class="mp-showcase-cells-demo__caption">
|
||||
The number in the block = the value <code>data-mp-cells</code>. The card occupies that many cells in a row. On a narrow screen, there are fewer cells—cards with a width greater than the number of cells are carried over to the next row. As <code>data-mp-cells-sm</code> / <code>data-mp-cells-xs</code> the card changes its width when the view is narrowed.
|
||||
</figcaption>
|
||||
</figure>
|
||||
<table class="mp-showcase-cells-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">List width</th>
|
||||
<th scope="col">Cells per row</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>≥ 1180px</td>
|
||||
<td>6</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>≥ 880px</td>
|
||||
<td>4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>≥ 520px</td>
|
||||
<td>2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>already</td>
|
||||
<td>1 (entire row)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="section__prose">
|
||||
A pair on six cells <code>3 + 3</code> or a trio <code>2 + 2 + 2</code> usually looks neater than a random set.
|
||||
Decor (<code>mp-card--decor</code>) fills in the gaps in the grid.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="grid" class="section section--surface mp-showcase-grid-section" aria-labelledby="grid-title">
|
||||
<div class="section__inner">
|
||||
<h2 id="grid-title" class="section__title">Live masonry grid</h2>
|
||||
<p class="section__prose">
|
||||
A demo of the “Activity” block from the homepage: here, all card types are in a single live grid. Each card’s caption shows the type of
|
||||
layout and <code>data-mp-cells</code>. Change the window width and font in the theme panel, watch how the grid
|
||||
rearranges itself, and apply the best layout to the homepage.
|
||||
</p>
|
||||
{{> mini-projects-showcase-grid}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="fixed-height" class="section section--surface mp-showcase-grid-section" aria-labelledby="fixed-height-title">
|
||||
<div class="section__inner">
|
||||
<h2 id="fixed-height-title" class="section__title">Fixed Height</h2>
|
||||
<p class="section__prose">
|
||||
Attribute <code>data-mp-height="…"</code> sets <code>li.mini-projects-grid__item</code> sets a fixed card height in pixels. Text is truncated with an ellipsis if it doesn’t fit.
|
||||
</p>
|
||||
<ol
|
||||
id="mini-projects-fixed-height-grid"
|
||||
class="mini-projects-grid mp-showcase-grid"
|
||||
data-mini-projects-list
|
||||
>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="4" data-mp-cells-sm="4" data-mp-cells-xs="2" data-mp-height="600">
|
||||
<a class="mp-card mp-card--link mp-card--stack" href="#" aria-label="[4→4→2] height=600 · stack · link">
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="{{assetPrefix}}assets/shared/placeholder.webp"
|
||||
alt=""
|
||||
width="560"
|
||||
height="315"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
[4→4→2] height=600 · stack · link
|
||||
|
||||
<span class="mp-card__ext" aria-hidden="true"></span></h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">website</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">4 → 4 → 2. data-mp-cells-sm="4" data-mp-cells-xs="2". Height 600px.</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2" data-mp-height="600" data-mp-height-sm="400">
|
||||
<a class="mp-card mp-card--link mp-card--stack" href="#" aria-label="[2] height=600 · stack · link">
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="{{assetPrefix}}assets/shared/placeholder.webp"
|
||||
alt=""
|
||||
width="560"
|
||||
height="315"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
[2] height=600 · stack · link
|
||||
|
||||
<span class="mp-card__ext" aria-hidden="true"></span></h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">note</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Two cells, height 600px.</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2" data-mp-height="400">
|
||||
<a class="mp-card mp-card--link mp-card--stack" href="#" aria-label="[2] height=400 · stack · link">
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="{{assetPrefix}}assets/shared/placeholder.webp"
|
||||
alt=""
|
||||
width="560"
|
||||
height="315"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
[2] height=400 · stack · link
|
||||
|
||||
<span class="mp-card__ext" aria-hidden="true"></span></h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">Utility</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Two cells, 400px high.</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2" data-mp-height="400">
|
||||
<a class="mp-card mp-card--link mp-card--stack" href="#" aria-label="[2] height=400 · stack · link">
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="{{assetPrefix}}assets/shared/placeholder.webp"
|
||||
alt=""
|
||||
width="560"
|
||||
height="315"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
[2] height=400 · stack · link
|
||||
|
||||
<span class="mp-card__ext" aria-hidden="true"></span></h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">template</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Two cells, 400px tall.</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2" data-mp-height="400">
|
||||
<a class="mp-card mp-card--link mp-card--stack" href="#" aria-label="[2] height=400 · stack · link">
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="{{assetPrefix}}assets/shared/placeholder.webp"
|
||||
alt=""
|
||||
width="560"
|
||||
height="315"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
[2] height=400 · stack · link
|
||||
|
||||
<span class="mp-card__ext" aria-hidden="true"></span></h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">plugin</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Two cells, 400px tall.</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="3" data-mp-cells-sm="2" data-mp-cells-xs="1">
|
||||
<a class="mp-card mp-card--link mp-card--compact" href="#" aria-label="[3→2→1] · compact · link">
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
[3→2→1] · compact · link
|
||||
|
||||
<span class="mp-card__ext" aria-hidden="true"></span></h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">plugin</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">3 → 2 → 1. data-mp-cells-sm="2" data-mp-cells-xs="1". Auto-height.</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="3" data-mp-cells-sm="2" data-mp-cells-xs="1">
|
||||
<a class="mp-card mp-card--link mp-card--compact" href="#" aria-label="[3→2→1] · compact · link">
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
[3→2→1] · compact · link
|
||||
|
||||
<span class="mp-card__ext" aria-hidden="true"></span></h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">document</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">3 → 2 → 1. data-mp-cells-sm="2" data-mp-cells-xs="1".</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{{> footer}}
|
||||
{{> cookie-notice}}
|
||||
{{> nav-more-floating}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="dark" data-radius="medium" data-font="merriweather">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Case Study: A mobile app for a network of clinics—appointment scheduling, medical services, navigation, and quick actions."
|
||||
/>
|
||||
<title>Mobile App for a Network of Clinics — Case Study</title>
|
||||
{{> head-links}}
|
||||
<style>
|
||||
.case-article {
|
||||
max-width: 48rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.case-article h2 {
|
||||
margin: 2rem 0 0.75rem;
|
||||
font-size: clamp(1.15rem, 2vw, 1.35rem);
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article .section__prose {
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article a:not(.btn) {
|
||||
color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var t = JSON.parse(localStorage.getItem("siteThemeV1") || "{}");
|
||||
var d = document.documentElement;
|
||||
var m = t.mode;
|
||||
if (m !== "light" && m !== "dark" && m !== "system") m = "system";
|
||||
var eff =
|
||||
m === "system"
|
||||
? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: m;
|
||||
d.dataset.theme = eff;
|
||||
var h = typeof t.h === "number" ? t.h : typeof t.hue === "number" ? t.hue : null;
|
||||
var s = typeof t.s === "number" ? t.s : typeof t.sat === "number" ? t.sat : null;
|
||||
var l = typeof t.l === "number" ? t.l : typeof t.light === "number" ? t.light : null;
|
||||
if (h !== null) d.style.setProperty("--theme-h", String(h));
|
||||
if (s !== null) d.style.setProperty("--theme-s", String(s));
|
||||
if (l !== null) {
|
||||
var L = Number(l);
|
||||
if (Number.isFinite(L)) d.style.setProperty("--theme-l", String(Math.min(70, Math.max(20, L))));
|
||||
}
|
||||
var rad = t.radius;
|
||||
if (rad === "soft" || rad === "medium" || rad === "strong") {
|
||||
d.dataset.radius = rad;
|
||||
} else {
|
||||
d.dataset.radius = "medium";
|
||||
}
|
||||
var f = t.font;
|
||||
if (f === "onest") f = "geologica";
|
||||
if (f === "anonymous") f = "mono";
|
||||
if (f === "merriweather" || f === "mono" || f === "geologica") {
|
||||
d.dataset.font = f;
|
||||
} else {
|
||||
d.dataset.font = "merriweather";
|
||||
}
|
||||
var mb = t.meshBg;
|
||||
if (mb === "off") {
|
||||
d.dataset.meshBg = "off";
|
||||
} else {
|
||||
d.dataset.meshBg = "on";
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body data-current-case="mobile-clinic-app">
|
||||
{{> header-case}}
|
||||
{{> fab-stack}}
|
||||
<main id="case-main" class="section section--tight">
|
||||
<div class="section__inner case-article">
|
||||
<p class="hero__eyebrow">UX/UI · Mobile App</p>
|
||||
<h1 class="case-page__h1">Mobile App for a Clinic Network</h1>
|
||||
<p class="case-page__sub">
|
||||
Booking doctor appointments and managing medical services: a simple user journey, intuitive navigation, and quick access to actions.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
The project’s focus is on alleviating patient anxiety and eliminating unnecessary steps: clear appointment statuses, predictable forms, and reliance on familiar mobile app patterns.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
A full breakdown of the case study will be published here; for now, this page serves as an entry point from the portfolio section on the homepage.
|
||||
</p>
|
||||
<h2>Cover</h2>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/mobile-clinic-app/cover.webp"
|
||||
alt="Mobile App for a Network of Clinics — Case Study Cover"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Current preview for the portfolio card</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{> projects-related}}{{> projects-section}}
|
||||
{{> contact-section}}
|
||||
{{> footer}}
|
||||
{{> cookie-notice}}
|
||||
{{> nav-more-floating}}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded',function(){
|
||||
var s=document.body.dataset.currentCase;
|
||||
if(!s)return;
|
||||
document.querySelectorAll('[data-related-slug="'+s+'"]').forEach(function(el){el.hidden=true;});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,150 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="dark" data-radius="medium" data-font="merriweather">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Case Study: A Column-Based Plugin for Obsidian — Design and UX of a Note-Taking Plugin."
|
||||
/>
|
||||
<title>Column View for Obsidian — Case Study</title>
|
||||
{{> head-links}}
|
||||
{{> head-plyr}}
|
||||
<style>
|
||||
.case-article {
|
||||
max-width: 48rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.case-article h2 {
|
||||
margin: 2rem 0 0.75rem;
|
||||
font-size: clamp(1.15rem, 2vw, 1.35rem);
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article h3 {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article .section__prose {
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article a:not(.btn) {
|
||||
color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var t = JSON.parse(localStorage.getItem("siteThemeV1") || "{}");
|
||||
var d = document.documentElement;
|
||||
var m = t.mode;
|
||||
if (m !== "light" && m !== "dark" && m !== "system") m = "system";
|
||||
var eff =
|
||||
m === "system"
|
||||
? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: m;
|
||||
d.dataset.theme = eff;
|
||||
var h = typeof t.h === "number" ? t.h : typeof t.hue === "number" ? t.hue : null;
|
||||
var s = typeof t.s === "number" ? t.s : typeof t.sat === "number" ? t.sat : null;
|
||||
var l = typeof t.l === "number" ? t.l : typeof t.light === "number" ? t.light : null;
|
||||
if (h !== null) d.style.setProperty("--theme-h", String(h));
|
||||
if (s !== null) d.style.setProperty("--theme-s", String(s));
|
||||
if (l !== null) {
|
||||
var L = Number(l);
|
||||
if (Number.isFinite(L)) d.style.setProperty("--theme-l", String(Math.min(70, Math.max(20, L))));
|
||||
}
|
||||
var rad = t.radius;
|
||||
if (rad === "soft" || rad === "medium" || rad === "strong") {
|
||||
d.dataset.radius = rad;
|
||||
} else {
|
||||
d.dataset.radius = "medium";
|
||||
}
|
||||
var f = t.font;
|
||||
if (f === "onest") f = "geologica";
|
||||
if (f === "anonymous") f = "mono";
|
||||
if (f === "merriweather" || f === "mono" || f === "geologica") {
|
||||
d.dataset.font = f;
|
||||
} else {
|
||||
d.dataset.font = "merriweather";
|
||||
}
|
||||
var mb = t.meshBg;
|
||||
if (mb === "off") {
|
||||
d.dataset.meshBg = "off";
|
||||
} else {
|
||||
d.dataset.meshBg = "on";
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
{{> header-case}}
|
||||
{{> fab-stack}}
|
||||
<main id="case-main" class="section section--tight">
|
||||
<div class="section__inner case-article">
|
||||
<p class="hero__eyebrow">Obsidian · Plugin</p>
|
||||
<h1 class="case-page__h1">Column View — a plugin for Obsidian</h1>
|
||||
<p class="case-page__sub">Design and development of an interface for multi-column note management</p>
|
||||
|
||||
<p class="section__prose">
|
||||
<!-- Здесь будет введение -->
|
||||
</p>
|
||||
|
||||
<h2 id="context">Context</h2>
|
||||
<p class="section__prose">
|
||||
<!-- Контекст проекта -->
|
||||
</p>
|
||||
|
||||
<h2 id="task">Task</h2>
|
||||
<p class="section__prose">
|
||||
<!-- Описание задачи -->
|
||||
</p>
|
||||
|
||||
<h2 id="solution">Solution</h2>
|
||||
<p class="section__prose">
|
||||
<!-- Что было сделано -->
|
||||
</p>
|
||||
|
||||
<h3>Usage Filter</h3>
|
||||
<figure class="case-media">
|
||||
<div class="case-video-shell">
|
||||
<video controls preload="metadata" playsinline poster="{{assetPrefix}}assets/obsidian-plugin-columns-view/filter use.webp" data-case-video-player style="display: block; width: 100%; height: auto">
|
||||
<source src="{{assetPrefix}}assets/obsidian-plugin-columns-view/filter use.webm" type="video/webm" />
|
||||
<source src="{{assetPrefix}}assets/obsidian-plugin-columns-view/filter use.mp4" type="video/mp4" />
|
||||
Your browser does not support video.
|
||||
</video>
|
||||
</div>
|
||||
<figcaption>Filtering notes in a columnar view.</figcaption>
|
||||
</figure>
|
||||
|
||||
<h3>Display Configuration</h3>
|
||||
<figure class="case-media">
|
||||
<div class="case-video-shell">
|
||||
<video controls preload="metadata" playsinline poster="{{assetPrefix}}assets/obsidian-plugin-columns-view/view configure.webp" data-case-video-player style="display: block; width: 100%; height: auto">
|
||||
<source src="{{assetPrefix}}assets/obsidian-plugin-columns-view/view configure.webm" type="video/webm" />
|
||||
<source src="{{assetPrefix}}assets/obsidian-plugin-columns-view/view configure.mp4" type="video/mp4" />
|
||||
Your browser does not support video.
|
||||
</video>
|
||||
</div>
|
||||
<figcaption>Column and display settings.</figcaption>
|
||||
</figure>
|
||||
|
||||
<h2 id="result">Result</h2>
|
||||
<p class="section__prose">
|
||||
<!-- Итоги -->
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{> projects-related}}{{> projects-section}}
|
||||
{{> contact-section}}
|
||||
{{> footer}}
|
||||
{{> cookie-notice}}
|
||||
{{> nav-more-floating}}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,262 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="dark" data-radius="medium" data-font="merriweather">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Case Study: Redesign of the Primo RPA Orchestrator Web Dashboard — design oversight, implementation of Ant Design and Storybook, and token synchronization with Figma."
|
||||
/>
|
||||
<title>Web Dashboard for Automation Orchestration — Case Study</title>
|
||||
{{> head-links}}
|
||||
<style>
|
||||
.case-article {
|
||||
max-width: 48rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.case-article h2 {
|
||||
margin: 2rem 0 0.75rem;
|
||||
font-size: clamp(1.15rem, 2vw, 1.35rem);
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article h3 {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
margin: 1.25rem 0 0.5rem;
|
||||
}
|
||||
.case-article .section__prose {
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 0.75rem 0 1rem;
|
||||
}
|
||||
.case-article ul {
|
||||
margin: 0.5rem 0 1rem 1.25rem;
|
||||
padding: 0;
|
||||
}
|
||||
.case-article li {
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
.case-article a:not(.btn) {
|
||||
color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var t = JSON.parse(localStorage.getItem("siteThemeV1") || "{}");
|
||||
var d = document.documentElement;
|
||||
var m = t.mode;
|
||||
if (m !== "light" && m !== "dark" && m !== "system") m = "system";
|
||||
var eff =
|
||||
m === "system"
|
||||
? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: m;
|
||||
d.dataset.theme = eff;
|
||||
var h = typeof t.h === "number" ? t.h : typeof t.hue === "number" ? t.hue : null;
|
||||
var s = typeof t.s === "number" ? t.s : typeof t.sat === "number" ? t.sat : null;
|
||||
var l = typeof t.l === "number" ? t.l : typeof t.light === "number" ? t.light : null;
|
||||
if (h !== null) d.style.setProperty("--theme-h", String(h));
|
||||
if (s !== null) d.style.setProperty("--theme-s", String(s));
|
||||
if (l !== null) {
|
||||
var L = Number(l);
|
||||
if (Number.isFinite(L)) d.style.setProperty("--theme-l", String(Math.min(70, Math.max(20, L))));
|
||||
}
|
||||
var rad = t.radius;
|
||||
if (rad === "soft" || rad === "medium" || rad === "strong") {
|
||||
d.dataset.radius = rad;
|
||||
} else {
|
||||
d.dataset.radius = "medium";
|
||||
}
|
||||
var f = t.font;
|
||||
if (f === "onest") f = "geologica";
|
||||
if (f === "anonymous") f = "mono";
|
||||
if (f === "merriweather" || f === "mono" || f === "geologica") {
|
||||
d.dataset.font = f;
|
||||
} else {
|
||||
d.dataset.font = "merriweather";
|
||||
}
|
||||
var mb = t.meshBg;
|
||||
if (mb === "off") {
|
||||
d.dataset.meshBg = "off";
|
||||
} else {
|
||||
d.dataset.meshBg = "on";
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body data-current-case="orc-automation-panel">
|
||||
{{> header-case}}
|
||||
{{> fab-stack}}
|
||||
<main id="case-main" class="section section--tight">
|
||||
<div class="section__inner case-article">
|
||||
<p class="hero__eyebrow">Primo RPA · UX/UI · Web App</p>
|
||||
<h1 class="case-page__h1">Web Dashboard for Automation Orchestration</h1>
|
||||
<p class="case-page__sub">
|
||||
Primo RPA Orchestrator: launching robots, managing projects, and monitoring processes in a web dashboard for administrators and automation developers.
|
||||
</p>
|
||||
|
||||
<div class="case-block" id="obzor">
|
||||
<h2>Context</h2>
|
||||
<p class="section__prose">
|
||||
In the Primo RPA stack, the orchestrator manages the scheduling, queue, and statuses of RPA robots on the client’s infrastructure. It can be accessed not only from the development studio but also from a browser: users view projects, restart tasks, and troubleshoot failures.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
By that point, the product was already in use, but the interface was noticeably outdated: users faced navigation issues, visual clutter in tables, and confusing workflows in some sections. The goal was to refresh the interface’s look, modernize it, and fix the accumulated UX issues.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="rol">
|
||||
<h2>Role and Approach</h2>
|
||||
<p class="section__prose">
|
||||
My role in this project was more of a curatorial one—I didn’t do the design myself, but rather structured the process so that the design and development teams could work in sync.
|
||||
</p>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">💡</span>
|
||||
<div class="case-callout__content">
|
||||
<p>I studied the product “from scratch”: I analyzed the orchestrator interface, identified user pain points, and formulated objectives for the update.</p>
|
||||
</div>
|
||||
</aside>
|
||||
<p class="section__prose">
|
||||
At the same time, I was looking for a designer who could take on the main visual design work. Elizaveta was selected—out of more than 150 applications, she was the best fit for the upcoming workload.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
Over the course of three months, I brought the new team member up to speed on the project details: I assigned tasks, provided feedback on the mockups she created, and shared context on user scenarios. Together with the development team, we planned the order in which to update the service’s design—we revised screens based on priority, incorporating changes into ongoing releases, without “stopping everything and redesigning from scratch.”
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="tekhnicheskiy-stek">
|
||||
<h2>Technical Stack and Tools</h2>
|
||||
<p class="section__prose">
|
||||
The latest version of <a href="https://ant.design/" target="_blank" rel="noopener noreferrer">Ant Design</a> was chosen as the UI framework. It was already in use on the project, and its latest release introduced <a href="https://ant.design/theme-editor" target="_blank" rel="noopener noreferrer">the Theme Editor</a>—a feature that allowed us to customize the appearance of components using design tokens without manually overriding styles. This provided both designers and developers with a unified language for describing the user interface.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
<a href="https://storybook.js.org/" target="_blank" rel="noopener noreferrer">Storybook</a> served as the single source of truth and the bridge between design and development. In Storybook, components weren’t just documented—they actually worked in an isolated environment. This allowed designers to see the elements in action and developers to integrate components into the dashboard more quickly.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="dizayn-sistema">
|
||||
<h2>Design System and Synchronization</h2>
|
||||
<p class="section__prose">
|
||||
To streamline our workflow, we synchronized tokens from Ant Design with those in Figma—colors, padding, typography, and corner radii were standardized to consistent values. Here’s how it worked: if tokens were updated in Figma, they could be exported as JSON and immediately imported into the code without any issues—instead of having a developer manually search for changes and update each token individually. This approach also allowed us to view the design more systematically.
|
||||
</p>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">📌</span>
|
||||
<div class="case-callout__content">
|
||||
<p>The main goal of this close collaboration between design and development was to translate the original vision into the product as accurately as possible without losing sight of the details along the way.</p>
|
||||
</div>
|
||||
</aside>
|
||||
<p class="section__prose">
|
||||
Components in Figma were built from scratch—based on Ant Design references, but without using pre-made libraries. The decision to avoid pre-made solutions was a conscious one: we needed full customization to fit our workflow and the ability to use new Figma features that weren’t supported in libraries from the Figma community.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
For Storybook, we selected and configured plugins that were relevant to our tasks—accessibility checks, viewing component states, and switching themes and locales. This turned Storybook into more than just a showcase for components; it became a full-fledged design review tool.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="rabochaya-sreda">
|
||||
<h2>Designer’s Work Environment</h2>
|
||||
<p class="section__prose">
|
||||
We set up the environment so that designers could see the live implementation of components on their own computers using a local Storybook instance and Git. This allowed for more precise control over how the intended design was translated into code and enabled us to quickly spot discrepancies early on.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
We also discussed a step to accelerate Storybook support by allowing designers to write the JSX for implemented components themselves—this shortened the path from mockup to a documented component in Storybook.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
We fine-tuned the workflow for adding icons to the library. The key feature was that icons could be displayed in Storybook with filtering and sorting capabilities. At the same time, designers could update icons without involving a developer—they simply pushed new files to the repository.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
Being able to see the updated icon immediately in the browser allowed us to optimize it better and eliminate display glitches when using Boolean operations.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
We also developed an approach and set of rules for Figma that not only guided the designer but also ensured the creation of consistent and functional icons.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="ux-uluchsheniya">
|
||||
<h2>UX Improvements</h2>
|
||||
<p class="section__prose">
|
||||
During the interface update, Elizaveta also proposed a number of user experience improvements. Some of them were successfully implemented—for example, clearer grouping of elements in complex forms and redesigned loading states. However, the priority was not to disrupt the existing UX that users had grown accustomed to, so many ideas were saved for the backlog for the next iteration.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="proekty">
|
||||
<h2>Project List</h2>
|
||||
<p class="section__prose">
|
||||
The project table is one of the most frequently viewed screens. In the old version, there were many columns, a dense grid, and small text: the project status and name were difficult to read, especially on a laptop.
|
||||
</p>
|
||||
<p class="section__prose">In the new version:</p>
|
||||
<ul>
|
||||
<li>the columns have been reduced to what’s necessary for daily work;</li>
|
||||
<li>statuses and actions have been moved to predictable areas within each row;</li>
|
||||
<li>indentation and typography have been aligned with the rest of the panel using Ant Design.</li>
|
||||
</ul>
|
||||
|
||||
<h3>It used to be</h3>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/orc-primo-rpa/projects--table-old.webp"
|
||||
alt="List of projects in the orchestrator — previous version of the table"
|
||||
width="1919"
|
||||
height="867"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
</figure>
|
||||
|
||||
<h3>Now</h3>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/orc-primo-rpa/projects--table-new.webp"
|
||||
alt="List of Projects in the Orchestrator — Updated Table"
|
||||
width="1366"
|
||||
height="768"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
<div class="case-block" id="itog">
|
||||
<h2>Results and Reflection</h2>
|
||||
<p class="section__prose">
|
||||
The projects screen became easier to scan: less visual noise, and it’s faster to find the right project and its status. After rolling out the new design to several clients, we received positive feedback and many ideas for further improvements.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
I stepped away from the project once Elizaveta felt confident in her role and was able to handle design tasks independently. I occasionally answered specific questions, but overall, the handoff went smoothly.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
A little over a year later, the team’s processes have changed significantly. According to the team, tools powered by neural networks—such as Claude Design—are now actively involved in the creation and handoff of designs. What we used to build manually using tokens, Storybook, and environment synchronization is now, to some extent, automated.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
I believe the work was completed fairly quickly and to a high standard—amid constant changes in the market and within the company, we managed not only to update the interface but also to establish a sustainable process that continued to function even without my involvement.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{> projects-related}}{{> projects-section}}
|
||||
{{> contact-section}}
|
||||
{{> footer}}
|
||||
{{> cookie-notice}}
|
||||
{{> nav-more-floating}}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded',function(){
|
||||
var s=document.body.dataset.currentCase;
|
||||
if(!s)return;
|
||||
document.querySelectorAll('[data-related-slug="'+s+'"]').forEach(function(el){el.hidden=true;});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,133 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="dark" data-radius="medium" data-font="merriweather">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Case Study: This website was developed using an AI IDE."
|
||||
/>
|
||||
<title>This Website — Developed with AI IDE — Case Study</title>
|
||||
{{> head-links}}
|
||||
{{> head-plyr}}
|
||||
<style>
|
||||
.case-article {
|
||||
max-width: 48rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.case-article h2 {
|
||||
margin: 2rem 0 0.75rem;
|
||||
font-size: clamp(1.15rem, 2vw, 1.35rem);
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article h3 {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article .section__prose {
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article a:not(.btn) {
|
||||
color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var t = JSON.parse(localStorage.getItem("siteThemeV1") || "{}");
|
||||
var d = document.documentElement;
|
||||
var m = t.mode;
|
||||
if (m !== "light" && m !== "dark" && m !== "system") m = "system";
|
||||
var eff =
|
||||
m === "system"
|
||||
? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: m;
|
||||
d.dataset.theme = eff;
|
||||
var h = typeof t.h === "number" ? t.h : typeof t.hue === "number" ? t.hue : null;
|
||||
var s = typeof t.s === "number" ? t.s : typeof t.sat === "number" ? t.sat : null;
|
||||
var l = typeof t.l === "number" ? t.l : typeof t.light === "number" ? t.light : null;
|
||||
if (h !== null) d.style.setProperty("--theme-h", String(h));
|
||||
if (s !== null) d.style.setProperty("--theme-s", String(s));
|
||||
if (l !== null) {
|
||||
var L = Number(l);
|
||||
if (Number.isFinite(L)) d.style.setProperty("--theme-l", String(Math.min(70, Math.max(20, L))));
|
||||
}
|
||||
var rad = t.radius;
|
||||
if (rad === "soft" || rad === "medium" || rad === "strong") {
|
||||
d.dataset.radius = rad;
|
||||
} else {
|
||||
d.dataset.radius = "medium";
|
||||
}
|
||||
var f = t.font;
|
||||
if (f === "onest") f = "geologica";
|
||||
if (f === "anonymous") f = "mono";
|
||||
if (f === "merriweather" || f === "mono" || f === "geologica") {
|
||||
d.dataset.font = f;
|
||||
} else {
|
||||
d.dataset.font = "merriweather";
|
||||
}
|
||||
var mb = t.meshBg;
|
||||
if (mb === "off") {
|
||||
d.dataset.meshBg = "off";
|
||||
} else {
|
||||
d.dataset.meshBg = "on";
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body data-current-case="site-with-ai-ide">
|
||||
{{> header-case}}
|
||||
{{> fab-stack}}
|
||||
<main id="case-main" class="section section--tight">
|
||||
<div class="section__inner case-article">
|
||||
<p class="hero__eyebrow">Development · AI IDE</p>
|
||||
<h1 class="case-page__h1">This website</h1>
|
||||
<p class="case-page__sub">Website Development Using AI IDE</p>
|
||||
|
||||
<p class="section__prose">
|
||||
<!-- Здесь будет введение -->
|
||||
</p>
|
||||
|
||||
<h2 id="context">Context</h2>
|
||||
<p class="section__prose">
|
||||
<!-- Контекст проекта -->
|
||||
</p>
|
||||
|
||||
<h2 id="task">Task</h2>
|
||||
<p class="section__prose">
|
||||
<!-- Описание задачи -->
|
||||
</p>
|
||||
|
||||
<h2 id="solution">Solution</h2>
|
||||
<p class="section__prose">
|
||||
<!-- Что было сделано -->
|
||||
</p>
|
||||
|
||||
<h2 id="result">Result</h2>
|
||||
<p class="section__prose">
|
||||
<!-- Итоги -->
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{> mini-projects-related}}
|
||||
{{> contact-section}}
|
||||
{{> footer}}
|
||||
{{> cookie-notice}}
|
||||
{{> nav-more-floating}}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded',function(){
|
||||
var s=document.body.dataset.currentCase;
|
||||
if(!s)return;
|
||||
document.querySelectorAll('[data-related-slug="'+s+'"]').forEach(function(el){el.hidden=true;});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="dark" data-radius="medium" data-font="merriweather">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Case Study: A Task-Assignment Service Concept—Transparency in Processes and Teamwork."
|
||||
/>
|
||||
<title>Task Distribution Service Concept — Case Study</title>
|
||||
{{> head-links}}
|
||||
<style>
|
||||
.case-article {
|
||||
max-width: 48rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.case-article h2 {
|
||||
margin: 2rem 0 0.75rem;
|
||||
font-size: clamp(1.15rem, 2vw, 1.35rem);
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article .section__prose {
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article a:not(.btn) {
|
||||
color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var t = JSON.parse(localStorage.getItem("siteThemeV1") || "{}");
|
||||
var d = document.documentElement;
|
||||
var m = t.mode;
|
||||
if (m !== "light" && m !== "dark" && m !== "system") m = "system";
|
||||
var eff =
|
||||
m === "system"
|
||||
? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: m;
|
||||
d.dataset.theme = eff;
|
||||
var h = typeof t.h === "number" ? t.h : typeof t.hue === "number" ? t.hue : null;
|
||||
var s = typeof t.s === "number" ? t.s : typeof t.sat === "number" ? t.sat : null;
|
||||
var l = typeof t.l === "number" ? t.l : typeof t.light === "number" ? t.light : null;
|
||||
if (h !== null) d.style.setProperty("--theme-h", String(h));
|
||||
if (s !== null) d.style.setProperty("--theme-s", String(s));
|
||||
if (l !== null) {
|
||||
var L = Number(l);
|
||||
if (Number.isFinite(L)) d.style.setProperty("--theme-l", String(Math.min(70, Math.max(20, L))));
|
||||
}
|
||||
var rad = t.radius;
|
||||
if (rad === "soft" || rad === "medium" || rad === "strong") {
|
||||
d.dataset.radius = rad;
|
||||
} else {
|
||||
d.dataset.radius = "medium";
|
||||
}
|
||||
var f = t.font;
|
||||
if (f === "onest") f = "geologica";
|
||||
if (f === "anonymous") f = "mono";
|
||||
if (f === "merriweather" || f === "mono" || f === "geologica") {
|
||||
d.dataset.font = f;
|
||||
} else {
|
||||
d.dataset.font = "merriweather";
|
||||
}
|
||||
var mb = t.meshBg;
|
||||
if (mb === "off") {
|
||||
d.dataset.meshBg = "off";
|
||||
} else {
|
||||
d.dataset.meshBg = "on";
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body data-current-case="task-distribution-concept">
|
||||
{{> header-case}}
|
||||
{{> fab-stack}}
|
||||
<main id="case-main" class="section section--tight">
|
||||
<div class="section__inner case-article">
|
||||
<p class="hero__eyebrow">UX/UI · Analytics</p>
|
||||
<h1 class="case-page__h1">Task Assignment Service Concept</h1>
|
||||
<p class="case-page__sub">
|
||||
Product concept: setting, assigning, and tracking tasks with an emphasis on process transparency and seamless teamwork.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
The goal of the concept is to show how the team views the workflow without “black boxes”: who is working on what, where the bottlenecks are, and which statuses are critical for the next step.
|
||||
</p>
|
||||
<p class="section__prose">
|
||||
A detailed case study will be posted here later; the page is already linked to a portfolio entry on the website.
|
||||
</p>
|
||||
<h2>Cover</h2>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/task-distribution-concept/cover.webp"
|
||||
alt="Task Distribution Service Concept — Case Cover"
|
||||
width="1200"
|
||||
height="675"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<figcaption>Current preview for the portfolio card</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{> projects-related}}{{> projects-section}}
|
||||
{{> contact-section}}
|
||||
{{> footer}}
|
||||
{{> cookie-notice}}
|
||||
{{> nav-more-floating}}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded',function(){
|
||||
var s=document.body.dataset.currentCase;
|
||||
if(!s)return;
|
||||
document.querySelectorAll('[data-related-slug="'+s+'"]').forEach(function(el){el.hidden=true;});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,594 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="dark" data-radius="medium" data-font="merriweather">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Task interface for the operations department: tabs, filters, view modes, Figma prototypes, and research scenarios."
|
||||
/>
|
||||
<title>Task Tracker for a Back-Office System — Case Study</title>
|
||||
{{> head-links}}
|
||||
{{> head-plyr}}
|
||||
<style>
|
||||
.case-article {
|
||||
max-width: 48rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.case-article h2 {
|
||||
margin: 2rem 0 0.75rem;
|
||||
font-size: clamp(1.15rem, 2vw, 1.35rem);
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article h3 {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article .section__prose {
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article img,
|
||||
.case-article video {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 0.75rem 0 1rem;
|
||||
}
|
||||
.case-article ul,
|
||||
.case-article ol {
|
||||
margin: 0.5rem 0 1rem 1.25rem;
|
||||
padding: 0;
|
||||
}
|
||||
.case-article li {
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
.case-article a:not(.btn) {
|
||||
color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var t = JSON.parse(localStorage.getItem("siteThemeV1") || "{}");
|
||||
var d = document.documentElement;
|
||||
var m = t.mode;
|
||||
if (m !== "light" && m !== "dark" && m !== "system") m = "system";
|
||||
var eff =
|
||||
m === "system"
|
||||
? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: m;
|
||||
d.dataset.theme = eff;
|
||||
var h = typeof t.h === "number" ? t.h : typeof t.hue === "number" ? t.hue : null;
|
||||
var s = typeof t.s === "number" ? t.s : typeof t.sat === "number" ? t.sat : null;
|
||||
var l = typeof t.l === "number" ? t.l : typeof t.light === "number" ? t.light : null;
|
||||
if (h !== null) d.style.setProperty("--theme-h", String(h));
|
||||
if (s !== null) d.style.setProperty("--theme-s", String(s));
|
||||
if (l !== null) {
|
||||
var L = Number(l);
|
||||
if (Number.isFinite(L)) d.style.setProperty("--theme-l", String(Math.min(70, Math.max(20, L))));
|
||||
}
|
||||
var rad = t.radius;
|
||||
if (rad === "soft" || rad === "medium" || rad === "strong") {
|
||||
d.dataset.radius = rad;
|
||||
} else {
|
||||
d.dataset.radius = "medium";
|
||||
}
|
||||
var f = t.font;
|
||||
if (f === "onest") f = "geologica";
|
||||
if (f === "anonymous") f = "mono";
|
||||
if (f === "merriweather" || f === "mono" || f === "geologica") {
|
||||
d.dataset.font = f;
|
||||
} else {
|
||||
d.dataset.font = "merriweather";
|
||||
}
|
||||
var mb = t.meshBg;
|
||||
if (mb === "off") {
|
||||
d.dataset.meshBg = "off";
|
||||
} else {
|
||||
d.dataset.meshBg = "on";
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body data-current-case="task-tracker">
|
||||
{{> header-case}}
|
||||
{{> fab-stack}}
|
||||
<main id="case-main" class="section section--tight">
|
||||
<div class="section__inner case-article">
|
||||
<p class="hero__eyebrow">Test Assignment · Back Office</p>
|
||||
<h1 class="case-page__h1">Task Tracker for an Internal Back-Office System</h1>
|
||||
<h2 id="zadanie">Assignment</h2>
|
||||
<p class="section__prose">
|
||||
Implement an interface for planning and assigning tasks to employees in the operations department who work in a back-office system. There are a large number of diverse tasks that can be grouped into the following categories: Type-1, Type-2, Type-3, Type-4, Type-5, Type-6, Type-7. Each task has its own automatically assigned priority. Users have access only to tasks from specific categories.
|
||||
</p>
|
||||
<p class="section__prose">The following needs to be implemented:</p>
|
||||
<ul>
|
||||
<li>
|
||||
Main screen:
|
||||
<ul>
|
||||
<li>where the user can view all open general tasks and select some for themselves.</li>
|
||||
<li>A personal to-do list displaying both manually selected tasks and automatically assigned ones.</li>
|
||||
<li>For a selected task, there is the option to assign it to another employee or return it to the general list.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>A screen where users can see what tasks each person is working on.</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="vvedenie">Introduction</h2>
|
||||
<p class="section__prose">
|
||||
Since I personally have extensive experience working with task trackers, I’m drawing on my own experience and that of my colleagues, while taking the project requirements into account.
|
||||
</p>
|
||||
|
||||
<h2 id="referensy">References</h2>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img src="{{assetPrefix}}assets/task-tracker/refs.webp" alt="References" width="1200" height="800" loading="lazy" decoding="async" data-image-viewer />
|
||||
</figure>
|
||||
<ul>
|
||||
<li>Task trackers (Trello, Monday, YouTrack, etc.)</li>
|
||||
<li>Services for visual database management (Notion, Airtable, etc.)</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="reshenie">Solution</h2>
|
||||
|
||||
<h3 id="skrinkast">Screencast</h3>
|
||||
<figure class="case-media" style="margin: 0.85rem 0 1.2rem">
|
||||
<div class="case-video-shell">
|
||||
<video controls preload="metadata" playsinline poster="{{assetPrefix}}assets/task-tracker/full_preview.webp" data-case-video-player style="display: block; width: 100%; height: auto; margin: 0">
|
||||
<source src="{{assetPrefix}}assets/task-tracker/full_preview.webm" type="video/webm" />
|
||||
<source src="{{assetPrefix}}assets/task-tracker/full_preview.mp4" type="video/mp4" />
|
||||
Your browser does not support video.
|
||||
</video>
|
||||
</div>
|
||||
</figure>
|
||||
<div class="case-links-grid">
|
||||
|
||||
|
||||
<a
|
||||
class="case-link-card"
|
||||
href="https://www.figma.com/proto/aFvwvTZuksD0JTo6yFXyUz/STENN-(Factoring)?page-id=0%3A1&node-id=20%3A1291&viewport=-5744%2C876%2C1.35&scaling=contain&starting-point-node-id=20%3A1291"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
><span class="case-link-card__media"><img src="{{assetPrefix}}assets/task-tracker/tab_all-tasks.webp" alt="Figma Prototype Preview" width="1200" height="800" loading="lazy" decoding="async" /></span></a>
|
||||
|
||||
|
||||
<a
|
||||
class="case-link-card"
|
||||
href="https://www.figma.com/proto/aFvwvTZuksD0JTo6yFXyUz/STENN-(Factoring)?page-id=0%3A1&node-id=20%3A1291&viewport=-5744%2C876%2C1.35&scaling=contain&starting-point-node-id=20%3A1291"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
><span class="case-link-card__body"><span class="case-link-card__title">Figma Prototype</span>
|
||||
<span class="case-link-card__meta">Interactive scenario based on tasks and views</span>
|
||||
<span class="case-link-card__url">figma.com/proto/.../STENN-(Factoring)</span>
|
||||
|
||||
|
||||
</span></a></div>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">📌</span>
|
||||
<div class="case-callout__content">
|
||||
<p>It is assumed that an employee cannot create tasks but can view and partially edit them; therefore, an administrator or manager configures which tasks will be available to the employee.</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<h3 id="tabs">Tabs</h3>
|
||||
<p class="section__prose">On the <strong>All Tasks</strong> tab, the employee can view all available tasks.</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img src="{{assetPrefix}}assets/task-tracker/tab_all-tasks.webp" alt="Untitled" width="1200" height="800" loading="lazy" decoding="async" data-image-viewer />
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
The "<strong>My tasks</strong>" tab displays only those tasks assigned to the current employee. Tasks are grouped by status (the status is hidden in the task card).
|
||||
</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img src="{{assetPrefix}}assets/task-tracker/tab_my-tasks.webp" alt="Untitled" width="1200" height="800" loading="lazy" decoding="async" data-image-viewer />
|
||||
</figure>
|
||||
<p class="section__prose">The <strong>“Task by Assignee”</strong> tab displays all tasks broken down by assignee.</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img src="{{assetPrefix}}assets/task-tracker/tab_tasks-by-assign.webp" alt="Untitled" width="1200" height="800" loading="lazy" decoding="async" data-image-viewer />
|
||||
</figure>
|
||||
<p class="section__prose">Options for the current tab are located on the right-hand side.</p>
|
||||
<figure class="case-media case-media--natural">
|
||||
<img src="{{assetPrefix}}assets/task-tracker/actions_for_page.webp" alt="Untitled" width="1200" height="800" loading="lazy" decoding="async" />
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
All changes are saved when you leave the tab and when you close the entire <strong>Tasks</strong> page. These settings are individual, and each employee can customize the page as they see fit.
|
||||
</p>
|
||||
<ul>
|
||||
<li>Group—allows you to create additional groupings based on task properties (category, priority, assignee, status, creation date, modification date).</li>
|
||||
<li>View—allows you to change the display of current tasks to a different view: board, list, or table.</li>
|
||||
<li>Filter—allows you to hide tasks based on their properties.</li>
|
||||
<li>
|
||||
Sort—allows you to determine the order in which tasks are displayed based on their properties. If sorting by status (High, Medium, Low) is enabled, you can choose either alphabetical order or reverse order, or you can create your own order. Dates and numbers are sorted in ascending or descending order.
|
||||
</li>
|
||||
<li>
|
||||
Search on page — allows you to display only those tasks whose title or description contains a specific search word or phrase. (Search functionality can be improved by adding search operators if research demonstrates the need.)
|
||||
</li>
|
||||
</ul>
|
||||
<p class="section__prose">
|
||||
The page uses lazy loading for tasks. As you scroll down, new tasks will load. The number of tasks is displayed in the headers; this can be helpful for estimating how far you need to scroll to see everything. There may be a large number of tasks, in which case it might make sense to add pagination as well.
|
||||
</p>
|
||||
|
||||
<h3 id="views">Display Options (Views)</h3>
|
||||
<p class="section__prose">
|
||||
Different views of the same tasks can be useful for various contexts in which the <strong>Tasks</strong> page is used, as well as to accommodate employees’ personal preferences, which increases their efficiency. An employee who isn’t annoyed by the interface works more productively and contributes more.
|
||||
</p>
|
||||
<figure class="case-media case-media--natural">
|
||||
<img src="{{assetPrefix}}assets/task-tracker/dropdown_menu_for_views.webp" alt="Untitled" width="1200" height="800" loading="lazy" decoding="async" />
|
||||
</figure>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">💡</span>
|
||||
<div class="case-callout__content">
|
||||
<p>When you change the view, the icon next to the <strong>“View”</strong> option also changes.</p>
|
||||
</div>
|
||||
</aside>
|
||||
<p class="section__prose">
|
||||
The <strong>Board</strong> view makes it easier to use the drag-and-drop feature (see below). It’s also visually easier to determine the number of tasks in a category, which I imagine could be helpful. When tasks are grouped by completion status, moving cards from left to right creates a natural horizontal flow of tasks from point A to point B.
|
||||
</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img src="{{assetPrefix}}assets/task-tracker/view_board.webp" alt="Untitled" width="1200" height="800" loading="lazy" decoding="async" data-image-viewer />
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
The <strong>List</strong> view allows you to see more information in a smaller screen area. At the same time, this view is visually and functionally simpler than the table view, which means less cognitive load on the user. Also, all task titles start at the far left, making it easy <em>to scan</em> the list.
|
||||
</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img src="{{assetPrefix}}assets/task-tracker/view_list.webp" alt="Untitled" width="1200" height="800" loading="lazy" decoding="async" data-image-viewer />
|
||||
</figure>
|
||||
<p class="section__prose">
|
||||
The <strong>Table</strong> view allows you to work with all task fields immediately, without having to open the task in a modal window or a separate page. The table allows you to customize the width and order of columns, which can be convenient in certain use cases.
|
||||
</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img src="{{assetPrefix}}assets/task-tracker/view_table.webp" alt="Untitled" width="1200" height="800" loading="lazy" decoding="async" data-image-viewer />
|
||||
</figure>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">💡</span>
|
||||
<div class="case-callout__content">
|
||||
<p>Sections can be collapsed to fit only the necessary amount of content on the screen. (It might even make sense to save the collapsed/expanded state after switching views, tabs, or reloading the page.)</p>
|
||||
</div>
|
||||
</aside>
|
||||
<figure class="case-media" style="margin: 0.85rem 0 1.2rem">
|
||||
<div class="case-video-shell">
|
||||
<video controls preload="metadata" playsinline poster="{{assetPrefix}}assets/task-tracker/expand-collapse_group.webp" data-case-video-player style="display: block; width: 100%; height: auto; margin: 0">
|
||||
<source src="{{assetPrefix}}assets/task-tracker/expand-collapse_group.webm" type="video/webm" />
|
||||
<source src="{{assetPrefix}}assets/task-tracker/expand-collapse_group.mp4" type="video/mp4" />
|
||||
Your browser does not support video.
|
||||
</video>
|
||||
</div>
|
||||
</figure>
|
||||
<div class="case-links-grid">
|
||||
|
||||
|
||||
<a
|
||||
class="case-link-card"
|
||||
href="https://www.figma.com/proto/aFvwvTZuksD0JTo6yFXyUz/STENN-(Task-Tracker)?page-id=0%3A1&node-id=39%3A700&viewport=-1875%2C-125%2C0.55&scaling=min-zoom&starting-point-node-id=20%3A1291&hide-ui=1"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
><span class="case-link-card__media"><img src="{{assetPrefix}}assets/task-tracker/view_board.webp" alt="Preview of the "Board" View Prototype" width="1200" height="800" loading="lazy" decoding="async" /></span></a>
|
||||
|
||||
|
||||
<a
|
||||
class="case-link-card"
|
||||
href="https://www.figma.com/proto/aFvwvTZuksD0JTo6yFXyUz/STENN-(Task-Tracker)?page-id=0%3A1&node-id=39%3A700&viewport=-1875%2C-125%2C0.55&scaling=min-zoom&starting-point-node-id=20%3A1291&hide-ui=1"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
><span class="case-link-card__body"><span class="case-link-card__title">Prototype in Figma</span>
|
||||
<span class="case-link-card__meta">Display options: Board, List, Table</span>
|
||||
<span class="case-link-card__url">figma.com/proto/.../STENN- (Task Tracker)</span>
|
||||
|
||||
|
||||
</span></a></div>
|
||||
|
||||
<h3 id="drag">Drag & Drop</h3>
|
||||
<p class="section__prose">
|
||||
In all views, you can drag and drop elements (cards, list items, and table rows and columns). This promotes more productive work and feels natural to the user. For example, if you need to distribute tasks among employees (for an admin or manager), change the status of multiple tasks at once, or do so within a short period of time.
|
||||
</p>
|
||||
<p class="section__prose"><strong>Board view</strong></p>
|
||||
<figure class="case-media" style="margin: 0.85rem 0 1.2rem">
|
||||
<div class="case-video-shell">
|
||||
<video controls preload="metadata" playsinline poster="{{assetPrefix}}assets/task-tracker/drag-drop-item_board-view.webp" data-case-video-player style="display: block; width: 100%; height: auto; margin: 0">
|
||||
<source src="{{assetPrefix}}assets/task-tracker/drag-drop-item_board-view.webm" type="video/webm" />
|
||||
<source src="{{assetPrefix}}assets/task-tracker/drag-drop-item_board-view.mp4" type="video/mp4" />
|
||||
Your browser does not support video.
|
||||
</video>
|
||||
</div>
|
||||
</figure>
|
||||
<div class="case-links-grid">
|
||||
|
||||
|
||||
<a
|
||||
class="case-link-card"
|
||||
href="https://www.figma.com/proto/aFvwvTZuksD0JTo6yFXyUz/STENN-Task-Tracker?node-id=108%3A970&viewport=2248%2C341%2C0.33&scaling=contain&starting-point-node-id=108%3A1968&show-proto-sidebar=1"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
><span class="case-link-card__media" aria-hidden="true"><img src="{{assetPrefix}}assets/task-tracker/view_board.webp" alt="" width="1200" height="800" loading="lazy" decoding="async" /></span></a>
|
||||
|
||||
|
||||
<a
|
||||
class="case-link-card"
|
||||
href="https://www.figma.com/proto/aFvwvTZuksD0JTo6yFXyUz/STENN-Task-Tracker?node-id=108%3A970&viewport=2248%2C341%2C0.33&scaling=contain&starting-point-node-id=108%3A1968&show-proto-sidebar=1"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
><span class="case-link-card__body"><span class="case-link-card__title">Figma prototype</span>
|
||||
<span class="case-link-card__meta">Drag & Drop, Board view</span>
|
||||
<span class="case-link-card__url">figma.com/proto/.../STENN-Task-Tracker</span>
|
||||
|
||||
|
||||
</span></a></div>
|
||||
<p class="section__prose"><strong>List view</strong></p>
|
||||
<figure class="case-media" style="margin: 0.85rem 0 1.2rem">
|
||||
<div class="case-video-shell">
|
||||
<video controls preload="metadata" playsinline poster="{{assetPrefix}}assets/task-tracker/drag-drop-item_list-view.webp" data-case-video-player style="display: block; width: 100%; height: auto; margin: 0">
|
||||
<source src="{{assetPrefix}}assets/task-tracker/drag-drop-item_list-view.webm" type="video/webm" />
|
||||
<source src="{{assetPrefix}}assets/task-tracker/drag-drop-item_list-view.mp4" type="video/mp4" />
|
||||
Your browser does not support video.
|
||||
</video>
|
||||
</div>
|
||||
</figure>
|
||||
<div class="case-links-grid">
|
||||
|
||||
|
||||
<a
|
||||
class="case-link-card"
|
||||
href="https://www.figma.com/proto/aFvwvTZuksD0JTo6yFXyUz/STENN-Task-Tracker?node-id=108%3A970&viewport=2248%2C341%2C0.33&scaling=contain&starting-point-node-id=108%3A2607&show-proto-sidebar=1"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
><span class="case-link-card__media" aria-hidden="true"><img src="{{assetPrefix}}assets/task-tracker/view_list.webp" alt="" width="1200" height="800" loading="lazy" decoding="async" /></span></a>
|
||||
|
||||
|
||||
<a
|
||||
class="case-link-card"
|
||||
href="https://www.figma.com/proto/aFvwvTZuksD0JTo6yFXyUz/STENN-Task-Tracker?node-id=108%3A970&viewport=2248%2C341%2C0.33&scaling=contain&starting-point-node-id=108%3A2607&show-proto-sidebar=1"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
><span class="case-link-card__body"><span class="case-link-card__title">Prototype in Figma</span>
|
||||
<span class="case-link-card__meta">Drag & Drop, List view</span>
|
||||
<span class="case-link-card__url">figma.com/proto/.../STENN-Task-Tracker</span>
|
||||
|
||||
|
||||
</span></a></div>
|
||||
<p class="section__prose"><strong>Advanced interactive prototype in ProtoPie</strong></p>
|
||||
<p class="section__prose">
|
||||
A schematic representation of how Drag & Drop works. This type of prototype helps designers better understand the nuances that need to be considered in the user flow, as well as when preparing specifications for developers. Also, by testing this prototype, you may realize that the feature needs to work completely differently and decide to redesign it right away. This saves overall development time.
|
||||
</p>
|
||||
<figure class="case-media" style="margin: 0.85rem 0 1.2rem">
|
||||
<div class="case-video-shell">
|
||||
<video controls preload="metadata" playsinline poster="{{assetPrefix}}assets/task-tracker/drag-drop-action_example.webp" data-case-video-player style="display: block; width: 100%; height: auto; margin: 0">
|
||||
<source src="{{assetPrefix}}assets/task-tracker/drag-drop-action_example.webm" type="video/webm" />
|
||||
<source src="{{assetPrefix}}assets/task-tracker/drag-drop-action_example.mp4" type="video/mp4" />
|
||||
Your browser does not support video.
|
||||
</video>
|
||||
</div>
|
||||
</figure>
|
||||
<div class="case-links-grid">
|
||||
|
||||
|
||||
<a
|
||||
class="case-link-card case-link-card--favicon"
|
||||
href="https://cloud.protopie.io/p/e7a45bd3f2?touchHint=false&ui=false&scaleToFit=false&cursorType=arrow&mockup=false&bgColor=%23F5F5F5&playSpeed=1&playerAppPopup=true"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
><span class="case-link-card__media" aria-hidden="true"><img src="https://www.google.com/s2/favicons?domain=cloud.protopie.io&sz=128" alt="" width="24" height="24" decoding="async" /></span></a>
|
||||
|
||||
|
||||
<a
|
||||
class="case-link-card case-link-card--favicon"
|
||||
href="https://cloud.protopie.io/p/e7a45bd3f2?touchHint=false&ui=false&scaleToFit=false&cursorType=arrow&mockup=false&bgColor=%23F5F5F5&playSpeed=1&playerAppPopup=true"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
><span class="case-link-card__body"><span class="case-link-card__title">Prototype | drag_drop | ProtoPie Cloud</span>
|
||||
<span class="case-link-card__meta">Interactive</span> <span class="case-link-card__title">prototype</span><span class="case-link-card__meta"> in the ProtoPie cloud</span>
|
||||
cloud<span class="case-link-card__url">.protopie.io/...</span>
|
||||
|
||||
|
||||
</span></a></div>
|
||||
<p class="section__prose">
|
||||
In the prototype, only the blue card can be moved, and not all positions work correctly. You can view the recording, which is available on the same prototype page.
|
||||
</p>
|
||||
|
||||
<h2 id="stil">Style</h2>
|
||||
<p class="section__prose">
|
||||
The visual style can be anything you like without changing <em>the framework</em> itself<em>.</em> For example, it could be a bit more colorful with more expressive fonts.
|
||||
</p>
|
||||
<p class="section__prose">Light-colored theme</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img src="{{assetPrefix}}assets/task-tracker/light_theme_color.webp" alt="Light-colored theme" width="1200" height="800" loading="lazy" decoding="async" data-image-viewer />
|
||||
</figure>
|
||||
<div class="case-links-grid">
|
||||
|
||||
|
||||
<a
|
||||
class="case-link-card case-link-card--favicon"
|
||||
href="https://www.figma.com/file/aFvwvTZuksD0JTo6yFXyUz/STENN-(Task-Tracker)?node-id=174%3A5703"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
><span class="case-link-card__media" aria-hidden="true"><img src="https://www.google.com/s2/favicons?domain=www.figma.com&sz=128" alt="" width="24" height="24" decoding="async" /></span></a>
|
||||
|
||||
|
||||
<a
|
||||
class="case-link-card case-link-card--favicon"
|
||||
href="https://www.figma.com/file/aFvwvTZuksD0JTo6yFXyUz/STENN-(Task-Tracker)?node-id=174%3A5703"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
><span class="case-link-card__body"><span class="case-link-card__title">Figma Frame</span>
|
||||
<span class="case-link-card__meta">Light-colored theme</span>
|
||||
<span class="case-link-card__url">figma.com/file/.../STENN- (Task-Tracker)</span>
|
||||
|
||||
|
||||
</span></a></div>
|
||||
<p class="section__prose">Neutral dark theme</p>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img src="{{assetPrefix}}assets/task-tracker/dark_theme.webp" alt="Neutral Dark Theme" width="1200" height="800" loading="lazy" decoding="async" data-image-viewer />
|
||||
</figure>
|
||||
<div class="case-links-grid">
|
||||
|
||||
|
||||
<a
|
||||
class="case-link-card case-link-card--favicon"
|
||||
href="https://www.figma.com/proto/aFvwvTZuksD0JTo6yFXyUz/STENN--Task-Tracker-?node-id=135-5400&t=BzGQOTE0ty7u2E50-1"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
><span class="case-link-card__media" aria-hidden="true"><img src="https://www.google.com/s2/favicons?domain=www.figma.com&sz=128" alt="" width="24" height="24" decoding="async" /></span></a>
|
||||
|
||||
|
||||
<a
|
||||
class="case-link-card case-link-card--favicon"
|
||||
href="https://www.figma.com/proto/aFvwvTZuksD0JTo6yFXyUz/STENN--Task-Tracker-?node-id=135-5400&t=BzGQOTE0ty7u2E50-1"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
><span class="case-link-card__body"><span class="case-link-card__title">Frame in Figma</span>
|
||||
<span class="case-link-card__meta">Neutral dark theme</span>
|
||||
<span class="case-link-card__url">figma.com/proto/.../STENN--Task-Tracker-</span>
|
||||
|
||||
|
||||
</span></a></div>
|
||||
|
||||
<h2 id="issledovaniya">Research</h2>
|
||||
<p class="section__prose">
|
||||
During the Figma prototyping phase, you can conduct interviews with several employees—for example, 5 people. First, add real tasks from the current system to the prototype; about 20 should be enough. Next, you can hold a video call with the respondent’s screen shared or meet in person to present a few tasks and ask some questions. During the session, the respondent can interact with the prototype, but if something doesn’t work, the interviewer can note the attempt and explain that this feature isn’t working yet, as well as ask what the respondent expects to see next.
|
||||
</p>
|
||||
<h3 id="zadania">Tasks</h3>
|
||||
<ol>
|
||||
<li>Find your current tasks on the <strong>Tasks</strong> page under the <strong>All Tasks</strong> tab</li>
|
||||
<li>Assign tasks to yourself to work on</li>
|
||||
<li>Find a task related to a specific topic (the interviewer provides an example) that they have already completed</li>
|
||||
<li>Show tasks that belong only to a specific category and must be completed by the end of the week</li>
|
||||
<li>Request that the priority of several tasks be changed at once</li>
|
||||
<li>Rate the visual appeal of the interface on a scale of 1 to 10</li>
|
||||
<li>Rate your overall impression of the proposed concept on a scale of 1 to 10</li>
|
||||
<li>Suggest additional features that would be useful</li>
|
||||
</ol>
|
||||
<h3 id="analitika">Analytics</h3>
|
||||
<p class="section__prose">To assess the relevance of the implemented features, the following metrics must be collected.</p>
|
||||
<ul>
|
||||
<li>
|
||||
Number of page views for the "<strong>Tasks"</strong> page <code>page_views_tasks</code> with the option to break them down
|
||||
<ul>
|
||||
<li>
|
||||
by tabs
|
||||
<ul>
|
||||
<li>All tasks <code>all_tasks_tab</code></li>
|
||||
<li>My tasks <code>my_tasks_tab</code></li>
|
||||
<li>Tasks by Assignment <code>tasks_by_assign</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
by role
|
||||
<ul>
|
||||
<li>Admin <code>admin</code></li>
|
||||
<li>Manager <code>manager</code></li>
|
||||
<li>User <code>user</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
by search attempts
|
||||
<ul>
|
||||
<li>Search Used <code>search used</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
Number <strong>of</strong> enabled custom settings <code>custom_settings</code> with the option to group
|
||||
<ul>
|
||||
<li>
|
||||
by tabs
|
||||
<ul>
|
||||
<li>All tasks <code>all_tasks_tab</code></li>
|
||||
<li>My tasks <code>my_tasks_tab</code></li>
|
||||
<li>Tasks by Assignment <code>tasks_by_assign</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
by role
|
||||
<ul>
|
||||
<li>Admin <code>admin</code></li>
|
||||
<li>Manager <code>manager</code></li>
|
||||
<li>User <code>user</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
by type
|
||||
<ul>
|
||||
<li>Group <code>group</code></li>
|
||||
<li>Filter <code>filter</code></li>
|
||||
<li>Sort <code>sort</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
Number of included species <strong>View Used</strong> <code>view_used</code> with the option to break down
|
||||
<ul>
|
||||
<li>
|
||||
by tabs
|
||||
<ul>
|
||||
<li>All tasks <code>all_tasks_tab</code></li>
|
||||
<li>My tasks <code>my_tasks_tab</code></li>
|
||||
<li>Tasks by Assignment <code>tasks_by_assign</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
by role
|
||||
<ul>
|
||||
<li>Admin <code>admin</code></li>
|
||||
<li>Manager <code>manager</code></li>
|
||||
<li>User <code>user</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="section__prose">
|
||||
Then, over the next six months (or sooner, depending on user activity), we’ll be able to see the results and conduct a study (in the form of interviews) on the features that are used least frequently and those that have received complaints, in order to gather high-quality data and get a complete picture of what’s happening.
|
||||
</p>
|
||||
|
||||
<h2 id="dop">Additional details</h2>
|
||||
<p class="section__prose">
|
||||
When creating mockups in Figma, the following were used: auto-layout features; interactive components; plugins (<a href="https://www.figma.com/community/plugin/735098390272716381/Iconify">Iconify</a>, <a href="https://www.figma.com/community/plugin/917796239623005893/Fitter">Fitter</a>, <a href="https://www.figma.com/community/plugin/1021879782774011952">Protofly</a>, <a href="https://www.figma.com/community/plugin/889676455045507628/Shadow-Maker">Shadow Maker</a>, <a href="https://www.figma.com/community/plugin/986289377230504703">Dark Side</a>, etc.); and the Community file (<a href="https://www.figma.com/community/file/831142089959745518">+40 Types of Cursor</a>). About 60% of the layers are named and organized for easier workflow. An 8pt grid is used, with additional sizes of 2pt and 4pt. Color styles are available for light and dark themes. The design adapts to changing window sizes (no mobile version).
|
||||
</p>
|
||||
<div class="case-links-grid">
|
||||
|
||||
|
||||
<a
|
||||
class="case-link-card case-link-card--favicon"
|
||||
href="https://www.figma.com/file/aFvwvTZuksD0JTo6yFXyUz/STENN-(Task-Tracker)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
><span class="case-link-card__media" aria-hidden="true"><img src="https://www.google.com/s2/favicons?domain=www.figma.com&sz=128" alt="" width="24" height="24" decoding="async" /></span></a>
|
||||
|
||||
|
||||
<a
|
||||
class="case-link-card case-link-card--favicon"
|
||||
href="https://www.figma.com/file/aFvwvTZuksD0JTo6yFXyUz/STENN-(Task-Tracker)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
><span class="case-link-card__body"><span class="case-link-card__title">Source file in Figma</span>
|
||||
<span class="case-link-card__meta">STENN Project (Task-Tracker)</span>
|
||||
<span class="case-link-card__url">figma.com/file/.../STENN- (Task-Tracker)</span>
|
||||
|
||||
|
||||
</span></a></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{> projects-related}}{{> projects-section}}
|
||||
{{> contact-section}}
|
||||
{{> footer}}
|
||||
{{> cookie-notice}}
|
||||
{{> nav-more-floating}}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded',function(){
|
||||
var s=document.body.dataset.currentCase;
|
||||
if(!s)return;
|
||||
document.querySelectorAll('[data-related-slug="'+s+'"]').forEach(function(el){el.hidden=true;});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,511 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="dark" data-radius="medium" data-font="merriweather">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta name="description" content="A demo page for the case study featuring all the developed elements: link cards and media blocks." />
|
||||
<title>Case Study Elements Demo — template</title>
|
||||
{{> head-links}}
|
||||
{{> head-plyr}}
|
||||
<style>
|
||||
.case-article {
|
||||
max-width: 48rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.case-article h2 {
|
||||
margin: 2rem 0 0.75rem;
|
||||
font-size: clamp(1.15rem, 2vw, 1.35rem);
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article h3 {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article .section__prose {
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text);
|
||||
}
|
||||
.case-article a:not(.btn) {
|
||||
color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var t = JSON.parse(localStorage.getItem("siteThemeV1") || "{}");
|
||||
var d = document.documentElement;
|
||||
var m = t.mode;
|
||||
if (m !== "light" && m !== "dark" && m !== "system") m = "system";
|
||||
var eff =
|
||||
m === "system"
|
||||
? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: m;
|
||||
d.dataset.theme = eff;
|
||||
var h = typeof t.h === "number" ? t.h : typeof t.hue === "number" ? t.hue : null;
|
||||
var s = typeof t.s === "number" ? t.s : typeof t.sat === "number" ? t.sat : null;
|
||||
var l = typeof t.l === "number" ? t.l : typeof t.light === "number" ? t.light : null;
|
||||
if (h !== null) d.style.setProperty("--theme-h", String(h));
|
||||
if (s !== null) d.style.setProperty("--theme-s", String(s));
|
||||
if (l !== null) {
|
||||
var L = Number(l);
|
||||
if (Number.isFinite(L)) d.style.setProperty("--theme-l", String(Math.min(70, Math.max(20, L))));
|
||||
}
|
||||
var rad = t.radius;
|
||||
if (rad === "soft" || rad === "medium" || rad === "strong") {
|
||||
d.dataset.radius = rad;
|
||||
} else {
|
||||
d.dataset.radius = "medium";
|
||||
}
|
||||
var f = t.font;
|
||||
if (f === "onest") f = "geologica";
|
||||
if (f === "anonymous") f = "mono";
|
||||
if (f === "merriweather" || f === "mono" || f === "geologica") {
|
||||
d.dataset.font = f;
|
||||
} else {
|
||||
d.dataset.font = "merriweather";
|
||||
}
|
||||
var mb = t.meshBg;
|
||||
if (mb === "off") {
|
||||
d.dataset.meshBg = "off";
|
||||
} else {
|
||||
d.dataset.meshBg = "on";
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
{{> header-minimal}}
|
||||
{{> fab-stack-home-contact}}
|
||||
<main id="main" class="section section--tight">
|
||||
<div class="section__inner case-article">
|
||||
<p class="hero__eyebrow">Demo · template</p>
|
||||
<h1 class="case-page__h1">Case Study Example: Demo Elements</h1>
|
||||
<p class="case-page__sub">Text blocks and elements for quickly building a longread</p>
|
||||
<p class="section__prose case-page__code">The media for the demo is located in <code>public/assets/</code> (the page builder does not copy them). See <code>public/assets/README.md</code>.</p>
|
||||
|
||||
<h2>Context</h2>
|
||||
<p class="section__prose">
|
||||
This section shows an example of a text block for the introduction. Here, you can briefly describe the product, the audience, and the project’s limitations.
|
||||
The text is neutral: the goal is to quickly sketch out the structure and check the page’s visual rhythm.
|
||||
</p>
|
||||
|
||||
<h2>Task</h2>
|
||||
<p class="section__prose">
|
||||
We need to put together a demo longread with the main UI elements: link cards, media blocks, and examples of content layout.
|
||||
It’s also important to check how the blocks behave on narrow and wide screens.
|
||||
</p>
|
||||
|
||||
<h2>Solution</h2>
|
||||
<p class="section__prose">Below are some ready-made examples that you can copy into real-world projects.</p>
|
||||
|
||||
<h3>Link Cards</h3>
|
||||
<div class="case-links-grid">
|
||||
|
||||
|
||||
<a class="case-link-card" href="https://www.examples.com/" target="_blank" rel="noopener noreferrer"><span class="case-link-card__media" aria-hidden="true"><img src="{{assetPrefix}}assets/template-elements-demo/wallpaper-1.jpg" alt="" loading="lazy" decoding="async" /></span></a>
|
||||
|
||||
|
||||
<a class="case-link-card" href="https://www.examples.com/" target="_blank" rel="noopener noreferrer"><span class="case-link-card__body"><span class="case-link-card__title">Source with preview</span>
|
||||
<span class="case-link-card__meta">Short caption: what the user will see when they click the link</span>
|
||||
www.examples.com
|
||||
|
||||
|
||||
|
||||
|
||||
</span></a><a class="case-link-card case-link-card--favicon" href="https://www.examples.com/docs" target="_blank" rel="noopener noreferrer"><span class="case-link-card__media" aria-hidden="true"><img src="https://www.google.com/s2/favicons?domain=www.examples.com&sz=128" alt="" decoding="async" /></span></a>
|
||||
|
||||
|
||||
<a class="case-link-card case-link-card--favicon" href="https://www.examples.com/docs" target="_blank" rel="noopener noreferrer"><span class="case-link-card__body"><span class="case-link-card__title">Source with favicon</span>
|
||||
<span class="case-link-card__meta">Option for simpler links without a screenshot</span>
|
||||
<span class="case-link-card__url">www.examples.com/docs</span>
|
||||
|
||||
|
||||
</span></a></div>
|
||||
|
||||
<h3>Image: 50% of the original size (no preview)</h3>
|
||||
<figure class="case-media case-media--natural">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/hello.gif" alt="Full-size demo GIF" loading="lazy" decoding="async" />
|
||||
<figcaption>Image at reduced actual size, centered.</figcaption>
|
||||
</figure>
|
||||
|
||||
<h3>Image: width of the content (with preview)</h3>
|
||||
<figure class="case-media case-media--content">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/wallpaper-1.jpg" alt="Demo images based on content width" loading="lazy" decoding="async" data-image-viewer />
|
||||
<figcaption>Image matches the width of the content. Clicking opens the viewer.</figcaption>
|
||||
</figure>
|
||||
|
||||
<h3>Image: slightly wider than the content (with preview)</h3>
|
||||
<figure class="case-media case-media--wide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/wallpaper-1.jpg" alt="The demo image is wider than the content" loading="lazy" decoding="async" data-image-viewer />
|
||||
<figcaption>The image extends slightly beyond the content column.</figcaption>
|
||||
</figure>
|
||||
|
||||
<h3>Long screenshot: collapsed height and button inside the preview</h3>
|
||||
<p class="section__prose">
|
||||
Block <code>data-case-tall-media</code>: width is specified by the <code>data-case-tall-width="wide"</code> (like a “wide” image) or <code>content</code> (per column). When collapsed, the top part of the frame is visible; at the bottom of the preview, there’s a solid “fog.” An “Expand” / “Collapse” button with a chevron icon appears above the bottom of the frame, inside the border; when expanded, the fog disappears; when collapsed, the page scrolls back to the preview. The preview height is set <code>.case-media-tall__clip</code> via <code>--case-tall-max</code> (default <code>min(70vh, 32rem)</code>).
|
||||
</p>
|
||||
<figure class="case-media" data-case-tall-media data-case-tall-width="wide">
|
||||
<div class="case-media-tall" data-case-tall-root>
|
||||
<div class="case-media-tall__clip" id="template-demo-tall-clip" data-case-tall-clip>
|
||||
<img
|
||||
src="{{assetPrefix}}assets/template-elements-demo/img-long.webp"
|
||||
alt="Demo: Long vertical screenshot of the interface"
|
||||
width="1200"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
data-image-viewer
|
||||
/>
|
||||
<div class="case-media-tall__fog" aria-hidden="true"></div>
|
||||
</div>
|
||||
<div class="case-media-tall__actions">
|
||||
<button
|
||||
type="button"
|
||||
class="case-media-tall__btn btn btn--ghost btn--flat"
|
||||
data-case-tall-toggle
|
||||
aria-expanded="false"
|
||||
aria-controls="template-demo-tall-clip"
|
||||
><span class="case-media-tall__btn-chev" aria-hidden="true"></span>
|
||||
|
||||
<span data-case-tall-label-more>Expand</span>
|
||||
<span data-case-tall-label-less hidden>Collapse</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<figcaption>Long image <code>img-long.webp</code> (source PNG next to it in <code>public/assets/template-elements-demo/</code>).</figcaption>
|
||||
</figure>
|
||||
|
||||
<h3>Gallery: grid based on content width</h3>
|
||||
<p class="section__prose">
|
||||
The number of columns on a wide screen is set via <code>figure</code>: the <code>style="--case-gallery-cols: 3"</code>. Below these thresholds, the grid collapses to two or one column. Clicking on a thumbnail opens the general case viewer.
|
||||
</p>
|
||||
<figure class="case-media case-media-gallery" style="--case-gallery-cols: 3">
|
||||
<div class="case-media-gallery__grid">
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/small-image-1.webp" alt="Demo Thumbnail 1" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/small-image-2.webp" alt="Demo Thumbnail 2" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/small-image-3.webp" alt="Demo Thumbnail 3" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/small-image-4.webp" alt="Demo Thumbnail 4" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/small-image-5.webp" alt="Demo Thumbnail 5" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/small-image-6.webp" alt="Demo Thumbnail 6" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/small-image-7.webp" alt="Demo Thumbnail 7" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
</div>
|
||||
<figcaption>Grid in an article column; in this example, three columns on a desktop.</figcaption>
|
||||
</figure>
|
||||
|
||||
<h3>Gallery: Full-page grid with side margins</h3>
|
||||
<p class="section__prose">
|
||||
The <code>case-media-gallery--wide</code> — same logic for extending beyond a column as in the single-column <code>case-media--wide</code>. Here, there are four columns on a wide screen (<code>--case-gallery-cols: 4</code>).
|
||||
</p>
|
||||
<figure class="case-media case-media-gallery case-media-gallery--wide" style="--case-gallery-cols: 4">
|
||||
<div class="case-media-gallery__grid">
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/small-image-1.webp" alt="Wide shot, frame 1" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/small-image-2.webp" alt="Wide shot, take 2" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/small-image-3.webp" alt="Wide shot, take 3" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/small-image-4.webp" alt="Wide shot, take 4" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/small-image-5.webp" alt="Wide shot, take 5" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/small-image-6.webp" alt="Wide shot, shot 6" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-gallery__item">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/small-image-7.webp" alt="Wide shot, shot 7" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
</div>
|
||||
<figcaption>Width is the same as for a `wide` image; on a narrow screen, the `wide` wrapper collapses to 100% of the column width (see media queries).</figcaption>
|
||||
</figure>
|
||||
|
||||
<h3>Slider: one image, arrows, and dots</h3>
|
||||
<p class="section__prose">
|
||||
Block <code>.case-media-slider</code> displays slides one at a time: navigation using “prev/next” arrows,
|
||||
paging using dots (the active one is highlighted in accent color). Keyboard support (←/→, Home/End on dots),
|
||||
touch/mouse swipe, and lazy loading of adjacent frames via <code>data-src</code>. Clicking on the active
|
||||
slide opens the general case viewer.
|
||||
</p>
|
||||
<figure class="case-media case-media-slider" data-case-media-slider>
|
||||
<div class="case-media-slider__viewport" data-slider-viewport>
|
||||
<div class="case-media-slider__track" data-slider-track>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-demo-1.webp" alt="Slider, Frame 1" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-demo-2.webp" alt="Slider, Frame 2" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-demo-3.webp" alt="Slider, Frame 3" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-demo-4.webp" alt="Slider, Frame 4" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
</div>
|
||||
<div class="case-media-slider__nav-group">
|
||||
<button type="button" class="case-media-slider__nav case-media-slider__nav--prev" data-slider-prev aria-label="Previous image">
|
||||
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--left icon-press" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button type="button" class="case-media-slider__nav case-media-slider__nav--next" data-slider-next aria-label="Next image">
|
||||
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--right icon-press" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="case-media-slider__dots" data-slider-dots role="tablist" aria-label="Selecting an Image">
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Image 1"></button>
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Image 2"></button>
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Image 3"></button>
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Figure 4"></button>
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
<p class="section__prose">
|
||||
The same slider, but with the modifier <code>case-media--wide</code> — extends beyond the content column,
|
||||
like single wide images. The previous/next buttons and dots behave the same way.
|
||||
</p>
|
||||
<figure class="case-media case-media-slider case-media--wide" data-case-media-slider>
|
||||
<div class="case-media-slider__viewport" data-slider-viewport>
|
||||
<div class="case-media-slider__track" data-slider-track>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-1.webp" alt="Wide slider, frame 1" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-2.webp" alt="Wide slider, frame 2" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-3.webp" alt="Wide slider, frame 3" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-4.webp" alt="Wide slider, frame 4" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-5.webp" alt="Wide Slider, Frame 5" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-6.webp" alt="Wide slider, frame 6" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-7.webp" alt="Wide slider, frame 7" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
</div>
|
||||
<div class="case-media-slider__nav-group">
|
||||
<button type="button" class="case-media-slider__nav case-media-slider__nav--prev" data-slider-prev aria-label="Previous image">
|
||||
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--left icon-press" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button type="button" class="case-media-slider__nav case-media-slider__nav--next" data-slider-next aria-label="Next image">
|
||||
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--right icon-press" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="case-media-slider__dots" data-slider-dots role="tablist" aria-label="Selecting an Image">
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Image 1"></button>
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Image 2"></button>
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Image 3"></button>
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Figure 4"></button>
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Image 5"></button>
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Image 6"></button>
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Figure 7"></button>
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
<p class="section__prose">
|
||||
Slider with horizontal (wide) frames of the same size—all images
|
||||
<code>1024×800</code>.
|
||||
</p>
|
||||
<figure class="case-media case-media-slider case-media--natural" data-case-media-slider style="--slider-natural-w: 1024px">
|
||||
<div class="case-media-slider__viewport" data-slider-viewport>
|
||||
<div class="case-media-slider__track" data-slider-track>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-1.webp" alt="Wide Slider, Frame 1" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-2.webp" alt="Wide Slider, Frame 2" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-3.webp" alt="Wide Slider, Frame 3" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-4.webp" alt="Wide Slider, Frame 4" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-5.webp" alt="Wide slider, frame 5" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-6.webp" alt="Wide Slider, Frame 6" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-7.webp" alt="Wide Slider, Frame 7" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
</div>
|
||||
<div class="case-media-slider__nav-group">
|
||||
<button type="button" class="case-media-slider__nav case-media-slider__nav--prev" data-slider-prev aria-label="Previous image">
|
||||
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--left icon-press" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button type="button" class="case-media-slider__nav case-media-slider__nav--next" data-slider-next aria-label="Next image">
|
||||
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--right icon-press" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="case-media-slider__dots" data-slider-dots role="tablist" aria-label="Selecting an Image">
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Image 1"></button>
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Image 2"></button>
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Image 3"></button>
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Figure 4"></button>
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Image 5"></button>
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Image 6"></button>
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Figure 7"></button>
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
<p class="section__prose">
|
||||
The same slider, but with vertical frames of the same size—all images
|
||||
<code>600×800</code>.
|
||||
</p>
|
||||
<figure class="case-media case-media-slider case-media--natural" data-case-media-slider style="--slider-natural-w: 600px">
|
||||
<div class="case-media-slider__viewport" data-slider-viewport>
|
||||
<div class="case-media-slider__track" data-slider-track>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-portrait-1.webp" alt="Vertical slider, frame 1" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-portrait-2.webp" alt="Vertical slider, frame 2" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-portrait-3.webp" alt="Vertical slider, frame 3" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-portrait-4.webp" alt="Vertical slider, frame 4" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-portrait-5.webp" alt="Vertical slider, frame 5" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-portrait-6.webp" alt="Vertical slider, frame 6" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-portrait-7.webp" alt="Vertical slider, frame 7" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
</div>
|
||||
<div class="case-media-slider__nav-group">
|
||||
<button type="button" class="case-media-slider__nav case-media-slider__nav--prev" data-slider-prev aria-label="Previous image">
|
||||
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--left icon-press" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button type="button" class="case-media-slider__nav case-media-slider__nav--next" data-slider-next aria-label="Next image">
|
||||
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--right icon-press" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="case-media-slider__dots" data-slider-dots role="tablist" aria-label="Selecting an Image">
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Image 1"></button>
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Image 2"></button>
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Image 3"></button>
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Figure 4"></button>
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Image 5"></button>
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Image 6"></button>
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Figure 7"></button>
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
<p class="section__prose">
|
||||
A separate example: a toggle for the frame-switching animation mode—
|
||||
<code>slide</code> (slide), <code>fade</code> (crossfade), and <code>none</code> (no transition).
|
||||
The mode is set by the attribute <code>data-slider-anim</code> on the shape.
|
||||
</p>
|
||||
<figure class="case-media case-media-slider" data-case-media-slider data-slider-anim="fade">
|
||||
<div class="case-media-slider__anim-switch" data-slider-anim-switch role="group" aria-label="Slider Animation Mode">
|
||||
<button type="button" data-mode="slide" aria-pressed="false">Slide</button>
|
||||
<button type="button" data-mode="fade" aria-pressed="true">Fade</button>
|
||||
<button type="button" data-mode="none" aria-pressed="false">None</button>
|
||||
</div>
|
||||
<div class="case-media-slider__viewport" data-slider-viewport>
|
||||
<div class="case-media-slider__track" data-slider-track>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-demo-1.webp" alt="Animation example, frame 1" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-demo-2.webp" alt="Animation example, frame 2" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-demo-3.webp" alt="Animation example, frame 3" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
<div class="case-media-slider__slide">
|
||||
<img src="{{assetPrefix}}assets/template-elements-demo/slider-demo-4.webp" alt="Animation example, frame 4" loading="lazy" decoding="async" data-image-viewer />
|
||||
</div>
|
||||
</div>
|
||||
<div class="case-media-slider__nav-group">
|
||||
<button type="button" class="case-media-slider__nav case-media-slider__nav--prev" data-slider-prev aria-label="Previous image">
|
||||
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--left icon-press" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button type="button" class="case-media-slider__nav case-media-slider__nav--next" data-slider-next aria-label="Next image">
|
||||
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--right icon-press" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="case-media-slider__dots" data-slider-dots role="tablist" aria-label="Selecting an Image">
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Image 1"></button>
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Image 2"></button>
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Image 3"></button>
|
||||
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Go to Figure 4"></button>
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
<h3>Callout: Important paragraph with an emoji</h3>
|
||||
<aside class="case-callout" data-callout>
|
||||
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">📌</span>
|
||||
<div class="case-callout__content">
|
||||
<p>This is an example of a highlighted callout block. The emoji is aligned to the left and pulses to visually emphasize an important point.</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<h3>Video: Card with a shadow and fixed small corners</h3>
|
||||
<figure class="case-media" style="margin: 0.85rem 0 1.2rem">
|
||||
<div class="case-video-shell">
|
||||
<video controls preload="metadata" playsinline poster="{{assetPrefix}}assets/template-elements-demo/video-1.webp" data-case-video-player style="display: block; width: 100%; height: auto">
|
||||
<source src="{{assetPrefix}}assets/template-elements-demo/video-1.webm" type="video/webm" />
|
||||
<source src="{{assetPrefix}}assets/template-elements-demo/video-1.mp4" type="video/mp4" />
|
||||
Your browser does not support video.
|
||||
</video>
|
||||
</div>
|
||||
<figcaption>An example of a case-style video card.</figcaption>
|
||||
</figure>
|
||||
|
||||
<h2>Result</h2>
|
||||
<p class="section__prose">
|
||||
The template is ready to be copied into a new case study: all that’s left is to replace the fish with the actual text and insert the project’s real links and media.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{> projects-section-mp}}{{> projects-section}}
|
||||
{{> contact-section}}
|
||||
{{> footer}}
|
||||
{{> cookie-notice}}
|
||||
{{> nav-more-floating}}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,844 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="dark" data-radius="medium">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Nikolai Mamadaev's Resume — Product Designer with over 10 years of experience."
|
||||
/>
|
||||
<title>Resume — Nikolai Mamadaev</title>
|
||||
{{> head-links}}
|
||||
<style>
|
||||
.resume-doc {
|
||||
max-width: 52rem;
|
||||
margin: 0 auto;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-card);
|
||||
padding: clamp(1.5rem, 3vw, 2.5rem);
|
||||
}
|
||||
.resume-header {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.resume-photo {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: var(--radius-sm);
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
order: 1;
|
||||
margin-left: auto;
|
||||
}
|
||||
@media (max-width: 32rem) {
|
||||
.resume-header {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
.resume-photo {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
.resume-doc h2 {
|
||||
margin: 2rem 0 1rem;
|
||||
font-size: clamp(0.95rem, 1.35vw, 1.1rem);
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
letter-spacing: 0.04em;
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
.resume-doc h2:first-of-type {
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
}
|
||||
.resume-doc .section__prose + .section__prose {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
.resume-doc .section__prose a {
|
||||
color: var(--accent);
|
||||
}
|
||||
#skills,
|
||||
#about {
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
}
|
||||
.resume-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.5rem 1.5rem;
|
||||
margin: 0.75rem 0 1.5rem;
|
||||
font-size: 0.92rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
@media (max-width: 32rem) {
|
||||
.resume-meta {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
.resume-meta dt {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
letter-spacing: 0.03em;
|
||||
margin-bottom: 0.1rem;
|
||||
}
|
||||
.resume-meta dd {
|
||||
margin: 0;
|
||||
}
|
||||
.resume-meta a {
|
||||
color: var(--accent);
|
||||
}
|
||||
.resume-exp {
|
||||
margin: 1.5rem 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.resume-exp__header {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.resume-exp__company {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
color: var(--text);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.resume-exp__meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.resume-exp__meta-row a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid var(--accent);
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.resume-exp__meta-row a:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.resume-exp__sep {
|
||||
color: var(--line);
|
||||
}
|
||||
.resume-exp__period {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.resume-exp__desc {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.92rem;
|
||||
margin: 0.5rem 0;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.resume-exp__section {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.resume-exp__cols {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 10rem;
|
||||
gap: 0 1.5rem;
|
||||
}
|
||||
@media (max-width: 48rem) {
|
||||
.resume-exp__cols {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
.resume-exp__section-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.92rem;
|
||||
color: var(--text);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.resume-exp__label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
letter-spacing: 0.03em;
|
||||
margin: 0.75rem 0 0.35rem;
|
||||
}
|
||||
.resume-exp ul {
|
||||
margin: 0;
|
||||
padding-left: 1.25rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.resume-exp li + li {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.resume-skills {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.resume-skill {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.resume-skill__title {
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
min-width: 8rem;
|
||||
flex-shrink: 0;
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.02em;
|
||||
margin: 0;
|
||||
}
|
||||
.resume-skill__desc {
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
.resume-about {
|
||||
margin: 1.5rem 0;
|
||||
font-size: 0.92rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.55;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{{> header-minimal}}
|
||||
{{> fab-stack-resume}}
|
||||
<main id="main" class="section section--surface section--tight">
|
||||
<div class="section__inner resume-doc">
|
||||
<div class="resume-header">
|
||||
<img class="resume-photo" src="{{assetPrefix}}assets/index/me_static.webp" alt="Nikolai Mamadaev" width="120" height="120" />
|
||||
<div>
|
||||
<p class="hero__eyebrow">Resume</p>
|
||||
<h1 class="case-page__h1">Nikolai Mamadaev</h1>
|
||||
<p class="section__prose"><strong>Senior UX/UI Designer, Product Designer</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="sep sep--air">
|
||||
|
||||
<dl class="resume-meta">
|
||||
<div>
|
||||
<dt>Portfolio</dt>
|
||||
<dd><a href="https://mamadaevv.site" target="_blank" rel="noopener">mamadaevv.site</a></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Salary Range</dt>
|
||||
<dd>250,000 – 310,000 rubles</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Work Format</dt>
|
||||
<dd>Remote</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Email</dt>
|
||||
<dd><a href="mailto:nik@mamadaevv.site">nik@mamadaevv.site</a></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Social Media</dt>
|
||||
<dd><a href="https://t.me/mamadaevv" target="_blank" rel="noopener noreferrer">@mamadaevv</a></dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<hr class="sep sep--air">
|
||||
|
||||
<h2 id="experience">Work Experience</h2>
|
||||
|
||||
<hr class="sep sep--air">
|
||||
|
||||
<div class="resume-exp">
|
||||
<div class="resume-exp__header">
|
||||
<p class="resume-exp__company">Astratech</p>
|
||||
<p class="resume-exp__meta-row"><span>January 2026 — Present</span></p>
|
||||
</div>
|
||||
<p class="resume-exp__desc">Development and support of digital systems and tools to improve employee productivity.</p>
|
||||
|
||||
<div class="resume-exp__section">
|
||||
<div class="resume-exp__cols">
|
||||
<div>
|
||||
<p class="resume-exp__label">Responsibilities:</p>
|
||||
<ul>
|
||||
<li>Worked on interface design</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="resume-exp__label">Achievements:</p>
|
||||
<ul>
|
||||
<li>Successfully completed assigned tasks</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="resume-exp__label">Stack:</p>
|
||||
<ul>
|
||||
<li>Figma</li>
|
||||
<li>HTML/CSS</li>
|
||||
<li>AI/LLM</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="sep sep--line">
|
||||
|
||||
<div class="resume-exp">
|
||||
<div class="resume-exp__header">
|
||||
<p class="resume-exp__company">Primo RPA</p>
|
||||
<p class="resume-exp__meta-row"><span>February 2023 — November 2025 (2 years and 10 months)</span><a href="https://primo-rpa.com/" target="_blank" rel="noopener noreferrer">·primo-rpa.com</a></p>
|
||||
</div>
|
||||
<p class="resume-exp__desc">A Russian enterprise-grade RPA platform: a suite of desktop robot development environments and web services for management, analytics, and ML. I worked as a lead designer on the entire product line, consisting of 6 products.</p>
|
||||
|
||||
<div class="resume-exp__section">
|
||||
<p class="resume-exp__section-title">AI Server Platform (SaaS)</p>
|
||||
<p class="resume-exp__desc">A control panel for training, running, testing, and other operations involving neural networks deployed within the company’s internal network.</p>
|
||||
<div class="resume-exp__cols">
|
||||
<div>
|
||||
<p class="resume-exp__label">Responsibilities:</p>
|
||||
<ul>
|
||||
<li>Designing the model management interface: training, deployment, testing, versioning, and monitoring</li>
|
||||
<li>Developing workflows for Smart OCR—extracting data from various types of documents</li>
|
||||
<li>Designing document markup and validating recognition results</li>
|
||||
<li>Managing the states of long-running processes: model training, progress, errors, rollback</li>
|
||||
<li>Preparing interactive prototypes and overseeing implementation</li>
|
||||
<li>Conducting interviews with ML engineers and business users to validate behavioral models</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="resume-exp__label">Achievements:</p>
|
||||
<ul>
|
||||
<li>Packaged complex ML logic into an interface that is understandable not only to ML engineers but also to business analysts without a technical background</li>
|
||||
<li>Streamlined work with models into a single, predictable 4-step workflow instead of 9 disparate screens</li>
|
||||
<li>Reduced the time required to configure a new model from 40 to 15 minutes, based on test results</li>
|
||||
<li>Designed a document annotation interface that reduced support requests on this topic by 60%</li>
|
||||
<li>Conducted 8 usability sessions and, based on the results, revised the key model training scenario</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="resume-exp__label">Stack:</p>
|
||||
<ul>
|
||||
<li>ML / neural networks</li>
|
||||
<li>Smart OCR</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="sep sep--air">
|
||||
|
||||
<div class="resume-exp__section">
|
||||
<p class="resume-exp__section-title">Robot Automation Studio (Windows)</p>
|
||||
<p class="resume-exp__desc">An environment for developing and debugging robots for business process automation.</p>
|
||||
<div class="resume-exp__cols">
|
||||
<div>
|
||||
<p class="resume-exp__label">Responsibilities:</p>
|
||||
<ul>
|
||||
<li>Designing the interface for a professional desktop IDE: workspace, toolbars, process tree, debugger</li>
|
||||
<li>Developing the product’s component library and visual language</li>
|
||||
<li>Designing debugging scenarios: breakpoints, step-by-step execution, variable inspection</li>
|
||||
<li>Designing support for large activity trees and nesting</li>
|
||||
<li>Working in tandem with the development team on component implementation, providing author supervision</li>
|
||||
<li>Conducting regular design reviews within the team</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="resume-exp__label">Achievements:</p>
|
||||
<ul>
|
||||
<li>Updated the product’s visual language to the Windows 10/11 standard—components are now a generation more modern</li>
|
||||
<li>Redesigned over 80 screens and dialogs, bringing them into a unified grid and typography</li>
|
||||
<li>Compiled a library of 140 components, covering 90% of the interface’s needs</li>
|
||||
<li>Converted icons and some illustrations to vector graphics: ensuring correct display at any zoom level and DPI</li>
|
||||
<li>Reduced the time it took the team to build a new screen from 2 days to 4 hours by using pre-built components</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="resume-exp__label">Stack:</p>
|
||||
<ul>
|
||||
<li>Desktop IDE</li>
|
||||
<li>Windows 10/11</li>
|
||||
<li>Component library</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="sep sep--air">
|
||||
|
||||
<div class="resume-exp__section">
|
||||
<p class="resume-exp__section-title">Robot Automation Studio (Linux)</p>
|
||||
<p class="resume-exp__desc">An environment for developing and debugging robots for business process automation.</p>
|
||||
<div class="resume-exp__cols">
|
||||
<div>
|
||||
<p class="resume-exp__label">Responsibilities:</p>
|
||||
<ul>
|
||||
<li>Adapting the Studio design for the Linux version using the Avalonia framework</li>
|
||||
<li>Designing components while taking into account the framework’s technical limitations</li>
|
||||
<li>Synchronizing the visual language between the Windows and Linux versions of the product</li>
|
||||
<li>Collaborating with the development team to determine what is technically feasible in Avalonia and what requires workarounds</li>
|
||||
<li>Preparing component behavior specifications for implementation</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="resume-exp__label">Achievements:</p>
|
||||
<ul>
|
||||
<li>Compiled an extensive library of 95 components for Avalonia, which did not exist in the project before I joined</li>
|
||||
<li>Ensured product consistency across two platforms with different technology stacks</li>
|
||||
<li>Reduced the number of visual discrepancies between versions from several dozen to just a few isolated cases</li>
|
||||
<li>Provided the Linux version team with a ready-made set of solutions instead of having to design each screen from scratch</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="resume-exp__label">Stack:</p>
|
||||
<ul>
|
||||
<li>Avalonia</li>
|
||||
<li>.NET</li>
|
||||
<li>Linux</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="sep sep--air">
|
||||
|
||||
<div class="resume-exp__section">
|
||||
<p class="resume-exp__section-title">Idea Hub Dashboard (SaaS)</p>
|
||||
<p class="resume-exp__desc">Enables the collection and analysis of employee suggestions, performance evaluation, and monitoring of automation implementation status. Analytics on key performance indicators and a high-level view of automation.</p>
|
||||
<div class="resume-exp__cols">
|
||||
<div>
|
||||
<p class="resume-exp__label">Responsibilities:</p>
|
||||
<ul>
|
||||
<li>Designing dashboards and analytical views for robotization KPIs</li>
|
||||
<li>Managing the idea lifecycle: submission → evaluation → prioritization → implementation → impact measurement</li>
|
||||
<li>Section information architecture, working with dense tables, filters, and bulk operations</li>
|
||||
<li>Designing a role-based model: employee, analyst, manager</li>
|
||||
<li>Selecting data visualization types for specific management questions</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="resume-exp__label">Achievements:</p>
|
||||
<ul>
|
||||
<li>Integrated high-level analytics and work on a specific idea into a single continuous workflow without switching between sections</li>
|
||||
<li>Designed data visualizations so that managers can see the impact of automation without having to manually export data to Excel</li>
|
||||
<li>Reduced the process from idea submission to evaluation from 6 steps to 3</li>
|
||||
<li>Designed 12 analytics widgets covering customers’ key questions about the ROI of automation</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="resume-exp__label">Stack:</p>
|
||||
<ul>
|
||||
<li>Analytics / Dashboards</li>
|
||||
<li>KPIs</li>
|
||||
<li>Data Visualization</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="sep sep--air">
|
||||
|
||||
<div class="resume-exp__section">
|
||||
<p class="resume-exp__section-title">Orchestrator Automation Management Tool (SaaS)</p>
|
||||
<p class="resume-exp__desc">Ensures seamless system operation and management of hundreds of robots: launching, monitoring, and management.</p>
|
||||
<div class="resume-exp__cols">
|
||||
<div>
|
||||
<p class="resume-exp__label">Responsibilities:</p>
|
||||
<ul>
|
||||
<li>Designing a management interface for a fleet of hundreds of robots: schedules, queues, priorities, and monitoring</li>
|
||||
<li>Developing state management and error handling for bulk launches</li>
|
||||
<li>Designing a system for notifications and alerts regarding failures</li>
|
||||
<li>Working with complex tables: sorting, filtering, bulk actions, saved views</li>
|
||||
<li>Prototyping complex scenarios for validation prior to development</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="resume-exp__label">Achievements:</p>
|
||||
<ul>
|
||||
<li>Designed a system to manage hundreds of objects so that the operator doesn’t get lost in the scale: from the big picture to a specific robot in just 2 clicks</li>
|
||||
<li>Aligned the service with a unified design system alongside the rest of the product line’s web products</li>
|
||||
<li>Reduced operator response time to robot failures from 12 to 4 minutes thanks to a redesigned alert system</li>
|
||||
<li>Consolidated 5 different robot list views into a single customizable one</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="resume-exp__label">Stack:</p>
|
||||
<ul>
|
||||
<li>Monitoring</li>
|
||||
<li>Schedules / Queues</li>
|
||||
<li>Notifications / Alerts</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="sep sep--air">
|
||||
|
||||
<div class="resume-exp__section">
|
||||
<p class="resume-exp__section-title">Q&A Portal (WEB)</p>
|
||||
<p class="resume-exp__desc">A platform for posting questions and receiving answers from support</p>
|
||||
<div class="resume-exp__cols">
|
||||
<div>
|
||||
<p class="resume-exp__label">Responsibilities:</p>
|
||||
<ul>
|
||||
<li>Designing workflows for posting questions, providing answers, and searching the accumulated knowledge base</li>
|
||||
<li>Designing the portal interface based on the company’s common design system</li>
|
||||
<li>Developing a role model: user, expert, support team, moderator</li>
|
||||
<li>Designing search mechanics and suggestions for similar questions when posting</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="resume-exp__label">Achievements:</p>
|
||||
<ul>
|
||||
<li>Launched the portal using off-the-shelf components from the design system in 3 weeks instead of the planned 8</li>
|
||||
<li>Reduced the support workload by approximately 25% by reusing existing answers</li>
|
||||
<li>The suggestion feature for similar questions reduced duplicate questions by 45%</li>
|
||||
<li>Ensured visual consistency with other products in the line—the portal does not look like a third-party service</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="resume-exp__label">Stack:</p>
|
||||
<ul>
|
||||
<li>Web portal</li>
|
||||
<li>Design system</li>
|
||||
<li>Knowledge base</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="sep sep--air">
|
||||
|
||||
<div class="resume-exp__section">
|
||||
<p class="resume-exp__section-title">Company-wide</p>
|
||||
<ul>
|
||||
<li>I initiated a visual refresh of the entire product line—the company decided to redesign the products upon my arrival</li>
|
||||
<li>I developed and implemented a unified design system for web services, integrating the company’s brand identity with the new interface structure</li>
|
||||
<li>The design system was adopted by four product teams; it includes 210 components and theming tokens</li>
|
||||
<li>Reduced the average time to prepare mockups across the product line by 35%</li>
|
||||
<li>Created and maintained a UI kit to ensure consistency and accelerate development</li>
|
||||
<li>Conducted more than 20 user studies and usability sessions during my tenure</li>
|
||||
<li>Oversaw implementation, participated in design reviews, product discussions, and release planning</li>
|
||||
<li>Served as the sole lead designer for an entire product line consisting of 6 products and two platforms</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="sep sep--line">
|
||||
|
||||
<div class="resume-exp">
|
||||
<div class="resume-exp__header">
|
||||
<p class="resume-exp__company">Lunacy (Icons8)</p>
|
||||
<p class="resume-exp__meta-row"><span>July 2022 – January 2023 (7 months)</span> <span class="resume-exp__sep">·</span> <a href="https://icons8.com/lunacy" target="_blank" rel="noopener noreferrer">icons8.com/lunacy</a></p>
|
||||
</div>
|
||||
<p class="resume-exp__desc">A graphic editor for working on interface designs and other mockups. Similar to Sketch App and Figma.</p>
|
||||
|
||||
<div class="resume-exp__section">
|
||||
<div class="resume-exp__cols">
|
||||
<div>
|
||||
<p class="resume-exp__label">Responsibilities:</p>
|
||||
<ul>
|
||||
<li>Designing the graphic editor’s interface: tools, property panels, working with the canvas and layers</li>
|
||||
<li>Documenting solutions as user flows and handing them off to developers</li>
|
||||
<li>Creating complex interactive prototypes in Protopie to demonstrate tool behavior</li>
|
||||
<li>Illustrating missing icons in the product’s style</li>
|
||||
<li>Making suggestions for improving the editor’s functionality</li>
|
||||
<li>Participating in discussions about product solutions with the team</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="resume-exp__label">Achievements:</p>
|
||||
<ul>
|
||||
<li>Designed 6 functional blocks for the editor, including work with components and styles</li>
|
||||
<li>Created 15+ prototypes in Protopie, which helped eliminate unsuccessful solutions before development</li>
|
||||
<li>Some of my feature suggestions were included in product releases</li>
|
||||
<li>Designed over 60 icons to complement the editor’s system set</li>
|
||||
<li>Worked on a product for a professional audience of designers, where the requirements for interface details are higher than usual</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="resume-exp__label">Stack:</p>
|
||||
<ul>
|
||||
<li>Protopie</li>
|
||||
<li>Graphics editor</li>
|
||||
<li>Icons / Illustrations</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="sep sep--line">
|
||||
|
||||
<div class="resume-exp">
|
||||
<div class="resume-exp__header">
|
||||
<p class="resume-exp__company">MANU Video Game Maker</p>
|
||||
<p class="resume-exp__meta-row"><span>January 2019 — December 2021 (3 years)</span><a href="https://manu.co/" target="_blank" rel="noopener noreferrer">·manu.co</a></p>
|
||||
</div>
|
||||
<p class="resume-exp__desc">A game engine for creating video games without writing code.</p>
|
||||
|
||||
<div class="resume-exp__section">
|
||||
<div class="resume-exp__cols">
|
||||
<div>
|
||||
<p class="resume-exp__label">Responsibilities:</p>
|
||||
<ul>
|
||||
<li>End-to-end product design of the engine: from interaction concepts to the final UI</li>
|
||||
<li>Designing interface behavior for complex user scenarios</li>
|
||||
<li>User research: observation, in-depth interviews, surveys</li>
|
||||
<li>Creating user personas and empathy maps</li>
|
||||
<li>Working with product analytics and metrics tied to user scenarios</li>
|
||||
<li>Layout of the website’s homepage on Tilda, designing the site’s information architecture together with the team</li>
|
||||
<li>Supervising the work of the web graphic designer</li>
|
||||
<li>Hiring for the product department: conducting interviews and selecting specialists</li>
|
||||
<li>Leading the department and establishing workflows for the entire product team</li>
|
||||
<li>Overseeing the creation of user documentation</li>
|
||||
<li>Implementing automation tools (Integromat, Zapier) for internal tasks</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="resume-exp__label">Achievements:</p>
|
||||
<ul>
|
||||
<li>Led a large-scale redesign of the entire program: established a fundamentally new concept and style</li>
|
||||
<li>Designed the interface behavior for over 100 complex user scenarios</li>
|
||||
<li>Designed the product’s design system from scratch: 120 components</li>
|
||||
<li>Received a lot of positive, and in some cases enthusiastic, user feedback—the product became much easier to navigate</li>
|
||||
<li>Onboarding completion rates increased from 34% to 58% following the redesign</li>
|
||||
<li>Conducted 25 in-depth interviews and 4 rounds of surveys, and revised 3 key scenarios based on the results</li>
|
||||
<li>Hired 4 specialists for the product department and grew from a designer to a team lead</li>
|
||||
<li>Established departmental processes: design reviews, backlog management, and handoff to development</li>
|
||||
<li>Automated the team’s internal routines, freeing up time for product tasks</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="resume-exp__label">Stack:</p>
|
||||
<ul>
|
||||
<li>Tilda</li>
|
||||
<li>Integromat / Zapier</li>
|
||||
<li>User research</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="sep sep--line">
|
||||
|
||||
<div class="resume-exp">
|
||||
<div class="resume-exp__header">
|
||||
<p class="resume-exp__company">Aegees Messenger</p>
|
||||
<p class="resume-exp__meta-row"><span>October 2017 — December 2018 (1 year and 3 months)</span><a href="https://aegees.com/" target="_blank" rel="noopener noreferrer">·aegees.com</a></p>
|
||||
</div>
|
||||
<p class="resume-exp__desc">A cross-platform messenger with secure storage and audio calls.</p>
|
||||
|
||||
<div class="resume-exp__section">
|
||||
<div class="resume-exp__cols">
|
||||
<div>
|
||||
<p class="resume-exp__label">Responsibilities:</p>
|
||||
<ul>
|
||||
<li>Designing the messenger for five platforms simultaneously: Android, iOS, macOS, Windows, and Linux</li>
|
||||
<li>Developing use cases for secure storage and audio calls</li>
|
||||
<li>Designing scenarios for registration, encryption, and access recovery</li>
|
||||
<li>Adapting a unified product logic to each platform’s guidelines</li>
|
||||
<li>Supervising the work of international illustrators in the preparation of graphic assets</li>
|
||||
<li>Preparation of materials for app stores</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="resume-exp__label">Achievements:</p>
|
||||
<ul>
|
||||
<li>Developed a unified visual language for the product while adhering to the guidelines of five platforms simultaneously</li>
|
||||
<li>Designed over 90 screens across five platform versions</li>
|
||||
<li>Ensured a steady flow of graphic assets from external contractors without compromising the design style</li>
|
||||
<li>Simplified the secure storage workflow so that people without technical training could use it</li>
|
||||
<li>Worked in the sensitive areas of privacy and security, where a single interface error can cost user trust</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="resume-exp__label">Stack:</p>
|
||||
<ul>
|
||||
<li>Android</li>
|
||||
<li>iOS</li>
|
||||
<li>macOS</li>
|
||||
<li>Windows</li>
|
||||
<li>Linux</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="sep sep--line">
|
||||
|
||||
<div class="resume-exp">
|
||||
<div class="resume-exp__header">
|
||||
<p class="resume-exp__company">Orion Innovation (formerly MERA)</p>
|
||||
<p class="resume-exp__meta-row"><span>July 2016 – October 2017 (1 year and 4 months)</span><a href="https://www.orioninnovation.com/" target="_blank" rel="noopener noreferrer">·orioninnovation.com</a></p>
|
||||
</div>
|
||||
<p class="resume-exp__desc">Software development for external clients.</p>
|
||||
|
||||
<div class="resume-exp__section">
|
||||
<div class="resume-exp__cols">
|
||||
<div>
|
||||
<p class="resume-exp__label">Responsibilities:</p>
|
||||
<ul>
|
||||
<li>Worked in a design department with up to 8 people</li>
|
||||
<li>Creating user personas and mapping product functionality based on user needs</li>
|
||||
<li>Creating wireframes and designing the final UI</li>
|
||||
<li>Designing use cases, screen maps, and micro-animation demos</li>
|
||||
<li>Preparing mood boards and design concepts, and discussing them with clients</li>
|
||||
<li>Preparing and delivering presentations on design concepts</li>
|
||||
<li>Turning mockups into prototypes, writing scripts, and conducting qualitative usability testing</li>
|
||||
<li>Making revisions based on test results</li>
|
||||
<li>Overseeing the implementation of designs by developers</li>
|
||||
<li>Participating in design reviews: analyzing colleagues’ work and defending your own decisions</li>
|
||||
<li>Direct communication with international clients to discuss design and receive project briefs</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="resume-exp__label">Achievements:</p>
|
||||
<ul>
|
||||
<li>Conducted 10 usability testing sessions, from writing the test script to implementing changes</li>
|
||||
<li>Learned to work with the Smart TV platform, which was new to me, and brought the project to successful release in the app store</li>
|
||||
<li>Communicated directly with international clients, without intermediaries</li>
|
||||
<li>Actively participated in improving the company’s design department workflow</li>
|
||||
<li>Completed the full product design cycle on 5 client projects</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="resume-exp__label">Stack:</p>
|
||||
<ul>
|
||||
<li>Smart TV</li>
|
||||
<li>Usability testing</li>
|
||||
<li>Mood boards / design concepts</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="sep sep--line">
|
||||
|
||||
<div class="resume-exp">
|
||||
<div class="resume-exp__header">
|
||||
<p class="resume-exp__company">SpaceshipApps Studio</p>
|
||||
<p class="resume-exp__meta-row"><span>June 2015 — June 2016 (1 year and 1 month)</span><a href="https://spaceshipapps.ru/" target="_blank" rel="noopener noreferrer">·spaceshipapps.ru</a></p>
|
||||
</div>
|
||||
<p class="resume-exp__desc">Mobile app and website development studio.</p>
|
||||
|
||||
<div class="resume-exp__section">
|
||||
<div class="resume-exp__cols">
|
||||
<div>
|
||||
<p class="resume-exp__label">Responsibilities:</p>
|
||||
<ul>
|
||||
<li>Designing mobile and web products as part of the studio’s workflow</li>
|
||||
<li>Worked across various sectors: social gaming, beauty services, food delivery, voice interfaces, legal services, and collaborative tools</li>
|
||||
<li>Responsive web design and design of administrative dashboards</li>
|
||||
<li>Illustrating icons and preparing graphics for projects</li>
|
||||
<li>Preparing mockups for handoff to development</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="resume-exp__label">Achievements:</p>
|
||||
<ul>
|
||||
<li>Launched 6 products in one year: a geolocation-based social game with real-world quests, an app for booking beauty professionals, a food delivery service, a voice-based word game, a website and admin panel for a copyright registration service, and a mobile “whiteboard” for collaboration</li>
|
||||
<li>Learned to quickly get up to speed in new subject areas and maintain quality while keeping up with the studio’s fast pace</li>
|
||||
<li>Handled the full range of tasks: mobile, web, admin panels, and graphics</li>
|
||||
<li>Laid the groundwork for transitioning from execution-focused design to product-focused design</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="resume-exp__label">Stack:</p>
|
||||
<ul>
|
||||
<li>Mobile</li>
|
||||
<li>Web</li>
|
||||
<li>Dashboards</li>
|
||||
<li>Graphics</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="sep sep--air">
|
||||
|
||||
<h2 id="skills">Skills</h2>
|
||||
<div class="resume-skills">
|
||||
<div class="resume-skill">
|
||||
<p class="resume-skill__title">Design</p>
|
||||
<p class="resume-skill__desc">UX/UI, product design, information architecture, responsive design, design systems</p>
|
||||
</div>
|
||||
<div class="resume-skill">
|
||||
<p class="resume-skill__title">Research</p>
|
||||
<p class="resume-skill__desc">Interviews, surveys, usability testing, JTBD, empathy maps, conversion, retention, NPS</p>
|
||||
</div>
|
||||
<div class="resume-skill">
|
||||
<p class="resume-skill__title">Tools</p>
|
||||
<p class="resume-skill__desc">Figma, Protopie, Sketch, Adobe Creative Suite, Tilda, AI tools</p>
|
||||
</div>
|
||||
<div class="resume-skill">
|
||||
<p class="resume-skill__title">Management</p>
|
||||
<p class="resume-skill__desc">Team building, Agile, Kanban, project supervision, working with stakeholders</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="sep sep--air">
|
||||
|
||||
<h2 id="about">About Me</h2>
|
||||
<p class="section__prose resume-about">
|
||||
I’ve been in design for over 10 years, and in recent years I’ve focused on product design for complex B2B products. I work at the intersection of design and product management: I conduct research, formulate hypotheses and features, help with prioritization, and participate in release planning.
|
||||
</p>
|
||||
<p class="section__prose resume-about">
|
||||
I love diving into complex topics and making them understandable—which is why I feel right at home with products like IDEs, orchestration tools, and ML dashboards, where a typical designer might find the work boring or intimidating.
|
||||
</p>
|
||||
<p class="section__prose resume-about">
|
||||
I don’t wait for someone else to solve a problem—I build my own tools. I’ve created a private AI plugin for Figma, automate processes using n8n and PostgreSQL, and turn my ideas into working products through AI-assisted development. My skills for Alisa have attracted over 37,000 and 33,000 users, and the Slider Set file for designers has been downloaded more than 20,000 times.
|
||||
</p>
|
||||
<p class="section__prose resume-about">
|
||||
I’m active in the community: I run the Telegram channel “Design Isn’t Pizza,” share useful resources on the Figma Community, gave a talk on analytics at a Yandex meetup, and have won hackathons—my most memorable victory was at the Yandex.I’m currently in St. Petersburg working on a QR code payment app.
|
||||
</p>
|
||||
<p class="section__prose resume-about">
|
||||
English—B1. I work remotely and communicate smoothly with a distributed team.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{> footer}}
|
||||
{{> cookie-notice}}
|
||||
<script>
|
||||
(function () {
|
||||
var printBtn = document.querySelector('[data-resume-print-btn]');
|
||||
if (!printBtn) return;
|
||||
|
||||
printBtn.addEventListener('click', function () {
|
||||
var doc = document.querySelector('.resume-doc');
|
||||
if (!doc) return;
|
||||
|
||||
var styles = [];
|
||||
document.querySelectorAll('link[rel="stylesheet"]').forEach(function (l) {
|
||||
styles.push('<link rel="stylesheet" href="' + l.href + '">');
|
||||
});
|
||||
document.querySelectorAll('style').forEach(function (s) {
|
||||
styles.push('<style>' + s.textContent + '</style>');
|
||||
});
|
||||
|
||||
var win = window.open('', '_blank');
|
||||
if (!win) return;
|
||||
win.document.write(
|
||||
'<!DOCTYPE html><html><head><meta charset="utf-8">' +
|
||||
styles.join('') +
|
||||
'<style>@media print{body{margin:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;}}</style>' +
|
||||
'</head><body>' + doc.outerHTML + '</body></html>'
|
||||
);
|
||||
win.document.close();
|
||||
win.onload = function () {
|
||||
setTimeout(function () { win.print(); }, 500);
|
||||
};
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
<div class="content-edit-fab-stack" aria-label="Content Edits" data-content-edit-ui>
|
||||
<button
|
||||
type="button"
|
||||
class="fab fab--secondary glass-neutral content-edit-fab"
|
||||
data-content-edit-mode-toggle
|
||||
hidden
|
||||
aria-pressed="false"
|
||||
aria-label="Content Editing Mode"
|
||||
title="Edit Mode"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M4 20h4l10.5-10.5a2.12 2.12 0 0 0 0-3L16.5 4.5a2.12 2.12 0 0 0-3 0L3 15v5z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.75"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span class="content-edit-fab__badge" hidden aria-hidden="true">0</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1,12 +1,12 @@
|
||||
<aside class="cookie-notice glass-neutral" hidden data-cookie-notice>
|
||||
<p class="cookie-notice__text">
|
||||
This site uses cookies for a comfortable experience. Read more in
|
||||
<a href="{{pagePrefix}}cookie-policy/" data-cookie-policy-link>cookie policy</a>.
|
||||
</p>
|
||||
<button type="button" class="btn btn--secondary btn--flat cookie-notice__accept" data-cookie-accept>
|
||||
<span>Accept</span>
|
||||
<span class="cookie-notice__accept-icon" aria-hidden="true">
|
||||
<img src="{{assetPrefix}}assets/shared/icons/сookie.svg" alt="" width="20" height="20" loading="eager" decoding="async" />
|
||||
</span>
|
||||
</button>
|
||||
</aside>
|
||||
<aside class="cookie-notice glass-neutral" hidden data-cookie-notice>
|
||||
<p class="cookie-notice__text">
|
||||
This site uses cookies to ensure a smooth user experience. For more information, see the
|
||||
<a href="{{pagePrefix}}cookie-policy/" data-cookie-policy-link>cookie policy</a>.
|
||||
</p>
|
||||
<button type="button" class="btn btn--secondary btn--flat cookie-notice__accept" data-cookie-accept>
|
||||
<span>Accept</span>
|
||||
|
||||
<span class="cookie-notice__accept-icon" aria-hidden="true"><img src="{{assetPrefix}}assets/shared/icons/сookie.svg" alt="" width="20" height="20" loading="eager" decoding="async" /></span>
|
||||
|
||||
</button>
|
||||
</aside>
|
||||
@@ -0,0 +1,62 @@
|
||||
<div class="fab-stack" aria-label="Quick Actions">
|
||||
<div class="theme-panel-fab-wrap">
|
||||
<button type="button" class="fab fab--secondary glass-neutral" data-theme-panel-btn aria-expanded="false" aria-controls="theme-panel" aria-label="Style Settings">
|
||||
<span class="theme-panel-fab__icon theme-panel-fab__icon--open-hide" aria-hidden="true"></span>
|
||||
<svg class="theme-panel-fab__icon theme-panel-fab__icon--close" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M6 6L18 18M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<div id="theme-panel" class="theme-picker__panel glass-neutral" role="dialog" aria-label="Theme and Design" hidden data-theme-panel>
|
||||
<div class="theme-picker__stack">
|
||||
<section class="theme-picker__section" aria-labelledby="theme-accent-title">
|
||||
<h3 id="theme-accent-title" class="theme-picker__section-title">Accent Color</h3>
|
||||
<div class="colorpicker" data-colorpicker>
|
||||
<div class="colorpicker__plane-box">
|
||||
<canvas class="colorpicker__plane" width="320" height="180" data-sl-canvas aria-label="Saturation and brightness of the accent"></canvas>
|
||||
<div class="colorpicker__knob" data-sl-knob></div>
|
||||
</div>
|
||||
<div class="colorpicker__hue-wrap">
|
||||
<label class="colorpicker__hue-sr" for="theme-hue-slider">Accent shade</label>
|
||||
<input id="theme-hue-slider" class="colorpicker__hue" type="range" min="0" max="360" value="210" data-hue-slider aria-valuemin="0" aria-valuemax="360" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="theme-picker__section" aria-labelledby="theme-mode-title">
|
||||
<h3 id="theme-mode-title" class="theme-picker__section-title">Theme</h3>
|
||||
<div class="radius-seg theme-mode-seg" role="radiogroup" aria-labelledby="theme-mode-title">
|
||||
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-theme-mode="system" aria-label="As in the system"><span class="radius-seg__viz" aria-hidden="true"><span class="theme-mode-seg__ico theme-mode-seg__ico--system"></span></span></button>
|
||||
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-theme-mode="light" aria-label="Light-colored theme"><span class="radius-seg__viz" aria-hidden="true"><span class="theme-mode-seg__ico theme-mode-seg__ico--light"></span></span></button>
|
||||
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-theme-mode="dark" aria-label="Dark Theme"><span class="radius-seg__viz" aria-hidden="true"><span class="theme-mode-seg__ico theme-mode-seg__ico--dark"></span></span></button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="theme-picker__section" aria-labelledby="theme-radius-title">
|
||||
<h3 id="theme-radius-title" class="theme-picker__section-title">Corner Rounding</h3>
|
||||
<div class="radius-seg" role="radiogroup" aria-labelledby="theme-radius-title">
|
||||
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-radius-option="soft" aria-label="Slight rounding"><span class="radius-seg__viz" aria-hidden="true"><span class="radius-seg__arc radius-seg__arc--soft"></span></span></button>
|
||||
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-radius-option="medium" aria-label="Average rounding"><span class="radius-seg__viz" aria-hidden="true"><span class="radius-seg__arc radius-seg__arc--medium"></span></span></button>
|
||||
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-radius-option="strong" aria-label="Significant rounding"><span class="radius-seg__viz" aria-hidden="true"><span class="radius-seg__arc radius-seg__arc--strong"></span></span></button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="theme-picker__section" aria-labelledby="theme-typography-title">
|
||||
<h3 id="theme-typography-title" class="theme-picker__section-title">Typography</h3>
|
||||
<div class="radius-seg" role="radiogroup" aria-labelledby="theme-typography-title">
|
||||
<button type="button" class="radius-seg__btn theme-font-seg__btn" role="radio" aria-checked="false" data-font-option="geologica" aria-label="Sans"><span class="theme-font-seg__label">Sans</span></button>
|
||||
<button type="button" class="radius-seg__btn theme-font-seg__btn" role="radio" aria-checked="false" data-font-option="merriweather" aria-label="Sans Serif"><span class="theme-font-seg__label">Sans Serif</span></button>
|
||||
<button type="button" class="radius-seg__btn theme-font-seg__btn" role="radio" aria-checked="false" data-font-option="mono" aria-label="Mono"><span class="theme-font-seg__label">Mono</span></button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="theme-picker__section" aria-labelledby="theme-mesh-bg-title">
|
||||
<h3 id="theme-mesh-bg-title" class="theme-picker__section-title">Page Background</h3>
|
||||
<div class="radius-seg radius-seg--two" role="radiogroup" aria-labelledby="theme-mesh-bg-title">
|
||||
<button type="button" class="radius-seg__btn theme-mesh-bg-seg__btn" role="radio" aria-checked="false" data-mesh-bg-option="on" aria-label="Show background gradient"><span class="theme-mesh-bg-seg__label">On</span></button>
|
||||
<button type="button" class="radius-seg__btn theme-mesh-bg-seg__btn" role="radio" aria-checked="false" data-mesh-bg-option="off" aria-label="Hide the background gradient"><span class="theme-mesh-bg-seg__label">Off</span></button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a class="fab fab--primary" href="{{pagePrefix}}#contact" aria-label="Contact Us">
|
||||
<span class="fab-primary__ico" aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
{{> content-edit-fab-stack}}
|
||||
@@ -0,0 +1,12 @@
|
||||
<div class="fab-stack" aria-label="Quick Actions">
|
||||
<div class="theme-panel-fab-wrap">
|
||||
<button type="button" class="fab fab--secondary glass-neutral" data-resume-print-btn aria-label="Print Resume">
|
||||
<span class="fab-secondary__ico fab-secondary__ico--print" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
<a href="/assets/resume/Mamadaev_Nik__CV__Product_Designer_UX-UI.pdf" download="Mamadaev_Nik__CV__Product_Designer_UX-UI.pdf" class="fab fab--primary fab--label" aria-label="Download the resume as a PDF">
|
||||
<span class="fab-primary__ico fab-primary__ico--pdf" aria-hidden="true"></span>
|
||||
<span class="fab__label">Download as PDF</span>
|
||||
</a>
|
||||
</div>
|
||||
{{> content-edit-fab-stack}}
|
||||
@@ -0,0 +1,3 @@
|
||||
<section class="section section--surface" aria-hidden="true">
|
||||
<div class="section__inner"></div>
|
||||
</section>
|
||||
@@ -0,0 +1,8 @@
|
||||
<footer class="footer">
|
||||
<div class="footer__inner">
|
||||
<span class="footer__chunk footer__chunk--date">© 2015–2026<span class="footer__sep" aria-hidden="true"> ·
|
||||
</span></span>
|
||||
<span class="footer__chunk footer__chunk--main">{{i18n:footer.credit}}</span> ·<span class="footer__chunk footer__chunk--meta footer__attr"><span class="footer__sep" aria-hidden="true"> </span><a href="{{pagePrefix}}cases/design-system/">{{i18n:footer.designSystem}}</a><span class="footer__sep" aria-hidden="true">·
|
||||
</span><a href="{{pagePrefix}}cookie-policy/">{{i18n:footer.cookiePolicy}}</a></span> ·<span class="footer__chunk footer__chunk--lang"><span class="footer__sep" aria-hidden="true">
|
||||
</span>{{> lang-switch}}</span></div>
|
||||
</footer>
|
||||
@@ -0,0 +1 @@
|
||||
<link rel="stylesheet" href="/vendor/plyr/plyr.css" />
|
||||
@@ -0,0 +1,2 @@
|
||||
<script src="/js/skill-connectors.js" defer></script>
|
||||
<script type="module" src="/js/main.js"></script>
|
||||
@@ -1,7 +1,7 @@
|
||||
<a class="skip-link" href="#case-main">{{i18n:skip.case}}</a>
|
||||
<header class="header" data-theme-picker>
|
||||
<div class="header__inner">
|
||||
<a class="logo" href="{{pagePrefix}}" aria-label="{{i18n:logo.aria}}">НМ</a>
|
||||
<a class="logo" href="{{pagePrefix}}" aria-label="{{i18n:logo.aria}}">{{i18n:logo.text}}</a>
|
||||
<div class="header__cluster">
|
||||
<nav class="nav" aria-label="{{i18n:nav.aria}}">
|
||||
<a href="#contact">{{i18n:nav.contact}}</a>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<header class="header" data-theme-picker>
|
||||
<div class="header__inner">
|
||||
<a class="logo" href="#" aria-label="{{i18n:logo.aria}}">НМ</a>
|
||||
<a class="logo" href="#" aria-label="{{i18n:logo.aria}}">{{i18n:logo.text}}</a>
|
||||
<div class="header__cluster">
|
||||
<nav class="nav" aria-label="{{i18n:nav.aria}}" data-top-nav>
|
||||
<a href="#projects">{{i18n:nav.projects}}</a>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<header class="header" data-theme-picker>
|
||||
<div class="header__inner">
|
||||
<a class="logo" href="{{pagePrefix}}" aria-label="{{i18n:logo.aria}}">НМ</a>
|
||||
<a class="logo" href="{{pagePrefix}}" aria-label="{{i18n:logo.aria}}">{{i18n:logo.text}}</a>
|
||||
<div class="header__cluster">
|
||||
<nav class="nav" aria-label="{{i18n:nav.aria}}">
|
||||
<a href="{{pagePrefix}}">{{i18n:back.home}}</a>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<nav class="lang-switch" aria-label="{{i18n:lang.aria}}" data-lang-switch>
|
||||
<a class="lang-switch__btn" data-lang="ru" href="/">{{i18n:lang.ru}}</a>
|
||||
<a class="lang-switch__btn" data-lang="en" href="/en/">{{i18n:lang.en}}</a>
|
||||
</nav>
|
||||
<script>
|
||||
(function () {
|
||||
var p = location.pathname.replace(/^\/+/, "");
|
||||
var isEn = p.indexOf("en/") === 0;
|
||||
var base = isEn ? p.slice(3) : p;
|
||||
var links = document.querySelectorAll("[data-lang-switch] [data-lang]");
|
||||
for (var i = 0; i < links.length; i++) {
|
||||
var b = links[i];
|
||||
var l = b.getAttribute("data-lang");
|
||||
b.setAttribute("href", (l === "en" ? "/en/" : "/") + base);
|
||||
if ((l === "en") === isEn) b.setAttribute("aria-current", "true");
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,52 @@
|
||||
<section class="section section--surface" data-reveal>
|
||||
<div class="section__inner">
|
||||
<h2 class="section__title">See also</h2>
|
||||
<ol class="mini-projects-grid" data-mini-projects-list>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="3" data-mp-cells-sm="2" data-related-slug="ai-assistant">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--compact"
|
||||
href="{{casesPrefix}}ai-assistant-figma/"
|
||||
aria-label="AI Assistant — Open a Case"
|
||||
>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">AI Assistant</h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">Figma plugin</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">A Figma plugin with AI features.</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="3" data-mp-cells-sm="2" data-related-slug="card-columns">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--compact"
|
||||
href="{{casesPrefix}}card-columns-obsidian/"
|
||||
aria-label="Card Columns — Open a Case"
|
||||
>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">Card Columns</h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">Obsidian Plugin</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">A plugin for displaying cards in columns in Obsidian.</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="3" data-mp-cells-sm="2" data-related-slug="site-with-ai-ide">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--compact"
|
||||
href="{{casesPrefix}}site-with-ai-ide/"
|
||||
aria-label="This website — Open a Case"
|
||||
>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">This website</h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">Development with AI IDE</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Website development using AI IDE.</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,190 @@
|
||||
<ol
|
||||
id="mini-projects-showcase-grid"
|
||||
class="mini-projects-grid mp-showcase-grid"
|
||||
data-mini-projects-list
|
||||
>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a class="mp-card mp-card--link mp-card--stack" href="#" aria-label="[2] stack · link">
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="../assets/shared/placeholder.webp"
|
||||
alt=""
|
||||
width="560"
|
||||
height="315"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
[2] stack · link
|
||||
|
||||
<span class="mp-card__ext" aria-hidden="true"></span></h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">website</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">A base pair in a row of six cells.</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<div class="mp-card mp-card--stack">
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="../assets/shared/placeholder.webp"
|
||||
alt=""
|
||||
width="560"
|
||||
height="315"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">[2] stack</h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">note</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Stack without an external link.</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<div class="mp-card mp-card--horizontal">
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="../assets/shared/placeholder.webp"
|
||||
alt=""
|
||||
width="360"
|
||||
height="203"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">[2] horizontal</h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">utility</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Horizontal, two cells wide, without a link.</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<article class="mp-card mp-card--compact">
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">[2] compact</h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">template</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">A two-cell text card.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="1">
|
||||
<a class="mp-card mp-card--link mp-card--compact" href="#" aria-label="[1] compact · link">
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
[1] compact · link
|
||||
|
||||
<span class="mp-card__ext" aria-hidden="true"></span></h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">cli</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">A narrow column with a link.</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="1">
|
||||
<article class="mp-card mp-card--compact">
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">[1] compact</h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">document</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Compact, one cell wide.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<div class="mp-card mp-card--decor" aria-hidden="true">
|
||||
<span class="mp-card__decor-pattern" aria-hidden="true"></span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="3">
|
||||
<a class="mp-card mp-card--link mp-card--stack" href="#" aria-label="[3] stack · link">
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="../assets/shared/placeholder.webp"
|
||||
alt=""
|
||||
width="560"
|
||||
height="315"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
[3] stack · link
|
||||
|
||||
<span class="mp-card__ext" aria-hidden="true"></span></h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">Appendix</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">
|
||||
Half of a six-cell row. A longer description to check for line breaks and height after changing the font.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="3">
|
||||
<a class="mp-card mp-card--link mp-card--horizontal" href="#" aria-label="[3] horizontal · link">
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="../assets/shared/placeholder.webp"
|
||||
alt=""
|
||||
width="360"
|
||||
height="203"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
[3] horizontal · link
|
||||
|
||||
<span class="mp-card__ext" aria-hidden="true"></span></h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">plugin</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Wide horizontal: preview and text in a single column.</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="3">
|
||||
<div class="mp-card mp-card--horizontal">
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="../assets/shared/placeholder.webp"
|
||||
alt=""
|
||||
width="360"
|
||||
height="203"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">[3] horizontal</h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">widget</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Three-cell Horizontal, without a link.</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,217 @@
|
||||
<div class="mp-showcase-specimens" aria-label="Card Types">
|
||||
<div class="mp-showcase-specimen-row">
|
||||
<div class="mp-showcase-specimen__card">
|
||||
<p class="mp-showcase-specimen__label"><code>mp-card--stack</code> + <code>mp-card--link</code></p>
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--stack"
|
||||
href="#"
|
||||
aria-label="Stack with a link — example card"
|
||||
>
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="../assets/shared/placeholder.webp"
|
||||
alt=""
|
||||
width="560"
|
||||
height="315"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
Stack with a link
|
||||
|
||||
<span class="mp-card__ext" aria-hidden="true"></span></h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">website</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">
|
||||
A small script: drafts generated from keywords, without external APIs.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mp-showcase-specimen__desc">
|
||||
<p class="section__prose">
|
||||
<strong>Stack with a link.</strong> Full-width preview, text at the bottom. For an external link:
|
||||
<code>mp-card--link</code> and <code>mp-card__ext</code> in the title (as in the "Experience" block).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mp-showcase-specimen-row">
|
||||
<div class="mp-showcase-specimen__card">
|
||||
<p class="mp-showcase-specimen__label"><code>mp-card--stack</code> without a link</p>
|
||||
<div class="mp-card mp-card--stack">
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="../assets/shared/placeholder.webp"
|
||||
alt=""
|
||||
width="560"
|
||||
height="315"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">Stack without a link</h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">note</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">
|
||||
A collection of databases and a rollup for a weekly overview, for personal use only.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mp-showcase-specimen__desc">
|
||||
<p class="section__prose">
|
||||
<strong>Stack without a link.</strong> Same view. Wrapper <code>div</code> or <code>article</code>, without
|
||||
<code>mp-card--link</code>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mp-showcase-specimen-row">
|
||||
<div class="mp-showcase-specimen__card">
|
||||
<p class="mp-showcase-specimen__label"><code>mp-card--horizontal</code> + <code>mp-card--link</code></p>
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--horizontal"
|
||||
href="#"
|
||||
aria-label="Horizontal with a link — example card"
|
||||
>
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="../assets/shared/placeholder.webp"
|
||||
alt=""
|
||||
width="360"
|
||||
height="203"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
Horizontal with a link
|
||||
|
||||
<span class="mp-card__ext" aria-hidden="true"></span></h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">app</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">
|
||||
Local timer with short sounds and a feature to block unnecessary tabs.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mp-showcase-specimen__desc">
|
||||
<p class="section__prose">
|
||||
<strong>Horizontal with a link.</strong> Thumbnail on the left, text on the right. On a narrow screen, the card
|
||||
stacks vertically.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mp-showcase-specimen-row">
|
||||
<div class="mp-showcase-specimen__card">
|
||||
<p class="mp-showcase-specimen__label"><code>mp-card--horizontal</code> No link</p>
|
||||
<div class="mp-card mp-card--horizontal">
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="../assets/shared/placeholder.webp"
|
||||
alt=""
|
||||
width="360"
|
||||
height="203"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">Horizontal without a link</h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">Template</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">
|
||||
Static website with a schedule and an application form on a separate service.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mp-showcase-specimen__desc">
|
||||
<p class="section__prose">
|
||||
<strong>Horizontal without a link.</strong> Same layout; the card does not link to an external URL.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mp-showcase-specimen-row">
|
||||
<div class="mp-showcase-specimen__card">
|
||||
<p class="mp-showcase-specimen__label"><code>mp-card--compact</code> + <code>mp-card--link</code></p>
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--compact"
|
||||
href="#"
|
||||
aria-label="Compact with a link — example card"
|
||||
>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
Compact with a link
|
||||
|
||||
<span class="mp-card__ext" aria-hidden="true"></span></h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">script</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">
|
||||
Prefixes, numbering, and a dry run before writing files to disk.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mp-showcase-specimen__desc">
|
||||
<p class="section__prose">
|
||||
<strong>Compact with a link.</strong> Text only, no <code>mp-card__media</code>. In the grid, often with
|
||||
<code>data-mp-cells="1"</code>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mp-showcase-specimen-row">
|
||||
<div class="mp-showcase-specimen__card">
|
||||
<p class="mp-showcase-specimen__label"><code>mp-card--compact</code> no link</p>
|
||||
<article class="mp-card mp-card--compact">
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">Compact without a link</h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">document</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">
|
||||
Markdown checklist before releasing minor UI changes.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="mp-showcase-specimen__desc">
|
||||
<p class="section__prose">
|
||||
<strong>Compact without a link.</strong> A short note or checklist without an external link.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mp-showcase-specimen-row">
|
||||
<div class="mp-showcase-specimen__card">
|
||||
<p class="mp-showcase-specimen__label"><code>mp-card--decor</code></p>
|
||||
<div class="mp-card mp-card--decor" aria-hidden="true">
|
||||
<span class="mp-card__decor-pattern" aria-hidden="true"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mp-showcase-specimen__desc">
|
||||
<p class="section__prose">
|
||||
<strong>Decorative cell.</strong> A placeholder in the grid, with no project or link. Inside
|
||||
<code>span.mp-card__decor-pattern</code>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<div id="nav-more-menu-floating" class="nav-more__floating" hidden data-nav-more-menu-floating aria-hidden="true"></div>
|
||||
@@ -0,0 +1,2 @@
|
||||
<script src="/js/skill-connectors.js" defer></script>
|
||||
<script type="module" src="/js/main.js"></script>
|
||||
@@ -0,0 +1,167 @@
|
||||
<section class="section section--surface" data-reveal>
|
||||
<div class="section__inner">
|
||||
<h2 class="section__title">See also</h2>
|
||||
<ol class="mini-projects-grid" data-mini-projects-list>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="3" data-mp-cells-sm="2" data-related-slug="ai-platform-primo-rpa">
|
||||
<a class="mp-card mp-card--link mp-card--stack" href="{{casesPrefix}}ai-platform-primo-rpa/" aria-label="AI Platform — Open a Case">
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="{{assetPrefix}}assets/ai-platform-primo-rpa/cover.webp"
|
||||
alt=""
|
||||
width="400"
|
||||
height="225"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">Primo AI Server</h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">UX/UI</li>
|
||||
<li class="mp-card__tag">Web App</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">An interface for training and using AI models in business processes: working with data, automation scenarios, and Smart OCR.</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="3" data-mp-cells-sm="2" data-related-slug="lunacy">
|
||||
<a class="mp-card mp-card--link mp-card--stack" href="{{casesPrefix}}lunacy/" aria-label="Lunacy Image Editor — Open a Case">
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="{{assetPrefix}}assets/lunacy/cover.webp"
|
||||
alt=""
|
||||
width="400"
|
||||
height="225"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">Lunacy</h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">UX/UI</li>
|
||||
<li class="mp-card__tag">Desktop App</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Worked on the Lunacy interface as part of the Icons8 team: user flow, complex prototypes, and improvements to functionality and interface logic.</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="3" data-mp-cells-sm="2" data-related-slug="manu-game-maker" hidden>
|
||||
<a class="mp-card mp-card--link mp-card--stack" href="{{casesPrefix}}manu-game-maker/" aria-label="MANU Game Engine — Open a Case">
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="{{assetPrefix}}assets/manu-game-maker/cover.webp"
|
||||
alt=""
|
||||
width="400"
|
||||
height="225"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">MANU Game Engine</h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">Product Design</li>
|
||||
<li class="mp-card__tag">Desktop App</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Redesign and development of the game engine’s user interface: a new operational concept, an updated visual system, and the development of hundreds of complex user scenarios.</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="3" data-mp-cells-sm="2" data-related-slug="mobile-clinic-app" hidden>
|
||||
<a class="mp-card mp-card--link mp-card--stack" href="{{casesPrefix}}mobile-clinic-app/" aria-label="Mobile app for a network of clinics — open a case">
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="{{assetPrefix}}assets/mobile-clinic-app/cover.webp"
|
||||
alt=""
|
||||
width="400"
|
||||
height="225"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">Mobile app for a network of clinics</h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">UX/UI</li>
|
||||
<li class="mp-card__tag">Mobile App</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Design of a mobile app for scheduling doctor appointments and managing medical services. Focus on a simple user journey, intuitive navigation, and quick access to necessary actions.</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="3" data-mp-cells-sm="2" data-related-slug="orc-automation-panel">
|
||||
<a class="mp-card mp-card--link mp-card--stack" href="{{casesPrefix}}orc-automation-panel/" aria-label="Web Panel for Automation Orchestration — Open a Case">
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="{{assetPrefix}}assets/orc-primo-rpa/cover.webp"
|
||||
alt=""
|
||||
width="400"
|
||||
height="225"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">Primo Orc</h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">Mentoring and Coaching</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Primo RPA orchestrator’s web dashboard: projects, deployment, and monitoring of RPA robots. Redesign of key screens for production without interrupting releases.</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="3" data-mp-cells-sm="2" data-related-slug="task-distribution-concept" hidden>
|
||||
<a class="mp-card mp-card--link mp-card--stack" href="{{casesPrefix}}task-distribution-concept/" aria-label="The concept of a task-assignment service—open a case">
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="{{assetPrefix}}assets/task-distribution-concept/cover.webp"
|
||||
alt=""
|
||||
width="400"
|
||||
height="225"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">Task Distribution Service Concept</h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">UX/UI</li>
|
||||
<li class="mp-card__tag">Analytics</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Product concept for a service to create, assign, and track tasks, with an emphasis on process transparency and seamless teamwork.</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="3" data-mp-cells-sm="2" data-related-slug="task-tracker" hidden>
|
||||
<a class="mp-card mp-card--link mp-card--stack" href="{{casesPrefix}}task-tracker/" aria-label="Task Tracker for the Back-Office System — Open a Case">
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="{{assetPrefix}}assets/task-tracker/cover.webp"
|
||||
alt=""
|
||||
width="400"
|
||||
height="225"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">Task tracker for a back-office system</h3>
|
||||
<ul class="mp-card__tags" aria-label="Project Type">
|
||||
<li class="mp-card__tag">Product Design</li>
|
||||
<li class="mp-card__tag">Web App</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">An interface for planning and assigning tasks in the operations department: shared pool, personal to-do list, overview by assignee; tabs, filters, board/list/table views, and drag-and-drop functionality.</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,80 @@
|
||||
<script>
|
||||
(function () {
|
||||
var FONT_CSS = {
|
||||
merriweather: "/assets/fonts/merriweather.css",
|
||||
geologica: "/assets/fonts/geologica.css",
|
||||
mono: "/assets/fonts/mono.css",
|
||||
};
|
||||
try {
|
||||
var t = JSON.parse(localStorage.getItem("siteThemeV1") || "{}");
|
||||
var d = document.documentElement;
|
||||
var m = t.mode;
|
||||
if (m !== "light" && m !== "dark" && m !== "system") m = "system";
|
||||
var eff =
|
||||
m === "system"
|
||||
? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: m;
|
||||
d.dataset.theme = eff;
|
||||
var h = typeof t.h === "number" ? t.h : typeof t.hue === "number" ? t.hue : null;
|
||||
var s = typeof t.s === "number" ? t.s : typeof t.sat === "number" ? t.sat : null;
|
||||
var l = typeof t.l === "number" ? t.l : typeof t.light === "number" ? t.light : null;
|
||||
if (h !== null) d.style.setProperty("--theme-h", String(h));
|
||||
if (s !== null) d.style.setProperty("--theme-s", String(s));
|
||||
if (l !== null) {
|
||||
var L = Number(l);
|
||||
if (Number.isFinite(L)) d.style.setProperty("--theme-l", String(Math.min(70, Math.max(20, L))));
|
||||
}
|
||||
var hVal = h !== null ? h : 210;
|
||||
var sVal = s !== null ? s : 90;
|
||||
var lVal = l !== null ? Math.min(70, Math.max(20, Number(l))) : 40;
|
||||
var H = (((hVal % 360) + 360) % 360) / 360;
|
||||
var S = Math.min(100, Math.max(0, sVal)) / 100;
|
||||
var Lv = Math.min(100, Math.max(0, lVal)) / 100;
|
||||
var r, g, b;
|
||||
if (S === 0) { r = g = b = Lv; }
|
||||
else {
|
||||
var q = Lv < 0.5 ? Lv * (1 + S) : Lv + S - Lv * S;
|
||||
var p = 2 * Lv - q;
|
||||
var hue2rgb = function(p, q, t) {
|
||||
if (t < 0) t += 1; if (t > 1) t -= 1;
|
||||
if (t < 1/6) return p + (q - p) * 6 * t;
|
||||
if (t < 1/2) return q;
|
||||
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
|
||||
return p;
|
||||
};
|
||||
r = hue2rgb(p, q, H + 1/3);
|
||||
g = hue2rgb(p, q, H);
|
||||
b = hue2rgb(p, q, H - 1/3);
|
||||
}
|
||||
var lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
d.style.setProperty("--on-accent", lum > 0.58 ? "hsl(220 14% 12%)" : "hsl(0 0% 98%)");
|
||||
var rad = t.radius;
|
||||
if (rad === "soft" || rad === "medium" || rad === "strong") {
|
||||
d.dataset.radius = rad;
|
||||
} else {
|
||||
d.dataset.radius = "medium";
|
||||
}
|
||||
var f = t.font;
|
||||
if (f === "onest") f = "geologica";
|
||||
if (f === "anonymous") f = "mono";
|
||||
if (f !== "merriweather" && f !== "mono" && f !== "geologica") f = "merriweather";
|
||||
d.dataset.font = f;
|
||||
var mb = t.meshBg;
|
||||
if (mb === "off") {
|
||||
d.dataset.meshBg = "off";
|
||||
} else {
|
||||
d.dataset.meshBg = "on";
|
||||
}
|
||||
var href = FONT_CSS[f];
|
||||
if (href && !document.querySelector('link[data-theme-font="' + f + '"]')) {
|
||||
var link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = href;
|
||||
link.setAttribute("data-theme-font", f);
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
Reference in new issue
Block a user