Skip to content

Commit 0189cf3

Browse files
authored
Add examples sidebar to playground (#21)
* Add sidebar with example cases to playground * Add data transformation example * Add preselection with URL query parameter support * Fix expression syntax highlighting
1 parent 5b0905b commit 0189cf3

5 files changed

Lines changed: 321 additions & 16 deletions

File tree

samples/language-service-sample/app.js

Lines changed: 188 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,180 @@ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e)
4343

4444
initTheme();
4545

46+
// Examples data
47+
const exampleCases = [
48+
{
49+
id: 'math',
50+
title: 'Mathematical Expression',
51+
description: 'Basic math operations with variables',
52+
expression: '(x + y) * multiplier + sqrt(16)',
53+
context: {
54+
x: 10,
55+
y: 5,
56+
multiplier: 3
57+
}
58+
},
59+
{
60+
id: 'arrays',
61+
title: 'Working with Arrays',
62+
description: 'Array functions like sum, min, max',
63+
expression: 'sum(numbers) + max(numbers) - min(numbers)',
64+
context: {
65+
numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
66+
values: [15, 25, 35]
67+
}
68+
},
69+
{
70+
id: 'objects',
71+
title: 'Object Manipulation',
72+
description: 'Access nested object properties',
73+
expression: 'user.profile.score * level.multiplier + bonus.points',
74+
context: {
75+
user: {
76+
name: "Alice",
77+
profile: {
78+
score: 85,
79+
rank: "Gold"
80+
}
81+
},
82+
level: {
83+
current: 5,
84+
multiplier: 1.5
85+
},
86+
bonus: {
87+
points: 100,
88+
active: true
89+
}
90+
}
91+
},
92+
{
93+
id: 'map-filter',
94+
title: 'Map and Filter Functions',
95+
description: 'Transform and filter data with callbacks',
96+
expression: 'sum(map(filter(items, item => item > 3), x => x * 2))',
97+
context: {
98+
items: [1, 2, 3, 4, 5, 6, 7, 8],
99+
threshold: 3
100+
}
101+
},
102+
{
103+
id: 'complex',
104+
title: 'Complex Objects',
105+
description: 'Work with deeply nested data structures',
106+
expression: 'company.departments[0].employees.length * company.settings.bonusRate + sum(map(company.departments, d => d.budget))',
107+
context: {
108+
company: {
109+
name: "TechCorp",
110+
departments: [
111+
{
112+
name: "Engineering",
113+
budget: 500000,
114+
employees: ["John", "Jane", "Bob"]
115+
},
116+
{
117+
name: "Marketing",
118+
budget: 200000,
119+
employees: ["Alice", "Carol"]
120+
}
121+
],
122+
settings: {
123+
bonusRate: 0.15,
124+
fiscalYear: 2024
125+
}
126+
}
127+
}
128+
},
129+
{
130+
id: 'data-transform',
131+
title: 'Data Transformation',
132+
description: 'Flatten nested objects and transform rows',
133+
expression: "map(f(row) = {_id: row.rowId} + flatten(row.data, ''), $event)",
134+
context: {
135+
"$event": [
136+
{"rowId": 1, "state": "saved", "data": { "InventoryId": 1256, "Description": "Bal", "Weight": { "Unit": "g", "Amount": 120 } }},
137+
{"rowId": 2, "state": "new", "data": { "InventoryId": 2344, "Description": "Basket", "Weight": { "Unit": "g", "Amount": 300 } }},
138+
{"rowId": 3, "state": "unchanged", "data": { "InventoryId": 9362, "Description": "Wood", "Weight": { "Unit": "kg", "Amount": 18 } }}
139+
]
140+
}
141+
}
142+
];
143+
144+
// Render examples sidebar
145+
function renderExamplesSidebar() {
146+
const examplesList = document.getElementById('examplesList');
147+
if (!examplesList) return;
148+
149+
examplesList.innerHTML = exampleCases.map(example => `
150+
<button
151+
class="example-item w-full text-left p-3 rounded-lg transition-all duration-200
152+
hover:bg-white dark:hover:bg-[#2d2d2d]
153+
hover:shadow-sm hover:border-indigo-200 dark:hover:border-[#3c3c3c]
154+
border border-transparent
155+
group"
156+
data-example-id="${example.id}"
157+
>
158+
<div class="flex items-start gap-2">
159+
<div class="flex-shrink-0 w-6 h-6 rounded bg-indigo-100 dark:bg-[#3c3c3c] flex items-center justify-center mt-0.5">
160+
<svg class="w-3.5 h-3.5 text-indigo-600 dark:text-[#569cd6]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
161+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
162+
</svg>
163+
</div>
164+
<div class="flex-1 min-w-0">
165+
<p class="text-sm font-medium text-gray-800 dark:text-[#cccccc] truncate group-hover:text-indigo-600 dark:group-hover:text-[#569cd6]">
166+
${example.title}
167+
</p>
168+
<p class="text-xs text-gray-500 dark:text-[#808080] mt-0.5 line-clamp-2">
169+
${example.description}
170+
</p>
171+
</div>
172+
</div>
173+
</button>
174+
`).join('');
175+
176+
// Add click handlers
177+
examplesList.querySelectorAll('.example-item').forEach(button => {
178+
button.addEventListener('click', () => {
179+
const exampleId = button.dataset.exampleId;
180+
const example = exampleCases.find(e => e.id === exampleId);
181+
if (example) {
182+
loadExample(example);
183+
}
184+
});
185+
});
186+
}
187+
188+
// Load example into editors
189+
function loadExample(example) {
190+
if (typeof expressionEditor !== 'undefined' && expressionEditor) {
191+
expressionEditor.getModel().setValue(example.expression);
192+
}
193+
if (typeof contextEditor !== 'undefined' && contextEditor) {
194+
contextEditor.getModel().setValue(JSON.stringify(example.context, null, 2));
195+
}
196+
}
197+
198+
// Initialize sidebar
199+
renderExamplesSidebar();
200+
201+
// Get example ID from URL query parameter
202+
function getExampleFromUrl() {
203+
const params = new URLSearchParams(window.location.search);
204+
return params.get('example');
205+
}
206+
207+
// Load example from URL if present (called after Monaco initializes)
208+
function loadExampleFromUrl() {
209+
const exampleId = getExampleFromUrl();
210+
if (exampleId) {
211+
const example = exampleCases.find(e => e.id === exampleId);
212+
if (example) {
213+
loadExample(example);
214+
return true;
215+
}
216+
}
217+
return false;
218+
}
219+
46220
// Split pane resizing
47221
(function() {
48222
const resizer = document.getElementById('resizer');
@@ -281,24 +455,22 @@ require(['vs/editor/editor.main'], function () {
281455
});
282456

283457
// Syntax highlighting
458+
let highlightDecorations = [];
459+
284460
function applyHighlighting() {
285461
const doc = makeTextDocument(expressionModel);
286462
const tokens = ls.getHighlighting(doc);
287-
const rangesByClass = new Map();
288-
for (const t of tokens) {
463+
const decorations = tokens.map(t => {
289464
const start = expressionModel.getPositionAt(t.start);
290465
const end = expressionModel.getPositionAt(t.end);
291-
const range = new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column);
292-
const cls = 'tok-' + t.type;
293-
if (!rangesByClass.has(cls)) rangesByClass.set(cls, []);
294-
rangesByClass.get(cls).push({range, options: {inlineClassName: cls}});
295-
}
296-
297-
window.__exprEvalDecos = window.__exprEvalDecos || {};
298-
for (const [cls, decos] of rangesByClass.entries()) {
299-
const prev = window.__exprEvalDecos[cls] || [];
300-
window.__exprEvalDecos[cls] = expressionEditor.deltaDecorations(prev, decos);
301-
}
466+
return {
467+
range: new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column),
468+
options: { inlineClassName: 'tok-' + t.type }
469+
};
470+
});
471+
472+
// deltaDecorations replaces old decorations with new ones atomically
473+
highlightDecorations = expressionEditor.deltaDecorations(highlightDecorations, decorations);
302474
}
303475

