mirror of
https://github.com/ever-co/ever-gauzy.git
synced 2026-10-02 01:54:50 +08:00
Merge pull request #10182 from ever-co/fix/polish-all-table-pages
Fix/polish all table pages
This commit is contained in:
@@ -2,12 +2,14 @@
|
||||
// `@forward` re-exports to importers but does NOT bring members into this file,
|
||||
// so `nb-theme()` needs its own `@use`.
|
||||
@use 'themes' as *;
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
:host {
|
||||
nb-card-body {
|
||||
height: calc(100vh - 17.5rem) !important;
|
||||
}
|
||||
}
|
||||
// REMOVED: `:host nb-card-body { height: calc(100vh - 17.5rem) !important }`.
|
||||
// A hand-tuned viewport constant that out-ranked the flex column `_pg-card`
|
||||
// gives every other list page, so this table stood at a different height from
|
||||
// its siblings and had back the problem the shared partial exists to solve — a
|
||||
// gap under a short table, a clipped long one. The same override was taken off
|
||||
// Expenses for the same reason.
|
||||
|
||||
// Approve / Refuse are plain `[nbButton] status="success|danger"` buttons and
|
||||
// now take their colour from the theme like every other action in the app (see
|
||||
@@ -27,3 +29,28 @@ ngx-request-approval-action {
|
||||
gap: 10px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// The same block Invoices carries: density tokens, the table surface, row
|
||||
// hover/selection, the tag-chip stack, the status pills, the pager and the
|
||||
// toolbar scale.
|
||||
//
|
||||
// The default `$actions` selector (`nb-card-header`) is the right one here
|
||||
// precisely because this page puts a SECOND button beside the title — "Approval
|
||||
// policy" — outside `.gauzy-button-container`. Naming the container alone would
|
||||
// have left that one at full size next to a compacted toolbar.
|
||||
@include ga-hub.list-page();
|
||||
|
||||
// ── The Name cell ───────────────────────────────────────────────────────
|
||||
// `ga-picture-name-tags` renders `<ngx-avatar class="report-table">`, which pins
|
||||
// a 20px image inside a pill padded 3px: 26px of chip, 2px short of the 28px
|
||||
// `ngx-contact-links` costs the Invoices Contact column, and so 2px short of the
|
||||
// Invoices row. Only that inset is restated — the image and the name stay the
|
||||
// renderer's own 20px/12px — which takes the chip to the same 28px and the row
|
||||
// to the same height.
|
||||
//
|
||||
// Table-scoped: `ga-card-grid` instantiates the same `renderComponent` for the
|
||||
// card layout, which is not paying for a table row.
|
||||
:host .table-scroll-container ::ng-deep angular2-smart-table {
|
||||
@include ga-hub.avatar-chip();
|
||||
}
|
||||
|
||||
@@ -74,9 +74,11 @@
|
||||
></angular2-smart-table>
|
||||
</div>
|
||||
</div>
|
||||
@if (smartTableSource) {
|
||||
<ngx-pagination [source]="smartTableSource"></ngx-pagination>
|
||||
}
|
||||
<div class="pagination-container">
|
||||
@if (smartTableSource) {
|
||||
<ngx-pagination [source]="smartTableSource"></ngx-pagination>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<ga-card-grid
|
||||
[loading]="loading"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@use 'gauzy/_gauzy-table' as *;
|
||||
@use 'gauzy/_gauzy-cards' as *;
|
||||
@use '@shared/_pg-card' as *;
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
.manage-btn {
|
||||
display: flex;
|
||||
@@ -41,3 +42,29 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// The same block Invoices carries: density tokens, the table surface, row
|
||||
// hover/selection, the tag-chip stack, the status pills, the pager and the
|
||||
// toolbar scale.
|
||||
//
|
||||
// The pager needed the `.pagination-container` wrapper every sibling list page
|
||||
// already had — this template rendered a bare `<ngx-pagination>` straight into
|
||||
// the card body, so it took neither the shared right-alignment nor the compact
|
||||
// `<nav><ul><li>` sizing, and sat left-aligned at full size under a table that
|
||||
// had just been brought down to the shared density.
|
||||
@include ga-hub.list-page();
|
||||
|
||||
// ── The Full Name cell ───────────────────────────────────────────────────────
|
||||
// `ga-picture-name-tags` renders `<ngx-avatar class="report-table">`, which pins
|
||||
// a 20px image inside a pill padded 3px: 26px of chip, 2px short of the 28px
|
||||
// `ngx-contact-links` costs the Invoices Contact column, and so 2px short of the
|
||||
// Invoices row. Only that inset is restated — the image and the name stay the
|
||||
// renderer's own 20px/12px — which takes the chip to the same 28px and the row
|
||||
// to the same height.
|
||||
//
|
||||
// Table-scoped: `ga-card-grid` instantiates the same `renderComponent` for the
|
||||
// card layout, which is not paying for a table row.
|
||||
:host .table-scroll-container ::ng-deep angular2-smart-table {
|
||||
@include ga-hub.avatar-chip();
|
||||
}
|
||||
|
||||
+18
@@ -1,5 +1,6 @@
|
||||
@use 'gauzy/_gauzy-table' as *;
|
||||
@use 'gauzy/_gauzy-dialogs' as *;
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
.education-form {
|
||||
width: 100%;
|
||||
@@ -56,3 +57,20 @@ form {
|
||||
@include input-appearance(2rem, var(--gauzy-sidebar-background-4));
|
||||
}
|
||||
}
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// A tab of the candidate profile, brought onto the same footing as the
|
||||
// Candidates list it is opened from: density tokens, the table surface, row
|
||||
// hover/selection, the tag-chip stack, the status pills and the pager.
|
||||
//
|
||||
// The default `$actions` (`nb-card-header`), where this used to pass `null` on
|
||||
// the grounds that the tab had no compact toolbar. It has one: the header holds
|
||||
// `ngx-gauzy-button-action` unconditionally — Add, the layout selector, and the
|
||||
// Edit / Delete group in table mode — every button of it `size="small"`, which is
|
||||
// the exact row the mixin's button block is written for. `null` left that row at
|
||||
// full size over rows that had just been compacted.
|
||||
//
|
||||
// The add FORM is in this header too, but its Save and Cancel take no `size`, so
|
||||
// they are medium; `page-header-buttons()` only redefines the small-button and
|
||||
// icon-button-small tokens, and reaches neither.
|
||||
@include ga-hub.list-page();
|
||||
|
||||
+19
@@ -1,5 +1,6 @@
|
||||
@use 'gauzy/_gauzy-table' as *;
|
||||
@use 'gauzy/_gauzy-dialogs' as *;
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
.experience-form {
|
||||
width: 100%;
|
||||
@@ -54,3 +55,21 @@ form {
|
||||
@include input-appearance(2rem, var(--gauzy-sidebar-background-4));
|
||||
}
|
||||
}
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// A tab of the candidate profile, brought onto the same footing as the
|
||||
// Candidates list it is opened from: density tokens, the table surface, row
|
||||
// hover/selection, the tag-chip stack, the status pills and the pager.
|
||||
//
|
||||
// The default `$actions` (`nb-card-header`), where this used to pass `null` on
|
||||
// the grounds that the tab had no compact toolbar. It has one: the header holds
|
||||
// `ngx-gauzy-button-action` unconditionally — Add, the layout selector, and the
|
||||
// Edit / Delete group in table mode — every button of it `size="small"`, which is
|
||||
// the exact row the mixin's button block is written for. `null` left that row at
|
||||
// full size over rows that had just been compacted.
|
||||
//
|
||||
// The add form's Save and Cancel sit in this header too and ARE `size="small"`,
|
||||
// so they come down with it. That is the point rather than a side effect: they
|
||||
// are the same size class as the toolbar beside them, and leaving them out would
|
||||
// put two different small-button scales on one header line.
|
||||
@include ga-hub.list-page();
|
||||
|
||||
+21
@@ -1,5 +1,6 @@
|
||||
@use 'gauzy/_gauzy-table' as *;
|
||||
@use 'gauzy/_gauzy-dialogs' as *;
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
.form {
|
||||
padding: 0;
|
||||
@@ -288,3 +289,23 @@
|
||||
max-height: calc(100vh - 28rem);
|
||||
@include input-appearance(2rem, var(--gauzy-sidebar-background-4));
|
||||
}
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// A tab of the candidate profile, brought onto the same footing as the
|
||||
// Candidates list it is opened from: density tokens, the table surface, row
|
||||
// hover/selection, the tag-chip stack, the status pills and the pager.
|
||||
//
|
||||
// `$actions: '.main-btn'`, not `null`. This tab puts TWO different things in its
|
||||
// header, on an `@if (!showAddCard)` / `@else`: the list state renders
|
||||
// `.main-btn`, which is exactly the compact toolbar the mixin's button block is
|
||||
// written for (`ngx-gauzy-button-action` — Add, plus View / Edit / Delete), and
|
||||
// the form state renders `.add-card`. `null` left that toolbar at full size above
|
||||
// a table that had just been compacted.
|
||||
//
|
||||
// The default `nb-card-header` would have worked too — `page-header-buttons()`
|
||||
// only defines the `--button-*-small-*` and `--icon-button-*-small-*` tokens, and
|
||||
// the form's Save and Cancel take no `size`, so they are medium and no container
|
||||
// named here reaches them. `.main-btn` is the narrower statement of the same
|
||||
// thing: it names the row the tokens are FOR, rather than the header that happens
|
||||
// to contain it.
|
||||
@include ga-hub.list-page('.main-btn');
|
||||
|
||||
+14
@@ -1,5 +1,6 @@
|
||||
@use 'gauzy/_gauzy-table' as *;
|
||||
@use 'gauzy/_gauzy-overrides' as *;
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
.checkboxes {
|
||||
display: flex;
|
||||
@@ -29,3 +30,16 @@
|
||||
}
|
||||
@include input-appearance(2rem, var(--gauzy-card-1));
|
||||
}
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// A tab of the candidate profile, brought onto the same footing as the
|
||||
// Candidates list it is opened from: density tokens, the table surface, row
|
||||
// hover/selection, the tag-chip stack, the status pills and the pager.
|
||||
//
|
||||
// The default `$actions` (`nb-card-header`), where this used to pass `null` on
|
||||
// the grounds that the tab had no compact toolbar. It has one, and nothing else:
|
||||
// the header is a single `ngx-gauzy-button-action` — Add, the layout selector,
|
||||
// and the Edit / Delete group in table mode — every button of it `size="small"`,
|
||||
// which is the exact row the mixin's button block is written for. `null` left
|
||||
// that row at full size over rows that had just been compacted.
|
||||
@include ga-hub.list-page();
|
||||
|
||||
+10
@@ -1,6 +1,7 @@
|
||||
@use 'gauzy/_gauzy-cards' as *;
|
||||
@use 'gauzy/_gauzy-table' as *;
|
||||
@use 'gauzy/_gauzy-overrides' as ga-overrides;
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
:host .card {
|
||||
display: flex;
|
||||
@@ -91,3 +92,12 @@
|
||||
@include nb-ltr(padding-right, 0.5rem !important);
|
||||
@include nb-rtl(padding-left, 0.5rem !important);
|
||||
}
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// The same block the Candidates list this page sits beside now carries: density
|
||||
// tokens, the table surface, row hover/selection, the tag-chip stack, the status
|
||||
// pills, the pager and the toolbar scale.
|
||||
//
|
||||
// `$actions` is `.card-header` rather than `nb-card-header`: this component's
|
||||
// root IS an `nb-card-body`, and its action row is a plain div inside it.
|
||||
@include ga-hub.list-page('.card-header');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@use '@shared/_pg-card' as *;
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
/*
|
||||
* REMOVED: the `.contact-list` block (`.member-card`, `.contact-info`,
|
||||
@@ -52,3 +53,12 @@
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// Customers, Clients, Leads and Visitors all render this component, and all four
|
||||
// take the same table treatment as Invoices: density tokens, the table surface,
|
||||
// row hover/selection, the tag-chip stack, the status pills, the pager and the
|
||||
// toolbar scale. The buttons sit in `nb-card-header`, so the default `$actions`
|
||||
// selector reaches them.
|
||||
@include ga-hub.list-page();
|
||||
|
||||
@@ -85,6 +85,22 @@ nb-card {
|
||||
@include ga-hub.rows();
|
||||
}
|
||||
|
||||
// ── The page-header action row ───────────────────────────────────────────────
|
||||
// The seventh block of the list-page contract, and the one this sheet never
|
||||
// had: the six above brought the TABLE down to the shared density while "+ Add",
|
||||
// the selection strip, "Manage invites" and the grid-vs-list selector all stayed
|
||||
// at their full 0.375rem/0.875rem box around a 1rem icon. A toolbar a size and a
|
||||
// half larger than the rows beneath it is the most visible half of the
|
||||
// inconsistency, so this page read as unconverted even though most of it was.
|
||||
//
|
||||
// `nb-card-header` rather than `.gauzy-button-container`, because this header
|
||||
// holds a second group: "Manage invites" is a `<button nbButton class="action"
|
||||
// size="small">` in `.card-header-title`, outside the toolbar container. Naming
|
||||
// only the container would have left it behind — see the note in the mixin.
|
||||
:host nb-card-header {
|
||||
@include ga-hub.page-header-buttons();
|
||||
}
|
||||
|
||||
// ── Status pills ─────────────────────────────────────────────────────────────
|
||||
// Every other list page routes its statuses through `ga-status-badge`, which is
|
||||
// what the canonical block points `status-badge-tints()` at. This page does not:
|
||||
@@ -107,53 +123,21 @@ nb-card {
|
||||
}
|
||||
|
||||
// ── Full Name cell ───────────────────────────────────────────────────────────
|
||||
// `ga-picture-name-tags` renders `<ngx-avatar class="report-table">`, and that
|
||||
// context in `avatar.component.scss` pins a 20px image, a 12px name and an 8px
|
||||
// gap inside a padded tinted pill — 26px of chip in rows the density tokens
|
||||
// build to a 1rem line box, which made Full Name both the widest-reading and
|
||||
// the tallest cell in the table and left every row taller than the equivalent
|
||||
// row on Payments.
|
||||
// `ga-picture-name-tags` renders `<ngx-avatar class="report-table">`, and this
|
||||
// sheet used to shrink it: a 1rem image, a 0.6875rem name and a 2px-inset pill,
|
||||
// about 20px of chip, so that a person cost the row the same 1rem line box a
|
||||
// glyph does.
|
||||
//
|
||||
// Sized off the `gauzy-people-*` tokens, so the chip matches the scale the rest
|
||||
// of the row already follows. Table-scoped, not page-wide: `ga-card-grid`
|
||||
// instantiates the same `renderComponent` and its cards have room for the full
|
||||
// treatment.
|
||||
// That block is gone — it is what made this table read a size smaller than the
|
||||
// Invoices table it is meant to match. The image and the name are the renderer's
|
||||
// own 20px/12px again; only the pill's vertical inset is restated, which takes a
|
||||
// 20px image to the same 28px that the 18px-plus-5px `ngx-contact-links` chip
|
||||
// costs the Invoices Contact column. See `avatar-chip()` for the arithmetic.
|
||||
//
|
||||
// SPECIFICITY: `:host-context(.report-table) X` shims to
|
||||
// `.report-table[_nghost] X[_ngcontent]` — (0,4,0) for `.inner-wrapper`,
|
||||
// `.link-text` and `.image-container` — while `:host ::ng-deep` buys only the
|
||||
// one `[_nghost]` attribute. Naming `.report-table` (it sits on the host
|
||||
// element) plus one doubled class clears those. The nested `img` is the
|
||||
// exception: it shims to `.report-table[_nghost] .image-container[_ngc]
|
||||
// img[_ngc]`, (0,5,1), which no reachable combination of doubled classes here
|
||||
// beats without reading as line noise, so those two declarations are
|
||||
// `!important` — the same register that rule already uses for its own
|
||||
// `border-radius`.
|
||||
:host ::ng-deep angular2-smart-table ngx-avatar.report-table {
|
||||
.inner-wrapper.inner-wrapper {
|
||||
gap: var(--gauzy-people-gap, 0.375rem);
|
||||
// Keeps the original asymmetry — a hair of inset around the image, more
|
||||
// breathing room after the name — just scaled to the smaller chip. Not zero
|
||||
// on the image side: `.inner-wrapper` is a stadium with `overflow: hidden`,
|
||||
// so a flush image would be clipped by the left-hand curve.
|
||||
@include nb-ltr(padding, 0.125rem 0.375rem 0.125rem 0.125rem);
|
||||
@include nb-rtl(padding, 0.125rem 0.125rem 0.125rem 0.375rem);
|
||||
}
|
||||
|
||||
.link-text.link-text {
|
||||
font-size: var(--gauzy-people-font-size, 0.6875rem);
|
||||
line-height: var(--gauzy-people-avatar-size, 1rem);
|
||||
}
|
||||
|
||||
.image-container.image-container {
|
||||
width: var(--gauzy-people-avatar-size, 1rem);
|
||||
height: var(--gauzy-people-avatar-size, 1rem);
|
||||
|
||||
img {
|
||||
width: var(--gauzy-people-avatar-size, 1rem) !important;
|
||||
height: var(--gauzy-people-avatar-size, 1rem) !important;
|
||||
}
|
||||
}
|
||||
// Table-scoped, as the old block was: `ga-card-grid` instantiates the same
|
||||
// `renderComponent` for the card layout, which is not paying for a table row.
|
||||
:host .table-scroll-container ::ng-deep angular2-smart-table {
|
||||
@include ga-hub.avatar-chip();
|
||||
}
|
||||
|
||||
// ── Screen Capture cell ──────────────────────────────────────────────────────
|
||||
|
||||
+46
@@ -1,6 +1,7 @@
|
||||
@use 'gauzy/_gauzy-cards' as *;
|
||||
@use 'gauzy/_gauzy-table' as *;
|
||||
@use 'gauzy/_gauzy-overrides' as ga-overrides;
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
nb-card-body {
|
||||
background: nb-theme(gauzy-card-2);
|
||||
@@ -31,6 +32,20 @@ nb-card-body {
|
||||
.table-scroll-container {
|
||||
flex-grow: 10;
|
||||
max-height: unset;
|
||||
// Against `card-list-page()`'s `min-height: 100%`. That default is for the
|
||||
// pages whose container sits in a card body that is NOT a flex column,
|
||||
// where nothing else would make a short list fill the card. This body IS
|
||||
// one, and it holds the toolbar above the table: 100% of the body plus the
|
||||
// toolbar's own height is more than the body has, so the surface ran past
|
||||
// the bottom of the card. `flex-grow: 10` above already fills whatever the
|
||||
// toolbar leaves, and `min-height: 0` is what lets a flex item shrink back
|
||||
// to that — the default `auto` floors it at its content.
|
||||
//
|
||||
// Here rather than after the include: this selector is (0,5,3) against the
|
||||
// mixin's (0,3,1), so it holds no matter where the include lands — which
|
||||
// matters because the four sheets that `@forward` this file emit their own
|
||||
// `card-list-page()` call after everything in it.
|
||||
min-height: 0;
|
||||
}
|
||||
.grid {
|
||||
flex-grow: 10;
|
||||
@@ -118,3 +133,34 @@ nb-card-body {
|
||||
nb-card {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
// ── The list-page contract, for a table built by hand ────────────────────────
|
||||
//
|
||||
// Expense categories is the ORIGIN of the `nb-card > nb-card-body.custom-table`
|
||||
// row pattern — Vendors and Employment types `@forward` this file, and
|
||||
// Positions, Employee levels and Documents reach it through Vendors — and it was
|
||||
// the one page of the six still drawn the old way: every row its own card
|
||||
// (`gauzy-card-1`, a radius, a 0.5rem bottom margin) floating in an unbounded
|
||||
// scroll container, and a 6px rail that only exists while a row is selected, so
|
||||
// clicking one shifted it sideways.
|
||||
//
|
||||
// `card-list-page()` is the part of the list-page contract that does not depend
|
||||
// on `angular2-smart-table`: the density tokens, the toolbar scale, the bounded
|
||||
// table surface, and rows flattened onto it with one hairline between them and a
|
||||
// rail reserved at rest. Placed at the END of the file, after the `.custom-table`
|
||||
// paint above, because the mixin deliberately writes at that same specificity and
|
||||
// relies on source order to settle it.
|
||||
//
|
||||
// The five sheets that forward this one call `card-list-page()` themselves with
|
||||
// their own arguments; those calls are emitted after this one and still win, so
|
||||
// Vendors keeps its 1.125rem row gutter and Employment types keeps its card-body
|
||||
// toolbar. The cost is one duplicated block in each of those sheets — the same
|
||||
// declarations, immediately overridden — which is the price of the row treatment
|
||||
// finally reaching the page that defines the pattern.
|
||||
//
|
||||
// `$actions: '.gauzy-button-container'`, not the default `nb-card-header`: this
|
||||
// page's header holds the back arrow and the title and nothing else — the
|
||||
// toolbar is the first child of the card BODY, above the table. The default
|
||||
// named a block with no buttons in it, so the one thing the toolbar scale was
|
||||
// there to compact was the one thing it never reached.
|
||||
@include ga-hub.card-list-page('.gauzy-button-container');
|
||||
|
||||
@@ -22,3 +22,12 @@
|
||||
// different height from its siblings and had back the problem the shared
|
||||
// partial exists to solve (a gap under a short table, a clipped long one).
|
||||
@forward '@shared/_pg-card';
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// The same block Invoices carries: density tokens, the table surface, row
|
||||
// hover/selection, the tag-chip stack, the status pills, the pager's
|
||||
// `<nav><ul><li>` and the toolbar scale. Markup is the standard list shape, so
|
||||
// the default `$actions` selector (`nb-card-header`) reaches this page's
|
||||
// `.gauzy-button-container` and its sibling filter buttons alike.
|
||||
@include ga-hub.list-page();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@use 'gauzy/_gauzy-table' as *;
|
||||
@use 'gauzy/tabset-actions' as *;
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
:host {
|
||||
nb-tabset {
|
||||
@@ -62,3 +63,14 @@
|
||||
:host .gauzy-button-container {
|
||||
@include tabset-action-row(1rem);
|
||||
}
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// The same block Invoices carries: density tokens, the table surface, row
|
||||
// hover/selection, the tag-chip stack, the status pills, the pager and the
|
||||
// toolbar scale. One `#tableLayout` template serves all three record tabs (time
|
||||
// frames, KPIs, goal general settings), so the block reaches every one of them.
|
||||
//
|
||||
// `$actions` is the toolbar, not `nb-card-header`: this page's header holds only
|
||||
// the title, and the action row rides the tab strip inside the card BODY (see
|
||||
// `tabset-action-row()` above).
|
||||
@include ga-hub.list-page('.gauzy-button-container');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@use 'gauzy/_gauzy-table' as *;
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
.global {
|
||||
display: flex;
|
||||
@@ -235,3 +236,182 @@ h6 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── The list-page contract ───────────────────────────────────────────────────
|
||||
// The article list is built BY HAND — a `.table-scroll` holding one
|
||||
// `.custom-table` div per article — so nothing in `list-page()` ever reached it:
|
||||
// every selector in that bundle is anchored on `angular2-smart-table`. And
|
||||
// `card-list-page()`, which is the mixin for exactly this shape (Vendors,
|
||||
// Positions, Employee levels, Documents and Employment types all take it), is
|
||||
// anchored on `.table-scroll-container` — a class this page deliberately does
|
||||
// not use, because the global rule for it in `_overrides.scss` would overwrite
|
||||
// the height chain here (`calc(100vh - 21rem)`, and `calc(100vh - 28.7rem)` once
|
||||
// the filter row opens). So the same blocks are spelled out below against this
|
||||
// page's own markup.
|
||||
//
|
||||
// What they replace: 14px/17px body text under a 16px article name, in rows
|
||||
// padded 1rem and drawn as separately floating cards with their own radius and
|
||||
// an 8px gap to the next, on a container with no ring at all — the last list
|
||||
// under Organization still drawn the way the whole section was before the
|
||||
// density pass.
|
||||
|
||||
// The tokens sit on `:host` rather than on the list, and that placement is the
|
||||
// point: `ngx-avatar` and `ngx-no-data-message` are separate components, and
|
||||
// custom properties are the only thing that crosses those boundaries.
|
||||
:host {
|
||||
@include ga-hub.density-tokens();
|
||||
}
|
||||
|
||||
// `nb-card-header`, not `.gauzy-button-container` — this header holds THREE
|
||||
// button groups and only one of them is in that container: "Add article" sits on
|
||||
// the category line above it, and "Clear" sits in the filter row below it. Every
|
||||
// metric the mixin sets is a custom property, so one declaration on the header
|
||||
// reaches all three, `ngx-gauzy-button-action`'s own template included.
|
||||
:host nb-card-header {
|
||||
@include ga-hub.page-header-buttons();
|
||||
}
|
||||
|
||||
// ── One surface, with the rows on it ─────────────────────────────────────────
|
||||
// `row-gap` and the container's own padding go with the cards: the rows are
|
||||
// flush now and carry their own inset, the same as every `<tr>` in the app.
|
||||
:host .table-scroll {
|
||||
@include ga-hub.surface();
|
||||
|
||||
padding: 0;
|
||||
row-gap: 0;
|
||||
}
|
||||
|
||||
// The row itself, flattened onto that surface. Padding, type scale and the
|
||||
// hairline are `card-list-page()`'s values, so an article row and a vendor row
|
||||
// measure the same.
|
||||
//
|
||||
// The rule is drawn INSIDE the row's box, so nothing moves; the transparent
|
||||
// leading edge is there at rest so that selecting a row cannot shift it either.
|
||||
:host .table-scroll .custom-table {
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
line-height: var(--gauzy-table-line-height, 1.25rem);
|
||||
box-shadow: inset 0 -1px 0 0 var(--gauzy-overlay-border-color, rgba(126, 126, 143, 0.18));
|
||||
border-inline-start: 3px solid transparent;
|
||||
}
|
||||
|
||||
// The rule under the last article would be a second edge just inside the
|
||||
// surface's own.
|
||||
:host .table-scroll .custom-table:last-of-type {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:host .table-scroll .custom-table:hover:not(.selected) {
|
||||
background: var(--gauzy-hover-tint, rgba(126, 126, 143, 0.12));
|
||||
}
|
||||
|
||||
// Selection was an 8px box-shadow rail hung outside the row's leading edge, in
|
||||
// the SAME neutral tint as the fill — so it read as part of the row rather than
|
||||
// as a marker, and the row jumped 8px sideways on click. The primary accent on
|
||||
// the edge that is already reserved above, which is what every other card list
|
||||
// in the app uses; the hairline is restated because this rule replaces the
|
||||
// `box-shadow` that carried it.
|
||||
:host .table-scroll .custom-table.selected {
|
||||
background: var(--gauzy-active-tint, rgba(126, 126, 143, 0.2));
|
||||
border-inline-start-color: var(--color-primary-default);
|
||||
box-shadow: inset 0 -1px 0 0 var(--gauzy-overlay-border-color, rgba(126, 126, 143, 0.18));
|
||||
}
|
||||
|
||||
// ── Inside the row ───────────────────────────────────────────────────────────
|
||||
:host .table-scroll .custom-table {
|
||||
.row-name {
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
// The article name was 16px/19px — a step above the page's own control text
|
||||
// and two above a table cell, which is what made one article read as a card
|
||||
// heading rather than as a row.
|
||||
.name {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
font-size: 0.8125rem;
|
||||
line-height: var(--gauzy-table-line-height, 1.25rem);
|
||||
}
|
||||
|
||||
// The description and the byline, at the secondary scale the chips use.
|
||||
> p {
|
||||
margin-bottom: 0.25rem;
|
||||
color: var(--gauzy-text-color-2, var(--text-hint-color));
|
||||
}
|
||||
|
||||
// DRAFT and EMPLOYEES: the same box a status pill gets on every other list
|
||||
// page, from the same three tokens. At 14px/600 in a 12px-radius lozenge they
|
||||
// were the loudest thing in the row and a good deal taller than the name
|
||||
// beside them. `margin-inline-start` because the pair sat on a `margin-left`
|
||||
// that stayed on the left in RTL.
|
||||
.draft,
|
||||
.privacy {
|
||||
min-height: var(--gauzy-table-badge-height, 1rem);
|
||||
margin: 0;
|
||||
margin-inline-start: 0.375rem;
|
||||
padding-block: var(--gauzy-table-badge-padding-y, 0.1875rem);
|
||||
padding-inline: var(--gauzy-table-chip-padding-x, 0.3125rem);
|
||||
border-radius: var(--gauzy-table-badge-radius, 0.25rem);
|
||||
font-size: var(--gauzy-table-chip-font-size, 0.625rem);
|
||||
line-height: var(--gauzy-table-chip-line-height, 0.75rem);
|
||||
}
|
||||
|
||||
// "Written by …" and "Created at …". `.create-at` carried a flat
|
||||
// `margin-left: 100px` — a hard offset that neither flipped in RTL nor tracked
|
||||
// the width of the names beside it; `auto` on the leading edge parks the
|
||||
// timestamp against the row's trailing edge instead, which is where the
|
||||
// equivalent column sits on a real table.
|
||||
.user-info {
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: var(--gauzy-table-chip-font-size, 0.625rem);
|
||||
line-height: var(--gauzy-table-chip-line-height, 0.75rem);
|
||||
color: var(--gauzy-text-color-2, var(--text-hint-color));
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
// The negative pull existed to lift 30px avatars back onto the "Written by"
|
||||
// line; at the people scale they are on that line already, so it only lifted
|
||||
// them off it.
|
||||
.image-wrap {
|
||||
margin: 0;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.create-at {
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
margin: 0;
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The author avatars, at the people scale the rest of the app's rows use. Same
|
||||
// selector as the block higher up in this file (which pinned 24px), so it is
|
||||
// source order that settles it.
|
||||
//
|
||||
// `.image-container` AND the image: the container is what RESERVES the space, and
|
||||
// avatar.component.scss sizes it on its own (`size="sm"` in the template, so a
|
||||
// 32px box). Shrinking only the picture left each author sitting in a 32px slot
|
||||
// holding a 20px image — 12px of empty gutter before the name, on every avatar in
|
||||
// the row. `!important` for the same reason the image line carries it: the rules
|
||||
// being overridden nest under the avatar's `:host`, which shims to (0,4,0), and
|
||||
// this block reaches them from outside at (0,2,1).
|
||||
:host ::ng-deep ngx-avatar {
|
||||
.image-container,
|
||||
img {
|
||||
height: var(--gauzy-people-avatar-size, 1rem) !important;
|
||||
width: var(--gauzy-people-avatar-size, 1rem) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,11 @@
|
||||
@forward '@shared/_pg-card';
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// Density tokens, the table surface, row hover/selection, the tag-chip stack,
|
||||
// the status pills, the pager's `<nav><ul><li>` and the toolbar scale — the same
|
||||
// block Invoices carries. This page has the standard list markup
|
||||
// (`nb-card-header.card-custom-header` > `.gauzy-button-container`, a
|
||||
// `.table-scroll-container` and a `.pagination-container`), so the default
|
||||
// `$actions` selector reaches its toolbar unchanged.
|
||||
@include ga-hub.list-page();
|
||||
|
||||
@@ -39,15 +39,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label for="inputOrganizationContact" class="label"
|
||||
>{{ 'INVOICES_PAGE.CONTACT' | translate }}
|
||||
</label>
|
||||
<ga-contact-select
|
||||
[addTag]="true"
|
||||
[placeholder]="'POP_UPS.ALL_CONTACTS' | translate"
|
||||
formControlName="organizationContact"
|
||||
(onChanged)="selectOrganizationContact($event)"
|
||||
></ga-contact-select>
|
||||
<div class="form-group">
|
||||
<label for="inputOrganizationContact" class="label">
|
||||
{{ 'INVOICES_PAGE.CONTACT' | translate }}
|
||||
</label>
|
||||
<ga-contact-select
|
||||
[addTag]="true"
|
||||
[placeholder]="'POP_UPS.ALL_CONTACTS' | translate"
|
||||
formControlName="organizationContact"
|
||||
(onChanged)="selectOrganizationContact($event)"
|
||||
></ga-contact-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -119,13 +121,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12 mb-4">
|
||||
<ga-tags-color-input
|
||||
[selectedTags]="form.get('tags').value"
|
||||
(selectedTagsEvent)="selectedTagsEvent($event)"
|
||||
[isOrgLevel]="true"
|
||||
>
|
||||
</ga-tags-color-input>
|
||||
<div class="col-sm-12">
|
||||
<div class="form-group">
|
||||
<ga-tags-color-input
|
||||
[selectedTags]="form.get('tags').value"
|
||||
(selectedTagsEvent)="selectedTagsEvent($event)"
|
||||
[isOrgLevel]="true"
|
||||
></ga-tags-color-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -183,6 +186,7 @@
|
||||
placeholder="{{ 'INVOICES_PAGE.INVOICE_TYPE.SELECT_PROJECTS' | translate }}"
|
||||
formControlName="project"
|
||||
id="inputProject"
|
||||
class="item-selector"
|
||||
(change)="selectProject($event)"
|
||||
[multiple]="true"
|
||||
appendTo="body"
|
||||
@@ -211,6 +215,7 @@
|
||||
placeholder="{{ 'INVOICES_PAGE.INVOICE_TYPE.SELECT_TASKS' | translate }}"
|
||||
formControlName="task"
|
||||
id="inputTask"
|
||||
class="item-selector"
|
||||
(change)="selectTask($event)"
|
||||
[multiple]="true"
|
||||
appendTo="body"
|
||||
@@ -239,6 +244,7 @@
|
||||
placeholder="{{ 'INVOICES_PAGE.INVOICE_TYPE.SELECT_PRODUCTS' | translate }}"
|
||||
formControlName="product"
|
||||
id="inputProduct"
|
||||
class="item-selector"
|
||||
(change)="selectProduct($event)"
|
||||
[multiple]="true"
|
||||
appendTo="body"
|
||||
@@ -267,6 +273,7 @@
|
||||
placeholder="{{ 'INVOICES_PAGE.INVOICE_TYPE.SELECT_EXPENSES' | translate }}"
|
||||
formControlName="expense"
|
||||
id="inputExpense"
|
||||
class="item-selector"
|
||||
(change)="selectExpense($event)"
|
||||
[multiple]="true"
|
||||
appendTo="body"
|
||||
@@ -309,7 +316,6 @@
|
||||
status="success"
|
||||
[disabled]="!invoiceType"
|
||||
nbButton
|
||||
class="ml-3"
|
||||
>
|
||||
{{ 'INVOICES_PAGE.INVOICE_TYPE.GENERATE_FOR_UNINVOICED_EXPENSES' | translate }}
|
||||
</button>
|
||||
@@ -489,7 +495,7 @@
|
||||
</div>
|
||||
</form>
|
||||
@if (shouldLoadTable) {
|
||||
<div>
|
||||
<div class="items">
|
||||
<div class="table-scroll-container custom-table">
|
||||
<angular2-smart-table
|
||||
[class.ga-table-loading]="loading"
|
||||
@@ -517,12 +523,12 @@
|
||||
</div>
|
||||
}
|
||||
</nb-card-body>
|
||||
<nb-card-footer class="text-left">
|
||||
<button class="mr-3" size="small" status="basic" outline nbButton (click)="cancel()">
|
||||
<nb-card-footer>
|
||||
<button size="small" status="basic" outline nbButton (click)="cancel()">
|
||||
{{ 'BUTTONS.CANCEL' | translate }}
|
||||
</button>
|
||||
<button
|
||||
class="mr-3 gray"
|
||||
class="gray"
|
||||
(click)="addInvoice('DRAFT')"
|
||||
size="small"
|
||||
status="primary"
|
||||
@@ -532,7 +538,6 @@
|
||||
{{ 'BUTTONS.SAVE_AS_DRAFT' | translate }}
|
||||
</button>
|
||||
<button
|
||||
class="mr-3"
|
||||
(click)="sendToContact()"
|
||||
status="success"
|
||||
size="small"
|
||||
@@ -542,7 +547,6 @@
|
||||
{{ 'BUTTONS.SAVE_AND_SEND_CONTACT' | translate }}
|
||||
</button>
|
||||
<button
|
||||
class="mr-3"
|
||||
(click)="sendViaEmail()"
|
||||
status="success"
|
||||
size="small"
|
||||
|
||||
@@ -7,14 +7,28 @@ $shadow: 0 0 0 nb-theme(button-outline-width)
|
||||
$alpha: 0.1
|
||||
),
|
||||
inset nb-theme(button-outline-focus-inset-shadow-length) transparent;
|
||||
|
||||
// "Generate invoice items" — the one action that has to be taken before this
|
||||
// form has anything to save, so it is the only button in the body and it needs
|
||||
// to read as one.
|
||||
//
|
||||
// It was drawn `background-color: var(--gauzy-card-2)` with a TRANSPARENT
|
||||
// border: the exact colour of the card body behind it and no edge, so at rest it
|
||||
// was green text floating on the page with no button around it, and hovering it
|
||||
// pinned the border transparent again so nothing appeared. Same green, drawn the
|
||||
// way every other outline button in the app is drawn (see the `.green` block in
|
||||
// `gauzy/_gauzy-dialogs`): a translucent ring at rest that goes solid on hover.
|
||||
[nbButton].gen.appearance-outline.status-basic {
|
||||
background-color: var(--gauzy-card-2);
|
||||
border-color: transparent;
|
||||
background-color: transparent;
|
||||
border-color: rgba(
|
||||
$color: $green,
|
||||
$alpha: 0.4
|
||||
);
|
||||
box-shadow: $shadow;
|
||||
border-width: 2px;
|
||||
color: $green;
|
||||
&:hover {
|
||||
border-color: transparent;
|
||||
border-color: $green;
|
||||
}
|
||||
}
|
||||
[nbButton].gen.appearance-outline:hover {
|
||||
|
||||
@@ -365,7 +365,7 @@
|
||||
</div>
|
||||
</form>
|
||||
@if (shouldLoadTable) {
|
||||
<div>
|
||||
<div class="items">
|
||||
<div class="table-scroll-container custom-table">
|
||||
<angular2-smart-table
|
||||
[class.ga-table-loading]="loading"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@use 'gauzy/_gauzy-table' as *;
|
||||
@use 'gauzy/_gauzy-dialogs' as *;
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
// Row actions of the invoice-item grid. These were painted with literals: the
|
||||
// add/save pill as `#00d68f` and — the visible one — the cancel/delete pill as a
|
||||
@@ -62,6 +63,10 @@
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: nb-theme(text-paragraph-font-size);
|
||||
// Money, so the digits have to line up as the figures change: with the
|
||||
// proportional default the whole chip reflows every time a quantity is
|
||||
// edited in the grid above it.
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,26 +78,221 @@
|
||||
@include dialog(var(--gauzy-card-2), var(--gauzy-card-1));
|
||||
}
|
||||
|
||||
// ── The title line ───────────────────────────────────────────────────────────
|
||||
// A bare `d-flex` aligned the back arrow and the heading on their box TOPS, and
|
||||
// the heading's own 0.5rem bottom margin then tipped it half a step below the
|
||||
// arrow beside it. Centring the row and dropping that margin is what
|
||||
// invoice-view's header already does; this puts Add / Edit on the same line.
|
||||
nb-card-header {
|
||||
&.d-flex {
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
// invoice-edit wraps the arrow and the title in these (and hangs the Payments
|
||||
// button off the outer one); invoice-add puts them straight in the header.
|
||||
.card-header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ── The form body ────────────────────────────────────────────────────────────
|
||||
// Two columns: the record's own fields on the left, the Discount and Taxes
|
||||
// panels on the right.
|
||||
.form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 36px;
|
||||
// The step down to the item grid belongs to that block (`.items` below), which
|
||||
// is the only thing that ever follows the form. As a margin here it applied
|
||||
// just as much when no grid had been generated yet, leaving 36px of nothing
|
||||
// between the last field and the card footer.
|
||||
margin-bottom: 0;
|
||||
|
||||
.block-wrap {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
// The columns were two 48% blocks pushed apart by `space-between` on a row
|
||||
// that could not wrap, so the 4% between them shrank with the window and the
|
||||
// fields inside kept squeezing with no floor. A wrapping row with a real gap
|
||||
// and a basis wide enough for a pair of half-width controls puts them side
|
||||
// by side while there is room for both and stacks them when there is not —
|
||||
// the same behaviour with no breakpoint to keep in step with the grid.
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem 1.5rem;
|
||||
align-items: flex-start;
|
||||
|
||||
.block {
|
||||
width: 48%;
|
||||
flex: 1 1 22rem;
|
||||
// A flex item's `min-width` floor is its own content, so without this a
|
||||
// long contact name or a wide select pushes its column past its share of
|
||||
// the row instead of the control handling the overflow itself.
|
||||
min-width: 0;
|
||||
// Both columns start on the same line. The left one carries `mt-2` in the
|
||||
// template and the right one carried nothing, so the first panel sat half
|
||||
// a step above the first field beside it.
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
// The Terms field. A bare `<textarea>` is two rows tall, which in a column of
|
||||
// one-line controls reads as a slightly-too-tall input rather than somewhere
|
||||
// to write a paragraph. Still draggable, but only on the axis that has room.
|
||||
textarea[nbInput] {
|
||||
min-height: 5rem;
|
||||
resize: vertical;
|
||||
}
|
||||
}
|
||||
|
||||
// ── The item-type selectors ──────────────────────────────────────────────────
|
||||
// Projects / Tasks / Products / Expenses on the Add form: `ng-select`s in
|
||||
// multiple mode, whose selected values are chips inside the trigger.
|
||||
//
|
||||
// The chips already wrapped — `flex-wrap: wrap` on `.ng-value-container` is
|
||||
// ng-select's own default — but nothing past the first line was ever visible:
|
||||
// `includes/_ng-select.scss` pins EVERY `.ng-select-container` in the app to
|
||||
// `height: 40px`, and ng-select's component sheet gives it `overflow: hidden`.
|
||||
// Between them, picking a fifth project made the four before it disappear.
|
||||
//
|
||||
// So let the trigger grow with its content, cap it at two rows of chips, and
|
||||
// scroll the VALUE box rather than the whole trigger — that keeps the clear
|
||||
// button and the arrow on their own axis while the chips move under them.
|
||||
//
|
||||
// SPECIFICITY: every selector below is spelled out to its full ng-select depth
|
||||
// on purpose. `@ng-select/ng-select/themes/default.theme.css` is listed after
|
||||
// `styles.scss` in the app's `styles` array, so its five-class rules win every
|
||||
// tie; naming the same chain plus this component's `:host` attribute is what
|
||||
// carries these past it without `!important`.
|
||||
|
||||
// The chip's box, and the cap derived FROM it rather than pinned beside it: a
|
||||
// magic `max-height` next to the padding and line height that decide a chip's
|
||||
// real height is a pair that drifts the first time either side is retuned.
|
||||
$chip-line-height: 1.125rem;
|
||||
$chip-padding-y: 0.125rem;
|
||||
$chip-padding-x: 0.5rem;
|
||||
$chip-gap: 0.25rem;
|
||||
$value-box-padding-y: 0.25rem;
|
||||
// How many rows of chips the field shows before it starts scrolling.
|
||||
$visible-chip-rows: 2;
|
||||
$chip-height: $chip-line-height + $chip-padding-y * 2;
|
||||
|
||||
:host ::ng-deep .item-selector.ng-select-multiple {
|
||||
.ng-select-container {
|
||||
height: auto;
|
||||
// NO `align-items` here. It was `flex-start` — on the reasoning that an arrow
|
||||
// centred against four rows of chips floats halfway down the field — and that
|
||||
// was the wrong trade: the field is EMPTY or one line deep almost all of the
|
||||
// time, and top-pinning threw the placeholder and the arrow off the trigger's
|
||||
// centre in exactly that case. The arrow wrapper has no height of its own, so
|
||||
// top-aligning it shrank it to its glyph instead of letting it stretch.
|
||||
//
|
||||
// Inheriting the shared `center` keeps one line perfectly centred and puts
|
||||
// the arrow on the middle of a tall chip stack, which is what every
|
||||
// component library does with a chips input.
|
||||
}
|
||||
|
||||
.ng-select-container .ng-value-container {
|
||||
flex-wrap: wrap;
|
||||
// chip↔chip spacing, so the chips carry no margins of their own. The INLINE
|
||||
// padding is left to the shared `ng-select-overrides()` block, which sets it
|
||||
// `!important` so every trigger in the app starts its text on one x.
|
||||
gap: $chip-gap;
|
||||
// Symmetric, where ng-select's own theme pads this box `5px` at the top and
|
||||
// nothing at the bottom — which is what tipped the chips above the trigger's
|
||||
// centre line even before the wrapping was fixed.
|
||||
padding-block: $value-box-padding-y;
|
||||
// Two rows of chips and the gap between them, plus this box's own padding
|
||||
// (`box-sizing: border-box` app-wide, so the padding is inside the cap).
|
||||
// Anything past that scrolls.
|
||||
max-height: $visible-chip-rows * $chip-height + ($visible-chip-rows - 1) * $chip-gap +
|
||||
$value-box-padding-y * 2;
|
||||
overflow-y: auto;
|
||||
|
||||
// The app's scrollbar — the same one the dropdown panels and card bodies use.
|
||||
scrollbar-width: thin;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: nb-theme(scrollbar-width);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--gauzy-scrollbar);
|
||||
border-radius: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
// The typing area. ng-select pads it `0 0 3px 3px`, i.e. 3px along the bottom
|
||||
// and none along the top — so it is the tallest thing on the line and every
|
||||
// chip beside it sits that much above the line's centre. The chip↔chip `gap`
|
||||
// above is what separates the caret from the chips now, so the block half of
|
||||
// that padding has nothing left to do; the inline 3px stays.
|
||||
.ng-select-container .ng-value-container .ng-input {
|
||||
padding-block: 0;
|
||||
}
|
||||
|
||||
// One chip. ng-select's theme paints it `#333` on a pale blue with a 2px
|
||||
// corner — light-mode literals, so in the four dark themes the chips were a
|
||||
// foreign pale-blue block with near-black text on them. A neutral tint, the
|
||||
// shared small radius and the same hairline every other small box on this page
|
||||
// carries, all of which resolve per theme.
|
||||
.ng-select-container .ng-value-container .ng-value {
|
||||
// `margin: 0` because the row's spacing is the container's `gap` now.
|
||||
// ng-select's own theme puts `margin-bottom: 5px` on every chip, which on a
|
||||
// wrapped row is 5px of dead space under the LAST line as well — the other
|
||||
// half of why the chips never sat on the centre line.
|
||||
margin: 0;
|
||||
padding: $chip-padding-y $chip-padding-x;
|
||||
background-color: var(--gauzy-hover-tint, rgba(126, 126, 143, 0.12));
|
||||
box-shadow: $action-hairline;
|
||||
border-radius: var(--gauzy-radius-sm, 0.375rem);
|
||||
color: var(--gauzy-text-color-1);
|
||||
font-size: nb-theme(text-caption-font-size);
|
||||
// Pinned, not left to `normal`: the chip is what sets the height of its row,
|
||||
// so an unresolved line box would make the row's height depend on the font
|
||||
// that happened to load — and the two-row cap above is derived from it.
|
||||
line-height: $chip-line-height;
|
||||
// The chip is a flex item and a block box; centre its own text in it so a
|
||||
// chip and the typing caret beside it share one centre line.
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
// The chip's label comes from this form's own `ng-label-tmp`, which wraps the
|
||||
// name in a `.selector-template`. A long project name ellipsizes inside the
|
||||
// chip instead of pushing it past the edge of the field.
|
||||
.ng-select-container .ng-value-container .ng-value .selector-template {
|
||||
display: block;
|
||||
// The chip above is a flex box, and a flex item will not shrink below its own
|
||||
// content unless it is told it may — without this `text-overflow` never
|
||||
// engages and a long project name widens the chip instead of picking up an
|
||||
// ellipsis.
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
// The generated item grid, its pager and the totals — one block, stepped away
|
||||
// from the form above it.
|
||||
.items {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.footer-block-wrap {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
@@ -109,18 +309,54 @@
|
||||
width: 98%;
|
||||
}
|
||||
|
||||
// ── The Discount and Taxes panels ────────────────────────────────────────────
|
||||
// Each one is a Bootstrap `.row` as well as a `.group`, and a `.row` bleeds 15px
|
||||
// past its container on both sides with negative margins — so the panel was
|
||||
// drawn wider than the column it belongs to, hanging into the gutter between the
|
||||
// two columns on one side and off the block on the other. The asymmetric
|
||||
// `9px 18px 2px 12px` padding was what that bleed had been tuned against.
|
||||
//
|
||||
// Zero the bleed, pad the panel evenly, and give it the radius and the hairline
|
||||
// every other surface on the page already carries. The fill stays where the
|
||||
// page's other surfaces are painted, on `.content .row.group` below.
|
||||
.group {
|
||||
border-radius: 0.6rem;
|
||||
margin-bottom: 20px;
|
||||
padding: 9px 18px 2px 12px;
|
||||
margin: 0 0 1rem;
|
||||
padding: 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: $action-hairline;
|
||||
|
||||
.label-group {
|
||||
font-size: 14px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
// A section title, not a field label: one step above the 0.75rem that
|
||||
// `input-appearance` gives every `.label`, and in the primary text colour
|
||||
// rather than the muted one the fields under it use. It was pinned at 14px,
|
||||
// so it did not follow the root font size either.
|
||||
font-size: nb-theme(text-subtitle-2-font-size);
|
||||
font-weight: 600;
|
||||
line-height: 17px;
|
||||
line-height: 1.25;
|
||||
letter-spacing: -0.01em;
|
||||
text-align: left;
|
||||
margin-bottom: 18px;
|
||||
color: var(--gauzy-text-color-1);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
// The `.row`s INSIDE the panel bleed the same 15px, which put the first field
|
||||
// flush against the panel's padding edge and the second one past it. A 0.5rem
|
||||
// gutter keeps a pair of half-width fields inside the panel with a real gap
|
||||
// between them.
|
||||
> .row {
|
||||
margin-inline: -0.5rem;
|
||||
|
||||
> [class*='col-'] {
|
||||
padding-inline: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
// The panel's own bottom padding is the space under its last row of fields, so
|
||||
// the field's `.form-group` margin would otherwise double it.
|
||||
> .row:last-child > [class*='col-'] > .form-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,15 +365,40 @@
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
// Was an `ml-3` utility on the second button. A gap spaces however many
|
||||
// buttons the selected invoice type puts on the line, and lets them wrap
|
||||
// rather than overflow the column.
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
// Was `20px -1rem`: a negative inline margin that pulled the checkbox out past
|
||||
// both edges of its column, so it lined up with neither the panels above it nor
|
||||
// the fields inside them.
|
||||
.discountCheckbox {
|
||||
margin: 20px -1rem;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
nb-card {
|
||||
background-color: var(--gauzy-card-2);
|
||||
}
|
||||
|
||||
// The action row under the card body. Its buttons spaced themselves with `mr-3`,
|
||||
// a Bootstrap utility — so the spacing was `!important`, it had to be repeated
|
||||
// on every button, and the last one left a trailing 1rem inside the footer. One
|
||||
// flex line with a gap says it once; the cancel below is for invoice-edit, whose
|
||||
// template still carries the utility.
|
||||
nb-card-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
|
||||
[nbButton] {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
div.button-action {
|
||||
background-color: var(--gauzy-card-2);
|
||||
padding: 6px 8px;
|
||||
@@ -152,8 +413,14 @@ div.button-action {
|
||||
.custom-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--gauzy-card-2);
|
||||
padding: 12px;
|
||||
// `gauzy-card-1`, not `-2`: this element also carries
|
||||
// `.table-scroll-container`, which `list-page()` at the bottom of this file
|
||||
// paints with the shared table `surface()` — card-1, a hairline and a radius.
|
||||
// Painting it card-2 here left that ring and radius drawn around a block the
|
||||
// exact colour of the card body behind it, so the item grid read as a line
|
||||
// on the page rather than as the bounded table surface every list page has.
|
||||
background-color: var(--gauzy-card-1);
|
||||
padding: 0.75rem;
|
||||
.table {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -173,17 +440,43 @@ div.button-action {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
// Save as draft: the neutral step between the outline Cancel and the two success
|
||||
// buttons beside it, painted with the grey `_gauzy-dialogs` already shares with
|
||||
// the rest of the app rather than with a literal repeated here.
|
||||
//
|
||||
// The block this replaces set the fill on the button but hung its hover state
|
||||
// off a `[nbButton]` DESCENDANT of it — markup that does not exist — so the
|
||||
// button stayed flat under the pointer while every other button in the row
|
||||
// responded.
|
||||
:host button.gray {
|
||||
background-color: rgba(126, 126, 143, 1);
|
||||
color: nb-theme(text-control-color);
|
||||
border-color: var(--button-filled-basic-border-color);
|
||||
[nbButton].appearance-filled.status-basic {
|
||||
background-color: nb-theme(button-filled-basic-background-color);
|
||||
border-color: nb-theme(button-filled-basic-border-color);
|
||||
color: nb-theme(button-filled-basic-text-color);
|
||||
:hover {
|
||||
color: nb-theme(text-basic-color);
|
||||
background-color: rgba($color: rgba(126, 126, 143), $alpha: 0.1);
|
||||
&[nbButton].appearance-filled {
|
||||
background-color: $gray;
|
||||
border-color: $gray;
|
||||
color: nb-theme(text-control-color);
|
||||
|
||||
&:hover:not([disabled]) {
|
||||
background-color: rgba($color: $gray, $alpha: 0.85);
|
||||
border-color: rgba($color: $gray, $alpha: 0.85);
|
||||
}
|
||||
|
||||
&:focus:not(:hover):not(:active) {
|
||||
box-shadow: 0 0 0 nb-theme(button-outline-width) rgba($color: $gray, $alpha: 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// The invoice-items grid, brought onto the same footing as the Invoices list
|
||||
// this form is opened from: density tokens, the table surface, row
|
||||
// hover/selection, the tag-chip stack, the status pills and the pager. Emitted
|
||||
// into invoice-add's sheet too, through the `@forward` at the top of that file —
|
||||
// Add and Edit render the same grid.
|
||||
//
|
||||
// The density tokens only reach cell type and padding and the FILTER row's
|
||||
// controls (see `--gauzy-table-control-height` in `_overrides.scss`); the inline
|
||||
// row editors this grid is built on keep their own control sizing.
|
||||
//
|
||||
// `$actions: null`: there is no page-header action row here. This form's buttons
|
||||
// are full-size `nb-card-header` form actions (Save, Send, …), not the compact
|
||||
// list toolbar the mixin's button block is written for.
|
||||
@include ga-hub.list-page(null);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@use 'gauzy/_gauzy-table' as *;
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
.date-and-value {
|
||||
display: flex;
|
||||
@@ -71,3 +72,14 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// An invoice's payments are the same table as `/pages/accounting/payments`, one
|
||||
// click in, so they take the same block: density tokens, the table surface, row
|
||||
// hover/selection, the tag-chip stack, the status pills and the toolbar scale.
|
||||
//
|
||||
// `$actions` is `.custom-container` — the panel below the invoice summary that
|
||||
// holds both the toolbar and the table; this page's `nb-card-header` carries only
|
||||
// the invoice's title. There is no pager on this table, so the pager half of the
|
||||
// block simply matches nothing.
|
||||
@include ga-hub.list-page('.custom-container');
|
||||
|
||||
@@ -1 +1,9 @@
|
||||
@forward '@shared/_pg-card';
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// Received invoices and received estimates render this component, and both are
|
||||
// the same table as `/pages/accounting/invoices` — so they take the same block
|
||||
// that page does: density tokens, the table surface, row hover/selection, the
|
||||
// tag-chip stack, the status pills, the pager and the toolbar scale.
|
||||
@include ga-hub.list-page();
|
||||
|
||||
@@ -75,8 +75,8 @@
|
||||
</nb-accordion-item-header>
|
||||
<nb-accordion-item-body>
|
||||
<form [formGroup]="searchForm" (ngSubmit)="search()">
|
||||
<div class="row w-100">
|
||||
<div class="col-sm-4">
|
||||
<div class="row w-100 search-filters">
|
||||
<div class="col search-filter-col">
|
||||
<div class="form-group">
|
||||
<label for="inputInvoiceNumber" class="label">
|
||||
<span>
|
||||
@@ -103,7 +103,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="col search-filter-col">
|
||||
<div class="form-group">
|
||||
<label for="inputInvoiceDate" class="label">
|
||||
<span>
|
||||
@@ -137,7 +137,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="col search-filter-col">
|
||||
<div class="form-group">
|
||||
<label for="inputDueDate" class="label">{{
|
||||
'INVOICES_PAGE.DUE_DATE' | translate
|
||||
@@ -157,9 +157,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<div class="col search-filter-col">
|
||||
<div class="form-group">
|
||||
<label for="inputOrganizationContact" class="label">
|
||||
<span>{{ 'INVOICES_PAGE.CONTACT' | translate }}</span>
|
||||
@@ -171,7 +169,7 @@
|
||||
></ga-contact-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<div class="col search-filter-col">
|
||||
<div class="form-group">
|
||||
<label for="inputTotalValue" class="label">
|
||||
{{ 'INVOICES_PAGE.TOTAL_VALUE' | translate }}
|
||||
@@ -186,7 +184,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<div class="col search-filter-col">
|
||||
<div class="form-group">
|
||||
<ga-currency
|
||||
formControlName="currency"
|
||||
@@ -196,15 +194,17 @@
|
||||
</ga-currency>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<ga-tags-color-input
|
||||
[selectedTags]="searchForm.get('tags').value"
|
||||
(selectedTagsEvent)="selectedTagsEvent($event)"
|
||||
[isOrgLevel]="true"
|
||||
>
|
||||
</ga-tags-color-input>
|
||||
<div class="col search-filter-col">
|
||||
<div class="form-group">
|
||||
<ga-tags-color-input
|
||||
[selectedTags]="searchForm.get('tags').value"
|
||||
(selectedTagsEvent)="selectedTagsEvent($event)"
|
||||
[isOrgLevel]="true"
|
||||
>
|
||||
</ga-tags-color-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<div class="col search-filter-col">
|
||||
<div class="form-group">
|
||||
<label for="inputStatus" class="label">
|
||||
{{ 'INVOICES_PAGE.STATUS' | translate }}
|
||||
@@ -240,30 +240,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
status="success"
|
||||
class="ml-3"
|
||||
size="small"
|
||||
nbButton
|
||||
>
|
||||
{{ 'BUTTONS.SEARCH' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="reset"
|
||||
status="basic"
|
||||
outline
|
||||
size="small"
|
||||
nbButton
|
||||
(click)="reset()"
|
||||
>
|
||||
{{ 'BUTTONS.RESET' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="row search-actions">
|
||||
<button type="submit" status="success" size="small" nbButton>
|
||||
{{ 'BUTTONS.SEARCH' | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="reset"
|
||||
status="basic"
|
||||
outline
|
||||
size="small"
|
||||
nbButton
|
||||
(click)="reset()"
|
||||
>
|
||||
{{ 'BUTTONS.RESET' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</nb-accordion-item-body>
|
||||
@@ -303,80 +293,167 @@
|
||||
</div>
|
||||
</nb-accordion-item-header>
|
||||
<nb-accordion-item-body>
|
||||
<div class="row comments-container">
|
||||
<div class="col-6">
|
||||
<div class="comments-container">
|
||||
<section class="comment-panel comment-composer">
|
||||
<header class="comment-panel-header">
|
||||
<nb-icon icon="edit-2-outline" pack="eva"></nb-icon>
|
||||
<span class="comment-panel-title">
|
||||
{{ 'BUTTONS.ADD_COMMENT' | translate }}
|
||||
</span>
|
||||
</header>
|
||||
@if (selectedInvoice) {
|
||||
{{ 'INVOICES_PAGE.COMMENT' | translate }}
|
||||
<form
|
||||
class="comment-form"
|
||||
[formGroup]="historyForm"
|
||||
#historyFormDirective="ngForm"
|
||||
(ngSubmit)="addComment(historyFormDirective)"
|
||||
>
|
||||
<div class="col-8 mt-3 px-0">
|
||||
<input
|
||||
nbInput
|
||||
type="text"
|
||||
placeholder="{{ 'INVOICES_PAGE.TITLE' | translate }}"
|
||||
fullWidth
|
||||
formControlName="title"
|
||||
/>
|
||||
</div>
|
||||
<div class="col px-0 mt-3">
|
||||
<textarea
|
||||
nbInput
|
||||
placeholder="{{ 'INVOICES_PAGE.COMMENT' | translate }}"
|
||||
fullWidth
|
||||
formControlName="comment"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<button
|
||||
type="submit"
|
||||
class="mx-0"
|
||||
nbButton
|
||||
status="info"
|
||||
[disabled]="historyForm.invalid"
|
||||
>
|
||||
{{ 'BUTTONS.ADD_COMMENT' | translate }}
|
||||
</button>
|
||||
<div class="comment-form-row">
|
||||
<span class="comment-avatar" aria-hidden="true">
|
||||
@if (currentUser?.imageUrl) {
|
||||
<img [src]="currentUser.imageUrl" alt="" />
|
||||
} @else {
|
||||
{{ authorInitials(currentUser?.name) }}
|
||||
}
|
||||
</span>
|
||||
<div class="comment-form-fields">
|
||||
<input
|
||||
nbInput
|
||||
type="text"
|
||||
fullWidth
|
||||
fieldSize="small"
|
||||
class="comment-form-title"
|
||||
formControlName="title"
|
||||
placeholder="{{
|
||||
'FORM.PLACEHOLDERS.TITLE' | translate
|
||||
}}"
|
||||
/>
|
||||
<div class="comment-form-box">
|
||||
<textarea
|
||||
nbInput
|
||||
fullWidth
|
||||
fieldSize="small"
|
||||
rows="3"
|
||||
formControlName="comment"
|
||||
placeholder="{{
|
||||
'INVOICES_PAGE.COMMENT_PLACEHOLDER' | translate
|
||||
}}"
|
||||
></textarea>
|
||||
<div class="comment-form-actions">
|
||||
<button
|
||||
type="button"
|
||||
nbButton
|
||||
ghost
|
||||
size="tiny"
|
||||
status="basic"
|
||||
[disabled]="historyForm.pristine"
|
||||
(click)="resetComment(historyFormDirective)"
|
||||
>
|
||||
{{ 'BUTTONS.RESET' | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
nbButton
|
||||
size="tiny"
|
||||
status="primary"
|
||||
[disabled]="historyForm.invalid"
|
||||
>
|
||||
<nb-icon
|
||||
icon="paper-plane-outline"
|
||||
pack="eva"
|
||||
></nb-icon>
|
||||
{{ 'BUTTONS.ADD_COMMENT' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
@if (!disableButton) {
|
||||
<div class="col-6">
|
||||
{{ 'INVOICES_PAGE.COMMENTS' | translate }}
|
||||
<div class="comments w-100">
|
||||
<nb-list class="history-list">
|
||||
@for (history of histories; track history) {
|
||||
<nb-list-item
|
||||
class="history-list-item"
|
||||
style="align-items: baseline"
|
||||
>
|
||||
<div class="d-flex">
|
||||
<div>
|
||||
<ngx-avatar
|
||||
[src]="history.user.imageUrl"
|
||||
[name]="history.user.name"
|
||||
[value]="history.user"
|
||||
></ngx-avatar>
|
||||
<div class="history-date history-item">
|
||||
{{ history.createdAt | dateTimeFormat }}
|
||||
</div>
|
||||
<div class="history-item history-title">
|
||||
<span>{{ history.title }}</span>
|
||||
</div>
|
||||
<div class="history-item history-comment">
|
||||
<span>{{ history.action }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nb-list-item>
|
||||
}
|
||||
</nb-list>
|
||||
} @else {
|
||||
<div class="comment-empty">
|
||||
<nb-icon icon="file-text-outline" pack="eva"></nb-icon>
|
||||
<span>
|
||||
{{
|
||||
(!isEstimate
|
||||
? 'INVOICES_PAGE.SELECT_INVOICE_TO_VIEW_HISTORY'
|
||||
: 'INVOICES_PAGE.ESTIMATES.SELECT_ESTIMATE_TO_VIEW_HISTORY'
|
||||
) | translate
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
<section class="comment-panel comment-thread">
|
||||
<header class="comment-panel-header">
|
||||
<nb-icon icon="message-square-outline" pack="eva"></nb-icon>
|
||||
<span class="comment-panel-title">
|
||||
{{ 'INVOICES_PAGE.COMMENTS' | translate }}
|
||||
</span>
|
||||
@if (selectedInvoice && histories.length) {
|
||||
<span class="comment-count">{{ histories.length }}</span>
|
||||
}
|
||||
</header>
|
||||
<div class="comments">
|
||||
@if (selectedInvoice && histories.length) {
|
||||
<ul class="comment-list">
|
||||
@for (history of histories; track history.id) {
|
||||
<li class="comment-item">
|
||||
<span class="comment-avatar" aria-hidden="true">
|
||||
@if (history.user?.imageUrl) {
|
||||
<img [src]="history.user.imageUrl" alt="" />
|
||||
} @else {
|
||||
{{ authorInitials(history.user?.name) }}
|
||||
}
|
||||
</span>
|
||||
<div class="comment-body">
|
||||
<div class="comment-byline">
|
||||
@if (canOpenUserProfile && history.user?.id) {
|
||||
<a
|
||||
class="comment-author"
|
||||
[routerLink]="[
|
||||
'/pages/users/edit',
|
||||
history.user.id
|
||||
]"
|
||||
>
|
||||
{{ history.user?.name }}
|
||||
</a>
|
||||
} @else {
|
||||
<span class="comment-author">
|
||||
{{ history.user?.name }}
|
||||
</span>
|
||||
}
|
||||
<span
|
||||
class="comment-time"
|
||||
[nbTooltip]="
|
||||
history.createdAt | dateTimeFormat
|
||||
"
|
||||
>
|
||||
{{ commentTimeAgo(history.createdAt) }}
|
||||
</span>
|
||||
</div>
|
||||
@if (history.title) {
|
||||
<div class="comment-subject">
|
||||
{{ history.title }}
|
||||
</div>
|
||||
}
|
||||
<p class="comment-text">{{ history.action }}</p>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
} @else {
|
||||
<div class="comment-empty">
|
||||
<nb-icon icon="message-square-outline" pack="eva"></nb-icon>
|
||||
<span class="comment-empty-title">
|
||||
{{ 'INVOICES_PAGE.NO_COMMENTS' | translate }}
|
||||
</span>
|
||||
<span class="comment-empty-hint">
|
||||
{{ 'INVOICES_PAGE.NO_COMMENTS_HINT' | translate }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
</nb-accordion-item-body>
|
||||
</nb-accordion-item>
|
||||
@@ -445,7 +522,7 @@
|
||||
<nb-select
|
||||
filled
|
||||
status="basic"
|
||||
class="action select-nb mr-2 ml-1"
|
||||
class="action select-nb"
|
||||
[placeholder]="'INVOICES_PAGE.SET_STATUS' | translate | titlecase"
|
||||
[(selected)]="status"
|
||||
[disabled]="disableButton"
|
||||
@@ -463,7 +540,7 @@
|
||||
<nb-select
|
||||
filled
|
||||
status="basic"
|
||||
class="action select-nb mr-2"
|
||||
class="action select-nb"
|
||||
[placeholder]="'INVOICES_PAGE.SET_STATUS' | translate | titlecase"
|
||||
[(selected)]="status"
|
||||
[disabled]="disableButton"
|
||||
|
||||
@@ -95,33 +95,12 @@
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.history-list-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.history-list-select {
|
||||
font-weight: bold;
|
||||
// margin: 20px 0;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.history-action {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.history-date {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -259,23 +238,27 @@ ga-pagination {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.comments {
|
||||
// `scroll` reserved a scrollbar gutter on the History tab even when an invoice
|
||||
// has one comment or none, so the empty panel showed a dead scrollbar track.
|
||||
overflow-y: auto;
|
||||
height: 15rem;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 85px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
// Same hairline ring as the shared `.action` block in `gauzy/_gauzy-table.scss`
|
||||
// — this file carries a near-verbatim copy of it (with the drop shadow spelled
|
||||
// out as a literal rather than read from `--gauzy-shadow`).
|
||||
.action {
|
||||
box-shadow: inset 0 0 0 1px var(--gauzy-overlay-border-color, rgba(126, 126, 143, 0.18));
|
||||
// The one line the copy had dropped, and the one that matters on the dark
|
||||
// theme: Nebular draws a REAL 1px border on every filled button, and for
|
||||
// `status="basic"` it colours it `color-basic-default-border` →
|
||||
// `color-basic-300` → #edf1f7. Neither `dark` nor `gauzy-dark` restates the
|
||||
// `color-basic-*` ramp, so that near-white is what the whole toolbar strip
|
||||
// beside the tabs was ringed with — and the hover/active/focus steps are
|
||||
// whiter still (#f7f9fc / #e4e9f2 / #c5cee0). Only the FILL was overridden
|
||||
// below, which left every button outlined in white on a near-black page.
|
||||
//
|
||||
// Dropping the border rather than recolouring it, because the ring above is
|
||||
// already this button's edge: it is the same hairline the cards, overlays and
|
||||
// table use, it self-adjusts on the light themes, and it costs no layout box
|
||||
// — so the strip also stops standing 2px taller than the identical strip on
|
||||
// every other list page, all of which take the shared block and its
|
||||
// `border: none` with it.
|
||||
border: none;
|
||||
|
||||
&[nbButton].appearance-filled.status-basic {
|
||||
background-color: var(--gauzy-card-2);
|
||||
@@ -303,14 +286,134 @@ textarea {
|
||||
}
|
||||
}
|
||||
|
||||
&.select-nb ::ng-deep {
|
||||
// ── The "Set status" trigger, rebuilt as a View button ──────────────
|
||||
// Matching the select's metrics one by one got the numbers right and still
|
||||
// left it reading as a form field, because its BOX MODEL is a field's: the
|
||||
// label is an inline `<span>` in a block button, and the chevron is taken out
|
||||
// of the flow and parked in a gutter the padding reserves for it — hence a
|
||||
// wide control with the label jammed left and the arrow far away.
|
||||
//
|
||||
// So the box is rebuilt the way `[nbButton]` builds one: inline-flex, centred,
|
||||
// with the chevron an ordinary sibling of the label separated by the same
|
||||
// `button-small-icon-offset` that sits between View's eye icon and its text.
|
||||
// Nothing is pinned after that — the padding, line box and icon size are the
|
||||
// button's own tokens, so the height comes out of the same arithmetic rather
|
||||
// than being hard-coded to match.
|
||||
&.select-nb {
|
||||
// The ring belongs on the inner <button>, which is the box that is painted.
|
||||
box-shadow: none;
|
||||
// `button { margin: 5px }` near the top of this file spaces every button on
|
||||
// the strip, and it cannot reach `.select-button`: that <button> lives in
|
||||
// nb-select's template and carries THAT component's content attribute, not
|
||||
// this one. The trigger wore `mr-2 ml-1` instead — 0.5rem / 0.25rem and
|
||||
// nothing vertical. Those are Bootstrap utilities, i.e. `!important`, so
|
||||
// they could not be overridden from here and came off the template; the
|
||||
// host now takes the same 5px the buttons do.
|
||||
margin: 5px;
|
||||
|
||||
.select-button {
|
||||
box-shadow: inset 0 0 0 1px var(--gauzy-overlay-border-color, rgba(126, 126, 143, 0.18));
|
||||
::ng-deep .select-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: nb-theme(button-small-icon-offset);
|
||||
// Nebular's stock `select-min-width` is 13rem, and the app does not take
|
||||
// it: of the eight themes `themes.scss` registers, six restate the token
|
||||
// as 6rem, and the two that do not — `gauzy-light` and `gauzy-dark`, the
|
||||
// two the app ships on — extend `default` and `dark`, which are among the
|
||||
// six. `nb-register-theme` flattens a parent's map into the child at
|
||||
// registration, so both inherit 6rem rather than falling back to the stock
|
||||
// value. Either way this trigger takes none of it: the box hugs its label,
|
||||
// the way every button on the strip does.
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
padding: nb-theme(button-filled-small-padding);
|
||||
// View computes to 1.5rem: a 1rem line box inside 0.25rem of padding,
|
||||
// with no border since the rule above drops it. The padding and the line
|
||||
// box below reproduce that arithmetic, and the height is stated outright
|
||||
// as well — a derived height is only ever as tall as the TALLEST thing in
|
||||
// the flex line, so anything that resolves larger than the line box (the
|
||||
// chevron, or a line-height that fails to resolve) would silently push
|
||||
// the box past the buttons again.
|
||||
height: 1.5rem;
|
||||
// Nebular stacks `select-icon-offset` (2rem) on top of that shorthand
|
||||
// from a `[dir=ltr]`-qualified rule, to clear the absolutely positioned
|
||||
// chevron. There is no gutter to reserve now that the chevron is back in
|
||||
// the flow, so the inline-end side is restated the same way — it is the
|
||||
// only way to out-rank it.
|
||||
@include nb-ltr(padding-right, 0.625rem);
|
||||
@include nb-rtl(padding-left, 0.625rem);
|
||||
line-height: nb-theme(button-small-text-line-height);
|
||||
font-size: nb-theme(button-small-text-font-size);
|
||||
font-weight: nb-theme(button-text-font-weight);
|
||||
// `select-filled-basic-text-color` is `color-basic-800` (#222b45), a navy
|
||||
// that all but vanishes on the near-black panel. This is what `.secondary`
|
||||
// paints View with.
|
||||
color: var(--text-hint-color);
|
||||
background: var(--gauzy-card-2);
|
||||
height: 2rem;
|
||||
// `select-filled-basic-border-color` resolves down the same chain as the
|
||||
// buttons' — `color-basic-default-border` → `color-basic-300` → #edf1f7 —
|
||||
// so the one control here that is not a button was ringed in white
|
||||
// alongside them. `border-color` alone would not do it:
|
||||
// `nb-select.appearance-filled .select-button` sets the style and the
|
||||
// width too, and this selector out-ranks it, so the whole shorthand goes.
|
||||
border: none;
|
||||
border-radius: nb-theme(button-rectangle-border-radius);
|
||||
box-shadow: inset 0 0 0 1px var(--gauzy-overlay-border-color, rgba(126, 126, 143, 0.18));
|
||||
|
||||
// Nebular draws a select's PLACEHOLDER from its own pair of tokens —
|
||||
// 0.875rem at weight 400, against the buttons' 0.75rem at 700 — and "Set
|
||||
// Status" IS the placeholder, i.e. the state on screen until a row is
|
||||
// picked.
|
||||
&.placeholder {
|
||||
font-size: nb-theme(button-small-text-font-size);
|
||||
font-weight: nb-theme(button-text-font-weight);
|
||||
color: var(--text-hint-color);
|
||||
}
|
||||
|
||||
// Nebular repaints a filled select on hover and focus out of the
|
||||
// `color-basic-*` ramp — the same near-whites the border rule above got
|
||||
// rid of. The buttons never move: this page's `.action` fill out-ranks
|
||||
// Nebular's hover on an `[nbButton]`, and these restore that standoff.
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:focus:hover {
|
||||
background: var(--gauzy-card-2);
|
||||
}
|
||||
|
||||
// Disabled — which is how the strip starts out, before a row is picked.
|
||||
// A button dims its LABEL and keeps its fill; a select swaps its fill for
|
||||
// an opaque `background-basic-color-2` instead. Last of the three, so it
|
||||
// settles the tie with `.placeholder` above.
|
||||
&[disabled] {
|
||||
background: var(--gauzy-card-2);
|
||||
color: var(--text-disabled-color);
|
||||
}
|
||||
|
||||
nb-icon {
|
||||
// Back into the flow, which is the whole point of the block above:
|
||||
// `position: absolute` + `top: 50%` + `translateY(-50%)` is what put
|
||||
// the chevron in a gutter instead of beside the label.
|
||||
position: static;
|
||||
transform: none;
|
||||
// View's icon is a 0.875rem BOX — `[nbButton].size-#{size} nb-icon` sets
|
||||
// an explicit width and height from `--button-small-icon-size`, which
|
||||
// `page-header-buttons()` compacts. A select's chevron gets neither: it
|
||||
// is sized `1.5em` and then taken out of the flow, so its box never had
|
||||
// to be right. Back in the flow it is a flex item whose height counts
|
||||
// toward the button's, and `nb-icon`'s only other sizing is the global
|
||||
// `width/height: 1em` — so both are spelled out here exactly as the
|
||||
// button spells them.
|
||||
font-size: nb-theme(button-small-icon-size);
|
||||
width: nb-theme(button-small-icon-size);
|
||||
height: nb-theme(button-small-icon-size);
|
||||
color: var(--text-hint-color);
|
||||
}
|
||||
}
|
||||
|
||||
// The open-state flip, which Nebular writes as a SECOND transform including
|
||||
// the centring translate it no longer needs.
|
||||
&.open ::ng-deep .select-button nb-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -440,60 +543,411 @@ nb-tab {
|
||||
@include ga-overrides.dialog(var(--gauzy-card-1), var(--gauzy-card-1));
|
||||
}
|
||||
|
||||
// ── History tab: comment thread ──────────────────────────────────────────────
|
||||
// A composer on the left and the thread on the right, rather than the pair of bare
|
||||
// Bootstrap `col-6`s this used to be. A grid instead of a `.row`: with neither
|
||||
// column drawing a box, the space between them is the ONLY thing holding them
|
||||
// apart, and a `.row` cannot give a real gutter (it pads each child inwards, then
|
||||
// pulls the whole thing back out with negative margins). This gap plus each
|
||||
// panel's own side padding is what that space adds up to. One breakpoint is enough
|
||||
// to stack them on a narrow window.
|
||||
//
|
||||
// The height is pinned for the same reason the old `.comments` carried a fixed
|
||||
// 15rem: this accordion shares `.custom-content-body` (`calc(100vh - 19.5rem)`)
|
||||
// with the table below it, so a thread that grew with its content would eat the
|
||||
// table's rows one comment at a time. The thread scrolls inside that budget; the
|
||||
// composer keeps its natural size and its column absorbs the rest.
|
||||
.comments-container {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
|
||||
gap: 0.75rem;
|
||||
height: 15rem;
|
||||
margin-inline: 0;
|
||||
|
||||
& nb-list {
|
||||
@media (max-width: 991px) {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Flat: no fill and no ring of its own. Each panel is a REGION of the accordion
|
||||
// body, not a card sitting on it, and boxing the two of them inside a body that is
|
||||
// already a bordered surface put three edges between a comment and the page. What
|
||||
// separates them now is the column gap and the rule under each panel's own title.
|
||||
.comment-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
padding: 0.5rem 0.75rem 0.75rem;
|
||||
color: var(--gauzy-text-color-2);
|
||||
}
|
||||
|
||||
.comment-panel-header {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding-bottom: 0.625rem;
|
||||
margin-bottom: 0.875rem;
|
||||
border-bottom: thin solid var(--gauzy-overlay-border-color, rgba(126, 126, 143, 0.18));
|
||||
|
||||
nb-icon {
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--gauzy-text-color-2);
|
||||
}
|
||||
}
|
||||
|
||||
.comment-panel-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1rem;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--gauzy-text-color-1);
|
||||
}
|
||||
|
||||
// Comment count, on the far end of the thread's header line.
|
||||
.comment-count {
|
||||
@include nb-ltr(margin-left, auto);
|
||||
@include nb-rtl(margin-right, auto);
|
||||
min-width: 1.25rem;
|
||||
padding: 0.0625rem 0.375rem;
|
||||
border-radius: 999px;
|
||||
background: var(--gauzy-hover-tint, rgba(126, 126, 143, 0.12));
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
line-height: 1rem;
|
||||
text-align: center;
|
||||
color: var(--gauzy-text-color-2);
|
||||
}
|
||||
|
||||
// The composer's avatar column: the avatar itself and the gap between it and the
|
||||
// fields beside it. Declared once so the avatar's own box, the row's gap and the
|
||||
// centring nudge below cannot drift apart the next time the avatar is resized.
|
||||
$composer-avatar: 1.5rem;
|
||||
$composer-gap: 0.5rem;
|
||||
|
||||
// One avatar treatment for the composer and for every comment. The shared
|
||||
// `ngx-avatar` is deliberately NOT used here: it renders nothing at all when its
|
||||
// `src` is empty, so every author who never uploaded a photo left a hole where the
|
||||
// avatar belongs. `authorInitials()` in the component covers that case, and this
|
||||
// is the box both a photo and the initials are drawn in.
|
||||
.comment-avatar {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: $composer-avatar;
|
||||
height: $composer-avatar;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
background: var(--gauzy-active-tint, rgba(126, 126, 143, 0.2));
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: var(--gauzy-text-color-1);
|
||||
user-select: none;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
// The composer is a face beside a stack of fields — one flex row holding the
|
||||
// avatar and a flex COLUMN holding the subject and the comment box. That nesting
|
||||
// is what makes both fields start on the same line as each other, past the avatar,
|
||||
// without either of them having to carry a margin that guesses the avatar's width.
|
||||
.comment-form-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: $composer-gap;
|
||||
}
|
||||
|
||||
.comment-form-fields {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
// Centres the avatar on the subject field it opens beside — half the difference
|
||||
// between the two heights.
|
||||
.comment-form-row .comment-avatar {
|
||||
margin-top: calc((#{ga-overrides.$compact-control-height} - #{$composer-avatar}) / 2);
|
||||
}
|
||||
|
||||
// The subject is a field of its own beside the avatar, NOT a second row inside the
|
||||
// comment box: sharing that box's edge made the two read as one input that had
|
||||
// somehow split in two. So it gets the ordinary treatment — the same borderless,
|
||||
// hairline-ringed field the Search tab's controls use, at the same height.
|
||||
//
|
||||
// `input-appearance()` — pulled in by the `:host nb-accordion` dialog include
|
||||
// above — paints every field in this accordion `--gauzy-card-1`, borderless, on a
|
||||
// 42px floor, all as `!important`, so both the fill and the floor have to be
|
||||
// re-stated at that weight to be reachable at all.
|
||||
.comment-form-title[nbInput] {
|
||||
// `fullWidth` on the element resolves to Nebular's `.input-full-width` (0,1,0),
|
||||
// which this out-ranks: as a column child the field is stretched to the column's
|
||||
// width already, and a second 100% on top of that is what it does not need.
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
min-height: ga-overrides.$compact-control-height !important;
|
||||
height: ga-overrides.$compact-control-height;
|
||||
padding-block: 0;
|
||||
background-color: var(--gauzy-card-2) !important;
|
||||
border: none;
|
||||
box-shadow: ga-overrides.$default-box-shadow-inset;
|
||||
font-size: 0.75rem;
|
||||
|
||||
// The hairline is the field's only edge, so focus has to move it rather than add
|
||||
// to it — with the border gone, Nebular's own focus state has nothing to colour.
|
||||
&:focus {
|
||||
box-shadow: inset 0 0 0 1px nb-theme(color-primary-500);
|
||||
}
|
||||
}
|
||||
|
||||
// The comment box is ONE control, the way a chat input is: the box owns the
|
||||
// border, the fill and the focus ring, and the body and its buttons live inside
|
||||
// it. Previously the field drew its own outline and the buttons sat on the panel
|
||||
// below it, which read as two separate widgets stacked up.
|
||||
.comment-form-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.375rem 0.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--gauzy-card-2);
|
||||
box-shadow: ga-overrides.$default-box-shadow-inset;
|
||||
|
||||
// Focus lands on a field inside; the ring belongs on the box that draws the edge.
|
||||
&:focus-within {
|
||||
box-shadow: inset 0 0 0 1px nb-theme(color-primary-500);
|
||||
}
|
||||
|
||||
// The textarea has to disappear into the box: no fill, no edge of its own, no
|
||||
// inset padding (the box already provides it). `border: none` because Nebular
|
||||
// draws one on `[nbInput]` by default — inside the box that would be a second
|
||||
// line weight against the box's own.
|
||||
//
|
||||
// Replaces the file-wide `textarea { height: 85px; resize: none }` this page
|
||||
// used to carry for this one control.
|
||||
textarea[nbInput] {
|
||||
min-height: 3.5rem !important;
|
||||
height: 3.5rem;
|
||||
padding-block: 0.125rem;
|
||||
padding-inline: 0;
|
||||
background-color: transparent !important;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.45;
|
||||
resize: none;
|
||||
}
|
||||
}
|
||||
|
||||
// The toolbar row inside the box, along its bottom edge.
|
||||
.comment-form-actions {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.25rem;
|
||||
margin-top: 0.25rem;
|
||||
|
||||
// Qualified with `[nbButton]` so these out-rank Nebular's own `.size-tiny`
|
||||
// paddings (0,2,0) rather than tying with them; the file-wide
|
||||
// `button { margin: 5px }` is the only thing that would otherwise space them,
|
||||
// and the flex `gap` owns that here.
|
||||
button[nbButton] {
|
||||
margin: 0;
|
||||
gap: 0.25rem;
|
||||
padding: 0.1875rem 0.5rem;
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
& .col-6:nth-child(1) {
|
||||
padding-top: 15px;
|
||||
color: var(--gauzy-text-color-2);
|
||||
// Nebular gives an icon beside a label its own margin; the `gap` above is the
|
||||
// spacing, so that margin has to go.
|
||||
button[nbButton] nb-icon {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
& textarea {
|
||||
font-weight: 400;
|
||||
}
|
||||
.comment-thread .comments {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
// `scroll` reserved a scrollbar gutter on the History tab even when an invoice
|
||||
// has one comment or none, so the empty panel showed a dead scrollbar track.
|
||||
overflow-y: auto;
|
||||
// A trackpad flick that reaches the end of the thread should not then scroll the
|
||||
// page behind it.
|
||||
overscroll-behavior-y: contain;
|
||||
// Keeps the last character of a comment off the scrollbar.
|
||||
@include nb-ltr(padding-right, 0.25rem);
|
||||
@include nb-rtl(padding-left, 0.25rem);
|
||||
}
|
||||
|
||||
.comment-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.625rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
// One rule between comments and no fill behind them: the thread is a list of
|
||||
// entries on the panel, not a stack of cards, and a hairline separates two of them
|
||||
// with far less weight than a filled block each. The last entry drops its rule so
|
||||
// the list does not end on a line with nothing under it.
|
||||
.comment-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding-bottom: 0.625rem;
|
||||
border-bottom: thin solid var(--gauzy-overlay-border-color, rgba(126, 126, 143, 0.18));
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Smaller here than on the composer: in the thread the avatar identifies a line of
|
||||
// text, while on the composer it sits against a full-height input.
|
||||
.comment-item .comment-avatar {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
font-size: 0.5625rem;
|
||||
}
|
||||
|
||||
.comment-body {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.comment-byline {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
// The name, and where it is a link, a link that does not announce itself as one
|
||||
// until you reach for it — an underline on every name would turn the thread into a
|
||||
// column of blue rules.
|
||||
.comment-author {
|
||||
overflow: hidden;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.125rem;
|
||||
color: var(--gauzy-text-color-1);
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
a.comment-author {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
color: nb-theme(color-primary-500);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
// Relative age ("3 hours ago"), with the exact timestamp on the tooltip — the
|
||||
// trade every comment thread makes, and what lets the byline sit on one line
|
||||
// instead of the three stacked rows this used to be.
|
||||
.comment-time {
|
||||
flex: 0 0 auto;
|
||||
@include nb-ltr(margin-left, auto);
|
||||
@include nb-rtl(margin-right, auto);
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.125rem;
|
||||
color: var(--text-hint-color);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
// The line under the name, the way a thread puts an author's subtitle there: one
|
||||
// step down in size and weight from the name, and muted, so the eye goes name →
|
||||
// comment and picks this up only on the way past. It carried the name's own weight
|
||||
// before, which made every entry read as two headings stacked.
|
||||
.comment-subject {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.125rem;
|
||||
color: var(--text-hint-color);
|
||||
overflow-wrap: anywhere;
|
||||
|
||||
&::first-letter {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
// `pre-wrap` so a comment keeps the line breaks its author typed — the old single
|
||||
// `<span>` collapsed them into one run — and `overflow-wrap` so an unbroken URL or
|
||||
// reference number cannot push the entry wider than its column.
|
||||
.comment-text {
|
||||
margin: 0.3125rem 0 0;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.45;
|
||||
color: var(--gauzy-text-color-1);
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
|
||||
&::first-letter {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
// Both panels' resting state: no invoice picked yet (composer) and no comments on
|
||||
// the one that is (thread). The old markup rendered nothing at all in either case,
|
||||
// so the History tab opened as two empty boxes with nothing to say which.
|
||||
.comment-empty {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
padding: 1rem 0.75rem;
|
||||
text-align: center;
|
||||
color: var(--text-hint-color);
|
||||
|
||||
nb-icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-bottom: 0.125rem;
|
||||
font-size: 1.25rem;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
& .col-6:nth-child(2) {
|
||||
background: var(--gauzy-card-1);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 15px 6px 0 15px;
|
||||
span {
|
||||
max-width: 32ch;
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.125rem;
|
||||
}
|
||||
|
||||
.comment-empty-title {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.25rem;
|
||||
color: var(--gauzy-text-color-2);
|
||||
|
||||
& nb-list-item {
|
||||
padding-left: 0;
|
||||
position: relative;
|
||||
|
||||
& .history-item.history-date {
|
||||
color: var(--text-hint-color);
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
& .history-item.history-title {
|
||||
margin-top: 10px;
|
||||
text-transform: capitalize;
|
||||
font-weight: bold;
|
||||
}
|
||||
& .history-item.history-comment {
|
||||
margin-top: 6px;
|
||||
font-size: 0.85rem;
|
||||
&.history-comment::first-letter {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// border bottom
|
||||
& nb-list-item::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100%;
|
||||
border-bottom: thin solid var(--accordion-header-border-color);
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -659,12 +1113,45 @@ nb-tab {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
// Submit / reset sit in a bare `.row`; the file-wide `button { margin: 5px }`
|
||||
// is the only thing spacing them.
|
||||
button {
|
||||
// The file-wide `button { margin: 5px }` near the top was the only thing
|
||||
// spacing anything in this form.
|
||||
margin: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
// ── Submit / reset ───────────────────────────────────────────────────────
|
||||
// The two used to be `<div>`-wrapped items in a bare `.row`, left-aligned
|
||||
// under the first filter with their leading gap coming from an `ml-3` on the
|
||||
// submit button — so they read as a third row of fields rather than as the
|
||||
// form's actions. The wrappers are gone (the buttons are the flex items now)
|
||||
// and the pair closes the form off against the same edge the filter row ends
|
||||
// on.
|
||||
.search-actions {
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
// `.row`'s own negative gutters, which would hang the buttons past the
|
||||
// accordion's padding on the right.
|
||||
margin-inline: 0;
|
||||
// Clear of the filter row above — with the fields' own 0.25rem underneath
|
||||
// them that comes to 0.75rem: enough to read as the form's footer without
|
||||
// spending height the table below is short of.
|
||||
margin-top: 0.5rem;
|
||||
gap: 0.375rem;
|
||||
|
||||
// The compact button scale the page header already runs on this page (see
|
||||
// the `page-header-buttons()` includes further up), instead of Nebular's
|
||||
// stock `small` box — that is what takes the size down, and it puts the
|
||||
// form's actions on the same scale as the toolbar above the table rather
|
||||
// than a step larger than everything around them.
|
||||
@include ga-hub.page-header-buttons();
|
||||
|
||||
button {
|
||||
// The row's `gap` spaces them now, so neither the file-wide 5px nor the
|
||||
// 0.25rem above it should still be adding to the ends of the row.
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -738,13 +1225,167 @@ nb-tab {
|
||||
padding: 0.375rem 0.5rem;
|
||||
}
|
||||
|
||||
// ── Search form: controls sized to their content ─────────────────────────────
|
||||
// The fields are laid out on a Bootstrap grid (`col-sm-4` / `-3` / `-2`) and every
|
||||
// control carries `fullWidth`, so each one stretched to a third or a quarter of
|
||||
// the card — a number field nearly 500px wide for a 6-digit estimate number.
|
||||
// Capping the COLUMN rather than the control keeps the grid's wrapping and order
|
||||
// intact (the cap only bites where the percentage is the larger of the two) and
|
||||
// carries the label above the field with it.
|
||||
:host .search-tab nb-accordion .row > [class*='col-'] {
|
||||
max-width: 14rem;
|
||||
// ── Search form: all eight filters on one line ───────────────────────────────
|
||||
// The fields used to sit on two Bootstrap rows (`col-sm-4` / `-3` / `-2`) with
|
||||
// every control carrying `fullWidth`, so each stretched to a third or a quarter
|
||||
// of the card — a number field nearly 500px wide for a 6-digit estimate number —
|
||||
// and the form stood two field-rows tall above a table that is already short on
|
||||
// height. They are now one `.search-filters` row: equal-width flex columns that
|
||||
// are allowed to shrink, which is what fits all eight on a single line instead of
|
||||
// letting Bootstrap's percentage widths wrap them.
|
||||
:host .search-tab nb-accordion .search-filters {
|
||||
flex-wrap: nowrap;
|
||||
// Tops aligned, so the labels — and therefore the controls under them — share
|
||||
// one baseline across the row.
|
||||
align-items: flex-start;
|
||||
margin-inline: 0;
|
||||
// Replaces the columns' own 0.75rem gutters, halved: eight fields on one line
|
||||
// are better off spending that width on the controls.
|
||||
gap: 0.5rem;
|
||||
|
||||
> .search-filter-col {
|
||||
// `flex-basis: 0`, not `auto`, so the eight share the line evenly rather than
|
||||
// each claiming its content width.
|
||||
flex: 1 1 0;
|
||||
// A flex item's floor is `min-content` by default, and for a number input or
|
||||
// a select that is wider than an eighth of the card — without this the row
|
||||
// overflows the accordion instead of shrinking.
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
// ── The one control that did not obey that ─────────────────────────────────
|
||||
// Nebular's own `nb-select-theme()` floors every trigger at
|
||||
// `nb-theme(select-min-width)`, which resolves to 6rem in all eight registered
|
||||
// themes (see the note on `.select-nb` higher up for why that includes the two
|
||||
// the app ships on). Eight of those side by side need 8 x 6rem + 7 x 0.5rem =
|
||||
// 824px of ROW before a column is wide enough to hold one — well past the point
|
||||
// where the card itself is 824px wide, and far past the 768px at which this row
|
||||
// is allowed to wrap. Between the two, the Status trigger kept a floor its
|
||||
// column could not give it and hung out past the accordion, taking the fields
|
||||
// beside it with it.
|
||||
//
|
||||
// Unset rather than lowered: the premise of the row is that the eight columns
|
||||
// divide the line evenly and shrink together, and a floor on one of them is the
|
||||
// one thing that cannot hold. This is the same treatment `_overrides.scss`
|
||||
// applies to selects inside `.card-scroll`. `!important` because that same file
|
||||
// restates the floor with one of its own under `.nb-theme-default`; under every
|
||||
// other theme the specificity here would have been enough on its own.
|
||||
::ng-deep nb-select .select-button {
|
||||
min-width: 0 !important;
|
||||
|
||||
// Nebular gives the button `text-overflow: ellipsis`, but `[nbButton]` is an
|
||||
// `inline-flex` container, so that applies to nothing — the label is a child
|
||||
// `<span>`, and a flex item will not shrink below its content unless it is
|
||||
// told it may. Without the pair, a status name at an eighth of the card is
|
||||
// cut off mid-glyph by the button's own `overflow: hidden`. Same treatment
|
||||
// the `ng-select` triggers in this row get below.
|
||||
> span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
// ── The labels, so the controls under them start at one height ─────────────
|
||||
// Every label in the row is pinned to the same type size, line box and bottom
|
||||
// margin. Two of the eight are drawn by child components (`ga-currency`,
|
||||
// `ga-tags-color-input`), which the `.label` rule higher up in this file —
|
||||
// scoped, no `::ng-deep` — never reached: those two kept Nebular's default
|
||||
// 0.5rem gap under the label while the six from this template had 0.1875rem,
|
||||
// so their inputs sat several pixels lower than the rest. That, and not the
|
||||
// grid, is what stopped the fields lining up.
|
||||
//
|
||||
// The single-line clamp is the other half: the labels differ a lot in length
|
||||
// ("Currency" against "Organization contact"), and at an eighth of the card the
|
||||
// long ones wrapped to a second line and pushed their input a whole line down.
|
||||
::ng-deep .label {
|
||||
display: block;
|
||||
margin-bottom: 0.1875rem;
|
||||
font-size: nb-theme(text-label-font-size);
|
||||
line-height: 0.8125rem;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
// Same reach: `ga-currency` renders its own `.form-group` wrapper.
|
||||
::ng-deep .form-group {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
// ── One type size across the row ───────────────────────────────────────────
|
||||
// The eight filters are four different widgets — a plain `nbInput`, two
|
||||
// datepicker inputs, three `ng-select`s (contact, currency, tags) and the
|
||||
// `nb-select` for status — and each arrives with its own size: `ng-select`
|
||||
// reads `select-medium-text-font-size` from `ng-select-overrides`, a selected
|
||||
// tag is an `nb-badge` at `text-caption-font-size`, and a bare input inherits
|
||||
// the paragraph size. On two rows that was easy to miss; on one line it reads
|
||||
// as four different fields.
|
||||
//
|
||||
// Status is the one that was already right (`#inputStatus .select-button`
|
||||
// higher up in this file), so its 0.75rem is the row's size. Set on each text
|
||||
// surface rather than left to inherit, because the library sheets put a size
|
||||
// on the descendants themselves.
|
||||
::ng-deep {
|
||||
input,
|
||||
input::placeholder,
|
||||
.ng-select,
|
||||
.ng-select .ng-select-container,
|
||||
.ng-select .ng-value-container,
|
||||
.ng-select .ng-value,
|
||||
.ng-select .ng-value-label,
|
||||
.ng-select .ng-placeholder,
|
||||
.ng-select .ng-input > input,
|
||||
nb-select .select-button,
|
||||
nb-select .select-button.placeholder,
|
||||
nb-tag-list input,
|
||||
.tag-label,
|
||||
.tag-overflow {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
// At an eighth of the card a contact or currency name is routinely longer
|
||||
// than its trigger. `ng-select`'s own container clips it, which cuts the text
|
||||
// mid-glyph; a flex item will not shrink below its content unless told it
|
||||
// may, so both halves are needed for an ellipsis to engage instead.
|
||||
.ng-select.ng-select-single .ng-select-container .ng-value-container {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ng-select.ng-select-single .ng-select-container .ng-value {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
// The two date fields park a calendar glyph inside the control (`.input-icon
|
||||
// .icon`). At full width the placeholder never reached it; at an eighth of the
|
||||
// card it runs underneath, so the glyph is pulled in tight against the edge and
|
||||
// the text gets the room that buys.
|
||||
.input-date {
|
||||
padding-inline-end: 1.5rem;
|
||||
}
|
||||
|
||||
.input-icon .icon {
|
||||
@include nb-ltr(right, 0.5rem);
|
||||
@include nb-rtl(left, 0.5rem);
|
||||
}
|
||||
}
|
||||
|
||||
// One line is the point of the row, so it holds for every desktop width — the
|
||||
// columns shrink instead of wrapping. Below the tablet breakpoint eight controls
|
||||
// on a line stop being controls at all, so there the row wraps in the same order,
|
||||
// a few filters per line.
|
||||
@media (max-width: 767.98px) {
|
||||
:host .search-tab nb-accordion .search-filters {
|
||||
flex-wrap: wrap;
|
||||
|
||||
> .search-filter-col {
|
||||
flex: 1 1 9rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
PermissionsEnum,
|
||||
ICurrency,
|
||||
IInvoiceItemCreateInput,
|
||||
IUser,
|
||||
InvoiceTabsEnum,
|
||||
DiscountTaxTypeEnum,
|
||||
IDateRangePicker
|
||||
@@ -796,6 +797,80 @@ export class InvoicesComponent extends PaginationFilterBaseComponent implements
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The user the History tab composes comments as. Read through a getter rather
|
||||
* than a field so the avatar follows a mid-session user switch, and so the
|
||||
* template does not have to reach into the private `store`.
|
||||
*/
|
||||
get currentUser(): IUser {
|
||||
return this.store.user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a comment author's name should be a link to their profile.
|
||||
*
|
||||
* `/pages/users/edit/:id` is guarded by `ORG_USERS_EDIT`, and its guard
|
||||
* redirects to the dashboard rather than refusing — so without this check a
|
||||
* viewer who lacks the permission would click a name and silently land on a
|
||||
* different page. They get plain text instead.
|
||||
*/
|
||||
get canOpenUserProfile(): boolean {
|
||||
return this.ngxPermissionsService.getPermission(PermissionsEnum.ORG_USERS_EDIT) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initials fallback for a comment author with no `imageUrl`. The shared
|
||||
* `ngx-avatar` renders nothing at all when its `src` is empty, which left a
|
||||
* hole where the avatar should be for every user who never uploaded a photo —
|
||||
* the common case on a fresh workspace.
|
||||
*
|
||||
* @param name - the author's display name
|
||||
* @returns up to two upper-cased initials, or `?` when there is no name
|
||||
*/
|
||||
authorInitials(name: string): string {
|
||||
const initials = (name || '')
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((part: string) => part.charAt(0))
|
||||
.join('');
|
||||
return initials ? initials.toUpperCase() : '?';
|
||||
}
|
||||
|
||||
/**
|
||||
* Relative age of a comment ("3 hours ago"), the way every comment thread
|
||||
* dates its entries. The exact timestamp stays available on the tooltip, so
|
||||
* nothing is lost by not printing it inline.
|
||||
*
|
||||
* `createdAt` arrives here as the `Date.toString()` slice built in
|
||||
* `selectInvoice`, not as an ISO string, so it goes through `new Date` first
|
||||
* rather than moment's (deprecated) free-form string parser.
|
||||
*
|
||||
* A record with no usable date renders nothing rather than moment's literal
|
||||
* "Invalid date": the line is a subtitle beside the author's name, and a blank
|
||||
* one reads as "no timestamp" while that string reads as a broken comment. The
|
||||
* tooltip beside it is already empty in the same case.
|
||||
*
|
||||
* @param createdAt - the history record's creation date
|
||||
* @returns a humanized, locale-aware distance from now, or an empty string
|
||||
*/
|
||||
commentTimeAgo(createdAt: string | Date): string {
|
||||
const parsed = new Date(createdAt);
|
||||
return isNaN(parsed.getTime()) ? '' : moment(parsed).fromNow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the comment composer without submitting it.
|
||||
*
|
||||
* @param historyFormDirective - the composer's `ngForm`, reset alongside the
|
||||
* form group so the controls drop their touched/dirty state too
|
||||
*/
|
||||
resetComment(historyFormDirective): void {
|
||||
historyFormDirective.resetForm();
|
||||
this.historyForm.reset();
|
||||
}
|
||||
|
||||
async addComment(historyFormDirective) {
|
||||
if (this.historyForm.invalid) {
|
||||
return;
|
||||
@@ -806,8 +881,7 @@ export class InvoicesComponent extends PaginationFilterBaseComponent implements
|
||||
const action = comment;
|
||||
await this.createInvoiceHistory(action, title);
|
||||
|
||||
historyFormDirective.resetForm();
|
||||
this.historyForm.reset();
|
||||
this.resetComment(historyFormDirective);
|
||||
|
||||
const invoice = await this.invoicesService.getById(invoiceId, [
|
||||
'invoiceItems',
|
||||
@@ -901,6 +975,11 @@ export class InvoicesComponent extends PaginationFilterBaseComponent implements
|
||||
return +new Date(b.createdAt) - +new Date(a.createdAt);
|
||||
});
|
||||
this.histories = histories;
|
||||
} else {
|
||||
// Deselecting used to leave the previous invoice's comments standing in
|
||||
// the History tab. The thread is now guarded on `selectedInvoice`, but
|
||||
// dropping the records keeps the count badge honest either way.
|
||||
this.histories = [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1,10 @@
|
||||
@forward '@shared/_pg-card';
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// Density tokens, the table surface, row hover/selection, the tag-chip stack,
|
||||
// the status pills, the pager's `<nav><ul><li>` and the toolbar scale — the same
|
||||
// block Invoices carries. The action row here is `ngx-gauzy-button-action`
|
||||
// inside `nb-card-header`, which the default `$actions` selector covers: the
|
||||
// tokens are custom properties, so they cross into that component on their own.
|
||||
@include ga-hub.list-page();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@forward '@shared/_pg-card';
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
:host {
|
||||
.card-header-title {
|
||||
@@ -15,3 +16,10 @@
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// The same block the Pipelines list this page drills into already carries:
|
||||
// density tokens, the table surface, row hover/selection, the tag-chip stack,
|
||||
// the status pills, the pager and the toolbar scale. Without it a pipeline's
|
||||
// deals were drawn a size larger than the pipeline list one click behind them.
|
||||
@include ga-hub.list-page();
|
||||
|
||||
@@ -185,8 +185,15 @@ export class PipelinesComponent extends PaginationFilterBaseComponent implements
|
||||
* Sets up the initial view configuration and subscribes to changes in the component layout.
|
||||
*/
|
||||
setView() {
|
||||
// Set the default view component name to PROPOSALS
|
||||
this.viewComponentName = ComponentEnum.PROPOSALS;
|
||||
// `PIPELINES`, not `PROPOSALS`. This page registered itself under the
|
||||
// Proposals key, and `job-proposal-ui`'s own proposal page registers the
|
||||
// SAME key — so the two shared one stored layout preference. Setting
|
||||
// Proposals to the card view silently switched Pipelines to `ga-card-grid`
|
||||
// as well, which is why this page could render as cards while every style
|
||||
// written for its table was correct and simply never applied; toggling the
|
||||
// layout on either page also moved the other. `ComponentEnum.PIPELINES` was
|
||||
// declared for this page and, until now, referenced nowhere.
|
||||
this.viewComponentName = ComponentEnum.PIPELINES;
|
||||
|
||||
// Subscribe to changes in the component layout
|
||||
this.store
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
@for (stage of pipelineStages; track stage) {
|
||||
<div>
|
||||
<div>{{stage.name}}</div>
|
||||
<div>{{stage.description}}</div>
|
||||
<!--
|
||||
Only when there is one, and `trim()` because a blank string is not one. Most
|
||||
stages carry no description, and an element emitted unconditionally is an
|
||||
empty flex item holding the chip's gap open on its trailing edge — a chip
|
||||
wider than its own text. A whitespace-only value looks identical on screen
|
||||
and is truthy, so a bare `@if` would let exactly that case back through.
|
||||
Dropping the node is what the stylesheet's `:nth-child(2)` description rule
|
||||
expects; see the note there.
|
||||
-->
|
||||
@if (stage.description?.trim()) {
|
||||
<div>{{stage.description}}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,18 +1,81 @@
|
||||
div > div {
|
||||
&:first-child {
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 15px;
|
||||
letter-spacing: 0em;
|
||||
text-align: left;
|
||||
}
|
||||
&:last-child {
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 15px;
|
||||
letter-spacing: 0em;
|
||||
text-align: left;
|
||||
}
|
||||
// ── A pipeline's stages, as chips on one line box ────────────────────────────
|
||||
//
|
||||
// WHAT THIS REPLACES: a bare `div > div` block setting 12px/15px on each of the
|
||||
// two lines the template emits per stage — a name line and a description line —
|
||||
// with the stages themselves left as plain blocks, stacked. So the cell cost the
|
||||
// row 30px PER STAGE and nothing capped it: a pipeline with five stages made a
|
||||
// 150px row, in a table whose other three columns are a string, a string and a
|
||||
// 1rem status pill. It was the one cell on this page that read no density tokens
|
||||
// at all, and the only reason a Pipelines row did not measure what an Invoices
|
||||
// row does.
|
||||
//
|
||||
// The treatment is the app's existing answer to "several small labels in one
|
||||
// cell" — the tag chips `ga-notes-with-tags` renders — rather than anything new:
|
||||
// the same `--gauzy-table-chip-*` scale, the same `--gauzy-table-badge-*` box,
|
||||
// so a stage chip and the status pill in the very next column are drawn alike.
|
||||
//
|
||||
// Chips WRAP rather than truncating to one line with an overflow count (the
|
||||
// `ngx-people-list` treatment). A pipeline's stages are the payload of this
|
||||
// column — the thing the row is read for — so a tall-ish cell on a pipeline with
|
||||
// many stages is the honest outcome; what is fixed is that each stage now costs
|
||||
// one chip instead of two full-size text lines.
|
||||
//
|
||||
// Plain custom properties with literal fallbacks, no `@use 'themes'`: this sheet
|
||||
// had no imports and needs none, and the tokens reach it by inheritance from the
|
||||
// page's `:host` the same way every other cell renderer here is sized.
|
||||
:host {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--gauzy-table-chip-gap, 0.125rem);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// One stage. `inline-flex` with a baseline cross-axis so the name and its
|
||||
// description sit on the same line inside the chip instead of stacking.
|
||||
:host > div {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 0.25rem;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
min-height: var(--gauzy-table-badge-height, 1rem);
|
||||
padding-block: var(--gauzy-table-badge-padding-y, 0.1875rem);
|
||||
padding-inline: var(--gauzy-table-chip-padding-x, 0.3125rem);
|
||||
border-radius: var(--gauzy-table-badge-radius, 0.25rem);
|
||||
background: var(--gauzy-sidebar-background-3, rgba(126, 126, 143, 0.1));
|
||||
}
|
||||
|
||||
// The stage name, and the description after it. Both truncate on WIDTH, so a
|
||||
// short one renders in full and only a genuinely long one picks up an ellipsis —
|
||||
// a stage name is short and a description is not, and it is the description that
|
||||
// would otherwise push the chip past the column.
|
||||
:host > div > div {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-size: var(--gauzy-table-chip-font-size, 0.625rem);
|
||||
line-height: var(--gauzy-table-chip-line-height, 0.75rem);
|
||||
letter-spacing: 0em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
:host > div > div:first-child {
|
||||
flex: 0 1 auto;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// `:nth-child(2)`, not `:last-child`. The template now emits the description
|
||||
// element only when the stage has a non-blank one (a `display: none` on `:empty`
|
||||
// used to do that job, which leant on a zero-length interpolation text node still
|
||||
// counting as empty and could not catch a whitespace-only description at all).
|
||||
// With the node genuinely absent, the NAME is both first and last child, and a
|
||||
// `:last-child` rule at this same specificity would come after the `:first-child`
|
||||
// one above and paint every description-less stage muted and unbolded.
|
||||
:host > div > div:nth-child(2) {
|
||||
flex: 1 1 auto;
|
||||
max-width: 12rem;
|
||||
font-weight: 400;
|
||||
color: var(--gauzy-text-color-2, var(--text-hint-color));
|
||||
}
|
||||
|
||||
@@ -38,25 +38,40 @@
|
||||
// attributes and a class before anything else, which clears that outright — no
|
||||
// `!important` anywhere below.
|
||||
:host .table-scroll-container ::ng-deep angular2-smart-table {
|
||||
// ── Project cell ───────────────────────────────────────────────────────────
|
||||
// `gauzy-project-organization` draws a 28px logo beside a 14px/17px name and an
|
||||
// 11px/13px caption, i.e. a 28px chip in rows the density tokens build to a
|
||||
// 1rem line box — the single biggest reason a row here stood taller than the
|
||||
// same row on Employees or Payments. Sized off the shared `gauzy-people-*`
|
||||
// tokens, so the project chip matches the people cells beside it.
|
||||
// ── Project and Contact cells ──────────────────────────────────────────────
|
||||
// Two blocks used to sit here, shrinking `gauzy-project-organization`'s logo
|
||||
// to 1rem and `ngx-contact-links`' avatar and pill to about 20px of chip, so
|
||||
// that each cost the row a single 1rem line box.
|
||||
//
|
||||
// Both are gone. `ngx-contact-links` is the very renderer the Invoices Contact
|
||||
// column draws — an 18px avatar and a 12px/15px name in a pill padded 5px, a
|
||||
// 28px chip — so overriding it here was this page setting itself BELOW the
|
||||
// table it is meant to match, using the identical component. Left alone it is
|
||||
// now exact by construction.
|
||||
//
|
||||
// The project logo comes back to the `gauzy-people-*` scale rather than to its
|
||||
// own 28px: those tokens are the restored 1.25rem/0.75rem, which puts the logo
|
||||
// and the name on the same line box as the people chip in the next column.
|
||||
.project-render {
|
||||
gap: var(--gauzy-people-gap, 0.375rem);
|
||||
gap: var(--gauzy-people-gap, 0.5rem);
|
||||
|
||||
img {
|
||||
width: var(--gauzy-people-avatar-size, 1rem);
|
||||
height: var(--gauzy-people-avatar-size, 1rem);
|
||||
// A 35 %-black drop shadow under a 16px thumbnail is a smudge, not a lift.
|
||||
width: var(--gauzy-people-avatar-size, 1.25rem);
|
||||
height: var(--gauzy-people-avatar-size, 1.25rem);
|
||||
// A 35 %-black drop shadow under a small thumbnail is a smudge, not a lift.
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: var(--gauzy-people-font-size, 0.6875rem);
|
||||
line-height: var(--gauzy-people-avatar-size, 1rem);
|
||||
font-size: var(--gauzy-people-font-size, 0.75rem);
|
||||
// The TABLE line box, not the avatar size. The name is one line of a stack,
|
||||
// not a line box that has to clear an avatar beside it: a project with a
|
||||
// member count puts this over `.member`, so a 1.25rem name and a 0.75rem
|
||||
// caption add up to 2rem of cell — taller than the 28px `ngx-contact-links`
|
||||
// chip in the next column, which is the row height this page is being
|
||||
// brought to. At 1rem the pair comes to exactly that 28px, and the logo
|
||||
// beside them still gets its own 1.25rem box.
|
||||
line-height: var(--gauzy-table-line-height, 1rem);
|
||||
}
|
||||
|
||||
span .member {
|
||||
@@ -65,38 +80,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ── Contact cell ───────────────────────────────────────────────────────────
|
||||
// `ngx-contact-links` draws an 18px avatar and 12px/15px text inside a pill
|
||||
// padded 5px/14px — a 28px control in a 1rem row. Same scale-down as the
|
||||
// people chips, and the pill keeps its tint so the cell still reads as a link
|
||||
// rather than as plain text.
|
||||
ngx-contact-links .inner-wrapper {
|
||||
gap: var(--gauzy-people-gap, 0.375rem);
|
||||
@include nb-ltr(padding, 0.125rem 0.375rem 0.125rem 0.125rem);
|
||||
@include nb-rtl(padding, 0.125rem 0.125rem 0.125rem 0.375rem);
|
||||
|
||||
.names-wrapper {
|
||||
max-width: var(--gauzy-people-name-max-width, 8rem);
|
||||
}
|
||||
|
||||
.link-text {
|
||||
font-size: var(--gauzy-people-font-size, 0.6875rem);
|
||||
line-height: var(--gauzy-people-avatar-size, 1rem);
|
||||
}
|
||||
|
||||
.avatar img,
|
||||
.prefix {
|
||||
width: var(--gauzy-people-avatar-size, 1rem);
|
||||
height: var(--gauzy-people-avatar-size, 1rem);
|
||||
}
|
||||
|
||||
.prefix {
|
||||
padding: 0;
|
||||
font-size: var(--gauzy-people-initials-font-size, 0.5rem);
|
||||
line-height: var(--gauzy-people-avatar-size, 1rem);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Public / Private cell ──────────────────────────────────────────────────
|
||||
// The cell is an `nb-toggle`, which Nebular sizes entirely from theme tokens:
|
||||
// a 50x30 switch with a 28px knob and a 0.8125rem label on a 1.5rem line — the
|
||||
|
||||
+149
-2
@@ -1,4 +1,5 @@
|
||||
@use 'gauzy/_gauzy-table' as *;
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
// Removed `.setting-name` (font-size 24px / bold) and `.body-header` (flex row,
|
||||
// margin-bottom 35px). This stylesheet is emitted for exactly two templates —
|
||||
@@ -103,8 +104,10 @@
|
||||
// `.setting-name` and `.body-header`, matched nothing in either template that
|
||||
// uses this stylesheet.
|
||||
//
|
||||
// The rows are laid out with fixed column widths (~57rem), so this really does
|
||||
// need to scroll sideways on a narrow viewport; what it does NOT need is
|
||||
// The rows are laid out with fixed column widths — about 41rem for the employee
|
||||
// list and 28rem for the organization one, off the `--gauzy-recurring-col*`
|
||||
// properties at the foot of this file — so this can still need to scroll
|
||||
// sideways on a narrow viewport; what it does NOT need is
|
||||
// `overflow-x: scroll`, which reserves a scrollbar gutter unconditionally. With
|
||||
// `auto` the bar only exists when there is something to scroll, and the two
|
||||
// hide-the-bar declarations now cover Firefox as well as WebKit rather than
|
||||
@@ -130,3 +133,147 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
// ── The list-page contract ───────────────────────────────────────────────────
|
||||
// Both Recurring Expenses pages take this block: Employees > Recurring Expenses
|
||||
// directly, and Organization > Recurring Expenses through the `@forward` in
|
||||
// `expense-recurring.component.scss`. The two templates differ by one column and
|
||||
// are otherwise the same markup.
|
||||
//
|
||||
// That markup is a list built BY HAND — a `.sub-header` band of column labels
|
||||
// over a `.table-scroll` of `ga-recurring-expense-block` rows — so neither
|
||||
// `list-page()` (anchored on `angular2-smart-table`) nor `card-list-page()`
|
||||
// (anchored on `.table-scroll-container`) ever reached it. What that left: 84px
|
||||
// rows, each a separately floating card with its own fill, radius and 8px bottom
|
||||
// margin, 14px values under a 0.75rem-padded `gauzy-card-3` label slab, and a
|
||||
// toolbar at full size — the last two lists in these sections still drawn the
|
||||
// way the whole app was before the density pass.
|
||||
//
|
||||
// The row component is styled from its own sheet rather than reached with
|
||||
// `::ng-deep` from here: `ga-recurring-expense-block` has exactly two consumers
|
||||
// and they are the two pages this file serves, so there is no third caller for a
|
||||
// page-scoped override to protect. It reads the density tokens below, which
|
||||
// inherit into it — the same way `ngx-people-list` and `ngx-created-by-user`
|
||||
// already size themselves.
|
||||
|
||||
:host {
|
||||
@include ga-hub.density-tokens();
|
||||
|
||||
// ── The three metrics the label band and the rows MUST agree on ────────────
|
||||
// They are custom properties, and on `:host`, for one reason: the labels are
|
||||
// in this component's template and the values are in
|
||||
// `recurring-expense-block.component.scss`, so a literal would have to be
|
||||
// written twice in two files and kept in step by hand. That is exactly how the
|
||||
// page arrived here — `42px`, `11rem` and `17.5rem` were each spelled out in
|
||||
// both places.
|
||||
//
|
||||
// The inset was 42px, room for a selection rail and a settings fly-out; the
|
||||
// rail is 3px now (see below) and the fly-out is `display: none`.
|
||||
--gauzy-recurring-row-inset: 0.5rem;
|
||||
--gauzy-recurring-col: 8rem;
|
||||
--gauzy-recurring-col-big: 12rem;
|
||||
}
|
||||
|
||||
:host nb-card-header {
|
||||
@include ga-hub.page-header-buttons();
|
||||
}
|
||||
|
||||
// ── One surface, around the labels AND the rows ──────────────────────────────
|
||||
// On `.container` rather than on `.table-scroll`, which is the only interesting
|
||||
// choice here: the label band is a `thead` in everything but markup, and a
|
||||
// surface that started below it would have left the header outside the frame it
|
||||
// labels. `.container` is the one element that wraps both.
|
||||
//
|
||||
// `.table-scroll` inside it keeps the scrolling, in both axes, exactly as before;
|
||||
// the ring is on the box around it, so it stays put rather than scrolling with
|
||||
// the rows.
|
||||
:host .container {
|
||||
@include ga-hub.surface();
|
||||
|
||||
// A flex column, so the list below can take exactly the room this surface has
|
||||
// rather than guessing at it — see the note on `.table-scroll`.
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
// ── The label band, as the table header it already is ────────────────────────
|
||||
// It was a `gauzy-card-3` slab with a radius and 0.75rem of padding, i.e. a
|
||||
// second surface sitting on the first, with labels at the body size. Now: the
|
||||
// header type scale, the header padding, and the hairline that separates a
|
||||
// `thead` from the rows under it.
|
||||
//
|
||||
// The transparent 3px leading edge matches the selection rail the rows reserve,
|
||||
// so a label and the value under it start at the same x — the alignment the old
|
||||
// pair of hand-matched 18px / 42px insets was trying to hold.
|
||||
:host .sub-header.header-content {
|
||||
// The fixed part of the column: a `thead` is not what should give way when the
|
||||
// card is short.
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
padding: var(--gauzy-table-header-padding-y, 0.3125rem) var(--gauzy-table-header-padding-x, 0.4375rem);
|
||||
border-inline-start: 3px solid transparent;
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
box-shadow: inset 0 -1px 0 0 var(--gauzy-overlay-border-color, rgba(126, 126, 143, 0.18));
|
||||
font-size: var(--gauzy-table-header-font-size, 0.75rem);
|
||||
font-weight: 600;
|
||||
line-height: var(--gauzy-table-header-line-height, 0.8125rem);
|
||||
color: var(--gauzy-text-color-2, var(--text-hint-color, rgba(113, 113, 122, 1)));
|
||||
}
|
||||
|
||||
// SPECIFICITY: the rules these replace are `[dir=ltr]` / `[dir=rtl]` pairs at
|
||||
// (0,3,0), so naming both classes on the band is what carries these past them.
|
||||
:host .sub-header.header-content .header-info {
|
||||
@include nb-ltr(padding-left, var(--gauzy-recurring-row-inset));
|
||||
@include nb-rtl(padding-right, var(--gauzy-recurring-row-inset));
|
||||
}
|
||||
|
||||
:host .sub-header.header-content .block .block-item {
|
||||
width: var(--gauzy-recurring-col);
|
||||
}
|
||||
|
||||
:host .sub-header.header-content .block .block-item-big {
|
||||
width: var(--gauzy-recurring-col-big);
|
||||
}
|
||||
|
||||
// ── The scroll port ──────────────────────────────────────────────────────────
|
||||
// The 18px leading pad was there to line the cards up under the label band's own
|
||||
// 18px margin; both are gone, and the rows now carry their inset themselves.
|
||||
//
|
||||
// `height: calc(100vh - 23.5rem)` goes with them. That number was an estimate of
|
||||
// the chrome above this list, and the label band is a good deal shorter than it
|
||||
// was when the estimate was taken — the list would now stop several rem short of
|
||||
// the surface's bottom edge, leaving a band of empty ring under the last row. As
|
||||
// a flex child it takes exactly the room the surface has, which is both accurate
|
||||
// and immune to the next change above it; `min-height: 0` because a flex child
|
||||
// that is itself a scroll port defaults to `min-height: auto` and would
|
||||
// otherwise refuse to shrink below its content.
|
||||
:host .table-scroll {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
// The Employee column's avatar chip, at the Invoices scale. `ngx-avatar` pins a
|
||||
// 20px image inside a pill padded 3px — 26px of chip, 2px short of the 28px the
|
||||
// Invoices Contact column costs its row — so only that inset is restated; the
|
||||
// image and the name are the renderer's own.
|
||||
//
|
||||
// Reached through `.table-scroll` for the specificity: the avatar nests its
|
||||
// rules under `:host-context(.report-table)`, which shims to (0,4,0), and that
|
||||
// wrapper — this component's own element, so it carries this component's content
|
||||
// attribute — is what takes the block past it without `!important`.
|
||||
:host .table-scroll ::ng-deep {
|
||||
@include ga-hub.avatar-chip();
|
||||
}
|
||||
|
||||
// The empty state stands in for the list INSIDE that frame now, rather than
|
||||
// floating on the card body, so it must not be taller than the box framing it.
|
||||
// `max-height: 100%` bounds it wherever the parent has a definite height and
|
||||
// resolves to `none` where it does not — the same treatment `.card-scroll` gets
|
||||
// in `styles/_overrides.scss`.
|
||||
:host .container .no-data {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
@@ -134,12 +134,22 @@ nb-card-body {
|
||||
// attribute. The doubled classes make up the difference, the same idiom the
|
||||
// Invoices and Pipelines sheets use on `.tab-link.tab-link`.
|
||||
gauzy-task-badge-view .badge-color.badge-color {
|
||||
height: var(--gauzy-table-badge-height, 1rem);
|
||||
padding: var(--gauzy-table-chip-padding-y, 0) var(--gauzy-table-chip-padding-x, 0.3125rem);
|
||||
// Same box as `ga-status-badge` now draws, from the same two tokens: the
|
||||
// pills in the Status column and the pills every other list page shows were
|
||||
// otherwise a different height and a different corner from each other.
|
||||
//
|
||||
// `min-height` rather than `height`, and `padding-block` rather than a
|
||||
// padding shorthand with 0 in the block axis. The old pair pinned a 1rem box
|
||||
// and then spent nothing on vertical padding — which it could not have done
|
||||
// anyway, since under `border-box` padding inside a pinned height comes out
|
||||
// of the text's box rather than adding to the pill.
|
||||
min-height: var(--gauzy-table-badge-height, 1rem);
|
||||
padding-block: var(--gauzy-table-badge-padding-y, 0.1875rem);
|
||||
padding-inline: var(--gauzy-table-chip-padding-x, 0.3125rem);
|
||||
gap: var(--gauzy-table-chip-gap, 0.125rem);
|
||||
font-size: var(--gauzy-table-header-font-size, 0.75rem);
|
||||
line-height: var(--gauzy-table-header-line-height, 0.8125rem);
|
||||
border-radius: var(--gauzy-radius-sm, 0.375rem);
|
||||
border-radius: var(--gauzy-table-badge-radius, 0.25rem);
|
||||
|
||||
.badge-img.badge-img {
|
||||
width: var(--gauzy-table-header-font-size, 0.75rem);
|
||||
@@ -199,6 +209,19 @@ nb-card-body {
|
||||
|
||||
// The page-header action row: "+ Add", the selection strip and the grid-vs-list
|
||||
// selector, compacted to the table's scale (see the mixin).
|
||||
:host .gauzy-button-container {
|
||||
//
|
||||
// `nb-card-header`, not `.gauzy-button-container` — which is what this was, and
|
||||
// is the same mistake the mixin's own doc comment warns about. This header holds
|
||||
// TWO button groups: the toolbar in `.tasks-component__settings >
|
||||
// .gauzy-button-container`, and the project-settings gear, which is a bare
|
||||
// `<button nbButton size="small">` sitting in `.card-header-title` beside the
|
||||
// heading. Naming only the container left that gear at its full 0.5625rem box
|
||||
// next to a toolbar that had shrunk to 0.25rem, so the one control the eye lands
|
||||
// on first was the one still drawn oversized.
|
||||
//
|
||||
// Every metric the row needs is a CUSTOM PROPERTY, so one declaration on the
|
||||
// header reaches both groups — `ngx-gauzy-button-action`'s own template and
|
||||
// `ga-layout-selector` included, across their component boundaries.
|
||||
:host nb-card-header {
|
||||
@include ga-hub.page-header-buttons();
|
||||
}
|
||||
|
||||
@@ -9,41 +9,19 @@
|
||||
// one still drawn a size larger than its neighbours.
|
||||
@include ga-hub.list-page();
|
||||
|
||||
// ── Row breathing room ───────────────────────────────────────────────────────
|
||||
// ── No step up from the shared cell box ──────────────────────────────────────
|
||||
//
|
||||
// `density-tokens()` sizes a row for a line of TEXT: 1rem of line box plus
|
||||
// 0.1875rem above and below. Text is the right thing to size that box around
|
||||
// because a glyph only ever fills about two thirds of its line box, so the row
|
||||
// still reads as padded while sitting at the floor.
|
||||
// This page used to raise `--gauzy-table-cell-padding-*` to 0.4375rem/0.625rem
|
||||
// and the header pair with it, because half its columns are people and, at the
|
||||
// time, a person was pinned to a 1rem avatar — a disc that came within 3px of
|
||||
// the rule above and the rule below it in ten consecutive rows.
|
||||
//
|
||||
// Half of this table's columns are people — Managers and Members, the two
|
||||
// widest — and a person renders as an AVATAR, which fills its line box edge to
|
||||
// edge: 1rem of disc in a 1rem box. At the shared floor those discs came within
|
||||
// 3px of the rule above and the rule below them, in ten consecutive rows, which
|
||||
// is what made a page of people read as one solid block with no rhythm for the
|
||||
// eye to track along.
|
||||
//
|
||||
// 0.4375rem instead, which is the same ~30px row the rest of the section lands
|
||||
// on once its text cells are padded (Tags runs 0.375rem around a 1rem line box)
|
||||
// — the extra 1px a side here is the avatar being taller than the text it sits
|
||||
// beside, not this page choosing a different scale.
|
||||
//
|
||||
// The gutter comes up to the app-wide 0.625rem with it: at 0.4375rem the Name
|
||||
// column's first character sat against the surface's own ring, and the Tags
|
||||
// column — the one the header's filter funnel hangs over — had less clear space
|
||||
// than the chips inside it.
|
||||
//
|
||||
// Scoped to this page rather than pushed into `density-tokens()`: the shared
|
||||
// values are what two dozen other tables are drawn at, and the avatar columns
|
||||
// are what justifies the step.
|
||||
:host {
|
||||
--gauzy-table-cell-padding-y: 0.4375rem;
|
||||
--gauzy-table-cell-padding-x: 0.625rem;
|
||||
// The heading row with them, or the header ends up shorter than the rows it
|
||||
// heads.
|
||||
--gauzy-table-header-padding-y: 0.4375rem;
|
||||
--gauzy-table-header-padding-x: 0.625rem;
|
||||
}
|
||||
// The height is in the chip now rather than in the cell: `ngx-people-list` is
|
||||
// back at a 1.25rem avatar with `--gauzy-people-chip-padding-y` around it, which
|
||||
// is the same 28px the Invoices Contact chip costs its row. So a `<td>` here is
|
||||
// the Invoices `<td>` — 0.1875rem/0.4375rem around it — and the row lands at the
|
||||
// same height from the same direction, instead of this one page holding a
|
||||
// padding value of its own to make up the difference.
|
||||
|
||||
// ── The Tags cell ────────────────────────────────────────────────────────────
|
||||
//
|
||||
|
||||
+5
-3
@@ -32,9 +32,11 @@
|
||||
(userRowSelect)="selectTimeOffPolicy($event)"
|
||||
></angular2-smart-table>
|
||||
</div>
|
||||
@if (smartTableSource) {
|
||||
<ngx-pagination [source]="smartTableSource"></ngx-pagination>
|
||||
}
|
||||
<div class="pagination-container">
|
||||
@if (smartTableSource) {
|
||||
<ngx-pagination [source]="smartTableSource"></ngx-pagination>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<!-- Card Grid View -->
|
||||
@case (componentLayoutStyleEnum.CARDS_GRID) {
|
||||
|
||||
@@ -1 +1,12 @@
|
||||
@forward '@shared/_pg-card';
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// The same block Invoices carries: density tokens, the table surface, row
|
||||
// hover/selection, the tag-chip stack, the status pills, the pager and the
|
||||
// toolbar scale.
|
||||
//
|
||||
// The pager needed the `.pagination-container` wrapper the sibling list pages
|
||||
// already had — the template rendered a bare `<ngx-pagination>` into the card
|
||||
// body, so it was neither right-aligned nor compacted.
|
||||
@include ga-hub.list-page();
|
||||
|
||||
@@ -89,9 +89,12 @@
|
||||
#timeOffTable
|
||||
></angular2-smart-table>
|
||||
</div>
|
||||
@if (smartTableSource) {
|
||||
<ngx-pagination [source]="smartTableSource"></ngx-pagination>
|
||||
} }
|
||||
<div class="pagination-container">
|
||||
@if (smartTableSource) {
|
||||
<ngx-pagination [source]="smartTableSource"></ngx-pagination>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<!-- Grid Card View -->
|
||||
@case (componentLayoutStyleEnum.CARDS_GRID) {
|
||||
<div class="custom-grid">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@use '@shared/_pg-card' as *;
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
nb-radio-group {
|
||||
display: flex;
|
||||
@@ -46,3 +47,29 @@ nb-radio-group {
|
||||
margin-right: 16px;
|
||||
border-radius: nb-theme(border-radius);
|
||||
}
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// The same block Invoices carries: density tokens, the table surface, row
|
||||
// hover/selection, the tag-chip stack, the status pills, the pager and the
|
||||
// toolbar scale. The header holds `ngx-gauzy-button-action` plus this page's own
|
||||
// filter strip and archive toggle, and the default `$actions` selector takes all
|
||||
// of them together.
|
||||
//
|
||||
// The pager needed the `.pagination-container` wrapper the sibling list pages
|
||||
// already had — the template rendered a bare `<ngx-pagination>` into the card
|
||||
// body, so it was neither right-aligned nor compacted.
|
||||
@include ga-hub.list-page();
|
||||
|
||||
// ── The Employee cell ───────────────────────────────────────────────────────
|
||||
// `ga-picture-name-tags` renders `<ngx-avatar class="report-table">`, which pins
|
||||
// a 20px image inside a pill padded 3px: 26px of chip, 2px short of the 28px
|
||||
// `ngx-contact-links` costs the Invoices Contact column, and so 2px short of the
|
||||
// Invoices row. Only that inset is restated — the image and the name stay the
|
||||
// renderer's own 20px/12px — which takes the chip to the same 28px and the row
|
||||
// to the same height.
|
||||
//
|
||||
// Table-scoped: `ga-card-grid` instantiates the same `renderComponent` for the
|
||||
// card layout, which is not paying for a table row.
|
||||
:host .table-scroll-container ::ng-deep angular2-smart-table {
|
||||
@include ga-hub.avatar-chip();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@use '@shared/_pg-card' as *;
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
:host {
|
||||
nb-card-body {
|
||||
@@ -23,3 +24,25 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// Density tokens, the table surface, row hover/selection, the tag-chip stack,
|
||||
// the status pills, the pager's `<nav><ul><li>` and the toolbar scale — the same
|
||||
// block Invoices carries. The toolbar is `ngx-gauzy-button-action` in
|
||||
// `nb-card-header`, which the default `$actions` selector covers.
|
||||
@include ga-hub.list-page();
|
||||
|
||||
// The one renderer `list-page()` does not reach: the People column renders
|
||||
// `PictureNameTagsComponent`, i.e. `<ngx-avatar class="report-table">`, whose own
|
||||
// sheet pins a 20px image inside a pill padded 3px — 26px of chip, 2px short of
|
||||
// the 28px `ngx-contact-links` costs the Invoices Contact column, and so 2px
|
||||
// short of the Invoices row. `avatar-chip()` restates only that inset; the image
|
||||
// and the name stay the renderer's own. Every other people table in the app
|
||||
// (Employees, Candidates, Approvals, Time Off, Recurring Expenses) carries this
|
||||
// include — Users was the table left 2px shorter than the contract it shares.
|
||||
//
|
||||
// Table-scoped: `ga-card-grid` instantiates the same `renderComponent` for the
|
||||
// card layout, which is not paying for a table row.
|
||||
:host .table-scroll-container ::ng-deep angular2-smart-table {
|
||||
@include ga-hub.avatar-chip();
|
||||
}
|
||||
|
||||
+13
-19
@@ -176,28 +176,22 @@
|
||||
// lays its row out in columns. A bare `.row` here would have redesigned three
|
||||
// pages that were not asked about.
|
||||
//
|
||||
// THE CELL GUTTER, and why it is declared here rather than in the template.
|
||||
//
|
||||
// The cells used to carry Bootstrap's `p-0`. Every Bootstrap utility is
|
||||
// `!important` — `.p-0 { padding: 0 !important }` — so no gutter could be added
|
||||
// to these columns from a stylesheet at ANY specificity, and the grid had none:
|
||||
// THE CELL GUTTER is Bootstrap's own. The cells used to carry `p-0`, and every
|
||||
// Bootstrap utility is `!important` — `.p-0 { padding: 0 !important }` — so no
|
||||
// gutter could be added to these columns from a stylesheet at ANY specificity:
|
||||
// the website's ellipsis ended one pixel before the first tag chip began, and
|
||||
// "Phone" and "Email" were two headings with nothing between them. The template
|
||||
// drops `p-0`; this replaces it, which is what makes a per-edge value possible.
|
||||
// drops `p-0`, which is what lets the grid's own COLUMN PADDING apply: on
|
||||
// Bootstrap 4.6 (this app's version) a `.col-*` carries a fixed 15px either side
|
||||
// and the `.row` pulls the outer pair back with a -15px margin. There is no
|
||||
// gutter custom property to reach for here — `--bs-gutter-x` is Bootstrap 5 — so
|
||||
// the value is the framework's and is changed by overriding the padding, not by
|
||||
// setting a variable.
|
||||
//
|
||||
// Zero leading, 1rem trailing. A Bootstrap `.row` bleeds 15px past its container
|
||||
// through negative margins, and the row's own 1.125rem gutter is sized to cover
|
||||
// exactly that (see the `card-list-page()` call above) — so a leading pad would
|
||||
// push the first cell off the edge that establishes, and the last column needs
|
||||
// no trailing edge of its own. 1rem matches what a smart-table cell gets from
|
||||
// `--gauzy-table-cell-padding-x` on either side of its border.
|
||||
:host .vendor-row > [class*='col-'] {
|
||||
padding-inline: 0 1rem;
|
||||
}
|
||||
|
||||
:host .vendor-row > [class*='col-']:last-child {
|
||||
padding-inline-end: 0;
|
||||
}
|
||||
// A hand-written `padding-inline: 0 1rem` pair used to sit here in its place. It
|
||||
// is gone: a literal that had to be kept in step by eye with the row's own
|
||||
// gutter and with the cell padding it was copied from, for a spacing the grid
|
||||
// already provides.
|
||||
|
||||
// The name is the row's identity: it is the only value every vendor has, the
|
||||
// one the list is scanned by, and the one the row is selected by. At the same
|
||||
|
||||
Reference in New Issue
Block a user