@getaster/elements 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +4 -0
- package/README.md +13 -4
- package/bin/aster-theme-check.js +35 -4
- package/dist/base/element.d.ts +8 -1
- package/dist/base/element.js +7 -2
- package/dist/button/button.d.ts +17 -2
- package/dist/button/button.js +42 -8
- package/dist/composition/builtins.d.ts +623 -1
- package/dist/composition/builtins.js +232 -4
- package/dist/date/calendar.d.ts +3 -0
- package/dist/date/calendar.js +8 -2
- package/dist/date/date-field.d.ts +23 -3
- package/dist/date/date-field.js +77 -9
- package/dist/date/dates.d.ts +10 -2
- package/dist/date/dates.js +23 -3
- package/dist/dialog/config.d.ts +4 -0
- package/dist/dialog/config.js +8 -0
- package/dist/dialog/dialog.d.ts +34 -6
- package/dist/dialog/dialog.js +151 -14
- package/dist/field/bound-field.d.ts +17 -1
- package/dist/field/bound-field.js +68 -7
- package/dist/field/check.js +1 -0
- package/dist/field/field.js +1 -1
- package/dist/field/input.d.ts +1 -0
- package/dist/field/input.js +2 -2
- package/dist/field/number-field.js +2 -2
- package/dist/filter/filter-builder.d.ts +7 -1
- package/dist/filter/filter-builder.js +8 -1
- package/dist/form/controls.d.ts +40 -8
- package/dist/form/controls.js +58 -15
- package/dist/form/custom-field.d.ts +1 -0
- package/dist/form/custom-field.js +23 -0
- package/dist/form/features.d.ts +27 -0
- package/dist/form/features.js +52 -0
- package/dist/form/form.d.ts +67 -7
- package/dist/form/form.js +267 -24
- package/dist/form/policy.d.ts +29 -0
- package/dist/form/policy.js +16 -0
- package/dist/grid/appearance.d.ts +365 -0
- package/dist/grid/appearance.js +814 -0
- package/dist/grid/features.d.ts +38 -0
- package/dist/grid/features.js +78 -0
- package/dist/grid/grid.d.ts +216 -4
- package/dist/grid/grid.js +1853 -301
- package/dist/grid/policy.d.ts +58 -0
- package/dist/grid/policy.js +17 -0
- package/dist/grid/view.d.ts +32 -2
- package/dist/grid/view.js +89 -4
- package/dist/index.d.ts +14 -11
- package/dist/index.js +2 -1
- package/dist/menu/config.d.ts +7 -0
- package/dist/menu/config.js +44 -0
- package/dist/menu/menu.d.ts +23 -0
- package/dist/menu/menu.js +69 -17
- package/dist/picker/picker.js +8 -3
- package/dist/section/accordion.d.ts +26 -0
- package/dist/section/accordion.js +86 -8
- package/dist/section/policy.d.ts +8 -0
- package/dist/section/policy.js +17 -0
- package/dist/section/section.d.ts +30 -0
- package/dist/section/section.js +106 -3
- package/dist/select/choice-element.d.ts +12 -0
- package/dist/select/choice-element.js +36 -0
- package/dist/select/choice.d.ts +12 -2
- package/dist/select/choice.js +93 -28
- package/dist/select/select.d.ts +12 -0
- package/dist/select/select.js +37 -1
- package/dist/styles/contract.d.ts +14 -0
- package/dist/styles/contract.js +15 -0
- package/dist/switch/switch.d.ts +16 -0
- package/dist/switch/switch.js +56 -6
- package/dist/tree/navigator.d.ts +3 -0
- package/dist/tree/navigator.js +26 -2
- package/dist/window/chrome.d.ts +5 -1
- package/dist/window/chrome.js +4 -3
- package/dist/window/config.d.ts +71 -0
- package/dist/window/config.js +40 -0
- package/dist/window/window.d.ts +51 -13
- package/dist/window/window.js +233 -38
- package/dist/yaml.d.ts +19 -0
- package/dist/yaml.js +57 -0
- package/package.json +10 -4
- package/styles/aster-ui.css +233 -10
- package/styles/base.css +36 -0
- package/styles/contract.ts +16 -0
- package/styles/elements.css +197 -10
- package/styles/fonts/OFL-bricolage-grotesque.txt +93 -0
- package/styles/fonts/OFL-fraunces.txt +93 -0
- package/styles/fonts/bricolage-grotesque-latin-opsz-normal.woff2 +0 -0
- package/styles/fonts/fraunces-latin-full-normal.woff2 +0 -0
- package/styles/fonts.css +17 -0
- package/styles/theme-harbour.css +5 -0
- package/styles/theme-lamplight.css +5 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** A capability switch: on or off, a field-name allowlist, or a predicate. */
|
|
2
|
+
export type Capability = boolean | readonly string[] | ((field: string) => boolean);
|
|
3
|
+
/** The plain-data form a composition catalog can carry. Predicates stay in JavaScript. */
|
|
4
|
+
export type CapabilityValue = boolean | readonly string[];
|
|
5
|
+
/**
|
|
6
|
+
* Per-field capability overrides. They narrow the global entry; they never
|
|
7
|
+
* grant what it refuses.
|
|
8
|
+
*/
|
|
9
|
+
export interface FieldPolicy {
|
|
10
|
+
/** Whether this field may be sorted, when the global entry permits it. */
|
|
11
|
+
sortable?: Capability;
|
|
12
|
+
/** Whether this field may be filtered, when the global entry permits it. */
|
|
13
|
+
filterable?: Capability;
|
|
14
|
+
/** Whether this field may be grouped on, when the global entry permits it. */
|
|
15
|
+
groupable?: Capability;
|
|
16
|
+
/** Whether this field's column may be resized, when the global entry permits it. */
|
|
17
|
+
resizable?: Capability;
|
|
18
|
+
/** Whether this field's column may be moved, when the global entry permits it. */
|
|
19
|
+
movable?: Capability;
|
|
20
|
+
/** Whether this field's column may be hidden, when the global entry permits it. */
|
|
21
|
+
hideable?: Capability;
|
|
22
|
+
/** Whether this field's column may be frozen, when the global entry permits it. */
|
|
23
|
+
freezable?: Capability;
|
|
24
|
+
/** Whether this field may be edited, when the global entry permits it. */
|
|
25
|
+
editable?: Capability;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* What the page allows, keyed by field name. Policy can only restrict:
|
|
29
|
+
* schema and the grid's own structure refuse first, and a page rule's
|
|
30
|
+
* read-only reason outranks a policy reason. It never joins [[object:GridView]]:
|
|
31
|
+
* a saved view cannot carry another person's capabilities.
|
|
32
|
+
*/
|
|
33
|
+
export interface GridPolicy {
|
|
34
|
+
/** Global sort capability, default on. */
|
|
35
|
+
sortable?: Capability;
|
|
36
|
+
/** Global filter capability, default on. */
|
|
37
|
+
filterable?: Capability;
|
|
38
|
+
/** Global grouping capability, default on. */
|
|
39
|
+
groupable?: Capability;
|
|
40
|
+
/** Global column-resize capability, default on. */
|
|
41
|
+
resizable?: Capability;
|
|
42
|
+
/** Global column-move capability, default on. */
|
|
43
|
+
movable?: Capability;
|
|
44
|
+
/** Global column-hide capability, default on. */
|
|
45
|
+
hideable?: Capability;
|
|
46
|
+
/** Global column-freeze capability, default on. */
|
|
47
|
+
freezable?: Capability;
|
|
48
|
+
/** Global edit capability, default off. */
|
|
49
|
+
editable?: Capability;
|
|
50
|
+
/** Whether rows may be selected, default on. */
|
|
51
|
+
selectable?: boolean;
|
|
52
|
+
/** Per-field overrides, by field name. */
|
|
53
|
+
fields?: Readonly<Record<string, FieldPolicy>>;
|
|
54
|
+
}
|
|
55
|
+
/** One entry's answer for `field`; `fallback` when the entry says nothing. */
|
|
56
|
+
export declare function permits(entry: Capability | undefined, field: string, fallback: boolean): boolean;
|
|
57
|
+
/** The host's answer: the global entry, narrowed by the field's own entry. */
|
|
58
|
+
export declare function policyPermits(policy: GridPolicy, capability: keyof FieldPolicy, field: string, fallback: boolean): boolean;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** One entry's answer for `field`; `fallback` when the entry says nothing. */
|
|
2
|
+
export function permits(entry, field, fallback) {
|
|
3
|
+
if (entry === undefined)
|
|
4
|
+
return fallback;
|
|
5
|
+
switch (typeof entry) {
|
|
6
|
+
case "boolean":
|
|
7
|
+
return entry;
|
|
8
|
+
case "function":
|
|
9
|
+
return entry(field);
|
|
10
|
+
default:
|
|
11
|
+
return entry.includes(field);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** The host's answer: the global entry, narrowed by the field's own entry. */
|
|
15
|
+
export function policyPermits(policy, capability, field, fallback) {
|
|
16
|
+
return permits(policy[capability], field, fallback) && permits(policy.fields?.[field]?.[capability], field, true);
|
|
17
|
+
}
|
package/dist/grid/view.d.ts
CHANGED
|
@@ -24,9 +24,39 @@ export interface GridView {
|
|
|
24
24
|
/** How many leading visible columns stay in place when scrolling sideways. */
|
|
25
25
|
readonly frozen: number;
|
|
26
26
|
}
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Which view entries the grid can apply. A refused entry is a capability
|
|
29
|
+
* the schema, the grid's structure or the page policy doesn't allow.
|
|
30
|
+
*/
|
|
31
|
+
export interface ViewCapabilities {
|
|
32
|
+
readonly sortable?: (field: string) => boolean;
|
|
33
|
+
readonly filterable?: (field: string) => boolean;
|
|
34
|
+
readonly groupable?: (field: string) => boolean;
|
|
35
|
+
readonly hideable?: (field: string) => boolean;
|
|
36
|
+
readonly freezable?: (field: string) => boolean;
|
|
37
|
+
}
|
|
38
|
+
/** A view entry that the derived presentation leaves out, and why it was refused. */
|
|
39
|
+
export interface ViewRefusal {
|
|
40
|
+
readonly kind: "sort" | "filter" | "criteria" | "group" | "hidden" | "frozen";
|
|
41
|
+
readonly field: string;
|
|
42
|
+
}
|
|
43
|
+
/** Fields a criteria expression reads, at the level it filters; related-record clauses name the navigation. */
|
|
44
|
+
export declare function criteriaFields(criteria: Criteria): readonly string[];
|
|
45
|
+
/** The view entries `allowed` refuses, for the drift note. The view itself is never rewritten. */
|
|
46
|
+
export declare function viewRefusals(view: GridView, allowed: ViewCapabilities): readonly ViewRefusal[];
|
|
47
|
+
/** The columns shown: a column the page forbids hiding stays visible. */
|
|
48
|
+
export declare function effectiveColumns(view: GridView, hideable?: (field: string) => boolean): readonly GridColumn[];
|
|
49
|
+
/** The frozen prefix, cut where the page forbids freezing a column. */
|
|
50
|
+
export declare function effectiveFrozen(view: GridView, visible: readonly GridColumn[], freezable?: (field: string) => boolean): number;
|
|
51
|
+
/** The grouping levels in force: stored levels the page refuses are left out. */
|
|
52
|
+
export declare function effectiveGroups(view: GridView | undefined, groupable?: (field: string) => boolean): readonly string[];
|
|
53
|
+
/**
|
|
54
|
+
* The query a view describes, within `base` (such as "this project only").
|
|
55
|
+
* Entries `allowed` refuses are left out of the derived query while the view
|
|
56
|
+
* keeps them, so a person's intent resumes when the page allows it again.
|
|
57
|
+
*/
|
|
28
58
|
/** `sortPaths` maps a column to what it sorts by, for a lookup column the related record's display field. */
|
|
29
|
-
export declare function viewQuery(set: string, view: GridView, base?: Criteria, sortPaths?: Readonly<Record<string, string
|
|
59
|
+
export declare function viewQuery(set: string, view: GridView, base?: Criteria, sortPaths?: Readonly<Record<string, string>>, allowed?: ViewCapabilities): Query;
|
|
30
60
|
/** Every field of `type` that has a value to show, in declaration order, the key last. */
|
|
31
61
|
export declare function defaultView(type: EntityType): GridView;
|
|
32
62
|
/** The column's track: its width, or a share of the space with a floor that suits its type. */
|
package/dist/grid/view.js
CHANGED
|
@@ -1,10 +1,95 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Fields a criteria expression reads, at the level it filters; related-record clauses name the navigation. */
|
|
2
|
+
export function criteriaFields(criteria) {
|
|
3
|
+
if (!criteria || typeof criteria !== "object")
|
|
4
|
+
return [];
|
|
5
|
+
const node = criteria;
|
|
6
|
+
switch (node.op) {
|
|
7
|
+
case "and":
|
|
8
|
+
case "or":
|
|
9
|
+
return Array.isArray(node.args) ? node.args.flatMap(criteriaFields) : [];
|
|
10
|
+
case "not":
|
|
11
|
+
return node.arg ? criteriaFields(node.arg) : [];
|
|
12
|
+
default: {
|
|
13
|
+
const fields = [];
|
|
14
|
+
if (typeof node.field === "string")
|
|
15
|
+
fields.push(node.field);
|
|
16
|
+
// A field-to-field comparison reads both operands; each must pass capability checks.
|
|
17
|
+
if (typeof node.other === "string")
|
|
18
|
+
fields.push(node.other);
|
|
19
|
+
return fields;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** The view entries `allowed` refuses, for the drift note. The view itself is never rewritten. */
|
|
24
|
+
export function viewRefusals(view, allowed) {
|
|
25
|
+
const refusals = [];
|
|
26
|
+
if (allowed.sortable)
|
|
27
|
+
for (const key of view.sort)
|
|
28
|
+
if (!allowed.sortable(key.field))
|
|
29
|
+
refusals.push({ kind: "sort", field: key.field });
|
|
30
|
+
if (allowed.filterable) {
|
|
31
|
+
for (const field of Object.keys(view.filters))
|
|
32
|
+
if (!allowed.filterable(field))
|
|
33
|
+
refusals.push({ kind: "filter", field });
|
|
34
|
+
if (view.criteria)
|
|
35
|
+
for (const field of new Set(criteriaFields(view.criteria)))
|
|
36
|
+
if (!allowed.filterable(field))
|
|
37
|
+
refusals.push({ kind: "criteria", field });
|
|
38
|
+
}
|
|
39
|
+
if (allowed.groupable)
|
|
40
|
+
for (const field of groupFields(view))
|
|
41
|
+
if (!allowed.groupable(field))
|
|
42
|
+
refusals.push({ kind: "group", field });
|
|
43
|
+
if (allowed.hideable)
|
|
44
|
+
for (const column of view.columns)
|
|
45
|
+
if (column.hidden && !allowed.hideable(column.field))
|
|
46
|
+
refusals.push({ kind: "hidden", field: column.field });
|
|
47
|
+
// Frozen columns are a prefix: the first one the page refuses ends the frozen run.
|
|
48
|
+
if (allowed.freezable) {
|
|
49
|
+
const visible = effectiveColumns(view, allowed.hideable);
|
|
50
|
+
for (let i = 0; i < Math.min(view.frozen, visible.length); i++) {
|
|
51
|
+
const field = visible[i].field;
|
|
52
|
+
if (!allowed.freezable(field)) {
|
|
53
|
+
refusals.push({ kind: "frozen", field });
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return refusals;
|
|
59
|
+
}
|
|
60
|
+
/** The columns shown: a column the page forbids hiding stays visible. */
|
|
61
|
+
export function effectiveColumns(view, hideable) {
|
|
62
|
+
return view.columns.filter((column) => !column.hidden || (hideable !== undefined && !hideable(column.field)));
|
|
63
|
+
}
|
|
64
|
+
/** The frozen prefix, cut where the page forbids freezing a column. */
|
|
65
|
+
export function effectiveFrozen(view, visible, freezable) {
|
|
66
|
+
const wanted = Math.min(view.frozen, visible.length);
|
|
67
|
+
if (!freezable)
|
|
68
|
+
return wanted;
|
|
69
|
+
let frozen = 0;
|
|
70
|
+
while (frozen < wanted && freezable(visible[frozen].field))
|
|
71
|
+
frozen++;
|
|
72
|
+
return frozen;
|
|
73
|
+
}
|
|
74
|
+
/** The grouping levels in force: stored levels the page refuses are left out. */
|
|
75
|
+
export function effectiveGroups(view, groupable) {
|
|
76
|
+
const fields = groupFields(view);
|
|
77
|
+
return groupable ? fields.filter((field) => groupable(field)) : fields;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The query a view describes, within `base` (such as "this project only").
|
|
81
|
+
* Entries `allowed` refuses are left out of the derived query while the view
|
|
82
|
+
* keeps them, so a person's intent resumes when the page allows it again.
|
|
83
|
+
*/
|
|
2
84
|
/** `sortPaths` maps a column to what it sorts by, for a lookup column the related record's display field. */
|
|
3
|
-
export function viewQuery(set, view, base, sortPaths = {}) {
|
|
4
|
-
const
|
|
85
|
+
export function viewQuery(set, view, base, sortPaths = {}, allowed = {}) {
|
|
86
|
+
const sort = allowed.sortable ? view.sort.filter((key) => allowed.sortable(key.field)) : view.sort;
|
|
87
|
+
const filters = Object.entries(view.filters).filter(([field]) => !allowed.filterable || allowed.filterable(field));
|
|
88
|
+
const criteria = view.criteria && (!allowed.filterable || criteriaFields(view.criteria).every(allowed.filterable)) ? view.criteria : undefined;
|
|
89
|
+
const parts = [...(base ? [base] : []), ...filters.map(([, filter]) => filter), ...(criteria ? [criteria] : [])];
|
|
5
90
|
const filter = parts.length === 0 ? undefined : parts.length === 1 ? parts[0] : { op: "and", args: parts };
|
|
6
91
|
const paths = { ...sortPaths, ...Object.fromEntries(view.columns.filter(c => c.sortField).map(c => [c.field, c.sortField])) };
|
|
7
|
-
const orderBy =
|
|
92
|
+
const orderBy = sort.map((k) => (paths[k.field] ? { ...k, field: paths[k.field] } : k));
|
|
8
93
|
return { set, ...(filter ? { filter } : {}), ...(orderBy.length ? { orderBy } : {}) };
|
|
9
94
|
}
|
|
10
95
|
/** Every field of `type` that has a value to show, in declaration order, the key last. */
|
package/dist/index.d.ts
CHANGED
|
@@ -14,11 +14,11 @@ export { CaretGroup } from "./caret/caret-group.ts";
|
|
|
14
14
|
export { Tabs, type TabItem } from "./tabs/tabs.ts";
|
|
15
15
|
export { TileGrid, type TileContent, type TileState, type TileRenderer, type TilePresenter, type TileOpenDetail } from "./tile-grid/tile-grid.ts";
|
|
16
16
|
export { Listbox, type RowMenuDetail, type RowRenderer } from "./listbox/listbox.ts";
|
|
17
|
-
export { Button } from "./button/button.ts";
|
|
17
|
+
export { Button, type ButtonSize, type ButtonTone } from "./button/button.ts";
|
|
18
18
|
export { Field } from "./field/field.ts";
|
|
19
19
|
export { Switch, type SwitchBinding } from "./switch/switch.ts";
|
|
20
20
|
export { Dialog } from "./dialog/dialog.ts";
|
|
21
|
-
export { Window, type WindowState, type WindowBounds, type WindowPlacement } from "./window/window.ts";
|
|
21
|
+
export { Window, type WindowChrome, type WindowState, type WindowBounds, type WindowPlacement } from "./window/window.ts";
|
|
22
22
|
export { Workspace, type WorkspaceEntry, type WorkspaceSnapshot } from "./window/workspace.ts";
|
|
23
23
|
export { Dashboard } from "./dashboard/dashboard.ts";
|
|
24
24
|
export type { DashboardValue, DashboardConfig, DashboardSize, DashboardWidget, DashboardSnapshot } from "./dashboard/layout.ts";
|
|
@@ -27,12 +27,13 @@ export { Toaster, announce, type ToastAction } from "./toast/toast.ts";
|
|
|
27
27
|
export { Menu, type MenuItem } from "./menu/menu.ts";
|
|
28
28
|
export { morph, setScheme } from "./motion.ts";
|
|
29
29
|
export { absoluteTime, compactMoney, relativeTime } from "./format.ts";
|
|
30
|
-
export { BoundField, type ControlContext } from "./field/bound-field.ts";
|
|
30
|
+
export { BoundField, type ControlContext, type ValidateOn } from "./field/bound-field.ts";
|
|
31
31
|
export { CompositeField, type CompositeFieldContext } from "./field/composite-field.ts";
|
|
32
32
|
export { NumberField, type NumericType, type NumericValue } from "./field/number-field.ts";
|
|
33
33
|
export { Stack, HorizontalLayout, VerticalLayout } from "./layout/stack.ts";
|
|
34
34
|
export { Breadcrumbs, type BreadcrumbItem } from "./breadcrumbs/breadcrumbs.ts";
|
|
35
|
-
export { Accordion, type AccordionItem } from "./section/accordion.ts";
|
|
35
|
+
export { Accordion, type AccordionItem, type AccordionReorderDetail } from "./section/accordion.ts";
|
|
36
|
+
export type { VisibilityMode } from "./section/policy.ts";
|
|
36
37
|
export { Check } from "./field/check.ts";
|
|
37
38
|
export { formatValue, parseInput } from "./field/values.ts";
|
|
38
39
|
export { Select } from "./select/select.ts";
|
|
@@ -43,13 +44,14 @@ export { PickerOptions, type OptionsSpec } from "./picker/options.ts";
|
|
|
43
44
|
export { TreePicker } from "./picker/tree-picker.ts";
|
|
44
45
|
export { TreeChoices, type TreeChoicesSpec } from "./picker/tree-choices.ts";
|
|
45
46
|
export { DateField } from "./date/date-field.ts";
|
|
46
|
-
export { formatDate, parseDate, setClock } from "./date/dates.ts";
|
|
47
|
-
export { Section } from "./section/section.ts";
|
|
48
|
-
export { Form, type FormField, type FormItem, type FormSection, type FormValue } from "./form/form.ts";
|
|
49
|
-
export { controlFor } from "./form/controls.ts";
|
|
47
|
+
export { formatDate, parseDate, setClock, type DateFormat } from "./date/dates.ts";
|
|
48
|
+
export { Section, type SectionChrome } from "./section/section.ts";
|
|
49
|
+
export { Form, type FormChrome, type FormFeature, type FormField, type FormItem, type FormPolicy, type FormSection, type FormSubmitDetail, type FormValue } from "./form/form.ts";
|
|
50
|
+
export { controlFor, type FormControl, type FormControlContext, type FormControlName, type FormControlRender } from "./form/controls.ts";
|
|
50
51
|
export { Split } from "./split/split.ts";
|
|
51
52
|
export { ListEditor, type ListColumn } from "./list-edit/list-edit.ts";
|
|
52
|
-
export { Grid, type CellRenderer, type CellFormatter, type
|
|
53
|
+
export { Grid, type CellRenderer, type CellFormatter, type GridChrome, type GridCsvOptions, type GridFeature, type GridScrollAlign, type GridSummary, type GridToolbar, type Expansion, type ExpansionEditor, type GridTree, type Lookup, type RowControl, type RowState } from "./grid/grid.ts";
|
|
54
|
+
export { GRID_LABEL_DEFAULTS, type CellClass, type CellClassRule, type CellStyleContext, type ClassNames, type ColumnAlign, type ColumnFormat, type ColumnPresentation, type ColumnValueMap, type GridLabel, type GridLabels, type GridPresentation, type Hilite, type MappedValue, type NumberFormatStyle, type PresentationTone, type RowClass, type RowClassRule, type RowStyleContext, type StyleDecoration, type ValueMapValue, type ValueMatch, type ValueMatchOperator } from "./grid/appearance.ts";
|
|
53
55
|
export { RowHeights } from "./grid/rows.ts";
|
|
54
56
|
export { TreeNavigator } from "./tree/navigator.ts";
|
|
55
57
|
export { Carousel, type Slide } from "./carousel/carousel.ts";
|
|
@@ -58,6 +60,7 @@ export { FilterBuilder } from "./filter/filter-builder.ts";
|
|
|
58
60
|
export { ComponentHost, type ComponentAdapter, type ComponentSession, type ComponentMount, type ComponentEnvironment } from "./component/component.ts";
|
|
59
61
|
export { echartsAdapter, echartsColors, type EChartsAdapterOptions, type EChartsRuntime, type EChartsInstance } from "./component/echarts.ts";
|
|
60
62
|
export { Slicer } from "./slicer/slicer.ts";
|
|
61
|
-
export { defaultView, sortBy, viewQuery, type GridColumn, type GridView } from "./grid/view.ts";
|
|
62
|
-
export {
|
|
63
|
+
export { defaultView, sortBy, viewQuery, type GridColumn, type GridView, type ViewCapabilities } from "./grid/view.ts";
|
|
64
|
+
export type { Capability, CapabilityValue, FieldPolicy, GridPolicy } from "./grid/policy.ts";
|
|
65
|
+
export { COMPONENT_VARS, COMPONENT_VAR_NAMES, DENSITIES, DENSITY, DENSITY_NAMES, TOKENS, TOKEN_NAMES } from "./styles/contract.ts";
|
|
63
66
|
export { CompositeSessions, type CompositeSessionsOptions, type CompositeKeyedSession } from "./composition/sessions.ts";
|
package/dist/index.js
CHANGED
|
@@ -48,6 +48,7 @@ export { controlFor } from "./form/controls.js";
|
|
|
48
48
|
export { Split } from "./split/split.js";
|
|
49
49
|
export { ListEditor } from "./list-edit/list-edit.js";
|
|
50
50
|
export { Grid } from "./grid/grid.js";
|
|
51
|
+
export { GRID_LABEL_DEFAULTS } from "./grid/appearance.js";
|
|
51
52
|
export { RowHeights } from "./grid/rows.js";
|
|
52
53
|
export { TreeNavigator } from "./tree/navigator.js";
|
|
53
54
|
export { Carousel } from "./carousel/carousel.js";
|
|
@@ -57,5 +58,5 @@ export { ComponentHost } from "./component/component.js";
|
|
|
57
58
|
export { echartsAdapter, echartsColors } from "./component/echarts.js";
|
|
58
59
|
export { Slicer } from "./slicer/slicer.js";
|
|
59
60
|
export { defaultView, sortBy, viewQuery } from "./grid/view.js";
|
|
60
|
-
export { DENSITIES, DENSITY, DENSITY_NAMES, TOKENS, TOKEN_NAMES } from "./styles/contract.js";
|
|
61
|
+
export { COMPONENT_VARS, COMPONENT_VAR_NAMES, DENSITIES, DENSITY, DENSITY_NAMES, TOKENS, TOKEN_NAMES } from "./styles/contract.js";
|
|
61
62
|
export { CompositeSessions } from "./composition/sessions.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `aria-keyshortcuts` value for a human shortcut hint ("Ctrl+S", "⌘K"),
|
|
3
|
+
* or `undefined` when it isn't a keyboard shortcut this attribute can carry.
|
|
4
|
+
*/
|
|
5
|
+
export declare function ariaKeyshortcuts(shortcut: string | undefined): string | undefined;
|
|
6
|
+
/** Whether a level reserves an icon cell, so labels line up across its items. */
|
|
7
|
+
export declare function hasMenuIcons(items: readonly object[]): boolean;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const MODIFIERS = {
|
|
2
|
+
ctrl: "Control", control: "Control", "⌃": "Control",
|
|
3
|
+
cmd: "Meta", command: "Meta", meta: "Meta", "⌘": "Meta",
|
|
4
|
+
alt: "Alt", option: "Alt", "⌥": "Alt",
|
|
5
|
+
shift: "Shift", "⇧": "Shift",
|
|
6
|
+
};
|
|
7
|
+
const NAMED = {
|
|
8
|
+
space: "Space", enter: "Enter", escape: "Escape", esc: "Escape", tab: "Tab", backspace: "Backspace",
|
|
9
|
+
delete: "Delete", home: "Home", end: "End", pageup: "PageUp", pagedown: "PageDown",
|
|
10
|
+
arrowup: "ArrowUp", arrowdown: "ArrowDown", arrowleft: "ArrowLeft", arrowright: "ArrowRight",
|
|
11
|
+
up: "ArrowUp", down: "ArrowDown", left: "ArrowLeft", right: "ArrowRight",
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The `aria-keyshortcuts` value for a human shortcut hint ("Ctrl+S", "⌘K"),
|
|
15
|
+
* or `undefined` when it isn't a keyboard shortcut this attribute can carry.
|
|
16
|
+
*/
|
|
17
|
+
export function ariaKeyshortcuts(shortcut) {
|
|
18
|
+
if (!shortcut)
|
|
19
|
+
return undefined;
|
|
20
|
+
let text = shortcut.trim();
|
|
21
|
+
const modifiers = [];
|
|
22
|
+
// Apple-style hints prefix the key with symbols (⇧⌘K) instead of joining with "+".
|
|
23
|
+
while (/^[⌘⌥⇧⌃]/.test(text)) {
|
|
24
|
+
modifiers.push(MODIFIERS[text[0]]);
|
|
25
|
+
text = text.slice(1).trim();
|
|
26
|
+
}
|
|
27
|
+
const parts = text.split("+").map((part) => part.trim()).filter(Boolean);
|
|
28
|
+
const key = parts.pop();
|
|
29
|
+
if (!key)
|
|
30
|
+
return undefined;
|
|
31
|
+
for (const part of parts) {
|
|
32
|
+
const modifier = MODIFIERS[part.toLowerCase()];
|
|
33
|
+
if (!modifier)
|
|
34
|
+
return undefined;
|
|
35
|
+
modifiers.push(modifier);
|
|
36
|
+
}
|
|
37
|
+
const named = NAMED[key.toLowerCase()] ?? (/^F([1-9]|1[0-2])$/i.test(key) ? key.toUpperCase() : undefined);
|
|
38
|
+
const last = named ?? (key.length === 1 && /\S/.test(key) ? key.toUpperCase() : undefined);
|
|
39
|
+
return last ? [...modifiers, last].join("+") : undefined;
|
|
40
|
+
}
|
|
41
|
+
/** Whether a level reserves an icon cell, so labels line up across its items. */
|
|
42
|
+
export function hasMenuIcons(items) {
|
|
43
|
+
return items.some((item) => item.icon !== undefined);
|
|
44
|
+
}
|
package/dist/menu/menu.d.ts
CHANGED
|
@@ -5,20 +5,28 @@ export type MenuItem = {
|
|
|
5
5
|
readonly label: string;
|
|
6
6
|
readonly run: () => void;
|
|
7
7
|
readonly disabled?: string;
|
|
8
|
+
readonly icon?: View;
|
|
9
|
+
readonly shortcut?: string;
|
|
8
10
|
} | {
|
|
9
11
|
readonly kind: "link";
|
|
10
12
|
readonly label: string;
|
|
11
13
|
readonly href: string;
|
|
12
14
|
readonly activate?: (event: MouseEvent) => void;
|
|
15
|
+
readonly icon?: View;
|
|
16
|
+
readonly shortcut?: string;
|
|
13
17
|
} | {
|
|
14
18
|
readonly kind: "check" | "radio";
|
|
15
19
|
readonly label: string;
|
|
16
20
|
readonly checked: boolean;
|
|
17
21
|
readonly run: () => void;
|
|
22
|
+
readonly icon?: View;
|
|
23
|
+
readonly shortcut?: string;
|
|
18
24
|
} | {
|
|
19
25
|
readonly kind: "submenu";
|
|
20
26
|
readonly label: string;
|
|
21
27
|
readonly items: readonly MenuItem[];
|
|
28
|
+
readonly icon?: View;
|
|
29
|
+
readonly shortcut?: string;
|
|
22
30
|
} | {
|
|
23
31
|
readonly kind: "group";
|
|
24
32
|
readonly label: string;
|
|
@@ -41,6 +49,21 @@ export type MenuItem = {
|
|
|
41
49
|
*/
|
|
42
50
|
export declare class Menu extends AsterElement {
|
|
43
51
|
#private;
|
|
52
|
+
/** Whether items with an `icon` draw it. Default `true`; an icon column appears only when one is set. */
|
|
53
|
+
get showIcons(): boolean;
|
|
54
|
+
set showIcons(v: boolean);
|
|
55
|
+
/** Whether items with a `shortcut` show its hint. Default `true`. */
|
|
56
|
+
get showShortcuts(): boolean;
|
|
57
|
+
set showShortcuts(v: boolean);
|
|
58
|
+
/** Where a submenu opens: `"auto"` flips at the edges (default), `"end"` or `"start"` fixes the side. */
|
|
59
|
+
get submenuDirection(): "auto" | "start" | "end";
|
|
60
|
+
set submenuDirection(v: "auto" | "start" | "end");
|
|
61
|
+
/** Whether choosing an action closes the menu. Default `true`. */
|
|
62
|
+
get autoDismiss(): boolean;
|
|
63
|
+
set autoDismiss(v: boolean);
|
|
64
|
+
/** Text shown when the menu has nothing to choose. Default `""`: nothing is drawn. */
|
|
65
|
+
get emptyMessage(): string;
|
|
66
|
+
set emptyMessage(v: string);
|
|
44
67
|
/** Opens at a point, such as a right-click. */
|
|
45
68
|
openAt(point: {
|
|
46
69
|
x: number;
|
package/dist/menu/menu.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { html, nothing } from "lit-html";
|
|
2
2
|
import { AsterElement, define, uid } from "../base/element.js";
|
|
3
3
|
import { openWhen, placeStyle } from "../overlay/anchor.js";
|
|
4
|
+
import { ariaKeyshortcuts, hasMenuIcons } from "./config.js";
|
|
4
5
|
/**
|
|
5
6
|
* A menu in the top layer (Popover API), following the APG menu pattern: it
|
|
6
7
|
* opens at a point (a context menu) or against an element, focuses its
|
|
@@ -23,11 +24,39 @@ export class Menu extends AsterElement {
|
|
|
23
24
|
#anchor = this.prop(undefined);
|
|
24
25
|
/** Check and radio marks changed since the menu opened; items keep the state they were given. */
|
|
25
26
|
#marks = this.prop(new Map());
|
|
27
|
+
#showIcons = this.prop(true);
|
|
28
|
+
#showShortcuts = this.prop(true);
|
|
29
|
+
#submenuDirection = this.prop("auto");
|
|
30
|
+
#autoDismiss = this.prop(true);
|
|
31
|
+
#emptyMessage = this.prop("");
|
|
26
32
|
#id = uid("aster-menu");
|
|
27
33
|
#opener = null;
|
|
28
34
|
/** The item to focus once its menu has rendered and its popover is open. */
|
|
29
35
|
#focusNext;
|
|
30
36
|
#stop;
|
|
37
|
+
/** Whether items with an `icon` draw it. Default `true`; an icon column appears only when one is set. */
|
|
38
|
+
get showIcons() { return this.#showIcons.value; }
|
|
39
|
+
set showIcons(v) { this.#showIcons.value = v === true; }
|
|
40
|
+
/** Whether items with a `shortcut` show its hint. Default `true`. */
|
|
41
|
+
get showShortcuts() { return this.#showShortcuts.value; }
|
|
42
|
+
set showShortcuts(v) { this.#showShortcuts.value = v === true; }
|
|
43
|
+
/** Where a submenu opens: `"auto"` flips at the edges (default), `"end"` or `"start"` fixes the side. */
|
|
44
|
+
get submenuDirection() { return this.#submenuDirection.value; }
|
|
45
|
+
set submenuDirection(v) {
|
|
46
|
+
if (v !== "auto" && v !== "start" && v !== "end")
|
|
47
|
+
throw new TypeError('submenuDirection must be "auto", "start" or "end"');
|
|
48
|
+
this.#submenuDirection.value = v;
|
|
49
|
+
}
|
|
50
|
+
/** Whether choosing an action closes the menu. Default `true`. */
|
|
51
|
+
get autoDismiss() { return this.#autoDismiss.value; }
|
|
52
|
+
set autoDismiss(v) { this.#autoDismiss.value = v === true; }
|
|
53
|
+
/** Text shown when the menu has nothing to choose. Default `""`: nothing is drawn. */
|
|
54
|
+
get emptyMessage() { return this.#emptyMessage.value; }
|
|
55
|
+
set emptyMessage(v) {
|
|
56
|
+
if (typeof v !== "string")
|
|
57
|
+
throw new TypeError("emptyMessage must be a string");
|
|
58
|
+
this.#emptyMessage.value = v;
|
|
59
|
+
}
|
|
31
60
|
/** Opens at a point, such as a right-click. */
|
|
32
61
|
openAt(point, items, label, opener) {
|
|
33
62
|
this.style.setProperty("--menu-x", `${point.x}px`);
|
|
@@ -59,16 +88,28 @@ export class Menu extends AsterElement {
|
|
|
59
88
|
levels.push({ items: parent.items, label: parent.label });
|
|
60
89
|
}
|
|
61
90
|
const anchored = this.#anchor.value;
|
|
91
|
+
const direction = this.#submenuDirection.value;
|
|
92
|
+
const icons = this.#showIcons.value;
|
|
93
|
+
const shortcuts = this.#showShortcuts.value;
|
|
62
94
|
return html `${levels.map((level, depth) => {
|
|
63
95
|
const place = depth > 0 ? placeStyle(this.#itemId(depth - 1, path[depth - 1])) : anchored ? placeStyle(anchored) : nothing;
|
|
64
|
-
|
|
65
|
-
|
|
96
|
+
const items = focusables(level.items);
|
|
97
|
+
const empty = items.length === 0;
|
|
98
|
+
return html `<div class="aster-menu" popover="manual" role=${empty ? nothing : "menu"} aria-label=${empty ? nothing : level.label || nothing}
|
|
99
|
+
data-depth=${depth} ?data-anchored=${depth > 0 || anchored !== undefined}
|
|
100
|
+
data-submenu=${depth > 0 && direction !== "auto" ? direction : nothing}
|
|
101
|
+
style=${place} tabindex="-1" ${openWhen(true)}
|
|
66
102
|
@keydown=${(e) => this.#key(e, depth)}>
|
|
67
|
-
${this.#
|
|
103
|
+
${empty ? this.#empty() : nothing}
|
|
104
|
+
${this.#entries(level.items, depth, { next: 0 }, icons && hasMenuIcons(items), shortcuts)}
|
|
68
105
|
</div>`;
|
|
69
106
|
})}`;
|
|
70
107
|
}
|
|
71
|
-
#
|
|
108
|
+
#empty() {
|
|
109
|
+
const message = this.#emptyMessage.value;
|
|
110
|
+
return message ? html `<div class="aster-menu-empty">${message}</div>` : nothing;
|
|
111
|
+
}
|
|
112
|
+
#entries(items, depth, counter, icons, shortcuts) {
|
|
72
113
|
return items.map((item) => {
|
|
73
114
|
if (item.kind === "separator")
|
|
74
115
|
return html `<div role="separator" class="aster-menu-separator"></div>`;
|
|
@@ -76,35 +117,41 @@ export class Menu extends AsterElement {
|
|
|
76
117
|
const id = `${this.#id}-g${depth}-${counter.next}`;
|
|
77
118
|
return html `<div role="group" aria-labelledby=${id}>
|
|
78
119
|
<div class="aster-menu-heading" id=${id}>${item.label}</div>
|
|
79
|
-
${this.#entries(item.items, depth, counter)}
|
|
120
|
+
${this.#entries(item.items, depth, counter, icons, shortcuts)}
|
|
80
121
|
</div>`;
|
|
81
122
|
}
|
|
82
123
|
const index = counter.next++;
|
|
83
124
|
const id = this.#itemId(depth, index);
|
|
84
125
|
const expanded = this.#path.value[depth] === index;
|
|
85
126
|
const enter = () => this.#hover(depth, index, item);
|
|
127
|
+
const icon = icons ? html `<span class="aster-menu-icon" aria-hidden="true">${item.icon ?? nothing}</span>` : nothing;
|
|
128
|
+
const shortcut = shortcuts && item.shortcut
|
|
129
|
+
? html `<span class="aster-menu-shortcut" aria-hidden="true">${item.shortcut}</span>`
|
|
130
|
+
: nothing;
|
|
131
|
+
const keys = shortcuts ? ariaKeyshortcuts(item.shortcut) : undefined;
|
|
86
132
|
switch (item.kind) {
|
|
87
133
|
case "link":
|
|
88
|
-
return html `<a role="menuitem" tabindex="-1" id=${id} href=${item.href} @pointerenter=${enter}
|
|
89
|
-
@click=${(event) => { this.
|
|
134
|
+
return html `<a role="menuitem" tabindex="-1" id=${id} href=${item.href} aria-keyshortcuts=${keys ?? nothing} @pointerenter=${enter}
|
|
135
|
+
@click=${(event) => { if (this.#autoDismiss.peek())
|
|
136
|
+
this.close(); item.activate?.(event); }}><span class="aster-menu-mark" aria-hidden="true"></span>${icon}${item.label}${shortcut}</a>`;
|
|
90
137
|
case "check":
|
|
91
138
|
case "radio":
|
|
92
139
|
{
|
|
93
140
|
const checked = this.#marks.value.get(item) ?? item.checked;
|
|
94
141
|
return html `<button type="button" role=${item.kind === "check" ? "menuitemcheckbox" : "menuitemradio"} tabindex="-1"
|
|
95
|
-
id=${id} aria-checked=${checked ? "true" : "false"} @pointerenter=${enter} @click=${() => this.#choose(item)}>
|
|
96
|
-
<span class="aster-menu-mark" aria-hidden="true">${checked ? (item.kind === "check" ? "✓" : "•") : ""}</span>${item.label}</button>`;
|
|
142
|
+
id=${id} aria-checked=${checked ? "true" : "false"} aria-keyshortcuts=${keys ?? nothing} @pointerenter=${enter} @click=${() => this.#choose(item)}>
|
|
143
|
+
<span class="aster-menu-mark" aria-hidden="true">${checked ? (item.kind === "check" ? "✓" : "•") : ""}</span>${icon}${item.label}${shortcut}</button>`;
|
|
97
144
|
}
|
|
98
145
|
case "submenu":
|
|
99
146
|
return html `<button type="button" role="menuitem" tabindex="-1" id=${id} aria-haspopup="menu"
|
|
100
|
-
aria-expanded=${expanded ? "true" : "false"} style=${`anchor-name: --${id}`}
|
|
147
|
+
aria-expanded=${expanded ? "true" : "false"} aria-keyshortcuts=${keys ?? nothing} style=${`anchor-name: --${id}`}
|
|
101
148
|
@pointerenter=${enter} @click=${() => this.#openSub(depth, index, true)}>
|
|
102
|
-
<span class="aster-menu-mark" aria-hidden="true"></span>${item.label}<span class="aster-menu-more" aria-hidden="true">›</span></button>`;
|
|
149
|
+
<span class="aster-menu-mark" aria-hidden="true"></span>${icon}${item.label}${shortcut}<span class="aster-menu-more" aria-hidden="true">›</span></button>`;
|
|
103
150
|
default:
|
|
104
151
|
return html `<button type="button" role="menuitem" tabindex="-1" id=${id}
|
|
105
|
-
aria-disabled=${item.disabled ? "true" : nothing} title=${item.disabled ?? nothing}
|
|
152
|
+
aria-disabled=${item.disabled ? "true" : nothing} aria-keyshortcuts=${keys ?? nothing} title=${item.disabled ?? nothing}
|
|
106
153
|
@pointerenter=${enter} @click=${() => this.#choose(item)}>
|
|
107
|
-
<span class="aster-menu-mark" aria-hidden="true"></span>${item.label}</button>`;
|
|
154
|
+
<span class="aster-menu-mark" aria-hidden="true"></span>${icon}${item.label}${shortcut}</button>`;
|
|
108
155
|
}
|
|
109
156
|
});
|
|
110
157
|
}
|
|
@@ -126,10 +173,11 @@ export class Menu extends AsterElement {
|
|
|
126
173
|
// After the popovers this render opened (they open in a microtask too).
|
|
127
174
|
queueMicrotask(() => {
|
|
128
175
|
const target = this.#level(next.depth)[next.index];
|
|
129
|
-
if (!target)
|
|
130
|
-
return;
|
|
131
176
|
this.#focusNext = undefined;
|
|
132
|
-
target
|
|
177
|
+
if (target)
|
|
178
|
+
target.focus();
|
|
179
|
+
else
|
|
180
|
+
this.#container(next.depth)?.focus();
|
|
133
181
|
});
|
|
134
182
|
}
|
|
135
183
|
#itemId(depth, index) {
|
|
@@ -139,6 +187,9 @@ export class Menu extends AsterElement {
|
|
|
139
187
|
const menu = this.querySelector(`.aster-menu[data-depth="${depth}"]`);
|
|
140
188
|
return menu ? [...menu.querySelectorAll(':scope > [role^="menuitem"], :scope > [role="group"] > [role^="menuitem"]')] : [];
|
|
141
189
|
}
|
|
190
|
+
#container(depth) {
|
|
191
|
+
return this.querySelector(`.aster-menu[data-depth="${depth}"]`);
|
|
192
|
+
}
|
|
142
193
|
#choose(item) {
|
|
143
194
|
if (item.kind === "submenu" || item.kind === "link")
|
|
144
195
|
return;
|
|
@@ -156,7 +207,8 @@ export class Menu extends AsterElement {
|
|
|
156
207
|
item.run();
|
|
157
208
|
return;
|
|
158
209
|
}
|
|
159
|
-
this.
|
|
210
|
+
if (this.#autoDismiss.peek())
|
|
211
|
+
this.close();
|
|
160
212
|
item.run();
|
|
161
213
|
}
|
|
162
214
|
#openSub(depth, index, focus) {
|
package/dist/picker/picker.js
CHANGED
|
@@ -100,7 +100,8 @@ export class Picker extends BoundField {
|
|
|
100
100
|
const multiple = this.#multiple.value;
|
|
101
101
|
const chosen = multiple ? this.#values.value : c.value === null || c.value === undefined ? [] : [c.value];
|
|
102
102
|
const query = this.#query.value;
|
|
103
|
-
const
|
|
103
|
+
const disabled = !c.state.enabled;
|
|
104
|
+
const open = this.#open.value && !c.state.readOnly && !disabled;
|
|
104
105
|
const list = this.#list();
|
|
105
106
|
const loaded = (list?.items.value ?? []).filter((e) => e !== undefined);
|
|
106
107
|
const searching = this.#searching.value;
|
|
@@ -171,14 +172,18 @@ export class Picker extends BoundField {
|
|
|
171
172
|
aria-required=${c.state.required ? "true" : nothing}
|
|
172
173
|
aria-invalid=${c.invalid ? "true" : nothing}
|
|
173
174
|
aria-describedby=${describedBy ?? nothing}
|
|
174
|
-
?readonly=${c.state.readOnly}
|
|
175
|
+
?readonly=${c.state.readOnly} ?disabled=${disabled}
|
|
175
176
|
autocomplete="off" spellcheck="false"
|
|
176
177
|
@input=${(e) => {
|
|
178
|
+
if (disabled)
|
|
179
|
+
return;
|
|
177
180
|
// Typing replaces the shown name, even where the engine didn't select it on click.
|
|
178
181
|
const t = e.target.value;
|
|
179
182
|
this.#type(query === undefined && label && t.startsWith(label) && t.length > label.length ? t.slice(label.length) : t);
|
|
180
183
|
}}
|
|
181
184
|
@click=${(e) => {
|
|
185
|
+
if (disabled)
|
|
186
|
+
return;
|
|
182
187
|
if (this.#open.peek())
|
|
183
188
|
return this.#close();
|
|
184
189
|
this.#show();
|
|
@@ -191,7 +196,7 @@ export class Picker extends BoundField {
|
|
|
191
196
|
this.#close();
|
|
192
197
|
}}
|
|
193
198
|
@keydown=${(e) => {
|
|
194
|
-
if (c.state.readOnly)
|
|
199
|
+
if (c.state.readOnly || disabled)
|
|
195
200
|
return;
|
|
196
201
|
const isOpen = this.#open.peek();
|
|
197
202
|
switch (e.key) {
|