@nordmanai/nif 0.2.4 → 0.2.5

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.
@@ -5,7 +5,7 @@ export type NmTextareaProps = ComponentProps<'textarea'> & {
5
5
  openInPanel?: boolean;
6
6
  panelTitle?: string;
7
7
  panelButtonLabel?: string;
8
- panelDefaultTab?: 'code' | 'preview';
8
+ panelDefaultTab?: 'code' | 'view';
9
9
  renderMarkdown?: (markdown: string) => ReactNode;
10
10
  };
11
11
  export declare const NmTextarea: import("react").ForwardRefExoticComponent<Omit<NmTextareaProps, "ref"> & import("react").RefAttributes<HTMLTextAreaElement>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nordmanai/nif",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
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.4",
2
+ "version": "0.2.5",
3
3
  "versions": {
4
4
  "@animate-ui/react": "^2.4.2",
5
5
  "@base-ui/react": "^1.9.0",
@@ -1768,22 +1768,27 @@
1768
1768
  "components/nm-popover/nm-popover.tsx",
1769
1769
  "components/nm-prose/nm-prose.css",
1770
1770
  "components/nm-prose/nm-prose.tsx",
1771
- "components/nm-segmented/nm-segmented.css",
1772
- "components/nm-segmented/nm-segmented.tsx",
1771
+ "components/nm-resize-handle/nm-resize-handle.css",
1772
+ "components/nm-resize-handle/nm-resize-handle.tsx",
1773
1773
  "components/nm-select/nm-select.css",
1774
1774
  "components/nm-select/nm-select.tsx",
1775
+ "components/nm-side-panel/nm-side-panel.css",
1776
+ "components/nm-side-panel/nm-side-panel.tsx",
1775
1777
  "components/nm-small-icon-button/nm-small-icon-button.tsx",
1776
1778
  "components/nm-textarea/nm-textarea.css",
1777
1779
  "components/nm-textarea/nm-textarea.tsx",
1780
+ "components/nm-toolbar/nm-toolbar.css",
1781
+ "components/nm-toolbar/nm-toolbar.tsx",
1778
1782
  "components/nordman-icon-button/nordman-icon-button.css",
1779
1783
  "components/nordman-icon-button/nordman-icon-button.tsx",
1784
+ "config/nm-button.ts",
1780
1785
  "config/nm-collection.ts",
1781
1786
  "config/nm-dropdown.ts",
1782
1787
  "config/nm-field.ts",
1783
1788
  "config/nm-icon-button.ts",
1784
1789
  "config/nm-icons-extended.ts",
1785
1790
  "config/nm-icons.ts",
1786
- "config/nm-segmented.ts",
1791
+ "config/nm-side-panel.ts",
1787
1792
  "config/nm-small-icon-button.ts",
1788
1793
  "config/nordman-chrome.ts",
1789
1794
  "kit-logger.ts",
@@ -1811,10 +1816,12 @@
1811
1816
  "input",
1812
1817
  "popover",
1813
1818
  "prose",
1814
- "segmented",
1819
+ "resize-handle",
1815
1820
  "select",
1821
+ "side-panel",
1816
1822
  "small-icon-button",
1817
1823
  "textarea",
1824
+ "toolbar",
1818
1825
  "nordman-icon-button"
1819
1826
  ]
1820
1827
  },
@@ -2965,20 +2972,25 @@
2965
2972
  "components/nm-popover/nm-popover.tsx",
2966
2973
  "components/nm-prose/nm-prose.css",
2967
2974
  "components/nm-prose/nm-prose.tsx",
2968
- "components/nm-segmented/nm-segmented.css",
2969
- "components/nm-segmented/nm-segmented.tsx",
2975
+ "components/nm-resize-handle/nm-resize-handle.css",
2976
+ "components/nm-resize-handle/nm-resize-handle.tsx",
2970
2977
  "components/nm-select/nm-select.css",
2971
2978
  "components/nm-select/nm-select.tsx",
