@apliteni/apliteni-ui 0.46.0 → 0.48.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.46.0",
3
+ "version": "0.48.0",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
package/react/README.md CHANGED
@@ -389,3 +389,38 @@ entire batch, and adds a new event. Timeline only calls `onUndo`; it does not
389
389
  modify history.
390
390
  Import both the kit CSS and React CSS. The Privileged story shows confirmation
391
391
  and a reversing event.
392
+
393
+ ## Fields
394
+
395
+ `TextField`, `TextArea`, `SelectField`, and `FileField` pair a label with a native
396
+ control. Each creates its own IDs and links hints or errors automatically. Use
397
+ native control props. `SelectField` accepts option children.
398
+
399
+ ```tsx
400
+ <TextField label="Weight" type="number" unit="kg" required />
401
+ <TextArea label="Notes" hint="Optional" />
402
+ <SelectField label="Currency"><option>EUR</option><option>USD</option></SelectField>
403
+ <FileField label="Attachment" accept=".pdf" hint="PDF, up to 5 MB."
404
+ onFileChange={file => setAttachment(file)} />
405
+ ```
406
+
407
+ `FileField` accepts one file from a picker or a drop and reports it through
408
+ `onFileChange`. Use `accept` and `hint` to describe allowed file types and size.
409
+ Your application must validate files before upload.
410
+
411
+ ## Toast
412
+
413
+ Wrap your app in `Toast`, then call `useToast()` inside it to add a notice.
414
+ Pass `title`, optional `text`, and a `tone`: success (default), danger, warn, info,
415
+ or neutral. Notices disappear after five seconds, pausing while hovered or focused.
416
+ Danger notices are announced assertively; other tones are polite. An `action` with a `label` and
417
+ `onClick` stays until selected or dismissed. New notices appear at the bottom.
418
+
419
+ ```tsx
420
+ function SaveButton() {
421
+ const push = useToast();
422
+ return <Button onClick={() => push({ title: 'Saved', text: 'Your changes were saved.' })}>Save</Button>;
423
+ }
424
+
425
+ <Toast><SaveButton /></Toast>
426
+ ```
@@ -208,3 +208,320 @@
208
208
  .ui-timeline__undo:focus-visible {
209
209
  color: var(--pink);
210
210
  }
