eidotter 0.37.2 → 0.37.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/RetroEffects/components/RetroEffects.d.ts +28 -5
- package/dist/components/RetroEffects/components/RetroEffects.js +24 -8
- package/dist/components/Select/components/Select.d.ts +69 -17
- package/dist/components/Select/components/Select.js +95 -8
- package/dist/components/registry.js +23 -0
- package/dist/eidotter.css +1 -1
- package/dist/index.es.js +6822 -3031
- package/dist/index.umd.js +4 -4
- package/package.json +1 -1
|
@@ -28,15 +28,38 @@ export interface RetroEffectsProps {
|
|
|
28
28
|
* it, and a warm phosphor glow settles. Skipped entirely under
|
|
29
29
|
* prefers-reduced-motion. Opt-in; intended as the portfolio-wide launch
|
|
30
30
|
* pattern (DMNC-1047).
|
|
31
|
+
*
|
|
32
|
+
* The sequence opens with a full-viewport opaque blackout that is part of the
|
|
33
|
+
* server-rendered HTML, so the page is never visible before the turn-on — the
|
|
34
|
+
* animation is the reveal, with no flash of content before boot (DMNC-1184).
|
|
35
|
+
* The open is pure CSS, so it works before hydration and with JS disabled
|
|
36
|
+
* (the cover self-resolves; nothing stays hidden).
|
|
37
|
+
*
|
|
38
|
+
* Assumes the default `powered` (on). Combining `boot` with `powered={false}`
|
|
39
|
+
* is contradictory — the power-off transform collapses the whole overlay,
|
|
40
|
+
* including the boot cover — so the no-flash guarantee does not apply there.
|
|
31
41
|
*/
|
|
32
42
|
boot?: boolean;
|
|
33
43
|
/**
|
|
34
44
|
* Gate `boot` to once per browser tab/session (via sessionStorage). The boot
|
|
35
|
-
* sequence plays on the first load of a tab
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
45
|
+
* sequence plays on the first load of a tab; on later mounts within the same
|
|
46
|
+
* tab the session flag suppresses it. A new tab/visit replays it. No-op unless
|
|
47
|
+
* `boot` is also set. Falls back to playing if storage is unavailable (e.g.
|
|
48
|
+
* private mode).
|
|
49
|
+
*
|
|
50
|
+
* **SSR caveat (DMNC-1184):** because the no-flash cover ships in the server
|
|
51
|
+
* HTML and the server can't read `sessionStorage`, suppression on a returning
|
|
52
|
+
* visitor is decided post-hydration. The practical effect by navigation type:
|
|
53
|
+
* - **SPA route change** (overlay lives in a persistent layout, no remount):
|
|
54
|
+
* never shows the cover — fully suppressed, as before.
|
|
55
|
+
* - **Full reload / hard refresh** (a returning tab re-mounts the overlay):
|
|
56
|
+
* the SSR cover paints, then JS tears it down on hydration. On a fast host
|
|
57
|
+
* that's a brief blackout (the panels' 140ms open delay means the turn-on
|
|
58
|
+
* usually isn't reached before teardown); only a slow hydration lets the
|
|
59
|
+
* turn-on visibly replay. It never flashes bare content.
|
|
60
|
+
* This is the deliberate trade for a flash-free first paint; eliminating the
|
|
61
|
+
* brief returning-visitor blackout entirely needs server-visible session state
|
|
62
|
+
* (a cookie or a pre-paint inline script), which is consumer-side.
|
|
40
63
|
*
|
|
41
64
|
* Like `boot`, this is evaluated once on mount; changing it afterwards has no
|
|
42
65
|
* effect (remount the component — e.g. via `key` — to re-evaluate).
|
|
@@ -42,11 +42,18 @@ const markBootedThisSession = (key) => {
|
|
|
42
42
|
export const RetroEffects = ({ scanlines = true, glow = true, flicker = true, bloom = false, powered = true, boot = false, bootOnce = false, bootStorageKey = DEFAULT_BOOT_STORAGE_KEY, intensity = 1, className, onPowerStateChange, onPowerOn, onPowerOff, onBootComplete, }) => {
|
|
43
43
|
const prevPoweredRef = useRef(powered);
|
|
44
44
|
const [powerState, setPowerState] = useState(powered ? 'on' : 'off');
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
45
|
+
// The opaque boot cover renders on first paint for BOTH the gated and
|
|
46
|
+
// non-gated paths whenever `boot` is set. This is the no-FOUC fix (DMNC-1184):
|
|
47
|
+
// the full-viewport panels are present in the server HTML, so the page is
|
|
48
|
+
// never visible before the turn-on — the animation is the reveal. `booting`
|
|
49
|
+
// mirrors the server value on the client's first render (no sessionStorage
|
|
50
|
+
// read here), so there is no hydration mismatch. The gated (bootOnce) decision
|
|
51
|
+
// is made post-mount: if this tab already booted, the cover is torn down by
|
|
52
|
+
// the decision effect once it runs. The server can't read sessionStorage, so a
|
|
53
|
+
// returning visitor's full reload paints the cover and suppresses it on
|
|
54
|
+
// hydration (a brief blackout, not a bare-content flash) — see the bootOnce
|
|
55
|
+
// JSDoc for the SSR trade.
|
|
56
|
+
const [booting, setBooting] = useState(boot);
|
|
50
57
|
// Boot completes exactly once per mount. Guards against the StrictMode dev
|
|
51
58
|
// double-invoke of effects and the (theoretical) animationend + safety-timeout
|
|
52
59
|
// race both calling through twice.
|
|
@@ -85,12 +92,16 @@ export const RetroEffects = ({ scanlines = true, glow = true, flicker = true, bl
|
|
|
85
92
|
if (!boot || !bootOnce)
|
|
86
93
|
return;
|
|
87
94
|
if (hasBootedThisSession(bootStorageKeyRef.current)) {
|
|
88
|
-
// Already shown this session —
|
|
89
|
-
//
|
|
95
|
+
// Already shown this session — tear down the SSR cover (it was rendered to
|
|
96
|
+
// prevent the first-paint flash, DMNC-1184) and signal completion so
|
|
97
|
+
// consumers sequencing boot text off onBootComplete don't stall. No replay.
|
|
98
|
+
setBooting(false);
|
|
90
99
|
signalBootComplete();
|
|
91
100
|
return;
|
|
92
101
|
}
|
|
93
|
-
|
|
102
|
+
// First visit this tab — keep the cover up; it plays and records the session
|
|
103
|
+
// flag on completion (`completeBoot`). `booting` is already true from the
|
|
104
|
+
// initial state, so there is nothing to set here.
|
|
94
105
|
// Mount-only: gating is a first-load decision, not reactive to prop churn.
|
|
95
106
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
96
107
|
}, []);
|
|
@@ -100,6 +111,11 @@ export const RetroEffects = ({ scanlines = true, glow = true, flicker = true, bl
|
|
|
100
111
|
useEffect(() => {
|
|
101
112
|
if (!booting)
|
|
102
113
|
return;
|
|
114
|
+
// Already settled (e.g. the gated already-booted branch tore the SSR cover
|
|
115
|
+
// down and signalled in the decision effect, which runs first): don't arm a
|
|
116
|
+
// safety timer that we'd immediately clear on the next render.
|
|
117
|
+
if (bootSignaledRef.current)
|
|
118
|
+
return;
|
|
103
119
|
if (prefersReducedMotion()) {
|
|
104
120
|
completeBoot();
|
|
105
121
|
return;
|
|
@@ -1,31 +1,83 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
export interface SelectOption {
|
|
3
|
+
/** Stable value submitted / reported on change. */
|
|
3
4
|
value: string;
|
|
5
|
+
/** Visible label. */
|
|
4
6
|
label: string;
|
|
7
|
+
/** Disable this single option. */
|
|
8
|
+
disabled?: boolean;
|
|
5
9
|
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
value: string;
|
|
10
|
+
type SelectSize = 'sm' | 'md' | 'lg' | 'small' | 'medium' | 'large';
|
|
11
|
+
interface SelectBaseProps {
|
|
9
12
|
/** Options to render. */
|
|
10
13
|
options: SelectOption[];
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
id?: string;
|
|
15
|
-
/** Accessible label when there is no visible `<label>`. */
|
|
14
|
+
/** Visible field label (rendered above the control). */
|
|
15
|
+
label?: string;
|
|
16
|
+
/** Accessible label when there is no visible `label`. */
|
|
16
17
|
'aria-label'?: string;
|
|
17
|
-
/**
|
|
18
|
+
/** Text shown when nothing is selected. */
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
/** Helper text below the control. */
|
|
21
|
+
description?: string;
|
|
22
|
+
/** Error message — shown (and marks the field invalid) when `variant="error"`. */
|
|
23
|
+
errorMessage?: string;
|
|
24
|
+
/** Visual / validation variant. */
|
|
25
|
+
variant?: 'default' | 'error';
|
|
26
|
+
/** Control size. small/medium/large are @deprecated aliases for sm/md/lg. */
|
|
27
|
+
size?: SelectSize;
|
|
28
|
+
/** Disable the whole control. */
|
|
18
29
|
disabled?: boolean;
|
|
19
|
-
/**
|
|
30
|
+
/** Mark the field required (React Aria). */
|
|
31
|
+
isRequired?: boolean;
|
|
32
|
+
/** Enable type-to-filter (ComboBox for single-select; filter input for multi-select). */
|
|
33
|
+
searchable?: boolean;
|
|
34
|
+
/** Placeholder for the search/filter input (searchable variants). */
|
|
35
|
+
searchPlaceholder?: string;
|
|
36
|
+
/** Message shown when filtering matches nothing. */
|
|
37
|
+
emptyMessage?: React.ReactNode;
|
|
38
|
+
/** Form field name (a hidden native input is emitted for form submission). */
|
|
39
|
+
name?: string;
|
|
40
|
+
/** Id applied to the focusable control (for external `<label htmlFor>`). */
|
|
41
|
+
id?: string;
|
|
42
|
+
/** Additional CSS class merged onto the root. */
|
|
20
43
|
className?: string;
|
|
21
44
|
}
|
|
45
|
+
interface SingleSelectProps extends SelectBaseProps {
|
|
46
|
+
/** Single-selection mode (default). */
|
|
47
|
+
multiple?: false;
|
|
48
|
+
/** Selected value (controlled). Empty string means nothing selected. */
|
|
49
|
+
value?: string;
|
|
50
|
+
/** Initial value (uncontrolled). */
|
|
51
|
+
defaultValue?: string;
|
|
52
|
+
/** Fired with the new value on change. */
|
|
53
|
+
onChange?: (value: string) => void;
|
|
54
|
+
}
|
|
55
|
+
interface MultiSelectProps extends SelectBaseProps {
|
|
56
|
+
/** Multi-selection mode — `value`/`onChange` operate on string arrays. */
|
|
57
|
+
multiple: true;
|
|
58
|
+
/** Selected values (controlled). */
|
|
59
|
+
value?: string[];
|
|
60
|
+
/** Initial values (uncontrolled). */
|
|
61
|
+
defaultValue?: string[];
|
|
62
|
+
/** Fired with the new array of values on change. */
|
|
63
|
+
onChange?: (value: string[]) => void;
|
|
64
|
+
}
|
|
65
|
+
export type SelectProps = SingleSelectProps | MultiSelectProps;
|
|
22
66
|
/**
|
|
23
|
-
* DOS-styled
|
|
24
|
-
*
|
|
67
|
+
* DOS-styled Select / Dropdown built on React Aria primitives (DMNC-593).
|
|
68
|
+
*
|
|
69
|
+
* Three modes via props:
|
|
70
|
+
* - **default** — single-select `Select` (button + popover + listbox)
|
|
71
|
+
* - **`searchable`** — single-select `ComboBox` with type-to-filter
|
|
72
|
+
* - **`multiple`** — multi-select listbox; `value`/`onChange` use string arrays
|
|
73
|
+
* (combine with `searchable` for a filterable multi-select)
|
|
25
74
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* (token-driven). Upstream component fix tracked at eidotter#349.
|
|
75
|
+
* React Aria handles keyboard nav, focus management, and ARIA; phosphor glow on
|
|
76
|
+
* focus/open lives in `Select.css`. The single-value API (`value` / `options` /
|
|
77
|
+
* `onChange`) is backward-compatible with the previous native-`<select>` MVP.
|
|
30
78
|
*/
|
|
31
|
-
export declare const Select:
|
|
79
|
+
export declare const Select: {
|
|
80
|
+
(props: SelectProps): React.ReactElement;
|
|
81
|
+
displayName: string;
|
|
82
|
+
};
|
|
83
|
+
export {};
|
|
@@ -1,14 +1,101 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useId, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { Select as AriaSelect, SelectValue as AriaSelectValue, ComboBox as AriaComboBox, ListBox as AriaListBox, ListBoxItem as AriaListBoxItem, Popover as AriaPopover, Button as AriaButton, Input as AriaInput, Label as AriaLabel, Text as AriaText, } from 'react-aria-components';
|
|
3
5
|
import { cn } from '../../../utils/cn.js';
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Shared style fragments — layout in Tailwind, phosphor in Select.css
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
const sizeTrigger = {
|
|
10
|
+
sm: 'text-dos-text-sm px-2.5 py-1 min-h-7 gap-2',
|
|
11
|
+
md: 'text-dos-text-md px-3 py-2 min-h-8 gap-2',
|
|
12
|
+
lg: 'text-dos-text-lg px-4 py-2.5 min-h-10 gap-2',
|
|
13
|
+
};
|
|
14
|
+
const normalizeSize = (size = 'md') => ({ small: 'sm', medium: 'md', large: 'lg' }[size] ?? size);
|
|
15
|
+
const labelClass = 'eidotter-select__label text-dos-text-brand font-dos text-dos-text-xs uppercase tracking-wider';
|
|
16
|
+
const descClass = 'eidotter-select__description text-dos-text-muted font-dos text-dos-text-xs';
|
|
17
|
+
const errClass = 'eidotter-select__error text-dos-text-error font-dos text-dos-text-xs';
|
|
18
|
+
const triggerBase = 'eidotter-select__trigger flex items-center w-full bg-transparent text-dos-text-brand font-dos ' +
|
|
19
|
+
'border-2 border-dos-border-default outline-none box-border';
|
|
20
|
+
const optionClass = 'eidotter-select__option font-dos';
|
|
21
|
+
const DEFAULT_EMPTY = 'NO MATCHES';
|
|
22
|
+
/** Map the empty `''` sentinel / nullish to React Aria's `null` (placeholder). */
|
|
23
|
+
const toKey = (value) => (value == null || value === '' ? null : value);
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Single-select — React Aria Select (button + popover + listbox)
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
const SingleSelect = ({ options, label, placeholder, description, errorMessage, variant = 'default', size, value, defaultValue, onChange, disabled, isRequired, name, id, className, 'aria-label': ariaLabel, }) => {
|
|
28
|
+
const isInvalid = variant === 'error';
|
|
29
|
+
const isControlled = value !== undefined;
|
|
30
|
+
const sz = normalizeSize(size);
|
|
31
|
+
return (_jsxs(AriaSelect, { "aria-label": !label ? ariaLabel : undefined, selectedKey: isControlled ? toKey(value) : undefined, defaultSelectedKey: !isControlled ? defaultValue : undefined, onSelectionChange: (key) => onChange?.(key == null ? '' : String(key)), isDisabled: disabled, isInvalid: isInvalid, isRequired: isRequired, name: name, placeholder: placeholder, className: cn('eidotter-select flex flex-col gap-1', className), children: [label && _jsx(AriaLabel, { className: labelClass, children: label }), _jsxs(AriaButton, { id: id, className: cn(triggerBase, sizeTrigger[sz]), children: [_jsx(AriaSelectValue, { className: "eidotter-select__value flex-1 text-left truncate" }), _jsx("span", { "aria-hidden": "true", className: "eidotter-select__arrow", children: "\u25BC" })] }), description && !isInvalid && (_jsx(AriaText, { slot: "description", className: descClass, children: description })), errorMessage && isInvalid && (_jsx(AriaText, { slot: "errorMessage", className: errClass, children: errorMessage })), _jsx(AriaPopover, { className: "eidotter-select__popover", children: _jsx(AriaListBox, { className: "eidotter-select__listbox", items: options, children: (item) => (_jsx(AriaListBoxItem, { id: item.value, textValue: item.label, isDisabled: item.disabled, className: optionClass, children: item.label })) }) })] }));
|
|
32
|
+
};
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Searchable single-select — React Aria ComboBox (input + popover + listbox)
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
const SearchableSelect = ({ options, label, placeholder, description, errorMessage, variant = 'default', size, value, defaultValue, onChange, disabled, isRequired, name, id, className, emptyMessage = DEFAULT_EMPTY, 'aria-label': ariaLabel, }) => {
|
|
37
|
+
const isInvalid = variant === 'error';
|
|
38
|
+
const isControlled = value !== undefined;
|
|
39
|
+
const sz = normalizeSize(size);
|
|
40
|
+
return (_jsxs(AriaComboBox, { "aria-label": !label ? ariaLabel : undefined, defaultItems: options, selectedKey: isControlled ? toKey(value) : undefined, defaultSelectedKey: !isControlled ? defaultValue : undefined, onSelectionChange: (key) => onChange?.(key == null ? '' : String(key)), isDisabled: disabled, isInvalid: isInvalid, isRequired: isRequired, name: name, menuTrigger: "focus", allowsEmptyCollection: true, className: cn('eidotter-select eidotter-select--searchable flex flex-col gap-1', className), children: [label && _jsx(AriaLabel, { className: labelClass, children: label }), _jsxs("div", { className: cn(triggerBase, sizeTrigger[sz]), children: [_jsx(AriaInput, { id: id, placeholder: placeholder, className: "eidotter-select__input flex-1 min-w-0 bg-transparent text-inherit outline-none" }), _jsx(AriaButton, { className: "eidotter-select__arrow-btn", "aria-label": "Show options", children: _jsx("span", { "aria-hidden": "true", className: "eidotter-select__arrow", children: "\u25BC" }) })] }), description && !isInvalid && (_jsx(AriaText, { slot: "description", className: descClass, children: description })), errorMessage && isInvalid && (_jsx(AriaText, { slot: "errorMessage", className: errClass, children: errorMessage })), _jsx(AriaPopover, { className: "eidotter-select__popover", children: _jsx(AriaListBox, { className: "eidotter-select__listbox", renderEmptyState: () => _jsx("div", { className: "eidotter-select__empty", children: emptyMessage }), children: (item) => (_jsx(AriaListBoxItem, { id: item.value, textValue: item.label, isDisabled: item.disabled, className: optionClass, children: item.label })) }) })] }));
|
|
41
|
+
};
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Multi-select — Button trigger + standalone Popover + multi ListBox
|
|
44
|
+
// (React Aria has no high-level multi-select; this composes the primitives.)
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
const MultiSelect = ({ options, label, placeholder = 'Select…', description, errorMessage, variant = 'default', size, value, defaultValue, onChange, disabled, name, id, className, searchable, searchPlaceholder = 'FILTER…', emptyMessage = DEFAULT_EMPTY, 'aria-label': ariaLabel, }) => {
|
|
47
|
+
const isInvalid = variant === 'error';
|
|
48
|
+
const isControlled = value !== undefined;
|
|
49
|
+
const sz = normalizeSize(size);
|
|
50
|
+
const triggerRef = useRef(null);
|
|
51
|
+
const [open, setOpen] = useState(false);
|
|
52
|
+
const [query, setQuery] = useState('');
|
|
53
|
+
const [internal, setInternal] = useState(defaultValue ?? []);
|
|
54
|
+
const selected = isControlled ? value ?? [] : internal;
|
|
55
|
+
const baseId = useId();
|
|
56
|
+
const labelId = `${baseId}-label`;
|
|
57
|
+
const selectedSet = useMemo(() => new Set(isControlled ? value ?? [] : internal), [isControlled, value, internal]);
|
|
58
|
+
const labelMap = useMemo(() => new Map(options.map((o) => [o.value, o.label])), [options]);
|
|
59
|
+
const filtered = useMemo(() => {
|
|
60
|
+
if (!searchable || !query.trim())
|
|
61
|
+
return options;
|
|
62
|
+
const q = query.trim().toLowerCase();
|
|
63
|
+
return options.filter((o) => o.label.toLowerCase().includes(q));
|
|
64
|
+
}, [options, searchable, query]);
|
|
65
|
+
const handleChange = (selection) => {
|
|
66
|
+
const next = selection === 'all' ? options.map((o) => o.value) : Array.from(selection, String);
|
|
67
|
+
if (!isControlled)
|
|
68
|
+
setInternal(next);
|
|
69
|
+
onChange?.(next);
|
|
70
|
+
};
|
|
71
|
+
// Reset the filter each time the popover opens so stale queries don't hide
|
|
72
|
+
// options on the next open.
|
|
73
|
+
const handleOpenChange = (next) => {
|
|
74
|
+
setOpen(next);
|
|
75
|
+
if (next)
|
|
76
|
+
setQuery('');
|
|
77
|
+
};
|
|
78
|
+
const triggerText = selected.length === 0 ? placeholder : selected.map((v) => labelMap.get(v) ?? v).join(', ');
|
|
79
|
+
return (_jsxs("div", { className: cn('eidotter-select eidotter-select--multi flex flex-col gap-1', className), children: [label && (_jsx("span", { id: labelId, className: labelClass, children: label })), _jsxs(AriaButton, { ref: triggerRef, id: id, isDisabled: disabled, "aria-label": !label ? ariaLabel : undefined, "aria-labelledby": label ? labelId : undefined, "aria-haspopup": "listbox", "aria-expanded": open, onPress: () => handleOpenChange(!open), className: cn(triggerBase, sizeTrigger[sz], isInvalid && 'eidotter-select__trigger--error'), children: [_jsx("span", { className: cn('eidotter-select__value flex-1 text-left truncate', selected.length === 0 && 'eidotter-select__value--placeholder text-dos-text-muted'), children: triggerText }), _jsx("span", { "aria-hidden": "true", className: "eidotter-select__arrow", children: "\u25BC" })] }), description && !isInvalid && _jsx("span", { className: descClass, children: description }), errorMessage && isInvalid && _jsx("span", { className: errClass, children: errorMessage }), name && selected.map((v) => _jsx("input", { type: "hidden", name: name, value: v }, v)), _jsxs(AriaPopover, { triggerRef: triggerRef, isOpen: open, onOpenChange: handleOpenChange, className: "eidotter-select__popover", children: [searchable && (_jsx("input", { autoFocus: true, type: "text", className: "eidotter-select__search font-dos", placeholder: searchPlaceholder, value: query, onChange: (e) => setQuery(e.target.value), "aria-label": "Filter options" })), _jsx(AriaListBox, { "aria-label": ariaLabel || label || 'Options', selectionMode: "multiple", selectedKeys: selectedSet, onSelectionChange: handleChange, items: filtered, autoFocus: searchable ? undefined : 'first', renderEmptyState: () => _jsx("div", { className: "eidotter-select__empty", children: emptyMessage }), className: "eidotter-select__listbox", children: (item) => (_jsx(AriaListBoxItem, { id: item.value, textValue: item.label, isDisabled: item.disabled, className: cn(optionClass, 'eidotter-select__option--multi'), children: ({ isSelected }) => (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", className: "eidotter-select__check", children: isSelected ? '[x]' : '[ ]' }), _jsx("span", { className: "flex-1", children: item.label })] })) })) })] })] }));
|
|
80
|
+
};
|
|
4
81
|
/**
|
|
5
|
-
* DOS-styled
|
|
6
|
-
* Ported from Steuerdash.
|
|
82
|
+
* DOS-styled Select / Dropdown built on React Aria primitives (DMNC-593).
|
|
7
83
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
84
|
+
* Three modes via props:
|
|
85
|
+
* - **default** — single-select `Select` (button + popover + listbox)
|
|
86
|
+
* - **`searchable`** — single-select `ComboBox` with type-to-filter
|
|
87
|
+
* - **`multiple`** — multi-select listbox; `value`/`onChange` use string arrays
|
|
88
|
+
* (combine with `searchable` for a filterable multi-select)
|
|
89
|
+
*
|
|
90
|
+
* React Aria handles keyboard nav, focus management, and ARIA; phosphor glow on
|
|
91
|
+
* focus/open lives in `Select.css`. The single-value API (`value` / `options` /
|
|
92
|
+
* `onChange`) is backward-compatible with the previous native-`<select>` MVP.
|
|
12
93
|
*/
|
|
13
|
-
export const Select = (
|
|
94
|
+
export const Select = (props) => {
|
|
95
|
+
if (props.multiple)
|
|
96
|
+
return _jsx(MultiSelect, { ...props });
|
|
97
|
+
if (props.searchable)
|
|
98
|
+
return _jsx(SearchableSelect, { ...props });
|
|
99
|
+
return _jsx(SingleSelect, { ...props });
|
|
100
|
+
};
|
|
14
101
|
Select.displayName = 'Select';
|
|
@@ -308,6 +308,29 @@ export const componentRegistry = {
|
|
|
308
308
|
{ version: '0.3.0', type: 'added', description: 'Initial RetroEffects — scanlines, glow, flicker, bloom, intensity, powered' },
|
|
309
309
|
],
|
|
310
310
|
},
|
|
311
|
+
Select: {
|
|
312
|
+
origin: 'steuerdash',
|
|
313
|
+
consumers: ['steuerdash'],
|
|
314
|
+
since: '0.36.0',
|
|
315
|
+
originNote: 'DOS-styled dropdown. MVP was a native <select> ported from Steuerdash; rebuilt on React Aria Select/ComboBox in DMNC-593 with default/searchable/multi modes',
|
|
316
|
+
variants: {
|
|
317
|
+
'mode:default': { description: 'Single-select — React Aria Select (button + popover + listbox)', since: '0.38.0', usedBy: ['steuerdash'] },
|
|
318
|
+
'searchable:true': { description: 'Type-to-filter ComboBox single-select', since: '0.38.0' },
|
|
319
|
+
'multiple:true': { description: 'Multi-select listbox; value/onChange use string arrays', since: '0.38.0' },
|
|
320
|
+
'variant:error': { description: 'Invalid state — red border + errorMessage slot', since: '0.38.0' },
|
|
321
|
+
'size:sm': { description: 'Compact trigger (28px min-height)', since: '0.38.0' },
|
|
322
|
+
'size:md': { description: 'Standard trigger (32px min-height)', since: '0.38.0', usedBy: ['steuerdash'] },
|
|
323
|
+
'size:lg': { description: 'Tall trigger (40px min-height)', since: '0.38.0' },
|
|
324
|
+
},
|
|
325
|
+
platforms: {
|
|
326
|
+
react: { path: 'src/components/Select', status: 'canonical' },
|
|
327
|
+
swiftui: { status: 'planned' },
|
|
328
|
+
},
|
|
329
|
+
changelog: [
|
|
330
|
+
{ version: '0.38.0', type: 'changed', description: 'Rebuild on React Aria (Select/ComboBox/ListBox) with phosphor glow; add searchable + multiple modes, label/description/error chrome, sizes. Backward-compatible value/options/onChange API (DMNC-593)' },
|
|
331
|
+
{ version: '0.36.0', type: 'added', description: 'Initial Select — native <select> MVP ported from Steuerdash (DMNC-860)' },
|
|
332
|
+
],
|
|
333
|
+
},
|
|
311
334
|
Skeleton: { origin: 'eidotter', consumers: ['dmnctech'], originNote: 'DOS/CGA loading placeholder — shade-character rows (\u2591\u2592\u2593) with CRT hum-bar band rolling top\u2192bottom (DMNC-1018)' },
|
|
312
335
|
Stat: {
|
|
313
336
|
origin: 'steuerdash',
|