@nordmanai/nif 0.2.4 → 0.2.8

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.
@@ -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
  )
@@ -0,0 +1,3 @@
1
+ /** The edge a drawer slides in from. */
2
+ export const NM_DRAWER_SIDES = ['left', 'right'] as const;
3
+ export type NmDrawerSide = (typeof NM_DRAWER_SIDES)[number];
@@ -0,0 +1,37 @@
1
+ /** Which edge of the view the sidebar sits on. */
2
+ export const NM_SIDEBAR_SIDES = ['left', 'right'] as const;
3
+ export type NmSidebarSide = (typeof NM_SIDEBAR_SIDES)[number];
4
+
5
+ /** `sidebar` is flush with the edge. `floating` is a raised card inside a margin. `inset` sits in a margin and the main area is the raised card. */
6
+ export const NM_SIDEBAR_VARIANTS = ['sidebar', 'floating', 'inset'] as const;
7
+ export type NmSidebarVariant = (typeof NM_SIDEBAR_VARIANTS)[number];
8
+
9
+ /**
10
+ * How the sidebar collapses.
11
+ * `offcanvas` slides out of view; the trigger lives in the main area.
12
+ * `icon` keeps a column of icons, one per row.
13
+ * `toggle` keeps only the toggle button, in the sidebar's own header.
14
+ * `none` never collapses.
15
+ */
16
+ export const NM_SIDEBAR_COLLAPSIBLE = ['offcanvas', 'icon', 'toggle', 'none'] as const;
17
+ export type NmSidebarCollapsible = (typeof NM_SIDEBAR_COLLAPSIBLE)[number];
18
+
19
+ export const NM_SIDEBAR_STATES = ['expanded', 'collapsed'] as const;
20
+ export type NmSidebarState = (typeof NM_SIDEBAR_STATES)[number];
21
+
22
+ /** Row heights of a menu button: the small, default and large control heights. */
23
+ export const NM_SIDEBAR_BUTTON_SIZES = ['sm', 'default', 'lg'] as const;
24
+ export type NmSidebarButtonSize = (typeof NM_SIDEBAR_BUTTON_SIZES)[number];
25
+
26
+ /** `default` is a quiet row that fills on hover. `outline` draws the row's line and raises it. */
27
+ export const NM_SIDEBAR_BUTTON_VARIANTS = ['default', 'outline'] as const;
28
+ export type NmSidebarButtonVariant = (typeof NM_SIDEBAR_BUTTON_VARIANTS)[number];
29
+
30
+ /** The key that toggles the sidebar with the platform modifier: ⌘B on Mac, Ctrl+B elsewhere. */
31
+ export const NM_SIDEBAR_SHORTCUT = 'b';
32
+
33
+ /** How long the pointer may be between the toggle and the peeked sidebar before the peek closes, in ms. */
34
+ export const NM_SIDEBAR_PEEK_LEAVE_MS = 200;
35
+
36
+ /** Below this width the sidebar opens over the page instead of beside it. The same breakpoint as the tokens' phone step. */
37
+ export const NM_SIDEBAR_MOBILE_QUERY = '(max-width: 48rem)';
@@ -205,8 +205,17 @@
205
205
  --nm-width-tile: 9rem;
206
206
  --nm-width-panel-min: 16rem;
207
207
  --nm-width-panel-max: 80%;
208
+ /* The sidebar beside the main area: open, on a phone, and collapsed to icons (a square row inside the group padding). */
209
+ --nm-width-sidebar: 16rem;
210
+ --nm-width-sidebar-mobile: 18rem;
211
+ /* A drawer over the page from an edge. */
212
+ --nm-width-drawer: 20rem;
213
+ --nm-width-sidebar-icon: calc(var(--control-h) + var(--nm-space-8) * 2);
208
214
  --nm-height-workspace-sm: 20rem;
209
215
  --nm-height-workspace-lg: 36rem;
216
+ /* A sidebar shown inside a page. */
217
+ --nm-height-sidebar-sm: 26rem;
218
+ --nm-height-sidebar-lg: 42rem;
210
219
  /* Typography. Line height, tracking and weight come from here. The typeface itself is in assets/tokens.css. */
211
220
  /* Structural dividers and resize handles. Invisible at Contrast 0, and they appear as Contrast rises. */
212
221
  --nm-divider: var(--nm-canvas);
@@ -268,6 +277,7 @@
268
277
  --nm-code-gutter: 3ch;
269
278
  --nm-width-date: 11rem;
270
279
  --nm-width-dialog: 40rem;
280
+ --nm-width-alert-dialog: 26rem;
271
281
  /* A page column, such as a settings page: a share of the view, never narrower or wider than its bounds. */
272
282
  --nm-width-column: 90%;
273
283
  --nm-width-column-min: 20rem;