mirror of
https://github.com/ever-co/ever-gauzy.git
synced 2026-10-02 01:54:50 +08:00
Fix/polish organizations pages 2 (#10160)
* style(tables): add the shared list-page block to the table hub
Two mixins, so the eighteen data tables under /pages/organization stop being
eighteen places for the same contract to drift.
`list-page($actions)` bundles what Payments, Invoices, Pipelines, Tasks,
Employees and Job Employees each spell out by hand: density tokens, the table
surface, row hover/selection, the tag-chip stack, the status pills, the pager's
`<nav><ul><li>` and the page-header action row. Those pages interleave
page-specific rules between the blocks, which is why they stay written out; these
have no such exceptions to interleave.
`$actions` defaults to `nb-card-header` rather than `.gauzy-button-container`,
and that is the point: several of these pages put a second row of buttons beside
the title — Inventory's Merchants / Warehouses / Product categories / Product
types, Equipment's "Equipment Sharing" — outside the toolbar container entirely.
Naming only the container left those at full size next to a compacted "+ Add".
Every token the row needs is a custom property, so one declaration on the header
reaches all of them, `ngx-gauzy-button-action`'s own template included.
`thumbnail-row-padding()` restores the app-wide cell padding on a table that
carries a picture column. `density-tokens()` runs at 0.1875rem/0.4375rem, which
is right for a row whose tallest thing is a line of text; a thumbnail row is not
that row, so the tighter box buys no height there and only presses the picture
against the rules above and below it. Set on `angular2-smart-table`, since the
density rules in `_overrides.scss` read these as custom properties inherited by
the `td` — no specificity needed against a `:root`-prefixed block.
The pictures themselves are deliberately left alone: shrinking them to the row's
scale costs the photo its information, and `ga-no-image` clips its "Image not
available" caption mid-word, so a missing image reads as a broken one.
* style(equipment): bring the equipment table onto the list-page scale
Adds the shared block, so this table reads as the same surface as Job Employees
and the rest of the list pages rather than as one drawn a size larger.
The action row takes the block's `nb-card-header` default, which this page needs
twice over: its toolbar sits in a bare `<div class="align-self-end">` with no
container class, and the "Equipment Sharing" button sits beside the title,
outside the toolbar altogether.
Three cells hard-code a box that the density tokens cannot reach, all scoped to
the table rather than the page because `ga-card-grid` renders the same components
for the card layout, where the full-size treatment belongs:
* Image — `ImageRowComponent` keeps its 74x60 photo; only the cell padding
around it goes back to the pre-density default.
* Equipment Name — `ga-picture-name-tags` renders `<ngx-avatar
class="report-table">`, which pins a 20px image, a 12px name and an 8px gap
inside a padded pill. Sized off the shared `gauzy-people-*` tokens, the same
treatment Employees gives its own Full Name cell.
* Auto Approve — a bare tick or cross, but `nb-icon` defaults to 1.5rem, half a
rem taller than the row's whole line box, so a column carrying nothing but a
glyph was setting the height of every row. Re-tokenised through Nebular's own
`icon-*` keys, so there is no specificity to fight.
Also removes `:host nb-card-body { height: calc($card-height - 0.5rem)
!important }`. `gauzy/_gauzy-cards` already supplies that same chrome estimate as
a FLEX BASIS, which `flex: 1 1 auto` then corrects against the card's real
height; restating it at page level with `!important` turned a starting point into
a floor and a ceiling.
* style(equipment-sharing): bring the sharing table onto the list-page scale
Adds the shared block: density tokens, the table surface, row hover/selection,
the status pills, the pager and the button scale.
The Status column already routes through `ga-status-badge` (see `statusMapper()`
in the component), which is exactly what the block's tint rules address — the
saturated fill with white text becomes the accent-on-tint treatment the rest of
the app uses, and lands the accessible colours with it.
The action row takes the `nb-card-header` default, which also picks up the
"Equipment Sharing Policy" button sitting beside the title.
* style(equipment-sharing-policy): bring the policy table onto the list-page scale
Adds the shared block: density tokens, the table surface, row hover/selection,
the tag-chip stack, the pager's `<nav><ul><li>` and the toolbar scale — so this
table reads as the same surface as its neighbours.
* style(tags): bring the tags table onto the list-page scale
Adds the shared block. The action row takes the `nb-card-header` default, which
covers this page's `.gauzy-action` toolbar — it lays its header out itself,
beside the tag-type rail, and never adopted the shared container class.
The pager was the reason for the one template change: `<ngx-pagination>` was
rendered bare, with no `.pagination-container` around it, so it was one of three
pagers in the section the styling could not reach. Wrapping it matches every
other list page and the Job Employees reference. No bindings, handlers or
conditions touched.
The wrapper also has to opt out of growing: `.table-scroll-container` beside it
takes `flex-grow: 10` from this sheet, which would otherwise leave the pager
fighting it for the remainder of the card body.
* style(approval-policy): bring the approval-policy table onto the list-page scale
Adds the shared block: density tokens, the table surface, row hover/selection,
the tag-chip stack, the pager's `<nav><ul><li>` and the toolbar scale.
* style(departments): bring the departments table onto the list-page scale
Adds the shared block. The Members column renders `EmployeeWithLinksComponent`,
which draws through `ngx-people-list` — already built on the `gauzy-people-*`
tokens, so the density tokens this block puts on `:host` reach it across the
component boundary and the people cells come onto the row's scale for free.
* style(teams): bring the teams table onto the list-page scale
Adds the shared block. Members and Managers both render
`EmployeeWithLinksComponent` and the Tags column renders `TagsOnlyComponent`, all
three of which already read the shared tokens — so the density block on `:host`
carries them onto the row's scale across the component boundary.
* fix(projects): pass the content column's height through the routing shell
This component is the /pages/organization/projects shell: its whole template is a
`<router-outlet>`, and the list, create and edit pages render into it.
`styles/_overrides.scss` hands the routed page the viewport-bounded height the
content column already has — but with `nb-layout-column > router-outlet + *`, a
DIRECT-child selector, so on a nested route it lands on THIS component and stops.
Below it the page had no definite height for `nb-card { height: 100% }` to
resolve against, the card collapsed to its content, and the table inside came out
short of the card, taking its scrollbar with it.
NOT fixed with `:host { display: flex }`, which is the obvious way to hand a flex
height on and cannot win: `pages.component.scss` carries
`:host ::ng-deep router-outlet + * { display: block }`, which shims to
`[_nghost-pages] router-outlet + *` — (0,1,1) — and reaches every routed
component in the app. A component's own `:host` is `[_nghost-x]`, (0,1,0), and
loses. This host stays `display: block` whatever this file says.
What the host DOES get from that same `_overrides` rule is `flex: 1 1 auto` — a
different property, so nothing overrides it — which makes its own height
definite. A percentage resolves against that perfectly well and, unlike a flex
child, does not care what the parent's `display` ended up being. So the routed
page simply takes 100% of the shell.
`::ng-deep` is required and is not a leak: the routed component is a DOM sibling
of `<router-outlet>` but belongs to a different component, so it carries none of
this template's `_ngcontent` attribute and a plain `router-outlet + *` would shim
to a selector matching nothing. Leading with `:host` keeps the rule inside this
shell, and setting no `display` keeps it clear of the rule above.
* style(projects): bring the projects table onto the list-page scale
Adds the shared block, with the action row on the `nb-card-header` default — this
page's toolbar sits in a bare `<div class="d-flex justify-content-end">` with no
container class of its own.
Three cells hard-code a box the tokens cannot reach, all scoped to the table
because `ga-card-grid` renders the same components for the card layout:
* Project — `gauzy-project-organization` draws a 28px logo beside a 14px/17px
name and an 11px/13px caption, the single biggest reason a row here stood
taller than the same row on Employees. Sized off the shared `gauzy-people-*`
tokens so the project chip matches the people cells beside it.
* Contact — `ngx-contact-links` draws an 18px avatar and 12px/15px text inside
a pill padded 5px/14px. Same scale-down; the pill keeps its tint so the cell
still reads as a link.
* Public/Private — an `nb-toggle`, which Nebular sizes entirely from theme
tokens: a 50x30 switch with a 28px knob and a label on a 1.5rem line, i.e.
twice the row's whole line box. Re-tokenised to 20x12, which keeps Nebular's
own 1.667 proportion so it reads as the same control drawn smaller — the same
numbers Employees gives its Screen Capture column.
Also records why the card fits: this page is reached through a routing shell, so
the height chain is repaired in the shell's own stylesheet, which is the only
place it can be.
* fix(inventory): pass the content column's height through the routing shell
This component is the /pages/organization/inventory shell: its whole template is
one wrapper around a `<router-outlet>`, and the items table, the item form and
the item view render into it. It carried no stylesheet at all.
`styles/_overrides.scss` hands the routed page the viewport-bounded height the
content column already has — but with `nb-layout-column > router-outlet + *`, a
DIRECT-child selector, so on a nested route it lands on THIS component and stops.
Below it the page had no definite height for `nb-card { height: 100% }` to
resolve against, the card collapsed to its content, and the table inside came out
short of the card, taking its scrollbar with it.
NOT fixed with `:host { display: flex }`, which is the obvious way to hand a flex
height on and cannot win: `pages.component.scss` carries
`:host ::ng-deep router-outlet + * { display: block }`, which shims to
`[_nghost-pages] router-outlet + *` — (0,1,1) — and reaches every routed
component in the app. A component's own `:host` is `[_nghost-x]`, (0,1,0), and
loses. This host stays `display: block` whatever this file says, and a
`flex: 1 1 auto` on the wrapper would have nothing to grow inside.
What the host DOES get from that same `_overrides` rule is `flex: 1 1 auto` — a
different property, so nothing overrides it — which makes its own height
definite. So the wrapper takes 100% of it rather than trying to be a flex child
of a box that is not a flex container, and the routed page grows inside the
wrapper, which IS one.
`::ng-deep` is required and is not a leak: the routed component is a DOM sibling
of `<router-outlet>` but belongs to a different component, so it carries none of
this template's `_ngcontent` attribute and a plain `router-outlet + *` would shim
to a selector matching nothing. Leading with `:host` keeps the rule inside this
shell, and setting no `display` keeps it clear of the rule above.
* style(inventory): bring the shared inventory-table sheet onto the list-page scale
This file is taken by five tables — product types, product categories,
merchants, warehouses and the inventory items table — so the shared block lands
once for all of them: density tokens, the table surface, row hover/selection, the
tag-chip stack, the pager's `<nav><ul><li>` and the toolbar scale. They are the
same card with the same `.table-scroll-container`, `.pagination-container` and
`.gauzy-button-container` five times over.
Scoped to the table on top of that:
* The Image / Logo / Icon column — `ImageRowComponent` (74x60),
`ItemImgTagsComponent` (70x64) and `IconRowComponent` (a 35px disc) keep
their sizes; only the cell padding around them goes back to the pre-density
default, since a picture sets the row height whatever the text does.
* Name and description — both renderers hard-code a 14px/17px name over a
12px/15px caption, so this one column read a step louder than every other
column in the row. Pointed at the table's own sizes, which is a match rather
than a reduction. The 4px between the two lines is left alone.
* Active / Inactive — `EnabledStatusComponent` emits a bare
`<div class="badge badge-success">`, so the only thing painting it was
bootstrap.css: a saturated fill with white text, and the inherited
`color-success-default` measures 2.9:1 on white. Its counterpart was a literal
`#ccc` with whatever text colour it inherited — the one chip in these tables
that ignored the theme. Both now take the accent-on-tint treatment
`ga-status-badge` gets everywhere else.
* fix(inventory): let the items card fill the content column
Removes `:host nb-card-body { height: calc($card-height - 0.5rem) !important }`.
`gauzy/_gauzy-cards` already gives the body that same chrome estimate, but as a
FLEX BASIS — `flex: 1 1 auto` then grows or shrinks it to whatever the card
actually has, so the estimate being a guess never mattered. Restating it at page
level with `!important` turned a starting point into a floor and a ceiling.
/pages/tasks/dashboard, which fits, pins that height only on its `.project`
variant; with none selected its body is a plain flex child, and that is the whole
difference between the two pages.
With the line gone this page's compiled card and body rules are identical to that
reference, byte for byte.
The other half of the fit is not in this file: the height chain has to reach this
page at all before `nb-card { height: 100% }` can resolve, and that is repaired in
the routing shell.
* style(inventory): bring the warehouse product tables onto the list-page scale
The warehouse's own product table and the product picker it opens are the same
surface as the page behind them, so both take the shared block, the thumbnail
cell padding, and a `.pagination-container` around their pagers.
Both rendered `<ngx-pagination>` bare, which is why their `<nav><ul><li>` was out
of reach of the styling; wrapping it matches every other list page. No bindings,
handlers or conditions touched. Both wrappers also opt out of growing, since
`.table-scroll-container` beside them takes `flex-grow: 10`.
The action rows differ and are named rather than left at the `nb-card-header`
default: the products table has no card header at all — it IS a card body — and
puts its toolbar in a bare `<div class="d-flex justify-content-end">`, so the
button component itself is the one stable handle, and the tokens are custom
properties that inherit into its template from there. The picker has no action
row at all, so it passes `null`.
The picker's select cell keeps its literal `height: 79px`: `display: flex` takes
that `td` out of the row's own height resolution, so it has to state a height or
the checkbox stops lining up with the data beside it, and 79px is the 74x60
thumbnail in the next column — which this change leaves at 74x60.
* style(inventory): bring the variant tables onto the list-page scale
The variant table in the item editor and the variants table on the item view are
the same surface as the inventory list they hang off, so both take the shared
block. Neither has a pager, but the block is the whole contract and splitting it
per page is how the list pages drifted apart in the first place.
Scoped to the table on both: the Image column is `ImageRowComponent`, which keeps
its 74x60 photo and gets back the pre-density cell padding around it; the Enabled
column is `EnabledStatusComponent`, whose `.badge-success` was painted only by
bootstrap.css — a saturated fill with white text at 2.9:1 — and whose disabled
counterpart was a literal `#ccc`.
The editor's table names `.gauzy-button-container` for its action row; the item
view has no action row and passes `null`.
* style(tabset): draw both tab families from one definition
Nebular ships two components for one control — `nb-tabset` switches a panel,
`nb-route-tabset` switches a route — and every sheet in this repo that dressed a
tab strip had named only the route one. So the two were drawn differently
wherever both appear in the same app: the route strips (organization edit,
employee edit) took a 16px box and stacked their icons above the label below
`dsk`, while every `nb-tabset` (Employment types, Time off, Candidates, Jobs,
Inventory) kept the theme's own box and a 0.5rem inline icon gap.
The body of that rule becomes `tab-strip($strip)` and is included for both, so
they cannot drift again. The strip class is a parameter rather than a bare `ul`,
which would also catch a list inside a tab's CONTENT and strip its indent.
Also restores the keyboard focus ring the `outline: none` on `.tab-link` took
with it. That line is there to drop the ring a mouse click leaves behind, and it
removed the keyboard's too — on the one control a keyboard user moves through
with arrow keys. `:focus-visible` gives it back to both families without
bringing the mouse ring back.
* style(tabset): centre nb-tabset labels and stop shouting route tabs
The same oversight as the sheet before this one, in the two places that dress a
tab strip from the theme side.
`nb-tabset-overrides()` centred a tab link's contents for the route family only,
so an `nb-tabset` link took Nebular's own `display: flex` and nothing else and
its label sat wherever the box put it, while a route tab's was centred in the
same box. The icon-gap reset was route-only for the same reason: Nebular gives
the label a 0.5rem inline margin that would be added to the 10px gap
`includes/_tabset.scss` sets on the icon. Both rules now name both families.
And the reverse case in the themes: Nebular ships UPPERCASE for BOTH families
(`styles/themes/_mapping.scss`) and only the `nb-tabset` half of it was ever
overridden here, which is why an `nb-route-tabset` read MAIN / LOCATION /
SETTINGS while every `nb-tabset` in the app read its tabs in sentence case.
`route-tabset-tab-text-transform: capitalize` alongside the existing key, in
both registered themes — the same fix, and the same shout, the ghost-button line
below it describes.
* style(tabset): keep the action row on the strip's box below dsk
The action row that sits ON a tab strip is boxed to match the strip, and it
matched only at the default breakpoint: below `dsk` the strip takes the 16px box
from `styles/includes/_tabset.scss` and this row kept the theme's line height,
so it was no longer the strip's own row there.
Both families now take that 16px box — that sheet stopped naming only the route
one — so this row follows it at the same breakpoint. The inline padding stays
zeroed, as it is at the default size: the row is positioned by the strip's own
end, not padded away from it.
* style(table-hub): add the list-page contract for hand-built card tables
`list-page()` anchors every selector that matters on the `angular2-smart-table`
element, so it never reached the five pages under `/pages/organization` that
build their table BY HAND — Vendors, Positions, Employee levels and Documents
share one stylesheet, Employment types has its own. What that looked like:
full-size toolbar buttons beside the compacted ones on Departments and Teams,
rows better than twice the height, and a stack of separately floating cards
where the rest of the section has one bounded surface with rows on it.
`card-list-page()` takes the three pieces of `list-page()` that are NOT anchored
on the smart table — the density tokens, the toolbar scale, the table surface —
and adds the one thing these rows need that a `<tr>` never did: they arrive as
`nb-card > nb-card-body.custom-table`, carrying a card's background, radius,
ring and bottom margin, all of which is flattened onto the surface and replaced
with one inset hairline per row (a single-column list has neither aligned
columns nor anything else to give it structure).
Hover and selection come with it, for all five pages rather than the four that
share the Vendors sheet. `expense-categories` gives both states the same
`rgba(50, 50, 50, 0.03)`, so hovering looked exactly like selecting and 3% of
black over a near-black card is nothing at all — no visible selection on the
four dark themes. Selection also arrived as a 6px border that exists only when
selected, so the row jumped sideways on click. Now: the neutral tints, primary
on the leading edge, and a transparent edge of the same width at rest.
Two constraints are load-bearing and noted at their rules. The row paint is
deliberately `:host .custom-table`, the same specificity `expense-categories`
uses, because the hover and selection rules sit one pseudo-class above it —
anything stronger would out-rank them and paint every hovered row transparent;
that also means the mixin MUST be included after the `@forward`. And the surface
takes `min-height: 100%` rather than `flex: 1 1 auto`, since two of the five put
this container in a card BODY that is not a flex column.
The row gutter is a parameter: the default suits a row holding one value across
its width, while a row whose markup is a Bootstrap `.row` needs at least the
15px that grid bleeds past its container with negative margins.
* style(vendors): move the column titles onto the table surface
Four pages `@forward` this sheet — Vendors, Positions, Employee levels and
Documents — and all four build their table as a stack of `nb-card` rows, so they
take `card-list-page()`. The gutter is passed as 1.125rem: a row here is a
Bootstrap `.row`, which bleeds 15px past its container through negative margins,
so at the default 0.4375rem the first cell would hang 8px off the left edge of
the surface. The local hover/selection block goes with it — it is now in the
shared mixin, in logical properties, and reaches Employment types too.
Vendors is the only one of the four with column titles, and they were a filled
card-2 band in the CARD HEADER: a second surface floating 10px above the rows
with its own rounded corners, and one that could not line up with the values it
titles, since it sat in a container with 1rem of padding while the rows sat in
one with 18px. The `pl-3` on the first title was there to paper over exactly
that difference.
They are now the first child of the scroll container, boxed exactly like a row —
same padding, same 3px rail reserved on the leading edge for the selection
border — so the titles sit over their columns by construction rather than by
nudging, and `pl-3` comes off. The treatment is the one `list-page()` gives a
smart table's `thead`: no fill, the muted heading colour, one hairline
underneath, and `position: sticky` so it stays while the rows scroll under it —
which is why the fill has to be the opaque surface colour rather than none.
The `@if` on the layout enum moves with the block; it guarded the titles in the
header and still guards them in their new home.
* style(employment-types): bring the hand-built table onto the list-page scale
The fifth of the hand-built tables, and the one that does not share the Vendors
sheet — so it had neither the list-page scale nor any row hover or selection at
all, only what `expense-categories` gives it. Rows were separately floating
cards on the tab body, and the toolbar sat at full size beside the compacted
buttons on Departments and Teams.
The action row is the one thing this page positions itself: it does not sit on
the title line like the rest of the family, it sits ON the tab strip
(`tabset-action-row`), so the toolbar scale is asked for by name —
`.gauzy-button-container` — rather than taken on `nb-card-header`, where this
page has no buttons at all. The `+ Add` button in the template takes `.action`
with the rest of the section, so it is picked up by that scale.
* style(organization): compact the add button on the remaining card-list pages
Positions, Employee levels and Documents take the Vendors sheet and therefore
the toolbar scale that came with it, but their `+ Add` buttons were never
classed `.action`, so the scale had nothing to select and they stayed at full
size beside the compacted ones on Departments and Teams. One class each; no
bindings, permissions or handlers touched.
Documents also had its file timestamp set at `text-caption-font-size`, which is
a step ABOVE the row's own size rather than under it — the secondary value was
rendering larger than the file name it hangs off. It now takes
`--gauzy-table-font-size`, the size the shared card-list block sets the row at.
* fix(company-logo): slug the brand class and fall back to a generic glyph
The company name was interpolated into the class list as it stood
(`'fab fa-' + rowData?.name | lowercase`), which is only a class at all for a
single-word name. "Ever Technologies LTD" produced THREE classes — `fa-ever`,
`technologies` and `ltd` — so any company whose name happens to contain a word
the page styles (`selected`, `primary`, `action`, `row`) was styled by it. The
name is now slugged to one class whatever it contains: lower case, ASCII, hyphen
separated, which is also the form Font Awesome's own names take, so "Pay Pal"
finds `fa-pay-pal` where before it looked for `fa-pay`.
The bigger half is that Font Awesome only ships a logo for companies that ARE
brands, and this table lists a tenant's own vendors — so for most rows `fa-<name>`
matched nothing and the cell drew a blank. FA still gives every icon element a
fixed 1.25em box, so the column paid for an icon it never showed, and the rows
that did match a brand were the only ones with anything on their leading edge.
FA 7 draws its glyph from a custom property (`content: var(--fa)`, set by the
`fa-<name>` class), and custom properties inherit — so a value declared on the
HOST is what the `<i>` gets when no `fa-<name>` class set one, and is overridden
by the class whenever one did. A generic building for a company Font Awesome has
never heard of, with no branch in the template and no list of which names it
knows. The font stack gains the free face behind the brands file, since that is
where the fallback glyph lives, and weight 900 to reach its solid face — harmless
to the brands file, which ships one weight and is matched whatever is asked of
it.
Sizing follows the row rather than a fixed 18px/21px, and the 0.75rem of padding
on all four sides — the better part of a centimetre around a one-em glyph, and
for every logo-less row the only thing there at all — becomes the one gap an icon
column needs, to the name beside it.
* style(inventory): put the section buttons on the title's line
Merchants / Warehouses / Product categories / Product types sat in a nested
`.card-header-title` block of their own, which the outer row's
`align-items: center` then centred against the TWO-line title block — the
heading plus the breadcrumb trail `ngx-header-title` parks under it — dropping
them ~12px below `+ Add`. The heading and the buttons are now direct siblings of
ONE title row, the shape Employees and Equipment already use, so they share the
title's line and therefore `+ Add`'s.
They stay wrapped in a single block so the row's `justify-content:
space-between` (`styles/_overrides.scss`) pushes the four of them to the row's
end as a group, rather than spreading them evenly across it as four separate
flex items would be.
That block is flex, not a plain box: as inline-blocks the buttons sit on a
baseline and the descender space under it makes the box a few pixels taller than
they are — the same offset this change removes, in miniature. The two
declarations that settle the vertical axis are `align-self: flex-start` and
`min-height: gauzy-page-title-row-height`, the pair `_overrides.scss` already
puts on `.ga-page-header-actions` beside it: centring alone is not enough while
the two blocks are centred in different boxes, so both are anchored to the row's
top edge in the same box and share one centre by construction.
`flex-wrap` folds the group onto a second line on a narrow viewport rather than
squeezing the labels, and `min-width: 0` lets it shrink at all.
* style(tags): size the columns, the name chip and the filter rail
The four column widths are a RATIO the library hands to the `<th>`s, so they
have to add up to the table: 20/20/70/10 came to 120%, which is why Description
alone took better than half the row and the other three were squeezed into what
was left. They now total 100. The Name column's `class: 'text-center'` goes —
`class` is not one of the library's settings and was dropped on the floor;
`classHeader` and `classContent` are the real hooks, and the Counter and Tag
type columns use them.
Counter is right-aligned with tabular figures: a count belongs on its units
digit, where "3990" and "1890000" no longer start in the same place, and Inter's
proportional default figures otherwise wander a pixel or two per row. Its
heading follows, via `justify-content` rather than `text-align` — `a.sort` is
laid out `row-reverse`, so the end of its main axis is the left edge. Six-figure
usage counts are common here and are now grouped by the browser locale. Tag type
and Description fall back to an em dash and Tag type takes the table's muted
colour, so the row's three values stop competing at equal weight and a blank
column reads as deliberately empty rather than as a table that failed to load.
The name chip was `width: 100%` — ten saturated full-bleed bars of identical
length down the left edge, whether the label was "VIP" or "dependencies". Sized
to its text the colour is an accent again and the names line up under the "Name"
heading. `nb-badge` sets `position: absolute` on its own host, which is what
forced the wrapper to state a height for a chip contributing none; one
`position: relative` puts it back in flow so the wrapper can align it like any
other child.
The rail: chips take the control step of the radius scale rather than a 20px
stadium — the one shape that matches nothing around it — and so do the search
field and the badges. A chip may now wrap, since "Organization Department" is a
type Gauzy ships and a chip that could neither wrap nor shrink was wider than
the 230px rail, cut off by the card edge and dragging a horizontal scrollbar in
with it. The rail's type drops to 0.75rem throughout, the size of the table
headings beside it, so a filter stops out-typing the rows it filters; the
section label was the largest type on this half of the page and sat over the
smallest controls on it.
The search input opts out of the app-wide `min-height` floor that
`text-inputs-overrides` puts on every bare `input`: it was drawn ~10px taller
than the pill around it, invisible only because the field is transparent, but
the click target and the caret were both outside the box.
And the chips are reachable: they are the page's only filter control and plain
`nb-list-item`s, so they get `role`, `tabindex`, Enter/Space, `aria-pressed` and
a focus ring. The selected one gains an accent inset ring — the tint alone is
one step off hover and the resting chip already carries a neutral hairline, so
which of a dozen chips is filtering the table has to be findable from across the
page.
Rows get their air back locally: `density-tokens()` sizes a row for a line of
text and this table is four short single-line values and a chip, so nothing ever
pushed a row past that floor. The cells take the app-wide default instead, the
same value `thumbnail-row-padding()` hands the picture tables, with the heading
row raised to match. Scoped to this page — widening the shared token would
relayout two dozen tables nobody asked about.
* style(teams): size the columns and give the avatar rows their own rhythm
Set explicit column widths (20/30/30/20) so `table-layout: auto` stops sizing
the two people columns off their widest cell and holding the space as empty,
and drop the inert `class: align-row` the library never reads.
Step the row padding to 0.4375rem: an avatar fills its line box edge to edge
where a glyph fills about two thirds of it, so the shared text floor left the
discs 3px from the rules above and below them.
Zero the `ga-only-tags` block gap in the Tags column, which has chips and no
text above them for that gap to separate.
* fix(ui): address review findings on the tags, vendors and tabset polish
Keep the counter heading on its column under RTL. `justify-content` resolves
against the flex container's own direction, so `flex-start` on the `row-reverse`
sort link swung left while the cells stayed physically right. Overridden with
`:host-context([dir=rtl])` rather than `nb-rtl()`, which expands to `[dir=rtl] &`
and would ask for a `:host` nested inside another element.
Give the tab strip's `dsk` box a single source of truth. `px2rem(16px)` was
hand-copied into `_tabset.scss` and `_tabset-actions.scss`, where the action row
has to match the block value to stay vertically centred on the tab labels — the
exact drift the mixin's "WHY THIS EXISTS" note was written about. Both now read
`$tabset-dsk-tab-padding-*` from `var.scss`, which both sheets already use.
Close the vendors table surface at the top. `surface()` draws its outline with
`box-shadow: inset`, which is part of the element's background layer, so the
opaque sticky head painted straight over the top line and the leading edge. A
real border is geometry rather than paint: it sizes the content box the head is
laid out in, fixing top, leading edge and corners at once and mirroring under
RTL on its own.
Correct the `class` rationale. `class` IS declared on the library's `IColumn`,
which is why a dozen tables pass it without a type error, but `Column` never
reads it and it reaches no element — the effect claim was right, the reason was
not.
Correct the documents timestamp fallback and its reasoning. The row is
0.8125rem, so the previous `text-caption-font-size` was already smaller, not
larger; the fallback now sits below the row size instead of mirroring it.
Model the tag-type rail as a radiogroup. The options are mutually exclusive, so
`role="button"` + `aria-pressed` announced them as independent toggles and never
said the previous choice had been cleared. Now `role="radio"` + `aria-checked`
in a `radiogroup`, with one tab stop and arrow/Home/End navigation.
Reword the comment wording CSpell rejects.
* fixed the spell error
* fix(ui): clear the SonarCloud findings on the polish branch
Reliability, which is what failed the quality gate:
The Font Awesome stack in the company-logo cell ended on two names that
are not web-safe, so it had no last resort at all; it ends on a generic
now. And the slug trim's `/^-+|-+$/g` had two ambiguous quantifiers for
the engine to backtrack over — redundant ones, since the replace above
it has already collapsed every run of non-alphanumerics into a single
hyphen, so at most one can sit at either end. `/^-|-$/g` is the same
output with nothing to retry.
Maintainability:
`String(name ?? '')` took an `unknown`, so a non-primitive stringified
to `[object Object]` and slugged into a class of its own; it now takes
the value only when it is a string or a number.
Three sheets reopened a selector they had already written. Tags needed
the care: its second `:host` widens two density tokens that
`list-page()` sets on `:host`, so merging upward would have let the
shared value win — the include moves above the block instead, and the
tokens sit at the top of the rule rather than after the nested
selectors, which Sass deprecates. Vendors' stray `padding-block` moves
in beside the shorthand it overrides, and the projects shell's second
`:host` folds into its first.
* fix(ui): keep the tag filter rail reachable, and the vendor chips inside their cell
The rail is a radiogroup, so its tab stop is the checked radio — and a
refresh used to take that radio away. `getTags()` emptied
`filterOptions` down to "All" and `getTagTypes()` refilled it a request
later, so for the length of that request `selectedFilterValue` named a
type no rendered option carried: every radio went `tabindex="-1"`, and a
keyboard user whose option had just been re-rendered away had nothing
left to Tab back into.
Three things, because the window was only half of it. `getTagTypes()`
now builds the whole list and swaps it in one statement, so the rail
never renders a state the selection does not match, and `getTags()` no
longer touches it. The `@for` tracks `option.value` rather than the
option object — the old identity tracking rebuilt every radio on a list
of fresh literals, focused one included, which is what actually dropped
focus to the document. And the tab stop falls back to the first option
when nothing is checked, which is what the radiogroup pattern asks for
and covers the cases where the selection really has gone: an
organization switch, or a tag-type fetch that failed.
Elsewhere: a vendor tag longer than its two-column cell kept its chip
wider than the cell and ran it out through the side of the table, since
the chips may not wrap; they stop at the cell's edge with an ellipsis
now. And the specificity noted in the Teams tag-cell comment was (0,2,5)
for a selector that is (0,2,4) — the override still wins, but the count
was wrong for anyone extending the rule.
* fix(tags): drop a superseded load's response instead of applying it
The refresh generation gated whether the second load STARTED, which is
not the same question as whether its answer is still wanted. A
`getTagTypes()` that was current when it went out can be answered after
a newer one — two requests for the same resource settle in either order
— and on arrival it rebuilt the filter rail from the organization the
user had already left, then reconciled the selection against it.
Both loads now take the generation and check it again when their own
request comes home: a superseded response is dropped rather than
applied. `getTags()` gets the same treatment at its own network
boundary, since `allTags` and the pagination totals are what the
reconcile then reads, and leaving half the pipeline guarded would only
have moved the race.
Three details worth naming. `finally` runs on those early returns too,
so the reconcile and the spinner are guarded there rather than left to
fire for a pass that has been superseded — the spinner belongs to
whichever pass is current, and that pass always reaches its own
`finally`. A failure is still logged whatever its generation, because a
console line costs nothing and a swallowed error is hard to find later;
only the toast and the rail reset are held back. And the generation
parameter defaults to the current one, so a standalone call to either
method behaves exactly as it did.
This commit is contained in:
@@ -310,6 +310,7 @@
|
||||
"echarts",
|
||||
"editorplaceholder",
|
||||
"effectful",
|
||||
"ellipsised",
|
||||
"electronjs",
|
||||
"electronmon",
|
||||
"elif",
|
||||
@@ -806,12 +807,15 @@
|
||||
"Rahul",
|
||||
"raleway",
|
||||
"randomcolor",
|
||||
"radiogroup",
|
||||
"rangepicker",
|
||||
"ratelimit",
|
||||
"RDCL",
|
||||
"RADIOGROUP",
|
||||
"reanchor",
|
||||
"rebinding",
|
||||
"recents",
|
||||
"relayout",
|
||||
"reconnections",
|
||||
"Recu",
|
||||
"rediss",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
@forward '@shared/_pg-card';
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
.gauzy-button-container {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
// ── 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 Job Employees, Payments, Invoices, Pipelines, Tasks and Employees carry,
|
||||
// so this table reads as the same surface as the rest of the app rather than as
|
||||
// one still drawn a size larger than its neighbours.
|
||||
@include ga-hub.list-page();
|
||||
|
||||
@@ -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 Job Employees, Payments, Invoices, Pipelines, Tasks and Employees carry,
|
||||
// so this table reads as the same surface as the rest of the app rather than as
|
||||
// one still drawn a size larger than its neighbours.
|
||||
@include ga-hub.list-page();
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
</nb-card-body>
|
||||
</nb-card>
|
||||
<ng-template #visibleButton>
|
||||
<button nbButton status="success" size="small" (click)="openDialog(addEditTemplate, false)">
|
||||
<button nbButton class="action" status="success" size="small" (click)="openDialog(addEditTemplate, false)">
|
||||
<nb-icon icon="plus-outline"> </nb-icon>
|
||||
{{ 'BUTTONS.ADD' | translate }}
|
||||
</button>
|
||||
|
||||
@@ -33,7 +33,18 @@
|
||||
> div:last-child {
|
||||
flex: 0 0 auto;
|
||||
color: var(--gauzy-text-color-2, var(--text-hint-color));
|
||||
font-size: nb-theme(text-caption-font-size);
|
||||
// The row is 0.8125rem (`card-list-page()` sets that on `.custom-table`),
|
||||
// and this is the row's secondary value, so it wants to sit a step under the
|
||||
// file name rather than level with it. `text-caption-font-size` is a fixed
|
||||
// 0.75rem that does not move with the page's density tokens; the table token
|
||||
// does, and lands at 0.6875rem here — a visible step down that stays one if
|
||||
// the density scale is ever retuned.
|
||||
//
|
||||
// The fallback has to stay BELOW the row's 0.8125rem: set to that same
|
||||
// 0.8125rem it would render the timestamp at exactly the file name's size
|
||||
// the moment the token went missing, which is the one outcome this rule
|
||||
// exists to prevent.
|
||||
font-size: var(--gauzy-table-font-size, 0.75rem);
|
||||
}
|
||||
|
||||
// `min-width: 0` as well as on the wrapper: a flex item's default
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
</ng-template>
|
||||
<ng-template #visibleButton>
|
||||
<ng-template ngxPermissionsOnly="ALL_ORG_EDIT">
|
||||
<button nbButton status="success" size="small" (click)="openDialog(addTemplate, false)">
|
||||
<button nbButton class="action" status="success" size="small" (click)="openDialog(addTemplate, false)">
|
||||
<nb-icon icon="plus-outline"> </nb-icon>
|
||||
{{ 'BUTTONS.ADD' | translate }}
|
||||
</button>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</nb-card-body>
|
||||
</nb-card>
|
||||
<ng-template #visibleButton>
|
||||
<button (click)="openDialog(editableTemplate, false)" nbButton status="success" size="small">
|
||||
<button (click)="openDialog(editableTemplate, false)" nbButton class="action" status="success" size="small">
|
||||
<nb-icon icon="plus-outline"></nb-icon>
|
||||
{{ 'BUTTONS.ADD' | translate }}
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@use 'gauzy/_gauzy-dialogs' as *;
|
||||
@use 'gauzy/tabset-actions' as *;
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
@forward '../expenses/expense-categories/expense-categories.component';
|
||||
|
||||
:host {
|
||||
@@ -78,3 +79,20 @@ nb-card-body {
|
||||
.no-data{
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// ── The list-page contract, for a table built by hand ────────────────────────
|
||||
//
|
||||
// This page renders a stack of `nb-card` rows rather than the
|
||||
// `angular2-smart-table` every other page under `/pages/organization` does, so
|
||||
// the shared `list-page()` block — anchored on that element — never reached it,
|
||||
// and the page drifted: full-size toolbar buttons beside compacted ones on
|
||||
// Departments and Teams, and rows that were separately floating cards on the tab
|
||||
// body rather than rows on one surface.
|
||||
//
|
||||
// `card-list-page()` is the part of that contract which does not depend on the
|
||||
// smart table. The action row is the one thing this page positions itself: it
|
||||
// does not sit on the title line like the rest of the family, it sits ON the tab
|
||||
// strip (`tabset-action-row` above), so the toolbar scale is asked for by name
|
||||
// here rather than taken on `nb-card-header` — where this page has no buttons at
|
||||
// all.
|
||||
@include ga-hub.card-list-page('.gauzy-button-container');
|
||||
|
||||
+9
@@ -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 Job Employees, Payments, Invoices, Pipelines, Tasks and Employees carry,
|
||||
// so this table reads as the same surface as the rest of the app rather than as
|
||||
// one still drawn a size larger than its neighbours.
|
||||
@include ga-hub.list-page();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@forward '@shared/_pg-card';
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
// Everything else this sheet used to hold was a hand-rolled card grid
|
||||
// (`.flex-container` / `.flex-item` / `.info-line` / `.card-footer`) and six
|
||||
@@ -14,3 +15,13 @@
|
||||
// The `badge-*` names that the e2e page objects do select (`div.badge-success`
|
||||
// and friends, in the Invoices / Estimates / Proposals / Candidates specs) are
|
||||
// painted by those pages, not by this component-scoped sheet.
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// Density tokens, the table surface, row hover/selection, the status pills, the
|
||||
// pager and the toolbar scale — the same block Job Employees and the rest of the
|
||||
// list pages carry, so this table reads as the same surface as its neighbours
|
||||
// rather than as one drawn a size larger.
|
||||
//
|
||||
// The Status column here already routes through `ga-status-badge` (see
|
||||
// `statusMapper()`), which is exactly what the block's tint rules address.
|
||||
@include ga-hub.list-page();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@use '@shared/_pg-card' as *;
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
[nbButton].sharing.appearance-filled.status-primary {
|
||||
color: nb-theme(text-primary-color);
|
||||
@@ -6,8 +7,87 @@
|
||||
background-color: nb-theme(color-primary-transparent-default);
|
||||
}
|
||||
|
||||
:host {
|
||||
nb-card-body {
|
||||
height: calc($card-height - 0.5rem) !important;
|
||||
// ── Fitting the column ───────────────────────────────────────────────────────
|
||||
// REMOVED from here: `:host nb-card-body { height: calc($card-height - 0.5rem)
|
||||
// !important }`.
|
||||
//
|
||||
// `gauzy/_gauzy-cards` already gives the body that same chrome estimate, but as a
|
||||
// FLEX BASIS — `flex: 1 1 auto` then grows or shrinks it to whatever the card
|
||||
// actually has, so the estimate being a guess never mattered. Restating it at
|
||||
// page level with `!important` turned a starting point into a floor and a
|
||||
// ceiling. `/pages/tasks/dashboard`, which fits, pins that height only on its
|
||||
// `.project` variant; with none selected its body is a plain flex child, and that
|
||||
// is the whole difference.
|
||||
//
|
||||
// This page is routed straight into `nb-layout-column`, so unlike Inventory and
|
||||
// Projects its height chain was never broken — there is nothing else to do here.
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// Same surface, same density, same pager and same button scale as Job Employees,
|
||||
// Payments, Invoices and the rest of the list pages.
|
||||
//
|
||||
// The action row defaults to the whole card header, which this page needs: its
|
||||
// toolbar sits in a bare `<div class="align-self-end">` with no container class,
|
||||
// and the "Equipment Sharing" button sits beside the title, outside the toolbar
|
||||
// altogether.
|
||||
@include ga-hub.list-page();
|
||||
|
||||
// ── Cells that hard-code a box ───────────────────────────────────────────────
|
||||
// Table-scoped, not page-wide: `ga-card-grid` renders the very same components
|
||||
// for the card layout, where the full-size treatment belongs.
|
||||
:host .table-scroll-container ::ng-deep angular2-smart-table {
|
||||
// The Image column is a 74x60 photo and stays one: only the cell padding around
|
||||
// it goes back to what it was before the density pass.
|
||||
@include ga-hub.thumbnail-row-padding();
|
||||
|
||||
// ── Equipment 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. Sized off the shared `gauzy-people-*`
|
||||
// tokens, so this column matches the people cells every other list page
|
||||
// renders. Same treatment, and the same specificity notes, as Employees.
|
||||
ngx-avatar.report-table {
|
||||
.inner-wrapper.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);
|
||||
}
|
||||
|
||||
.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);
|
||||
|
||||
// SPECIFICITY: the avatar's own rule shims to `.report-table[_nghost]
|
||||
// .image-container[_ngc] img[_ngc]` — (0,5,1) — which no readable
|
||||
// combination of doubled classes here beats, so these two are `!important`,
|
||||
// the same register that rule already uses for its own `border-radius`.
|
||||
img {
|
||||
width: var(--gauzy-people-avatar-size, 1rem) !important;
|
||||
height: var(--gauzy-people-avatar-size, 1rem) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Auto Approve cell ──────────────────────────────────────────────────────
|
||||
// A bare tick or cross. `nb-icon` defaults to 1.5rem, half a rem taller than
|
||||
// the row's whole line box, so a column carrying nothing but a glyph was
|
||||
// setting the height of every row it appeared in. Nebular sizes the icon
|
||||
// entirely from `icon-*` tokens, so redefining them is the whole change — no
|
||||
// override of its rules, and nothing to keep in step if that sheet moves.
|
||||
ngx-auto-approve {
|
||||
--icon-font-size: var(--gauzy-table-line-height, 1rem);
|
||||
--icon-line-height: var(--gauzy-table-line-height, 1rem);
|
||||
--icon-width: var(--gauzy-table-line-height, 1rem);
|
||||
--icon-height: var(--gauzy-table-line-height, 1rem);
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
@@ -1,4 +1,5 @@
|
||||
@use 'gauzy/_gauzy-table' as *;
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
:host ngx-gauzy-button-action ::ng-deep {
|
||||
.transition-container span {
|
||||
@@ -9,3 +10,23 @@
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// The variant table is the same surface as the inventory list it hangs off, so
|
||||
// it takes the same density tokens, table surface and row hover/selection.
|
||||
// There is no pager here, but the block is the whole contract and splitting it
|
||||
// per page is how the list pages drifted apart in the first place.
|
||||
@include ga-hub.list-page('.gauzy-button-container');
|
||||
|
||||
// The Image column is `ImageRowComponent` (74x60), which keeps its size and its
|
||||
// padding; the Enabled column is `EnabledStatusComponent`, whose `.badge-success`
|
||||
// was painted only by `bootstrap.css` — a saturated fill with white text at 2.9:1.
|
||||
:host .table-scroll-container ::ng-deep angular2-smart-table {
|
||||
@include ga-hub.thumbnail-row-padding();
|
||||
@include ga-hub.status-badge-tints();
|
||||
|
||||
.badge-disabled {
|
||||
color: var(--gauzy-text-color-2, var(--text-hint-color));
|
||||
background-color: var(--gauzy-active-tint, rgba(126, 126, 143, 0.2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,71 @@ import { Component } from '@angular/core';
|
||||
@Component({
|
||||
selector: 'ga-inventory-layout-selector',
|
||||
template: `<div class="content"><router-outlet></router-outlet></div>`,
|
||||
styles: [
|
||||
`
|
||||
/* ── Passing the column's height through ──────────────────────────
|
||||
This component is the /pages/organization/inventory shell: its whole
|
||||
template is one wrapper around a router-outlet, and the items table,
|
||||
the item form and the item view render into it.
|
||||
|
||||
styles/_overrides.scss hands the routed page the viewport-bounded
|
||||
height the content column already has — but with
|
||||
"nb-layout-column > router-outlet + *", a DIRECT-child selector, so on
|
||||
a nested route it lands on THIS component and stops. Below it the page
|
||||
had no definite height for nb-card's "height: 100%" to resolve against,
|
||||
the card collapsed to its content, and the table inside it came out
|
||||
short of the card — which is what made its scrollbar short too.
|
||||
|
||||
WHY NOT ":host { display: flex }", which is the obvious way to hand a
|
||||
flex height on: it cannot win. pages.component.scss carries
|
||||
":host ::ng-deep router-outlet + * { display: block }", which shims to
|
||||
"[_nghost-pages] router-outlet + *" — (0,1,1) — and reaches EVERY routed
|
||||
component in the app, this one included. A component's own ":host" is
|
||||
"[_nghost-x]", (0,1,0), and loses. So the host here stays "display:
|
||||
block" whatever this file says, and a "flex: 1 1 auto" on the wrapper
|
||||
would have nothing to grow inside.
|
||||
|
||||
What the host DOES get from that same _overrides rule is "flex: 1 1
|
||||
auto" — a different property, so nothing overrides it — which makes its
|
||||
own height definite. So the wrapper takes 100% of it rather than trying
|
||||
to be a flex child of a box that is not a flex container, and the
|
||||
routed page grows inside the wrapper, which IS one. */
|
||||
:host {
|
||||
/* Releases the automatic minimum size (a flex item refuses to shrink
|
||||
below its content), so the page fits the column instead of driving
|
||||
it. Deliberately no "display" here: see the note above. */
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* ::ng-deep is required and is not a leak: the routed component is a
|
||||
SIBLING of the router-outlet in the DOM but belongs to a different
|
||||
component, so it carries none of this template's _ngcontent attribute
|
||||
and a plain "router-outlet + *" would shim to a selector matching
|
||||
nothing. Leading with :host keeps the rule inside this shell.
|
||||
|
||||
This sets no "display", so it neither fights nor is fought by the
|
||||
pages.component rule above — "flex" and "min-height" are ours alone. */
|
||||
:host ::ng-deep router-outlet {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host ::ng-deep router-outlet + * {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
`
|
||||
],
|
||||
standalone: false
|
||||
})
|
||||
export class InventoryComponent { }
|
||||
export class InventoryComponent {}
|
||||
|
||||
+62
@@ -1,4 +1,5 @@
|
||||
@use '@shared/_pg-card' as *;
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
// `@shared/_pg-card` forwards `gauzy/_gauzy-table`, so the shared `.action`,
|
||||
// `.actions`, `button`, `.gauzy-button-container` and `.card-custom-header`
|
||||
@@ -45,3 +46,64 @@
|
||||
nb-card-body {
|
||||
overflow: unset;
|
||||
}
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// Density tokens, the table surface, row hover/selection, the tag-chip stack,
|
||||
// the pager's `<nav><ul><li>` and the toolbar scale — the same block Job
|
||||
// Employees and the rest of the list pages carry.
|
||||
//
|
||||
// Emitted into all five sheets that take this file (product types, product
|
||||
// categories, merchants, warehouses, inventory items), which is the point: they
|
||||
// are the same card with the same `.table-scroll-container`,
|
||||
// `.pagination-container` and `.gauzy-button-container` five times over.
|
||||
@include ga-hub.list-page();
|
||||
|
||||
// ── Cells that hard-code a box ───────────────────────────────────────────────
|
||||
// Table-scoped, not page-wide: `ga-card-grid` renders the very same components
|
||||
// for the card layout, where the full-size treatment belongs.
|
||||
:host .table-scroll-container ::ng-deep angular2-smart-table {
|
||||
// The Image / Logo / Icon column of all five tables: `ImageRowComponent`
|
||||
// (74x60), `ItemImgTagsComponent` (70x64) and `IconRowComponent` (a 35px disc).
|
||||
// The pictures keep their size; only the cell padding around them goes back to
|
||||
// what it was before the density pass.
|
||||
@include ga-hub.thumbnail-row-padding();
|
||||
|
||||
// ── Name and description ───────────────────────────────────────────────────
|
||||
// `ItemImgTagsComponent` and `NameWithDescriptionComponent` both hard-code a
|
||||
// 14px/17px name over a 12px/15px caption, so this one column went on reading a
|
||||
// step louder than every other column in the row. Pointed at the table's own
|
||||
// sizes instead — the name at the cell size, the caption at the secondary-chip
|
||||
// size below it — which is a match rather than a reduction: these read whatever
|
||||
// the density tokens on this page say.
|
||||
//
|
||||
// The `margin-bottom: 4px` between the two lines is deliberately left alone.
|
||||
// It exists to separate them, and the picture beside them already sets the row
|
||||
// height here, so there is no height to win by closing it up.
|
||||
.name {
|
||||
font-size: var(--gauzy-table-font-size, 0.8125rem);
|
||||
line-height: var(--gauzy-table-line-height, 1.25rem);
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: var(--gauzy-table-chip-font-size, 0.6875rem);
|
||||
line-height: var(--gauzy-table-chip-line-height, 0.875rem);
|
||||
}
|
||||
|
||||
// ── Active / Inactive pill ─────────────────────────────────────────────────
|
||||
// `EnabledStatusComponent` emits a bare `<div class="badge badge-success">`,
|
||||
// so the only thing painting it was `bootstrap.css` (loaded whole in
|
||||
// `project.json`) — a saturated fill with white text, i.e. the filled colour
|
||||
// block the rest of the app has moved away from, and the inherited
|
||||
// `color-success-default` measures 2.9:1 on white. Same accent-on-tint
|
||||
// treatment `ga-status-badge` gets everywhere else.
|
||||
@include ga-hub.status-badge-tints();
|
||||
|
||||
// Its counterpart was a literal `#ccc` fill with whatever text colour it
|
||||
// inherited — the one chip in these tables that ignored the theme, and
|
||||
// invisible against the light card. The neutral tint is what the rest of the
|
||||
// app uses for a quiet fill and works on the near-black canvas too.
|
||||
.badge-disabled {
|
||||
color: var(--gauzy-text-color-2, var(--text-hint-color));
|
||||
background-color: var(--gauzy-active-tint, rgba(126, 126, 143, 0.2));
|
||||
}
|
||||
}
|
||||
|
||||
+7
-5
@@ -15,11 +15,13 @@
|
||||
(userRowSelect)="onUserRowSelect($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>
|
||||
</nb-card-body>
|
||||
<nb-card-footer>
|
||||
<button
|
||||
|
||||
+21
@@ -1,5 +1,6 @@
|
||||
@use "gauzy/_gauzy-table" as *;
|
||||
@use "gauzy/_gauzy-dialogs" as *;
|
||||
@use "gauzy/_gauzy-table-hub" as ga-hub;
|
||||
|
||||
:host {
|
||||
@include input-appearance(2rem, var(--gauzy-sidebar-background-3));
|
||||
@@ -37,3 +38,23 @@
|
||||
height: 79px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// The product picker is the same table as the page behind it, so it reads at the
|
||||
// same density, on the same surface, with the same pager `<nav><ul><li>`. No
|
||||
// action row here — the dialog's only buttons are its own header and footer —
|
||||
// so the toolbar argument is `null`.
|
||||
@include ga-hub.list-page(null);
|
||||
|
||||
// The pager sits beside a table that takes `flex-grow: 10` above, so it has to
|
||||
// opt out of the growth rather than fight it for the remainder.
|
||||
:host .pagination-container {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
// The Image column is `ImageRowComponent`, a 74x60 photo that keeps its size —
|
||||
// which is also what keeps the 79px select cell above in step with it. Only the
|
||||
// cell padding goes back to the pre-density default.
|
||||
:host .table-scroll-container ::ng-deep angular2-smart-table {
|
||||
@include ga-hub.thumbnail-row-padding();
|
||||
}
|
||||
|
||||
+7
-5
@@ -16,11 +16,13 @@
|
||||
(userRowSelect)="selectWarehouse($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>
|
||||
</nb-card-body>
|
||||
|
||||
<ng-template #visible>
|
||||
|
||||
+25
@@ -1,5 +1,6 @@
|
||||
@use "gauzy/gauzy-overrides" as *;
|
||||
@use "gauzy/gauzy-table" as *;
|
||||
@use "gauzy/_gauzy-table-hub" as ga-hub;
|
||||
|
||||
:host {
|
||||
@include input-appearance(2rem, var(--gauzy-sidebar-background-3));
|
||||
@@ -28,3 +29,27 @@
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// The warehouse's own product table is the same surface as every other list
|
||||
// page, so it takes the same density tokens, table surface, row hover/selection,
|
||||
// pager `<nav><ul><li>` and toolbar scale.
|
||||
//
|
||||
// The action row is named explicitly rather than left at the default: this
|
||||
// template has no `nb-card-header` at all — it IS a card body — and puts its
|
||||
// toolbar in a bare `<div class="d-flex justify-content-end">`. The button
|
||||
// component itself is the one stable handle, and every token the row needs is a
|
||||
// custom property, so they inherit into its own template from there.
|
||||
@include ga-hub.list-page('::ng-deep ngx-gauzy-button-action');
|
||||
|
||||
// The pager sits beside a table that takes `flex-grow: 10` above, so it has to
|
||||
// opt out of the growth rather than fight it for the remainder.
|
||||
:host .pagination-container {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
// The Image column is `ImageRowComponent`, a 74x60 photo that keeps its size;
|
||||
// only the cell padding around it goes back to the pre-density default.
|
||||
:host .table-scroll-container ::ng-deep angular2-smart-table {
|
||||
@include ga-hub.thumbnail-row-padding();
|
||||
}
|
||||
|
||||
+14
-11
@@ -1,17 +1,20 @@
|
||||
<nb-card>
|
||||
<nb-card-header class="d-flex flex-column pb-0">
|
||||
<!-- The heading and the section buttons are direct siblings of ONE
|
||||
`.card-header-title` row — the shape Employees and Equipment already
|
||||
use. They used to sit in a nested `.card-header-title` block of their
|
||||
own, which the outer row's `align-items: center` then centred against
|
||||
the TWO-line title block (the heading plus the breadcrumb trail parked
|
||||
under it), dropping them ~12px below `+ Add`. On the title's own row
|
||||
they share its line, and therefore `+ Add`'s. -->
|
||||
<div class="card-header-title">
|
||||
<div class="card-header-title">
|
||||
<h4>
|
||||
<ngx-header-title [allowEmployee]="false">
|
||||
{{ 'INVENTORY_PAGE.HEADER' | translate }}
|
||||
</ngx-header-title>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-header-title">
|
||||
<div class="mr-2">
|
||||
<ng-container [ngTemplateOutlet]="subActionButtons"></ng-container>
|
||||
</div>
|
||||
<h4>
|
||||
<ngx-header-title [allowEmployee]="false">
|
||||
{{ 'INVENTORY_PAGE.HEADER' | translate }}
|
||||
</ngx-header-title>
|
||||
</h4>
|
||||
<div class="sub-actions">
|
||||
<ng-container [ngTemplateOutlet]="subActionButtons"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gauzy-button-container">
|
||||
|
||||
+58
-2
@@ -1,6 +1,62 @@
|
||||
// `@shared/_pg-card` is loaded for its CSS, not for a member: it emits the page
|
||||
// frame (the card chain, the scroll-container flex rules) and loading it FIRST is
|
||||
// what puts that frame ahead of everything below, so the two blocks in this file
|
||||
// are resolving a cascade they can see rather than one they hope for. The
|
||||
// `product-types` forward brings the shared inventory-table treatment, which five
|
||||
// tables take from one place.
|
||||
@use '@shared/_pg-card' as *;
|
||||
@forward '../manage-product-types/product-types.component';
|
||||
|
||||
:host nb-card-body {
|
||||
height: calc($card-height - 0.5rem) !important;
|
||||
// ── Fitting the column ───────────────────────────────────────────────────────
|
||||
// REMOVED from here: `:host nb-card-body { height: calc($card-height - 0.5rem)
|
||||
// !important }`.
|
||||
//
|
||||
// `gauzy/_gauzy-cards` already gives the body that same chrome estimate, but as a
|
||||
// FLEX BASIS — `flex: 1 1 auto` then grows or shrinks it to whatever the card
|
||||
// actually has, so the estimate being a guess never mattered. Restating it at
|
||||
// page level with `!important` turned a starting point into a floor and a
|
||||
// ceiling. `/pages/tasks/dashboard`, which fits, pins that height only on its
|
||||
// `.project` variant; with none selected its body is a plain flex child, and that
|
||||
// is the whole difference.
|
||||
//
|
||||
// The other half of the fit is not in this file: see `inventory.component.ts`'s
|
||||
// own note for the height chain that has to reach this page before `nb-card
|
||||
// { height: 100% }` can resolve at all.
|
||||
|
||||
// ── The section buttons on the title's line ──────────────────────────────────
|
||||
// Merchants / Warehouses / Product categories / Product types are wrapped in ONE
|
||||
// block, so the title row's `justify-content: space-between`
|
||||
// (styles/_overrides.scss) pushes the four of them to the row's end as a group —
|
||||
// beside `+ Add` — instead of spreading them evenly across the row, which is what
|
||||
// four separate flex items would have got.
|
||||
//
|
||||
// Flex rather than a plain block, for the vertical axis: as inline-blocks in a
|
||||
// block box the buttons sit on a baseline, and the descender space under it makes
|
||||
// the box a few pixels taller than the buttons. The row centres that box, so the
|
||||
// group's own centre would land below the one `+ Add` is on — the offset this
|
||||
// change exists to remove, in miniature. A flex box is exactly the buttons' height.
|
||||
//
|
||||
// `flex-wrap` so a narrow viewport folds the group onto a second line rather than
|
||||
// squeezing the labels; `min-width: 0` so it may shrink at all, the flex default
|
||||
// being to refuse to go under the content's width.
|
||||
//
|
||||
// The two declarations that actually settle the vertical axis are `align-self`
|
||||
// and `min-height`, and they are the same pair `styles/_overrides.scss` puts on
|
||||
// `.ga-page-header-actions` — the block holding `+ Add`. Centring alone is not
|
||||
// enough, because the two blocks are centred in DIFFERENT boxes: the title row's
|
||||
// cross-size is whatever its tallest item is, and once `ngx-header-title` parks
|
||||
// the breadcrumb trail in this row the block runs two lines deep, so a centred
|
||||
// group lands ~12px below `+ Add`, which is centred on the title LINE. Anchoring
|
||||
// this group to the row's TOP and giving it `gauzy-page-title-row-height` — the
|
||||
// box the heading pads its line box out to — puts both groups in the same box,
|
||||
// at the same top edge, so they share one centre by construction rather than by
|
||||
// the two blocks happening to measure the same.
|
||||
:host nb-card-header .card-header-title > .sub-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
align-self: flex-start;
|
||||
min-height: var(--gauzy-page-title-row-height, 2.125rem);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
+24
-1
@@ -1,3 +1,5 @@
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
.featured-image {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -100,4 +102,25 @@ img.featured-image {
|
||||
nb-card-body {
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// The variants table on the item view is the same surface as the inventory list
|
||||
// it hangs off, so it takes the same density tokens, table surface and row
|
||||
// hover/selection. There is no pager and no action row here, but the block is
|
||||
// the whole contract and splitting it per page is how the list pages drifted
|
||||
// apart in the first place.
|
||||
@include ga-hub.list-page(null);
|
||||
|
||||
// The Image column is `ImageRowComponent` (74x60), which keeps its size and its
|
||||
// padding; the Enabled column is `EnabledStatusComponent`, whose `.badge-success`
|
||||
// was painted only by `bootstrap.css` — a saturated fill with white text at 2.9:1.
|
||||
:host .table-scroll-container ::ng-deep angular2-smart-table {
|
||||
@include ga-hub.thumbnail-row-padding();
|
||||
@include ga-hub.status-badge-tints();
|
||||
|
||||
.badge-disabled {
|
||||
color: var(--gauzy-text-color-2, var(--text-hint-color));
|
||||
background-color: var(--gauzy-active-tint, rgba(126, 126, 143, 0.2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template #visibleButton>
|
||||
<button nbButton status="success" size="small" (click)="openDialog(addTemplate, false)">
|
||||
<button nbButton class="action" status="success" size="small" (click)="openDialog(addTemplate, false)">
|
||||
<nb-icon icon="plus-outline"> </nb-icon>
|
||||
{{ 'BUTTONS.ADD' | translate }}
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
@forward '@shared/_pg-card';
|
||||
@use '@shared/_pg-card' as *;
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
:host {
|
||||
.custom-body-content {
|
||||
@@ -7,3 +9,118 @@
|
||||
height: calc(100vh - 15.25rem) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE ON FITTING THE COLUMN: this page is reached through a routing shell
|
||||
// (`ngx-project-layout`), so the rule that hands the content column's height to
|
||||
// the routed page — `nb-layout-column > router-outlet + *` in
|
||||
// `styles/_overrides.scss`, a DIRECT-child selector — never reached it, and
|
||||
// `nb-card { height: 100% }` had nothing to resolve against. That is repaired in
|
||||
// the shell's own stylesheet, which is the only place it can be; this file needs
|
||||
// nothing for it.
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// Density tokens, the table surface, row hover/selection, the tag-chip stack,
|
||||
// the pager's `<nav><ul><li>` and the toolbar scale — the same block Job
|
||||
// Employees and the rest of the list pages carry.
|
||||
//
|
||||
// The action row defaults to the whole card header, which this page needs: its
|
||||
// toolbar sits in a bare `<div class="d-flex justify-content-end">` with no
|
||||
// container class of its own.
|
||||
@include ga-hub.list-page();
|
||||
|
||||
// ── Cells that hard-code a box ───────────────────────────────────────────────
|
||||
// Table-scoped, not page-wide: `ga-card-grid` renders the very same components
|
||||
// for the card layout, where the full-size treatment belongs.
|
||||
//
|
||||
// SPECIFICITY: each renderer nests its rules under its own `:host` or under a
|
||||
// class carrying its own content attribute, so the tightest of them reaches
|
||||
// (0,3,1). The `:host .table-scroll-container ::ng-deep` prefix here carries two
|
||||
// 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-render {
|
||||
gap: var(--gauzy-people-gap, 0.375rem);
|
||||
|
||||
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.
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: var(--gauzy-people-font-size, 0.6875rem);
|
||||
line-height: var(--gauzy-people-avatar-size, 1rem);
|
||||
}
|
||||
|
||||
span .member {
|
||||
font-size: var(--gauzy-table-chip-font-size, 0.625rem);
|
||||
line-height: var(--gauzy-table-chip-line-height, 0.75rem);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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
|
||||
// line height alone is half again the row's whole 1rem line box, and the
|
||||
// switch is twice it.
|
||||
//
|
||||
// Because every one of those is `nb-theme(...)` — i.e. `var(--toggle-*)` under
|
||||
// `$nb-enable-css-custom-properties` — redefining the tokens is the entire
|
||||
// fix: no override of Nebular's own rules, so no specificity to fight and
|
||||
// nothing to keep in step if that sheet changes. The knob follows the switch,
|
||||
// which Nebular positions with `calc(100% - switcher - border - border)` plus
|
||||
// a 1px margin.
|
||||
//
|
||||
// 20x12 keeps Nebular's own 1.667 width:height proportion, so it reads as the
|
||||
// same control drawn smaller rather than a squashed one — the same numbers
|
||||
// the Employees page gives its Screen Capture column.
|
||||
gauzy-visibility {
|
||||
--toggle-height: 0.75rem;
|
||||
--toggle-width: 1.25rem;
|
||||
// `height - 2px`: the knob carries a 1px margin, so this is what centres it
|
||||
// and leaves the same 1px inset at each end the stock proportions give.
|
||||
--toggle-switcher-size: 0.625rem;
|
||||
--toggle-switcher-icon-size: 0.375rem;
|
||||
--toggle-text-font-size: var(--gauzy-table-header-font-size, 0.75rem);
|
||||
--toggle-text-line-height: var(--gauzy-table-badge-height, 1rem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,58 @@
|
||||
@use 'themes' as *;
|
||||
|
||||
// ── Passing the column's height through ──────────────────────────────────────
|
||||
// This component is the `/pages/organization/projects` shell: its whole template
|
||||
// is a `<router-outlet>`, and the list, create and edit pages render into it.
|
||||
//
|
||||
// `styles/_overrides.scss` hands the routed page the viewport-bounded height the
|
||||
// content column already has — but with `nb-layout-column > router-outlet + *`,
|
||||
// a DIRECT-child selector, so on a nested route it lands on THIS component and
|
||||
// stops. Below it the page had no definite height for `nb-card { height: 100% }`
|
||||
// to resolve against, the card collapsed to its content, and the table inside it
|
||||
// came out short of the card — which is what made its scrollbar short too.
|
||||
//
|
||||
// WHY NOT `:host { display: flex }`, which is the obvious way to hand a flex
|
||||
// height on: it cannot win. `pages.component.scss` carries
|
||||
// `:host ::ng-deep router-outlet + * { display: block }`, which shims to
|
||||
// `[_nghost-pages] router-outlet + *` — (0,1,1) — and reaches EVERY routed
|
||||
// component in the app, this one included. A component's own `:host` is
|
||||
// `[_nghost-x]`, (0,1,0), and loses. So this host stays `display: block` whatever
|
||||
// this file says.
|
||||
//
|
||||
// What the host DOES get from that same `_overrides` rule is `flex: 1 1 auto` —
|
||||
// a different property, so nothing overrides it — which makes its own height
|
||||
// definite. A percentage resolves against that perfectly well, and unlike a flex
|
||||
// child it does not care what the parent's `display` ended up being. So the
|
||||
// routed page simply takes 100% of the shell.
|
||||
//
|
||||
// `::ng-deep` is required and is not a leak: the routed component is a SIBLING of
|
||||
// `<router-outlet>` in the DOM but belongs to a different component, so it
|
||||
// carries none of this template's `_ngcontent` attribute and a plain
|
||||
// `router-outlet + *` would shim to a selector matching nothing. Leading with
|
||||
// `:host` keeps the rule inside this shell.
|
||||
:host {
|
||||
nb-card {
|
||||
nb-card-body {
|
||||
overflow: unset;
|
||||
}
|
||||
// Releases the automatic minimum size (a flex item refuses to shrink below its
|
||||
// content), so the page fits the column instead of driving it. Deliberately no
|
||||
// `display` here: see the note above.
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
|
||||
// The routed page's card scrolls in its own table container, so the body must
|
||||
// not clip what that container hangs outside it (the sticky head, a dropdown).
|
||||
nb-card nb-card-body {
|
||||
overflow: unset;
|
||||
}
|
||||
}
|
||||
|
||||
:host ::ng-deep router-outlet {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// No `display` set, so this neither fights nor is fought by the
|
||||
// `pages.component` rule above — `height` and `min-height` are ours alone.
|
||||
:host ::ng-deep router-outlet + * {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
<div>
|
||||
<!--
|
||||
The tag's own colour is handed to the stylesheet as a TOKEN rather than as a
|
||||
finished `background` / `color` pair, because the chip now derives three
|
||||
values from it (a wash, an edge and a label) and an inline style would win
|
||||
over every one of them. `background()` normalises whatever the record holds
|
||||
into a valid hex, so the token is always a colour the stylesheet can mix.
|
||||
-->
|
||||
<nb-badge
|
||||
class="color"
|
||||
position="relative"
|
||||
[style.background]="background(rowData.color)"
|
||||
[style.color]="backgroundContrast(rowData.color)"
|
||||
[style.--tag-color]="background(rowData.color)"
|
||||
[text]="rowData.name"
|
||||
></nb-badge>
|
||||
</div>
|
||||
|
||||
@@ -2,35 +2,91 @@
|
||||
* The tag-name cell of the Tags table: an `nb-badge` painted with the tag's own
|
||||
* colour.
|
||||
*
|
||||
* `nb-badge` sets `position: absolute` on its own host, so it contributes no
|
||||
* height at all — the wrapper below has to state one, and the chip's own numbers
|
||||
* have to add up to it or the chip spills out of the box that is supposed to
|
||||
* hold it. They did not: 12.5px of padding around a 17px line is a 42px chip in
|
||||
* a 32px frame, and the 19px margins above and below were what kept the overflow
|
||||
* from colliding with the rows either side. That cost every row ~70px, in a
|
||||
* table the density pass had just brought down to ~34px.
|
||||
* The chip is sized by its LABEL, not by the column. It used to be
|
||||
* `width: 100%`, which drew every tag as a full-bleed colour bar of identical
|
||||
* length — ten saturated blocks stacked down the left edge of the table, all the
|
||||
* same size whether the label was "VIP" or "dependencies". Sized to its text it
|
||||
* reads as what it is (a tag), the colour returns to being an accent rather than
|
||||
* the loudest thing on the page, and the names line up on their left edge under
|
||||
* the "Name" heading instead of floating in the middle of the column.
|
||||
*
|
||||
* Both boxes now come from the shared in-cell badge tokens and are the same
|
||||
* height, so the chip fills its frame instead of bursting it and this column
|
||||
* costs a row no more than any other chip in the app. The tokens live in
|
||||
* `$gauzy-density` (themes.scss), which is merged into all eight themes; the
|
||||
* fallbacks are their current values, for the two material themes that register
|
||||
* against Nebular's own themes rather than the gauzy default.
|
||||
* `nb-badge` sets `position: absolute` on its own host (badge.component.scss) —
|
||||
* it is built to be pinned to a corner of something. That is what forced the
|
||||
* wrapper below to state a height for a chip that contributed none, and an
|
||||
* absolutely-positioned box cannot take part in the wrapper's flex layout. One
|
||||
* `position: relative` puts it back in flow, so the wrapper aligns it like any
|
||||
* other child and the two boxes can no longer disagree about their height.
|
||||
*
|
||||
* Both boxes come from the shared in-cell badge tokens and are the same height,
|
||||
* so this column costs a row no more than any other chip in the app. The tokens
|
||||
* live in `$gauzy-density` (themes.scss), which is merged into all eight themes;
|
||||
* the fallbacks are their current values, for the two material themes that
|
||||
* register against Nebular's own themes rather than the gauzy default.
|
||||
*
|
||||
* COLOUR: the chip is a WASH of the tag's colour, not a block of it. The tag
|
||||
* palette is user data and most of it is fully saturated — pure red, pure yellow,
|
||||
* pure green — so ten flat chips down the left edge were the highest-chroma thing
|
||||
* on a page whose every other surface is a near-neutral grey, and the first
|
||||
* column out-shouted the description the row is actually read for. At 18% the
|
||||
* hue still names the tag from across the table while the row reads as a row.
|
||||
*
|
||||
* All three values below are mixed from one token, `--tag-color`, which the
|
||||
* template sets from the record (see the note there). Mixing with `transparent`
|
||||
* in srgb is premultiplied, so the wash is the tag's own hue at 18% alpha — NOT
|
||||
* the hue darkened toward black, which is what a naive `rgba()` over an unknown
|
||||
* surface would have given on the light themes.
|
||||
*/
|
||||
.color {
|
||||
width: 100%;
|
||||
// Back into normal flow — see the note above.
|
||||
position: relative;
|
||||
// A flex item, so this computes to `block`: a block container is what
|
||||
// `text-overflow` needs, which an `inline-flex` chip would not give it.
|
||||
display: inline-block;
|
||||
// Never wider than its cell; a long name is truncated with an ellipsis
|
||||
// instead of forcing the column open. `min-width` because a flex item's `auto` minimum is its
|
||||
// min-content width — with `nowrap` that is the whole label, i.e. the cap
|
||||
// above would never be reached.
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
// Exactly one line box, matching the wrapper, with the line height doing the
|
||||
// vertical centering.
|
||||
height: var(--gauzy-table-badge-height, 1.25rem);
|
||||
padding: 0 var(--gauzy-table-chip-padding-x, 0.375rem);
|
||||
border-radius: 4px;
|
||||
// The control step of the radius scale (themes.scss) — the step tags, badges
|
||||
// and buttons share, including the filter chips in the rail beside this table.
|
||||
border-radius: var(--gauzy-radius-sm, 0.375rem);
|
||||
// An 18% wash of the tag's colour. The surface under it is the row, so the chip
|
||||
// tints whatever it lands on and needs no per-theme value of its own.
|
||||
background: color-mix(in srgb, var(--tag-color, transparent) 18%, transparent);
|
||||
// The edge is what keeps a washed chip a CHIP: at 18% a three-letter label like
|
||||
// "VIP" is a smudge with no shape to it, and two chips in adjacent rows have no
|
||||
// boundary between them. `inset` rather than a `border` so the ring costs the
|
||||
// box no width — the chip is sized to a fixed line box below.
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tag-color, transparent) 40%, transparent);
|
||||
// The tag name is this column's primary value, so it stays at the table's own
|
||||
// cell size rather than dropping to the smaller secondary-chip step.
|
||||
font-size: var(--gauzy-table-font-size, 0.8125rem);
|
||||
font-weight: 600;
|
||||
line-height: var(--gauzy-table-badge-height, 1.25rem);
|
||||
letter-spacing: 0em;
|
||||
text-align: center;
|
||||
/*
|
||||
* The LABEL keeps the tag's hue, but pulled toward the theme's own text
|
||||
* colour — the one lever that flips with the theme: dark text darkens the label
|
||||
* on the light surfaces, light text lightens it on the dark ones, so a single
|
||||
* declaration covers all eight themes with no `:host-context` branch. (The
|
||||
* dashboard switcher's default-star is mixed the same way, for the same
|
||||
* reason.)
|
||||
*
|
||||
* The white/black pick this replaces cannot survive the wash: it was computed
|
||||
* against the FLAT tag colour, and a flat black chip and an 18% black wash want
|
||||
* OPPOSITE labels — white text on the wash is unreadable on the light themes.
|
||||
*
|
||||
* 45% is sized for the pathological tags, the ones whose colour IS the surface
|
||||
* colour: a black tag on a dark theme and a white tag on a light one both land
|
||||
* near 4.7:1, and every tag that is actually a colour sits far above it (a blue
|
||||
* tag on a light theme is ~11:1). 55% takes yellow-on-light down to 3.5:1.
|
||||
*/
|
||||
color: color-mix(in srgb, var(--tag-color, currentColor) 45%, var(--text-basic-color));
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -42,7 +98,9 @@ div {
|
||||
// Was `height: 100%` immediately followed by `height: 32px` — the first never
|
||||
// applied. One height now, and it is the height the chip above is drawn at.
|
||||
height: var(--gauzy-table-badge-height, 1.25rem);
|
||||
justify-content: center;
|
||||
// Under the "Name" heading, which is left-aligned like every other heading in
|
||||
// the table. Centring only ever looked deliberate while the chip was as wide
|
||||
// as the column and there was nothing left to centre.
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -7,12 +7,30 @@
|
||||
</nb-card-header>
|
||||
@if (filterOptions.length) {
|
||||
<div class="list">
|
||||
<nb-list>
|
||||
@for (option of filterOptions; track option) {
|
||||
<!-- These options filter the table by ONE tag type at a time: picking
|
||||
one clears the last. `role="button"` + `aria-pressed` announced
|
||||
them as independent toggles, so a screen reader user was never
|
||||
told that the previous choice had been turned off. A radiogroup
|
||||
says exactly that, and carries the single-tab-stop + arrow-key
|
||||
behaviour such a user expects from it (`onFilterKeydown`). -->
|
||||
<nb-list role="radiogroup" [attr.aria-label]="'TAGS_PAGE.TAGS_TYPE' | translate">
|
||||
<!-- Tracked by VALUE, not by the option object: every refresh builds
|
||||
a fresh list of fresh literals, so identity tracking tore down
|
||||
and rebuilt every radio — including the one the keyboard user
|
||||
was standing on, which sent focus to the document. A tag type's
|
||||
id (and `''` for All) is the same across those rebuilds, so the
|
||||
elements are reused and focus stays where it was put. -->
|
||||
@for (option of filterOptions; track option.value; let i = $index) {
|
||||
<nb-list-item
|
||||
class="filter-item"
|
||||
role="radio"
|
||||
[attr.aria-checked]="selectedFilterValue === option.value"
|
||||
[attr.tabindex]="i === filterTabStopIndex ? 0 : -1"
|
||||
[class.selected]="selectedFilterValue === option.value"
|
||||
(click)="selectedFilterOption(option.value)"
|
||||
(keydown.enter)="selectedFilterOption(option.value)"
|
||||
(keydown.space)="$event.preventDefault(); selectedFilterOption(option.value)"
|
||||
(keydown)="onFilterKeydown($event, i)"
|
||||
>
|
||||
{{ option.displayName }}
|
||||
</nb-list-item>
|
||||
@@ -54,9 +72,11 @@
|
||||
style="cursor: pointer"
|
||||
></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>
|
||||
} @else {
|
||||
<ga-card-grid
|
||||
[loading]="loading"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@use 'gauzy/_gauzy-cards' as *;
|
||||
@use 'gauzy/_gauzy-table' as *;
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
|
||||
/*
|
||||
* Every `var(--gauzy-…)` below carries a fallback. `material-light` and
|
||||
@@ -12,26 +13,41 @@
|
||||
*/
|
||||
|
||||
:host .search {
|
||||
margin: 20px 0 0 0;
|
||||
// 20px of air under a 17px heading was the widest gap on the page, in the
|
||||
// narrowest column on it.
|
||||
margin: 0.5rem 0 0;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
background-color: var(--gauzy-sidebar-background-3, rgba(126, 126, 143, 0.1));
|
||||
border-radius: 8px;
|
||||
height: 32px;
|
||||
padding: 0 8px;
|
||||
// The radius scale (themes.scss) has one step for things that read as a
|
||||
// CONTROL — buttons, selects, tags, badges — and this field, the chips below
|
||||
// it and the buttons in the page header are all of them.
|
||||
border-radius: var(--gauzy-radius-sm, 0.375rem);
|
||||
height: 1.75rem;
|
||||
padding: 0 0.5rem;
|
||||
|
||||
::ng-deep {
|
||||
i.control-icon::before {
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
height: 1.75rem;
|
||||
background-color: transparent;
|
||||
box-shadow: unset;
|
||||
margin: 0 2px;
|
||||
// `text-inputs-overrides` puts a `min-height: $default-height` (~2.375rem)
|
||||
// floor on every bare `input` in the app, so this field was drawn some
|
||||
// 10px taller than the pill around it — invisible only because the field is
|
||||
// transparent and borderless, but the click target and the caret were both
|
||||
// outside the box. The floor is a floor for FORM fields; this is a filter
|
||||
// affordance in a rail, so it opts out and takes the pill's height.
|
||||
min-height: 0;
|
||||
// One step under the app's control text size: the rail is a filter, and
|
||||
// nothing in it should read larger than the table it filters.
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,15 +66,47 @@
|
||||
* its own `:hover`.
|
||||
*/
|
||||
.filter-item {
|
||||
padding: 10px 15px;
|
||||
// A chip is a control, so it takes the control step of the radius scale
|
||||
// (themes.scss: 0.375rem for buttons, selects, tags, badges, menu rows)
|
||||
// rather than the 20px stadium it was drawn at. That scale exists because a
|
||||
// near-full pill on a ~32px box is the one shape that never matches anything
|
||||
// around it: the search field above these chips, the "+ Add" button in the
|
||||
// header and the colour badges in the table are now all the same corner.
|
||||
border-radius: var(--gauzy-radius-sm, 0.375rem);
|
||||
box-shadow: var(--gauzy-shadow, 0 1px 1px 0 rgba(0, 0, 0, 0.15));
|
||||
background: var(--gauzy-card-3, rgba(126, 126, 143, 0.05));
|
||||
border-radius: 20px;
|
||||
height: 36px;
|
||||
// `nb-list-item` draws a `list-item-divider` along its bottom edge — right for
|
||||
// a stacked list, wrong for pills in a wrapping row, where it left a hairline
|
||||
// hanging under each chip.
|
||||
// a stacked list, wrong for chips in a wrapping row, where it left a hairline
|
||||
// hanging under each one.
|
||||
border: none;
|
||||
|
||||
// A tag type is free text, and "Organization Department" is one of the types
|
||||
// Gauzy ships: a chip that can neither wrap nor shrink is simply wider than
|
||||
// the rail it sits in, so its label was cut off by the card edge and the list
|
||||
// grew a horizontal scrollbar. Capped at the rail's width, a long label wraps
|
||||
// INSIDE the chip, which a wrapping row has room for — the column has 230px
|
||||
// across and the whole page down.
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: 100%;
|
||||
// 1.75rem is the app compact control height (`gauzy-header-control-height`,
|
||||
// and the size the pager draws its own buttons at); `min-height` rather than
|
||||
// `height` because a two-line label has to be allowed past it.
|
||||
min-height: 1.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
// Two steps down from the 14px it inherited. These are filter controls, not
|
||||
// body copy: at 12px the rail is set at the same size as the table headings
|
||||
// beside it instead of out-typing the rows it filters, and it fits more chips
|
||||
// per row, which is what keeps a twelve-type rail from needing a scroll.
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.15;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
// Last resort for a single unbroken word, which has no space to wrap at.
|
||||
overflow-wrap: anywhere;
|
||||
transition: background-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
// Underlining the label was the only hover feedback a chip had, which reads as a
|
||||
@@ -69,6 +117,15 @@
|
||||
background: var(--gauzy-hover-tint, rgba(126, 126, 143, 0.12));
|
||||
}
|
||||
|
||||
// These chips are the page's only filter control and they are plain
|
||||
// `nb-list-item`s, so nothing draws a focus ring for them by default. The
|
||||
// template makes them reachable (`role` / `tabindex` / Enter / Space); this is
|
||||
// what makes the reached one visible.
|
||||
.filter-item:focus-visible {
|
||||
outline: 2px solid var(--color-primary-500, #6e49e8);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
// Which type is currently filtering the table. Selecting one used to change the
|
||||
// table and leave the rail looking untouched, so the only way to tell what you
|
||||
// were looking at was to remember what you clicked. Declared after `:hover` at
|
||||
@@ -78,6 +135,12 @@
|
||||
background: var(--gauzy-active-tint, rgba(126, 126, 143, 0.2));
|
||||
color: nb-theme(text-basic-color);
|
||||
font-weight: 600;
|
||||
// The tint alone is one step off hover, and the resting chip already carries a
|
||||
// neutral hairline of its own (`gauzy-shadow`), so a second neutral ring would
|
||||
// have changed nothing: which of a dozen chips is filtering the table has to be
|
||||
// findable from across the page. The accent ring is the one thing in the rail
|
||||
// that is not grey, and it marks the chip without filling it in as a block.
|
||||
box-shadow: inset 0 0 0 1px var(--color-primary-500, #6e49e8);
|
||||
}
|
||||
|
||||
:host nb-card.filter {
|
||||
@@ -87,14 +150,25 @@
|
||||
@include nb-rtl(padding-left, 4px);
|
||||
.list {
|
||||
max-height: calc(100vh - 15.8rem);
|
||||
overflow: auto;
|
||||
// Vertical only. The chips are capped at the rail's width now, so there is
|
||||
// nothing left to scroll across — and a horizontal bar under a wrapping list
|
||||
// was never the answer to a label that did not fit.
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
// "Tags type" is a section label over a rail of filters, not a page title: at
|
||||
// 0.75rem/600 it is set exactly like the column headings in the table beside
|
||||
// it, which is the same job. It was the largest type on this half of the page
|
||||
// and sat over the smallest controls on it.
|
||||
nb-card-header {
|
||||
font-size: 14px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 17px;
|
||||
line-height: 1rem;
|
||||
letter-spacing: 0em;
|
||||
// The default card-header padding (0.875rem 1rem) is sized for a card title
|
||||
// with nothing under it; this one carries the search field too.
|
||||
padding: 0.75rem 0.75rem 0.625rem;
|
||||
@include nb-ltr(padding-left, 12px);
|
||||
@include nb-rtl(padding-right, 12px);
|
||||
}
|
||||
@@ -103,10 +177,13 @@
|
||||
:host nb-list {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
// Each chip keeps its own height: a two-line one would otherwise stretch the
|
||||
// single-line chips beside it into tall half-empty boxes.
|
||||
align-items: flex-start;
|
||||
@include nb-ltr(padding-left, 10px);
|
||||
@include nb-rtl(padding-right, 10px);
|
||||
padding-bottom: 21px;
|
||||
gap: 10px;
|
||||
padding-bottom: 1rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.gauzy-action {
|
||||
@@ -115,7 +192,44 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// ── The canonical list-page block ────────────────────────────────────────────
|
||||
// Density tokens, the table surface, row hover/selection, the tag-chip stack,
|
||||
// the pager's `<nav><ul><li>` and the toolbar scale — the same block Job
|
||||
// Employees and the rest of the list pages carry.
|
||||
//
|
||||
// The action row defaults to the whole card header, which covers this page's
|
||||
// `.gauzy-action` toolbar — it lays its header out itself, beside the tag-type
|
||||
// rail, and never adopted the shared container class.
|
||||
//
|
||||
// ABOVE the block below rather than beneath it: that block reopens `:host` to
|
||||
// widen two of the density tokens this one sets there, and a custom property is
|
||||
// won by whichever declaration the cascade reads last.
|
||||
@include ga-hub.list-page();
|
||||
|
||||
:host {
|
||||
// ── Row breathing room ─────────────────────────────────────────────────────
|
||||
//
|
||||
// `density-tokens()` sizes a row for a line of TEXT: 1rem of line box plus
|
||||
// 0.1875rem above and below, i.e. ~22px. That is the right box when the cell's
|
||||
// tallest thing is its text, and it is what the tables that carry a wrapping
|
||||
// note or a two-line address want.
|
||||
//
|
||||
// This table is four short single-line values and a chip, so nothing ever
|
||||
// pushes a row past the minimum and every row came out at that floor — a block
|
||||
// of ten rows with no air between them, where the eye has nothing to track
|
||||
// along. The cells take the app-wide default instead (0.375rem, the same value
|
||||
// `thumbnail-row-padding()` hands the picture tables), which puts the rows back
|
||||
// at ~28px.
|
||||
//
|
||||
// Scoped to this page rather than pushed into `density-tokens()`: the shared
|
||||
// value is what the rest of the list pages are drawn at, and widening it there
|
||||
// would reflow two dozen tables that nobody has asked about.
|
||||
--gauzy-table-cell-padding-y: 0.375rem;
|
||||
// And the heading row with them, or the header ends up the shortest row in a
|
||||
// table it is supposed to head (0.8125rem of label in a 23px box against 28px
|
||||
// rows).
|
||||
--gauzy-table-header-padding-y: 0.4375rem;
|
||||
|
||||
nb-card.tags-component,
|
||||
nb-card.tags-component nb-card-body {
|
||||
background-color: var(--gauzy-card-2, rgba(126, 126, 143, 0.08));
|
||||
@@ -150,3 +264,82 @@
|
||||
margin: 0 -10px;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Column treatments ────────────────────────────────────────────────────────
|
||||
//
|
||||
// `classHeader` and `classContent` are angular2-smart-table's own per-column
|
||||
// class hooks: the first lands on the `th`, the second on the div a cell renders
|
||||
// its value into. (They are NOT the `class` key several tables in this repo
|
||||
// pass: `class` is declared on the library's `IColumn`, so it type-checks, but
|
||||
// its `Column` class never reads it and it reaches no element — which is why the
|
||||
// Name column's `text-center` never did anything.) Both land inside the
|
||||
// library's own template, so these rules have to cross that boundary.
|
||||
|
||||
// A count belongs on its units digit: left-aligned, "3990" and "1890000" start
|
||||
// in the same place and say nothing about which is larger. `tabular-nums` fixes
|
||||
// the digit advance too — Inter's default figures are proportional, so a column
|
||||
// of them wanders by a pixel or two per row.
|
||||
:host ::ng-deep angular2-smart-table table td .ga-numeric-cell {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
// …and the heading over those numbers, on the same edge.
|
||||
//
|
||||
// `a.sort` is laid out `row-reverse` (that is how the sort caret ends up BEFORE
|
||||
// the label — see `_overrides.scss`), so the END of its main axis is the LEFT
|
||||
// edge: `flex-start` is what pins it right. `text-align` does nothing at all to
|
||||
// a flex box, which is why this names the flex property instead.
|
||||
//
|
||||
// SPECIFICITY: the global rule is `angular2-smart-table table
|
||||
// tr.angular2-smart-titles th a.sort` — (0,2,4). The host attribute that
|
||||
// `::ng-deep` leaves in front, plus `.ga-numeric-cell`, take this to (0,3,4).
|
||||
:host ::ng-deep angular2-smart-table table th.ga-numeric-cell {
|
||||
a.sort {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
// The same column with sorting turned off renders a plain span instead.
|
||||
.angular2-smart-sort {
|
||||
display: block;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
// RTL: the heading has to keep following the digits.
|
||||
//
|
||||
// The cells align on the units digit, and a number's units digit is its
|
||||
// rightmost one in every script — so `text-align: right` above is deliberately
|
||||
// PHYSICAL and the column stays right-aligned under RTL. The plain-span heading
|
||||
// is physical for the same reason and needs nothing here.
|
||||
//
|
||||
// `a.sort` is the one that drifts, because `justify-content` is resolved against
|
||||
// the flex container's own direction rather than against the page: in RTL the
|
||||
// `row-reverse` main axis runs left-to-right, so the `flex-start` above swings
|
||||
// the label to the LEFT while its column stays right. `flex-end` is the same
|
||||
// physical edge under RTL that `flex-start` is under LTR.
|
||||
//
|
||||
// `:host-context()` and not `nb-rtl()`: that mixin expands to `[dir=rtl] &`,
|
||||
// which here would ask for a `:host` nested inside `[dir=rtl]` and match nothing
|
||||
// — the same trap documented over `.ga-has-filter-toggle` in
|
||||
// `_gauzy-table-filters.scss`.
|
||||
:host-context([dir='rtl']) ::ng-deep angular2-smart-table table th.ga-numeric-cell a.sort {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
// The tag type is the row's SECONDARY fact — the coloured name chip beside it is
|
||||
// the primary one, and the description is what the row is actually read for. In
|
||||
// the table's muted text colour (the one its own headings use) the three stop
|
||||
// competing at equal weight, and the em dash a tag with no type falls back to
|
||||
// reads as "deliberately empty" rather than as content.
|
||||
:host ::ng-deep angular2-smart-table table td .ga-secondary-cell {
|
||||
color: var(--gauzy-text-color-2, var(--text-hint-color));
|
||||
}
|
||||
|
||||
// The pager is a flex child of a flex-column card body, so it must not be
|
||||
// stretched by the table beside it — `.table-scroll-container` above takes
|
||||
// `flex-grow: 10`, which would otherwise leave the pager fighting for the
|
||||
// remainder.
|
||||
:host .pagination-container {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@@ -238,12 +238,15 @@ export class TagsComponent extends PaginationFilterBaseComponent implements Afte
|
||||
display: false,
|
||||
perPage: pagination ? pagination.itemsPerPage : this.minItemPerPage
|
||||
},
|
||||
// The four widths are a RATIO the library hands to the `<th>`s, so they
|
||||
// have to add up to the table: 20/20/70/10 came to 120%, which is why
|
||||
// Description alone took better than half the row and the other three
|
||||
// were squeezed into what was left.
|
||||
columns: {
|
||||
name: {
|
||||
title: this.getTranslation('TAGS_PAGE.TAGS_NAME'),
|
||||
type: 'custom',
|
||||
width: '20%',
|
||||
class: 'text-center',
|
||||
width: '22%',
|
||||
renderComponent: TagsColorComponent,
|
||||
componentInitFunction: (instance: TagsColorComponent, cell: Cell) => {
|
||||
instance.rowData = cell.getRow().getData();
|
||||
@@ -253,26 +256,46 @@ export class TagsComponent extends PaginationFilterBaseComponent implements Afte
|
||||
tagTypeName: {
|
||||
title: this.getTranslation('TAGS_PAGE.TAGS_TYPE'),
|
||||
type: 'string',
|
||||
width: '20%',
|
||||
isFilterable: false
|
||||
width: '18%',
|
||||
isFilterable: false,
|
||||
// `classContent` is the library's own per-column class hook and
|
||||
// lands on the div the cell renders into. Note it is NOT `class`,
|
||||
// which the Name column used to pass: `class` is declared on the
|
||||
// library's `IColumn` (so it type-checks, which is why a dozen
|
||||
// tables in this repo still pass it) but its `Column` class never
|
||||
// reads it, so it reaches no element. What the class does is in
|
||||
// `tags.component.scss`.
|
||||
classContent: 'ga-secondary-cell',
|
||||
valuePrepareFunction: (value: string) => value || '—'
|
||||
},
|
||||
description: {
|
||||
title: this.getTranslation('TAGS_PAGE.TAGS_DESCRIPTION'),
|
||||
type: 'string',
|
||||
width: '70%',
|
||||
isFilterable: false
|
||||
width: '45%',
|
||||
isFilterable: false,
|
||||
// Most tags carry no description, and a column of blank cells reads
|
||||
// as a table that failed to load rather than as one with nothing to
|
||||
// say.
|
||||
valuePrepareFunction: (value: string) => value || '—'
|
||||
},
|
||||
counter: {
|
||||
title: this.getTranslation('Counter'),
|
||||
type: 'string',
|
||||
width: '10%',
|
||||
width: '15%',
|
||||
isFilterable: false,
|
||||
// Right-aligned, tabular figures — see `tags.component.scss`.
|
||||
classHeader: 'ga-numeric-cell',
|
||||
classContent: 'ga-numeric-cell',
|
||||
valuePrepareFunction: (_: any, cell: Cell) => {
|
||||
if (cell instanceof Cell) {
|
||||
const data = cell.getRow().getData();
|
||||
return this.getCounter(data);
|
||||
}
|
||||
return this.getCounter(cell);
|
||||
// Two callers, two shapes: the table passes a `Cell`, the card
|
||||
// grid passes the row itself (`CardGridComponent.getValue`).
|
||||
const data = cell instanceof Cell ? cell.getRow().getData() : cell;
|
||||
const count = this.getCounter(data);
|
||||
// Six-figure usage counts are common here (1890000) and unreadable
|
||||
// without digit grouping; the grouping follows the browser locale,
|
||||
// so it reads the way the viewer expects rather than the way en-US
|
||||
// does.
|
||||
return Number.isFinite(count) ? count.toLocaleString() : '—';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -302,7 +325,13 @@ export class TagsComponent extends PaginationFilterBaseComponent implements Afte
|
||||
return counter;
|
||||
};
|
||||
|
||||
async getTagTypes() {
|
||||
/**
|
||||
* @param generation the refresh this load belongs to — see `loadTagsThenTypes()`.
|
||||
* A load whose generation has been superseded while its request was in flight
|
||||
* drops the response instead of rebuilding the rail from it. Defaults to the
|
||||
* current refresh, which is what a standalone call wants.
|
||||
*/
|
||||
async getTagTypes(generation: number = this.loadGeneration) {
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
@@ -314,32 +343,64 @@ export class TagsComponent extends PaginationFilterBaseComponent implements Afte
|
||||
organizationId
|
||||
});
|
||||
|
||||
// Superseded while the request was out. Two requests for the same
|
||||
// resource can settle in either order, so this response may describe the
|
||||
// organization the user has already left — and the rail it would rebuild
|
||||
// is one the newer pass has already built correctly.
|
||||
if (generation !== this.loadGeneration) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tagTypes = items;
|
||||
|
||||
this.filterOptions.push(
|
||||
// Assigned whole rather than pushed onto a list `getTags()` has emptied.
|
||||
// The rail is a RADIOGROUP whose tab stop is the checked option, so an
|
||||
// interval where it holds only "All" while `selectedFilterValue` still
|
||||
// names a type is one where the checked option does not exist: the
|
||||
// focused radio is re-rendered away under a keyboard user mid-request,
|
||||
// and — before `filterTabStopIndex` — nothing was left to Tab back into.
|
||||
// Building the new list here and swapping it in one statement means the
|
||||
// rail never renders a state the selection does not match.
|
||||
this.filterOptions = [
|
||||
{ value: '', displayName: 'All' },
|
||||
...this.tagTypes.map((tagType) => {
|
||||
return {
|
||||
value: tagType.id,
|
||||
displayName: tagType.type
|
||||
};
|
||||
})
|
||||
);
|
||||
];
|
||||
} catch (error) {
|
||||
// Logged whatever its generation — a failure is worth seeing in the
|
||||
// console even once the pass that caused it has been superseded.
|
||||
console.error('Error while retrieving tag types', error);
|
||||
if (generation !== this.loadGeneration) {
|
||||
return;
|
||||
}
|
||||
this.toastrService.danger('TAGS_PAGE.TAGS_FETCH_FAILED', 'Error fetching tag types');
|
||||
// A failed fetch may be a failed ORGANIZATION SWITCH, and the types still
|
||||
// on screen would then be the previous organization's. "All" alone is the
|
||||
// honest rail, and the reconcile below moves the selection onto it.
|
||||
this.filterOptions = [{ value: '', displayName: 'All' }];
|
||||
} finally {
|
||||
this.reconcileSelectedFilter();
|
||||
this.loading = false;
|
||||
// `finally` runs on the stale returns above as well, and neither of these
|
||||
// belongs to a superseded pass: the reconcile would judge the newer rail
|
||||
// against this one's `allTags`, and the spinner is the newer pass's to
|
||||
// clear when its own request comes home.
|
||||
if (generation === this.loadGeneration) {
|
||||
this.reconcileSelectedFilter();
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops a filter selection that no longer exists.
|
||||
*
|
||||
* `getTags()` resets `filterOptions` to just "All" and this method refills it from
|
||||
* the current organization's tag types, so a chip that was selected a moment ago can
|
||||
* simply be gone — switching organization is the usual way. Left alone, the rail then
|
||||
* highlights nothing at all, not even "All".
|
||||
* `getTagTypes()` rebuilds `filterOptions` from the current organization's tag
|
||||
* types, so a chip that was selected a moment ago can simply be gone — switching
|
||||
* organization is the usual way. Left alone, the rail then highlights nothing at
|
||||
* all, not even "All".
|
||||
*
|
||||
* The table needs the same treatment. `getTags()` runs BEFORE this method and skips
|
||||
* reloading while `_isFiltered` is still set, so it will have kept the previous
|
||||
@@ -364,25 +425,36 @@ export class TagsComponent extends PaginationFilterBaseComponent implements Afte
|
||||
* against `allTags`, which `getTags()` is what refreshes; un-awaited they raced and
|
||||
* the reconcile could run against the previous organization's tags.
|
||||
*
|
||||
* The generation check drops the second half of a pass that a newer refresh has
|
||||
* already superseded — a pagination, search or organization change arriving while
|
||||
* the first request is still in flight. It does not abort the in-flight HTTP call
|
||||
* (these are promises, not cancellable observables), and overlapping refreshes
|
||||
* were possible before this too, since both loads were fired un-awaited; this
|
||||
* closes the specific window the reconcile depends on.
|
||||
* GENERATIONS. A pagination, search or organization change can arrive while the
|
||||
* first request is still in flight, so every refresh takes a number and both
|
||||
* loads carry it. The number is checked twice: here, before the second load is
|
||||
* started at all, and again inside each load when its own request comes home.
|
||||
*
|
||||
* The second check is what overlapping passes actually need. Starting a load is
|
||||
* not the same as finishing one — two requests for the same resource can settle
|
||||
* in either order — so a `getTagTypes()` that was current when it started can
|
||||
* still be answered after a newer one, and without the check it would rebuild
|
||||
* the filter rail from the organization the user has already left and then
|
||||
* reconcile the selection against it.
|
||||
*
|
||||
* None of this aborts the in-flight call (these are promises, not cancellable
|
||||
* observables): a superseded response is fetched and then dropped on arrival.
|
||||
*/
|
||||
private async loadTagsThenTypes(): Promise<void> {
|
||||
const generation = ++this.loadGeneration;
|
||||
await this.getTags();
|
||||
await this.getTags(generation);
|
||||
if (generation !== this.loadGeneration) {
|
||||
return;
|
||||
}
|
||||
await this.getTagTypes();
|
||||
await this.getTagTypes(generation);
|
||||
}
|
||||
|
||||
async getTags() {
|
||||
/**
|
||||
* @param generation the refresh this load belongs to — see `loadTagsThenTypes()`.
|
||||
* Defaults to the current refresh, which is what a standalone call wants.
|
||||
*/
|
||||
async getTags(generation: number = this.loadGeneration) {
|
||||
this.allTags = [];
|
||||
this.filterOptions = [{ value: '', displayName: 'All' }];
|
||||
|
||||
try {
|
||||
const { tenantId } = this.store.user;
|
||||
@@ -396,6 +468,13 @@ export class TagsComponent extends PaginationFilterBaseComponent implements Afte
|
||||
['tagType']
|
||||
);
|
||||
|
||||
// Superseded while the request was out, as in `getTagTypes()`: these rows
|
||||
// and the pagination they total would be the previous organization's, and
|
||||
// `allTags` is what the reconcile reads.
|
||||
if (generation !== this.loadGeneration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { activePage, itemsPerPage } = this.getPagination();
|
||||
|
||||
this.allTags = items;
|
||||
@@ -413,9 +492,16 @@ export class TagsComponent extends PaginationFilterBaseComponent implements Afte
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error while retrieving tags', error);
|
||||
if (generation !== this.loadGeneration) {
|
||||
return;
|
||||
}
|
||||
this.toastrService.danger(error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
// The spinner belongs to whichever pass is current; a superseded one
|
||||
// leaves it up for the pass that replaced it.
|
||||
if (generation === this.loadGeneration) {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,6 +517,77 @@ export class TagsComponent extends PaginationFilterBaseComponent implements Afte
|
||||
return this.componentLayoutStyleEnum.CARDS_GRID === this.dataLayoutStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Which option of the rail carries its single tab stop.
|
||||
*
|
||||
* A radiogroup owes the keyboard exactly one, and the CHECKED option is
|
||||
* normally it (see `onFilterKeydown`). When nothing is checked the group still
|
||||
* needs one, or it cannot be reached by Tab at all — the pattern's answer is
|
||||
* the first option, which is what the `-1` branch below returns.
|
||||
*
|
||||
* That is not a hypothetical here: `filterOptions` is rebuilt on every refresh
|
||||
* while `selectedFilterValue` still names the type the last list carried, and a
|
||||
* type that has genuinely gone (an organization switch, a failed fetch) leaves
|
||||
* the selection matching nothing until `reconcileSelectedFilter()` clears it.
|
||||
* Tying the tab stop to the selection alone made every radio `tabindex="-1"`
|
||||
* for those windows, with the focused one re-rendered away underneath whoever
|
||||
* was using it.
|
||||
*/
|
||||
get filterTabStopIndex(): number {
|
||||
const checked = this.filterOptions.findIndex((option) => option.value === this.selectedFilterValue);
|
||||
return checked === -1 ? 0 : checked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Arrow-key navigation for the tag-type rail.
|
||||
*
|
||||
* The rail is a RADIOGROUP, not a row of toggle buttons: the options are
|
||||
* mutually exclusive, so picking one clears the last. A radiogroup is one
|
||||
* tab stop with the arrows moving between (and selecting) the options —
|
||||
* which is also why the template gives `tabindex="0"` to one option only
|
||||
* (`filterTabStopIndex`). Tab therefore enters the rail on the active filter
|
||||
* and leaves it again, instead of stepping through every tag type.
|
||||
*
|
||||
* Home/End go to the ends, as the pattern expects.
|
||||
*
|
||||
* @param event the originating keydown, whose target is the focused option
|
||||
* @param index position of that option in `filterOptions`
|
||||
*/
|
||||
onFilterKeydown(event: KeyboardEvent, index: number) {
|
||||
const count = this.filterOptions.length;
|
||||
if (!count) {
|
||||
return;
|
||||
}
|
||||
let next: number;
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
case 'ArrowRight':
|
||||
next = (index + 1) % count;
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
case 'ArrowLeft':
|
||||
next = (index - 1 + count) % count;
|
||||
break;
|
||||
case 'Home':
|
||||
next = 0;
|
||||
break;
|
||||
case 'End':
|
||||
next = count - 1;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
// The rail scrolls, so the browser would page it under us on Arrow/Home/End.
|
||||
event.preventDefault();
|
||||
this.selectedFilterOption(this.filterOptions[next].value);
|
||||
// Selection and focus move together in a radiogroup. Queried off the group
|
||||
// rather than off a sibling list, so the lookup survives whatever wrapper
|
||||
// `nb-list` renders around the items.
|
||||
const option = event.currentTarget as HTMLElement;
|
||||
const options = option.closest('[role="radiogroup"]')?.querySelectorAll<HTMLElement>('[role="radio"]');
|
||||
options?.item(next)?.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select Filter
|
||||
*
|
||||
|
||||
@@ -1 +1,69 @@
|
||||
@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 Job Employees, Payments, Invoices, Pipelines, Tasks and Employees carry,
|
||||
// so this table reads as the same surface as the rest of the app rather than as
|
||||
// one still drawn a size larger than its neighbours.
|
||||
@include ga-hub.list-page();
|
||||
|
||||
// ── Row breathing room ───────────────────────────────────────────────────────
|
||||
//
|
||||
// `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.
|
||||
//
|
||||
// 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 Tags cell ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// `ga-only-tags` is built for a cell that renders a VALUE with its chips
|
||||
// underneath, so it pushes the chip row down by `--gauzy-table-chip-block-gap`
|
||||
// (0.5rem on a list page) to keep the chips from riding up into the text.
|
||||
//
|
||||
// This column has no text above its chips — it is chips and nothing else — so
|
||||
// that gap had nothing to separate: it dropped the chip off the row's centre
|
||||
// line and made the one row carrying a tag ~8px taller than the nine that do
|
||||
// not, which reads as a hitch in the row rhythm rather than as a tagged row.
|
||||
//
|
||||
// SPECIFICITY: the renderer's own `margin-top` is `!important` (it is resolving
|
||||
// a tug-of-war with Bootstrap's `.mt-2` utility in the template, which is
|
||||
// `!important` too), so the only way past it is a more specific `!important` —
|
||||
// `.tags[_ngcontent]` is (0,2,0) — a class and an attribute — and the block
|
||||
// below is (0,2,4): `::ng-deep` leaves no content attribute behind it, so what
|
||||
// counts is the host attribute and `.tags` over the four element names between
|
||||
// them.
|
||||
:host ::ng-deep angular2-smart-table table td ga-only-tags .tags {
|
||||
margin-top: 0 !important;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -430,10 +430,17 @@ export class TeamsComponent extends PaginationFilterBaseComponent implements OnI
|
||||
display: false,
|
||||
perPage: pagination ? pagination.itemsPerPage : 10
|
||||
},
|
||||
// The four widths are a RATIO the library hands to the `<th>`s, so they
|
||||
// have to add up to the table. Left unset, `table-layout: auto` sized
|
||||
// every column off its widest cell, and the two people columns are the
|
||||
// widest by construction — a named person plus an avatar stack — so
|
||||
// they took better than a third of the row EACH and held most of it as
|
||||
// empty space, while Name and Tags were squeezed into what was left.
|
||||
columns: {
|
||||
name: {
|
||||
title: this.getTranslation('SM_TABLE.NAME'),
|
||||
type: 'string',
|
||||
width: '20%',
|
||||
filter: {
|
||||
type: 'custom',
|
||||
component: InputFilterComponent
|
||||
@@ -445,6 +452,7 @@ export class TeamsComponent extends PaginationFilterBaseComponent implements OnI
|
||||
managers: {
|
||||
title: this.getTranslation('ORGANIZATIONS_PAGE.EDIT.TEAMS_PAGE.MANAGERS'),
|
||||
type: 'custom',
|
||||
width: '30%',
|
||||
isFilterable: false,
|
||||
renderComponent: EmployeeWithLinksComponent,
|
||||
componentInitFunction: (instance: EmployeeWithLinksComponent, cell: Cell) => {
|
||||
@@ -455,6 +463,7 @@ export class TeamsComponent extends PaginationFilterBaseComponent implements OnI
|
||||
members: {
|
||||
title: this.getTranslation('ORGANIZATIONS_PAGE.EDIT.TEAMS_PAGE.MEMBERS'),
|
||||
type: 'custom',
|
||||
width: '30%',
|
||||
isFilterable: false,
|
||||
renderComponent: EmployeeWithLinksComponent,
|
||||
componentInitFunction: (instance: EmployeeWithLinksComponent, cell: Cell) => {
|
||||
@@ -465,7 +474,7 @@ export class TeamsComponent extends PaginationFilterBaseComponent implements OnI
|
||||
notes: {
|
||||
title: this.getTranslation('MENU.TAGS'),
|
||||
type: 'custom',
|
||||
class: 'align-row',
|
||||
width: '20%',
|
||||
renderComponent: TagsOnlyComponent,
|
||||
componentInitFunction: (instance: TagsOnlyComponent, cell: Cell) => {
|
||||
instance.rowData = cell.getRow().getData();
|
||||
|
||||
+94
-26
@@ -16,29 +16,58 @@
|
||||
>
|
||||
</ngx-gauzy-button-action>
|
||||
</div>
|
||||
@if (dataLayoutStyle === componentLayoutStyleEnum.TABLE) {
|
||||
<div class="columns-header row align-items-center">
|
||||
<div class="col-md-2 pl-3 float-left text-truncate">
|
||||
{{ 'ORGANIZATIONS_PAGE.NAME' | translate }}
|
||||
</div>
|
||||
<div class="col-md-2 p-0 float-left text-truncate">
|
||||
{{ 'ORGANIZATIONS_PAGE.PHONE' | translate }}
|
||||
</div>
|
||||
<div class="col-md-2 p-0 float-left text-truncate">
|
||||
{{ 'ORGANIZATIONS_PAGE.EMAIL' | translate }}
|
||||
</div>
|
||||
<div class="col-md-3 p-0 float-left text-truncate">
|
||||
{{ 'ORGANIZATIONS_PAGE.WEBSITE' | translate }}
|
||||
</div>
|
||||
<div class="col-md-3 p-0 float-left text-truncate">
|
||||
{{ 'ORGANIZATIONS_PAGE.TAGS' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</nb-card-header>
|
||||
<nb-card-body [nbSpinner]="loading" nbSpinnerStatus="primary" nbSpinnerSize="large">
|
||||
@if (dataLayoutStyle === componentLayoutStyleEnum.TABLE) {
|
||||
<div class="table-scroll-container" infiniteScroll [scrollWindow]="false" (scrolled)="onScroll()">
|
||||
<!--
|
||||
The column titles are the first child of the scroll container, boxed like
|
||||
a row and pinned with `position: sticky` — the same place and the same
|
||||
behaviour as a smart table's own `thead` on every other page in this
|
||||
section. They used to sit in the card header above it, in a container with
|
||||
different padding, which is what the `pl-3` on the first title was for.
|
||||
-->
|
||||
<div class="columns-header">
|
||||
<!--
|
||||
`vendor-row` is what every rule for this grid is anchored on. Three
|
||||
other pages `@forward` this component's stylesheet (Positions,
|
||||
Employee levels, Documents) and not one of them lays its row out in
|
||||
columns, so a bare `.row` would have carried the column treatment
|
||||
onto three pages that never asked for it.
|
||||
|
||||
The ratio: 3/2/3/2/2. It was 2/2/2/3/3, which spent a quarter of the
|
||||
table on a URL that is ellipsised at any width and gave the vendor's
|
||||
NAME — the only value every row has, and the one the row is found by
|
||||
— the same room as its phone number. The name and the email are the
|
||||
two full values here, so they take the width; the website keeps
|
||||
enough to show its host, which is the part of a URL anyone reads.
|
||||
|
||||
`p-0` is gone from all ten cells, titles and values alike. It is
|
||||
Bootstrap's utility for it, and every Bootstrap utility is
|
||||
`!important` — `padding: 0 !important` — so no gutter could be added
|
||||
to these columns from a stylesheet at any specificity, which is why
|
||||
the website's ellipsis ran straight into the first tag chip. The
|
||||
cell gutter is set in `vendors.component.scss` instead, where it can
|
||||
be trailing-only and can exempt the last column.
|
||||
-->
|
||||
<div class="row align-items-center vendor-row">
|
||||
<div class="col-md-3 text-truncate">
|
||||
{{ 'ORGANIZATIONS_PAGE.NAME' | translate }}
|
||||
</div>
|
||||
<div class="col-md-2 text-truncate">
|
||||
{{ 'ORGANIZATIONS_PAGE.PHONE' | translate }}
|
||||
</div>
|
||||
<div class="col-md-3 text-truncate">
|
||||
{{ 'ORGANIZATIONS_PAGE.EMAIL' | translate }}
|
||||
</div>
|
||||
<div class="col-md-2 text-truncate">
|
||||
{{ 'ORGANIZATIONS_PAGE.WEBSITE' | translate }}
|
||||
</div>
|
||||
<div class="col-md-2 text-truncate">
|
||||
{{ 'ORGANIZATIONS_PAGE.TAGS' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@for (vendor of vendors; track vendor.id) {
|
||||
<nb-card class="p-0">
|
||||
<nb-card-body
|
||||
@@ -46,22 +75,61 @@
|
||||
[class.selected]="selected?.state && vendor === selected?.vendor"
|
||||
(click)="selectVendor(vendor)"
|
||||
>
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-2 p-0 float-left text-truncate">
|
||||
<!--
|
||||
`float-left` is gone from all five cells: a `.row` is
|
||||
`display: flex`, and a float on a flex item is ignored outright.
|
||||
|
||||
Every optional value is now guarded, and falls back to an em dash
|
||||
where it is missing — the treatment the Tags table takes. Four of
|
||||
this page's five columns are optional and most vendors fill none
|
||||
of them, so a row of blanks read as a table that had failed to
|
||||
load rather than as a vendor with nothing but a name.
|
||||
|
||||
The guards are not only cosmetic. `gauzy-email` renders
|
||||
`<a href="mailto:{{email}}">` and `gauzy-external-link` renders
|
||||
`<a href="{{website}}">` unconditionally, so every vendor without
|
||||
one carried a live link to `mailto:undefined` / `undefined` with
|
||||
no text to click — invisible, but real, and the external one
|
||||
opened a tab.
|
||||
-->
|
||||
<div class="row align-items-center vendor-row">
|
||||
<div class="col-md-3 text-truncate ga-name-cell">
|
||||
<ga-company-logo [rowData]="vendor"></ga-company-logo>
|
||||
{{ vendor.name }}
|
||||
</div>
|
||||
<div class="col-md-2 p-0 float-left text-truncate">
|
||||
<div class="col-md-2 text-truncate">
|
||||
@if (vendor.phone) {
|
||||
{{ vendor.phone }}
|
||||
} @else {
|
||||
<span class="ga-empty-cell">—</span>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-2 p-0 float-left text-truncate">
|
||||
<div class="col-md-3 text-truncate">
|
||||
@if (vendor.email) {
|
||||
<gauzy-email [rowData]="vendor"></gauzy-email>
|
||||
} @else {
|
||||
<span class="ga-empty-cell">—</span>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-3 p-0 float-left text-truncate">
|
||||
<div class="col-md-2 text-truncate">
|
||||
@if (vendor.website) {
|
||||
<gauzy-external-link [rowData]="vendor"></gauzy-external-link>
|
||||
} @else {
|
||||
<span class="ga-empty-cell">—</span>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-3 float-left p-0 text-truncate">
|
||||
<!--
|
||||
No `text-truncate` on the chips: it is `overflow: hidden` plus
|
||||
`text-overflow`, and neither does anything useful to a wrapping
|
||||
flex row — it cannot draw an ellipsis for a chip, it can only
|
||||
cut one in half.
|
||||
-->
|
||||
<div class="col-md-2">
|
||||
@if (vendor.tags?.length) {
|
||||
<ga-only-tags [rowData]="vendor"></ga-only-tags>
|
||||
} @else {
|
||||
<span class="ga-empty-cell">—</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</nb-card-body>
|
||||
@@ -141,7 +209,7 @@
|
||||
</ng-template>
|
||||
<ng-template #visibleButton>
|
||||
<ng-container *ngxPermissionsOnly="['ALL_ORG_EDIT']">
|
||||
<button nbButton status="success" size="small" (click)="openDialog(addEditTemplate, false)">
|
||||
<button nbButton class="action" status="success" size="small" (click)="openDialog(addEditTemplate, false)">
|
||||
<nb-icon icon="plus-outline"> </nb-icon>
|
||||
{{ 'BUTTONS.ADD' | translate }}
|
||||
</button>
|
||||
|
||||
+238
-45
@@ -1,4 +1,5 @@
|
||||
@use 'gauzy/_gauzy-cards' as *;
|
||||
@use 'gauzy/_gauzy-table-hub' as ga-hub;
|
||||
@forward '../expenses/expense-categories/expense-categories.component';
|
||||
|
||||
.editable {
|
||||
@@ -57,38 +58,23 @@
|
||||
}
|
||||
|
||||
/*
|
||||
* Row hover / selection, also restated after the forward.
|
||||
* The PAGE body — and nothing else.
|
||||
*
|
||||
* `expense-categories` gives BOTH states the same `rgba(50, 50, 50, 0.03)` fill,
|
||||
* so hovering a row looked exactly like selecting one, and 3 % of black over a
|
||||
* near-black card is nothing at all — on the four dark themes there was no
|
||||
* visible selection. Selection also arrived as a 6px border that only exists
|
||||
* when the row is selected, so the whole row jumped 6px sideways on click.
|
||||
* `:not(.custom-table)` is the whole point of this rule, because `nb-ltr()`
|
||||
* prepends its selector: what this compiles to is
|
||||
* `[dir=ltr] [_nghost] nb-card-body[_ngcontent]`, i.e. (0,3,1), and the row
|
||||
* padding in `card-list-page()` below is `:host .custom-table` — (0,3,0). A row
|
||||
* IS an `nb-card-body`, so it matched both, and the one that won was this one:
|
||||
* every row on all four pages that share this sheet was drawn with the PAGE
|
||||
* body's 1rem/18px box rather than the 0.5rem/1.125rem one meant for it. That is
|
||||
* where ~60px rows came from, which is the exact thing `card-list-page()`'s own
|
||||
* comment says it fixes — the fix has simply never applied.
|
||||
*
|
||||
* Now: the neutral tints (they darken a light surface and lighten a dark one, so
|
||||
* one value works in all eight themes), selection carried by the primary colour
|
||||
* on the leading edge, and a transparent edge of the same width at rest so
|
||||
* nothing moves.
|
||||
* Excluding the rows here rather than out-specifying them there: the rows' own
|
||||
* padding is then settled in one place, by the shared contract, at the
|
||||
* specificity it was written for.
|
||||
*/
|
||||
:host .custom-table {
|
||||
@include nb-ltr(border-left, 3px solid transparent);
|
||||
@include nb-rtl(border-right, 3px solid transparent);
|
||||
|
||||
// `:not(.selected)` because this rule is declared after `.selected` at the same
|
||||
// specificity — without it, hovering a selected row would repaint it as merely
|
||||
// hovered.
|
||||
&:hover:not(.selected) {
|
||||
background: var(--gauzy-hover-tint, rgba(126, 126, 143, 0.12));
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: var(--gauzy-active-tint, rgba(126, 126, 143, 0.2));
|
||||
@include nb-ltr(border-left, 3px solid var(--color-primary-default));
|
||||
@include nb-rtl(border-right, 3px solid var(--color-primary-default));
|
||||
}
|
||||
}
|
||||
|
||||
:host nb-card-body {
|
||||
:host nb-card-body:not(.custom-table) {
|
||||
@include nb-ltr(padding, 1rem 0.5rem 1rem 18px);
|
||||
@include nb-rtl(padding, 1rem 18px 1rem 0.5rem);
|
||||
.no-data {
|
||||
@@ -101,22 +87,229 @@
|
||||
}
|
||||
}
|
||||
|
||||
.columns-header {
|
||||
// Same story as `.editable` above: `nb-theme(gauzy-card-2)` is a bare
|
||||
// `var(--gauzy-card-2)` and that key is missing from the two material themes,
|
||||
// where the column-title band therefore had no background and merged into the
|
||||
// card behind it.
|
||||
background-color: var(--gauzy-card-2, rgba(126, 126, 143, 0.08));
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
// Logical, so the title band still indents from the reading edge under RTL —
|
||||
// the row of `col-md-*` cells it labels is mirrored, but `padding-left` was not.
|
||||
padding-inline-start: 12px;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
// ── The list-page contract, for a table built by hand ────────────────────────
|
||||
//
|
||||
// Four pages share this sheet — Vendors, Positions, Employee levels and
|
||||
// Documents all `@forward` it — and all four build their table as a stack of
|
||||
// `nb-card` rows rather than rendering the `angular2-smart-table` every other
|
||||
// page under `/pages/organization` does, so the shared `list-page()` block never
|
||||
// reached them. `card-list-page()` is the part of that contract which does not
|
||||
// depend on the smart table: the density tokens, the toolbar scale, the table
|
||||
// surface, and rows that read as rows instead of as floating cards.
|
||||
//
|
||||
// The gutter: a row here is a Bootstrap `.row`, which bleeds 15px past its
|
||||
// container through negative margins, so the row's own padding has to cover that
|
||||
// before it can inset anything — at the default 0.4375rem the first cell would
|
||||
// hang 8px off the left edge of the surface. 1.125rem lands the cells 6px inside
|
||||
// it, and the head below is boxed to match.
|
||||
@include ga-hub.card-list-page($row-padding-x: 1.125rem);
|
||||
|
||||
// ── The head ─────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Vendors is the only page of the four with column titles, and they used to be a
|
||||
// filled card-2 band in the CARD HEADER, floating 10px above the rows with its
|
||||
// own rounded corners — a second surface, and one that could not line up with
|
||||
// the values it titles: it sat in a container with 1rem of padding while the
|
||||
// rows sat in one with 18px, and the first title carried a `pl-3` to paper over
|
||||
// the difference.
|
||||
//
|
||||
// It is now the first child of the scroll container and boxed exactly like a
|
||||
// row (same padding, same 3px selection rail reserved on the leading edge), so
|
||||
// the titles sit over their columns by construction rather than by nudging. The
|
||||
// treatment is the one `list-page()` gives a smart table's `thead`: no fill, the
|
||||
// muted heading colour, one hairline underneath.
|
||||
// The surface's outline has to survive the sticky head below.
|
||||
//
|
||||
// `surface()` draws it with `box-shadow: inset 0 0 0 1px`, which is part of THIS
|
||||
// element's background layer — every in-flow child paints on top of it. The head
|
||||
// is an opaque child pinned to this box's inner top edge, so it erased the top
|
||||
// line outright and the leading edge for its own height, and the boxed surface
|
||||
// read as open at the top: the first line under the card was the head's own
|
||||
// bottom hairline, a few pixels down.
|
||||
//
|
||||
// A real border is GEOMETRY rather than paint. It sizes the content box the head
|
||||
// is laid out in, so the head cannot reach the outline at all — no per-edge
|
||||
// patching, and it mirrors under RTL on its own. Same 1px, same colour, same
|
||||
// outer size: the app-wide reset is `border-box`, so nothing moves but the
|
||||
// content, which comes in by 1px.
|
||||
//
|
||||
// SPECIFICITY: `card-list-page()` paints the surface at this same
|
||||
// `:host .table-scroll-container`, so this must stay BELOW the include above.
|
||||
:host .table-scroll-container {
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--gauzy-overlay-border-color, rgba(126, 126, 143, 0.18));
|
||||
}
|
||||
|
||||
:host .columns-header {
|
||||
padding: var(--gauzy-table-header-padding-y, 0.5rem) 1.125rem;
|
||||
// The head follows the rows (see `.custom-table:has(.vendor-row)` below), or it
|
||||
// ends up the shortest band in a table it is supposed to head:
|
||||
// `--gauzy-table-header-padding-y` is 0.3125rem, which put a 25px header over
|
||||
// 44px rows. Still one step under the rows, which is the convention — a heading
|
||||
// is not a row. The block axis only: the shorthand above keeps the 1.125rem
|
||||
// inline gutter the row cells are drawn to.
|
||||
padding-block: 0.625rem;
|
||||
// The same 3px rail the rows reserve for the selection edge, so the titles sit
|
||||
// over their columns. Logical, as theirs is.
|
||||
border-inline-start: 3px solid transparent;
|
||||
border-bottom: 1px solid 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: 15px;
|
||||
line-height: var(--gauzy-table-header-line-height, 0.9375rem);
|
||||
letter-spacing: 0em;
|
||||
color: var(--gauzy-text-color-2, var(--text-hint-color));
|
||||
// Pinned while the rows scroll under it, which is what the global sheet does
|
||||
// for a smart table's own `thead` (`_overrides.scss`) — and why the fill has to
|
||||
// be the opaque surface colour rather than none. That opacity is also why the
|
||||
// container above trades its inset outline for a real border: an opaque child
|
||||
// pinned to the inner top edge would otherwise paint straight over it.
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background: var(--gauzy-card-1, var(--background-basic-color-1));
|
||||
}
|
||||
|
||||
// ── The row grid ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// EVERYTHING BELOW IS ANCHORED ON `.vendor-row`, and deliberately so: Positions,
|
||||
// Employee levels and Documents all `@forward` this sheet, and none of the three
|
||||
// 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 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.
|
||||
//
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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
|
||||
// weight and colour as a phone number, five columns of equal-weight text left
|
||||
// nothing for the eye to anchor on.
|
||||
:host .vendor-row .ga-name-cell {
|
||||
font-weight: 600;
|
||||
color: var(--text-basic-color);
|
||||
}
|
||||
|
||||
// A missing value, in the muted colour the column headings take — the same
|
||||
// treatment the Tags table gives its empty cells. It has to read as "this vendor
|
||||
// has no phone number", which a blank cell does not.
|
||||
:host .vendor-row .ga-empty-cell {
|
||||
color: var(--gauzy-text-color-2, var(--text-hint-color));
|
||||
}
|
||||
|
||||
:host .vendor-row ::ng-deep {
|
||||
// The brand glyph is DECORATION next to the name it sits on, so it drops to
|
||||
// the muted colour rather than sharing the name's full contrast. `ga-company-logo`
|
||||
// paints it `--gauzy-text-color-1`, one of the keys the two material themes
|
||||
// never register, so this restates it with a fallback they can resolve.
|
||||
ga-company-logo i {
|
||||
color: var(--gauzy-text-color-2, var(--text-hint-color));
|
||||
}
|
||||
|
||||
/*
|
||||
* The two links.
|
||||
*
|
||||
* Both components paint themselves a literal `rgba(0, 136, 254, 1)` with a
|
||||
* permanent underline. That blue is in none of the eight themes, and an
|
||||
* always-underlined full URL was the highest-contrast, busiest thing in a row
|
||||
* whose actual subject is the vendor's name. The theme's own link colour says
|
||||
* "clickable" on its own; the underline is what a pointer is for.
|
||||
*
|
||||
* SPECIFICITY: their sheets are `a[_ngcontent-x]`, i.e. (0,1,1). Through
|
||||
* `::ng-deep` this reads `[_nghost-v] .vendor-row[_ngcontent-v] gauzy-email a`
|
||||
* — (0,3,2) — so no `!important` is needed to settle it.
|
||||
*/
|
||||
gauzy-email a,
|
||||
gauzy-external-link a {
|
||||
color: var(--text-primary-color);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* The chips.
|
||||
*
|
||||
* `ga-only-tags` renders its chips UNDER a name/level block it also renders,
|
||||
* and reserves `--gauzy-table-chip-block-gap` above them for it. This page
|
||||
* binds only `[rowData]`, never `[value]`, so that block never appears and the
|
||||
* gap was 0.5rem of top margin under nothing — paid by every row, since the
|
||||
* component emitted the stack whether the vendor had tags or not. The template
|
||||
* now withholds the component entirely when there are none; this settles the
|
||||
* margin for the rows that do.
|
||||
*
|
||||
* `!important` mirrors the declaration it overrides: the component's own rule
|
||||
* carries one, because it is outbidding Bootstrap's `.mt-2`.
|
||||
*/
|
||||
ga-only-tags {
|
||||
// A custom element is `display: inline` by default, and this one's children
|
||||
// are blocks — so its own box said nothing about the height of what was in
|
||||
// it, and the row had to centre an inline box around a flex stack.
|
||||
display: block;
|
||||
|
||||
.tags {
|
||||
margin-top: 0 !important;
|
||||
// Centred, not `flex-start`. The chips are ~16px in a cell whose
|
||||
// neighbours are a 1.25rem line of text, so left at the top of the box
|
||||
// they sat a couple of pixels above the line every other value in the row
|
||||
// is on — and a row WITHOUT tags renders an em dash, which is that line
|
||||
// exactly. The `min-height` is what gives the centring something to work
|
||||
// against: it makes a chip cell the same height as a text cell, so the
|
||||
// two kinds of row centre identically.
|
||||
align-items: center;
|
||||
min-height: var(--gauzy-table-line-height, 1.25rem);
|
||||
}
|
||||
|
||||
// A tag name is a label, not a sentence: it breaks the chip rather than the
|
||||
// column. The cell above dropped `text-truncate`, which is where the chips
|
||||
// were inheriting `white-space: nowrap` from.
|
||||
nb-badge.color {
|
||||
white-space: nowrap;
|
||||
// ...but a label can be longer than the two columns this cell is given,
|
||||
// and one that may not wrap would then carry the chip straight out
|
||||
// through the side of the table. Ellipsis at the cell's edge instead: the
|
||||
// chips are flex items, so `max-width` also lifts the automatic minimum
|
||||
// size that would otherwise hold a flex item open at its content width.
|
||||
// `max-width`, not `width` — a tag that fits is still sized by its text.
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Row rhythm ───────────────────────────────────────────────────────────────
|
||||
//
|
||||
// `card-list-page()` sets a row 0.5rem of block padding, which suits the pages
|
||||
// that put ONE value in a row (Positions, Employee levels, Documents). This one
|
||||
// is a five-column table: the columns already separate the values horizontally,
|
||||
// so what tells one row from the next is the air above and below it, and at
|
||||
// 0.5rem five columns of text ran together into a block.
|
||||
//
|
||||
// 0.75rem puts a row at ~44px against a 1.25rem line box. Scoped with
|
||||
// `:has(.vendor-row)` rather than by `.custom-table` alone, which the three
|
||||
// pages that `@forward` this sheet also use — they are single-value lists and
|
||||
// the shared 0.5rem is the right box for them.
|
||||
:host .custom-table:has(.vendor-row) {
|
||||
padding-block: 0.75rem;
|
||||
}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
<i [class]="'fab fa-'+rowData?.name | lowercase"></i>
|
||||
<i [class]="iconClass"></i>
|
||||
|
||||
+42
-8
@@ -1,10 +1,44 @@
|
||||
/*
|
||||
* The brand glyph in a company row.
|
||||
*
|
||||
* Font Awesome only ships a logo for companies that ARE brands, and the table
|
||||
* this renders in lists a tenant's own vendors — so for most rows `fa-<name>`
|
||||
* matched nothing at all and the cell drew a blank: FA gives every icon element
|
||||
* a fixed `--fa-width` box (1.25em), so the column still paid for an icon it
|
||||
* never showed, and the rows that did match a brand were the only ones with
|
||||
* anything on their leading edge.
|
||||
*
|
||||
* FA 7 draws its glyph from a custom property (`content: var(--fa)`, set by the
|
||||
* `fa-<name>` class), and custom properties inherit — so a value declared on the
|
||||
* HOST is what an `<i>` gets when no `fa-<name>` class set one of its own, and is
|
||||
* overridden by the class whenever one did. No branch in the template, and no
|
||||
* need to know which names Font Awesome has a logo for.
|
||||
*/
|
||||
:host {
|
||||
// A generic building, for a company Font Awesome has never heard of.
|
||||
--fa: '\f1ad';
|
||||
}
|
||||
|
||||
i {
|
||||
font-family: Font Awesome 7 Brands;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 21px;
|
||||
letter-spacing: 0em;
|
||||
text-align: left;
|
||||
color: var(--gauzy-text-color-1);
|
||||
padding: 0.75rem;
|
||||
// The brands file first, the free faces behind it: a real logo is rendered
|
||||
// from the brands file, and the fallback above — which the brands file does
|
||||
// not contain — falls through to the free face that does.
|
||||
// ...and a generic behind both, which is the last resort every font stack owes
|
||||
// the browser: neither FA face is a web-safe name, and if a build ships without
|
||||
// them the glyph falls back to a face that exists rather than to whatever the
|
||||
// browser happens to default to.
|
||||
font-family: 'Font Awesome 7 Brands', 'Font Awesome 7 Free', sans-serif;
|
||||
// ...which is the solid face, where that glyph lives. Harmless to the brands
|
||||
// file, which ships one weight (400) and is matched whatever is asked of it,
|
||||
// and FA sets `font-synthesis: none`, so nothing is emboldened to fit.
|
||||
font-weight: 900;
|
||||
// Sized and spaced by the row it sits in rather than by a fixed 18px/21px.
|
||||
font-size: 1em;
|
||||
line-height: inherit;
|
||||
color: var(--gauzy-text-color-1);
|
||||
// Was 0.75rem on all four sides — the better part of a centimetre around a
|
||||
// one-em glyph, and for every row without a brand logo it was all there was.
|
||||
// The gap to the company name is the only spacing an icon column needs.
|
||||
padding: 0;
|
||||
margin-inline-end: 0.375rem;
|
||||
}
|
||||
|
||||
+47
-5
@@ -1,12 +1,54 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ga-company-logo',
|
||||
templateUrl: './company-logo.component.html',
|
||||
styleUrls: ['./company-logo.component.scss'],
|
||||
standalone: false
|
||||
selector: 'ga-company-logo',
|
||||
templateUrl: './company-logo.component.html',
|
||||
styleUrls: ['./company-logo.component.scss'],
|
||||
standalone: false
|
||||
})
|
||||
export class CompanyLogoComponent {
|
||||
@Input() value: string | number;
|
||||
@Input() rowData: any;
|
||||
|
||||
/** The Font Awesome classes for this row's glyph. Recomputed when the row changes. */
|
||||
iconClass: string = 'fab';
|
||||
|
||||
private _rowData: any;
|
||||
|
||||
@Input()
|
||||
set rowData(data: any) {
|
||||
this._rowData = data;
|
||||
this.iconClass = CompanyLogoComponent.toIconClass(data?.name);
|
||||
}
|
||||
get rowData(): any {
|
||||
return this._rowData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns a company name into the brand class Font Awesome would know it by.
|
||||
*
|
||||
* The name used to be interpolated into the class list as it stood
|
||||
* (`'fab fa-' + rowData?.name | lowercase`), which is only a class at all for a
|
||||
* single-word name: "Ever Technologies LTD" produced THREE classes —
|
||||
* `fa-ever`, `technologies` and `ltd` — so any company whose name happened to
|
||||
* contain a word the page styles (`selected`, `primary`, `action`, `row`) was
|
||||
* styled by it. A slug is one class whatever the name is: lower case, ASCII,
|
||||
* hyphen separated, which is also the form Font Awesome's own names take, so
|
||||
* "Pay Pal" now finds `fa-pay-pal` where before it looked for `fa-pay`.
|
||||
*
|
||||
* Names that match no brand keep the bare `fab`, and the stylesheet gives that
|
||||
* element a generic glyph — see `--fa` there.
|
||||
*/
|
||||
private static toIconClass(name: unknown): string {
|
||||
// A name that is not a primitive is no name: `String()` would hand back
|
||||
// `[object Object]` and that slugs to a class of its very own.
|
||||
const raw = typeof name === 'string' || typeof name === 'number' ? `${name}` : '';
|
||||
const slug = raw
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
// One hyphen at either end is all there can be — the replace above has
|
||||
// already collapsed every run of non-alphanumerics into a single one — so
|
||||
// this needs no quantifier, and without one there is nothing to backtrack.
|
||||
.replace(/^-|-$/g, '');
|
||||
return slug ? `fab fa-${slug}` : 'fab';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -682,13 +682,20 @@ $default-box-shadow: $control-hairline;
|
||||
// that would add 8px to every tab bar in the app, and this pass is allowed to
|
||||
// change type sizes, not the boxes around them.
|
||||
@mixin nb-tabset-overrides() {
|
||||
:root nb-route-tabset .route-tabset .route-tab a.tab-link.tab-link {
|
||||
// Both families, one rule: an `nb-tabset` tab link used to take Nebular's own
|
||||
// `display: flex` and nothing else, so its label sat wherever the box put it
|
||||
// while a route tab's was centred in it.
|
||||
:root nb-route-tabset .route-tabset .route-tab a.tab-link.tab-link,
|
||||
:root nb-tabset .tabset .tab a.tab-link.tab-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:root nb-route-tabset .route-tabset .route-tab.route-tab a.tab-link.tab-link nb-icon + .tab-text {
|
||||
// Nebular gives the label a 0.5rem inline margin of its own, which would be
|
||||
// added to the 10px gap `includes/_tabset.scss` sets on the icon.
|
||||
:root nb-route-tabset .route-tabset .route-tab.route-tab a.tab-link.tab-link nb-icon + .tab-text,
|
||||
:root nb-tabset .tabset .tab.tab a.tab-link.tab-link nb-icon + .tab-text {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
--gauzy-table-chip-block-gap: 0.5rem;
|
||||
--gauzy-people-avatar-size: 1rem;
|
||||
--gauzy-people-font-size: 0.6875rem;
|
||||
|
||||
// Thumbnail columns (Image / Logo / Icon) are deliberately left out of this block.
|
||||
// Every metric above is text or a chip and scales with the row's 1rem line box;
|
||||
// a picture does not, and the renderers' own 74x60 / 70x64 / 35px boxes are
|
||||
// left exactly as they are — see `thumbnail-row-padding()` below.
|
||||
}
|
||||
|
||||
@mixin surface() {
|
||||
@@ -203,3 +208,258 @@
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/// The whole list-page contract in one include: density tokens, the table
|
||||
/// surface, row hover/selection, the tag-chip stack, the status pills, the pager
|
||||
/// and the page-header action row.
|
||||
///
|
||||
/// WHY A BUNDLE, when Payments, Invoices, Pipelines, Tasks, Employees and Job
|
||||
/// Employees each spell the same seven blocks out by hand: those pages interleave
|
||||
/// page-specific rules between them (a toggle given its own tokens here, an
|
||||
/// avatar chip resized there), and reading them as one list is how those
|
||||
/// exceptions stay visible. The tables under `/pages/organization` have no such
|
||||
/// exceptions to interleave — they are the same card, the same
|
||||
/// `.table-scroll-container`, the same `.pagination-container` and the same
|
||||
/// toolbar, a dozen and a half times over — so restating the block once per page
|
||||
/// would only create a dozen and a half places for it to drift.
|
||||
///
|
||||
/// MUST be included at the top level of a COMPONENT stylesheet: every selector
|
||||
/// below is anchored at `:host`, and `::ng-deep` is only stripped (rather than
|
||||
/// left in the output as an unmatchable pseudo-element) where a real
|
||||
/// encapsulation attribute precedes it.
|
||||
///
|
||||
/// @param {String} $actions - what holds the page's action buttons, as a selector
|
||||
/// relative to `:host`. The default is the CARD HEADER rather than
|
||||
/// `.gauzy-button-container`, and that is the point: several of these pages put
|
||||
/// a second row of buttons beside the title — Inventory's Merchants /
|
||||
/// Warehouses / Product categories / Product types, Equipment's "Equipment
|
||||
/// Sharing" — outside the toolbar container entirely. Naming only the container
|
||||
/// left those at full size next to a compacted "+ Add", which is the one thing
|
||||
/// in the header that then read as oversized. Every token the row needs is a
|
||||
/// CUSTOM PROPERTY, so one declaration on the header reaches all of them,
|
||||
/// `ngx-gauzy-button-action`'s own template included.
|
||||
///
|
||||
/// A page whose buttons sit in the card BODY passes its own selector; pass
|
||||
/// `null` for a table with no action row at all.
|
||||
@mixin list-page($actions: 'nb-card-header') {
|
||||
// The tokens sit on `:host`, not on the table, and that placement is the
|
||||
// point: the renderers that consume them (`ngx-people-list`, `ga-only-tags`,
|
||||
// `ga-notes-with-tags`, `ngx-created-by-user`, the loading skeleton) are
|
||||
// separate components, and custom properties are the only thing that crosses
|
||||
// those boundaries.
|
||||
:host {
|
||||
@include density-tokens();
|
||||
}
|
||||
|
||||
:host .table-scroll-container {
|
||||
@include surface();
|
||||
}
|
||||
|
||||
:host ::ng-deep angular2-smart-table {
|
||||
@include rows();
|
||||
}
|
||||
|
||||
:host ::ng-deep {
|
||||
@include tag-stack-spacing();
|
||||
}
|
||||
|
||||
:host ::ng-deep ga-status-badge {
|
||||
@include status-badge-tints();
|
||||
}
|
||||
|
||||
:host .pagination-container {
|
||||
@include pager-container();
|
||||
}
|
||||
|
||||
// The pager's `<nav><ul class="pagination"><li>`: its own sheet draws 0.875rem
|
||||
// numerals in a 0.75rem x 1.1rem box and 0.5rem-padded icon chips, i.e. a
|
||||
// control row taller than four table rows put together.
|
||||
//
|
||||
// SPECIFICITY: those rules nest under `:host`, so `li span` shims to
|
||||
// `[_nghost-pg] li[_ngcontent-pg] span[_ngcontent-pg]` — (0,3,2). Naming
|
||||
// `.pagination-container` (which lives in the CALLING component's template, so
|
||||
// it carries that component's content attribute) before `::ng-deep` buys two
|
||||
// attributes and a class, taking the block to (0,3,3).
|
||||
:host .pagination-container ::ng-deep ngx-pagination {
|
||||
@include pager();
|
||||
}
|
||||
|
||||
@if $actions {
|
||||
:host #{$actions} {
|
||||
@include page-header-buttons();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The same contract as `list-page()`, for the pages that build their table BY
|
||||
/// HAND instead of rendering `angular2-smart-table`: a stack of
|
||||
/// `nb-card > nb-card-body.custom-table` rows inside a `.table-scroll-container`.
|
||||
///
|
||||
/// Five pages under `/pages/organization` are built that way — Vendors,
|
||||
/// Positions, Employee levels and Documents share one stylesheet, Employment
|
||||
/// types has its own — and every selector in `list-page()` that matters is
|
||||
/// anchored on the smart-table element, so none of it ever reached them. What
|
||||
/// that looked like: full-size toolbar buttons beside compacted ones on
|
||||
/// Departments and Teams, rows better than twice the height, and a stack of
|
||||
/// separately floating cards where the rest of the section has one bounded
|
||||
/// surface with rows on it.
|
||||
///
|
||||
/// So this takes the three pieces of `list-page()` that are NOT anchored on the
|
||||
/// smart table — the density tokens, the toolbar scale and the table surface —
|
||||
/// and adds the one thing those rows need that a `<tr>` never did: they arrive
|
||||
/// as cards, carrying a card's background, radius, ring and bottom margin.
|
||||
///
|
||||
/// MUST be included AFTER any `@forward` of a sheet that paints `.custom-table`
|
||||
/// (all five forward `expense-categories`, which paints it `gauzy-card-1`): the
|
||||
/// row rule below is deliberately at the same specificity as that one, so it is
|
||||
/// source order that settles it — see the note on the rule.
|
||||
///
|
||||
/// @param {String} $actions - the block holding the page's action buttons, as a
|
||||
/// selector relative to `:host`. `nb-card-header` for a page whose toolbar is
|
||||
/// on the title line; Employment types puts its row in the card BODY over a
|
||||
/// tabset and passes its own selector. `null` for a page with no action row.
|
||||
/// @param {Length} $row-padding-x - the row's own gutter. The default suits a
|
||||
/// row that holds one value across its width. A page whose row markup
|
||||
/// is a Bootstrap `.row` needs at least the 15px that grid bleeds past its
|
||||
/// container with negative margins, or the first cell hangs off the surface —
|
||||
/// Vendors passes 1.125rem for exactly that.
|
||||
@mixin card-list-page($actions: 'nb-card-header', $row-padding-x: 0.75rem) {
|
||||
:host {
|
||||
@include density-tokens();
|
||||
}
|
||||
|
||||
@if $actions {
|
||||
:host #{$actions} {
|
||||
@include page-header-buttons();
|
||||
}
|
||||
}
|
||||
|
||||
:host .table-scroll-container {
|
||||
@include surface();
|
||||
// The surface is the TABLE, so it is the size of the space the table was
|
||||
// given — not the size of the rows that happen to be in it. Four employment
|
||||
// types in a container that hugs its content is a small bordered box
|
||||
// floating in a tall empty card, which is not what any page in this section
|
||||
// looks like: `@shared/_pg-card` fills the body on all the smart-table
|
||||
// pages, and the rows there simply stop part-way down a full-height surface.
|
||||
//
|
||||
// `min-height` rather than `flex: 1 1 auto`, which is how the smart-table
|
||||
// pages do it: two of the five put this container in a card BODY, which is
|
||||
// not a flex column, so a flex declaration would be quietly ignored there —
|
||||
// and making it one would relayout the card-grid branch of four pages that
|
||||
// nobody asked about. Both parents have a definite height already, so a
|
||||
// percentage resolves in either.
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
// The row's wrapper. Flattened onto the surface above — left alone it would be
|
||||
// a card on a card.
|
||||
:host .table-scroll-container nb-card {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
// Paint and box for the row itself.
|
||||
//
|
||||
// SPECIFICITY: deliberately `:host .custom-table`, the same selector
|
||||
// `expense-categories` paints it with, rather than something stronger. The
|
||||
// hover and selection rules these pages inherit are `:host .custom-table:hover`
|
||||
// / `.selected`, one pseudo-class ABOVE this — reaching for
|
||||
// `.table-scroll-container nb-card-body` here would out-rank them and paint
|
||||
// every row transparent under the pointer.
|
||||
:host .custom-table {
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
// `:host nb-card-body` on these pages sets the PAGE body's padding and
|
||||
// matches the row bodies too, which is where ~60px rows came from.
|
||||
padding-block: 0.5rem;
|
||||
padding-inline: $row-padding-x;
|
||||
// The app's control text size, NOT the 0.6875rem a smart-table cell takes.
|
||||
// That step is what buys a twelve-column table its density; three of these
|
||||
// five pages put a single value in the row (Positions, Employee levels and
|
||||
// Employment types render one `ga-notes-with-tags` and nothing else), and a
|
||||
// single column set at cell size reads as a page that has been zoomed out.
|
||||
font-size: 0.8125rem;
|
||||
line-height: var(--gauzy-table-line-height, 1.25rem);
|
||||
// A smart table separates its rows with nothing, and is right to: twelve
|
||||
// columns of aligned values are their own grid, and the hover tint says
|
||||
// which row you are on. A single-column list has neither — flattening the
|
||||
// cards off these rows without putting a rule back left four names in a box
|
||||
// with no structure at all. One hairline, drawn INSIDE the row's box so
|
||||
// nothing moves and the selected row's 6px rail is unaffected.
|
||||
box-shadow: inset 0 -1px 0 0 var(--gauzy-overlay-border-color, rgba(126, 126, 143, 0.18));
|
||||
}
|
||||
|
||||
// The rule under the last row would be a second edge just inside the surface's
|
||||
// own.
|
||||
:host .table-scroll-container nb-card:last-of-type .custom-table {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* Row hover and selection, for all five pages rather than the four that share
|
||||
* the Vendors sheet — Employment types forwards only `expense-categories`, and
|
||||
* so had neither.
|
||||
*
|
||||
* `expense-categories` gives BOTH states the same `rgba(50, 50, 50, 0.03)`
|
||||
* fill, so hovering a row looked exactly like selecting one, and 3% of black
|
||||
* over a near-black card is nothing at all — on the four dark themes there was
|
||||
* no visible selection. Selection also arrived as a 6px border that exists
|
||||
* only when the row is selected, so the whole row jumped 6px sideways on
|
||||
* click.
|
||||
*
|
||||
* Now: the neutral tints (they darken a light surface and lighten a dark one,
|
||||
* so one value works in all eight themes), selection carried by the primary
|
||||
* colour on the leading edge, and a transparent edge of the same width at rest
|
||||
* so nothing moves.
|
||||
*
|
||||
* Logical properties, which also settles the cascade: the rules these replace
|
||||
* are `[dir=ltr]` / `[dir=rtl]` pairs, and naming `.table-scroll-container`
|
||||
* buys back the specificity that prefix carries.
|
||||
*/
|
||||
:host .table-scroll-container .custom-table {
|
||||
border-inline-start: 3px solid transparent;
|
||||
}
|
||||
|
||||
// `:not(.selected)` so hovering a selected row does not repaint it as merely
|
||||
// hovered.
|
||||
:host .table-scroll-container .custom-table:hover:not(.selected) {
|
||||
background: var(--gauzy-hover-tint, rgba(126, 126, 143, 0.12));
|
||||
}
|
||||
|
||||
:host .table-scroll-container .custom-table.selected {
|
||||
background: var(--gauzy-active-tint, rgba(126, 126, 143, 0.2));
|
||||
border-inline-start-color: var(--color-primary-default);
|
||||
}
|
||||
}
|
||||
|
||||
/// Cell padding for a table that carries a picture column.
|
||||
///
|
||||
/// `density-tokens()` runs the list-page family at 0.1875rem / 0.4375rem, which
|
||||
/// is right for a row whose tallest thing is a line of text. A thumbnail row is
|
||||
/// not that row: `ImageRowComponent` (74x60), `ItemImgTagsComponent` (70x64) and
|
||||
/// `IconRowComponent` (a 35px disc) are several line boxes tall whatever the
|
||||
/// text around them does, so the tighter box buys no height here — it only
|
||||
/// presses the picture against the rule above and below it.
|
||||
///
|
||||
/// The pictures themselves are left alone. Shrinking them to the row's scale was
|
||||
/// tried and is wrong twice over: the photo stops carrying information, and
|
||||
/// `ga-no-image` — which is what most of these cells actually render — clips its
|
||||
/// "Image not available" caption mid-word, so a missing image reads as a broken
|
||||
/// one. So these tables keep the picture they had and the padding they had; what
|
||||
/// they take from the density pass is the type scale, the surface, the row
|
||||
/// treatment and the pager.
|
||||
///
|
||||
/// Set on `angular2-smart-table` rather than on the cells: the density rules in
|
||||
/// `_overrides.scss` read these as custom properties from the `td`, and a custom
|
||||
/// property set on the table inherits down to every cell in it — so this needs no
|
||||
/// specificity at all against a `:root`-prefixed block it could not otherwise
|
||||
/// out-rank. Values are the app-wide defaults from `$gauzy-density` (themes.scss).
|
||||
@mixin thumbnail-row-padding() {
|
||||
--gauzy-table-cell-padding-y: 0.375rem;
|
||||
--gauzy-table-cell-padding-x: 0.625rem;
|
||||
--gauzy-table-header-padding-y: 0.5rem;
|
||||
--gauzy-table-header-padding-x: 0.625rem;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,16 @@
|
||||
padding-inline: 0;
|
||||
height: nb-theme(tabset-tab-text-line-height);
|
||||
|
||||
// Below `dsk` the strip swaps the theme token for a fixed box in
|
||||
// `styles/includes/_tabset.scss` — both families do, since that sheet stopped
|
||||
// naming only the route one — so this row has to follow it there or it is no
|
||||
// longer the strip's own row. Read from the shared token rather than copied,
|
||||
// so the strip cannot be retuned without this row coming with it.
|
||||
@include respond(dsk) {
|
||||
padding-block: $tabset-dsk-tab-padding-block;
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
// Centre on that line. The buttons are taller than a tab label, so they
|
||||
// overhang the line box symmetrically, which is what keeps them centred on
|
||||
// the strip instead of hanging below it.
|
||||
|
||||
@@ -2,17 +2,44 @@
|
||||
@forward 'var';
|
||||
@use 'var' as *;
|
||||
|
||||
nb-route-tabset {
|
||||
ul.route-tabset {
|
||||
// A tab strip is a tab strip. Nebular ships two components for one control —
|
||||
// `nb-tabset` switches a panel, `nb-route-tabset` switches a route — and every
|
||||
// sheet in this repo that dressed them had named only the route one, so the two
|
||||
// were drawn differently wherever both appear in the same app: the route strips
|
||||
// (organization edit, employee edit) took a 16px box and stacked their icons,
|
||||
// and every `nb-tabset` (Employment types, Time off, Candidates, Jobs,
|
||||
// Inventory) kept the theme's own box and a 0.5rem inline icon gap.
|
||||
//
|
||||
// One definition, included for both, so they cannot drift again.
|
||||
//
|
||||
// @param {String} $strip - the class on the strip's own `<ul>`. Named rather
|
||||
// than matched with a bare `ul`, which would also catch a list in a tab's
|
||||
// CONTENT and strip its indent.
|
||||
@mixin tab-strip($strip) {
|
||||
ul.#{$strip} {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tab-link {
|
||||
outline: none;
|
||||
@include respond(dsk) {
|
||||
padding: px2rem(16px) px2rem(16px);
|
||||
|
||||
// `outline: none` is there to drop the ring a mouse click leaves behind, and
|
||||
// it took the keyboard's with it — on the one control a keyboard user moves
|
||||
// through with arrows. Both families get it back the same way.
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--color-primary-500, #6e49e8);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
// Shared with `tabset-action-row()`, which has to match the block value to
|
||||
// stay as tall as this strip — see the note on the tokens in `var.scss`.
|
||||
@include respond(dsk) {
|
||||
padding: $tabset-dsk-tab-padding-block $tabset-dsk-tab-padding-inline;
|
||||
}
|
||||
|
||||
nb-icon {
|
||||
margin-right: 10px;
|
||||
|
||||
@include respond(dsk) {
|
||||
display: block;
|
||||
margin: auto;
|
||||
@@ -21,3 +48,11 @@ nb-route-tabset {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nb-route-tabset {
|
||||
@include tab-strip('route-tabset');
|
||||
}
|
||||
|
||||
nb-tabset {
|
||||
@include tab-strip('tabset');
|
||||
}
|
||||
|
||||
@@ -327,7 +327,14 @@ $nb-themes: nb-register-theme(
|
||||
tag-border-radius: 0.375rem,
|
||||
tabset-tab-hover-underline-color: none,
|
||||
tabset-tab-focus-underline-color: none,
|
||||
// Nebular ships UPPERCASE for BOTH tab families (styles/themes/_mapping.scss)
|
||||
// and only the `nb-tabset` half of it was ever overridden here — the same
|
||||
// oversight, and the same shout, the ghost-button line below describes. It is
|
||||
// what made an `nb-route-tabset` (organization edit, employee edit) read MAIN
|
||||
// / LOCATION / SETTINGS while every `nb-tabset` in the app read its tabs in
|
||||
// sentence case.
|
||||
tabset-tab-text-transform: capitalize,
|
||||
route-tabset-tab-text-transform: capitalize,
|
||||
button-filled-text-transform: none,
|
||||
// Ghost buttons were the one appearance still inheriting Nebular's stock
|
||||
// UPPERCASE (dialog Cancel vs filled Create read as two different apps).
|
||||
@@ -471,7 +478,14 @@ $nb-themes: nb-register-theme(
|
||||
tag-border-radius: 0.375rem,
|
||||
tabset-tab-hover-underline-color: none,
|
||||
tabset-tab-focus-underline-color: none,
|
||||
// Nebular ships UPPERCASE for BOTH tab families (styles/themes/_mapping.scss)
|
||||
// and only the `nb-tabset` half of it was ever overridden here — the same
|
||||
// oversight, and the same shout, the ghost-button line below describes. It is
|
||||
// what made an `nb-route-tabset` (organization edit, employee edit) read MAIN
|
||||
// / LOCATION / SETTINGS while every `nb-tabset` in the app read its tabs in
|
||||
// sentence case.
|
||||
tabset-tab-text-transform: capitalize,
|
||||
route-tabset-tab-text-transform: capitalize,
|
||||
button-filled-text-transform: none,
|
||||
// Ghost buttons were the one appearance still inheriting Nebular's stock
|
||||
// UPPERCASE (dialog Cancel vs filled Create read as two different apps).
|
||||
|
||||
@@ -17,6 +17,23 @@ $break-dsk: 1532px;
|
||||
@return #{$em}rem;
|
||||
}
|
||||
|
||||
// ── Tab strip metrics ────────────────────────────────────────────────────────
|
||||
//
|
||||
// The `dsk` box of a `.tab-link`, in the one place both sheets that depend on it
|
||||
// can read it:
|
||||
// * `includes/_tabset.scss` draws the box (`tab-strip()`);
|
||||
// * `gauzy/_tabset-actions.scss` REPRODUCES its block value, because
|
||||
// `tabset-action-row()` only stays vertically centred on the tab labels for
|
||||
// as long as it is exactly as tall as the strip.
|
||||
//
|
||||
// Those two lived as a hand-copied `px2rem(16px)` in each file, which is the
|
||||
// drift `_tabset-actions.scss`'s own "WHY THIS EXISTS" note was written about:
|
||||
// change the strip alone and the action buttons silently lose their centring
|
||||
// again. Split by axis because only the BLOCK value carries that contract — the
|
||||
// action row zeroes its own inline padding.
|
||||
$tabset-dsk-tab-padding-block: px2rem(16px);
|
||||
$tabset-dsk-tab-padding-inline: px2rem(16px);
|
||||
|
||||
@mixin respond($media) {
|
||||
@if $media==xs {
|
||||
@media only screen and (max-width: $break-xs) {
|
||||
|
||||
Reference in New Issue
Block a user