@rmdes/indiekit-frontend 1.0.0-beta.25

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 (213) hide show
  1. package/README.md +10 -0
  2. package/assets/app-icon-any.svg +5 -0
  3. package/assets/app-icon-maskable.svg +5 -0
  4. package/assets/icon.svg +3 -0
  5. package/assets/not-found.svg +7 -0
  6. package/assets/offline.svg +7 -0
  7. package/assets/plug-in.svg +4 -0
  8. package/components/actions/macro.njk +3 -0
  9. package/components/actions/styles.css +19 -0
  10. package/components/actions/template.njk +19 -0
  11. package/components/add-another/index.js +170 -0
  12. package/components/add-another/macro.njk +3 -0
  13. package/components/add-another/styles.css +63 -0
  14. package/components/add-another/template.njk +39 -0
  15. package/components/app/styles.css +45 -0
  16. package/components/authorize/macro.njk +3 -0
  17. package/components/authorize/styles.css +23 -0
  18. package/components/authorize/template.njk +14 -0
  19. package/components/avatar/macro.njk +3 -0
  20. package/components/avatar/styles.css +23 -0
  21. package/components/avatar/template.njk +1 -0
  22. package/components/back-link/macro.njk +3 -0
  23. package/components/back-link/styles.css +46 -0
  24. package/components/back-link/template.njk +5 -0
  25. package/components/badge/macro.njk +3 -0
  26. package/components/badge/styles.css +85 -0
  27. package/components/badge/template.njk +7 -0
  28. package/components/bookmarklet/macro.njk +3 -0
  29. package/components/bookmarklet/styles.css +18 -0
  30. package/components/bookmarklet/template.njk +1 -0
  31. package/components/button/macro.njk +3 -0
  32. package/components/button/styles.css +108 -0
  33. package/components/button/template.njk +17 -0
  34. package/components/card/macro.njk +3 -0
  35. package/components/card/styles.css +93 -0
  36. package/components/card/template.njk +46 -0
  37. package/components/card-grid/macro.njk +3 -0
  38. package/components/card-grid/styles.css +9 -0
  39. package/components/card-grid/template.njk +8 -0
  40. package/components/character-count/index.js +185 -0
  41. package/components/character-count/macro.njk +3 -0
  42. package/components/character-count/styles.css +3 -0
  43. package/components/character-count/template.njk +14 -0
  44. package/components/checkboxes/index.js +130 -0
  45. package/components/checkboxes/macro.njk +3 -0
  46. package/components/checkboxes/styles.css +96 -0
  47. package/components/checkboxes/template.njk +96 -0
  48. package/components/details/macro.njk +3 -0
  49. package/components/details/styles.css +24 -0
  50. package/components/details/template.njk +12 -0
  51. package/components/error-message/macro.njk +3 -0
  52. package/components/error-message/styles.css +5 -0
  53. package/components/error-message/template.njk +9 -0
  54. package/components/error-summary/index.js +147 -0
  55. package/components/error-summary/macro.njk +3 -0
  56. package/components/error-summary/styles.css +24 -0
  57. package/components/error-summary/template.njk +27 -0
  58. package/components/event-duration/index.js +26 -0
  59. package/components/event-duration/styles.css +7 -0
  60. package/components/field/macro.njk +3 -0
  61. package/components/field/styles.css +14 -0
  62. package/components/field/template.njk +4 -0
  63. package/components/fieldset/macro.njk +3 -0
  64. package/components/fieldset/styles.css +27 -0
  65. package/components/fieldset/template.njk +13 -0
  66. package/components/file-input/index.js +123 -0
  67. package/components/file-input/macro.njk +3 -0
  68. package/components/file-input/styles.css +3 -0
  69. package/components/file-input/template.njk +45 -0
  70. package/components/footer/macro.njk +3 -0
  71. package/components/footer/styles.css +18 -0
  72. package/components/footer/template.njk +8 -0
  73. package/components/geo-input/index.js +105 -0
  74. package/components/geo-input/macro.njk +3 -0
  75. package/components/geo-input/styles.css +3 -0
  76. package/components/geo-input/template.njk +35 -0
  77. package/components/header/macro.njk +3 -0
  78. package/components/header/styles.css +60 -0
  79. package/components/header/template.njk +11 -0
  80. package/components/heading/macro.njk +3 -0
  81. package/components/heading/styles.css +24 -0
  82. package/components/heading/template.njk +20 -0
  83. package/components/hint/macro.njk +3 -0
  84. package/components/hint/styles.css +10 -0
  85. package/components/hint/template.njk +3 -0
  86. package/components/icon/styles.css +14 -0
  87. package/components/input/macro.njk +3 -0
  88. package/components/input/styles.css +111 -0
  89. package/components/input/template.njk +45 -0
  90. package/components/label/macro.njk +3 -0
  91. package/components/label/styles.css +4 -0
  92. package/components/label/template.njk +4 -0
  93. package/components/logo/macro.njk +3 -0
  94. package/components/logo/styles.css +24 -0
  95. package/components/logo/template.njk +3 -0
  96. package/components/main/styles.css +14 -0
  97. package/components/mention/macro.njk +3 -0
  98. package/components/mention/styles.css +9 -0
  99. package/components/mention/template.njk +6 -0
  100. package/components/navigation/macro.njk +3 -0
  101. package/components/navigation/styles.css +19 -0
  102. package/components/navigation/template.njk +11 -0
  103. package/components/notification-banner/index.js +24 -0
  104. package/components/notification-banner/macro.njk +3 -0
  105. package/components/notification-banner/styles.css +49 -0
  106. package/components/notification-banner/template.njk +20 -0
  107. package/components/pagination/macro.njk +3 -0
  108. package/components/pagination/styles.css +91 -0
  109. package/components/pagination/template.njk +42 -0
  110. package/components/progress/macro.njk +3 -0
  111. package/components/progress/styles.css +38 -0
  112. package/components/progress/template.njk +10 -0
  113. package/components/prose/macro.njk +3 -0
  114. package/components/prose/styles.css +12 -0
  115. package/components/prose/template.njk +3 -0
  116. package/components/radios/index.js +83 -0
  117. package/components/radios/macro.njk +3 -0
  118. package/components/radios/styles.css +104 -0
  119. package/components/radios/template.njk +87 -0
  120. package/components/section/macro.njk +3 -0
  121. package/components/section/styles.css +12 -0
  122. package/components/section/template.njk +19 -0
  123. package/components/select/macro.njk +3 -0
  124. package/components/select/styles.css +37 -0
  125. package/components/select/template.njk +48 -0
  126. package/components/share-preview/index.js +54 -0
  127. package/components/share-preview/macro.njk +3 -0
  128. package/components/share-preview/styles.css +36 -0
  129. package/components/share-preview/template.njk +14 -0
  130. package/components/skip-link/macro.njk +3 -0
  131. package/components/skip-link/styles.css +12 -0
  132. package/components/skip-link/template.njk +3 -0
  133. package/components/summary/macro.njk +3 -0
  134. package/components/summary/styles.css +60 -0
  135. package/components/summary/template.njk +19 -0
  136. package/components/tag/macro.njk +3 -0
  137. package/components/tag/styles.css +14 -0
  138. package/components/tag/template.njk +5 -0
  139. package/components/tag-input/index.js +69 -0
  140. package/components/tag-input/macro.njk +3 -0
  141. package/components/tag-input/sanitizer.js +27 -0
  142. package/components/tag-input/styles.css +161 -0
  143. package/components/tag-input/template.njk +29 -0
  144. package/components/textarea/index.js +298 -0
  145. package/components/textarea/macro.njk +3 -0
  146. package/components/textarea/styles.css +42 -0
  147. package/components/textarea/template.njk +40 -0
  148. package/components/user/macro.njk +3 -0
  149. package/components/user/styles.css +20 -0
  150. package/components/user/template.njk +15 -0
  151. package/components/warning-text/macro.njk +3 -0
  152. package/components/warning-text/styles.css +15 -0
  153. package/components/warning-text/template.njk +8 -0
  154. package/components/widget/macro.njk +3 -0
  155. package/components/widget/styles.css +28 -0
  156. package/components/widget/template.njk +20 -0
  157. package/index.js +10 -0
  158. package/layouts/default.njk +122 -0
  159. package/layouts/document.njk +15 -0
  160. package/layouts/error.njk +19 -0
  161. package/layouts/form.njk +22 -0
  162. package/lib/esbuild.js +22 -0
  163. package/lib/filters/index.js +10 -0
  164. package/lib/filters/locale.js +17 -0
  165. package/lib/filters/string.js +29 -0
  166. package/lib/filters/url.js +69 -0
  167. package/lib/globals/attributes.js +23 -0
  168. package/lib/globals/classes.js +19 -0
  169. package/lib/globals/error-list.js +21 -0
  170. package/lib/globals/field-data.js +20 -0
  171. package/lib/globals/icon.js +80 -0
  172. package/lib/globals/index.js +12 -0
  173. package/lib/globals/item-id.js +17 -0
  174. package/lib/globals/summary-rows.js +39 -0
  175. package/lib/lightningcss.js +41 -0
  176. package/lib/markdown-it.js +27 -0
  177. package/lib/nunjucks.js +43 -0
  178. package/lib/serviceworker.js +240 -0
  179. package/lib/sharp.js +39 -0
  180. package/lib/utils/theme.js +115 -0
  181. package/lib/utils/wrap-element.js +11 -0
  182. package/locales/de.json +60 -0
  183. package/locales/en.json +60 -0
  184. package/locales/es-419.json +60 -0
  185. package/locales/es.json +60 -0
  186. package/locales/fr.json +60 -0
  187. package/locales/hi.json +60 -0
  188. package/locales/id.json +60 -0
  189. package/locales/it.json +60 -0
  190. package/locales/nl.json +60 -0
  191. package/locales/pl.json +60 -0
  192. package/locales/pt-BR.json +60 -0
  193. package/locales/pt.json +60 -0
  194. package/locales/sr.json +60 -0
  195. package/locales/sv.json +60 -0
  196. package/locales/zh-Hans-CN.json +60 -0
  197. package/package.json +64 -0
  198. package/scripts/app.js +23 -0
  199. package/styles/app.css +73 -0
  200. package/styles/base/embedded.css +29 -0
  201. package/styles/base/forms.css +55 -0
  202. package/styles/base/grouping.css +43 -0
  203. package/styles/base/interactive.css +19 -0
  204. package/styles/base/sections.css +59 -0
  205. package/styles/base/tables.css +30 -0
  206. package/styles/base/text.css +71 -0
  207. package/styles/config/custom-properties.css +211 -0
  208. package/styles/scopes/flow.css +114 -0
  209. package/styles/utilities/container.css +6 -0
  210. package/styles/utilities/visually-hidden.css +10 -0
  211. package/styles/vendor/codemirror.css +116 -0
  212. package/styles/vendor/easy-markdown-editor.css +219 -0
  213. package/styles/vendor/markdown-it-prism.css +79 -0
