Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
30 changes: 30 additions & 0 deletions examples/app/calculator/data/state.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"textdirection": "ltr",
"language": "en",
"title": "Calculator",
"mode": "standard",
"displayValue": "0",
"expression": "",
"columns": "4",
"buttons": [
{ "label": "AC", "value": "clear", "type": "action", "span": "1" },
{ "label": "±", "value": "negate", "type": "action", "span": "1" },
{ "label": "%", "value": "percent", "type": "action", "span": "1" },
{ "label": "÷", "value": "/", "type": "operator", "span": "1" },
{ "label": "7", "value": "7", "type": "number", "span": "1" },
{ "label": "8", "value": "8", "type": "number", "span": "1" },
{ "label": "9", "value": "9", "type": "number", "span": "1" },
{ "label": "×", "value": "*", "type": "operator", "span": "1" },
{ "label": "4", "value": "4", "type": "number", "span": "1" },
{ "label": "5", "value": "5", "type": "number", "span": "1" },
{ "label": "6", "value": "6", "type": "number", "span": "1" },
{ "label": "−", "value": "-", "type": "operator", "span": "1" },
{ "label": "1", "value": "1", "type": "number", "span": "1" },
{ "label": "2", "value": "2", "type": "number", "span": "1" },
{ "label": "3", "value": "3", "type": "number", "span": "1" },
{ "label": "+", "value": "+", "type": "operator", "span": "1" },
{ "label": "0", "value": "0", "type": "number", "span": "2" },
{ "label": ".", "value": ".", "type": "number", "span": "1" },
{ "label": "=", "value": "=", "type": "equal", "span": "1" }
]
}
18 changes: 18 additions & 0 deletions examples/app/calculator/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "calculator-example",
"version": "1.0.0",
"type": "module",
"private": true,
"scripts": {
"start:client": "esbuild src/index.ts --bundle --outfile=dist/index.js --format=esm --sourcemap --watch",
"start:server": "cargo run -p webui-cli -- start ./src --state ./data/state.json --plugin=fast --servedir ./dist --port 3001 --watch",
"start": "cargo xtask dev calculator"
},
"devDependencies": {
"@microsoft/fast-element": "catalog:",
"@microsoft/fast-html": "catalog:",
"esbuild": "catalog:",
"tslib": "catalog:",
"typescript": "catalog:"
}
}
66 changes: 66 additions & 0 deletions examples/app/calculator/src/calc-app/calc-app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
:host {
display: block;
width: 100%;
max-width: 380px;
}

.calculator {
background: var(--color-surface);
border-radius: var(--border-radius-xl);
padding: var(--spacing-l);
box-shadow: var(--shadow-card);
display: flex;
flex-direction: column;
gap: var(--spacing-m);
}

/* Mode tabs */
.mode-tabs {
display: flex;
gap: var(--spacing-xs);
padding: var(--spacing-xs);
background: rgba(0, 0, 0, 0.15);
border-radius: var(--border-radius-s);
}

.mode-tab {
flex: 1;
padding: var(--spacing-s) var(--spacing-m);
border: none;
border-radius: var(--border-radius-s);
background: var(--color-tab-bg);
color: var(--color-tab-text);
font-family: var(--font-family-base);
font-size: var(--font-size-s);
font-weight: 500;
cursor: pointer;
transition:
background var(--transition-fast),
color var(--transition-fast);
text-transform: capitalize;
}

.mode-tab:hover {
color: var(--color-display-text);
background: rgba(255, 255, 255, 0.05);
}

.mode-tab[data-active] {
background: var(--color-tab-active-bg);
color: var(--color-tab-active-text);
}

/* Button grid */
.button-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--spacing-s);
}

:host([columns="5"]) .button-grid {
grid-template-columns: repeat(5, 1fr);
}

:host([columns="6"]) .button-grid {
grid-template-columns: repeat(6, 1fr);
}
26 changes: 26 additions & 0 deletions examples/app/calculator/src/calc-app/calc-app.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<template shadowrootmode="open"
@button-press="{onButtonPress(e)}"
>
<div class="calculator">
<div class="mode-tabs" @click="{onModeSelect(e)}">
<button class="mode-tab" data-mode="standard" data-active>Standard</button>
<button class="mode-tab" data-mode="scientific">Scientific</button>
</div>

<calc-display
expression="{{expression}}"
value="{{displayValue}}"
></calc-display>