211
+
212
+ /* ../src/styles/input.css */
213
+ .ui-field {
214
+ display: block;
215
+ }
216
+ .ui-field__label {
217
+ display: block;
218
+ color: var(--strong);
219
+ font-size: var(--text-base);
220
+ font-weight: var(--weight-medium);
221
+ margin-bottom: 8px;
222
+ }
223
+ .ui-field__req {
224
+ color: var(--pink);
225
+ margin-left: 4px;
226
+ }
227
+ .ui-field__hint {
228
+ color: var(--text);
229
+ font-size: var(--text-sm);
230
+ margin-top: 7px;
231
+ line-height: 1.5;
232
+ }
233
+ .ui-field__error {
234
+ color: var(--pink);
235
+ font-size: var(--text-sm);
236
+ margin-top: 7px;
237
+ display: flex;
238
+ align-items: center;
239
+ gap: 6px;
240
+ }
241
+ .ui-field__error svg {
242
+ width: 16px;
243
+ height: 16px;
244
+ flex: none;
245
+ stroke-width: 2.4;
246
+ }
247
+ .ui-input,
248
+ .ui-textarea,
249
+ .ui-select {
250
+ font-family: var(--font-sans);
251
+ font-size: var(--text-base);
252
+ color: var(--strong);
253
+ background: var(--surface-2);
254
+ border: 1px solid var(--border);
255
+ border-radius: var(--radius-md);
256
+ padding: 12px 15px;
257
+ width: 100%;
258
+ transition: border-color var(--dur-med) var(--ease), box-shadow var(--dur-med) var(--ease);
259
+ }
260
+ .ui-textarea {
261
+ resize: vertical;
262
+ min-height: 96px;
263
+ line-height: 1.55;
264
+ }
265
+ .ui-input:hover,
266
+ .ui-textarea:hover,
267
+ .ui-select:hover {
268
+ border-color: var(--border-strong);
269
+ }
270
+ .ui-input:focus-visible,
271
+ .ui-textarea:focus-visible,
272
+ .ui-select:focus-visible {
273
+ outline: 2px solid transparent;
274
+ border-color: var(--accent);
275
+ box-shadow: var(--ring);
276
+ }
277
+ .ui-input::placeholder,
278
+ .ui-textarea::placeholder {
279
+ color: var(--muted);
280
+ opacity: 0.9;
281
+ }
282
+ .ui-input:disabled,
283
+ .ui-select:disabled,
284
+ .ui-textarea:disabled {
285
+ background: var(--disabled-surface);
286
+ border-color: var(--disabled-border);
287
+ color: var(--disabled-ink);
288
+ cursor: not-allowed;
289
+ }
290
+ .ui-input.is-invalid,
291
+ .ui-select.is-invalid,
292
+ .ui-textarea.is-invalid {
293
+ border-color: var(--pink);
294
+ }
295
+ .ui-input-group {
296
+ position: relative;
297
+ display: flex;
298
+ align-items: center;
299
+ }
300
+ .ui-input-group .ui-input {
301
+ padding-left: 42px;
302
+ }
303
+ .ui-input-group__icon {
304
+ position: absolute;
305
+ left: 14px;
306
+ width: 17px;
307
+ height: 17px;
308
+ color: var(--muted);
309
+ pointer-events: none;
310
+ }
311
+ .ui-input-group__icon svg {
312
+ width: 100%;
313
+ height: 100%;
314
+ stroke: currentColor;
315
+ fill: none;
316
+ stroke-width: 2.2;
317
+ }
318
+ .ui-select {
319
+ appearance: none;
320
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23a29db6' stroke-width='2.4' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
321
+ background-repeat: no-repeat;
322
+ background-position: right 15px center;
323
+ padding-right: 40px;
324
+ cursor: pointer;
325
+ }
326
+ :root[data-theme=light] .ui-select {
327
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%235c6270' stroke-width='2.4' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
328
+ }
329
+ .ui-check {
330
+ display: inline-flex;
331
+ align-items: flex-start;
332
+ gap: 11px;
333
+ cursor: pointer;
334
+ font-size: var(--text-base);
335
+ color: var(--text);
336
+ line-height: 1.5;
337
+ }
338
+ .ui-check input {
339
+ appearance: none;
340
+ width: 19px;
341
+ height: 19px;
342
+ flex: none;
343
+ margin-top: 1px;
344
+ border: 1.5px solid var(--border-strong);
345
+ border-radius: var(--radius-xs);
346
+ background: var(--surface-2);
347
+ cursor: pointer;
348
+ transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
349
+ position: relative;
350
+ }
351
+ .ui-check input::before {
352
+ content: "";
353
+ position: absolute;
354
+ left: 50%;
355
+ top: 50%;
356
+ width: 24px;
357
+ height: 24px;
358
+ transform: translate(-50%, -50%);
359
+ }
360
+ .ui-check input[type=radio] {
361
+ border-radius: 50%;
362
+ }
363
+ .ui-check input:hover {
364
+ border-color: var(--accent);
365
+ }
366
+ .ui-check input:checked {
367
+ background: var(--accent);
368
+ border-color: var(--accent);
369
+ }
370
+ .ui-check input[type=checkbox]:checked::after {
371
+ content: "";
372
+ position: absolute;
373
+ left: 5px;
374
+ top: 1.5px;
375
+ width: 5px;
376
+ height: 10px;
377
+ border: solid var(--accent-contrast);
378
+ border-width: 0 2px 2px 0;
379
+ transform: rotate(45deg);
380
+ }
381
+ .ui-check input[type=radio]:checked::after {
382
+ content: "";
383
+ position: absolute;
384
+ inset: 4px;
385
+ border-radius: 50%;
386
+ background: var(--accent-contrast);
387
+ }
388
+ .ui-check input:focus-visible {
389
+ outline: 2px solid transparent;
390
+ box-shadow: var(--ring);
391
+ }
392
+ .ui-switch {
393
+ --sw-w: 44px;
394
+ --sw-h: 26px;
395
+ position: relative;
396
+ display: inline-block;
397
+ width: var(--sw-w);
398
+ height: var(--sw-h);
399
+ flex: none;
400
+ }
401
+ .ui-switch input {
402
+ position: absolute;
403
+ opacity: 0;
404
+ width: 0;
405
+ height: 0;
406
+ }
407
+ .ui-switch__track {
408
+ position: absolute;
409
+ inset: 0;
410
+ background: var(--surface-3);
411
+ border-radius: var(--radius-pill);
412
+ cursor: pointer;
413
+ transition: background var(--dur-med) var(--ease);
414
+ }
415
+ .ui-switch__track::before {
416
+ content: "";
417
+ position: absolute;
418
+ top: 3px;
419
+ left: 3px;
420
+ width: calc(var(--sw-h) - 6px);
421
+ height: calc(var(--sw-h) - 6px);
422
+ background: var(--control-knob);
423
+ border: 1px solid var(--border-strong);
424
+ box-sizing: border-box;
425
+ border-radius: 50%;
426
+ transition: transform var(--dur-med) var(--ease);
427
+ }
428
+ .ui-switch input:checked + .ui-switch__track {
429
+ background: var(--accent);
430
+ }
431
+ .ui-switch input:checked + .ui-switch__track::before {
432
+ transform: translateX(calc(var(--sw-w) - var(--sw-h)));
433
+ }
434
+ .ui-switch input:focus-visible + .ui-switch__track {
435
+ outline: 2px solid transparent;
436
+ box-shadow: var(--ring);
437
+ }
438
+ .ui-switch input:disabled + .ui-switch__track {
439
+ opacity: 0.5;
440
+ cursor: not-allowed;
441
+ }
442
+
443
+ /* src/Field.css */
444
+ .ui-field-number {
445
+ display: flex;
446
+ align-items: center;
447
+ position: relative;
448
+ }
449
+ .ui-field-number .ui-input {
450
+ padding-right: 4.5em;
451
+ }
452
+ .ui-field-number__unit {
453
+ position: absolute;
454
+ right: 15px;
455
+ color: var(--text);
456
+ pointer-events: none;
457
+ }
458
+ .ui-file {
459
+ display: flex;
460
+ flex-direction: column;
461
+ align-items: center;
462
+ gap: 8px;
463
+ padding: 24px;
464
+ border: 1px dashed var(--border);
465
+ border-radius: var(--radius-md);
466
+ background: var(--surface-2);
467
+ color: var(--text);
468
+ font-size: var(--text-base);
469
+ text-align: center;
470
+ overflow-wrap: anywhere;
471
+ }
472
+ .ui-file:hover {
473
+ border-color: var(--border-strong);
474
+ }
475
+ .ui-file:focus-within,
476
+ .ui-file.is-dragging {
477
+ border-color: var(--accent);
478
+ box-shadow: var(--ring);
479
+ outline: 2px solid transparent;
480
+ }
481
+ .ui-file.is-invalid {
482
+ border-color: var(--pink);
483
+ }
484
+ .ui-file__glyph {
485
+ display: flex;
486
+ padding: 12px;
487
+ border-radius: var(--radius-md);
488
+ background: var(--surface-3);
489
+ }
490
+ .ui-file__glyph svg {
491
+ width: 24px;
492
+ height: 24px;
493
+ }
494
+ .ui-file__choose {
495
+ position: relative;
496
+ display: inline-flex;
497
+ margin-top: 8px;
498
+ }
499
+ .ui-file__input {
500
+ position: absolute;
501
+ inset: 0;
502
+ width: 100%;
503
+ height: 100%;
504
+ opacity: 0;
505
+ cursor: pointer;
506
+ }
507
+ .ui-file.is-disabled {
508
+ background: var(--disabled-surface);
509
+ border-color: var(--disabled-border);
510
+ color: var(--disabled-ink);
511
+ }
512
+ .ui-file.is-disabled .ui-file__input {
513
+ cursor: not-allowed;
514
+ }
515
+
516
+ /* src/Toast.css */
517
+ .rx-toast-stack {
518
+ position: fixed;
519
+ right: var(--space-4);
520
+ bottom: var(--space-4);
521
+ max-width: calc(100% - 2 * var(--space-4));
522
+ z-index: var(--z-toast);
523
+ pointer-events: none;
524
+ }
525
+ .rx-toast-stack > .ui-toast {
526
+ pointer-events: auto;
527
+ }
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { ReactNode, ButtonHTMLAttributes, RefObject, MouseEvent, ElementType, ComponentPropsWithoutRef } from 'react';
2
+ import { ReactNode, ButtonHTMLAttributes, RefObject, MouseEvent, ElementType, ComponentPropsWithoutRef, InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes } from 'react';
3
3
 
