@nordmanai/nif 0.2.0 → 0.2.2

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.
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from 'react';
1
+ import { type ButtonHTMLAttributes } from 'react';
2
2
  import type { NmIconId } from '../../config/nm-icons.ts';
3
3
  import { NM_BUTTON_SIZES, NM_BUTTON_TONES, type NmButtonSize, type NmButtonTone } from '../../config/nm-button.ts';
4
4
  export type NmButtonProps = {
@@ -15,5 +15,7 @@ export type NmButtonProps = {
15
15
  testId?: string;
16
16
  onClick?: () => void;
17
17
  };
18
- export declare function NmButton(props: NmButtonProps): ReactNode;
18
+ /** Any other button attribute, ref and event goes to the button, so a Base UI trigger can render a NmButton. */
19
+ type NmButtonNativeProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, keyof NmButtonProps | 'children' | 'aria-pressed'>;
20
+ export declare const NmButton: import("react").ForwardRefExoticComponent<NmButtonProps & NmButtonNativeProps & import("react").RefAttributes<HTMLButtonElement>>;
19
21
  export { NM_BUTTON_SIZES, NM_BUTTON_TONES };
@@ -0,0 +1,5 @@
1
+ import type { ComponentProps } from 'react';
2
+ /** A key or a shortcut, drawn as a key cap. Use it in tooltips, menus and docs. The shadcn Kbd composition. */
3
+ export declare function NmKbd({ className, ...props }: ComponentProps<'kbd'>): import("react").JSX.Element;
4
+ /** Several keys of one shortcut, side by side. */
5
+ export declare function NmKbdGroup({ className, ...props }: ComponentProps<'span'>): import("react").JSX.Element;
@@ -1,6 +1,7 @@
1
1
  import type { ComponentProps, ReactNode } from 'react';
2
2
  import { Toolbar as ToolbarPrimitive } from '@base-ui/react/toolbar';
3
3
  import type { NmIconId } from '../../config/nm-icons.ts';
4
+ import { type NmButtonTone } from '../../config/nm-button.ts';
4
5
  import { type NmIconButtonTone } from '../../config/nm-icon-button.ts';
5
6
  /** Base UI Toolbar, in the shadcn composition, styled by NIF tokens. One tab stop, arrow keys move between controls. */
6
7
  export declare function NmToolbar({ className, ...props }: ToolbarPrimitive.Root.Props): import("react").JSX.Element;
@@ -9,11 +10,14 @@ export declare function NmToolbarGroup({ className, ...props }: ToolbarPrimitive
9
10
  export declare function NmToolbarSeparator({ className, ...props }: ToolbarPrimitive.Separator.Props): import("react").JSX.Element;
10
11
  /** Takes the free space, pushing what follows to the end. */
11
12
  export declare function NmToolbarSpacer({ className, ...props }: ComponentProps<'span'>): import("react").JSX.Element;
12
- /** An icon button, or a button with text. `label` is its accessible name. */
13
+ /**
14
+ * An icon button. Give it children and it is a NIF button with text instead, in the tone of your choice.
15
+ * `label` is its accessible name.
16
+ */
13
17
  export declare function NmToolbarButton({ icon, label, tone, pressed, className, children, ...props }: Omit<ToolbarPrimitive.Button.Props, 'children'> & {
14
18
  icon?: NmIconId;
15
19
  label: string;
16
- tone?: NmIconButtonTone;
20
+ tone?: NmIconButtonTone | NmButtonTone;
17
21
  pressed?: boolean;
18
22
  children?: ReactNode;
19
23
  }): import("react").JSX.Element;
@@ -3,7 +3,6 @@ export declare const NM_ICON_BUTTON_TONES: {
3
3
  readonly raised: 'raised';
4
4
  readonly quiet: 'quiet';
5
5
  readonly solid: 'solid';
6
- readonly subtle: 'subtle';
7
6
  readonly panel: 'panel';
8
7
  readonly invert: 'invert';
9
8
  };
@@ -16,7 +15,6 @@ export declare const NM_ICON_BUTTON_CLASSES: {
16
15
  readonly raised: 'nm-icon-button--raised';
17
16
  readonly quiet: 'nm-icon-button--quiet';
18
17
  readonly solid: 'nm-icon-button--solid';
19
- readonly subtle: 'nm-icon-button--subtle';
20
18
  readonly panel: 'nm-icon-button--panel';
21
19
  readonly invert: 'nm-icon-button--invert';
22
20
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nordmanai/nif",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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.0",
2
+ "version": "0.2.2",
3
3
  "versions": {
4
4
  "@base-ui/react": "^1.9.0",
5
5
  "framer-motion": "^14.9.1",
@@ -90,7 +90,8 @@
90
90
  "resize-handle",
91
91
  "select",
92
92
  "combobox",
93
- "button-group"
93
+ "button-group",
94
+ "kbd"
94
95
  ],
95
96
  "items": {
96
97
  "ai": {
@@ -823,6 +824,16 @@
823
824
  ],
824
825
  "uses": []
825
826
  },
827
+ "kbd": {
828
+ "dir": "nm-kbd",
829
+ "files": [
830
+ "components/nm-kbd/nm-kbd.css",
831
+ "components/nm-kbd/nm-kbd.tsx",
832
+ "lib/nm-class.ts"
833
+ ],
834
+ "libs": [],
835
+ "uses": []
836
+ },
826
837
  "label": {
827
838
  "dir": "nm-label",
828
839
  "files": [
@@ -1183,6 +1194,7 @@
1183
1194
  "components/nm-side-panel/nm-side-panel.tsx",
1184
1195
  "components/nm-toolbar/nm-toolbar.css",
1185
1196
  "components/nm-toolbar/nm-toolbar.tsx",
1197
+ "config/nm-button.ts",
1186
1198
  "config/nm-icon-button.ts",
1187
1199
  "config/nm-icons.ts",
1188
1200
  "config/nm-side-panel.ts",
@@ -1320,6 +1332,7 @@
1320
1332
  "components/nm-icon/nm-lucide.tsx",
1321
1333
  "components/nm-toolbar/nm-toolbar.css",
1322
1334
  "components/nm-toolbar/nm-toolbar.tsx",
1335
+ "config/nm-button.ts",
1323
1336
  "config/nm-icon-button.ts",
1324
1337
  "config/nm-icons.ts",
1325
1338
  "kit-logger.ts",
@@ -30,20 +30,6 @@
30
30
  height: var(--control-h-lg);
31
31
  }
32
32
 
33
- /* An icon alone is a square the height of the button. */
34
- .nm-button--icon {
35
- width: var(--control-h);
36
- padding: 0;
37
- }
38
-
39
- .nm-button--icon.nm-button--chip {
40
- width: var(--control-h-sm);
41
- }
42
-
43
- .nm-button--icon.nm-button--large {
44
- width: var(--control-h-lg);
45
- }
46
-
47
33
  .nm-button--block {
48
34
  width: 100%;
49
35
  }
@@ -98,11 +84,21 @@
98
84
  cursor: default;
99
85
  }
100
86
 
87
+ /* An icon alone is a square the height of the button, in every size. */
88
+ .nm-button--icon,
101
89
  .nm-button:has(.nm-button__icon):not(:has(.nm-button__label)) {
102
90
  width: var(--control-h);
103
91
  padding: 0;
104
92
  }
105
93
 
94
+ :is(.nm-button--icon, .nm-button:has(.nm-button__icon):not(:has(.nm-button__label))).nm-button--chip {
95
+ width: var(--control-h-sm);
96
+ }
97
+
98
+ :is(.nm-button--icon, .nm-button:has(.nm-button__icon):not(:has(.nm-button__label))).nm-button--large {
99
+ width: var(--control-h-lg);
100
+ }
101
+
106
102
  .nm-button__icon {
107
103
  width: var(--nm-icon-button);
108
104
  height: var(--nm-icon-button);
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from 'react';
1
+ import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react';
2
2
  import type { NmIconId } from '../../config/nm-icons.ts';
3
3
  import {
4
4
  NM_BUTTON_CLASSES,
@@ -27,32 +27,39 @@ export type NmButtonProps = {
27
27
  onClick?: () => void;
28
28
  };
29
29
 
30
- export function NmButton(props: NmButtonProps): ReactNode {
31
- const size = props.size ?? NM_BUTTON_SIZES.default;
30
+ /** Any other button attribute, ref and event goes to the button, so a Base UI trigger can render a NmButton. */
31
+ type NmButtonNativeProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, keyof NmButtonProps | 'children' | 'aria-pressed'>;
32
+
33
+ export const NmButton = forwardRef<HTMLButtonElement, NmButtonProps & NmButtonNativeProps>(function NmButton(props, ref): ReactNode {
34
+ const { label, tone, size: sizeProp, block, type, disabled, pressed, icon, iconOnly, testId, onClick, className: extra, ...rest } = props;
35
+ const size = sizeProp ?? NM_BUTTON_SIZES.default;
32
36
  const className = [
33
37
  NM_BUTTON_CLASSES.root,
34
- NM_BUTTON_CLASSES[props.tone],
38
+ NM_BUTTON_CLASSES[tone],
35
39
  NM_BUTTON_CLASSES[size],
36
- props.block ? NM_BUTTON_CLASSES.block : null,
37
- props.iconOnly ? NM_BUTTON_CLASSES.icon : null,
40
+ block ? NM_BUTTON_CLASSES.block : null,
41
+ iconOnly ? NM_BUTTON_CLASSES.icon : null,
42
+ extra,
38
43
  ]
39
44
  .filter(Boolean)
40
45
  .join(' ');
41
- logger.debug('render', props.label, { tone: props.tone, size });
46
+ logger.debug('render', label, { tone, size });
42
47
  return (
43
48
  <button
49
+ {...rest}
50
+ ref={ref}
44
51
  className={className}
45
- type={props.type ?? 'button'}
46
- disabled={props.disabled}
47
- aria-pressed={props.pressed}
48
- aria-label={props.iconOnly ? props.label : undefined}
49
- data-testid={props.testId}
50
- onClick={props.onClick}
52
+ type={type ?? 'button'}
53
+ disabled={disabled}
54
+ aria-pressed={pressed}
55
+ aria-label={iconOnly ? label : undefined}
56
+ data-testid={testId}
57
+ onClick={onClick}
51
58
  >
52
- {props.icon ? <NmIcon id={props.icon} className="nm-button__icon" /> : null}
53
- {props.iconOnly ? null : <span className="nm-button__label">{props.label}</span>}
59
+ {icon ? <NmIcon id={icon} className="nm-button__icon" /> : null}
60
+ {iconOnly ? null : <span className="nm-button__label">{label}</span>}
54
61
  </button>
55
62
  );
56
- }
63
+ });
57
64
 
58
65
  export { NM_BUTTON_SIZES, NM_BUTTON_TONES };
@@ -51,6 +51,17 @@
51
51
  margin-block-start: 0;
52
52
  }
53
53
 
54
+ /* Buttons, text and selects keep their size. Only a field gives way when the bar is narrow. */
55
+ .nm-button-group > :not(.nm-button-group, .nm-button-group__separator, .nm-input, .nm-input-group, .nm-combobox__group) {
56
+ flex-shrink: 0;
57
+ }
58
+
59
+ /* A select in a bar is as wide as its choices, not the whole bar. */
60
+ .nm-button-group > .nm-select__trigger {
61
+ width: auto;
62
+ min-width: var(--nm-width-date);
63
+ }
64
+
54
65
  .nm-button-group > :is(.nm-input, .nm-input-group, .nm-combobox__group) {
55
66
  flex: 1;
56
67
  min-width: var(--nm-width-popup-sm);
@@ -342,7 +342,7 @@ export function NmDropdown(props: NmDropdownProps): ReactNode {
342
342
  label={props.label}
343
343
  glyph={props.imageSrc == null ? (props.icon ?? 'ellipsis') : undefined}
344
344
  imageSrc={props.imageSrc}
345
- tone={NM_ICON_BUTTON_TONES.subtle}
345
+ tone={NM_ICON_BUTTON_TONES.quiet}
346
346
  disabled={props.disabled}
347
347
  expanded={open}
348
348
  popup="listbox"
@@ -37,7 +37,7 @@ export function NmIconBorder(props: NmIconBorderProps): ReactNode {
37
37
  label={props.label}
38
38
  glyph={props.icon}
39
39
  imageSrc={props.imageSrc}
40
- tone={NM_ICON_BUTTON_TONES.subtle}
40
+ tone={NM_ICON_BUTTON_TONES.quiet}
41
41
  pressed={props.pressed}
42
42
  checked={props.checked}
43
43
  disabled={props.disabled}
@@ -0,0 +1,23 @@
1
+ .nm-kbd {
2
+ box-sizing: border-box;
3
+ display: inline-flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+ min-width: var(--nm-icon-button);
7
+ height: var(--nm-icon-button);
8
+ padding: 0 var(--nm-space-4);
9
+ border-radius: var(--nm-radius-sm);
10
+ background: var(--nm-line);
11
+ color: var(--nm-fg-muted);
12
+ font-family: var(--nm-font);
13
+ font-size: var(--nm-type-ui-sm);
14
+ font-weight: var(--nm-weight-medium);
15
+ line-height: var(--nm-leading-none);
16
+ user-select: none;
17
+ }
18
+
19
+ .nm-kbd__group {
20
+ display: inline-flex;
21
+ align-items: center;
22
+ gap: var(--nm-space-4);
23
+ }
@@ -0,0 +1,12 @@
1
+ import type { ComponentProps } from 'react';
2
+ import { nmClass } from '../../lib/nm-class.ts';
3
+
4
+ /** A key or a shortcut, drawn as a key cap. Use it in tooltips, menus and docs. The shadcn Kbd composition. */
5
+ export function NmKbd({ className, ...props }: ComponentProps<'kbd'>) {
6
+ return <kbd data-slot="kbd" className={nmClass('nm-kbd', className)} {...props} />;
7
+ }
8
+
9
+ /** Several keys of one shortcut, side by side. */
10
+ export function NmKbdGroup({ className, ...props }: ComponentProps<'span'>) {
11
+ return <span data-slot="kbd-group" className={nmClass('nm-kbd__group', className)} {...props} />;
12
+ }
@@ -52,7 +52,7 @@ export function NmSegmented(props: NmSegmentedProps): ReactNode {
52
52
  const selected = item.id === props.active;
53
53
  const iconOnly = item.iconOnly === true;
54
54
  const className = iconOnly
55
- ? `${NM_SEGMENTED_CLASSES.item} ${NM_SEGMENTED_CLASSES.itemIcon} ${NM_ICON_BUTTON_CLASSES.root} ${NM_ICON_BUTTON_CLASSES[NM_ICON_BUTTON_TONES.subtle]}`
55
+ ? `${NM_SEGMENTED_CLASSES.item} ${NM_SEGMENTED_CLASSES.itemIcon} ${NM_ICON_BUTTON_CLASSES.root} ${NM_ICON_BUTTON_CLASSES[NM_ICON_BUTTON_TONES.quiet]}`
56
56
  : NM_SEGMENTED_CLASSES.item;
57
57
  return (
58
58
  <button
@@ -34,9 +34,3 @@
34
34
  flex: 1;
35
35
  min-width: 0;
36
36
  }
37
-
38
- .nm-toolbar__text {
39
- width: auto;
40
- gap: var(--nm-space-6);
41
- padding: 0 var(--nm-space-8);
42
- }
@@ -2,6 +2,7 @@ import type { ComponentProps, ReactNode } from 'react';
2
2
  import { Toolbar as ToolbarPrimitive } from '@base-ui/react/toolbar';
3
3
  import { nmClass } from '../../lib/nm-class.ts';
4
4
  import type { NmIconId } from '../../config/nm-icons.ts';
5
+ import { NM_BUTTON_TONES, nmButtonClassName, type NmButtonTone } from '../../config/nm-button.ts';
5
6
  import { nmIconButtonClassName, type NmIconButtonTone } from '../../config/nm-icon-button.ts';
6
7
  import { NmIcon } from '../nm-icon/nm-icon.tsx';
7
8
 
@@ -24,26 +25,31 @@ export function NmToolbarSpacer({ className, ...props }: ComponentProps<'span'>)
24
25
  return <span aria-hidden="true" data-slot="toolbar-spacer" className={nmClass('nm-toolbar__spacer', className)} {...props} />;
25
26
  }
26
27
 
27
- /** An icon button, or a button with text. `label` is its accessible name. */
28
+ /**
29
+ * An icon button. Give it children and it is a NIF button with text instead, in the tone of your choice.
30
+ * `label` is its accessible name.
31
+ */
28
32
  export function NmToolbarButton({
29
33
  icon,
30
34
  label,
31
- tone = 'quiet',
35
+ tone,
32
36
  pressed,
33
37
  className,
34
38
  children,
35
39
  ...props
36
- }: Omit<ToolbarPrimitive.Button.Props, 'children'> & { icon?: NmIconId; label: string; tone?: NmIconButtonTone; pressed?: boolean; children?: ReactNode }) {
40
+ }: Omit<ToolbarPrimitive.Button.Props, 'children'> & { icon?: NmIconId; label: string; tone?: NmIconButtonTone | NmButtonTone; pressed?: boolean; children?: ReactNode }) {
41
+ const withText = children != null;
42
+ const buttonTone = (Object.values(NM_BUTTON_TONES) as string[]).includes(tone ?? '') ? (tone as NmButtonTone) : 'muted';
37
43
  return (
38
44
  <ToolbarPrimitive.Button
39
45
  data-slot="toolbar-button"
40
46
  aria-label={label}
41
47
  aria-pressed={pressed}
42
- className={nmClass(nmIconButtonClassName(tone), children ? 'nm-toolbar__text' : undefined, className as string)}
48
+ className={nmClass(withText ? nmButtonClassName(buttonTone) : nmIconButtonClassName((tone as NmIconButtonTone | undefined) ?? 'quiet'), className as string)}
43
49
  {...props}
44
50
  >
45
- {icon ? <NmIcon id={icon} /> : null}
46
- {children}
51
+ {icon ? <NmIcon id={icon} className={withText ? 'nm-button__icon' : undefined} /> : null}
52
+ {withText ? <span className="nm-button__label">{children}</span> : null}
47
53
  </ToolbarPrimitive.Button>
48
54
  );
49
55
  }
@@ -3,6 +3,7 @@
3
3
  }
4
4
 
5
5
  .nm-tooltip {
6
+ position: relative;
6
7
  display: inline-flex;
7
8
  align-items: center;
8
9
  gap: var(--nm-space-6);
@@ -22,6 +23,7 @@
22
23
  }
23
24
 
24
25
  .nm-tooltip__arrow {
26
+ position: absolute;
25
27
  width: var(--nm-space-8);
26
28
  height: var(--nm-space-8);
27
29
  background: var(--nm-fg);
@@ -35,3 +37,18 @@
35
37
  .nm-tooltip__arrow[data-side='bottom'] {
36
38
  top: calc(var(--nm-space-4) * -1);
37
39
  }
40
+
41
+ /* On the right of its control the arrow points left, so it sits on the left edge. */
42
+ .nm-tooltip__arrow[data-side='right'] {
43
+ left: calc(var(--nm-space-4) * -1);
44
+ }
45
+
46
+ .nm-tooltip__arrow[data-side='left'] {
47
+ right: calc(var(--nm-space-4) * -1);
48
+ }
49
+
50
+ /* A tooltip is inverted, so a key cap inside it is too. */
51
+ .nm-tooltip .nm-kbd {
52
+ background: rgb(var(--nm-canvas-rgb) / 0.2);
53
+ color: var(--nm-canvas);
54
+ }
@@ -30,8 +30,10 @@
30
30
  cursor: default;
31
31
  }
32
32
 
33
+ /* Raised sits on a surface of its own with an edge, so it reads on any background. Panel has no edge. */
33
34
  .nm-icon-button--raised {
34
35
  background: var(--nm-raised);
36
+ box-shadow: inset 0 0 0 var(--nm-rule) var(--nm-line-strong);
35
37
  }
36
38
 
37
39
  .nm-icon-button--quiet {
@@ -39,6 +41,10 @@
39
41
  color: var(--nm-fg-subtle);
40
42
  }
41
43
 
44
+ .nm-icon-button--quiet[data-active='true'] {
45
+ color: var(--nm-fg);
46
+ }
47
+
42
48
  .nm-icon-button--quiet.nm-icon-button--active:not(:disabled) {
43
49
  background: var(--nm-raised);
44
50
  color: var(--nm-fg);
@@ -61,15 +67,6 @@
61
67
  color: var(--nm-canvas);
62
68
  }
63
69
 
64
- .nm-icon-button--subtle {
65
- background: transparent;
66
- color: var(--nm-fg-subtle);
67
- }
68
-
69
- .nm-icon-button--subtle[data-active='true'] {
70
- color: var(--nm-fg);
71
- }
72
-
73
70
  .nm-icon-button--panel,
74
71
  .nm-icon-button--panel:hover:not(:disabled):not([aria-disabled='true']),
75
72
  .nm-icon-button--panel:focus-visible:not(:disabled):not([aria-disabled='true']) {
@@ -3,7 +3,6 @@ export const NM_ICON_BUTTON_TONES = {
3
3
  raised: 'raised',
4
4
  quiet: 'quiet',
5
5
  solid: 'solid',
6
- subtle: 'subtle',
7
6
  panel: 'panel',
8
7
  invert: 'invert',
9
8
  } as const;
@@ -18,7 +17,6 @@ export const NM_ICON_BUTTON_CLASSES = {
18
17
  raised: 'nm-icon-button--raised',
19
18
  quiet: 'nm-icon-button--quiet',
20
19
  solid: 'nm-icon-button--solid',
21
- subtle: 'nm-icon-button--subtle',
22
20
  panel: 'nm-icon-button--panel',
23
21
  invert: 'nm-icon-button--invert',
24
22
  } as const;