@apliteni/apliteni-ui 0.77.0 → 0.78.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.
@@ -0,0 +1,87 @@
1
+ # File drop
2
+
3
+ ## Spend one row at rest
4
+
5
+ <!-- rule: one-row -->
6
+
7
+ **Rule:** At rest, a file drop is one row: the button that opens the picker and the accepted types beside it.
8
+
9
+ **Why:** A tall box holds the page's best space open for a file that arrives once a month.
10
+
11
+ **Do:** Put an Upload button and “PDF or CSV, up to 10 MB” on one line above the list they feed.
12
+
13
+ **Don't:** Draw a tall dashed box with a glyph, an invitation and a button stacked inside it.
14
+
15
+ **Except:** A page whose whole purpose is receiving one file may use the field-sized picker, which carries a label of its own.
16
+
17
+ ## Paint the target only while a file is over it
18
+
19
+ <!-- rule: on-drag -->
20
+
21
+ **Rule:** Show the drop target while a file is over the region that accepts it, and nowhere else.
22
+
23
+ **Why:** A target drawn before there is a file to catch covers content the reader still needs.
24
+
25
+ **Do:** Cover the panel that accepts the file the moment the file crosses it.
26
+
27
+ **Don't:** Cover the whole window when one panel accepts the file.
28
+
29
+ ## Keep the file in the row it arrived in
30
+
31
+ <!-- rule: in-the-row -->
32
+
33
+ **Rule:** After the drop, the file's name, size and progress take the place the button was in, with remove on the name's line.
34
+
35
+ **Why:** A row that changes in place keeps the page still while the file uploads.
36
+
37
+ **Do:** Give the name a line with remove at its end, put the size under it, and run the progress track the full width below both.
38
+
39
+ **Don't:** Push the list down with a progress card, or open a dialog over the page.
40
+
41
+ ## Fail in place
42
+
43
+ <!-- rule: failure -->
44
+
45
+ **Rule:** A refused file keeps its place, says what it did wrong under its name, and offers Retry beside Remove.
46
+
47
+ **Why:** A row that drops the file sends the reader back to choosing one. The reason is the one string here that has to be read in full, so it gets a line rather than a share of one.
48
+
49
+ **Do:** Keep the name on its line and say “Larger than 10 MB” under it, with Retry.
50
+
51
+ **Don't:** Replace the row with “Upload failed”, losing both the file and the retry.
52
+
53
+ ## Offer a button, not only a drag
54
+
55
+ <!-- rule: button-path -->
56
+
57
+ **Rule:** Give the reader a button that opens the picker, and let dragging be the shortcut.
58
+
59
+ **Why:** A keyboard cannot drag a file.
60
+
61
+ **Do:** Make the Upload button the control, reachable by keyboard and showing the focus ring.
62
+
63
+ **Don't:** Write “Drag a file here” with nothing to press.
64
+
65
+ ## State the limits once
66
+
67
+ <!-- rule: limits-once -->
68
+
69
+ **Rule:** Name the accepted types and the size limit once, beside the button, in the product's own words.
70
+
71
+ **Why:** Repeated limits teach the reader to skip them.
72
+
73
+ **Do:** Write “PDF or CSV, up to 10 MB” beside the button, and let a failure say what this file did wrong.
74
+
75
+ **Don't:** Repeat the limits under the button, in a tooltip and again in the error.
76
+
77
+ ## Choose the row, the region or the dialog
78
+
79
+ <!-- rule: which-shape -->
80
+
81
+ **Rule:** Use a row where the file joins a list on the page, a region when one panel owns the file, and a dialog only when the upload needs answers of its own.
82
+
83
+ **Why:** A dialog that asks nothing is one more thing to dismiss. The rule picks between three containers, so a drawing would show one of them and not the choice.
84
+
85
+ **Do:** Take a statement into the row under the statements it joins, and a replacement logo into the panel that shows it.
86
+
87
+ **Don't:** Open a dialog for one file that the row behind it could have taken.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.77.0",
3
+ "version": "0.78.0",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
package/react/README.md CHANGED
@@ -493,6 +493,34 @@ places a decorative kit glyph with `icon` — the same glyph names `Button` and
493
493
  `onFileChange`. Use `accept` and `hint` to describe allowed file types and size.
494
494
  Your application must validate files before upload.
495
495
 
496
+ ## File drop
497
+
498
+ `FileDrop` is the compact drop: one row with the button that opens the picker and
499
+ the accepted types beside it, and a drop target painted only while a file is over
500
+ the region. Use it where a file joins a list on the page; use `FileField` where a
501
+ labelled, field-sized picker belongs. The guideline page states when to use a row,
502
+ a region or a dialog.
503
+
504
+ ```tsx
505
+ <FileDrop note="PDF or CSV, up to 10 MB" accept=".pdf,.csv"
506
+ file={upload} onFile={send} onRemove={clear} onRetry={send} />
507
+ ```
508
+
509
+ Your application owns the upload and supplies the `file` it is holding: the
510
+ name, the `size` already written for a reader, `progress` while it uploads, and
511
+ the `error` when it failed. A file with no `status` is uploading, so a file
512
+ handed over before the request starts never reads as uploaded. A status the row
513
+ draws shows a mark and a word; where you give `progress` the track says it
514
+ instead, and the word goes to the track's accessible name. The file reads as a
515
+ stack: the name on top with the actions at its end, the size — or the failure
516
+ message — under it, and the track the full width below. No tier takes a second
517
+ line. The name truncates its stem and keeps its extension, with the whole name in
518
+ a `title`, and nothing is hidden or moved at any width. `Remove` is the icon-only
519
+ `x`, named by `removeLabel`; `Retry` keeps its word. Both appear only when you
520
+ handle them. Children render above the row, inside the region the target covers;
521
+ pass `dragging` to drive that target from a parent. `accept` filters the system
522
+ picker only — validate type and size yourself.
523
+
496
524
  Use `Field` when an existing labelled control needs the kit frame. Spread its
497
525
  render-prop attributes onto one labelable control, and apply the existing control
498
526
  class and invalid styling as appropriate:
@@ -82,6 +82,14 @@
82
82
  .ui-table__act .ui-btn + .ui-btn {
83
83
  margin-inline-start: var(--tap-gap) !important;
84
84
  }
85
+ .ui-drop__row {
86
+ --tap-clear-y: var(--tap-gap);
87
+ }
88
+ .ui-drop__actions {
89
+ gap: var(--tap-gap) !important;
90
+ --tap-clear-x: var(--tap-gap);
91
+ --tap-clear-y: var(--tap-gap);
92
+ }
85
93
  .ui-table__act {
86
94
  --tap-clear-x: var(--tap-gap);
87
95
  --tap-clear-y: 0px;
@@ -498,6 +506,129 @@
498
506
  cursor: not-allowed;
499
507
  }
500
508
 
509
+ /* ../src/styles/file-drop.css */
510
+ .ui-drop {
511
+ position: relative;
512
+ }
513
+ .ui-drop > * + .ui-drop__row {
514
+ margin-top: var(--space-3);
515
+ }
516
+ .ui-drop__row {
517
+ display: flex;
518
+ flex-wrap: wrap;
519
+ align-items: center;
520
+ min-height: var(--space-8);
521
+ gap: var(--space-2) var(--space-3);
522
+ font-size: var(--text-sm);
523
+ color: var(--text);
524
+ overflow-wrap: anywhere;
525
+ }
526
+ .ui-drop__note {
527
+ margin: 0;
528
+ }
529
+ .ui-drop__file {
530
+ display: grid;
531
+ flex: 1 1 auto;
532
+ gap: var(--space-2);
533
+ min-width: 0;
534
+ }
535
+ .ui-drop__line {
536
+ display: flex;
537
+ flex-wrap: nowrap;
538
+ align-items: center;
539
+ gap: var(--space-3);
540
+ min-width: 0;
541
+ }
542
+ .ui-drop__name {
543
+ display: flex;
544
+ flex: 1 1 auto;
545
+ min-width: var(--space-10);
546
+ color: var(--strong);
547
+ font-weight: var(--weight-medium);
548
+ }
549
+ .ui-drop__stem {
550
+ overflow: hidden;
551
+ text-overflow: ellipsis;
552
+ white-space: nowrap;
553
+ }
554
+ .ui-drop__ext {
555
+ flex: none;
556
+ }
557
+ .ui-drop__facts {
558
+ display: flex;
559
+ flex-wrap: nowrap;
560
+ align-items: center;
561
+ gap: var(--space-3);
562
+ min-width: 0;
563
+ }
564
+ .ui-drop__size {
565
+ flex: none;
566
+ white-space: nowrap;
567
+ }
568
+ .ui-drop__word {
569
+ overflow: hidden;
570
+ text-overflow: ellipsis;
571
+ white-space: nowrap;
572
+ flex: 0 1 auto;
573
+ min-width: 0;
574
+ }
575
+ .ui-drop__state {
576
+ display: inline-flex;
577
+ flex: 0 1 auto;
578
+ align-items: center;
579
+ gap: var(--space-2);
580
+ min-width: 0;
581
+ }
582
+ .ui-drop__state svg {
583
+ width: 16px;
584
+ height: 16px;
585
+ flex: none;
586
+ stroke-width: 2.4;
587
+ }
588
+ .ui-drop__error {
589
+ color: var(--pink);
590
+ }
591
+ .ui-drop__actions {
592
+ display: flex;
593
+ flex: none;
594
+ align-items: center;
595
+ gap: var(--space-2);
596
+ margin-left: auto;
597
+ }
598
+ .ui-drop__bar {
599
+ display: block;
600
+ width: 100%;
601
+ height: 6px;
602
+ border-radius: var(--radius-pill);
603
+ background: var(--surface-2);
604
+ overflow: hidden;
605
+ }
606
+ .ui-drop__bar span {
607
+ display: block;
608
+ height: 100%;
609
+ background: var(--accent);
610
+ }
611
+ .ui-drop__target {
612
+ position: absolute;
613
+ inset: 0;
614
+ display: flex;
615
+ align-items: center;
616
+ justify-content: center;
617
+ padding: var(--space-3);
618
+ border: 1px dashed var(--accent);
619
+ border-radius: var(--radius-md);
620
+ background: var(--surface);
621
+ color: var(--strong);
622
+ font-size: var(--text-sm);
623
+ text-align: center;
624
+ }
625
+ .ui-drop.is-disabled .ui-drop__row {
626
+ color: var(--disabled-ink);
627
+ }
628
+ .ui-drop.is-disabled .ui-drop__name {
629
+ color: var(--disabled-ink);
630
+ }
631
+
501
632
  /* src/Toast.css */
