Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
e7cfcd4
chore: `npm audit fix --force`
allozaur Jun 19, 2026
b1c5139
feat: Update sidebar toggle to use Logo
allozaur Jun 19, 2026
de72864
refactor: Clean up favicon SVG
allozaur Jun 19, 2026
152b9bc
feat: Refactor logo component and implement theme-aware favicon gener…
allozaur Jun 19, 2026
5a8d498
feat: Add configurable padding to generated PWA assets
allozaur Jun 19, 2026
9976f6f
test: Add unit tests for writeThemeFavicons
allozaur Jun 19, 2026
0a4918a
refactor: Componentization
allozaur Jun 19, 2026
3bfb7e8
feat: WIP
allozaur Jun 19, 2026
3d41709
feat: WIP
allozaur Jun 19, 2026
aca5f43
feat: WIP
allozaur Jun 19, 2026
adcb88a
feat: Mobile UI
allozaur Jun 19, 2026
8ae5a55
feat: add SEARCH route constant
allozaur Jun 20, 2026
73f8d59
feat: create SidebarNavigationSearchResults component
allozaur Jun 20, 2026
33d8dfd
refactor: use SidebarNavigationSearchResults in conversation list
allozaur Jun 20, 2026
80cd323
feat: enable mobile search navigation in sidebar actions
allozaur Jun 20, 2026
ca903cd
feat: add mobile search route and page
allozaur Jun 20, 2026
b588b96
fix: prevent sidebar overflow on mobile viewports
allozaur Jun 20, 2026
0c73e88
fix: Mobile sidebar
allozaur Jun 20, 2026
85174de
feat: Mobile Search WIP
allozaur Jun 20, 2026
4e6c307
feat: Mobile WIP
allozaur Jun 20, 2026
2794cf1
feat: Add PWA standalone detection and refine mobile UI
allozaur Jun 20, 2026
332ad9a
feat: Improve mobile layout, sidebar handling, and chat scrolling
allozaur Jun 20, 2026
0d859b1
feat: Improve mobile sidebar visibility and iOS Safari chat spacing
allozaur Jun 20, 2026
c352cab
fix: Disable auto-scroll on mobile
allozaur Jun 20, 2026
c5e9145
chore: Linting
allozaur Jun 20, 2026
5dad761
fix: Wrong condition
allozaur Jun 20, 2026
d314fb1
feat: Mobile chat scroll
allozaur Jun 20, 2026
cd9be08
refactor: WIP
allozaur Jun 20, 2026
e702725
fix: Desktop initial scroll always working again
allozaur Jun 20, 2026
43ed709
fix: Partial fix for mobile auto-scroll / initial scroll
allozaur Jun 20, 2026
0aa36d8
fix: Desktop auto-scroll on initial load and during streaming
allozaur Jun 20, 2026
2a8f36e
fix: Mobile scrolling logic
allozaur Jun 20, 2026
e6f045e
refactor: Clean up
allozaur Jun 22, 2026
4974f23
feat: Improve start UI
allozaur Jun 22, 2026
95d5e00
feat: Add `delay` to `fadeInView`
allozaur Jun 22, 2026
fed1c25
feat: Auto-scroll button
allozaur Jun 22, 2026
685b2b2
refactor: Cleanup
allozaur Jun 22, 2026
48c13d2
refactor: Extract chat dialogs and alerts into dedicated component
allozaur Jun 22, 2026
26a606b
refactor: Reorganize ChatScreen component structure and initialization
allozaur Jun 22, 2026
df0a064
feat: Improve auto-scroll after sending message
allozaur Jun 22, 2026
a338553
feat: UI improvements
allozaur Jun 22, 2026
c23e858
fix: Settings link
allozaur Jun 22, 2026
91ee50f
feat: UI improvements
allozaur Jun 22, 2026
54bc644
fix: better UI spacing
allozaur Jun 22, 2026
cb594a7
fix: Remove unneeded logic
allozaur Jun 22, 2026
d0d3c4d
fix: Chat Processing Info UI rendering
allozaur Jun 22, 2026
c5bdf51
feat: Improve mobile UI
allozaur Jun 22, 2026
7d72082
feat: UI improvement
allozaur Jun 22, 2026
6fd2256
fix: Conditional transition delay for Chat Messages based on route from
allozaur Jun 22, 2026
44ca742
fix: Delay mobile sidebar collapse for smoother transitions
allozaur Jun 22, 2026
0b6c170
fix: Mobile scroll down button + sidebar pointer events
allozaur Jun 23, 2026
c57507e
fix: Mobile UI
allozaur Jun 23, 2026
b9f07b3
fix: Auto scrolling
allozaur Jun 23, 2026
7e4d1c9
fix: Implement dynamic height calculations for chat auto-scroll posit…
allozaur Jun 23, 2026
631eed0
fix: Retrieve `autofocus` for Chat Form textarea
allozaur Jun 23, 2026
342d969
fix: Use proper class
allozaur Jun 23, 2026
4a4f4e8
refactor: extract scroll-to-bottom logic and fix message send flow
allozaur Jun 23, 2026
7b7473b
fix: update viewport store usage and remove conflicting autofocus
allozaur Jun 23, 2026
b65cea2
feat: add accessibility labels to scroll down button
allozaur Jun 23, 2026
940e4bb
fix: correct HTML structure in sidebar empty states
allozaur Jun 23, 2026
d5a2c20
fix: dynamically toggle processing info visibility
allozaur Jun 23, 2026
043864c
chore: remove commented exports and fix formatting
allozaur Jun 23, 2026
0b85680
fix
allozaur Jun 23, 2026
e33bdc5
fix: Mobile Chat Form Add Action Sheet interactions
allozaur Jun 24, 2026
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
3 changes: 1 addition & 2 deletions tools/ui/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,9 @@ vite.config.ts.timestamp-*
# PWA Artifacts
apple-splash-*.png
apple-touch-icon-*.png
favicon.ico
favicon-dark.ico
maskable-icon-*.png
pwa-*.png
static/favicon*

