@nordmanai/nif 0.1.0 → 0.2.1

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 (253) hide show
  1. package/README.md +18 -0
  2. package/bin/nif.mjs +196 -0
  3. package/dist/index.d.ts +30 -1
  4. package/dist/index.js +9661 -2499
  5. package/dist/styles.css +1 -1
  6. package/dist/tokens.css +4 -1
  7. package/dist/ui/components/nm-ai/nm-ai.d.ts +7 -7
  8. package/dist/ui/components/nm-brand-logo/nm-brand-logo.d.ts +10 -0
  9. package/dist/ui/components/nm-breadcrumb/nm-breadcrumb.d.ts +11 -0
  10. package/dist/ui/components/nm-button/nm-button.d.ts +2 -0
  11. package/dist/ui/components/nm-button-group/nm-button-group.d.ts +17 -0
  12. package/dist/ui/components/nm-calendar/nm-calendar.d.ts +3 -0
  13. package/dist/ui/components/nm-card/nm-card.d.ts +34 -0
  14. package/dist/ui/components/nm-chip/nm-chip.d.ts +38 -0
  15. package/dist/ui/components/nm-code-block/nm-code-block.d.ts +55 -0
  16. package/dist/ui/components/nm-color-picker/nm-color-picker.d.ts +15 -0
  17. package/dist/ui/components/nm-combobox/nm-combobox.d.ts +26 -0
  18. package/dist/ui/components/nm-command/nm-command.d.ts +37 -0
  19. package/dist/ui/components/nm-composer/nm-composer.d.ts +4 -0
  20. package/dist/ui/components/nm-context-menu/nm-context-menu.d.ts +9 -0
  21. package/dist/ui/components/nm-date-picker/nm-date-picker.d.ts +35 -0
  22. package/dist/ui/components/nm-dialog/nm-dialog.d.ts +13 -0
  23. package/dist/ui/components/nm-field/nm-field.d.ts +2 -0
  24. package/dist/ui/components/nm-form-field/nm-form-field.d.ts +18 -0
  25. package/dist/ui/components/nm-gallery/nm-gallery.d.ts +8 -0
  26. package/dist/ui/components/nm-input/nm-input.d.ts +5 -0
  27. package/dist/ui/components/nm-input-group/nm-input-group.d.ts +9 -0
  28. package/dist/ui/components/nm-label/nm-label.d.ts +2 -0
  29. package/dist/ui/components/nm-menubar/nm-menubar.d.ts +9 -0
  30. package/dist/ui/components/nm-native-select/nm-native-select.d.ts +5 -0
  31. package/dist/ui/components/nm-panel/nm-panel.d.ts +14 -0
  32. package/dist/ui/components/nm-prose/nm-prose.d.ts +6 -0
  33. package/dist/ui/components/nm-resize-handle/nm-resize-handle.d.ts +18 -0
  34. package/dist/ui/components/nm-select/nm-select.d.ts +22 -0
  35. package/dist/ui/components/nm-side-panel/nm-side-panel.d.ts +33 -0
  36. package/dist/ui/components/nm-slider/nm-slider.d.ts +5 -1
  37. package/dist/ui/components/nm-status-bar/nm-status-bar.d.ts +8 -0
  38. package/dist/ui/components/nm-table/nm-table.d.ts +38 -0
  39. package/dist/ui/components/nm-textarea/nm-textarea.d.ts +2 -0
  40. package/dist/ui/components/nm-toolbar/nm-toolbar.d.ts +21 -0
  41. package/dist/ui/config/nm-ai.d.ts +0 -3
  42. package/dist/ui/config/nm-appearance.d.ts +36 -1
  43. package/dist/ui/config/nm-button.d.ts +2 -1
  44. package/dist/ui/config/nm-composer.d.ts +1 -2
  45. package/dist/ui/config/nm-field.d.ts +2 -0
  46. package/dist/ui/config/nm-icons.d.ts +13 -1
  47. package/dist/ui/config/nm-radius.d.ts +4 -4
  48. package/dist/ui/config/nm-side-panel.d.ts +33 -0
  49. package/dist/ui/lib/appearance/appearanceState.d.ts +1 -0
  50. package/dist/ui/lib/appearance/deriveTokens.d.ts +2 -0
  51. package/package.json +12 -4
  52. package/registry/index.json +1634 -0
  53. package/registry/tokens.css +72 -0
  54. package/registry/ui/components/nm-ai/nm-ai.css +358 -0
  55. package/registry/ui/components/nm-ai/nm-ai.tsx +632 -0
  56. package/registry/ui/components/nm-app-panel/nm-app-panel.css +169 -0
  57. package/registry/ui/components/nm-ask/nm-ask.css +277 -0
  58. package/registry/ui/components/nm-avatar/nm-avatar.css +85 -0
  59. package/registry/ui/components/nm-avatar/nm-avatar.tsx +30 -0
  60. package/registry/ui/components/nm-bar-overflow/nm-bar-overflow.css +43 -0
  61. package/registry/ui/components/nm-bar-overflow/nm-bar-overflow.tsx +61 -0
  62. package/registry/ui/components/nm-bar-overflow/use-bar-overflow.ts +92 -0
  63. package/registry/ui/components/nm-brand-logo/nm-brand-logo.css +20 -0
  64. package/registry/ui/components/nm-brand-logo/nm-brand-logo.tsx +21 -0
  65. package/registry/ui/components/nm-breadcrumb/nm-breadcrumb.css +57 -0
  66. package/registry/ui/components/nm-breadcrumb/nm-breadcrumb.tsx +42 -0
  67. package/registry/ui/components/nm-button/nm-button.css +106 -0
  68. package/registry/ui/components/nm-button/nm-button.tsx +58 -0
  69. package/registry/ui/components/nm-button-group/nm-button-group.css +98 -0
  70. package/registry/ui/components/nm-button-group/nm-button-group.tsx +32 -0
  71. package/registry/ui/components/nm-calendar/nm-calendar.css +178 -0
  72. package/registry/ui/components/nm-calendar/nm-calendar.tsx +43 -0
  73. package/registry/ui/components/nm-card/nm-card.css +130 -0
  74. package/registry/ui/components/nm-card/nm-card.tsx +83 -0
  75. package/registry/ui/components/nm-chatbox/nm-chatbox-icon.tsx +39 -0
  76. package/registry/ui/components/nm-chatbox/nm-chatbox.css +682 -0
  77. package/registry/ui/components/nm-chatbox/nm-chatbox.tsx +101 -0
  78. package/registry/ui/components/nm-checkbox/nm-checkbox.css +61 -0
  79. package/registry/ui/components/nm-checkbox/nm-checkbox.tsx +14 -0
  80. package/registry/ui/components/nm-chip/nm-chip.css +112 -0
  81. package/registry/ui/components/nm-chip/nm-chip.tsx +86 -0
  82. package/registry/ui/components/nm-code-block/nm-code-block.css +240 -0
  83. package/registry/ui/components/nm-code-block/nm-code-block.tsx +196 -0
  84. package/registry/ui/components/nm-collection/nm-collection-bar.tsx +65 -0
  85. package/registry/ui/components/nm-collection/nm-collection-grid.tsx +69 -0
  86. package/registry/ui/components/nm-collection/nm-collection-query.tsx +99 -0
  87. package/registry/ui/components/nm-collection/nm-collection-row-menu.tsx +55 -0
  88. package/registry/ui/components/nm-collection/nm-collection-table.tsx +99 -0
  89. package/registry/ui/components/nm-collection/nm-collection-tile-actions.tsx +50 -0
  90. package/registry/ui/components/nm-collection/nm-collection-title.tsx +25 -0
  91. package/registry/ui/components/nm-collection/nm-collection.css +353 -0
  92. package/registry/ui/components/nm-collection/use-collection-scroll-reveal.ts +38 -0
  93. package/registry/ui/components/nm-color-picker/nm-color-picker.css +67 -0
  94. package/registry/ui/components/nm-color-picker/nm-color-picker.tsx +79 -0
  95. package/registry/ui/components/nm-combobox/nm-combobox.css +72 -0
  96. package/registry/ui/components/nm-combobox/nm-combobox.tsx +80 -0
  97. package/registry/ui/components/nm-command/nm-command.css +190 -0
  98. package/registry/ui/components/nm-command/nm-command.tsx +125 -0
  99. package/registry/ui/components/nm-composer/nm-composer.css +178 -0
  100. package/registry/ui/components/nm-composer/nm-composer.tsx +151 -0
  101. package/registry/ui/components/nm-context-menu/nm-context-menu.tsx +25 -0
  102. package/registry/ui/components/nm-date-picker/nm-date-picker.css +140 -0
  103. package/registry/ui/components/nm-date-picker/nm-date-picker.tsx +153 -0
  104. package/registry/ui/components/nm-dialog/nm-dialog.css +84 -0
  105. package/registry/ui/components/nm-dialog/nm-dialog.tsx +40 -0
  106. package/registry/ui/components/nm-dropdown/nm-dropdown-item.ts +68 -0
  107. package/registry/ui/components/nm-dropdown/nm-dropdown-options.tsx +242 -0
  108. package/registry/ui/components/nm-dropdown/nm-dropdown-panel.tsx +171 -0
  109. package/registry/ui/components/nm-dropdown/nm-dropdown-placement.ts +157 -0
  110. package/registry/ui/components/nm-dropdown/nm-dropdown.css +312 -0
  111. package/registry/ui/components/nm-dropdown/nm-dropdown.tsx +412 -0
  112. package/registry/ui/components/nm-dropdown-menu/nm-dropdown-menu.css +145 -0
  113. package/registry/ui/components/nm-dropdown-menu/nm-dropdown-menu.tsx +118 -0
  114. package/registry/ui/components/nm-field/nm-field.css +356 -0
  115. package/registry/ui/components/nm-field/nm-field.tsx +355 -0
  116. package/registry/ui/components/nm-form-field/nm-form-field.css +103 -0
  117. package/registry/ui/components/nm-form-field/nm-form-field.tsx +71 -0
  118. package/registry/ui/components/nm-gallery/nm-gallery.css +66 -0
  119. package/registry/ui/components/nm-gallery/nm-gallery.tsx +31 -0
  120. package/registry/ui/components/nm-hover-card/nm-hover-card.css +26 -0
  121. package/registry/ui/components/nm-hover-card/nm-hover-card.tsx +28 -0
  122. package/registry/ui/components/nm-icon/nm-icon.css +6 -0
  123. package/registry/ui/components/nm-icon/nm-icon.tsx +69 -0
  124. package/registry/ui/components/nm-icon/nm-lucide.tsx +180 -0
  125. package/registry/ui/components/nm-icon-border/nm-icon-border.tsx +56 -0
  126. package/registry/ui/components/nm-input/nm-input.css +75 -0
  127. package/registry/ui/components/nm-input/nm-input.tsx +7 -0
  128. package/registry/ui/components/nm-input-group/nm-input-group.css +66 -0
  129. package/registry/ui/components/nm-input-group/nm-input-group.tsx +20 -0
  130. package/registry/ui/components/nm-label/nm-label.css +11 -0
  131. package/registry/ui/components/nm-label/nm-label.tsx +6 -0
  132. package/registry/ui/components/nm-list-item/nm-list-item.css +177 -0
  133. package/registry/ui/components/nm-list-item/nm-list-item.tsx +112 -0
  134. package/registry/ui/components/nm-menubar/nm-menubar.css +32 -0
  135. package/registry/ui/components/nm-menubar/nm-menubar.tsx +19 -0
  136. package/registry/ui/components/nm-message/nm-message-actions.tsx +24 -0
  137. package/registry/ui/components/nm-message/nm-message-meta.tsx +49 -0
  138. package/registry/ui/components/nm-message/nm-message.css +686 -0
  139. package/registry/ui/components/nm-message/nm-text-shimmer.tsx +6 -0
  140. package/registry/ui/components/nm-message/nm-trace.tsx +219 -0
  141. package/registry/ui/components/nm-modal/nm-modal.css +763 -0
  142. package/registry/ui/components/nm-modal/nm-modal.tsx +497 -0
  143. package/registry/ui/components/nm-native-select/nm-native-select.css +47 -0
  144. package/registry/ui/components/nm-native-select/nm-native-select.tsx +23 -0
  145. package/registry/ui/components/nm-panel/nm-panel.css +73 -0
  146. package/registry/ui/components/nm-panel/nm-panel.tsx +35 -0
  147. package/registry/ui/components/nm-panel-bar/nm-panel-bar.css +20 -0
  148. package/registry/ui/components/nm-panel-bar/nm-panel-bar.tsx +48 -0
  149. package/registry/ui/components/nm-popover/nm-popover.css +43 -0
  150. package/registry/ui/components/nm-popover/nm-popover.tsx +40 -0
  151. package/registry/ui/components/nm-progress/nm-progress.css +21 -0
  152. package/registry/ui/components/nm-progress/nm-progress.tsx +14 -0
  153. package/registry/ui/components/nm-prose/nm-prose.css +81 -0
  154. package/registry/ui/components/nm-prose/nm-prose.tsx +10 -0
  155. package/registry/ui/components/nm-questionnaire/nm-questionnaire.css +182 -0
  156. package/registry/ui/components/nm-questionnaire/nm-questionnaire.tsx +221 -0
  157. package/registry/ui/components/nm-radio-group/nm-radio-group.css +48 -0
  158. package/registry/ui/components/nm-radio-group/nm-radio-group.tsx +15 -0
  159. package/registry/ui/components/nm-rail-icon/nm-rail-icon.css +6 -0
  160. package/registry/ui/components/nm-rail-icon/nm-rail-icon.tsx +28 -0
  161. package/registry/ui/components/nm-resize-handle/nm-resize-handle.css +45 -0
  162. package/registry/ui/components/nm-resize-handle/nm-resize-handle.tsx +102 -0
  163. package/registry/ui/components/nm-segmented/nm-segmented.css +104 -0
  164. package/registry/ui/components/nm-segmented/nm-segmented.tsx +77 -0
  165. package/registry/ui/components/nm-select/nm-select.css +78 -0
  166. package/registry/ui/components/nm-select/nm-select.tsx +64 -0
  167. package/registry/ui/components/nm-separator/nm-separator.css +14 -0
  168. package/registry/ui/components/nm-separator/nm-separator.tsx +6 -0
  169. package/registry/ui/components/nm-side-panel/nm-side-panel.css +93 -0
  170. package/registry/ui/components/nm-side-panel/nm-side-panel.tsx +86 -0
  171. package/registry/ui/components/nm-slider/nm-slider.css +53 -0
  172. package/registry/ui/components/nm-slider/nm-slider.tsx +40 -0
  173. package/registry/ui/components/nm-small-icon-button/nm-small-icon-button.tsx +66 -0
  174. package/registry/ui/components/nm-status-bar/nm-status-bar.css +50 -0
  175. package/registry/ui/components/nm-status-bar/nm-status-bar.tsx +28 -0
  176. package/registry/ui/components/nm-switch/nm-switch.css +45 -0
  177. package/registry/ui/components/nm-switch/nm-switch.tsx +10 -0
  178. package/registry/ui/components/nm-table/nm-table.css +155 -0
  179. package/registry/ui/components/nm-table/nm-table.tsx +95 -0
  180. package/registry/ui/components/nm-tabs/nm-tabs.css +28 -0
  181. package/registry/ui/components/nm-tabs/nm-tabs.tsx +32 -0
  182. package/registry/ui/components/nm-textarea/nm-textarea.css +41 -0
  183. package/registry/ui/components/nm-textarea/nm-textarea.tsx +6 -0
  184. package/registry/ui/components/nm-toggle/nm-toggle.css +55 -0
  185. package/registry/ui/components/nm-toggle/nm-toggle.tsx +16 -0
  186. package/registry/ui/components/nm-toolbar/nm-toolbar.css +42 -0
  187. package/registry/ui/components/nm-toolbar/nm-toolbar.tsx +54 -0
  188. package/registry/ui/components/nm-tooltip/nm-tooltip.css +37 -0
  189. package/registry/ui/components/nm-tooltip/nm-tooltip.tsx +35 -0
  190. package/registry/ui/components/nm-top-bar/nm-top-bar.css +29 -0
  191. package/registry/ui/components/nm-top-bar/nm-top-bar.tsx +34 -0
  192. package/registry/ui/components/nordman-icon-button/nordman-icon-button.css +119 -0
  193. package/registry/ui/components/nordman-icon-button/nordman-icon-button.tsx +185 -0
  194. package/registry/ui/components/nordman-login/nordman-login.css +195 -0
  195. package/registry/ui/components/nordman-login/nordman-login.tsx +136 -0
  196. package/registry/ui/components/nordman-login-field/nordman-login-field.tsx +55 -0
  197. package/registry/ui/components/nordman-login-forgot/nordman-login-forgot.tsx +56 -0
  198. package/registry/ui/components/nordman-login-google/nordman-login-google.tsx +28 -0
  199. package/registry/ui/components/nordman-login-hero/nordman-login-hero.tsx +38 -0
  200. package/registry/ui/components/nordman-login-register/nordman-login-register.tsx +70 -0
  201. package/registry/ui/components/nordman-login-reset/nordman-login-reset.tsx +71 -0
  202. package/registry/ui/components/nordman-login-reveal/nordman-login-reveal.tsx +41 -0
  203. package/registry/ui/components/nordman-login-sign-in/nordman-login-sign-in.tsx +69 -0
  204. package/registry/ui/components/nordman-toggle/nordman-toggle.css +28 -0
  205. package/registry/ui/components/nordman-toggle/nordman-toggle.tsx +35 -0
  206. package/registry/ui/config/nm-ai.ts +59 -0
  207. package/registry/ui/config/nm-appearance.ts +275 -0
  208. package/registry/ui/config/nm-bar-overflow.ts +10 -0
  209. package/registry/ui/config/nm-button.ts +38 -0
  210. package/registry/ui/config/nm-chatbox-icons.ts +67 -0
  211. package/registry/ui/config/nm-chatbox.ts +42 -0
  212. package/registry/ui/config/nm-collection.ts +101 -0
  213. package/registry/ui/config/nm-composer.ts +39 -0
  214. package/registry/ui/config/nm-dropdown.ts +62 -0
  215. package/registry/ui/config/nm-field.ts +43 -0
  216. package/registry/ui/config/nm-icon-button.ts +35 -0
  217. package/registry/ui/config/nm-icons.ts +417 -0
  218. package/registry/ui/config/nm-list-item.ts +14 -0
  219. package/registry/ui/config/nm-message.ts +106 -0
  220. package/registry/ui/config/nm-modal.ts +76 -0
  221. package/registry/ui/config/nm-panel-bar.ts +11 -0
  222. package/registry/ui/config/nm-questionnaire.ts +53 -0
  223. package/registry/ui/config/nm-radius.ts +56 -0
  224. package/registry/ui/config/nm-rail-icons.ts +27 -0
  225. package/registry/ui/config/nm-segmented.ts +19 -0
  226. package/registry/ui/config/nm-side-panel.ts +16 -0
  227. package/registry/ui/config/nm-site-palette.ts +30 -0
  228. package/registry/ui/config/nm-small-icon-button.ts +17 -0
  229. package/registry/ui/config/nm-tabs.ts +9 -0
  230. package/registry/ui/config/nm-top-bar.ts +4 -0
  231. package/registry/ui/config/nordman-chrome.ts +14 -0
  232. package/registry/ui/config/nordman-login.ts +28 -0
  233. package/registry/ui/content/nm-modal.ts +5 -0
  234. package/registry/ui/content/nordman-login.ts +85 -0
  235. package/registry/ui/kit-logger.ts +1 -0
  236. package/registry/ui/lib/appearance/appearanceState.ts +240 -0
  237. package/registry/ui/lib/appearance/applyAppearance.ts +36 -0
  238. package/registry/ui/lib/appearance/cloudOrbAppearance.ts +36 -0
  239. package/registry/ui/lib/appearance/color.ts +96 -0
  240. package/registry/ui/lib/appearance/deriveTokens.ts +218 -0
  241. package/registry/ui/lib/appearance/index.ts +6 -0
  242. package/registry/ui/lib/appearance/markTokens.ts +49 -0
  243. package/registry/ui/lib/appearance/readLayoutMetrics.ts +28 -0
  244. package/registry/ui/lib/bar-overflow.ts +41 -0
  245. package/registry/ui/lib/logger.ts +53 -0
  246. package/registry/ui/lib/menu-motion.ts +187 -0
  247. package/registry/ui/lib/nm-class.ts +4 -0
  248. package/registry/ui/lib/nordman-ui-locale.ts +43 -0
  249. package/registry/ui/styles/interface-theme.css +196 -0
  250. package/registry/ui/styles/light-appearance.css +72 -0
  251. package/registry/ui/styles/tokens.css +424 -0
  252. package/registry/ui/styles/utilities.css +12 -0
  253. package/dist/ui/components/nm-accordion/nm-accordion.d.ts +0 -6
