feat(settings): re-add journal config to Profile tab; clear briefing remnants
The Briefing Settings section was removed during the briefing→journal migration but its data (locations, temp unit, prep schedule) is still read by the journal backend. There was no UI to set any of it, so weather couldn't render and prep timing wasn't tunable. Re-adds the missing config inside the existing Profile tab — it's all "about the user" data and a separate Journal tab would just clutter the sidebar. New sections: Locations (after Work Schedule) - Home and Work place-name inputs with on-blur geocoding via /api/journal/weather/geocode - Temperature unit toggle (Celsius / Fahrenheit) - Status messages distinguish ok / pending / error Journal (before What the Assistant Has Learned) - Daily prep auto-generate toggle - Prep generation hour:minute (24-hour input) - Day rollover hour (so 1–3am entries still count as the previous day) - All controls disable cleanly when prep is off Cleanup - Profile "About You" desc: "chat and briefings" → "chat and the daily journal" - Profile "Interests" desc: "personalise news and briefing context" → "personalise the journal's daily prep and chat responses" - Profile "Work Schedule" desc: "Helps the briefing" → "Helps the journal" - Profile "What the Assistant Has Learned" desc: clarifies the summary is included in the journal's system prompt; observations come from journal + chat (not briefing) - General "Timezone" desc: "schedule briefings" → "schedule the daily journal prep" - Removed the dead `.briefing-*` CSS block (~190 lines of styles for retired briefing UI: feed-row, slot-row, add-feed-form, etc.) and replaced with fresh `.location-row`, `.unit-toggle`, `.unit-btn`, `.checkbox-label`, `.time-row`, `.time-input`, `.geo-msg` rules used by the new sections. unit-btn.active uses Moss action-primary per Hybrid; tokens flow through the rest. The "What the Assistant Has Learned" section was confirmed load-bearing for the journal — `journal_pipeline.py:139` calls `build_profile_context()` which feeds learned_summary plus other profile fields into the journal's system prompt. Not a remnant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+240
-157
@@ -4,7 +4,7 @@ import { ref, computed, watch, onMounted } from "vue";
|
||||
import { useSettingsStore } from "@/stores/settings";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useToastStore } from "@/stores/toast";
|
||||
import { apiGet, apiPost, apiPut, apiDelete, listGroups, createGroup, deleteGroup, listGroupMembers, addGroupMember, removeGroupMember, searchUsers, getFableMcpInfo, listApiKeys, createApiKey as apiCreateApiKey, revokeApiKey as apiRevokeApiKey, getVoiceStatus, getVoiceList, synthesiseSpeech, getProfile, updateProfile, consolidateProfile, clearProfileObservations, type ApiKeyEntry, type GroupEntry, type GroupMember, type UserSearchResult, type VoiceStatusResult, type VoiceEntry, type VoiceBlendEntry, type UserProfile } from "@/api/client";
|
||||
import { apiGet, apiPost, apiPut, apiDelete, listGroups, createGroup, deleteGroup, listGroupMembers, addGroupMember, removeGroupMember, searchUsers, getFableMcpInfo, listApiKeys, createApiKey as apiCreateApiKey, revokeApiKey as apiRevokeApiKey, getVoiceStatus, getVoiceList, synthesiseSpeech, getProfile, updateProfile, consolidateProfile, clearProfileObservations, getJournalConfig, saveJournalConfig, geocodeAddress, type ApiKeyEntry, type GroupEntry, type GroupMember, type UserSearchResult, type VoiceStatusResult, type VoiceEntry, type VoiceBlendEntry, type UserProfile, type JournalConfig } from "@/api/client";
|
||||
import { usePushStore } from "@/stores/push";
|
||||
import type { User } from "@/types/auth";
|
||||
import PaginationBar from "@/components/PaginationBar.vue";
|
||||
@@ -594,6 +594,91 @@ async function runConsolidate() {
|
||||
|
||||
function emptyTagsFetch(): Promise<string[]> { return Promise.resolve([]) }
|
||||
|
||||
// ── Journal config (locations, temp unit, prep schedule) ────────────────────
|
||||
const journalConfig = ref<JournalConfig>({
|
||||
prep_enabled: true,
|
||||
prep_hour: 5,
|
||||
prep_minute: 0,
|
||||
day_rollover_hour: 4,
|
||||
locations: { home: { label: 'Home' }, work: { label: 'Work' } },
|
||||
temp_unit: 'C',
|
||||
})
|
||||
const journalConfigSaving = ref(false)
|
||||
const journalConfigSaved = ref(false)
|
||||
const homeQuery = ref('')
|
||||
const workQuery = ref('')
|
||||
const homeGeoStatus = ref<'' | 'ok' | 'error' | 'pending'>('')
|
||||
const workGeoStatus = ref<'' | 'ok' | 'error' | 'pending'>('')
|
||||
const homeGeoMsg = ref('')
|
||||
const workGeoMsg = ref('')
|
||||
|
||||
async function loadJournalConfig() {
|
||||
try {
|
||||
const cfg = await getJournalConfig()
|
||||
journalConfig.value = {
|
||||
prep_enabled: cfg.prep_enabled ?? true,
|
||||
prep_hour: cfg.prep_hour ?? 5,
|
||||
prep_minute: cfg.prep_minute ?? 0,
|
||||
day_rollover_hour: cfg.day_rollover_hour ?? 4,
|
||||
morning_end_hour: cfg.morning_end_hour,
|
||||
midday_end_hour: cfg.midday_end_hour,
|
||||
locations: {
|
||||
home: cfg.locations?.home ?? { label: 'Home' },
|
||||
work: cfg.locations?.work ?? { label: 'Work' },
|
||||
},
|
||||
temp_unit: cfg.temp_unit ?? 'C',
|
||||
}
|
||||
homeQuery.value = cfg.locations?.home?.label && cfg.locations.home.lat ? cfg.locations.home.label : ''
|
||||
workQuery.value = cfg.locations?.work?.label && cfg.locations.work.lat ? cfg.locations.work.label : ''
|
||||
} catch { /* non-critical */ }
|
||||
}
|
||||
|
||||
async function geocodeFor(which: 'home' | 'work') {
|
||||
const query = (which === 'home' ? homeQuery.value : workQuery.value).trim()
|
||||
const statusRef = which === 'home' ? homeGeoStatus : workGeoStatus
|
||||
const msgRef = which === 'home' ? homeGeoMsg : workGeoMsg
|
||||
if (!query) {
|
||||
// Clear location
|
||||
if (!journalConfig.value.locations) journalConfig.value.locations = {}
|
||||
journalConfig.value.locations[which] = { label: which === 'home' ? 'Home' : 'Work' }
|
||||
statusRef.value = ''
|
||||
msgRef.value = ''
|
||||
return
|
||||
}
|
||||
statusRef.value = 'pending'
|
||||
msgRef.value = 'Looking up…'
|
||||
try {
|
||||
const result = await geocodeAddress(query)
|
||||
if (!result) {
|
||||
statusRef.value = 'error'
|
||||
msgRef.value = `No match for "${query}"`
|
||||
return
|
||||
}
|
||||
if (!journalConfig.value.locations) journalConfig.value.locations = {}
|
||||
journalConfig.value.locations[which] = {
|
||||
label: which === 'home' ? 'Home' : 'Work',
|
||||
lat: result.lat,
|
||||
lon: result.lon,
|
||||
}
|
||||
statusRef.value = 'ok'
|
||||
msgRef.value = `Found: ${result.display_name}`
|
||||
} catch {
|
||||
statusRef.value = 'error'
|
||||
msgRef.value = 'Geocoding failed'
|
||||
}
|
||||
}
|
||||
|
||||
async function saveJournalCfg() {
|
||||
journalConfigSaving.value = true
|
||||
journalConfigSaved.value = false
|
||||
try {
|
||||
await saveJournalConfig(journalConfig.value)
|
||||
journalConfigSaved.value = true
|
||||
setTimeout(() => { journalConfigSaved.value = false }, 2000)
|
||||
} catch { toastStore.show('Failed to save journal settings', 'error') }
|
||||
finally { journalConfigSaving.value = false }
|
||||
}
|
||||
|
||||
async function clearObservations() {
|
||||
if (!confirm('Clear all learned observations and the generated summary? This cannot be undone.')) return
|
||||
clearingObs.value = true
|
||||
@@ -645,6 +730,9 @@ onMounted(async () => {
|
||||
// Load user profile
|
||||
await loadProfile();
|
||||
|
||||
// Load journal config (locations, temp unit, prep schedule)
|
||||
await loadJournalConfig();
|
||||
|
||||
// Load voice settings
|
||||
if (allSettings.voice_tts_voice) voiceTtsVoice.value = allSettings.voice_tts_voice;
|
||||
if (allSettings.voice_tts_speed) voiceTtsSpeed.value = parseFloat(allSettings.voice_tts_speed);
|
||||
@@ -1388,7 +1476,7 @@ function formatUserDate(iso: string): string {
|
||||
<!-- Timezone -->
|
||||
<section class="settings-section full-width">
|
||||
<h2>Timezone</h2>
|
||||
<p class="section-desc">Used to schedule briefings and format times in chat. Set this to your local IANA timezone (e.g. America/New_York, Europe/London).</p>
|
||||
<p class="section-desc">Used to schedule the daily journal prep and format times in chat. Set this to your local IANA timezone (e.g. America/New_York, Europe/London).</p>
|
||||
<div class="field">
|
||||
<label for="user-timezone">Your timezone</label>
|
||||
<div style="display:flex; gap:0.5rem; align-items:center">
|
||||
@@ -1592,7 +1680,7 @@ function formatUserDate(iso: string): string {
|
||||
<div v-show="activeTab === 'profile'" class="settings-grid">
|
||||
<section class="settings-section full-width">
|
||||
<h2>About You</h2>
|
||||
<p class="section-desc">This information is used by the assistant to personalise responses in chat and briefings.</p>
|
||||
<p class="section-desc">This information is used by the assistant to personalise responses in chat and the daily journal.</p>
|
||||
<div class="assistant-grid">
|
||||
<div class="field">
|
||||
<label>Display Name</label>
|
||||
@@ -1651,7 +1739,7 @@ function formatUserDate(iso: string): string {
|
||||
|
||||
<section class="settings-section full-width">
|
||||
<h2>Interests</h2>
|
||||
<p class="section-desc">Topics you care about — used to personalise news and briefing context.</p>
|
||||
<p class="section-desc">Topics you care about — used to personalise the journal's daily prep and chat responses.</p>
|
||||
<TagInput v-model="profile.interests" placeholder="Add an interest…" :fetchTags="emptyTagsFetch" />
|
||||
<div class="actions">
|
||||
<button class="btn-save" @click="saveProfile" :disabled="profileSaving">{{ profileSaving ? 'Saving…' : 'Save' }}</button>
|
||||
@@ -1661,7 +1749,7 @@ function formatUserDate(iso: string): string {
|
||||
|
||||
<section class="settings-section full-width">
|
||||
<h2>Work Schedule</h2>
|
||||
<p class="section-desc">Helps the briefing understand when you're working and what's relevant each morning.</p>
|
||||
<p class="section-desc">Helps the journal understand when you're working and what's relevant each morning.</p>
|
||||
<div class="field">
|
||||
<label>Work Days</label>
|
||||
<div class="day-picker">
|
||||
@@ -1691,14 +1779,128 @@ function formatUserDate(iso: string): string {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-section full-width">
|
||||
<h2>Locations</h2>
|
||||
<p class="section-desc">Home and work locations are used by the journal's daily prep to fetch local weather. Place names are geocoded once on save.</p>
|
||||
<div class="field">
|
||||
<label>Home</label>
|
||||
<div class="location-row">
|
||||
<input
|
||||
v-model="homeQuery"
|
||||
type="text"
|
||||
class="input"
|
||||
placeholder="e.g. Brooklyn, NY"
|
||||
@blur="geocodeFor('home')"
|
||||
@keydown.enter.prevent="geocodeFor('home')"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="homeGeoStatus === 'ok'" class="geo-msg geo-ok">{{ homeGeoMsg }}</p>
|
||||
<p v-else-if="homeGeoStatus === 'error'" class="geo-msg geo-error">{{ homeGeoMsg }}</p>
|
||||
<p v-else-if="homeGeoStatus === 'pending'" class="geo-msg geo-pending">{{ homeGeoMsg }}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Work</label>
|
||||
<div class="location-row">
|
||||
<input
|
||||
v-model="workQuery"
|
||||
type="text"
|
||||
class="input"
|
||||
placeholder="e.g. Manhattan, NY"
|
||||
@blur="geocodeFor('work')"
|
||||
@keydown.enter.prevent="geocodeFor('work')"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="workGeoStatus === 'ok'" class="geo-msg geo-ok">{{ workGeoMsg }}</p>
|
||||
<p v-else-if="workGeoStatus === 'error'" class="geo-msg geo-error">{{ workGeoMsg }}</p>
|
||||
<p v-else-if="workGeoStatus === 'pending'" class="geo-msg geo-pending">{{ workGeoMsg }}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Temperature unit</label>
|
||||
<div class="unit-toggle">
|
||||
<button
|
||||
type="button"
|
||||
class="unit-btn"
|
||||
:class="{ active: journalConfig.temp_unit === 'C' }"
|
||||
@click="journalConfig.temp_unit = 'C'"
|
||||
>Celsius</button>
|
||||
<button
|
||||
type="button"
|
||||
class="unit-btn"
|
||||
:class="{ active: journalConfig.temp_unit === 'F' }"
|
||||
@click="journalConfig.temp_unit = 'F'"
|
||||
>Fahrenheit</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn-save" @click="saveJournalCfg" :disabled="journalConfigSaving">{{ journalConfigSaving ? 'Saving…' : 'Save' }}</button>
|
||||
<span v-if="journalConfigSaved" class="saved-msg">Saved!</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-section full-width">
|
||||
<h2>Journal</h2>
|
||||
<p class="section-desc">Controls when the daily journal prep generates and how the day is framed.</p>
|
||||
<div class="field">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="journalConfig.prep_enabled" />
|
||||
Generate daily prep automatically
|
||||
</label>
|
||||
<p class="field-hint">When enabled, the journal generates a morning briefing at the time below. When off, the journal still works — just without the auto-generated daily prep.</p>
|
||||
</div>
|
||||
<div class="assistant-grid">
|
||||
<div class="field">
|
||||
<label>Prep generates at</label>
|
||||
<div class="time-row">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="23"
|
||||
v-model.number="journalConfig.prep_hour"
|
||||
class="input time-input"
|
||||
:disabled="!journalConfig.prep_enabled"
|
||||
/>
|
||||
<span class="time-sep">:</span>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="59"
|
||||
step="5"
|
||||
v-model.number="journalConfig.prep_minute"
|
||||
class="input time-input"
|
||||
:disabled="!journalConfig.prep_enabled"
|
||||
/>
|
||||
</div>
|
||||
<p class="field-hint">24-hour. Generates each morning at this local time.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Day rolls over at</label>
|
||||
<div class="time-row">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="23"
|
||||
v-model.number="journalConfig.day_rollover_hour"
|
||||
class="input time-input"
|
||||
/>
|
||||
<span class="time-sep time-sep--quiet">:00</span>
|
||||
</div>
|
||||
<p class="field-hint">Hour when the journal switches to a new day. Default 4am — late-night entries (1–3am) still count as the previous day.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn-save" @click="saveJournalCfg" :disabled="journalConfigSaving">{{ journalConfigSaving ? 'Saving…' : 'Save' }}</button>
|
||||
<span v-if="journalConfigSaved" class="saved-msg">Saved!</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-section full-width">
|
||||
<h2>What the Assistant Has Learned</h2>
|
||||
<p class="section-desc">
|
||||
The assistant observes patterns from your briefing conversations and builds a summary over time.
|
||||
The assistant observes patterns from your journal and chat conversations and builds a summary over time. The summary is included in the journal's system prompt so the daily prep can reference what it knows about you.
|
||||
<span v-if="profile.observations_count > 0"> {{ profile.observations_count }} raw observation{{ profile.observations_count !== 1 ? 's' : '' }} stored.</span>
|
||||
</p>
|
||||
<div v-if="profile.learned_summary" class="learned-summary">{{ profile.learned_summary }}</div>
|
||||
<div v-else class="learned-empty">No learned summary yet. Observations accumulate from daily briefing conversations.</div>
|
||||
<div v-else class="learned-empty">No learned summary yet. Observations accumulate from journal and chat conversations.</div>
|
||||
<div class="actions" style="gap:0.5rem;flex-wrap:wrap">
|
||||
<button class="btn-secondary" @click="runConsolidate" :disabled="consolidating || profile.observations_count === 0">
|
||||
{{ consolidating ? 'Consolidating…' : 'Consolidate Now' }}
|
||||
@@ -3737,194 +3939,75 @@ FABLE_API_KEY={{ effectiveApiKey }}</pre>
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
/* Briefing tab */
|
||||
.briefing-location-row {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.briefing-input-group {
|
||||
/* Profile — Locations + Journal sections */
|
||||
.location-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.briefing-geo-confirmed {
|
||||
.geo-msg {
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-success, #22c55e);
|
||||
margin-top: 0.2rem;
|
||||
margin: 0.2rem 0 0;
|
||||
}
|
||||
.briefing-geo-error {
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-danger, #ef4444);
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
.briefing-day-toggles {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.briefing-unit-toggle {
|
||||
.geo-ok { color: var(--color-success); }
|
||||
.geo-error { color: var(--color-danger); }
|
||||
.geo-pending { color: var(--color-text-muted); font-style: italic; }
|
||||
|
||||
.unit-toggle {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
width: fit-content;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.briefing-unit-btn {
|
||||
padding: 0.35rem 1rem;
|
||||
.unit-btn {
|
||||
padding: 0.4rem 1rem;
|
||||
background: var(--color-bg-card);
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
transition: all 0.15s;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.briefing-unit-btn:first-child {
|
||||
.unit-btn:not(:last-child) {
|
||||
border-right: 1px solid var(--color-border);
|
||||
}
|
||||
.briefing-unit-btn.active {
|
||||
background: var(--color-primary);
|
||||
.unit-btn.active {
|
||||
background: var(--color-action-primary);
|
||||
color: #fff;
|
||||
}
|
||||
.briefing-day-btn {
|
||||
padding: 0.35rem 0.65rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background: var(--color-bg-card);
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.82rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
font-family: inherit;
|
||||
.unit-btn:hover:not(.active) {
|
||||
color: var(--color-text);
|
||||
}
|
||||
.briefing-day-btn.active {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
.briefing-slot-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.briefing-slot-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.45rem 0.75rem;
|
||||
border-radius: 8px;
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
.briefing-slot-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.briefing-slot-label {
|
||||
font-size: 0.88rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.briefing-slot-time {
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.briefing-feeds-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.briefing-feed-row {
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
border-radius: 6px;
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
.briefing-feed-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.briefing-feed-title {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
}
|
||||
.briefing-feed-url {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
}
|
||||
.briefing-btn-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
transition: color 0.15s;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.briefing-btn-remove:hover { color: var(--color-danger, #ef4444); }
|
||||
.briefing-feeds-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
.briefing-feeds-header h2 { margin: 0; }
|
||||
.briefing-feeds-header .section-desc { margin: 0.25rem 0 0; }
|
||||
.briefing-refresh-btn { white-space: nowrap; flex-shrink: 0; }
|
||||
.briefing-feed-title-row {
|
||||
|
||||
.time-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.briefing-feed-cat {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
background: color-mix(in srgb, var(--color-primary) 15%, transparent);
|
||||
color: var(--color-primary);
|
||||
font-weight: 500;
|
||||
.time-input {
|
||||
width: 4rem;
|
||||
text-align: center;
|
||||
}
|
||||
.briefing-feed-age {
|
||||
font-size: 0.72rem;
|
||||
.time-sep {
|
||||
font-size: 1.1rem;
|
||||
color: var(--color-text-muted);
|
||||
display: block;
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
.briefing-add-feed-form {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
.briefing-add-feed-inputs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.briefing-add-feed-inputs .input { flex: 1; min-width: 160px; }
|
||||
.briefing-cat-input { max-width: 180px; }
|
||||
.briefing-add-feed {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.time-sep--quiet { font-size: 0.9rem; }
|
||||
|
||||
/* API Keys tab */
|
||||
.api-key-create-form {
|
||||
|
||||
Reference in New Issue
Block a user