Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ version with its date and start a fresh empty `[Unreleased]` above it.

## [Unreleased]

### Changed

- The ribbon icon, view header logo, and model selector rows now use the new
Qoder mark introduced with the new Qoder app, replacing the old QoderIDE
glyph. Like the new app icon, the monochrome mark follows the theme's ink
tone — dark on light themes, light on dark themes — instead of brand green.

## [1.0.15] - 2026-09-21

### Added
Expand Down
5 changes: 4 additions & 1 deletion src/assets/qoder-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 10 additions & 23 deletions src/shared/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function appendCheckIcon(container: HTMLElement): void {

export const QODER_ICON: IconSvg = {
kind: 'raw',
viewBox: '0 0 180 180',
viewBox: '0 0 100 100',
svg: qoderIconSvg,
};

Expand Down Expand Up @@ -96,29 +96,22 @@ export const CHECK_ICON: IconSvg = {

export const QODERIAN_ICON_ID = 'qoderian-logo';

// Obsidian draws registered icons inside a `0 0 100 100` viewport, so the mark
// of the 180x180 app artwork (bounding box 32.4..147.6 x 30.2..148.4) is
// re-centered and scaled down to leave the padding ribbon icons expect.
const QODER_MARK_TRANSFORM = 'translate(-14.8 -14.29) scale(0.72)';
// Obsidian draws registered icons inside a `0 0 100 100` viewport. The mark
// fills 15.2..84.8 x 15.0..86.1 of the 100x100 artwork, so it is scaled up
// and re-centered to leave the padding ribbon icons expect.
const QODER_MARK_TRANSFORM = 'translate(-9.61 -10.31) scale(1.19)';
// `addIcon` takes markup as a string and the artwork is a build-time asset, so
// the mark paths are lifted textually instead of through a DOM round-trip.
const QODER_MARK_PATH_PATTERN = /<path[^>]*\sd="([^"]+)"/g;
// Ribbon and tab icons follow a single theme color, so the artwork's white
// front ring and green extruded body both become `currentColor`. Drawing them
// at the same strength collapses the mark into one blob, so the body behind
// the ring is dimmed to keep the depth the two-tone original conveys.
const QODER_MARK_BODY_OPACITY = '0.32';
const QODER_MARK_PATH_PATTERN = /<path\b[^>]*>/g;

export function createQoderianIconContent(sourceSvg: string = qoderIconSvg): string {
// The artwork draws the extruded body first and the front ring second; that
// order is preserved so the ring stays on top of the dimmed body.
// The artwork fills the outline body first and strokes the contour second;
// that order is preserved so the stroke stays on top of the fill.
const paths = Array.from(sourceSvg.matchAll(QODER_MARK_PATH_PATTERN))
.map(([, pathData], index) => (index === 0
? `<path opacity="${QODER_MARK_BODY_OPACITY}" d="${pathData}"/>`
: `<path d="${pathData}"/>`))
.map(([path]) => path)
.join('');

return `<g transform="${QODER_MARK_TRANSFORM}" fill="currentColor">${paths}</g>`;
return `<g transform="${QODER_MARK_TRANSFORM}">${paths}</g>`;
}

