@apliteni/apliteni-ui 0.45.0 → 0.47.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.45.0",
3
+ "version": "0.47.0",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
package/react/README.md CHANGED
@@ -364,3 +364,46 @@ opens, focus goes to the safe action. It renders an `alertdialog`, named by its
364
364
  and described by its body. Modal also accepts
365
365
  `initialFocusRef` for a mounted, focusable element inside its panel. Without it, Modal
366
366
  uses its normal opening focus.
367
+
368
+ ## Timeline
369
+
370
+ `Timeline` displays a record’s events in the order provided. Pass events from oldest
371
+ to newest, with stable IDs, machine-readable `dateTime` values, and formatted
372
+ `timestamp` text.
373
+ Descriptions should say what changed and to where.
374
+
375
+ ```tsx
376
+ <Timeline aria-label="Record history" events={[
377
+ {
378
+ id: 'created', actor: 'Demo operator', dateTime: '2026-09-01T09:00:00Z',
379
+ timestamp: '1 Sep, 09:00 UTC', description: 'Created the record in Unassigned.',
380
+ meta: 'Batch DEMO-12',
381
+ },
382
+ ]} />
383
+ ```
384
+
385
+ Omit `undo` for read-only events. For a reversible batch, pass
386
+ `undo: { label: 'Undo batch DEMO-12', onUndo }`. The app checks permissions,
387
+ asks for confirmation when the batch affects finalized records, reverses the
388
+ entire batch, and adds a new event. Timeline only calls `onUndo`; it does not
389
+ modify history.
390
+ Import both the kit CSS and React CSS. The Privileged story shows confirmation
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.
@@ -142,3 +142,373 @@
142
142
  display: block;
143
143
  }
144
144
  }
145
+
146
+ /* src/Timeline.css */
147
+ .ui-timeline {
148
+ margin: 0;
149
+ padding: 0;
150
+ list-style: none;
151
+ color: var(--text);
152
+ font-size: var(--text-sm);
153
+ line-height: 1.5;
154
+ }
155
+ .ui-timeline__event {
156
+ position: relative;
157
+ padding-inline-start: var(--space-6);
158
+ padding-block-end: var(--space-6);
159
+ overflow-wrap: anywhere;
160
+ }
161
+ .ui-timeline__event:last-child {
162
+ padding-block-end: 0;
163
+ }
164
+ .ui-timeline__event::before {
165
+ content: "";
166
+ position: absolute;
167
+ inset-inline-start: calc(var(--space-2) / 2);
168
+ inset-block-start: 0.75em;
169
+ inset-block-end: -0.75em;
170
+ width: 1px;
171
+ background: var(--border);
172
+ }
173
+ .ui-timeline__event:last-child::before {
174
+ display: none;
175
+ }
176
+ .ui-timeline__event::after {
177
+ content: "";
178
+ position: absolute;
179
+ inset-inline-start: 0;
180
+ inset-block-start: calc(0.75em - var(--space-2) / 2);
181
+ width: var(--space-2);
182
+ height: var(--space-2);
183
+ border-radius: 50%;
184
+ background: var(--accent);
185
+ }
186
+ .ui-timeline__head,
187
+ .ui-timeline__meta {
188
+ display: flex;
189
+ flex-wrap: wrap;
190
+ align-items: baseline;
191
+ column-gap: var(--space-3);
192
+ row-gap: var(--space-1);
193
+ }
194
+ .ui-timeline__actor {
195
+ font-weight: var(--weight-semibold);
196
+ }
197
+ .ui-timeline__head time,
198
+ .ui-timeline__meta {
199
+ font-size: var(--text-xs);
200
+ }
201
+ .ui-timeline__description {
202
+ margin: var(--space-1) 0;
203
+ }
204
+ .ui-timeline__undo {
205
+ white-space: normal;
206
+ }
207
+ .ui-timeline__undo:hover,
208
+ .ui-timeline__undo:focus-visible {
209
+ color: var(--pink);
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
+ }
@@ -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;
@@ -489,7 +489,44 @@ type TabsProps = {
489
489
  };
