456 files
This commit is contained in:
@@ -34,6 +34,8 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
_loading: { type: Object },
|
||||
_signed: { state: true },
|
||||
_review: { state: true },
|
||||
_ratings: { state: true },
|
||||
_rejecting: { state: true },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -49,6 +51,7 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
constructor() {
|
||||
super();
|
||||
this._loading = {};
|
||||
this._ratings = {}; // completion_id -> picked 1-3 star rating (#927)
|
||||
this._signed = {}; // safe photo path -> per-viewer signed path
|
||||
this._inflight = new Set();
|
||||
}
|
||||
@@ -212,7 +215,7 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
}
|
||||
|
||||
.time-group {
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
@@ -331,11 +334,11 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
}
|
||||
|
||||
.action-buttons.left {
|
||||
margin-right: 12px;
|
||||
margin-inline-end: 12px;
|
||||
}
|
||||
|
||||
.action-buttons.right {
|
||||
margin-left: 12px;
|
||||
margin-inline-start: 12px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
@@ -411,8 +414,8 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
}
|
||||
|
||||
.tm-rv-quote {
|
||||
border-left: 3px solid var(--accent-color, #ffc107);
|
||||
padding: 6px 0 6px 10px;
|
||||
border-inline-start: 3px solid var(--accent-color, #ffc107);
|
||||
padding-block: 6px; padding-inline: 10px 0;
|
||||
margin-bottom: 12px;
|
||||
font-size: 0.92rem;
|
||||
white-space: pre-wrap;
|
||||
@@ -468,6 +471,29 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
color: var(--primary-text-color, #33373d);
|
||||
}
|
||||
.tm-rv-btn.primary { background: #4caf50; color: #fff; }
|
||||
.tm-rv-btn.danger { background: #f44336; color: #fff; }
|
||||
|
||||
/* Reject-with-a-reason sheet (#976) */
|
||||
.tm-rj-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
|
||||
.tm-rj-chip {
|
||||
border: 2px solid var(--divider-color, #e0e0e0);
|
||||
background: var(--card-background-color, #fff);
|
||||
color: var(--primary-text-color, #33373d);
|
||||
border-radius: 999px;
|
||||
padding: 5px 12px;
|
||||
font: inherit;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tm-rj-chip.on { border-color: #f44336; background: rgba(244, 67, 54, 0.12); }
|
||||
.tm-rj-input { font-size: 0.95rem; font-weight: 600; }
|
||||
.tm-rj-input:focus { border-color: #f44336; }
|
||||
.tm-rj-hint {
|
||||
font-size: 0.75rem;
|
||||
color: var(--secondary-text-color, #6b7280);
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
/* Mandatory-miss review actions (#532) */
|
||||
.action-button.penalty {
|
||||
@@ -486,7 +512,7 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
}
|
||||
|
||||
.approval-item.mandatory {
|
||||
border-left: 5px solid var(--fun-red, #e74c3c);
|
||||
border-inline-start: 5px solid var(--fun-red, #e74c3c);
|
||||
}
|
||||
|
||||
.approval-item.mandatory .penalty-note {
|
||||
@@ -605,7 +631,7 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
/* Clean Pro — divided list */
|
||||
.ap-cp-item { padding: 10px 0; flex-wrap: wrap; row-gap: 8px; }
|
||||
.ap-cp-mid { flex: 1 1 130px; min-width: 130px; }
|
||||
.ap-cp-actions { display: flex; gap: 8px; flex: 0 0 auto; margin-left: auto; }
|
||||
.ap-cp-actions { display: flex; gap: 8px; flex: 0 0 auto; margin-inline-start: auto; }
|
||||
.ap-cp-title { font-weight: 600; }
|
||||
.ap-cp-sub { font-size: 12px; }
|
||||
.ap-cp-photo { width: 32px; height: 32px; border-radius: 8px; overflow: hidden;
|
||||
@@ -616,6 +642,22 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
|
||||
.ap-d-photo-link { line-height: 0; text-decoration: none; }
|
||||
|
||||
/* Quality rating star picker (#927) — shared by every design. */
|
||||
.ap-stars { display: inline-flex; align-items: center; gap: 1px; flex: none; }
|
||||
.ap-star {
|
||||
background: none; border: 0; margin: 0; padding: 2px 3px; cursor: pointer;
|
||||
font: inherit; font-size: 20px; line-height: 1; border-radius: 6px;
|
||||
color: var(--tmd-dim, var(--secondary-text-color, #9e9e9e));
|
||||
}
|
||||
.ap-star:hover, .ap-star.on { color: var(--tmd-gold, #f5b301); }
|
||||
/* Console rows are one tight line: the picker sits under the subtitle. */
|
||||
.ap-cn-stars { margin-top: 3px; }
|
||||
.ap-cn-stars .ap-star { font-size: 17px; padding: 1px 2px; }
|
||||
.ap-star:focus-visible { outline: 2px solid var(--tmd-accent, var(--primary-color)); outline-offset: 1px; }
|
||||
.ap-star:disabled { cursor: default; opacity: .5; }
|
||||
.tm-rv-stars { margin: 4px 0 10px; }
|
||||
.tm-rv-stars .ap-star { font-size: 28px; }
|
||||
|
||||
/* Scrollable list (parity with classic content) */
|
||||
.ap-d-scroll { max-height: 360px; overflow-y: auto; }
|
||||
`;
|
||||
@@ -633,6 +675,9 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
header_color: '#27ae60',
|
||||
...config,
|
||||
};
|
||||
// Dashboards saved before #1010 stored the English default title; treat it
|
||||
// as unset so the translated default is shown instead.
|
||||
if (this.config.title === "Pending Approvals") this.config.title = "";
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
@@ -646,7 +691,6 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
static getStubConfig() {
|
||||
return {
|
||||
entity: "sensor.taskmate_overview",
|
||||
title: "Pending Approvals",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -732,6 +776,7 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
`}
|
||||
</div>
|
||||
${this._renderReview()}
|
||||
${this._renderRejectSheet()}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
@@ -894,6 +939,7 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
${this._designHeader(hd, items.length, completions)}
|
||||
<div class="tmd-bd">${body}</div>
|
||||
${this._renderReview()}
|
||||
${this._renderRejectSheet()}
|
||||
</ha-card>`;
|
||||
}
|
||||
|
||||
@@ -924,7 +970,7 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
}
|
||||
if (it.kind === "claim") {
|
||||
const approve = () => this._handleApproveReward(it.id);
|
||||
const reject = () => this._handleRejectReward(it.id);
|
||||
const reject = () => this._handleRejectReward(it.id, it.title, it.childName);
|
||||
return this._apActionPair(approve, reject, isLoading, shape);
|
||||
}
|
||||
// mandatory miss: apply penalty (if any) / postpone / dismiss
|
||||
@@ -952,7 +998,7 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
|
||||
_apActionPair(approve, reject, isLoading, shape, completion = null) {
|
||||
const adjust = completion
|
||||
? html`<button class="btn ghost sm ${shape === "round" ? "round" : ""}" ?disabled="${isLoading}"
|
||||
? html`${shape === "round" ? "" : this._renderStars(completion)}<button class="btn ghost sm ${shape === "round" ? "round" : ""}" ?disabled="${isLoading}"
|
||||
title="${this._t('approvals.award_label')}"
|
||||
@click="${() => this._openReview(completion)}">${this._reviewGlyph(completion)}</button>`
|
||||
: "";
|
||||
@@ -1036,6 +1082,7 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
<div style="flex:1;min-width:0">
|
||||
<div class="ap-cn-title">${it.title}</div>
|
||||
<div class="muted num ap-cn-sub">${(it.childName || '').toUpperCase()} · +${it.points} XP</div>
|
||||
${it.kind === "completion" ? this._renderStars(it.completion, "ap-cn-stars") : ""}
|
||||
</div>
|
||||
${this._apPhotoDesigned(it, "ap-cn-photo")}
|
||||
${this._designActions(it, "round")}
|
||||
@@ -1078,7 +1125,7 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
return html`
|
||||
<div class="day-group">
|
||||
<div class="day-header">
|
||||
<ha-icon icon="mdi:gift-outline" style="--mdc-icon-size: 18px; vertical-align: -3px; margin-right: 6px;"></ha-icon>
|
||||
<ha-icon icon="mdi:gift-outline" style="--mdc-icon-size: 18px; vertical-align: -3px; margin-inline-end: 6px;"></ha-icon>
|
||||
${this._t('approvals.reward_claims_section')}
|
||||
</div>
|
||||
${claims.map((claim) => this._renderClaimItem(claim))}
|
||||
@@ -1098,7 +1145,7 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
<div class="action-buttons left">
|
||||
<button
|
||||
class="action-button reject ${isLoading ? 'loading' : ''}"
|
||||
@click="${() => this._handleRejectReward(claimId)}"
|
||||
@click="${() => this._handleRejectReward(claimId, rewardName, childName)}"
|
||||
title="${this._t('approvals.reject')}"
|
||||
?disabled="${isLoading}"
|
||||
>
|
||||
@@ -1107,7 +1154,7 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<span class="chore-name">
|
||||
<ha-icon icon="${claim.reward_icon || 'mdi:gift'}" style="--mdc-icon-size: 16px; vertical-align: -3px; margin-right: 4px;"></ha-icon>
|
||||
<ha-icon icon="${claim.reward_icon || 'mdi:gift'}" style="--mdc-icon-size: 16px; vertical-align: -3px; margin-inline-end: 4px;"></ha-icon>
|
||||
${rewardName}
|
||||
</span>
|
||||
<div class="item-details">
|
||||
@@ -1139,16 +1186,16 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
await this._callClaimService('approve_reward', claimId);
|
||||
}
|
||||
|
||||
async _handleRejectReward(claimId) {
|
||||
await this._callClaimService('reject_reward', claimId);
|
||||
_handleRejectReward(claimId, name = "", childName = "") {
|
||||
this._openRejectSheet({ kind: "reward", id: claimId, name, childName });
|
||||
}
|
||||
|
||||
async _callClaimService(service, claimId) {
|
||||
async _callClaimService(service, claimId, extra = null) {
|
||||
if (this._loading[claimId]) return;
|
||||
this._loading = { ...this._loading, [claimId]: true };
|
||||
this.requestUpdate();
|
||||
try {
|
||||
await this.hass.callService('taskmate', service, { claim_id: claimId });
|
||||
await this.hass.callService('taskmate', service, { claim_id: claimId, ...(extra || {}) });
|
||||
} catch (error) {
|
||||
console.error(`Failed to call ${service}:`, error);
|
||||
if (this.hass.callService) {
|
||||
@@ -1177,7 +1224,7 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
return html`
|
||||
<div class="day-group">
|
||||
<div class="day-header">
|
||||
<ha-icon icon="mdi:alert-octagon-outline" style="--mdc-icon-size: 18px; vertical-align: -3px; margin-right: 6px;"></ha-icon>
|
||||
<ha-icon icon="mdi:alert-octagon-outline" style="--mdc-icon-size: 18px; vertical-align: -3px; margin-inline-end: 6px;"></ha-icon>
|
||||
${this._t('approvals.mandatory_section')}
|
||||
</div>
|
||||
${misses.map((miss) => this._renderMissItem(miss))}
|
||||
@@ -1196,7 +1243,7 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
<div class="approval-item mandatory ${isLoading ? 'loading' : ''}">
|
||||
<div class="item-info">
|
||||
<span class="chore-name">
|
||||
<ha-icon icon="mdi:alert-circle-outline" style="--mdc-icon-size: 16px; vertical-align: -3px; margin-right: 4px;"></ha-icon>
|
||||
<ha-icon icon="mdi:alert-circle-outline" style="--mdc-icon-size: 16px; vertical-align: -3px; margin-inline-end: 4px;"></ha-icon>
|
||||
${choreName}
|
||||
</span>
|
||||
<div class="item-details">
|
||||
@@ -1503,6 +1550,7 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
<ha-icon icon="mdi:star"></ha-icon>
|
||||
${completion.points}
|
||||
</span>
|
||||
${this._renderStars(completion)}
|
||||
</div>
|
||||
${this._photoHref(completion.photo_url) ? html`
|
||||
<a class="approval-photo" href="${this._photoHref(completion.photo_url)}" target="_blank" rel="noopener"
|
||||
@@ -1533,11 +1581,157 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
}
|
||||
|
||||
async _handleApprove(completion) {
|
||||
await this._callService("approve_chore", completion.completion_id);
|
||||
const id = completion.completion_id;
|
||||
const rating = this._ratingMultipliers() ? this._ratings[id] || 0 : 0;
|
||||
await this._callService("approve_chore", id, rating ? { rating } : null);
|
||||
if (rating) this._clearRating(id);
|
||||
}
|
||||
|
||||
async _handleReject(completion) {
|
||||
await this._callService("reject_chore", completion.completion_id);
|
||||
/* ── Quality rating (#927) ────────────────────────────────────────────
|
||||
Published by the pending-approvals sensor only while the feature is on,
|
||||
so its absence is the "off" switch. Picking a star is a selection, not an
|
||||
approval — Approve still does the approving, and tapping the picked star
|
||||
again clears it back to "unrated" (100%). */
|
||||
|
||||
_ratingMultipliers() {
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity))
|
||||
|| this.hass?.states?.[this.config.entity]?.attributes || {};
|
||||
const m = attrs.quality_rating && attrs.quality_rating.multipliers;
|
||||
return Array.isArray(m) && m.length === 3 ? m : null;
|
||||
}
|
||||
|
||||
_pickRating(completion, n) {
|
||||
const id = completion.completion_id;
|
||||
const next = this._ratings[id] === n ? 0 : n;
|
||||
this._ratings = { ...this._ratings, [id]: next };
|
||||
// In the review sheet the parent sees the number they will pay, so a star
|
||||
// rescales it there rather than silently on the server.
|
||||
if (this._review && this._review.completion.completion_id === id) {
|
||||
const input = this.renderRoot && this.renderRoot.querySelector("#tm-review-points");
|
||||
const mults = this._ratingMultipliers();
|
||||
if (input && mults) {
|
||||
const base = Number(this._review.base) || 0;
|
||||
input.value = String(Math.max(0, Math.round(next ? base * mults[next - 1] : base)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_clearRating(id) {
|
||||
if (!(id in this._ratings)) return;
|
||||
const next = { ...this._ratings };
|
||||
delete next[id];
|
||||
this._ratings = next;
|
||||
}
|
||||
|
||||
_renderStars(completion, extraClass = "") {
|
||||
const mults = this._ratingMultipliers();
|
||||
if (!mults || !completion) return "";
|
||||
const id = completion.completion_id;
|
||||
const sel = this._ratings[id] || 0;
|
||||
const busy = !!this._loading[id];
|
||||
return html`<span class="ap-stars ${extraClass}" role="group" aria-label="${this._t('approvals.rating_label')}">
|
||||
${[1, 2, 3].map((n) => {
|
||||
const label = this._t('approvals.rating_star_title', { count: n, percent: Math.round(Number(mults[n - 1]) * 100) });
|
||||
// Filled vs outlined, not just gold vs grey, so the pick reads without colour.
|
||||
return html`<button type="button" class="ap-star ${n <= sel ? "on" : ""}" ?disabled="${busy}"
|
||||
aria-pressed="${n === sel ? "true" : "false"}" title="${label}" aria-label="${label}"
|
||||
@click="${(e) => { e.stopPropagation(); this._pickRating(completion, n); }}">${n <= sel ? "★" : "☆"}</button>`;
|
||||
})}
|
||||
</span>`;
|
||||
}
|
||||
|
||||
_handleReject(completion) {
|
||||
this._openRejectSheet({
|
||||
kind: "chore",
|
||||
id: completion.completion_id,
|
||||
name: completion.chore_name || "",
|
||||
childName: completion.child_name || "",
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Reject with a reason (#976) ─────────────────────────────────────
|
||||
Reject opens a small sheet: quick-pick chips or free text, both
|
||||
optional — an empty box rejects exactly as before. Rendered from both
|
||||
the classic and the designed paths. The input is uncontrolled so a
|
||||
coordinator refresh can't wipe what the parent is typing. */
|
||||
|
||||
_rejectChips() {
|
||||
return ["reject.chip_not_finished", "reject.chip_redo", "reject.chip_not_today"].map(k => this._t(k));
|
||||
}
|
||||
|
||||
_rejectInput() {
|
||||
return this.renderRoot && this.renderRoot.querySelector("#tm-reject-reason");
|
||||
}
|
||||
|
||||
_openRejectSheet(target) {
|
||||
this._rejecting = { ...target, chip: "" };
|
||||
this.updateComplete.then(() => {
|
||||
const input = this._rejectInput();
|
||||
if (!input) return;
|
||||
input.value = "";
|
||||
input.focus();
|
||||
});
|
||||
}
|
||||
|
||||
_closeRejectSheet() {
|
||||
this._rejecting = null;
|
||||
}
|
||||
|
||||
_pickRejectChip(text) {
|
||||
const input = this._rejectInput();
|
||||
const chip = this._rejecting && this._rejecting.chip === text ? "" : text;
|
||||
if (input) input.value = chip;
|
||||
this._rejecting = { ...this._rejecting, chip };
|
||||
}
|
||||
|
||||
async _confirmReject() {
|
||||
const target = this._rejecting;
|
||||
if (!target) return;
|
||||
const input = this._rejectInput();
|
||||
const reason = String((input && input.value) || "").trim().slice(0, 200);
|
||||
this._closeRejectSheet();
|
||||
const extra = reason ? { reason } : null;
|
||||
if (target.kind === "reward") await this._callClaimService("reject_reward", target.id, extra);
|
||||
else await this._callService("reject_chore", target.id, extra);
|
||||
}
|
||||
|
||||
_renderRejectSheet() {
|
||||
const target = this._rejecting;
|
||||
if (!target) return "";
|
||||
const stop = (e) => e.stopPropagation();
|
||||
const onKey = (e) => { if (e.key === "Enter") this._confirmReject(); };
|
||||
const onInput = (e) => {
|
||||
const chip = this._rejectChips().includes(e.target.value) ? e.target.value : "";
|
||||
if (chip !== target.chip) this._rejecting = { ...target, chip };
|
||||
};
|
||||
return html`
|
||||
<div class="tm-rv-overlay" @click="${() => this._closeRejectSheet()}">
|
||||
<div class="tm-rv-sheet tm-rj-sheet" role="dialog" aria-modal="true"
|
||||
aria-label="${this._t('reject.title', { name: target.name })}" @click="${stop}">
|
||||
<div class="tm-rv-title">${this._t('reject.title', { name: target.name })}</div>
|
||||
${target.childName ? html`<div class="tm-rv-sub">${target.childName}</div>` : ""}
|
||||
<label class="tm-rv-label" for="tm-reject-reason">${this._t('reject.reason_label')}</label>
|
||||
<div class="tm-rj-chips">
|
||||
${this._rejectChips().map(c => html`
|
||||
<button type="button" class="tm-rj-chip ${target.chip === c ? "on" : ""}"
|
||||
aria-pressed="${target.chip === c}"
|
||||
@click="${() => this._pickRejectChip(c)}">${c}</button>`)}
|
||||
</div>
|
||||
<input id="tm-reject-reason" class="tm-rv-points tm-rj-input" type="text" maxlength="200"
|
||||
placeholder="${this._t('reject.reason_placeholder')}"
|
||||
@input="${onInput}" @keydown="${onKey}">
|
||||
<div class="tm-rj-hint">${this._t('reject.reason_hint')}</div>
|
||||
<div class="tm-rv-row">
|
||||
<button class="tm-rv-btn ghost" @click="${() => this._closeRejectSheet()}">
|
||||
${this._t('common.cancel')}
|
||||
</button>
|
||||
<button class="tm-rv-btn danger" @click="${() => this._confirmReject()}">
|
||||
${this._t('reject.confirm')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async _handleApproveAll(completions) {
|
||||
@@ -1575,12 +1769,15 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
}
|
||||
|
||||
_openReview(completion) {
|
||||
this._review = { completion };
|
||||
const base = completion.suggested_points || completion.points || 0;
|
||||
this._review = { completion, base };
|
||||
this.requestUpdate();
|
||||
this.updateComplete.then(() => {
|
||||
const input = this.renderRoot && this.renderRoot.querySelector("#tm-review-points");
|
||||
if (!input) return;
|
||||
input.value = String(completion.suggested_points || completion.points || 0);
|
||||
const mults = this._ratingMultipliers();
|
||||
const picked = this._ratings[completion.completion_id] || 0;
|
||||
input.value = String(mults && picked ? Math.max(0, Math.round(base * mults[picked - 1])) : base);
|
||||
input.focus();
|
||||
input.select();
|
||||
});
|
||||
@@ -1597,8 +1794,12 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
const input = this.renderRoot && this.renderRoot.querySelector("#tm-review-points");
|
||||
const points = Math.max(0, Math.round(Number(input && input.value) || 0));
|
||||
const completionId = review.completion.completion_id;
|
||||
const rating = this._ratingMultipliers() ? this._ratings[completionId] || 0 : 0;
|
||||
this._closeReview();
|
||||
await this._callService("approve_chore", completionId, { points });
|
||||
// The typed award is paid as-is; a picked rating is recorded with it
|
||||
// (the stars above already rescaled the number the parent confirmed).
|
||||
await this._callService("approve_chore", completionId, rating ? { points, rating } : { points });
|
||||
if (rating) this._clearRating(completionId);
|
||||
}
|
||||
|
||||
_renderReview() {
|
||||
@@ -1624,6 +1825,11 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
|
||||
${photoUrl ? html`<div class="tm-rv-photo"><img src="${photoUrl}" alt="" loading="lazy"></div>` : ""}
|
||||
|
||||
${this._ratingMultipliers() ? html`
|
||||
<div class="tm-rv-label">${this._t('approvals.rating_label')}</div>
|
||||
<div class="tm-rv-stars">${this._renderStars(completion)}</div>
|
||||
` : ""}
|
||||
|
||||
<label class="tm-rv-label" for="tm-review-points">${this._t('approvals.award_label')}</label>
|
||||
<input id="tm-review-points" class="tm-rv-points" type="number"
|
||||
min="0" max="9999" inputmode="numeric">
|
||||
@@ -1697,7 +1903,7 @@ class TaskMateApprovalsCardEditor extends LitElement {
|
||||
.preset-swatch { width: 22px; height: 22px; border-radius: 50%; cursor: pointer; border: 2px solid var(--divider-color, #e0e0e0); transition: transform 0.1s; padding: 0; }
|
||||
.preset-swatch:hover { transform: scale(1.15); }
|
||||
.preset-swatch.active { border-color: var(--primary-text-color); box-shadow: 0 0 0 2px var(--primary-color); }
|
||||
.colour-reset { font-size: 0.78rem; color: var(--secondary-text-color); background: none; border: 1px solid var(--divider-color, #e0e0e0); border-radius: 4px; padding: 4px 10px; cursor: pointer; margin-left: auto; }
|
||||
.colour-reset { font-size: 0.78rem; color: var(--secondary-text-color); background: none; border: 1px solid var(--divider-color, #e0e0e0); border-radius: 4px; padding: 4px 10px; cursor: pointer; margin-inline-start: auto; }
|
||||
.colour-helper { color: var(--secondary-text-color); font-size: 0.82rem; line-height: 1.3; }
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user