Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Large diffs are not rendered by default.

26 changes: 13 additions & 13 deletions js/absence-main.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/absence-main.mjs.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/absence-personal-settings.mjs

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions js/index-EdbAt5mR.chunk.mjs → js/index-CrSaqOc9.chunk.mjs

Large diffs are not rendered by default.

Large diffs are not rendered by default.

76 changes: 76 additions & 0 deletions src/components/MeterBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<!--
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-
- A proportional bar for a table cell: how much of `max` this row's `value` is.
-
- Its job is to make a column of numbers scannable — the eye finds the long bar
- far faster than it finds the big number, which is the whole point of putting a
- table in front of somebody in the first place.
-->
<template>
<div
class="meter"
role="img"
:aria-label="ariaLabel">
<div class="meter__track">
<div
class="meter__fill"
:style="{ width: pct + '%', background: color }" />
</div>
</div>
</template>

<script>
export default {
name: 'MeterBar',
props: {
value: { type: Number, required: true },
/** The row the bar is measured against — usually the column's largest. */
max: { type: Number, required: true },
color: { type: String, default: 'var(--color-primary-element)' },
/**
* Screen readers get the number from the neighbouring cell, so the bar is
* decorative unless the caller gives it something of its own to say.
*/
ariaLabel: { type: String, default: '' },
},

computed: {
pct() {
if (!(this.max > 0) || !(this.value > 0)) {
return 0
}
// Floor at a sliver so a small non-zero value still reads as present
// rather than as nothing at all.
return Math.max(2, Math.min(100, (this.value / this.max) * 100))
},
},
}
</script>

<style scoped lang="scss">
.meter {
display: flex;
align-items: center;
min-width: 60px;

&__track {
width: 100%;
height: 8px;
border-radius: 4px;
background: var(--color-background-dark);
overflow: hidden;
}

&__fill {
height: 100%;
border-radius: 4px;
transition: width 320ms ease;
}
}

@media (prefers-reduced-motion: reduce) {
.meter__fill { transition: none; }
}
</style>
83 changes: 83 additions & 0 deletions src/components/StatTile.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<!--
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-
- A headline figure with its label — the tile used across the HR overviews.
-
- It carries an accent colour rather than painting every tile in the primary
- element: a row of identical blue numbers gives the eye nothing to hold on to,
- and the accent is free information (which leave type, good news or bad).
-->
<template>
<div class="tile" :style="{ '--tile-accent': accent }">
<span v-if="icon" class="tile__icon" aria-hidden="true">{{ icon }}</span>
<span class="tile__value">{{ value }}</span>
<span class="tile__label">{{ label }}</span>
<span v-if="caption" class="tile__caption">{{ caption }}</span>
</div>
</template>

<script>
export default {
name: 'StatTile',
props: {
/** The figure itself, already formatted and localised by the caller. */
value: { type: [String, Number], required: true },
label: { type: String, required: true },
/** Optional second line, for context the label has no room for. */
caption: { type: String, default: '' },
/** Emoji, matching the leave-type icons used elsewhere. */
icon: { type: String, default: '' },
accent: { type: String, default: 'var(--color-primary-element)' },
},
}
</script>

<style scoped lang="scss">
.tile {
position: relative;
display: flex;
flex-direction: column;
gap: 2px;
padding: calc(var(--default-grid-baseline, 4px) * 4);
border-radius: var(--border-radius-large, 12px);
background: color-mix(in srgb, var(--tile-accent) 8%, var(--color-main-background));
border: 1px solid color-mix(in srgb, var(--tile-accent) 22%, transparent);
overflow: hidden;

// A hairline of the accent at full strength: enough to tell tiles apart at a
// glance without tinting the whole card into unreadability.
&::before {
content: '';
position: absolute;
inset-block: 0;
inset-inline-start: 0;
width: 3px;
background: var(--tile-accent);
}

&__icon {
font-size: 1.3rem;
line-height: 1;
margin-bottom: 6px;
}

&__value {
font-size: 2rem;
font-weight: 700;
line-height: 1.1;
color: var(--tile-accent);
font-variant-numeric: tabular-nums;
}

&__label {
color: var(--color-main-text);
font-size: 0.9rem;
}

&__caption {
color: var(--color-text-maxcontrast);
font-size: 0.8rem;
}
}
</style>
91 changes: 87 additions & 4 deletions src/styles/page.scss
Original file line number Diff line number Diff line change
Expand Up @@ -79,36 +79,119 @@
width: 240px;
}

