@kenlo/sds-design-system 0.9.9

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 (246) hide show
  1. package/README.md +129 -0
  2. package/components/Accordion/Accordion.css +186 -0
  3. package/components/Accordion/accordion-variants.js +17 -0
  4. package/components/Accordion/accordion.marko +264 -0
  5. package/components/AccordionGroup/AccordionGroup.css +35 -0
  6. package/components/AccordionGroup/accordion-group.marko +17 -0
  7. package/components/Alert/Alert.css +104 -0
  8. package/components/Alert/alert-variants.js +35 -0
  9. package/components/Alert/alert.marko +94 -0
  10. package/components/AttachmentList/AttachmentList.css +92 -0
  11. package/components/AttachmentList/attachment-list.marko +51 -0
  12. package/components/AttachmentList/attachment-row.marko +167 -0
  13. package/components/Avatar/Avatar.css +110 -0
  14. package/components/Avatar/avatar-variants.js +12 -0
  15. package/components/Avatar/avatar.marko +71 -0
  16. package/components/Badge/Badge.css +381 -0
  17. package/components/Badge/badge-dismissible.marko +143 -0
  18. package/components/Badge/badge-variants.js +35 -0
  19. package/components/Badge/badge.marko +134 -0
  20. package/components/BottomSheet/BottomSheet.css +300 -0
  21. package/components/BottomSheet/bottom-sheet.marko +340 -0
  22. package/components/BulkActionBar/BulkActionBar.css +88 -0
  23. package/components/BulkActionBar/bulk-action-bar.marko +186 -0
  24. package/components/Button/Button.css +340 -0
  25. package/components/Button/button-variants.js +25 -0
  26. package/components/Button/button.marko +139 -0
  27. package/components/Card/Card.css +391 -0
  28. package/components/Card/card-body.marko +5 -0
  29. package/components/Card/card-collapsible.marko +94 -0
  30. package/components/Card/card-footer.marko +12 -0
  31. package/components/Card/card-header.marko +30 -0
  32. package/components/Card/card-info-pair.marko +17 -0
  33. package/components/Card/card-info-row.marko +12 -0
  34. package/components/Card/card-section.marko +14 -0
  35. package/components/Card/card-variants.js +24 -0
  36. package/components/Card/card.marko +124 -0
  37. package/components/Checkbox/Checkbox.css +249 -0
  38. package/components/Checkbox/checkbox-variants.js +21 -0
  39. package/components/Checkbox/checkbox.marko +128 -0
  40. package/components/ColorPicker/ColorPicker.css +253 -0
  41. package/components/ColorPicker/color-maths.js +104 -0
  42. package/components/ColorPicker/color-picker.marko +474 -0
  43. package/components/DatePicker/DatePicker.css +429 -0
  44. package/components/DatePicker/date-logic.js +225 -0
  45. package/components/DatePicker/date-picker.marko +492 -0
  46. package/components/Drawer/Drawer.css +174 -0
  47. package/components/Drawer/drawer.marko +262 -0
  48. package/components/Floating/Floating.css +28 -0
  49. package/components/Floating/floating-position.js +251 -0
  50. package/components/Floating/floating.marko +291 -0
  51. package/components/Form/Form.css +15 -0
  52. package/components/Form/form-context.js +69 -0
  53. package/components/Form/form.marko +73 -0
  54. package/components/GradientPicker/GradientPicker.css +179 -0
  55. package/components/GradientPicker/gradient-picker.marko +398 -0
  56. package/components/Grid/Grid.css +161 -0
  57. package/components/Grid/col.marko +36 -0
  58. package/components/Grid/grid-variants.js +43 -0
  59. package/components/Grid/grid.marko +25 -0
  60. package/components/Header/Header.css +457 -0
  61. package/components/Header/header.marko +523 -0
  62. package/components/Icon/Icon.css +44 -0
  63. package/components/Icon/icon-colors.js +31 -0
  64. package/components/Icon/icon.marko +56 -0
  65. package/components/IconButton/IconButton.css +85 -0
  66. package/components/IconButton/icon-button-variants.js +18 -0
  67. package/components/IconButton/icon-button.marko +103 -0
  68. package/components/Incrementer/Incrementer.css +94 -0
  69. package/components/Incrementer/incrementer.marko +70 -0
  70. package/components/Input/Input.css +547 -0
  71. package/components/Input/input-countries.js +109 -0
  72. package/components/Input/input-masks.js +218 -0
  73. package/components/Input/input-types.js +194 -0
  74. package/components/Input/input.marko +530 -0
  75. package/components/Menu/Menu.css +651 -0
  76. package/components/Menu/menu-variants.js +89 -0
  77. package/components/Menu/menu.marko +560 -0
  78. package/components/Modal/Modal.css +160 -0
  79. package/components/Modal/modal.marko +296 -0
  80. package/components/Nav/Nav.css +298 -0
  81. package/components/Nav/nav-variants.js +23 -0
  82. package/components/Nav/nav.marko +164 -0
  83. package/components/Notification/Notification.css +78 -0
  84. package/components/Notification/notification-variants.js +30 -0
  85. package/components/Notification/notification.marko +55 -0
  86. package/components/Pagination/Pagination.css +78 -0
  87. package/components/Pagination/pagination-pages.js +66 -0
  88. package/components/Pagination/pagination.marko +141 -0
  89. package/components/Pill/Pill.css +193 -0
  90. package/components/Pill/pill-variants.js +16 -0
  91. package/components/Pill/pill.marko +165 -0
  92. package/components/Popover/Popover.css +121 -0
  93. package/components/Popover/popover.marko +96 -0
  94. package/components/Radio/Radio.css +230 -0
  95. package/components/Radio/radio-variants.js +16 -0
  96. package/components/Radio/radio.marko +93 -0
  97. package/components/RadioGroup/RadioGroup.css +32 -0
  98. package/components/RadioGroup/radio-group.marko +93 -0
  99. package/components/SearchBar/SearchBar.css +181 -0
  100. package/components/SearchBar/search-bar-variants.js +24 -0
  101. package/components/SearchBar/search-bar.marko +157 -0
  102. package/components/Select/Select.css +427 -0
  103. package/components/Select/select-options.js +163 -0
  104. package/components/Select/select.marko +449 -0
  105. package/components/Separator/Separator.css +22 -0
  106. package/components/Separator/separator.marko +14 -0
  107. package/components/Skeleton/Skeleton.css +100 -0
  108. package/components/Skeleton/skeleton-avatar.marko +28 -0
  109. package/components/Skeleton/skeleton-button.marko +22 -0
  110. package/components/Skeleton/skeleton-image.marko +20 -0
  111. package/components/Skeleton/skeleton-input.marko +22 -0
  112. package/components/Skeleton/skeleton-variants.js +16 -0
  113. package/components/Skeleton/skeleton.marko +53 -0
  114. package/components/Spinner/Spinner.css +173 -0
  115. package/components/Spinner/spinner-geometry.js +39 -0
  116. package/components/Spinner/spinner-overlay.marko +97 -0
  117. package/components/Spinner/spinner.marko +88 -0
  118. package/components/StatCard/StatCard.css +90 -0
  119. package/components/StatCard/stat-card-group.marko +5 -0
  120. package/components/StatCard/stat-card.marko +57 -0
  121. package/components/Switch/Switch.css +209 -0
  122. package/components/Switch/switch.marko +99 -0
  123. package/components/Table/Table.css +270 -0
  124. package/components/Table/table-cell.marko +28 -0
  125. package/components/Table/table-header.marko +27 -0
  126. package/components/Table/table-logic.js +129 -0
  127. package/components/Table/table.marko +372 -0
  128. package/components/Text/Text.css +79 -0
  129. package/components/Text/text-variants.js +25 -0
  130. package/components/Text/text.marko +48 -0
  131. package/components/TextEditor/TextEditor.css +78 -0
  132. package/components/TextEditor/safe-url.js +50 -0
  133. package/components/TextEditor/text-editor-commands.js +22 -0
  134. package/components/TextEditor/text-editor.marko +253 -0
  135. package/components/Toast/Toast.css +88 -0
  136. package/components/Toast/toast-api.js +19 -0
  137. package/components/Toast/toast-store.js +118 -0
  138. package/components/Toast/toast-variants.js +16 -0
  139. package/components/Toast/toast.marko +93 -0
  140. package/components/Toast/toaster.marko +70 -0
  141. package/components/Toggle/Toggle.css +169 -0
  142. package/components/Toggle/toggle.marko +93 -0
  143. package/components/Tooltip/Tooltip.css +115 -0
  144. package/components/Tooltip/tooltip.marko +77 -0
  145. package/components/Uploader/Uploader.css +180 -0
  146. package/components/Uploader/crop-geometry.js +161 -0
  147. package/components/Uploader/uploader.marko +269 -0
  148. package/foundations/Scrollbar/Scrollbar.css +26 -0
  149. package/foundations/attrs.js +27 -0
  150. package/foundations/breakpoints.js +27 -0
  151. package/foundations/contrast.js +74 -0
  152. package/foundations/custom-scheme.js +222 -0
  153. package/foundations/overlay.js +108 -0
  154. package/foundations/styles.css +284 -0
  155. package/foundations/text.js +22 -0
  156. package/foundations/theme-server.js +81 -0
  157. package/foundations/theme.js +42 -0
  158. package/foundations/tokens.css +1656 -0
  159. package/icons/custom/custom-video-horizontal-filled.marko +6 -0
  160. package/icons/custom/custom-video-horizontal-regular.marko +6 -0
  161. package/icons/custom/custom-video-tour-3d-filled.marko +7 -0
  162. package/icons/custom/custom-video-tour-3d-regular.marko +9 -0
  163. package/icons/custom/custom-video-vertical-filled.marko +6 -0
  164. package/icons/custom/custom-video-vertical-regular.marko +6 -0
  165. package/icons/generated/fluent-add-regular.marko +3 -0
  166. package/icons/generated/fluent-alert-regular.marko +3 -0
  167. package/icons/generated/fluent-apps-regular.marko +3 -0
  168. package/icons/generated/fluent-arrow-download-regular.marko +3 -0
  169. package/icons/generated/fluent-arrow-left-regular.marko +3 -0
  170. package/icons/generated/fluent-arrow-right-regular.marko +3 -0
  171. package/icons/generated/fluent-arrow-rotate-clockwise-regular.marko +3 -0
  172. package/icons/generated/fluent-arrow-rotate-counterclockwise-regular.marko +3 -0
  173. package/icons/generated/fluent-border-all-regular.marko +3 -0
  174. package/icons/generated/fluent-calendar-regular.marko +3 -0
  175. package/icons/generated/fluent-checkmark-circle-filled.marko +3 -0
  176. package/icons/generated/fluent-checkmark-regular.marko +3 -0
  177. package/icons/generated/fluent-chevron-down-regular.marko +3 -0
  178. package/icons/generated/fluent-chevron-left-regular.marko +3 -0
  179. package/icons/generated/fluent-chevron-right-regular.marko +3 -0
  180. package/icons/generated/fluent-chevron-up-regular.marko +3 -0
  181. package/icons/generated/fluent-circle-half-fill-regular.marko +3 -0
  182. package/icons/generated/fluent-clock-regular.marko +3 -0
  183. package/icons/generated/fluent-color-regular.marko +3 -0
  184. package/icons/generated/fluent-crop-regular.marko +3 -0
  185. package/icons/generated/fluent-dark-theme-regular.marko +3 -0
  186. package/icons/generated/fluent-delete-regular.marko +3 -0
  187. package/icons/generated/fluent-desktop-regular.marko +3 -0
  188. package/icons/generated/fluent-dismiss-circle-filled.marko +3 -0
  189. package/icons/generated/fluent-dismiss-regular.marko +3 -0
  190. package/icons/generated/fluent-document-pdf-regular.marko +3 -0
  191. package/icons/generated/fluent-document-regular.marko +3 -0
  192. package/icons/generated/fluent-eye-off-regular.marko +3 -0
  193. package/icons/generated/fluent-eye-regular.marko +3 -0
  194. package/icons/generated/fluent-eyedropper-regular.marko +3 -0
  195. package/icons/generated/fluent-grid-regular.marko +3 -0
  196. package/icons/generated/fluent-history-regular.marko +3 -0
  197. package/icons/generated/fluent-home-regular.marko +3 -0
  198. package/icons/generated/fluent-image-regular.marko +3 -0
  199. package/icons/generated/fluent-info-regular.marko +3 -0
  200. package/icons/generated/fluent-link-regular.marko +3 -0
  201. package/icons/generated/fluent-more-horizontal-regular.marko +3 -0
  202. package/icons/generated/fluent-navigation-regular.marko +3 -0
  203. package/icons/generated/fluent-paint-brush-regular.marko +3 -0
  204. package/icons/generated/fluent-question-circle-regular.marko +3 -0
  205. package/icons/generated/fluent-search-regular.marko +3 -0
  206. package/icons/generated/fluent-settings-regular.marko +3 -0
  207. package/icons/generated/fluent-square-hint-regular.marko +3 -0
  208. package/icons/generated/fluent-subtract-regular.marko +3 -0
  209. package/icons/generated/fluent-text-align-center-regular.marko +3 -0
  210. package/icons/generated/fluent-text-align-left-regular.marko +3 -0
  211. package/icons/generated/fluent-text-align-right-regular.marko +3 -0
  212. package/icons/generated/fluent-text-bold-regular.marko +3 -0
  213. package/icons/generated/fluent-text-bullet-list-regular.marko +3 -0
  214. package/icons/generated/fluent-text-font-regular.marko +3 -0
  215. package/icons/generated/fluent-text-font-size-regular.marko +3 -0
  216. package/icons/generated/fluent-text-italic-regular.marko +3 -0
  217. package/icons/generated/fluent-text-number-list-ltr-regular.marko +3 -0
  218. package/icons/generated/fluent-video-regular.marko +3 -0
  219. package/icons/generated/fluent-warning-filled.marko +3 -0
  220. package/icons/generated/fluent-weather-moon-regular.marko +3 -0
  221. package/icons/generated/fluent-weather-sunny-regular.marko +3 -0
  222. package/icons/generated/fluent-zoom-in-regular.marko +3 -0
  223. package/icons/generated/fluent-zoom-out-regular.marko +3 -0
  224. package/icons/manifest.json +61 -0
  225. package/icons/social/social-facebook-color.marko +7 -0
  226. package/icons/social/social-facebook.marko +6 -0
  227. package/icons/social/social-google-color.marko +20 -0
  228. package/icons/social/social-google.marko +6 -0
  229. package/icons/social/social-instagram-color.marko +16 -0
  230. package/icons/social/social-instagram.marko +6 -0
  231. package/icons/social/social-linkedin-color.marko +8 -0
  232. package/icons/social/social-linkedin.marko +8 -0
  233. package/icons/social/social-messenger-color.marko +15 -0
  234. package/icons/social/social-messenger.marko +6 -0
  235. package/icons/social/social-pinterest-color.marko +6 -0
  236. package/icons/social/social-pinterest.marko +6 -0
  237. package/icons/social/social-telegram-color.marko +7 -0
  238. package/icons/social/social-telegram.marko +6 -0
  239. package/icons/social/social-whatsapp-color.marko +8 -0
  240. package/icons/social/social-whatsapp.marko +7 -0
  241. package/icons/social/social-x-color.marko +7 -0
  242. package/icons/social/social-x.marko +6 -0
  243. package/icons/social/social-youtube-color.marko +7 -0
  244. package/icons/social/social-youtube.marko +6 -0
  245. package/marko.json +203 -0
  246. package/package.json +55 -0
@@ -0,0 +1,78 @@
1
+ /* SDS TextEditor.
2
+ Ported from kdls-design-system v2.5.1 (0f3edf8) components/TextEditor/TextEditor.css
3
+ by scripts/port-css.mjs. Divergences beyond the mechanical rename are noted inline.
4
+ *//* ============================================================
5
+ SDS TextEditor
6
+ ============================================================ */
7
+
8
+ .sds-text-editor {
9
+ display: flex;
10
+ flex-direction: column;
11
+ border: 1px solid var(--border-interactive-color);
12
+ border-radius: var(--radius-md);
13
+ background: var(--component-bg-color);
14
+ font-family: var(--font-family-base);
15
+ overflow: hidden;
16
+ }
17
+ /* ── Toolbar ── */
18
+ .sds-text-editor__toolbar {
19
+ display: flex;
20
+ align-items: center;
21
+ gap: var(--spacing-4);
22
+ padding: var(--spacing-2) var(--spacing-3);
23
+ border-bottom: 1px solid var(--border-interactive-color);
24
+ flex-wrap: wrap;
25
+ }
26
+ /* ── Divider ── */
27
+ .sds-text-editor__divider {
28
+ width: 1px;
29
+ height: 20px;
30
+ background: var(--border-interactive-color);
31
+ margin: 0 var(--spacing-1);
32
+ flex-shrink: 0;
33
+ }
34
+ /* ── Content area ── */
35
+ .sds-text-editor__body {
36
+ padding: var(--spacing-4);
37
+ outline: none;
38
+ color: var(--body-color);
39
+ font-size: var(--font-size-body-md);
40
+ line-height: var(--lh-body-md);
41
+ min-height: var(--te-min-height, 200px);
42
+ word-break: break-word;
43
+ }
44
+ .sds-text-editor__body:empty::before {
45
+ content: attr(data-placeholder);
46
+ color: var(--placeholder-color);
47
+ pointer-events: none;
48
+ }
49
+ /* Images and videos inside editor */
50
+ .sds-text-editor__body img,
51
+ .sds-text-editor__body iframe {
52
+ max-width: 100%;
53
+ border-radius: var(--radius-sm);
54
+ display: block;
55
+ margin: var(--spacing-2) 0;
56
+ }
57
+ /* Links inside editor */
58
+ .sds-text-editor__body a {
59
+ color: var(--color-primary-50-solid);
60
+ text-decoration: underline;
61
+ }
62
+ /* Lists inside editor */
63
+ .sds-text-editor__body ul,
64
+ .sds-text-editor__body ol {
65
+ padding-left: var(--spacing-5);
66
+ margin: var(--spacing-2) 0;
67
+ }
68
+
69
+
70
+ /* The writing area takes focus and is the main thing on the component, so it shows it.
71
+ KDLS gives it no focus style: a contenteditable div with no ring is a control a keyboard
72
+ user cannot tell they have reached. */
73
+ .sds-text-editor__body:focus-visible {
74
+ outline: var(--focus-ring-width) solid var(--focus-stroke-color);
75
+ outline-offset: -2px;
76
+ }
77
+
78
+ /* The toolbar's own buttons are sds-icon-button and sds-menu, which carry their own. */
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Is this URL safe to put in a document?
3
+ *
4
+ * KDLS's TextEditor asks the visitor for a URL and builds HTML from the answer:
5
+ *
6
+ * exec('insertHTML', `<img src="${url}" style="max-width:100%" />`)
7
+ *
8
+ * Nothing checks the string. An answer that closes the src attribute and opens an event
9
+ * handler instead — `x" onerror="...` — is handed to onChange, stored, and rendered on a
10
+ * public page for every later visitor. That is stored XSS, and a design system for public
11
+ * sites is the last place it belongs. The same hole is in the video insert, and in
12
+ * createLink, where the scheme `javascript:` on its own is enough.
13
+ *
14
+ * So a URL is checked before it is used, and the HTML is built through the DOM rather than
15
+ * by string concatenation — see text-editor.marko.
16
+ */
17
+
18
+ /** Schemes that execute rather than fetch. `data:` is here because an SVG can carry script. */
19
+ const DANGEROUS = /^(javascript|data|vbscript|file|blob):/i;
20
+
21
+ /** Everything a browser strips before it parses a scheme, which is how `java\nscript:` gets in. */
22
+ const IGNORED = /[\u0000-\u0020]+/g;
23
+
24
+ /**
25
+ * A URL the editor will accept, or null.
26
+ *
27
+ * Relative URLs and fragments pass: they name no scheme, so they cannot name a dangerous
28
+ * one. Everything absolute must be http or https.
29
+ */
30
+ export function safeUrl(raw) {
31
+ if (typeof raw !== 'string') return null;
32
+
33
+ const url = raw.replace(IGNORED, '');
34
+ if (!url) return null;
35
+ if (DANGEROUS.test(url)) return null;
36
+
37
+ // A scheme is letters, digits, +, - or . before a colon. Anything else is relative.
38
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(url);
39
+ if (!scheme) return url;
40
+
41
+ return /^https?$/i.test(scheme[1]) ? url : null;
42
+ }
43
+
44
+ /** True when the URL can be the source of an embedded frame. */
45
+ export function safeEmbedUrl(raw) {
46
+ const url = safeUrl(raw);
47
+ if (!url) return null;
48
+ // An embed must be absolute: a relative one frames the page inside itself.
49
+ return /^https?:/i.test(url) ? url : null;
50
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * What the toolbar can do.
3
+ *
4
+ * Ported from kdls-design-system v2.5.1 (0f3edf8) components/TextEditor/TextEditor.tsx,
5
+ * where the same two lists sit at module scope. Pulled into their own file so the portal
6
+ * and the tests can name a command without reaching into the template.
7
+ */
8
+
9
+ export const FONT_SIZES = ['12', '14', '16', '18', '20', '24', '32'];
10
+
11
+ export const ALIGNMENTS = [
12
+ { label: 'Esquerda', command: 'justifyLeft' },
13
+ { label: 'Centro', command: 'justifyCenter' },
14
+ { label: 'Direita', command: 'justifyRight' },
15
+ ];
16
+
17
+ /** What each prompt asks for. Separated so a caller can replace the prompts entirely. */
18
+ export const PROMPTS = {
19
+ image: 'URL da imagem:',
20
+ video: 'URL do vídeo (YouTube embed):',
21
+ link: 'URL do link:',
22
+ };
@@ -0,0 +1,253 @@
1
+ <!-- sds-text-editor — ported from kdls-design-system v2.5.1 (0f3edf8)
2
+ components/TextEditor/TextEditor.tsx.
3
+
4
+ A contenteditable area with a toolbar: sizes, bold, italic, alignment, images, video,
5
+ links and lists. It uses document.execCommand, as KDLS does — the API is deprecated
6
+ and has no replacement that does not mean adopting a whole editor library, which is a
7
+ decision about dependencies rather than a port.
8
+
9
+ THE DIVERGENCE THAT MATTERS. KDLS builds HTML by concatenating a string the visitor
10
+ typed into a prompt:
11
+
12
+ exec('insertHTML', `<img src="${url}" style="max-width:100%" />`)
13
+
14
+ Nothing checks the string, so an answer that closes the src attribute and opens an
15
+ event handler instead is inserted as written, handed to onChange, stored, and rendered
16
+ on a public page for every later visitor. That is stored XSS in a design system for
17
+ public sites. The same hole is in the video insert and in createLink, where the scheme
18
+ `javascript:` alone is enough.
19
+
20
+ Two things fix it here. Every URL goes through safeUrl or safeEmbedUrl first — see
21
+ safe-url.js, which is tested on its own including the control-character tricks. And
22
+ the element is built with createElement and setAttribute rather than assembled as a
23
+ string, so there is no attribute for a quote to escape out of even if the check is
24
+ ever wrong.
25
+
26
+ The other divergence: the toolbar's state follows the selection. KDLS keeps `fontSize`
27
+ and `alignment` in React state and never reads them back from the document, so putting
28
+ the caret in a centred paragraph leaves the toolbar claiming the text is
29
+ left-aligned — the control describes the last button pressed rather than the text the
30
+ visitor is looking at. -->
31
+
32
+ import { safeUrl, safeEmbedUrl } from "./safe-url.js";
33
+ import { FONT_SIZES, ALIGNMENTS, PROMPTS } from "./text-editor-commands.js";
34
+ import fontSizeIcon from "../../icons/generated/fluent-text-font-size-regular.marko";
35
+ import boldIcon from "../../icons/generated/fluent-text-bold-regular.marko";
36
+ import italicIcon from "../../icons/generated/fluent-text-italic-regular.marko";
37
+ import alignLeftIcon from "../../icons/generated/fluent-text-align-left-regular.marko";
38
+ import alignCenterIcon from "../../icons/generated/fluent-text-align-center-regular.marko";
39
+ import alignRightIcon from "../../icons/generated/fluent-text-align-right-regular.marko";
40
+ import imageIcon from "../../icons/generated/fluent-image-regular.marko";
41
+ import videoIcon from "../../icons/generated/fluent-video-regular.marko";
42
+ import linkIcon from "../../icons/generated/fluent-link-regular.marko";
43
+ import bulletListIcon from "../../icons/generated/fluent-text-bullet-list-regular.marko";
44
+ import numberListIcon from "../../icons/generated/fluent-text-number-list-ltr-regular.marko";
45
+
46
+ class {
47
+ onCreate(input) {
48
+ this.state = { fontSize: "16", alignment: 0 };
49
+ }
50
+
51
+ onMount() {
52
+ const body = this.getEl("body");
53
+ if (body && this.input.defaultValue) body.innerHTML = this.input.defaultValue;
54
+
55
+ // The toolbar follows the caret. KDLS never reads the document back, so the buttons
56
+ // describe the last thing pressed instead of the text the visitor is looking at.
57
+ this.onSelectionChange = () => {
58
+ const editor = this.getEl("body");
59
+ if (!editor || !editor.contains(document.getSelection()?.anchorNode)) return;
60
+ const index = ALIGNMENTS.findIndex((a) => document.queryCommandState?.(a.command));
61
+ if (index !== -1) this.state.alignment = index;
62
+ };
63
+ document.addEventListener("selectionchange", this.onSelectionChange);
64
+ }
65
+
66
+ onDestroy() {
67
+ document.removeEventListener("selectionchange", this.onSelectionChange);
68
+ }
69
+
70
+ report() {
71
+ this.input.onChange?.(this.getEl("body")?.innerHTML ?? "");
72
+ }
73
+
74
+ exec(command, value) {
75
+ const body = this.getEl("body");
76
+ body?.focus();
77
+ document.execCommand(command, false, value);
78
+ this.report();
79
+ }
80
+
81
+ /**
82
+ * Puts a node where the caret is.
83
+ *
84
+ * Built through the DOM rather than as a string: there is no attribute for a quote to
85
+ * escape out of, whatever the URL turns out to contain.
86
+ */
87
+ insertNode(node) {
88
+ const body = this.getEl("body");
89
+ if (!body) return;
90
+ body.focus();
91
+ const selection = document.getSelection();
92
+ if (selection?.rangeCount && body.contains(selection.anchorNode)) {
93
+ const range = selection.getRangeAt(0);
94
+ range.deleteContents();
95
+ range.insertNode(node);
96
+ range.setStartAfter(node);
97
+ range.collapse(true);
98
+ selection.removeAllRanges();
99
+ selection.addRange(range);
100
+ } else {
101
+ body.appendChild(node);
102
+ }
103
+ this.report();
104
+ }
105
+
106
+ setFontSize(size) {
107
+ this.state.fontSize = size;
108
+ // execCommand's fontSize only understands 1 to 7, so the size is applied by finding
109
+ // what it produced and restyling it. Carried from KDLS, which is the usual workaround.
110
+ this.exec("fontSize", "7");
111
+ const body = this.getEl("body");
112
+ if (!body) return;
113
+ for (const font of body.querySelectorAll('font[size="7"]')) {
114
+ font.removeAttribute("size");
115
+ font.style.fontSize = `${size}px`;
116
+ }
117
+ this.report();
118
+ }
119
+
120
+ setAlignment(index) {
121
+ this.state.alignment = index;
122
+ this.exec(ALIGNMENTS[index].command);
123
+ }
124
+
125
+ /** Asks for a URL and refuses one that would execute. */
126
+ askFor(kind) {
127
+ const asked = this.input.promptFor
128
+ ? this.input.promptFor(kind)
129
+ : window.prompt(PROMPTS[kind]);
130
+ const url = kind === "video" ? safeEmbedUrl(asked) : safeUrl(asked);
131
+ if (asked && !url) this.input.onRejectedUrl?.(asked);
132
+ return url;
133
+ }
134
+
135
+ insertImage() {
136
+ const url = this.askFor("image");
137
+ if (!url) return;
138
+ const image = document.createElement("img");
139
+ image.setAttribute("src", url);
140
+ image.setAttribute("alt", "");
141
+ image.style.maxWidth = "100%";
142
+ this.insertNode(image);
143
+ }
144
+
145
+ insertVideo() {
146
+ const url = this.askFor("video");
147
+ if (!url) return;
148
+ const frame = document.createElement("iframe");
149
+ frame.setAttribute("src", url);
150
+ frame.setAttribute("width", "100%");
151
+ frame.setAttribute("height", "315");
152
+ frame.setAttribute("allowfullscreen", "");
153
+ frame.setAttribute("frameborder", "0");
154
+ frame.style.display = "block";
155
+ this.insertNode(frame);
156
+ }
157
+
158
+ insertLink() {
159
+ const url = this.askFor("link");
160
+ if (url) this.exec("createLink", url);
161
+ }
162
+ }
163
+
164
+ $ {
165
+ const {
166
+ placeholder = "Digite aqui...",
167
+ minHeight = 200,
168
+ label = "Editor de texto",
169
+ class: extraClass = "",
170
+ } = input;
171
+
172
+ const alignmentIcons = [alignLeftIcon, alignCenterIcon, alignRightIcon];
173
+ const current = ALIGNMENTS[state.alignment];
174
+ }
175
+
176
+ <div class=`sds-text-editor ${extraClass}`.trim()>
177
+ <div class="sds-text-editor__toolbar" role="toolbar" aria-label="Formatação de texto">
178
+ <sds-menu
179
+ trigger="navButton"
180
+ label=state.fontSize
181
+ icon=fontSizeIcon
182
+ placement="bottomLeft"
183
+ aria-label="Tamanho do texto"
184
+ items=FONT_SIZES.map((size) => ({
185
+ label: `${size}px`,
186
+ value: size,
187
+ active: state.fontSize === size,
188
+ onClick: () => component.setFontSize(size),
189
+ }))
190
+ />
191
+
192
+ <div class="sds-text-editor__divider"/>
193
+
194
+ <sds-icon-button aria-label="Negrito" onClick=() => component.exec("bold")>
195
+ <@icon><${boldIcon}/></@icon>
196
+ </sds-icon-button>
197
+ <sds-icon-button aria-label="Itálico" onClick=() => component.exec("italic")>
198
+ <@icon><${italicIcon}/></@icon>
199
+ </sds-icon-button>
200
+
201
+ <div class="sds-text-editor__divider"/>
202
+
203
+ <sds-menu
204
+ trigger="navButton"
205
+ icon=alignmentIcons[state.alignment]
206
+ placement="bottomLeft"
207
+ aria-label=`Alinhamento: ${current.label}`
208
+ items=ALIGNMENTS.map((alignment, index) => ({
209
+ label: alignment.label,
210
+ value: alignment.command,
211
+ icon: alignmentIcons[index],
212
+ active: state.alignment === index,
213
+ onClick: () => component.setAlignment(index),
214
+ }))
215
+ />
216
+
217
+ <div class="sds-text-editor__divider"/>
218
+
219
+ <sds-icon-button aria-label="Inserir imagem" onClick=() => component.insertImage()>
220
+ <@icon><${imageIcon}/></@icon>
221
+ </sds-icon-button>
222
+ <sds-icon-button aria-label="Inserir vídeo" onClick=() => component.insertVideo()>
223
+ <@icon><${videoIcon}/></@icon>
224
+ </sds-icon-button>
225
+ <sds-icon-button aria-label="Inserir link" onClick=() => component.insertLink()>
226
+ <@icon><${linkIcon}/></@icon>
227
+ </sds-icon-button>
228
+
229
+ <div class="sds-text-editor__divider"/>
230
+
231
+ <sds-icon-button aria-label="Lista" onClick=() => component.exec("insertUnorderedList")>
232
+ <@icon><${bulletListIcon}/></@icon>
233
+ </sds-icon-button>
234
+ <sds-icon-button aria-label="Lista numerada" onClick=() => component.exec("insertOrderedList")>
235
+ <@icon><${numberListIcon}/></@icon>
236
+ </sds-icon-button>
237
+ </div>
238
+
239
+ <!-- The initial HTML is written in onMount rather than rendered, so the server never
240
+ serialises markup it was handed by a caller. -->
241
+ <div
242
+ key="body"
243
+ class="sds-text-editor__body"
244
+ contenteditable="true"
245
+ role="textbox"
246
+ aria-multiline="true"
247
+ aria-label=label
248
+ tabindex="0"
249
+ style=`--te-min-height:${minHeight}px`
250
+ data-placeholder=placeholder
251
+ on-input("report")
252
+ />
253
+ </div>
@@ -0,0 +1,88 @@
1
+ /* SDS Toast.
2
+ Ported from kdls-design-system v2.5.1 (0f3edf8) components/Toast/Toast.css
3
+ by scripts/port-css.mjs. Divergences beyond the mechanical rename are noted inline.
4
+ *//* ============================================================
5
+ SDS Toast
6
+ Light: --color-neutral-90 bg (#1A1A1A) + --color-neutral-00 text (#FFFFFF)
7
+ Dark: tokens auto-invert → light bg + dark text
8
+ ============================================================ */
9
+
10
+ .sds-toast {
11
+ display: inline-flex;
12
+ align-items: center;
13
+ gap: var(--spacing-2);
14
+ padding: var(--spacing-3) var(--spacing-4);
15
+ min-height: 56px;
16
+ width: 360px;
17
+ /* Divergence: KDLS caps this at a flat 360px, which is wider than a 375px phone once
18
+ anything around it has padding — the toast then pushes the page sideways. The design
19
+ width is kept; what changes is that it gives way rather than overflow. */
20
+ max-width: min(360px, 100%);
21
+ background: var(--color-neutral-90);
22
+ color: var(--component-bg-color);
23
+ border-radius: var(--radius-md);
24
+ font-family: var(--font-family-base);
25
+ font-size: var(--font-size-body-md);
26
+ line-height: var(--lh-body-md);
27
+ box-shadow: var(--shadow-drop-md);
28
+ /* Enter: slides in from the left + fade, decelerating (no bounce) */
29
+ animation: sds-toast-enter 350ms cubic-bezier(0.05, 0.7, 0.1, 1) both;
30
+ }
31
+ /* Exit: slides back to the left + fade, accelerating */
32
+ .sds-toast--leaving {
33
+ animation: sds-toast-exit 220ms cubic-bezier(0.3, 0, 0.8, 0.15) both;
34
+ }
35
+ @keyframes sds-toast-enter {
36
+ from { opacity: 0; transform: translateX(-110%); }
37
+ to { opacity: 1; transform: translateX(0); }
38
+ }
39
+ @keyframes sds-toast-exit {
40
+ from { opacity: 1; transform: translateX(0); }
41
+ to { opacity: 0; transform: translateX(-110%); }
42
+ }
43
+ /* ── Toaster: fixed container in the bottom-left corner (via portal) ── */
44
+ .sds-toaster {
45
+ position: fixed;
46
+ bottom: var(--spacing-6);
47
+ left: var(--spacing-6);
48
+ z-index: var(--z-notice);
49
+ display: flex;
50
+ flex-direction: column;
51
+ gap: var(--spacing-3);
52
+ pointer-events: none;
53
+ }
54
+ .sds-toaster > * {
55
+ pointer-events: auto;
56
+ }
57
+ .sds-toast__icon {
58
+ display: inline-flex;
59
+ align-items: center;
60
+ justify-content: center;
61
+ width: 24px;
62
+ height: 24px;
63
+ flex-shrink: 0;
64
+ }
65
+ .sds-toast__message {
66
+ flex: 1;
67
+ }
68
+ .sds-toast__dismiss {
69
+ flex-shrink: 0;
70
+ color: inherit; /* <button> resets color; force inheritance from .sds-toast */
71
+ }
72
+ /* .sds-toast parent adds specificity 0,4,0 — beats neutral variant + dark mode overrides */
73
+ .sds-toast .sds-toast__dismiss.sds-icon-button .sds-icon-button__icon {
74
+ color: inherit;
75
+ opacity: 0.7;
76
+ transition: opacity 120ms ease;
77
+ }
78
+ .sds-toast .sds-toast__dismiss.sds-icon-button:hover .sds-icon-button__icon {
79
+ opacity: 1;
80
+ }
81
+ .sds-toast__dismiss.sds-icon-button--sm:hover:not(:disabled) {
82
+ background-color: var(--color-opacity-inverse-10);
83
+ }
84
+ .sds-toast__dismiss.sds-icon-button--sm:active:not(:disabled) {
85
+ background-color: var(--color-opacity-inverse-20);
86
+ }
87
+ /* The dark-theme values now live in the token itself — see --color-opacity-inverse-* in
88
+ tokens.css, which carries KDLS's lower alphas for the dark case. */
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Firing a toast from anywhere.
3
+ *
4
+ * Ported from kdls-design-system v2.5.1 (0f3edf8) components/Toast/toast-api.ts. Mount
5
+ * <sds-toaster/> once at the root of the page and call toast(...) from wherever — including
6
+ * from a catch block that knows nothing about components, which is the whole reason this is
7
+ * an imperative API rather than a prop.
8
+ */
9
+ import { addToast, dismissToast } from './toast-store.js';
10
+
11
+ export function toast(message, opts) {
12
+ return addToast(message, opts);
13
+ }
14
+
15
+ toast.success = (message, opts) => addToast(message, { ...opts, feedback: 'success' });
16
+ toast.error = (message, opts) => addToast(message, { ...opts, feedback: 'error' });
17
+ toast.alert = (message, opts) => addToast(message, { ...opts, feedback: 'alert' });
18
+ toast.neutral = (message, opts) => addToast(message, { ...opts, feedback: 'neutral' });
19
+ toast.dismiss = (id) => dismissToast(id);
@@ -0,0 +1,118 @@
1
+ /**
2
+ * The list of live toasts, their timers, and who to tell when it changes.
3
+ *
4
+ * Ported from kdls-design-system v2.5.1 (0f3edf8) components/Toast/toast-store.ts, which is
5
+ * already framework-free — a module-level list and a set of listeners. That is what lets
6
+ * `toast(...)` be called from anywhere, including from a catch block that is nowhere near a
7
+ * component, and it survives the move from React to Marko untouched in behaviour.
8
+ *
9
+ * The only change is the class name in the comment below, because the exit animation it
10
+ * points at is now sds-toast--leaving.
11
+ */
12
+
13
+ /** Fixed by the design system: 4s, or 8s when the visitor needs time to read a failure. */
14
+ const DURATION_DEFAULT = 4000;
15
+ const DURATION_ERROR = 8000;
16
+
17
+ /** Must match the exit animation in Toast.css (.sds-toast--leaving). */
18
+ export const TOAST_EXIT_MS = 220;
19
+
20
+ let toasts = [];
21
+ let seq = 0;
22
+ const listeners = new Set();
23
+ const running = new Map();
24
+
25
+ function emit() {
26
+ const snapshot = [...toasts];
27
+ listeners.forEach((listener) => listener(snapshot));
28
+ }
29
+
30
+ /**
31
+ * Subscribes to list changes. Calls back immediately with the current state, so a listener
32
+ * that arrives after a toast was fired still sees it. Returns the unsubscribe function.
33
+ */
34
+ export function subscribe(listener) {
35
+ listeners.add(listener);
36
+ listener([...toasts]);
37
+ return () => { listeners.delete(listener); };
38
+ }
39
+
40
+ function arm(id, ms) {
41
+ const timeout = setTimeout(() => startLeaving(id), ms);
42
+ running.set(id, { timeout, startedAt: Date.now(), remaining: ms });
43
+ }
44
+
45
+ function startLeaving(id) {
46
+ const item = toasts.find((t) => t.id === id);
47
+ if (!item || item.leaving) return;
48
+ const timer = running.get(id);
49
+ if (timer) {
50
+ clearTimeout(timer.timeout);
51
+ running.delete(id);
52
+ }
53
+ toasts = toasts.map((t) => (t.id === id ? { ...t, leaving: true } : t));
54
+ emit();
55
+ setTimeout(() => remove(id), TOAST_EXIT_MS);
56
+ }
57
+
58
+ function remove(id) {
59
+ toasts = toasts.filter((t) => t.id !== id);
60
+ emit();
61
+ }
62
+
63
+ /** Adds a toast and schedules its own dismissal. Returns the id. */
64
+ export function addToast(message, opts = {}) {
65
+ const id = ++seq;
66
+ const feedback = opts.feedback ?? 'neutral';
67
+ const duration = feedback === 'error' ? DURATION_ERROR : DURATION_DEFAULT;
68
+ const item = {
69
+ id,
70
+ message,
71
+ feedback,
72
+ actionLabel: opts.actionLabel,
73
+ onAction: opts.onAction,
74
+ actionDisabled: opts.actionDisabled,
75
+ actionDisabledTooltip: opts.actionDisabledTooltip,
76
+ actionDisabledTooltipPosition: opts.actionDisabledTooltipPosition,
77
+ showDismiss: opts.showDismiss ?? true,
78
+ leaving: false,
79
+ };
80
+ toasts = [...toasts, item];
81
+ emit();
82
+ arm(id, duration);
83
+ return id;
84
+ }
85
+
86
+ /** Closes a toast by hand, through the same exit animation. */
87
+ export function dismissToast(id) {
88
+ startLeaving(id);
89
+ }
90
+
91
+ /**
92
+ * Holds every timer where it is. The container calls this on hover: a toast that vanishes
93
+ * while someone is reaching for its action is a toast that wasted their time.
94
+ */
95
+ export function pauseAll() {
96
+ running.forEach((timer) => {
97
+ clearTimeout(timer.timeout);
98
+ const elapsed = Date.now() - timer.startedAt;
99
+ timer.remaining = Math.max(0, timer.remaining - elapsed);
100
+ });
101
+ }
102
+
103
+ /** Starts them again with whatever time they had left. */
104
+ export function resumeAll() {
105
+ running.forEach((timer, id) => {
106
+ timer.timeout = setTimeout(() => startLeaving(id), timer.remaining);
107
+ timer.startedAt = Date.now();
108
+ });
109
+ }
110
+
111
+ /** Tests only: the list is module state and outlives them. */
112
+ export function resetToasts() {
113
+ running.forEach((timer) => clearTimeout(timer.timeout));
114
+ running.clear();
115
+ toasts = [];
116
+ seq = 0;
117
+ emit();
118
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The Toast's vocabulary.
3
+ *
4
+ * Ported from kdls-design-system v2.5.1 (0f3edf8) components/Toast/Toast.tsx, where it was
5
+ * a TypeScript union that disappears at build time. Checked at render here, or an unknown
6
+ * value renders an unstyled panel with no icon and nobody finds out until a customer does.
7
+ */
8
+
9
+ export const TOAST_FEEDBACK = ['neutral', 'success', 'alert', 'error'];
10
+
11
+ export function assertToastFeedback(value) {
12
+ if (!TOAST_FEEDBACK.includes(value)) {
13
+ throw new Error(`Unknown toast feedback: "${value}". Allowed: ${TOAST_FEEDBACK.join(', ')}`);
14
+ }
15
+ return value;
16
+ }