@enigmax/primitives 0.22.0 → 0.24.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-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-DZMHY3SU.js +777 -0
- package/dist/{chunk-FWVWX67R.js → chunk-FGBIZDV2.js} +3 -3
- package/dist/chunk-N6PDHMAX.js +213 -0
- package/dist/chunk-NONGREXC.js +248 -0
- package/dist/{chunk-R4ZAEE7V.js → chunk-QJJ34E4G.js} +34 -10
- package/dist/chunk-S2FPN5ID.js +115 -0
- package/dist/chunk-S7EE57YB.js +9 -0
- package/dist/chunk-VKL3DEIQ.js +804 -0
- package/dist/chunk-WSQC3PCC.js +466 -0
- package/dist/chunk-Z3VDE7OA.js +532 -0
- package/dist/clipboard-menu-B_ouitfS.d.ts +264 -0
- package/dist/color-D_rZ83Oc.d.ts +152 -0
- package/dist/color-OPV3BJV6.js +452 -0
- package/dist/{index-BHpOZncw.d.ts → index-ZPvlf9vs.d.ts} +53 -5
- package/dist/index.d.ts +6 -2
- package/dist/index.js +9 -4
- package/dist/keys-D2zJs1uB.d.ts +100 -0
- package/dist/next/index.d.ts +10 -3
- package/dist/next/index.js +22 -13
- package/dist/react/context-menu.d.ts +219 -0
- package/dist/react/context-menu.js +6 -0
- package/dist/react/index.d.ts +12 -5
- package/dist/react/index.js +21 -12
- package/dist/react/input.d.ts +3 -3
- package/dist/react/input.js +2 -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 +10 -3
- package/dist/react-router/index.js +22 -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 +23 -2
- package/recipes/color/styles.css +143 -0
- package/recipes/context-menu/styles.css +185 -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 +432 -5
- package/src/core/clipboard-menu.ts +270 -0
- package/src/core/color.ts +248 -0
- 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 +92 -1
- package/src/react/context-menu/context.ts +64 -0
- package/src/react/context-menu/index.tsx +108 -0
- package/src/react/context-menu/root.tsx +970 -0
- package/src/react/context-menu/styles.ts +194 -0
- package/src/react/index.ts +100 -1
- package/src/react/input/color-styles.ts +156 -0
- package/src/react/input/color.tsx +466 -0
- package/src/react/input/index.tsx +54 -6
- package/src/react/input/types.ts +59 -3
- 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/password-C8lG4Zm9.d.ts +0 -71
- /package/dist/{chunk-U3V4EHOB.js → chunk-MSOCCQGH.js} +0 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The context menu's baseline look.
|
|
3
|
+
*
|
|
4
|
+
* A string rather than a stylesheet because `<ContextMenu.Root>` injects it, for the same
|
|
5
|
+
* reason the select does: a popup with no styles is not a plain-looking control, it is
|
|
6
|
+
* transparent text lying on top of the page - and this one is drawn at the pointer, over
|
|
7
|
+
* whatever happens to be under it. `scripts/sync-recipes.mjs` generates
|
|
8
|
+
* `recipes/context-menu/styles.css` from here, and CI fails when the two drift.
|
|
9
|
+
*
|
|
10
|
+
* Everything below is either a custom property or an attribute selector: override the
|
|
11
|
+
* properties on `:root` for a different-looking menu without writing one selector. The sheet
|
|
12
|
+
* is PREPENDED to `<head>`, so anything the document already has wins on source order at
|
|
13
|
+
* equal specificity.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const CONTEXT_MENU_STYLES = `
|
|
17
|
+
:root {
|
|
18
|
+
--enigma-menu-bg: #1c1c1c;
|
|
19
|
+
--enigma-menu-border: #333333;
|
|
20
|
+
--enigma-menu-radius: 0.625rem;
|
|
21
|
+
--enigma-menu-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
|
|
22
|
+
--enigma-menu-text: #f5f5f5;
|
|
23
|
+
--enigma-menu-muted: #a3a3a3;
|
|
24
|
+
--enigma-menu-font-size: 0.8125rem;
|
|
25
|
+
--enigma-menu-min-width: 12rem;
|
|
26
|
+
--enigma-menu-max-height: 20rem;
|
|
27
|
+
|
|
28
|
+
--enigma-menu-active-bg: #2a2a2a;
|
|
29
|
+
--enigma-menu-accent: #fbbf24;
|
|
30
|
+
--enigma-menu-danger: #f87171;
|
|
31
|
+
--enigma-menu-danger-bg: rgba(248, 113, 113, 0.12);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@media (prefers-color-scheme: light) {
|
|
35
|
+
:root {
|
|
36
|
+
--enigma-menu-bg: #ffffff;
|
|
37
|
+
--enigma-menu-border: #e5e5e5;
|
|
38
|
+
--enigma-menu-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);
|
|
39
|
+
--enigma-menu-text: #171717;
|
|
40
|
+
--enigma-menu-muted: #737373;
|
|
41
|
+
--enigma-menu-active-bg: #f5f5f5;
|
|
42
|
+
--enigma-menu-accent: #b45309;
|
|
43
|
+
--enigma-menu-danger: #b91c1c;
|
|
44
|
+
--enigma-menu-danger-bg: rgba(185, 28, 28, 0.08);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Every panel is positioned in VIEWPORT coordinates, which is what a pointer event reports
|
|
49
|
+
and what a menu opened at the pointer needs. Fixed rather than absolute so a scrolling
|
|
50
|
+
ancestor cannot drag it away from the place it was opened. */
|
|
51
|
+
[data-enigma-menu-panel] {
|
|
52
|
+
position: fixed; z-index: 60;
|
|
53
|
+
box-sizing: border-box;
|
|
54
|
+
min-width: var(--enigma-menu-min-width);
|
|
55
|
+
max-width: min(22rem, calc(100vw - 1rem));
|
|
56
|
+
padding: 0.25rem;
|
|
57
|
+
color: var(--enigma-menu-text);
|
|
58
|
+
background: var(--enigma-menu-bg);
|
|
59
|
+
border: 1px solid var(--enigma-menu-border);
|
|
60
|
+
border-radius: var(--enigma-menu-radius);
|
|
61
|
+
box-shadow: var(--enigma-menu-shadow);
|
|
62
|
+
font-size: var(--enigma-menu-font-size);
|
|
63
|
+
animation: enigma-menu-in 90ms ease-out;
|
|
64
|
+
/* Measured before it is placed, and placed before it is seen: without this the panel is
|
|
65
|
+
painted at 0,0 for one frame and jumps to the pointer on the next. */
|
|
66
|
+
visibility: hidden;
|
|
67
|
+
}
|
|
68
|
+
[data-enigma-menu-panel][data-placed] { visibility: visible; }
|
|
69
|
+
/* No focus ring around the panel. It takes focus so the keyboard has somewhere to be, not
|
|
70
|
+
because it is a control: what MOVES is the highlighted row, drawn by [data-active] and
|
|
71
|
+
announced through aria-activedescendant. A ring around the whole menu says the menu is the
|
|
72
|
+
thing focused, which is not what is happening - and it appears the moment you press an
|
|
73
|
+
arrow, over a surface that was already showing you where you are. Nothing is lost by
|
|
74
|
+
removing it, because nothing inside is reachable by Tab: the menu owns that key. */
|
|
75
|
+
[data-enigma-menu-panel]:focus,
|
|
76
|
+
[data-enigma-menu-panel]:focus-visible { outline: none; }
|
|
77
|
+
@keyframes enigma-menu-in {
|
|
78
|
+
from { opacity: 0; transform: scale(0.98); }
|
|
79
|
+
to { opacity: 1; transform: none; }
|
|
80
|
+
}
|
|
81
|
+
@media (prefers-reduced-motion: reduce) {
|
|
82
|
+
[data-enigma-menu-panel] { animation: none; }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* The heading: what the menu is acting ON. A long file name is cut rather than allowed to
|
|
86
|
+
set the panel's width, and the full one stays reachable through its title attribute. */
|
|
87
|
+
[data-enigma-menu-title] {
|
|
88
|
+
margin: 0; padding: 0.375rem 0.5rem 0.25rem;
|
|
89
|
+
font-size: 0.6875rem; font-weight: 600;
|
|
90
|
+
color: var(--enigma-menu-muted);
|
|
91
|
+
overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
[data-enigma-menu-search] {
|
|
95
|
+
width: 100%; box-sizing: border-box;
|
|
96
|
+
padding: 0.375rem 0.5rem 0.5rem;
|
|
97
|
+
font: inherit; font-size: var(--enigma-menu-font-size);
|
|
98
|
+
color: var(--enigma-menu-text); background: transparent;
|
|
99
|
+
border: 0; border-bottom: 1px solid var(--enigma-menu-border);
|
|
100
|
+
outline: none;
|
|
101
|
+
}
|
|
102
|
+
[data-enigma-menu-search]::-webkit-search-cancel-button,
|
|
103
|
+
[data-enigma-menu-search]::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }
|
|
104
|
+
|
|
105
|
+
[data-enigma-menu-list] {
|
|
106
|
+
max-height: var(--enigma-menu-max-height);
|
|
107
|
+
overflow-y: auto; overscroll-behavior: contain;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
[data-enigma-menu-item] {
|
|
111
|
+
display: flex; align-items: center; gap: 0.5rem;
|
|
112
|
+
padding: 0.3125rem 0.5rem; border-radius: 0.375rem;
|
|
113
|
+
line-height: 1.4;
|
|
114
|
+
color: var(--enigma-menu-text);
|
|
115
|
+
/* The hand, not the arrow. A native menu uses the arrow because the whole surface is
|
|
116
|
+
drawn by the OS and nothing on a page looks like it; here the rows sit on a web page,
|
|
117
|
+
where the hand is what says "this is a thing you press". */
|
|
118
|
+
cursor: pointer; user-select: none;
|
|
119
|
+
}
|
|
120
|
+
[data-enigma-menu-item][data-active] { background: var(--enigma-menu-active-bg); }
|
|
121
|
+
[data-enigma-menu-item][data-disabled] { opacity: 0.45; cursor: not-allowed; }
|
|
122
|
+
[data-enigma-menu-item][data-destructive] { color: var(--enigma-menu-danger); }
|
|
123
|
+
[data-enigma-menu-item][data-destructive][data-active] { background: var(--enigma-menu-danger-bg); }
|
|
124
|
+
|
|
125
|
+
[data-enigma-menu-item-text] { display: grid; min-width: 0; }
|
|
126
|
+
[data-enigma-menu-item-label] { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
127
|
+
[data-enigma-menu-item-description] {
|
|
128
|
+
font-size: 0.6875rem; color: var(--enigma-menu-muted);
|
|
129
|
+
overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
[data-enigma-menu-icon] { display: inline-flex; align-items: center; flex: none; width: 1rem; max-height: 1.25em; line-height: 0; }
|
|
133
|
+
|
|
134
|
+
/* The shortcut, on the right, the way every desktop menu prints it. margin-left: auto is
|
|
135
|
+
what pushes it there, and flex: none is what stops a long label squashing it. */
|
|
136
|
+
[data-enigma-menu-shortcut] {
|
|
137
|
+
display: flex; align-items: center; gap: 0.125rem;
|
|
138
|
+
flex: none; margin-left: auto; padding-left: 1.5rem;
|
|
139
|
+
font-size: 0.6875rem; color: var(--enigma-menu-muted);
|
|
140
|
+
font-variant-numeric: tabular-nums;
|
|
141
|
+
}
|
|
142
|
+
[data-enigma-menu-shortcut] kbd { font: inherit; }
|
|
143
|
+
|
|
144
|
+
/* The submenu arrow shares the shortcut's slot, so a row never has both pushed apart. */
|
|
145
|
+
[data-enigma-menu-arrow] {
|
|
146
|
+
flex: none; margin-left: auto;
|
|
147
|
+
width: 0.35rem; height: 0.35rem;
|
|
148
|
+
border-right: 1.5px solid currentColor; border-top: 1.5px solid currentColor;
|
|
149
|
+
transform: rotate(45deg);
|
|
150
|
+
opacity: 0.6;
|
|
151
|
+
}
|
|
152
|
+
[data-enigma-menu-shortcut] + [data-enigma-menu-arrow] { margin-left: 0.5rem; }
|
|
153
|
+
|
|
154
|
+
/* Check and radio: drawn rather than loaded, and reserving their space either way so
|
|
155
|
+
choosing a row does not shift the label under the pointer. */
|
|
156
|
+
[data-enigma-menu-check] {
|
|
157
|
+
flex: none; width: 1rem; height: 1rem;
|
|
158
|
+
display: grid; place-items: center;
|
|
159
|
+
color: var(--enigma-menu-accent);
|
|
160
|
+
}
|
|
161
|
+
[data-enigma-menu-check]::after { content: ""; width: 0.35rem; height: 0.6rem; opacity: 0; }
|
|
162
|
+
[data-enigma-menu-item][aria-checked="true"] [data-enigma-menu-check]::after {
|
|
163
|
+
opacity: 1;
|
|
164
|
+
border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
|
|
165
|
+
transform: rotate(45deg) translate(-0.05rem, -0.05rem);
|
|
166
|
+
}
|
|
167
|
+
[data-enigma-menu-item][role="menuitemradio"] [data-enigma-menu-check]::after {
|
|
168
|
+
width: 0.4rem; height: 0.4rem; border-radius: 999px; background: currentColor;
|
|
169
|
+
transform: none; border: 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
[data-enigma-menu-separator] {
|
|
173
|
+
height: 1px; margin: 0.25rem 0.25rem;
|
|
174
|
+
background: var(--enigma-menu-border);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
[data-enigma-menu-label] {
|
|
178
|
+
margin: 0.375rem 0 0.125rem; padding: 0 0.5rem;
|
|
179
|
+
font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em;
|
|
180
|
+
color: var(--enigma-menu-muted);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
[data-enigma-menu-status] {
|
|
184
|
+
margin: 0; padding: 0.625rem 0.5rem;
|
|
185
|
+
font-size: var(--enigma-menu-font-size); color: var(--enigma-menu-muted);
|
|
186
|
+
/* The quoted query is cut in the text as well, but a pasted string with no spaces has no
|
|
187
|
+
break opportunity at all - so this is what stops it stretching the panel. */
|
|
188
|
+
overflow-wrap: anywhere;
|
|
189
|
+
}
|
|
190
|
+
[data-enigma-menu-status][data-error] { color: var(--enigma-menu-danger); }
|
|
191
|
+
|
|
192
|
+
/* The end-of-window marker: one pixel, so it has a box to be scrolled into. */
|
|
193
|
+
[data-enigma-menu-more] { height: 1px; }
|
|
194
|
+
`;
|
package/src/react/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ export { type MarqueeOptions, type MarqueeInstance, type MarqueeHover } from "@/
|
|
|
5
5
|
export { useInput, type UseInputResult } from "@/react/use-input";
|
|
6
6
|
export { useSearch, type UseSearchResult } from "@/react/use-search";
|
|
7
7
|
export { type InputOptions, type InputAction, type InputIcon } from "@/core/input";
|
|
8
|
-
export { type SearchOptions, type SearchMatch, type FuseConstructor } from "@/core/search";
|
|
8
|
+
export { shortenQuery, type SearchOptions, type SearchMatch, type FuseConstructor } from "@/core/search";
|
|
9
9
|
export { useButton, type UseButtonResult } from "@/react/use-button";
|
|
10
10
|
export { Button, type ButtonProps } from "@/react/button";
|
|
11
11
|
export { setLinkComponent, getLinkComponent } from "@/react/link";
|
|
@@ -19,13 +19,33 @@ export {
|
|
|
19
19
|
type BreachChecker,
|
|
20
20
|
type BreachState,
|
|
21
21
|
type BreachStatus,
|
|
22
|
+
type ColorLabels,
|
|
23
|
+
type ColorPanelPlacement,
|
|
22
24
|
type PasswordOnlyProps,
|
|
23
25
|
type SearchOnlyProps,
|
|
26
|
+
type ColorOnlyProps,
|
|
24
27
|
type PlainOnlyProps
|
|
25
28
|
} from "@/react/input/types";
|
|
26
29
|
// The meter as a standalone component, for a form that renders it somewhere else. Re-exported
|
|
27
30
|
// from the chunk rather than through `Input`, so importing only `Input` leaves it behind.
|
|
28
31
|
export { PasswordStrength, type PasswordStrengthProps } from "@/react/input/password";
|
|
32
|
+
// The colour maths, for a form that has to read or write the value the picker produced. The
|
|
33
|
+
// picker itself arrives through `<Input type="color">`, so nothing here reaches into its chunk.
|
|
34
|
+
export {
|
|
35
|
+
parseColor,
|
|
36
|
+
formatColor,
|
|
37
|
+
toHex,
|
|
38
|
+
rgbToHsv,
|
|
39
|
+
hsvToRgb,
|
|
40
|
+
rgbToHsl,
|
|
41
|
+
hslToRgb,
|
|
42
|
+
colorEquals,
|
|
43
|
+
type Rgb,
|
|
44
|
+
type Hsv,
|
|
45
|
+
type Hsl,
|
|
46
|
+
type ColorFormat,
|
|
47
|
+
type FormatColorOptions
|
|
48
|
+
} from "@/core/color";
|
|
29
49
|
export { Slot, mergeSlotProps, type SlotProps } from "@/react/slot";
|
|
30
50
|
export {
|
|
31
51
|
generatePassword,
|
|
@@ -91,3 +111,82 @@ export {
|
|
|
91
111
|
type RecentEntry,
|
|
92
112
|
type RecentStore
|
|
93
113
|
} from "@/core/palette";
|
|
114
|
+
export {
|
|
115
|
+
Select,
|
|
116
|
+
SelectRoot,
|
|
117
|
+
SelectTrigger,
|
|
118
|
+
SelectValue,
|
|
119
|
+
SelectContent,
|
|
120
|
+
SelectSearch,
|
|
121
|
+
SelectList,
|
|
122
|
+
SelectOptionRow,
|
|
123
|
+
useSelectContext,
|
|
124
|
+
type SelectProps,
|
|
125
|
+
type SelectRootProps,
|
|
126
|
+
type SelectSingleProps,
|
|
127
|
+
type SelectMultipleProps,
|
|
128
|
+
type SelectItem,
|
|
129
|
+
type SelectContextValue
|
|
130
|
+
} from "@/react/select";
|
|
131
|
+
export { createSelect, type SelectOption, type SelectOptions, type SelectState, type SelectInstance } from "@/core/select";
|
|
132
|
+
export {
|
|
133
|
+
ContextMenu,
|
|
134
|
+
ContextMenuRoot,
|
|
135
|
+
ContextMenuTrigger,
|
|
136
|
+
ContextMenuContent,
|
|
137
|
+
ContextMenuPanel,
|
|
138
|
+
ContextMenuSearch,
|
|
139
|
+
ContextMenuList,
|
|
140
|
+
ContextMenuRow,
|
|
141
|
+
useContextMenuContext,
|
|
142
|
+
type ContextMenuProps,
|
|
143
|
+
type ContextMenuRootProps,
|
|
144
|
+
type ContextMenuTriggerProps,
|
|
145
|
+
type ContextMenuContentProps,
|
|
146
|
+
type ContextMenuItem,
|
|
147
|
+
type ContextMenuNode,
|
|
148
|
+
type ContextMenuContextValue
|
|
149
|
+
} from "@/react/context-menu";
|
|
150
|
+
export {
|
|
151
|
+
CLIPBOARD_PREFIX,
|
|
152
|
+
clipboardEntries,
|
|
153
|
+
clipboardAction,
|
|
154
|
+
clipboardHasText,
|
|
155
|
+
inspectClipboardTarget,
|
|
156
|
+
performClipboardAction,
|
|
157
|
+
type ClipboardAction,
|
|
158
|
+
type ClipboardTarget,
|
|
159
|
+
type ClipboardMenuOptions,
|
|
160
|
+
type ClipboardMenuLabels
|
|
161
|
+
} from "@/core/clipboard-menu";
|
|
162
|
+
export {
|
|
163
|
+
createContextMenu,
|
|
164
|
+
isAction,
|
|
165
|
+
type ContextMenuEntry,
|
|
166
|
+
type ContextMenuAction,
|
|
167
|
+
type ContextMenuOptions,
|
|
168
|
+
type ContextMenuState,
|
|
169
|
+
type ContextMenuInstance
|
|
170
|
+
} from "@/core/context-menu";
|
|
171
|
+
export {
|
|
172
|
+
SelectionList,
|
|
173
|
+
useSelection,
|
|
174
|
+
type SelectionListProps,
|
|
175
|
+
type UseSelectionOptions,
|
|
176
|
+
type UseSelectionResult,
|
|
177
|
+
type SelectionRenderer,
|
|
178
|
+
type SelectionRowRender,
|
|
179
|
+
type MarqueeRect
|
|
180
|
+
} from "@/react/selection";
|
|
181
|
+
export {
|
|
182
|
+
createSelection,
|
|
183
|
+
DEFAULT_SELECTION_SHORTCUTS,
|
|
184
|
+
type SelectionCommand,
|
|
185
|
+
type SelectionCommandName,
|
|
186
|
+
type SelectionCommandEvent,
|
|
187
|
+
type SelectionShortcuts,
|
|
188
|
+
type SelectionOptions,
|
|
189
|
+
type SelectionState,
|
|
190
|
+
type SelectionInstance
|
|
191
|
+
} from "@/core/selection";
|
|
192
|
+
export { parseShortcut, matchesShortcut, shortcutTokens, shortcutText, isApplePlatform, type Shortcut, type ShortcutSpec } from "@/core/keys";
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The colour picker's baseline look.
|
|
3
|
+
*
|
|
4
|
+
* The rest of `<Input>` ships no styles, and this one does, for the reason the select's sheet
|
|
5
|
+
* gives: an unstyled POPUP is not a plain-looking control, it is transparent boxes lying on
|
|
6
|
+
* top of the page. A saturation square with no size is nothing at all - there is no useful
|
|
7
|
+
* "unstyled" version of a gradient someone drags a handle across - so the panel arrives with
|
|
8
|
+
* a look, and `styles={false}` takes it away for anyone who wants to draw their own.
|
|
9
|
+
*
|
|
10
|
+
* A string rather than a stylesheet because the picker injects it. `scripts/sync-recipes.mjs`
|
|
11
|
+
* generates `recipes/color/styles.css` from here and CI fails when the two drift, so this
|
|
12
|
+
* module is the source and the `.css` is the copy for anyone who prefers an import.
|
|
13
|
+
*
|
|
14
|
+
* Everything below is a custom property or an attribute selector: override the properties on
|
|
15
|
+
* `:root` for a different-looking picker without writing a single selector. The sheet is
|
|
16
|
+
* PREPENDED to `<head>`, so anything the document already has wins on source order at equal
|
|
17
|
+
* specificity.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const COLOR_STYLES = `
|
|
21
|
+
:root {
|
|
22
|
+
--enigma-color-panel-bg: #1c1c1c;
|
|
23
|
+
--enigma-color-panel-border: #333333;
|
|
24
|
+
--enigma-color-panel-radius: 0.625rem;
|
|
25
|
+
--enigma-color-panel-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
|
|
26
|
+
--enigma-color-panel-width: 13.5rem;
|
|
27
|
+
--enigma-color-text: #f5f5f5;
|
|
28
|
+
--enigma-color-muted: #a3a3a3;
|
|
29
|
+
--enigma-color-border: #404040;
|
|
30
|
+
--enigma-color-focus: #a3a3a3;
|
|
31
|
+
--enigma-color-area-height: 8rem;
|
|
32
|
+
--enigma-color-rail-height: 0.625rem;
|
|
33
|
+
--enigma-color-thumb: #ffffff;
|
|
34
|
+
--enigma-color-thumb-ring: rgba(0, 0, 0, 0.55);
|
|
35
|
+
/* The light square of the transparency chequerboard. The dark one is the surface it sits
|
|
36
|
+
on, which is why only one colour is a property. */
|
|
37
|
+
--enigma-color-checker: rgba(255, 255, 255, 0.22);
|
|
38
|
+
--enigma-color-checker-size: 0.5rem;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@media (prefers-color-scheme: light) {
|
|
42
|
+
:root {
|
|
43
|
+
--enigma-color-panel-bg: #ffffff;
|
|
44
|
+
--enigma-color-panel-border: #e5e5e5;
|
|
45
|
+
--enigma-color-panel-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);
|
|
46
|
+
--enigma-color-text: #171717;
|
|
47
|
+
--enigma-color-muted: #737373;
|
|
48
|
+
--enigma-color-border: #d4d4d4;
|
|
49
|
+
--enigma-color-focus: #737373;
|
|
50
|
+
--enigma-color-thumb-ring: rgba(0, 0, 0, 0.35);
|
|
51
|
+
--enigma-color-checker: rgba(0, 0, 0, 0.14);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* The anchor the panel hangs from. Inline so it sits in the field's own row, and the
|
|
56
|
+
positioning context so the panel needs no portal and no scroll listener: it moves with
|
|
57
|
+
the field because it is INSIDE it. */
|
|
58
|
+
[data-enigma-color] { position: relative; display: inline-flex; flex: none; }
|
|
59
|
+
|
|
60
|
+
[data-enigma-color-swatch] {
|
|
61
|
+
display: block; width: 1.25rem; height: 1.25rem; padding: 0;
|
|
62
|
+
background: none; border: 1px solid var(--enigma-color-border); border-radius: 0.3125rem;
|
|
63
|
+
cursor: pointer; overflow: hidden;
|
|
64
|
+
}
|
|
65
|
+
[data-enigma-color-swatch]:focus-visible { outline: 2px solid var(--enigma-color-focus); outline-offset: 2px; }
|
|
66
|
+
[data-enigma-color-swatch]:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
67
|
+
|
|
68
|
+
/* The chequerboard, so a colour with alpha reads as transparent rather than as a lighter
|
|
69
|
+
colour - which is the whole difference between #ff000080 and #ff8080. */
|
|
70
|
+
[data-enigma-color-checkers] {
|
|
71
|
+
background-image:
|
|
72
|
+
conic-gradient(var(--enigma-color-checker) 0 25%, transparent 0 50%, var(--enigma-color-checker) 0 75%, transparent 0);
|
|
73
|
+
background-size: var(--enigma-color-checker-size) var(--enigma-color-checker-size);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
[data-enigma-color-fill] { display: block; width: 100%; height: 100%; }
|
|
77
|
+
/* An unparseable field has no colour to show, so the swatch shows the chequerboard alone
|
|
78
|
+
rather than the last good colour: a stale swatch says the value is fine when it is not. */
|
|
79
|
+
[data-enigma-color-swatch][data-invalid] [data-enigma-color-fill] { background: none; }
|
|
80
|
+
|
|
81
|
+
[data-enigma-color-panel] {
|
|
82
|
+
position: absolute; z-index: 60; top: calc(100% + 0.375rem); left: 0;
|
|
83
|
+
display: grid; gap: 0.625rem;
|
|
84
|
+
box-sizing: border-box; width: var(--enigma-color-panel-width); padding: 0.625rem;
|
|
85
|
+
color: var(--enigma-color-text); background: var(--enigma-color-panel-bg);
|
|
86
|
+
border: 1px solid var(--enigma-color-panel-border);
|
|
87
|
+
border-radius: var(--enigma-color-panel-radius);
|
|
88
|
+
box-shadow: var(--enigma-color-panel-shadow);
|
|
89
|
+
}
|
|
90
|
+
/* Flipped when the field sits near the bottom of the window. Without it the panel opens off
|
|
91
|
+
the screen and the controls are unreachable, which is the same defect the select fixes. */
|
|
92
|
+
[data-enigma-color-panel][data-side="top"] { top: auto; bottom: calc(100% + 0.375rem); }
|
|
93
|
+
|
|
94
|
+
[data-enigma-color-area] {
|
|
95
|
+
position: relative; height: var(--enigma-color-area-height);
|
|
96
|
+
border-radius: 0.375rem; cursor: crosshair;
|
|
97
|
+
/* The drag IS the control here, so the page must not scroll under it. The marquee's
|
|
98
|
+
warning about touch-action is the opposite case: a full-width row still has to let the
|
|
99
|
+
page move. */
|
|
100
|
+
touch-action: none;
|
|
101
|
+
background:
|
|
102
|
+
linear-gradient(to top, #000000, rgba(0, 0, 0, 0)),
|
|
103
|
+
linear-gradient(to right, #ffffff, rgba(255, 255, 255, 0)),
|
|
104
|
+
hsl(var(--enigma-color-hue, 0), 100%, 50%);
|
|
105
|
+
}
|
|
106
|
+
[data-enigma-color-area]:focus-visible,
|
|
107
|
+
[data-enigma-color-rail]:focus-visible { outline: 2px solid var(--enigma-color-focus); outline-offset: 2px; }
|
|
108
|
+
|
|
109
|
+
[data-enigma-color-controls] { display: flex; align-items: center; gap: 0.5rem; }
|
|
110
|
+
[data-enigma-color-rails] { display: grid; gap: 0.5rem; flex: 1; min-width: 0; }
|
|
111
|
+
|
|
112
|
+
[data-enigma-color-rail] {
|
|
113
|
+
position: relative; height: var(--enigma-color-rail-height);
|
|
114
|
+
border-radius: 999px; cursor: pointer; touch-action: none;
|
|
115
|
+
}
|
|
116
|
+
[data-enigma-color-rail="hue"] {
|
|
117
|
+
background: linear-gradient(to right, #ff0000, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000);
|
|
118
|
+
}
|
|
119
|
+
[data-enigma-color-rail="alpha"] { background-image: none; }
|
|
120
|
+
[data-enigma-color-rail="alpha"] [data-enigma-color-gradient] {
|
|
121
|
+
position: absolute; inset: 0; border-radius: inherit;
|
|
122
|
+
background: linear-gradient(to right, transparent, var(--enigma-color-opaque, #000000));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* One handle for the square and the rails. It must never take the pointer: a press that
|
|
126
|
+
lands on the handle instead of the surface under it would start no drag at all. */
|
|
127
|
+
[data-enigma-color-thumb] {
|
|
128
|
+
position: absolute; pointer-events: none;
|
|
129
|
+
width: 0.75rem; height: 0.75rem; margin: -0.375rem 0 0 -0.375rem;
|
|
130
|
+
border: 2px solid var(--enigma-color-thumb); border-radius: 999px;
|
|
131
|
+
box-shadow: 0 0 0 1px var(--enigma-color-thumb-ring), inset 0 0 0 1px var(--enigma-color-thumb-ring);
|
|
132
|
+
}
|
|
133
|
+
[data-enigma-color-rail] [data-enigma-color-thumb] { top: 50%; }
|
|
134
|
+
|
|
135
|
+
[data-enigma-color-preview] {
|
|
136
|
+
position: relative; flex: none; width: 1.75rem; height: 1.75rem;
|
|
137
|
+
border: 1px solid var(--enigma-color-border); border-radius: 999px; overflow: hidden;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
[data-enigma-color-eyedropper] {
|
|
141
|
+
display: grid; place-items: center; flex: none;
|
|
142
|
+
width: 1.75rem; height: 1.75rem; padding: 0;
|
|
143
|
+
color: var(--enigma-color-muted); background: none;
|
|
144
|
+
border: 1px solid var(--enigma-color-border); border-radius: 0.375rem; cursor: pointer;
|
|
145
|
+
}
|
|
146
|
+
[data-enigma-color-eyedropper]:hover { color: var(--enigma-color-text); }
|
|
147
|
+
[data-enigma-color-eyedropper]:focus-visible { outline: 2px solid var(--enigma-color-focus); outline-offset: 2px; }
|
|
148
|
+
|
|
149
|
+
[data-enigma-color-swatches] { display: flex; flex-wrap: wrap; gap: 0.25rem; }
|
|
150
|
+
[data-enigma-color-preset] {
|
|
151
|
+
width: 1.125rem; height: 1.125rem; padding: 0;
|
|
152
|
+
border: 1px solid var(--enigma-color-border); border-radius: 0.25rem; cursor: pointer;
|
|
153
|
+
}
|
|
154
|
+
[data-enigma-color-preset]:focus-visible { outline: 2px solid var(--enigma-color-focus); outline-offset: 2px; }
|
|
155
|
+
[data-enigma-color-preset][aria-pressed="true"] { box-shadow: 0 0 0 2px var(--enigma-color-focus); }
|
|
156
|
+
`;
|