@nordmanai/nif 0.1.0 → 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 (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 +110 -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 +87 -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,196 @@
1
+ import { createContext, useContext, useEffect, useState, type ComponentProps, type ReactNode } from 'react';
2
+ import { nmClass } from '../../lib/nm-class.ts';
3
+ import { NmButton } from '../nm-button/nm-button.tsx';
4
+ import { NordmanIconButton } from '../nordman-icon-button/nordman-icon-button.tsx';
5
+
6
+ export const NM_CODE_BLOCK_VARIANTS = ['default', 'destructive'] as const;
7
+ export const NM_CODE_BLOCK_SIZES = ['default', 'sm'] as const;
8
+ export type NmCodeBlockVariant = (typeof NM_CODE_BLOCK_VARIANTS)[number];
9
+ export type NmCodeBlockSize = (typeof NM_CODE_BLOCK_SIZES)[number];
10
+
11
+ const CollapseContext = createContext<(collapsed: boolean) => void>(() => {});
12
+
13
+ /**
14
+ * Displays code, terminal output and diffs with an optional header, line numbers and a copy button.
15
+ * The shadcn Code Block composition, styled by NIF tokens. Colours for syntax come from the --nm-syntax tokens.
16
+ */
17
+ export function NmCodeBlock({
18
+ className,
19
+ variant = 'default',
20
+ size = 'default',
21
+ ...props
22
+ }: ComponentProps<'div'> & { variant?: NmCodeBlockVariant; size?: NmCodeBlockSize }) {
23
+ const [collapsed, setCollapsed] = useState(false);
24
+ return (
25
+ <CollapseContext.Provider value={setCollapsed}>
26
+ <div
27
+ data-slot="code-block"
28
+ data-variant={variant}
29
+ data-size={size}
30
+ data-collapsed={collapsed ? '' : undefined}
31
+ className={nmClass('nm-code-block', className)}
32
+ {...props}
33
+ />
34
+ </CollapseContext.Provider>
35
+ );
36
+ }
37
+
38
+ export function NmCodeBlockHeader({ className, ...props }: ComponentProps<'div'>) {
39
+ return <div data-slot="code-block-header" className={nmClass('nm-code-block__header', className)} {...props} />;
40
+ }
41
+
42
+ export function NmCodeBlockTitle({ className, ...props }: ComponentProps<'div'>) {
43
+ return <div data-slot="code-block-title" className={nmClass('nm-code-block__title', className)} {...props} />;
44
+ }
45
+
46
+ /** Buttons for the header. Placed straight in the block, they float over its top right corner. */
47
+ export function NmCodeBlockActions({ className, ...props }: ComponentProps<'div'>) {
48
+ return <div data-slot="code-block-actions" className={nmClass('nm-code-block__actions', className)} {...props} />;
49
+ }
50
+
51
+ /** An icon button that copies `value`. It shows a tick for a moment afterwards. */
52
+ export function NmCodeBlockCopyButton({ value, label = 'Copy code', copiedLabel = 'Copied' }: { value: string; label?: string; copiedLabel?: string }) {
53
+ const [copied, setCopied] = useState(false);
54
+ return (
55
+ <NordmanIconButton
56
+ label={copied ? copiedLabel : label}
57
+ glyph={copied ? 'check' : 'copy'}
58
+ tone="quiet"
59
+ onClick={() => {
60
+ void navigator.clipboard?.writeText(value);
61
+ setCopied(true);
62
+ window.setTimeout(() => setCopied(false), 1500);
63
+ }}
64
+ />
65
+ );
66
+ }
67
+
68
+ /** One line. `variant` marks it as highlighted, added or removed. */
69
+ export function NmCodeBlockLine({
70
+ className,
71
+ variant = 'default',
72
+ children,
73
+ html,
74
+ language,
75
+ ...props
76
+ }: Omit<ComponentProps<'span'>, 'children'> & { variant?: 'default' | 'highlighted' | 'added' | 'removed'; children?: ReactNode; html?: string; language?: NmCodeLanguage }) {
77
+ const classes = nmClass('nm-code-block__line', className);
78
+ if (html != null) {
79
+ return <span data-slot="code-block-line" data-variant={variant} className={classes} dangerouslySetInnerHTML={{ __html: html === '' ? ' ' : html }} {...props} />;
80
+ }
81
+ return (
82
+ <span data-slot="code-block-line" data-variant={variant} className={classes} {...props}>
83
+ {children === '' || children == null ? ' ' : typeof children === 'string' && language ? tokenize(children, language) : children}
84
+ </span>
85
+ );
86
+ }
87
+
88
+ /** Splits highlighted markup from shiki into one string per line. */
89
+ export function nmHighlightedLines(html: string): string[] {
90
+ const inner = html.match(/<code[^>]*>([\s\S]*)<\/code>/)?.[1] ?? html;
91
+ return inner.split('\n').map((line) => line.replace(/^<span class="line">([\s\S]*)<\/span>$/, '$1'));
92
+ }
93
+
94
+ export const NM_CODE_LANGUAGES = ['tsx', 'ts', 'js', 'json', 'css', 'bash'] as const;
95
+ export type NmCodeLanguage = (typeof NM_CODE_LANGUAGES)[number];
96
+
97
+ const KEYWORDS = /^(?:import|export|from|default|const|let|var|function|return|if|else|for|while|new|await|async|type|interface|extends|as|of|in|class|true|false|null|undefined|typeof|void|throw|try|catch|npm|npx|install|run|cd)$/;
98
+
99
+ /** Small tokenizer for the languages the kit shows. Per line, so it never needs a parser. */
100
+ function tokenize(line: string, language: NmCodeLanguage): ReactNode[] {
101
+ const comment = language === 'bash' ? /#.*$/ : language === 'css' ? /\/\*.*?(?:\*\/|$)/ : /\/\/.*$|\/\*.*?(?:\*\/|$)/;
102
+ const re = new RegExp(
103
+ `(${comment.source})|('(?:[^'\\\\]|\\\\.)*'?|"(?:[^"\\\\]|\\\\.)*"?|\`(?:[^\`\\\\]|\\\\.)*\`?)|(</?[A-Za-z][\\w.]*)|(\\b\\d[\\d._]*\\b)|([A-Za-z_$][\\w$-]*)(?=\\()|([A-Za-z_$][\\w$-]*)`,
104
+ 'g',
105
+ );
106
+ const out: ReactNode[] = [];
107
+ let last = 0;
108
+ let key = 0;
109
+ const push = (text: string, token?: string) =>
110
+ out.push(token ? <span key={key++} className="nm-code-block__token" data-token={token}>{text}</span> : text);
111
+ for (const m of line.matchAll(re)) {
112
+ if (m.index > last) push(line.slice(last, m.index));
113
+ const [text, cmt, str, tag, num, fn, word] = m;
114
+ if (cmt) push(text, 'comment');
115
+ else if (str) push(text, 'string');
116
+ else if (tag) {
117
+ push(tag.startsWith('</') ? '</' : '<');
118
+ push(tag.replace(/^<\/?/, ''), /^[A-Z]/.test(tag.replace(/^<\/?/, '')) ? 'constant' : 'keyword');
119
+ } else if (num) push(text, 'constant');
120
+ else if (fn) push(text, 'function');
121
+ else if (word && KEYWORDS.test(word)) push(text, 'keyword');
122
+ else push(text);
123
+ last = m.index + text.length;
124
+ }
125
+ if (last < line.length) push(line.slice(last));
126
+ return out;
127
+ }
128
+
129
+ export type NmCodeBlockContentProps = Omit<ComponentProps<'pre'>, 'children'> & {
130
+ /** A string, or a list of NmCodeBlockLine. */
131
+ children?: ReactNode;
132
+ /** Code already highlighted, as shiki writes it. Used in place of children. */
133
+ html?: string;
134
+ /** Colour the code in this language. Use `html` instead for a highlighter of your own. */
135
+ language?: NmCodeLanguage;
136
+ wrap?: boolean;
137
+ lineNumbers?: boolean;
138
+ startLine?: number;
139
+ /** Show only this many lines, dimmed, with a button to show the rest. */
140
+ collapsedLines?: number;
141
+ labels?: { show: string; hide: string };
142
+ };
143
+
144
+ /** The pre that scrolls or wraps. */
145
+ export function NmCodeBlockContent({
146
+ className,
147
+ children,
148
+ html,
149
+ language,
150
+ wrap = false,
151
+ lineNumbers = false,
152
+ startLine = 1,
153
+ collapsedLines,
154
+ labels = { show: 'View code', hide: 'Hide code' },
155
+ style,
156
+ ...props
157
+ }: NmCodeBlockContentProps) {
158
+ const setCollapsed = useContext(CollapseContext);
159
+ const [open, setOpen] = useState(false);
160
+ const lines: ReactNode[] =
161
+ html != null
162
+ ? nmHighlightedLines(html).map((line, i) => <NmCodeBlockLine key={i} html={line} />)
163
+ : typeof children === 'string'
164
+ ? children.replace(/\n$/, '').split('\n').map((line, i) => <NmCodeBlockLine key={i}>{language ? tokenize(line, language) : line}</NmCodeBlockLine>)
165
+ : [children];
166
+ const collapsible = collapsedLines != null && lines.length > collapsedLines;
167
+ const collapsed = collapsible && !open;
168
+ useEffect(() => setCollapsed(collapsed), [collapsed, setCollapsed]);
169
+ return (
170
+ <>
171
+ <pre
172
+ data-slot="code-block-content"
173
+ data-wrap={wrap || undefined}
174
+ data-numbered={lineNumbers || undefined}
175
+ data-collapsed={collapsed || undefined}
176
+ dir="ltr"
177
+ tabIndex={0}
178
+ style={{ counterReset: `line ${startLine - 1}`, ...(collapsedLines != null ? { ['--nm-code-collapsed' as string]: collapsedLines } : null), ...style }}
179
+ className={nmClass('nm-code-block__content', className)}
180
+ {...props}
181
+ >
182
+ <code data-slot="code-block-code" className="nm-code-block__code">{lines}</code>
183
+ </pre>
184
+ {collapsible ? (
185
+ <span className="nm-code-block__toggle">
186
+ <NmButton label={open ? labels.hide : labels.show} tone="panel" size="compact" onClick={() => setOpen(!open)} />
187
+ </span>
188
+ ) : null}
189
+ </>
190
+ );
191
+ }
192
+
193
+ /** A row under the code, for a note or a button. */
194
+ export function NmCodeBlockFooter({ className, ...props }: ComponentProps<'div'>) {
195
+ return <div data-slot="code-block-footer" className={nmClass('nm-code-block__footer', className)} {...props} />;
196
+ }
@@ -0,0 +1,65 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { NmIconId } from '../../config/nm-icons.ts';
3
+ import {
4
+ NM_COLLECTION_CLASSES,
5
+ NM_COLLECTION_ICON_SRC,
6
+ NM_COLLECTION_VIEWS,
7
+ type NmCollectionView,
8
+ } from '../../config/nm-collection.ts';
9
+ import { NmIconBorder } from '../nm-icon-border/nm-icon-border.tsx';
10
+ import { NmCollectionTitle } from './nm-collection-title.tsx';
11
+ import { createLogger } from '../../kit-logger.ts';
12
+
13
+ const logger = createLogger('interface-nm-collection-bar');
14
+
15
+ export type NmCollectionBarProps = {
16
+ title: string;
17
+ titleSrc: string;
18
+ titleIcon?: NmIconId;
19
+ filterIcon?: NmIconId;
20
+ view: NmCollectionView;
21
+ gridLabel: string;
22
+ tableLabel: string;
23
+ switchLabel: string;
24
+ showSwitch?: boolean;
25
+ onView: (view: NmCollectionView) => void;
26
+ };
27
+
28
+ function nextCollectionView(view: NmCollectionView): NmCollectionView {
29
+ return view === NM_COLLECTION_VIEWS.grid ? NM_COLLECTION_VIEWS.table : NM_COLLECTION_VIEWS.grid;
30
+ }
31
+
32
+ export function NmCollectionBar(props: NmCollectionBarProps): ReactNode {
33
+ const gridOn = props.view === NM_COLLECTION_VIEWS.grid;
34
+ const switchLabel = gridOn ? props.gridLabel : props.tableLabel;
35
+ const showSwitch = props.showSwitch !== false;
36
+ logger.debug('render', props.view, { title: props.title });
37
+ return (
38
+ <div
39
+ className={NM_COLLECTION_CLASSES.cluster}
40
+ role={showSwitch ? 'group' : undefined}
41
+ aria-label={showSwitch ? switchLabel : undefined}
42
+ data-testid="collection-bar"
43
+ >
44
+ <NmCollectionTitle
45
+ title={props.title}
46
+ titleSrc={props.titleSrc}
47
+ titleIcon={props.titleIcon}
48
+ />
49
+ {showSwitch ? (
50
+ <NmIconBorder
51
+ label={switchLabel}
52
+ icon={props.filterIcon ?? (gridOn ? undefined : 'table-properties')}
53
+ imageSrc={props.filterIcon || !gridOn ? undefined : NM_COLLECTION_ICON_SRC.grid}
54
+ pressed={gridOn}
55
+ testId="collection-view-switch"
56
+ onClick={() => {
57
+ const next = nextCollectionView(props.view);
58
+ logger.debug('toggle', next);
59
+ props.onView(next);
60
+ }}
61
+ />
62
+ ) : null}
63
+ </div>
64
+ );
65
+ }
@@ -0,0 +1,69 @@
1
+ import type { ReactNode } from 'react';
2
+ import {
3
+ NM_COLLECTION_CLASSES,
4
+ NM_COLLECTION_MEDIA_KINDS,
5
+ type NmCollectionItem,
6
+ type NmCollectionItemActions,
7
+ } from '../../config/nm-collection.ts';
8
+ import { NmCollectionTileActions } from './nm-collection-tile-actions.tsx';
9
+ import { createLogger } from '../../kit-logger.ts';
10
+
11
+ const logger = createLogger('interface-nm-collection-grid');
12
+
13
+ export type NmCollectionGridProps = {
14
+ items: readonly NmCollectionItem[];
15
+ label: string;
16
+ actions?: NmCollectionItemActions;
17
+ onSelect: (id: string) => void;
18
+ };
19
+
20
+ function CollectionTileMedia(item: NmCollectionItem): ReactNode {
21
+ if (item.media?.kind === NM_COLLECTION_MEDIA_KINDS.image) {
22
+ return <img className={NM_COLLECTION_CLASSES.media} src={item.media.src} alt="" />;
23
+ }
24
+ if (item.media?.kind === NM_COLLECTION_MEDIA_KINDS.video) {
25
+ return (
26
+ <video
27
+ className={NM_COLLECTION_CLASSES.media}
28
+ src={item.media.src}
29
+ muted
30
+ playsInline
31
+ preload="metadata"
32
+ />
33
+ );
34
+ }
35
+ return <span className={NM_COLLECTION_CLASSES.name}>{item.title}</span>;
36
+ }
37
+
38
+ export function NmCollectionGrid(props: NmCollectionGridProps): ReactNode {
39
+ logger.debug('render', 'grid', { count: props.items.length });
40
+ if (props.items.length === 0) {
41
+ return null;
42
+ }
43
+ return (
44
+ <ul
45
+ className={NM_COLLECTION_CLASSES.grid}
46
+ aria-label={props.label}
47
+ data-testid="collection-grid"
48
+ >
49
+ {props.items.map((item) => (
50
+ <li key={item.id} className={NM_COLLECTION_CLASSES.tile} data-media={item.media?.kind}>
51
+ <button
52
+ className={NM_COLLECTION_CLASSES.hit}
53
+ type="button"
54
+ aria-label={item.title}
55
+ onClick={() => {
56
+ logger.debug('select', item.id);
57
+ props.onSelect(item.id);
58
+ }}
59
+ >
60
+ {CollectionTileMedia(item)}
61
+ </button>
62
+ {props.actions ? (
63
+ <NmCollectionTileActions itemId={item.id} title={item.title} actions={props.actions} />
64
+ ) : null}
65
+ </li>
66
+ ))}
67
+ </ul>
68
+ );
69
+ }
@@ -0,0 +1,99 @@
1
+ import type { ReactNode } from 'react';
2
+ import { NM_COLLECTION_CLASSES, NM_COLLECTION_ICON_SRC } from '../../config/nm-collection.ts';
3
+ import { createLogger } from '../../kit-logger.ts';
4
+ import { NmField } from '../nm-field/nm-field.tsx';
5
+
6
+ const logger = createLogger('interface-nm-collection-query');
7
+
8
+ export type NmCollectionQueryProps = {
9
+ imageLabel: string;
10
+ videoLabel: string;
11
+ audioLabel: string;
12
+ searchLabel: string;
13
+ imageOn: boolean;
14
+ videoOn: boolean;
15
+ audioOn: boolean;
16
+ query: string;
17
+ onImage: (on: boolean) => void;
18
+ onVideo: (on: boolean) => void;
19
+ onAudio: (on: boolean) => void;
20
+ onQuery: (query: string) => void;
21
+ filters?: boolean;
22
+ search?: boolean;
23
+ };
24
+
25
+ export function NmCollectionQuery(props: NmCollectionQueryProps): ReactNode {
26
+ logger.debug('render', 'query', {
27
+ image: props.imageOn,
28
+ video: props.videoOn,
29
+ audio: props.audioOn,
30
+ query: props.query.length,
31
+ });
32
+ return (
33
+ <>
34
+ {props.filters === false ? null : (
35
+ <>
36
+ <CollectionFilter
37
+ label={props.imageLabel}
38
+ icon={NM_COLLECTION_ICON_SRC.image}
39
+ on={props.imageOn}
40
+ testId="collection-filter-image"
41
+ onToggle={props.onImage}
42
+ />
43
+ <CollectionFilter
44
+ label={props.videoLabel}
45
+ icon={NM_COLLECTION_ICON_SRC.video}
46
+ on={props.videoOn}
47
+ testId="collection-filter-video"
48
+ onToggle={props.onVideo}
49
+ />
50
+ <CollectionFilter
51
+ label={props.audioLabel}
52
+ icon={NM_COLLECTION_ICON_SRC.audio}
53
+ on={props.audioOn}
54
+ testId="collection-filter-audio"
55
+ onToggle={props.onAudio}
56
+ />
57
+ </>
58
+ )}
59
+ {props.search === false ? null : (
60
+ <NmField
61
+ kind="search"
62
+ icon="search"
63
+ className={NM_COLLECTION_CLASSES.search}
64
+ value={props.query}
65
+ placeholder={props.searchLabel}
66
+ label={props.searchLabel}
67
+ testId="collection-search"
68
+ onChange={props.onQuery}
69
+ />
70
+ )}
71
+ </>
72
+ );
73
+ }
74
+
75
+ function CollectionFilter(props: {
76
+ label: string;
77
+ icon: string;
78
+ on: boolean;
79
+ testId: string;
80
+ onToggle: (on: boolean) => void;
81
+ }): ReactNode {
82
+ return (
83
+ <button
84
+ className={NM_COLLECTION_CLASSES.filter}
85
+ type="button"
86
+ aria-pressed={props.on}
87
+ data-active={props.on ? 'true' : 'false'}
88
+ data-testid={props.testId}
89
+ onClick={() => {
90
+ const next = !props.on;
91
+ logger.debug('filter', props.testId, { on: next });
92
+ props.onToggle(next);
93
+ }}
94
+ >
95
+ <img src={props.icon} alt="" />
96
+ {props.label}
97
+ </button>
98
+ );
99
+ }
@@ -0,0 +1,55 @@
1
+ import type { ReactNode } from 'react';
2
+ import {
3
+ NM_COLLECTION_ACTIONS,
4
+ NM_COLLECTION_CLASSES,
5
+ type NmCollectionItemActions,
6
+ } from '../../config/nm-collection.ts';
7
+ import {
8
+ NM_SMALL_ICON_BUTTON_CLASSES,
9
+ NM_SMALL_ICON_BUTTON_TONES,
10
+ } from '../../config/nm-small-icon-button.ts';
11
+ import { NM_DROPDOWN_ALIGN, NM_DROPDOWN_TRIGGER } from '../../config/nm-dropdown.ts';
12
+ import { NmDropdown } from '../nm-dropdown/nm-dropdown.tsx';
13
+ import { createLogger } from '../../kit-logger.ts';
14
+
15
+ const logger = createLogger('interface-nm-collection-row-menu');
16
+
17
+ export function NmCollectionRowMenu(props: {
18
+ itemId: string;
19
+ title: string;
20
+ actions: NmCollectionItemActions;
21
+ }): ReactNode {
22
+ logger.debug('render', props.itemId);
23
+ return (
24
+ <div className={NM_COLLECTION_CLASSES.menu}>
25
+ <NmDropdown
26
+ label={`${props.actions.menuLabel} ${props.title}`}
27
+ trigger={NM_DROPDOWN_TRIGGER.icon}
28
+ align={NM_DROPDOWN_ALIGN.end}
29
+ icon="ellipsis"
30
+ triggerClassName={`${NM_SMALL_ICON_BUTTON_CLASSES.root} ${NM_SMALL_ICON_BUTTON_CLASSES[NM_SMALL_ICON_BUTTON_TONES.quiet]}`}
31
+ items={[
32
+ {
33
+ id: NM_COLLECTION_ACTIONS.open,
34
+ label: props.actions.openLabel,
35
+ icon: 'arrow-up-right',
36
+ },
37
+ {
38
+ id: NM_COLLECTION_ACTIONS.delete,
39
+ label: props.actions.deleteLabel,
40
+ icon: 'trash',
41
+ },
42
+ ]}
43
+ onSelect={(id) => {
44
+ logger.debug(id, props.itemId);
45
+ if (id === NM_COLLECTION_ACTIONS.open) {
46
+ props.actions.onOpen(props.itemId);
47
+ }
48
+ if (id === NM_COLLECTION_ACTIONS.delete) {
49
+ props.actions.onDelete(props.itemId);
50
+ }
51
+ }}
52
+ />
53
+ </div>
54
+ );
55
+ }
@@ -0,0 +1,99 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { NmIconId } from '../../config/nm-icons.ts';
3
+ import {
4
+ NM_COLLECTION_CLASSES,
5
+ type NmCollectionColumn,
6
+ type NmCollectionItem,
7
+ type NmCollectionItemActions,
8
+ } from '../../config/nm-collection.ts';
9
+ import { NM_SMALL_ICON_BUTTON_CLASSES } from '../../config/nm-small-icon-button.ts';
10
+ import { NmCollectionRowMenu } from './nm-collection-row-menu.tsx';
11
+ import { createLogger } from '../../kit-logger.ts';
12
+ import { NmIcon } from '../nm-icon/nm-icon.tsx';
13
+
14
+ const logger = createLogger('interface-nm-collection-table');
15
+
16
+ export type NmCollectionTableProps = {
17
+ items: readonly NmCollectionItem[];
18
+ label: string;
19
+ nameLabel?: string;
20
+ columns?: readonly NmCollectionColumn[];
21
+ headerIcon?: NmIconId;
22
+ actions?: NmCollectionItemActions;
23
+ onSelect: (id: string) => void;
24
+ };
25
+
26
+ export function NmCollectionTable(props: NmCollectionTableProps): ReactNode {
27
+ logger.debug('render', 'table', { count: props.items.length });
28
+ if (props.items.length === 0) {
29
+ return null;
30
+ }
31
+ const columns = props.columns ?? [];
32
+ const columnar = columns.length > 0;
33
+ return (
34
+ <div
35
+ className={NM_COLLECTION_CLASSES.table}
36
+ role={columnar ? 'table' : 'list'}
37
+ aria-label={props.label}
38
+ data-testid="collection-table"
39
+ >
40
+ {columnar ? (
41
+ <div className={`${NM_COLLECTION_CLASSES.row} ${NM_COLLECTION_CLASSES.head}`} role="row">
42
+ <span className={NM_COLLECTION_CLASSES.name} role="columnheader">
43
+ {props.nameLabel}
44
+ </span>
45
+ <span className={NM_COLLECTION_CLASSES.facts}>
46
+ {columns.map((column) => (
47
+ <span key={column.id} className={NM_COLLECTION_CLASSES.cell} role="columnheader">
48
+ {column.label}
49
+ </span>
50
+ ))}
51
+ </span>
52
+ {props.headerIcon ? (
53
+ <span className={NM_COLLECTION_CLASSES.menu} aria-hidden="true">
54
+ <span className={NM_SMALL_ICON_BUTTON_CLASSES.root}>
55
+ <NmIcon id={props.headerIcon} />
56
+ </span>
57
+ </span>
58
+ ) : null}
59
+ </div>
60
+ ) : null}
61
+ {props.items.map((item) => (
62
+ <div
63
+ key={item.id}
64
+ className={NM_COLLECTION_CLASSES.row}
65
+ role={columnar ? 'row' : 'listitem'}
66
+ data-columns={columnar ? 'true' : undefined}
67
+ >
68
+ <button
69
+ className={NM_COLLECTION_CLASSES.rowHit}
70
+ type="button"
71
+ onClick={() => {
72
+ logger.debug('select', item.id);
73
+ props.onSelect(item.id);
74
+ }}
75
+ >
76
+ <span className={NM_COLLECTION_CLASSES.name} role={columnar ? 'cell' : undefined}>
77
+ {item.title}
78
+ </span>
79
+ {columnar ? (
80
+ <span className={NM_COLLECTION_CLASSES.facts}>
81
+ {columns.map((column) => (
82
+ <span key={column.id} className={NM_COLLECTION_CLASSES.cell} role="cell">
83
+ {item.cells?.[column.id] ?? ''}
84
+ </span>
85
+ ))}
86
+ </span>
87
+ ) : null}
88
+ {!columnar && item.meta ? (
89
+ <span className={NM_COLLECTION_CLASSES.meta}>{item.meta}</span>
90
+ ) : null}
91
+ </button>
92
+ {props.actions ? (
93
+ <NmCollectionRowMenu itemId={item.id} title={item.title} actions={props.actions} />
94
+ ) : null}
95
+ </div>
96
+ ))}
97
+ </div>
98
+ );
99
+ }
@@ -0,0 +1,50 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { NmIconId } from '../../config/nm-icons.ts';
3
+ import {
4
+ NM_COLLECTION_ACTIONS,
5
+ NM_COLLECTION_CLASSES,
6
+ type NmCollectionItemActions,
7
+ } from '../../config/nm-collection.ts';
8
+ import { NmSmallIconButton } from '../nm-small-icon-button/nm-small-icon-button.tsx';
9
+ import { createLogger } from '../../kit-logger.ts';
10
+
11
+ const logger = createLogger('interface-nm-collection-tile-actions');
12
+
13
+ const TILE_ACTIONS = [
14
+ { id: NM_COLLECTION_ACTIONS.open, icon: 'arrow-up-right', label: 'openLabel', run: 'onOpen' },
15
+ {
16
+ id: NM_COLLECTION_ACTIONS.download,
17
+ icon: 'download',
18
+ label: 'downloadLabel',
19
+ run: 'onDownload',
20
+ },
21
+ { id: NM_COLLECTION_ACTIONS.delete, icon: 'trash', label: 'deleteLabel', run: 'onDelete' },
22
+ ] as const satisfies readonly {
23
+ id: string;
24
+ icon: NmIconId;
25
+ label: 'openLabel' | 'downloadLabel' | 'deleteLabel';
26
+ run: 'onOpen' | 'onDownload' | 'onDelete';
27
+ }[];
28
+
29
+ export function NmCollectionTileActions(props: {
30
+ itemId: string;
31
+ title: string;
32
+ actions: NmCollectionItemActions;
33
+ }): ReactNode {
34
+ logger.debug('render', props.itemId);
35
+ return (
36
+ <div className={NM_COLLECTION_CLASSES.actions}>
37
+ {TILE_ACTIONS.map((action) => (
38
+ <NmSmallIconButton
39
+ key={action.id}
40
+ label={`${props.actions[action.label]} ${props.title}`}
41
+ icon={action.icon}
42
+ onClick={() => {
43
+ logger.debug(action.id, props.itemId);
44
+ props.actions[action.run](props.itemId);
45
+ }}
46
+ />
47
+ ))}
48
+ </div>
49
+ );
50
+ }
@@ -0,0 +1,25 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { NmIconId } from '../../config/nm-icons.ts';
3
+ import { NM_COLLECTION_CLASSES } from '../../config/nm-collection.ts';
4
+ import { NmIcon } from '../nm-icon/nm-icon.tsx';
5
+
6
+ export type NmCollectionTitleProps = {
7
+ /** Accessible name (icon-only; not shown as visible text). */
8
+ title: string;
9
+ titleSrc: string;
10
+ titleIcon?: NmIconId;
11
+ };
12
+
13
+ /** Collection header identity chip — icon only, raised background on hover. */
14
+ export function NmCollectionTitle(props: NmCollectionTitleProps): ReactNode {
15
+ return (
16
+ <div
17
+ className={NM_COLLECTION_CLASSES.title}
18
+ role="img"
19
+ aria-label={props.title}
20
+ data-testid="collection-title"
21
+ >
22
+ {props.titleIcon ? <NmIcon id={props.titleIcon} /> : <img src={props.titleSrc} alt="" />}
23
+ </div>
24
+ );
25
+ }