kempo-ui 0.4.8 → 0.4.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +24 -0
- package/CHANGELOG.md +19 -0
- package/docs/components/timestamp.html +95 -2
- package/docs-src/components/timestamp.page.html +95 -2
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/tasks/_template.md +39 -0
- package/tasks/released/0001-identify-timestamp-formats/documentation-page-screenshot.png +0 -0
- package/tasks/released/0001-identify-timestamp-formats/format-examples-and-notes.png +0 -0
- package/tasks/released/0001-identify-timestamp-formats/supported-formats-section.png +0 -0
- package/tasks/released/0001-identify-timestamp-formats/validation-snapshot.txt +236 -0
- package/tasks/released/0001-identify-timestamp-formats.md +114 -0
- package/tasks/released/0002-markdown-editor-unit-tests.md +139 -0
- package/tests/components/MarkdownEditor.browser-test.js +462 -0
- package/tests/components/Timestamp.browser-test.js +4 -4
- package/.github/ISSUE_TEMPLATE/change-request.md +0 -23
- package/.github/skills/get-icon/SKILL.md +0 -144
- package/.github/skills/highlight-code/SKILL.md +0 -91
- package/.github/skills/new-component/SKILL.md +0 -363
- package/.github/workflows/test.yml +0 -23
- package/tests/components/Chat.browser-test.js +0 -540
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import MarkdownEditor from '../../src/components/MarkdownEditor.js';
|
|
2
|
+
|
|
3
|
+
const createMarkdownEditor = async (attrs = {}) => {
|
|
4
|
+
const container = document.createElement('div');
|
|
5
|
+
const parts = [];
|
|
6
|
+
if(attrs.value !== undefined) parts.push(`value="${attrs.value.replace(/"/g, '"')}"`);
|
|
7
|
+
if(attrs.name !== undefined) parts.push(`name="${attrs.name}"`);
|
|
8
|
+
if(attrs.placeholder !== undefined) parts.push(`placeholder="${attrs.placeholder}"`);
|
|
9
|
+
if(attrs.mode !== undefined) parts.push(`mode="${attrs.mode}"`);
|
|
10
|
+
if(attrs.controls !== undefined) parts.push(`controls="${attrs.controls}"`);
|
|
11
|
+
if(attrs.disabled) parts.push('disabled');
|
|
12
|
+
if(attrs.required) parts.push('required');
|
|
13
|
+
if(attrs.readonly) parts.push('readonly');
|
|
14
|
+
if(attrs['allowed-tags'] !== undefined) parts.push(`allowed-tags="${attrs['allowed-tags']}"`);
|
|
15
|
+
if(attrs['disallowed-tags'] !== undefined) parts.push(`disallowed-tags="${attrs['disallowed-tags']}"`);
|
|
16
|
+
if(attrs['scripts-enabled']) parts.push('scripts-enabled');
|
|
17
|
+
|
|
18
|
+
container.innerHTML = `<k-markdown-editor ${parts.join(' ')}></k-markdown-editor>`;
|
|
19
|
+
document.body.appendChild(container);
|
|
20
|
+
const el = container.querySelector('k-markdown-editor');
|
|
21
|
+
await el.updateComplete;
|
|
22
|
+
return { container, el };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const cleanup = (container) => {
|
|
26
|
+
if(container && container.parentNode){
|
|
27
|
+
container.parentNode.removeChild(container);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default {
|
|
32
|
+
'should create markdown editor element': async ({pass, fail}) => {
|
|
33
|
+
const { container, el } = await createMarkdownEditor();
|
|
34
|
+
if(!el){
|
|
35
|
+
cleanup(container);
|
|
36
|
+
return fail('MarkdownEditor element should be created');
|
|
37
|
+
}
|
|
38
|
+
if(!(el instanceof MarkdownEditor)){
|
|
39
|
+
cleanup(container);
|
|
40
|
+
return fail('Element should be instance of MarkdownEditor');
|
|
41
|
+
}
|
|
42
|
+
cleanup(container);
|
|
43
|
+
pass('MarkdownEditor element created correctly');
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
'should have default mode of write': async ({pass, fail}) => {
|
|
47
|
+
const { container, el } = await createMarkdownEditor();
|
|
48
|
+
if(el.mode !== 'write'){
|
|
49
|
+
cleanup(container);
|
|
50
|
+
return fail(`Expected default mode 'write', got '${el.mode}'`);
|
|
51
|
+
}
|
|
52
|
+
cleanup(container);
|
|
53
|
+
pass('Default mode is write');
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
'should initialize with empty value': async ({pass, fail}) => {
|
|
57
|
+
const { container, el } = await createMarkdownEditor();
|
|
58
|
+
if(el.value !== ''){
|
|
59
|
+
cleanup(container);
|
|
60
|
+
return fail(`Expected empty value, got '${el.value}'`);
|
|
61
|
+
}
|
|
62
|
+
cleanup(container);
|
|
63
|
+
pass('Initializes with empty value');
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
'should set value attribute': async ({pass, fail}) => {
|
|
67
|
+
const testValue = '# Hello World';
|
|
68
|
+
const { container, el } = await createMarkdownEditor({ value: testValue });
|
|
69
|
+
if(el.value !== testValue){
|
|
70
|
+
cleanup(container);
|
|
71
|
+
return fail(`Expected value '${testValue}', got '${el.value}'`);
|
|
72
|
+
}
|
|
73
|
+
cleanup(container);
|
|
74
|
+
pass('Value attribute is set correctly');
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
'should update value property': async ({pass, fail}) => {
|
|
78
|
+
const { container, el } = await createMarkdownEditor();
|
|
79
|
+
const newValue = '## Test Content';
|
|
80
|
+
el.value = newValue;
|
|
81
|
+
await el.updateComplete;
|
|
82
|
+
if(el.value !== newValue){
|
|
83
|
+
cleanup(container);
|
|
84
|
+
return fail(`Expected value '${newValue}', got '${el.value}'`);
|
|
85
|
+
}
|
|
86
|
+
cleanup(container);
|
|
87
|
+
pass('Value property updates correctly');
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
'should render basic markdown': async ({pass, fail}) => {
|
|
91
|
+
const { container, el } = await createMarkdownEditor({ value: '**bold text**' });
|
|
92
|
+
const rendered = el.renderedHtml;
|
|
93
|
+
if(!rendered.includes('bold') || !rendered.includes('<strong>') || !rendered.includes('</strong>')){
|
|
94
|
+
cleanup(container);
|
|
95
|
+
return fail(`Expected bold markdown to render, got: ${rendered}`);
|
|
96
|
+
}
|
|
97
|
+
cleanup(container);
|
|
98
|
+
pass('Basic markdown renders correctly');
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
'should render italic markdown': async ({pass, fail}) => {
|
|
102
|
+
const { container, el } = await createMarkdownEditor({ value: '_italic text_' });
|
|
103
|
+
const rendered = el.renderedHtml;
|
|
104
|
+
if(!rendered.includes('italic') || !rendered.includes('<em>') || !rendered.includes('</em>')){
|
|
105
|
+
cleanup(container);
|
|
106
|
+
return fail(`Expected italic markdown to render, got: ${rendered}`);
|
|
107
|
+
}
|
|
108
|
+
cleanup(container);
|
|
109
|
+
pass('Italic markdown renders correctly');
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
'should render markdown headings': async ({pass, fail}) => {
|
|
113
|
+
const { container, el } = await createMarkdownEditor({ value: '# Heading 1\n## Heading 2' });
|
|
114
|
+
const rendered = el.renderedHtml;
|
|
115
|
+
if(!rendered.includes('<h1>') || !rendered.includes('<h2>')){
|
|
116
|
+
cleanup(container);
|
|
117
|
+
return fail(`Expected heading tags, got: ${rendered}`);
|
|
118
|
+
}
|
|
119
|
+
cleanup(container);
|
|
120
|
+
pass('Heading markdown renders correctly');
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
'should render markdown lists': async ({pass, fail}) => {
|
|
124
|
+
const { container, el } = await createMarkdownEditor({ value: '- Item 1\n- Item 2' });
|
|
125
|
+
const rendered = el.renderedHtml;
|
|
126
|
+
if(!rendered.includes('<ul>') || !rendered.includes('<li>')){
|
|
127
|
+
cleanup(container);
|
|
128
|
+
return fail(`Expected list tags, got: ${rendered}`);
|
|
129
|
+
}
|
|
130
|
+
cleanup(container);
|
|
131
|
+
pass('List markdown renders correctly');
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
'should render markdown code blocks': async ({pass, fail}) => {
|
|
135
|
+
const { container, el } = await createMarkdownEditor({ value: '```\nconst x = 1;\n```' });
|
|
136
|
+
const rendered = el.renderedHtml;
|
|
137
|
+
if(!rendered.includes('<code>') || !rendered.includes('const x')){
|
|
138
|
+
cleanup(container);
|
|
139
|
+
return fail(`Expected code block, got: ${rendered}`);
|
|
140
|
+
}
|
|
141
|
+
cleanup(container);
|
|
142
|
+
pass('Code block markdown renders correctly');
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
'should switch to preview mode': async ({pass, fail}) => {
|
|
146
|
+
const { container, el } = await createMarkdownEditor({ mode: 'write' });
|
|
147
|
+
el.setMode('preview');
|
|
148
|
+
await el.updateComplete;
|
|
149
|
+
if(el.mode !== 'preview'){
|
|
150
|
+
cleanup(container);
|
|
151
|
+
return fail(`Expected mode 'preview', got '${el.mode}'`);
|
|
152
|
+
}
|
|
153
|
+
cleanup(container);
|
|
154
|
+
pass('Mode switches to preview');
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
'should switch to write mode': async ({pass, fail}) => {
|
|
158
|
+
const { container, el } = await createMarkdownEditor({ mode: 'preview' });
|
|
159
|
+
el.setMode('write');
|
|
160
|
+
await el.updateComplete;
|
|
161
|
+
if(el.mode !== 'write'){
|
|
162
|
+
cleanup(container);
|
|
163
|
+
return fail(`Expected mode 'write', got '${el.mode}'`);
|
|
164
|
+
}
|
|
165
|
+
cleanup(container);
|
|
166
|
+
pass('Mode switches to write');
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
'togglePreview should alternate modes': async ({pass, fail}) => {
|
|
170
|
+
const { container, el } = await createMarkdownEditor({ mode: 'write' });
|
|
171
|
+
el.togglePreview();
|
|
172
|
+
await el.updateComplete;
|
|
173
|
+
if(el.mode !== 'preview'){
|
|
174
|
+
cleanup(container);
|
|
175
|
+
return fail(`Expected mode 'preview' after toggle, got '${el.mode}'`);
|
|
176
|
+
}
|
|
177
|
+
el.togglePreview();
|
|
178
|
+
await el.updateComplete;
|
|
179
|
+
if(el.mode !== 'write'){
|
|
180
|
+
cleanup(container);
|
|
181
|
+
return fail(`Expected mode 'write' after second toggle, got '${el.mode}'`);
|
|
182
|
+
}
|
|
183
|
+
cleanup(container);
|
|
184
|
+
pass('togglePreview alternates modes');
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
'setMode should change the mode property': async ({pass, fail}) => {
|
|
188
|
+
const { container, el } = await createMarkdownEditor({ mode: 'write' });
|
|
189
|
+
el.setMode('preview');
|
|
190
|
+
await el.updateComplete;
|
|
191
|
+
if(el.mode !== 'preview'){
|
|
192
|
+
cleanup(container);
|
|
193
|
+
return fail(`Expected mode 'preview' after setMode, got '${el.mode}'`);
|
|
194
|
+
}
|
|
195
|
+
cleanup(container);
|
|
196
|
+
pass('setMode changes the mode property');
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
'input event should fire on textarea input': async ({pass, fail}) => {
|
|
200
|
+
const { container, el } = await createMarkdownEditor();
|
|
201
|
+
let eventFired = false;
|
|
202
|
+
let eventValue = null;
|
|
203
|
+
el.addEventListener('input', e => {
|
|
204
|
+
eventFired = true;
|
|
205
|
+
eventValue = e.detail.value;
|
|
206
|
+
});
|
|
207
|
+
const textarea = el.shadowRoot.querySelector('textarea');
|
|
208
|
+
if(!textarea){
|
|
209
|
+
cleanup(container);
|
|
210
|
+
return fail('textarea not found in shadow DOM');
|
|
211
|
+
}
|
|
212
|
+
textarea.value = 'test content';
|
|
213
|
+
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
214
|
+
await el.updateComplete;
|
|
215
|
+
if(!eventFired){
|
|
216
|
+
cleanup(container);
|
|
217
|
+
return fail('input event should fire on textarea input');
|
|
218
|
+
}
|
|
219
|
+
if(eventValue !== 'test content'){
|
|
220
|
+
cleanup(container);
|
|
221
|
+
return fail(`Expected event value 'test content', got '${eventValue}'`);
|
|
222
|
+
}
|
|
223
|
+
cleanup(container);
|
|
224
|
+
pass('input event fires with correct value');
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
'clear method should empty the editor': async ({pass, fail}) => {
|
|
228
|
+
const { container, el } = await createMarkdownEditor({ value: 'some content' });
|
|
229
|
+
el.clear();
|
|
230
|
+
await el.updateComplete;
|
|
231
|
+
if(el.value !== ''){
|
|
232
|
+
cleanup(container);
|
|
233
|
+
return fail(`Expected empty value after clear, got '${el.value}'`);
|
|
234
|
+
}
|
|
235
|
+
cleanup(container);
|
|
236
|
+
pass('clear() empties the editor');
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
'focus method should focus the editor': async ({pass, fail}) => {
|
|
240
|
+
const { container, el } = await createMarkdownEditor();
|
|
241
|
+
el.focus();
|
|
242
|
+
if(el.mode !== 'write'){
|
|
243
|
+
cleanup(container);
|
|
244
|
+
return fail('focus() should switch to write mode');
|
|
245
|
+
}
|
|
246
|
+
cleanup(container);
|
|
247
|
+
pass('focus() works correctly');
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
'isEmpty getter should return true for empty value': async ({pass, fail}) => {
|
|
251
|
+
const { container, el } = await createMarkdownEditor({ value: '' });
|
|
252
|
+
if(!el.isEmpty){
|
|
253
|
+
cleanup(container);
|
|
254
|
+
return fail('isEmpty should return true for empty value');
|
|
255
|
+
}
|
|
256
|
+
cleanup(container);
|
|
257
|
+
pass('isEmpty returns true for empty value');
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
'isEmpty getter should return false for non-empty value': async ({pass, fail}) => {
|
|
261
|
+
const { container, el } = await createMarkdownEditor({ value: 'content' });
|
|
262
|
+
if(el.isEmpty){
|
|
263
|
+
cleanup(container);
|
|
264
|
+
return fail('isEmpty should return false for non-empty value');
|
|
265
|
+
}
|
|
266
|
+
cleanup(container);
|
|
267
|
+
pass('isEmpty returns false for non-empty value');
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
'disabled attribute should prevent editing': async ({pass, fail}) => {
|
|
271
|
+
const { container, el } = await createMarkdownEditor({ disabled: true });
|
|
272
|
+
if(!el.disabled){
|
|
273
|
+
cleanup(container);
|
|
274
|
+
return fail('disabled property should be true');
|
|
275
|
+
}
|
|
276
|
+
cleanup(container);
|
|
277
|
+
pass('disabled attribute sets disabled property');
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
'readonly attribute should set readonly mode': async ({pass, fail}) => {
|
|
281
|
+
const { container, el } = await createMarkdownEditor({ readonly: true });
|
|
282
|
+
if(!el.readonly){
|
|
283
|
+
cleanup(container);
|
|
284
|
+
return fail('readonly property should be true');
|
|
285
|
+
}
|
|
286
|
+
cleanup(container);
|
|
287
|
+
pass('readonly attribute sets readonly property');
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
'required attribute should set required property': async ({pass, fail}) => {
|
|
291
|
+
const { container, el } = await createMarkdownEditor({ required: true });
|
|
292
|
+
if(!el.required){
|
|
293
|
+
cleanup(container);
|
|
294
|
+
return fail('required property should be true');
|
|
295
|
+
}
|
|
296
|
+
cleanup(container);
|
|
297
|
+
pass('required attribute sets required property');
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
'should sanitize script tags by default': async ({pass, fail}) => {
|
|
301
|
+
const { container, el } = await createMarkdownEditor({ value: '<script>alert("xss")</script>' });
|
|
302
|
+
const rendered = el.renderedHtml;
|
|
303
|
+
if(rendered.includes('<script>')){
|
|
304
|
+
cleanup(container);
|
|
305
|
+
return fail('Script tags should be removed by default');
|
|
306
|
+
}
|
|
307
|
+
cleanup(container);
|
|
308
|
+
pass('Script tags are sanitized');
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
'should sanitize style tags': async ({pass, fail}) => {
|
|
312
|
+
const { container, el } = await createMarkdownEditor({ value: '<style>body{color:red}</style>' });
|
|
313
|
+
const rendered = el.renderedHtml;
|
|
314
|
+
if(rendered.includes('<style>')){
|
|
315
|
+
cleanup(container);
|
|
316
|
+
return fail('Style tags should always be removed');
|
|
317
|
+
}
|
|
318
|
+
cleanup(container);
|
|
319
|
+
pass('Style tags are sanitized');
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
'should sanitize iframe tags': async ({pass, fail}) => {
|
|
323
|
+
const { container, el } = await createMarkdownEditor({ value: '<iframe src="evil.com"></iframe>' });
|
|
324
|
+
const rendered = el.renderedHtml;
|
|
325
|
+
if(rendered.includes('<iframe')){
|
|
326
|
+
cleanup(container);
|
|
327
|
+
return fail('Iframe tags should always be removed');
|
|
328
|
+
}
|
|
329
|
+
cleanup(container);
|
|
330
|
+
pass('Iframe tags are sanitized');
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
'should handle very long markdown content': async ({pass, fail}) => {
|
|
334
|
+
const longContent = '# Heading\n' + 'Lorem ipsum dolor sit amet. '.repeat(100);
|
|
335
|
+
const { container, el } = await createMarkdownEditor({ value: longContent });
|
|
336
|
+
if(el.value !== longContent){
|
|
337
|
+
cleanup(container);
|
|
338
|
+
return fail('Long content should be preserved');
|
|
339
|
+
}
|
|
340
|
+
if(!el.renderedHtml || el.renderedHtml.length === 0){
|
|
341
|
+
cleanup(container);
|
|
342
|
+
return fail('Long content should render');
|
|
343
|
+
}
|
|
344
|
+
cleanup(container);
|
|
345
|
+
pass('Handles very long markdown content');
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
'should handle unicode content': async ({pass, fail}) => {
|
|
349
|
+
const unicodeContent = '你好 مرحبا Привет 🎉';
|
|
350
|
+
const { container, el } = await createMarkdownEditor({ value: unicodeContent });
|
|
351
|
+
if(el.value !== unicodeContent){
|
|
352
|
+
cleanup(container);
|
|
353
|
+
return fail('Unicode content should be preserved');
|
|
354
|
+
}
|
|
355
|
+
cleanup(container);
|
|
356
|
+
pass('Handles unicode content');
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
'should handle special characters': async ({pass, fail}) => {
|
|
360
|
+
const specialContent = 'Test & <special> "characters" \'quotes\'';
|
|
361
|
+
const { container, el } = await createMarkdownEditor({ value: specialContent });
|
|
362
|
+
if(el.value !== specialContent){
|
|
363
|
+
cleanup(container);
|
|
364
|
+
return fail('Special characters should be preserved');
|
|
365
|
+
}
|
|
366
|
+
cleanup(container);
|
|
367
|
+
pass('Handles special characters');
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
'placeholder attribute should be set': async ({pass, fail}) => {
|
|
371
|
+
const placeholder = 'Enter markdown here';
|
|
372
|
+
const { container, el } = await createMarkdownEditor({ placeholder });
|
|
373
|
+
if(el.placeholder !== placeholder){
|
|
374
|
+
cleanup(container);
|
|
375
|
+
return fail(`Expected placeholder '${placeholder}', got '${el.placeholder}'`);
|
|
376
|
+
}
|
|
377
|
+
cleanup(container);
|
|
378
|
+
pass('Placeholder attribute is set');
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
'name attribute should be set': async ({pass, fail}) => {
|
|
382
|
+
const name = 'markdown_field';
|
|
383
|
+
const { container, el } = await createMarkdownEditor({ name });
|
|
384
|
+
if(el.name !== name){
|
|
385
|
+
cleanup(container);
|
|
386
|
+
return fail(`Expected name '${name}', got '${el.name}'`);
|
|
387
|
+
}
|
|
388
|
+
cleanup(container);
|
|
389
|
+
pass('Name attribute is set');
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
'getSelection should return selection object': async ({pass, fail}) => {
|
|
393
|
+
const { container, el } = await createMarkdownEditor({ value: 'test content' });
|
|
394
|
+
const selection = el.getSelection();
|
|
395
|
+
if(!selection || typeof selection !== 'object'){
|
|
396
|
+
cleanup(container);
|
|
397
|
+
return fail('getSelection should return an object');
|
|
398
|
+
}
|
|
399
|
+
if(selection.start === undefined || selection.end === undefined || selection.text === undefined){
|
|
400
|
+
cleanup(container);
|
|
401
|
+
return fail('Selection object should have start, end, and text properties');
|
|
402
|
+
}
|
|
403
|
+
cleanup(container);
|
|
404
|
+
pass('getSelection returns proper selection object');
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
'insertAtCursor should insert text': async ({pass, fail}) => {
|
|
408
|
+
const { container, el } = await createMarkdownEditor({ value: 'hello world' });
|
|
409
|
+
el.insertAtCursor('test');
|
|
410
|
+
await el.updateComplete;
|
|
411
|
+
if(!el.value.includes('test')){
|
|
412
|
+
cleanup(container);
|
|
413
|
+
return fail('insertAtCursor should insert text');
|
|
414
|
+
}
|
|
415
|
+
cleanup(container);
|
|
416
|
+
pass('insertAtCursor inserts text correctly');
|
|
417
|
+
},
|
|
418
|
+
|
|
419
|
+
'insertLinePrefix should add prefix to selected lines': async ({pass, fail}) => {
|
|
420
|
+
const { container, el } = await createMarkdownEditor({ value: 'line1\nline2\nline3' });
|
|
421
|
+
const textarea = el.shadowRoot.querySelector('textarea');
|
|
422
|
+
if(!textarea){
|
|
423
|
+
cleanup(container);
|
|
424
|
+
return fail('textarea not found in shadow DOM');
|
|
425
|
+
}
|
|
426
|
+
await el.updateComplete;
|
|
427
|
+
textarea.focus();
|
|
428
|
+
textarea.selectionStart = 0;
|
|
429
|
+
textarea.selectionEnd = textarea.value.length;
|
|
430
|
+
el.insertLinePrefix('- ');
|
|
431
|
+
await el.updateComplete;
|
|
432
|
+
if(!el.value.startsWith('- ')){
|
|
433
|
+
cleanup(container);
|
|
434
|
+
return fail(`insertLinePrefix should add prefix to lines, got: ${el.value}`);
|
|
435
|
+
}
|
|
436
|
+
cleanup(container);
|
|
437
|
+
pass('insertLinePrefix adds prefix correctly');
|
|
438
|
+
},
|
|
439
|
+
|
|
440
|
+
'GitHub flavored markdown should render tables': async ({pass, fail}) => {
|
|
441
|
+
const tableMarkdown = '| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |';
|
|
442
|
+
const { container, el } = await createMarkdownEditor({ value: tableMarkdown });
|
|
443
|
+
const rendered = el.renderedHtml;
|
|
444
|
+
if(!rendered.includes('<table>')){
|
|
445
|
+
cleanup(container);
|
|
446
|
+
return fail('GFM table markdown should render table element');
|
|
447
|
+
}
|
|
448
|
+
cleanup(container);
|
|
449
|
+
pass('GFM table markdown renders correctly');
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
'should have required ARIA attributes for accessibility': async ({pass, fail}) => {
|
|
453
|
+
const { container, el } = await createMarkdownEditor();
|
|
454
|
+
const shadow = el.shadowRoot;
|
|
455
|
+
if(!shadow){
|
|
456
|
+
cleanup(container);
|
|
457
|
+
return fail('Component should have shadow DOM');
|
|
458
|
+
}
|
|
459
|
+
cleanup(container);
|
|
460
|
+
pass('Component has shadow DOM for accessibility');
|
|
461
|
+
},
|
|
462
|
+
};
|
|
@@ -65,14 +65,14 @@ export default {
|
|
|
65
65
|
'should have default timestamp as 0': async ({pass, fail}) => {
|
|
66
66
|
const { container, timestamp } = await createTimestamp();
|
|
67
67
|
|
|
68
|
-
if(timestamp.timestamp !==
|
|
68
|
+
if(timestamp.timestamp !== ''){
|
|
69
69
|
cleanup(container);
|
|
70
|
-
fail(`Expected timestamp
|
|
70
|
+
fail(`Expected timestamp '', got ${timestamp.timestamp}`);
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
cleanup(container);
|
|
75
|
-
pass('Default timestamp is
|
|
75
|
+
pass('Default timestamp is empty string');
|
|
76
76
|
},
|
|
77
77
|
|
|
78
78
|
'should have default format as empty string': async ({pass, fail}) => {
|
|
@@ -104,7 +104,7 @@ export default {
|
|
|
104
104
|
'should set timestamp from attribute': async ({pass, fail}) => {
|
|
105
105
|
const { container, timestamp } = await createTimestamp({ timestamp: testTimestamp });
|
|
106
106
|
|
|
107
|
-
if(timestamp.timestamp !== testTimestamp){
|
|
107
|
+
if(timestamp.timestamp !== String(testTimestamp)){
|
|
108
108
|
cleanup(container);
|
|
109
109
|
fail(`Expected timestamp ${testTimestamp}, got ${timestamp.timestamp}`);
|
|
110
110
|
return;
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: Change Request
|
|
3
|
-
about: Report a Bug, or suggest a feature
|
|
4
|
-
title: ''
|
|
5
|
-
labels: ''
|
|
6
|
-
assignees: ''
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
## What is the current behavior
|
|
11
|
-
|
|
12
|
-
## What is the desired behavior
|
|
13
|
-
|
|
14
|
-
### Pros
|
|
15
|
-
|
|
16
|
-
### Cons
|
|
17
|
-
|
|
18
|
-
## What is the suggested fix
|
|
19
|
-
|
|
20
|
-
## Tasks
|
|
21
|
-
- [ ] Apply the change
|
|
22
|
-
- [ ] Add docs and examples
|
|
23
|
-
- [ ] Add unit tests
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: get-icon
|
|
3
|
-
description: Fetches icons from the Google Material Design Icon set, saves it to this project, and then formats it.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Get Icon
|
|
7
|
-
|
|
8
|
-
## When to Use
|
|
9
|
-
|
|
10
|
-
Use this skill any time an icon is needed — whether the user explicitly asks for one or you determine that a UI element (button, control, action, etc.) would benefit from one. For example, if a user asks you to build a "bold" button for a WYSIWYG toolbar, you should proactively source an appropriate icon without being explicitly told to.
|
|
11
|
-
|
|
12
|
-
## Overview
|
|
13
|
-
|
|
14
|
-
Icons live in the `icons/` directory as SVG files. The project uses the **Material Symbols Outlined** style from Google's CDN. The workflow has three stages:
|
|
15
|
-
|
|
16
|
-
1. **Check local icons first** — the icon may already exist
|
|
17
|
-
2. **Find** the correct Material Symbols name if it doesn't
|
|
18
|
-
3. **Fetch, format, and save** using the script (directional handling is automatic)
|
|
19
|
-
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
## Stage 1: Check Local Icons First
|
|
23
|
-
|
|
24
|
-
Before going to the internet, list the contents of `icons/` and look for an existing icon that fits the need. Use `file_search` or `list_dir` on the `icons/` directory.
|
|
25
|
-
|
|
26
|
-
- If a **clear match** exists (e.g. `icons/format_bold.svg` for a bold button), use it — no download needed.
|
|
27
|
-
- If a **reasonable match** exists for a directional icon (e.g. `icons/arrow.svg` for a left-arrow), use it with the appropriate `direction` attribute — no download needed.
|
|
28
|
-
- If **nothing fits**, proceed to Stage 2.
|
|
29
|
-
|
|
30
|
-
---
|
|
31
|
-
|
|
32
|
-
## Stage 2: Find the Icon Name
|
|
33
|
-
|
|
34
|
-
If the user describes an icon but doesn't supply an exact name, search Google Material Symbols to find the right one.
|
|
35
|
-
|
|
36
|
-
Run the search script, which fetches icon names and tags from GitHub and caches them locally in `node_modules/.cache/kempo-ui/`. On subsequent runs it only re-downloads if the icon list has changed (SHA check — ~1KB request vs ~500KB full list). Search matches both icon names **and tags**, so searching `chevron` will also surface `keyboard_arrow_right` because it has a `chevron` tag:
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
npm run listicons -- <search_term>
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Example:
|
|
43
|
-
```bash
|
|
44
|
-
npm run listicons -- bold
|
|
45
|
-
# → format_bold
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
npm run listicons -- chevron
|
|
50
|
-
# → chevron_right
|
|
51
|
-
# chevron_left
|
|
52
|
-
# keyboard_arrow_right
|
|
53
|
-
# keyboard_arrow_left
|
|
54
|
-
# ...
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
The results show **full icon names including direction suffixes** so you can see what variants are available. Pick the best match for the user's intent. If no results are returned, try different search terms (e.g. `type` instead of `font`, `text` instead of `word`).
|
|
58
|
-
|
|
59
|
-
---
|
|
60
|
-
|
|
61
|
-
## Stage 3: Fetch, Format, and Save
|
|
62
|
-
|
|
63
|
-
Run the fetch script with the chosen icon name. The script automatically handles directional icons — if the name you pass is directional (has a `_left`, `_up`, `_down`, `_backward` suffix, or has no exact match but a `_right`/`_forward` variant exists), it will prompt:
|
|
64
|
-
|
|
65
|
-
```
|
|
66
|
-
"chevron_left" is a directional icon. kempo-ui uses right direction only and applies CSS rotation.
|
|
67
|
-
Save right-facing variant as "chevron.svg"? (Y/n)
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
npm run geticon -- <icon_name> [custom_name] [-y]
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
- `icon_name` — the Material Symbols name as returned by the search script
|
|
75
|
-
- `custom_name` — optional rename (e.g. `keyboard_double_arrow_right double_chevron` saves as `double_chevron.svg`)
|
|
76
|
-
- `-y` — auto-accept the directional prompt without user interaction
|
|
77
|
-
|
|
78
|
-
Examples:
|
|
79
|
-
```bash
|
|
80
|
-
# Non-directional — downloads silently
|
|
81
|
-
npm run geticon -- format_bold
|
|
82
|
-
|
|
83
|
-
# Directional — prompts to confirm saving right-facing variant as "chevron"
|
|
84
|
-
npm run geticon -- chevron_left
|
|
85
|
-
|
|
86
|
-
# Directional with rename and auto-accept
|
|
87
|
-
npm run geticon -- keyboard_double_arrow_up double_chevron -y
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
If the script exits with a non-zero code mentioning a 404, the icon name is wrong — return to Stage 2 and search for the correct name.
|
|
91
|
-
|
|
92
|
-
The script produces minified output with only `xmlns` and `viewBox` on `<svg>`, and only `fill="currentColor"` and `d` on each `<path>`:
|
|
93
|
-
```svg
|
|
94
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path fill="currentColor" d="..."/></svg>
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
## Directional Icons and `<k-icon>`
|
|
100
|
-
|
|
101
|
-
When the script saves a right-facing icon (after a directional prompt), the `<k-icon>` component handles rotation via its `direction` attribute:
|
|
102
|
-
|
|
103
|
-
| `direction` value | Rotation applied |
|
|
104
|
-
|---|---|
|
|
105
|
-
| *(omitted)* | 0° — points right |
|
|
106
|
-
| `down` | 90° |
|
|
107
|
-
| `left` | 180° |
|
|
108
|
-
| `up` | 270° |
|
|
109
|
-
|
|
110
|
-
**Example:** A user asking for a "left arrow" after downloading `arrow.svg`:
|
|
111
|
-
```html
|
|
112
|
-
<k-icon name="arrow" direction="left"></k-icon>
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
---
|
|
116
|
-
|
|
117
|
-
## Complete Example
|
|
118
|
-
|
|
119
|
-
**User asks to "add a bold button" (implicit icon need)**
|
|
120
|
-
|
|
121
|
-
1. **Local check:** `icons/format_bold.svg` exists → use it directly, no download needed
|
|
122
|
-
2. **Use:** `<k-icon name="format_bold"></k-icon>`
|
|
123
|
-
|
|
124
|
-
**User asks to "add a thumbs up icon" (explicit, not in local icons)**
|
|
125
|
-
|
|
126
|
-
1. **Local check:** No match in `icons/`
|
|
127
|
-
2. **Find:** `npm run listicons -- thumbs+up` → `thumb_up`
|
|
128
|
-
3. **Arrow check:** Not directional, proceed normally
|
|
129
|
-
4. **Run:** `npm run geticon -- thumb_up`
|
|
130
|
-
5. **Result:** `icons/thumb_up.svg` saved and build triggered
|
|
131
|
-
|
|
132
|
-
**User asks for a "left arrow icon"**
|
|
133
|
-
|
|
134
|
-
1. **Local check:** `icons/arrow.svg` exists and is right-facing
|
|
135
|
-
2. **Arrow check:** Directional — right-facing variant already present
|
|
136
|
-
3. **No download needed:** use `<k-icon name="arrow" direction="left"></k-icon>`
|
|
137
|
-
|
|
138
|
-
**User asks for a "left arrow icon" (no arrow in local icons)**
|
|
139
|
-
|
|
140
|
-
1. **Local check:** No arrow icon found
|
|
141
|
-
2. **Find:** `npm run listicons -- arrow` → consider `arrow_forward` or `arrow_right`
|
|
142
|
-
3. **Arrow check:** Directional — download the right-facing variant with a generic name
|
|
143
|
-
4. **Run:** `npm run geticon -- arrow_forward arrow`
|
|
144
|
-
5. **Use:** `<k-icon name="arrow" direction="left"></k-icon>`
|