2979
+ "components/nm-side-panel/nm-side-panel.css",
2980
+ "components/nm-side-panel/nm-side-panel.tsx",
2972
2981
  "components/nm-textarea/nm-textarea.css",
2973
2982
  "components/nm-textarea/nm-textarea.tsx",
2983
+ "components/nm-toolbar/nm-toolbar.css",
2984
+ "components/nm-toolbar/nm-toolbar.tsx",
2974
2985
  "components/nordman-icon-button/nordman-icon-button.css",
2975
2986
  "components/nordman-icon-button/nordman-icon-button.tsx",
2987
+ "config/nm-button.ts",
2976
2988
  "config/nm-dropdown.ts",
2977
2989
  "config/nm-field.ts",
2978
2990
  "config/nm-icon-button.ts",
2979
2991
  "config/nm-icons-extended.ts",
2980
2992
  "config/nm-icons.ts",
2981
- "config/nm-segmented.ts",
2993
+ "config/nm-side-panel.ts",
2982
2994
  "config/nordman-chrome.ts",
2983
2995
  "kit-logger.ts",
2984
2996
  "lib/logger.ts",
@@ -3003,9 +3015,11 @@
3003
3015
  "input",
3004
3016
  "popover",
3005
3017
  "prose",
3006
- "segmented",
3018
+ "resize-handle",
3007
3019
  "select",
3020
+ "side-panel",
3008
3021
  "textarea",
3022
+ "toolbar",
3009
3023
  "nordman-icon-button"
3010
3024
  ]
3011
3025
  },
@@ -4250,12 +4264,16 @@
4250
4264
  "components/nm-prose/nm-prose.tsx",
4251
4265
  "components/nm-questionnaire/nm-questionnaire.css",
4252
4266
  "components/nm-questionnaire/nm-questionnaire.tsx",
4253
- "components/nm-segmented/nm-segmented.css",
4254
- "components/nm-segmented/nm-segmented.tsx",
4267
+ "components/nm-resize-handle/nm-resize-handle.css",
4268
+ "components/nm-resize-handle/nm-resize-handle.tsx",
4255
4269
  "components/nm-select/nm-select.css",
4256
4270
  "components/nm-select/nm-select.tsx",
4271
+ "components/nm-side-panel/nm-side-panel.css",
4272
+ "components/nm-side-panel/nm-side-panel.tsx",
4257
4273
  "components/nm-textarea/nm-textarea.css",
4258
4274
  "components/nm-textarea/nm-textarea.tsx",
4275
+ "components/nm-toolbar/nm-toolbar.css",
4276
+ "components/nm-toolbar/nm-toolbar.tsx",
4259
4277
  "components/nordman-icon-button/nordman-icon-button.css",
4260
4278
  "components/nordman-icon-button/nordman-icon-button.tsx",
4261
4279
  "config/nm-button.ts",
@@ -4265,7 +4283,7 @@
4265
4283
  "config/nm-icons-extended.ts",
4266
4284
  "config/nm-icons.ts",
4267
4285
  "config/nm-questionnaire.ts",
4268
- "config/nm-segmented.ts",
4286
+ "config/nm-side-panel.ts",
4269
4287
  "config/nordman-chrome.ts",
4270
4288
  "kit-logger.ts",
4271
4289
  "lib/logger.ts",
@@ -4293,9 +4311,11 @@
4293
4311
  "popover",
4294
4312
  "progress",
4295
4313
  "prose",
4296
- "segmented",
4314
+ "resize-handle",
4297
4315
  "select",
4316
+ "side-panel",
4298
4317
  "textarea",
4318
+ "toolbar",
4299
4319
  "nordman-icon-button"
4300
4320
  ]
4301
4321
  },
@@ -5450,27 +5470,36 @@
5450
5470
  "components/nm-icon/nm-lucide.tsx",
5451
5471
  "components/nm-prose/nm-prose.css",
5452
5472
  "components/nm-prose/nm-prose.tsx",