490
490
  declare function Tabs({ items, value, onChange, label }: TabsProps): react.JSX.Element;
491
491
 
492
+ type TimelineEvent = {
493
+ id: string;
494
+ actor: string;
495
+ dateTime: string;
496
+ timestamp: string;
497
+ description: string;
498
+ meta?: ReactNode;
499
+ undo?: {
500
+ label: string;
501
+ onUndo: () => void;
502
+ };
503
+ };
504
+ type TimelineProps = Omit<ComponentPropsWithoutRef<'ol'>, 'children' | 'reversed' | 'start'> & {
505
+ events: readonly TimelineEvent[];
506
+ };
507
+ declare function Timeline({ events, className, ...rest }: TimelineProps): react.JSX.Element;
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
+
492
529
  declare const PAGE_SIZES: readonly number[];
493
530
  declare const DEFAULT_PAGE_SIZE: number;
494
531
 
495
- 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, sortTableRows };
532
+ 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, sortTableRows };
@@ -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: [
@@ -1876,6 +1876,156 @@ function Tabs({ items, value, onChange, label }) {
1876
1876
  ] });
1877
1877
  }
1878
1878
 
1879
+ // src/Timeline.tsx
1880
+ import { jsx as jsx21, jsxs as jsxs17 } from "react/jsx-runtime";
1881
+ function Timeline({ events, className, ...rest }) {
1882
+ return /* @__PURE__ */ jsx21("ol", { ...rest, role: "list", className: ["ui-timeline", className].filter(Boolean).join(" "), children: events.map((event) => /* @__PURE__ */ jsxs17("li", { className: "ui-timeline__event", children: [
1883
+ /* @__PURE__ */ jsxs17("div", { className: "ui-timeline__head", children: [
1884
+ /* @__PURE__ */ jsx21("span", { className: "ui-timeline__actor", children: event.actor }),
1885
+ /* @__PURE__ */ jsx21("time", { dateTime: event.dateTime, children: event.timestamp })
1886
+ ] }),
1887
+ /* @__PURE__ */ jsx21("p", { className: "ui-timeline__description", children: event.description }),
1888
+ (event.meta != null || event.undo) && /* @__PURE__ */ jsxs17("div", { className: "ui-timeline__meta", children: [
1889
+ event.meta != null && /* @__PURE__ */ jsx21("span", { children: event.meta }),
1890
+ event.undo && /* @__PURE__ */ jsx21(Button, { variant: "ghost", size: "xs", className: "ui-btn ui-btn--ghost ui-btn--xs ui-timeline__undo", "aria-label": event.undo.label, onClick: event.undo.onUndo, children: "Undo" })
1891
+ ] })
1892
+ ] }, event.id)) });
1893
+ }
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
+
1879
2029
  // src/index.ts
1880
2030
  var PAGE_SIZES = KIT_PAGE_SIZES;
1881
2031
  var DEFAULT_PAGE_SIZE3 = KIT_DEFAULT_PAGE_SIZE;
@@ -1894,6 +2044,7 @@ export {
1894
2044
  Drawer,
1895
2045
  DrawerSection,
1896
2046
  Dropdown,
2047
+ FileField,
1897
2048
  FilterBar,
1898
2049
  Icon,
1899
2050
  KeyValueList,
@@ -1903,10 +2054,14 @@ export {
1903
2054
  Pagination,
1904
2055
  RowIdentity,
1905
2056
  Segmented,
2057
+ SelectField,
1906
2058
  Skeleton,
1907
2059
  SkeletonTable,
1908
2060
  Snippet,
1909
2061
  StatBand,
1910
2062
  Tabs,
2063
+ TextArea,
2064
+ TextField,
2065
+ Timeline,
1911
2066
  sortTableRows
1912
2067
  };
@@ -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