Merge pull request #471 from EdouardVanbelle/refactor/ui
@@ -57,17 +57,6 @@ check:
|
|||||||
cargo fmt --all
|
cargo fmt --all
|
||||||
cargo clippy --all-features --all-targets -- -D warnings
|
cargo clippy --all-features --all-targets -- -D warnings
|
||||||
|
|
||||||
# Frontend design-system / a11y guardrails — pure Node, no extra deps.
|
|
||||||
# Single gate: WCAG contrast, heading order, locale completeness, dead tokens,
|
|
||||||
# brand-mark drift.
|
|
||||||
# (Also run `stylelint static/css/**/*.css`, `biome check`, `tsc -p jsconfig.json
|
|
||||||
# --noEmit` once node_modules is installed — those need devDependencies.)
|
|
||||||
frontend-check:
|
|
||||||
node scripts/check-contrast.mjs
|
|
||||||
node scripts/check-headings.mjs
|
|
||||||
node scripts/check-locales.mjs
|
|
||||||
node scripts/check-dead-tokens.mjs
|
|
||||||
node scripts/check-brand-drift.mjs
|
|
||||||
|
|
||||||
wasm-check:
|
wasm-check:
|
||||||
cd wasm/oxicloud-hash; cargo fmt --all
|
cd wasm/oxicloud-hash; cargo fmt --all
|
||||||
@@ -94,7 +83,10 @@ front-dev:
|
|||||||
PROFILE=dev cargo run
|
PROFILE=dev cargo run
|
||||||
|
|
||||||
# front: check all (linter, format, type, icons, translations...)
|
# front: check all (linter, format, type, icons, translations...)
|
||||||
front-check: front-fmt front-lint front-type front-rules front-check-icons front-check-i18n
|
front-check: front-fmt front-lint front-type front-rules front-check-icons front-check-i18n frontend-check
|
||||||
|
|
||||||
|
# kept for compatibility
|
||||||
|
frontend-check: front-design
|
||||||
|
|
||||||
front-fmt:
|
front-fmt:
|
||||||
biome format static/
|
biome format static/
|
||||||
@@ -116,7 +108,6 @@ front-check-icons:
|
|||||||
front-check-i18n:
|
front-check-i18n:
|
||||||
tools/check-missing-translations.py --check-only
|
tools/check-missing-translations.py --check-only
|
||||||
|
|
||||||
|
|
||||||
# end-to-end Playwright tests
|
# end-to-end Playwright tests
|
||||||
front-test:
|
front-test:
|
||||||
cd tests/e2e && npm test
|
cd tests/e2e && npm test
|
||||||
@@ -125,6 +116,19 @@ front-test:
|
|||||||
front-test-update-snapshot:
|
front-test-update-snapshot:
|
||||||
cd tests/e2e && npm test -- --update-snapshots=all
|
cd tests/e2e && npm test -- --update-snapshots=all
|
||||||
|
|
||||||
|
# Frontend design-system / a11y guardrails — pure Node, no extra deps.
|
||||||
|
# Single gate: WCAG contrast, heading order, locale completeness, dead tokens,
|
||||||
|
# brand-mark drift.
|
||||||
|
# (Also run `stylelint static/css/**/*.css`, `biome check`, `tsc -p jsconfig.json
|
||||||
|
# --noEmit` once node_modules is installed — those need devDependencies.)
|
||||||
|
front-design:
|
||||||
|
node scripts/check-contrast.mjs
|
||||||
|
node scripts/check-headings.mjs
|
||||||
|
node scripts/check-locales.mjs
|
||||||
|
node scripts/check-dead-tokens.mjs
|
||||||
|
node scripts/check-brand-drift.mjs
|
||||||
|
|
||||||
|
|
||||||
# Hurl API functional tests (starts postgres + server, tears down after)
|
# Hurl API functional tests (starts postgres + server, tears down after)
|
||||||
api-test:
|
api-test:
|
||||||
bash tests/api/run.sh
|
bash tests/api/run.sh
|
||||||
|
|||||||
@@ -19,11 +19,19 @@ const state = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** Click an element by selector (no-op if absent). */
|
/** Click an element by selector (no-op if absent). */
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {String} selector
|
||||||
|
*/
|
||||||
function click(selector) {
|
function click(selector) {
|
||||||
/** @type {HTMLElement | null} */ (document.querySelector(selector))?.click();
|
/** @type {HTMLElement | null} */ (document.querySelector(selector))?.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Activate the sidebar nav button whose label key matches `nav.<section>`. */
|
/** Activate the sidebar nav button whose label key matches `nav.<section>`. */
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {String} section
|
||||||
|
*/
|
||||||
function navTo(section) {
|
function navTo(section) {
|
||||||
document.querySelectorAll('.nav-item').forEach((it) => {
|
document.querySelectorAll('.nav-item').forEach((it) => {
|
||||||
const key = it.querySelector('span[data-i18n]')?.getAttribute('data-i18n');
|
const key = it.querySelector('span[data-i18n]')?.getAttribute('data-i18n');
|
||||||
@@ -76,6 +84,10 @@ function render() {
|
|||||||
.join('');
|
.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {String} query
|
||||||
|
*/
|
||||||
function filter(query) {
|
function filter(query) {
|
||||||
const q = query.trim().toLowerCase();
|
const q = query.trim().toLowerCase();
|
||||||
state.filtered = q ? state.items.filter((c) => c.label.toLowerCase().includes(q)) : state.items.slice();
|
state.filtered = q ? state.items.filter((c) => c.label.toLowerCase().includes(q)) : state.items.slice();
|
||||||
@@ -83,6 +95,11 @@ function filter(query) {
|
|||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} delta
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
function move(delta) {
|
function move(delta) {
|
||||||
if (!state.filtered.length) return;
|
if (!state.filtered.length) return;
|
||||||
state.index = (state.index + delta + state.filtered.length) % state.filtered.length;
|
state.index = (state.index + delta + state.filtered.length) % state.filtered.length;
|
||||||
|
|||||||
@@ -1011,8 +1011,14 @@ const ui = {
|
|||||||
// Let ui.js delegation handle this container again
|
// Let ui.js delegation handle this container again
|
||||||
delete filesList.dataset.managedBy;
|
delete filesList.dataset.managedBy;
|
||||||
|
|
||||||
// A clickable, flat-sorting column header (Drive-style). The arrow is
|
/**
|
||||||
// shown only on the active column; direction toggles on re-click.
|
* A clickable, flat-sorting column header (Drive-style). The arrow is
|
||||||
|
* shown only on the active column; direction toggles on re-click.
|
||||||
|
* @param {String} field
|
||||||
|
* @param {String} key
|
||||||
|
* @param {String} label
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
const sortCol = (field, key, label) =>
|
const sortCol = (field, key, label) =>
|
||||||
`<button type="button" class="list-header-sort" data-sort-field="${field}">` +
|
`<button type="button" class="list-header-sort" data-sort-field="${field}">` +
|
||||||
`<span data-i18n="${key}">${label}</span>` +
|
`<span data-i18n="${key}">${label}</span>` +
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ function gridMetaDate(value) {
|
|||||||
const date = value instanceof Date ? value : new Date(typeof value === 'number' && value < 1e12 ? value * 1000 : value);
|
const date = value instanceof Date ? value : new Date(typeof value === 'number' && value < 1e12 ? value * 1000 : value);
|
||||||
if (Number.isNaN(date.getTime())) return '';
|
if (Number.isNaN(date.getTime())) return '';
|
||||||
const ageDays = (Date.now() - date.getTime()) / 86_400_000;
|
const ageDays = (Date.now() - date.getTime()) / 86_400_000;
|
||||||
return ageDays > 30 ? formatDateShort(value) : formatRelativeTime(value);
|
return ageDays > 30 ? formatDateShort(date) : formatRelativeTime(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -713,7 +713,7 @@ export class ResourceListComponent {
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid-meta" title="${escapeHtml(formattedDate)}">
|
<div class="grid-meta" title="${escapeHtml(formattedDate)}">
|
||||||
<span class="grid-meta__date">${escapeHtml(relDate)}</span>
|
<span class="grid-meta__date">${escapeHtml(relDate)}</span>
|
||||||
${isShared && folder.owner_id ? `<span class="grid-meta__owner-slot" data-owner-id="${escapeHtml(folder.owner_id)}"></span>` : ''}
|
${folder.owner_id ? `<span class="grid-meta__owner-slot" data-owner-id="${escapeHtml(folder.owner_id)}"></span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="owner-cell${this._ownerVisible ? '' : ' hidden'}" data-owner-id="${escapeHtml(folder.owner_id || '')}"></div>
|
<div class="owner-cell${this._ownerVisible ? '' : ' hidden'}" data-owner-id="${escapeHtml(folder.owner_id || '')}"></div>
|
||||||
${cfg.showPath ? `<div class="path-cell" title="${escapeHtml(folder.path || '')}">${escapeHtml(folder.path || '')}</div>` : ''}
|
${cfg.showPath ? `<div class="path-cell" title="${escapeHtml(folder.path || '')}">${escapeHtml(folder.path || '')}</div>` : ''}
|
||||||
@@ -728,7 +728,7 @@ export class ResourceListComponent {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
el.querySelector('.resource-icon-slot')?.replaceWith(buildResourceIcon(folder, 'folder'));
|
el.querySelector('.resource-icon-slot')?.replaceWith(buildResourceIcon(folder, 'folder'));
|
||||||
if (isShared && folder.owner_id) {
|
if (folder.owner_id) {
|
||||||
el.querySelector('.grid-meta__owner-slot')?.replaceWith(createUserVignette(folder.owner_id, 'xs', { showName: false }));
|
el.querySelector('.grid-meta__owner-slot')?.replaceWith(createUserVignette(folder.owner_id, 'xs', { showName: false }));
|
||||||
}
|
}
|
||||||
return el;
|
return el;
|
||||||
@@ -741,6 +741,7 @@ export class ResourceListComponent {
|
|||||||
* @returns {HTMLElement}
|
* @returns {HTMLElement}
|
||||||
*/
|
*/
|
||||||
_createFileItem(file, labels) {
|
_createFileItem(file, labels) {
|
||||||
|
console.log(file);
|
||||||
const cfg = this._cfg;
|
const cfg = this._cfg;
|
||||||
const typeLabel = labels.fileTypeLabel(file.category || '');
|
const typeLabel = labels.fileTypeLabel(file.category || '');
|
||||||
const fileSize = file.size_formatted || formatFileSize(file.size);
|
const fileSize = file.size_formatted || formatFileSize(file.size);
|
||||||
@@ -775,7 +776,7 @@ export class ResourceListComponent {
|
|||||||
<div class="grid-meta" title="${escapeHtml(formattedDate)}">
|
<div class="grid-meta" title="${escapeHtml(formattedDate)}">
|
||||||
<span class="grid-meta__date">${escapeHtml(relDate)}</span>
|
<span class="grid-meta__date">${escapeHtml(relDate)}</span>
|
||||||
<span class="grid-meta__size">${escapeHtml(fileSize)}</span>
|
<span class="grid-meta__size">${escapeHtml(fileSize)}</span>
|
||||||
${isShared && file.owner_id ? `<span class="grid-meta__owner-slot" data-owner-id="${escapeHtml(file.owner_id)}"></span>` : ''}
|
${file.owner_id ? `<span class="grid-meta__owner-slot" data-owner-id="${escapeHtml(file.owner_id)}"></span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="owner-cell${this._ownerVisible ? '' : ' hidden'}" data-owner-id="${escapeHtml(file.owner_id || '')}"></div>
|
<div class="owner-cell${this._ownerVisible ? '' : ' hidden'}" data-owner-id="${escapeHtml(file.owner_id || '')}"></div>
|
||||||
${cfg.showPath ? `<div class="path-cell" title="${escapeHtml(file.path || '')}">${escapeHtml(file.path || '')}</div>` : ''}
|
${cfg.showPath ? `<div class="path-cell" title="${escapeHtml(file.path || '')}">${escapeHtml(file.path || '')}</div>` : ''}
|
||||||
@@ -790,7 +791,7 @@ export class ResourceListComponent {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
el.querySelector('.resource-icon-slot')?.replaceWith(buildResourceIcon(file, 'file'));
|
el.querySelector('.resource-icon-slot')?.replaceWith(buildResourceIcon(file, 'file'));
|
||||||
if (isShared && file.owner_id) {
|
if (file.owner_id) {
|
||||||
el.querySelector('.grid-meta__owner-slot')?.replaceWith(createUserVignette(file.owner_id, 'xs', { showName: false }));
|
el.querySelector('.grid-meta__owner-slot')?.replaceWith(createUserVignette(file.owner_id, 'xs', { showName: false }));
|
||||||
}
|
}
|
||||||
return el;
|
return el;
|
||||||
|
|||||||
@@ -69,6 +69,13 @@ function _applyPhoto(avatar, photoUrl, name) {
|
|||||||
|
|
||||||
// ── Component ──────────────────────────────────────────────────────────────────
|
// ── Component ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Tracks the hover-tooltip cleanup for each vignette so a caller that
|
||||||
|
// re-renders the vignette (e.g. replaceChildren on a storage update) can
|
||||||
|
// dispose the previous tooltip — otherwise its body-portalled popover orphans
|
||||||
|
// and, if it was visible at re-render time, stays stuck on screen.
|
||||||
|
/** @type {WeakMap<HTMLElement, () => void>} */
|
||||||
|
const _tooltipCleanups = new WeakMap();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Available sizes. Each maps to a `.user-vignette--{size}` CSS modifier:
|
* Available sizes. Each maps to a `.user-vignette--{size}` CSS modifier:
|
||||||
* xs → 20 px (chip avatar, small inline contexts)
|
* xs → 20 px (chip avatar, small inline contexts)
|
||||||
@@ -112,13 +119,6 @@ function _applyPhoto(avatar, photoUrl, name) {
|
|||||||
* @param {VignetteOptions} [options]
|
* @param {VignetteOptions} [options]
|
||||||
* @returns {HTMLElement}
|
* @returns {HTMLElement}
|
||||||
*/
|
*/
|
||||||
// Tracks the hover-tooltip cleanup for each vignette so a caller that
|
|
||||||
// re-renders the vignette (e.g. replaceChildren on a storage update) can
|
|
||||||
// dispose the previous tooltip — otherwise its body-portalled popover orphans
|
|
||||||
// and, if it was visible at re-render time, stays stuck on screen.
|
|
||||||
/** @type {WeakMap<HTMLElement, () => void>} */
|
|
||||||
const _tooltipCleanups = new WeakMap();
|
|
||||||
|
|
||||||
export function createUserVignette(userId, size = 'sm', { showName = true, showEmail = false, showOrigin = true, noTooltip = false } = {}) {
|
export function createUserVignette(userId, size = 'sm', { showName = true, showEmail = false, showOrigin = true, noTooltip = false } = {}) {
|
||||||
const colorIdx = _colorIndex(userId);
|
const colorIdx = _colorIndex(userId);
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ function formatQuotaSize(bytes) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {Date | number| null} value
|
* @param {Date | string | number| null} value
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function formatDateTime(value) {
|
function formatDateTime(value) {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 68 KiB |