Created
June 13, 2025 19:04
-
-
Save habdelra/6d8c99d81f05e2ec9147c182a8f4a23c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { eq, not } from '@cardstack/boxel-ui/helpers'; | |
| // ═══ [EDIT TRACKING: ON] Mark all changes with ⁽ⁿ⁾ ═══ | |
| import { | |
| CardDef, | |
| field, | |
| contains, | |
| Component, | |
| } from 'https://cardstack.com/base/card-api'; // ⁽¹⁾ Core imports | |
| import StringField from 'https://cardstack.com/base/string'; | |
| import NumberField from 'https://cardstack.com/base/number'; | |
| import CalculatorIcon from '@cardstack/boxel-icons/calculator'; // ⁽²⁾ Icon import | |
| import { Button } from '@cardstack/boxel-ui/components'; | |
| import { fn, concat } from '@ember/helper'; | |
| import { on } from '@ember/modifier'; | |
| import { action } from '@ember/object'; | |
| import { tracked } from '@glimmer/tracking'; | |
| class Isolated extends Component<typeof GraphingCalculator> { | |
| // ⁽⁷⁾ Main calculator interface | |
| @tracked functionInput = this.args.model?.currentFunction ?? 'x^2'; | |
| @tracked xMinInput = String(this.args.model?.xMin ?? -10); | |
| @tracked xMaxInput = String(this.args.model?.xMax ?? 10); | |
| @tracked yMinInput = String(this.args.model?.yMin ?? -10); | |
| @tracked yMaxInput = String(this.args.model?.yMax ?? 10); | |
| @tracked gridSpacingInput = String(this.args.model?.gridSpacing ?? 1); | |
| @tracked calculationResult = ''; | |
| @tracked expressionInput = ''; | |
| @tracked isGraphing = false; | |
| @tracked errorMessage = ''; | |
| // ⁽⁸⁾ Function parsing and evaluation | |
| evaluateFunction(x: number): number | null { | |
| try { | |
| let expr = this.functionInput | |
| .replace(/\^/g, '**') | |
| .replace(/sin/g, 'Math.sin') | |
| .replace(/cos/g, 'Math.cos') | |
| .replace(/tan/g, 'Math.tan') | |
| .replace(/log/g, 'Math.log10') | |
| .replace(/ln/g, 'Math.log') | |
| .replace(/sqrt/g, 'Math.sqrt') | |
| .replace(/abs/g, 'Math.abs') | |
| .replace(/pi/g, 'Math.PI') | |
| .replace(/e(?![a-zA-Z])/g, 'Math.E') | |
| .replace(/\bx\b/g, x.toString()); | |
| const result = eval(expr); | |
| return isFinite(result) ? result : null; | |
| } catch (e) { | |
| return null; | |
| } | |
| } | |
| // ⁽⁹⁾ Graph plotting calculations | |
| get graphPoints(): Array<{ x: number; y: number }> { | |
| const points = []; | |
| const steps = 400; | |
| const xMin = parseFloat(this.xMinInput); | |
| const xMax = parseFloat(this.xMaxInput); | |
| const yMin = parseFloat(this.yMinInput); | |
| const yMax = parseFloat(this.yMaxInput); | |
| const stepSize = (xMax - xMin) / steps; | |
| for (let i = 0; i <= steps; i++) { | |
| const x = xMin + i * stepSize; | |
| const y = this.evaluateFunction(x); | |
| if (y !== null && y >= yMin && y <= yMax) { | |
| points.push({ x, y }); | |
| } | |
| } | |
| return points; | |
| } | |
| // ⁽¹⁰⁾ SVG coordinate transformations | |
| scaleX(x: number): number { | |
| const width = 600; | |
| const xMin = parseFloat(this.xMinInput); | |
| const xMax = parseFloat(this.xMaxInput); | |
| return ((x - xMin) / (xMax - xMin)) * width; | |
| } | |
| scaleY(y: number): number { | |
| const height = 400; | |
| const yMin = parseFloat(this.yMinInput); | |
| const yMax = parseFloat(this.yMaxInput); | |
| return height - ((y - yMin) / (yMax - yMin)) * height; | |
| } | |
| // ⁽¹¹⁾ Grid line calculations | |
| get gridLines(): Array<{ | |
| type: 'vertical' | 'horizontal'; | |
| position: number; | |
| value: number; | |
| }> { | |
| const lines = []; | |
| const spacing = this.gridSpacingInput; | |
| // Vertical grid lines | |
| for ( | |
| let x = Math.ceil(this.xMinInput / spacing) * spacing; | |
| x <= this.xMaxInput; | |
| x += spacing | |
| ) { | |
| if (x >= this.xMinInput && x <= this.xMaxInput) { | |
| lines.push({ type: 'vertical', position: this.scaleX(x), value: x }); | |
| } | |
| } | |
| // Horizontal grid lines | |
| for ( | |
| let y = Math.ceil(this.yMinInput / spacing) * spacing; | |
| y <= this.yMaxInput; | |
| y += spacing | |
| ) { | |
| if (y >= this.yMinInput && y <= this.yMaxInput) { | |
| lines.push({ type: 'horizontal', position: this.scaleY(y), value: y }); | |
| } | |
| } | |
| return lines; | |
| } | |
| // ⁽¹²⁾ Path string for SVG | |
| get graphPath(): string { | |
| const points = this.graphPoints; | |
| if (points.length === 0) return ''; | |
| let path = `M ${this.scaleX(points[0].x)} ${this.scaleY(points[0].y)}`; | |
| for (let i = 1; i < points.length; i++) { | |
| const x = this.scaleX(points[i].x); | |
| const y = this.scaleY(points[i].y); | |
| path += ` L ${x} ${y}`; | |
| } | |
| return path; | |
| } | |
| // ⁽¹³⁾ Action handlers | |
| updateFunction = (event: Event) => { | |
| const target = event.target as HTMLInputElement; | |
| this.functionInput = target.value; | |
| this.errorMessage = ''; | |
| this.updateModel(); | |
| }; | |
| updateXMin = (event: Event) => { | |
| const target = event.target as HTMLInputElement; | |
| this.xMinInput = String(parseFloat(target.value) || -10); | |
| }; | |
| updateXMax = (event: Event) => { | |
| const target = event.target as HTMLInputElement; | |
| this.xMaxInput = String(parseFloat(target.value) || 10); | |
| }; | |
| updateYMin = (event: Event) => { | |
| const target = event.target as HTMLInputElement; | |
| this.yMinInput = String(parseFloat(target.value) || -10); | |
| }; | |
| updateYMax = (event: Event) => { | |
| const target = event.target as HTMLInputElement; | |
| this.yMaxInput = String(parseFloat(target.value) || 10); | |
| }; | |
| updateGridSpacing = (event: Event) => { | |
| const target = event.target as HTMLInputElement; | |
| this.gridSpacingInput = String(parseFloat(target.value) || 1); | |
| }; | |
| updateExpression = (event: Event) => { | |
| const target = event.target as HTMLInputElement; | |
| this.expressionInput = target.value; | |
| }; | |
| updateViewport = () => { | |
| this.updateModel(); | |
| }; | |
| calculateExpression = () => { | |
| if (!this.expressionInput.trim()) return; | |
| try { | |
| const expr = this.expressionInput | |
| .replace(/\^/g, '**') | |
| .replace(/sin/g, 'Math.sin') | |
| .replace(/cos/g, 'Math.cos') | |
| .replace(/tan/g, 'Math.tan') | |
| .replace(/log/g, 'Math.log10') | |
| .replace(/ln/g, 'Math.log') | |
| .replace(/sqrt/g, 'Math.sqrt') | |
| .replace(/pi/g, 'Math.PI') | |
| .replace(/e(?![a-zA-Z])/g, 'Math.E'); | |
| const result = eval(expr); | |
| this.calculationResult = isFinite(result) | |
| ? result.toString() | |
| : 'Invalid result'; | |
| } catch (e) { | |
| this.calculationResult = 'Error: Invalid expression'; | |
| } | |
| }; | |
| zoomIn = () => { | |
| const centerX = (this.xMaxInput + this.xMinInput) / 2; | |
| const centerY = (this.yMaxInput + this.yMinInput) / 2; | |
| const rangeX = (this.xMaxInput - this.xMinInput) * 0.8; | |
| const rangeY = (this.yMaxInput - this.yMinInput) * 0.8; | |
| this.xMinInput = centerX - rangeX / 2; | |
| this.xMaxInput = centerX + rangeX / 2; | |
| this.yMinInput = centerY - rangeY / 2; | |
| this.yMaxInput = centerY + rangeY / 2; | |
| this.updateViewport(); | |
| }; | |
| zoomOut = () => { | |
| const centerX = (this.xMaxInput + this.xMinInput) / 2; | |
| const centerY = (this.yMaxInput + this.yMinInput) / 2; | |
| const rangeX = (this.xMaxInput - this.xMinInput) * 1.25; | |
| const rangeY = (this.yMaxInput - this.yMinInput) * 1.25; | |
| this.xMinInput = centerX - rangeX / 2; | |
| this.xMaxInput = centerX + rangeX / 2; | |
| this.yMinInput = centerY - rangeY / 2; | |
| this.yMaxInput = centerY + rangeY / 2; | |
| this.updateViewport(); | |
| }; | |
| resetView = () => { | |
| this.xMinInput = -10; | |
| this.xMaxInput = 10; | |
| this.yMinInput = -10; | |
| this.yMaxInput = 10; | |
| this.gridSpacingInput = 1; | |
| this.updateViewport(); | |
| }; | |
| insertFunction = (func: string) => { | |
| this.functionInput = func; | |
| this.updateModel(); | |
| }; | |
| updateModel() { | |
| if (this.args.model) { | |
| this.args.model.currentFunction = this.functionInput; | |
| this.args.model.xMin = this.xMinInput; | |
| this.args.model.xMax = this.xMaxInput; | |
| this.args.model.yMin = this.yMinInput; | |
| this.args.model.yMax = this.yMaxInput; | |
| this.args.model.gridSpacing = this.gridSpacingInput; | |
| } | |
| } | |
| <template> | |
| <!-- ⁽¹⁴⁾ Calculator interface layout --> | |
| <div class='calculator-stage'> | |
| <div class='calculator-container'> | |
| <header class='calculator-header'> | |
| <h1> | |
| <svg | |
| class='calc-icon' | |
| viewBox='0 0 24 24' | |
| fill='none' | |
| stroke='currentColor' | |
| stroke-width='2' | |
| > | |
| <rect x='4' y='4' width='16' height='16' rx='2' /> | |
| <rect x='9' y='9' width='1' height='1' /> | |
| <rect x='14' y='9' width='1' height='1' /> | |
| <rect x='9' y='14' width='1' height='1' /> | |
| <rect x='14' y='14' width='1' height='1' /> | |
| </svg> | |
| Advanced Graphing Calculator | |
| </h1> | |
| </header> | |
| <div class='calculator-layout'> | |
| <!-- ⁽¹⁵⁾ Function input and controls --> | |
| <div class='controls-panel'> | |
| <section class='function-section'> | |
| <label class='input-label'>Function: y =</label> | |
| <input | |
| type='text' | |
| class='function-input' | |
| value={{this.functionInput}} | |
| placeholder='Enter function (e.g., x^2, sin(x), 2*x+1)' | |
| {{on 'input' this.updateFunction}} | |
| /> | |
| <div class='function-buttons'> | |
| <Button | |
| class='func-btn' | |
| {{on 'click' (fn this.insertFunction 'x^2')}} | |
| >x²</Button> | |
| <Button | |
| class='func-btn' | |
| {{on 'click' (fn this.insertFunction 'x^3')}} | |
| >x³</Button> | |
| <Button | |
| class='func-btn' | |
| {{on 'click' (fn this.insertFunction 'sin(x)')}} | |
| >sin(x)</Button> | |
| <Button | |
| class='func-btn' | |
| {{on 'click' (fn this.insertFunction 'cos(x)')}} | |
| >cos(x)</Button> | |
| <Button | |
| class='func-btn' | |
| {{on 'click' (fn this.insertFunction 'tan(x)')}} | |
| >tan(x)</Button> | |
| <Button | |
| class='func-btn' | |
| {{on 'click' (fn this.insertFunction 'sqrt(x)')}} | |
| >√x</Button> | |
| <Button | |
| class='func-btn' | |
| {{on 'click' (fn this.insertFunction 'abs(x)')}} | |
| >|x|</Button> | |
| <Button | |
| class='func-btn' | |
| {{on 'click' (fn this.insertFunction 'log(x)')}} | |
| >log(x)</Button> | |
| </div> | |
| </section> | |
| <!-- ⁽¹⁶⁾ Viewport controls --> | |
| <section class='viewport-section'> | |
| <h3>Viewport</h3> | |
| <div class='viewport-controls'> | |
| <div class='range-control'> | |
| <label>X: </label> | |
| <input | |
| type='number' | |
| class='range-input' | |
| value={{this.xMinInput}} | |
| {{on 'input' this.updateXMin}} | |
| {{on 'blur' this.updateViewport}} | |
| /> | |
| <span>to</span> | |
| <input | |
| type='number' | |
| class='range-input' | |
| value={{this.xMaxInput}} | |
| {{on 'input' this.updateXMax}} | |
| {{on 'blur' this.updateViewport}} | |
| /> | |
| </div> | |
| <div class='range-control'> | |
| <label>Y: </label> | |
| <input | |
| type='number' | |
| class='range-input' | |
| value={{this.yMinInput}} | |
| {{on 'input' this.updateYMin}} | |
| {{on 'blur' this.updateViewport}} | |
| /> | |
| <span>to</span> | |
| <input | |
| type='number' | |
| class='range-input' | |
| value={{this.yMaxInput}} | |
| {{on 'input' this.updateYMax}} | |
| {{on 'blur' this.updateViewport}} | |
| /> | |
| </div> | |
| <div class='range-control'> | |
| <label>Grid: </label> | |
| <input | |
| type='number' | |
| class='range-input' | |
| value={{this.gridSpacingInput}} | |
| {{on 'input' this.updateGridSpacing}} | |
| {{on 'blur' this.updateViewport}} | |
| /> | |
| </div> | |
| </div> | |
| <div class='zoom-controls'> | |
| <Button class='zoom-btn' {{on 'click' this.zoomIn}}>Zoom In</Button> | |
| <Button class='zoom-btn' {{on 'click' this.zoomOut}}>Zoom Out</Button> | |
| <Button | |
| class='zoom-btn reset' | |
| {{on 'click' this.resetView}} | |
| >Reset</Button> | |
| </div> | |
| </section> | |
| <!-- ⁽¹⁷⁾ Calculator --> | |
| <section class='calc-section'> | |
| <h3>Calculator</h3> | |
| <div class='calc-input-group'> | |
| <input | |
| type='text' | |
| class='calc-input' | |
| value={{this.expressionInput}} | |
| placeholder='Enter expression to calculate' | |
| {{on 'input' this.updateExpression}} | |
| /> | |
| <Button | |
| class='calc-btn' | |
| {{on 'click' this.calculateExpression}} | |
| >Calculate</Button> | |
| </div> | |
| {{#if this.calculationResult}} | |
| <div class='calc-result'>= {{this.calculationResult}}</div> | |
| {{/if}} | |
| </section> | |
| </div> | |
| <!-- ⁽¹⁸⁾ Graph display --> | |
| <div class='graph-panel'> | |
| <div class='graph-container'> | |
| <svg class='graph-svg' viewBox='0 0 600 400'> | |
| <!-- Grid lines --> | |
| <defs> | |
| <pattern | |
| id='grid' | |
| width='20' | |
| height='20' | |
| patternUnits='userSpaceOnUse' | |
| > | |
| <path | |
| d='M 20 0 L 0 0 0 20' | |
| fill='none' | |
| stroke='#e5e7eb' | |
| stroke-width='1' | |
| /> | |
| </pattern> | |
| </defs> | |
| <!-- Grid background --> | |
| <rect width='600' height='400' fill='url(#grid)' /> | |
| <!-- Grid lines with labels --> | |
| {{#each this.gridLines as |line|}} | |
| {{#if (eq line.type 'vertical')}} | |
| <line | |
| x1={{line.position}} | |
| y1='0' | |
| x2={{line.position}} | |
| y2='400' | |
| stroke='#d1d5db' | |
| stroke-width='1' | |
| /> | |
| {{#if (not (eq line.value 0))}} | |
| <text | |
| x={{line.position}} | |
| y='395' | |
| text-anchor='middle' | |
| font-size='10' | |
| fill='#6b7280' | |
| >{{line.value}}</text> | |
| {{/if}} | |
| {{else}} | |
| <line | |
| x1='0' | |
| y1={{line.position}} | |
| x2='600' | |
| y2={{line.position}} | |
| stroke='#d1d5db' | |
| stroke-width='1' | |
| /> | |
| {{#if (not (eq line.value 0))}} | |
| <text | |
| x='5' | |
| y={{line.position}} | |
| text-anchor='start' | |
| dominant-baseline='middle' | |
| font-size='10' | |
| fill='#6b7280' | |
| >{{line.value}}</text> | |
| {{/if}} | |
| {{/if}} | |
| {{/each}} | |
| <!-- Axes --> | |
| <line | |
| x1={{this.scaleX 0}} | |
| y1='0' | |
| x2={{this.scaleX 0}} | |
| y2='400' | |
| stroke='#374151' | |
| stroke-width='2' | |
| /> | |
| <line | |
| x1='0' | |
| y1={{this.scaleY 0}} | |
| x2='600' | |
| y2={{this.scaleY 0}} | |
| stroke='#374151' | |
| stroke-width='2' | |
| /> | |
| <!-- Function graph --> | |
| {{#if this.graphPath}} | |
| <path | |
| d={{this.graphPath}} | |
| stroke='#3b82f6' | |
| stroke-width='2' | |
| fill='none' | |
| /> | |
| {{/if}} | |
| </svg> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <style scoped> | |
| /* ⁽¹⁹⁾ Professional calculator styling */ | |
| .calculator-stage { | |
| width: 100%; | |
| height: 100vh; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| padding: 1rem; | |
| font-family: | |
| 'Inter', | |
| -apple-system, | |
| sans-serif; | |
| } | |
| .calculator-container { | |
| background: white; | |
| border-radius: 16px; | |
| box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); | |
| width: 100%; | |
| max-width: 1200px; | |
| height: 90vh; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .calculator-header { | |
| background: linear-gradient(135deg, #1e40af, #3730a3); | |
| color: white; | |
| padding: 1rem 2rem; | |
| border-radius: 16px 16px 0 0; | |
| } | |
| .calculator-header h1 { | |
| margin: 0; | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| } | |
| .calc-icon { | |
| width: 2rem; | |
| height: 2rem; | |
| } | |
| .calculator-layout { | |
| display: grid; | |
| grid-template-columns: 350px 1fr; | |
| height: 100%; | |
| } | |
| .controls-panel { | |
| background: #f8fafc; | |
| border-right: 1px solid #e2e8f0; | |
| padding: 1.5rem; | |
| overflow-y: auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1.5rem; | |
| } | |
| .function-section h3, | |
| .viewport-section h3, | |
| .calc-section h3 { | |
| margin: 0 0 1rem 0; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| color: #1e293b; | |
| } | |
| .input-label { | |
| display: block; | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| color: #374151; | |
| margin-bottom: 0.5rem; | |
| } | |
| .function-input { | |
| width: 100%; | |
| padding: 0.75rem; | |
| border: 2px solid #d1d5db; | |
| border-radius: 8px; | |
| font-size: 0.875rem; | |
| font-family: 'Monaco', 'Menlo', monospace; | |
| transition: border-color 0.2s ease; | |
| } | |
| .function-input:focus { | |
| outline: none; | |
| border-color: #3b82f6; | |
| box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); | |
| } | |
| .function-buttons { | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 0.5rem; | |
| margin-top: 1rem; | |
| } | |
| /* ⁽²⁰⁾ Button styling */ | |
| .func-btn { | |
| background: #e2e8f0; | |
| border: 1px solid #cbd5e1; | |
| border-radius: 6px; | |
| padding: 0.5rem; | |
| font-size: 0.75rem; | |
| font-weight: 500; | |
| color: #475569; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .func-btn:hover { | |
| background: #cbd5e1; | |
| border-color: #94a3b8; | |
| transform: translateY(-1px); | |
| } | |
| .viewport-controls { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.75rem; | |
| } | |
| .range-control { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| font-size: 0.875rem; | |
| } | |
| .range-control label { | |
| min-width: 1.5rem; | |
| font-weight: 500; | |
| color: #374151; | |
| } | |
| .range-input { | |
| width: 4rem; | |
| padding: 0.25rem 0.5rem; | |
| border: 1px solid #d1d5db; | |
| border-radius: 4px; | |
| font-size: 0.75rem; | |
| text-align: center; | |
| } | |
| .zoom-controls { | |
| display: flex; | |
| gap: 0.5rem; | |
| margin-top: 1rem; | |
| } | |
| .zoom-btn { | |
| background: #3b82f6; | |
| color: white; | |
| border: none; | |
| border-radius: 6px; | |
| padding: 0.5rem 0.75rem; | |
| font-size: 0.75rem; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| flex: 1; | |
| } | |
| .zoom-btn:hover { | |
| background: #2563eb; | |
| transform: translateY(-1px); | |
| } | |
| .zoom-btn.reset { | |
| background: #ef4444; | |
| } | |
| .zoom-btn.reset:hover { | |
| background: #dc2626; | |
| } | |
| .calc-input-group { | |
| display: flex; | |
| gap: 0.5rem; | |
| } | |
| .calc-input { | |
| flex: 1; | |
| padding: 0.5rem; | |
| border: 1px solid #d1d5db; | |
| border-radius: 6px; | |
| font-size: 0.875rem; | |
| font-family: 'Monaco', 'Menlo', monospace; | |
| } | |
| .calc-btn { | |
| background: #059669; | |
| color: white; | |
| border: none; | |
| border-radius: 6px; | |
| padding: 0.5rem 1rem; | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .calc-btn:hover { | |
| background: #047857; | |
| } | |
| .calc-result { | |
| margin-top: 0.75rem; | |
| padding: 0.75rem; | |
| background: #ecfdf5; | |
| border: 1px solid #a7f3d0; | |
| border-radius: 6px; | |
| font-family: 'Monaco', 'Menlo', monospace; | |
| font-weight: 600; | |
| color: #065f46; | |
| } | |
| .graph-panel { | |
| background: white; | |
| padding: 1.5rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .graph-container { | |
| width: 100%; | |
| height: 100%; | |
| max-width: 600px; | |
| max-height: 400px; | |
| border: 2px solid #e5e7eb; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| background: white; | |
| } | |
| .graph-svg { | |
| width: 100%; | |
| height: 100%; | |
| display: block; | |
| } | |
| /* ⁽²¹⁾ Responsive design */ | |
| @media (max-width: 1000px) { | |
| .calculator-layout { | |
| grid-template-columns: 1fr; | |
| grid-template-rows: auto 1fr; | |
| } | |
| .controls-panel { | |
| border-right: none; | |
| border-bottom: 1px solid #e2e8f0; | |
| overflow-y: visible; | |
| } | |
| } | |
| @media (max-width: 600px) { | |
| .function-buttons { | |
| grid-template-columns: repeat(2, 1fr); | |
| } | |
| .zoom-controls { | |
| flex-direction: column; | |
| } | |
| } | |
| </style> | |
| </template> | |
| } | |
| class Embedded extends Component<typeof GraphingCalculator> { | |
| // ⁽²²⁾ Compact view | |
| <template> | |
| <div class='calculator-card'> | |
| <div class='card-header'> | |
| <svg | |
| class='card-icon' | |
| viewBox='0 0 24 24' | |
| fill='none' | |
| stroke='currentColor' | |
| stroke-width='2' | |
| > | |
| <rect x='4' y='4' width='16' height='16' rx='2' /> | |
| <rect x='9' y='9' width='1' height='1' /> | |
| <rect x='14' y='9' width='1' height='1' /> | |
| </svg> | |
| <h3>{{if | |
| @model.currentFunction | |
| (concat 'f(x) = ' @model.currentFunction) | |
| 'Graphing Calculator' | |
| }}</h3> | |
| </div> | |
| <div class='mini-graph'> | |
| <svg viewBox='0 0 200 120'> | |
| <rect width='200' height='120' fill='#f8fafc' stroke='#e2e8f0' /> | |
| <line x1='100' y1='0' x2='100' y2='120' stroke='#d1d5db' /> | |
| <line x1='0' y1='60' x2='200' y2='60' stroke='#d1d5db' /> | |
| <path | |
| d='M 0 120 Q 50 60 100 60 T 200 0' | |
| stroke='#3b82f6' | |
| stroke-width='2' | |
| fill='none' | |
| /> | |
| </svg> | |
| </div> | |
| <div class='card-info'> | |
| <span>Range: | |
| {{if | |
| @model.xMin | |
| (concat @model.xMin ' to ' @model.xMax) | |
| 'Default view' | |
| }}</span> | |
| </div> | |
| </div> | |
| <style scoped> | |
| /* ⁽²³⁾ Card styles */ | |
| .calculator-card { | |
| background: white; | |
| border-radius: 8px; | |
| padding: 1rem; | |
| font-size: 0.8125rem; | |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | |
| } | |
| .card-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| margin-bottom: 0.75rem; | |
| } | |
| .card-icon { | |
| width: 1.25rem; | |
| height: 1.25rem; | |
| color: #3b82f6; | |
| } | |
| .card-header h3 { | |
| margin: 0; | |
| font-size: 0.875rem; | |
| font-weight: 600; | |
| color: #1e293b; | |
| } | |
| .mini-graph { | |
| margin-bottom: 0.75rem; | |
| } | |
| .mini-graph svg { | |
| width: 100%; | |
| height: 80px; | |
| border-radius: 4px; | |
| } | |
| .card-info { | |
| font-size: 0.75rem; | |
| color: #6b7280; | |
| text-align: center; | |
| } | |
| </style> | |
| </template> | |
| } | |
| export class GraphingCalculator extends CardDef { | |
| // ⁽³⁾ Calculator definition | |
| static displayName = 'Graphing Calculator'; | |
| static icon = CalculatorIcon; | |
| static prefersWideFormat = true; | |
| @field currentFunction = contains(StringField); // ⁽⁴⁾ Function expression | |
| @field xMin = contains(NumberField); // ⁽⁵⁾ Viewport bounds | |
| @field xMax = contains(NumberField); | |
| @field yMin = contains(NumberField); | |
| @field yMax = contains(NumberField); | |
| @field gridSpacing = contains(NumberField); | |
| // ⁽⁶⁾ Computed title from current function | |
| @field title = contains(StringField, { | |
| computeVia: function (this: GraphingCalculator) { | |
| try { | |
| const func = this.currentFunction ?? 'y = x'; | |
| return `Graphing Calculator - ${func}`; | |
| } catch (e) { | |
| console.error('GraphingCalculator: Error computing title', e); | |
| return 'Graphing Calculator'; | |
| } | |
| }, | |
| }); | |
| static isolated = Isolated; | |
| static embedded = Embedded; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment