@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
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # SDS — Site Design System
2
+
3
+ Componentes Marko 5 para os produtos **públicos** da Kenlo: Site, Landing Page e Blog.
4
+
5
+ O SDS é irmão do [KDLS](https://www.npmjs.com/package/@kenlo/kdls-design-system), não uma
6
+ versão dele. O KDLS é React e atende os produtos internos; o SDS é Marko e atende o que o
7
+ visitante vê. Os dois nascem dos mesmos tokens de design, e cada um é dono do seu código.
8
+
9
+ **Sem marca.** O site de um cliente não é um site da Kenlo, então nenhuma cor, gradiente ou
10
+ alias da marca existe aqui. O que sobrou é o *encaixe*: um slot de cor primária que cada
11
+ site preenche com a sua.
12
+
13
+ ---
14
+
15
+ ## Instalação
16
+
17
+ ```bash
18
+ npm install @kenlo/sds-design-system
19
+ ```
20
+
21
+ O Marko 5 é `peerDependency` — o projeto que consome já tem o seu.
22
+
23
+ ---
24
+
25
+ ## Começando
26
+
27
+ Duas linhas e o sistema está de pé:
28
+
29
+ ```js
30
+ // 1. Os tokens. Sem eles nenhum componente tem cor, espaço ou tipografia.
31
+ import "@kenlo/sds-design-system/foundations/tokens.css";
32
+
33
+ // 2. O CSS de cada componente que você usar.
34
+ import "@kenlo/sds-design-system/components/Button/Button.css";
35
+ ```
36
+
37
+ As tags não precisam de import. O `marko.json` na raiz do pacote é o catálogo: o compilador
38
+ do Marko acha `<sds-button>` sozinho.
39
+
40
+ ```marko
41
+ <sds-button variant="primary" onClick=salvar>Salvar</sds-button>
42
+
43
+ <sds-input type="tel-inter" label="Telefone" onChange=(e, valor) => ... />
44
+
45
+ <sds-card bordered hoverable>
46
+ <sds-text variant="titleSm">Apartamento 42</sds-text>
47
+ </sds-card>
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Tema
53
+
54
+ Quatro atributos no `<html>` decidem a aparência inteira. Um atributo no valor padrão é
55
+ **removido**, não escrito — então "sem atributo" é o padrão de instalação.
56
+
57
+ | Atributo | Valores | Padrão |
58
+ |---|---|---|
59
+ | `data-theme` | `light`, `dark`, `system` | `light` |
60
+ | `data-color-scheme` | `neutral`, `leaf`, `sand`, `wood`, `clay`, `flower`, `dusk`, `sky`, `sea`, `lake` | `neutral` |
61
+ | `data-radius` | `flat`, `soft`, `medium`, `rounded` | `soft` |
62
+ | `data-font` | `manrope`, `inter`, `helvetica`, `source-sans`, `nunito`, `montserrat` | `manrope` |
63
+
64
+ Há duas portas para escrevê-los, e a diferença importa:
65
+
66
+ ```js
67
+ // No servidor, durante o SSR — o primeiro quadro já sai pintado certo.
68
+ import { themeAttributes } from "@kenlo/sds-design-system/foundations/theme-server.js";
69
+ `<html ${themeAttributes({ colorScheme: "leaf", radius: "flat" })}>`
70
+
71
+ // No navegador — para mudança ao vivo, como o preview do CMS.
72
+ import { configureSds } from "@kenlo/sds-design-system/foundations/theme.js";
73
+ configureSds({ theme: "dark" });
74
+ ```
75
+
76
+ Aplicar o tema só pelo navegador pinta o primeiro quadro errado e corrige na frente do
77
+ visitante. A metade servidor existe para evitar isso.
78
+
79
+ ---
80
+
81
+ ## Cor do cliente
82
+
83
+ Cada site tem a sua cor, e ela entra em tempo de execução:
84
+
85
+ ```js
86
+ import { applyCustomColor, applyCustomGradient, clearCustomScheme }
87
+ from "@kenlo/sds-design-system/foundations/custom-scheme.js";
88
+
89
+ applyCustomColor("#2E7D32"); // nove tons sólidos
90
+ applyCustomGradient(["#77A6E0", "#BF71E5"]); // nove gradientes
91
+ clearCustomScheme();
92
+ ```
93
+
94
+ As duas preenchem `--color-primary-10..90`, os companheiros `-solid` e o
95
+ `--color-on-primary` — este último escolhido por luminância relativa (WCAG), para o texto
96
+ sobre a cor ficar legível seja ela qual for. Uma chamada substitui a outra; nunca empilham.
97
+
98
+ ---
99
+
100
+ ## O que vem dentro
101
+
102
+ - **46 componentes** — de `sds-accordion` a `sds-uploader`
103
+ - **85 ícones** — Fluent, sociais e próprios, como componentes Marko (sem requisição a terceiros)
104
+ - **Tokens** de cor, espaço, tipografia, sombra, raio e movimento
105
+ - **Tema claro e escuro**, dez esquemas de cor, quatro esquemas de canto, seis fontes
106
+
107
+ O pacote leva **código-fonte**, não build: o seu projeto compila os `.marko` com o
108
+ toolchain que já tem.
109
+
110
+ ---
111
+
112
+ ## Documentação
113
+
114
+ O portal tem todos os componentes com controles ao vivo, a tabela de props de cada um e o
115
+ código pronto para copiar. Para subir localmente:
116
+
117
+ ```bash
118
+ git clone https://gitlab.com/kenlo1/kenlo-sites/frontend/sds-design-system.git
119
+ npm install
120
+ npm run dev
121
+ ```
122
+
123
+ ---
124
+
125
+ ## Versionamento
126
+
127
+ O pacote é público na npm porque os projetos que o consomem precisam instalá-lo sem
128
+ credencial, igual ao KDLS. **Público não quer dizer aberto:** o código é proprietário da
129
+ Kenlo (`UNLICENSED`) e as contribuições passam pelo repositório no GitLab.
@@ -0,0 +1,186 @@
1
+ /* ============================================================
2
+ KDLS Accordion
3
+ ============================================================ */
4
+
5
+ .sds-accordion {
6
+ width: 100%;
7
+ background: var(--component-bg-color);
8
+ border: 1px solid transparent;
9
+ border-radius: var(--radius-md);
10
+ font-family: var(--font-family-base);
11
+ overflow: hidden;
12
+ }
13
+
14
+ /* ── Header ── */
15
+ .sds-accordion__header {
16
+ display: flex;
17
+ align-items: center;
18
+ gap: var(--spacing-3);
19
+ padding: var(--spacing-4) var(--spacing-6);
20
+ cursor: pointer;
21
+ user-select: none;
22
+ outline: none;
23
+ transition: background-color 120ms ease;
24
+ }
25
+
26
+ .sds-accordion__header:hover {
27
+ background-color: var(--color-neutral-10);
28
+ }
29
+
30
+ .sds-accordion__header:focus-visible {
31
+ box-shadow: var(--focus-ring-inset);
32
+ }
33
+
34
+ /* ── Title ── */
35
+ .sds-accordion__title {
36
+ flex: 1;
37
+ display: flex;
38
+ align-items: center;
39
+ gap: var(--spacing-2);
40
+ }
41
+
42
+ /* Optional icon to the left of the title — 24px, aligned and non-shrinking */
43
+ .sds-accordion__title-icon {
44
+ display: inline-flex;
45
+ align-items: center;
46
+ justify-content: center;
47
+ width: 24px;
48
+ height: 24px;
49
+ font-size: 24px;
50
+ flex-shrink: 0;
51
+ color: var(--title-color);
52
+ }
53
+
54
+ /* ── Title size: lg — larger vertical padding ── */
55
+ .sds-accordion--title-lg .sds-accordion__header {
56
+ padding-top: var(--spacing-6);
57
+ padding-bottom: var(--spacing-6);
58
+ }
59
+
60
+ /* ── Action link (mirrors Button--link) ── */
61
+ .sds-accordion__action {
62
+ display: inline-flex;
63
+ align-items: center;
64
+ gap: var(--spacing-1);
65
+ padding: 0 var(--spacing-1);
66
+ height: 24px;
67
+ border: none;
68
+ background: transparent;
69
+ cursor: pointer;
70
+ font-family: var(--font-family-base);
71
+ font-size: var(--font-size-title-sm);
72
+ font-weight: var(--font-weight-medium);
73
+ line-height: var(--lh-title-sm);
74
+ color: var(--color-primary-50-solid);
75
+ flex-shrink: 0;
76
+ text-decoration: none;
77
+ transition: color 120ms ease;
78
+ }
79
+
80
+ .sds-accordion__action:hover {
81
+ color: var(--color-primary-60-solid);
82
+ }
83
+
84
+ .sds-accordion__action:active {
85
+ color: var(--color-primary-70-solid);
86
+ }
87
+
88
+ .sds-accordion__action:focus-visible {
89
+ outline: none;
90
+ box-shadow: var(--focus-ring);
91
+ border-radius: var(--radius-sm);
92
+ }
93
+
94
+ /* ── Chevron ── */
95
+ .sds-accordion__chevron {
96
+ display: inline-flex;
97
+ align-items: center;
98
+ font-size: 24px;
99
+ flex-shrink: 0;
100
+ color: var(--title-color);
101
+ transition: transform 200ms ease;
102
+ }
103
+
104
+ .sds-accordion__chevron--open,
105
+ details.sds-accordion[open]:not(.sds-accordion--closing) .sds-accordion__chevron {
106
+ transform: rotate(180deg);
107
+ }
108
+
109
+ /* ── Body ── */
110
+ .sds-accordion__body-wrap {
111
+ display: grid;
112
+ grid-template-rows: 0fr;
113
+ transition: grid-template-rows 200ms ease;
114
+ }
115
+
116
+ .sds-accordion__body-wrap--open,
117
+ details.sds-accordion[open]:not(.sds-accordion--closing) > .sds-accordion__body-wrap {
118
+ grid-template-rows: 1fr;
119
+ }
120
+
121
+ .sds-accordion__body-clip {
122
+ overflow: hidden;
123
+ min-height: 0;
124
+ }
125
+
126
+ .sds-accordion__body {
127
+ padding: var(--spacing-4) var(--spacing-6) var(--spacing-6);
128
+ }
129
+
130
+ /* ── bordered=true — header is always clickable, so the stroke is always the interactive one ── */
131
+ .sds-accordion--bordered {
132
+ border-color: var(--border-interactive-color);
133
+ }
134
+
135
+ /* ── Native <details> variant ──────────────────────────────────────────────────
136
+ The browser hides a closed <details>'s content, which is exactly what kills the
137
+ grid-template-rows transition the stateful variant animates with. These rules force
138
+ the content to stay laid out so the transition has something to animate, and hand the
139
+ clipping to the wrapper instead.
140
+
141
+ Whether this holds in every engine is the open question this variant exists to
142
+ answer — see the portal, where the two sit side by side. */
143
+ details.sds-accordion > summary {
144
+ list-style: none;
145
+ cursor: pointer;
146
+ }
147
+
148
+ details.sds-accordion > summary::-webkit-details-marker,
149
+ details.sds-accordion > summary::marker {
150
+ display: none;
151
+ content: "";
152
+ }
153
+
154
+ /* MEASURED in the portal, Chromium, 2026-09-10 — the question this variant existed to
155
+ answer, and its answer.
156
+
157
+ Both accordions were opened and their wrapper height sampled every 60ms:
158
+
159
+ native <details> 0 → 55 → 115 → 132 → 132 px
160
+ stateful (class) 0 → 55 → 115 → 130 → 132 px
161
+
162
+ They animate identically. A closed <details> does hide its content, but that does not
163
+ stop the grid-template-rows transition on a child wrapper from running when it opens.
164
+ No override is needed, and an earlier attempt to force one
165
+ (content-visibility: visible on the closed wrapper) was actively harmful: it kept the
166
+ content laid out at full height, so the accordion never closed at all.
167
+
168
+ That was the OPENING only, and it was the wrong half to measure. Closing:
169
+
170
+ native <details> 132 → 132 → 132 → 132 px snaps shut
171
+ class-driven 132 → 79 → 26 → 2 → 0 animates
172
+
173
+ A closed <details> hides its content, so there is nothing left to animate on the way
174
+ out. The .sds-accordion--closing class is how the component buys that back: it holds
175
+ the element open while the transition runs, and the script closes it for real on
176
+ transitionend.
177
+
178
+ Not measured: Gecko and WebKit. Worth re-running there before the first production
179
+ release. */
180
+
181
+ /* The out-animation. `--closing` wins over [open] so the rows collapse while the element
182
+ is still open; without it the browser hides the content first and the transition has
183
+ nothing to move. */
184
+ details.sds-accordion.sds-accordion--closing > .sds-accordion__body-wrap {
185
+ grid-template-rows: 0fr;
186
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The Accordion's vocabulary.
3
+ *
4
+ * Ported from kdls-design-system v2.5.1 (0f3edf8) components/Accordion/Accordion.tsx.
5
+ *
6
+ * Two sizes, and they mean the title's weight in the page: `lg` titles read as titleMd,
7
+ * `md` as titleSm. The component branched on `size === "lg"` and accepted anything else
8
+ * silently, so `size="large"` rendered the small title and looked like a styling bug.
9
+ */
10
+ export const ACCORDION_SIZES = ['md', 'lg'];
11
+
12
+ export function assertAccordionSize(size) {
13
+ if (!ACCORDION_SIZES.includes(size)) {
14
+ throw new Error(`Unknown accordion size: "${size}". Allowed: ${ACCORDION_SIZES.join(', ')}`);
15
+ }
16
+ return size;
17
+ }
@@ -0,0 +1,264 @@
1
+ <!-- sds-accordion — <details> underneath, animated on top.
2
+
3
+ Ported from kdls-design-system v2.5.1 (0f3edf8) components/Accordion/Accordion.tsx.
4
+
5
+ MEASURED, 2026-09-10, Chromium, in the portal:
6
+
7
+ opening native <details> 0 → 55 → 115 → 132 px animates
8
+ class-driven 0 → 55 → 115 → 130 → 132 animates
9
+ closing native <details> 132 → 132 → 132 → 132 snaps shut
10
+ class-driven 132 → 79 → 26 → 2 → 0 animates
11
+
12
+ A closed <details> hides its content, which a height transition needs to measure —
13
+ so the open animates and the close does not. That asymmetry is visible and it is the
14
+ one thing the native element gets wrong here.
15
+
16
+ And the cost of fixing it is small. Measured by tests/ssr/accordion-cost.cjs, which
17
+ renders the same page twice — once with this component, once with a copy that has no
18
+ class block — hydration costs ~160 bytes of HTML per instance, which Brotli reduces
19
+ to 7.6: 255 bytes for twenty accordions, 378 for fifty. The payload is highly
20
+ repetitive, which is exactly what compression is good at.
21
+
22
+ (An earlier note here said 2 bytes per instance. That run was made while Icon still
23
+ hydrated, so both pages carried Icon's payload and it cancelled out of the delta.
24
+ The conclusion did not change; the number was too small.)
25
+
26
+ So this component hydrates — and keeps <details> anyway. The element still owns
27
+ open and closed, keyboard handling, the correct ARIA and find-in-page; a visitor
28
+ whose JavaScript never arrives can still open and close the panel, just without the
29
+ closing animation. The script only intercepts the close long enough to animate it.
30
+
31
+ `destroyOnClose` is not ported, and cannot be while the panel is a <details>: the
32
+ element keeps its content in the document when closed, which is what gives it
33
+ find-in-page and what lets a closing transition have something to measure. Dropping
34
+ the content would take both away to save markup that compresses to nothing. A caller
35
+ who genuinely must not render the body until it opens should render the body
36
+ conditionally itself, which costs the same and says so at the call site.
37
+
38
+ This is not a licence to hydrate everything. What is cheap here is cheap because the
39
+ input is a short title. The 492 KB payload measured on the listing page came from
40
+ components serialising a whole property object — the cost scales with what you put
41
+ in, not with the word `class`. -->
42
+
43
+ import { assertPlacement } from "../Floating/floating-position.js";
44
+ import { assertAccordionSize } from "./accordion-variants.js";
45
+
46
+ class {
47
+ onCreate(input) {
48
+ this.state = { closing: false, open: !!input.defaultOpen, actionLoading: false };
49
+ }
50
+
51
+ /** The caller's answer when it has one, this component's when it does not. */
52
+ get isOpen() {
53
+ return this.input.open !== undefined ? this.input.open : this.state.open;
54
+ }
55
+
56
+ /** The <details> itself. `getEl(key)` does not resolve a component's own root element. */
57
+ get root() {
58
+ return this.el ?? this.getEl("root");
59
+ }
60
+
61
+ onUpdate() {
62
+ const details = this.root;
63
+ if (!details) return;
64
+
65
+ // The browser owns `details.open` once it has toggled the element itself, and a
66
+ // rendered `open=null` does not take it back. So the close is applied here, after the
67
+ // render that dropped the hold — with no hold and nothing asking for open, it shuts.
68
+ const wantsOpen = this.input.open !== undefined ? this.input.open : this.state.open;
69
+ if (!wantsOpen && !this.state.closing && details.open) details.open = false;
70
+
71
+ // Controlled: the caller flipped `open` to false while the element is still open, so
72
+ // the closing transition has to be started from here rather than from a click.
73
+ if (this.input.open === undefined) return;
74
+ if (!this.input.open && details.open && !this.state.closing) this.state.closing = true;
75
+ if (this.input.open && !details.open) details.open = true;
76
+ }
77
+
78
+ onDestroy() {
79
+ clearTimeout(this.closeTimer);
80
+ }
81
+
82
+ report(next) {
83
+ if (this.input.open === undefined) this.state.open = next;
84
+ this.input.onOpenChange?.(next);
85
+ }
86
+
87
+ handleSummaryClick(event) {
88
+ const details = this.root;
89
+
90
+ // `details.open` alone cannot answer "is this open" any more: the element is held open
91
+ // for the length of the closing transition, so mid-close it reads as open and a click
92
+ // meant to cancel the close was read as a second close.
93
+ // Mid-close the element is still `open`, so the browser's own toggle would close it.
94
+ // Cancelling the close is all this click means: drop the hold and stay open.
95
+ if (this.state.closing) {
96
+ event.preventDefault();
97
+ clearTimeout(this.closeTimer);
98
+ this.state.closing = false;
99
+ this.report(true);
100
+ return;
101
+ }
102
+
103
+ if (!details.open) {
104
+ // Opening needs no help from the browser's point of view — but a controlled
105
+ // accordion must not open itself, only ask.
106
+ if (this.input.open !== undefined) event.preventDefault();
107
+ this.report(true);
108
+ return;
109
+ }
110
+
111
+ // Closing does need help: <details> would hide the content immediately, leaving
112
+ // nothing to animate. Hold it open, run the transition, and close for real when it
113
+ // ends. The same hold serves the controlled case, where the caller decides whether
114
+ // the close happens at all.
115
+ event.preventDefault();
116
+ this.report(false);
117
+ if (this.input.open === undefined) this.state.closing = true;
118
+
119
+ // The transition is the only thing that closes the element for real, and it does not
120
+ // always arrive: prefers-reduced-motion, transitions switched off, a background tab.
121
+ // Without this the hold never lifts — the panel stays open and `--closing` latches.
122
+ clearTimeout(this.closeTimer);
123
+ this.closeTimer = setTimeout(() => this.finishClosing(), 250);
124
+ }
125
+
126
+ handleTransitionEnd(event) {
127
+ if (event.propertyName !== "grid-template-rows" || !this.state.closing) return;
128
+ this.finishClosing();
129
+ }
130
+
131
+ /**
132
+ * Lifts the hold and shuts the element.
133
+ *
134
+ * Both halves are needed. Dropping `closing` is what the render reads, but the browser
135
+ * owns `details.open` once it has toggled the element itself and a rendered `open=null`
136
+ * does not take it back — so the property is set here too.
137
+ */
138
+ finishClosing() {
139
+ clearTimeout(this.closeTimer);
140
+ const details = this.root;
141
+ if (details) details.open = false;
142
+ this.state.closing = false;
143
+ }
144
+
145
+ /**
146
+ * The header action runs inside the summary, so its click would toggle the panel. It is
147
+ * stopped here rather than in CSS, and the spinner follows a promise when the caller
148
+ * returns one — KDLS does the same, which is what makes `actionLoading` optional.
149
+ */
150
+ async handleAction(event) {
151
+ event.preventDefault();
152
+ event.stopPropagation();
153
+ const result = this.input.onAction?.(event);
154
+ if (!(result instanceof Promise)) return;
155
+ this.state.actionLoading = true;
156
+ try {
157
+ await result;
158
+ } finally {
159
+ this.state.actionLoading = false;
160
+ }
161
+ }
162
+ }
163
+
164
+ $ {
165
+ const {
166
+ title,
167
+ icon,
168
+ size = "lg",
169
+ bordered = false,
170
+ actionLabel,
171
+ actionLoading = false,
172
+ actionDisabled = false,
173
+ actionDisabledTooltip,
174
+ actionDisabledTooltipPosition = "top",
175
+ class: extraClass = "",
176
+ } = input;
177
+
178
+ assertPlacement(actionDisabledTooltipPosition);
179
+
180
+ assertAccordionSize(size);
181
+
182
+ const open = component.isOpen;
183
+ const destroyOnClose = input.destroyOnClose ?? false;
184
+
185
+ // An icon may arrive as an attribute tag (<@icon>…</@icon>, what a page writes) or as a
186
+ // template value (what a test or a loop passes). Normalising here means callers never
187
+ // have to care.
188
+ const renderable = (value) => (value && value.renderBody) || value;
189
+
190
+ const rootClass = [
191
+ "sds-accordion",
192
+ // No --open class: the element is a <details>, so `[open]` already carries the state
193
+ // and the stylesheet reads it there. A second spelling of the same fact is the drift
194
+ // that leaves KDLS's popover arrow unstyled.
195
+ bordered ? "sds-accordion--bordered" : "",
196
+ size === "lg" ? "sds-accordion--title-lg" : "",
197
+ state.closing ? "sds-accordion--closing" : "",
198
+ extraClass,
199
+ ].filter(Boolean).join(" ");
200
+ }
201
+
202
+ <!-- `open || state.closing`: the element has to keep its `open` attribute for the length
203
+ of the closing transition. It did not — `report(false)` sets `state.open = false` and
204
+ `state.closing = true` in the same render, so the attribute was removed in the very
205
+ pass that added `--closing`, the browser hid the panel, and there was nothing left to
206
+ animate. The close snapped shut, `transitionend` never fired, `--closing` latched,
207
+ and the next click opened an empty collapsed panel. -->
208
+ <details key="root" class=rootClass open=((open || state.closing) || null) on-transitionend("handleTransitionEnd")>
209
+ <summary class="sds-accordion__header" on-click("handleSummaryClick")>
210
+ <span class="sds-accordion__title">
211
+ <if(icon)>
212
+ <span class="sds-accordion__title-icon" aria-hidden="true">
213
+ <sds-icon>
214
+ <${renderable(icon)}/>
215
+ </sds-icon>
216
+ </span>
217
+ </if>
218
+ <sds-text as="span" variant=(size === "lg" ? "titleMd" : "titleSm")>
219
+ ${title}
220
+ </sds-text>
221
+ </span>
222
+
223
+ <if(actionLabel)>
224
+ <!-- Inside the summary, so a click on it would toggle the panel — handleAction stops
225
+ that. KDLS wraps the button in a span with a stopPropagation handler; the button
226
+ is a real control either way, so the guard belongs on the action itself. -->
227
+ <span class="sds-accordion__action">
228
+ <sds-button
229
+ variant="link"
230
+ label=actionLabel
231
+ onClick=(event) => component.handleAction(event)
232
+ isLoading=(state.actionLoading || actionLoading)
233
+ disabled=actionDisabled
234
+ disabledTooltip=actionDisabledTooltip
235
+ disabledTooltipPosition=actionDisabledTooltipPosition
236
+ />
237
+ </span>
238
+ </if>
239
+
240
+ <span class="sds-accordion__chevron" aria-hidden="true">
241
+ <sds-icon>
242
+ <fluent-chevron-down-regular/>
243
+ </sds-icon>
244
+ </span>
245
+ </summary>
246
+
247
+ <div class="sds-accordion__body-wrap">
248
+ <div class="sds-accordion__body-clip">
249
+ <div class="sds-accordion__body">
250
+ // `destroyOnClose` was accepted by callers and read by nobody, so a body asked to
251
+ // be thrown away stayed in the DOM — with whatever it held still mounted, still
252
+ // fetching, still focusable by Tab. KDLS defaults it to false, which is what keeps
253
+ // the closing transition possible: the body has to exist to shrink.
254
+ //
255
+ // `state.closing` is in the condition for the same reason the `open` attribute is:
256
+ // removing the body at the start of the close would collapse the panel instantly
257
+ // instead of animating it away.
258
+ <if(!destroyOnClose || open || state.closing)>
259
+ <${input.renderBody}/>
260
+ </if>
261
+ </div>
262
+ </div>
263
+ </div>
264
+ </details>
@@ -0,0 +1,35 @@
1
+ /* SDS AccordionGroup.
2
+ Ported from kdls-design-system v2.5.1 (0f3edf8) components/AccordionGroup/AccordionGroup.css
3
+ by scripts/port-css.mjs. Divergences beyond the mechanical rename are noted inline.
4
+ *//* ============================================================
5
+ SDS AccordionGroup
6
+ ============================================================ */
7
+
8
+ .sds-accordion-group {
9
+ display: flex;
10
+ flex-direction: column;
11
+ width: 100%;
12
+ }
13
+ /* ── connected (default) — single block with collapsed borders ── */
14
+ .sds-accordion-group .sds-accordion {
15
+ border-radius: 0;
16
+ }
17
+ .sds-accordion-group .sds-accordion + .sds-accordion {
18
+ margin-top: -1px;
19
+ }
20
+ .sds-accordion-group .sds-accordion:first-child {
21
+ border-radius: var(--radius-md) var(--radius-md) 0 0;
22
+ }
23
+ .sds-accordion-group .sds-accordion:last-child {
24
+ border-radius: 0 0 var(--radius-md) var(--radius-md);
25
+ }
26
+ /* ── spaced — each accordion independent, with space between them ── */
27
+ .sds-accordion-group--spaced {
28
+ gap: var(--spacing-6);
29
+ }
30
+ .sds-accordion-group--spaced .sds-accordion,
31
+ .sds-accordion-group--spaced .sds-accordion:first-child,
32
+ .sds-accordion-group--spaced .sds-accordion:last-child {
33
+ border-radius: var(--radius-md);
34
+ margin-top: 0;
35
+ }
@@ -0,0 +1,17 @@
1
+ <!-- sds-accordion-group — ported from kdls-design-system v2.5.1 (0f3edf8)
2
+ components/AccordionGroup/AccordionGroup.tsx.
3
+
4
+ Layout only: it spaces or joins the accordions inside it and knows nothing else about
5
+ them. KDLS cloned its children to pass the variant down; nothing needs passing, so the
6
+ accordions are ordinary children here. -->
7
+
8
+ $ {
9
+ const { variant = "connected", class: extraClass = "" } = input;
10
+ if (variant !== "connected" && variant !== "spaced") {
11
+ throw new Error(`Unknown accordion group variant: "${variant}". Allowed: connected, spaced`);
12
+ }
13
+ }
14
+
15
+ <div class=`sds-accordion-group sds-accordion-group--${variant} ${extraClass}`.trim()>
16
+ <${input.renderBody}/>
17
+ </div>