kempo-ui 0.4.9 → 0.4.11

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.
@@ -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
+ };
@@ -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>`