@aquera/nile-elements 1.8.5 → 1.8.7

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.
Files changed (115) hide show
  1. package/README.md +8 -0
  2. package/dist/index.cjs.js +1 -1
  3. package/dist/index.esm.js +1 -1
  4. package/dist/index.js +915 -321
  5. package/dist/nile-markdown/index.cjs.js +2 -0
  6. package/dist/nile-markdown/index.cjs.js.map +1 -0
  7. package/dist/nile-markdown/index.esm.js +1 -0
  8. package/dist/nile-markdown/nile-markdown.cjs.js +30 -0
  9. package/dist/nile-markdown/nile-markdown.cjs.js.map +1 -0
  10. package/dist/nile-markdown/nile-markdown.css.cjs.js +2 -0
  11. package/dist/nile-markdown/nile-markdown.css.cjs.js.map +1 -0
  12. package/dist/nile-markdown/nile-markdown.css.esm.js +152 -0
  13. package/dist/nile-markdown/nile-markdown.esm.js +3 -0
  14. package/dist/nile-markdown-editor/index.cjs.js +2 -0
  15. package/dist/nile-markdown-editor/index.cjs.js.map +1 -0
  16. package/dist/nile-markdown-editor/index.esm.js +1 -0
  17. package/dist/nile-markdown-editor/nile-markdown-editor.cjs.js +2 -0
  18. package/dist/nile-markdown-editor/nile-markdown-editor.cjs.js.map +1 -0
  19. package/dist/nile-markdown-editor/nile-markdown-editor.css.cjs.js +2 -0
  20. package/dist/nile-markdown-editor/nile-markdown-editor.css.cjs.js.map +1 -0
  21. package/dist/nile-markdown-editor/nile-markdown-editor.css.esm.js +255 -0
  22. package/dist/nile-markdown-editor/nile-markdown-editor.esm.js +143 -0
  23. package/dist/nile-option/nile-option.cjs.js +1 -1
  24. package/dist/nile-option/nile-option.cjs.js.map +1 -1
  25. package/dist/nile-option/nile-option.css.cjs.js +1 -1
  26. package/dist/nile-option/nile-option.css.cjs.js.map +1 -1
  27. package/dist/nile-option/nile-option.css.esm.js +22 -1
  28. package/dist/nile-option/nile-option.esm.js +12 -2
  29. package/dist/nile-select/nile-select.cjs.js +1 -1
  30. package/dist/nile-select/nile-select.cjs.js.map +1 -1
  31. package/dist/nile-select/nile-select.css.cjs.js +1 -1
  32. package/dist/nile-select/nile-select.css.cjs.js.map +1 -1
  33. package/dist/nile-select/nile-select.css.esm.js +16 -7
  34. package/dist/nile-select/nile-select.esm.js +2 -2
  35. package/dist/nile-select/virtual-scroll-helper.cjs.js +1 -1
  36. package/dist/nile-select/virtual-scroll-helper.cjs.js.map +1 -1
  37. package/dist/nile-select/virtual-scroll-helper.esm.js +2 -0
  38. package/dist/nile-virtual-select/nile-virtual-select.cjs.js +3 -3
  39. package/dist/nile-virtual-select/nile-virtual-select.cjs.js.map +1 -1
  40. package/dist/nile-virtual-select/nile-virtual-select.css.cjs.js +1 -1
  41. package/dist/nile-virtual-select/nile-virtual-select.css.cjs.js.map +1 -1
  42. package/dist/nile-virtual-select/nile-virtual-select.css.esm.js +4 -3
  43. package/dist/nile-virtual-select/nile-virtual-select.esm.js +4 -4
  44. package/dist/nile-virtual-select/renderer.cjs.js +1 -1
  45. package/dist/nile-virtual-select/renderer.cjs.js.map +1 -1
  46. package/dist/nile-virtual-select/renderer.esm.js +14 -12
  47. package/dist/src/index.d.ts +2 -0
  48. package/dist/src/index.js +2 -0
  49. package/dist/src/index.js.map +1 -1
  50. package/dist/src/nile-markdown/index.d.ts +1 -0
  51. package/dist/src/nile-markdown/index.js +2 -0
  52. package/dist/src/nile-markdown/index.js.map +1 -0
  53. package/dist/src/nile-markdown/nile-markdown.css.d.ts +10 -0
  54. package/dist/src/nile-markdown/nile-markdown.css.js +163 -0
  55. package/dist/src/nile-markdown/nile-markdown.css.js.map +1 -0
  56. package/dist/src/nile-markdown/nile-markdown.d.ts +91 -0
  57. package/dist/src/nile-markdown/nile-markdown.js +167 -0
  58. package/dist/src/nile-markdown/nile-markdown.js.map +1 -0
  59. package/dist/src/nile-markdown/nile-markdown.test.d.ts +1 -0
  60. package/dist/src/nile-markdown/nile-markdown.test.js +192 -0
  61. package/dist/src/nile-markdown/nile-markdown.test.js.map +1 -0
  62. package/dist/src/nile-markdown-editor/index.d.ts +1 -0
  63. package/dist/src/nile-markdown-editor/index.js +2 -0
  64. package/dist/src/nile-markdown-editor/index.js.map +1 -0
  65. package/dist/src/nile-markdown-editor/nile-markdown-editor.css.d.ts +10 -0
  66. package/dist/src/nile-markdown-editor/nile-markdown-editor.css.js +266 -0
  67. package/dist/src/nile-markdown-editor/nile-markdown-editor.css.js.map +1 -0
  68. package/dist/src/nile-markdown-editor/nile-markdown-editor.d.ts +121 -0
  69. package/dist/src/nile-markdown-editor/nile-markdown-editor.js +615 -0
  70. package/dist/src/nile-markdown-editor/nile-markdown-editor.js.map +1 -0
  71. package/dist/src/nile-markdown-editor/nile-markdown-editor.test.d.ts +1 -0
  72. package/dist/src/nile-markdown-editor/nile-markdown-editor.test.js +268 -0
  73. package/dist/src/nile-markdown-editor/nile-markdown-editor.test.js.map +1 -0
  74. package/dist/src/nile-option/nile-option.css.js +22 -1
  75. package/dist/src/nile-option/nile-option.css.js.map +1 -1
  76. package/dist/src/nile-option/nile-option.d.ts +3 -0
  77. package/dist/src/nile-option/nile-option.js +21 -0
  78. package/dist/src/nile-option/nile-option.js.map +1 -1
  79. package/dist/src/nile-select/nile-select.css.js +16 -7
  80. package/dist/src/nile-select/nile-select.css.js.map +1 -1
  81. package/dist/src/nile-select/nile-select.d.ts +7 -0
  82. package/dist/src/nile-select/nile-select.js +35 -0
  83. package/dist/src/nile-select/nile-select.js.map +1 -1
  84. package/dist/src/nile-select/virtual-scroll-helper.js +2 -0
  85. package/dist/src/nile-select/virtual-scroll-helper.js.map +1 -1
  86. package/dist/src/nile-virtual-select/nile-virtual-select.css.js +4 -3
  87. package/dist/src/nile-virtual-select/nile-virtual-select.css.js.map +1 -1
  88. package/dist/src/nile-virtual-select/nile-virtual-select.d.ts +4 -0
  89. package/dist/src/nile-virtual-select/nile-virtual-select.js +11 -1
  90. package/dist/src/nile-virtual-select/nile-virtual-select.js.map +1 -1
  91. package/dist/src/nile-virtual-select/renderer.d.ts +2 -2
  92. package/dist/src/nile-virtual-select/renderer.js +6 -4
  93. package/dist/src/nile-virtual-select/renderer.js.map +1 -1
  94. package/dist/src/version.js +1 -1
  95. package/dist/src/version.js.map +1 -1
  96. package/dist/tsconfig.tsbuildinfo +1 -1
  97. package/package.json +2 -1
  98. package/src/index.ts +3 -1
  99. package/src/nile-markdown/index.ts +1 -0
  100. package/src/nile-markdown/nile-markdown.css.ts +164 -0
  101. package/src/nile-markdown/nile-markdown.test.ts +252 -0
  102. package/src/nile-markdown/nile-markdown.ts +179 -0
  103. package/src/nile-markdown-editor/index.ts +1 -0
  104. package/src/nile-markdown-editor/nile-markdown-editor.css.ts +267 -0
  105. package/src/nile-markdown-editor/nile-markdown-editor.test.ts +402 -0
  106. package/src/nile-markdown-editor/nile-markdown-editor.ts +710 -0
  107. package/src/nile-option/nile-option.css.ts +22 -1
  108. package/src/nile-option/nile-option.ts +18 -0
  109. package/src/nile-select/nile-select.css.ts +16 -7
  110. package/src/nile-select/nile-select.ts +32 -0
  111. package/src/nile-select/virtual-scroll-helper.ts +2 -0
  112. package/src/nile-virtual-select/nile-virtual-select.css.ts +4 -3
  113. package/src/nile-virtual-select/nile-virtual-select.ts +9 -1
  114. package/src/nile-virtual-select/renderer.ts +9 -3
  115. package/vscode-html-custom-data.json +115 -3
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Copyright Aquera Inc 2023
3
+ *
4
+ * This source code is licensed under the BSD-3-Clause license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ import { css } from 'lit';
9
+
10
+ /**
11
+ * Markdown editor CSS
12
+ */
13
+ export const styles = css`
14
+ :host {
15
+ position: relative;
16
+ display: block;
17
+ font-family: inherit;
18
+ -webkit-font-smoothing: var(--nile-webkit-font-smoothing, var(--ng-webkit-font-smoothing));
19
+ -moz-osx-font-smoothing: var( --nile-moz-osx-font-smoothing,var(--ng-moz-osx-font-smoothing));
20
+ text-rendering: var(--nile-text-rendering, var(--ng-text-rendering));
21
+ }
22
+
23
+ .editor {
24
+ width: 100%;
25
+ border: 1px solid var(--nile-colors-neutral-400, var(--ng-colors-border-neutral));
26
+ border-radius: var(--nile-radius-radius-xl, var(--ng-radius-md));
27
+ background: var(--nile-colors-white-base, var(--ng-colors-bg-primary));
28
+ box-sizing: border-box;
29
+ display: flex;
30
+ flex-direction: column;
31
+ }
32
+
33
+ .editor--disabled {
34
+ border-color: var(--nile-colors-neutral-500, var(--ng-colors-border-disabled));
35
+ background: var(--nile-colors-dark-200, var(--ng-colors-bg-disabled-subtle));
36
+ cursor: not-allowed;
37
+ pointer-events: none;
38
+ }
39
+
40
+ .header {
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: space-between;
44
+ gap: 6px;
45
+ flex-wrap: wrap;
46
+ padding: 8px;
47
+ border-bottom: 1px solid var(--nile-colors-neutral-400, var(--ng-colors-border-neutral));
48
+ background: var(--nile-colors-neutral-100, var(--ng-colors-bg-secondary));
49
+ border-radius: 8px 8px 0 0;
50
+ width: 100%;
51
+ box-sizing: border-box;
52
+ }
53
+
54
+ .editor--disabled .header {
55
+ border-color: var(--nile-colors-neutral-500, var(--ng-colors-border-disabled));
56
+ background: var(--nile-colors-dark-200, var(--ng-colors-bg-disabled-subtle));
57
+ }
58
+
59
+ .tabs {
60
+ display: flex;
61
+ }
62
+
63
+ /* Slimmer segmented control inside the editor header. */
64
+ .tabs nile-button-toggle::part(base) {
65
+ height: 28px;
66
+ padding: var(--nile-spacing-xxs, var(--ng-spacing-xs))
67
+ var(--nile-spacing-md, var(--ng-spacing-md));
68
+ font-size: var(--nile-type-scale-2, var(--ng-font-size-text-sm));
69
+ }
70
+
71
+ .tab__content {
72
+ display: inline-flex;
73
+ align-items: center;
74
+ gap: var(--nile-spacing-xs, var(--ng-spacing-xs));
75
+ }
76
+
77
+
78
+
79
+ .toolbar {
80
+ display: flex;
81
+ align-items: center;
82
+ flex-wrap: wrap;
83
+ gap: 6px;
84
+ min-width: 0;
85
+ }
86
+
87
+ .toolbar__button {
88
+ display: inline-flex;
89
+ align-items: center;
90
+ justify-content: center;
91
+ width: 32px;
92
+ height: 32px;
93
+ padding: 0px 6px;
94
+ box-sizing: border-box;
95
+ border: none;
96
+ background: transparent;
97
+ border-radius: var(--nile-radius-radius-lg, var(--ng-radius-md));
98
+ cursor: pointer;
99
+ color: var(--nile-colors-dark-900, var(--ng-colors-text-primary-900));
100
+ font-family: inherit;
101
+ font-size: var(--nile-type-scale-3, var(--ng-font-size-text-md));
102
+ transition: background 0.15s ease;
103
+ }
104
+
105
+ .toolbar__button:hover {
106
+ background: var(--nile-colors-dark-100, var(--ng-colors-bg-primary-hover));
107
+ }
108
+
109
+ .toolbar__menu {
110
+ display: inline-flex;
111
+ }
112
+
113
+ .toolbar__menu .toolbar__button {
114
+ width: auto;
115
+ gap: 2px;
116
+ }
117
+
118
+ .toolbar__button nile-icon {
119
+ pointer-events: none;
120
+ }
121
+
122
+ .toolbar__divider {
123
+ margin: 0 4px;
124
+ display: inline-block;
125
+ height: 24px;
126
+ width: 1px;
127
+ background: var(--nile-colors-neutral-400, var(--ng-colors-border-neutral));
128
+ }
129
+
130
+ .toolbar__glyph--heading {
131
+ font-weight: bold;
132
+ }
133
+
134
+ .toolbar__glyph--strikethrough {
135
+ text-decoration: line-through;
136
+ }
137
+
138
+ .toolbar__glyph--code {
139
+ font-family: var(--nile-font-family-mono, monospace);
140
+ font-size: var(--nile-type-scale-1, var(--ng-font-size-text-xs));
141
+ }
142
+
143
+ .body {
144
+ display: flex;
145
+ align-items: stretch;
146
+ overflow: hidden;
147
+ min-height: 160px;
148
+ border-radius: 0 0 var(--nile-radius-radius-xl, var(--ng-radius-md))
149
+ var(--nile-radius-radius-xl, var(--ng-radius-md));
150
+ }
151
+
152
+ .pane {
153
+ flex: 1 1 0;
154
+ min-width: 0;
155
+ }
156
+
157
+ /* Draggable divider between the write and preview panes in split mode. */
158
+ .gutter {
159
+ flex: 0 0 auto;
160
+ position: relative;
161
+ width: 1px;
162
+ align-self: stretch;
163
+ background: var(--nile-colors-neutral-400, var(--ng-colors-border-neutral));
164
+ cursor: col-resize;
165
+ touch-action: none;
166
+ }
167
+
168
+ /* Widen the hit area without shifting the 1px visual line. */
169
+ .gutter::before {
170
+ content: '';
171
+ position: absolute;
172
+ inset: 0 -4px;
173
+ }
174
+
175
+ .gutter:hover,
176
+ .body--dragging .gutter {
177
+ background: var(--nile-colors-primary-500, var(--ng-colors-border-brand));
178
+ }
179
+
180
+ /* Keep the drag smooth: no text selection or pointer capture by the panes. */
181
+ .body--dragging {
182
+ cursor: col-resize;
183
+ user-select: none;
184
+ -webkit-user-select: none;
185
+ }
186
+
187
+ .body--dragging .pane {
188
+ pointer-events: none;
189
+ }
190
+
191
+ .pane--write {
192
+ display: flex;
193
+ }
194
+
195
+ textarea {
196
+ flex: 1;
197
+ /* The body owns the resize/height now; the textarea fills it and scrolls
198
+ its own overflow. min-height:0 lets it shrink when the body shrinks. */
199
+ min-height: 0;
200
+ width: 100%;
201
+ padding: 12px;
202
+ border: none;
203
+ outline: none;
204
+ resize: none;
205
+ box-sizing: border-box;
206
+ tab-size: 4;
207
+ -moz-tab-size: 4;
208
+ word-break: break-word;
209
+ font-family: inherit;
210
+ font-size: inherit;
211
+ line-height: inherit;
212
+ color: inherit;
213
+ background: var(--nile-colors-white-base, var(--ng-colors-bg-primary));
214
+ border-radius: 0 0 var(--nile-radius-radius-xl, var(--ng-radius-md))
215
+ var(--nile-radius-radius-xl, var(--ng-radius-md));
216
+ }
217
+
218
+ textarea::placeholder {
219
+ color: var(--nile-colors-neutral-500, var(--ng-colors-text-secondary));
220
+ font-family: var(--nile-font-family-sans-serif, var(--ng-font-family-body));
221
+ opacity: 0.7;
222
+ }
223
+
224
+ .editor--disabled textarea {
225
+ background: var(
226
+ --nile-colors-dark-200,
227
+ var(--ng-colors-bg-disabled-subtle)
228
+ );
229
+ color: var(--nile-colors-dark-500, var(--ng-colors-text-disabled));
230
+ pointer-events: none;
231
+ user-select: none;
232
+ -webkit-user-select: none;
233
+ }
234
+
235
+ .pane--preview {
236
+ min-height: 0;
237
+ padding: 12px;
238
+ overflow: auto;
239
+ box-sizing: border-box;
240
+ word-wrap: break-word;
241
+ background: var(--nile-colors-white-base, var(--ng-colors-bg-primary));
242
+ border-radius: 0 0 var(--nile-radius-radius-xl, var(--ng-radius-md))
243
+ var(--nile-radius-radius-xl, var(--ng-radius-md));
244
+ }
245
+
246
+ .editor--disabled .pane--preview {
247
+ background: var(
248
+ --nile-colors-dark-200,
249
+ var(--ng-colors-bg-disabled-subtle)
250
+ );
251
+ color: var(--nile-colors-dark-500, var(--ng-colors-text-disabled));
252
+ }
253
+
254
+ .editor--split .pane--preview {
255
+ border-radius: 0 0 var(--nile-radius-radius-xl, var(--ng-radius-md)) 0;
256
+ }
257
+
258
+ .editor--split textarea {
259
+ border-radius: 0 0 0 var(--nile-radius-radius-xl, var(--ng-radius-md));
260
+ }
261
+
262
+ .preview-empty {
263
+ color: var(--nile-colors-neutral-500, var(--ng-colors-text-secondary));
264
+ font-size: var(--nile-type-scale-2, var(--ng-font-size-text-sm));
265
+ opacity: 0.7;
266
+ }
267
+ `;
@@ -0,0 +1,402 @@
1
+ import { expect, fixture, html, oneEvent } from '@open-wc/testing';
2
+ import './nile-markdown-editor';
3
+ import NileMarkdownEditor from './nile-markdown-editor';
4
+
5
+ const getTextarea = (el: NileMarkdownEditor) =>
6
+ el.shadowRoot!.querySelector('textarea')!;
7
+
8
+ describe('NileMarkdownEditor', () => {
9
+ // === RENDERING ===
10
+ it('1. should render without errors', async () => {
11
+ const el = await fixture<NileMarkdownEditor>(
12
+ html`<nile-markdown-editor></nile-markdown-editor>`
13
+ );
14
+ expect(el).to.exist;
15
+ });
16
+
17
+ it('2. should have a shadow root', async () => {
18
+ const el = await fixture<NileMarkdownEditor>(
19
+ html`<nile-markdown-editor></nile-markdown-editor>`
20
+ );
21
+ expect(el.shadowRoot).to.not.be.null;
22
+ });
23
+
24
+ it('3. should render a textarea in write mode', async () => {
25
+ const el = await fixture<NileMarkdownEditor>(
26
+ html`<nile-markdown-editor></nile-markdown-editor>`
27
+ );
28
+ expect(getTextarea(el)).to.exist;
29
+ });
30
+
31
+ it('4. should have base, header, toolbar, textarea parts', async () => {
32
+ const el = await fixture<NileMarkdownEditor>(
33
+ html`<nile-markdown-editor></nile-markdown-editor>`
34
+ );
35
+ expect(el.shadowRoot!.querySelector('[part~="base"]')).to.exist;
36
+ expect(el.shadowRoot!.querySelector('[part~="header"]')).to.exist;
37
+ expect(el.shadowRoot!.querySelector('[part~="toolbar"]')).to.exist;
38
+ expect(el.shadowRoot!.querySelector('[part~="textarea"]')).to.exist;
39
+ });
40
+
41
+ it('5. should be instance of NileMarkdownEditor', async () => {
42
+ const el = await fixture<NileMarkdownEditor>(
43
+ html`<nile-markdown-editor></nile-markdown-editor>`
44
+ );
45
+ expect(el).to.be.instanceOf(NileMarkdownEditor);
46
+ });
47
+
48
+ // === DEFAULT PROPERTIES ===
49
+ it('6. should default to write mode', async () => {
50
+ const el = await fixture<NileMarkdownEditor>(
51
+ html`<nile-markdown-editor></nile-markdown-editor>`
52
+ );
53
+ expect(el.mode).to.equal('write');
54
+ });
55
+
56
+ it('7. should default value to empty string', async () => {
57
+ const el = await fixture<NileMarkdownEditor>(
58
+ html`<nile-markdown-editor></nile-markdown-editor>`
59
+ );
60
+ expect(el.value).to.equal('');
61
+ });
62
+
63
+ it('8. should default rows to 8', async () => {
64
+ const el = await fixture<NileMarkdownEditor>(
65
+ html`<nile-markdown-editor></nile-markdown-editor>`
66
+ );
67
+ expect(el.rows).to.equal(8);
68
+ });
69
+
70
+ // === VALUE ===
71
+ it('9. should pass value to the textarea', async () => {
72
+ const el = await fixture<NileMarkdownEditor>(
73
+ html`<nile-markdown-editor value="# Hi"></nile-markdown-editor>`
74
+ );
75
+ expect(getTextarea(el).value).to.equal('# Hi');
76
+ });
77
+
78
+ it('10. should update value on textarea input', async () => {
79
+ const el = await fixture<NileMarkdownEditor>(
80
+ html`<nile-markdown-editor></nile-markdown-editor>`
81
+ );
82
+ const ta = getTextarea(el);
83
+ ta.value = 'typed';
84
+ ta.dispatchEvent(new InputEvent('input', { bubbles: true }));
85
+ expect(el.value).to.equal('typed');
86
+ });
87
+
88
+ // === MODES ===
89
+ it('11. should show preview pane in preview mode', async () => {
90
+ const el = await fixture<NileMarkdownEditor>(
91
+ html`<nile-markdown-editor
92
+ mode="preview"
93
+ value="# Title"
94
+ ></nile-markdown-editor>`
95
+ );
96
+ const preview = el.shadowRoot!.querySelector('[part~="preview"]');
97
+ expect(preview).to.exist;
98
+ expect(el.shadowRoot!.querySelector('textarea')).to.not.exist;
99
+ });
100
+
101
+ it('12. should render markdown in the preview', async () => {
102
+ const el = await fixture<NileMarkdownEditor>(
103
+ html`<nile-markdown-editor
104
+ mode="preview"
105
+ value="# Title"
106
+ ></nile-markdown-editor>`
107
+ );
108
+ const md = el.shadowRoot!.querySelector('nile-markdown')!;
109
+ await md.updateComplete;
110
+ expect(md.shadowRoot!.querySelector('h1')!.textContent).to.equal('Title');
111
+ });
112
+
113
+ it('13. should show both panes in split mode', async () => {
114
+ const el = await fixture<NileMarkdownEditor>(
115
+ html`<nile-markdown-editor
116
+ mode="split"
117
+ value="text"
118
+ ></nile-markdown-editor>`
119
+ );
120
+ expect(el.shadowRoot!.querySelector('textarea')).to.exist;
121
+ expect(el.shadowRoot!.querySelector('[part~="preview"]')).to.exist;
122
+ });
123
+
124
+ it('14. should switch mode via tab click and emit nile-mode-change', async () => {
125
+ const el = await fixture<NileMarkdownEditor>(
126
+ html`<nile-markdown-editor></nile-markdown-editor>`
127
+ );
128
+ const tabs = el.shadowRoot!.querySelectorAll('nile-button-toggle');
129
+ setTimeout(() => (tabs[1] as HTMLElement).click());
130
+ const event = await oneEvent(el, 'nile-mode-change');
131
+ expect(event.detail.mode).to.equal('preview');
132
+ expect(el.mode).to.equal('preview');
133
+ });
134
+
135
+ it('15. should show empty preview placeholder', async () => {
136
+ const el = await fixture<NileMarkdownEditor>(
137
+ html`<nile-markdown-editor mode="preview"></nile-markdown-editor>`
138
+ );
139
+ const empty = el.shadowRoot!.querySelector('.preview-empty');
140
+ expect(empty).to.exist;
141
+ });
142
+
143
+ // === TOOLBAR ===
144
+ it('16. should render toolbar buttons', async () => {
145
+ const el = await fixture<NileMarkdownEditor>(
146
+ html`<nile-markdown-editor></nile-markdown-editor>`
147
+ );
148
+ const buttons = el.shadowRoot!.querySelectorAll('.toolbar__button');
149
+ expect(buttons.length).to.equal(9);
150
+ });
151
+
152
+ it('17. should hide toolbar with hide-toolbar', async () => {
153
+ const el = await fixture<NileMarkdownEditor>(
154
+ html`<nile-markdown-editor hide-toolbar></nile-markdown-editor>`
155
+ );
156
+ expect(el.shadowRoot!.querySelector('[part~="toolbar"]')).to.not.exist;
157
+ });
158
+
159
+ it('18. bold button should wrap the selection', async () => {
160
+ const el = await fixture<NileMarkdownEditor>(
161
+ html`<nile-markdown-editor value="hello world"></nile-markdown-editor>`
162
+ );
163
+ const ta = getTextarea(el);
164
+ ta.setSelectionRange(0, 5);
165
+ const bold =
166
+ el.shadowRoot!.querySelector<HTMLButtonElement>('[title^="Bold"]')!;
167
+ bold.click();
168
+ expect(el.value).to.equal('**hello** world');
169
+ });
170
+
171
+ it('19. bold button should unwrap an already-bold selection', async () => {
172
+ const el = await fixture<NileMarkdownEditor>(
173
+ html`<nile-markdown-editor
174
+ value="**hello** world"
175
+ ></nile-markdown-editor>`
176
+ );
177
+ const ta = getTextarea(el);
178
+ ta.setSelectionRange(2, 7); // "hello"
179
+ el.shadowRoot!.querySelector<HTMLButtonElement>('[title^="Bold"]')!.click();
180
+ expect(el.value).to.equal('hello world');
181
+ });
182
+
183
+ it('20. bold button should insert placeholder without selection', async () => {
184
+ const el = await fixture<NileMarkdownEditor>(
185
+ html`<nile-markdown-editor></nile-markdown-editor>`
186
+ );
187
+ getTextarea(el).setSelectionRange(0, 0);
188
+ el.shadowRoot!.querySelector<HTMLButtonElement>('[title^="Bold"]')!.click();
189
+ expect(el.value).to.equal('**bold text**');
190
+ });
191
+
192
+ it('21. heading button should prefix the line', async () => {
193
+ const el = await fixture<NileMarkdownEditor>(
194
+ html`<nile-markdown-editor value="Title"></nile-markdown-editor>`
195
+ );
196
+ getTextarea(el).setSelectionRange(0, 0);
197
+ el.shadowRoot!.querySelector<HTMLButtonElement>(
198
+ '[title="Heading"]'
199
+ )!.click();
200
+ expect(el.value).to.equal('### Title');
201
+ });
202
+
203
+ it('22. heading button should toggle the prefix off', async () => {
204
+ const el = await fixture<NileMarkdownEditor>(
205
+ html`<nile-markdown-editor value="### Title"></nile-markdown-editor>`
206
+ );
207
+ getTextarea(el).setSelectionRange(0, 0);
208
+ el.shadowRoot!.querySelector<HTMLButtonElement>(
209
+ '[title="Heading"]'
210
+ )!.click();
211
+ expect(el.value).to.equal('Title');
212
+ });
213
+
214
+ it('23. bulleted list should prefix every selected line', async () => {
215
+ const el = await fixture<NileMarkdownEditor>(
216
+ html`<nile-markdown-editor
217
+ value=${'one\ntwo\nthree'}
218
+ ></nile-markdown-editor>`
219
+ );
220
+ const ta = getTextarea(el);
221
+ ta.setSelectionRange(0, ta.value.length);
222
+ el.shadowRoot!.querySelector<HTMLButtonElement>(
223
+ '[title="Bulleted list"]'
224
+ )!.click();
225
+ expect(el.value).to.equal('- one\n- two\n- three');
226
+ });
227
+
228
+ it('24. numbered list should number every selected line', async () => {
229
+ const el = await fixture<NileMarkdownEditor>(
230
+ html`<nile-markdown-editor value=${'one\ntwo'}></nile-markdown-editor>`
231
+ );
232
+ const ta = getTextarea(el);
233
+ ta.setSelectionRange(0, ta.value.length);
234
+ el.shadowRoot!.querySelector<HTMLButtonElement>(
235
+ '[title="Numbered list"]'
236
+ )!.click();
237
+ expect(el.value).to.equal('1. one\n2. two');
238
+ });
239
+
240
+ it('25. link button should insert a markdown link', async () => {
241
+ const el = await fixture<NileMarkdownEditor>(
242
+ html`<nile-markdown-editor value="docs"></nile-markdown-editor>`
243
+ );
244
+ const ta = getTextarea(el);
245
+ ta.setSelectionRange(0, 4);
246
+ el.shadowRoot!.querySelector<HTMLButtonElement>('[title^="Link"]')!.click();
247
+ expect(el.value).to.equal('[docs](url)');
248
+ });
249
+
250
+ // === EVENTS ===
251
+ it('26. should emit nile-input on typing', async () => {
252
+ const el = await fixture<NileMarkdownEditor>(
253
+ html`<nile-markdown-editor></nile-markdown-editor>`
254
+ );
255
+ const ta = getTextarea(el);
256
+ setTimeout(() => {
257
+ ta.value = 'abc';
258
+ ta.dispatchEvent(new InputEvent('input', { bubbles: true }));
259
+ });
260
+ const event = await oneEvent(el, 'nile-input');
261
+ expect(event.detail.value).to.equal('abc');
262
+ });
263
+
264
+ it('27. should emit nile-input on toolbar action', async () => {
265
+ const el = await fixture<NileMarkdownEditor>(
266
+ html`<nile-markdown-editor></nile-markdown-editor>`
267
+ );
268
+ setTimeout(() =>
269
+ el
270
+ .shadowRoot!.querySelector<HTMLButtonElement>('[title^="Bold"]')!
271
+ .click()
272
+ );
273
+ const event = await oneEvent(el, 'nile-input');
274
+ expect(event.detail.value).to.equal('**bold text**');
275
+ });
276
+
277
+ it('28. should emit nile-change on textarea change', async () => {
278
+ const el = await fixture<NileMarkdownEditor>(
279
+ html`<nile-markdown-editor></nile-markdown-editor>`
280
+ );
281
+ const ta = getTextarea(el);
282
+ setTimeout(() => {
283
+ ta.value = 'done';
284
+ ta.dispatchEvent(new InputEvent('input', { bubbles: true }));
285
+ ta.dispatchEvent(new Event('change', { bubbles: true }));
286
+ });
287
+ const event = await oneEvent(el, 'nile-change');
288
+ expect(event.detail.value).to.equal('done');
289
+ });
290
+
291
+ // === KEYBOARD SHORTCUTS ===
292
+ it('29. Ctrl+B should bold the selection', async () => {
293
+ const el = await fixture<NileMarkdownEditor>(
294
+ html`<nile-markdown-editor value="hi"></nile-markdown-editor>`
295
+ );
296
+ const ta = getTextarea(el);
297
+ ta.setSelectionRange(0, 2);
298
+ ta.dispatchEvent(
299
+ new KeyboardEvent('keydown', { key: 'b', ctrlKey: true, bubbles: true })
300
+ );
301
+ expect(el.value).to.equal('**hi**');
302
+ });
303
+
304
+ // === DISABLED / READONLY ===
305
+ it('30. should disable the textarea when disabled', async () => {
306
+ const el = await fixture<NileMarkdownEditor>(
307
+ html`<nile-markdown-editor disabled></nile-markdown-editor>`
308
+ );
309
+ expect(getTextarea(el).disabled).to.be.true;
310
+ });
311
+
312
+ it('31. should make the textarea readonly when readonly', async () => {
313
+ const el = await fixture<NileMarkdownEditor>(
314
+ html`<nile-markdown-editor readonly></nile-markdown-editor>`
315
+ );
316
+ expect(getTextarea(el).readOnly).to.be.true;
317
+ });
318
+
319
+ it('32. toolbar actions should not modify a readonly editor', async () => {
320
+ const el = await fixture<NileMarkdownEditor>(
321
+ html`<nile-markdown-editor readonly value="text"></nile-markdown-editor>`
322
+ );
323
+ el.shadowRoot!.querySelector<HTMLButtonElement>('[title^="Bold"]')!.click();
324
+ expect(el.value).to.equal('text');
325
+ });
326
+
327
+ // === MISC ===
328
+ it('33. should reflect mode attribute', async () => {
329
+ const el = await fixture<NileMarkdownEditor>(
330
+ html`<nile-markdown-editor mode="split"></nile-markdown-editor>`
331
+ );
332
+ expect(el.getAttribute('mode')).to.equal('split');
333
+ });
334
+
335
+ it('34. should apply placeholder to the textarea', async () => {
336
+ const el = await fixture<NileMarkdownEditor>(
337
+ html`<nile-markdown-editor
338
+ placeholder="Type here"
339
+ ></nile-markdown-editor>`
340
+ );
341
+ expect(getTextarea(el).placeholder).to.equal('Type here');
342
+ });
343
+
344
+ it('35. should have static styles', () => {
345
+ expect(NileMarkdownEditor.styles).to.exist;
346
+ });
347
+
348
+ // === TOOLS ALLOWLIST ===
349
+ it('36. should show all tools by default', async () => {
350
+ const el = await fixture<NileMarkdownEditor>(
351
+ html`<nile-markdown-editor></nile-markdown-editor>`
352
+ );
353
+ expect(el.shadowRoot!.querySelectorAll('.toolbar__button').length).to.equal(9);
354
+ });
355
+
356
+ it('37. should only show allowlisted tools', async () => {
357
+ const el = await fixture<NileMarkdownEditor>(
358
+ html`<nile-markdown-editor
359
+ tools='["bold","italic","link"]'
360
+ ></nile-markdown-editor>`
361
+ );
362
+ const buttons = el.shadowRoot!.querySelectorAll('.toolbar__button');
363
+ expect(buttons.length).to.equal(3);
364
+ expect(el.shadowRoot!.querySelector('[title^="Bold"]')).to.exist;
365
+ expect(el.shadowRoot!.querySelector('[title^="Italic"]')).to.exist;
366
+ expect(el.shadowRoot!.querySelector('[title^="Link"]')).to.exist;
367
+ expect(el.shadowRoot!.querySelector('[title^="Heading"]')).to.not.exist;
368
+ });
369
+
370
+ it('38. should drop empty groups (no dangling dividers)', async () => {
371
+ // "link" is alone in the last group; "bold" is in the first group.
372
+ const el = await fixture<NileMarkdownEditor>(
373
+ html`<nile-markdown-editor tools='["bold","link"]'></nile-markdown-editor>`
374
+ );
375
+ const dividers = el.shadowRoot!.querySelectorAll('.toolbar__divider');
376
+ // Two non-empty groups => exactly one divider between them.
377
+ expect(dividers.length).to.equal(1);
378
+ });
379
+
380
+ it('39. should hide the whole toolbar when no allowlisted tool matches', async () => {
381
+ const el = await fixture<NileMarkdownEditor>(
382
+ html`<nile-markdown-editor tools='["nonexistent"]'></nile-markdown-editor>`
383
+ );
384
+ expect(el.shadowRoot!.querySelector('[part~="toolbar"]')).to.not.exist;
385
+ });
386
+
387
+ it('40. should disable a tool keyboard shortcut when it is not allowlisted', async () => {
388
+ const el = await fixture<NileMarkdownEditor>(
389
+ html`<nile-markdown-editor
390
+ tools='["italic"]'
391
+ value="hello world"
392
+ ></nile-markdown-editor>`
393
+ );
394
+ const ta = getTextarea(el);
395
+ ta.setSelectionRange(0, 5);
396
+ // Ctrl+B is bold, which is NOT allowlisted -> no change.
397
+ ta.dispatchEvent(
398
+ new KeyboardEvent('keydown', { key: 'b', ctrlKey: true, bubbles: true })
399
+ );
400
+ expect(el.value).to.equal('hello world');
401
+ });
402
+ });