@fristil/designsystem 0.2.0

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 (314) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/LICENSE +21 -0
  3. package/README.md +101 -0
  4. package/dist/cli.d.ts +31 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +261 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/components/css/accordion/accordion.d.ts +37 -0
  9. package/dist/components/css/accordion/accordion.d.ts.map +1 -0
  10. package/dist/components/css/accordion/accordion.js +31 -0
  11. package/dist/components/css/accordion/accordion.js.map +1 -0
  12. package/dist/components/css/alert/alert.d.ts +35 -0
  13. package/dist/components/css/alert/alert.d.ts.map +1 -0
  14. package/dist/components/css/alert/alert.js +29 -0
  15. package/dist/components/css/alert/alert.js.map +1 -0
  16. package/dist/components/css/avatar/avatar.d.ts +40 -0
  17. package/dist/components/css/avatar/avatar.d.ts.map +1 -0
  18. package/dist/components/css/avatar/avatar.js +30 -0
  19. package/dist/components/css/avatar/avatar.js.map +1 -0
  20. package/dist/components/css/badge/badge.d.ts +24 -0
  21. package/dist/components/css/badge/badge.d.ts.map +1 -0
  22. package/dist/components/css/badge/badge.js +24 -0
  23. package/dist/components/css/badge/badge.js.map +1 -0
  24. package/dist/components/css/breadcrumbs/breadcrumbs.d.ts +25 -0
  25. package/dist/components/css/breadcrumbs/breadcrumbs.d.ts.map +1 -0
  26. package/dist/components/css/breadcrumbs/breadcrumbs.js +23 -0
  27. package/dist/components/css/breadcrumbs/breadcrumbs.js.map +1 -0
  28. package/dist/components/css/button/button.d.ts +26 -0
  29. package/dist/components/css/button/button.d.ts.map +1 -0
  30. package/dist/components/css/button/button.js +25 -0
  31. package/dist/components/css/button/button.js.map +1 -0
  32. package/dist/components/css/card/card.d.ts +36 -0
  33. package/dist/components/css/card/card.d.ts.map +1 -0
  34. package/dist/components/css/card/card.js +28 -0
  35. package/dist/components/css/card/card.js.map +1 -0
  36. package/dist/components/css/checkbox/checkbox.d.ts +32 -0
  37. package/dist/components/css/checkbox/checkbox.d.ts.map +1 -0
  38. package/dist/components/css/checkbox/checkbox.js +27 -0
  39. package/dist/components/css/checkbox/checkbox.js.map +1 -0
  40. package/dist/components/css/dialog/dialog.d.ts +30 -0
  41. package/dist/components/css/dialog/dialog.d.ts.map +1 -0
  42. package/dist/components/css/dialog/dialog.js +28 -0
  43. package/dist/components/css/dialog/dialog.js.map +1 -0
  44. package/dist/components/css/divider/divider.d.ts +28 -0
  45. package/dist/components/css/divider/divider.d.ts.map +1 -0
  46. package/dist/components/css/divider/divider.js +22 -0
  47. package/dist/components/css/divider/divider.js.map +1 -0
  48. package/dist/components/css/error-text/error-text.d.ts +24 -0
  49. package/dist/components/css/error-text/error-text.d.ts.map +1 -0
  50. package/dist/components/css/error-text/error-text.js +18 -0
  51. package/dist/components/css/error-text/error-text.js.map +1 -0
  52. package/dist/components/css/fieldset/fieldset.d.ts +55 -0
  53. package/dist/components/css/fieldset/fieldset.d.ts.map +1 -0
  54. package/dist/components/css/fieldset/fieldset.js +39 -0
  55. package/dist/components/css/fieldset/fieldset.js.map +1 -0
  56. package/dist/components/css/file-upload/file-upload.d.ts +41 -0
  57. package/dist/components/css/file-upload/file-upload.d.ts.map +1 -0
  58. package/dist/components/css/file-upload/file-upload.js +32 -0
  59. package/dist/components/css/file-upload/file-upload.js.map +1 -0
  60. package/dist/components/css/heading/heading.d.ts +29 -0
  61. package/dist/components/css/heading/heading.d.ts.map +1 -0
  62. package/dist/components/css/heading/heading.js +23 -0
  63. package/dist/components/css/heading/heading.js.map +1 -0
  64. package/dist/components/css/help-text/help-text.d.ts +24 -0
  65. package/dist/components/css/help-text/help-text.d.ts.map +1 -0
  66. package/dist/components/css/help-text/help-text.js +23 -0
  67. package/dist/components/css/help-text/help-text.js.map +1 -0
  68. package/dist/components/css/input/input.d.ts +39 -0
  69. package/dist/components/css/input/input.d.ts.map +1 -0
  70. package/dist/components/css/input/input.js +44 -0
  71. package/dist/components/css/input/input.js.map +1 -0
  72. package/dist/components/css/label/label.d.ts +33 -0
  73. package/dist/components/css/label/label.d.ts.map +1 -0
  74. package/dist/components/css/label/label.js +23 -0
  75. package/dist/components/css/label/label.js.map +1 -0
  76. package/dist/components/css/link/link.d.ts +21 -0
  77. package/dist/components/css/link/link.d.ts.map +1 -0
  78. package/dist/components/css/link/link.js +14 -0
  79. package/dist/components/css/link/link.js.map +1 -0
  80. package/dist/components/css/list/list.d.ts +29 -0
  81. package/dist/components/css/list/list.d.ts.map +1 -0
  82. package/dist/components/css/list/list.js +23 -0
  83. package/dist/components/css/list/list.js.map +1 -0
  84. package/dist/components/css/pagination/pagination.d.ts +27 -0
  85. package/dist/components/css/pagination/pagination.d.ts.map +1 -0
  86. package/dist/components/css/pagination/pagination.js +25 -0
  87. package/dist/components/css/pagination/pagination.js.map +1 -0
  88. package/dist/components/css/paragraph/paragraph.d.ts +36 -0
  89. package/dist/components/css/paragraph/paragraph.d.ts.map +1 -0
  90. package/dist/components/css/paragraph/paragraph.js +26 -0
  91. package/dist/components/css/paragraph/paragraph.js.map +1 -0
  92. package/dist/components/css/radio/radio.d.ts +32 -0
  93. package/dist/components/css/radio/radio.d.ts.map +1 -0
  94. package/dist/components/css/radio/radio.js +27 -0
  95. package/dist/components/css/radio/radio.js.map +1 -0
  96. package/dist/components/css/search/search.d.ts +33 -0
  97. package/dist/components/css/search/search.d.ts.map +1 -0
  98. package/dist/components/css/search/search.js +31 -0
  99. package/dist/components/css/search/search.js.map +1 -0
  100. package/dist/components/css/select/select.d.ts +23 -0
  101. package/dist/components/css/select/select.d.ts.map +1 -0
  102. package/dist/components/css/select/select.js +18 -0
  103. package/dist/components/css/select/select.js.map +1 -0
  104. package/dist/components/css/shared.d.ts +26 -0
  105. package/dist/components/css/shared.d.ts.map +1 -0
  106. package/dist/components/css/shared.js +34 -0
  107. package/dist/components/css/shared.js.map +1 -0
  108. package/dist/components/css/skeleton/skeleton.d.ts +29 -0
  109. package/dist/components/css/skeleton/skeleton.d.ts.map +1 -0
  110. package/dist/components/css/skeleton/skeleton.js +23 -0
  111. package/dist/components/css/skeleton/skeleton.js.map +1 -0
  112. package/dist/components/css/skip-link/skip-link.d.ts +18 -0
  113. package/dist/components/css/skip-link/skip-link.d.ts.map +1 -0
  114. package/dist/components/css/skip-link/skip-link.js +16 -0
  115. package/dist/components/css/skip-link/skip-link.js.map +1 -0
  116. package/dist/components/css/spinner/spinner.d.ts +33 -0
  117. package/dist/components/css/spinner/spinner.d.ts.map +1 -0
  118. package/dist/components/css/spinner/spinner.js +25 -0
  119. package/dist/components/css/spinner/spinner.js.map +1 -0
  120. package/dist/components/css/sr-only/sr-only.d.ts +17 -0
  121. package/dist/components/css/sr-only/sr-only.d.ts.map +1 -0
  122. package/dist/components/css/sr-only/sr-only.js +14 -0
  123. package/dist/components/css/sr-only/sr-only.js.map +1 -0
  124. package/dist/components/css/switch/switch.d.ts +33 -0
  125. package/dist/components/css/switch/switch.d.ts.map +1 -0
  126. package/dist/components/css/switch/switch.js +29 -0
  127. package/dist/components/css/switch/switch.js.map +1 -0
  128. package/dist/components/css/table/table.d.ts +36 -0
  129. package/dist/components/css/table/table.d.ts.map +1 -0
  130. package/dist/components/css/table/table.js +28 -0
  131. package/dist/components/css/table/table.js.map +1 -0
  132. package/dist/components/css/tag/tag.d.ts +35 -0
  133. package/dist/components/css/tag/tag.d.ts.map +1 -0
  134. package/dist/components/css/tag/tag.js +27 -0
  135. package/dist/components/css/tag/tag.js.map +1 -0
  136. package/dist/components/css/textarea/textarea.d.ts +23 -0
  137. package/dist/components/css/textarea/textarea.d.ts.map +1 -0
  138. package/dist/components/css/textarea/textarea.js +18 -0
  139. package/dist/components/css/textarea/textarea.js.map +1 -0
  140. package/dist/components/css/toggle-group/toggle-group.d.ts +31 -0
  141. package/dist/components/css/toggle-group/toggle-group.d.ts.map +1 -0
  142. package/dist/components/css/toggle-group/toggle-group.js +29 -0
  143. package/dist/components/css/toggle-group/toggle-group.js.map +1 -0
  144. package/dist/components/css/tooltip/tooltip.d.ts +30 -0
  145. package/dist/components/css/tooltip/tooltip.d.ts.map +1 -0
  146. package/dist/components/css/tooltip/tooltip.js +28 -0
  147. package/dist/components/css/tooltip/tooltip.js.map +1 -0
  148. package/dist/components/frittstaende/calendar/fs-calendar.d.ts +52 -0
  149. package/dist/components/frittstaende/calendar/fs-calendar.d.ts.map +1 -0
  150. package/dist/components/frittstaende/calendar/fs-calendar.js +462 -0
  151. package/dist/components/frittstaende/calendar/fs-calendar.js.map +1 -0
  152. package/dist/components/frittstaende/date-field/fs-date-field.d.ts +110 -0
  153. package/dist/components/frittstaende/date-field/fs-date-field.d.ts.map +1 -0
  154. package/dist/components/frittstaende/date-field/fs-date-field.js +264 -0
  155. package/dist/components/frittstaende/date-field/fs-date-field.js.map +1 -0
  156. package/dist/components/frittstaende/suggestion/fs-suggestion.d.ts +133 -0
  157. package/dist/components/frittstaende/suggestion/fs-suggestion.d.ts.map +1 -0
  158. package/dist/components/frittstaende/suggestion/fs-suggestion.js +280 -0
  159. package/dist/components/frittstaende/suggestion/fs-suggestion.js.map +1 -0
  160. package/dist/components/frittstaende/toast/fs-toast.d.ts +67 -0
  161. package/dist/components/frittstaende/toast/fs-toast.d.ts.map +1 -0
  162. package/dist/components/frittstaende/toast/fs-toast.js +108 -0
  163. package/dist/components/frittstaende/toast/fs-toast.js.map +1 -0
  164. package/dist/components/ramme/error-summary/fs-error-summary.d.ts +70 -0
  165. package/dist/components/ramme/error-summary/fs-error-summary.d.ts.map +1 -0
  166. package/dist/components/ramme/error-summary/fs-error-summary.js +155 -0
  167. package/dist/components/ramme/error-summary/fs-error-summary.js.map +1 -0
  168. package/dist/components/ramme/field/field-core.d.ts +70 -0
  169. package/dist/components/ramme/field/field-core.d.ts.map +1 -0
  170. package/dist/components/ramme/field/field-core.js +60 -0
  171. package/dist/components/ramme/field/field-core.js.map +1 -0
  172. package/dist/components/ramme/field/fs-field.d.ts +49 -0
  173. package/dist/components/ramme/field/fs-field.d.ts.map +1 -0
  174. package/dist/components/ramme/field/fs-field.js +118 -0
  175. package/dist/components/ramme/field/fs-field.js.map +1 -0
  176. package/dist/components/ramme/popover/fs-popover.d.ts +68 -0
  177. package/dist/components/ramme/popover/fs-popover.d.ts.map +1 -0
  178. package/dist/components/ramme/popover/fs-popover.js +167 -0
  179. package/dist/components/ramme/popover/fs-popover.js.map +1 -0
  180. package/dist/components/ramme/tabs/fs-tabs.d.ts +61 -0
  181. package/dist/components/ramme/tabs/fs-tabs.d.ts.map +1 -0
  182. package/dist/components/ramme/tabs/fs-tabs.js +156 -0
  183. package/dist/components/ramme/tabs/fs-tabs.js.map +1 -0
  184. package/dist/dom.d.ts +33 -0
  185. package/dist/dom.d.ts.map +1 -0
  186. package/dist/dom.js +77 -0
  187. package/dist/dom.js.map +1 -0
  188. package/dist/fs.d.ts +199 -0
  189. package/dist/fs.d.ts.map +1 -0
  190. package/dist/fs.js +128 -0
  191. package/dist/fs.js.map +1 -0
  192. package/dist/index.d.ts +84 -0
  193. package/dist/index.d.ts.map +1 -0
  194. package/dist/index.js +48 -0
  195. package/dist/index.js.map +1 -0
  196. package/dist/jsx/react.d.ts +127 -0
  197. package/dist/jsx/react.d.ts.map +1 -0
  198. package/dist/jsx/react.js +2 -0
  199. package/dist/jsx/react.js.map +1 -0
  200. package/dist/react.d.ts +207 -0
  201. package/dist/react.d.ts.map +1 -0
  202. package/dist/react.js +109 -0
  203. package/dist/react.js.map +1 -0
  204. package/dist/tailwind/preset.d.ts +120 -0
  205. package/dist/tailwind/preset.d.ts.map +1 -0
  206. package/dist/tailwind/preset.js +107 -0
  207. package/dist/tailwind/preset.js.map +1 -0
  208. package/dist/takeover.d.ts +79 -0
  209. package/dist/takeover.d.ts.map +1 -0
  210. package/dist/takeover.js +119 -0
  211. package/dist/takeover.js.map +1 -0
  212. package/dist/tokens/color.d.ts +85 -0
  213. package/dist/tokens/color.d.ts.map +1 -0
  214. package/dist/tokens/color.js +212 -0
  215. package/dist/tokens/color.js.map +1 -0
  216. package/dist/tokens/theme.d.ts +52 -0
  217. package/dist/tokens/theme.d.ts.map +1 -0
  218. package/dist/tokens/theme.js +197 -0
  219. package/dist/tokens/theme.js.map +1 -0
  220. package/dist/tokens/tokens.d.ts +210 -0
  221. package/dist/tokens/tokens.d.ts.map +1 -0
  222. package/dist/tokens/tokens.js +238 -0
  223. package/dist/tokens/tokens.js.map +1 -0
  224. package/package.json +334 -0
  225. package/src/components/css/accordion/accordion.css +74 -0
  226. package/src/components/css/accordion/accordion.ts +50 -0
  227. package/src/components/css/alert/alert.css +50 -0
  228. package/src/components/css/alert/alert.ts +48 -0
  229. package/src/components/css/avatar/avatar.css +64 -0
  230. package/src/components/css/avatar/avatar.ts +57 -0
  231. package/src/components/css/badge/badge.css +42 -0
  232. package/src/components/css/badge/badge.ts +43 -0
  233. package/src/components/css/breadcrumbs/breadcrumbs.css +50 -0
  234. package/src/components/css/breadcrumbs/breadcrumbs.ts +29 -0
  235. package/src/components/css/button/button.css +101 -0
  236. package/src/components/css/button/button.ts +44 -0
  237. package/src/components/css/card/card.css +48 -0
  238. package/src/components/css/card/card.ts +50 -0
  239. package/src/components/css/checkbox/checkbox.css +96 -0
  240. package/src/components/css/checkbox/checkbox.ts +50 -0
  241. package/src/components/css/dialog/dialog.css +50 -0
  242. package/src/components/css/dialog/dialog.ts +36 -0
  243. package/src/components/css/divider/divider.css +18 -0
  244. package/src/components/css/divider/divider.ts +41 -0
  245. package/src/components/css/error-text/error-text.css +21 -0
  246. package/src/components/css/error-text/error-text.ts +37 -0
  247. package/src/components/css/fieldset/fieldset.css +57 -0
  248. package/src/components/css/fieldset/fieldset.ts +87 -0
  249. package/src/components/css/file-upload/file-upload.css +92 -0
  250. package/src/components/css/file-upload/file-upload.ts +65 -0
  251. package/src/components/css/heading/heading.css +33 -0
  252. package/src/components/css/heading/heading.ts +42 -0
  253. package/src/components/css/help-text/help-text.css +24 -0
  254. package/src/components/css/help-text/help-text.ts +42 -0
  255. package/src/components/css/input/input.css +131 -0
  256. package/src/components/css/input/input.ts +76 -0
  257. package/src/components/css/label/label.css +34 -0
  258. package/src/components/css/label/label.ts +51 -0
  259. package/src/components/css/link/link.css +36 -0
  260. package/src/components/css/link/link.ts +29 -0
  261. package/src/components/css/list/list.css +40 -0
  262. package/src/components/css/list/list.ts +42 -0
  263. package/src/components/css/pagination/pagination.css +77 -0
  264. package/src/components/css/pagination/pagination.ts +33 -0
  265. package/src/components/css/paragraph/paragraph.css +31 -0
  266. package/src/components/css/paragraph/paragraph.ts +53 -0
  267. package/src/components/css/radio/radio.css +76 -0
  268. package/src/components/css/radio/radio.ts +50 -0
  269. package/src/components/css/search/search.css +49 -0
  270. package/src/components/css/search/search.ts +52 -0
  271. package/src/components/css/select/select.css +95 -0
  272. package/src/components/css/select/select.ts +40 -0
  273. package/src/components/css/shared.ts +40 -0
  274. package/src/components/css/skeleton/skeleton.css +49 -0
  275. package/src/components/css/skeleton/skeleton.ts +43 -0
  276. package/src/components/css/skip-link/skip-link.css +36 -0
  277. package/src/components/css/skip-link/skip-link.ts +22 -0
  278. package/src/components/css/spinner/spinner.css +48 -0
  279. package/src/components/css/spinner/spinner.ts +48 -0
  280. package/src/components/css/sr-only/sr-only.css +16 -0
  281. package/src/components/css/sr-only/sr-only.ts +18 -0
  282. package/src/components/css/switch/switch.css +101 -0
  283. package/src/components/css/switch/switch.ts +46 -0
  284. package/src/components/css/table/table.css +63 -0
  285. package/src/components/css/table/table.ts +50 -0
  286. package/src/components/css/tag/tag.css +58 -0
  287. package/src/components/css/tag/tag.ts +49 -0
  288. package/src/components/css/textarea/textarea.css +71 -0
  289. package/src/components/css/textarea/textarea.ts +40 -0
  290. package/src/components/css/toggle-group/toggle-group.css +82 -0
  291. package/src/components/css/toggle-group/toggle-group.ts +37 -0
  292. package/src/components/css/tooltip/tooltip.css +47 -0
  293. package/src/components/css/tooltip/tooltip.ts +36 -0
  294. package/src/components/frittstaende/calendar/fs-calendar.ts +525 -0
  295. package/src/components/frittstaende/date-field/date-field.css +54 -0
  296. package/src/components/frittstaende/date-field/fs-date-field.ts +309 -0
  297. package/src/components/frittstaende/suggestion/fs-suggestion.ts +333 -0
  298. package/src/components/frittstaende/suggestion/suggestion.css +63 -0
  299. package/src/components/frittstaende/toast/fs-toast.ts +146 -0
  300. package/src/components/frittstaende/toast/toast.css +71 -0
  301. package/src/components/ramme/error-summary/error-summary.css +45 -0
  302. package/src/components/ramme/error-summary/fs-error-summary.ts +197 -0
  303. package/src/components/ramme/field/field-core.ts +132 -0
  304. package/src/components/ramme/field/field.css +6 -0
  305. package/src/components/ramme/field/fs-field.ts +154 -0
  306. package/src/components/ramme/popover/fs-popover.ts +205 -0
  307. package/src/components/ramme/popover/popover.css +34 -0
  308. package/src/components/ramme/tabs/fs-tabs.ts +182 -0
  309. package/src/components/ramme/tabs/tabs.css +68 -0
  310. package/src/tailwind/tailwind.css +134 -0
  311. package/src/tokens/color.ts +267 -0
  312. package/src/tokens/theme.ts +350 -0
  313. package/src/tokens/tokens.css +214 -0
  314. package/src/tokens/tokens.ts +293 -0
