CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 16s
CI & Build / integration (push) Successful in 23s
CI & Build / TypeScript typecheck (push) Successful in 25s
CI & Build / Python tests (push) Successful in 57s
CI & Build / Build & push image (push) Successful in 43s
#2277 counted ~150 "raw colour literals bypassing the tokens". Measuring them told a different story: 184 sat in `var(--token, #fallback)` position, and a check against theme.css shows every one of those tokens IS declared. So the fallbacks could not render. Not drift — vestigial. They were also not this palette. The most common were Tailwind and Flat-UI defaults — #6366f1 indigo, #22c55e green, #f59e0b amber, #3b82f6 blue, #e74c3c and #27ae60 — a second, unsanctioned colour scheme sitting in the codebase looking like the app's colours to anyone reading it. Removing them is not tidying. #2319's lesson is that a fallback is WORSE than a missing token: a missing token renders as nothing and someone eventually notices, while a fallback renders something plausible forever. These 184 were one token rename away from silently repainting the app in Tailwind. The design token check would catch the rename — but the fallback is precisely the thing that would make it invisible if the check were ever bypassed. Literal count 152 -> 45, which matters beyond the number: a report that is mostly unreachable noise is one people stop reading, and then it stops working while still passing. What remains should be genuinely worth looking at. Done with a paren-aware transform, not a regex — `var(--x, rgba(0,0,0,.5))` nests parens and `[^)]+` would cut at the first one and leave `))` behind. Verified after: every changed line is a fallback strip and nothing else, and every var() reference still resolves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
172 lines
4.7 KiB
Vue
172 lines
4.7 KiB
Vue
<script setup lang="ts">
|
|
import { ref, watch, computed } from "vue";
|
|
|
|
const props = defineProps<{
|
|
modelValue: Record<string, unknown> | null;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
(e: "update:modelValue", val: Record<string, unknown> | null): void;
|
|
}>();
|
|
|
|
type RecurrenceType = "none" | "interval" | "calendar";
|
|
type IntervalUnit = "day" | "week" | "month" | "year";
|
|
type CalendarUnit = "month" | "year";
|
|
|
|
const rType = ref<RecurrenceType>("none");
|
|
const intervalEvery = ref(1);
|
|
const intervalUnit = ref<IntervalUnit>("week");
|
|
const calendarUnit = ref<CalendarUnit>("month");
|
|
const calendarDay = ref(1);
|
|
const calendarMonth = ref(1);
|
|
|
|
function ruleFromState(): Record<string, unknown> | null {
|
|
if (rType.value === "none") return null;
|
|
if (rType.value === "interval") {
|
|
return { type: "interval", every: intervalEvery.value, unit: intervalUnit.value };
|
|
}
|
|
// calendar
|
|
const rule: Record<string, unknown> = {
|
|
type: "calendar",
|
|
unit: calendarUnit.value,
|
|
day_of_month: calendarDay.value,
|
|
};
|
|
if (calendarUnit.value === "year") {
|
|
rule.month = calendarMonth.value;
|
|
}
|
|
return rule;
|
|
}
|
|
|
|
function loadFromRule(rule: Record<string, unknown> | null) {
|
|
if (!rule) {
|
|
rType.value = "none";
|
|
return;
|
|
}
|
|
const t = rule.type as string;
|
|
if (t === "interval") {
|
|
rType.value = "interval";
|
|
intervalEvery.value = (rule.every as number) ?? 1;
|
|
intervalUnit.value = (rule.unit as IntervalUnit) ?? "week";
|
|
} else if (t === "calendar") {
|
|
rType.value = "calendar";
|
|
calendarUnit.value = (rule.unit as CalendarUnit) ?? "month";
|
|
calendarDay.value = (rule.day_of_month as number) ?? 1;
|
|
calendarMonth.value = (rule.month as number) ?? 1;
|
|
} else {
|
|
rType.value = "none";
|
|
}
|
|
}
|
|
|
|
// Load initial value
|
|
loadFromRule(props.modelValue);
|
|
|
|
// Watch for external changes (e.g., task loaded)
|
|
watch(() => props.modelValue, (val) => {
|
|
loadFromRule(val);
|
|
}, { deep: true });
|
|
|
|
// Emit on any state change
|
|
function onChange() {
|
|
emit("update:modelValue", ruleFromState());
|
|
}
|
|
|
|
const monthNames = [
|
|
"January","February","March","April","May","June",
|
|
"July","August","September","October","November","December",
|
|
];
|
|
|
|
const calendarDayMax = computed(() =>
|
|
calendarUnit.value === "month" ? 28 : 28
|
|
);
|
|
</script>
|
|
|
|
<template>
|
|
<div class="rec-editor">
|
|
<select v-model="rType" class="sb-select" @change="onChange">
|
|
<option value="none">No recurrence</option>
|
|
<option value="interval">Every N days/weeks/months</option>
|
|
<option value="calendar">On a calendar date</option>
|
|
</select>
|
|
|
|
<div v-if="rType === 'interval'" class="rec-row">
|
|
<span class="rec-label">Every</span>
|
|
<input
|
|
v-model.number="intervalEvery"
|
|
type="number"
|
|
min="1"
|
|
max="365"
|
|
class="rec-num-input"
|
|
@input="onChange"
|
|
/>
|
|
<select v-model="intervalUnit" class="sb-select rec-unit" @change="onChange">
|
|
<option value="day">day(s)</option>
|
|
<option value="week">week(s)</option>
|
|
<option value="month">month(s)</option>
|
|
<option value="year">year(s)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div v-if="rType === 'calendar'" class="rec-row rec-col">
|
|
<div class="rec-row">
|
|
<span class="rec-label">Unit</span>
|
|
<select v-model="calendarUnit" class="sb-select" @change="onChange">
|
|
<option value="month">Monthly</option>
|
|
<option value="year">Yearly</option>
|
|
</select>
|
|
</div>
|
|
<div class="rec-row">
|
|
<span class="rec-label">Day</span>
|
|
<input
|
|
v-model.number="calendarDay"
|
|
type="number"
|
|
min="1"
|
|
:max="calendarDayMax"
|
|
class="rec-num-input"
|
|
@input="onChange"
|
|
/>
|
|
</div>
|
|
<div v-if="calendarUnit === 'year'" class="rec-row">
|
|
<span class="rec-label">Month</span>
|
|
<select v-model.number="calendarMonth" class="sb-select" @change="onChange">
|
|
<option v-for="(name, i) in monthNames" :key="i + 1" :value="i + 1">{{ name }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.rec-editor {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.4rem;
|
|
}
|
|
.rec-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
.rec-col {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
}
|
|
.rec-label {
|
|
font-size: 0.78rem;
|
|
color: var(--color-text-muted);
|
|
min-width: 2.5rem;
|
|
}
|
|
.rec-num-input {
|
|
width: 4rem;
|
|
padding: 0.25rem 0.4rem;
|
|
border: 1px solid var(--color-input-border);
|
|
border-radius: var(--radius-sm);
|
|
background: var(--color-bg);
|
|
color: var(--color-text);
|
|
font-size: 0.85rem;
|
|
font-family: inherit;
|
|
}
|
|
.rec-num-input:focus { outline: none; border-color: var(--color-primary); }
|
|
.rec-unit { min-width: 6rem; }
|
|
</style>
|