304476
// Result display functions
@@ -443,6 +615,9 @@ require(['vs/editor/editor.main'], function () {
443615
applyHighlighting();
444616
evaluate();
445617

618+
// Load example from URL query parameter if present
619+
loadExampleFromUrl();
620+
446621
// Event listeners for changes
447622
expressionModel.onDidChangeContent(() => {
448623
applyHighlighting();

samples/language-service-sample/index.html

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,21 @@ <h1 class="text-xl font-bold bg-gradient-to-r from-indigo-600 to-purple-600 dark
5353

5454
<!-- Main Content -->
5555
<main id="mainContent">
56+
<!-- Examples Sidebar -->
57+
<aside id="examplesSidebar" class="w-64 bg-gray-50 dark:bg-[#252526] border-r border-gray-200 dark:border-[#3c3c3c] flex-shrink-0 flex flex-col">
58+
<div class="h-10 bg-gray-100 dark:bg-[#333333] border-b border-gray-200 dark:border-[#3c3c3c] flex items-center px-4">
59+
<svg class="w-4 h-4 text-indigo-500 dark:text-[#569cd6] mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
60+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
61+
</svg>
62+
<span class="text-sm font-medium text-gray-700 dark:text-[#cccccc]">Examples</span>
63+
</div>
64+
<div id="examplesList" class="flex-1 overflow-y-auto p-2 space-y-1">
65+
<!-- Examples will be populated by JavaScript -->
66+
</div>
67+
</aside>
68+
5669
<!-- Expression Editor Pane -->
57-
<div id="leftPane" class="pane bg-white dark:bg-[#1e1e1e]" style="width: 60%;">
70+
<div id="leftPane" class="pane bg-white dark:bg-[#1e1e1e]" style="width: calc(60% - var(--sidebar-width));">
5871
<div class="pane-header h-10 bg-gray-100 dark:bg-[#252526] border-b border-gray-200 dark:border-[#3c3c3c] flex items-center px-4">
5972
<span class="text-sm font-medium text-gray-600 dark:text-[#cccccc]">Expression</span>
6073
<span class="ml-2 text-xs text-gray-400 dark:text-[#808080]">Enter your expr-eval expression</span>

samples/language-service-sample/serve-sample.cjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ function send(res, status, body, headers = {}) {
2323

2424
const server = http.createServer((req, res) => {
2525
let urlPath = decodeURIComponent(req.url || '/');
26+
27+
// Strip query string
28+
const queryIndex = urlPath.indexOf('?');
29+
if (queryIndex !== -1) {
30+
urlPath = urlPath.substring(0, queryIndex);
31+
}
2632

2733
if (urlPath === '/' || urlPath === '/index.html') {
2834
urlPath = 'samples/language-service-sample/index.html';

samples/language-service-sample/styles.css

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
/* Base styles */
2+
:root {
3+
--sidebar-width: 256px;
4+
}
5+
26
html, body {
37
height: 100%;
48
margin: 0;
@@ -79,6 +83,7 @@ html, body {
7983
.tok-function { color: #795e26 !important; }
8084
.tok-punctuation { color: #383a42 !important; }
8185
.tok-name { color: #001080 !important; }
86+
.tok-constant { color: #0070c1 !important; }
8287

8388
/* Dark theme token colors */
8489
.dark .tok-number { color: #b5cea8 !important; }
@@ -88,6 +93,7 @@ html, body {
8893
.dark .tok-function { color: #dcdcaa !important; }
8994
.dark .tok-punctuation { color: #d4d4d4 !important; }
9095
.dark .tok-name { color: #9cdcfe !important; }
96+
.dark .tok-constant { color: #4fc1ff !important; }
9197

9298
/* Error styling animation */
9399
@keyframes shake {
@@ -99,3 +105,108 @@ html, body {
99105
.error-shake {
100106
animation: shake 0.3s ease-in-out;
101107
}
108+
109+
/* Examples sidebar styles */
110+
#examplesSidebar {
111+
min-width: 200px;
112+
width: var(--sidebar-width);
113+
display: flex;
114+
flex-direction: column;
115+
flex-shrink: 0;
116+
background-color: #f9fafb;
117+
border-right: 1px solid #e5e7eb;
118+
}
119+
120+
.dark #examplesSidebar {
121+
background-color: #252526;
122+
border-right-color: #3c3c3c;
123+
}
124+
125+
#examplesSidebar > div:first-child {
126+
height: 40px;
127+
background-color: #f3f4f6;
128+
border-bottom: 1px solid #e5e7eb;
129+
display: flex;
130+
align-items: center;
131+
padding: 0 16px;
132+
flex-shrink: 0;
133+
}
134+
135+
.dark #examplesSidebar > div:first-child {
136+
background-color: #333333;
137+
border-bottom-color: #3c3c3c;
138+
}
139+
140+
#examplesList {
141+
flex: 1;
142+
overflow-y: auto;
143+
padding: 8px;
144+
}
145+
146+
.example-item {
147+
width: 100%;
148+
text-align: left;
149+
padding: 12px;
150+
border-radius: 8px;
151+
border: 1px solid transparent;
152+
background: transparent;
153+
cursor: pointer;
154+
margin-bottom: 4px;
155+
transition: all 0.2s ease;
156+
}
157+
158+
.example-item:hover {
159+
background-color: #ffffff;
160+
border-color: #e0e7ff;
161+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
162+
}
163+
164+
.dark .example-item:hover {
165+
background-color: #2d2d2d;
166+
border-color: #3c3c3c;
167+
}
168+
169+
.example-item:focus {
170+
outline: none;
171+
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.3);
172+
}
173+
174+
.example-item:active {
175+
transform: scale(0.98);
176+
}
177+
178+
.example-item p:first-child {
179+
font-size: 14px;
180+
font-weight: 500;
181+
color: #1f2937;
182+
margin: 0;
183+
}
184+
185+
.dark .example-item p:first-child {
186+
color: #cccccc;
187+
}
188+
189+
.example-item:hover p:first-child {
190+
color: #4f46e5;
191+
}
192+
193+
.dark .example-item:hover p:first-child {
194+
color: #569cd6;
195+
}
196+
197+
.example-item p:last-child {
198+
font-size: 12px;
199+
color: #6b7280;
200+
margin: 4px 0 0 0;
201+
}
202+
203+
.dark .example-item p:last-child {
204+
color: #808080;
205+
}
206+
207+
.line-clamp-2 {
208+
display: -webkit-box;
209+
-webkit-line-clamp: 2;
210+
-webkit-box-orient: vertical;
211+
overflow: hidden;
212+
}

0 commit comments

Comments
 (0)