@@ -0,0 +1,47 @@
1
+ @layer fristil {
2
+ .fs-tooltip {
3
+ position: relative;
4
+ display: inline-flex;
5
+ }
6
+
7
+ .fs-tooltip__bubble {
8
+ position: absolute;
9
+ inset-block-end: calc(100% + var(--size-2));
10
+ inset-inline-start: 50%;
11
+ transform: translateX(-50%);
12
+ z-index: 40;
13
+ inline-size: max-content;
14
+ max-inline-size: var(--fs-tooltip-width, 16rem);
15
+ padding: var(--fs-tooltip-padding, var(--size-1) var(--size-2));
16
+ font-size: var(--font-size-s);
17
+ line-height: 1.4;
18
+ color: var(--semantic-page-background);
19
+ background: var(--semantic-page-foreground);
20
+ border-radius: var(--fs-tooltip-radius, var(--size-1));
21
+ /* Boblen er skjult til utløseren får fokus eller musa. `visibility` og
22
+ ikke `display`, så teksten blir stående i tilgjengelighetstreet og kan
23
+ pekes på med aria-describedby. */
24
+ visibility: hidden;
25
+ opacity: 0;
26
+ }
27
+
28
+ .fs-tooltip:hover .fs-tooltip__bubble,
29
+ .fs-tooltip:focus-within .fs-tooltip__bubble {
30
+ visibility: visible;
31
+ opacity: 1;
32
+ }
33
+
34
+ @media (prefers-reduced-motion: no-preference) {
35
+ .fs-tooltip__bubble {
36
+ transition: opacity 120ms ease;
37
+ }
38
+ }
39
+
40
+ /* Høykontrast: boblen har ingen ramme, og flatene tvinges til samme farge
41
+ som siden. Uten en kant flyter teksten rett oppå innholdet under. */
42
+ @media (forced-colors: active) {
43
+ .fs-tooltip__bubble {
44
+ border: 1px solid CanvasText;
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,36 @@
1
+ import { attributes } from "../shared.js"
2
+
3
+ export const TOOLTIP_CLASS = "fs-tooltip" as const
4
+ /** Selve boblen med teksten. */
5
+ export const TOOLTIP_BUBBLE_CLASS = "fs-tooltip__bubble" as const
6
+
7
+ export type TooltipAttributes = {
8
+ class: typeof TOOLTIP_CLASS
9
+ }
10
+
11
+ /**
12
+ * Attributtene for omslaget rundt en utløser og en hjelpeboble.
13
+ *
14
+ * Boblen vises på hover og på fokus. Fokusdelen er ikke valgfri: en boble som
15
+ * bare kommer med musa finnes ikke for den som bruker tastatur.
16
+ *
17
+ * Teksten i boblen må kobles til utløseren med `aria-describedby`. Uten den
18
+ * er boblen bare noe som dukker opp på skjermen, og leses aldri opp.
19
+ *
20
+ * Bruk den til et tillegg, aldri til noe brukeren må ha. På en berøringsskjerm
21
+ * finnes hverken hover eller fokus før noe trykkes.
22
+ *
23
+ * ```ts
24
+ * <span {...tooltip()}>
25
+ * <button aria-describedby="hint">Arkiver</button>
26
+ * <span class={tooltip.bubble} role="tooltip" id="hint">Flyttes til arkivet</span>
27
+ * </span>
28
+ * ```
29
+ */
30
+ export const tooltip = Object.assign(
31
+ (): TooltipAttributes => attributes({ class: TOOLTIP_CLASS }),
32
+ {
33
+ /** Klassen på boblen med teksten. */
34
+ bubble: TOOLTIP_BUBBLE_CLASS,
35
+ },
36
+ )
@@ -0,0 +1,525 @@
1
+ import { css, html, LitElement } from "lit"
2
+
3
+ export const FS_CALENDAR_TAG = "fs-calendar" as const
4
+
5
+ type CalendarCell = {
6
+ date: Date
7
+ iso: string
8
+ inMonth: boolean
9
+ isToday: boolean
10
+ isSelected: boolean
11
+ }
12
+
13
+ /**
14
+ * Deler dagene inn i uker på sju.
15
+ *
16
+ * role="grid" krever role="row" mellom seg og cellene sine. Uten radene
17
+ * melder skjermlesere rutenettet som tomt, og cellene som løsrevne knapper.
18
+ */
19
+ function splitInWeeks(cells: CalendarCell[]): CalendarCell[][] {
20
+ const weeks: CalendarCell[][] = []
21
+ for (let i = 0; i < cells.length; i += 7) {
22
+ weeks.push(cells.slice(i, i + 7))
23
+ }
24
+ return weeks
25
+ }
26
+
27
+ /**
28
+ * Delnavnene på én dagknapp.
29
+ *
30
+ * Tilstanden må være egne delnavn og ikke bare `data-*`. En attributtselektor
31
+ * etter `::part()` treffer ikke. `::part(day)[data-selected="true"]` er målt
32
+ * uten virkning, så uten `day-selected` ville en konsument ikke hatt noen vei
33
+ * til den valgte dagen utenfra.
34
+ */
35
+ function dayParts(cell: CalendarCell): string {
36
+ const names = ["day"]
37
+ if (!cell.inMonth) names.push("day-outside")
38
+ if (cell.isToday) names.push("day-today")
39
+ if (cell.isSelected) names.push("day-selected")
40
+ return names.join(" ")
41
+ }
42
+
43
+ const weekdayFormatter = new Intl.DateTimeFormat("nb-NO", {
44
+ weekday: "short",
45
+ timeZone: "UTC",
46
+ })
47
+
48
+ const monthFormatter = new Intl.DateTimeFormat("nb-NO", {
49
+ month: "long",
50
+ year: "numeric",
51
+ timeZone: "UTC",
52
+ })
53
+
54
+ function parseIsoDate(value: string | undefined): Date | null {
55
+ if (!value) return null
56
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value)
57
+ if (!match) return null
58
+
59
+ const year = Number(match[1])
60
+ const month = Number(match[2]) - 1
61
+ const day = Number(match[3])
62
+ return new Date(Date.UTC(year, month, day))
63
+ }
64
+
65
+ function formatIsoDate(date: Date): string {
66
+ return date.toISOString().slice(0, 10)
67
+ }
68
+
69
+ function startOfMonth(date: Date): Date {
70
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1))
71
+ }
72
+
73
+ function addDays(date: Date, delta: number): Date {
74
+ return new Date(
75
+ Date.UTC(
76
+ date.getUTCFullYear(),
77
+ date.getUTCMonth(),
78
+ date.getUTCDate() + delta,
79
+ ),
80
+ )
81
+ }
82
+
83
+ function addMonths(date: Date, delta: number): Date {
84
+ return new Date(
85
+ Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + delta, 1),
86
+ )
87
+ }
88
+
89
+ function mondayBasedWeekday(date: Date): number {
90
+ return (date.getUTCDay() + 6) % 7
91
+ }
92
+
93
+ function getToday(): Date {
94
+ const today = new Date()
95
+ return new Date(
96
+ Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()),
97
+ )
98
+ }
99
+
100
+ function getCalendarCells(
101
+ monthDate: Date,
102
+ selectedIso?: string,
103
+ ): CalendarCell[] {
104
+ const selectedDate = parseIsoDate(selectedIso)
105
+ const todayIso = formatIsoDate(getToday())
106
+ const monthStart = startOfMonth(monthDate)
107
+ const firstVisible = addDays(monthStart, -mondayBasedWeekday(monthStart))
108
+
109
+ return Array.from({ length: 42 }, (_, index) => {
110
+ const date = addDays(firstVisible, index)
111
+ const iso = formatIsoDate(date)
112
+ return {
113
+ date,
114
+ iso,
115
+ inMonth: date.getUTCMonth() === monthStart.getUTCMonth(),
116
+ isToday: iso === todayIso,
117
+ isSelected: selectedDate ? iso === formatIsoDate(selectedDate) : false,
118
+ }
119
+ })
120
+ }
121
+
122
+ export class FsCalendar extends LitElement {
123
+ static properties = {
124
+ open: { type: Boolean, reflect: true },
125
+ disabled: { type: Boolean, reflect: true },
126
+ value: { type: String, reflect: true },
127
+ triggerHidden: {
128
+ type: Boolean,
129
+ reflect: true,
130
+ attribute: "trigger-hidden",
131
+ },
132
+ }
133
+
134
+ static styles = css`
135
+ :host {
136
+ display: inline-block;
137
+ font-family: inherit;
138
+ color: var(--semantic-page-foreground);
139
+ position: relative;
140
+ }
141
+
142
+ .trigger {
143
+ display: inline-grid;
144
+ place-items: center;
145
+ min-width: var(--fs-calendar-trigger-size, var(--size-10));
146
+ min-height: var(--fs-calendar-trigger-size, var(--size-10));
147
+ padding: var(--fs-calendar-trigger-padding, 0 var(--size-3));
148
+ font-family: inherit;
149
+ font-size: var(--font-size-m);
150
+ line-height: 1;
151
+ color: var(--semantic-page-foreground);
152
+ background: var(--semantic-page-background);
153
+ border: 1px solid var(--semantic-field-border);
154
+ border-radius: var(--fs-calendar-radius, var(--size-1));
155
+ cursor: pointer;
156
+ }
157
+
158
+ .trigger:hover:not(:disabled) {
159
+ background: var(--semantic-interactive-background);
160
+ border-color: var(--semantic-interactive-main);
161
+ }
162
+
163
+ .trigger:focus-visible,
164
+ .month-button:focus-visible,
165
+ .day:focus-visible {
166
+ outline: 2px solid var(--semantic-interactive-main);
167
+ outline-offset: 2px;
168
+ }
169
+
170
+ .popup {
171
+ position: absolute;
172
+ inset-inline-start: 0;
173
+ inset-block-start: calc(100% + var(--size-1));
174
+ z-index: 30;
175
+ width: var(--fs-calendar-popup-width, min(20rem, 92vw));
176
+ padding: var(--fs-calendar-popup-padding, var(--size-3));
177
+ border: 1px solid var(--semantic-field-border);
178
+ border-radius: var(--fs-calendar-radius, var(--size-1));
179
+ background: var(--semantic-page-background);
180
+ box-shadow: var(--semantic-shadow-overlay);
181
+ }
182
+
183
+ .popup[hidden] {
184
+ display: none;
185
+ }
186
+
187
+ .popup-header {
188
+ display: flex;
189
+ align-items: center;
190
+ justify-content: space-between;
191
+ gap: var(--size-2);
192
+ margin-bottom: var(--size-2);
193
+ }
194
+
195
+ .month-label {
196
+ font-size: var(--font-size-s);
197
+ font-weight: 700;
198
+ text-transform: capitalize;
199
+ }
200
+
201
+ .month-controls {
202
+ display: inline-flex;
203
+ gap: var(--size-1);
204
+ }
205
+
206
+ .month-button,
207
+ .day {
208
+ font-family: inherit;
209
+ border: 1px solid var(--semantic-field-border);
210
+ background: var(--semantic-page-background);
211
+ color: var(--semantic-page-foreground);
212
+ border-radius: var(--fs-calendar-radius, var(--size-1));
213
+ }
214
+
215
+ .month-button {
216
+ min-width: var(--fs-calendar-day-size, var(--size-8));
217
+ min-height: var(--fs-calendar-day-size, var(--size-8));
218
+ cursor: pointer;
219
+ }
220
+
221
+ .weekday-row,
222
+ .day-grid {
223
+ display: grid;
224
+ grid-template-columns: repeat(7, minmax(0, 1fr));
225
+ gap: var(--size-1);
226
+ }
227
+
228
+ /* Ukeradene finnes for ARIA-strukturens skyld. display: contents lar
229
+ dagene ligge rett i rutenettet, så oppsettet er uendret. */
230
+ .week {
231
+ display: contents;
232
+ }
233
+
234
+ .weekday {
235
+ text-align: center;
236
+ font-size: var(--font-size-xxs);
237
+ font-weight: 700;
238
+ text-transform: uppercase;
239
+ letter-spacing: 0.04em;
240
+ color: var(--semantic-muted-foreground);
241
+ padding-block: var(--size-1);
242
+ }
243
+
244
+ .day {
245
+ min-height: var(--fs-calendar-day-size, var(--size-8));
246
+ cursor: pointer;
247
+ font-size: var(--font-size-xs);
248
+ }
249
+
250
+ /* Dagene fra nabomåneden er dempet, men fullt klikkbare. De er altså
251
+ ikke deaktiverte kontroller, og må derfor ha lesbar kontrast. */
252
+ .day[data-outside="true"] {
253
+ color: var(--semantic-neutral-foreground);
254
+ background: var(--semantic-neutral-background);
255
+ }
256
+
257
+ .day[data-today="true"] {
258
+ border-color: var(--semantic-interactive-main);
259
+ }
260
+
261
+ .day[data-selected="true"] {
262
+ background: var(--semantic-interactive-main);
263
+ color: var(--semantic-interactive-contrast);
264
+ border-color: var(--semantic-interactive-main);
265
+ }
266
+
267
+ /*
268
+ * Høykontrast: egne farger overstyres i modusen, så valgt dag og de andre
269
+ * blir like. Systemfargene slipper gjennom, og Highlight med
270
+ * HighlightText er paret operativsystemet bruker til noe som er valgt.
271
+ *
272
+ * Merk: ingen backtick-tegn i kommentarer her. De lukker malen, og resten
273
+ * av fila blir lest som JavaScript.
274
+ */
275
+ @media (forced-colors: active) {
276
+ .day[data-selected="true"] {
277
+ background: Highlight;
278
+ color: HighlightText;
279
+ border-color: Highlight;
280
+ }
281
+ }
282
+
283
+ :host([disabled]) .trigger {
284
+ background: var(--semantic-disabled-background);
285
+ color: var(--semantic-disabled-foreground);
286
+ border-color: var(--semantic-disabled-background);
287
+ /* Knappen er en ekte <button disabled>; pointer-events: none ville
288
+ bare hindret markøren i å vises. */
289
+ cursor: not-allowed;
290
+ }
291
+
292
+ :host([trigger-hidden]) .trigger {
293
+ display: none;
294
+ }
295
+
296
+ :host([trigger-hidden]) .popup {
297
+ inset-block-start: var(--size-1);
298
+ }
299
+ `
300
+
301
+ /** @attr */
302
+ open = false
303
+ /** @attr */
304
+ disabled = false
305
+ /** @attr */
306
+ value = ""
307
+ /** @attr trigger-hidden */
308
+ triggerHidden = false
309
+
310
+ private viewMonth = getToday()
311
+
312
+ connectedCallback() {
313
+ super.connectedCallback()
314
+ this.ownerDocument.addEventListener("click", this.handleDocumentClick)
315
+ }
316
+
317
+ disconnectedCallback() {
318
+ this.ownerDocument.removeEventListener("click", this.handleDocumentClick)
319
+ super.disconnectedCallback()
320
+ }
321
+
322
+ updated(changed: Map<string, unknown>) {
323
+ if (changed.has("value")) {
324
+ this.viewMonth = parseIsoDate(this.value) ?? this.viewMonth
325
+ }
326
+ }
327
+
328
+ render() {
329
+ const monthCells = this.open
330
+ ? getCalendarCells(this.viewMonth, this.value)
331
+ : []
332
+
333
+ return html`
334
+ <button
335
+ class="trigger"
336
+ part="trigger"
337
+ type="button"
338
+ aria-label="Åpne kalender"
339
+ aria-expanded=${this.open ? "true" : "false"}
340
+ ?disabled=${this.disabled}
341
+ @click=${this.togglePopup}
342
+ >
343
+ 📅
344
+ </button>
345
+
346
+ <div class="popup" part="popup" ?hidden=${!this.open} @keydown=${this.handlePopupKeydown}>
347
+ <div class="popup-header" part="popup-header">
348
+ <div class="month-label" part="month-label" aria-live="polite">${monthFormatter.format(this.viewMonth)}</div>
349
+ <div class="month-controls" part="month-controls">
350
+ <button class="month-button" part="month-button" type="button" aria-label="Forrige måned" @click=${this.goToPreviousMonth}>‹</button>
351
+ <button class="month-button" part="month-button" type="button" aria-label="Neste måned" @click=${this.goToNextMonth}>›</button>
352
+ </div>
353
+ </div>
354
+
355
+ <div class="weekday-row" part="weekday-row" aria-hidden="true">
356
+ ${[0, 1, 2, 3, 4, 5, 6].map(
357
+ (index) =>
358
+ html`<div class="weekday" part="weekday">${weekdayFormatter.format(new Date(Date.UTC(2024, 0, 1 + index)))}</div>`,
359
+ )}
360
+ </div>
361
+
362
+ <div class="day-grid" part="day-grid" role="grid" aria-label=${monthFormatter.format(this.viewMonth)}>
363
+ ${splitInWeeks(monthCells).map(
364
+ (uke) => html`
365
+ <div class="week" role="row">
366
+ ${uke.map(
367
+ (cell) => html`
368
+ <button
369
+ class="day"
370
+ part=${dayParts(cell)}
371
+ type="button"
372
+ role="gridcell"
373
+ data-date=${cell.iso}
374
+ data-outside=${String(!cell.inMonth)}
375
+ data-today=${String(cell.isToday)}
376
+ data-selected=${String(cell.isSelected)}
377
+ aria-selected=${String(cell.isSelected)}
378
+ @click=${this.selectCalendarDate}
379
+ >
380
+ ${cell.date.getUTCDate()}
381
+ </button>
382
+ `,
383
+ )}
384
+ </div>
385
+ `,
386
+ )}
387
+ </div>
388
+ </div>
389
+ `
390
+ }
391
+
392
+ openPopup() {
393
+ if (this.disabled) return
394
+ this.viewMonth = parseIsoDate(this.value) ?? getToday()
395
+ this.open = true
396
+ this.requestUpdate()
397
+ queueMicrotask(() => {
398
+ const selected = this.renderRoot.querySelector<HTMLButtonElement>(
399
+ `.day[data-selected="true"]`,
400
+ )
401
+ selected?.focus()
402
+ })
403
+ }
404
+
405
+ closePopup() {
406
+ if (!this.open) return
407
+
408
+ // Panelet skjules med [hidden]. Står fokus på en dag inne i det, mister
409
+ // nettleseren fokus til <body>, og en tastaturbruker havner på toppen av
410
+ // siden. Fokus skal derfor tilbake til knappen som åpnet panelet.
411
+ // Lukkes panelet fordi brukeren klikket eller tabbet ut, står fokus
412
+ // allerede et annet sted, og da skal vi ikke rive det til oss.
413
+ const fokusStoInniPanelet =
414
+ (this.renderRoot as ShadowRoot).activeElement !== null
415
+
416
+ this.open = false
417
+ this.requestUpdate()
418
+
419
+ if (fokusStoInniPanelet) {
420
+ queueMicrotask(() => {
421
+ this.renderRoot.querySelector<HTMLButtonElement>(".trigger")?.focus()
422
+ })
423
+ }
424
+ }
425
+
426
+ private togglePopup = () => {
427
+ if (this.open) {
428
+ this.closePopup()
429
+ } else {
430
+ this.openPopup()
431
+ }
432
+ }
433
+
434
+ private goToPreviousMonth = () => {
435
+ this.viewMonth = addMonths(this.viewMonth, -1)
436
+ this.requestUpdate()
437
+ }
438
+
439
+ private goToNextMonth = () => {
440
+ this.viewMonth = addMonths(this.viewMonth, 1)
441
+ this.requestUpdate()
442
+ }
443
+
444
+ private selectCalendarDate = (event: Event) => {
445
+ const target = event.currentTarget as HTMLButtonElement | null
446
+ const iso = target?.dataset.date
447
+ if (!iso) return
448
+
449
+ this.value = iso
450
+ this.viewMonth = parseIsoDate(iso) ?? this.viewMonth
451
+
452
+ // Lukk før hendelsene sendes. closePopup køer et mikrotaskkall som
453
+ // flytter fokus til knappen. En lytter som selv vil flytte fokus, slik
454
+ // fs-date-field gjør til inputfeltet, køer sitt etterpå og vinner.
455
+ this.closePopup()
456
+
457
+ this.dispatchEvent(
458
+ new CustomEvent("date-select", {
459
+ detail: { value: iso },
460
+ bubbles: true,
461
+ composed: true,
462
+ }),
463
+ )
464
+ this.dispatchEvent(new Event("input", { bubbles: true, composed: true }))
465
+ this.dispatchEvent(new Event("change", { bubbles: true, composed: true }))
466
+ }
467
+
468
+ private handlePopupKeydown = (event: KeyboardEvent) => {
469
+ if (event.key === "Escape") {
470
+ event.preventDefault()
471
+ this.closePopup()
472
+ return
473
+ }
474
+
475
+ const current = event.target as HTMLButtonElement | null
476
+ if (!current?.classList.contains("day")) return
477
+
478
+ const iso = current.dataset.date
479
+ if (!iso) return
480
+
481
+ const targetDate = parseIsoDate(iso)
482
+ if (!targetDate) return
483
+
484
+ let nextDate: Date | null = null
485
+ if (event.key === "ArrowLeft") nextDate = addDays(targetDate, -1)
486
+ if (event.key === "ArrowRight") nextDate = addDays(targetDate, 1)
487
+ if (event.key === "ArrowUp") nextDate = addDays(targetDate, -7)
488
+ if (event.key === "ArrowDown") nextDate = addDays(targetDate, 7)
489
+ if (event.key === "Home") nextDate = startOfMonth(targetDate)
490
+ if (event.key === "End")
491
+ nextDate = addDays(addMonths(startOfMonth(targetDate), 1), -1)
492
+
493
+ if (!nextDate) return
494
+
495
+ event.preventDefault()
496
+ this.viewMonth = startOfMonth(nextDate)
497
+ this.requestUpdate()
498
+
499
+ queueMicrotask(() => {
500
+ const nextButton = this.renderRoot.querySelector<HTMLButtonElement>(
501
+ `.day[data-date="${formatIsoDate(nextDate)}"]`,
502
+ )
503
+ nextButton?.focus()
504
+ })
505
+ }
506
+
507
+ private handleDocumentClick = (event: Event) => {
508
+ if (!this.open) return
509
+ const path = event.composedPath()
510
+ if (path.includes(this)) return
511
+ this.closePopup()
512
+ }
513
+ }
514
+
515
+ declare global {
516
+ interface HTMLElementTagNameMap {
517
+ "fs-calendar": FsCalendar
518
+ }
519
+ }
520
+
521
+ export function defineFsCalendar(tagName = FS_CALENDAR_TAG): void {
522
+ if (!customElements.get(tagName)) {
523
+ customElements.define(tagName, FsCalendar)
524
+ }
525
+ }
@@ -0,0 +1,54 @@
1
+ @import "../../css/label/label.css";
2
+ @import "../../css/input/input.css";
3
+ @import "../../css/help-text/help-text.css";
4
+ @import "../../css/error-text/error-text.css";
5
+
6
+ @layer fristil {
7
+ /* Oppsettet lå tidligere i style=-attributter på elementene. Inline stil
8
+ slår alt en konsument kan skrive uten !important, så komponenten var i
9
+ praksis låst. Her er den vanlig CSS i laget vårt, som alt annet. */
10
+
11
+ .fs-date-field__field {
12
+ position: relative;
13
+ display: inline-grid;
14
+ width: 100%;
15
+ }
16
+
17
+ /* Plass til ikonknappen inne i feltet */
18
+ .fs-date-field__field .fs-input {
19
+ padding-inline-end: var(--fs-date-field-icon-space, var(--size-10));
20
+ }
21
+
22
+ .fs-date-field__icon-btn {
23
+ position: absolute;
24
+ inset-block-start: 50%;
25
+ inset-inline-end: var(--size-2);
26
+ transform: translateY(-50%);
27
+ display: inline-grid;
28
+ place-items: center;
29
+ width: var(--fs-date-field-icon-size, var(--size-7));
30
+ height: var(--fs-date-field-icon-size, var(--size-7));
31
+ padding: 0;
32
+ border: none;
33
+ background: transparent;
34
+ color: var(--semantic-page-foreground);
35
+ border-radius: var(--fs-date-field-icon-radius, var(--size-1));
36
+ cursor: pointer;
37
+ }
38
+
39
+ .fs-date-field__icon-btn:disabled {
40
+ color: var(--semantic-disabled-foreground);
41
+ cursor: not-allowed;
42
+ }
43
+
44
+ /* Kalenderen er et null-stort feste panelet henger under, slik at panelet
45
+ ikke påvirker høyden på feltet. */
46
+ .fs-date-field__calendar {
47
+ position: absolute;
48
+ inset-block-start: 100%;
49
+ inset-inline-start: 0;
50
+ width: 0;
51
+ height: 0;
52
+ overflow: visible;
53
+ }
54
+ }