# Storybook
*storybook.log
Expand Down
14 changes: 7 additions & 7 deletions tools/ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion tools/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"bits-ui": "2.18.1",
"clsx": "2.1.1",
"dexie": "4.4.3",
"dompurify": "3.4.5",
"dompurify": "3.4.11",
"eslint": "9.39.4",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-storybook": "10.4.2",
Expand Down
9 changes: 8 additions & 1 deletion tools/ui/pwa-assets-dark.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { defineConfig } from '@vite-pwa/assets-generator/config';
import { FAVICON_COLORS, PWA_ASSET_GENERATOR } from './src/lib/constants/pwa';
import { writeThemeFavicons } from './scripts/favicon-colorize';

writeThemeFavicons(FAVICON_COLORS.LIGHT, FAVICON_COLORS.DARK, {
padding: PWA_ASSET_GENERATOR.FAVICON_PADDING
});

export default defineConfig({
headLinkOptions: {
Expand All @@ -7,7 +13,8 @@ export default defineConfig({
preset: {
transparent: {
sizes: [],
favicons: [[48, 'favicon-dark.ico']]
favicons: [[48, 'favicon-dark.ico']],
padding: PWA_ASSET_GENERATOR.FAVICON_PADDING
},
maskable: {
sizes: []
Expand Down
21 changes: 19 additions & 2 deletions tools/ui/pwa-assets.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,32 @@ import {
} from '@vite-pwa/assets-generator/config';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { THEME_COLORS, PWA_GENERATOR_DEVICES, PWA_ASSET_GENERATOR } from './src/lib/constants/pwa';
import {
THEME_COLORS,
PWA_GENERATOR_DEVICES,
PWA_ASSET_GENERATOR,
FAVICON_COLORS
} from './src/lib/constants/pwa';
import { SplashOrientation } from './src/lib/enums/splash.enums';
import { writeThemeFavicons } from './scripts/favicon-colorize';

writeThemeFavicons(FAVICON_COLORS.LIGHT, FAVICON_COLORS.DARK, {
padding: PWA_ASSET_GENERATOR.FAVICON_PADDING
});

export default defineConfig({
headLinkOptions: {
preset: PWA_ASSET_GENERATOR.LINK_PRESET
},
preset: combinePresetAndAppleSplashScreens(
minimal2023Preset,
{
...minimal2023Preset,
// tiny margin so favicon.ico / pwa-*.png breathe inside the canvas
transparent: {
...minimal2023Preset.transparent,
padding: PWA_ASSET_GENERATOR.FAVICON_PADDING
}
},
{
padding: PWA_ASSET_GENERATOR.SPLASH_PADDING,
resizeOptions: {
Expand Down
107 changes: 107 additions & 0 deletions tools/ui/scripts/favicon-colorize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const HERE = dirname(fileURLToPath(import.meta.url));
const PROJECT_ROOT = resolve(HERE, '..');

const DEFAULT_LOGO = resolve(PROJECT_ROOT, 'src/lib/assets/logo.svg');
const DEFAULT_OUT_DIR = resolve(PROJECT_ROOT, 'static');
const DEFAULT_OUT_LIGHT = resolve(DEFAULT_OUT_DIR, 'favicon.svg');
const DEFAULT_OUT_DARK = resolve(DEFAULT_OUT_DIR, 'favicon-dark.svg');

const CURRENT_COLOR = 'currentColor';

export interface ColorizedFavicon {
light: string;
dark: string;
}

export interface WriteThemeFaviconsOptions {
sourcePath?: string;
lightOutPath?: string;
darkOutPath?: string;
/**
* Fraction of the icon (0..1) to leave as an even margin on each side.
* Applied by wrapping the inner content in a `<g transform="...">` so the
* source `src/lib/assets/logo.svg` is not modified. Pass 0 to disable.
*/
padding?: number;
}

/**
* Replace every `currentColor` occurrence in the SVG with the given color.
* Pure: no filesystem access, so it is straightforward to unit-test.
*/
export function colorizeFaviconSvg(
svg: string,
lightColor: string,
darkColor: string
): ColorizedFavicon {
return {
light: svg.replaceAll(CURRENT_COLOR, lightColor),
dark: svg.replaceAll(CURRENT_COLOR, darkColor)
};
}

/**
* Shrink the inner SVG content uniformly and re-center it so `padding` (a
* 0..1 fraction) is reserved as equal margin on each side. Returns the input
* unchanged for non-positive padding, missing/invalid `viewBox`, or unexpected
* markup so the caller always gets a renderable SVG.
*/
export function padFaviconSvg(svg: string, padding: number): string {
if (!(padding > 0) || padding >= 1) return svg;

const viewBoxMatch = svg.match(/viewBox\s*=\s*["']([^"']+)["']/i);
if (!viewBoxMatch) return svg;

const parts = viewBoxMatch[1]
.trim()
.split(/[\s,]+/)
.map(Number);
if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) return svg;

const [, , width, height] = parts;
if (width <= 0 || height <= 0) return svg;

const scale = 1 - padding;
const translateX = (padding * width) / 2;
const translateY = (padding * height) / 2;

const openTagStart = svg.search(/<svg\b/i);
if (openTagStart === -1) return svg;
const openTagEnd = svg.indexOf('>', openTagStart);
if (openTagEnd === -1) return svg;
const closeStart = svg.lastIndexOf('</svg');
if (closeStart === -1 || closeStart <= openTagEnd) return svg;

const openTag = svg.slice(0, openTagEnd + 1);
const inner = svg.slice(openTagEnd + 1, closeStart);
const closeTag = svg.slice(closeStart);

const group = `<g transform="translate(${translateX} ${translateY}) scale(${scale})">`;
return `${openTag}${group}${inner}</g>${closeTag}`;
}

/**
* Read `src/lib/assets/logo.svg`, colorize it for both themes, and write
* the results to the static directory so the PWA asset generator can consume
* them. Paths can be overridden for tests.
*/
export function writeThemeFavicons(
lightColor: string,
darkColor: string,
{
sourcePath = DEFAULT_LOGO,
lightOutPath = DEFAULT_OUT_LIGHT,
darkOutPath = DEFAULT_OUT_DARK,
padding = 0
}: WriteThemeFaviconsOptions = {}
): void {
const source = readFileSync(sourcePath, 'utf-8');
const { light, dark } = colorizeFaviconSvg(source, lightColor, darkColor);
mkdirSync(dirname(lightOutPath), { recursive: true });
writeFileSync(lightOutPath, padFaviconSvg(light, padding));
writeFileSync(darkOutPath, padFaviconSvg(dark, padding));
}
7 changes: 6 additions & 1 deletion tools/ui/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,15 @@

--chat-form-area-height: 8rem;
--chat-form-area-offset: 2rem;
--chat-form-padding-top: 6rem;
--max-message-height: max(24rem, min(80dvh, calc(100dvh - var(--chat-form-area-height) - 12rem)));
}

@media (min-width: 640px) {
:root {
--chat-form-area-height: 24rem;
--chat-form-area-offset: 12rem;
--chat-form-padding-top: 6rem;
}
}

Expand Down Expand Up @@ -141,7 +143,6 @@
@apply bg-background text-foreground;
scrollbar-width: thin;
scrollbar-gutter: stable;
overflow: hidden; /* Added due to Mermaid rendering somehow causing the double scrollbar */
}

/* Global scrollbar styling - visible only on hover */
Expand Down Expand Up @@ -193,3 +194,7 @@
scrollbar-width: none;
}
}

.mermaidTooltip {
display: none !important;
}
14 changes: 8 additions & 6 deletions tools/ui/src/lib/actions/fade-in-view.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import { isElementInViewport } from '$lib/utils/viewport';
*/
export function fadeInView(
node: HTMLElement,
options: { duration?: number; y?: number; skipIfVisible?: boolean } = {}
options: { duration?: number; y?: number; delay?: number; skipIfVisible?: boolean } = {}
) {
const { duration = 300, y = 0, skipIfVisible = false } = options;
const { duration = 300, y = 0, delay = 0, skipIfVisible = false } = options;

if (skipIfVisible && isElementInViewport(node)) {
return;
Expand All @@ -27,10 +27,12 @@ export function fadeInView(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
requestAnimationFrame(() => {
node.style.opacity = '1';
node.style.transform = 'translateY(0)';
});
setTimeout(() => {
requestAnimationFrame(() => {
node.style.opacity = '1';
node.style.transform = 'translateY(0)';
});
}, delay);
observer.disconnect();
}
}
Expand Down
7 changes: 7 additions & 0 deletions tools/ui/src/lib/assets/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
75 changes: 46 additions & 29 deletions tools/ui/src/lib/components/app/actions/ActionIcon.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
ariaLabel?: string;
class?: string;
disabled?: boolean;
href?: string;
icon: Component;
iconSize?: string;
onclick: (e?: MouseEvent) => void;
onclick?: (e?: MouseEvent) => void;
size?: ButtonSize;
stopPropagationOnClick?: boolean;
tooltip: string;
tooltip?: string;
variant?: ButtonVariant;
tooltipSide?: TooltipSide;
}
Expand All @@ -22,6 +23,7 @@
icon,
tooltip,
variant = 'ghost',
href = '',
size = 'sm',
class: className = '',
disabled = false,
Expand All @@ -31,34 +33,49 @@
onclick,
ariaLabel
}: Props = $props();

let innerWidth = $state(0);
const showTooltip = $derived(!!tooltip && innerWidth > 768);
</script>

<Tooltip.Root>
<Tooltip.Trigger>
<!-- prevent another nested button element -->
{#snippet child({ props })}
<Button
{...props}
{variant}
{size}
{disabled}
onclick={(e: MouseEvent) => {
if (stopPropagationOnClick) e.stopPropagation();
{#snippet button(props = {})}
<Button
{...props}
{href}
{variant}
{size}
{disabled}
onclick={(e: MouseEvent) => {
if (stopPropagationOnClick) e.stopPropagation();

onclick?.(e);
}}
class="h-6 w-6 p-0 {className} flex hover:bg-transparent data-[state=open]:bg-transparent!"
aria-label={ariaLabel || tooltip}
>
{#if icon}
{@const IconComponent = icon}

<IconComponent class={iconSize} />
{/if}
</Button>
{/snippet}

{#if showTooltip}
<Tooltip.Root>
<Tooltip.Trigger>
<!-- prevent another nested button element -->
{#snippet child({ props })}
{@render button(props)}
{/snippet}
</Tooltip.Trigger>

onclick?.(e);
}}
class="h-6 w-6 p-0 {className} flex hover:bg-transparent data-[state=open]:bg-transparent!"
aria-label={ariaLabel || tooltip}
>
{#if icon}
{@const IconComponent = icon}
<IconComponent class={iconSize} />
{/if}
</Button>
{/snippet}
</Tooltip.Trigger>
<Tooltip.Content side={tooltipSide}>
<p>{tooltip}</p>
</Tooltip.Content>
</Tooltip.Root>
{:else}
{@render button({ href })}
{/if}

<Tooltip.Content side={tooltipSide}>
<p>{tooltip}</p>
</Tooltip.Content>
</Tooltip.Root>
<svelte:window bind:innerWidth />
Loading
Loading