@leef-l/macos-ui-kit 0.0.0-stage → 0.1.1

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.
Files changed (45) hide show
  1. package/README.md +215 -2
  2. package/dist/assets/Inter Font License.txt +93 -0
  3. package/dist/assets/Inter-Medium.ttf +0 -0
  4. package/dist/component.d.ts +47 -0
  5. package/dist/controls/badge.d.ts +27 -0
  6. package/dist/controls/button.d.ts +41 -0
  7. package/dist/controls/checkbox.d.ts +40 -0
  8. package/dist/controls/combobox.d.ts +105 -0
  9. package/dist/controls/datepicker.d.ts +42 -0
  10. package/dist/controls/disclosure.d.ts +40 -0
  11. package/dist/controls/numberfield.d.ts +60 -0
  12. package/dist/controls/progress.d.ts +33 -0
  13. package/dist/controls/segmented.d.ts +64 -0
  14. package/dist/controls/select.d.ts +42 -0
  15. package/dist/controls/slider.d.ts +49 -0
  16. package/dist/controls/spinner.d.ts +24 -0
  17. package/dist/controls/splitview.d.ts +76 -0
  18. package/dist/controls/switch.d.ts +47 -0
  19. package/dist/controls/table.d.ts +103 -0
  20. package/dist/controls/textarea.d.ts +49 -0
  21. package/dist/controls/textfield.d.ts +63 -0
  22. package/dist/dom.d.ts +35 -0
  23. package/dist/index.d.ts +85 -0
  24. package/dist/index.js +2053 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/layout/sidebar.d.ts +47 -0
  27. package/dist/layout/statusbar.d.ts +30 -0
  28. package/dist/layout/tabview.d.ts +74 -0
  29. package/dist/layout/titlebar.d.ts +49 -0
  30. package/dist/layout/toolbar.d.ts +54 -0
  31. package/dist/layout/traffic-lights.d.ts +37 -0
  32. package/dist/macos-ui-kit.css +2 -0
  33. package/dist/macos-ui-kit.iife.js +2 -0
  34. package/dist/macos-ui-kit.iife.js.map +1 -0
  35. package/dist/mobile/shell.d.ts +59 -0
  36. package/dist/overlay.d.ts +41 -0
  37. package/dist/overlays/dialog.d.ts +46 -0
  38. package/dist/overlays/menu.d.ts +58 -0
  39. package/dist/overlays/popover.d.ts +90 -0
  40. package/dist/overlays/sheet.d.ts +56 -0
  41. package/dist/overlays/toast.d.ts +37 -0
  42. package/dist/overlays/tooltip.d.ts +48 -0
  43. package/dist/style.css.d.ts +2 -0
  44. package/dist/theme.d.ts +100 -0
  45. package/package.json +55 -4
