changes
This commit is contained in:
@@ -0,0 +1,392 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Interaction } from "@web/public/interaction";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
export class PortalSearchLive extends Interaction {
|
||||
static selector = ".o_portal_wrap";
|
||||
|
||||
dynamicContent = {
|
||||
".fp_subtask_search": {
|
||||
"t-on-input": this.debounced(this.onSubtaskInput, 80),
|
||||
"t-on-keydown": this.onSubtaskKeydown,
|
||||
},
|
||||
".o_portal_search_panel input[name='search']": {
|
||||
"t-on-input": this.debounced(this.onGlobalInput, 120),
|
||||
},
|
||||
};
|
||||
|
||||
setup() {
|
||||
this._lastQ = new WeakMap();
|
||||
}
|
||||
|
||||
start() {
|
||||
// Apply once on load (handles server-rendered ?search=... and the empty initial state)
|
||||
for (const input of this.el.querySelectorAll(".fp_subtask_search")) {
|
||||
this.filterCard(input);
|
||||
}
|
||||
const g = this.el.querySelector(".o_portal_search_panel input[name='search']");
|
||||
if (g && g.value) {
|
||||
this.filterTables(g);
|
||||
}
|
||||
}
|
||||
|
||||
onSubtaskInput(ev) {
|
||||
this.filterCard(ev.target);
|
||||
}
|
||||
|
||||
onSubtaskKeydown(ev) {
|
||||
if (ev.key === "Enter") {
|
||||
ev.preventDefault();
|
||||
this.filterCard(ev.target);
|
||||
} else if (ev.key === "Escape") {
|
||||
ev.target.value = "";
|
||||
this.filterCard(ev.target);
|
||||
}
|
||||
}
|
||||
|
||||
onGlobalInput(ev) {
|
||||
this.filterTables(ev.target);
|
||||
}
|
||||
|
||||
filterCard(input) {
|
||||
const card = input.closest(".card");
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
const list = card.querySelector("ul.list-group");
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
const q = (input.value || "").trim().toLowerCase();
|
||||
if (this._lastQ.get(input) === q) {
|
||||
return;
|
||||
}
|
||||
this._lastQ.set(input, q);
|
||||
let visible = 0;
|
||||
for (const li of list.children) {
|
||||
if (li.querySelector("form")) {
|
||||
// Skip inline "+ subtask" forms; show/hide them with their parent row.
|
||||
continue;
|
||||
}
|
||||
if (!q) {
|
||||
li.style.display = "";
|
||||
li.classList.remove("d-none");
|
||||
visible += 1;
|
||||
continue;
|
||||
}
|
||||
const text = (li.textContent || "").toLowerCase();
|
||||
const match = text.indexOf(q) !== -1;
|
||||
li.style.display = match ? "" : "none";
|
||||
li.classList.toggle("d-none", !match);
|
||||
if (match) {
|
||||
visible += 1;
|
||||
}
|
||||
}
|
||||
// Toggle a "no matches" empty hint
|
||||
let hint = card.querySelector(".fp_subtask_no_match");
|
||||
if (q && visible === 0) {
|
||||
if (!hint) {
|
||||
hint = document.createElement("div");
|
||||
hint.className = "fp_subtask_no_match card-body text-muted small";
|
||||
hint.textContent = "No sub-tasks match your search.";
|
||||
list.insertAdjacentElement("afterend", hint);
|
||||
}
|
||||
hint.style.display = "";
|
||||
} else if (hint) {
|
||||
hint.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
filterTables(input) {
|
||||
const q = (input.value || "").trim().toLowerCase();
|
||||
const tables = this.el.querySelectorAll("table.table, table.o_portal_my_doc_table");
|
||||
tables.forEach((table) => {
|
||||
table.querySelectorAll("tbody").forEach((tbody) => {
|
||||
let visibleTaskRows = 0;
|
||||
let groupHeaderRow = null;
|
||||
tbody.querySelectorAll("tr").forEach((row) => {
|
||||
const isGroupHeader =
|
||||
row.getAttribute("name") === "grouped_tasks_groupby_columns" ||
|
||||
row.classList.contains("table-light");
|
||||
if (isGroupHeader) {
|
||||
if (groupHeaderRow !== null) {
|
||||
groupHeaderRow.style.display = visibleTaskRows ? "" : "none";
|
||||
}
|
||||
groupHeaderRow = row;
|
||||
visibleTaskRows = 0;
|
||||
return;
|
||||
}
|
||||
if (!q) {
|
||||
row.style.display = "";
|
||||
visibleTaskRows += 1;
|
||||
return;
|
||||
}
|
||||
const text = (row.textContent || "").toLowerCase();
|
||||
const match = text.indexOf(q) !== -1;
|
||||
row.style.display = match ? "" : "none";
|
||||
if (match) {
|
||||
visibleTaskRows += 1;
|
||||
}
|
||||
});
|
||||
if (groupHeaderRow !== null) {
|
||||
groupHeaderRow.style.display = visibleTaskRows ? "" : "none";
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("public.interactions").add("fusion_project_portal.portal_search_live", PortalSearchLive);
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// /my/projects: client-side search + sort + group on the redesigned card grid.
|
||||
// All projects are server-rendered into the page; this just toggles visibility
|
||||
// and re-orders existing DOM nodes — no RPC.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SORT_KEYS = {
|
||||
name: (el) => (el.dataset.name || "").toLowerCase(),
|
||||
pct: (el) => -parseFloat(el.dataset.pct || "0"),
|
||||
tasks: (el) => -parseInt(el.dataset.tasks || "0", 10),
|
||||
activity: (el) => {
|
||||
const v = el.dataset.activity || "";
|
||||
// sort newest first; missing values go last
|
||||
return v ? `0${v.split("").reverse().join("")}` : "z";
|
||||
},
|
||||
};
|
||||
|
||||
const SORT_LABELS = {
|
||||
name: "Name",
|
||||
pct: "% Complete",
|
||||
tasks: "Task Count",
|
||||
activity: "Last Activity",
|
||||
};
|
||||
|
||||
export class FusionProjectsPage extends Interaction {
|
||||
static selector = ".fp_projects_page";
|
||||
|
||||
dynamicContent = {
|
||||
".fp_projects_search": {
|
||||
"t-on-input": this.debounced(this.onSearchInput, 80),
|
||||
"t-on-keydown": this.onSearchKeydown,
|
||||
},
|
||||
".fp_projects_group_picker .btn": {
|
||||
"t-on-click.prevent.withTarget": this.onGroupClick,
|
||||
},
|
||||
".fp_projects_sort_picker .dropdown-item": {
|
||||
"t-on-click.prevent.withTarget": this.onSortClick,
|
||||
},
|
||||
};
|
||||
|
||||
setup() {
|
||||
this._sortKey = "name";
|
||||
this._group = this.el.dataset.defaultGroup || "status";
|
||||
this._cards = Array.from(this.el.querySelectorAll(".fp_project_card"));
|
||||
}
|
||||
|
||||
start() {
|
||||
this._applyGroup(this._group, /*initial=*/ true);
|
||||
this._applySort();
|
||||
}
|
||||
|
||||
onSearchInput(ev) {
|
||||
this._applyFilter((ev.target.value || "").trim().toLowerCase());
|
||||
}
|
||||
|
||||
onSearchKeydown(ev) {
|
||||
if (ev.key === "Escape") {
|
||||
ev.preventDefault();
|
||||
ev.target.value = "";
|
||||
this._applyFilter("");
|
||||
}
|
||||
}
|
||||
|
||||
onGroupClick(ev, currentTargetEl) {
|
||||
const group = currentTargetEl.dataset.group;
|
||||
if (!group || group === this._group) {
|
||||
return;
|
||||
}
|
||||
this.el.querySelectorAll(".fp_projects_group_picker .btn").forEach((b) =>
|
||||
b.classList.toggle("active", b === currentTargetEl)
|
||||
);
|
||||
this._applyGroup(group);
|
||||
this._applySort();
|
||||
// Re-apply the active search after re-grouping
|
||||
const searchEl = this.el.querySelector(".fp_projects_search");
|
||||
this._applyFilter((searchEl && searchEl.value || "").trim().toLowerCase());
|
||||
}
|
||||
|
||||
onSortClick(ev, currentTargetEl) {
|
||||
const key = currentTargetEl.dataset.sort;
|
||||
if (!key || !(key in SORT_KEYS)) {
|
||||
return;
|
||||
}
|
||||
this._sortKey = key;
|
||||
const labelEl = this.el.querySelector(".fp_sort_label");
|
||||
if (labelEl) {
|
||||
labelEl.textContent = SORT_LABELS[key];
|
||||
}
|
||||
this.el.querySelectorAll(".fp_projects_sort_picker .dropdown-item").forEach((item) =>
|
||||
item.classList.toggle("active", item === currentTargetEl)
|
||||
);
|
||||
this._applySort();
|
||||
}
|
||||
|
||||
_applyFilter(q) {
|
||||
let visibleTotal = 0;
|
||||
const perColumn = new Map();
|
||||
for (const card of this._cards) {
|
||||
const matches =
|
||||
!q ||
|
||||
(card.dataset.name || "").toLowerCase().includes(q) ||
|
||||
(card.dataset.customer || "").toLowerCase().includes(q);
|
||||
card.classList.toggle("d-none", !matches);
|
||||
if (matches) {
|
||||
visibleTotal += 1;
|
||||
const col = card.closest(".fp_projects_col");
|
||||
const key = col ? (col.dataset.bucket || col.dataset.group || "") : "";
|
||||
perColumn.set(key, (perColumn.get(key) || 0) + 1);
|
||||
}
|
||||
}
|
||||
// Update column counts and toggle column-empty fallback
|
||||
for (const col of this.el.querySelectorAll(".fp_projects_col")) {
|
||||
const key = col.dataset.bucket || col.dataset.group || "";
|
||||
const n = perColumn.get(key) || 0;
|
||||
const countEl = col.querySelector(".fp_col_count");
|
||||
if (countEl) {
|
||||
countEl.textContent = String(n);
|
||||
}
|
||||
const empty = col.querySelector(".fp_col_empty");
|
||||
if (empty) {
|
||||
empty.classList.toggle("d-none", n > 0);
|
||||
}
|
||||
}
|
||||
const noMatch = this.el.querySelector(".fp_projects_no_match");
|
||||
if (noMatch) {
|
||||
noMatch.classList.toggle("d-none", visibleTotal !== 0 || !q);
|
||||
}
|
||||
}
|
||||
|
||||
_applyGroup(group, initial = false) {
|
||||
this._group = group;
|
||||
this.el.dataset.currentGroup = group;
|
||||
const cols = this.el.querySelector(".fp_projects_cols");
|
||||
if (!cols) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (group === "status") {
|
||||
// Restore the original 3 status columns rendered server-side
|
||||
this._restoreStatusColumns(cols);
|
||||
return;
|
||||
}
|
||||
if (group === "none") {
|
||||
this._renderFlat(cols);
|
||||
return;
|
||||
}
|
||||
if (group === "customer") {
|
||||
this._renderByCustomer(cols);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_restoreStatusColumns(cols) {
|
||||
// Cache the original markup once so we can flip back without RPC
|
||||
if (!this._originalCols) {
|
||||
this._originalCols = cols.innerHTML;
|
||||
} else {
|
||||
cols.innerHTML = this._originalCols;
|
||||
this._cards = Array.from(this.el.querySelectorAll(".fp_project_card"));
|
||||
}
|
||||
}
|
||||
|
||||
_renderFlat(cols) {
|
||||
if (!this._originalCols) {
|
||||
this._originalCols = cols.innerHTML;
|
||||
}
|
||||
const cards = Array.from(this.el.querySelectorAll(".fp_project_card"));
|
||||
cols.innerHTML = "";
|
||||
const col = this._buildColumn("All projects", "gray", cards.length);
|
||||
col.dataset.group = "all";
|
||||
const body = col.querySelector(".fp_projects_col_body");
|
||||
for (const card of cards) {
|
||||
body.appendChild(card);
|
||||
}
|
||||
cols.appendChild(col);
|
||||
this._cards = cards;
|
||||
}
|
||||
|
||||
_renderByCustomer(cols) {
|
||||
if (!this._originalCols) {
|
||||
this._originalCols = cols.innerHTML;
|
||||
}
|
||||
const cards = Array.from(this.el.querySelectorAll(".fp_project_card"));
|
||||
const groups = new Map();
|
||||
for (const card of cards) {
|
||||
const key = (card.dataset.customer || "Unassigned").trim() || "Unassigned";
|
||||
if (!groups.has(key)) {
|
||||
groups.set(key, []);
|
||||
}
|
||||
groups.get(key).push(card);
|
||||
}
|
||||
const orderedKeys = Array.from(groups.keys()).sort((a, b) => {
|
||||
if (a === "Unassigned") return 1;
|
||||
if (b === "Unassigned") return -1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
cols.innerHTML = "";
|
||||
for (const key of orderedKeys) {
|
||||
const items = groups.get(key);
|
||||
const col = this._buildColumn(key, "gray", items.length);
|
||||
col.dataset.group = key;
|
||||
const body = col.querySelector(".fp_projects_col_body");
|
||||
for (const card of items) {
|
||||
body.appendChild(card);
|
||||
}
|
||||
cols.appendChild(col);
|
||||
}
|
||||
this._cards = cards;
|
||||
}
|
||||
|
||||
_buildColumn(label, dot, count) {
|
||||
const col = document.createElement("div");
|
||||
col.className = "fp_projects_col";
|
||||
col.innerHTML = `
|
||||
<div class="fp_projects_col_head">
|
||||
<span class="fp_col_label"><span class="fp_dot fp_dot_${dot}"></span>${this._escape(label)}</span>
|
||||
<span class="fp_col_count">${count}</span>
|
||||
</div>
|
||||
<div class="fp_projects_col_body"></div>
|
||||
`;
|
||||
return col;
|
||||
}
|
||||
|
||||
_escape(s) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = s;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
_applySort() {
|
||||
const fn = SORT_KEYS[this._sortKey] || SORT_KEYS.name;
|
||||
for (const col of this.el.querySelectorAll(".fp_projects_col")) {
|
||||
const body = col.querySelector(".fp_projects_col_body");
|
||||
if (!body) continue;
|
||||
const cards = Array.from(body.querySelectorAll(".fp_project_card"));
|
||||
cards.sort((a, b) => {
|
||||
const av = fn(a);
|
||||
const bv = fn(b);
|
||||
if (av < bv) return -1;
|
||||
if (av > bv) return 1;
|
||||
return 0;
|
||||
});
|
||||
for (const c of cards) {
|
||||
body.appendChild(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("public.interactions").add("fusion_project_portal.projects_page", FusionProjectsPage);
|
||||
@@ -0,0 +1,263 @@
|
||||
// Fusion portal: redesigned /my/projects page
|
||||
// Tokens are wrapped in CSS custom properties so a future portal dark-mode
|
||||
// pass can flip the surface colors without rewriting any rules.
|
||||
|
||||
$fp-page-bg: var(--fp-page-bg, #f3f4f6);
|
||||
$fp-card-bg: var(--fp-card-bg, #ffffff);
|
||||
$fp-border: var(--fp-border, #d8dadd);
|
||||
$fp-text: var(--fp-text, #0f172a);
|
||||
$fp-muted: var(--fp-muted, #6b7280);
|
||||
$fp-track: var(--fp-track, #e5e7eb);
|
||||
|
||||
$fp-blue-bg: var(--fp-blue-bg, #dbeafe);
|
||||
$fp-blue-fg: var(--fp-blue-fg, #1d4ed8);
|
||||
$fp-green-bg: var(--fp-green-bg, #dcfce7);
|
||||
$fp-green-fg: var(--fp-green-fg, #166534);
|
||||
$fp-amber-bg: var(--fp-amber-bg, #fef3c7);
|
||||
$fp-amber-fg: var(--fp-amber-fg, #92400e);
|
||||
$fp-cyan-bg: var(--fp-cyan-bg, #cffafe);
|
||||
$fp-cyan-fg: var(--fp-cyan-fg, #155e75);
|
||||
$fp-gray-bg: var(--fp-gray-bg, #e5e7eb);
|
||||
$fp-gray-fg: var(--fp-gray-fg, #374151);
|
||||
|
||||
$fp-bar-high: var(--fp-bar-high, #22c55e);
|
||||
$fp-bar-mid: var(--fp-bar-mid, #3b82f6);
|
||||
$fp-bar-low: var(--fp-bar-low, #f59e0b);
|
||||
|
||||
$fp-radius: 10px;
|
||||
$fp-radius-sm: 6px;
|
||||
|
||||
.fp_projects_page {
|
||||
.fp_projects_header {
|
||||
h3 {
|
||||
color: $fp-text;
|
||||
}
|
||||
.fp_projects_search_wrap {
|
||||
min-width: 220px;
|
||||
max-width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
// 3-column status grid
|
||||
.fp_projects_cols {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
|
||||
@media (max-width: 992px) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
// Flat / customer-grouped variants reuse a single column
|
||||
&[data-current-group="none"] .fp_projects_cols,
|
||||
&[data-current-group="customer"] .fp_projects_cols {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.fp_projects_col {
|
||||
min-width: 0;
|
||||
|
||||
.fp_projects_col_head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 4px 8px;
|
||||
border-bottom: 2px solid $fp-track;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.fp_col_label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: $fp-gray-fg;
|
||||
}
|
||||
.fp_col_count {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: $fp-muted;
|
||||
background: $fp-gray-bg;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
}
|
||||
|
||||
.fp_projects_col_body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.fp_col_empty {
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
padding: 22px 8px;
|
||||
background: $fp-card-bg;
|
||||
border: 1px dashed $fp-border;
|
||||
border-radius: $fp-radius;
|
||||
}
|
||||
}
|
||||
|
||||
.fp_dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 999px;
|
||||
display: inline-block;
|
||||
&.fp_dot_green { background: #22c55e; }
|
||||
&.fp_dot_amber { background: #f59e0b; }
|
||||
&.fp_dot_gray { background: #9ca3af; }
|
||||
}
|
||||
|
||||
// Card
|
||||
.fp_project_card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 12px 13px 11px;
|
||||
background: $fp-card-bg;
|
||||
border: 1px solid $fp-border;
|
||||
border-radius: $fp-radius;
|
||||
text-decoration: none;
|
||||
color: $fp-text;
|
||||
transition: border-color 120ms ease, box-shadow 120ms ease, transform 120ms ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #93c5fd;
|
||||
box-shadow: 0 4px 14px rgba(59, 130, 246, 0.10);
|
||||
transform: translateY(-1px);
|
||||
text-decoration: none;
|
||||
color: $fp-text;
|
||||
}
|
||||
|
||||
.fp_card_top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
|
||||
.fp_card_title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 1.25;
|
||||
color: $fp-text;
|
||||
word-break: break-word;
|
||||
}
|
||||
.fp_card_pct {
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
&.fp_pct_high { color: #15803d; }
|
||||
&.fp_pct_mid { color: #1d4ed8; }
|
||||
&.fp_pct_low { color: #b45309; }
|
||||
&.fp_card_pct_muted { color: #9ca3af; font-weight: 500; }
|
||||
}
|
||||
}
|
||||
|
||||
.fp_card_sub {
|
||||
font-size: 11px;
|
||||
color: $fp-muted;
|
||||
min-height: 1em;
|
||||
}
|
||||
|
||||
.fp_card_bar {
|
||||
height: 5px;
|
||||
background: $fp-track;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
|
||||
> span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: $fp-bar-mid;
|
||||
transition: width 200ms ease;
|
||||
}
|
||||
&.fp_bar_high > span { background: $fp-bar-high; }
|
||||
&.fp_bar_mid > span { background: $fp-bar-mid; }
|
||||
&.fp_bar_low > span { background: $fp-bar-low; }
|
||||
}
|
||||
|
||||
.fp_card_stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
|
||||
.fp_chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 10.5px;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
|
||||
&.fp_chip_blue { background: $fp-blue-bg; color: $fp-blue-fg; }
|
||||
&.fp_chip_green { background: $fp-green-bg; color: $fp-green-fg; }
|
||||
&.fp_chip_amber { background: $fp-amber-bg; color: $fp-amber-fg; }
|
||||
&.fp_chip_cyan { background: $fp-cyan-bg; color: $fp-cyan-fg; }
|
||||
&.fp_chip_gray { background: $fp-gray-bg; color: $fp-gray-fg; }
|
||||
}
|
||||
}
|
||||
|
||||
.fp_card_footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 2px;
|
||||
|
||||
.fp_avatars {
|
||||
display: inline-flex;
|
||||
.fp_avatar {
|
||||
position: relative;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
background: #e0e7ff;
|
||||
color: #3730a3;
|
||||
font-size: 9.5px;
|
||||
font-weight: 700;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: -6px;
|
||||
border: 2px solid $fp-card-bg;
|
||||
|
||||
&:first-child { margin-left: 0; }
|
||||
img {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.fp_avatar_text {
|
||||
// hidden when image loads; shown when image fails (handled in JS via classList)
|
||||
display: none;
|
||||
}
|
||||
&.fp_avatar_initials .fp_avatar_text { display: inline; }
|
||||
&.fp_avatar_initials img { display: none; }
|
||||
|
||||
&.fp_avatar_more {
|
||||
background: $fp-gray-bg;
|
||||
color: $fp-gray-fg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty page state
|
||||
.fp_projects_empty {
|
||||
background: $fp-card-bg;
|
||||
border: 1px dashed $fp-border;
|
||||
border-radius: $fp-radius;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user