@enigmax/primitives 0.22.0 → 0.23.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/dist/chunk-3BQVOOAM.js +388 -0
- package/dist/{chunk-QQFNAKMY.js → chunk-45UHLZYT.js} +1 -1
- package/dist/chunk-4VUHQFAT.js +130 -0
- package/dist/{chunk-D5A2ZMAG.js → chunk-4ZPMP47J.js} +6 -1
- package/dist/chunk-DSBYVA7V.js +713 -0
- package/dist/{chunk-FWVWX67R.js → chunk-FGBIZDV2.js} +3 -3
- package/dist/{chunk-R4ZAEE7V.js → chunk-KEVZ5XQV.js} +1 -1
- package/dist/chunk-N6PDHMAX.js +213 -0
- package/dist/chunk-NONGREXC.js +248 -0
- package/dist/chunk-VKL3DEIQ.js +804 -0
- package/dist/chunk-WSQC3PCC.js +466 -0
- package/dist/context-menu-D3FtTn7v.d.ts +174 -0
- package/dist/{index-BHpOZncw.d.ts → index-DQNnohoo.d.ts} +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +8 -4
- package/dist/keys-D2zJs1uB.d.ts +100 -0
- package/dist/next/index.d.ts +9 -2
- package/dist/next/index.js +20 -13
- package/dist/react/context-menu.d.ts +202 -0
- package/dist/react/context-menu.js +6 -0
- package/dist/react/index.d.ts +10 -3
- package/dist/react/index.js +19 -12
- package/dist/react/input.d.ts +2 -2
- package/dist/react/input.js +1 -1
- package/dist/react/palette.d.ts +1 -1
- package/dist/react/palette.js +3 -3
- package/dist/react/search.d.ts +1 -1
- package/dist/react/search.js +2 -2
- package/dist/react/select.d.ts +217 -0
- package/dist/react/select.js +7 -0
- package/dist/react/selection.d.ts +104 -0
- package/dist/react/selection.js +4 -0
- package/dist/react-router/index.d.ts +9 -2
- package/dist/react-router/index.js +20 -13
- package/dist/search/index.d.ts +2 -2
- package/dist/search/index.js +1 -1
- package/dist/{search-BD9-5O5U.d.ts → search-DYgqRp37.d.ts} +13 -1
- package/dist/{search-UQEXAPQB.js → search-PBZORZ7P.js} +1 -1
- package/dist/select-ClSy-J1f.d.ts +100 -0
- package/dist/selection-B_pmzHpy.d.ts +150 -0
- package/package.json +21 -2
- package/recipes/context-menu/styles.css +177 -0
- package/recipes/input/styles.css +9 -0
- package/recipes/palette/styles.css +3 -0
- package/recipes/search/tailwind.tsx +3 -2
- package/recipes/select/styles.css +230 -0
- package/recipes/toast/styles.css +1 -1
- package/registry.json +358 -1
- package/src/core/context-menu.ts +694 -0
- package/src/core/keys.ts +264 -0
- package/src/core/search.ts +19 -0
- package/src/core/select.ts +404 -0
- package/src/core/selection.ts +648 -0
- package/src/index.ts +60 -1
- package/src/react/context-menu/context.ts +57 -0
- package/src/react/context-menu/index.tsx +94 -0
- package/src/react/context-menu/root.tsx +846 -0
- package/src/react/context-menu/styles.ts +186 -0
- package/src/react/index.ts +68 -1
- package/src/react/palette/root.tsx +2 -2
- package/src/react/select/context.ts +56 -0
- package/src/react/select/index.tsx +96 -0
- package/src/react/select/root.tsx +839 -0
- package/src/react/select/styles.ts +238 -0
- package/src/react/selection/index.tsx +115 -0
- package/src/react/selection/use-selection.ts +260 -0
- /package/dist/{chunk-U3V4EHOB.js → chunk-3HDEZ2E7.js} +0 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { b as ShortcutSpec, a as ShortcutEvent } from './keys-D2zJs1uB.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The parts of a selectable list that are not rendering: what is selected, where the cursor
|
|
5
|
+
* is, which row the next Shift+click measures from, and which command a key press stands for.
|
|
6
|
+
*
|
|
7
|
+
* This is the file-manager selection model, on purpose and in full: a plain click replaces
|
|
8
|
+
* the selection, Ctrl toggles one row without losing the rest, Shift takes everything between
|
|
9
|
+
* the anchor and here, Ctrl+Shift adds that range to what was already there, Ctrl+A takes the
|
|
10
|
+
* lot and Escape drops it. Every one of those is a rule people already know from Explorer,
|
|
11
|
+
* Finder and every list built on them, and every one of them is a bug when a list re-invents
|
|
12
|
+
* it - a Shift+click that measures from the last CLICK instead of the anchor, a Ctrl+click
|
|
13
|
+
* that clears the selection, an Escape that does nothing.
|
|
14
|
+
*
|
|
15
|
+
* The commands are DATA. Each one has a default binding, any of them can be rebound, removed
|
|
16
|
+
* one at a time or turned off altogether, and a binding that is not a built-in command is
|
|
17
|
+
* still matched and reported - so a list can add "star" on Mod+D without this file knowing
|
|
18
|
+
* what starring is.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* What a key press means.
|
|
23
|
+
*
|
|
24
|
+
* The first group the list PERFORMS - it owns the selection, so it moves it. The second it
|
|
25
|
+
* only REPORTS, because deleting, renaming and opening are the caller's. Both are reported,
|
|
26
|
+
* and both can be stopped by the handler, so a list that wants to confirm a delete or animate
|
|
27
|
+
* its own select-all can.
|
|
28
|
+
*/
|
|
29
|
+
type SelectionCommand = "selectAll" | "clear" | "invert" | "moveUp" | "moveDown" | "moveLeft" | "moveRight" | "moveHome" | "moveEnd" | "movePageUp" | "movePageDown" | "extendUp" | "extendDown" | "extendLeft" | "extendRight" | "extendHome" | "extendEnd" | "extendPageUp" | "extendPageDown" | "cursorUp" | "cursorDown" | "toggleCursor" | "open" | "rename" | "delete" | "deletePermanent" | "copy" | "cut" | "paste";
|
|
30
|
+
/** A command name: one of the built-ins, or anything else the caller binds. */
|
|
31
|
+
type SelectionCommandName = SelectionCommand | (string & {});
|
|
32
|
+
/**
|
|
33
|
+
* What each command is bound to out of the box.
|
|
34
|
+
*
|
|
35
|
+
* `Mod` is Command on an Apple keyboard and Control everywhere else, which is what these
|
|
36
|
+
* shortcuts mean on both. `invert` ships unbound deliberately: Explorer has the action and no
|
|
37
|
+
* key for it, and inventing one would take a combination the page may already use.
|
|
38
|
+
*/
|
|
39
|
+
declare const DEFAULT_SELECTION_SHORTCUTS: Readonly<Record<SelectionCommand, ShortcutSpec>>;
|
|
40
|
+
/**
|
|
41
|
+
* The bindings, as the caller writes them.
|
|
42
|
+
*
|
|
43
|
+
* A missing entry keeps the default, `false` removes that one command, and a string or a list
|
|
44
|
+
* replaces it. `shortcuts: false` on the options removes every one of them at once - for a
|
|
45
|
+
* list inside an editor, or one whose keyboard belongs to something else.
|
|
46
|
+
*/
|
|
47
|
+
type SelectionShortcuts = Partial<Record<SelectionCommandName, ShortcutSpec>>;
|
|
48
|
+
/** The modifiers a click carried. A real MouseEvent or PointerEvent satisfies it. */
|
|
49
|
+
interface SelectionClickModifiers {
|
|
50
|
+
ctrlKey?: boolean;
|
|
51
|
+
metaKey?: boolean;
|
|
52
|
+
shiftKey?: boolean;
|
|
53
|
+
}
|
|
54
|
+
interface SelectionCommandEvent<Item> {
|
|
55
|
+
command: SelectionCommandName;
|
|
56
|
+
/**
|
|
57
|
+
* What the command applies to: everything selected, or the row under the cursor when
|
|
58
|
+
* nothing is. That fallback is the file-manager rule - pressing Delete with one row
|
|
59
|
+
* focused and none selected deletes that row.
|
|
60
|
+
*/
|
|
61
|
+
items: Item[];
|
|
62
|
+
ids: string[];
|
|
63
|
+
/** The row the cursor is on, selected or not. */
|
|
64
|
+
cursor: Item | null;
|
|
65
|
+
cursorIndex: number;
|
|
66
|
+
/** The press that matched, for a handler that needs the modifiers back. */
|
|
67
|
+
event: ShortcutEvent;
|
|
68
|
+
/** Stop what the list would have done - the moves, select-all, clear, invert. */
|
|
69
|
+
preventDefault(): void;
|
|
70
|
+
defaultPrevented: boolean;
|
|
71
|
+
}
|
|
72
|
+
interface SelectionOptions<Item> {
|
|
73
|
+
items?: readonly Item[];
|
|
74
|
+
/** How a row is identified across reorders and reloads. Default: its index, as a string. */
|
|
75
|
+
getId?: (item: Item, index: number) => string;
|
|
76
|
+
/** A row that cannot be selected or landed on. Still rendered, still counted. */
|
|
77
|
+
disabled?: (item: Item, index: number) => boolean;
|
|
78
|
+
/** One row at a time: Ctrl and Shift stop meaning anything. Default: many. */
|
|
79
|
+
multiple?: boolean;
|
|
80
|
+
/** Columns in a grid, so Left and Right move by one and Up and Down by a row. Default 1. */
|
|
81
|
+
columns?: number;
|
|
82
|
+
/** How far PageUp and PageDown jump. Default 10. */
|
|
83
|
+
page?: number;
|
|
84
|
+
/** Rebind, remove or extend the commands. `false` removes every binding. */
|
|
85
|
+
shortcuts?: SelectionShortcuts | false;
|
|
86
|
+
/** A command matched a press. Fired for built-ins too, before the list acts on them. */
|
|
87
|
+
onCommand?: (event: SelectionCommandEvent<Item>) => void;
|
|
88
|
+
/** Every state change. */
|
|
89
|
+
onChange?: (state: SelectionState<Item>) => void;
|
|
90
|
+
/** Only the selection, when it actually changed. */
|
|
91
|
+
onSelectionChange?: (ids: string[], items: Item[]) => void;
|
|
92
|
+
}
|
|
93
|
+
interface SelectionState<Item> {
|
|
94
|
+
/** Selected ids, in the order the rows appear - not the order they were clicked. */
|
|
95
|
+
selected: string[];
|
|
96
|
+
/** The same ids, for a renderer that asks per row. */
|
|
97
|
+
selectedSet: ReadonlySet<string>;
|
|
98
|
+
/** The selected rows themselves. */
|
|
99
|
+
items: Item[];
|
|
100
|
+
/** Where the keyboard is. -1 when the list has not been touched. */
|
|
101
|
+
cursor: number;
|
|
102
|
+
/** Where the next Shift+click or Shift+arrow measures from. */
|
|
103
|
+
anchor: number;
|
|
104
|
+
count: number;
|
|
105
|
+
/** Every selectable row is selected. What a header checkbox reads. */
|
|
106
|
+
allSelected: boolean;
|
|
107
|
+
/** Some but not all - the third state of that same checkbox. */
|
|
108
|
+
partiallySelected: boolean;
|
|
109
|
+
/** A rubber band is being dragged. */
|
|
110
|
+
marquee: boolean;
|
|
111
|
+
}
|
|
112
|
+
interface SelectionInstance<Item> {
|
|
113
|
+
readonly state: SelectionState<Item>;
|
|
114
|
+
/** A click on a row, with its modifiers. The whole Explorer rule, in one call. */
|
|
115
|
+
click(index: number, modifiers?: SelectionClickModifiers): void;
|
|
116
|
+
/** A key press. Returns whether a command matched, so the caller knows to prevent it. */
|
|
117
|
+
keyDown(event: ShortcutEvent): boolean;
|
|
118
|
+
select(ids: readonly string[]): void;
|
|
119
|
+
selectIndex(index: number, additive?: boolean): void;
|
|
120
|
+
toggle(index: number): void;
|
|
121
|
+
/** Everything from the anchor to here. `additive` keeps what was already selected. */
|
|
122
|
+
selectRange(index: number, additive?: boolean): void;
|
|
123
|
+
selectAll(): void;
|
|
124
|
+
clear(): void;
|
|
125
|
+
invert(): void;
|
|
126
|
+
/** Move the cursor. `extend` takes everything from the anchor, `carry` leaves the selection alone. */
|
|
127
|
+
moveCursor(delta: number, extend?: boolean, carry?: boolean): void;
|
|
128
|
+
setCursor(index: number): void;
|
|
129
|
+
/**
|
|
130
|
+
* What an action on this row applies to: the selection when the row is part of it, and the
|
|
131
|
+
* row alone when it is not. The rule every file manager's context menu follows - and the
|
|
132
|
+
* one that makes right-clicking outside a selection act on what was clicked.
|
|
133
|
+
*/
|
|
134
|
+
targets(index: number): Item[];
|
|
135
|
+
/** Start a rubber band. `additive` keeps the current selection as its base. */
|
|
136
|
+
beginMarquee(additive?: boolean): void;
|
|
137
|
+
/** The rows the band currently covers. */
|
|
138
|
+
marqueeTo(indices: readonly number[]): void;
|
|
139
|
+
endMarquee(): void;
|
|
140
|
+
/** The resolved bindings, so a menu can print the same shortcut the list listens for. */
|
|
141
|
+
bindings(): Record<string, ShortcutSpec>;
|
|
142
|
+
/** What one command is bound to, or false when it has no binding. */
|
|
143
|
+
binding(command: SelectionCommandName): ShortcutSpec;
|
|
144
|
+
update(options: Partial<SelectionOptions<Item>>): void;
|
|
145
|
+
subscribe(listener: (state: SelectionState<Item>) => void): () => void;
|
|
146
|
+
destroy(): void;
|
|
147
|
+
}
|
|
148
|
+
declare function createSelection<Item>(options?: SelectionOptions<Item>): SelectionInstance<Item>;
|
|
149
|
+
|
|
150
|
+
export { DEFAULT_SELECTION_SHORTCUTS as D, type SelectionClickModifiers as S, type SelectionCommand as a, type SelectionCommandEvent as b, type SelectionCommandName as c, type SelectionInstance as d, type SelectionOptions as e, type SelectionShortcuts as f, type SelectionState as g, createSelection as h };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enigmax/primitives",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Headless components: the behaviour, the timing and the accessibility, with no styles of their own. Framework-agnostic core plus thin adapters.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -25,6 +25,18 @@
|
|
|
25
25
|
"types": "./dist/react/palette.d.ts",
|
|
26
26
|
"import": "./dist/react/palette.js"
|
|
27
27
|
},
|
|
28
|
+
"./react/select": {
|
|
29
|
+
"types": "./dist/react/select.d.ts",
|
|
30
|
+
"import": "./dist/react/select.js"
|
|
31
|
+
},
|
|
32
|
+
"./react/context-menu": {
|
|
33
|
+
"types": "./dist/react/context-menu.d.ts",
|
|
34
|
+
"import": "./dist/react/context-menu.js"
|
|
35
|
+
},
|
|
36
|
+
"./react/selection": {
|
|
37
|
+
"types": "./dist/react/selection.d.ts",
|
|
38
|
+
"import": "./dist/react/selection.js"
|
|
39
|
+
},
|
|
28
40
|
"./react/button": {
|
|
29
41
|
"types": "./dist/react/button.d.ts",
|
|
30
42
|
"import": "./dist/react/button.js"
|
|
@@ -79,7 +91,9 @@
|
|
|
79
91
|
"./input.css": "./recipes/input/styles.css",
|
|
80
92
|
"./marquee.css": "./recipes/marquee/styles.css",
|
|
81
93
|
"./logo-marquee.css": "./recipes/logo-marquee/styles.css",
|
|
82
|
-
"./palette.css": "./recipes/palette/styles.css"
|
|
94
|
+
"./palette.css": "./recipes/palette/styles.css",
|
|
95
|
+
"./select.css": "./recipes/select/styles.css",
|
|
96
|
+
"./context-menu.css": "./recipes/context-menu/styles.css"
|
|
83
97
|
},
|
|
84
98
|
"scripts": {
|
|
85
99
|
"build": "tsup",
|
|
@@ -143,12 +157,14 @@
|
|
|
143
157
|
},
|
|
144
158
|
"keywords": [
|
|
145
159
|
"carousel",
|
|
160
|
+
"context-menu",
|
|
146
161
|
"enigma",
|
|
147
162
|
"flags",
|
|
148
163
|
"fuse",
|
|
149
164
|
"headless",
|
|
150
165
|
"input",
|
|
151
166
|
"marquee",
|
|
167
|
+
"menu",
|
|
152
168
|
"network",
|
|
153
169
|
"notifications",
|
|
154
170
|
"password",
|
|
@@ -156,6 +172,9 @@
|
|
|
156
172
|
"react",
|
|
157
173
|
"relative-time",
|
|
158
174
|
"search",
|
|
175
|
+
"select",
|
|
176
|
+
"selection",
|
|
177
|
+
"shortcuts",
|
|
159
178
|
"toast",
|
|
160
179
|
"unstyled",
|
|
161
180
|
"vanilla"
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The context-menu stylesheet.
|
|
3
|
+
*
|
|
4
|
+
* GENERATED from src/react/context-menu/styles.ts by scripts/sync-recipes.mjs - edit that file, not
|
|
5
|
+
* this one. It exists as a stylesheet as well as a string because the component injects it,
|
|
6
|
+
* and this is the same sheet for anyone who would rather import it, copy it with
|
|
7
|
+
* `enigma add context-menu --copy`, or fork it outright.
|
|
8
|
+
*/
|
|
9
|
+
:root {
|
|
10
|
+
--enigma-menu-bg: #1c1c1c;
|
|
11
|
+
--enigma-menu-border: #333333;
|
|
12
|
+
--enigma-menu-radius: 0.625rem;
|
|
13
|
+
--enigma-menu-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
|
|
14
|
+
--enigma-menu-text: #f5f5f5;
|
|
15
|
+
--enigma-menu-muted: #a3a3a3;
|
|
16
|
+
--enigma-menu-font-size: 0.8125rem;
|
|
17
|
+
--enigma-menu-min-width: 12rem;
|
|
18
|
+
--enigma-menu-max-height: 20rem;
|
|
19
|
+
|
|
20
|
+
--enigma-menu-active-bg: #2a2a2a;
|
|
21
|
+
--enigma-menu-accent: #fbbf24;
|
|
22
|
+
--enigma-menu-danger: #f87171;
|
|
23
|
+
--enigma-menu-danger-bg: rgba(248, 113, 113, 0.12);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@media (prefers-color-scheme: light) {
|
|
27
|
+
:root {
|
|
28
|
+
--enigma-menu-bg: #ffffff;
|
|
29
|
+
--enigma-menu-border: #e5e5e5;
|
|
30
|
+
--enigma-menu-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);
|
|
31
|
+
--enigma-menu-text: #171717;
|
|
32
|
+
--enigma-menu-muted: #737373;
|
|
33
|
+
--enigma-menu-active-bg: #f5f5f5;
|
|
34
|
+
--enigma-menu-accent: #b45309;
|
|
35
|
+
--enigma-menu-danger: #b91c1c;
|
|
36
|
+
--enigma-menu-danger-bg: rgba(185, 28, 28, 0.08);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Every panel is positioned in VIEWPORT coordinates, which is what a pointer event reports
|
|
41
|
+
and what a menu opened at the pointer needs. Fixed rather than absolute so a scrolling
|
|
42
|
+
ancestor cannot drag it away from the place it was opened. */
|
|
43
|
+
[data-enigma-menu-panel] {
|
|
44
|
+
position: fixed; z-index: 60;
|
|
45
|
+
box-sizing: border-box;
|
|
46
|
+
min-width: var(--enigma-menu-min-width);
|
|
47
|
+
max-width: min(22rem, calc(100vw - 1rem));
|
|
48
|
+
padding: 0.25rem;
|
|
49
|
+
color: var(--enigma-menu-text);
|
|
50
|
+
background: var(--enigma-menu-bg);
|
|
51
|
+
border: 1px solid var(--enigma-menu-border);
|
|
52
|
+
border-radius: var(--enigma-menu-radius);
|
|
53
|
+
box-shadow: var(--enigma-menu-shadow);
|
|
54
|
+
font-size: var(--enigma-menu-font-size);
|
|
55
|
+
animation: enigma-menu-in 90ms ease-out;
|
|
56
|
+
/* Measured before it is placed, and placed before it is seen: without this the panel is
|
|
57
|
+
painted at 0,0 for one frame and jumps to the pointer on the next. */
|
|
58
|
+
visibility: hidden;
|
|
59
|
+
}
|
|
60
|
+
[data-enigma-menu-panel][data-placed] { visibility: visible; }
|
|
61
|
+
@keyframes enigma-menu-in {
|
|
62
|
+
from { opacity: 0; transform: scale(0.98); }
|
|
63
|
+
to { opacity: 1; transform: none; }
|
|
64
|
+
}
|
|
65
|
+
@media (prefers-reduced-motion: reduce) {
|
|
66
|
+
[data-enigma-menu-panel] { animation: none; }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* The heading: what the menu is acting ON. A long file name is cut rather than allowed to
|
|
70
|
+
set the panel's width, and the full one stays reachable through its title attribute. */
|
|
71
|
+
[data-enigma-menu-title] {
|
|
72
|
+
margin: 0; padding: 0.375rem 0.5rem 0.25rem;
|
|
73
|
+
font-size: 0.6875rem; font-weight: 600;
|
|
74
|
+
color: var(--enigma-menu-muted);
|
|
75
|
+
overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
[data-enigma-menu-search] {
|
|
79
|
+
width: 100%; box-sizing: border-box;
|
|
80
|
+
padding: 0.375rem 0.5rem 0.5rem;
|
|
81
|
+
font: inherit; font-size: var(--enigma-menu-font-size);
|
|
82
|
+
color: var(--enigma-menu-text); background: transparent;
|
|
83
|
+
border: 0; border-bottom: 1px solid var(--enigma-menu-border);
|
|
84
|
+
outline: none;
|
|
85
|
+
}
|
|
86
|
+
[data-enigma-menu-search]::-webkit-search-cancel-button,
|
|
87
|
+
[data-enigma-menu-search]::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }
|
|
88
|
+
|
|
89
|
+
[data-enigma-menu-list] {
|
|
90
|
+
max-height: var(--enigma-menu-max-height);
|
|
91
|
+
overflow-y: auto; overscroll-behavior: contain;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
[data-enigma-menu-item] {
|
|
95
|
+
display: flex; align-items: center; gap: 0.5rem;
|
|
96
|
+
padding: 0.3125rem 0.5rem; border-radius: 0.375rem;
|
|
97
|
+
line-height: 1.4;
|
|
98
|
+
color: var(--enigma-menu-text);
|
|
99
|
+
/* The hand, not the arrow. A native menu uses the arrow because the whole surface is
|
|
100
|
+
drawn by the OS and nothing on a page looks like it; here the rows sit on a web page,
|
|
101
|
+
where the hand is what says "this is a thing you press". */
|
|
102
|
+
cursor: pointer; user-select: none;
|
|
103
|
+
}
|
|
104
|
+
[data-enigma-menu-item][data-active] { background: var(--enigma-menu-active-bg); }
|
|
105
|
+
[data-enigma-menu-item][data-disabled] { opacity: 0.45; cursor: not-allowed; }
|
|
106
|
+
[data-enigma-menu-item][data-destructive] { color: var(--enigma-menu-danger); }
|
|
107
|
+
[data-enigma-menu-item][data-destructive][data-active] { background: var(--enigma-menu-danger-bg); }
|
|
108
|
+
|
|
109
|
+
[data-enigma-menu-item-text] { display: grid; min-width: 0; }
|
|
110
|
+
[data-enigma-menu-item-label] { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
111
|
+
[data-enigma-menu-item-description] {
|
|
112
|
+
font-size: 0.6875rem; color: var(--enigma-menu-muted);
|
|
113
|
+
overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
[data-enigma-menu-icon] { display: inline-flex; align-items: center; flex: none; width: 1rem; max-height: 1.25em; line-height: 0; }
|
|
117
|
+
|
|
118
|
+
/* The shortcut, on the right, the way every desktop menu prints it. margin-left: auto is
|
|
119
|
+
what pushes it there, and flex: none is what stops a long label squashing it. */
|
|
120
|
+
[data-enigma-menu-shortcut] {
|
|
121
|
+
display: flex; align-items: center; gap: 0.125rem;
|
|
122
|
+
flex: none; margin-left: auto; padding-left: 1.5rem;
|
|
123
|
+
font-size: 0.6875rem; color: var(--enigma-menu-muted);
|
|
124
|
+
font-variant-numeric: tabular-nums;
|
|
125
|
+
}
|
|
126
|
+
[data-enigma-menu-shortcut] kbd { font: inherit; }
|
|
127
|
+
|
|
128
|
+
/* The submenu arrow shares the shortcut's slot, so a row never has both pushed apart. */
|
|
129
|
+
[data-enigma-menu-arrow] {
|
|
130
|
+
flex: none; margin-left: auto;
|
|
131
|
+
width: 0.35rem; height: 0.35rem;
|
|
132
|
+
border-right: 1.5px solid currentColor; border-top: 1.5px solid currentColor;
|
|
133
|
+
transform: rotate(45deg);
|
|
134
|
+
opacity: 0.6;
|
|
135
|
+
}
|
|
136
|
+
[data-enigma-menu-shortcut] + [data-enigma-menu-arrow] { margin-left: 0.5rem; }
|
|
137
|
+
|
|
138
|
+
/* Check and radio: drawn rather than loaded, and reserving their space either way so
|
|
139
|
+
choosing a row does not shift the label under the pointer. */
|
|
140
|
+
[data-enigma-menu-check] {
|
|
141
|
+
flex: none; width: 1rem; height: 1rem;
|
|
142
|
+
display: grid; place-items: center;
|
|
143
|
+
color: var(--enigma-menu-accent);
|
|
144
|
+
}
|
|
145
|
+
[data-enigma-menu-check]::after { content: ""; width: 0.35rem; height: 0.6rem; opacity: 0; }
|
|
146
|
+
[data-enigma-menu-item][aria-checked="true"] [data-enigma-menu-check]::after {
|
|
147
|
+
opacity: 1;
|
|
148
|
+
border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
|
|
149
|
+
transform: rotate(45deg) translate(-0.05rem, -0.05rem);
|
|
150
|
+
}
|
|
151
|
+
[data-enigma-menu-item][role="menuitemradio"] [data-enigma-menu-check]::after {
|
|
152
|
+
width: 0.4rem; height: 0.4rem; border-radius: 999px; background: currentColor;
|
|
153
|
+
transform: none; border: 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
[data-enigma-menu-separator] {
|
|
157
|
+
height: 1px; margin: 0.25rem 0.25rem;
|
|
158
|
+
background: var(--enigma-menu-border);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
[data-enigma-menu-label] {
|
|
162
|
+
margin: 0.375rem 0 0.125rem; padding: 0 0.5rem;
|
|
163
|
+
font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em;
|
|
164
|
+
color: var(--enigma-menu-muted);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
[data-enigma-menu-status] {
|
|
168
|
+
margin: 0; padding: 0.625rem 0.5rem;
|
|
169
|
+
font-size: var(--enigma-menu-font-size); color: var(--enigma-menu-muted);
|
|
170
|
+
/* The quoted query is cut in the text as well, but a pasted string with no spaces has no
|
|
171
|
+
break opportunity at all - so this is what stops it stretching the panel. */
|
|
172
|
+
overflow-wrap: anywhere;
|
|
173
|
+
}
|
|
174
|
+
[data-enigma-menu-status][data-error] { color: var(--enigma-menu-danger); }
|
|
175
|
+
|
|
176
|
+
/* The end-of-window marker: one pixel, so it has a box to be scrolled into. */
|
|
177
|
+
[data-enigma-menu-more] { height: 1px; }
|
package/recipes/input/styles.css
CHANGED
|
@@ -57,6 +57,15 @@
|
|
|
57
57
|
background: transparent; border: 0; outline: none;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/* The platform's own clear button, gone. A search field here already renders one - so
|
|
61
|
+
WebKit draws a SECOND cross next to it, and only in WebKit, which makes it a duplicate
|
|
62
|
+
half the visitors see and the other half never do. */
|
|
63
|
+
[data-enigma-input]::-webkit-search-cancel-button,
|
|
64
|
+
[data-enigma-input]::-webkit-search-decoration,
|
|
65
|
+
[data-enigma-input]::-webkit-search-results-button {
|
|
66
|
+
-webkit-appearance: none; appearance: none; display: none;
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
/* A breached password is worth showing on the field itself, not only in the message. */
|
|
61
70
|
[data-enigma-input-root][data-breached] [data-enigma-input-field] { border-color: var(--enigma-input-danger); }
|
|
62
71
|
|
|
@@ -214,6 +214,9 @@
|
|
|
214
214
|
text-align: center;
|
|
215
215
|
color: var(--enigma-palette-muted);
|
|
216
216
|
font-size: 0.875rem;
|
|
217
|
+
/* It quotes the query, and a pasted string with no spaces in it has no break
|
|
218
|
+
opportunity: without this it runs out of the panel rather than wrapping. */
|
|
219
|
+
overflow-wrap: anywhere;
|
|
217
220
|
}
|
|
218
221
|
|
|
219
222
|
[data-enigma-palette-footer] {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Fuse from "fuse.js";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
|
-
import { useSearch } from "@enigmax/primitives/react";
|
|
3
|
+
import { useSearch, shortenQuery } from "@enigmax/primitives/react";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* A styled search field with its result list, yours to edit.
|
|
@@ -46,12 +46,13 @@ export function Search<T>({
|
|
|
46
46
|
rounded-lg border border-neutral-700 bg-neutral-900 px-3 py-2.5
|
|
47
47
|
text-sm text-neutral-100 outline-none
|
|
48
48
|
placeholder:text-neutral-500 focus:border-neutral-400
|
|
49
|
+
[&::-webkit-search-cancel-button]:appearance-none
|
|
49
50
|
"
|
|
50
51
|
/>
|
|
51
52
|
|
|
52
53
|
{query && (
|
|
53
54
|
<p className="font-mono text-[11px] text-neutral-500">
|
|
54
|
-
{results.length} result{results.length === 1 ? "" : "s"} for "{query}"
|
|
55
|
+
{results.length} result{results.length === 1 ? "" : "s"} for "{shortenQuery(query)}"
|
|
55
56
|
</p>
|
|
56
57
|
)}
|
|
57
58
|
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The select stylesheet.
|
|
3
|
+
*
|
|
4
|
+
* GENERATED from src/react/select/styles.ts by scripts/sync-recipes.mjs - edit that file, not
|
|
5
|
+
* this one. It exists as a stylesheet as well as a string because the component injects it,
|
|
6
|
+
* and this is the same sheet for anyone who would rather import it, copy it with
|
|
7
|
+
* `enigma add select --copy`, or fork it outright.
|
|
8
|
+
*/
|
|
9
|
+
:root {
|
|
10
|
+
--enigma-select-bg: #171717;
|
|
11
|
+
--enigma-select-border: #404040;
|
|
12
|
+
--enigma-select-border-focus: #a3a3a3;
|
|
13
|
+
--enigma-select-text: #f5f5f5;
|
|
14
|
+
--enigma-select-muted: #a3a3a3;
|
|
15
|
+
--enigma-select-radius: 0.5rem;
|
|
16
|
+
--enigma-select-font-size: 0.875rem;
|
|
17
|
+
--enigma-select-padding: 0.5rem 0.75rem;
|
|
18
|
+
|
|
19
|
+
--enigma-select-panel-bg: #1c1c1c;
|
|
20
|
+
--enigma-select-panel-border: #333333;
|
|
21
|
+
--enigma-select-panel-radius: 0.625rem;
|
|
22
|
+
--enigma-select-panel-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
|
|
23
|
+
--enigma-select-panel-width: 100%;
|
|
24
|
+
--enigma-select-list-height: 15rem;
|
|
25
|
+
|
|
26
|
+
--enigma-select-active-bg: #2a2a2a;
|
|
27
|
+
--enigma-select-accent: #fbbf24;
|
|
28
|
+
--enigma-select-tag-bg: #2a2a2a;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@media (prefers-color-scheme: light) {
|
|
32
|
+
:root {
|
|
33
|
+
--enigma-select-bg: #ffffff;
|
|
34
|
+
--enigma-select-border: #d4d4d4;
|
|
35
|
+
--enigma-select-border-focus: #737373;
|
|
36
|
+
--enigma-select-text: #171717;
|
|
37
|
+
--enigma-select-muted: #737373;
|
|
38
|
+
--enigma-select-panel-bg: #ffffff;
|
|
39
|
+
--enigma-select-panel-border: #e5e5e5;
|
|
40
|
+
--enigma-select-panel-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);
|
|
41
|
+
--enigma-select-active-bg: #f5f5f5;
|
|
42
|
+
--enigma-select-accent: #b45309;
|
|
43
|
+
--enigma-select-tag-bg: #f0f0f0;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
[data-enigma-select-root] { position: relative; display: inline-block; min-width: 0; }
|
|
48
|
+
|
|
49
|
+
[data-enigma-select-trigger] {
|
|
50
|
+
display: flex; align-items: center; gap: 0.5rem;
|
|
51
|
+
width: 100%; min-width: 0; box-sizing: border-box;
|
|
52
|
+
padding: var(--enigma-select-padding);
|
|
53
|
+
font: inherit; font-size: var(--enigma-select-font-size); text-align: left;
|
|
54
|
+
color: var(--enigma-select-text); background: var(--enigma-select-bg);
|
|
55
|
+
border: 1px solid var(--enigma-select-border); border-radius: var(--enigma-select-radius);
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
}
|
|
58
|
+
[data-enigma-select-trigger]:focus-visible { outline: 2px solid var(--enigma-select-border-focus); outline-offset: 1px; }
|
|
59
|
+
[data-enigma-select-trigger][data-open] { border-color: var(--enigma-select-border-focus); }
|
|
60
|
+
[data-enigma-select-trigger]:disabled,
|
|
61
|
+
[data-enigma-select-trigger][aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }
|
|
62
|
+
/* Nothing to choose, or nothing yet: the control reads as unavailable and the caret fades,
|
|
63
|
+
because a caret at full strength promises a panel that will never open. Not the disabled
|
|
64
|
+
look, which would say the field is off rather than that the list is empty. */
|
|
65
|
+
[data-enigma-select-trigger][data-empty],
|
|
66
|
+
[data-enigma-select-trigger][data-loading] { opacity: 1; cursor: default; }
|
|
67
|
+
[data-enigma-select-trigger][data-empty] [data-enigma-select-caret],
|
|
68
|
+
[data-enigma-select-trigger][data-loading] [data-enigma-select-caret] { opacity: 0.25; }
|
|
69
|
+
|
|
70
|
+
/* The indicator: one slot, holding the caret and the × stacked in the same cell. Side by
|
|
71
|
+
side they would be two targets a pixel apart - one of which throws away your choice - and
|
|
72
|
+
the pair would change the trigger's width the moment a value appeared. */
|
|
73
|
+
[data-enigma-select-indicator] {
|
|
74
|
+
display: grid; place-items: center; flex: none;
|
|
75
|
+
margin-left: auto; width: 1rem; height: 1rem;
|
|
76
|
+
}
|
|
77
|
+
[data-enigma-select-indicator] > * { grid-area: 1 / 1; }
|
|
78
|
+
|
|
79
|
+
/* The caret, drawn rather than loaded: an icon font or an SVG file is a request, and this
|
|
80
|
+
is two borders. */
|
|
81
|
+
[data-enigma-select-caret] {
|
|
82
|
+
width: 0.4rem; height: 0.4rem;
|
|
83
|
+
/* Decoration, and it must not be the hit target: the rotation gives it a stacking
|
|
84
|
+
context, which paints it OVER the × sharing its cell and swallows the click. */
|
|
85
|
+
pointer-events: none;
|
|
86
|
+
border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
|
|
87
|
+
transform: translateY(-0.1rem) rotate(45deg);
|
|
88
|
+
opacity: 0.6; transition: transform 120ms ease-out, opacity 120ms ease-out;
|
|
89
|
+
}
|
|
90
|
+
[data-enigma-select-trigger][data-open] [data-enigma-select-caret] { transform: translateY(0.1rem) rotate(225deg); }
|
|
91
|
+
|
|
92
|
+
[data-enigma-select-value] {
|
|
93
|
+
display: flex; align-items: center; gap: 0.375rem;
|
|
94
|
+
/* min-width: 0 is what lets the ellipsis happen: a flex item's default minimum is its
|
|
95
|
+
content, so without it a long label pushes the caret out of the button. */
|
|
96
|
+
min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
|
|
97
|
+
}
|
|
98
|
+
[data-enigma-select-value][data-placeholder] { color: var(--enigma-select-muted); }
|
|
99
|
+
[data-enigma-select-value][data-tags] { flex-wrap: wrap; white-space: normal; }
|
|
100
|
+
|
|
101
|
+
[data-enigma-select-tag] {
|
|
102
|
+
display: inline-flex; align-items: center; gap: 0.25rem;
|
|
103
|
+
padding: 0.125rem 0.25rem 0.125rem 0.5rem;
|
|
104
|
+
font-size: 0.8125rem; line-height: 1.4;
|
|
105
|
+
background: var(--enigma-select-tag-bg); border-radius: 999px;
|
|
106
|
+
}
|
|
107
|
+
[data-enigma-select-tag][data-rest] { padding: 0.125rem 0.5rem; color: var(--enigma-select-muted); }
|
|
108
|
+
|
|
109
|
+
[data-enigma-select-tag-remove] {
|
|
110
|
+
display: grid; place-items: center;
|
|
111
|
+
width: 1rem; height: 1rem; border-radius: 999px;
|
|
112
|
+
color: var(--enigma-select-muted); font-size: 0.875rem; line-height: 1; cursor: pointer;
|
|
113
|
+
}
|
|
114
|
+
[data-enigma-select-tag-remove]:hover { color: var(--enigma-select-text); background: var(--enigma-select-active-bg); }
|
|
115
|
+
|
|
116
|
+
/* The ×, in the caret's place and only while the control is under the pointer or holds
|
|
117
|
+
focus. ":focus" and not ":focus-visible": a tap focuses the button, and on a touch screen
|
|
118
|
+
there is no hover to reveal it with. */
|
|
119
|
+
[data-enigma-select-clear] {
|
|
120
|
+
display: grid; place-items: center;
|
|
121
|
+
width: 1rem; height: 1rem; border-radius: 999px;
|
|
122
|
+
color: var(--enigma-select-muted); font-size: 0.875rem; line-height: 1; cursor: pointer;
|
|
123
|
+
opacity: 0; pointer-events: none; transition: opacity 120ms ease-out;
|
|
124
|
+
}
|
|
125
|
+
[data-enigma-select-trigger]:hover [data-enigma-select-clear],
|
|
126
|
+
[data-enigma-select-trigger]:focus [data-enigma-select-clear] { opacity: 1; pointer-events: auto; }
|
|
127
|
+
/* Scoped to a trigger that HAS a × - otherwise hovering a select with nothing to clear
|
|
128
|
+
would hide its caret and leave an empty slot. */
|
|
129
|
+
[data-enigma-select-trigger][data-clearable]:hover [data-enigma-select-caret],
|
|
130
|
+
[data-enigma-select-trigger][data-clearable]:focus [data-enigma-select-caret] { opacity: 0; }
|
|
131
|
+
[data-enigma-select-clear]:hover { color: var(--enigma-select-text); background: var(--enigma-select-active-bg); }
|
|
132
|
+
|
|
133
|
+
[data-enigma-select-content] {
|
|
134
|
+
position: absolute; z-index: 50;
|
|
135
|
+
top: calc(100% + 0.25rem); left: 0;
|
|
136
|
+
min-width: var(--enigma-select-panel-width); box-sizing: border-box;
|
|
137
|
+
padding: 0.25rem;
|
|
138
|
+
background: var(--enigma-select-panel-bg);
|
|
139
|
+
border: 1px solid var(--enigma-select-panel-border);
|
|
140
|
+
border-radius: var(--enigma-select-panel-radius);
|
|
141
|
+
box-shadow: var(--enigma-select-panel-shadow);
|
|
142
|
+
animation: enigma-select-in 120ms ease-out;
|
|
143
|
+
/* A bound, because the content is arbitrary: a long option label or a query typed into
|
|
144
|
+
the filter would otherwise widen the panel with it. min-width still wins for a
|
|
145
|
+
trigger wider than this, which is what it is for. */
|
|
146
|
+
max-width: min(28rem, calc(100vw - 2rem));
|
|
147
|
+
}
|
|
148
|
+
/* Opening upwards is not a variant, it is the same panel measured against the window: near
|
|
149
|
+
the bottom of the screen the list would otherwise be unreachable. */
|
|
150
|
+
[data-enigma-select-content][data-side="top"] { top: auto; bottom: calc(100% + 0.25rem); }
|
|
151
|
+
[data-enigma-select-content][data-state="closed"] { animation: enigma-select-out 120ms ease-in forwards; pointer-events: none; }
|
|
152
|
+
|
|
153
|
+
@keyframes enigma-select-in {
|
|
154
|
+
from { opacity: 0; transform: translateY(-0.25rem); }
|
|
155
|
+
to { opacity: 1; transform: none; }
|
|
156
|
+
}
|
|
157
|
+
@keyframes enigma-select-out {
|
|
158
|
+
from { opacity: 1; }
|
|
159
|
+
to { opacity: 0; transform: translateY(-0.125rem); }
|
|
160
|
+
}
|
|
161
|
+
@media (prefers-reduced-motion: reduce) {
|
|
162
|
+
[data-enigma-select-content],
|
|
163
|
+
[data-enigma-select-content][data-state="closed"] { animation: none; }
|
|
164
|
+
[data-enigma-select-trigger]::after { transition: none; }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
[data-enigma-select-search] {
|
|
168
|
+
width: 100%; box-sizing: border-box;
|
|
169
|
+
padding: 0.375rem 0.5rem 0.5rem;
|
|
170
|
+
font: inherit; font-size: var(--enigma-select-font-size);
|
|
171
|
+
color: var(--enigma-select-text); background: transparent;
|
|
172
|
+
border: 0; border-bottom: 1px solid var(--enigma-select-panel-border);
|
|
173
|
+
outline: none;
|
|
174
|
+
}
|
|
175
|
+
/* WebKit's own clear cross, gone: it is drawn on no other engine, so leaving it in gives
|
|
176
|
+
half the visitors a control the other half never sees. */
|
|
177
|
+
[data-enigma-select-search]::-webkit-search-cancel-button,
|
|
178
|
+
[data-enigma-select-search]::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }
|
|
179
|
+
|
|
180
|
+
[data-enigma-select-list] {
|
|
181
|
+
max-height: var(--enigma-select-list-height);
|
|
182
|
+
margin-top: 0.25rem;
|
|
183
|
+
overflow-y: auto; overscroll-behavior: contain;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
[data-enigma-select-group-label] {
|
|
187
|
+
margin: 0.375rem 0 0.125rem; padding: 0 0.5rem;
|
|
188
|
+
font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em;
|
|
189
|
+
color: var(--enigma-select-muted);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
[data-enigma-select-option] {
|
|
193
|
+
display: flex; align-items: center; gap: 0.5rem;
|
|
194
|
+
padding: 0.4375rem 0.5rem; border-radius: 0.375rem;
|
|
195
|
+
font-size: var(--enigma-select-font-size); color: var(--enigma-select-text);
|
|
196
|
+
cursor: pointer;
|
|
197
|
+
}
|
|
198
|
+
[data-enigma-select-option][data-active] { background: var(--enigma-select-active-bg); }
|
|
199
|
+
[data-enigma-select-option][data-disabled] { opacity: 0.45; cursor: not-allowed; }
|
|
200
|
+
|
|
201
|
+
[data-enigma-select-option-text] { display: grid; min-width: 0; }
|
|
202
|
+
[data-enigma-select-option-label] { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
203
|
+
[data-enigma-select-option-description] {
|
|
204
|
+
font-size: 0.75rem; color: var(--enigma-select-muted);
|
|
205
|
+
overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
[data-enigma-select-icon] { display: inline-flex; align-items: center; flex: none; max-height: 1.25em; line-height: 0; }
|
|
209
|
+
|
|
210
|
+
/* The check, drawn with two borders and shown only where it belongs. Reserved space either
|
|
211
|
+
way, so choosing a row does not shift the label under the pointer. */
|
|
212
|
+
[data-enigma-select-check] {
|
|
213
|
+
flex: none; margin-left: auto;
|
|
214
|
+
width: 0.35rem; height: 0.6rem;
|
|
215
|
+
border-right: 2px solid var(--enigma-select-accent); border-bottom: 2px solid var(--enigma-select-accent);
|
|
216
|
+
transform: rotate(45deg) translate(-0.05rem, -0.05rem);
|
|
217
|
+
opacity: 0;
|
|
218
|
+
}
|
|
219
|
+
[data-enigma-select-option][data-selected] [data-enigma-select-check] { opacity: 1; }
|
|
220
|
+
|
|
221
|
+
/* The end-of-window marker: one pixel, so it has a box to be scrolled into. */
|
|
222
|
+
[data-enigma-select-more] { height: 1px; }
|
|
223
|
+
|
|
224
|
+
[data-enigma-select-empty] {
|
|
225
|
+
margin: 0; padding: 0.75rem 0.5rem;
|
|
226
|
+
font-size: var(--enigma-select-font-size); color: var(--enigma-select-muted);
|
|
227
|
+
/* The quoted query is cut in the text as well, but a pasted string with no spaces has
|
|
228
|
+
no break opportunity at all - so this is what stops it stretching the panel. */
|
|
229
|
+
overflow-wrap: anywhere;
|
|
230
|
+
}
|