feat(ui): establish shared desktop shell and assets

This commit is contained in:
sechmachine
2026-08-14 23:41:51 +07:00
parent 5967f7f70d
commit bac39812a3
15 changed files with 1822 additions and 0 deletions
@@ -0,0 +1,33 @@
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<body>
<a th:fragment="buttonLink(href, label, tone)" class="button" th:classappend="${tone == 'primary' ? ' button-primary' : tone == 'danger' ? ' button-danger' : ''}" th:href="${href}" th:text="${label}">Action</a>
<div th:fragment="input(name, label, value, error)" class="field">
<label class="field-label" th:for="${name}" th:text="${label}">Label</label>
<input class="control" th:id="${name}" th:name="${name}" th:value="${value}" th:attr="aria-invalid=${error != null},aria-describedby=${error != null ? name + '-error' : null}">
<p class="field-error" th:if="${error != null}" th:id="${name + '-error'}" th:text="${error}">Error</p>
</div>
<div th:fragment="select(name, label, options, selected, error)" class="field">
<label class="field-label" th:for="${name}" th:text="${label}">Label</label>
<select class="control" th:id="${name}" th:name="${name}" th:attr="aria-invalid=${error != null},aria-describedby=${error != null ? name + '-error' : null}">
<option th:each="option : ${options}" th:value="${option.key}" th:text="${option.value}" th:selected="${option.key == selected}">Option</option>
</select>
<p class="field-error" th:if="${error != null}" th:id="${name + '-error'}" th:text="${error}">Error</p>
</div>
<label th:fragment="checkbox(name, label, checked)" class="checkbox"><input type="checkbox" th:id="${name}" th:name="${name}" th:checked="${checked}"><span th:text="${label}">Label</span></label>
<section th:fragment="card(title, content)" class="panel"><header class="panel-header"><h2 class="panel-title" th:text="${title}">Title</h2></header><div th:replace="${content}"></div></section>
<div th:fragment="metric(label, value, detail)" class="metric"><div class="metric-label" th:text="${label}">Label</div><div class="metric-value" th:text="${value}">Value</div><div class="metric-detail" th:text="${detail}">Detail</div></div>
<span th:fragment="badge(label, tone)" class="badge" th:classappend="${' badge-' + tone}" th:attr="aria-label=${'Status: ' + label}" th:text="${label}">Status</span>
<nav th:fragment="tabs(items, active)" class="tabs" aria-label="Views"><a th:each="item : ${items}" class="tab" th:href="${item.href}" th:text="${item.label}" th:attr="aria-current=${item.key == active} ? 'page' : null">View</a></nav>
<div th:fragment="table(caption, head, body)" class="table-scroll"><table class="data-table"><caption class="sr-only" th:text="${caption}">Data</caption><thead th:replace="${head}"></thead><tbody th:replace="${body}"></tbody></table></div>
<nav th:fragment="pagination(previous, next)" class="pagination" aria-label="Pagination"><a class="button" th:if="${previous != null}" th:href="${previous}">Previous</a><a class="button" th:if="${next != null}" th:href="${next}">Next</a></nav>
<div th:fragment="alert(tone, message)" class="alert" th:classappend="${' alert-' + tone}" role="alert" th:text="${message}">Message</div>
<dialog th:fragment="confirmation(id, title, description)" th:id="${id}" th:attr="aria-labelledby=${id + '-title'},aria-describedby=${id + '-description'}"><h2 th:id="${id + '-title'}" th:text="${title}">Confirm</h2><p th:id="${id + '-description'}" th:text="${description}">Consequence</p><form method="dialog"><button class="button" value="cancel">Cancel</button><button class="button button-danger" value="confirm">Confirm</button></form></dialog>
<section th:fragment="empty(title, description)" class="empty-state"><h2 th:text="${title}">No records</h2><p th:text="${description}">Nothing to show.</p></section>
<div th:fragment="skeleton(label)" role="status" th:attr="aria-label=${label}"><div class="skeleton"></div></div>
<div th:fragment="notificationMenu(items)" class="panel notification-menu"><h2 class="panel-title">Notifications</h2><div th:if="${#lists.isEmpty(items)}">No unread notifications.</div><a th:each="item : ${items}" th:href="${item.actionUrl}" th:text="${item.title}">Notification</a></div>
</body>
</html>
@@ -0,0 +1,62 @@
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
th:fragment="shell(pageTitle, section, activeNav, primaryAction, content)">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title th:text="${pageTitle + ' · Lab Timesheet'}">Lab Timesheet</title>
<script th:src="@{/assets/theme.js}"></script>
<link rel="stylesheet" th:href="@{/assets/app.css}">
<script defer th:src="@{/assets/app.js}"></script>
</head>
<body>
<div class="app-shell">
<aside class="sidebar" aria-label="Primary navigation">
<a class="brand" th:href="@{/dashboard}">
<span class="brand-mark" aria-hidden="true">LT</span>
<span class="sidebar-label"><strong>Lab Timesheet</strong><br><small th:text="${section + ' workspace'}">Workspace</small></span>
</a>
<nav>
<p class="nav-label sidebar-label">Workspace</p>
<ul class="nav-list">
<li><a class="nav-link" th:href="@{/dashboard}" th:attr="aria-current=${activeNav == 'dashboard'} ? 'page' : null">
<svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#layout-dashboard}"></use></svg><span class="sidebar-label">Overview</span></a></li>
<li sec:authorize="hasRole('ADMIN')"><a class="nav-link" th:href="@{/admin/users}"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#users}"></use></svg><span class="sidebar-label">Accounts</span></a></li>
<li sec:authorize="hasRole('ADMIN')"><a class="nav-link" th:href="@{/admin/calendar}"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#calendar-days}"></use></svg><span class="sidebar-label">Global calendar</span></a></li>
<li sec:authorize="hasRole('MENTOR')"><a class="nav-link" th:href="@{/projects}"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#folder-kanban}"></use></svg><span class="sidebar-label">Owned Projects</span></a></li>
<li sec:authorize="hasRole('MENTOR')"><a class="nav-link" th:href="@{/attendance}"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#clock}"></use></svg><span class="sidebar-label">Intern attendance</span></a></li>
<li sec:authorize="hasRole('INTERN')"><a class="nav-link" th:href="@{/attendance/me}"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#clock}"></use></svg><span class="sidebar-label">My attendance</span></a></li>
<li sec:authorize="hasRole('INTERN')"><a class="nav-link" th:href="@{/projects}"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#folder-kanban}"></use></svg><span class="sidebar-label">My Projects</span></a></li>
<li sec:authorize="hasRole('INTERN')"><a class="nav-link" th:href="@{/tasks}"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#list-check}"></use></svg><span class="sidebar-label">Assigned Tasks</span></a></li>
</ul>
</nav>
<div class="sidebar-footer">
<div class="theme-field">
<label class="field-label sidebar-label" for="theme-preference">Theme</label>
<select id="theme-preference" data-theme-select aria-label="Theme">
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">System</option>
</select>
</div>
<a class="account" th:href="@{/profile}"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#circle-user-round}"></use></svg><span class="sidebar-label" sec:authentication="name">Profile</span></a>
<form class="logout-form" th:action="@{/logout}" method="post"><button class="nav-link" type="submit"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#log-out}"></use></svg><span class="sidebar-label">Logout</span></button></form>
</div>
</aside>
<div class="app-column">
<header class="app-header">
<button class="icon-button" type="button" data-sidebar-toggle aria-label="Toggle sidebar" title="Toggle sidebar"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#panel-left}"></use></svg></button>
<div class="header-title"><span class="breadcrumb" th:text="${section}">Section</span> / <span th:text="${pageTitle}">Page</span></div>
<div class="header-actions"><a class="icon-button" th:href="@{/notifications}" aria-label="Notifications" title="Notifications"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#bell}"></use></svg></a></div>
</header>
<div class="page">
<div class="page-heading">
<div class="page-heading-copy"><h1 class="page-title" th:text="${pageTitle}">Page</h1></div>
<div class="primary-action" th:if="${primaryAction != null}" th:replace="${primaryAction}"></div>
</div>
<th:block th:replace="${content}"></th:block>
</div>
</div>
</div>
</body>
</html>