Branding
Rolling release / release (push) Successful in 6m57s

This commit is contained in:
2026-09-12 14:06:02 -04:00
parent 0e52942b8b
commit 18802cb55d
23 changed files with 459 additions and 39 deletions
@@ -0,0 +1,33 @@
export const BRAND = {
name: 'Jarvis',
product: 'Jarvis QVAC',
tagline: 'Local voice for your desktop',
applicationId: 'io.qvac.Jarvis.Control',
uuid: '[email protected]',
colors: {
gold: '#F4B942',
goldBright: '#FFE09A',
goldDeep: '#C48A1A',
signal: '#4FD2FF',
ink: '#0B0E14',
slate: '#16191F',
paper: '#F6F7FB',
mute: '#AEB6C8',
dim: '#9AA8C0',
},
accents: [
{ id: 'gold', label: 'Gold', value: '#F4B942' },
{ id: 'signal', label: 'Signal', value: '#4FD2FF' },
{ id: 'copper', label: 'Copper', value: '#E07A3D' },
{ id: 'jade', label: 'Jade', value: '#3DDC97' },
{ id: 'rose', label: 'Rose', value: '#F071A8' },
],
};
export const DEFAULT_ACCENT = BRAND.colors.gold;
export const SIGNAL = BRAND.colors.signal;
export function normalizeAccent(value, fallback = DEFAULT_ACCENT) {
const color = String(value || '').trim();
return /^#[0-9A-Fa-f]{6}$/.test(color) ? color : fallback;
}
@@ -0,0 +1,40 @@
@define-color jarvis_gold #F4B942;
@define-color jarvis_gold_bright #FFE09A;
@define-color jarvis_signal #4FD2FF;
@define-color jarvis_ink #0B0E14;
@define-color jarvis_slate #16191F;
@define-color accent_color @jarvis_gold;
@define-color accent_bg_color @jarvis_gold;
@define-color accent_fg_color @jarvis_slate;
.jarvis-brand-name {
font-weight: 800;
letter-spacing: 0.6px;
}
.jarvis-brand-tagline {
font-size: 12px;
}
button.jarvis-swatch {
min-width: 24px;
min-height: 24px;
padding: 0;
border-radius: 999px;
border: 1px solid alpha(#ffffff, 0.22);
}
button.jarvis-swatch-gold { background: #F4B942; }
button.jarvis-swatch-signal { background: #4FD2FF; }
button.jarvis-swatch-copper { background: #E07A3D; }
button.jarvis-swatch-jade { background: #3DDC97; }
button.jarvis-swatch-rose { background: #F071A8; }
button.jarvis-swatch:hover {
border-color: alpha(#ffffff, 0.55);
}
button.jarvis-swatch:checked,
button.jarvis-swatch:active {
box-shadow: 0 0 0 2px @jarvis_ink, 0 0 0 4px @jarvis_gold;
}
@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="Jarvis">
<title>Jarvis</title>
<defs>
<radialGradient id="glow" cx="50%" cy="42%" r="48%">
<stop offset="0%" stop-color="#4FD2FF" stop-opacity=".22"/>
<stop offset="100%" stop-color="#0B0E14" stop-opacity="0"/>
</radialGradient>
</defs>
<rect width="128" height="128" rx="28" fill="#0B0E14"/>
<rect x="5" y="5" width="118" height="118" rx="24" fill="none" stroke="#F4B942" stroke-opacity=".2" stroke-width="1.5"/>
<circle cx="64" cy="58" r="42" fill="url(#glow)"/>
<g transform="translate(32 22) scale(1)">
<path d="M14.5 30.5c0-10.77 8.73-19.5 19.5-19.5 8.05 0 14.95 4.88 17.92 11.82" fill="none" stroke="#F4B942" stroke-width="6" stroke-linecap="round"/>
<path d="M34 11v29.5c0 6.35-5.15 11.5-11.5 11.5" fill="none" stroke="#F4B942" stroke-width="6" stroke-linecap="round"/>
<circle cx="34" cy="33" r="5.5" fill="#4FD2FF"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 971 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" role="img" aria-label="Jarvis">
<path d="M3.2 7.4A5.3 5.3 0 0 1 8.5 2.2c2.2 0 4.1 1.33 4.9 3.25" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>
<path d="M8.5 2.2v7.7A3.1 3.1 0 0 1 5.4 13" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>
<circle cx="8.5" cy="8.1" r="1.25" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 415 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none" role="img" aria-label="Jarvis">
<title>Jarvis</title>
<path d="M14.5 30.5c0-10.77 8.73-19.5 19.5-19.5 8.05 0 14.95 4.88 17.92 11.82" stroke="#F4B942" stroke-width="5.5" stroke-linecap="round"/>
<path d="M34 11v29.5c0 6.35-5.15 11.5-11.5 11.5" stroke="#F4B942" stroke-width="5.5" stroke-linecap="round"/>
<circle cx="34" cy="33" r="5" fill="#4FD2FF"/>
</svg>

After

Width:  |  Height:  |  Size: 438 B

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 64" role="img" aria-label="Jarvis">
<title>Jarvis</title>
<g transform="translate(8 10) scale(.68)">
<path d="M14.5 30.5c0-10.77 8.73-19.5 19.5-19.5 8.05 0 14.95 4.88 17.92 11.82" fill="none" stroke="#F4B942" stroke-width="5.5" stroke-linecap="round"/>
<path d="M34 11v29.5c0 6.35-5.15 11.5-11.5 11.5" fill="none" stroke="#F4B942" stroke-width="5.5" stroke-linecap="round"/>
<circle cx="34" cy="33" r="5" fill="#4FD2FF"/>
</g>
<text x="64" y="42" fill="#F6F7FB" font-family="Adwaita Sans, Cantarell, sans-serif" font-size="26" font-weight="700" letter-spacing="3.2">JARVIS</text>
</svg>

After

Width:  |  Height:  |  Size: 664 B

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="Jarvis">
<title>Jarvis</title>
<defs>
<radialGradient id="glow" cx="50%" cy="42%" r="48%">
<stop offset="0%" stop-color="#4FD2FF" stop-opacity=".22"/>
<stop offset="100%" stop-color="#0B0E14" stop-opacity="0"/>
</radialGradient>
</defs>
<rect width="128" height="128" rx="28" fill="#0B0E14"/>
<rect x="5" y="5" width="118" height="118" rx="24" fill="none" stroke="#F4B942" stroke-opacity=".2" stroke-width="1.5"/>
<circle cx="64" cy="58" r="42" fill="url(#glow)"/>
<g transform="translate(32 22) scale(1)">
<path d="M14.5 30.5c0-10.77 8.73-19.5 19.5-19.5 8.05 0 14.95 4.88 17.92 11.82" fill="none" stroke="#F4B942" stroke-width="6" stroke-linecap="round"/>
<path d="M34 11v29.5c0 6.35-5.15 11.5-11.5 11.5" fill="none" stroke="#F4B942" stroke-width="6" stroke-linecap="round"/>
<circle cx="34" cy="33" r="5.5" fill="#4FD2FF"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 971 B

@@ -0,0 +1,36 @@
{
"name": "Jarvis",
"product": "Jarvis QVAC",
"tagline": "Local voice for your desktop",
"voice": "Precise, warm, local-first. Gold is presence. Cyan is thought.",
"applicationId": "io.qvac.Jarvis.Control",
"uuid": "[email protected]",
"colors": {
"gold": "#F4B942",
"goldBright": "#FFE09A",
"goldDeep": "#C48A1A",
"signal": "#4FD2FF",
"ink": "#0B0E14",
"slate": "#16191F",
"paper": "#F6F7FB",
"mute": "#AEB6C8",
"dim": "#9AA8C0"
},
"accents": [
{ "id": "gold", "label": "Gold", "value": "#F4B942" },
{ "id": "signal", "label": "Signal", "value": "#4FD2FF" },
{ "id": "copper", "label": "Copper", "value": "#E07A3D" },
{ "id": "jade", "label": "Jade", "value": "#3DDC97" },
{ "id": "rose", "label": "Rose", "value": "#F071A8" }
],
"type": {
"ui": "Adwaita Sans / Cantarell",
"wordmarkTracking": "0.8px",
"wordmarkWeight": 700
},
"radius": {
"hud": 16,
"chip": 999,
"row": 8
}
}
@@ -14,7 +14,8 @@ import { ConversationView, JarvisOsd, ComputerUseChrome, SessionPanel, coerceTex
const BUS = 'io.qvac.Jarvis';
const PATH = '/io/qvac/Jarvis';
const IFACE = 'io.qvac.Jarvis.Session';
const GLYPHS = { ARMED: '◯', LISTENING: '◌', THINKING: '◉', SPEAKING: '◎', SLEEPING: '◐' };
const DEFAULT_ACCENT = '#F4B942';
const SIGNAL = '#4FD2FF';
class JarvisProxy {
async connect() {
@@ -55,17 +56,23 @@ export default class JarvisExtension extends Extension {
this._theme = St.ThemeContext.get_for_stage(global.stage).get_theme(); this._stylesheet = Gio.File.new_for_path(`${this.path}/stylesheet.css`); try { this._theme.load_stylesheet(this._stylesheet); } catch (error) { log(`Jarvis stylesheet unavailable: ${error.message}`); }
this.settings = this.getSettings();
this.proxy = new JarvisProxy();
this.popup = new ConversationView({ compact: true });
this.popup = new ConversationView({ compact: true, brandDir: this.path });
this.osd = new JarvisOsd();
this.cu = new ComputerUseChrome(); this.cu.attach();
this.session = new SessionPanel(); this.session.attach();
this.session = new SessionPanel({ brandDir: this.path }); this.session.attach();
this._bindSurface(this.popup);
this._bindSurface(this.session.view);
this._applyAccessibility();
this._removeShellService = installShellService();
this._indicator = new PanelMenu.Button(0.0, 'Jarvis QVAC', false); this._indicator.accessible_name = 'Jarvis voice assistant';
this._panelBox = new St.BoxLayout({ style_class: 'jarvis-panel-box', y_align: Clutter.ActorAlign.CENTER });
this._glyph = new St.Label({ text: '◯ Jarvis', style_class: 'jarvis-panel-glyph', y_align: Clutter.ActorAlign.CENTER }); this._glyph.accessible_name = 'Jarvis idle';
this._mark = new St.Icon({ icon_name: 'audio-input-microphone-symbolic', icon_size: 16, style_class: 'jarvis-panel-mark', y_align: Clutter.ActorAlign.CENTER });
try {
const file = Gio.File.new_for_path(`${this.path}/brand/icons/jarvis-mark.svg`);
if (file.query_exists(null)) this._mark.gicon = new Gio.FileIcon({ file });
} catch (error) { log(`Jarvis mark unavailable: ${error.message}`); }
this._glyph = new St.Label({ text: 'Jarvis', style_class: 'jarvis-panel-glyph', y_align: Clutter.ActorAlign.CENTER }); this._glyph.accessible_name = 'Jarvis idle';
this._panelBox.add_child(this._mark);
this._panelBox.add_child(this._glyph);
this.osd.attach(this._panelBox);
this._indicator.add_child(this._panelBox);
@@ -264,17 +271,27 @@ export default class JarvisExtension extends Extension {
}
_setState(state) {
const value = safeText(state);
this._glyph.text = `${GLYPHS[value] || '◯'} Jarvis`;
this._glyph.text = 'Jarvis';
this._glyph.accessible_name = `Jarvis ${value.toLowerCase()}`;
this._indicator.accessible_name = `Jarvis ${value.toLowerCase()}`;
if (this._panelBox) {
for (const name of ['armed', 'listening', 'speaking', 'thinking', 'sleeping']) {
this._panelBox.remove_style_class_name(`jarvis-state-${name}`);
}
this._panelBox.add_style_class_name(`jarvis-state-${value.toLowerCase()}`);
}
this._eachView((view) => view.setState(value));
this.osd.setState(value);
}
_applyAccent() {
const style = `--jarvis-accent: ${this.settings.get_string('accent-color')};`;
const color = this.settings.get_string('accent-color');
const accent = /^#[0-9A-Fa-f]{6}$/.test(String(color || '')) ? color : DEFAULT_ACCENT;
const style = `--jarvis-accent: ${accent}; --jarvis-signal: ${SIGNAL};`;
this.popup.root.set_style(style);
this.session.root.set_style(style);
this.osd.root.set_style(style);
this.cu.root.set_style(style);
this._panelBox?.set_style(style);
}
_applyAccessibility() {
try {
@@ -300,7 +317,7 @@ export default class JarvisExtension extends Extension {
this.osd?.destroy(); this.cu?.destroy(); this.session?.destroy(); this.popup?.destroy();
this._indicator?.destroy();
if (this._theme && this._stylesheet) { try { this._theme.unload_stylesheet(this._stylesheet); } catch {} }
this.popup = this.session = this.osd = this.cu = this._indicator = this._glyph = this.proxy = null;
this.popup = this.session = this.osd = this.cu = this._indicator = this._glyph = this._mark = this.proxy = null;
}
}
@@ -1,7 +1,7 @@
{
"uuid": "[email protected]",
"name": "Jarvis QVAC",
"description": "Local-first QVAC voice assistant in the GNOME top bar",
"name": "Jarvis",
"description": "Local voice for your desktop. Jarvis QVAC lives in the GNOME top bar.",
"shell-version": ["46", "47", "48", "49", "50"],
"version": 2,
"session-modes": ["user"],
@@ -149,8 +149,9 @@ export class SettingsEditor {
button.connect('clicked', () => { for (const field of fields) { this.setValue(field, field.default); this.rows.get(field.key)?._setValue(field.default); } });
reset.add_suffix(button); group.add(reset); page.add(group);
}
page(parent, title, groups, iconName, preview = false) {
page(parent, title, groups, iconName, preview = false, intro = null) {
const page = new Adw.PreferencesPage({ title, icon_name: iconName });
if (intro) page.add(intro);
const fields = this.fields.filter(field => groups.includes(field.group));
this.controls(page, fields, { preview });
for (const name of groups) {
@@ -2,17 +2,61 @@ import Adw from 'gi://Adw';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import Gtk from 'gi://Gtk';
import Gdk from 'gi://Gdk';
import { SettingsEditor } from './settings-editor.js';
import { BRAND, normalizeAccent } from './brand.js';
const BUS = 'io.qvac.Jarvis';
const PATH = '/io/qvac/Jarvis';
const IFACE = 'io.qvac.Jarvis.Session';
const OVERLAY_STYLES = ['tray', 'expanded'];
function entryRow(settings, title, key) {
const row = new Adw.EntryRow({ title });
row.set_text(settings.get_string(key));
row.connect('changed', () => settings.set_string(key, row.get_text()));
function applyBrandCss(directory) {
try {
const css = new Gtk.CssProvider();
css.load_from_path(`${directory}/brand/gtk.css`);
Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(), css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
} catch {}
}
function brandBanner(directory) {
const row = new Adw.ActionRow({ title: BRAND.product, subtitle: BRAND.tagline });
try {
const image = Gtk.Image.new_from_file(`${directory}/brand/icons/jarvis-mark.svg`);
image.set_pixel_size(36);
row.add_prefix(image);
} catch {}
row.activatable = false;
return row;
}
function accentRow(settings) {
const current = normalizeAccent(settings.get_string('accent-color'));
const row = new Adw.ActionRow({
title: 'Accent color',
subtitle: 'Gold is the Jarvis default. This tints the HUD immediately.',
});
const box = new Gtk.Box({ spacing: 6, valign: Gtk.Align.CENTER });
for (const accent of BRAND.accents) {
const button = new Gtk.Button({ tooltip_text: accent.label, valign: Gtk.Align.CENTER });
button.add_css_class('circular');
button.add_css_class('jarvis-swatch');
button.add_css_class(`jarvis-swatch-${accent.id}`);
button.set_size_request(24, 24);
button.connect('clicked', () => settings.set_string('accent-color', accent.value));
box.append(button);
}
const entry = new Gtk.Entry({ text: current, width_chars: 8, maxlength: 7, valign: Gtk.Align.CENTER });
entry.connect('changed', () => {
const value = normalizeAccent(entry.get_text(), '');
if (value) settings.set_string('accent-color', value);
});
settings.connect('changed::accent-color', () => {
const value = normalizeAccent(settings.get_string('accent-color'));
if (entry.get_text() !== value) entry.set_text(value);
});
row.add_suffix(box);
row.add_suffix(entry);
return row;
}
@@ -107,11 +151,14 @@ function grantRow() {
}
export function fillSettingsWindow(window, settings, directory) {
window.set_title('Jarvis QVAC');
window.set_title(BRAND.product);
window.set_default_size(840, 780);
window.search_enabled = true;
applyBrandCss(directory);
const editor = new SettingsEditor(directory, settings);
const voice = editor.page(window, 'Voice', ['Speech', 'Voice design'], 'audio-speakers-symbolic', true);
const intro = new Adw.PreferencesGroup();
intro.add(brandBanner(directory));
const voice = editor.page(window, 'Voice', ['Speech', 'Voice design'], 'audio-speakers-symbolic', true, intro);
window.add(voice);
const listening = editor.page(window, 'Listening', ['Listening', 'Wake and privacy', 'Detection tuning', 'Audio routing'], 'audio-input-microphone-symbolic');
window.add(listening);
@@ -119,7 +166,7 @@ export function fillSettingsWindow(window, settings, directory) {
const desktop = editor.page(window, 'Desktop', ['Desktop access', 'Desktop images'], 'preferences-desktop-display-symbolic');
const desktopGroup = new Adw.PreferencesGroup({ title: 'Overlay and shortcuts', description: 'These appearance settings take effect immediately.' });
desktopGroup.add(strvRow(settings, 'Hotkey', 'hotkey'));
desktopGroup.add(entryRow(settings, 'Accent color', 'accent-color'));
desktopGroup.add(accentRow(settings));
desktopGroup.add(comboRow(settings, 'Desktop layout', 'Tray keeps Jarvis in the top bar; expanded opens a conversation panel', 'overlay-style', OVERLAY_STYLES));
desktop.add(desktopGroup);
const grantGroup = new Adw.PreferencesGroup({ title: 'Temporary desktop access' });
@@ -1,21 +1,61 @@
.jarvis-panel-box { spacing: 6px; }
.jarvis-panel-glyph { color: var(--jarvis-accent, #F4B942); font-size: 14px; }
.jarvis-panel-state { padding: 1px 8px; border-radius: 999px; background-color: rgba(244, 185, 66, .22); }
.jarvis-panel-state-label { color: var(--jarvis-accent, #F4B942); font-size: 12px; font-weight: bold; }
/* Jarvis brand tokens. Gold is presence; cyan is thought. Accent is overridden live via --jarvis-accent. */
.jarvis-panel-box {
spacing: 7px;
--jarvis-signal: #4FD2FF;
--jarvis-ink: #0B0E14;
--jarvis-slate: #16191F;
--jarvis-paper: #F6F7FB;
--jarvis-mute: #AEB6C8;
--jarvis-dim: #9AA8C0;
}
.jarvis-panel-mark { color: var(--jarvis-accent, #F4B942); icon-size: 16px; }
.jarvis-panel-glyph {
color: var(--jarvis-accent, #F4B942);
font-size: 13px;
font-weight: 700;
letter-spacing: 0.8px;
}
.jarvis-panel-state { padding: 2px 9px; border-radius: 999px; background-color: alpha(var(--jarvis-accent, #F4B942), .22); }
.jarvis-panel-state-label { color: var(--jarvis-accent, #F4B942); font-size: 11px; font-weight: 700; letter-spacing: 0.3px; }
.jarvis-panel-box.jarvis-state-thinking .jarvis-panel-state,
.jarvis-panel-state.jarvis-state-thinking { background-color: alpha(#4FD2FF, .22); }
.jarvis-panel-box.jarvis-state-thinking .jarvis-panel-state-label,
.jarvis-panel-state.jarvis-state-thinking .jarvis-panel-state-label { color: #4FD2FF; }
.jarvis-panel-box.jarvis-state-sleeping .jarvis-panel-state,
.jarvis-panel-state.jarvis-state-sleeping { background-color: rgba(174, 182, 200, .18); }
.jarvis-panel-box.jarvis-state-sleeping .jarvis-panel-state-label,
.jarvis-panel-state.jarvis-state-sleeping .jarvis-panel-state-label { color: #AEB6C8; }
.jarvis-menu { max-width: 360px; }
.jarvis-menu-item { padding: 0; }
.jarvis-popup { width: 320px; padding: 12px 14px; spacing: 8px; color: #f6f7fb; }
.jarvis-session { width: 420px; padding: 14px 16px; spacing: 8px; border-radius: 16px; background-color: rgba(11, 14, 20, .94); border: 1px solid rgba(244, 185, 66, .42); color: #f6f7fb; box-shadow: 0 12px 40px rgba(0, 0, 0, .45); }
.jarvis-popup {
width: 320px;
padding: 12px 14px;
spacing: 8px;
color: #f6f7fb;
--jarvis-signal: #4FD2FF;
}
.jarvis-session {
width: 420px;
padding: 14px 16px;
spacing: 8px;
border-radius: 16px;
background-color: rgba(11, 14, 20, .94);
border: 1px solid alpha(var(--jarvis-accent, #F4B942), .42);
color: #f6f7fb;
box-shadow: 0 12px 40px rgba(0, 0, 0, .45);
--jarvis-signal: #4FD2FF;
}
.jarvis-popup-header { spacing: 8px; }
.jarvis-title { font-weight: bold; letter-spacing: 0.4px; font-size: 14px; }
.jarvis-local { color: var(--jarvis-accent, #F4B942); font-size: 11px; }
.jarvis-mark { icon-size: 18px; color: var(--jarvis-accent, #F4B942); }
.jarvis-title { font-weight: 700; letter-spacing: 0.6px; font-size: 15px; color: #f6f7fb; }
.jarvis-local { color: var(--jarvis-accent, #F4B942); font-size: 11px; font-weight: 700; letter-spacing: 0.4px; }
.jarvis-status-line { color: #aeb6c8; font-size: 12px; }
.jarvis-tabs { spacing: 6px; }
.jarvis-tab { padding: 4px 10px; border-radius: 999px; background-color: transparent; color: #9aa8c0; font-size: 12px; }
.jarvis-tab:hover, .jarvis-tab:focus { color: #f6f7fb; background-color: rgba(255, 255, 255, .08); }
.jarvis-tab-active { background-color: var(--jarvis-accent, #F4B942); color: #16191f; font-weight: bold; }
.jarvis-tab-active { background-color: var(--jarvis-accent, #F4B942); color: #16191f; font-weight: 700; }
.jarvis-thinking { color: #aeb6c8; font-size: 12px; padding: 8px 10px; border-left: 2px solid #4FD2FF; background-color: rgba(79, 210, 255, .06); border-radius: 8px; }
.jarvis-notice { color: #f4b942; font-size: 12px; }
.jarvis-notice { color: var(--jarvis-accent, #F4B942); font-size: 12px; }
.jarvis-confirm { spacing: 8px; padding: 4px 0; }
.jarvis-confirm-actions { spacing: 6px; }
.jarvis-confirm-label { color: #f6f7fb; font-size: 13px; }
@@ -26,22 +66,26 @@
.jarvis-session-thinking { height: 200px; }
.jarvis-transcript { spacing: 6px; }
.jarvis-row { padding: 6px 10px; border-radius: 8px; font-size: 13px; }
.jarvis-row-user { border-left: 2px solid var(--jarvis-accent, #F4B942); background-color: rgba(244, 185, 66, .08); }
.jarvis-row-user { border-left: 2px solid var(--jarvis-accent, #F4B942); background-color: alpha(var(--jarvis-accent, #F4B942), .08); }
.jarvis-row-jarvis { border-right: 2px solid #4FD2FF; background-color: rgba(79, 210, 255, .05); }
.jarvis-row-tool { background-color: transparent; color: #9aa8c0; font-size: 12px; padding: 2px 6px; }
.jarvis-chip-scroll { height: 32px; }
.jarvis-chips { spacing: 6px; }
.jarvis-chip { padding: 5px 9px; border-radius: 999px; background-color: rgba(255, 255, 255, .08); }
.jarvis-chip:hover, .jarvis-chip:focus { background-color: rgba(244, 185, 66, .25); }
.jarvis-chip:hover, .jarvis-chip:focus { background-color: alpha(var(--jarvis-accent, #F4B942), .25); }
.jarvis-chip-quiet { background-color: transparent; color: #aeb6c8; padding: 5px 8px; }
.jarvis-chip-quiet:hover, .jarvis-chip-quiet:focus { color: #f6f7fb; background-color: rgba(255, 255, 255, .08); }
.jarvis-talk { background-color: #F4B942; color: #16191f; font-weight: bold; padding: 8px 14px; }
.jarvis-talk { background-color: var(--jarvis-accent, #F4B942); color: #16191f; font-weight: 700; padding: 8px 14px; }
.jarvis-talk:hover, .jarvis-talk:focus { background-color: #ffe09a; }
.jarvis-talk:active { background-color: #ffe09a; }
.jarvis-controls { spacing: 6px; }
.jarvis-settings { font-size: 12px; padding: 4px 8px; }
.jarvis-popup StEntry, .jarvis-session StEntry { border-radius: 10px; padding: 8px 10px; background-color: rgba(255, 255, 255, .06); color: #f6f7fb; border: 1px solid rgba(255, 255, 255, .15); }
.jarvis-popup StEntry:focus, .jarvis-session StEntry:focus { border-color: #F4B942; }
.jarvis-popup StEntry:focus, .jarvis-session StEntry:focus { border-color: var(--jarvis-accent, #F4B942); }
.jarvis-cu { padding: 8px 12px; spacing: 4px; border-radius: 12px; background-color: rgba(11, 14, 20, .82); border: 1px solid rgba(79, 210, 255, .4); }
.jarvis-job, .jarvis-target, .jarvis-cu-step { color: #4FD2FF; font-size: 12px; }
.jarvis-agent-cursor { color: var(--jarvis-accent, #F4B942); font-size: 22px; }
.jarvis-high-contrast { border-width: 2px; background-color: #000; }
.jarvis-high-contrast .jarvis-title,
.jarvis-high-contrast .jarvis-panel-glyph,
.jarvis-high-contrast .jarvis-status-line { color: #fff; }
+26 -3
View File
@@ -1,9 +1,26 @@
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import St from 'gi://St';
import Clutter from 'gi://Clutter';
import Pango from 'gi://Pango';
function brandMark(size = 16, directory = '') {
if (typeof Gio === 'undefined' || typeof St.Icon !== 'function' || !directory) return null;
try {
const file = Gio.File.new_for_path(`${directory}/brand/icons/jarvis-mark.svg`);
if (!file.query_exists(null)) return null;
return new St.Icon({
gicon: new Gio.FileIcon({ file }),
icon_size: size,
style_class: 'jarvis-mark',
y_align: Clutter.ActorAlign.CENTER,
});
} catch {
return null;
}
}
export const STATES = new Set(['ARMED', 'LISTENING', 'THINKING', 'SPEAKING', 'SLEEPING']);
export const POPUP_ROWS = 8;
export const SESSION_WIDTH = 420;
@@ -56,17 +73,19 @@ function wrapLabel(label) {
}
export class ConversationView {
constructor({ compact = true, maxRows = compact ? POPUP_ROWS : 40 } = {}) {
constructor({ compact = true, maxRows = compact ? POPUP_ROWS : 40, brandDir = '' } = {}) {
this.compact = compact;
this.maxRows = maxRows;
this.root = new St.BoxLayout({ style_class: compact ? 'jarvis-popup' : 'jarvis-session', vertical: true, reactive: true, can_focus: true, x_expand: true });
this.root.accessible_name = compact ? 'Jarvis voice assistant' : 'Jarvis conversation';
this.header = new St.BoxLayout({ style_class: 'jarvis-popup-header', x_expand: true });
this.mark = brandMark(compact ? 16 : 20, brandDir);
this.title = new St.Label({ text: 'Jarvis', style_class: 'jarvis-title', x_align: Clutter.ActorAlign.START });
this.title.accessible_name = 'Jarvis status';
this.status = new St.Label({ text: 'LOCAL', style_class: 'jarvis-local' });
this.status.accessible_name = 'Local model status';
if (this.status.clutter_text) this.status.clutter_text.ellipsize = Pango.EllipsizeMode.END;
if (this.mark) this.header.add_child(this.mark);
this.header.add_child(this.title);
this.header.add_child(new St.Widget({ x_expand: true }));
this.header.add_child(this.status);
@@ -418,6 +437,10 @@ export class JarvisOsd {
this.label.text = text;
this.root.visible = Boolean(text);
this.root.accessible_name = text ? `Jarvis ${text}` : 'Jarvis status';
for (const name of ['armed', 'listening', 'speaking', 'thinking', 'sleeping']) {
this.root.remove_style_class_name(`jarvis-state-${name}`);
}
if (this._state) this.root.add_style_class_name(`jarvis-state-${this._state.toLowerCase()}`);
}
_clearTimer() { if (this._timeout) { GLib.Source.remove(this._timeout); this._timeout = 0; } }
hide() { this._wake = false; this._clearTimer(); this.root.visible = false; }
@@ -463,8 +486,8 @@ export class ComputerUseChrome {
}
export class SessionPanel {
constructor() {
this.view = new ConversationView({ compact: false });
constructor({ brandDir = '' } = {}) {
this.view = new ConversationView({ compact: false, brandDir });
this.root = this.view.root;
this.minimized = true;
}