502
633
  .rx-toast-stack {
503
634
  position: fixed;
@@ -607,6 +607,47 @@ type FileFieldProps = FieldMessage & Omit<InputHTMLAttributes<HTMLInputElement>,
607
607
  };
608
608
  declare function FileField({ label, hint, error, required, disabled, onFileChange, className, ...props }: FileFieldProps): react.JSX.Element;
609
609
 
610
+ type FileDropStatus = 'uploading' | 'done' | 'error';
611
+ /** The file the consumer is uploading. */
612
+ type FileDropFile = {
613
+ name: string;
614
+ /** Already written for a reader; the kit picks no unit. */
615
+ size?: string;
616
+ /** Left out, the file is uploading: the kit never reports a success nobody claimed. */
617
+ status?: FileDropStatus;
618
+ /** 0 to 100. Left out, the word stands without a bar. */
619
+ progress?: number;
620
+ /** What this file did wrong, when the status is an error. */
621
+ error?: string;
622
+ /** Replaces the word the status carries. */
623
+ state?: string;
624
+ };
625
+ type FileDropProps = {
626
+ /** The button that opens the picker. */
627
+ label?: string;
628
+ /** The accepted types and the size limit, stated once beside the button. */
629
+ note?: string;
630
+ /** Filters the system picker only; the consumer still validates the file. */
631
+ accept?: string;
632
+ /** The words on the drop target. */
633
+ dropLabel?: string;
634
+ /** Names the icon-only remove button; it carries no visible word. */
635
+ removeLabel?: string;
636
+ retryLabel?: string;
637
+ /** The file the consumer is holding. Left out, the row is at rest. */
638
+ file?: FileDropFile | null;
639
+ /** Drives the target from a parent that owns the drag region. */
640
+ dragging?: boolean;
641
+ disabled?: boolean;
642
+ onFile?: (file: File) => void;
643
+ onRemove?: () => void;
644
+ onRetry?: () => void;
645
+ /** What the region accepts the file for; the target covers it while dragging. */
646
+ children?: ReactNode;
647
+ className?: string;
648
+ };
649
+ declare function FileDrop({ label, note, accept, dropLabel, removeLabel, retryLabel, file, dragging, disabled, onFile, onRemove, onRetry, children, className, }: FileDropProps): react.JSX.Element;
650
+
610
651
  type SearchFieldProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'aria-label'> & {
611
652
  /** The control's name. A toolbar search box shows no label, so this is the
612
653
  only name it has. */
@@ -827,4 +868,4 @@ declare const SidebarNav: react.ForwardRefExoticComponent<Omit<Omit<react.Detail
827
868
  declare const PAGE_SIZES: readonly number[];
828
869
  declare const DEFAULT_PAGE_SIZE: number;
829
870
 
830
- export { AppShell, type AppShellProps, type AppShellSection, BackLink, type BackLinkOwnProps, type BackLinkProps, Badge, BusyRegion, type BusyRegionProps, Button, type ButtonLinkProps, type ButtonProps, Callout, type CalloutProps, type CalloutVariant, Card, Checkbox, type CheckboxProps, 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, EmptyState, type EmptyStateProps, type EmptyStateVariant, Field, type FieldControlProps, type FieldProps, FileField, type FileFieldProps, type Filter, FilterBar, type FilterBarProps, Icon, KeyValueList, type KeyValueListProps, type KeyValueRow, Modal, type ModalProps, type NativeButtonProps, NumericValue, type NumericValueProps, PAGE_SIZES, Pagination, type PaginationProps, Pill, type PillProps, RowIdentity, type RowIdentityProps, SearchField, type SearchFieldProps, type SegmentOption, Segmented, type SegmentedProps, SelectField, type SelectFieldProps, SidebarNav, type SidebarNavBadge, type SidebarNavItem, type SidebarNavLeaf, type SidebarNavProps, type SidebarNavSection, Skeleton, type SkeletonProps, SkeletonTable, type SkeletonTableProps, Snippet, type SnippetProps, StatBand, type StatBandProps, type StatDelta, type StatFigure, type StatTone, type StatVariant, StatusDot, type StatusDotProps, Success, SuccessCheck, type SuccessCheckProps, type SuccessCountdown, SuccessPanel, type SuccessPanelProps, type SuccessProps, Switch, type SwitchProps, THEME_INIT_SCRIPT, type TabItem, type TableSort, Tabs, type TabsProps, TextArea, type TextAreaProps, TextField, type TextFieldProps, ThemeToggle, type ThemeToggleProps, Timeline, type TimelineEvent, type TimelineEventKind, type TimelineProps, Toast, type ToastNotice, type ToastProps, Tooltip, type TooltipProps, sortTableRows, useToast };
871
+ export { AppShell, type AppShellProps, type AppShellSection, BackLink, type BackLinkOwnProps, type BackLinkProps, Badge, BusyRegion, type BusyRegionProps, Button, type ButtonLinkProps, type ButtonProps, Callout, type CalloutProps, type CalloutVariant, Card, Checkbox, type CheckboxProps, 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, EmptyState, type EmptyStateProps, type EmptyStateVariant, Field, type FieldControlProps, type FieldProps, FileDrop, type FileDropFile, type FileDropProps, type FileDropStatus, FileField, type FileFieldProps, type Filter, FilterBar, type FilterBarProps, Icon, KeyValueList, type KeyValueListProps, type KeyValueRow, Modal, type ModalProps, type NativeButtonProps, NumericValue, type NumericValueProps, PAGE_SIZES, Pagination, type PaginationProps, Pill, type PillProps, RowIdentity, type RowIdentityProps, SearchField, type SearchFieldProps, type SegmentOption, Segmented, type SegmentedProps, SelectField, type SelectFieldProps, SidebarNav, type SidebarNavBadge, type SidebarNavItem, type SidebarNavLeaf, type SidebarNavProps, type SidebarNavSection, Skeleton, type SkeletonProps, SkeletonTable, type SkeletonTableProps, Snippet, type SnippetProps, StatBand, type StatBandProps, type StatDelta, type StatFigure, type StatTone, type StatVariant, StatusDot, type StatusDotProps, Success, SuccessCheck, type SuccessCheckProps, type SuccessCountdown, SuccessPanel, type SuccessPanelProps, type SuccessProps, Switch, type SwitchProps, THEME_INIT_SCRIPT, type TabItem, type TableSort, Tabs, type TabsProps, TextArea, type TextAreaProps, TextField, type TextFieldProps, ThemeToggle, type ThemeToggleProps, Timeline, type TimelineEvent, type TimelineEventKind, type TimelineProps, Toast, type ToastNotice, type ToastProps, Tooltip, type TooltipProps, sortTableRows, useToast };
@@ -2372,14 +2372,165 @@ function FileField({ label, hint, error, required, disabled, onFileChange, class
2372
2372
  ) });
2373
2373
  }
2374
2374
 