@@ -0,0 +1,60 @@
1
+ import { Component } from "../component";
2
+ import { type Child } from "../dom";
3
+ import "./numberfield.css";
4
+ export interface NumberFieldOptions {
5
+ /** Label rendered above the field; it is wired to the input through `for`/`id`. */
6
+ label?: Child;
7
+ /** Smallest accepted value; also bounds the native spinner. */
8
+ min?: number;
9
+ /** Largest accepted value; also bounds the native spinner. */
10
+ max?: number;
11
+ /** Increment used by the stepper buttons and the arrow keys. Default 1. */
12
+ step?: number;
13
+ /** Initial value; defaults to `min`, then 0. */
14
+ value?: number;
15
+ /**
16
+ * Fires only for a legal value — a finite number inside `min`/`max`.
17
+ * A blank or out-of-range entry stays in the field but is not committed.
18
+ */
19
+ onChange?: (value: number) => void;
20
+ }
21
+ /**
22
+ * A numeric field on a native `<input type="number">`.
23
+ *
24
+ * Validation runs on every `input` event: a blank, non-numeric or out-of-range
25
+ * entry is left exactly as typed but never committed, because handing the
26
+ * caller a number the field itself rejects would put its state at odds with
27
+ * what is on screen. `getValue()` therefore reports the last committed value,
28
+ * never a half-typed one.
29
+ *
30
+ * The native spinner cannot be themed, so it is hidden and replaced by two
31
+ * chevron buttons that step by `step` and disable themselves at the bounds.
32
+ */
33
+ export declare class NumberField extends Component<HTMLElement> {
34
+ protected readonly root: HTMLElement;
35
+ private readonly input;
36
+ private readonly stepUp;
37
+ private readonly stepDown;
38
+ private readonly min?;
39
+ private readonly max?;
40
+ private readonly step?;
41
+ private readonly onChange?;
42
+ private committed;
43
+ constructor(options?: NumberFieldOptions);
44
+ /** The last committed value. A blank or illegal entry is not reported. */
45
+ get value(): number;
46
+ /** Replace the value; out-of-range input is clamped rather than committed as-is. */
47
+ setValue(value: number): void;
48
+ /** Enable or disable the field and its stepper buttons. */
49
+ setDisabled(disabled: boolean): void;
50
+ /** Validate on every keystroke; only legal values are committed. */
51
+ private handleInput;
52
+ /** Step from the committed value and commit the result. */
53
+ private nudge;
54
+ /** True when a value sits inside the field's bounds. */
55
+ private isInRange;
56
+ /** Fold a value back into the bounds. */
57
+ private clamp;
58
+ /** Disable a stepper button at the edge of the range, or with the field. */
59
+ private syncStepperState;
60
+ }
@@ -0,0 +1,33 @@
1
+ import { Component } from "../component";
2
+ import "./progress.css";
3
+ export interface ProgressOptions {
4
+ /** Progress from 0 to 100. Ignored while `indeterminate` is set. */
5
+ value?: number;
6
+ /** Show the barber-pole used when the duration is unknown. */
7
+ indeterminate?: boolean;
8
+ }
9
+ /**
10
+ * A progress bar with a determinate and an indeterminate face.
11
+ *
12
+ * Both states are the same element; only the `is-` class and the ARIA surface
13
+ * change. Indeterminate removes `aria-valuenow` outright — a stale value would
14
+ * tell a screen reader a percentage that does not exist — while
15
+ * `aria-valuemin`/`aria-valuemax` stay, so the widget still describes its scale.
16
+ */
17
+ export declare class Progress extends Component<HTMLElement> {
18
+ protected readonly root: HTMLElement;
19
+ private readonly fill;
20
+ private indeterminate;
21
+ private current;
22
+ constructor(options?: ProgressOptions);
23
+ /** The percentage currently shown, 0–100. */
24
+ get value(): number;
25
+ /** Set the percentage; values outside 0–100 are folded into the range. */
26
+ setValue(value: number): void;
27
+ /** Switch between the determinate bar and the barber-pole. */
28
+ setIndeterminate(indeterminate: boolean): void;
29
+ /** Apply the ARIA surface and the class that match the current state. */
30
+ private syncState;
31
+ /** Fold a value into 0–100. */
32
+ private constrain;
33
+ }
@@ -0,0 +1,64 @@
1
+ import { Component } from "../component";
2
+ import "./segmented.css";
3
+ export interface SegmentedOption {
4
+ label: string;
5
+ value: string;
6
+ /** Optional leading SVG path data. */
7
+ icon?: string;
8
+ disabled?: boolean;
9
+ }
10
+ export interface SegmentedControlOptions {
11
+ options: SegmentedOption[];
12
+ /** Initially selected value. Defaults to the first enabled option. */
13
+ value?: string;
14
+ /** Stretch to the container's width. */
15
+ block?: boolean;
16
+ onChange?: (value: string) => void;
17
+ }
18
+ /**
19
+ * A macOS segmented control.
20
+ *
21
+ * Semantics follow the native control: it is a radio group, so it exposes
22
+ * `role="radiogroup"` with one `role="radio"` per segment, arrow keys move the
23
+ * selection, and only the selected segment is in the tab order (roving
24
+ * tabindex). That is what a screen-reader user expects from this widget.
25
+ *
26
+ * The selection pill is a single element moved with `transform`, so switching
27
+ * segments never re-lays-out the buttons themselves.
28
+ */
29
+ export declare class SegmentedControl extends Component<HTMLDivElement> {
30
+ protected readonly root: HTMLDivElement;
31
+ private readonly thumb;
32
+ private readonly buttons;
33
+ private readonly options;
34
+ private readonly onChange?;
35
+ private current;
36
+ constructor(options: SegmentedControlOptions);
37
+ /**
38
+ * Apply a selection.
39
+ *
40
+ * `notify` is true only for a user-initiated change (click or arrow key), so
41
+ * the programmatic `setValue` below stays silent — the same contract every
42
+ * other setter in the kit has.
43
+ */
44
+ private applySelection;
45
+ /**
46
+ * Position the selection pill.
47
+ *
48
+ * Uses `offsetLeft`/`offsetWidth` (layout reads) once per change, then
49
+ * writes only transform and a width variable, so the animation stays on the
50
+ * compositor.
51
+ *
52
+ * The measurement is skipped when the control has no layout box: a segmented
53
+ * control built inside a hidden tab panel reports 0 for every offset, which
54
+ * would collapse the pill to zero width and leave it at the far left. A
55
+ * ResizeObserver re-measures when the control finally gets a size.
56
+ */
57
+ private moveThumb;
58
+ /** The selected value. */
59
+ get value(): string;
60
+ /** Change the selection programmatically. */
61
+ setValue(value: string): void;
62
+ /** Disable every segment. */
63
+ setDisabled(disabled: boolean): void;
64
+ }
@@ -0,0 +1,42 @@
1
+ import { Component } from "../component";
2
+ import "./select.css";
3
+ export interface SelectOption {
4
+ label: string;
5
+ value: string;
6
+ disabled?: boolean;
7
+ }
8
+ export interface SelectOptions {
9
+ options: SelectOption[];
10
+ /** Initially selected value. Defaults to the first enabled option. */
11
+ value?: string;
12
+ disabled?: boolean;
13
+ /** Stretch to the container's width. */
14
+ block?: boolean;
15
+ /** Accessible name, since the control has no visible label. */
16
+ ariaLabel?: string;
17
+ onChange?: (value: string) => void;
18
+ }
19
+ /**
20
+ * A macOS pop-up button.
21
+ *
22
+ * Built on a real `<select>`. Replacing it with a custom listbox would mean
23
+ * reimplementing type-ahead, mobile wheel pickers, screen-reader announcement
24
+ * and the OS popup — a lot of behaviour to lose for a styling win. Only the
25
+ * appearance is replaced (`appearance: none` plus our own chevron).
26
+ */
27
+ export declare class Select extends Component<HTMLDivElement> {
28
+ protected readonly root: HTMLDivElement;
29
+ private readonly control;
30
+ private readonly onChange?;
31
+ constructor(options: SelectOptions);
32
+ /** The small dropdown chevron drawn over the select. */
33
+ private makeChevron;
34
+ /** Replace the option list. */
35
+ setOptions(options: SelectOption[], value?: string): void;
36
+ /** The selected value. */
37
+ get value(): string;
38
+ /** Change the selection programmatically. */
39
+ setValue(value: string): void;
40
+ /** Enable or disable the control. */
41
+ setDisabled(disabled: boolean): void;
42
+ }
@@ -0,0 +1,49 @@
1
+ import { Component } from "../component";
2
+ import { type Child } from "../dom";
3
+ import "./slider.css";
4
+ export interface SliderOptions {
5
+ /** Label rendered above the track; it is wired to the input through `for`/`id`. */
6
+ label?: Child;
7
+ /** Smallest value. Default 0. */
8
+ min?: number;
9
+ /** Largest value. Default 100. */
10
+ max?: number;
11
+ /** Increment between stops. Default 1. */
12
+ step?: number;
13
+ /** Initial value; defaults to `min`. */
14
+ value?: number;
15
+ /** Show the current value at the trailing edge. */
16
+ showValue?: boolean;
17
+ /** Fires on every move with the new number. */
18
+ onChange?: (value: number) => void;
19
+ }
20
+ /**
21
+ * A horizontal slider on a native `<input type="range">`.
22
+ *
23
+ * The range input already handles pointer capture, dragging, Home/End and the
24
+ * arrow keys, so the component only adds the macOS look and an optional
25
+ * read-out. The filled part of the track is a gradient stop driven by
26
+ * `--mac-slider-value`, so a move costs one custom-property write instead of a
27
+ * second element to resize.
28
+ */
29
+ export declare class Slider extends Component<HTMLElement> {
30
+ protected readonly root: HTMLElement;
31
+ private readonly input;
32
+ private readonly valueNode;
33
+ private readonly onChange?;
34
+ private readonly min;
35
+ private readonly max;
36
+ constructor(options?: SliderOptions);
37
+ /** The current value, clamped to the configured range. */
38
+ get value(): number;
39
+ /** Replace the value; out-of-range input is clamped rather than committed as-is. */
40
+ setValue(value: number): void;
41
+ /** Enable or disable the slider, using the native attribute. */
42
+ setDisabled(disabled: boolean): void;
43
+ /** Paint the filled part of the track up to the current value. */
44
+ private syncFill;
45
+ /** Keep the read-out in step with the value. */
46
+ private renderValue;
47
+ /** Fold a value back into the bounds. */
48
+ private clamp;
49
+ }
@@ -0,0 +1,24 @@
1
+ import { Component } from "../component";
2
+ import "./spinner.css";
3
+ export type SpinnerSize = "small" | "default" | "large";
4
+ export interface SpinnerOptions {
5
+ /** Optional caption beside the ring. */
6
+ label?: string;
7
+ size?: SpinnerSize;
8
+ /** Accessible name. Defaults to the label, then to a generic busy message. */
9
+ ariaLabel?: string;
10
+ }
11
+ /**
12
+ * An indeterminate activity indicator.
13
+ *
14
+ * Semantics matter here: the spinner is announced through `role="progressbar"`
15
+ * with no `aria-valuenow`, which is the standard way to say "busy, duration
16
+ * unknown". Setting a value would tell a screen reader the work is measurable.
17
+ */
18
+ export declare class Spinner extends Component<HTMLSpanElement> {
19
+ protected readonly root: HTMLSpanElement;
20
+ private readonly labelNode;
21
+ constructor(options?: SpinnerOptions);
22
+ /** Replace the visible caption. */
23
+ setLabel(label: string): void;
24
+ }
@@ -0,0 +1,76 @@
1
+ import { Component } from "../component";
2
+ import "./splitview.css";
3
+ /** Split direction: `horizontal` places the panes side by side. */
4
+ export type SplitViewOrientation = "horizontal" | "vertical";
5
+ export interface SplitViewOptions {
6
+ /** `horizontal` puts the panes left/right, `vertical` stacks them. */
7
+ orientation: SplitViewOrientation;
8
+ /** Content of the leading pane (left or top). */
9
+ primary?: Node;
10
+ /** Content of the trailing pane (right or bottom); it takes the remainder. */
11
+ secondary?: Node;
12
+ /** Initial primary size: pixels as a number, or any CSS length e.g. "40%". */
13
+ primarySize?: number | string;
14
+ /** Smallest allowed primary size in pixels. Default 0. */
15
+ minPrimary?: number;
16
+ /** Smallest allowed secondary size in pixels. Default 0. */
17
+ minSecondary?: number;
18
+ /** Fires while dragging and on keyboard resize, with the new size in pixels. */
19
+ onResize?: (size: number) => void;
20
+ }
21
+ /**
22
+ * A draggable two-pane split view.
23
+ *
24
+ * The primary pane carries an explicit `flex-basis` driven by a custom property
25
+ * (`--mac-splitview-primary`), so a drag writes one number instead of touching
26
+ * both panes' styles; the secondary pane simply takes the remainder. That also
27
+ * means a resize never re-enters layout twice per pointer event.
28
+ *
29
+ * The divider is a real focus stop: `role="separator"` with
30
+ * `aria-orientation`, `aria-valuenow/min/max` and arrow-key resizing, which is
31
+ * what assistive tech needs to expose an adjustable split.
32
+ *
33
+ * Dragging uses pointer capture, so a fast drag that outruns the one-pixel
34
+ * divider keeps delivering moves to it instead of losing the gesture.
35
+ */
36
+ export declare class SplitView extends Component<HTMLDivElement> {
37
+ protected readonly root: HTMLDivElement;
38
+ private readonly orientation;
39
+ private readonly primaryPane;
40
+ private readonly secondaryPane;
41
+ private readonly divider;
42
+ private readonly minPrimary;
43
+ private readonly minSecondary;
44
+ private readonly onResize?;
45
+ private dragging;
46
+ private dragPointer;
47
+ /** Pointer coordinate when the drag started. */
48
+ private dragStart;
49
+ /** Primary pane extent when the drag started. */
50
+ private dragOrigin;
51
+ constructor(options: SplitViewOptions);
52
+ /**
53
+ * Set the primary pane's size: a pixel number or any CSS length ("40%").
54
+ *
55
+ * A number is clamped like a drag would be, so the panes never violate the
56
+ * declared minimums. Silent — the caller already knows, and a programmatic
57
+ * resize is not a user gesture.
58
+ */
59
+ setPrimarySize(size: number | string): void;
60
+ private onPointerDown;
61
+ private onPointerMove;
62
+ private onPointerUp;
63
+ private endDrag;
64
+ private onKeyDown;
65
+ /** Clamp, write and (optionally) report a new primary size in pixels. */
66
+ private applySize;
67
+ /** One write to the custom property; the flex basis picks it up. */
68
+ private writeSize;
69
+ /** Fold a pixel size into the min/max the options declare. */
70
+ private clampSize;
71
+ private availableExtent;
72
+ private paneExtent;
73
+ private pointerAxis;
74
+ /** Keep `aria-valuenow/min/max` in step with the pixels actually on screen. */
75
+ private syncAria;
76
+ }
@@ -0,0 +1,47 @@
1
+ import { Component } from "../component";
2
+ import "./switch.css";
3
+ export interface SwitchOptions {
4
+ /** Initial on/off state. Default off. */
5
+ checked?: boolean;
6
+ disabled?: boolean;
7
+ /**
8
+ * Accessible name, exposed as `aria-label`. A switch is drawn as a bare
9
+ * track, so the label cannot sit inside the control itself.
10
+ */
11
+ label?: string;
12
+ /** Fires with the new state after a user toggle; setters stay silent. */
13
+ onChange?: (checked: boolean) => void;
14
+ }
15
+ /**
16
+ * A macOS switch on a native `<button role="switch">`.
17
+ *
18
+ * The track and the knob are two elements, and the knob travels with
19
+ * `transform` only: animating `left` or `width` would lay the control out on
20
+ * every frame (design rule 7). Pointer activation arrives as a `click`, while
21
+ * Space and Enter are handled on `keydown` so the state flips once per press
22
+ * and Space never scrolls the pane behind the switch.
23
+ */
24
+ export declare class Switch extends Component<HTMLElement> {
25
+ protected readonly root: HTMLButtonElement;
26
+ private readonly onChange?;
27
+ private isOn;
28
+ constructor(options?: SwitchOptions);
29
+ /** The current state. */
30
+ get checked(): boolean;
31
+ /** Set the state; a write that changes nothing is skipped, and `onChange` stays silent. */
32
+ setChecked(checked: boolean): void;
33
+ /** Enable or disable the switch, using the native attribute. */
34
+ setDisabled(disabled: boolean): void;
35
+ /** Move keyboard focus onto the switch. */
36
+ focus(): void;
37
+ /** Flip the state from a user gesture and report it. */
38
+ private toggle;
39
+ /**
40
+ * Write the state to `aria-checked`.
41
+ *
42
+ * That attribute is what the stylesheet slides the knob on and what a
43
+ * screen reader announces, so one write keeps the picture and the
44
+ * semantics in step.
45
+ */
46
+ private syncState;
47
+ }
@@ -0,0 +1,103 @@
1
+ import { Component } from "../component";
2
+ import "./table.css";
3
+ /** Horizontal alignment of a column's header and cells. */
4
+ export type TableAlign = "left" | "center" | "right";
5
+ /** One column definition. */
6
+ export interface TableColumn {
7
+ /** Property name read from each row object. */
8
+ key: string;
9
+ /** Header label. */
10
+ title: string;
11
+ /** Fixed width in CSS pixels; columns without one share the rest. */
12
+ width?: number;
13
+ /** Alignment for the header and every cell in the column. Default `left`. */
14
+ align?: TableAlign;
15
+ /**
16
+ * Custom cell renderer — the main extension point.
17
+ *
18
+ * Called once per cell during a render with the value at `column.key`, the
19
+ * whole row and the row index. Returning a `Node` lets the cell hold live
20
+ * UI (a badge, a button, a switch); returning a string renders as text.
21
+ * Omit it to get the default `String(value)` treatment.
22
+ */
23
+ render?: (value: any, row: any, index: number) => Node | string;
24
+ }
25
+ export interface TableOptions {
26
+ columns: TableColumn[];
27
+ rows: any[];
28
+ /**
29
+ * Stable identity per row, used by selection and by the `is-selected`
30
+ * class. Defaults to a primitive `row.id` when present, otherwise the row
31
+ * index — so pass one if rows are reordered or paginated.
32
+ */
33
+ rowKey?: (row: any, index: number) => string;
34
+ /** Render a leading checkbox column with select-all in the header. */
35
+ selectable?: boolean;
36
+ /** Keys selected on first paint; only meaningful with `selectable`. */
37
+ selectedKeys?: string[];
38
+ /** Fires after a user-driven selection change, with the new key list. */
39
+ onSelectionChange?: (keys: string[]) => void;
40
+ /** Text shown in place of rows when `rows` is empty. */
41
+ emptyText?: string;
42
+ }
43
+ /**
44
+ * A macOS data table.
45
+ *
46
+ * Built on a real `<table>` (`th scope="col"`, a body of `<tr>`s) so screen
47
+ * readers, header associations and text selection behave; the styling and the
48
+ * independent horizontal scroller are what this component adds.
49
+ *
50
+ * Rendering strategy: every row is built into a `DocumentFragment` and the
51
+ * body is swapped in one `replaceChildren()` call. Appending row by row would
52
+ * re-run layout once per row, which on a few hundred rows is the difference
53
+ * between one frame and a visible stall.
54
+ *
55
+ * Events are delegated from the (stable) body element, so re-rendering rows
56
+ * never leaks listeners.
57
+ */
58
+ export declare class Table extends Component<HTMLDivElement> {
59
+ protected readonly root: HTMLDivElement;
60
+ private readonly body;
61
+ private readonly columns;
62
+ private readonly rowKey;
63
+ private readonly selectable;
64
+ private readonly emptyText;
65
+ private readonly onSelectionChange?;
66
+ /** Selected row keys, in selection order. */
67
+ private readonly selected;
68
+ /** Select-all box in the header; null when the table is not selectable. */
69
+ private headerCheck;
70
+ /** Row checkboxes, index-aligned with `rowKeys`. */
71
+ private rowChecks;
72
+ /** Keys of the currently rendered rows, in row order. */
73
+ private rowKeys;
74
+ private rows;
75
+ constructor(options: TableOptions);
76
+ /**
77
+ * Replace the row data.
78
+ *
79
+ * Existing selection is kept for keys that still exist and dropped for
80
+ * keys that do not; like every setter in the kit, this does not raise
81
+ * `onSelectionChange`.
82
+ */
83
+ setRows(rows: any[]): void;
84
+ /** Keys of the selected rows, in the order they were selected. */
85
+ get selectedKeys(): string[];
86
+ /** Drop the selection without raising `onSelectionChange`. */
87
+ clearSelection(): void;
88
+ /** `<col>` per column, so `width` is only ever a hint on one element. */
89
+ private makeColGroup;
90
+ private makeHeaderRow;
91
+ /** Rebuild the whole body in one insertion. */
92
+ private renderRows;
93
+ private makeRow;
94
+ private makeCell;
95
+ private makeEmptyRow;
96
+ private onBodyChange;
97
+ /** Header box: check selects everything, uncheck clears everything. */
98
+ private toggleAll;
99
+ private syncRowChecks;
100
+ /** Header box state: unchecked, checked, or the mixed dash. */
101
+ private syncHeaderCheck;
102
+ private emitSelection;
103
+ }
@@ -0,0 +1,49 @@
1
+ import { Component } from "../component";
2
+ import { type Child } from "../dom";
3
+ import "./textarea.css";
4
+ export interface TextAreaOptions {
5
+ /** Label rendered above the field. */
6
+ label?: Child;
7
+ placeholder?: string;
8
+ /** Initial text. */
9
+ value?: string;
10
+ /** Visible rows; drives the initial height. */
11
+ rows?: number;
12
+ /** Hard limit; shows a counter when set. */
13
+ maxLength?: number;
14
+ disabled?: boolean;
15
+ readOnly?: boolean;
16
+ onInput?: (value: string) => void;
17
+ onChange?: (value: string) => void;
18
+ }
19
+ /**
20
+ * A multi-line text field.
21
+ *
22
+ * Built on a real `<textarea>` for the same reason TextField uses a real input:
23
+ * caret navigation, IME composition, undo and selection are the browser's, and a
24
+ * `div[contenteditable]` would mean reimplementing them badly.
25
+ *
26
+ * Like TextField, `onInput` fires per keystroke and `onChange` only on commit
27
+ * (blur), which is what the DOM's own event means.
28
+ */
29
+ export declare class TextArea extends Component<HTMLDivElement> {
30
+ protected readonly root: HTMLDivElement;
31
+ private readonly control;
32
+ private readonly counter;
33
+ private readonly maxLength?;
34
+ private readonly onChange?;
35
+ constructor(options?: TextAreaOptions);
36
+ /** The current text. */
37
+ get value(): string;
38
+ /** Replace the text. */
39
+ setValue(value: string): void;
40
+ /** Enable or disable the editor. */
41
+ setDisabled(disabled: boolean): void;
42
+ /** Move keyboard focus into the editor. */
43
+ focus(): void;
44
+ /**
45
+ * Keep the character counter in step, and flag the field when it is at or
46
+ * over the limit.
47
+ */
48
+ private updateCounter;
49
+ }
@@ -0,0 +1,63 @@
1
+ import { Component } from "../component";
2
+ import { type Child } from "../dom";
3
+ import "./textfield.css";
4
+ export interface TextFieldOptions {
5
+ /** Label rendered above the field; it is wired to the input through `for`/`id`. */
6
+ label?: Child;
7
+ /** Placeholder shown while the field is empty. */
8
+ placeholder?: string;
9
+ /** Leading SVG path data, drawn at the control's icon size. */
10
+ icon?: string;
11
+ /** Show a clear button at the trailing edge once the field holds text. */
12
+ clearable?: boolean;
13
+ disabled?: boolean;
14
+ readOnly?: boolean;
15
+ /** Initial value. */
16
+ value?: string;
17
+ /** Fires on every keystroke with the current value. */
18
+ onInput?: (value: string) => void;
19
+ /**
20
+ * Fires when the value is committed: blur, Enter, or the clear button.
21
+ * Typing alone only raises `onInput`.
22
+ */
23
+ onChange?: (value: string) => void;
24
+ }
25
+ /**
26
+ * A single-line text field on a native `<input type="text">`.
27
+ *
28
+ * The input is the real element rather than a styled `div`, so the caret,
29
+ * selection, IME composition and `readOnly` come from the browser. The label
30
+ * gets a generated id and is linked with `for`/`id`, so clicking it focuses the
31
+ * field without a line of JS.
32
+ *
33
+ * `onInput` reports every keystroke; `onChange` reports committed values only,
34
+ * which is what the DOM's own `change` event means.
35
+ */
36
+ export declare class TextField extends Component<HTMLElement> {
37
+ protected readonly root: HTMLElement;
38
+ private readonly input;
39
+ private readonly clearButton;
40
+ private readonly onInput?;
41
+ private readonly onChange?;
42
+ /** True while the value has changed since the last commit. */
43
+ private dirty;
44
+ constructor(options?: TextFieldOptions);
45
+ /** The current value. */
46
+ get value(): string;
47
+ /** Replace the value; does not raise the callbacks, as a DOM write would not. */
48
+ setValue(value: string): void;
49
+ /** Enable or disable the input, using the native attribute. */
50
+ setDisabled(disabled: boolean): void;
51
+ /** Move keyboard focus into the input. */
52
+ focus(): void;
53
+ /**
54
+ * Report the value once per edit.
55
+ *
56
+ * Enter can be followed by a native `change` on blur, so a commit only fires
57
+ * while the field is dirty; `force` is for the clear button, which must raise
58
+ * `onChange` even when nothing was typed.
59
+ */
60
+ private commit;
61
+ /** The clear button only appears when there is something to clear. */
62
+ private syncClearState;
63
+ }
package/dist/dom.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Small DOM helpers shared by every macOS UI Kit component.
3
+ *
4
+ * The kit builds elements imperatively instead of shipping a framework: a
5
+ * desktop window creates its UI once and then updates small pieces of it, which
6
+ * is exactly the workload where direct DOM work is fastest and a framework
7
+ * runtime is pure overhead.
8
+ */
9
+ /** Anything that can be placed inside an element. */
10
+ export type Child = Node | string | number | null | undefined | false;
11
+ /**
12
+ * Create an element with attributes and children in one call.
13
+ *
14
+ * Attributes are assigned as properties when the name is a known property on
15
+ * the element (so `value`, `checked`, `disabled` behave correctly) and as
16
+ * attributes otherwise. `class`, `dataset`, `style`, `on*` handlers and
17
+ * `textContent` get special treatment.
18
+ */
19
+ export declare function el<K extends keyof HTMLElementTagNameMap>(tag: K, attrs?: Attrs, ...children: Child[]): HTMLElementTagNameMap[K];
20
+ export type Attrs = Record<string, unknown>;
21
+ /** Append children, skipping null/undefined/false and stringifying numbers. */
22
+ export declare function append(parent: Element, children: Child[]): void;
23
+ /** Remove every child of a node. */
24
+ export declare function clear(node: Element): void;
25
+ /** Create an SVG element from a path definition, sized for a control icon. */
26
+ export declare function svgIcon(path: string, size?: number, viewBox?: string): SVGSVGElement;
27
+ /**
28
+ * Run `fn` on the next animation frame, coalescing repeated calls.
29
+ *
30
+ * Scroll and resize handlers use this so a burst of events produces one layout
31
+ * read instead of dozens.
32
+ */
33
+ export declare function rafThrottle(fn: () => void): () => void;
34
+ /** Generate a unique id, used to link labels to controls. */
35
+ export declare function uid(prefix?: string): string;