5453
- "components/nm-segmented/nm-segmented.css",
5454
- "components/nm-segmented/nm-segmented.tsx",
5473
+ "components/nm-resize-handle/nm-resize-handle.css",
5474
+ "components/nm-resize-handle/nm-resize-handle.tsx",
5475
+ "components/nm-side-panel/nm-side-panel.css",
5476
+ "components/nm-side-panel/nm-side-panel.tsx",
5455
5477
  "components/nm-textarea/nm-textarea.css",
5456
5478
  "components/nm-textarea/nm-textarea.tsx",
5479
+ "components/nm-toolbar/nm-toolbar.css",
5480
+ "components/nm-toolbar/nm-toolbar.tsx",
5481
+ "config/nm-button.ts",
5457
5482
  "config/nm-icon-button.ts",
5458
5483
  "config/nm-icons-extended.ts",
5459
5484
  "config/nm-icons.ts",
5460
- "config/nm-segmented.ts",
5485
+ "config/nm-side-panel.ts",
5461
5486
  "kit-logger.ts",
5462
5487
  "lib/logger.ts",
5463
- "lib/nm-class.ts"
5488
+ "lib/nm-class.ts",
5489
+ "lib/nm-motion.ts"
5464
5490
  ],
5465
5491
  "libs": [
5466
5492
  "@animate-ui/react",
5493
+ "@base-ui/react",
5467
5494
  "lucide-react",
5468
5495
  "motion"
5469
5496
  ],
5470
5497
  "uses": [
5471
5498
  "icon",
5472
5499
  "prose",
5473
- "segmented"
5500
+ "resize-handle",
5501
+ "side-panel",
5502
+ "toolbar"
5474
5503
  ]
5475
5504
  },
5476
5505
  "toggle": {
@@ -88,43 +88,36 @@
88
88
  position: fixed;
89
89
  inset: 0;
90
90
  z-index: var(--nm-menu-z);
91
- background: rgb(var(--nm-canvas-rgb) / 0.6);
92
- backdrop-filter: blur(4px);
93
- -webkit-backdrop-filter: blur(4px);
91
+ background: rgb(var(--nm-canvas-rgb) / 0.5);
94
92
  }
95
93
 