2375
+ // src/FileDrop.tsx
2376
+ import { useRef as useRef16, useState as useState10 } from "react";
2377
+ import { Fragment as Fragment8, jsx as jsx29, jsxs as jsxs24 } from "react/jsx-runtime";
2378
+ var MARKS = {
2379
+ uploading: "clock",
2380
+ done: "circleCheck",
2381
+ error: "circleAlert"
2382
+ };
2383
+ var WORDS = {
2384
+ uploading: "Uploading",
2385
+ done: "Uploaded",
2386
+ error: "Upload failed"
2387
+ };
2388
+ var splitName = (name) => {
2389
+ const cut = name.lastIndexOf(".");
2390
+ return cut > 0 ? [name.slice(0, cut), name.slice(cut)] : [name, ""];
2391
+ };
2392
+ var carriesFiles = (event) => Array.from(event.dataTransfer.types).includes("Files");
2393
+ var cx6 = (...a) => a.filter(Boolean).join(" ");
2394
+ function FileDrop({
2395
+ label = "Upload",
2396
+ note,
2397
+ accept,
2398
+ dropLabel = "Drop to upload",
2399
+ removeLabel = "Remove file",
2400
+ retryLabel = "Retry",
2401
+ file,
2402
+ dragging,
2403
+ disabled,
2404
+ onFile,
2405
+ onRemove,
2406
+ onRetry,
2407
+ children,
2408
+ className
2409
+ }) {
2410
+ const input = useRef16(null);
2411
+ const [over, setOver] = useState10(false);
2412
+ const status = file?.status ?? "uploading";
2413
+ const word = status === "error" ? file?.error ?? WORDS.error : file?.state ?? WORDS[status];
2414
+ const [stem, ext] = splitName(file?.name ?? "");
2415
+ const hasBar = status === "uploading" && file?.progress !== void 0;
2416
+ const showWord = !hasBar;
2417
+ const showSize = Boolean(file?.size) && status !== "error";
2418
+ const showTarget = (dragging ?? over) && !disabled;
2419
+ const take = (chosen) => {
2420
+ if (chosen && !disabled) onFile?.(chosen);
2421
+ };
2422
+ return /* @__PURE__ */ jsxs24(
2423
+ "div",
2424
+ {
2425
+ className: cx6("ui-drop", disabled && "is-disabled", className),
2426
+ onDragOver: (event) => {
2427
+ if (disabled || !carriesFiles(event)) return;
2428
+ event.preventDefault();
2429
+ setOver(true);
2430
+ },
2431
+ onDragLeave: (event) => {
2432
+ if (!event.currentTarget.contains(event.relatedTarget)) setOver(false);
2433
+ },
2434
+ onDrop: (event) => {
2435
+ if (disabled || !carriesFiles(event)) return;
2436
+ event.preventDefault();
2437
+ setOver(false);
2438
+ take(event.dataTransfer.files[0]);
2439
+ },
2440
+ children: [
2441
+ children,
2442
+ /* @__PURE__ */ jsxs24("div", { className: "ui-drop__row", children: [
2443
+ file ? /* @__PURE__ */ jsxs24("div", { className: cx6("ui-drop__file", status === "error" && "ui-drop__file--failed"), children: [
2444
+ /* @__PURE__ */ jsxs24("div", { className: "ui-drop__line", children: [
2445
+ /* @__PURE__ */ jsxs24("span", { className: "ui-drop__name", title: file.name, children: [
2446
+ /* @__PURE__ */ jsx29("span", { className: "ui-drop__stem", children: stem }),
2447
+ /* @__PURE__ */ jsx29("span", { className: "ui-drop__ext", children: ext })
2448
+ ] }),
2449
+ (onRemove || status === "error" && onRetry) && /* @__PURE__ */ jsxs24("span", { className: "ui-drop__actions", children: [
2450
+ status === "error" && onRetry && /* @__PURE__ */ jsx29(Button, { size: "sm", icon: "refresh", onClick: onRetry, disabled, children: retryLabel }),
2451
+ onRemove && /* @__PURE__ */ jsx29(
2452
+ Button,
2453
+ {
2454
+ variant: "ghost",
2455
+ size: "sm",
2456
+ icon: "x",
2457
+ iconOnly: true,
2458
+ "aria-label": removeLabel,
2459
+ onClick: onRemove,
2460
+ disabled
2461
+ }
2462
+ )
2463
+ ] })
2464
+ ] }),
2465
+ (showSize || showWord) && /* @__PURE__ */ jsxs24(
2466
+ "div",
2467
+ {
2468
+ className: cx6("ui-drop__facts", status === "error" && "ui-drop__error"),
2469
+ role: status === "error" ? "alert" : void 0,
2470
+ children: [
2471
+ showSize && /* @__PURE__ */ jsx29("span", { className: "ui-drop__size", children: file.size }),
2472
+ showWord && /* @__PURE__ */ jsxs24("span", { className: "ui-drop__state", children: [
2473
+ /* @__PURE__ */ jsx29(Icon, { name: MARKS[status] }),
2474
+ /* @__PURE__ */ jsx29("span", { className: "ui-drop__word", title: word, children: word })
2475
+ ] })
2476
+ ]
2477
+ }
2478
+ ),
2479
+ hasBar && /* @__PURE__ */ jsx29(
2480
+ "span",
2481
+ {
2482
+ className: "ui-drop__bar",
2483
+ role: "progressbar",
2484
+ "aria-label": `${word} ${file.name}`,
2485
+ "aria-valuenow": file.progress,
2486
+ "aria-valuemin": 0,
2487
+ "aria-valuemax": 100,
2488
+ children: /* @__PURE__ */ jsx29("span", { style: { width: `${file.progress}%` } })
2489
+ }
2490
+ )
2491
+ ] }) : /* @__PURE__ */ jsxs24(Fragment8, { children: [
2492
+ /* @__PURE__ */ jsx29(
2493
+ Button,
2494
+ {
2495
+ size: "sm",
2496
+ icon: "upload",
2497
+ disabled,
2498
+ onClick: () => input.current?.click(),
2499
+ children: label
2500
+ }
2501
+ ),
2502
+ note && /* @__PURE__ */ jsx29("p", { className: "ui-drop__note", children: note })
2503
+ ] }),
2504
+ /* @__PURE__ */ jsx29(
2505
+ "input",
2506
+ {
2507
+ ref: input,
2508
+ type: "file",
2509
+ accept,
2510
+ disabled,
2511
+ hidden: true,
2512
+ tabIndex: -1,
2513
+ onChange: (event) => {
2514
+ take(event.currentTarget.files?.[0]);
2515
+ event.currentTarget.value = "";
2516
+ }
2517
+ }
2518
+ )
2519
+ ] }),
2520
+ showTarget && /* @__PURE__ */ jsx29("div", { className: "ui-drop__target", "aria-hidden": "true", children: dropLabel })
2521
+ ]
2522
+ }
2523
+ );
2524
+ }
2525
+
2375
2526
  // src/SearchField.tsx
2376
2527
  import { forwardRef as forwardRef7 } from "react";
2377
2528
  import { icon as icon5 } from "@apliteni/apliteni-ui";