@@ -0,0 +1,48 @@
1
+ import type { ReactNode } from 'react';
2
+ import {
3
+ NmSegmented,
4
+ type NmSegmentedItem,
5
+ } from '../nm-segmented/nm-segmented.tsx';
6
+ import type { NmSegmentedTone } from '../../config/nm-segmented.ts';
7
+ import { NM_PANEL_BAR_CLASSES, NM_PANEL_BAR_LAYOUT } from '../../config/nm-panel-bar.ts';
8
+ import { createLogger } from '../../kit-logger.ts';
9
+
10
+ const logger = createLogger('interface-nm-panel-bar');
11
+
12
+ export type NmPanelBarProps = {
13
+ label: string;
14
+ testId?: string;
15
+ items: readonly NmSegmentedItem[];
16
+ active: string;
17
+ toggleLabel: string;
18
+ toggleTestId?: string;
19
+ tone?: NmSegmentedTone;
20
+ layout?: (typeof NM_PANEL_BAR_LAYOUT)[keyof typeof NM_PANEL_BAR_LAYOUT];
21
+ onSelect: (id: string) => void;
22
+ trailing?: ReactNode;
23
+ };
24
+
25
+ export function NmPanelBar(props: NmPanelBarProps): ReactNode {
26
+ logger.debug('render', props.active, { items: props.items.length });
27
+ return (
28
+ <header
29
+ className={
30
+ props.layout === NM_PANEL_BAR_LAYOUT.icons
31
+ ? `${NM_PANEL_BAR_CLASSES.root} ${NM_PANEL_BAR_CLASSES.icons}`
32
+ : NM_PANEL_BAR_CLASSES.root
33
+ }
34
+ aria-label={props.label}
35
+ data-testid={props.testId}
36
+ >
37
+ <NmSegmented
38
+ label={props.toggleLabel}
39
+ active={props.active}
40
+ testId={props.toggleTestId}
41
+ tone={props.tone}
42
+ items={props.items}
43
+ onSelect={props.onSelect}
44
+ />
45
+ {props.trailing ? <div className={NM_PANEL_BAR_CLASSES.actions}>{props.trailing}</div> : null}
46
+ </header>
47
+ );
48
+ }
@@ -0,0 +1,43 @@
1
+ .nm-popover__positioner {
2
+ z-index: var(--nm-menu-z);
3
+ }
4
+
5
+ .nm-popover {
6
+ box-sizing: border-box;
7
+ display: flex;
8
+ flex-direction: column;
9
+ gap: var(--nm-space-12);
10
+ width: var(--nm-width-popup);
11
+ padding: var(--nm-panel-pad);
12
+ border: var(--nm-rule) solid var(--nm-line-strong);
13
+ border-radius: var(--nm-control-menu-radius);
14
+ background: var(--nm-panel);
15
+ color: var(--nm-fg);
16
+ font-family: var(--nm-font);
17
+ font-size: var(--nm-type-ui);
18
+ outline: none;
19
+ transition: opacity 0.12s;
20
+ }
21
+
22
+ .nm-popover[data-starting-style],
23
+ .nm-popover[data-ending-style] {
24
+ opacity: 0;
25
+ }
26
+
27
+ .nm-popover__header {
28
+ display: flex;
29
+ flex-direction: column;
30
+ gap: var(--nm-space-2);
31
+ }
32
+
33
+ .nm-popover__title {
34
+ margin: 0;
35
+ font-size: var(--nm-type-ui);
36
+ font-weight: var(--nm-weight-medium);
37
+ }
38
+
39
+ .nm-popover__description {
40
+ margin: 0;
41
+ color: var(--nm-fg-muted);
42
+ font-size: var(--nm-type-ui-sm);
43
+ }
@@ -0,0 +1,40 @@
1
+ import type { ComponentProps } from 'react';
2
+ import { Popover as PopoverPrimitive } from '@base-ui/react/popover';
3
+ import { nmClass } from '../../lib/nm-class.ts';
4
+
5
+ export function NmPopover(props: PopoverPrimitive.Root.Props) {
6
+ return <PopoverPrimitive.Root data-slot="popover" {...props} />;
7
+ }
8
+
9
+ export function NmPopoverTrigger(props: PopoverPrimitive.Trigger.Props) {
10
+ return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
11
+ }
12
+
13
+ export function NmPopoverContent({
14
+ className,
15
+ align = 'center',
16
+ alignOffset = 0,
17
+ side = 'bottom',
18
+ sideOffset = 8,
19
+ ...props
20
+ }: PopoverPrimitive.Popup.Props & Pick<PopoverPrimitive.Positioner.Props, 'align' | 'alignOffset' | 'side' | 'sideOffset'>) {
21
+ return (
22
+ <PopoverPrimitive.Portal>
23
+ <PopoverPrimitive.Positioner align={align} alignOffset={alignOffset} side={side} sideOffset={sideOffset} className="nm-popover__positioner">
24
+ <PopoverPrimitive.Popup data-slot="popover-content" className={nmClass('nm-popover', className as string)} {...props} />
25
+ </PopoverPrimitive.Positioner>
26
+ </PopoverPrimitive.Portal>
27
+ );
28
+ }
29
+
30
+ export function NmPopoverHeader({ className, ...props }: ComponentProps<'div'>) {
31
+ return <div data-slot="popover-header" className={nmClass('nm-popover__header', className)} {...props} />;
32
+ }
33
+
34
+ export function NmPopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
35
+ return <PopoverPrimitive.Title data-slot="popover-title" className={nmClass('nm-popover__title', className as string)} {...props} />;
36
+ }
37
+
38
+ export function NmPopoverDescription({ className, ...props }: PopoverPrimitive.Description.Props) {
39
+ return <PopoverPrimitive.Description data-slot="popover-description" className={nmClass('nm-popover__description', className as string)} {...props} />;
40
+ }
@@ -0,0 +1,21 @@
1
+ .nm-progress {
2
+ display: flex;
3
+ flex-wrap: wrap;
4
+ width: 100%;
5
+ }
6
+
7
+ .nm-progress__track {
8
+ position: relative;
9
+ flex: 1 0 100%;
10
+ height: var(--nm-space-4);
11
+ overflow: hidden;
12
+ border-radius: var(--toggle-r);
13
+ background: var(--nm-panel);
14
+ }
15
+
16
+ .nm-progress__indicator {
17
+ height: 100%;
18
+ border-radius: inherit;
19
+ background: var(--nm-fg);
20
+ transition: width 0.2s;
21
+ }
@@ -0,0 +1,14 @@
1
+ import { Progress as ProgressPrimitive } from '@base-ui/react/progress';
2
+ import { nmClass } from '../../lib/nm-class.ts';
3
+
4
+ /** Base UI Progress, in the shadcn composition, styled by NIF tokens. */
5
+ export function NmProgress({ className, children, value, ...props }: ProgressPrimitive.Root.Props) {
6
+ return (
7
+ <ProgressPrimitive.Root data-slot="progress" value={value} className={nmClass('nm-progress', className as string)} {...props}>
8
+ {children}
9
+ <ProgressPrimitive.Track data-slot="progress-track" className="nm-progress__track">
10
+ <ProgressPrimitive.Indicator data-slot="progress-indicator" className="nm-progress__indicator" />
11
+ </ProgressPrimitive.Track>
12
+ </ProgressPrimitive.Root>
13
+ );
14
+ }
@@ -0,0 +1,81 @@
1
+ .nm-prose {
2
+ box-sizing: border-box;
3
+ max-width: var(--nm-width-prose, 48rem);
4
+ margin: 0 auto;
5
+ padding: var(--nm-space-24);
6
+ color: var(--nm-fg);
7
+ font-family: var(--nm-font);
8
+ font-size: var(--nm-type-ui);
9
+ line-height: var(--nm-leading-relaxed);
10
+ }
11
+
12
+ .nm-prose > :first-child {
13
+ margin-top: 0;
14
+ }
15
+
16
+ .nm-prose :is(h1, h2, h3, h4) {
17
+ margin: var(--nm-space-24) 0 var(--nm-space-8);
18
+ font-weight: var(--nm-weight-semibold);
19
+ line-height: var(--nm-leading-tight);
20
+ }
21
+
22
+ .nm-prose h1 {
23
+ font-size: var(--nm-type-lede);
24
+ }
25
+
26
+ .nm-prose h2 {
27
+ font-size: calc(var(--nm-type-ui) * 1.25);
28
+ }
29
+
30
+ .nm-prose :is(h3, h4) {
31
+ font-size: var(--nm-type-ui);
32
+ }
33
+
34
+ .nm-prose :is(p, ul, ol, blockquote, pre, table) {
35
+ margin: 0 0 var(--nm-space-12);
36
+ }
37
+
38
+ .nm-prose :is(ul, ol) {
39
+ padding-inline-start: var(--nm-space-24);
40
+ }
41
+
42
+ .nm-prose li {
43
+ margin: var(--nm-space-4) 0;
44
+ }
45
+
46
+ .nm-prose blockquote {
47
+ padding-inline-start: var(--nm-space-12);
48
+ border-inline-start: var(--nm-space-2) solid var(--nm-line-strong);
49
+ color: var(--nm-fg-muted);
50
+ }
51
+
52
+ .nm-prose a {
53
+ color: var(--nm-fg);
54
+ text-decoration: underline;
55
+ text-underline-offset: var(--nm-space-2);
56
+ }
57
+
58
+ .nm-prose :not(pre) > code {
59
+ padding: var(--nm-space-2) var(--nm-space-6);
60
+ border-radius: var(--nm-radius-sm);
61
+ background: var(--nm-panel);
62
+ font-family: var(--nm-font-mono);
63
+ font-size: var(--nm-type-ui-sm);
64
+ }
65
+
66
+ .nm-prose hr {
67
+ margin: var(--nm-space-24) 0;
68
+ border: 0;
69
+ border-top: var(--nm-rule) solid var(--nm-divider);
70
+ }
71
+
72
+ .nm-prose table {
73
+ width: 100%;
74
+ border-collapse: collapse;
75
+ }
76
+
77
+ .nm-prose :is(th, td) {
78
+ padding: var(--nm-space-8) var(--nm-space-12);
79
+ border-bottom: var(--nm-rule) solid var(--nm-line);
80
+ text-align: start;
81
+ }
@@ -0,0 +1,10 @@
1
+ import type { ComponentProps } from 'react';
2
+ import { nmClass } from '../../lib/nm-class.ts';
3
+
4
+ /**
5
+ * Rendered text: markdown, docs and long answers. Headings, paragraphs, lists, quotes, code, rules, links and
6
+ * tables take their type, space and colour from NIF tokens, so the plain elements inside it need no classes.
7
+ */
8
+ export function NmProse({ className, ...props }: ComponentProps<'article'>) {
9
+ return <article data-slot="prose" className={nmClass('nm-prose', className)} {...props} />;
10
+ }
@@ -0,0 +1,182 @@
1
+ .nm-questionnaire {
2
+ box-sizing: border-box;
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: var(--nm-space-16);
6
+ width: 100%;
7
+ max-width: var(--nm-width-form);
8
+ color: var(--nm-fg);
9
+ font-family: var(--nm-font);
10
+ font-size: var(--nm-type-ui);
11
+ }
12
+
13
+ .nm-questionnaire--card {
14
+ padding: var(--nm-space-24);
15
+ border: var(--nm-rule) solid var(--nm-line-strong);
16
+ border-radius: var(--nm-radius-lg);
17
+ background: var(--nm-raised);
18
+ }
19
+
20
+ .nm-questionnaire__progress {
21
+ display: flex;
22
+ flex-direction: column;
23
+ gap: var(--nm-space-8);
24
+ }
25
+
26
+ .nm-questionnaire__progress-label {
27
+ color: var(--nm-fg-subtle);
28
+ font-size: var(--nm-type-ui-sm);
29
+ }
30
+
31
+ .nm-questionnaire__item {
32
+ display: flex;
33
+ flex-direction: column;
34
+ gap: var(--nm-space-12);
35
+ min-width: 0;
36
+ margin: 0;
37
+ padding: 0;
38
+ border: 0;
39
+ }
40
+
41
+ .nm-questionnaire__item[hidden] {
42
+ display: none;
43
+ }
44
+
45
+ .nm-questionnaire__title {
46
+ padding: 0;
47
+ font-size: var(--nm-type-body);
48
+ font-weight: var(--nm-weight-medium);
49
+ line-height: var(--nm-leading-snug);
50
+ }
51
+
52
+ .nm-questionnaire__description {
53
+ margin: 0;
54
+ color: var(--nm-fg-muted);
55
+ line-height: var(--nm-leading-normal);
56
+ }
57
+
58
+ .nm-questionnaire__choices {
59
+ display: flex;
60
+ flex-direction: column;
61
+ gap: var(--nm-space-8);
62
+ }
63
+
64
+ .nm-questionnaire__choice {
65
+ position: relative;
66
+ display: flex;
67
+ align-items: flex-start;
68
+ gap: var(--nm-space-12);
69
+ box-sizing: border-box;
70
+ padding: var(--nm-space-12);
71
+ border: var(--nm-rule) solid var(--nm-line-strong);
72
+ border-radius: var(--nm-control-r);
73
+ background: transparent;
74
+ cursor: pointer;
75
+ }
76
+
77
+ .nm-questionnaire__choice:hover {
78
+ background: var(--nm-inset);
79
+ }
80
+
81
+ .nm-questionnaire__choice:has(input:checked) {
82
+ border-color: var(--nm-fg);
83
+ }
84
+
85
+ .nm-questionnaire__choice:has(input:focus-visible) {
86
+ outline: var(--nm-rule) solid var(--focus);
87
+ outline-offset: var(--nm-space-2);
88
+ }
89
+
90
+ .nm-questionnaire__input {
91
+ position: absolute;
92
+ opacity: 0;
93
+ pointer-events: none;
94
+ }
95
+
96
+ .nm-questionnaire__mark {
97
+ display: inline-flex;
98
+ flex-shrink: 0;
99
+ align-items: center;
100
+ justify-content: center;
101
+ box-sizing: border-box;
102
+ width: var(--nm-icon-button);
103
+ height: var(--nm-icon-button);
104
+ margin-top: var(--nm-space-2);
105
+ border: var(--nm-rule) solid var(--nm-stroke);
106
+ border-radius: 50%;
107
+ color: var(--nm-canvas);
108
+ }
109
+
110
+ .nm-questionnaire__choice[data-multiple='true'] .nm-questionnaire__mark {
111
+ border-radius: var(--nm-radius-sm);
112
+ }
113
+
114
+ .nm-questionnaire__choice:has(input:checked) .nm-questionnaire__mark {
115
+ border-color: var(--nm-fg);
116
+ background: var(--nm-fg);
117
+ }
118
+
119
+ .nm-questionnaire__choice-body {
120
+ display: flex;
121
+ flex: 1;
122
+ flex-direction: column;
123
+ gap: var(--nm-space-2);
124
+ min-width: 0;
125
+ }
126
+
127
+ .nm-questionnaire__choice-label {
128
+ font-weight: var(--nm-weight-medium);
129
+ }
130
+
131
+ .nm-questionnaire__choice-description {
132
+ color: var(--nm-fg-muted);
133
+ font-size: var(--nm-type-ui-sm);
134
+ }
135
+
136
+ .nm-questionnaire__key {
137
+ flex-shrink: 0;
138
+ min-width: var(--nm-icon-button);
139
+ padding: 0 var(--nm-space-4);
140
+ border-radius: var(--nm-radius-sm);
141
+ background: var(--nm-panel);
142
+ color: var(--nm-fg-subtle);
143
+ font-family: var(--nm-font-mono);
144
+ font-size: var(--nm-type-ui-sm);
145
+ text-align: center;
146
+ text-transform: uppercase;
147
+ }
148
+
149
+ .nm-questionnaire__error {
150
+ margin: 0;
151
+ color: var(--nm-destructive);
152
+ font-size: var(--nm-type-ui-sm);
153
+ }
154
+
155
+ .nm-questionnaire__actions {
156
+ display: flex;
157
+ align-items: center;
158
+ gap: var(--nm-space-8);
159
+ }
160
+
161
+ .nm-questionnaire__actions [data-push] {
162
+ margin-left: auto;
163
+ }
164
+
165
+ .nm-questionnaire__summary {
166
+ display: flex;
167
+ flex-direction: column;
168
+ gap: var(--nm-space-12);
169
+ margin: 0;
170
+ padding: 0;
171
+ list-style: none;
172
+ }
173
+
174
+ .nm-questionnaire__summary li {
175
+ display: flex;
176
+ flex-direction: column;
177
+ gap: var(--nm-space-2);
178
+ }
179
+
180
+ .nm-questionnaire__summary li > span:last-child {
181
+ color: var(--nm-fg-muted);
182
+ }
@@ -0,0 +1,221 @@
1
+ import { useEffect, useRef, useState, type KeyboardEvent, type ReactNode } from 'react';
2
+ import {
3
+ NM_QUESTIONNAIRE_CLASSES as C,
4
+ NM_QUESTIONNAIRE_COPY as COPY,
5
+ type NmQuestion,
6
+ type NmQuestionAnswer,
7
+ type NmQuestionnaireAnswers,
8
+ type NmQuestionnaireShortcuts,
9
+ type NmQuestionnaireVariant,
10
+ } from '../../config/nm-questionnaire.ts';
11
+ import { NmButton } from '../nm-button/nm-button.tsx';
12
+ import { NmField } from '../nm-field/nm-field.tsx';
13
+ import { NmIcon } from '../nm-icon/nm-icon.tsx';
14
+ import { NmProgress } from '../nm-progress/nm-progress.tsx';
15
+
16
+ export type NmQuestionnaireProps = {
17
+ questions: readonly NmQuestion[];
18
+ /** Answers so far. Pass them with `onChange` to control the form, or to resume one. */
19
+ answers?: NmQuestionnaireAnswers;
20
+ onChange?: (answers: NmQuestionnaireAnswers) => void;
21
+ onSubmit: (answers: NmQuestionnaireAnswers) => void;
22
+ /** The step to show first. */
23
+ defaultStep?: number;
24
+ shortcuts?: NmQuestionnaireShortcuts;
25
+ variant?: NmQuestionnaireVariant;
26
+ submitLabel?: string;
27
+ /** After submit the form shows what was answered. */
28
+ submitted?: boolean;
29
+ };
30
+
31
+ const EMPTY: NmQuestionAnswer = { values: [], text: '' };
32
+
33
+ function answered(answer: NmQuestionAnswer | undefined): boolean {
34
+ return answer != null && (answer.values.length > 0 || answer.text.trim() !== '');
35
+ }
36
+
37
+ function shortcutKey(mode: NmQuestionnaireShortcuts, index: number): string | null {
38
+ if (mode === 'letters') {
39
+ return String.fromCharCode(97 + index);
40
+ }
41
+ if (mode === 'numbers') {
42
+ return String(index + 1);
43
+ }
44
+ return null;
45
+ }
46
+
47
+ export function NmQuestionnaire(props: NmQuestionnaireProps): ReactNode {
48
+ const { questions } = props;
49
+ const [inner, setInner] = useState<NmQuestionnaireAnswers>({});
50
+ const answers = props.answers ?? inner;
51
+ const [step, setStep] = useState(Math.min(props.defaultStep ?? 0, questions.length - 1));
52
+ const [error, setError] = useState<string | null>(null);
53
+ const rootRef = useRef<HTMLFormElement>(null);
54
+ const shortcuts = props.shortcuts ?? 'none';
55
+ const last = step === questions.length - 1;
56
+ const question = questions[step];
57
+
58
+ useEffect(() => {
59
+ rootRef.current?.querySelector<HTMLElement>(`[data-step="${step}"] input:checked, [data-step="${step}"] input`)?.focus({ preventScroll: true });
60
+ }, [step]);
61
+
62
+ function update(name: string, next: NmQuestionAnswer): void {
63
+ const merged = { ...answers, [name]: next };
64
+ setError(null);
65
+ setInner(merged);
66
+ props.onChange?.(merged);
67
+ }
68
+
69
+ function pick(q: NmQuestion, value: string): void {
70
+ const current = answers[q.name] ?? EMPTY;
71
+ if (q.multiple === true) {
72
+ const has = current.values.includes(value);
73
+ update(q.name, { ...current, values: has ? current.values.filter((v) => v !== value) : [...current.values, value] });
74
+ return;
75
+ }
76
+ update(q.name, { values: [value], text: '' });
77
+ }
78
+
79
+ function type(q: NmQuestion, text: string): void {
80
+ const current = answers[q.name] ?? EMPTY;
81
+ update(q.name, { values: q.multiple === true ? current.values : [], text });
82
+ }
83
+
84
+ function valid(q: NmQuestion): boolean {
85
+ return q.required !== true || answered(answers[q.name]);
86
+ }
87
+
88
+ function go(to: number): void {
89
+ setError(null);
90
+ setStep(to);
91
+ }
92
+
93
+ function next(): void {
94
+ if (!valid(question)) {
95
+ setError(COPY.required);
96
+ return;
97
+ }
98
+ go(step + 1);
99
+ }
100
+
101
+ function submit(): void {
102
+ const firstInvalid = questions.findIndex((q) => !valid(q));
103
+ if (firstInvalid !== -1) {
104
+ setStep(firstInvalid);
105
+ setError(COPY.required);
106
+ return;
107
+ }
108
+ props.onSubmit(answers);
109
+ }
110
+
111
+ function onKeyDown(event: KeyboardEvent<HTMLFormElement>): void {
112
+ const target = event.target as HTMLElement;
113
+ if (shortcuts === 'none' || target.tagName === 'INPUT' && (target as HTMLInputElement).type === 'text') {
114
+ return;
115
+ }
116
+ const index = question.choices.findIndex((_, i) => shortcutKey(shortcuts, i) === event.key.toLowerCase());
117
+ if (index !== -1 && !event.metaKey && !event.ctrlKey && !event.altKey) {
118
+ event.preventDefault();
119
+ pick(question, question.choices[index].value);
120
+ }
121
+ }
122
+
123
+ if (props.submitted === true) {
124
+ return (
125
+ <div className={`${C.root}${props.variant === 'plain' ? '' : ` ${C.card}`}`}>
126
+ <ul className={C.summary}>
127
+ {questions.map((q) => {
128
+ const a = answers[q.name];
129
+ const labels = (a?.values ?? []).map((v) => q.choices.find((c) => c.value === v)?.label ?? v);
130
+ if (a?.text) {
131
+ labels.push(a.text);
132
+ }
133
+ return (
134
+ <li key={q.name}>
135
+ <span>{q.title}</span>
136
+ <span>{labels.length > 0 ? labels.join(', ') : COPY.skipped}</span>
137
+ </li>
138
+ );
139
+ })}
140
+ </ul>
141
+ </div>
142
+ );
143
+ }
144
+
145
+ return (
146
+ <form
147
+ ref={rootRef}
148
+ className={`${C.root}${props.variant === 'plain' ? '' : ` ${C.card}`}`}
149
+ onKeyDown={onKeyDown}
150
+ onSubmit={(event) => {
151
+ event.preventDefault();
152
+ if (last) {
153
+ submit();
154
+ } else {
155
+ next();
156
+ }
157
+ }}
158
+ >
159
+ <div className={C.progress}>
160
+ <span className={C.progressLabel}>{COPY.progress(step, questions.length)}</span>
161
+ <NmProgress value={step + 1} max={questions.length} aria-label={COPY.progress(step, questions.length)} />
162
+ </div>
163
+ {questions.map((q, index) => {
164
+ const answer = answers[q.name] ?? EMPTY;
165
+ const active = index === step;
166
+ const describedBy = `${q.name}-description`;
167
+ return (
168
+ <fieldset key={q.name} className={C.item} hidden={!active} data-step={index} aria-describedby={describedBy} aria-invalid={active && error != null ? true : undefined}>
169
+ <legend className={C.title}>{q.title}</legend>
170
+ {q.description ? <p id={describedBy} className={C.description}>{q.description}</p> : null}
171
+ <div className={C.choices}>
172
+ {q.choices.map((choice, i) => {
173
+ const key = shortcutKey(shortcuts, i);
174
+ const checked = answer.values.includes(choice.value);
175
+ return (
176
+ <label key={choice.value} className={C.choice} data-multiple={q.multiple === true ? 'true' : undefined}>
177
+ <input
178
+ className={C.input}
179
+ type={q.multiple === true ? 'checkbox' : 'radio'}
180
+ name={q.name}
181
+ value={choice.value}
182
+ checked={checked}
183
+ onChange={() => pick(q, choice.value)}
184
+ />
185
+ <span className={C.mark} aria-hidden="true">{checked ? <NmIcon id="check" /> : null}</span>
186
+ <span className={C.choiceBody}>
187
+ <span className={C.choiceLabel}>{choice.label}</span>
188
+ {choice.description ? <span className={C.choiceDescription}>{choice.description}</span> : null}
189
+ </span>
190
+ {key ? <kbd className={C.key} aria-hidden="true">{key}</kbd> : null}
191
+ </label>
192
+ );
193
+ })}
194
+ {q.input ? (
195
+ <NmField
196
+ label={q.input.label}
197
+ caption={q.input.label}
198
+ layout="stack"
199
+ placeholder={q.input.placeholder}
200
+ value={answer.text}
201
+ onChange={(text) => type(q, text)}
202
+ />
203
+ ) : null}
204
+ </div>
205
+ {active && error != null ? <p className={C.error} role="alert">{error}</p> : null}
206
+ </fieldset>
207
+ );
208
+ })}
209
+ <div className={C.actions}>
210
+ {step > 0 ? <NmButton label={COPY.previous} tone="outline" size="compact" onClick={() => go(step - 1)} /> : null}
211
+ <span data-push="" />
212
+ {question.required !== true && !last ? <NmButton label={COPY.skip} tone="outline" size="compact" onClick={() => go(step + 1)} /> : null}
213
+ {last ? (
214
+ <NmButton label={props.submitLabel ?? COPY.submit} tone="filled" size="compact" type="submit" />
215
+ ) : (
216
+ <NmButton label={COPY.next} tone="filled" size="compact" type="submit" />
217
+ )}
218
+ </div>
219
+ </form>
220
+ );
221
+ }