<div class="button-grid">
<for each="btn in buttons">
<calc-button
label="{{btn.label}}"
value="{{btn.value}}"
btn-type="{{btn.type}}"
btn-span="{{btn.span}}"
></calc-button>
</for>
</div>
</div>
</template>
142 changes: 142 additions & 0 deletions examples/app/calculator/src/calc-app/calc-app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { FASTElement, attr, observable } from '@microsoft/fast-element';
import { RenderableFASTElement } from '@microsoft/fast-html';
import type { CalcState, ButtonDef } from '../modes/engine.js';
import { createInitialState, getMode, getModeKeys } from '../modes/engine.js';
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
import '../modes/standard.js';
import '../modes/scientific.js';

interface ButtonData {
label: string;
value: string;
type: string;
span: string;
}

export class CalcApp extends RenderableFASTElement(FASTElement) {
@attr mode!: string;
@attr({ attribute: 'display-value' }) displayValue!: string;
@attr expression!: string;
@attr({ attribute: 'columns' }) gridColumns!: string;

@observable buttons!: ButtonData[];

private state!: CalcState;

async prepare(): Promise<void> {
this.mode = this.getAttribute('mode') || 'standard';
this.displayValue = this.getAttribute('display-value') || '0';
this.expression = this.getAttribute('expression') || '';
this.gridColumns = this.getAttribute('columns') || '4';
this.state = createInitialState();

// Read button data from pre-rendered DOM (mirrors todo-fast pattern)
const buttons: ButtonData[] = [];
if (this.shadowRoot) {
for (const el of this.shadowRoot.querySelectorAll('calc-button')) {
buttons.push({
label: el.getAttribute('label') || '',
value: el.getAttribute('value') || '',
type: el.getAttribute('btn-type') || '',
span: el.getAttribute('btn-span') || '1',
});
}
}

if (buttons.length > 0) {
this.buttons = buttons;
} else {
this.loadButtonsFromEngine();
}
}

private boundKeydown = this.onKeydown.bind(this);

connectedCallback(): void {
super.connectedCallback();
document.addEventListener('keydown', this.boundKeydown);
this.updateActiveModeTab();
}

disconnectedCallback(): void {
super.disconnectedCallback();
document.removeEventListener('keydown', this.boundKeydown);
}

onButtonPress(e: CustomEvent<{ value: string }>): void {
this.handleInput(e.detail.value);
}

onModeSelect(e: MouseEvent): void {
const target = e.composedPath()[0] as HTMLElement;
const btn = target.closest('[data-mode]') as HTMLElement | null;
if (!btn) return;

const newMode = btn.getAttribute('data-mode');
if (newMode && newMode !== this.mode) {
this.mode = newMode;
this.state = {
...this.state,
expression: '',
resetOnNext: true,
error: null,
};
this.expression = '';
this.loadButtonsFromEngine();
this.updateActiveModeTab();
}
}

private onKeydown(e: KeyboardEvent): void {
const keyMap: Record<string, string> = {
'0': '0', '1': '1', '2': '2', '3': '3', '4': '4',
'5': '5', '6': '6', '7': '7', '8': '8', '9': '9',
'.': '.', '+': '+', '-': '-', '*': '*', '/': '/',
Enter: '=', '=': '=', Escape: 'clear', Backspace: 'clear',
'%': 'percent',
};

const input = keyMap[e.key];
if (input) {
e.preventDefault();
this.handleInput(input);
}
}

private handleInput(input: string): void {
const engine = getMode(this.mode);
if (!engine) return;

this.state = engine.processInput(input, this.state);
this.displayValue = this.state.display;
this.expression = this.state.expression;
}

private loadButtonsFromEngine(): void {
const engine = getMode(this.mode);
if (!engine) return;

this.gridColumns = String(engine.columns);
this.buttons = engine.buttons.map((b: ButtonDef) => ({
label: b.label,
value: b.value,
type: b.type,
span: String(b.span ?? 1),
}));
}

private updateActiveModeTab(): void {
if (!this.shadowRoot) return;
for (const tab of this.shadowRoot.querySelectorAll('[data-mode]')) {
if (tab.getAttribute('data-mode') === this.mode) {
tab.setAttribute('data-active', '');
} else {
tab.removeAttribute('data-active');
}
}
}
}

CalcApp.defineAsync({
name: 'calc-app',
templateOptions: 'defer-and-hydrate',
});
Loading
Loading