2378
- import { jsx as jsx29, jsxs as jsxs24 } from "react/jsx-runtime";
2529
+ import { jsx as jsx30, jsxs as jsxs25 } from "react/jsx-runtime";
2379
2530
  var SearchField = forwardRef7(function SearchField2({ ariaLabel, className, ...props }, ref) {
2380
- return /* @__PURE__ */ jsxs24("div", { className: "ui-input-group", children: [
2381
- /* @__PURE__ */ jsx29("span", { className: "ui-input-group__icon", dangerouslySetInnerHTML: { __html: icon5("search") } }),
2382
- /* @__PURE__ */ jsx29(
2531
+ return /* @__PURE__ */ jsxs25("div", { className: "ui-input-group", children: [
2532
+ /* @__PURE__ */ jsx30("span", { className: "ui-input-group__icon", dangerouslySetInnerHTML: { __html: icon5("search") } }),
2533
+ /* @__PURE__ */ jsx30(
2383
2534
  "input",
2384
2535
  {
2385
2536
  ref,
@@ -2393,9 +2544,9 @@ var SearchField = forwardRef7(function SearchField2({ ariaLabel, className, ...p
2393
2544
  });
2394
2545
 
2395
2546
  // src/Toast.tsx
2396
- import { createContext as createContext2, useCallback as useCallback3, useContext as useContext2, useEffect as useEffect11, useLayoutEffect as useLayoutEffect4, useRef as useRef16, useState as useState10 } from "react";
2547
+ import { createContext as createContext2, useCallback as useCallback3, useContext as useContext2, useEffect as useEffect11, useLayoutEffect as useLayoutEffect4, useRef as useRef17, useState as useState11 } from "react";
2397
2548
  import { createPortal as createPortal4 } from "react-dom";
2398
- import { jsx as jsx30, jsxs as jsxs25 } from "react/jsx-runtime";
2549
+ import { jsx as jsx31, jsxs as jsxs26 } from "react/jsx-runtime";
2399
2550
  var Context = createContext2(null);
2400
2551
  var glyphs = { success: "circleCheck", danger: "circleX", warn: "circleAlert", info: "info", neutral: "bolt" };
2401
2552
  function useToast() {
@@ -2404,12 +2555,12 @@ function useToast() {
2404
2555
  return push;
2405
2556
  }
2406
2557
  function Notice({ notice, remove }) {
2407
- const root = useRef16(null);
2408
- const [leaving2, setLeaving] = useState10(false);
2409
- const dismissed = useRef16(false);
2410
- const remaining = useRef16(5e3);
2411
- const [hovered, setHovered] = useState10(false);
2412
- const [focused, setFocused] = useState10(false);
2558
+ const root = useRef17(null);
2559
+ const [leaving2, setLeaving] = useState11(false);
2560
+ const dismissed = useRef17(false);
2561
+ const remaining = useRef17(5e3);
2562
+ const [hovered, setHovered] = useState11(false);
2563
+ const [focused, setFocused] = useState11(false);
2413
2564
  const paused = hovered || focused;
2414
2565
  const tone = notice.tone ?? "success";
2415
2566
  const dismiss = useCallback3(() => {
@@ -2441,7 +2592,7 @@ function Notice({ notice, remove }) {
2441
2592
  remaining.current = Math.max(0, remaining.current - (Date.now() - started));
2442
2593
  };
2443
2594
  }, [dismiss, leaving2, notice.action, paused]);
2444
- return /* @__PURE__ */ jsxs25(
2595
+ return /* @__PURE__ */ jsxs26(
2445
2596
  "div",
2446
2597
  {
2447
2598
  ref: root,
@@ -2455,12 +2606,12 @@ function Notice({ notice, remove }) {
2455
2606
  if (!event.currentTarget.contains(event.relatedTarget)) setFocused(false);
2456
2607
  },
2457
2608
  children: [
2458
- /* @__PURE__ */ jsx30("span", { className: "ui-toast__icon", children: /* @__PURE__ */ jsx30(Icon, { name: glyphs[tone] }) }),
2459
- /* @__PURE__ */ jsxs25("div", { className: "ui-toast__body", children: [
2460
- /* @__PURE__ */ jsx30("div", { className: "ui-toast__title", children: notice.title }),
2461
- notice.text && /* @__PURE__ */ jsx30("div", { className: "ui-toast__text", children: notice.text })
2609
+ /* @__PURE__ */ jsx31("span", { className: "ui-toast__icon", children: /* @__PURE__ */ jsx31(Icon, { name: glyphs[tone] }) }),
2610
+ /* @__PURE__ */ jsxs26("div", { className: "ui-toast__body", children: [
2611
+ /* @__PURE__ */ jsx31("div", { className: "ui-toast__title", children: notice.title }),
2612
+ notice.text && /* @__PURE__ */ jsx31("div", { className: "ui-toast__text", children: notice.text })
2462
2613
  ] }),
2463
- notice.action && /* @__PURE__ */ jsx30(
2614
+ notice.action && /* @__PURE__ */ jsx31(
2464
2615
  "button",
2465
2616
  {
2466
2617
  type: "button",
@@ -2475,8 +2626,8 @@ function Notice({ notice, remove }) {
2475
2626
  children: notice.action.label
2476
2627
  }
2477
2628
  ),
2478
- /* @__PURE__ */ jsx30("button", { type: "button", className: "ui-toast__close", "aria-label": "Dismiss", disabled: leaving2, onClick: dismiss, children: /* @__PURE__ */ jsx30(Icon, { name: "x" }) }),
2479
- !notice.action && /* @__PURE__ */ jsx30(
2629
+ /* @__PURE__ */ jsx31("button", { type: "button", className: "ui-toast__close", "aria-label": "Dismiss", disabled: leaving2, onClick: dismiss, children: /* @__PURE__ */ jsx31(Icon, { name: "x" }) }),
2630
+ !notice.action && /* @__PURE__ */ jsx31(
2480
2631
  "span",
2481
2632
  {
2482
2633
  className: "ui-toast__timer is-running",
@@ -2509,40 +2660,40 @@ function usePublishedReach(stack2, count) {
2509
2660
  }, [stack2, count]);
2510
2661
  }
2511
2662
  function Toast({ children }) {
2512
- const [entries, setEntries] = useState10([]);
2513
- const nextId = useRef16(0);
2514
- const stack2 = useRef16(null);
2663
+ const [entries, setEntries] = useState11([]);
2664
+ const nextId = useRef17(0);
2665
+ const stack2 = useRef17(null);
2515
2666
  const push = useCallback3((notice) => {
2516
2667
  const id = nextId.current++;
2517
2668
  const remove = () => setEntries((current) => current.filter((entry) => entry.id !== id));
2518
2669
  setEntries((current) => [...current, { id, notice, remove }]);
2519
2670
  }, []);
2520
2671
  usePublishedReach(stack2, entries.length);
2521
- return /* @__PURE__ */ jsxs25(Context.Provider, { value: push, children: [
2672
+ return /* @__PURE__ */ jsxs26(Context.Provider, { value: push, children: [
2522
2673
  children,
2523
2674
  typeof document !== "undefined" && createPortal4(
2524
- /* @__PURE__ */ jsx30("div", { className: "ui-toast-stack rx-toast-stack", ref: stack2, children: entries.map((entry) => /* @__PURE__ */ jsx30(Notice, { notice: entry.notice, remove: entry.remove }, entry.id)) }),
2675
+ /* @__PURE__ */ jsx31("div", { className: "ui-toast-stack rx-toast-stack", ref: stack2, children: entries.map((entry) => /* @__PURE__ */ jsx31(Notice, { notice: entry.notice, remove: entry.remove }, entry.id)) }),
2525
2676
  document.body
2526
2677
  )
2527
2678
  ] });
2528
2679
  }
2529
2680
 
2530
2681
  // src/AppShell.tsx
2531
- import { useEffect as useEffect13, useId as useId12, useState as useState12 } from "react";
2682
+ import { useEffect as useEffect13, useId as useId12, useState as useState13 } from "react";
2532
2683
  import { applyTheme, paletteHotkey, prism, railCollapsed, themeIcon, themeName } from "@apliteni/apliteni-ui";
2533
2684
 
2534
2685
  // src/SidebarNav.tsx
2535
- import { forwardRef as forwardRef8, useEffect as useEffect12, useId as useId11, useState as useState11 } from "react";
2686
+ import { forwardRef as forwardRef8, useEffect as useEffect12, useId as useId11, useState as useState12 } from "react";
2536
2687
  import { ENTRANCE_FALLBACK_MS } from "@apliteni/apliteni-ui/motion";
2537
- import { Fragment as Fragment8, jsx as jsx31, jsxs as jsxs26 } from "react/jsx-runtime";
2688
+ import { Fragment as Fragment9, jsx as jsx32, jsxs as jsxs27 } from "react/jsx-runtime";
2538
2689
  var badgeText = (badge) => typeof badge === "object" ? badge.text : badge;
2539
2690
  function Row({ item, active, activeIs, collapsed, renderLink, sub }) {
2540
2691
  const uid = useId11();
2541
2692
  const group = !sub && !!item.items?.length;
2542
2693
  const childActive = group && item.items.some((child) => child.id === active);
2543
- const [expanded, setExpanded] = useState11();
2694
+ const [expanded, setExpanded] = useState12();
2544
2695
  const open = expanded ?? item.defaultOpen ?? childActive;
2545
- const [entering, setEntering] = useState11(false);
2696
+ const [entering, setEntering] = useState12(false);
2546
2697
  useEffect12(() => {
2547
2698
  if (!entering) return;
2548
2699
  const timer = setTimeout(() => setEntering(false), ENTRANCE_FALLBACK_MS);
@@ -2560,15 +2711,15 @@ function Row({ item, active, activeIs, collapsed, renderLink, sub }) {
2560
2711
  item.disabled && "is-disabled",
2561
2712
  item.danger && "is-danger"
2562
2713
  ].filter(Boolean).join(" ");
2563
- const children = /* @__PURE__ */ jsxs26(Fragment8, { children: [
2564
- (item.leading ?? item.icon) && /* @__PURE__ */ jsx31("span", { className: "ui-nav__ic", children: item.leading ?? /* @__PURE__ */ jsx31(Icon, { name: item.icon }) }),
2565
- /* @__PURE__ */ jsx31("span", { className: "ui-nav__label", children: item.label }),
2566
- count != null && count !== "" && /* @__PURE__ */ jsx31("span", { className: `ui-nav__badge is-${typeof item.badge === "object" ? item.badge.tone ?? "neutral" : "neutral"}`, children: count }),
2567
- group && /* @__PURE__ */ jsx31("span", { className: "ui-nav__caret", "aria-hidden": "true" })
2714
+ const children = /* @__PURE__ */ jsxs27(Fragment9, { children: [
2715
+ (item.leading ?? item.icon) && /* @__PURE__ */ jsx32("span", { className: "ui-nav__ic", children: item.leading ?? /* @__PURE__ */ jsx32(Icon, { name: item.icon }) }),
2716
+ /* @__PURE__ */ jsx32("span", { className: "ui-nav__label", children: item.label }),
2717
+ count != null && count !== "" && /* @__PURE__ */ jsx32("span", { className: `ui-nav__badge is-${typeof item.badge === "object" ? item.badge.tone ?? "neutral" : "neutral"}`, children: count }),
2718
+ group && /* @__PURE__ */ jsx32("span", { className: "ui-nav__caret", "aria-hidden": "true" })
2568
2719
  ] });
2569
2720
  const common = { className, "aria-label": name, title: collapsed ? name : void 0, children };
2570
- if (group) return /* @__PURE__ */ jsxs26("li", { className: `ui-nav__group${open ? " is-open" : ""}`, children: [
2571
- /* @__PURE__ */ jsx31(
2721
+ if (group) return /* @__PURE__ */ jsxs27("li", { className: `ui-nav__group${open ? " is-open" : ""}`, children: [
2722
+ /* @__PURE__ */ jsx32(
2572
2723
  "button",
2573
2724
  {
2574
2725
  ...common,
@@ -2582,22 +2733,22 @@ function Row({ item, active, activeIs, collapsed, renderLink, sub }) {
2582
2733
  }
2583
2734
  }
2584
2735
  ),
2585
- /* @__PURE__ */ jsx31("ul", { id: uid, className: `ui-nav__sub${entering ? " is-entering" : ""}`, hidden: !open, onAnimationEnd: () => setEntering(false), children: item.items.map((child) => /* @__PURE__ */ jsx31(Row, { item: child, active, activeIs, collapsed, renderLink, sub: true }, child.id)) })
2736
+ /* @__PURE__ */ jsx32("ul", { id: uid, className: `ui-nav__sub${entering ? " is-entering" : ""}`, hidden: !open, onAnimationEnd: () => setEntering(false), children: item.items.map((child) => /* @__PURE__ */ jsx32(Row, { item: child, active, activeIs, collapsed, renderLink, sub: true }, child.id)) })
2586
2737
  ] });
2587
- if (item.disabled) return /* @__PURE__ */ jsx31("li", { children: /* @__PURE__ */ jsx31("span", { ...common, role: "link", "aria-disabled": "true" }) });
2738
+ if (item.disabled) return /* @__PURE__ */ jsx32("li", { children: /* @__PURE__ */ jsx32("span", { ...common, role: "link", "aria-disabled": "true" }) });
2588
2739
  const props = {
2589
2740
  ...common,
2590
2741
  href: item.href ?? `#${item.id}`,
2591
2742
  target: item.target,
2592
2743
  "aria-current": isActive ? activeIs === "section" ? "true" : "page" : void 0
2593
2744
  };
2594
- return /* @__PURE__ */ jsx31("li", { children: renderLink ? renderLink(item, props) : /* @__PURE__ */ jsx31("a", { ...props }) });
2745
+ return /* @__PURE__ */ jsx32("li", { children: renderLink ? renderLink(item, props) : /* @__PURE__ */ jsx32("a", { ...props }) });
2595
2746
  }
2596
2747
  function Section({ section, ...props }) {
2597
2748
  const uid = useId11();
2598
- return /* @__PURE__ */ jsxs26("div", { className: "ui-nav__section", children: [
2599
- section.label && /* @__PURE__ */ jsx31("div", { className: "ui-nav__cap", id: uid, "aria-hidden": props.collapsed || void 0, children: section.label }),
2600
- /* @__PURE__ */ jsx31("ul", { className: "ui-nav__list", "aria-labelledby": section.label ? uid : void 0, children: section.items.map((item) => /* @__PURE__ */ jsx31(Row, { item, ...props }, item.id)) })
2749
+ return /* @__PURE__ */ jsxs27("div", { className: "ui-nav__section", children: [
2750
+ section.label && /* @__PURE__ */ jsx32("div", { className: "ui-nav__cap", id: uid, "aria-hidden": props.collapsed || void 0, children: section.label }),
2751
+ /* @__PURE__ */ jsx32("ul", { className: "ui-nav__list", "aria-labelledby": section.label ? uid : void 0, children: section.items.map((item) => /* @__PURE__ */ jsx32(Row, { item, ...props }, item.id)) })
2601
2752
  ] });
2602
2753
  }
2603
2754
  var SidebarNav = forwardRef8(function SidebarNav2({
@@ -2612,8 +2763,8 @@ var SidebarNav = forwardRef8(function SidebarNav2({
2612
2763
  "aria-label": label = "Sidebar",
2613
2764
  ...rest
2614
2765
  }, ref) {
2615
- return /* @__PURE__ */ jsxs26("nav", { ...rest, ref, "aria-label": label, className: ["ui-nav", "ui-nav--side", collapsed && "is-collapsed", className].filter(Boolean).join(" "), children: [
2616
- (sections?.length ? sections : [{ items }]).map((section, index) => /* @__PURE__ */ jsx31(
2766
+ return /* @__PURE__ */ jsxs27("nav", { ...rest, ref, "aria-label": label, className: ["ui-nav", "ui-nav--side", collapsed && "is-collapsed", className].filter(Boolean).join(" "), children: [
2767
+ (sections?.length ? sections : [{ items }]).map((section, index) => /* @__PURE__ */ jsx32(
2617
2768
  Section,
2618
2769
  {
2619
2770
  section,
@@ -2624,12 +2775,12 @@ var SidebarNav = forwardRef8(function SidebarNav2({
2624
2775
  },
2625
2776
  index
2626
2777
  )),
2627
- footer && /* @__PURE__ */ jsx31("div", { className: "ui-nav__foot", children: footer })
2778
+ footer && /* @__PURE__ */ jsx32("div", { className: "ui-nav__foot", children: footer })
2628
2779
  ] });
2629
2780
  });
2630
2781
 
2631
2782
  // src/AppShell.tsx
2632
- import { Fragment as Fragment9, jsx as jsx32, jsxs as jsxs27 } from "react/jsx-runtime";
2783
+ import { Fragment as Fragment10, jsx as jsx33, jsxs as jsxs28 } from "react/jsx-runtime";
2633
2784
  var path = (value) => value.split(/[?#]/)[0].replace(/\/+$/, "") || "/";
2634
2785
  function AppShell({
2635
2786
  sections,
@@ -2651,10 +2802,10 @@ function AppShell({
2651
2802
  renderLink
2652
2803
  }) {
2653
2804
  const uid = useId12();
2654
- const [collapsed, setCollapsed] = useState12(defaultCollapsed);
2655
- const [search, setSearch] = useState12(false);
2656
- const [more, setMore] = useState12(false);
2657
- const [theme, setTheme] = useState12("dark");
2805
+ const [collapsed, setCollapsed] = useState13(defaultCollapsed);
2806
+ const [search, setSearch] = useState13(false);
2807
+ const [more, setMore] = useState13(false);
2808
+ const [theme, setTheme] = useState13("dark");
2658
2809
  const current = path(pathname);
2659
2810
  const active = sections.filter((section) => current === path(section.href) || path(section.href) !== "/" && current.startsWith(`${path(section.href)}/`)).sort((a, b) => path(b.href).length - path(a.href).length)[0];
2660
2811
  useEffect13(() => {
@@ -2702,15 +2853,15 @@ function AppShell({
2702
2853
  onClick: (event) => {
2703
2854
  if (!event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey) setMore(false);
2704
2855
  },
2705
- children: /* @__PURE__ */ jsxs27(Fragment9, { children: [
2706
- /* @__PURE__ */ jsx32("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsx32(Icon, { name: section.icon }) }),
2707
- /* @__PURE__ */ jsx32("span", { className: "ui-nav__label", children: section.label }),
2708
- section.count != null && /* @__PURE__ */ jsx32("span", { className: "ui-nav__badge is-neutral", children: section.count })
2856
+ children: /* @__PURE__ */ jsxs28(Fragment10, { children: [
2857
+ /* @__PURE__ */ jsx33("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsx33(Icon, { name: section.icon }) }),
2858
+ /* @__PURE__ */ jsx33("span", { className: "ui-nav__label", children: section.label }),
2859
+ section.count != null && /* @__PURE__ */ jsx33("span", { className: "ui-nav__badge is-neutral", children: section.count })
2709
2860
  ] })
2710
2861
  };
2711
- return /* @__PURE__ */ jsx32("li", { children: renderLink ? renderLink(section, linkProps) : /* @__PURE__ */ jsx32("a", { ...linkProps }) }, section.href);
2862
+ return /* @__PURE__ */ jsx33("li", { children: renderLink ? renderLink(section, linkProps) : /* @__PURE__ */ jsx33("a", { ...linkProps }) }, section.href);
2712
2863
  });
2713
- const sidebar = (items, label) => /* @__PURE__ */ jsx32(
2864
+ const sidebar = (items, label) => /* @__PURE__ */ jsx33(
2714
2865
  SidebarNav,
2715
2866
  {
2716
2867
  "aria-label": label,
@@ -2722,7 +2873,7 @@ function AppShell({
2722
2873
  const linkProps = { ...props, onClick: (event) => {
2723
2874
  if (!event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey) setMore(false);
2724
2875
  } };
2725
- return renderLink ? renderLink(section, linkProps) : /* @__PURE__ */ jsx32("a", { ...linkProps });
2876
+ return renderLink ? renderLink(section, linkProps) : /* @__PURE__ */ jsx33("a", { ...linkProps });
2726
2877
  }
2727
2878
  }
2728
2879
  );
@@ -2730,33 +2881,33 @@ function AppShell({
2730
2881
  const visible = sections.length > 4 ? sections.slice(0, 3) : sections;
2731
2882
  const remaining = sections.slice(visible.length);
2732
2883
  const initials = account.name.trim().split(/\s+/).map((part) => part[0]).slice(0, 2).join("");
2733
- return /* @__PURE__ */ jsxs27("div", { className: `ui-app ui-app--topbar ui-react-app${collapsed ? " is-collapsed" : ""}`, children: [
2734
- /* @__PURE__ */ jsxs27("div", { className: "ui-app__rail", children: [
2735
- /* @__PURE__ */ jsx32("div", { className: "ui-app__head", children: /* @__PURE__ */ jsxs27("a", { className: "ui-app__brand", href: brandHref, "aria-label": word, children: [
2736
- brand ?? /* @__PURE__ */ jsx32("i", { "aria-hidden": "true", className: "ui-react-app__mark", dangerouslySetInnerHTML: { __html: prism(uid, 24) } }),
2737
- /* @__PURE__ */ jsx32("span", { children: word })
2884
+ return /* @__PURE__ */ jsxs28("div", { className: `ui-app ui-app--topbar ui-react-app${collapsed ? " is-collapsed" : ""}`, children: [
2885
+ /* @__PURE__ */ jsxs28("div", { className: "ui-app__rail", children: [
2886
+ /* @__PURE__ */ jsx33("div", { className: "ui-app__head", children: /* @__PURE__ */ jsxs28("a", { className: "ui-app__brand", href: brandHref, "aria-label": word, children: [
2887
+ brand ?? /* @__PURE__ */ jsx33("i", { "aria-hidden": "true", className: "ui-react-app__mark", dangerouslySetInnerHTML: { __html: prism(uid, 24) } }),
2888
+ /* @__PURE__ */ jsx33("span", { children: word })
2738
2889
  ] }) }),
2739
2890
  sidebar(sections, "Sections"),
2740
- /* @__PURE__ */ jsx32("div", { className: "ui-app__foot", children: /* @__PURE__ */ jsx32("div", { className: "ui-app__fold-row", children: /* @__PURE__ */ jsxs27("button", { type: "button", className: "ui-nav__item ui-app__fold", "aria-expanded": !collapsed, "aria-label": foldLabel, onClick: fold, children: [
2741
- /* @__PURE__ */ jsx32("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsxs27("svg", { className: "ui-icon", viewBox: "0 0 24 24", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
2742
- /* @__PURE__ */ jsx32("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
2743
- /* @__PURE__ */ jsx32("path", { className: "ui-app__fold-seam", d: "M9 3v18" }),
2744
- /* @__PURE__ */ jsx32("path", { className: "ui-app__fold-arrow", d: "m16 15-3-3 3-3" })
2891
+ /* @__PURE__ */ jsx33("div", { className: "ui-app__foot", children: /* @__PURE__ */ jsx33("div", { className: "ui-app__fold-row", children: /* @__PURE__ */ jsxs28("button", { type: "button", className: "ui-nav__item ui-app__fold", "aria-expanded": !collapsed, "aria-label": foldLabel, onClick: fold, children: [
2892
+ /* @__PURE__ */ jsx33("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsxs28("svg", { className: "ui-icon", viewBox: "0 0 24 24", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
2893
+ /* @__PURE__ */ jsx33("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
2894
+ /* @__PURE__ */ jsx33("path", { className: "ui-app__fold-seam", d: "M9 3v18" }),
2895
+ /* @__PURE__ */ jsx33("path", { className: "ui-app__fold-arrow", d: "m16 15-3-3 3-3" })
2745
2896
  ] }) }),
2746
- /* @__PURE__ */ jsx32("span", { className: "ui-nav__label", children: foldLabel })
2897
+ /* @__PURE__ */ jsx33("span", { className: "ui-nav__label", children: foldLabel })
2747
2898
  ] }) }) })
2748
2899
  ] }),
2749
- /* @__PURE__ */ jsxs27("div", { className: "ui-app__well", children: [
2750
- /* @__PURE__ */ jsxs27("header", { className: "ui-app__bar", children: [
2751
- /* @__PURE__ */ jsxs27("button", { type: "button", className: "ui-app__search", "aria-haspopup": "dialog", onClick: () => setSearch(true), children: [
2752
- /* @__PURE__ */ jsx32("span", { className: "ui-app__search-ic", children: /* @__PURE__ */ jsx32(Icon, { name: "search" }) }),
2753
- /* @__PURE__ */ jsx32("span", { className: "ui-app__search-txt", children: "Search or run a command\u2026" }),
2754
- /* @__PURE__ */ jsx32("kbd", { className: "ui-cmdk__key", children: paletteHotkey() })
2900
+ /* @__PURE__ */ jsxs28("div", { className: "ui-app__well", children: [
2901
+ /* @__PURE__ */ jsxs28("header", { className: "ui-app__bar", children: [
2902
+ /* @__PURE__ */ jsxs28("button", { type: "button", className: "ui-app__search", "aria-haspopup": "dialog", onClick: () => setSearch(true), children: [
2903
+ /* @__PURE__ */ jsx33("span", { className: "ui-app__search-ic", children: /* @__PURE__ */ jsx33(Icon, { name: "search" }) }),
2904
+ /* @__PURE__ */ jsx33("span", { className: "ui-app__search-txt", children: "Search or run a command\u2026" }),
2905
+ /* @__PURE__ */ jsx33("kbd", { className: "ui-cmdk__key", children: paletteHotkey() })
2755
2906
  ] }),
2756
- /* @__PURE__ */ jsx32("span", { className: "ui-app__bar-gap" }),
2907
+ /* @__PURE__ */ jsx33("span", { className: "ui-app__bar-gap" }),
2757
2908
  bandControl,
2758
- /* @__PURE__ */ jsx32("button", { type: "button", className: "toggle", "aria-label": themeName(theme), title: themeName(theme), onClick: () => applyTheme(theme === "light" ? "dark" : "light"), children: /* @__PURE__ */ jsx32("span", { className: "ic", "aria-hidden": "true", dangerouslySetInnerHTML: { __html: themeIcon(theme) } }) }),
2759
- /* @__PURE__ */ jsx32("div", { className: "ui-app__user ui-app__user--bar", children: /* @__PURE__ */ jsx32(
2909
+ /* @__PURE__ */ jsx33("button", { type: "button", className: "toggle", "aria-label": themeName(theme), title: themeName(theme), onClick: () => applyTheme(theme === "light" ? "dark" : "light"), children: /* @__PURE__ */ jsx33("span", { className: "ic", "aria-hidden": "true", dangerouslySetInnerHTML: { __html: themeIcon(theme) } }) }),
2910
+ /* @__PURE__ */ jsx33("div", { className: "ui-app__user ui-app__user--bar", children: /* @__PURE__ */ jsx33(
2760
2911
  Dropdown,
2761
2912
  {
2762
2913
  variant: "menu",
@@ -2765,27 +2916,27 @@ function AppShell({
2765
2916
  ariaLabel: `Signed in as ${account.name}, ${account.email}`,
2766
2917
  triggerClass: "ui-app__user-trigger",
2767
2918
  panelClass: "ui-app__user-panel",
2768
- triggerContent: /* @__PURE__ */ jsx32("span", { className: "ui-app__av", "aria-hidden": "true", children: initials }),
2769
- header: /* @__PURE__ */ jsxs27("div", { className: "ui-dropdown__head", children: [
2770
- /* @__PURE__ */ jsx32("b", { children: account.name }),
2771
- /* @__PURE__ */ jsx32("span", { children: account.email })
2919
+ triggerContent: /* @__PURE__ */ jsx33("span", { className: "ui-app__av", "aria-hidden": "true", children: initials }),
2920
+ header: /* @__PURE__ */ jsxs28("div", { className: "ui-dropdown__head", children: [
2921
+ /* @__PURE__ */ jsx33("b", { children: account.name }),
2922
+ /* @__PURE__ */ jsx33("span", { children: account.email })
2772
2923
  ] }),
2773
2924
  items: [{ label: "Sign out", icon: "logout", danger: true }],
2774
2925
  onSelect: onSignOut
2775
2926
  }
2776
2927
  ) })
2777
2928
  ] }),
2778
- /* @__PURE__ */ jsxs27("main", { className: `ui-app__main${width === "wide" ? " ui-app__main--wide" : ""}`, children: [
2779
- back && /* @__PURE__ */ jsx32(BackLink, { ...back }),
2780
- /* @__PURE__ */ jsx32("h1", { children: title }),
2781
- lede && /* @__PURE__ */ jsx32("p", { className: "ui-app__sub", children: lede }),
2782
- actions && /* @__PURE__ */ jsx32("div", { className: "ui-react-app__actions", children: actions }),
2783
- /* @__PURE__ */ jsx32("div", { className: "ui-app__body", children })
2929
+ /* @__PURE__ */ jsxs28("main", { className: `ui-app__main${width === "wide" ? " ui-app__main--wide" : ""}`, children: [
2930
+ back && /* @__PURE__ */ jsx33(BackLink, { ...back }),
2931
+ /* @__PURE__ */ jsx33("h1", { children: title }),
2932
+ lede && /* @__PURE__ */ jsx33("p", { className: "ui-app__sub", children: lede }),
2933
+ actions && /* @__PURE__ */ jsx33("div", { className: "ui-react-app__actions", children: actions }),
2934
+ /* @__PURE__ */ jsx33("div", { className: "ui-app__body", children })
2784
2935
  ] })
2785
2936
  ] }),
2786
- /* @__PURE__ */ jsx32("nav", { className: "ui-nav ui-react-app__bottom", "aria-label": "Sections on mobile", children: /* @__PURE__ */ jsxs27("ul", { className: "ui-nav__list", children: [
2937
+ /* @__PURE__ */ jsx33("nav", { className: "ui-nav ui-react-app__bottom", "aria-label": "Sections on mobile", children: /* @__PURE__ */ jsxs28("ul", { className: "ui-nav__list", children: [
2787
2938
  links(visible),
2788
- remaining.length > 0 && /* @__PURE__ */ jsx32("li", { children: /* @__PURE__ */ jsxs27(
2939
+ remaining.length > 0 && /* @__PURE__ */ jsx33("li", { children: /* @__PURE__ */ jsxs28(
2789
2940
  "button",
2790
2941
  {
2791
2942
  type: "button",
@@ -2794,20 +2945,20 @@ function AppShell({
2794
2945
  "aria-expanded": more,
2795
2946
  onClick: () => setMore(true),
2796
2947
  children: [
2797
- /* @__PURE__ */ jsx32("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsx32(Icon, { name: "moreHorizontal" }) }),
2798
- /* @__PURE__ */ jsx32("span", { className: "ui-nav__label", children: "More" })
2948
+ /* @__PURE__ */ jsx33("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsx33(Icon, { name: "moreHorizontal" }) }),
2949
+ /* @__PURE__ */ jsx33("span", { className: "ui-nav__label", children: "More" })
2799
2950
  ]
2800
2951
  }
2801
2952
  ) })
2802
2953
  ] }) }),
2803
- /* @__PURE__ */ jsx32(Drawer, { open: more, onClose: () => setMore(false), title: "More sections", side: "bottom", children: sidebar(remaining, "More sections") }),
2804
- /* @__PURE__ */ jsx32(CommandPalette, { ...palette ?? { groups: [{ items: sections.map((section) => ({ id: section.href, ...section })) }] }, open: search, onClose: () => setSearch(false) })
2954
+ /* @__PURE__ */ jsx33(Drawer, { open: more, onClose: () => setMore(false), title: "More sections", side: "bottom", children: sidebar(remaining, "More sections") }),
2955
+ /* @__PURE__ */ jsx33(CommandPalette, { ...palette ?? { groups: [{ items: sections.map((section) => ({ id: section.href, ...section })) }] }, open: search, onClose: () => setSearch(false) })
2805
2956
  ] });
2806
2957
  }
2807
2958
 
2808
2959
  // src/EmptyState.tsx
2809
2960
  import { icon as kitIcon2, illo } from "@apliteni/apliteni-ui";
2810
- import { jsx as jsx33, jsxs as jsxs28 } from "react/jsx-runtime";
2961
+ import { jsx as jsx34, jsxs as jsxs29 } from "react/jsx-runtime";
2811
2962
  var cases = {
2812
2963
  "first-run": { title: "Nothing needs you yet", sub: "New items will appear here when they need your attention.", icon: "check" },
2813
2964
  "no-matches": { title: "No results in this view", sub: "Try another search or clear your filters.", icon: "search" },
@@ -2819,18 +2970,18 @@ function EmptyState({ variant = "first-run", title, sub, icon: icon7, art, actio
2819
2970
  const heading = title ?? preset.title;
2820
2971
  const guidance = sub ?? preset.sub;
2821
2972
  const glyph = icon7 ?? preset.icon;
2822
- return /* @__PURE__ */ jsxs28("div", { className: "ui-empty", children: [
2823
- art ? typeof art === "string" ? /* @__PURE__ */ jsx33("div", { className: "ui-empty__art", dangerouslySetInnerHTML: { __html: /<svg/.test(art) ? art : illo(art) } }) : /* @__PURE__ */ jsx33("div", { className: "ui-empty__art", "aria-hidden": "true", children: art }) : glyph ? /* @__PURE__ */ jsx33("div", { className: "ui-empty__icon", dangerouslySetInnerHTML: { __html: kitIcon2(glyph) } }) : null,
2824
- heading && /* @__PURE__ */ jsx33("div", { className: "ui-empty__title", children: heading }),
2825
- guidance && /* @__PURE__ */ jsx33("div", { className: "ui-empty__sub", children: guidance }),
2826
- actions && /* @__PURE__ */ jsx33("div", { className: "ui-empty__actions", children: actions })
2973
+ return /* @__PURE__ */ jsxs29("div", { className: "ui-empty", children: [
2974
+ art ? typeof art === "string" ? /* @__PURE__ */ jsx34("div", { className: "ui-empty__art", dangerouslySetInnerHTML: { __html: /<svg/.test(art) ? art : illo(art) } }) : /* @__PURE__ */ jsx34("div", { className: "ui-empty__art", "aria-hidden": "true", children: art }) : glyph ? /* @__PURE__ */ jsx34("div", { className: "ui-empty__icon", dangerouslySetInnerHTML: { __html: kitIcon2(glyph) } }) : null,
2975
+ heading && /* @__PURE__ */ jsx34("div", { className: "ui-empty__title", children: heading }),
2976
+ guidance && /* @__PURE__ */ jsx34("div", { className: "ui-empty__sub", children: guidance }),
2977
+ actions && /* @__PURE__ */ jsx34("div", { className: "ui-empty__actions", children: actions })
2827
2978
  ] });
2828
2979
  }
2829
2980
 
2830
2981
  // src/ThemeToggle.tsx
2831
2982
  import { useSyncExternalStore } from "react";
2832
2983
  import { icon as icon6 } from "@apliteni/apliteni-ui";
2833
- import { jsx as jsx34, jsxs as jsxs29 } from "react/jsx-runtime";
2984
+ import { jsx as jsx35, jsxs as jsxs30 } from "react/jsx-runtime";
2834
2985
  var STORAGE_KEY = "apliteni-strategy-theme";
2835
2986
  var CHOICE_EVENT = "apliteni-theme-choice";
2836
2987
  var THEME_INIT_SCRIPT = `(()=>{let t;try{t=localStorage.getItem('apliteni-strategy-theme')}catch{}document.documentElement.setAttribute('data-theme',t==='dark'||t==='light'?t:window.matchMedia?.('(prefers-color-scheme: light)').matches?'light':'dark')})()`;
@@ -2888,10 +3039,10 @@ function ThemeToggle({ labelled = false }) {
2888
3039
  }
2889
3040
  window.dispatchEvent(new Event(CHOICE_EVENT));
2890
3041
  };
2891
- const button = /* @__PURE__ */ jsx34("button", { type: "button", className: "toggle", "aria-label": name, title: name, onClick: toggle, children: /* @__PURE__ */ jsx34("span", { className: "ic", "aria-hidden": "true", dangerouslySetInnerHTML: { __html: icon6(theme === "auto" ? "monitor" : theme === "light" ? "sun" : "moon") } }) });
2892
- return labelled ? /* @__PURE__ */ jsxs29("span", { className: "ui-theme-toggle", children: [
3042
+ const button = /* @__PURE__ */ jsx35("button", { type: "button", className: "toggle", "aria-label": name, title: name, onClick: toggle, children: /* @__PURE__ */ jsx35("span", { className: "ic", "aria-hidden": "true", dangerouslySetInnerHTML: { __html: icon6(theme === "auto" ? "monitor" : theme === "light" ? "sun" : "moon") } }) });
3043
+ return labelled ? /* @__PURE__ */ jsxs30("span", { className: "ui-theme-toggle", children: [
2893
3044
  button,
2894
- /* @__PURE__ */ jsxs29("span", { children: [
3045
+ /* @__PURE__ */ jsxs30("span", { children: [
2895
3046
  label,
2896
3047
  " theme"
2897
3048
  ] })
@@ -2899,11 +3050,11 @@ function ThemeToggle({ labelled = false }) {
2899
3050
  }
2900
3051
 
2901
3052
  // src/Success.tsx
2902
- import { forwardRef as forwardRef9, useEffect as useEffect14, useRef as useRef17, useState as useState13 } from "react";
2903
- import { jsx as jsx35, jsxs as jsxs30 } from "react/jsx-runtime";
3053
+ import { forwardRef as forwardRef9, useEffect as useEffect14, useRef as useRef18, useState as useState14 } from "react";
3054
+ import { jsx as jsx36, jsxs as jsxs31 } from "react/jsx-runtime";
2904
3055
  var SuccessCheck = forwardRef9(function SuccessCheck2({ variant = "line", className, ...rest }, ref) {
2905
3056
  const circled = variant === "circled";
2906
- return /* @__PURE__ */ jsxs30(
3057
+ return /* @__PURE__ */ jsxs31(
2907
3058
  "svg",
2908
3059
  {
2909
3060
  ...rest,
@@ -2915,18 +3066,18 @@ var SuccessCheck = forwardRef9(function SuccessCheck2({ variant = "line", classN
2915
3066
  "aria-hidden": "true",
2916
3067
  focusable: "false",
2917
3068
  children: [
2918
- circled && /* @__PURE__ */ jsx35("path", { className: "ui-sx__circle", d: "M22 11.08V12a10 10 0 1 1-5.93-9.14" }),
2919
- /* @__PURE__ */ jsx35("path", { className: "ui-sx__tick", d: circled ? "M22 4L12 14.01l-3-3" : "M20 6L9 17l-5-5" })
3069
+ circled && /* @__PURE__ */ jsx36("path", { className: "ui-sx__circle", d: "M22 11.08V12a10 10 0 1 1-5.93-9.14" }),
3070
+ /* @__PURE__ */ jsx36("path", { className: "ui-sx__tick", d: circled ? "M22 4L12 14.01l-3-3" : "M20 6L9 17l-5-5" })
2920
3071
  ]
2921
3072
  }
2922
3073
  );
2923
3074
  });
2924
3075
  var SuccessPanel = forwardRef9(function SuccessPanel2({ title = "Done", sub, check = "line", className, ...rest }, ref) {
2925
3076
  const circled = check === "circled";
2926
- return /* @__PURE__ */ jsxs30("div", { ...rest, ref, className: ["ui-success", className].filter(Boolean).join(" "), children: [
2927
- /* @__PURE__ */ jsx35("div", { className: ["ui-success__check", circled && "ui-success__check--circled"].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx35(SuccessCheck, { variant: circled ? "circled" : "line" }) }),
2928
- /* @__PURE__ */ jsx35("div", { className: "ui-success__title", children: title }),
2929
- sub && /* @__PURE__ */ jsx35("div", { className: "ui-success__sub", children: sub })
3077
+ return /* @__PURE__ */ jsxs31("div", { ...rest, ref, className: ["ui-success", className].filter(Boolean).join(" "), children: [
3078
+ /* @__PURE__ */ jsx36("div", { className: ["ui-success__check", circled && "ui-success__check--circled"].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx36(SuccessCheck, { variant: circled ? "circled" : "line" }) }),
3079
+ /* @__PURE__ */ jsx36("div", { className: "ui-success__title", children: title }),
3080
+ sub && /* @__PURE__ */ jsx36("div", { className: "ui-success__sub", children: sub })
2930
3081
  ] });
2931
3082
  });
2932
3083
  var pieces = [
@@ -2946,8 +3097,8 @@ var pieces = [
2946
3097
  [80, 0.36, -20, "c", 1.05]
2947
3098
  ];
2948
3099
  function Countdown({ seconds, label = "Redirecting", onDone }) {
2949
- const [remaining, setRemaining] = useState13(seconds);
2950
- const callback = useRef17(onDone);
3100
+ const [remaining, setRemaining] = useState14(seconds);
3101
+ const callback = useRef18(onDone);
2951
3102
  useEffect14(() => {
2952
3103
  callback.current = onDone;
2953
3104
  }, [onDone]);
@@ -2963,12 +3114,12 @@ function Countdown({ seconds, label = "Redirecting", onDone }) {
2963
3114
  }, 1e3);
2964
3115
  return () => clearInterval(timer);
2965
3116
  }, [seconds]);
2966
- return /* @__PURE__ */ jsxs30("div", { className: "ui-sx__count", "data-sx-count": "", style: { "--sx-secs": `${seconds}s` }, children: [
2967
- /* @__PURE__ */ jsx35("span", { className: "ui-sx__count-ring", "aria-hidden": "true" }),
2968
- /* @__PURE__ */ jsxs30("span", { className: "ui-sx__count-text", children: [
3117
+ return /* @__PURE__ */ jsxs31("div", { className: "ui-sx__count", "data-sx-count": "", style: { "--sx-secs": `${seconds}s` }, children: [
3118
+ /* @__PURE__ */ jsx36("span", { className: "ui-sx__count-ring", "aria-hidden": "true" }),
3119
+ /* @__PURE__ */ jsxs31("span", { className: "ui-sx__count-text", children: [
2969
3120
  label,
2970
3121
  " in ",
2971
- /* @__PURE__ */ jsx35("b", { "data-sx-num": "", children: remaining }),
3122
+ /* @__PURE__ */ jsx36("b", { "data-sx-num": "", children: remaining }),
2972
3123
  "s"
2973
3124
  ] })
2974
3125
  ] });
@@ -2995,7 +3146,7 @@ var Success = forwardRef9(function Success2({
2995
3146
  const seconds = countdown?.seconds;
2996
3147
  const duration = seconds != null && Number.isFinite(seconds) && seconds >= 1 ? Math.floor(seconds) : 5;
2997
3148
  const mark = check === "circled" ? "circled" : "line";
2998
- return /* @__PURE__ */ jsxs30(
3149
+ return /* @__PURE__ */ jsxs31(
2999
3150
  "div",
3000
3151
  {
3001
3152
  role: "status",
@@ -3004,16 +3155,16 @@ var Success = forwardRef9(function Success2({
3004
3155
  ref,
3005
3156
  className: ["ui-sx", `ui-sx--${layout}`, `ui-sx--check-${mark}`, confetti && "ui-sx--confetti", className].filter(Boolean).join(" "),
3006
3157
  children: [
3007
- confetti && /* @__PURE__ */ jsx35("div", { className: "ui-sx__confetti", "aria-hidden": "true", children: pieces.map(
3008
- ([x, d, r, t, s], index) => /* @__PURE__ */ jsx35("i", { className: `ui-sx__piece ui-sx__piece--${t}`, style: { left: `${x}%`, "--sx-d": `${d}s`, "--sx-r": `${r}deg`, "--sx-s": s } }, index)
3158
+ confetti && /* @__PURE__ */ jsx36("div", { className: "ui-sx__confetti", "aria-hidden": "true", children: pieces.map(
3159
+ ([x, d, r, t, s], index) => /* @__PURE__ */ jsx36("i", { className: `ui-sx__piece ui-sx__piece--${t}`, style: { left: `${x}%`, "--sx-d": `${d}s`, "--sx-r": `${r}deg`, "--sx-s": s } }, index)
3009
3160
  ) }),
3010
- /* @__PURE__ */ jsxs30("div", { className: "ui-sx__inner", children: [
3011
- /* @__PURE__ */ jsx35("div", { className: "ui-sx__visual", children: /* @__PURE__ */ jsx35(SuccessCheck, { variant: mark }) }),
3012
- /* @__PURE__ */ jsxs30("div", { className: "ui-sx__content", children: [
3013
- /* @__PURE__ */ jsx35(Heading, { className: "ui-sx__title", children: title }),
3014
- body && /* @__PURE__ */ jsx35("p", { className: "ui-sx__body", children: body }),
3015
- hasActions && /* @__PURE__ */ jsx35("div", { className: "ui-sx__actions", children: actions }),
3016
- countdown && /* @__PURE__ */ jsx35(Countdown, { seconds: duration, label: countdown.label, onDone: onCountdownEnd }, duration)
3161
+ /* @__PURE__ */ jsxs31("div", { className: "ui-sx__inner", children: [
3162
+ /* @__PURE__ */ jsx36("div", { className: "ui-sx__visual", children: /* @__PURE__ */ jsx36(SuccessCheck, { variant: mark }) }),
3163
+ /* @__PURE__ */ jsxs31("div", { className: "ui-sx__content", children: [
3164
+ /* @__PURE__ */ jsx36(Heading, { className: "ui-sx__title", children: title }),
3165
+ body && /* @__PURE__ */ jsx36("p", { className: "ui-sx__body", children: body }),
3166
+ hasActions && /* @__PURE__ */ jsx36("div", { className: "ui-sx__actions", children: actions }),
3167
+ countdown && /* @__PURE__ */ jsx36(Countdown, { seconds: duration, label: countdown.label, onDone: onCountdownEnd }, duration)
3017
3168
  ] })
3018
3169
  ] })
3019
3170
  ]
@@ -3044,6 +3195,7 @@ export {
3044
3195
  Dropdown,
3045
3196
  EmptyState,
3046
3197
  Field,
3198
+ FileDrop,
3047
3199
  FileField,
3048
3200
  FilterBar,
3049
3201
  Icon,
package/src/index.css CHANGED
@@ -24,6 +24,7 @@
24
24
  @import "./styles/segmented.css";
25
25
  @import "./styles/tabs.css";
26
26
  @import "./styles/input.css";
27
+ @import "./styles/file-drop.css";
27
28
  @import "./styles/dropdown.css";
28
29
  @import "./styles/tooltip.css";
29
30
  @import "./styles/nav.css";
package/src/inline.js CHANGED
@@ -38,6 +38,7 @@ export const styles = {
38
38
  segmented: read('styles/segmented.css'),
39
39
  tabs: read('styles/tabs.css'),
40
40
  input: read('styles/input.css'),
41
+ fileDrop: read('styles/file-drop.css'),
41
42
  dropdown: read('styles/dropdown.css'),
42
43
  tooltip: read('styles/tooltip.css'),
43
44
  nav: read('styles/nav.css'),
@@ -73,6 +74,7 @@ export const cssText = [
73
74
  styles.segmented,
74
75
  styles.tabs,
75
76
  styles.input,
77
+ styles.fileDrop,
76
78
  styles.dropdown,
77
79
  styles.tooltip,
78
80
  styles.nav,
@@ -0,0 +1,83 @@
1
+ /* File drop — .ui-drop. One row at rest; the field-sized box stays with .ui-file.
2
+ * why: docs/specification.md#react-file-drop */
3
+ .ui-drop { position: relative; }
4
+
5
+ /* What the region accepts the file for, above the control. */
6
+ .ui-drop > * + .ui-drop__row { margin-top: var(--space-3); }
7
+
8
+ .ui-drop__row {
9
+ display: flex;
10
+ flex-wrap: wrap;
11
+ align-items: center;
12
+ /* One height at rest, whichever control is in it: the picker button carries a
13
+ glyph and a note beside it does not. */
14
+ min-height: var(--space-8);
15
+ gap: var(--space-2) var(--space-3);
16
+ font-size: var(--text-sm);
17
+ color: var(--text);
18
+ overflow-wrap: anywhere;
19
+ }
20
+
21
+ .ui-drop__note { margin: 0; }
22
+
23
+ /* The file in hand is a stack, not a line: the name owns the top tier, what the
24
+ page has a rule about sits under it, and the track runs the width below both.
25
+ One line asked to carry all three truncates the name to nothing at 320. */
26
+ .ui-drop__file { display: grid; flex: 1 1 auto; gap: var(--space-2); min-width: 0; }
27
+
28
+ /* The name tier. Never wraps: the actions stay on the name's line at every
29
+ width, and the name gives way to them by truncating. */
30
+ .ui-drop__line { display: flex; flex-wrap: nowrap; align-items: center; gap: var(--space-3); min-width: 0; }
31
+
32
+ /* The extension is the half that says what kind of file this is, so the stem
33
+ gives way and the tail never does — `frankfurt-settlement-state….pdf`, the
34
+ way Finder, Drive and Dropbox cut a name. */
35
+ .ui-drop__name { display: flex; flex: 1 1 auto; min-width: var(--space-10); color: var(--strong); font-weight: var(--weight-medium); }
36
+ .ui-drop__stem { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
37
+ .ui-drop__ext { flex: none; }
38
+
39
+ /* The facts tier: the size while it uploads, the message when it fails. Never
40
+ wraps either — a second tier of text here would be a third tier on the row. */
41
+ .ui-drop__facts { display: flex; flex-wrap: nowrap; align-items: center; gap: var(--space-3); min-width: 0; }
42
+ .ui-drop__size { flex: none; white-space: nowrap; }
43
+ .ui-drop__word { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 0 1 auto; min-width: 0; }
44
+
45
+ /* A status the track does not already draw is a circled mark and a word,
46
+ because colour alone is not a status. */
47
+ .ui-drop__state { display: inline-flex; flex: 0 1 auto; align-items: center; gap: var(--space-2); min-width: 0; }
48
+ .ui-drop__state svg { width: 16px; height: 16px; flex: none; stroke-width: 2.4; }
49
+ .ui-drop__error { color: var(--pink); }
50
+
51
+ /* Closes the name's line on the edge the content ends on, in every state. */
52
+ .ui-drop__actions { display: flex; flex: none; align-items: center; gap: var(--space-2); margin-left: auto; }
53
+
54
+ /* The width of the row it reports on, so 40% reads as 40% rather than as a dash.
55
+ The ground is the kit's token for sunken tracks; --surface-3 was invisible. */
56
+ .ui-drop__bar {
57
+ display: block;
58
+ width: 100%;
59
+ height: 6px;
60
+ border-radius: var(--radius-pill);
61
+ background: var(--surface-2); /* ring-gap: inherit — a progress track holds no focus target. */
62
+ overflow: hidden;
63
+ }
64
+ .ui-drop__bar span { display: block; height: 100%; background: var(--accent); }
65
+
66
+ /* Covers the region rather than joining it, so the row keeps its place. */
67
+ .ui-drop__target {
68
+ position: absolute;
69
+ inset: 0;
70
+ display: flex;
71
+ align-items: center;
72
+ justify-content: center;
73
+ padding: var(--space-3);
74
+ border: 1px dashed var(--accent);
75
+ border-radius: var(--radius-md);
76
+ background: var(--surface); /* ring-gap: inherit — the target is painted while a file is over the region and holds no focus target. */
77
+ color: var(--strong);
78
+ font-size: var(--text-sm);
79
+ text-align: center;
80
+ }
81
+
82
+ .ui-drop.is-disabled .ui-drop__row { /* muted-ink: state — the drop accepts nothing */ color: var(--disabled-ink); }
83
+ .ui-drop.is-disabled .ui-drop__name { /* muted-ink: state — the drop accepts nothing */ color: var(--disabled-ink); }
@@ -128,6 +128,15 @@
128
128
  mark's zone 9px inside `Revoke`. Nothing down, because a dense table's
129
129
  rows share an edge and its padding is not clearance to spend. */
130
130
  .ui-table__act .ui-btn + .ui-btn { margin-inline-start: var(--tap-gap) !important; }
131
+
132
+ /* A file drop's pair opens both ways: the row's own floor leaves clearance
133
+ under a button shorter than the target, and the panel's padding is below. */
134
+ .ui-drop__row { --tap-clear-y: var(--tap-gap); }
135
+ .ui-drop__actions {
136
+ gap: var(--tap-gap) !important;
137
+ --tap-clear-x: var(--tap-gap);
138
+ --tap-clear-y: var(--tap-gap);
139
+ }
131
140
  .ui-table__act { --tap-clear-x: var(--tap-gap); --tap-clear-y: 0px; }
132
141
 
133
142
  /* Down only, like the nine rows below: a toast is a full row at this width,