4
4
  declare function Icon({ name }: {
5
5
  name: string;
@@ -506,7 +506,42 @@ type TimelineProps = Omit<ComponentPropsWithoutRef<'ol'>, 'children' | 'reversed
506
506
  };
507
507
  declare function Timeline({ events, className, ...rest }: TimelineProps): react.JSX.Element;
508
508
 
509
+ type FieldMessage = {
510
+ label: string;
511
+ hint?: string;
512
+ error?: string;
513
+ };
514
+ type Wiring = 'id' | 'aria-describedby' | 'aria-invalid';
515
+ type TextFieldProps = FieldMessage & Omit<InputHTMLAttributes<HTMLInputElement>, Wiring | 'type'> & {
516
+ type?: 'text' | 'number' | 'email';
517
+ unit?: string;
518
+ };
519
+ declare function TextField({ label, hint, error, required, type, unit, className, ...props }: TextFieldProps): react.JSX.Element;
520
+ type TextAreaProps = FieldMessage & Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, Wiring>;
521
+ declare function TextArea({ label, hint, error, required, className, ...props }: TextAreaProps): react.JSX.Element;
522
+ type SelectFieldProps = FieldMessage & Omit<SelectHTMLAttributes<HTMLSelectElement>, Wiring>;
523
+ declare function SelectField({ label, hint, error, required, className, children, ...props }: SelectFieldProps): react.JSX.Element;
524
+ type FileFieldProps = FieldMessage & Omit<InputHTMLAttributes<HTMLInputElement>, Wiring | 'type' | 'multiple' | 'value' | 'defaultValue' | 'onChange'> & {
525
+ onFileChange?: (file: File | null) => void;
526
+ };
527
+ declare function FileField({ label, hint, error, required, disabled, onFileChange, className, ...props }: FileFieldProps): react.JSX.Element;
528
+
529
+ type ToastNotice = {
530
+ tone?: 'success' | 'danger' | 'warn' | 'info' | 'neutral';
531
+ title: string;
532
+ text?: string;
533
+ action?: {
534
+ label: string;
535
+ onClick: () => void;
536
+ };
537
+ };
538
+ type ToastProps = {
539
+ children: ReactNode;
540
+ };
541
+ declare function useToast(): (notice: ToastNotice) => void;
542
+ declare function Toast({ children }: ToastProps): react.JSX.Element;
543
+
509
544
  declare const PAGE_SIZES: readonly number[];
510
545
  declare const DEFAULT_PAGE_SIZE: number;
511
546
 
512
- export { BackLink, type BackLinkOwnProps, type BackLinkProps, Badge, BusyRegion, type BusyRegionProps, Button, type ButtonProps, Card, type Column, type CommandGroup, type CommandItem, CommandPalette, type CommandPaletteProps, Confirm, type ConfirmProps, DEFAULT_PAGE_SIZE, DataTable, type DataTableProps, DeltaValue, type DeltaValueProps, Denied, type DeniedProps, Drawer, type DrawerProps, DrawerSection, type DrawerSectionProps, Dropdown, type DropdownBadge, type DropdownEntry, type DropdownItem, type DropdownProps, type DropdownRowProps, type DropdownSearch, type DropdownSection, type DropdownSeparator, type Filter, FilterBar, type FilterBarProps, Icon, KeyValueList, type KeyValueListProps, type KeyValueRow, Modal, type ModalProps, NumericValue, type NumericValueProps, PAGE_SIZES, Pagination, type PaginationProps, RowIdentity, type RowIdentityProps, type SegmentOption, Segmented, type SegmentedProps, Skeleton, type SkeletonProps, SkeletonTable, type SkeletonTableProps, Snippet, type SnippetProps, StatBand, type StatBandProps, type StatDelta, type StatFigure, type StatTone, type StatVariant, type TabItem, type TableSort, Tabs, type TabsProps, Timeline, type TimelineEvent, type TimelineProps, sortTableRows };
547
+ export { BackLink, type BackLinkOwnProps, type BackLinkProps, Badge, BusyRegion, type BusyRegionProps, Button, type ButtonProps, Card, type Column, type CommandGroup, type CommandItem, CommandPalette, type CommandPaletteProps, Confirm, type ConfirmProps, DEFAULT_PAGE_SIZE, DataTable, type DataTableProps, DeltaValue, type DeltaValueProps, Denied, type DeniedProps, Drawer, type DrawerProps, DrawerSection, type DrawerSectionProps, Dropdown, type DropdownBadge, type DropdownEntry, type DropdownItem, type DropdownProps, type DropdownRowProps, type DropdownSearch, type DropdownSection, type DropdownSeparator, FileField, type FileFieldProps, type Filter, FilterBar, type FilterBarProps, Icon, KeyValueList, type KeyValueListProps, type KeyValueRow, Modal, type ModalProps, NumericValue, type NumericValueProps, PAGE_SIZES, Pagination, type PaginationProps, RowIdentity, type RowIdentityProps, type SegmentOption, Segmented, type SegmentedProps, SelectField, type SelectFieldProps, Skeleton, type SkeletonProps, SkeletonTable, type SkeletonTableProps, Snippet, type SnippetProps, StatBand, type StatBandProps, type StatDelta, type StatFigure, type StatTone, type StatVariant, type TabItem, type TableSort, Tabs, type TabsProps, TextArea, type TextAreaProps, TextField, type TextFieldProps, Timeline, type TimelineEvent, type TimelineProps, Toast, type ToastNotice, type ToastProps, sortTableRows, useToast };
@@ -51,7 +51,7 @@ var cx = (...a) => a.filter(Boolean).join(" ");
51
51
  var Button = forwardRef(function Button2({
52
52
  variant = "secondary",
53
53
  size = "md",
54
- icon: icon4,
54
+ icon: icon5,
55
55
  iconRight,
56
56
  iconOnly,
57
57
  block,
@@ -107,7 +107,7 @@ var Button = forwardRef(function Button2({
107
107
  iconOnly && "ui-btn--icon"
108
108
  );
109
109
  const labelled = rest["aria-label"] != null || rest["aria-labelledby"] != null;
110
- const fallback = typeof children === "string" && children.trim() ? children.trim() : icon4;
110
+ const fallback = typeof children === "string" && children.trim() ? children.trim() : icon5;
111
111
  const named = iconOnly && !labelled && fallback ? { "aria-label": fallback, title: rest.title ?? fallback } : {};
112
112
  return /* @__PURE__ */ jsxs(
113
113
  "button",
@@ -141,7 +141,7 @@ var Button = forwardRef(function Button2({
141
141
  if (!blockKey(event)) onKeyUp?.(event);
142
142
  },
143
143
  children: [
144
- icon4 && /* @__PURE__ */ jsx2(Icon, { name: icon4 }),
144
+ icon5 && /* @__PURE__ */ jsx2(Icon, { name: icon5 }),
145
145
  !iconOnly && renderedChildren != null && /* @__PURE__ */ jsx2("span", { className: "ui-btn__label-slot", children: /* @__PURE__ */ jsx2("span", { className: "ui-btn__label", ref: labelRef, children: renderedChildren }) }),
146
146
  iconRight && /* @__PURE__ */ jsx2(Icon, { name: iconRight }),
147
147
  busy && /* @__PURE__ */ jsxs("span", { className: "ui-btn__dots", "aria-hidden": "true", children: [
@@ -1200,12 +1200,12 @@ function Denied({
1200
1200
  title = "You don\u2019t have access",
1201
1201
  sub,
1202
1202
  need,
1203
- icon: icon4 = "lock",
1203
+ icon: icon5 = "lock",
1204
1204
  className,
1205
1205
  children
1206
1206
  }) {
1207
1207
  return /* @__PURE__ */ jsxs10("div", { className: cx4("ui-denied", className), children: [
1208
- /* @__PURE__ */ jsx12("div", { className: "ui-denied__seal", children: /* @__PURE__ */ jsx12(Icon, { name: icon4 }) }),
1208
+ /* @__PURE__ */ jsx12("div", { className: "ui-denied__seal", children: /* @__PURE__ */ jsx12(Icon, { name: icon5 }) }),
1209
1209
  /* @__PURE__ */ jsx12("div", { className: "ui-denied__title", children: title }),
1210
1210
  sub && /* @__PURE__ */ jsx12("div", { className: "ui-denied__sub", children: sub }),
1211
1211
  need && /* @__PURE__ */ jsxs10("div", { className: "ui-denied__need", children: [
@@ -1892,6 +1892,253 @@ function Timeline({ events, className, ...rest }) {
1892
1892
  ] }, event.id)) });
1893
1893
  }
1894
1894
 
1895
+ // src/Field.tsx
1896
+ import { useEffect as useEffect7, useId as useId8, useRef as useRef12, useState as useState7 } from "react";
1897
+ import { esc, icon as icon4 } from "@apliteni/apliteni-ui";
1898
+ import { jsx as jsx22, jsxs as jsxs18 } from "react/jsx-runtime";
1899
+ function Frame({ id, label, hint, error, required, children }) {
1900
+ return /* @__PURE__ */ jsxs18("div", { className: "ui-field", children: [
1901
+ /* @__PURE__ */ jsxs18("label", { className: "ui-field__label", htmlFor: id, children: [
1902
+ label,
1903
+ required && /* @__PURE__ */ jsx22("span", { className: "ui-field__req", "aria-hidden": "true", children: "*" })
1904
+ ] }),
1905
+ children,
1906
+ (error || hint) && /* @__PURE__ */ jsx22(
1907
+ "div",
1908
+ {
1909
+ id: `${id}-message`,
1910
+ className: error ? "ui-field__error" : "ui-field__hint",
1911
+ dangerouslySetInnerHTML: { __html: (error ? icon4("alert") : "") + esc(error || hint) }
1912
+ }
1913
+ )
1914
+ ] });
1915
+ }
1916
+ function wiring(id, hint, error) {
1917
+ return { id, "aria-describedby": error || hint ? `${id}-message` : void 0, "aria-invalid": error ? true : void 0 };
1918
+ }
1919
+ function TextField({ label, hint, error, required, type = "text", unit, className = "", ...props }) {
1920
+ const id = useId8();
1921
+ const hasUnit = type === "number" && !!unit;
1922
+ const describedBy = [error || hint ? `${id}-message` : "", hasUnit ? `${id}-unit` : ""].filter(Boolean).join(" ") || void 0;
1923
+ const control = /* @__PURE__ */ jsx22(
1924
+ "input",
1925
+ {
1926
+ ...props,
1927
+ ...wiring(id, hint, error),
1928
+ required,
1929
+ type,
1930
+ "aria-describedby": describedBy,
1931
+ inputMode: type === "number" ? "decimal" : props.inputMode,
1932
+ className: `ui-input${error ? " is-invalid" : ""} ${className}`
1933
+ }
1934
+ );
1935
+ return /* @__PURE__ */ jsx22(Frame, { ...{ id, label, hint, error, required }, children: hasUnit ? /* @__PURE__ */ jsxs18("div", { className: "ui-field-number", children: [
1936
+ control,
1937
+ /* @__PURE__ */ jsx22("span", { id: `${id}-unit`, className: "ui-field-number__unit", children: unit })
1938
+ ] }) : control });
1939
+ }
1940
+ function TextArea({ label, hint, error, required, className = "", ...props }) {
1941
+ const id = useId8();
1942
+ return /* @__PURE__ */ jsx22(Frame, { ...{ id, label, hint, error, required }, children: /* @__PURE__ */ jsx22(
1943
+ "textarea",
1944
+ {
1945
+ rows: 4,
1946
+ ...props,
1947
+ ...wiring(id, hint, error),
1948
+ required,
1949
+ className: `ui-textarea${error ? " is-invalid" : ""} ${className}`
1950
+ }
1951
+ ) });
1952
+ }
1953
+ function SelectField({ label, hint, error, required, className = "", children, ...props }) {
1954
+ const id = useId8();
1955
+ return /* @__PURE__ */ jsx22(Frame, { ...{ id, label, hint, error, required }, children: /* @__PURE__ */ jsx22(
1956
+ "select",
1957
+ {
1958
+ ...props,
1959
+ ...wiring(id, hint, error),
1960
+ required,
1961
+ className: `ui-select${error ? " is-invalid" : ""} ${className}`,
1962
+ children
1963
+ }
1964
+ ) });
1965
+ }
1966
+ function FileField({ label, hint, error, required, disabled, onFileChange, className = "", ...props }) {
1967
+ const id = useId8();
1968
+ const input = useRef12(null);
1969
+ const [file, setFile] = useState7(null);
1970
+ const [dragging, setDragging] = useState7(false);
1971
+ useEffect7(() => {
1972
+ const form = input.current?.form;
1973
+ const reset = () => {
1974
+ setFile(null);
1975
+ setDragging(false);
1976
+ };
1977
+ form?.addEventListener("reset", reset);
1978
+ return () => form?.removeEventListener("reset", reset);
1979
+ }, []);
1980
+ const update = (next) => {
1981
+ setFile(next);
1982
+ onFileChange?.(next);
1983
+ };
1984
+ return /* @__PURE__ */ jsx22(Frame, { ...{ id, label, hint, error, required }, children: /* @__PURE__ */ jsxs18(
1985
+ "div",
1986
+ {
1987
+ className: `ui-file${dragging && !disabled ? " is-dragging" : ""}${file ? " has-file" : ""}${error ? " is-invalid" : ""}${disabled ? " is-disabled" : ""}`,
1988
+ onDragOver: (event) => {
1989
+ event.preventDefault();
1990
+ if (!disabled) setDragging(true);
1991
+ },
1992
+ onDragLeave: (event) => {
1993
+ if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
1994
+ },
1995
+ onDrop: (event) => {
1996
+ event.preventDefault();
1997
+ setDragging(false);
1998
+ if (disabled || !event.dataTransfer.files.length || !input.current) return;
1999
+ const transfer = new DataTransfer();
2000
+ transfer.items.add(event.dataTransfer.files[0]);
2001
+ input.current.files = transfer.files;
2002
+ update(transfer.files[0]);
2003
+ },
2004
+ children: [
2005
+ /* @__PURE__ */ jsx22("span", { className: "ui-file__glyph", children: /* @__PURE__ */ jsx22(Icon, { name: "upload" }) }),
2006
+ /* @__PURE__ */ jsx22("div", { "aria-live": "polite", children: file ? file.name : "Drop a file here" }),
2007
+ /* @__PURE__ */ jsx22("div", { children: "or choose one from your device" }),
2008
+ /* @__PURE__ */ jsxs18("span", { className: "ui-file__choose", children: [
2009
+ /* @__PURE__ */ jsx22("span", { className: "ui-btn ui-btn--secondary ui-btn--sm", "aria-hidden": "true", "aria-disabled": disabled || void 0, children: file ? "Replace file" : "Choose file" }),
2010
+ /* @__PURE__ */ jsx22(
2011
+ "input",
2012
+ {
2013
+ ...props,
2014
+ ...wiring(id, hint, error),
2015
+ ref: input,
2016
+ type: "file",
2017
+ required,
2018
+ disabled,
2019
+ className: `ui-file__input ${className}`,
2020
+ onChange: (event) => update(event.currentTarget.files?.[0] ?? null)
2021
+ }
2022
+ )
2023
+ ] })
2024
+ ]
2025
+ }
2026
+ ) });
2027
+ }
2028
+
2029
+ // src/Toast.tsx
2030
+ import { createContext as createContext2, useCallback as useCallback3, useContext as useContext2, useEffect as useEffect8, useRef as useRef13, useState as useState8 } from "react";
2031
+ import { createPortal as createPortal4 } from "react-dom";
2032
+ import { jsx as jsx23, jsxs as jsxs19 } from "react/jsx-runtime";
2033
+ var Context = createContext2(null);
2034
+ var glyphs = { success: "circleCheck", danger: "circleX", warn: "circleAlert", info: "info", neutral: "bolt" };
2035
+ function useToast() {
2036
+ const push = useContext2(Context);
2037
+ if (!push) throw new Error("useToast requires a Toast provider");
2038
+ return push;
2039
+ }
2040
+ function Notice({ notice, remove }) {
2041
+ const root = useRef13(null);
2042
+ const [leaving2, setLeaving] = useState8(false);
2043
+ const dismissed = useRef13(false);
2044
+ const remaining = useRef13(5e3);
2045
+ const [hovered, setHovered] = useState8(false);
2046
+ const [focused, setFocused] = useState8(false);
2047
+ const paused = hovered || focused;
2048
+ const tone = notice.tone ?? "success";
2049
+ const dismiss = useCallback3(() => {
2050
+ if (dismissed.current) return;
2051
+ dismissed.current = true;
2052
+ if (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches) remove();
2053
+ else setLeaving(true);
2054
+ }, [remove]);
2055
+ useEffect8(() => {
2056
+ if (leaving2) {
2057
+ const element = root.current;
2058
+ const finish = (event) => {
2059
+ if (event.target === element) remove();
2060
+ };
2061
+ element?.addEventListener("animationend", finish);
2062
+ const timer = setTimeout(remove, 260);
2063
+ return () => {
2064
+ clearTimeout(timer);
2065
+ element?.removeEventListener("animationend", finish);
2066
+ };
2067
+ }
2068
+ }, [leaving2, remove]);
2069
+ useEffect8(() => {
2070
+ if (leaving2 || notice.action || paused) return;
2071
+ const started = Date.now();
2072
+ const timer = setTimeout(dismiss, remaining.current);
2073
+ return () => {
2074
+ clearTimeout(timer);
2075
+ remaining.current = Math.max(0, remaining.current - (Date.now() - started));
2076
+ };
2077
+ }, [dismiss, leaving2, notice.action, paused]);
2078
+ return /* @__PURE__ */ jsxs19(
2079
+ "div",
2080
+ {
2081
+ ref: root,
2082
+ className: `ui-toast ui-toast--${tone} ui-toast--soft${leaving2 ? " is-leaving" : ""}`,
2083
+ role: tone === "danger" ? "alert" : "status",
2084
+ "aria-live": tone === "danger" ? "assertive" : "polite",
2085
+ onMouseEnter: () => setHovered(true),
2086
+ onMouseLeave: () => setHovered(false),
2087
+ onFocus: () => setFocused(true),
2088
+ onBlur: (event) => {
2089
+ if (!event.currentTarget.contains(event.relatedTarget)) setFocused(false);
2090
+ },
2091
+ children: [
2092
+ /* @__PURE__ */ jsx23("span", { className: "ui-toast__icon", children: /* @__PURE__ */ jsx23(Icon, { name: glyphs[tone] }) }),
2093
+ /* @__PURE__ */ jsxs19("div", { className: "ui-toast__body", children: [
2094
+ /* @__PURE__ */ jsx23("div", { className: "ui-toast__title", children: notice.title }),
2095
+ notice.text && /* @__PURE__ */ jsx23("div", { className: "ui-toast__text", children: notice.text })
2096
+ ] }),
2097
+ notice.action && /* @__PURE__ */ jsx23(
2098
+ "button",
2099
+ {
2100
+ type: "button",
2101
+ className: "ui-toast__action",
2102
+ disabled: leaving2,
2103
+ onClick: () => {
2104
+ if (!dismissed.current) {
2105
+ dismiss();
2106
+ notice.action.onClick();
2107
+ }
2108
+ },
2109
+ children: notice.action.label
2110
+ }
2111
+ ),
2112
+ /* @__PURE__ */ jsx23("button", { type: "button", className: "ui-toast__close", "aria-label": "Dismiss", disabled: leaving2, onClick: dismiss, children: /* @__PURE__ */ jsx23(Icon, { name: "x" }) }),
2113
+ !notice.action && /* @__PURE__ */ jsx23(
2114
+ "span",
2115
+ {
2116
+ className: "ui-toast__timer is-running",
2117
+ "aria-hidden": "true",
2118
+ style: { animationPlayState: paused ? "paused" : "running" }
2119
+ }
2120
+ )
2121
+ ]
2122
+ }
2123
+ );
2124
+ }
2125
+ function Toast({ children }) {
2126
+ const [entries, setEntries] = useState8([]);
2127
+ const nextId = useRef13(0);
2128
+ const push = useCallback3((notice) => {
2129
+ const id = nextId.current++;
2130
+ const remove = () => setEntries((current) => current.filter((entry) => entry.id !== id));
2131
+ setEntries((current) => [...current, { id, notice, remove }]);
2132
+ }, []);
2133
+ return /* @__PURE__ */ jsxs19(Context.Provider, { value: push, children: [
2134
+ children,
2135
+ typeof document !== "undefined" && createPortal4(
2136
+ /* @__PURE__ */ jsx23("div", { className: "ui-toast-stack rx-toast-stack", children: entries.map((entry) => /* @__PURE__ */ jsx23(Notice, { notice: entry.notice, remove: entry.remove }, entry.id)) }),
2137
+ document.body
2138
+ )
2139
+ ] });
2140
+ }
2141
+
1895
2142
  // src/index.ts
1896
2143
  var PAGE_SIZES = KIT_PAGE_SIZES;
1897
2144
  var DEFAULT_PAGE_SIZE3 = KIT_DEFAULT_PAGE_SIZE;
@@ -1910,6 +2157,7 @@ export {
1910
2157
  Drawer,
1911
2158
  DrawerSection,
1912
2159
  Dropdown,
2160
+ FileField,
1913
2161
  FilterBar,
1914
2162
  Icon,
1915
2163
  KeyValueList,
@@ -1919,11 +2167,16 @@ export {
1919
2167
  Pagination,
1920
2168
  RowIdentity,
1921
2169
  Segmented,
2170
+ SelectField,
1922
2171
  Skeleton,
1923
2172
  SkeletonTable,
1924
2173
  Snippet,
1925
2174
  StatBand,
1926
2175
  Tabs,
2176
+ TextArea,
2177
+ TextField,
1927
2178
  Timeline,
1928
- sortTableRows
2179
+ Toast,
2180
+ sortTableRows,
2181
+ useToast
1929
2182
  };
@@ -63,7 +63,7 @@
63
63
  cursor: not-allowed;
64
64
  }
65
65
 
66
- .ui-input.is-invalid,
66
+ .ui-input.is-invalid, .ui-select.is-invalid,
67
67
  .ui-textarea.is-invalid { border-color: var(--pink); }
68
68
  /* Invalid fields keep the shared focus indicator; their border carries the error. */
69
69