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
2 changes: 1 addition & 1 deletion template-owned/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
},
"dependencies": {
"@vectojs/core": "1.36.0",
"@vectojs/desktop": "0.4.0",
"@vectojs/desktop": "0.6.0",
"@vectojs/devtools": "0.11.1",
"@vectojs/styles": "0.3.2",
"@vectojs/ui": "2.16.7"
Expand Down
2 changes: 1 addition & 1 deletion template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
},
"dependencies": {
"@vectojs/core": "1.36.0",
"@vectojs/desktop": "0.4.0",
"@vectojs/desktop": "0.6.0",
"@vectojs/devtools": "0.11.1",
"@vectojs/styles": "0.3.2",
"@vectojs/ui": "2.16.7"
Expand Down
183 changes: 176 additions & 7 deletions template/src/app/ui-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,99 @@
*/

import { Entity, type IRenderer } from '@vectojs/core';
import { Button, Stack, Text } from '@vectojs/ui';
import {
Button,
DOCUMENT_SCROLL_PHYSICS,
Input,
ScrollView,
Stack,
Text,
TextArea,
} from '@vectojs/ui';
import { appTheme } from '../model/app-theme';

type TextRole = 'text' | 'textMuted' | null;

class ThemedText extends Text {
constructor(
content: string,
options: ConstructorParameters<typeof Text>[1],
private readonly role: TextRole,
) {
super(content, options);
}

public override render(renderer: IRenderer): void {
if (this.role) this.color = appTheme()[this.role];
super.render(renderer);
}
}

type ButtonRole = 'primary' | 'secondary' | 'danger';

class ThemedButton extends Button {
private pressed = false;

constructor(
label: string,
private readonly role: ButtonRole,
options: ConstructorParameters<typeof Button>[1],
) {
super(label, options);
}

public override render(renderer: IRenderer): void {
const theme = appTheme();
this.bg =
this.role === 'primary'
? theme.accent
: this.role === 'danger'
? theme.dangerSurface
: theme.surfaceSunken;
this.hoverBg = this.role === 'primary' ? theme.accentHover : theme.surfaceRaised;
this.color =
this.role === 'primary'
? theme.accentText
: this.role === 'danger'
? theme.danger
: theme.text;
this.focusColor = theme.focus;
const idleBg = this.bg;
if (this.pressed && !this.disabled) this.bg = this.hoverBg;
super.render(renderer);
this.bg = idleBg;
}

public setPressed(pressed: boolean): void {
if (this.pressed === pressed) return;
this.pressed = pressed;
this.scene?.markDirty();
}
}

export class ThemedInput extends Input {
public override render(renderer: IRenderer): void {
const theme = appTheme();
this.bg = theme.inputSurface;
this.border = theme.border;
this.color = theme.text;
this.placeholderColor = theme.textMuted;
this.selectionColor = theme.accent;
super.render(renderer);
}
}

export class ThemedTextArea extends TextArea {
public override render(renderer: IRenderer): void {
const theme = appTheme();
this.bg = theme.inputSurface;
this.border = theme.border;
this.color = theme.text;
this.placeholderColor = theme.textMuted;
this.selectionColor = theme.accent;
super.render(renderer);
}
}

export function t(
content: string,
Expand All @@ -14,30 +106,64 @@ export function t(
maxWidth?: number,
): Text {
const font = `${bold ? '600' : '400'} ${size}px "Segoe UI", system-ui, sans-serif`;
const el = new Text(content, { font, color, ...(maxWidth ? { maxWidth } : {}) });
const role = color === '#1e293b' ? 'text' : null;
const el = new ThemedText(
content,
{
font,
color: role ? appTheme().text : color,
...(maxWidth ? { maxWidth } : {}),
},
role,
);
el.height = size + 6;
return el;
}

export function p(content: string, size = 12, color = '#475569', maxWidth?: number): Text {
const font = `400 ${size}px/1.5 "Segoe UI", system-ui, sans-serif`;
const el = new Text(content, { font, color, ...(maxWidth ? { maxWidth } : {}) });
const role = color === '#475569' ? 'textMuted' : null;
const el = new ThemedText(
content,
{
font,
color: role ? appTheme().textMuted : color,
...(maxWidth ? { maxWidth } : {}),
},
role,
);
el.height = size + 8;
return el;
}

export function btn(label: string, primary: boolean, onClick: () => void): Button {
const b = new Button(label, {
bg: primary ? '#2563eb' : '#f1f5f9',
hoverBg: primary ? '#1d4ed8' : '#e2e8f0',
color: primary ? '#ffffff' : '#0f172a',
return themedButton(label, primary ? 'primary' : 'secondary', onClick);
}

export function themedButton(label: string, role: ButtonRole, onClick: () => void): Button {
const theme = appTheme();
const b = new ThemedButton(label, role, {
bg:
role === 'primary'
? theme.accent
: role === 'danger'
? theme.dangerSurface
: theme.surfaceSunken,
hoverBg: role === 'primary' ? theme.accentHover : theme.surfaceRaised,
color: role === 'primary' ? theme.accentText : role === 'danger' ? theme.danger : theme.text,
font: '500 12px "Segoe UI", system-ui, sans-serif',
padding: 6,
radius: 4,
focusColor: theme.focus,
height: 28,
onClick,
});
b.a11yProjection = 'eager';
const themed = b as ThemedButton;
b.on('pointerdown', () => themed.setPressed(true));
b.on('pointerup', () => themed.setPressed(false));
b.on('pointercancel', () => themed.setPressed(false));
b.on('pointerleave', () => themed.setPressed(false));
return b;
}

Expand Down Expand Up @@ -77,3 +203,46 @@ export class ClientRoot extends Entity {
this.content.height = Math.max(0, this.height - this.inset * 2);
}
}

/** Insets a document-like stack and scrolls it when a window reaches its minimum size. */
export class ScrollableClientRoot extends Entity {
private readonly scroll: ScrollView;

constructor(
private readonly content: Stack,
private readonly responsiveText: Text[],
private readonly inset = 18,
) {
super();
this.clipChildren = true;
this.scroll = new ScrollView({
width: 1,
height: 1,
scrollPhysics: DOCUMENT_SCROLL_PHYSICS,
});
this.scroll.content.add(content);
this.add(this.scroll);
}

public override isPointInside(gx: number, gy: number): boolean {
const local = this.worldToLocal(gx, gy);
if (!local) return false;
return local.x >= 0 && local.y >= 0 && local.x <= this.width && local.y <= this.height;
}

public override render(_r: IRenderer): void {
const width = Math.max(0, this.width - this.inset * 2);
const height = Math.max(0, this.height - this.inset * 2);
for (const text of this.responsiveText) {
if (text.maxWidth !== width) text.setMaxWidth(width);
}
this.content.width = width;
this.content.layout();
this.scroll.x = this.inset;
this.scroll.y = this.inset;
this.scroll.width = width;
this.scroll.height = height;
this.scroll.content.width = width;
this.scroll.content.height = this.content.height;
}
}
12 changes: 9 additions & 3 deletions template/src/apps/_hrule.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
/** Horizontal rule divider shared by app layouts. */

import { Rect } from '@vectojs/core';
import { Rect, type IRenderer } from '@vectojs/core';
import { appTheme } from '../model/app-theme';

export class HRule extends Rect {
constructor(color = '#cbd5e1') {
super({ width: 100, height: 1, fill: color });
constructor() {
super({ width: 100, height: 1, fill: appTheme().border });
}

public override render(renderer: IRenderer): void {
this.fill = appTheme().border;
super.render(renderer);
}
}
25 changes: 20 additions & 5 deletions template/src/apps/about.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@
*/

import type { AppDefinition } from '@vectojs/desktop';
import { ClientRoot, p, t, vstack } from '../app/ui-helpers';
import { p, ScrollableClientRoot, t, vstack } from '../app/ui-helpers';
import { appIconSvg } from '../desktop/icons';
import { HRule } from './_hrule';

export const aboutApp: AppDefinition = {
id: 'about',
title: 'About VectoJS WebOS',
icon: '💻',
iconSvg: appIconSvg('about'),
instances: 'single',
defaultWidth: 520,
defaultHeight: 440,
minWidth: 400,
minHeight: 300,
create: () => {
const title = t('VectoJS WebOS', 16);
const ver = p('Version 0.1.0');
Expand Down Expand Up @@ -43,22 +46,34 @@ export const aboutApp: AppDefinition = {
const shortcuts = p(
'Shortcuts:\n• Start Menu: Ctrl+Space / Meta+Space\n• New Terminal: Ctrl+Alt+T\n• New Notes: Ctrl+N\n• Close Window: Ctrl+W',
);
const architectureTitle = t('Architecture Highlights', 14);
const shortcutsTitle = t('Desktop Shortcuts', 14);
const stack = vstack(
[
title,
ver,
new HRule(),
t('Architecture Highlights', 14),
architectureTitle,
spec1,
spec2,
spec3,
spec4,
new HRule(),
t('Desktop Shortcuts', 14),
shortcutsTitle,
shortcuts,
],
8,
);
return new ClientRoot(stack, 18);
return new ScrollableClientRoot(stack, [
title,
ver,
architectureTitle,
spec1,
spec2,
spec3,
spec4,
shortcutsTitle,
shortcuts,
]);
},
};
Loading