export interface CreateIconSvgOptions {
Expand Down Expand Up @@ -158,12 +151,6 @@ export function createIconSvg(
if (sourceSvg) {
for (const child of Array.from(sourceSvg.children)) {
const clone = ownerDocument.importNode(child, true) as SVGElement;
// The source artwork is an app-icon variant with a dark rounded-square
// backdrop. In a compact model selector that backdrop turns into the
// grey block seen on every option, so keep only the Qoder mark.
if (clone.tagName.toLowerCase() === 'g') {
clone.querySelector(':scope > rect')?.remove();
}
svg.appendChild(clone);
}
}
Expand Down
7 changes: 0 additions & 7 deletions src/style/base/variables.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,3 @@ body.theme-light .qoderian-container {
--qoderian-brand: #35CC5F;
--qoderian-brand-rgb: 53, 204, 95;
}

/* The Qoder glyph has a white half for dark surfaces. On light Obsidian
themes that half disappears into the canvas, so use the same ink tone as
the plugin title while retaining the native green half. */
body.theme-light .qoderian-icon path[fill="#FFFFFF"] {
fill: #202124;
}
4 changes: 3 additions & 1 deletion src/style/components/header.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
.qoderian-logo {
display: flex;
align-items: center;
color: var(--qoderian-brand);
/* The Qoder mark is monochrome, so it follows the theme's ink tone like the
app icon does: dark on light themes, light on dark themes. */
color: var(--text-normal);
}

/* Update badge: an icon button in the header actions, shown only while a newer
Expand Down
9 changes: 5 additions & 4 deletions src/style/toolbar/model-selector.css
Original file line number Diff line number Diff line change
Expand Up @@ -214,10 +214,11 @@
opacity: 0.7;
}

/* Qoder's dual-green mark stays branded on the active option, while inactive
options inherit the muted selector text just like PR #973. */
.qoderian-model-option:not(.selected) .qoderian-model-icon * {
fill: currentColor;
/* The mark is monochrome and follows the theme's ink tone, so the selected
row's green label must not bleed into the icon. */
.qoderian-model-option.selected .qoderian-model-icon {
color: var(--text-normal);
opacity: 1;
}

.qoderian-model-option {
Expand Down
23 changes: 10 additions & 13 deletions tests/unit/shared/icons.test.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,28 @@
import { createQoderianIconContent, QODERIAN_ICON_ID } from '@/shared/icons';

const SOURCE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="180" height="180" viewBox="0 0 180 180">`
+ `<defs><clipPath id="mask"><rect x="0" y="0" width="180" height="180" rx="40"/></clipPath></defs>`
+ `<g clip-path="url(#mask)"><rect x="0" y="0" width="180" height="180" fill="#111113"/>`
+ `<path d="M10 20 L30 40 Z" fill="#2ADB5C"/><path d="M50 60 L70 80 Z" fill="#FFFFFF"/></g></svg>`;
const SOURCE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100" fill="none">`
+ `<path fill-rule="evenodd" clip-rule="evenodd" d="M10 20 L30 40 Z" fill="currentColor"/>`
+ `<path fill="none" stroke="currentColor" stroke-width="1.7" d="M50 60 L70 80 Z"/></svg>`;

describe('createQoderianIconContent', () => {
it('keeps only the mark paths, dropping the artwork backdrop and clip ids', () => {
it('lifts the mark paths verbatim, keeping their paint attributes', () => {
const content = createQoderianIconContent(SOURCE_SVG);

expect(content).toContain('d="M10 20 L30 40 Z"');
expect(content).toContain('<path d="M50 60 L70 80 Z"/>');
expect(content).toContain('<path fill-rule="evenodd" clip-rule="evenodd" d="M10 20 L30 40 Z" fill="currentColor"/>');
expect(content).toContain('<path fill="none" stroke="currentColor" stroke-width="1.7" d="M50 60 L70 80 Z"/>');
expect(content).not.toContain('rect');
expect(content).not.toContain('clip-path');
});

it('renders the mark in one theme color, dimming the body behind the front ring', () => {
it('renders the mark in one theme color inside the ribbon viewport', () => {
const content = createQoderianIconContent(SOURCE_SVG);

expect(content).toContain('fill="currentColor"');
expect(content).toContain('<path opacity="0.32" d="M10 20 L30 40 Z"/>');
expect(content).toMatch(/transform="translate\([-\d. ]+\) scale\(0?\.\d+\)"/);
expect(content).not.toContain('#2ADB5C');
expect(content).not.toContain('#FFFFFF');
expect(content).toContain('stroke="currentColor"');
expect(content).toMatch(/^<g transform="translate\(-[\d.]+ -[\d.]+\) scale\([\d.]+\)"?>/);
});

it('draws the front ring after the body so it stays on top', () => {
it('draws the filled body before the outlined contour', () => {
const content = createQoderianIconContent(SOURCE_SVG);

expect(content.indexOf('M10 20')).toBeLessThan(content.indexOf('M50 60'));
Expand Down
Loading