96
- .nm-textarea-panel {
94
+ .nm-side-panel.nm-textarea-side-panel {
97
95
  position: fixed;
98
96
  top: 0;
99
97
  right: 0;
100
98
  bottom: 0;
101
99
  z-index: calc(var(--nm-menu-z) + 1);
100
+ box-sizing: border-box;
102
101
  display: flex;
102
+ flex: none;
103
103
  flex-direction: column;
104
- width: min(var(--nm-width-dialog), 100%);
105
- border-left: var(--nm-rule) solid var(--nm-line-strong);
106
- background: var(--nm-panel);
107
- color: var(--nm-fg);
108
- }
109
-
110
- .nm-textarea-panel__header {
111
- display: flex;
112
- align-items: center;
113
- justify-content: space-between;
114
- padding: var(--nm-space-12) var(--nm-space-16);
115
- border-bottom: var(--nm-rule) solid var(--nm-line-strong);
116
- gap: var(--nm-space-12);
104
+ width: var(--nm-side-panel-basis, 50%);
105
+ border-inline-start: var(--nm-rule) solid var(--nm-divider);
106
+ background: var(--nm-canvas);
117
107
  }
118
108
 
119
- .nm-textarea-panel__title-group {
109
+ .nm-textarea-panel__body {
110
+ flex: 1;
120
111
  display: flex;
121
- align-items: center;
122
- gap: var(--nm-space-12);
123
- min-width: 0;
112
+ flex-direction: column;
113
+ min-height: 0;
114
+ padding: var(--nm-space-12);
115
+ overflow: hidden;
124
116
  }
125
117
 
126
118
  .nm-textarea-panel__title {
127
119
  margin: 0;
120
+ padding-inline-start: var(--nm-space-8);
128
121
  font-size: var(--nm-type-ui);
129
122
  font-weight: var(--nm-weight-semibold);
130
123
  color: var(--nm-fg);
@@ -133,47 +126,6 @@
133
126
  text-overflow: ellipsis;
134
127
  }
135
128
 
136
- .nm-textarea-panel__actions {
137
- display: flex;
138
- align-items: center;
139
- gap: var(--nm-space-4);
140
- }
141
-
142
- .nm-textarea-panel__action-btn {
143
- display: inline-flex;
144
- align-items: center;
145
- justify-content: center;
146
- width: var(--control-h);
147
- height: var(--control-h);
148
- padding: 0;
149
- border: 0;
150
- border-radius: var(--nm-control-r);
151
- background: transparent;
152
- color: var(--nm-fg-muted);
153
- cursor: pointer;
154
- transition: color 0.15s, background-color 0.15s;
155
- }
156
-
157
- .nm-textarea-panel__action-btn:hover {
158
- background: var(--nm-raised);
159
- color: var(--nm-fg);
160
- }
161
-
162
- .nm-textarea-panel__action-btn:focus-visible {
163
- outline: var(--nm-rule) solid var(--nm-fg);
164
- outline-offset: var(--nm-rule);
165
- color: var(--nm-fg);
166
- }
167
-
168
- .nm-textarea-panel__body {
169
- flex: 1;
170
- display: flex;
171
- flex-direction: column;
172
- overflow: auto;
173
- min-height: 0;
174
- background: var(--nm-canvas);
175
- }
176
-
177
129
  .nm-textarea-panel__editor {
178
130
  box-sizing: border-box;
179
131
  flex: 1;
@@ -181,7 +133,8 @@
181
133
  height: 100%;
182
134
  min-height: 100%;
183
135
  padding: var(--nm-space-16);
184
- border: 0;
136
+ border: var(--nm-rule) solid var(--nm-line-strong);
137
+ border-radius: var(--nm-control-r);
185
138
  outline: none;
186
139
  resize: none;
187
140
  background: var(--nm-raised);
@@ -192,12 +145,20 @@
192
145
  line-height: var(--nm-leading-relaxed);
193
146
  }
194
147
 
148
+ .nm-textarea-panel__editor:focus-visible {
149
+ border-color: var(--nm-fg);
150
+ }
151
+
195
152
  .nm-textarea-panel__preview {
196
153
  box-sizing: border-box;
197
154
  flex: 1;
155
+ width: 100%;
156
+ height: 100%;
198
157
  padding: var(--nm-space-16);
199
158
  overflow-y: auto;
200
- background: var(--nm-canvas);
159
+ border: var(--nm-rule) solid var(--nm-line-strong);
160
+ border-radius: var(--nm-control-r);
161
+ background: var(--nm-raised);
201
162
  }
202
163
 
203
164
  .nm-textarea-panel__empty {
@@ -206,36 +167,3 @@
206
167
  font-size: var(--nm-type-ui-sm);
207
168
  font-style: italic;
208
169
  }
209
-
210
- .nm-textarea-panel__footer {
211
- display: flex;
212
- align-items: center;
213
- justify-content: space-between;
214
- padding: var(--nm-space-8) var(--nm-space-16);
215
- border-top: var(--nm-rule) solid var(--nm-line-strong);
216
- background: var(--nm-panel);
217
- font-size: var(--nm-type-ui-sm);
218
- color: var(--nm-fg-subtle);
219
- }
220
-
221
- .nm-textarea-panel__done {
222
- display: inline-flex;
223
- align-items: center;
224
- justify-content: center;
225
- height: var(--control-h);
226
- padding: 0 var(--control-px);
227
- border: var(--nm-rule) solid var(--nm-line-strong);
228
- border-radius: var(--nm-control-r);
229
- background: var(--nm-raised);
230
- color: var(--nm-fg);
231
- font-family: var(--nm-font);
232
- font-size: var(--nm-type-ui-sm);
233
- font-weight: var(--nm-weight-medium);
234
- cursor: pointer;
235
- transition: background-color 0.15s, border-color 0.15s;
236
- }
237
-
238
- .nm-textarea-panel__done:hover {
239
- background: var(--nm-panel);
240
- border-color: var(--nm-stroke);
241
- }
@@ -2,7 +2,6 @@ import {
2
2
  forwardRef,
3
3
  useCallback,
4
4
  useEffect,
5
- useId,
6
5
  useRef,
7
6
  useState,
8
7
  type ChangeEvent,
@@ -13,7 +12,8 @@ import { createPortal } from 'react-dom';
13
12
  import { nmClass } from '../../lib/nm-class.ts';
14
13
  import { NmIcon } from '../nm-icon/nm-icon.tsx';
15
14
  import { NmProse } from '../nm-prose/nm-prose.tsx';
16
- import { NmSegmented } from '../nm-segmented/nm-segmented.tsx';
15
+ import { NmSidePanel, NmSidePanelBody } from '../nm-side-panel/nm-side-panel.tsx';
16
+ import { NmToolbar, NmToolbarButton, NmToolbarSpacer } from '../nm-toolbar/nm-toolbar.tsx';
17
17
  import { createLogger } from '../../kit-logger.ts';
18
18
 
19
19
  const logger = createLogger('interface-nm-textarea');
@@ -25,7 +25,7 @@ export type NmTextareaProps = ComponentProps<'textarea'> & {
25
25
  openInPanel?: boolean;
26
26
  panelTitle?: string;
27
27
  panelButtonLabel?: string;
28
- panelDefaultTab?: 'code' | 'preview';
28
+ panelDefaultTab?: 'code' | 'view';
29
29
  renderMarkdown?: (markdown: string) => ReactNode;
30
30
  };
31
31
 
@@ -196,11 +196,6 @@ function parseMarkdown(md: string): ReactNode {
196
196
  return nodes;
197
197
  }
198
198
 
199
- const PANEL_TABS = [
200
- { id: 'code', label: 'Code', icon: 'code' as const },
201
- { id: 'preview', label: 'Preview', icon: 'presentation' as const },
202
- ] as const;
203
-
204
199
  export const NmTextarea = forwardRef<HTMLTextAreaElement, NmTextareaProps>(
205
200
  function NmTextarea(
206
201
  {
@@ -220,7 +215,7 @@ export const NmTextarea = forwardRef<HTMLTextAreaElement, NmTextareaProps>(
220
215
  ) {
221
216
  const isPanelEnabled = variant === 'panel' || openInPanel;
222
217
  const [panelOpen, setPanelOpen] = useState(false);
223
- const [tab, setTab] = useState<'code' | 'preview'>(panelDefaultTab);
218
+ const [view, setView] = useState<'code' | 'view'>(panelDefaultTab);
224
219
  const [copied, setCopied] = useState(false);
225
220
  const triggerRef = useRef<HTMLButtonElement>(null);
226
221
  const internalTextareaRef = useRef<HTMLTextAreaElement | null>(null);
@@ -277,10 +272,6 @@ export const NmTextarea = forwardRef<HTMLTextAreaElement, NmTextareaProps>(
277
272
  return () => window.removeEventListener('keydown', onKeyDown);
278
273
  }, [panelOpen, handleClose]);
279
274
 
280
- const titleId = useId();
281
- const lineCount = currentValue ? currentValue.split('\n').length : 0;
282
- const charCount = currentValue.length;
283
-
284
275
  const textareaElement = (
285
276
  <textarea
286
277
  className={nmClass('nm-textarea', className)}
@@ -312,52 +303,44 @@ export const NmTextarea = forwardRef<HTMLTextAreaElement, NmTextareaProps>(
312
303
  onClick={handleClose}
313
304
  aria-hidden="true"
314
305
  />
315
- <aside
316
- role="dialog"
317
- aria-modal="true"
318
- aria-labelledby={panelTitle ? titleId : undefined}
319
- aria-label={panelTitle ? undefined : 'Markdown Editor'}
320
- className="nm-textarea-panel"
306
+ <NmSidePanel
307
+ label={panelTitle ?? 'Markdown'}
308
+ open={panelOpen}
309
+ className="nm-textarea-side-panel"
321
310
  >
322
- <header className="nm-textarea-panel__header">
323
- <div className="nm-textarea-panel__title-group">
324
- {panelTitle ? (
325
- <h3 id={titleId} className="nm-textarea-panel__title">
326
- {panelTitle}
327
- </h3>
328
- ) : null}
329
- <NmSegmented
330
- label="Editor view"
331
- tone="pills"
332
- items={PANEL_TABS}
333
- active={tab}
334
- onSelect={(next) => setTab(next as 'code' | 'preview')}
335
- />
336
- </div>
337
- <div className="nm-textarea-panel__actions">
338
- <button
339
- type="button"
340
- className="nm-textarea-panel__action-btn"
341
- aria-label={copied ? 'Copied' : 'Copy markdown'}
342
- title={copied ? 'Copied' : 'Copy markdown'}
343
- onClick={handleCopy}
344
- >
345
- <NmIcon id={copied ? 'check' : 'copy'} />
346
- </button>
347
- <button
348
- type="button"
349
- className="nm-textarea-panel__action-btn"
350
- aria-label="Close panel"
351
- title="Close panel"
352
- onClick={handleClose}
353
- >
354
- <NmIcon id="x" />
355
- </button>
356
- </div>
357
- </header>
311
+ <NmToolbar aria-label={panelTitle ?? 'Markdown'}>
312
+ {panelTitle ? <span className="nm-textarea-panel__title">{panelTitle}</span> : null}
313
+ <NmToolbarButton
314
+ icon="code"
315
+ label="Code"
316
+ pressed={view === 'code'}
317
+ tone={view === 'code' ? 'panel' : 'quiet'}
318
+ onClick={() => setView('code')}
319
+ />
320
+ <NmToolbarButton
321
+ icon="presentation"
322
+ label="View"
323
+ pressed={view === 'view'}
324
+ tone={view === 'view' ? 'panel' : 'quiet'}
325
+ onClick={() => setView('view')}
326
+ />
327
+ <NmToolbarSpacer />
328
+ <NmToolbarButton
329
+ icon={copied ? 'check' : 'copy'}
330
+ label={copied ? 'Copied' : 'Copy'}
331
+ tone="quiet"
332
+ onClick={handleCopy}
333
+ />
334
+ <NmToolbarButton
335
+ icon="x"
336
+ label="Close"
337
+ tone="quiet"
338
+ onClick={handleClose}
339
+ />
340
+ </NmToolbar>
358
341
 
359
- <div className="nm-textarea-panel__body">
360
- {tab === 'code' ? (
342
+ <NmSidePanelBody className="nm-textarea-panel__body">
343
+ {view === 'code' ? (
361
344
  <textarea
362
345
  className="nm-textarea-panel__editor"
363
346
  value={currentValue}
@@ -367,27 +350,12 @@ export const NmTextarea = forwardRef<HTMLTextAreaElement, NmTextareaProps>(
367
350
  onChange={(e) => handlePanelTextChange(e.target.value)}
368
351
  />
369
352
  ) : (
370
- <div className="nm-textarea-panel__preview">
371
- <NmProse>
372
- {renderMarkdown ? renderMarkdown(currentValue) : parseMarkdown(currentValue)}
373
- </NmProse>
374
- </div>
353
+ <NmProse className="nm-textarea-panel__preview">
354
+ {renderMarkdown ? renderMarkdown(currentValue) : parseMarkdown(currentValue)}
355
+ </NmProse>
375
356
  )}
376
- </div>
377
-
378
- <footer className="nm-textarea-panel__footer">
379
- <span>
380
- {lineCount} {lineCount === 1 ? 'line' : 'lines'} · {charCount} characters
381
- </span>
382
- <button
383
- type="button"
384
- className="nm-textarea-panel__done"
385
- onClick={handleClose}
386
- >
387
- Done
388
- </button>
389
- </footer>
390
- </aside>
357
+ </NmSidePanelBody>
358
+ </NmSidePanel>
391
359
  </>,
392
360
  document.body,
393
361
  )