@torpor/ui 1.0.1 → 1.1.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.
Files changed (60) hide show
  1. package/dist/ColorPicker/ColorPicker.torp +36 -75
  2. package/dist/ColorPicker/ColorPickerInput.torp +122 -0
  3. package/dist/ColorPicker/ColorPickerTypes.ts +12 -0
  4. package/dist/ColorPicker/index.d.ts +40 -1
  5. package/dist/ColorPicker/index.js +2 -1
  6. package/dist/CommandPalette/CommandPalette.torp +49 -47
  7. package/dist/CommandPalette/CommandPaletteInput.torp +51 -0
  8. package/dist/CommandPalette/CommandPaletteItem.torp +62 -0
  9. package/dist/CommandPalette/CommandPaletteList.torp +55 -0
  10. package/dist/CommandPalette/CommandPaletteTypes.ts +32 -0
  11. package/dist/CommandPalette/index.d.ts +88 -2
  12. package/dist/CommandPalette/index.js +4 -1
  13. package/dist/DataGrid/DataGrid.torp +22 -22
  14. package/dist/DataGrid/DataGridCell.torp +50 -0
  15. package/dist/DataGrid/DataGridColumnHeader.torp +54 -0
  16. package/dist/DataGrid/DataGridTypes.ts +17 -0
  17. package/dist/DataGrid/index.d.ts +57 -2
  18. package/dist/DataGrid/index.js +3 -2
  19. package/dist/DatePicker/DatePicker.torp +45 -65
  20. package/dist/DatePicker/DatePickerContent.torp +82 -0
  21. package/dist/DatePicker/DatePickerTrigger.torp +56 -0
  22. package/dist/DatePicker/DatePickerTypes.ts +36 -0
  23. package/dist/DatePicker/index.d.ts +61 -2
  24. package/dist/DatePicker/index.js +3 -1
  25. package/dist/DateRangePicker/DateRangePicker.torp +52 -76
  26. package/dist/DateRangePicker/DateRangePickerContent.torp +90 -0
  27. package/dist/DateRangePicker/DateRangePickerTrigger.torp +59 -0
  28. package/dist/DateRangePicker/DateRangePickerTypes.ts +41 -0
  29. package/dist/DateRangePicker/index.d.ts +61 -2
  30. package/dist/DateRangePicker/index.js +3 -1
  31. package/dist/Progress/Progress.torp +24 -14
  32. package/dist/Progress/ProgressIndicator.torp +52 -0
  33. package/dist/Progress/ProgressTypes.ts +8 -0
  34. package/dist/Progress/index.d.ts +34 -1
  35. package/dist/Progress/index.js +2 -1
  36. package/dist/Rating/Rating.torp +41 -37
  37. package/dist/Rating/RatingStar.torp +71 -0
  38. package/dist/Rating/RatingTypes.ts +22 -0
  39. package/dist/Rating/index.d.ts +39 -1
  40. package/dist/Rating/index.js +2 -1
  41. package/dist/Slider/Slider.torp +48 -63
  42. package/dist/Slider/SliderHandle.torp +107 -0
  43. package/dist/Slider/SliderRange.torp +36 -0
  44. package/dist/Slider/SliderTypes.ts +34 -0
  45. package/dist/Slider/index.d.ts +65 -1
  46. package/dist/Slider/index.js +3 -1
  47. package/dist/TagInput/TagInput.torp +63 -84
  48. package/dist/TagInput/TagInputField.torp +68 -0
  49. package/dist/TagInput/TagInputSuggestions.torp +79 -0
  50. package/dist/TagInput/TagInputTag.torp +55 -0
  51. package/dist/TagInput/TagInputTypes.ts +42 -0
  52. package/dist/TagInput/index.d.ts +93 -1
  53. package/dist/TagInput/index.js +4 -1
  54. package/dist/TimePicker/TimePicker.torp +88 -85
  55. package/dist/TimePicker/TimePickerPart.torp +61 -0
  56. package/dist/TimePicker/TimePickerPeriod.torp +48 -0
  57. package/dist/TimePicker/TimePickerTypes.ts +37 -0
  58. package/dist/TimePicker/index.d.ts +61 -1
  59. package/dist/TimePicker/index.js +3 -1
  60. 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, parseHex } from "../utils/colorUtils";
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 handleSelect(value: string | undefined) {
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 handleInput(e: Event) {
95
- const el = e.currentTarget as HTMLInputElement;
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
- // Keep the input's value in sync with the text; typing sets the
112
- // property directly, so an attribute alone would not update it
113
- function syncInput() {
114
- if (hexInput && !$state.editing && hexInput.value !== $state.text) {
115
- hexInput.value = $state.text;
116
- }
117
- }
118
- $run(() => {
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
- <ColorPalette
130
- class="torp-color-picker-palette"
131
- colors={$props?.colors ?? defaultPalette}
132
- value={$state.value}
133
- disabled={getDisabled()}
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
- oninput={handleInput}
148
- onchange={handleChange}
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
- export { ColorPalette, ColorPicker };
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 };
@@ -1,3 +1,4 @@
1
1
  import ColorPalette from "./ColorPalette.torp";
2
2
  import ColorPicker from "./ColorPicker.torp";
3
- export { ColorPalette, ColorPicker };
3
+ import ColorPickerInput from "./ColorPickerInput.torp";
4
+ 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
- <input
166
- class="torp-command-palette-input"
167
- type="text"
168
- role="combobox"
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
+ }