@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,53 @@
1
+ import { attributes, createGuard } from "../shared.js"
2
+
3
+ export const PARAGRAPH_CLASS = "fs-paragraph" as const
4
+
5
+ export const paragraphSizes = ["small", "medium", "large"] as const
6
+ export const paragraphVariants = ["default", "lead"] as const
7
+
8
+ export type ParagraphSize = (typeof paragraphSizes)[number]
9
+ export type ParagraphVariant = (typeof paragraphVariants)[number]
10
+ export type NonDefaultParagraphSize = Exclude<ParagraphSize, "medium">
11
+ export type NonDefaultParagraphVariant = Exclude<ParagraphVariant, "default">
12
+
13
+ export type ParagraphOptions = {
14
+ /** Tekststørrelse. Standard: `medium`. */
15
+ size?: ParagraphSize
16
+ /** `lead` er ingressen rett under overskriften. Standard: `default`. */
17
+ variant?: ParagraphVariant
18
+ }
19
+
20
+ export type ParagraphAttributes = {
21
+ class: typeof PARAGRAPH_CLASS
22
+ "data-size"?: NonDefaultParagraphSize
23
+ "data-variant"?: NonDefaultParagraphVariant
24
+ }
25
+
26
+ /**
27
+ * Attributtene for et avsnitt.
28
+ *
29
+ * Bredden er begrenset til 70 tegn. Lengre linjer gjør det vanskelig å finne
30
+ * tilbake til starten av neste, og det er den vanligste grunnen til at en
31
+ * tekst er tung å lese selv når skriften er stor nok.
32
+ *
33
+ * ```ts
34
+ * <p {...paragraph({ variant: "lead" })}>Kort om hva siden handler om.</p>
35
+ * ```
36
+ */
37
+ export const paragraph = Object.assign(
38
+ ({
39
+ size = "medium",
40
+ variant = "default",
41
+ }: ParagraphOptions = {}): ParagraphAttributes =>
42
+ attributes({
43
+ class: PARAGRAPH_CLASS,
44
+ "data-size": size === "medium" ? undefined : size,
45
+ "data-variant": variant === "default" ? undefined : variant,
46
+ }),
47
+ {
48
+ sizes: paragraphSizes,
49
+ isSize: createGuard(paragraphSizes),
50
+ variants: paragraphVariants,
51
+ isVariant: createGuard(paragraphVariants),
52
+ },
53
+ )
@@ -0,0 +1,76 @@
1
+ @layer fristil {
2
+ .fs-radio {
3
+ box-sizing: border-box;
4
+ appearance: none;
5
+ flex: none;
6
+ display: inline-block;
7
+ inline-size: var(--fs-radio-size, var(--size-5));
8
+ block-size: var(--fs-radio-size, var(--size-5));
9
+ margin: 0;
10
+ background-color: var(--semantic-page-background);
11
+ border: 1px solid var(--semantic-field-border);
12
+ border-radius: 50%;
13
+ cursor: pointer;
14
+ }
15
+
16
+ .fs-radio:hover:not(:disabled) {
17
+ border-color: var(--semantic-field-border-hover);
18
+ }
19
+
20
+ .fs-radio:focus-visible {
21
+ outline: 2px solid var(--semantic-interactive-main);
22
+ outline-offset: 2px;
23
+ }
24
+
25
+ /* Prikken er en gradient og ikke et bilde, så den tar farge fra tokenet
26
+ direkte og trenger ingen egen variant for mørkt tema. */
27
+ .fs-radio:checked {
28
+ background-color: var(--semantic-interactive-main);
29
+ border-color: var(--semantic-interactive-main);
30
+ background-image: radial-gradient(
31
+ circle at center,
32
+ var(--semantic-interactive-contrast) 0 32%,
33
+ transparent 34%
34
+ );
35
+ }
36
+
37
+ .fs-radio:disabled {
38
+ background-color: var(--semantic-disabled-background);
39
+ border-color: var(--semantic-disabled-background);
40
+ cursor: not-allowed;
41
+ }
42
+
43
+ .fs-radio:disabled:checked {
44
+ background-color: var(--semantic-disabled-foreground);
45
+ border-color: var(--semantic-disabled-foreground);
46
+ }
47
+
48
+ .fs-radio[data-state="invalid"] {
49
+ border-color: var(--semantic-danger-foreground);
50
+ }
51
+
52
+ .fs-radio[data-state="invalid"]:focus-visible {
53
+ outline-color: var(--semantic-danger-foreground);
54
+ }
55
+
56
+ .fs-radio[data-state="success"] {
57
+ border-color: var(--semantic-success-foreground);
58
+ }
59
+
60
+ .fs-radio-row {
61
+ display: flex;
62
+ align-items: flex-start;
63
+ gap: var(--size-2);
64
+ line-height: 1.5;
65
+ }
66
+
67
+ /* Høykontrast: prikken er en gradient, og gradienter fjernes i modusen.
68
+ Nettleserens egen radioknapp tegnes riktig i alle temaer. */
69
+ @media (forced-colors: active) {
70
+ .fs-radio,
71
+ .fs-radio:checked {
72
+ appearance: auto;
73
+ background-image: none;
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,50 @@
1
+ import {
2
+ attributes,
3
+ type FieldState,
4
+ fieldStates,
5
+ isFieldState,
6
+ type NonDefaultFieldState,
7
+ } from "../shared.js"
8
+
9
+ export const RADIO_CLASS = "fs-radio" as const
10
+ /** Raden som holder knappen og teksten ved siden av hverandre. */
11
+ export const RADIO_ROW_CLASS = "fs-radio-row" as const
12
+
13
+ export type RadioOptions = {
14
+ /** Valideringstilstand. Standard: `default`. */
15
+ state?: FieldState
16
+ }
17
+
18
+ export type RadioAttributes = {
19
+ class: typeof RADIO_CLASS
20
+ type: "radio"
21
+ "data-state"?: NonDefaultFieldState
22
+ "aria-invalid"?: "true"
23
+ }
24
+
25
+ /**
26
+ * Attributtene for en radioknapp.
27
+ *
28
+ * Alle knappene i en gruppe må ha samme `name`. Det er den koblingen som gjør
29
+ * at bare én kan være valgt, og at piltastene flytter mellom dem. `name`
30
+ * setter du selv, siden bare du vet hva gruppen heter.
31
+ *
32
+ * ```ts
33
+ * <input {...radio()} name="leveringsmate" value="post" />
34
+ * ```
35
+ */
36
+ export const radio = Object.assign(
37
+ ({ state = "default" }: RadioOptions = {}): RadioAttributes =>
38
+ attributes({
39
+ class: RADIO_CLASS,
40
+ type: "radio" as const,
41
+ "data-state": state === "default" ? undefined : state,
42
+ "aria-invalid": state === "invalid" ? ("true" as const) : undefined,
43
+ }),
44
+ {
45
+ /** Klassen på raden rundt knappen og teksten. */
46
+ row: RADIO_ROW_CLASS,
47
+ states: fieldStates,
48
+ isState: isFieldState,
49
+ },
50
+ )
@@ -0,0 +1,49 @@
1
+ @import "../input/input.css";
2
+
3
+ @layer fristil {
4
+ /*
5
+ * Begge klassene i selektoren, ikke bare .fs-search.
6
+ *
7
+ * .fs-input setter padding med kortformen, og .fs-search bare
8
+ * padding-inline-start. Med lik spesifisitet avgjør rekkefølgen stilarkene
9
+ * lastes i, og importerer konsumenten input.css sist, forsvinner plassen
10
+ * til ikonet. Med begge klassene vinner regelen uansett rekkefølge.
11
+ */
12
+ .fs-input.fs-search {
13
+ padding-inline-start: var(--fs-search-icon-space, var(--size-10));
14
+ background-image: var(--semantic-icon-search);
15
+ background-repeat: no-repeat;
16
+ background-position: left var(--size-3) center;
17
+ background-size: var(--fs-search-icon-size, var(--size-5));
18
+ }
19
+
20
+ .fs-search:dir(rtl) {
21
+ background-position: right var(--size-3) center;
22
+ }
23
+
24
+ /* Nettleserens eget kryss i et type="search"-felt. Safari og Chrome tegner
25
+ det ulikt, og Firefox ikke i det hele tatt. */
26
+ .fs-search::-webkit-search-cancel-button {
27
+ cursor: pointer;
28
+ }
29
+
30
+ .fs-search-row {
31
+ display: flex;
32
+ align-items: stretch;
33
+ gap: var(--size-2);
34
+ }
35
+
36
+ .fs-search-row .fs-search {
37
+ flex: 1 1 auto;
38
+ min-inline-size: 0;
39
+ }
40
+
41
+ /* Høykontrast: forstørrelsesglasset er malt inn i bildet med fast farge, og
42
+ kan bli usynlig. Da er det bedre å ta det bort og gi plassen tilbake. */
43
+ @media (forced-colors: active) {
44
+ .fs-input.fs-search {
45
+ background-image: none;
46
+ padding-inline-start: var(--size-3);
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,52 @@
1
+ import { INPUT_CLASS, input } from "../input/input.js"
2
+ import {
3
+ attributes,
4
+ type FieldState,
5
+ fieldStates,
6
+ isFieldState,
7
+ } from "../shared.js"
8
+
9
+ export const SEARCH_CLASS = "fs-search" as const
10
+ /** Raden med søkefeltet og knappen ved siden av. */
11
+ export const SEARCH_ROW_CLASS = "fs-search-row" as const
12
+
13
+ export type SearchOptions = {
14
+ /** Valideringstilstand. Standard: `default`. */
15
+ state?: FieldState
16
+ }
17
+
18
+ export type SearchAttributes = {
19
+ class: `${typeof INPUT_CLASS} ${typeof SEARCH_CLASS}`
20
+ type: "search"
21
+ "data-state"?: "invalid" | "success"
22
+ "aria-invalid"?: "true"
23
+ }
24
+
25
+ /**
26
+ * Attributtene for et søkefelt.
27
+ *
28
+ * Feltet er en `fs-input` med plass til forstørrelsesglasset, så begge
29
+ * klassene settes. `type="search"` gir nettleserens eget kryss for å tømme
30
+ * feltet, og på mobil et tastatur med «Søk» i stedet for «Enter».
31
+ *
32
+ * ```ts
33
+ * <input {...search()} name="q" />
34
+ * ```
35
+ */
36
+ export const search = Object.assign(
37
+ ({ state = "default" }: SearchOptions = {}): SearchAttributes => {
38
+ const base = input({ type: "search", state })
39
+ return attributes({
40
+ ...base,
41
+ class: `${INPUT_CLASS} ${SEARCH_CLASS}` as const,
42
+ type: "search" as const,
43
+ "data-variant": undefined,
44
+ }) as SearchAttributes
45
+ },
46
+ {
47
+ /** Klassen på raden med feltet og knappen. */
48
+ row: SEARCH_ROW_CLASS,
49
+ states: fieldStates,
50
+ isState: isFieldState,
51
+ },
52
+ )
@@ -0,0 +1,95 @@
1
+ @layer fristil {
2
+ .fs-select {
3
+ /* Designsystemet kan ikke regne med at konsumenten har en CSS-reset.
4
+ Uten border-box legges padding og ramme utenpå width, og feltet
5
+ blir bredere enn boksen det ligger i. */
6
+ box-sizing: border-box;
7
+
8
+ display: block;
9
+ width: 100%;
10
+ padding: var(--size-2) calc(var(--size-4) + var(--size-2)) var(--size-2)
11
+ var(--size-3);
12
+ font-family: inherit;
13
+ font-size: var(--font-size-m);
14
+ line-height: 1.5;
15
+ color: var(--semantic-page-foreground);
16
+ background-color: var(--semantic-page-background);
17
+ border: 1px solid var(--semantic-field-border);
18
+ border-radius: var(--fs-select-radius, var(--size-1));
19
+ appearance: none;
20
+ background-image:
21
+ linear-gradient(
22
+ 45deg,
23
+ transparent 50%,
24
+ var(--semantic-muted-foreground) 50%
25
+ ),
26
+ linear-gradient(
27
+ 135deg,
28
+ var(--semantic-muted-foreground) 50%,
29
+ transparent 50%
30
+ );
31
+ background-position:
32
+ calc(100% - 1rem) calc(50% - 0.1rem),
33
+ calc(100% - 0.75rem) calc(50% - 0.1rem);
34
+ background-size:
35
+ 0.35rem 0.35rem,
36
+ 0.35rem 0.35rem;
37
+ background-repeat: no-repeat;
38
+ }
39
+
40
+ .fs-select:hover {
41
+ border-color: var(--semantic-field-border-hover);
42
+ }
43
+
44
+ .fs-select:focus-visible {
45
+ outline: 2px solid var(--semantic-interactive-main);
46
+ outline-offset: 2px;
47
+ border-color: var(--semantic-interactive-main);
48
+ }
49
+
50
+ .fs-select:disabled,
51
+ .fs-select[aria-disabled="true"] {
52
+ background-color: var(--semantic-disabled-background);
53
+ color: var(--semantic-disabled-foreground);
54
+ border-color: var(--semantic-disabled-background);
55
+ /* Ingen pointer-events: none her. Nettleseren blokkerer allerede et
56
+ deaktivert felt, og pointer-events ville bare hindret markøren i å
57
+ vises. Elementet blir da ikke truffet av musa i det hele tatt. */
58
+ cursor: not-allowed;
59
+ }
60
+
61
+ /* aria-disabled er bare en opplysning til skjermlesere; elementet virker
62
+ fortsatt. Her stopper pointer-events musa, på bekostning av markøren.
63
+ Skal feltet faktisk være av, bruk `disabled` eller `readonly`. */
64
+ .fs-select[aria-disabled="true"] {
65
+ pointer-events: none;
66
+ }
67
+
68
+ .fs-select[data-state="invalid"] {
69
+ border-color: var(--semantic-danger-foreground);
70
+ background-color: var(--semantic-danger-background);
71
+ }
72
+
73
+ .fs-select[data-state="invalid"]:focus-visible {
74
+ outline-color: var(--semantic-danger-foreground);
75
+ }
76
+
77
+ .fs-select[data-state="success"] {
78
+ border-color: var(--semantic-success-foreground);
79
+ background-color: var(--semantic-success-background);
80
+ }
81
+
82
+ .fs-select[data-state="success"]:focus-visible {
83
+ outline-color: var(--semantic-success-foreground);
84
+ }
85
+
86
+ /* Høykontrast: pilen er to gradienter, og de fjernes i modusen. Da er
87
+ nettleserens egen pil bedre enn ingen. */
88
+ @media (forced-colors: active) {
89
+ .fs-select {
90
+ appearance: auto;
91
+ background-image: none;
92
+ padding-inline-end: var(--size-3);
93
+ }
94
+ }
95
+ }
@@ -0,0 +1,40 @@
1
+ import {
2
+ attributes,
3
+ type FieldState,
4
+ fieldStates,
5
+ isFieldState,
6
+ type NonDefaultFieldState,
7
+ } from "../shared.js"
8
+
9
+ export const SELECT_CLASS = "fs-select" as const
10
+
11
+ export type SelectOptions = {
12
+ /** Valideringstilstand. Standard: `default`. */
13
+ state?: FieldState
14
+ }
15
+
16
+ export type SelectAttributes = {
17
+ class: typeof SELECT_CLASS
18
+ "data-state"?: NonDefaultFieldState
19
+ "aria-invalid"?: "true"
20
+ }
21
+
22
+ /**
23
+ * Attributtene for en nedtrekksliste.
24
+ *
25
+ * ```ts
26
+ * <select {...select({ state: "invalid" })} />
27
+ * ```
28
+ */
29
+ export const select = Object.assign(
30
+ ({ state = "default" }: SelectOptions = {}): SelectAttributes =>
31
+ attributes({
32
+ class: SELECT_CLASS,
33
+ "data-state": state === "default" ? undefined : state,
34
+ "aria-invalid": state === "invalid" ? ("true" as const) : undefined,
35
+ }),
36
+ {
37
+ states: fieldStates,
38
+ isState: isFieldState,
39
+ },
40
+ )
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Felles byggeklosser for komponent-API-et.
3
+ *
4
+ * Hver komponent eksporterer én funksjon som tar et valgobjekt og returnerer
5
+ * attributtene du sprer inn i elementet. Formen er lik for alle, slik at du
6
+ * bare trenger å lære den én gang.
7
+ */
8
+
9
+ /** Fjerner attributter uten verdi, så `{...spredning}` ikke setter tomme felt. */
10
+ export function attributes<T extends Record<string, unknown>>(values: T): T {
11
+ const result = {} as T
12
+ for (const [name, value] of Object.entries(values)) {
13
+ if (value !== undefined) {
14
+ ;(result as Record<string, unknown>)[name] = value
15
+ }
16
+ }
17
+ return result
18
+ }
19
+
20
+ /** Lager en typevakt for en liste av lovlige strenger. */
21
+ export function createGuard<T extends string>(allowed: readonly T[]) {
22
+ return (value: string): value is T =>
23
+ (allowed as readonly string[]).includes(value)
24
+ }
25
+
26
+ /**
27
+ * Valideringstilstand for skjemakontroller.
28
+ *
29
+ * Input, textarea og select hadde hver sin identiske type. Nå er det én, så
30
+ * en verdi kan sendes mellom dem uten konvertering.
31
+ */
32
+ export const fieldStates = ["default", "invalid", "success"] as const
33
+ export type FieldState = (typeof fieldStates)[number]
34
+ export type NonDefaultFieldState = Exclude<FieldState, "default">
35
+ export const isFieldState = createGuard(fieldStates)
36
+
37
+ /** Hvordan et påkrevd felt markeres i ledeteksten. */
38
+ export const requiredMarkers = ["symbol", "text"] as const
39
+ export type RequiredMarker = (typeof requiredMarkers)[number]
40
+ export const isRequiredMarker = createGuard(requiredMarkers)
@@ -0,0 +1,49 @@
1
+ @layer fristil {
2
+ .fs-skeleton {
3
+ box-sizing: border-box;
4
+ display: block;
5
+ inline-size: 100%;
6
+ block-size: var(--fs-skeleton-height, var(--size-4));
7
+ border-radius: var(--fs-skeleton-radius, var(--size-1));
8
+ background: var(--semantic-neutral-background);
9
+ /* Skimmeret er en gradient som beveger seg over flaten, ikke en
10
+ gjennomsiktighet som pulserer. Da blir kontrasten mot sideflaten den
11
+ samme hele veien. */
12
+ background-image: linear-gradient(
13
+ 90deg,
14
+ transparent 0%,
15
+ var(--semantic-page-background) 50%,
16
+ transparent 100%
17
+ );
18
+ background-size: 200% 100%;
19
+ background-repeat: no-repeat;
20
+ animation: fs-skeleton-shimmer 1600ms ease-in-out infinite;
21
+ }
22
+
23
+ .fs-skeleton[data-variant="text"] {
24
+ block-size: var(--font-size-m);
25
+ border-radius: var(--size-0-5);
26
+ }
27
+
28
+ .fs-skeleton[data-variant="circle"] {
29
+ inline-size: var(--fs-skeleton-height, var(--size-10));
30
+ block-size: var(--fs-skeleton-height, var(--size-10));
31
+ border-radius: 50%;
32
+ }
33
+
34
+ @keyframes fs-skeleton-shimmer {
35
+ from {
36
+ background-position: 150% 0;
37
+ }
38
+ to {
39
+ background-position: -50% 0;
40
+ }
41
+ }
42
+
43
+ @media (prefers-reduced-motion: reduce) {
44
+ .fs-skeleton {
45
+ animation: none;
46
+ background-image: none;
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,43 @@
1
+ import { attributes, createGuard } from "../shared.js"
2
+
3
+ export const SKELETON_CLASS = "fs-skeleton" as const
4
+
5
+ export const skeletonVariants = ["block", "text", "circle"] as const
6
+
7
+ export type SkeletonVariant = (typeof skeletonVariants)[number]
8
+ export type NonDefaultSkeletonVariant = Exclude<SkeletonVariant, "block">
9
+
10
+ export type SkeletonOptions = {
11
+ /** Form. Standard: `block`. */
12
+ variant?: SkeletonVariant
13
+ }
14
+
15
+ export type SkeletonAttributes = {
16
+ class: typeof SKELETON_CLASS
17
+ "data-variant"?: NonDefaultSkeletonVariant
18
+ "aria-hidden": "true"
19
+ }
20
+
21
+ /**
22
+ * Attributtene for en plassholder mens innholdet lastes.
23
+ *
24
+ * Formene er alltid skjult for skjermlesere. De sier ingenting om hva som
25
+ * kommer, og en rekke tomme bokser som leses opp er verre enn stillhet. Si
26
+ * fra om ventingen ett sted i stedet, med `role="status"`.
27
+ *
28
+ * ```ts
29
+ * <div {...skeleton({ variant: "text" })} />
30
+ * ```
31
+ */
32
+ export const skeleton = Object.assign(
33
+ ({ variant = "block" }: SkeletonOptions = {}): SkeletonAttributes =>
34
+ attributes({
35
+ class: SKELETON_CLASS,
36
+ "data-variant": variant === "block" ? undefined : variant,
37
+ "aria-hidden": "true" as const,
38
+ }),
39
+ {
40
+ variants: skeletonVariants,
41
+ isVariant: createGuard(skeletonVariants),
42
+ },
43
+ )
@@ -0,0 +1,36 @@
1
+ @layer fristil {
2
+ /* Lenken ligger utenfor skjermen til den får fokus. Den kan ikke skjules
3
+ med display eller visibility, for da kan tastaturet heller ikke nå den,
4
+ og hele poenget forsvinner. */
5
+ .fs-skip-link {
6
+ position: absolute;
7
+ inset-inline-start: var(--size-2);
8
+ inset-block-start: var(--size-2);
9
+ z-index: 100;
10
+ transform: translateY(calc(-100% - var(--size-4)));
11
+ display: inline-block;
12
+ padding: var(--fs-skip-link-padding, var(--size-2) var(--size-4));
13
+ font-family: inherit;
14
+ font-size: var(--font-size-m);
15
+ line-height: 1.5;
16
+ color: var(--semantic-interactive-contrast);
17
+ background: var(--semantic-interactive-main);
18
+ border-radius: var(--fs-skip-link-radius, var(--size-1));
19
+ text-decoration: underline;
20
+ }
21
+
22
+ /* `:focus` og ikke `:focus-visible`. Nettleseren regner ikke fokus satt
23
+ fra kode som «synlig fokus», og lenken ville da blitt stående skjult for
24
+ den som hopper dit fra et skript. */
25
+ .fs-skip-link:focus {
26
+ transform: translateY(0);
27
+ outline: 2px solid var(--semantic-page-foreground);
28
+ outline-offset: 2px;
29
+ }
30
+
31
+ @media (prefers-reduced-motion: no-preference) {
32
+ .fs-skip-link {
33
+ transition: transform 120ms ease;
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,22 @@
1
+ import { attributes } from "../shared.js"
2
+
3
+ export const SKIP_LINK_CLASS = "fs-skip-link" as const
4
+
5
+ export type SkipLinkAttributes = {
6
+ class: typeof SKIP_LINK_CLASS
7
+ }
8
+
9
+ /**
10
+ * Attributtene for en hopp-til-innhold-lenke.
11
+ *
12
+ * Lenken skal være det første elementet i `<body>`, og peke på id-en til
13
+ * hovedinnholdet. Den ligger utenfor skjermen til den får fokus, og kan
14
+ * derfor ikke skjules med `display: none`: da kan tastaturet heller ikke nå
15
+ * den, og hele poenget forsvinner.
16
+ *
17
+ * ```ts
18
+ * <a {...skipLink()} href="#hovedinnhold">Hopp til hovedinnhold</a>
19
+ * ```
20
+ */
21
+ export const skipLink = (): SkipLinkAttributes =>
22
+ attributes({ class: SKIP_LINK_CLASS })
@@ -0,0 +1,48 @@
1
+ @layer fristil {
2
+ .fs-spinner {
3
+ box-sizing: border-box;
4
+ display: inline-block;
5
+ inline-size: var(--fs-spinner-size, var(--size-6));
6
+ block-size: var(--fs-spinner-size, var(--size-6));
7
+ border: var(--fs-spinner-thickness, 2px) solid
8
+ var(--semantic-neutral-background);
9
+ border-block-start-color: var(--semantic-interactive-main);
10
+ border-radius: 50%;
11
+ animation: fs-spinner-rotate 800ms linear infinite;
12
+ }
13
+
14
+ .fs-spinner[data-size="small"] {
15
+ inline-size: var(--size-4);
16
+ block-size: var(--size-4);
17
+ }
18
+
19
+ .fs-spinner[data-size="large"] {
20
+ inline-size: var(--size-10);
21
+ block-size: var(--size-10);
22
+ border-width: 3px;
23
+ }
24
+
25
+ @keyframes fs-spinner-rotate {
26
+ to {
27
+ transform: rotate(360deg);
28
+ }
29
+ }
30
+
31
+ /* Brukeren har bedt om mindre bevegelse. Da snurrer den saktere framfor å
32
+ stå stille: en indikator som ikke beveger seg sier ikke lenger at noe
33
+ pågår. */
34
+ @media (prefers-reduced-motion: reduce) {
35
+ .fs-spinner {
36
+ animation-duration: 2400ms;
37
+ }
38
+ }
39
+
40
+ /* Høykontrast: alle fire kantene tvinges til samme farge, og ringen blir en
41
+ jevn sirkel. Den snurrer fortsatt, men `Highlight` gir tilbake punktet
42
+ som viser at det skjer noe. */
43
+ @media (forced-colors: active) {
44
+ .fs-spinner {
45
+ border-block-start-color: Highlight;
46
+ }
47
+ }
48
+ }