@nordmanai/nif 0.2.21 → 0.2.23

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.
@@ -34,8 +34,6 @@ export declare const NM_COMPOSER_CLASSES: {
34
34
  readonly bar: 'nm-composer__bar';
35
35
  readonly start: 'nm-composer__start';
36
36
  readonly end: 'nm-composer__end';
37
- readonly button: 'nm-composer__button';
38
- readonly primary: 'nm-composer__button--primary';
39
37
  readonly stopMark: 'nm-composer__stop-mark';
40
38
  };
41
39
  /** Set on the root with a shape value; every button in the bar, own or hosted, takes that shape. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nordmanai/nif",
3
- "version": "0.2.21",
3
+ "version": "0.2.23",
4
4
  "description": "Nordman Interface Framework: the one UI and UX system for every Nordman product. Base UI, in the shadcn composition, styled by NIF tokens.",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.2.20",
2
+ "version": "0.2.22",
3
3
  "versions": {
4
4
  "@animate-ui/react": "^2.4.2",
5
5
  "@base-ui/react": "^1.9.0",
@@ -2313,13 +2313,17 @@
2313
2313
  "components/nm-voice-orb/nm-voice-orb.css",
2314
2314
  "components/nm-voice-orb/nm-voice-orb.tsx",
2315
2315
  "components/nm-voice-orb/use-nm-voice-orb-theme.ts",
2316
+ "components/nordman-icon-button/nordman-icon-button.css",
2317
+ "components/nordman-icon-button/nordman-icon-button.tsx",
2316
2318
  "config/nm-appearance.ts",
2317
2319
  "config/nm-composer.ts",
2320
+ "config/nm-icon-button.ts",
2318
2321
  "config/nm-icons-extended.ts",
2319
2322
  "config/nm-icons.ts",
2320
2323
  "config/nm-shape.ts",
2321
2324
  "config/nm-site-palette.ts",
2322
2325
  "config/nm-voice-orb.ts",
2326
+ "config/nordman-chrome.ts",
2323
2327
  "kit-logger.ts",
2324
2328
  "lib/appearance/cloudOrbAppearance.ts",
2325
2329
  "lib/appearance/color.ts",
@@ -2337,7 +2341,8 @@
2337
2341
  "uses": [
2338
2342
  "chip",
2339
2343
  "icon",
2340
- "voice-orb"
2344
+ "voice-orb",
2345
+ "nordman-icon-button"
2341
2346
  ]
2342
2347
  },
2343
2348
  "context-menu": {
@@ -155,60 +155,11 @@
155
155
  gap: var(--nm-space-8);
156
156
  }
157
157
 
158
- /* 24px square at the default icon size: the icon plus the 6px micro-unit on each side. */
159
- .nm-composer__button {
160
- display: inline-flex;
161
- flex-shrink: 0;
162
- align-items: center;
163
- justify-content: center;
164
- box-sizing: content-box;
165
- width: var(--nm-icon-button);
166
- height: var(--nm-icon-button);
167
- padding: calc(var(--nm-space-6) / 2);
168
- border: 0;
169
- border-radius: var(--nm-control-r);
170
- background: transparent;
171
- color: var(--nm-fg-muted);
172
- cursor: pointer;
173
- }
174
-
175
- .nm-composer__button .nm-icon {
176
- width: 100%;
177
- height: 100%;
178
- }
179
-
180
- .nm-composer__button:hover:not(:disabled),
181
- .nm-composer__button:focus-visible,
182
- .nm-composer__button[aria-pressed='true'] {
183
- background: var(--nm-inset);
184
- color: var(--nm-fg);
185
- outline: none;
186
- }
187
-
188
- /* Send, Stop, Done and the voice button are the primary action: they take the theme accent. */
189
- .nm-composer__button--primary {
190
- background: var(--nm-accent);
191
- color: var(--nm-canvas);
192
- }
193
-
194
- .nm-composer__button--primary:hover:not(:disabled),
195
- .nm-composer__button--primary:focus-visible {
196
- background: var(--nm-accent);
197
- color: var(--nm-canvas);
198
- opacity: 0.88;
199
- }
200
-
201
- /* Round shape: every button in the bar, the composer's own and any hosted icon button, is a circle. */
202
- .nm-composer[data-button-shape='round'] .nm-composer__button,
158
+ /* Round shape: any icon button hosted in the bar is a circle. */
203
159
  .nm-composer[data-button-shape='round'] .nm-icon-button {
204
160
  border-radius: var(--nm-radius-round);
205
161
  }
206
162
 
207
- .nm-composer__button:disabled {
208
- cursor: default;
209
- opacity: 0.4;
210
- }
211
-
212
163
  .nm-composer__stop-mark {
213
164
  display: block;
214
165
  width: 60%;
@@ -1,5 +1,5 @@
1
1
  import { NmChip } from '../nm-chip/nm-chip.tsx';
2
- import { useCallback, useEffect, useLayoutEffect, useRef, useState, type KeyboardEvent, type ReactNode, type Ref, type RefObject, type TextareaHTMLAttributes } from 'react';
2
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState, type ComponentProps, type KeyboardEvent, type ReactNode, type Ref, type RefObject, type TextareaHTMLAttributes } from 'react';
3
3
  import {
4
4
  NM_COMPOSER_CLASSES as C,
5
5
  NM_COMPOSER_COPY as COPY,
@@ -10,6 +10,8 @@ import {
10
10
  } from '../../config/nm-composer.ts';
11
11
  import type { NmControlShape } from '../../config/nm-shape.ts';
12
12
  import { NmIcon } from '../nm-icon/nm-icon.tsx';
13
+ import { NordmanIconButton } from '../nordman-icon-button/nordman-icon-button.tsx';
14
+ import { NM_CHATBOX_ACTION_CLASS, NM_ICON_BUTTON_TONES } from '../../config/nm-icon-button.ts';
13
15
  import type { NmIconId } from '../../config/nm-icons.ts';
14
16
  import { NmVoiceOrb } from '../nm-voice-orb/nm-voice-orb.tsx';
15
17
  import type { OrbAdapter } from 'orb-ui';
@@ -183,6 +185,22 @@ function NmComposerInline(props: { text: string; field: RefObject<HTMLTextAreaEl
183
185
  );
184
186
  }
185
187
 
188
+ type ComposerButtonProps = Omit<ComponentProps<typeof NordmanIconButton>, 'tone' | 'className'> & {
189
+ /** The primary action takes the theme accent, as the chatbox send does. */
190
+ primary?: boolean;
191
+ };
192
+
193
+ /** Every button in the bar is the NIF icon button, so it has the same size, padding and states as everywhere else. */
194
+ function ComposerButton({ primary = false, ...props }: ComposerButtonProps) {
195
+ return (
196
+ <NordmanIconButton
197
+ {...props}
198
+ tone={primary ? NM_ICON_BUTTON_TONES.invert : NM_ICON_BUTTON_TONES.quiet}
199
+ className={primary ? NM_CHATBOX_ACTION_CLASS : undefined}
200
+ />
201
+ );
202
+ }
203
+
186
204
  export function NmComposer(props: NmComposerProps): ReactNode {
187
205
  const ownField = useRef<HTMLTextAreaElement>(null);
188
206
  const fieldNode = useRef<HTMLTextAreaElement | null>(null);
@@ -303,19 +321,13 @@ export function NmComposer(props: NmComposerProps): ReactNode {
303
321
  if (props.listening) {
304
322
  action = (
305
323
  <>
306
- <button type="button" className={C.button} aria-label={COPY.cancel} data-testid="dictation-cancel" disabled={props.disabled} onClick={props.onCancelListening}>
307
- <NmIcon id="x" />
308
- </button>
309
- <button type="button" className={`${C.button} ${C.primary}`} aria-label={COPY.done} data-testid="dictation-confirm" data-loading={props.confirming ? 'true' : undefined} disabled={props.disabled || props.confirming} onClick={props.onConfirmListening}>
310
- <NmIcon id={props.confirming ? 'loader' : 'check'} />
311
- </button>
324
+ <ComposerButton glyph="x" label={COPY.cancel} testId="dictation-cancel" shape={props.buttonShape} disabled={props.disabled} onClick={props.onCancelListening} />
325
+ <ComposerButton primary glyph={props.confirming ? 'loader' : 'check'} label={COPY.done} testId="dictation-confirm" shape={props.buttonShape} busy={props.confirming} disabled={props.disabled || props.confirming} onClick={props.onConfirmListening} />
312
326
  </>
313
327
  );
314
328
  } else if (props.voiceActive && !canSend) {
315
329
  action = props.voiceState === 'connecting' ? (
316
- <button type="button" className={C.button} aria-label={COPY.voiceEnd} data-testid="voice-orb-connecting" onClick={props.onVoiceStop}>
317
- <NmIcon id="loader" />
318
- </button>
330
+ <ComposerButton glyph="loader" label={COPY.voiceEnd} testId="voice-orb-connecting" shape={props.buttonShape} onClick={props.onVoiceStop} />
319
331
  ) : (
320
332
  <NmVoiceOrb
321
333
  adapter={props.voiceAdapter}
@@ -329,21 +341,17 @@ export function NmComposer(props: NmComposerProps): ReactNode {
329
341
  );
330
342
  } else if (props.busy) {
331
343
  action = (
332
- <button type="button" className={`${C.button} ${C.primary}`} aria-label={COPY.stop} data-testid="stop-button" onClick={props.onStop}>
344
+ <ComposerButton primary label={COPY.stop} testId="stop-button" shape={props.buttonShape} onClick={props.onStop}>
333
345
  <span className={C.stopMark} />
334
- </button>
346
+ </ComposerButton>
335
347
  );
336
348
  } else if (canSend || props.onVoice == null) {
337
349
  action = (
338
- <button type="submit" className={`${C.button} ${C.primary}`} aria-label={COPY.send} data-testid="send-button" disabled={props.disabled || !canSend}>
339
- <NmIcon id="send" />
340
- </button>
350
+ <ComposerButton primary type="submit" glyph="send" label={COPY.send} testId="send-button" shape={props.buttonShape} disabled={props.disabled || !canSend} />
341
351
  );
342
352
  } else {
343
353
  action = (
344
- <button type="button" className={`${C.button} ${C.primary}`} aria-label={COPY.voice} data-testid="voice-button" disabled={props.disabled} onClick={props.onVoice}>
345
- <NmIcon id="audio-lines" />
346
- </button>
354
+ <ComposerButton primary glyph="audio-lines" label={COPY.voice} testId="voice-button" shape={props.buttonShape} disabled={props.disabled} onClick={props.onVoice} />
347
355
  );
348
356
  }
349
357
 
@@ -373,9 +381,9 @@ export function NmComposer(props: NmComposerProps): ReactNode {
373
381
  <span className={C.queuedText}>{q.text}</span>
374
382
  <span className={C.queuedActions}>
375
383
  {props.onEditQueued ? (
376
- <button type="button" className={C.button} aria-label={COPY.editQueued} onClick={() => props.onEditQueued?.(q.id)}><NmIcon id="pen" /></button>
384
+ <ComposerButton glyph="pen" label={COPY.editQueued} shape={props.buttonShape} onClick={() => props.onEditQueued?.(q.id)} />
377
385
  ) : null}
378
- <button type="button" className={C.button} aria-label={COPY.removeQueued} onClick={() => props.onRemoveQueued?.(q.id)}><NmIcon id="x" /></button>
386
+ <ComposerButton glyph="x" label={COPY.removeQueued} shape={props.buttonShape} onClick={() => props.onRemoveQueued?.(q.id)} />
379
387
  </span>
380
388
  </li>
381
389
  ))}
@@ -418,21 +426,15 @@ export function NmComposer(props: NmComposerProps): ReactNode {
418
426
  <div className={C.bar}>
419
427
  <div className={C.start}>
420
428
  {props.listening ? null : props.addControl ?? (props.onAdd ? (
421
- <button type="button" className={C.button} aria-label={COPY.attach} disabled={props.disabled} onClick={props.onAdd}>
422
- <NmIcon id="plus" />
423
- </button>
429
+ <ComposerButton glyph="plus" label={COPY.attach} shape={props.buttonShape} disabled={props.disabled} onClick={props.onAdd} />
424
430
  ) : null)}
425
431
  {props.listening ? null : props.start}
426
432
  </div>
427
433
  <div className={C.end}>
428
434
  {props.voiceActive && props.onVoiceMute ? (
429
- <button type="button" className={C.button} aria-label={props.voiceMuted ? COPY.unmute : COPY.mute} aria-pressed={props.voiceMuted === true} data-testid="voice-mic-toggle" disabled={props.disabled} onClick={props.onVoiceMute}>
430
- <NmIcon id={props.voiceMuted ? 'mic-off' : 'mic'} />
431
- </button>
435
+ <ComposerButton glyph={props.voiceMuted ? 'mic-off' : 'mic'} label={props.voiceMuted ? COPY.unmute : COPY.mute} pressed={props.voiceMuted === true} testId="voice-mic-toggle" shape={props.buttonShape} disabled={props.disabled} onClick={props.onVoiceMute} />
432
436
  ) : props.onMic && !props.listening && !props.voiceActive ? (
433
- <button type="button" className={C.button} aria-label={COPY.mic} disabled={props.disabled} onClick={props.onMic}>
434
- <NmIcon id="mic" />
435
- </button>
437
+ <ComposerButton glyph="mic" label={COPY.mic} shape={props.buttonShape} disabled={props.disabled} onClick={props.onMic} />
436
438
  ) : null}
437
439
  {props.listening ? null : props.end}
438
440
  {action}
@@ -238,7 +238,7 @@
238
238
  min-height: calc(var(--nm-space-12) + var(--nm-icon-control) + var(--nm-space-8));
239
239
  }
240
240
  .nm-sidebar-provider:has(> .nm-sidebar__trigger--anchored) .nm-sidebar__header > .nm-sidebar__menu > .nm-sidebar__menu-item:first-child > .nm-sidebar__menu-button,
241
- .nm-sidebar-provider:has(> .nm-sidebar__trigger--anchored) .nm-sidebar__header > .nm-sidebar__brand {
241
+ .nm-sidebar__header > .nm-sidebar__brand {
242
242
  min-height: var(--nm-icon-control);
243
243
  }
244
244
  /* The product name: a row's geometry and type, semibold, with nothing to hover or press. */
@@ -41,8 +41,6 @@ export const NM_COMPOSER_CLASSES = {
41
41
  bar: 'nm-composer__bar',
42
42
  start: 'nm-composer__start',
43
43
  end: 'nm-composer__end',
44
- button: 'nm-composer__button',
45
- primary: 'nm-composer__button--primary',
46
44
  stopMark: 'nm-composer__stop-mark',
47
45
  } as const;
48
46
  /** Set on the root with a shape value; every button in the bar, own or hosted, takes that shape. */