@torpor/ui 1.0.2 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ColorPicker/ColorPicker.torp +36 -75
- package/dist/ColorPicker/ColorPickerInput.torp +122 -0
- package/dist/ColorPicker/ColorPickerTypes.ts +12 -0
- package/dist/ColorPicker/index.d.ts +40 -1
- package/dist/ColorPicker/index.js +2 -1
- package/dist/CommandPalette/CommandPalette.torp +49 -47
- package/dist/CommandPalette/CommandPaletteInput.torp +51 -0
- package/dist/CommandPalette/CommandPaletteItem.torp +62 -0
- package/dist/CommandPalette/CommandPaletteList.torp +55 -0
- package/dist/CommandPalette/CommandPaletteTypes.ts +32 -0
- package/dist/CommandPalette/index.d.ts +88 -2
- package/dist/CommandPalette/index.js +4 -1
- package/dist/DataGrid/DataGrid.torp +22 -22
- package/dist/DataGrid/DataGridCell.torp +50 -0
- package/dist/DataGrid/DataGridColumnHeader.torp +54 -0
- package/dist/DataGrid/DataGridTypes.ts +17 -0
- package/dist/DataGrid/index.d.ts +57 -2
- package/dist/DataGrid/index.js +3 -2
- package/dist/DatePicker/DatePicker.torp +45 -65
- package/dist/DatePicker/DatePickerContent.torp +82 -0
- package/dist/DatePicker/DatePickerTrigger.torp +56 -0
- package/dist/DatePicker/DatePickerTypes.ts +36 -0
- package/dist/DatePicker/index.d.ts +61 -2
- package/dist/DatePicker/index.js +3 -1
- package/dist/DateRangePicker/DateRangePicker.torp +52 -76
- package/dist/DateRangePicker/DateRangePickerContent.torp +90 -0
- package/dist/DateRangePicker/DateRangePickerTrigger.torp +59 -0
- package/dist/DateRangePicker/DateRangePickerTypes.ts +41 -0
- package/dist/DateRangePicker/index.d.ts +61 -2
- package/dist/DateRangePicker/index.js +3 -1
- package/dist/Progress/Progress.torp +24 -14
- package/dist/Progress/ProgressIndicator.torp +52 -0
- package/dist/Progress/ProgressTypes.ts +8 -0
- package/dist/Progress/index.d.ts +34 -1
- package/dist/Progress/index.js +2 -1
- package/dist/Rating/Rating.torp +41 -37
- package/dist/Rating/RatingStar.torp +71 -0
- package/dist/Rating/RatingTypes.ts +22 -0
- package/dist/Rating/index.d.ts +39 -1
- package/dist/Rating/index.js +2 -1
- package/dist/Slider/Slider.torp +48 -63
- package/dist/Slider/SliderHandle.torp +107 -0
- package/dist/Slider/SliderRange.torp +36 -0
- package/dist/Slider/SliderTypes.ts +34 -0
- package/dist/Slider/index.d.ts +65 -1
- package/dist/Slider/index.js +3 -1
- package/dist/TagInput/TagInput.torp +63 -84
- package/dist/TagInput/TagInputField.torp +68 -0
- package/dist/TagInput/TagInputSuggestions.torp +79 -0
- package/dist/TagInput/TagInputTag.torp +55 -0
- package/dist/TagInput/TagInputTypes.ts +42 -0
- package/dist/TagInput/index.d.ts +93 -1
- package/dist/TagInput/index.js +4 -1
- package/dist/TimePicker/TimePicker.torp +88 -85
- package/dist/TimePicker/TimePickerPart.torp +61 -0
- package/dist/TimePicker/TimePickerPeriod.torp +48 -0
- package/dist/TimePicker/TimePickerTypes.ts +37 -0
- package/dist/TimePicker/index.d.ts +61 -1
- package/dist/TimePicker/index.js +3 -1
- package/package.json +7 -7
|
@@ -6,9 +6,11 @@ import {
|
|
|
6
6
|
FormContextName,
|
|
7
7
|
} from "../Form/FormTypes";
|
|
8
8
|
import FormValues from "../Form/FormValues.torp";
|
|
9
|
-
import { defaultPalette
|
|
9
|
+
import { defaultPalette } from "../utils/colorUtils";
|
|
10
10
|
import createFormField from "../utils/formField";
|
|
11
11
|
import ColorPalette from "./ColorPalette.torp";
|
|
12
|
+
import ColorPickerInput from "./ColorPickerInput.torp";
|
|
13
|
+
import { type ColorPickerContext, ColorPickerContextName } from "./ColorPickerTypes";
|
|
12
14
|
|
|
13
15
|
interface ColorPickerProps {
|
|
14
16
|
/** An ID for the root element */
|
|
@@ -43,10 +45,19 @@ interface ColorPickerProps {
|
|
|
43
45
|
* ```
|
|
44
46
|
* <ColorPicker name="accent" showInput &value={$state.accent} />
|
|
45
47
|
* ```
|
|
48
|
+
*
|
|
49
|
+
* The structure of a ColorPicker is:
|
|
50
|
+
*
|
|
51
|
+
* ```
|
|
52
|
+
* <ColorPicker>
|
|
53
|
+
* <ColorPalette />
|
|
54
|
+
* <ColorPickerInput />
|
|
55
|
+
* </ColorPicker>
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* With no children, the palette and input are rendered automatically.
|
|
46
59
|
*/
|
|
47
60
|
export default function ColorPicker($props: ColorPickerProps | undefined) {
|
|
48
|
-
let hexInput: HTMLInputElement;
|
|
49
|
-
|
|
50
61
|
// Wire up the enclosing Form and Field, if any
|
|
51
62
|
const form = createFormField(
|
|
52
63
|
$props?.name,
|
|
@@ -60,96 +71,46 @@ export default function ColorPicker($props: ColorPickerProps | undefined) {
|
|
|
60
71
|
|
|
61
72
|
let $state = $watch({
|
|
62
73
|
value: undefined as string | undefined,
|
|
63
|
-
|
|
64
|
-
// Text while the hex input is being typed into; committed on blur or
|
|
65
|
-
// Enter, invalid text reverting like NumberInput
|
|
66
|
-
text: "",
|
|
67
|
-
editing: false,
|
|
68
74
|
});
|
|
69
75
|
|
|
70
76
|
$bind($state, $props, "value");
|
|
71
77
|
|
|
72
|
-
function
|
|
73
|
-
$state.text = value ?? "";
|
|
74
|
-
form.handleInput();
|
|
75
|
-
$props?.onchange?.(value);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function commit() {
|
|
79
|
-
$state.editing = false;
|
|
80
|
-
const next = parseHex($state.text);
|
|
81
|
-
if ($state.text.trim() !== "" && !next) {
|
|
82
|
-
// Not a color: revert to the last committed value
|
|
83
|
-
$state.text = $state.value ?? "";
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
78
|
+
function commitValue(next: string | undefined) {
|
|
86
79
|
if (next !== $state.value) {
|
|
87
80
|
$state.value = next;
|
|
88
81
|
form.handleInput();
|
|
89
82
|
$props?.onchange?.(next);
|
|
90
83
|
}
|
|
91
|
-
$state.text = $state.value ?? "";
|
|
92
84
|
}
|
|
93
85
|
|
|
94
|
-
function
|
|
95
|
-
|
|
96
|
-
$state.editing = true;
|
|
97
|
-
$state.text = el.value;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function handleKey(e: KeyboardEvent) {
|
|
101
|
-
if (e.key === "Enter") {
|
|
102
|
-
e.preventDefault();
|
|
103
|
-
commit();
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function handleChange() {
|
|
108
|
-
commit();
|
|
86
|
+
function handleSelect(value: string | undefined) {
|
|
87
|
+
commitValue(value);
|
|
109
88
|
}
|
|
110
89
|
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
$
|
|
119
|
-
const text = $state.text;
|
|
120
|
-
const editing = $state.editing;
|
|
121
|
-
if (hexInput && !editing && hexInput.value !== text) {
|
|
122
|
-
hexInput.value = text;
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
$onmount(() => syncInput());
|
|
90
|
+
// Set the context to pass down to the input
|
|
91
|
+
let context: ColorPickerContext = {
|
|
92
|
+
getDisabled,
|
|
93
|
+
getValue: () => $state.value,
|
|
94
|
+
getAriaLabel: () => $props?.ariaLabel,
|
|
95
|
+
commitValue,
|
|
96
|
+
};
|
|
97
|
+
$context[ColorPickerContextName] = context;
|
|
126
98
|
|
|
127
99
|
@render {
|
|
128
100
|
<div class={["torp-color-picker", $props?.class]} {$props?.style}>
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
ariaLabel={$props?.ariaLabel}
|
|
135
|
-
onchange={handleSelect}
|
|
136
|
-
/>
|
|
137
|
-
@if ($props?.showInput) {
|
|
138
|
-
<input
|
|
139
|
-
&ref={hexInput}
|
|
140
|
-
type="text"
|
|
141
|
-
class="torp-color-picker-input"
|
|
142
|
-
spellcheck="false"
|
|
143
|
-
placeholder="#rrggbb"
|
|
144
|
-
aria-label={$props?.ariaLabel ? `${$props.ariaLabel} hex value` : "Hex value"}
|
|
145
|
-
value={$state.editing ? $state.text : $state.value ?? ""}
|
|
101
|
+
<slot>
|
|
102
|
+
<ColorPalette
|
|
103
|
+
class="torp-color-picker-palette"
|
|
104
|
+
colors={$props?.colors ?? defaultPalette}
|
|
105
|
+
value={$state.value}
|
|
146
106
|
disabled={getDisabled()}
|
|
147
|
-
|
|
148
|
-
onchange={
|
|
149
|
-
onblur={handleChange}
|
|
150
|
-
onkeydown={handleKey}
|
|
107
|
+
ariaLabel={$props?.ariaLabel}
|
|
108
|
+
onchange={handleSelect}
|
|
151
109
|
/>
|
|
152
|
-
|
|
110
|
+
@if ($props?.showInput) {
|
|
111
|
+
<ColorPickerInput />
|
|
112
|
+
}
|
|
113
|
+
</slot>
|
|
153
114
|
</div>
|
|
154
115
|
|
|
155
116
|
<FormValues name={form.name} value={$state.value} />
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { ClassValue, StyleValue } from "@torpor/view";
|
|
2
|
+
import { parseHex } from "../utils/colorUtils";
|
|
3
|
+
import { type ColorPickerContext, ColorPickerContextName } from "./ColorPickerTypes";
|
|
4
|
+
|
|
5
|
+
interface ColorPickerInputProps {
|
|
6
|
+
/** An ID for the input element */
|
|
7
|
+
id?: string;
|
|
8
|
+
/** Classes for the input element */
|
|
9
|
+
class?: ClassValue;
|
|
10
|
+
/** Styles for the input element */
|
|
11
|
+
style?: StyleValue;
|
|
12
|
+
/** An accessible name for the input; falls back to the picker's ariaLabel */
|
|
13
|
+
ariaLabel?: string;
|
|
14
|
+
/** If set to true, the user cannot interact with this input */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The hex text field of a ColorPicker, for entering arbitrary colors as
|
|
20
|
+
* "#rrggbb". Values commit on blur or Enter, invalid text reverting like
|
|
21
|
+
* NumberInput.
|
|
22
|
+
*/
|
|
23
|
+
export default function ColorPickerInput($props: ColorPickerInputProps | undefined) {
|
|
24
|
+
let hexInput: HTMLInputElement | undefined;
|
|
25
|
+
|
|
26
|
+
// Get stuff out of the ColorPicker context
|
|
27
|
+
const context = $context[ColorPickerContextName] as ColorPickerContext;
|
|
28
|
+
if (!context) {
|
|
29
|
+
throw new Error("ColorPickerInput must be contained within a ColorPicker");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getDisabled(): boolean {
|
|
33
|
+
return $props?.disabled === true || context.getDisabled();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function label(): string {
|
|
37
|
+
const base = $props?.ariaLabel ?? context.getAriaLabel();
|
|
38
|
+
return base ? `${base} hex value` : "Hex value";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let $state = $watch({
|
|
42
|
+
// Text while the hex input is being typed into; committed on blur or
|
|
43
|
+
// Enter, invalid text reverting like NumberInput
|
|
44
|
+
text: "",
|
|
45
|
+
editing: false,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
function display(): string {
|
|
49
|
+
return $state.editing ? $state.text : context.getValue() ?? "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function commit() {
|
|
53
|
+
$state.editing = false;
|
|
54
|
+
const next = parseHex($state.text);
|
|
55
|
+
if ($state.text.trim() !== "" && !next) {
|
|
56
|
+
// Not a color: revert to the last committed value
|
|
57
|
+
$state.text = context.getValue() ?? "";
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
context.commitValue(next);
|
|
61
|
+
$state.text = context.getValue() ?? "";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function handleInput(e: Event) {
|
|
65
|
+
const el = e.currentTarget as HTMLInputElement;
|
|
66
|
+
$state.editing = true;
|
|
67
|
+
$state.text = el.value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function handleKey(e: KeyboardEvent) {
|
|
71
|
+
if (e.key === "Enter") {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
commit();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function handleChange() {
|
|
78
|
+
commit();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Keep the input's value in sync with the text; typing sets the
|
|
82
|
+
// property directly, so an attribute alone would not update it
|
|
83
|
+
function syncInput() {
|
|
84
|
+
if (hexInput && !$state.editing && hexInput.value !== display()) {
|
|
85
|
+
hexInput.value = display();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
$run(() => {
|
|
89
|
+
const text = display();
|
|
90
|
+
const editing = $state.editing;
|
|
91
|
+
if (hexInput && !editing && hexInput.value !== text) {
|
|
92
|
+
hexInput.value = text;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
$onmount(() => syncInput());
|
|
96
|
+
|
|
97
|
+
@render {
|
|
98
|
+
<input
|
|
99
|
+
{$props?.id}
|
|
100
|
+
type="text"
|
|
101
|
+
class={["torp-color-picker-input", $props?.class]}
|
|
102
|
+
{$props?.style}
|
|
103
|
+
spellcheck="false"
|
|
104
|
+
placeholder="#rrggbb"
|
|
105
|
+
aria-label={label()}
|
|
106
|
+
value={display()}
|
|
107
|
+
disabled={getDisabled()}
|
|
108
|
+
oninput={handleInput}
|
|
109
|
+
onchange={handleChange}
|
|
110
|
+
onblur={handleChange}
|
|
111
|
+
onkeydown={handleKey}
|
|
112
|
+
onmount={(el) => {
|
|
113
|
+
hexInput = el;
|
|
114
|
+
return () => {
|
|
115
|
+
if (hexInput === el) {
|
|
116
|
+
hexInput = undefined;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}}
|
|
120
|
+
/>
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const ColorPickerContextName: unique symbol = Symbol.for("torp.ColorPicker");
|
|
2
|
+
|
|
3
|
+
export interface ColorPickerContext {
|
|
4
|
+
/** Whether the user cannot interact with the picker */
|
|
5
|
+
getDisabled: () => boolean;
|
|
6
|
+
/** The selected color as "#rrggbb"; undefined if nothing is selected */
|
|
7
|
+
getValue: () => string | undefined;
|
|
8
|
+
/** An accessible name for the picker */
|
|
9
|
+
getAriaLabel: () => string | undefined;
|
|
10
|
+
/** Commits a hex input value, reporting it when it changes the value */
|
|
11
|
+
commitValue: (value: string | undefined) => void;
|
|
12
|
+
}
|
|
@@ -73,6 +73,17 @@ interface ColorPickerProps {
|
|
|
73
73
|
* ```
|
|
74
74
|
* <ColorPicker name="accent" showInput &value={$state.accent} />
|
|
75
75
|
* ```
|
|
76
|
+
*
|
|
77
|
+
* The structure of a ColorPicker is:
|
|
78
|
+
*
|
|
79
|
+
* ```
|
|
80
|
+
* <ColorPicker>
|
|
81
|
+
* <ColorPalette />
|
|
82
|
+
* <ColorPickerInput />
|
|
83
|
+
* </ColorPicker>
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* With no children, the palette and input are rendered automatically.
|
|
76
87
|
*/
|
|
77
88
|
declare function ColorPicker(
|
|
78
89
|
$parent: ParentNode,
|
|
@@ -82,4 +93,32 @@ declare function ColorPicker(
|
|
|
82
93
|
$slots?: Record<string, SlotRender>,
|
|
83
94
|
): void;
|
|
84
95
|
|
|
85
|
-
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
interface ColorPickerInputProps {
|
|
99
|
+
/** An ID for the input element */
|
|
100
|
+
id?: string;
|
|
101
|
+
/** Classes for the input element */
|
|
102
|
+
class?: ClassValue;
|
|
103
|
+
/** Styles for the input element */
|
|
104
|
+
style?: StyleValue;
|
|
105
|
+
/** An accessible name for the input; falls back to the picker's ariaLabel */
|
|
106
|
+
ariaLabel?: string;
|
|
107
|
+
/** If set to true, the user cannot interact with this input */
|
|
108
|
+
disabled?: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The hex text field of a ColorPicker, for entering arbitrary colors as
|
|
113
|
+
* "#rrggbb". Values commit on blur or Enter, invalid text reverting like
|
|
114
|
+
* NumberInput.
|
|
115
|
+
*/
|
|
116
|
+
declare function ColorPickerInput(
|
|
117
|
+
$parent: ParentNode,
|
|
118
|
+
$anchor: Node | null,
|
|
119
|
+
$props: ColorPickerInputProps | undefined,
|
|
120
|
+
$context?: Record<PropertyKey, any>,
|
|
121
|
+
$slots?: Record<string, SlotRender>,
|
|
122
|
+
): void;
|
|
123
|
+
|
|
124
|
+
export { ColorPalette, ColorPicker, ColorPickerInput };
|
|
@@ -3,6 +3,12 @@ import Modal from "../Modal/Modal.torp";
|
|
|
3
3
|
import ModalContent from "../Modal/ModalContent.torp";
|
|
4
4
|
import ModalOverlay from "../Modal/ModalOverlay.torp";
|
|
5
5
|
import getId from "../utils/getId";
|
|
6
|
+
import CommandPaletteInput from "./CommandPaletteInput.torp";
|
|
7
|
+
import CommandPaletteList from "./CommandPaletteList.torp";
|
|
8
|
+
import {
|
|
9
|
+
type CommandPaletteContext,
|
|
10
|
+
CommandPaletteContextName,
|
|
11
|
+
} from "./CommandPaletteTypes";
|
|
6
12
|
import type { Command } from "./CommandTypes";
|
|
7
13
|
|
|
8
14
|
interface CommandPaletteProps {
|
|
@@ -36,11 +42,20 @@ interface CommandPaletteProps {
|
|
|
36
42
|
* shortcut or programmatically.
|
|
37
43
|
*
|
|
38
44
|
* The structure of a CommandPalette is a Modal containing a search input and
|
|
39
|
-
* a filtered list of commands
|
|
45
|
+
* a filtered list of commands:
|
|
46
|
+
*
|
|
47
|
+
* ```
|
|
48
|
+
* <CommandPalette>
|
|
49
|
+
* <CommandPaletteInput />
|
|
50
|
+
* <CommandPaletteList />
|
|
51
|
+
* </CommandPalette>
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* With no children, the input and list are rendered automatically.
|
|
40
55
|
*/
|
|
41
56
|
export default function CommandPalette($props: CommandPaletteProps | undefined) {
|
|
42
57
|
let defaultId = getId();
|
|
43
|
-
let input: HTMLInputElement;
|
|
58
|
+
let input: HTMLInputElement | undefined;
|
|
44
59
|
|
|
45
60
|
function getDisabled(command: Command): boolean {
|
|
46
61
|
return command.disabled === true;
|
|
@@ -91,6 +106,10 @@ export default function CommandPalette($props: CommandPaletteProps | undefined)
|
|
|
91
106
|
return filteredCommands()[$state.activeIndex];
|
|
92
107
|
}
|
|
93
108
|
|
|
109
|
+
function listId(): string {
|
|
110
|
+
return `${$props?.id ?? defaultId}-list`;
|
|
111
|
+
}
|
|
112
|
+
|
|
94
113
|
function optionId(index: number): string {
|
|
95
114
|
return `${$props?.id ?? defaultId}-option-${index}`;
|
|
96
115
|
}
|
|
@@ -102,6 +121,30 @@ export default function CommandPalette($props: CommandPaletteProps | undefined)
|
|
|
102
121
|
$props?.onrun?.(command);
|
|
103
122
|
}
|
|
104
123
|
|
|
124
|
+
// Set the context to pass down to the input, list and items
|
|
125
|
+
let context: CommandPaletteContext = {
|
|
126
|
+
getListId: listId,
|
|
127
|
+
getItemId: optionId,
|
|
128
|
+
getActiveItemId: () => optionId($state.activeIndex),
|
|
129
|
+
getPlaceholder: () => $props?.placeholder,
|
|
130
|
+
getAriaLabel: () => $props?.ariaLabel,
|
|
131
|
+
handleInput,
|
|
132
|
+
handleKey,
|
|
133
|
+
registerInput: (el) => {
|
|
134
|
+
input = el;
|
|
135
|
+
},
|
|
136
|
+
unregisterInput: (el) => {
|
|
137
|
+
if (input === el) {
|
|
138
|
+
input = undefined;
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
getFilteredCommands: filteredCommands,
|
|
142
|
+
isActiveCommand: (index) => $state.activeIndex === index,
|
|
143
|
+
runCommand,
|
|
144
|
+
getEmptyText: () => $props?.emptyText ?? "No results",
|
|
145
|
+
};
|
|
146
|
+
$context[CommandPaletteContextName] = context;
|
|
147
|
+
|
|
105
148
|
function handleInput(e: Event) {
|
|
106
149
|
$state.search = (e.currentTarget as HTMLInputElement).value;
|
|
107
150
|
}
|
|
@@ -162,46 +205,10 @@ export default function CommandPalette($props: CommandPaletteProps | undefined)
|
|
|
162
205
|
<Modal {$state.visible}>
|
|
163
206
|
<ModalOverlay />
|
|
164
207
|
<ModalContent class="torp-command-palette-content">
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
aria-expanded="true"
|
|
170
|
-
aria-controls={`${$props?.id ?? defaultId}-list`}
|
|
171
|
-
aria-activedescendant={optionId($state.activeIndex)}
|
|
172
|
-
aria-autocomplete="list"
|
|
173
|
-
aria-label={$props?.ariaLabel}
|
|
174
|
-
placeholder={$props?.placeholder}
|
|
175
|
-
&ref={input}
|
|
176
|
-
oninput={handleInput}
|
|
177
|
-
onkeydown={handleKey}
|
|
178
|
-
/>
|
|
179
|
-
<ul class="torp-command-palette-list" role="listbox" id={`${$props?.id ?? defaultId}-list`}>
|
|
180
|
-
@for (let [index, command] of filteredCommands().entries()) {
|
|
181
|
-
@key = command.value
|
|
182
|
-
<li
|
|
183
|
-
class="torp-command-palette-item"
|
|
184
|
-
role="option"
|
|
185
|
-
id={optionId(index)}
|
|
186
|
-
aria-selected={$state.activeIndex === index ? "true" : "false"}
|
|
187
|
-
aria-disabled={getDisabled(command) ? "true" : undefined}
|
|
188
|
-
data-state={$state.activeIndex === index ? "active" : "inactive"}
|
|
189
|
-
onclick={() => runCommand(command)}
|
|
190
|
-
>
|
|
191
|
-
<span class="torp-command-palette-label">{command.label}</span>
|
|
192
|
-
@if (command.shortcut) {
|
|
193
|
-
<span class="torp-command-palette-shortcut" aria-hidden="true">
|
|
194
|
-
{command.shortcut}
|
|
195
|
-
</span>
|
|
196
|
-
}
|
|
197
|
-
</li>
|
|
198
|
-
}
|
|
199
|
-
@if (filteredCommands().length === 0) {
|
|
200
|
-
<li class="torp-command-palette-empty">
|
|
201
|
-
{$props?.emptyText ?? "No results"}
|
|
202
|
-
</li>
|
|
203
|
-
}
|
|
204
|
-
</ul>
|
|
208
|
+
<slot>
|
|
209
|
+
<CommandPaletteInput />
|
|
210
|
+
<CommandPaletteList />
|
|
211
|
+
</slot>
|
|
205
212
|
</ModalContent>
|
|
206
213
|
</Modal>
|
|
207
214
|
</div>
|
|
@@ -214,10 +221,5 @@ export default function CommandPalette($props: CommandPaletteProps | undefined)
|
|
|
214
221
|
display: flex;
|
|
215
222
|
flex-direction: column;
|
|
216
223
|
}
|
|
217
|
-
.torp-command-palette-list {
|
|
218
|
-
list-style: none;
|
|
219
|
-
margin: 0;
|
|
220
|
-
padding: 0;
|
|
221
|
-
}
|
|
222
224
|
}
|
|
223
225
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ClassValue, StyleValue } from "@torpor/view";
|
|
2
|
+
import { type CommandPaletteContext, CommandPaletteContextName } from "./CommandPaletteTypes";
|
|
3
|
+
|
|
4
|
+
interface CommandPaletteInputProps {
|
|
5
|
+
/** An ID for the input element */
|
|
6
|
+
id?: string;
|
|
7
|
+
/** Classes for the input element */
|
|
8
|
+
class?: ClassValue;
|
|
9
|
+
/** Styles for the input element */
|
|
10
|
+
style?: StyleValue;
|
|
11
|
+
/** Text to display in the input when it is empty; falls back to the palette's placeholder */
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
/** An accessible name for the input; falls back to the palette's ariaLabel */
|
|
14
|
+
ariaLabel?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The search input of a CommandPalette, filtering the commands as the user
|
|
19
|
+
* types.
|
|
20
|
+
*/
|
|
21
|
+
export default function CommandPaletteInput($props: CommandPaletteInputProps | undefined) {
|
|
22
|
+
// Get stuff out of the CommandPalette context
|
|
23
|
+
const context = $context[CommandPaletteContextName] as CommandPaletteContext;
|
|
24
|
+
if (!context) {
|
|
25
|
+
throw new Error("CommandPaletteInput must be contained within a CommandPalette");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@render {
|
|
29
|
+
<input
|
|
30
|
+
{$props?.id}
|
|
31
|
+
class={["torp-command-palette-input", $props?.class]}
|
|
32
|
+
{$props?.style}
|
|
33
|
+
type="text"
|
|
34
|
+
role="combobox"
|
|
35
|
+
aria-expanded="true"
|
|
36
|
+
aria-controls={context.getListId()}
|
|
37
|
+
aria-activedescendant={context.getActiveItemId()}
|
|
38
|
+
aria-autocomplete="list"
|
|
39
|
+
aria-label={$props?.ariaLabel ?? context.getAriaLabel()}
|
|
40
|
+
placeholder={$props?.placeholder ?? context.getPlaceholder()}
|
|
41
|
+
oninput={context.handleInput}
|
|
42
|
+
onkeydown={context.handleKey}
|
|
43
|
+
onmount={(el) => {
|
|
44
|
+
context.registerInput(el);
|
|
45
|
+
return () => {
|
|
46
|
+
context.unregisterInput(el);
|
|
47
|
+
};
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ClassValue, StyleValue } from "@torpor/view";
|
|
2
|
+
import { type CommandPaletteContext, CommandPaletteContextName } from "./CommandPaletteTypes";
|
|
3
|
+
import type { Command } from "./CommandTypes";
|
|
4
|
+
|
|
5
|
+
interface CommandPaletteItemProps {
|
|
6
|
+
/** An ID for the item element */
|
|
7
|
+
id?: string;
|
|
8
|
+
/** Classes for the item element */
|
|
9
|
+
class?: ClassValue;
|
|
10
|
+
/** Styles for the item element */
|
|
11
|
+
style?: StyleValue;
|
|
12
|
+
/** The item's position in the list */
|
|
13
|
+
index: number;
|
|
14
|
+
/** The command that choosing this item runs */
|
|
15
|
+
command: Command;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* One command of a CommandPalette, showing its label and shortcut hint.
|
|
20
|
+
* Choosing it runs the command and closes the palette.
|
|
21
|
+
*/
|
|
22
|
+
export default function CommandPaletteItem($props: CommandPaletteItemProps | undefined) {
|
|
23
|
+
// Get stuff out of the CommandPalette context
|
|
24
|
+
const context = $context[CommandPaletteContextName] as CommandPaletteContext;
|
|
25
|
+
if (!context) {
|
|
26
|
+
throw new Error("CommandPaletteItem must be contained within a CommandPalette");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getIndex(): number {
|
|
30
|
+
return $props?.index ?? 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getCommand(): Command | undefined {
|
|
34
|
+
return $props?.command;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getDisabled(): boolean {
|
|
38
|
+
return getCommand()?.disabled === true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@render {
|
|
42
|
+
<li
|
|
43
|
+
id={$props?.id ?? context.getItemId(getIndex())}
|
|
44
|
+
class={["torp-command-palette-item", $props?.class]}
|
|
45
|
+
{$props?.style}
|
|
46
|
+
role="option"
|
|
47
|
+
aria-selected={context.isActiveCommand(getIndex()) ? "true" : "false"}
|
|
48
|
+
aria-disabled={getDisabled() ? "true" : undefined}
|
|
49
|
+
data-state={context.isActiveCommand(getIndex()) ? "active" : "inactive"}
|
|
50
|
+
onclick={() => context.runCommand(getCommand())}
|
|
51
|
+
>
|
|
52
|
+
<slot>
|
|
53
|
+
<span class="torp-command-palette-label">{getCommand()?.label}</span>
|
|
54
|
+
@if (getCommand()?.shortcut) {
|
|
55
|
+
<span class="torp-command-palette-shortcut" aria-hidden="true">
|
|
56
|
+
{getCommand()?.shortcut}
|
|
57
|
+
</span>
|
|
58
|
+
}
|
|
59
|
+
</slot>
|
|
60
|
+
</li>
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ClassValue, StyleValue } from "@torpor/view";
|
|
2
|
+
import { type CommandPaletteContext, CommandPaletteContextName } from "./CommandPaletteTypes";
|
|
3
|
+
import CommandPaletteItem from "./CommandPaletteItem.torp";
|
|
4
|
+
|
|
5
|
+
interface CommandPaletteListProps {
|
|
6
|
+
/** An ID for the list element */
|
|
7
|
+
id?: string;
|
|
8
|
+
/** Classes for the list element */
|
|
9
|
+
class?: ClassValue;
|
|
10
|
+
/** Styles for the list element */
|
|
11
|
+
style?: StyleValue;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The list of a CommandPalette, showing the commands that match the current
|
|
16
|
+
* search text (or a message when none match).
|
|
17
|
+
*/
|
|
18
|
+
export default function CommandPaletteList($props: CommandPaletteListProps | undefined) {
|
|
19
|
+
// Get stuff out of the CommandPalette context
|
|
20
|
+
const context = $context[CommandPaletteContextName] as CommandPaletteContext;
|
|
21
|
+
if (!context) {
|
|
22
|
+
throw new Error("CommandPaletteList must be contained within a CommandPalette");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@render {
|
|
26
|
+
<ul
|
|
27
|
+
id={$props?.id ?? context.getListId()}
|
|
28
|
+
class={["torp-command-palette-list", $props?.class]}
|
|
29
|
+
{$props?.style}
|
|
30
|
+
role="listbox"
|
|
31
|
+
>
|
|
32
|
+
<slot>
|
|
33
|
+
@for (let [index, command] of context.getFilteredCommands().entries()) {
|
|
34
|
+
@key = command.value
|
|
35
|
+
<CommandPaletteItem {index} {command} />
|
|
36
|
+
}
|
|
37
|
+
</slot>
|
|
38
|
+
@if (context.getFilteredCommands().length === 0) {
|
|
39
|
+
<li class="torp-command-palette-empty">
|
|
40
|
+
{context.getEmptyText()}
|
|
41
|
+
</li>
|
|
42
|
+
}
|
|
43
|
+
</ul>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@style {
|
|
47
|
+
/* Headless: layout only, so the input sits above the list. All
|
|
48
|
+
appearance is up to the consuming theme */
|
|
49
|
+
.torp-command-palette-list {
|
|
50
|
+
list-style: none;
|
|
51
|
+
margin: 0;
|
|
52
|
+
padding: 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Command } from "./CommandTypes";
|
|
2
|
+
|
|
3
|
+
export const CommandPaletteContextName: unique symbol = Symbol.for("torp.CommandPalette");
|
|
4
|
+
|
|
5
|
+
export interface CommandPaletteContext {
|
|
6
|
+
/** The ID of the command list, for the input's aria-controls */
|
|
7
|
+
getListId: () => string;
|
|
8
|
+
/** The ID of a command option, for aria-activedescendant */
|
|
9
|
+
getItemId: (index: number) => string;
|
|
10
|
+
/** The ID of the active command option */
|
|
11
|
+
getActiveItemId: () => string;
|
|
12
|
+
/** Text to display in the input when it is empty */
|
|
13
|
+
getPlaceholder: () => string | undefined;
|
|
14
|
+
/** An accessible name for the input */
|
|
15
|
+
getAriaLabel: () => string | undefined;
|
|
16
|
+
/** Handles typing in the input */
|
|
17
|
+
handleInput: (e: Event) => void;
|
|
18
|
+
/** Handles arrow/Enter/Escape keys in the input */
|
|
19
|
+
handleKey: (e: KeyboardEvent) => void;
|
|
20
|
+
/** Called from the input when it is added */
|
|
21
|
+
registerInput: (el: HTMLInputElement) => void;
|
|
22
|
+
/** Called from the input when it is removed */
|
|
23
|
+
unregisterInput: (el: HTMLInputElement) => void;
|
|
24
|
+
/** The commands that match the current search text */
|
|
25
|
+
getFilteredCommands: () => Command[];
|
|
26
|
+
/** Whether a command is the active (highlighted) one */
|
|
27
|
+
isActiveCommand: (index: number) => boolean;
|
|
28
|
+
/** Runs a command and closes the palette */
|
|
29
|
+
runCommand: (command: Command | undefined) => void;
|
|
30
|
+
/** Text to display when no commands match the search */
|
|
31
|
+
getEmptyText: () => string;
|
|
32
|
+
}
|