@@ -0,0 +1,161 @@
1
+ tag-input-field {
2
+ display: block;
3
+ }
4
+
5
+ .tag-input {
6
+ background-color: var(--color-background);
7
+ border: var(--input-border-width) solid var(--color-outline-variant);
8
+ border-radius: var(--border-radius-small);
9
+ cursor: default;
10
+ display: flex;
11
+ flex-wrap: wrap;
12
+ font: var(--font-body);
13
+ gap: var(--space-2xs);
14
+ margin-block-end: var(--input-border-width-focus-offset);
15
+ overflow-x: auto;
16
+ padding-block: calc(var(--space-s) / 3);
17
+ padding-inline: var(--space-2xs);
18
+ position: relative;
19
+
20
+ & > * {
21
+ --fieldset-flow-space: 0;
22
+ }
23
+
24
+ &:focus-within {
25
+ border-color: var(--color-on-background);
26
+ border-width: var(--input-border-width-focus);
27
+ box-shadow: 0 0 0 var(--focus-width) var(--color-focus);
28
+ inset-block-start: calc(var(--input-border-width-focus-offset) * -1);
29
+ margin-block-end: calc(var(--input-border-width-focus-offset) * -2);
30
+ padding-inline-start: calc(
31
+ var(--space-2xs) - var(--input-border-width-focus-offset)
32
+ );
33
+ }
34
+ }
35
+
36
+ /* Input to add new tag */
37
+ .tag-input__input {
38
+ background-color: var(--color-background);
39
+ border: 0;
40
+ flex: 1;
41
+ font: var(--font-body);
42
+ inline-size: 100%;
43
+ padding-block: var(--space-2xs);
44
+ padding-inline: var(--space-xs);
45
+
46
+ &::placeholder {
47
+ color: var(--color-on-background);
48
+ opacity: 0.5;
49
+ }
50
+
51
+ &:disabled {
52
+ opacity: 0;
53
+ }
54
+
55
+ &:focus-visible {
56
+ box-shadow: none;
57
+ }
58
+ }
59
+
60
+ /* Input to edit existing tag */
61
+ .tag-input__edit {
62
+ background-color: transparent;
63
+ border: none;
64
+ font: var(--font-body);
65
+ opacity: 0;
66
+ padding: 0;
67
+ position: absolute;
68
+ z-index: -1;
69
+
70
+ &:focus {
71
+ box-shadow: none;
72
+ outline: none;
73
+ }
74
+ }
75
+
76
+ /* Remove button */
77
+ .tag-input__remove-button {
78
+ align-items: center;
79
+ background-color: transparent;
80
+ block-size: 1em;
81
+ border: none;
82
+ border-radius: var(--border-radius-small);
83
+ cursor: pointer;
84
+ display: inline-flex;
85
+ inline-size: 1em;
86
+ justify-content: center;
87
+ margin-inline-end: calc(var(--space-2xs) * -1);
88
+ margin-inline-start: var(--space-2xs);
89
+ outline: none;
90
+ padding: var(--space-2xs);
91
+ }
92
+
93
+ /* Remove button icon */
94
+ .tag-input__remove-icon {
95
+ block-size: 1em;
96
+ display: block;
97
+ inline-size: 1em;
98
+ stroke: currentcolor;
99
+ }
100
+
101
+ /* Tag - Editable */
102
+ .tag-input__tag--editable {
103
+ & .tag-input__edit {
104
+ opacity: 1;
105
+ position: static;
106
+ z-index: initial;
107
+ }
108
+
109
+ & .tag-input__text {
110
+ opacity: 0;
111
+ position: absolute;
112
+ white-space: nowrap;
113
+ z-index: -1;
114
+ }
115
+
116
+ & .tag-input__remove-button {
117
+ visibility: hidden;
118
+ }
119
+ }
120
+
121
+ /* Tag */
122
+ .tag-input__tag {
123
+ align-items: center;
124
+ background-color: var(--color-offset);
125
+ border-radius: var(--border-radius-small);
126
+ color: var(--color-on-offset);
127
+ display: inline-flex;
128
+ padding-block: calc(var(--space-2xs) / 2);
129
+ padding-inline: var(--space-xs);
130
+
131
+ &:hover {
132
+ background-color: var(--color-offset-variant);
133
+ }
134
+
135
+ &:hover .tag-input__remove-button:hover {
136
+ background-color: var(--color-shadow);
137
+ }
138
+ }
139
+
140
+ /* Disabled */
141
+ .tag-input--disabled {
142
+ background: var(--color-offset);
143
+ border-color: var(--color-offset);
144
+ color: var(--color-on-offset);
145
+ gap: 0;
146
+
147
+ & .tag-input__tag {
148
+ background-color: transparent;
149
+ color: var(--color-on-offset);
150
+ }
151
+ }
152
+
153
+ /* Tag - Selected */
154
+ .tag-input__tag--selected {
155
+ background-color: var(--color-primary);
156
+ color: var(--color-on-primary);
157
+
158
+ &:hover {
159
+ background-color: var(--color-primary-variant);
160
+ }
161
+ }
@@ -0,0 +1,29 @@
1
+ {% from "../input/macro.njk" import input with context %}
2
+ {% set tag = opts.localisation.tag | default(__("tagInput.defaultTag")) %}
3
+ {% set tags = opts.localisation.tags | default(__("tagInput.defaultTags")) %}
4
+ {{ input({
5
+ field: {
6
+ element: "tag-input-field",
7
+ attributes: {
8
+ placeholder: __("tagInput.placeholder", { tags: tags }),
9
+ "i18n-tag": "{{TAG}} " + tag + ".",
10
+ "i18n-edit": __("tagInput.edit", { tag: tag }),
11
+ "i18n-delete": __("tagInput.delete", { name: "{{TAG}}", tag: tag }),
12
+ "i18n-added": __("tagInput.added", { name: "{{TAG}}", tag: tag }),
13
+ "i18n-deleted": __("tagInput.deleted", { name: "{{TAG}}", tag: tag }),
14
+ "i18n-updated": __("tagInput.updated", { name: "{{TAG}}", tag: tag }),
15
+ "i18n-selected": __("tagInput.selected", { name: "{{TAG}}", tag: tag }),
16
+ "i18n-none-selected": __("tagInput.noneSelected", { tags: tags }),
17
+ "i18n-instruction": __("tagInput.instruction", { list: "{{TAGS}}", tags: tags })
18
+ }
19
+ },
20
+ classes: opts.classes,
21
+ id: opts.id or opts.name,
22
+ name: opts.name,
23
+ value: opts.value,
24
+ label: opts.label,
25
+ hint: opts.hint,
26
+ optional: opts.optional,
27
+ attributes: opts.attributes,
28
+ errorMessage: opts.errorMessage
29
+ }) }}
@@ -0,0 +1,298 @@
1
+ import EasyMDE from "easymde";
2
+
3
+ const paths = {
4
+ bold: "M17 30c6.1 0 10-3 10-8 0-3.5-2.7-6.3-6.5-6.5V15c3-.4 5-3 5-6 0-4.5-3.5-7-9-7H5v28h12ZM12 7h2c2.5 0 4 1 4 3 0 1.5-1.5 3-4 3h-2V7Zm0 18v-7h2.3c3.1 0 4.7 1.1 4.7 3.4 0 2.5-1.4 3.6-4.8 3.6H12Z",
5
+ code: "m13.5 8.5-3-3L2 14C.5 15.5.5 16.5 2 18l8.5 8.5 3-3L6 16l7.5-7.5Zm5 0 3-3L30 14c1.5 1.5 1.5 2.5 0 4l-8.5 8.5-3-3L26 16l-7.5-7.5Z",
6
+ fullscreen:
7
+ "M4 20H0v10c0 1.1.9 2 2 2h10v-4H4v-8Zm-4-8h4V4h8V0H2a2 2 0 0 0-2 2v10Zm28 16h-8v4h10a2 2 0 0 0 2-2V20h-4v8ZM20 0v4h8v8h4V2a2 2 0 0 0-2-2H20Z",
8
+ heading: "M27 30h-6V18H11v12H5V2h6v11h10V2h6z",
9
+ italic: "m21.5 30 .8-4h-6l4-20h6l.7-4H10l-.7 4h6l-4 20h-6l-.8 4z",
10
+ link: "M14 8v4H8a4 4 0 1 0 0 8h6v4H8A8 8 0 1 1 8 8h6Zm10 0a8 8 0 1 1 0 16h-6v-4h6a4 4 0 1 0 0-8h-6V8h6Zm-2 6v4H10v-4h12Z",
11
+ "ordered-list":
12
+ "M10 2h22v4H10Zm0 12h22v4H10Zm0 12h22v4H10ZM0 0h4v8H2V2H0V0Zm5 12h1v2l-3 4h3v2H0v-2l3.3-4H0v-2h5ZM0 26v-2h6v8H0v-2h4v-1H2v-2h2v-1H0Z",
13
+ quote:
14
+ "M4 6h7a3 3 0 0 1 3 3v6.6a4 4 0 0 1-1 2.5L6.6 26h-3l3-8H4a3 3 0 0 1-3-3V9a3 3 0 0 1 3-3Zm17 0h7a3 3 0 0 1 3 3v6.6a4 4 0 0 1-1 2.5L23.6 26h-3l3-8H21a3 3 0 0 1-3-3V9a3 3 0 0 1 3-3Z",
15
+ "side-by-side":
16
+ "M16 6c7 0 13.5 4 16 10-2.5 6-9 10-16 10S2.5 22 0 16C2.5 10 9 6 16 6Zm0 3a7 7 0 1 0 0 14 7 7 0 0 0 0-14Zm0 3a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z",
17
+ table:
18
+ "M32 2v28a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2C0 .9.9 0 2 0h28a2 2 0 0 1 2 2Zm-4 2H4v6h24V4ZM14 14H4v5h10v-5Zm0 9H4v5h10v-5Zm14-9H18v5h10v-5Zm0 9H18v5h10v-5Z",
19
+ undo: "M2 24h-.1A2 2 0 0 1 0 22V9h4v7.5A15 15 0 0 1 32 24h-4a11 11 0 0 0-21.2-4H15v4H2Z",
20
+ "unordered-list":
21
+ "M9 2h22v4H9Zm0 12h22v4H9Zm0 12h22v4H9ZM3 7a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm0 12a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm0 12a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z",
22
+ "upload-image":
23
+ "M2 0h28a2 2 0 0 1 2 2v28a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2C0 .9.9 0 2 0Zm26 24-9-14-7 11-3-4.8L4 24h24Z",
24
+ };
25
+
26
+ /**
27
+ * Get SVG icon
28
+ * @param {string} name - Icon name
29
+ * @returns {string} SVG
30
+ */
31
+ const getButtonSvg = (name) => {
32
+ return `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 32 32" focusable="false" aria-hidden="true">
33
+ <path fill="currentColor" d="${paths[name]}"/>
34
+ </svg>`;
35
+ };
36
+
37
+ /**
38
+ * Floating toolbar button definitions
39
+ * Maps button name to EasyMDE static action method name
40
+ */
41
+ const floatingToolbarButtons = [
42
+ { name: "bold", action: "toggleBold", title: "Bold" },
43
+ { name: "italic", action: "toggleItalic", title: "Italic" },
44
+ { name: "heading", action: "toggleHeadingSmaller", title: "Heading" },
45
+ { name: "code", action: "toggleCodeBlock", title: "Code" },
46
+ { name: "link", action: "drawLink", title: "Link" },
47
+ ];
48
+
49
+ /**
50
+ * Create an SVG element from a known icon path (safe — no user input)
51
+ * @param {string} name - Icon name from paths object
52
+ * @returns {SVGElement} SVG element
53
+ */
54
+ const createButtonSvgElement = (name) => {
55
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
56
+ svg.setAttribute("height", "1em");
57
+ svg.setAttribute("width", "1em");
58
+ svg.setAttribute("viewBox", "0 0 32 32");
59
+ svg.setAttribute("focusable", "false");
60
+ svg.setAttribute("aria-hidden", "true");
61
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
62
+ path.setAttribute("fill", "currentColor");
63
+ path.setAttribute("d", paths[name] || "");
64
+ svg.append(path);
65
+ return svg;
66
+ };
67
+
68
+ export const TextareaFieldComponent = class extends HTMLElement {
69
+ connectedCallback() {
70
+ this.editor = this.getAttribute("editor");
71
+
72
+ if (this.editor !== "") {
73
+ return;
74
+ }
75
+
76
+ this.editorEndpoint = this.getAttribute("editor-endpoint");
77
+ this.editorId = this.getAttribute("editor-id");
78
+ this.editorImageUpload = this.getAttribute("editor-image-upload");
79
+ this.editorLocale = this.getAttribute("editor-locale");
80
+ this.editorStatus = this.getAttribute("editor-status");
81
+ this.editorToolbar = this.getAttribute("editor-toolbar");
82
+ this.$label = this.querySelector("label");
83
+ this.$textarea = this.querySelector("textarea");
84
+
85
+ const status =
86
+ this?.editorStatus === "false"
87
+ ? false
88
+ : [
89
+ ...(this.editorImageUpload === "false" ? [] : ["upload-image"]),
90
+ "words",
91
+ "characters",
92
+ "autosave",
93
+ ];
94
+
95
+ const toolbar =
96
+ this?.editorToolbar === "false"
97
+ ? false
98
+ : [
99
+ "bold",
100
+ "italic",
101
+ "heading",
102
+ "quote",
103
+ "ordered-list",
104
+ "unordered-list",
105
+ "table",
106
+ "code",
107
+ "link",
108
+ ...(this.editorImageUpload === "false" ? [] : ["upload-image"]),
109
+ "|",
110
+ "undo",
111
+ "side-by-side",
112
+ "fullscreen",
113
+ ];
114
+
115
+ const editor = new EasyMDE({
116
+ autoDownloadFontAwesome: false,
117
+ autosave: {
118
+ enabled: true,
119
+ uniqueId: this.editorId || this.$textarea.id,
120
+ timeFormat: { locale: this.editorLocale || "en" },
121
+ },
122
+ blockStyles: {
123
+ bold: "**",
124
+ italic: "_",
125
+ },
126
+ element: this.$textarea,
127
+ imageUploadEndpoint: this.editorEndpoint,
128
+ imageUploadFunction: this.uploadFile,
129
+ minHeight: "6rem",
130
+ previewClass: ["editor-preview", "s-flow"],
131
+ status,
132
+ // @ts-ignore
133
+ toolbar,
134
+ unorderedListStyle: "-",
135
+ });
136
+
137
+ // Restore label behaviour
138
+ /** @type {HTMLTextAreaElement} */
139
+ const $codeMirrorTextarea = this.querySelector(".CodeMirror textarea");
140
+ this.$label.addEventListener("click", () => {
141
+ $codeMirrorTextarea.focus();
142
+ });
143
+
144
+ // Update character count
145
+ /** @type {HTMLElement} */
146
+ const $characters = this.querySelector(".editor-statusbar .characters");
147
+ editor.codemirror.on("update", () => {
148
+ if ($characters) {
149
+ $characters.innerHTML = String(editor.value().length);
150
+ }
151
+ });
152
+
153
+ const $editorToolbar = this.querySelector(".editor-toolbar");
154
+ if ($editorToolbar) {
155
+ // Use custom SVG icons
156
+ const buttons = $editorToolbar.querySelectorAll("button");
157
+ for (const button of buttons) {
158
+ button.innerHTML = getButtonSvg(button.classList[0]);
159
+ }
160
+
161
+ // Get toolbar height to offset editor and preview in fullscreen mode
162
+ const resizeObserver = new ResizeObserver((entries) => {
163
+ for (const entry of entries) {
164
+ this.style.setProperty(
165
+ "--toolbar-height",
166
+ `${entry.contentRect.height}px`,
167
+ );
168
+ }
169
+ });
170
+
171
+ resizeObserver.observe($editorToolbar);
172
+ }
173
+
174
+ // Floating selection toolbar
175
+ this._setupFloatingToolbar(editor);
176
+ }
177
+
178
+ /**
179
+ * Create and manage floating toolbar that appears on text selection
180
+ * @param {EasyMDE} editor - EasyMDE instance
181
+ */
182
+ _setupFloatingToolbar(editor) {
183
+ const cm = editor.codemirror;
184
+
185
+ // Create floating toolbar element
186
+ const $floating = document.createElement("div");
187
+ $floating.className = "editor-floating-toolbar";
188
+ $floating.setAttribute("role", "toolbar");
189
+ $floating.setAttribute("aria-label", "Formatting");
190
+
191
+ for (const btn of floatingToolbarButtons) {
192
+ const button = document.createElement("button");
193
+ button.type = "button";
194
+ button.className = `floating-btn floating-btn--${btn.name}`;
195
+ button.title = btn.title;
196
+ button.append(createButtonSvgElement(btn.name));
197
+ button.addEventListener("mousedown", (event) => {
198
+ // Prevent mousedown from stealing focus/clearing selection
199
+ event.preventDefault();
200
+ EasyMDE[btn.action](editor);
201
+ });
202
+ $floating.append(button);
203
+ }
204
+
205
+ this.append($floating);
206
+ this._$floatingToolbar = $floating;
207
+
208
+ // Track selection changes
209
+ cm.on("cursorActivity", () => {
210
+ const selection = cm.getSelection();
211
+ if (selection && selection.length > 0) {
212
+ this._showFloatingToolbar(cm);
213
+ } else {
214
+ this._hideFloatingToolbar();
215
+ }
216
+ });
217
+
218
+ // Hide on blur
219
+ cm.on("blur", () => {
220
+ setTimeout(() => this._hideFloatingToolbar(), 150);
221
+ });
222
+
223
+ // Reposition on scroll
224
+ const $cmScroll = this.querySelector(".CodeMirror-scroll");
225
+ if ($cmScroll) {
226
+ $cmScroll.addEventListener("scroll", () => {
227
+ if (this._$floatingToolbar.classList.contains("is-visible")) {
228
+ const selection = cm.getSelection();
229
+ if (selection && selection.length > 0) {
230
+ this._showFloatingToolbar(cm);
231
+ } else {
232
+ this._hideFloatingToolbar();
233
+ }
234
+ }
235
+ });
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Position and show the floating toolbar above the current selection
241
+ * @param {object} cm - CodeMirror instance
242
+ */
243
+ _showFloatingToolbar(cm) {
244
+ const $floating = this._$floatingToolbar;
245
+ const $cmElement = this.querySelector(".CodeMirror");
246
+ if (!$cmElement) return;
247
+
248
+ // Get coordinates of selection start relative to editor
249
+ const cursor = cm.getCursor("from");
250
+ const coords = cm.cursorCoords(cursor, "local");
251
+ const cmRect = $cmElement.getBoundingClientRect();
252
+ const containerRect = this.getBoundingClientRect();
253
+
254
+ // Position above the selection line
255
+ const top = coords.top + cmRect.top - containerRect.top - 44;
256
+ const left = coords.left + cmRect.left - containerRect.left;
257
+
258
+ // Clamp within container bounds
259
+ const maxLeft = containerRect.width - ($floating.offsetWidth || 200) - 8;
260
+ $floating.style.top = `${Math.max(0, top)}px`;
261
+ $floating.style.left = `${Math.max(8, Math.min(left, maxLeft))}px`;
262
+ $floating.classList.add("is-visible");
263
+ }
264
+
265
+ /**
266
+ * Hide the floating toolbar
267
+ */
268
+ _hideFloatingToolbar() {
269
+ if (this._$floatingToolbar) {
270
+ this._$floatingToolbar.classList.remove("is-visible");
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Upload file
276
+ * @param {object} file - File
277
+ * @param {Function} onSuccess - Success callback
278
+ * @param {Function} onError - Error callback
279
+ * @returns {Promise<string>} - File URL or error message
280
+ */
281
+ async uploadFile(file, onSuccess, onError) {
282
+ const formData = new FormData();
283
+ formData.append("file", file);
284
+
285
+ try {
286
+ const endpointResponse = await fetch(this.options.imageUploadEndpoint, {
287
+ method: "POST",
288
+ body: formData,
289
+ });
290
+
291
+ return endpointResponse.ok
292
+ ? onSuccess(endpointResponse.headers.get("location"))
293
+ : onError(endpointResponse.statusText);
294
+ } catch (error) {
295
+ onError(error.message);
296
+ }
297
+ }
298
+ };
@@ -0,0 +1,3 @@
1
+ {% macro textarea(opts) %}
2
+ {%- include "./template.njk" -%}
3
+ {% endmacro %}
@@ -0,0 +1,42 @@
1
+ textarea-field {
2
+ display: block;
3
+ }
4
+
5
+ .textarea {
6
+ appearance: none;
7
+ background-color: var(--color-background);
8
+ border: var(--input-border-width) solid var(--color-outline-variant);
9
+ border-radius: var(--border-radius-small);
10
+ font: var(--font-body);
11
+ inline-size: 100%;
12
+ margin-block-end: var(--input-border-width-focus-offset);
13
+ padding-block: calc(var(--space-s) / 2);
14
+ padding-inline: var(--space-s);
15
+
16
+ &:focus-visible {
17
+ border-color: var(--color-on-background);
18
+ border-width: var(--input-border-width-focus);
19
+ inset-block-start: calc(var(--input-border-width-focus-offset) * -1);
20
+ margin-block-end: calc(var(--input-border-width-focus-offset) * -1);
21
+ padding-inline-start: calc(
22
+ var(--space-s) - var(--input-border-width-focus-offset)
23
+ );
24
+ }
25
+
26
+ &:disabled,
27
+ &[readonly] {
28
+ background: var(--color-offset);
29
+ border-color: var(--color-offset);
30
+ color: var(--color-on-offset);
31
+ resize: none;
32
+ }
33
+ }
34
+
35
+ .textarea--error {
36
+ border-color: var(--color-error);
37
+ }
38
+
39
+ .textarea--monospace {
40
+ font: var(--font-code);
41
+ font-variant-numeric: tabular-nums;
42
+ }
@@ -0,0 +1,40 @@
1
+ {% from "../error-message/macro.njk" import errorMessage with context %}
2
+ {% from "../field/macro.njk" import field with context %}
3
+ {% from "../hint/macro.njk" import hint with context %}
4
+ {% from "../label/macro.njk" import label with context %}
5
+ {% set id = opts.id or opts.name | slugify({ decamelize: true }) %}
6
+ {% set describedBy = opts.describedBy if opts.describedBy else "" %}
7
+ {% call field({
8
+ element: "textarea-field",
9
+ classes: opts.field.classes,
10
+ attributes: opts.field.attributes
11
+ }) %}
12
+ {{- label({
13
+ for: id,
14
+ optional: opts.optional === true,
15
+ text: opts.label
16
+ }) if opts.label }}
17
+ {% if opts.hint %}
18
+ {%- set hintId = id + "-hint" %}
19
+ {%- set describedBy = describedBy + " " + hintId if describedBy else hintId %}
20
+ {{- hint({
21
+ id: hintId,
22
+ text: opts.hint
23
+ }) }}
24
+ {% endif %}
25
+ {% if opts.errorMessage %}
26
+ {%- set errorId = id + "-error" %}
27
+ {%- set describedBy = describedBy + " " + errorId if describedBy else errorId %}
28
+ {{- errorMessage({
29
+ id: errorId,
30
+ classes: opts.errorMessage.classes,
31
+ attributes: opts.errorMessage.attributes,
32
+ text: opts.errorMessage.text,
33
+ visuallyHiddenText: opts.errorMessage.visuallyHiddenText
34
+ }) }}
35
+ {% endif %}
36
+ <textarea class="{{ classes("textarea", opts) }}" id="{{ id }}" name="{{ opts.name }}" rows="{{ opts.rows or 3 }}"
37
+ {%- if describedBy %} aria-describedby="{{ describedBy }}"{% endif %}
38
+ {%- if opts.autocomplete %} autocomplete="{{ opts.autocomplete}}"{% endif %}
39
+ {{- attributes(opts.attributes) }}>{{ opts.value }}</textarea>
40
+ {%- endcall %}
@@ -0,0 +1,3 @@
1
+ {% macro user(opts) %}
2
+ {%- include "./template.njk" -%}
3
+ {% endmacro %}
@@ -0,0 +1,20 @@
1
+ .user {
2
+ align-items: center;
3
+ display: flex;
4
+ gap: var(--space-xs);
5
+ }
6
+
7
+ .user__body {
8
+ display: flex;
9
+ flex-direction: column;
10
+ gap: var(--space-2xs);
11
+ }
12
+
13
+ .user__name {
14
+ font: var(--user-name-font, var(--font-label));
15
+ }
16
+
17
+ .user__meta {
18
+ color: var(--color-on-offset);
19
+ font: var(--font-caption);
20
+ }
@@ -0,0 +1,15 @@
1
+ {% from "../avatar/macro.njk" import avatar with context %}
2
+ <div class="{{ classes("user", opts) }}">
3
+ {{ avatar(opts.avatar) if opts.avatar }}
4
+
5
+ <div class="user__body">
6
+ {% if opts.url %}
7
+ <a class="user__name" href="{{ opts.url }}">{{- opts.name | safe -}}</a>
8
+ {% else %}
9
+ <span class="user__name">{{- opts.name | safe -}}</span>
10
+ {% endif %}
11
+ {% if opts.meta %}
12
+ <span class="user__meta">{{ opts.meta | safe }}</span>
13
+ {% endif %}
14
+ </div>
15
+ </div>
@@ -0,0 +1,3 @@
1
+ {% macro warningText(opts) %}
2
+ {%- include "./template.njk" -%}
3
+ {% endmacro %}
@@ -0,0 +1,15 @@
1
+ .warning-text {
2
+ --icon-margin: var(--space-s);
3
+ --icon-size: 1.5em;
4
+ align-items: start;
5
+ background-color: var(--color-offset);
6
+ border-radius: var(--border-radius-small);
7
+ display: flex;
8
+ flex-direction: row;
9
+ font: var(--font-label);
10
+ padding: var(--space-m);
11
+
12
+ & .warning-text__text {
13
+ margin-block-start: var(--space-2xs);
14
+ }
15
+ }
@@ -0,0 +1,8 @@
1
+ <div class="{{ classes("warning-text", opts) }}"
2
+ {{- attributes(opts.attributes) }}>
3
+ {{ icon(opts.icon | default("warning")) }}
4
+ <strong class="warning-text__text">
5
+ {% if not opts.iconFallbackText %}<span class="-!-visually-hidden">{{ opts.iconFallbackText | default(__("warning")) }}</span>{% endif %}
6
+ {{ opts.text | safe }}
7
+ </strong>
8
+ </div>
@@ -0,0 +1,3 @@
1
+ {% macro widget(opts) %}
2
+ {%- include "./template.njk" -%}
3
+ {% endmacro %}
@@ -0,0 +1,28 @@
1
+ .widget-grid {
2
+ columns: 20rem auto;
3
+ gap: var(--space-l);
4
+ }
5
+
6
+ .widget {
7
+ --button-font: var(--font-caption);
8
+ background-color: var(--color-offset);
9
+ border-radius: var(--border-radius-small);
10
+ display: inline-block;
11
+ margin-block-end: var(--space-l);
12
+ min-inline-size: 100%;
13
+ padding: var(--space-s);
14
+ }
15
+
16
+ .widget__header {
17
+ align-items: center;
18
+ display: flex;
19
+ flex-wrap: wrap;
20
+ gap: var(--space-s);
21
+ justify-content: space-between;
22
+ padding-block-end: var(--space-s);
23
+ }
24
+
25
+ .widget__title {
26
+ --icon-size: 1.25em;
27
+ font: var(--font-label);
28
+ }