/* Wide tables scroll on their own so the page body never does. */
/**
* The one card surface. Three subtly different ones had grown up across the HR
* views (background-hover with no border here, main-background with a border
* there, a third padding scale on My leave), which is the kind of drift that
* reads as "unfinished" long before anybody can say why. Everything raised off
* the page background now uses this.
*/
.surface {
background: var(--color-main-background);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-large, 12px);
padding: calc(var(--default-grid-baseline, 4px) * 4);
}

/** A quieter surface for nested blocks that must not compete with the card. */
.surface--sunken {
background: var(--color-background-hover);
border-color: transparent;
}

/** Responsive tile row, for stat tiles and the export cards alike. */
.tiles {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: calc(var(--default-grid-baseline, 4px) * 4);
}

/** Muted one-liner under a page title. */
.page__hint {
margin: -12px 0 0;
color: var(--color-text-maxcontrast);
font-size: 0.9rem;
}

/*
* Wide tables scroll on their own so the page body never does. The wrap doubles
* as the table's surface, so a long HR table reads as one object instead of
* rules floating on the page background.
*/
.table-wrap {
overflow-x: auto;
background: var(--color-main-background);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-large, 12px);
}

.tbl {
width: 100%;
border-collapse: collapse;

th, td {
padding: 10px 12px;
padding: 12px 14px;
text-align: start;
border-bottom: 1px solid var(--color-border);
}

/* Align the first and last cells with the surface's own corner radius. */
th:first-child, td:first-child { padding-inline-start: 18px; }
th:last-child, td:last-child { padding-inline-end: 18px; }

th {
font-size: 0.8rem;
font-size: 0.76rem;
font-weight: 600;
color: var(--color-text-maxcontrast);
text-transform: uppercase;
letter-spacing: 0.04em;
letter-spacing: 0.05em;
white-space: nowrap;
background: var(--color-background-hover);
border-bottom-color: var(--color-border-dark, var(--color-border));
}

thead th:first-child { border-start-start-radius: var(--border-radius-large, 12px); }
thead th:last-child { border-start-end-radius: var(--border-radius-large, 12px); }

/* The last rule would otherwise double up with the surface's own border. */
tbody tr:last-child td { border-bottom: none; }

tbody tr {
transition: background-color 120ms ease;
}

tbody tr:hover {
background: var(--color-background-hover);
}

.num {
text-align: end;
font-variant-numeric: tabular-nums;
}

/**
* The figure a row is really about, when it sits among other numbers.
* Declared *before* the state colours below: a cell is routinely both
* `lead` and `neg`, the two have equal specificity, and whichever is
* declared last would win — which must be the one carrying the warning.
*/
.lead {
font-weight: 600;
color: var(--color-main-text);
}

.neg {
color: var(--color-error);
font-weight: 600;
}
}

/** Rows that open something. Pairs with a keyboard-reachable control inside. */
.tbl--interactive tbody tr {
cursor: pointer;
}

@media (prefers-reduced-motion: reduce) {
.tbl tbody tr { transition: none; }
}
}
9 changes: 2 additions & 7 deletions src/views/MyLeave.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@
</section>

<section v-if="leaveByMonth || sickByMonth" class="charts">
<div v-if="leaveByMonth" class="charts__card">
<div v-if="leaveByMonth" class="surface">
<BarChart :title="t('absence', 'Leave taken by month ({year})', { year })" :data="leaveByMonth" />
</div>
<div v-if="sickByMonth" class="charts__card">
<div v-if="sickByMonth" class="surface">
<BarChart :title="t('absence', 'Sick days by month ({year})', { year })" :data="sickByMonth" />
</div>
</section>
Expand Down Expand Up @@ -256,11 +256,6 @@ export default {
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: calc(var(--default-grid-baseline, 4px) * 3);

&__card {
background: var(--color-background-hover);
border-radius: var(--border-radius-large, 12px);
padding: calc(var(--default-grid-baseline, 4px) * 3);
}
}

.requests {
Expand Down
Loading
Loading