@scalar/components 0.31.1 → 0.32.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.
Files changed (54) hide show
  1. package/dist/components/ScalarCard/ScalarCardHeader.vue.d.ts.map +1 -1
  2. package/dist/components/ScalarCard/ScalarCardHeader.vue.js.map +1 -1
  3. package/dist/components/ScalarCard/ScalarCardHeader.vue.script.js +2 -2
  4. package/dist/components/ScalarCard/ScalarCardHeader.vue.script.js.map +1 -1
  5. package/dist/components/ScalarCombobox/ScalarCombobox.vue.d.ts +15 -1
  6. package/dist/components/ScalarCombobox/ScalarCombobox.vue.d.ts.map +1 -1
  7. package/dist/components/ScalarCombobox/ScalarCombobox.vue.js.map +1 -1
  8. package/dist/components/ScalarCombobox/ScalarCombobox.vue.script.js +20 -5
  9. package/dist/components/ScalarCombobox/ScalarCombobox.vue.script.js.map +1 -1
  10. package/dist/components/ScalarCombobox/ScalarComboboxMultiselect.vue.d.ts +15 -1
  11. package/dist/components/ScalarCombobox/ScalarComboboxMultiselect.vue.d.ts.map +1 -1
  12. package/dist/components/ScalarCombobox/ScalarComboboxMultiselect.vue.js.map +1 -1
  13. package/dist/components/ScalarCombobox/ScalarComboboxMultiselect.vue.script.js +22 -7
  14. package/dist/components/ScalarCombobox/ScalarComboboxMultiselect.vue.script.js.map +1 -1
  15. package/dist/components/ScalarCombobox/ScalarComboboxOptions.vue.d.ts +16 -1
  16. package/dist/components/ScalarCombobox/ScalarComboboxOptions.vue.d.ts.map +1 -1
  17. package/dist/components/ScalarCombobox/ScalarComboboxOptions.vue.js.map +1 -1
  18. package/dist/components/ScalarCombobox/ScalarComboboxOptions.vue.script.js +111 -45
  19. package/dist/components/ScalarCombobox/ScalarComboboxOptions.vue.script.js.map +1 -1
  20. package/dist/components/ScalarCombobox/types.d.ts +5 -0
  21. package/dist/components/ScalarCombobox/types.d.ts.map +1 -1
  22. package/dist/components/ScalarCombobox/types.js.map +1 -1
  23. package/dist/components/ScalarDropdown/ScalarDropdown.vue.d.ts.map +1 -1
  24. package/dist/components/ScalarDropdown/ScalarDropdown.vue.js.map +1 -1
  25. package/dist/components/ScalarDropdown/ScalarDropdown.vue.script.js +2 -2
  26. package/dist/components/ScalarDropdown/ScalarDropdown.vue.script.js.map +1 -1
  27. package/dist/components/ScalarMarkdown/ScalarMarkdown.vue.d.ts.map +1 -1
  28. package/dist/components/ScalarMarkdown/ScalarMarkdown.vue.js.map +1 -1
  29. package/dist/components/ScalarMarkdown/ScalarMarkdown.vue.script.js +4 -1
  30. package/dist/components/ScalarMarkdown/ScalarMarkdown.vue.script.js.map +1 -1
  31. package/dist/components/ScalarMarkdown/ScalarMarkdownSummary.vue.d.ts.map +1 -1
  32. package/dist/components/ScalarMarkdown/ScalarMarkdownSummary.vue.js.map +1 -1
  33. package/dist/components/ScalarMarkdown/ScalarMarkdownSummary.vue.script.js +6 -2
  34. package/dist/components/ScalarMarkdown/ScalarMarkdownSummary.vue.script.js.map +1 -1
  35. package/dist/components/ScalarMarkdown/index.d.ts +1 -0
  36. package/dist/components/ScalarMarkdown/index.d.ts.map +1 -1
  37. package/dist/components/ScalarMarkdown/index.js +2 -1
  38. package/dist/components/ScalarMarkdown/useMarkdownRenderHooks.d.ts +7 -0
  39. package/dist/components/ScalarMarkdown/useMarkdownRenderHooks.d.ts.map +1 -0
  40. package/dist/components/ScalarMarkdown/useMarkdownRenderHooks.js +46 -0
  41. package/dist/components/ScalarMarkdown/useMarkdownRenderHooks.js.map +1 -0
  42. package/dist/components/ScalarSidebar/ScalarSidebarGroupToggleButton.vue.js.map +1 -1
  43. package/dist/components/ScalarSidebar/ScalarSidebarGroupToggleButton.vue.script.js +1 -1
  44. package/dist/components/ScalarSidebar/ScalarSidebarGroupToggleButton.vue.script.js.map +1 -1
  45. package/dist/components/ScalarSidebar/ScalarSidebarGroupToggleSpacer.vue.js.map +1 -1
  46. package/dist/components/ScalarSidebar/ScalarSidebarGroupToggleSpacer.vue.script.js +1 -1
  47. package/dist/components/ScalarSidebar/ScalarSidebarGroupToggleSpacer.vue.script.js.map +1 -1
  48. package/dist/components/ScalarVirtualText/ScalarVirtualTextSearch.vue.d.ts +2 -2
  49. package/dist/index.d.ts +1 -1
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +2 -1
  52. package/dist/style.css +34 -9
  53. package/dist/vue-styles.css +2 -1
  54. package/package.json +6 -6
@@ -1,25 +1,30 @@
1
+ import ScalarLoading_default from "../ScalarLoading/ScalarLoading.vue.js";
1
2
  import { filterByOptionLabel } from "./filter-by-option-label.js";
2
3
  import ScalarListboxCheckbox_default from "../ScalarListbox/ScalarListboxCheckbox.vue.js";
3
4
  import ScalarComboboxOption_default from "./ScalarComboboxOption.vue.js";
4
5
  import ScalarComboboxOptionGroup_default from "./ScalarComboboxOptionGroup.vue.js";
5
6
  import { isGroups } from "./types.js";
6
- import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, mergeModels, onMounted, openBlock, ref, renderList, renderSlot, toDisplayString, unref, useId, useModel, useSlots, vModelText, vShow, watch, withCtx, withDirectives, withKeys, withModifiers } from "vue";
7
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, mergeModels, normalizeClass, onMounted, openBlock, ref, renderList, renderSlot, toDisplayString, unref, useId, useModel, useSlots, vModelText, vShow, watch, withCtx, withDirectives, withKeys, withModifiers } from "vue";
7
8
  import { ScalarIconMagnifyingGlass, ScalarIconPlus } from "@scalar/icons";
8
9
  //#region src/components/ScalarCombobox/ScalarComboboxOptions.vue?vue&type=script&setup=true&lang.ts
9
- var _hoisted_1 = { class: "relative flex m-1 mb-0.75" };
10
+ var _hoisted_1 = { class: "relative flex items-center m-1 mb-0.75" };
10
11
  var _hoisted_2 = [
11
12
  "aria-activedescendant",
12
13
  "aria-expanded",
13
14
  "aria-label",
14
15
  "placeholder"
15
16
  ];
16
- var _hoisted_3 = ["aria-multiselectable"];
17
- var _hoisted_4 = {
17
+ var _hoisted_3 = {
18
18
  key: 0,
19
+ class: "absolute right-1.75 top-1/2 flex -translate-y-1/2 items-center"
20
+ };
21
+ var _hoisted_4 = ["aria-busy", "aria-multiselectable"];
22
+ var _hoisted_5 = {
23
+ key: 1,
19
24
  class: "text-c-3 px-2.5 py-2",
20
25
  role: "status"
21
26
  };
22
- var _hoisted_5 = { class: "inline-block min-w-0 flex-1 truncate text-c-1" };
27
+ var _hoisted_6 = { class: "inline-block min-w-0 flex-1 truncate text-c-1" };
23
28
  var ScalarComboboxOptions_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
24
29
  inheritAttrs: false,
25
30
  __name: "ScalarComboboxOptions",
@@ -33,12 +38,15 @@ var ScalarComboboxOptions_vue_vue_type_script_setup_true_lang_default = /*@__PUR
33
38
  type: Function,
34
39
  default: filterByOptionLabel
35
40
  },
41
+ loader: {},
36
42
  multiselect: { type: Boolean }
37
43
  }, {
38
44
  "modelValue": { default: () => [] },
39
- "modelModifiers": {}
45
+ "modelModifiers": {},
46
+ "query": { default: "" },
47
+ "queryModifiers": {}
40
48
  }),
41
- emits: /*@__PURE__*/ mergeModels(["add"], ["update:modelValue"]),
49
+ emits: /*@__PURE__*/ mergeModels(["add"], ["update:modelValue", "update:query"]),
42
50
  setup(__props, { emit: __emit }) {
43
51
  const emit = __emit;
44
52
  const model = useModel(__props, "modelValue");
@@ -61,31 +69,73 @@ var ScalarComboboxOptions_vue_vue_type_script_setup_true_lang_default = /*@__PUR
61
69
  label: "",
62
70
  options: __props.options
63
71
  }]);
64
- const query = ref("");
65
- const activeRef = ref(model.value?.[0] ?? options.value[0]);
72
+ /** The search query, exposed so callers can search the options themselves */
73
+ const query = useModel(__props, "query");
74
+ /** The filtered list of options */
75
+ const filtered = computed(() => __props.filterFn(query.value, options.value, groups.value));
76
+ /** The list of filtered options with the "Add a new option" option */
77
+ const withAdd = computed(() => slots.add ? [...filtered.value, addOption] : filtered.value);
78
+ /**
79
+ * The selected option when it is in the list, otherwise the top option.
80
+ *
81
+ * With async search the selected option is often not in the loaded results (e.g. a branch
82
+ * past the first page), and pointing at a row that does not exist would let Enter select nothing.
83
+ */
84
+ const getInitialActive = () => {
85
+ const selected = model.value[0];
86
+ return selected && withAdd.value.some((option) => option.id === selected.id) ? selected : withAdd.value[0];
87
+ };
88
+ const activeRef = ref(getInitialActive());
66
89
  onMounted(() => {
67
90
  query.value = "";
68
- activeRef.value = model.value?.[0] ?? options.value[0];
91
+ activeRef.value = getInitialActive();
69
92
  if (model.value[0]) setTimeout(() => {
70
93
  const value = model.value[0];
71
94
  if (!value) return;
72
95
  document?.getElementById(getOptionId(value))?.scrollIntoView({ block: "nearest" });
73
96
  }, 10);
74
97
  });
75
- watch(() => query.value, () => activeRef.value = withAdd.value[0]);
76
- /** The filtered list of options */
77
- const filtered = computed(() => __props.filterFn(query.value, options.value, groups.value));
78
- /** The list of filtered options with the "Add a new option" option */
79
- const withAdd = computed(() => slots.add ? [...filtered.value, addOption] : filtered.value);
98
+ /**
99
+ * Whether the query changed since the options last updated. Async results arrive after the
100
+ * query changes, so the top hit of the new results should become active even when the
101
+ * previously active option is still somewhere in the list.
102
+ */
103
+ let queryChanged = false;
104
+ watch(() => query.value, () => {
105
+ activeRef.value = withAdd.value[0];
106
+ queryChanged = true;
107
+ });
108
+ /**
109
+ * Async options arrive after the query has changed, so the active option can point at a row
110
+ * that no longer exists or is no longer the top hit. Move it back to the top when that happens.
111
+ */
112
+ watch(withAdd, (list) => {
113
+ if (queryChanged || !list.some((option) => option.id === activeRef.value?.id)) activeRef.value = list[0];
114
+ queryChanged = false;
115
+ });
116
+ /** Whether the options are still loading */
117
+ const isLoading = computed(() => Boolean(__props.loader?.isLoading));
118
+ /** Show placeholder rows while loading with nothing to show yet */
119
+ const showSkeleton = computed(() => isLoading.value && !filtered.value.length);
120
+ /** Show the loader in the search input, unless the skeleton already says it is loading */
121
+ const showLoader = computed(() => Boolean(__props.loader?.isActive) && !showSkeleton.value);
122
+ /** Whether there is anything to show at the end of the search input */
123
+ const showSearchEnd = computed(() => Boolean(slots["search-end"] || showLoader.value));
124
+ /** Whether the listbox has anything to show */
125
+ const showList = computed(() => Boolean(filtered.value.length || slots.add || showSkeleton.value || __props.noResults && !isLoading.value));
80
126
  function toggleSelected(option) {
81
127
  if (!option) return;
82
128
  if (option.id === addOption.id) {
83
129
  addNew();
84
130
  return;
85
131
  }
86
- if (__props.multiselect) if (model.value.some((o) => o.id === option.id)) model.value = model.value.filter((o) => o.id !== option.id);
87
- else model.value = [...model.value, options.value.find((o) => o.id === option.id)];
88
- else model.value = [options.value.find((o) => o.id === option.id)];
132
+ if (__props.multiselect && model.value.some((o) => o.id === option.id)) {
133
+ model.value = model.value.filter((o) => o.id !== option.id);
134
+ return;
135
+ }
136
+ const match = options.value.find((o) => o.id === option.id);
137
+ if (!match) return;
138
+ model.value = __props.multiselect ? [...model.value, match] : [match];
89
139
  }
90
140
  function moveActive(dir) {
91
141
  const list = withAdd.value;
@@ -120,36 +170,52 @@ var ScalarComboboxOptions_vue_vue_type_script_setup_true_lang_default = /*@__PUR
120
170
  const input = ref(null);
121
171
  onMounted(() => setTimeout(() => input.value?.focus(), 0));
122
172
  return (_ctx, _cache) => {
123
- return openBlock(), createElementBlock(Fragment, null, [createElementVNode("div", _hoisted_1, [createVNode(unref(ScalarIconMagnifyingGlass), { class: "pointer-events-none absolute left-1.75 top-1/2 -translate-y-1/2 text-c-3 size-4" }), withDirectives(createElementVNode("input", {
124
- ref_key: "input",
125
- ref: input,
126
- "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => query.value = $event),
127
- "aria-activedescendant": activeRef.value ? getOptionId(activeRef.value) : void 0,
128
- "aria-autocomplete": "list",
129
- "aria-controls": id,
130
- "aria-expanded": Boolean(filtered.value.length || slots.add || __props.noResults),
131
- "aria-label": __props.inputLabel,
132
- class: "min-w-0 flex-1 rounded-md border-0 py-1.5 pl-7.25 pr-1.75 leading-none text-c-1",
133
- "data-1p-ignore": "",
134
- placeholder: __props.placeholder,
135
- role: "combobox",
136
- tabindex: "0",
137
- type: "text",
138
- onKeydown: [
139
- _cache[1] || (_cache[1] = withKeys(withModifiers(($event) => moveActive(1), ["prevent"]), ["down"])),
140
- _cache[2] || (_cache[2] = withKeys(withModifiers(($event) => activeRef.value && toggleSelected(activeRef.value), ["prevent"]), ["enter"])),
141
- _cache[3] || (_cache[3] = withKeys(withModifiers(($event) => __props.close?.(), ["prevent"]), ["esc"])),
142
- withKeys(handleSpace, ["space"]),
143
- _cache[4] || (_cache[4] = withKeys(withModifiers(($event) => moveActive(-1), ["prevent"]), ["up"]))
144
- ]
145
- }, null, 40, _hoisted_2), [[vModelText, query.value]])]), withDirectives(createElementVNode("ul", {
173
+ return openBlock(), createElementBlock(Fragment, null, [createElementVNode("div", _hoisted_1, [
174
+ createVNode(unref(ScalarIconMagnifyingGlass), { class: "pointer-events-none absolute left-1.75 top-1/2 -translate-y-1/2 text-c-3 size-4" }),
175
+ withDirectives(createElementVNode("input", {
176
+ ref_key: "input",
177
+ ref: input,
178
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => query.value = $event),
179
+ "aria-activedescendant": activeRef.value ? getOptionId(activeRef.value) : void 0,
180
+ "aria-autocomplete": "list",
181
+ "aria-controls": id,
182
+ "aria-expanded": showList.value,
183
+ "aria-label": __props.inputLabel,
184
+ class: normalizeClass(["min-w-0 flex-1 rounded-md border-0 py-1.5 pl-7.25 leading-none text-c-1", showSearchEnd.value ? "pr-7.25" : "pr-1.75"]),
185
+ "data-1p-ignore": "",
186
+ placeholder: __props.placeholder,
187
+ role: "combobox",
188
+ tabindex: "0",
189
+ type: "text",
190
+ onKeydown: [
191
+ _cache[1] || (_cache[1] = withKeys(withModifiers(($event) => moveActive(1), ["prevent"]), ["down"])),
192
+ _cache[2] || (_cache[2] = withKeys(withModifiers(($event) => activeRef.value && toggleSelected(activeRef.value), ["prevent"]), ["enter"])),
193
+ _cache[3] || (_cache[3] = withKeys(withModifiers(($event) => __props.close?.(), ["prevent"]), ["esc"])),
194
+ withKeys(handleSpace, ["space"]),
195
+ _cache[4] || (_cache[4] = withKeys(withModifiers(($event) => moveActive(-1), ["prevent"]), ["up"]))
196
+ ]
197
+ }, null, 42, _hoisted_2), [[vModelText, query.value]]),
198
+ showSearchEnd.value ? (openBlock(), createElementBlock("div", _hoisted_3, [renderSlot(_ctx.$slots, "search-end", { loader: __props.loader }, () => [showLoader.value ? (openBlock(), createBlock(unref(ScalarLoading_default), {
199
+ key: 0,
200
+ loader: __props.loader,
201
+ size: "md"
202
+ }, null, 8, ["loader"])) : createCommentVNode("", true)])])) : createCommentVNode("", true)
203
+ ]), withDirectives(createElementVNode("ul", {
146
204
  id,
205
+ "aria-busy": isLoading.value,
147
206
  "aria-multiselectable": __props.multiselect,
148
207
  class: "border-t p-0.75 custom-scroll overscroll-contain flex-1 min-h-0",
149
208
  role: "listbox",
150
209
  tabindex: "-1"
151
210
  }, [
152
- !filtered.value.length && !slots.add && __props.noResults ? (openBlock(), createElementBlock("li", _hoisted_4, toDisplayString(__props.noResults), 1)) : createCommentVNode("", true),
211
+ showSkeleton.value ? (openBlock(), createElementBlock(Fragment, { key: 0 }, renderList(3, (idx) => {
212
+ return createElementVNode("li", {
213
+ key: idx,
214
+ "aria-hidden": "true",
215
+ class: "flex px-2 py-1.5",
216
+ role: "presentation"
217
+ }, [createElementVNode("div", { class: normalizeClass(["h-lh rounded-md bg-b-3 animate-pulse", idx === 3 ? "w-2/3" : "w-full"]) }, null, 2)]);
218
+ }), 64)) : !filtered.value.length && !slots.add && __props.noResults && !isLoading.value ? (openBlock(), createElementBlock("li", _hoisted_5, toDisplayString(__props.noResults), 1)) : createCommentVNode("", true),
153
219
  (openBlock(true), createElementBlock(Fragment, null, renderList(groups.value, (group, i) => {
154
220
  return openBlock(), createBlock(ScalarComboboxOptionGroup_default, {
155
221
  id: `${id}-group-${i}`,
@@ -174,7 +240,7 @@ var ScalarComboboxOptions_vue_vue_type_script_setup_true_lang_default = /*@__PUR
174
240
  }, void 0, void 0, 0) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createVNode(unref(ScalarListboxCheckbox_default), {
175
241
  multiselect: __props.multiselect,
176
242
  selected: model.value.some((o) => o.id === option.id)
177
- }, null, 8, ["multiselect", "selected"]), createElementVNode("span", _hoisted_5, toDisplayString(option.label), 1)], 64))]),
243
+ }, null, 8, ["multiselect", "selected"]), createElementVNode("span", _hoisted_6, toDisplayString(option.label), 1)], 64))]),
178
244
  _: 2
179
245
  }, 1032, [
180
246
  "id",
@@ -188,7 +254,7 @@ var ScalarComboboxOptions_vue_vue_type_script_setup_true_lang_default = /*@__PUR
188
254
  }, 1032, ["id", "hidden"]);
189
255
  }), 128)),
190
256
  slots.add ? (openBlock(), createBlock(ScalarComboboxOption_default, {
191
- key: 1,
257
+ key: 2,
192
258
  id: getOptionId(addOption),
193
259
  active: activeRef.value?.id === addOption.id,
194
260
  onClick: addNew,
@@ -198,7 +264,7 @@ var ScalarComboboxOptions_vue_vue_type_script_setup_true_lang_default = /*@__PUR
198
264
  default: withCtx(({ active }) => [createVNode(unref(ScalarIconPlus), { class: "size-4 p-px" }), renderSlot(_ctx.$slots, "add", { active })]),
199
265
  _: 3
200
266
  }, 8, ["id", "active"])) : createCommentVNode("", true)
201
- ], 8, _hoisted_3), [[vShow, filtered.value.length || slots.add || __props.noResults]])], 64);
267
+ ], 8, _hoisted_4), [[vShow, showList.value]])], 64);
202
268
  };
203
269
  }
204
270
  });
@@ -1 +1 @@
1
- {"version":3,"file":"ScalarComboboxOptions.vue.script.js","names":["$slots"],"sources":["../../../src/components/ScalarCombobox/ScalarComboboxOptions.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Scalar Combobox Options component\n *\n * Renders the searchable option list for a combobox, including\n * filtering, keyboard navigation, and an optional \"add new\" action.\n *\n * @example\n * <ScalarComboboxOptions v-model=\"selected\" :options=\"options\" />\n */\nexport default {}\n</script>\n\n<!-- prettier-ignore-attribute generic -->\n<script\n setup\n lang=\"ts\"\n generic=\"O extends Option = Option, G extends OptionGroup<O> = OptionGroup<O>\">\nimport { filterByOptionLabel } from '@/components/ScalarCombobox/filter-by-option-label'\nimport { ScalarIconMagnifyingGlass, ScalarIconPlus } from '@scalar/icons'\nimport { computed, onMounted, ref, useId, watch } from 'vue'\n\nimport { ScalarListboxCheckbox } from '../ScalarListbox'\nimport ComboboxOption from './ScalarComboboxOption.vue'\nimport ComboboxOptionGroup from './ScalarComboboxOptionGroup.vue'\nimport {\n type ComboboxEmits,\n type ComboboxSlots,\n type FilterFunction,\n type Option,\n type OptionGroup,\n type OptionsOrGroups,\n isGroups,\n} from './types'\n\nconst {\n options: optionsOrGroups,\n placeholder,\n inputLabel,\n noResults,\n close,\n filterFn = filterByOptionLabel,\n multiselect,\n} = defineProps<{\n /** The options to display in the combobox */\n options: OptionsOrGroups<O, G>\n /** The placeholder text to display in the combobox */\n placeholder?: string\n /** An accessible label for the search input */\n inputLabel?: string\n /** The message to display when filtering returns no options */\n noResults?: string\n /** Close the popover without changing the current selection */\n close?: () => void\n /**\n * A function to filter the options based on a query,\n * if not provided, the options will be filtered by option label\n *\n * @see {@link FilterFunction} for more information\n */\n filterFn?: FilterFunction<O, G>\n /** Whether the combobox is in multiselect mode, defaults to false */\n multiselect?: boolean\n}>()\n\nconst emit = defineEmits<ComboboxEmits>()\n\nconst model = defineModel<O[]>({ default: () => [] })\n\nconst slots = defineSlots<Omit<ComboboxSlots<O, G>, 'default'>>()\n\ndefineOptions({ inheritAttrs: false })\n\n/** A unique ID for the combobox */\nconst id = `scalar-combobox-items-${useId()}`\n\n/** A static option entry for the \"Add a new option\" option */\nconst addOption: Option = { id: `${useId()}-add`, label: 'Add a new option' }\n\n/** Generate a unique ID for an option */\nfunction getOptionId(option: Option) {\n return `${id}-${option.id}`\n}\n\n/** A flat list of all options */\nconst options = computed<O[]>(() =>\n isGroups(optionsOrGroups)\n ? optionsOrGroups.flatMap((group) => group.options)\n : optionsOrGroups,\n)\n\n/** An list of all groups */\nconst groups = computed<G[]>(\n () =>\n isGroups(optionsOrGroups)\n ? optionsOrGroups // G extends OptionGroup<O>\n : /*\n We know G is an unextended OptionGroup<O> here because of the\n structure of the component props so we can cast it to G\n */\n [{ label: '', options: optionsOrGroups } as G], // G is OptionGroup<O>\n)\n\nconst query = ref<string>('')\nconst activeRef = ref<Option | undefined>(model.value?.[0] ?? options.value[0])\n\n// Clear the query on open and close\nonMounted(() => {\n // Clear the query\n query.value = ''\n\n // Set the active option to the selected option or the first option\n activeRef.value = model.value?.[0] ?? options.value[0]\n\n // Scroll to the selected option\n const selected = model.value[0]\n if (selected) {\n setTimeout(() => {\n const value = model.value[0]\n if (!value) {\n return\n }\n\n document\n ?.getElementById(getOptionId(value))\n ?.scrollIntoView({ block: 'nearest' })\n }, 10)\n }\n})\n\n// Set the top option as active when searching\nwatch(\n () => query.value,\n () => (activeRef.value = withAdd.value[0]),\n)\n\n/** The filtered list of options */\nconst filtered = computed<O[]>(() =>\n filterFn(query.value, options.value, groups.value),\n)\n\n/** The list of filtered options with the \"Add a new option\" option */\nconst withAdd = computed<Option[]>(() =>\n slots.add ? [...filtered.value, addOption] : filtered.value,\n)\n\nfunction toggleSelected(option: Option | undefined) {\n if (!option) {\n return\n }\n\n if (option.id === addOption.id) {\n addNew()\n return\n }\n\n if (multiselect) {\n // Remove from selection list\n if (model.value.some((o) => o.id === option.id)) {\n model.value = model.value.filter((o) => o.id !== option.id)\n }\n // Add to selection list\n else {\n model.value = [\n ...model.value,\n options.value.find((o) => o.id === option.id)!,\n ]\n }\n } else {\n // Set selection for single select mode\n model.value = [options.value.find((o) => o.id === option.id)!]\n }\n}\n\nfunction moveActive(dir: 1 | -1) {\n const list = withAdd.value\n\n // Find active index\n const activeIdx = list.findIndex(\n (option) => option.id === activeRef.value?.id,\n )\n\n // Calculate next index and exit if it's out of bounds\n const nextIdx = activeIdx + dir\n\n if (nextIdx < 0 || nextIdx > list.length - 1) {\n return\n }\n\n activeRef.value = list[nextIdx]! // We know it's in bounds from the check above\n\n if (!activeRef.value) {\n return\n }\n\n // Scroll to the active option\n document?.getElementById(getOptionId(activeRef.value))?.scrollIntoView({\n behavior: 'smooth',\n block: 'nearest',\n })\n}\n\nfunction addNew() {\n emit('add')\n query.value = ''\n}\n\n/**\n * Space toggles the active option in multiselect mode while the search box is empty.\n *\n * The options look like checkboxes, so keyboard users press Space to toggle them. Without this\n * handler Space types a leading space into the query, which filters out every option whose label\n * has no space and hides the whole list without any feedback (found by an accessibility audit).\n * Once a query has been typed, Space keeps inserting a character so multi-word labels stay\n * searchable. Key auto-repeat is ignored so holding Space after opening the popover with it does\n * not toggle the option repeatedly, and IME composition is left alone.\n */\nconst handleSpace = (event: KeyboardEvent): void => {\n if (!multiselect || query.value !== '' || event.isComposing) {\n return\n }\n // Suppress repeated spaces too, so holding the key does not start filtering.\n event.preventDefault()\n if (!event.repeat) {\n toggleSelected(activeRef.value)\n }\n}\n\n// Manual autofocus for the input\nconst input = ref<HTMLInputElement | null>(null)\n\n// This must be a setTimeout to ensure there is no scroll jump. nextTick does not work here.\nonMounted(() => setTimeout(() => input.value?.focus(), 0))\n</script>\n<template>\n <!-- Inset to line up with the options below -->\n <div class=\"relative flex m-1 mb-0.75\">\n <ScalarIconMagnifyingGlass\n class=\"pointer-events-none absolute left-1.75 top-1/2 -translate-y-1/2 text-c-3 size-4\" />\n <input\n ref=\"input\"\n v-model=\"query\"\n :aria-activedescendant=\"activeRef ? getOptionId(activeRef) : undefined\"\n aria-autocomplete=\"list\"\n :aria-controls=\"id\"\n :aria-expanded=\"Boolean(filtered.length || slots.add || noResults)\"\n :aria-label=\"inputLabel\"\n class=\"min-w-0 flex-1 rounded-md border-0 py-1.5 pl-7.25 pr-1.75 leading-none text-c-1\"\n data-1p-ignore\n :placeholder\n role=\"combobox\"\n tabindex=\"0\"\n type=\"text\"\n @keydown.down.prevent=\"moveActive(1)\"\n @keydown.enter.prevent=\"activeRef && toggleSelected(activeRef)\"\n @keydown.esc.prevent=\"close?.()\"\n @keydown.space=\"handleSpace\"\n @keydown.up.prevent=\"moveActive(-1)\" />\n </div>\n <ul\n v-show=\"filtered.length || slots.add || noResults\"\n :id=\"id\"\n :aria-multiselectable=\"multiselect\"\n class=\"border-t p-0.75 custom-scroll overscroll-contain flex-1 min-h-0\"\n role=\"listbox\"\n tabindex=\"-1\">\n <li\n v-if=\"!filtered.length && !slots.add && noResults\"\n class=\"text-c-3 px-2.5 py-2\"\n role=\"status\">\n {{ noResults }}\n </li>\n <ComboboxOptionGroup\n v-for=\"(group, i) in groups\"\n :id=\"`${id}-group-${i}`\"\n :key=\"i\"\n :hidden=\"\n // Only show the group label if there are some results\n !group.options.some((o) => filtered.some((f) => f.id === o.id)) ||\n // And it has a label\n !group.label\n \">\n <template #label>\n <slot\n v-if=\"$slots.group\"\n :group\n name=\"group\" />\n <template v-else>\n {{ group.label }}\n </template>\n </template>\n <template\n v-for=\"option in filtered\"\n :key=\"option.id\">\n <ComboboxOption\n v-if=\"group.options.some((o) => o.id === option.id)\"\n :id=\"getOptionId(option)\"\n v-slot=\"{ active, selected }\"\n :active=\"activeRef?.id === option.id\"\n :selected=\"model.some((o) => o.id === option.id)\"\n @click=\"toggleSelected(option)\"\n @mousedown.prevent\n @mouseenter=\"activeRef = option\">\n <slot\n v-if=\"$slots.option\"\n :active\n name=\"option\"\n :option\n :selected />\n <template v-else>\n <ScalarListboxCheckbox\n :multiselect\n :selected=\"model.some((o) => o.id === option.id)\" />\n <span class=\"inline-block min-w-0 flex-1 truncate text-c-1\">\n {{ option.label }}\n </span>\n </template>\n </ComboboxOption>\n </template>\n </ComboboxOptionGroup>\n <ComboboxOption\n v-if=\"slots.add\"\n :id=\"getOptionId(addOption)\"\n v-slot=\"{ active }\"\n :active=\"activeRef?.id === addOption.id\"\n @click=\"addNew\"\n @mousedown.prevent\n @mouseenter=\"activeRef = addOption\">\n <ScalarIconPlus class=\"size-4 p-px\" />\n <slot\n :active\n name=\"add\" />\n </ComboboxOption>\n </ul>\n</template>\n"],"mappings":"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"}
1
+ {"version":3,"file":"ScalarComboboxOptions.vue.script.js","names":["$slots"],"sources":["../../../src/components/ScalarCombobox/ScalarComboboxOptions.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Scalar Combobox Options component\n *\n * Renders the searchable option list for a combobox, including\n * filtering, keyboard navigation, and an optional \"add new\" action.\n *\n * @example\n * <ScalarComboboxOptions v-model=\"selected\" :options=\"options\" />\n */\nexport default {}\n</script>\n\n<!-- prettier-ignore-attribute generic -->\n<script\n setup\n lang=\"ts\"\n generic=\"O extends Option = Option, G extends OptionGroup<O> = OptionGroup<O>\">\nimport { filterByOptionLabel } from '@/components/ScalarCombobox/filter-by-option-label'\nimport { ScalarIconMagnifyingGlass, ScalarIconPlus } from '@scalar/icons'\nimport { computed, onMounted, ref, useId, watch } from 'vue'\n\nimport { ScalarListboxCheckbox } from '../ScalarListbox'\nimport { type LoadingState, ScalarLoading } from '../ScalarLoading'\nimport ComboboxOption from './ScalarComboboxOption.vue'\nimport ComboboxOptionGroup from './ScalarComboboxOptionGroup.vue'\nimport {\n type ComboboxEmits,\n type ComboboxSlots,\n type FilterFunction,\n type Option,\n type OptionGroup,\n type OptionsOrGroups,\n isGroups,\n} from './types'\n\nconst {\n options: optionsOrGroups,\n placeholder,\n inputLabel,\n noResults,\n close,\n filterFn = filterByOptionLabel,\n loader,\n multiselect,\n} = defineProps<{\n /** The options to display in the combobox */\n options: OptionsOrGroups<O, G>\n /** The placeholder text to display in the combobox */\n placeholder?: string\n /** An accessible label for the search input */\n inputLabel?: string\n /** The message to display when filtering returns no options */\n noResults?: string\n /** Close the popover without changing the current selection */\n close?: () => void\n /**\n * A function to filter the options based on a query,\n * if not provided, the options will be filtered by option label\n *\n * Pass `(_, options) => options` when the caller searches the options itself (e.g. on a\n * server) using the `query` model, so results the server already matched are shown as given.\n *\n * @see {@link FilterFunction} for more information\n */\n filterFn?: FilterFunction<O, G>\n /**\n * The loading state of the options, see `useLoadingState`\n *\n * Shows a spinner at the end of the search input while active, or an X when the state is\n * invalidated (e.g. a failed request). While loading with no options yet, the list shows\n * placeholder rows instead of the \"no results\" message and the spinner.\n */\n loader?: LoadingState\n /** Whether the combobox is in multiselect mode, defaults to false */\n multiselect?: boolean\n}>()\n\nconst emit = defineEmits<ComboboxEmits>()\n\nconst model = defineModel<O[]>({ default: () => [] })\n\nconst slots = defineSlots<Omit<ComboboxSlots<O, G>, 'default'>>()\n\ndefineOptions({ inheritAttrs: false })\n\n/** A unique ID for the combobox */\nconst id = `scalar-combobox-items-${useId()}`\n\n/** A static option entry for the \"Add a new option\" option */\nconst addOption: Option = { id: `${useId()}-add`, label: 'Add a new option' }\n\n/** Generate a unique ID for an option */\nfunction getOptionId(option: Option) {\n return `${id}-${option.id}`\n}\n\n/** A flat list of all options */\nconst options = computed<O[]>(() =>\n isGroups(optionsOrGroups)\n ? optionsOrGroups.flatMap((group) => group.options)\n : optionsOrGroups,\n)\n\n/** An list of all groups */\nconst groups = computed<G[]>(\n () =>\n isGroups(optionsOrGroups)\n ? optionsOrGroups // G extends OptionGroup<O>\n : /*\n We know G is an unextended OptionGroup<O> here because of the\n structure of the component props so we can cast it to G\n */\n [{ label: '', options: optionsOrGroups } as G], // G is OptionGroup<O>\n)\n\n/** The search query, exposed so callers can search the options themselves */\nconst query = defineModel<string>('query', { default: '' })\n/** The filtered list of options */\nconst filtered = computed<O[]>(() =>\n filterFn(query.value, options.value, groups.value),\n)\n\n/** The list of filtered options with the \"Add a new option\" option */\nconst withAdd = computed<Option[]>(() =>\n slots.add ? [...filtered.value, addOption] : filtered.value,\n)\n\n/**\n * The selected option when it is in the list, otherwise the top option.\n *\n * With async search the selected option is often not in the loaded results (e.g. a branch\n * past the first page), and pointing at a row that does not exist would let Enter select nothing.\n */\nconst getInitialActive = (): Option | undefined => {\n const selected = model.value[0]\n return selected && withAdd.value.some((option) => option.id === selected.id)\n ? selected\n : withAdd.value[0]\n}\n\nconst activeRef = ref<Option | undefined>(getInitialActive())\n\n// Clear the query on open and close\nonMounted(() => {\n // Clear the query\n query.value = ''\n\n // Set the active option to the selected option or the first option\n activeRef.value = getInitialActive()\n\n // Scroll to the selected option\n const selected = model.value[0]\n if (selected) {\n setTimeout(() => {\n const value = model.value[0]\n if (!value) {\n return\n }\n\n document\n ?.getElementById(getOptionId(value))\n ?.scrollIntoView({ block: 'nearest' })\n }, 10)\n }\n})\n\n/**\n * Whether the query changed since the options last updated. Async results arrive after the\n * query changes, so the top hit of the new results should become active even when the\n * previously active option is still somewhere in the list.\n */\nlet queryChanged = false\n\n// Set the top option as active when searching\nwatch(\n () => query.value,\n () => {\n activeRef.value = withAdd.value[0]\n queryChanged = true\n },\n)\n\n/**\n * Async options arrive after the query has changed, so the active option can point at a row\n * that no longer exists or is no longer the top hit. Move it back to the top when that happens.\n */\nwatch(withAdd, (list) => {\n if (\n queryChanged ||\n !list.some((option) => option.id === activeRef.value?.id)\n ) {\n activeRef.value = list[0]\n }\n queryChanged = false\n})\n\n/** Whether the options are still loading */\nconst isLoading = computed<boolean>(() => Boolean(loader?.isLoading))\n\n/** Show placeholder rows while loading with nothing to show yet */\nconst showSkeleton = computed<boolean>(\n () => isLoading.value && !filtered.value.length,\n)\n\n/** Show the loader in the search input, unless the skeleton already says it is loading */\nconst showLoader = computed<boolean>(\n () => Boolean(loader?.isActive) && !showSkeleton.value,\n)\n\n/** Whether there is anything to show at the end of the search input */\nconst showSearchEnd = computed<boolean>(() =>\n Boolean(slots['search-end'] || showLoader.value),\n)\n\n/** Whether the listbox has anything to show */\nconst showList = computed<boolean>(() =>\n Boolean(\n filtered.value.length ||\n slots.add ||\n showSkeleton.value ||\n (noResults && !isLoading.value),\n ),\n)\n\nfunction toggleSelected(option: Option | undefined) {\n if (!option) {\n return\n }\n\n if (option.id === addOption.id) {\n addNew()\n return\n }\n\n // Remove from selection list in multiselect mode\n if (multiselect && model.value.some((o) => o.id === option.id)) {\n model.value = model.value.filter((o) => o.id !== option.id)\n return\n }\n\n // The active option can be stale while async options load, so never select a missing one\n const match = options.value.find((o) => o.id === option.id)\n if (!match) {\n return\n }\n\n // Add to the selection in multiselect mode, replace it in single select mode\n model.value = multiselect ? [...model.value, match] : [match]\n}\n\nfunction moveActive(dir: 1 | -1) {\n const list = withAdd.value\n\n // Find active index\n const activeIdx = list.findIndex(\n (option) => option.id === activeRef.value?.id,\n )\n\n // Calculate next index and exit if it's out of bounds\n const nextIdx = activeIdx + dir\n\n if (nextIdx < 0 || nextIdx > list.length - 1) {\n return\n }\n\n activeRef.value = list[nextIdx]! // We know it's in bounds from the check above\n\n if (!activeRef.value) {\n return\n }\n\n // Scroll to the active option\n document?.getElementById(getOptionId(activeRef.value))?.scrollIntoView({\n behavior: 'smooth',\n block: 'nearest',\n })\n}\n\nfunction addNew() {\n emit('add')\n query.value = ''\n}\n\n/**\n * Space toggles the active option in multiselect mode while the search box is empty.\n *\n * The options look like checkboxes, so keyboard users press Space to toggle them. Without this\n * handler Space types a leading space into the query, which filters out every option whose label\n * has no space and hides the whole list without any feedback (found by an accessibility audit).\n * Once a query has been typed, Space keeps inserting a character so multi-word labels stay\n * searchable. Key auto-repeat is ignored so holding Space after opening the popover with it does\n * not toggle the option repeatedly, and IME composition is left alone.\n */\nconst handleSpace = (event: KeyboardEvent): void => {\n if (!multiselect || query.value !== '' || event.isComposing) {\n return\n }\n // Suppress repeated spaces too, so holding the key does not start filtering.\n event.preventDefault()\n if (!event.repeat) {\n toggleSelected(activeRef.value)\n }\n}\n\n// Manual autofocus for the input\nconst input = ref<HTMLInputElement | null>(null)\n\n// This must be a setTimeout to ensure there is no scroll jump. nextTick does not work here.\nonMounted(() => setTimeout(() => input.value?.focus(), 0))\n</script>\n<template>\n <!-- Inset to line up with the options below -->\n <div class=\"relative flex items-center m-1 mb-0.75\">\n <ScalarIconMagnifyingGlass\n class=\"pointer-events-none absolute left-1.75 top-1/2 -translate-y-1/2 text-c-3 size-4\" />\n <input\n ref=\"input\"\n v-model=\"query\"\n :aria-activedescendant=\"activeRef ? getOptionId(activeRef) : undefined\"\n aria-autocomplete=\"list\"\n :aria-controls=\"id\"\n :aria-expanded=\"showList\"\n :aria-label=\"inputLabel\"\n class=\"min-w-0 flex-1 rounded-md border-0 py-1.5 pl-7.25 leading-none text-c-1\"\n :class=\"showSearchEnd ? 'pr-7.25' : 'pr-1.75'\"\n data-1p-ignore\n :placeholder\n role=\"combobox\"\n tabindex=\"0\"\n type=\"text\"\n @keydown.down.prevent=\"moveActive(1)\"\n @keydown.enter.prevent=\"activeRef && toggleSelected(activeRef)\"\n @keydown.esc.prevent=\"close?.()\"\n @keydown.space=\"handleSpace\"\n @keydown.up.prevent=\"moveActive(-1)\" />\n <div\n v-if=\"showSearchEnd\"\n class=\"absolute right-1.75 top-1/2 flex -translate-y-1/2 items-center\">\n <slot\n :loader\n name=\"search-end\">\n <ScalarLoading\n v-if=\"showLoader\"\n :loader\n size=\"md\" />\n </slot>\n </div>\n </div>\n <ul\n v-show=\"showList\"\n :id=\"id\"\n :aria-busy=\"isLoading\"\n :aria-multiselectable=\"multiselect\"\n class=\"border-t p-0.75 custom-scroll overscroll-contain flex-1 min-h-0\"\n role=\"listbox\"\n tabindex=\"-1\">\n <template v-if=\"showSkeleton\">\n <li\n v-for=\"idx in 3\"\n :key=\"idx\"\n aria-hidden=\"true\"\n class=\"flex px-2 py-1.5\"\n role=\"presentation\">\n <div\n class=\"h-lh rounded-md bg-b-3 animate-pulse\"\n :class=\"idx === 3 ? 'w-2/3' : 'w-full'\" />\n </li>\n </template>\n <li\n v-else-if=\"!filtered.length && !slots.add && noResults && !isLoading\"\n class=\"text-c-3 px-2.5 py-2\"\n role=\"status\">\n {{ noResults }}\n </li>\n <ComboboxOptionGroup\n v-for=\"(group, i) in groups\"\n :id=\"`${id}-group-${i}`\"\n :key=\"i\"\n :hidden=\"\n // Only show the group label if there are some results\n !group.options.some((o) => filtered.some((f) => f.id === o.id)) ||\n // And it has a label\n !group.label\n \">\n <template #label>\n <slot\n v-if=\"$slots.group\"\n :group\n name=\"group\" />\n <template v-else>\n {{ group.label }}\n </template>\n </template>\n <template\n v-for=\"option in filtered\"\n :key=\"option.id\">\n <ComboboxOption\n v-if=\"group.options.some((o) => o.id === option.id)\"\n :id=\"getOptionId(option)\"\n v-slot=\"{ active, selected }\"\n :active=\"activeRef?.id === option.id\"\n :selected=\"model.some((o) => o.id === option.id)\"\n @click=\"toggleSelected(option)\"\n @mousedown.prevent\n @mouseenter=\"activeRef = option\">\n <slot\n v-if=\"$slots.option\"\n :active\n name=\"option\"\n :option\n :selected />\n <template v-else>\n <ScalarListboxCheckbox\n :multiselect\n :selected=\"model.some((o) => o.id === option.id)\" />\n <span class=\"inline-block min-w-0 flex-1 truncate text-c-1\">\n {{ option.label }}\n </span>\n </template>\n </ComboboxOption>\n </template>\n </ComboboxOptionGroup>\n <ComboboxOption\n v-if=\"slots.add\"\n :id=\"getOptionId(addOption)\"\n v-slot=\"{ active }\"\n :active=\"activeRef?.id === addOption.id\"\n @click=\"addNew\"\n @mousedown.prevent\n @mouseenter=\"activeRef = addOption\">\n <ScalarIconPlus class=\"size-4 p-px\" />\n <slot\n :active\n name=\"add\" />\n </ComboboxOption>\n </ul>\n</template>\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import type { LoadingState } from '../ScalarLoading/index.js';
1
2
  export type Option = {
2
3
  id: string;
3
4
  label: string;
@@ -49,5 +50,9 @@ export type ComboboxSlots<O extends Option = Option, G extends OptionGroup<O> =
49
50
  add?(props: {
50
51
  active: boolean;
51
52
  }): unknown;
53
+ /** Content at the end of the search input, replaces the loading indicator */
54
+ 'search-end'?(props: {
55
+ loader: LoadingState | undefined;
56
+ }): unknown;
52
57
  };
53
58
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/ScalarCombobox/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,MAAM,GAAG;IACnB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI;IACnD,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,CAAC,EAAE,CAAA;CACb,CAAA;AAED,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,CAAC,SAAS,WAAW,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,EAAE,CAAA;AAE7G;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,CAAC,SAAS,WAAW,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,CACjG,KAAK,EAAE,MAAM,EAEb,OAAO,EAAE,CAAC,EAAE,EAEZ,MAAM,EAAE,CAAC,EAAE,KACR,CAAC,EAAE,CAAA;AAOR,uEAAuE;AACvE,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,WAAW,EAAE,GAAG,OAAO,IAAI,WAAW,EAAE,CAKpF;AAED,+CAA+C;AAC/C,MAAM,MAAM,aAAa,GAAG;IAC1B,4DAA4D;IAC5D,GAAG,EAAE,EAAE,CAAA;CACR,CAAA;AAED,uCAAuC;AACvC,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,CAAC,SAAS,WAAW,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI;IAChG,uDAAuD;IACvD,OAAO,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAA;IAC1C,8CAA8C;IAC9C,MAAM,CAAC,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,CAAC,CAAC;QAAC,MAAM,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAA;IAC1E,6CAA6C;IAC7C,KAAK,CAAC,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,CAAC,CAAA;KAAE,GAAG,OAAO,CAAA;IACpC,iEAAiE;IACjE,GAAG,CAAC,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAA;CAC1C,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/ScalarCombobox/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAEpD,MAAM,MAAM,MAAM,GAAG;IACnB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI;IACnD,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,CAAC,EAAE,CAAA;CACb,CAAA;AAED,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,CAAC,SAAS,WAAW,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,EAAE,CAAA;AAE7G;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,CAAC,SAAS,WAAW,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,CACjG,KAAK,EAAE,MAAM,EAEb,OAAO,EAAE,CAAC,EAAE,EAEZ,MAAM,EAAE,CAAC,EAAE,KACR,CAAC,EAAE,CAAA;AAOR,uEAAuE;AACvE,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,WAAW,EAAE,GAAG,OAAO,IAAI,WAAW,EAAE,CAKpF;AAED,+CAA+C;AAC/C,MAAM,MAAM,aAAa,GAAG;IAC1B,4DAA4D;IAC5D,GAAG,EAAE,EAAE,CAAA;CACR,CAAA;AAED,uCAAuC;AACvC,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,CAAC,SAAS,WAAW,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI;IAChG,uDAAuD;IACvD,OAAO,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAA;IAC1C,8CAA8C;IAC9C,MAAM,CAAC,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,CAAC,CAAC;QAAC,MAAM,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAA;IAC1E,6CAA6C;IAC7C,KAAK,CAAC,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,CAAC,CAAA;KAAE,GAAG,OAAO,CAAA;IACpC,iEAAiE;IACjE,GAAG,CAAC,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAA;IACzC,6EAA6E;IAC7E,YAAY,CAAC,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,YAAY,GAAG,SAAS,CAAA;KAAE,GAAG,OAAO,CAAA;CACpE,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":[],"sources":["../../../src/components/ScalarCombobox/types.ts"],"sourcesContent":["export type Option = {\n id: string\n label: string\n}\n\nexport type OptionGroup<O extends Option = Option> = {\n label: string\n options: O[]\n}\n\nexport type OptionsOrGroups<O extends Option = Option, G extends OptionGroup<O> = OptionGroup<O>> = O[] | G[]\n\n/**\n * Filters which options appear while searching. The combobox passes the\n * current query, a flat list of every option (including those inside groups),\n * and the group objects when the list is grouped (otherwise an empty array).\n *\n * @example Match option labels, case-insensitive (similar to the default combobox filter):\n * ```ts\n * const filterByLabel = ((query, options) =>\n * query === ''\n * ? options\n * : options.filter((o) => o.label.toLowerCase().includes(query.toLowerCase()))\n * ) satisfies FilterFunction\n * ```\n */\nexport type FilterFunction<O extends Option = Option, G extends OptionGroup<O> = OptionGroup<O>> = (\n query: string,\n /* All of the (flattened) options, if there are groups this will be all of the options from all of the groups */\n options: O[],\n /* If there are no groups this array will be empty, if there are groups this will be all of the groups including their options */\n groups: G[],\n) => O[]\n\n/** Type guard to check if an option is a group */\nfunction isGroup(option: Option | OptionGroup | undefined): option is OptionGroup {\n return (option as OptionGroup | undefined)?.options !== undefined\n}\n\n/** Type guard to check if an array of options is an array of groups */\nexport function isGroups(options: Option[] | OptionGroup[]): options is OptionGroup[] {\n if (!options[0]) {\n return false // No options to test, so we assume it's not a group\n }\n return isGroup(options[0])\n}\n\n/** Available events emitted by the combobox */\nexport type ComboboxEmits = {\n /** Emitted when the \"Add a new option\" button is clicked */\n add: []\n}\n\n/** Available slots for the combobox */\nexport type ComboboxSlots<O extends Option = Option, G extends OptionGroup<O> = OptionGroup<O>> = {\n /** The reference element / trigger for the combobox */\n default(props: { open: boolean }): unknown\n /** A template to override the option label */\n option?(props: { option: O; active: boolean; selected: boolean }): unknown\n /** A template to override the group label */\n group?(props: { group: G }): unknown\n /** Creates an \"Add a new option\" button after the options list*/\n add?(props: { active: boolean }): unknown\n}\n"],"mappings":";;AAmCA,SAAS,QAAQ,QAAiE;CAChF,OAAQ,QAAoC,YAAY,KAAA;AAC1D;;AAGA,SAAgB,SAAS,SAA6D;CACpF,IAAI,CAAC,QAAQ,IACX,OAAO;CAET,OAAO,QAAQ,QAAQ,EAAE;AAC3B"}
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../../src/components/ScalarCombobox/types.ts"],"sourcesContent":["import type { LoadingState } from '../ScalarLoading'\n\nexport type Option = {\n id: string\n label: string\n}\n\nexport type OptionGroup<O extends Option = Option> = {\n label: string\n options: O[]\n}\n\nexport type OptionsOrGroups<O extends Option = Option, G extends OptionGroup<O> = OptionGroup<O>> = O[] | G[]\n\n/**\n * Filters which options appear while searching. The combobox passes the\n * current query, a flat list of every option (including those inside groups),\n * and the group objects when the list is grouped (otherwise an empty array).\n *\n * @example Match option labels, case-insensitive (similar to the default combobox filter):\n * ```ts\n * const filterByLabel = ((query, options) =>\n * query === ''\n * ? options\n * : options.filter((o) => o.label.toLowerCase().includes(query.toLowerCase()))\n * ) satisfies FilterFunction\n * ```\n */\nexport type FilterFunction<O extends Option = Option, G extends OptionGroup<O> = OptionGroup<O>> = (\n query: string,\n /* All of the (flattened) options, if there are groups this will be all of the options from all of the groups */\n options: O[],\n /* If there are no groups this array will be empty, if there are groups this will be all of the groups including their options */\n groups: G[],\n) => O[]\n\n/** Type guard to check if an option is a group */\nfunction isGroup(option: Option | OptionGroup | undefined): option is OptionGroup {\n return (option as OptionGroup | undefined)?.options !== undefined\n}\n\n/** Type guard to check if an array of options is an array of groups */\nexport function isGroups(options: Option[] | OptionGroup[]): options is OptionGroup[] {\n if (!options[0]) {\n return false // No options to test, so we assume it's not a group\n }\n return isGroup(options[0])\n}\n\n/** Available events emitted by the combobox */\nexport type ComboboxEmits = {\n /** Emitted when the \"Add a new option\" button is clicked */\n add: []\n}\n\n/** Available slots for the combobox */\nexport type ComboboxSlots<O extends Option = Option, G extends OptionGroup<O> = OptionGroup<O>> = {\n /** The reference element / trigger for the combobox */\n default(props: { open: boolean }): unknown\n /** A template to override the option label */\n option?(props: { option: O; active: boolean; selected: boolean }): unknown\n /** A template to override the group label */\n group?(props: { group: G }): unknown\n /** Creates an \"Add a new option\" button after the options list*/\n add?(props: { active: boolean }): unknown\n /** Content at the end of the search input, replaces the loading indicator */\n 'search-end'?(props: { loader: LoadingState | undefined }): unknown\n}\n"],"mappings":";;AAqCA,SAAS,QAAQ,QAAiE;CAChF,OAAQ,QAAoC,YAAY,KAAA;AAC1D;;AAGA,SAAgB,SAAS,SAA6D;CACpF,IAAI,CAAC,QAAQ,IACX,OAAO;CAET,OAAO,QAAQ,QAAQ,EAAE;AAC3B"}
@@ -1 +1 @@
1
- {"version":3,"file":"ScalarDropdown.vue.d.ts","sourceRoot":"","sources":["../../../src/components/ScalarDropdown/ScalarDropdown.vue"],"names":[],"mappings":"AA8OA,OAAO,EAAkB,KAAK,qBAAqB,EAAE,MAAM,mBAAmB,CAAA;AAK9E;;;;;;;;;;;GAWG;wBACkB,OAAO,YAAY;AAAxC,wBAAyC;AAGzC,QAAA,MAAM,YAAY;IAiLlB,0CAA0C;WACjC,OAAO;;;;IADhB,0CAA0C;WACjC,OAAO;;;;IA7Kd,kEAAkE;oBAClD;QACd,0CAA0C;QAC1C,IAAI,EAAE,OAAO,CAAA;KACd,GAAG,OAAO;IACX,iCAAiC;kBACnB;QACZ,0CAA0C;QAC1C,IAAI,EAAE,OAAO,CAAA;KACd,GAAG,OAAO;EAoST,CAAC;AACL,KAAK,eAAe,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG;IAChC,QAAO;QACN,MAAM,EAAE,CAAC,CAAC;KACV,CAAA;CACD,CAAC"}
1
+ {"version":3,"file":"ScalarDropdown.vue.d.ts","sourceRoot":"","sources":["../../../src/components/ScalarDropdown/ScalarDropdown.vue"],"names":[],"mappings":"AA+OA,OAAO,EAAkB,KAAK,qBAAqB,EAAE,MAAM,mBAAmB,CAAA;AAK9E;;;;;;;;;;;GAWG;wBACkB,OAAO,YAAY;AAAxC,wBAAyC;AAGzC,QAAA,MAAM,YAAY;IAkLlB,0CAA0C;WACjC,OAAO;;;;IADhB,0CAA0C;WACjC,OAAO;;;;IA9Kd,kEAAkE;oBAClD;QACd,0CAA0C;QAC1C,IAAI,EAAE,OAAO,CAAA;KACd,GAAG,OAAO;IACX,iCAAiC;kBACnB;QACZ,0CAA0C;QAC1C,IAAI,EAAE,OAAO,CAAA;KACd,GAAG,OAAO;EAqST,CAAC;AACL,KAAK,eAAe,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG;IAChC,QAAO;QACN,MAAM,EAAE,CAAC,CAAC;KACV,CAAA;CACD,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"ScalarDropdown.vue.js","names":[],"sources":["../../../src/components/ScalarDropdown/ScalarDropdown.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Scalar dropdown component\n *\n * @example\n * <ScalarDropdown>\n * <ScalarButton>Click Me</ScalarButton>\n * <template #items>\n * <ScalarDropdownItem>Item 1</ScalarDropdownItem>\n * <ScalarDropdownItem>Item 2</ScalarDropdownItem>\n * </template>\n * </ScalarDropdown>\n */\nexport default {}\n</script>\n<script setup lang=\"ts\">\nimport { useBindCx } from '@scalar/use-hooks/useBindCx'\nimport { onClickOutside } from '@vueuse/core'\nimport { computed, nextTick, ref, useId, watch } from 'vue'\n\nimport { ScalarFloating, type ScalarFloatingOptions } from '../ScalarFloating'\nimport ScalarDropdownMenu from './ScalarDropdownMenu.vue'\nimport { useDropdown } from './useDropdown'\n\ndefineProps<ScalarFloatingOptions>()\n\ndefineSlots<{\n /** The reference element for the element in the #floating slot */\n default?(props: {\n /** Whether or not the dropdown is open */\n open: boolean\n }): unknown\n /** The list of dropdown items */\n items?(props: {\n /** Whether or not the dropdown is open */\n open: boolean\n }): unknown\n}>()\n\ntype ScalarFloatingType = InstanceType<typeof ScalarFloating>\nconst floatingRef = ref<ScalarFloatingType>()\n\nconst menuRef = ref<HTMLUListElement>()\n\n/** Whether or not the dropdown is open */\nconst open = defineModel<boolean>('open', { default: false })\n\nconst { active } = useDropdown()\n\nconst fallbackTargetId = useId()\nconst targetId = ref<string>(fallbackTargetId)\nconst menuId = useId()\n\n/** Handle click events on the target */\nasync function handleTargetClick() {\n open.value = !open.value\n await nextTick()\n if (open.value) menuRef.value?.focus()\n}\n\n/** Handle keydown events on the target */\nasync function handleTargetKeydown(event: KeyboardEvent) {\n // Only handle the keys that are relevant to the dropdown\n if (['ArrowDown', 'ArrowUp', ' ', 'Enter'].includes(event.key))\n event.preventDefault()\n else return\n\n // Open the dropdown if it's not open\n if (!open.value) open.value = true\n\n // Move the active item if the key is an arrow key or space or enter\n await nextTick()\n\n // Focus the menu\n menuRef.value?.focus()\n\n // Move the active item if the key is an arrow key or space or enter\n if (['ArrowDown', ' ', 'Enter'].includes(event.key)) moveActive(1)\n else if (event.key === 'ArrowUp') moveActive(-1)\n}\n\n/** Watch the target reference for changes */\nwatch(\n () => floatingRef.value?.targetRef,\n (newTarget, oldTarget) => {\n if (newTarget) {\n // Create an id for the target if it doesn't have one\n if (newTarget.id) targetId.value = newTarget.id\n else {\n targetId.value = fallbackTargetId\n newTarget.setAttribute('id', targetId.value)\n }\n // Set the aria attributes\n newTarget.setAttribute('aria-haspopup', 'menu')\n newTarget.setAttribute('aria-expanded', `${open.value}`)\n if (open.value) newTarget.setAttribute('aria-controls', menuId)\n // Add the event listeners\n newTarget.addEventListener('click', handleTargetClick)\n newTarget.addEventListener('keydown', handleTargetKeydown)\n }\n if (oldTarget && oldTarget !== newTarget) {\n // Remove the id if it's using the fallback id\n if (oldTarget.id === fallbackTargetId) oldTarget.removeAttribute('id')\n // Remove the aria attributes\n oldTarget.removeAttribute('aria-controls')\n oldTarget.removeAttribute('aria-haspopup')\n oldTarget.removeAttribute('aria-expanded')\n // Remove the event listeners\n oldTarget.removeEventListener('click', handleTargetClick)\n oldTarget.removeEventListener('keydown', handleTargetKeydown)\n }\n },\n { immediate: true },\n)\n\n/** Watch the open state for changes */\nwatch(\n open,\n (o) => {\n const target = floatingRef.value?.targetRef\n if (!target) return\n // Update the aria-expanded attribute on the target\n target.setAttribute('aria-expanded', `${o}`)\n if (o) target.setAttribute('aria-controls', menuId)\n else target.removeAttribute('aria-controls')\n },\n { immediate: true },\n)\n\nasync function handleClose() {\n // Move focus back to the target\n floatingRef.value?.targetRef?.focus()\n // Close the dropdown\n open.value = false\n}\n\nfunction handleSelected() {\n if (!active.value || !menuRef.value) return\n\n const button = menuRef.value.querySelector<HTMLElement>(\n `#${active.value}[role=\"menuitem\"]:not([aria-disabled=\"true\"])`,\n )\n\n if (!button) return\n\n button.click()\n\n handleClose()\n}\n\n/** Move the active item in the dropdown */\nfunction moveActive(dir: 1 | -1) {\n if (!open.value || !menuRef.value) return\n\n // Get all menu items, filtering out disabled ones\n const list = Array.from(\n menuRef.value.querySelectorAll<HTMLElement>(\n '[role=\"menuitem\"]:not([aria-disabled=\"true\"])',\n ),\n )\n\n if (list.length === 0) return\n\n // Find the current active item's index\n const activeIdx = list.findIndex((item) => item.id === active.value)\n\n if (activeIdx === -1) {\n // If there is no active item, activate the first or last item\n const targetItem = list[dir > 0 ? 0 : list.length - 1]\n if (targetItem?.id) active.value = targetItem.id\n return\n }\n\n // Calculate next index and exit if it's out of bounds\n const nextIdx = activeIdx + dir\n\n if (nextIdx < 0 || nextIdx > list.length - 1) {\n return\n }\n\n const nextItem = list[nextIdx]\n\n if (nextItem?.id) active.value = nextItem.id\n}\n\n/** Close the dropdown if the user clicks outside of the menu and button */\nonClickOutside(menuRef, handleClose, {\n ignore: [computed(() => floatingRef.value?.targetRef)],\n})\n\ndefineOptions({ inheritAttrs: false })\nconst { cx } = useBindCx()\n</script>\n<template>\n <ScalarFloating\n ref=\"floatingRef\"\n v-bind=\"$props\"\n :placement=\"placement ?? 'bottom-start'\">\n <slot :open />\n <template\n v-if=\"open\"\n #floating=\"{ width }\">\n <!-- Background container -->\n <ScalarDropdownMenu\n :style=\"{ width }\"\n v-bind=\"cx('max-h-[inherit] max-w-[inherit]')\">\n <template #menu>\n <div\n :id=\"menuId\"\n ref=\"menuRef\"\n :aria-activedescendant=\"active\"\n :aria-labelledby=\"targetId\"\n class=\"flex flex-col p-0.75 outline-none\"\n role=\"menu\"\n tabindex=\"-1\"\n @click.stop=\"handleClose\"\n @keydown.down.prevent.stop=\"moveActive(1)\"\n @keydown.enter.prevent.stop=\"handleSelected\"\n @keydown.escape.prevent.stop=\"handleClose\"\n @keydown.space.prevent.stop=\"handleSelected\"\n @keydown.tab.prevent.stop=\"handleClose\"\n @keydown.up.prevent.stop=\"moveActive(-1)\">\n <slot\n name=\"items\"\n :open />\n </div>\n </template>\n </ScalarDropdownMenu>\n </template>\n </ScalarFloating>\n</template>\n"],"mappings":""}
1
+ {"version":3,"file":"ScalarDropdown.vue.js","names":[],"sources":["../../../src/components/ScalarDropdown/ScalarDropdown.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Scalar dropdown component\n *\n * @example\n * <ScalarDropdown>\n * <ScalarButton>Click Me</ScalarButton>\n * <template #items>\n * <ScalarDropdownItem>Item 1</ScalarDropdownItem>\n * <ScalarDropdownItem>Item 2</ScalarDropdownItem>\n * </template>\n * </ScalarDropdown>\n */\nexport default {}\n</script>\n<script setup lang=\"ts\">\nimport { useBindCx } from '@scalar/use-hooks/useBindCx'\nimport { onClickOutside } from '@vueuse/core'\nimport { computed, nextTick, ref, useId, watch } from 'vue'\n\nimport { ScalarFloating, type ScalarFloatingOptions } from '../ScalarFloating'\nimport ScalarDropdownMenu from './ScalarDropdownMenu.vue'\nimport { useDropdown } from './useDropdown'\n\ndefineProps<ScalarFloatingOptions>()\n\ndefineSlots<{\n /** The reference element for the element in the #floating slot */\n default?(props: {\n /** Whether or not the dropdown is open */\n open: boolean\n }): unknown\n /** The list of dropdown items */\n items?(props: {\n /** Whether or not the dropdown is open */\n open: boolean\n }): unknown\n}>()\n\ntype ScalarFloatingType = InstanceType<typeof ScalarFloating>\nconst floatingRef = ref<ScalarFloatingType>()\n\nconst menuRef = ref<HTMLUListElement>()\n\n/** Whether or not the dropdown is open */\nconst open = defineModel<boolean>('open', { default: false })\n\nconst { active } = useDropdown()\n\nconst fallbackTargetId = useId()\nconst targetId = ref<string>(fallbackTargetId)\nconst menuId = useId()\n\n/** Handle click events on the target */\nasync function handleTargetClick() {\n open.value = !open.value\n await nextTick()\n // Floating UI has not positioned the menu yet, so let focus land without scrolling to it\n if (open.value) menuRef.value?.focus({ preventScroll: true })\n}\n\n/** Handle keydown events on the target */\nasync function handleTargetKeydown(event: KeyboardEvent) {\n // Only handle the keys that are relevant to the dropdown\n if (['ArrowDown', 'ArrowUp', ' ', 'Enter'].includes(event.key))\n event.preventDefault()\n else return\n\n // Open the dropdown if it's not open\n if (!open.value) open.value = true\n\n // Move the active item if the key is an arrow key or space or enter\n await nextTick()\n\n // Focus the menu, without scrolling to it before Floating UI has positioned it\n menuRef.value?.focus({ preventScroll: true })\n\n // Move the active item if the key is an arrow key or space or enter\n if (['ArrowDown', ' ', 'Enter'].includes(event.key)) moveActive(1)\n else if (event.key === 'ArrowUp') moveActive(-1)\n}\n\n/** Watch the target reference for changes */\nwatch(\n () => floatingRef.value?.targetRef,\n (newTarget, oldTarget) => {\n if (newTarget) {\n // Create an id for the target if it doesn't have one\n if (newTarget.id) targetId.value = newTarget.id\n else {\n targetId.value = fallbackTargetId\n newTarget.setAttribute('id', targetId.value)\n }\n // Set the aria attributes\n newTarget.setAttribute('aria-haspopup', 'menu')\n newTarget.setAttribute('aria-expanded', `${open.value}`)\n if (open.value) newTarget.setAttribute('aria-controls', menuId)\n // Add the event listeners\n newTarget.addEventListener('click', handleTargetClick)\n newTarget.addEventListener('keydown', handleTargetKeydown)\n }\n if (oldTarget && oldTarget !== newTarget) {\n // Remove the id if it's using the fallback id\n if (oldTarget.id === fallbackTargetId) oldTarget.removeAttribute('id')\n // Remove the aria attributes\n oldTarget.removeAttribute('aria-controls')\n oldTarget.removeAttribute('aria-haspopup')\n oldTarget.removeAttribute('aria-expanded')\n // Remove the event listeners\n oldTarget.removeEventListener('click', handleTargetClick)\n oldTarget.removeEventListener('keydown', handleTargetKeydown)\n }\n },\n { immediate: true },\n)\n\n/** Watch the open state for changes */\nwatch(\n open,\n (o) => {\n const target = floatingRef.value?.targetRef\n if (!target) return\n // Update the aria-expanded attribute on the target\n target.setAttribute('aria-expanded', `${o}`)\n if (o) target.setAttribute('aria-controls', menuId)\n else target.removeAttribute('aria-controls')\n },\n { immediate: true },\n)\n\nasync function handleClose() {\n // Move focus back to the target\n floatingRef.value?.targetRef?.focus()\n // Close the dropdown\n open.value = false\n}\n\nfunction handleSelected() {\n if (!active.value || !menuRef.value) return\n\n const button = menuRef.value.querySelector<HTMLElement>(\n `#${active.value}[role=\"menuitem\"]:not([aria-disabled=\"true\"])`,\n )\n\n if (!button) return\n\n button.click()\n\n handleClose()\n}\n\n/** Move the active item in the dropdown */\nfunction moveActive(dir: 1 | -1) {\n if (!open.value || !menuRef.value) return\n\n // Get all menu items, filtering out disabled ones\n const list = Array.from(\n menuRef.value.querySelectorAll<HTMLElement>(\n '[role=\"menuitem\"]:not([aria-disabled=\"true\"])',\n ),\n )\n\n if (list.length === 0) return\n\n // Find the current active item's index\n const activeIdx = list.findIndex((item) => item.id === active.value)\n\n if (activeIdx === -1) {\n // If there is no active item, activate the first or last item\n const targetItem = list[dir > 0 ? 0 : list.length - 1]\n if (targetItem?.id) active.value = targetItem.id\n return\n }\n\n // Calculate next index and exit if it's out of bounds\n const nextIdx = activeIdx + dir\n\n if (nextIdx < 0 || nextIdx > list.length - 1) {\n return\n }\n\n const nextItem = list[nextIdx]\n\n if (nextItem?.id) active.value = nextItem.id\n}\n\n/** Close the dropdown if the user clicks outside of the menu and button */\nonClickOutside(menuRef, handleClose, {\n ignore: [computed(() => floatingRef.value?.targetRef)],\n})\n\ndefineOptions({ inheritAttrs: false })\nconst { cx } = useBindCx()\n</script>\n<template>\n <ScalarFloating\n ref=\"floatingRef\"\n v-bind=\"$props\"\n :placement=\"placement ?? 'bottom-start'\">\n <slot :open />\n <template\n v-if=\"open\"\n #floating=\"{ width }\">\n <!-- Background container -->\n <ScalarDropdownMenu\n :style=\"{ width }\"\n v-bind=\"cx('max-h-[inherit] max-w-[inherit]')\">\n <template #menu>\n <div\n :id=\"menuId\"\n ref=\"menuRef\"\n :aria-activedescendant=\"active\"\n :aria-labelledby=\"targetId\"\n class=\"flex flex-col p-0.75 outline-none\"\n role=\"menu\"\n tabindex=\"-1\"\n @click.stop=\"handleClose\"\n @keydown.down.prevent.stop=\"moveActive(1)\"\n @keydown.enter.prevent.stop=\"handleSelected\"\n @keydown.escape.prevent.stop=\"handleClose\"\n @keydown.space.prevent.stop=\"handleSelected\"\n @keydown.tab.prevent.stop=\"handleClose\"\n @keydown.up.prevent.stop=\"moveActive(-1)\">\n <slot\n name=\"items\"\n :open />\n </div>\n </template>\n </ScalarDropdownMenu>\n </template>\n </ScalarFloating>\n</template>\n"],"mappings":""}
@@ -46,7 +46,7 @@ var ScalarDropdown_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ d
46
46
  async function handleTargetClick() {
47
47
  open.value = !open.value;
48
48
  await nextTick();
49
- if (open.value) menuRef.value?.focus();
49
+ if (open.value) menuRef.value?.focus({ preventScroll: true });
50
50
  }
51
51
  /** Handle keydown events on the target */
52
52
  async function handleTargetKeydown(event) {
@@ -59,7 +59,7 @@ var ScalarDropdown_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ d
59
59
  else return;
60
60
  if (!open.value) open.value = true;
61
61
  await nextTick();
62
- menuRef.value?.focus();
62
+ menuRef.value?.focus({ preventScroll: true });
63
63
  if ([
64
64
  "ArrowDown",
65
65
  " ",
@@ -1 +1 @@
1
- {"version":3,"file":"ScalarDropdown.vue.script.js","names":["$props"],"sources":["../../../src/components/ScalarDropdown/ScalarDropdown.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Scalar dropdown component\n *\n * @example\n * <ScalarDropdown>\n * <ScalarButton>Click Me</ScalarButton>\n * <template #items>\n * <ScalarDropdownItem>Item 1</ScalarDropdownItem>\n * <ScalarDropdownItem>Item 2</ScalarDropdownItem>\n * </template>\n * </ScalarDropdown>\n */\nexport default {}\n</script>\n<script setup lang=\"ts\">\nimport { useBindCx } from '@scalar/use-hooks/useBindCx'\nimport { onClickOutside } from '@vueuse/core'\nimport { computed, nextTick, ref, useId, watch } from 'vue'\n\nimport { ScalarFloating, type ScalarFloatingOptions } from '../ScalarFloating'\nimport ScalarDropdownMenu from './ScalarDropdownMenu.vue'\nimport { useDropdown } from './useDropdown'\n\ndefineProps<ScalarFloatingOptions>()\n\ndefineSlots<{\n /** The reference element for the element in the #floating slot */\n default?(props: {\n /** Whether or not the dropdown is open */\n open: boolean\n }): unknown\n /** The list of dropdown items */\n items?(props: {\n /** Whether or not the dropdown is open */\n open: boolean\n }): unknown\n}>()\n\ntype ScalarFloatingType = InstanceType<typeof ScalarFloating>\nconst floatingRef = ref<ScalarFloatingType>()\n\nconst menuRef = ref<HTMLUListElement>()\n\n/** Whether or not the dropdown is open */\nconst open = defineModel<boolean>('open', { default: false })\n\nconst { active } = useDropdown()\n\nconst fallbackTargetId = useId()\nconst targetId = ref<string>(fallbackTargetId)\nconst menuId = useId()\n\n/** Handle click events on the target */\nasync function handleTargetClick() {\n open.value = !open.value\n await nextTick()\n if (open.value) menuRef.value?.focus()\n}\n\n/** Handle keydown events on the target */\nasync function handleTargetKeydown(event: KeyboardEvent) {\n // Only handle the keys that are relevant to the dropdown\n if (['ArrowDown', 'ArrowUp', ' ', 'Enter'].includes(event.key))\n event.preventDefault()\n else return\n\n // Open the dropdown if it's not open\n if (!open.value) open.value = true\n\n // Move the active item if the key is an arrow key or space or enter\n await nextTick()\n\n // Focus the menu\n menuRef.value?.focus()\n\n // Move the active item if the key is an arrow key or space or enter\n if (['ArrowDown', ' ', 'Enter'].includes(event.key)) moveActive(1)\n else if (event.key === 'ArrowUp') moveActive(-1)\n}\n\n/** Watch the target reference for changes */\nwatch(\n () => floatingRef.value?.targetRef,\n (newTarget, oldTarget) => {\n if (newTarget) {\n // Create an id for the target if it doesn't have one\n if (newTarget.id) targetId.value = newTarget.id\n else {\n targetId.value = fallbackTargetId\n newTarget.setAttribute('id', targetId.value)\n }\n // Set the aria attributes\n newTarget.setAttribute('aria-haspopup', 'menu')\n newTarget.setAttribute('aria-expanded', `${open.value}`)\n if (open.value) newTarget.setAttribute('aria-controls', menuId)\n // Add the event listeners\n newTarget.addEventListener('click', handleTargetClick)\n newTarget.addEventListener('keydown', handleTargetKeydown)\n }\n if (oldTarget && oldTarget !== newTarget) {\n // Remove the id if it's using the fallback id\n if (oldTarget.id === fallbackTargetId) oldTarget.removeAttribute('id')\n // Remove the aria attributes\n oldTarget.removeAttribute('aria-controls')\n oldTarget.removeAttribute('aria-haspopup')\n oldTarget.removeAttribute('aria-expanded')\n // Remove the event listeners\n oldTarget.removeEventListener('click', handleTargetClick)\n oldTarget.removeEventListener('keydown', handleTargetKeydown)\n }\n },\n { immediate: true },\n)\n\n/** Watch the open state for changes */\nwatch(\n open,\n (o) => {\n const target = floatingRef.value?.targetRef\n if (!target) return\n // Update the aria-expanded attribute on the target\n target.setAttribute('aria-expanded', `${o}`)\n if (o) target.setAttribute('aria-controls', menuId)\n else target.removeAttribute('aria-controls')\n },\n { immediate: true },\n)\n\nasync function handleClose() {\n // Move focus back to the target\n floatingRef.value?.targetRef?.focus()\n // Close the dropdown\n open.value = false\n}\n\nfunction handleSelected() {\n if (!active.value || !menuRef.value) return\n\n const button = menuRef.value.querySelector<HTMLElement>(\n `#${active.value}[role=\"menuitem\"]:not([aria-disabled=\"true\"])`,\n )\n\n if (!button) return\n\n button.click()\n\n handleClose()\n}\n\n/** Move the active item in the dropdown */\nfunction moveActive(dir: 1 | -1) {\n if (!open.value || !menuRef.value) return\n\n // Get all menu items, filtering out disabled ones\n const list = Array.from(\n menuRef.value.querySelectorAll<HTMLElement>(\n '[role=\"menuitem\"]:not([aria-disabled=\"true\"])',\n ),\n )\n\n if (list.length === 0) return\n\n // Find the current active item's index\n const activeIdx = list.findIndex((item) => item.id === active.value)\n\n if (activeIdx === -1) {\n // If there is no active item, activate the first or last item\n const targetItem = list[dir > 0 ? 0 : list.length - 1]\n if (targetItem?.id) active.value = targetItem.id\n return\n }\n\n // Calculate next index and exit if it's out of bounds\n const nextIdx = activeIdx + dir\n\n if (nextIdx < 0 || nextIdx > list.length - 1) {\n return\n }\n\n const nextItem = list[nextIdx]\n\n if (nextItem?.id) active.value = nextItem.id\n}\n\n/** Close the dropdown if the user clicks outside of the menu and button */\nonClickOutside(menuRef, handleClose, {\n ignore: [computed(() => floatingRef.value?.targetRef)],\n})\n\ndefineOptions({ inheritAttrs: false })\nconst { cx } = useBindCx()\n</script>\n<template>\n <ScalarFloating\n ref=\"floatingRef\"\n v-bind=\"$props\"\n :placement=\"placement ?? 'bottom-start'\">\n <slot :open />\n <template\n v-if=\"open\"\n #floating=\"{ width }\">\n <!-- Background container -->\n <ScalarDropdownMenu\n :style=\"{ width }\"\n v-bind=\"cx('max-h-[inherit] max-w-[inherit]')\">\n <template #menu>\n <div\n :id=\"menuId\"\n ref=\"menuRef\"\n :aria-activedescendant=\"active\"\n :aria-labelledby=\"targetId\"\n class=\"flex flex-col p-0.75 outline-none\"\n role=\"menu\"\n tabindex=\"-1\"\n @click.stop=\"handleClose\"\n @keydown.down.prevent.stop=\"moveActive(1)\"\n @keydown.enter.prevent.stop=\"handleSelected\"\n @keydown.escape.prevent.stop=\"handleClose\"\n @keydown.space.prevent.stop=\"handleSelected\"\n @keydown.tab.prevent.stop=\"handleClose\"\n @keydown.up.prevent.stop=\"moveActive(-1)\">\n <slot\n name=\"items\"\n :open />\n </div>\n </template>\n </ScalarDropdownMenu>\n </template>\n </ScalarFloating>\n</template>\n"],"mappings":"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"}
1
+ {"version":3,"file":"ScalarDropdown.vue.script.js","names":["$props"],"sources":["../../../src/components/ScalarDropdown/ScalarDropdown.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Scalar dropdown component\n *\n * @example\n * <ScalarDropdown>\n * <ScalarButton>Click Me</ScalarButton>\n * <template #items>\n * <ScalarDropdownItem>Item 1</ScalarDropdownItem>\n * <ScalarDropdownItem>Item 2</ScalarDropdownItem>\n * </template>\n * </ScalarDropdown>\n */\nexport default {}\n</script>\n<script setup lang=\"ts\">\nimport { useBindCx } from '@scalar/use-hooks/useBindCx'\nimport { onClickOutside } from '@vueuse/core'\nimport { computed, nextTick, ref, useId, watch } from 'vue'\n\nimport { ScalarFloating, type ScalarFloatingOptions } from '../ScalarFloating'\nimport ScalarDropdownMenu from './ScalarDropdownMenu.vue'\nimport { useDropdown } from './useDropdown'\n\ndefineProps<ScalarFloatingOptions>()\n\ndefineSlots<{\n /** The reference element for the element in the #floating slot */\n default?(props: {\n /** Whether or not the dropdown is open */\n open: boolean\n }): unknown\n /** The list of dropdown items */\n items?(props: {\n /** Whether or not the dropdown is open */\n open: boolean\n }): unknown\n}>()\n\ntype ScalarFloatingType = InstanceType<typeof ScalarFloating>\nconst floatingRef = ref<ScalarFloatingType>()\n\nconst menuRef = ref<HTMLUListElement>()\n\n/** Whether or not the dropdown is open */\nconst open = defineModel<boolean>('open', { default: false })\n\nconst { active } = useDropdown()\n\nconst fallbackTargetId = useId()\nconst targetId = ref<string>(fallbackTargetId)\nconst menuId = useId()\n\n/** Handle click events on the target */\nasync function handleTargetClick() {\n open.value = !open.value\n await nextTick()\n // Floating UI has not positioned the menu yet, so let focus land without scrolling to it\n if (open.value) menuRef.value?.focus({ preventScroll: true })\n}\n\n/** Handle keydown events on the target */\nasync function handleTargetKeydown(event: KeyboardEvent) {\n // Only handle the keys that are relevant to the dropdown\n if (['ArrowDown', 'ArrowUp', ' ', 'Enter'].includes(event.key))\n event.preventDefault()\n else return\n\n // Open the dropdown if it's not open\n if (!open.value) open.value = true\n\n // Move the active item if the key is an arrow key or space or enter\n await nextTick()\n\n // Focus the menu, without scrolling to it before Floating UI has positioned it\n menuRef.value?.focus({ preventScroll: true })\n\n // Move the active item if the key is an arrow key or space or enter\n if (['ArrowDown', ' ', 'Enter'].includes(event.key)) moveActive(1)\n else if (event.key === 'ArrowUp') moveActive(-1)\n}\n\n/** Watch the target reference for changes */\nwatch(\n () => floatingRef.value?.targetRef,\n (newTarget, oldTarget) => {\n if (newTarget) {\n // Create an id for the target if it doesn't have one\n if (newTarget.id) targetId.value = newTarget.id\n else {\n targetId.value = fallbackTargetId\n newTarget.setAttribute('id', targetId.value)\n }\n // Set the aria attributes\n newTarget.setAttribute('aria-haspopup', 'menu')\n newTarget.setAttribute('aria-expanded', `${open.value}`)\n if (open.value) newTarget.setAttribute('aria-controls', menuId)\n // Add the event listeners\n newTarget.addEventListener('click', handleTargetClick)\n newTarget.addEventListener('keydown', handleTargetKeydown)\n }\n if (oldTarget && oldTarget !== newTarget) {\n // Remove the id if it's using the fallback id\n if (oldTarget.id === fallbackTargetId) oldTarget.removeAttribute('id')\n // Remove the aria attributes\n oldTarget.removeAttribute('aria-controls')\n oldTarget.removeAttribute('aria-haspopup')\n oldTarget.removeAttribute('aria-expanded')\n // Remove the event listeners\n oldTarget.removeEventListener('click', handleTargetClick)\n oldTarget.removeEventListener('keydown', handleTargetKeydown)\n }\n },\n { immediate: true },\n)\n\n/** Watch the open state for changes */\nwatch(\n open,\n (o) => {\n const target = floatingRef.value?.targetRef\n if (!target) return\n // Update the aria-expanded attribute on the target\n target.setAttribute('aria-expanded', `${o}`)\n if (o) target.setAttribute('aria-controls', menuId)\n else target.removeAttribute('aria-controls')\n },\n { immediate: true },\n)\n\nasync function handleClose() {\n // Move focus back to the target\n floatingRef.value?.targetRef?.focus()\n // Close the dropdown\n open.value = false\n}\n\nfunction handleSelected() {\n if (!active.value || !menuRef.value) return\n\n const button = menuRef.value.querySelector<HTMLElement>(\n `#${active.value}[role=\"menuitem\"]:not([aria-disabled=\"true\"])`,\n )\n\n if (!button) return\n\n button.click()\n\n handleClose()\n}\n\n/** Move the active item in the dropdown */\nfunction moveActive(dir: 1 | -1) {\n if (!open.value || !menuRef.value) return\n\n // Get all menu items, filtering out disabled ones\n const list = Array.from(\n menuRef.value.querySelectorAll<HTMLElement>(\n '[role=\"menuitem\"]:not([aria-disabled=\"true\"])',\n ),\n )\n\n if (list.length === 0) return\n\n // Find the current active item's index\n const activeIdx = list.findIndex((item) => item.id === active.value)\n\n if (activeIdx === -1) {\n // If there is no active item, activate the first or last item\n const targetItem = list[dir > 0 ? 0 : list.length - 1]\n if (targetItem?.id) active.value = targetItem.id\n return\n }\n\n // Calculate next index and exit if it's out of bounds\n const nextIdx = activeIdx + dir\n\n if (nextIdx < 0 || nextIdx > list.length - 1) {\n return\n }\n\n const nextItem = list[nextIdx]\n\n if (nextItem?.id) active.value = nextItem.id\n}\n\n/** Close the dropdown if the user clicks outside of the menu and button */\nonClickOutside(menuRef, handleClose, {\n ignore: [computed(() => floatingRef.value?.targetRef)],\n})\n\ndefineOptions({ inheritAttrs: false })\nconst { cx } = useBindCx()\n</script>\n<template>\n <ScalarFloating\n ref=\"floatingRef\"\n v-bind=\"$props\"\n :placement=\"placement ?? 'bottom-start'\">\n <slot :open />\n <template\n v-if=\"open\"\n #floating=\"{ width }\">\n <!-- Background container -->\n <ScalarDropdownMenu\n :style=\"{ width }\"\n v-bind=\"cx('max-h-[inherit] max-w-[inherit]')\">\n <template #menu>\n <div\n :id=\"menuId\"\n ref=\"menuRef\"\n :aria-activedescendant=\"active\"\n :aria-labelledby=\"targetId\"\n class=\"flex flex-col p-0.75 outline-none\"\n role=\"menu\"\n tabindex=\"-1\"\n @click.stop=\"handleClose\"\n @keydown.down.prevent.stop=\"moveActive(1)\"\n @keydown.enter.prevent.stop=\"handleSelected\"\n @keydown.escape.prevent.stop=\"handleClose\"\n @keydown.space.prevent.stop=\"handleSelected\"\n @keydown.tab.prevent.stop=\"handleClose\"\n @keydown.up.prevent.stop=\"moveActive(-1)\">\n <slot\n name=\"items\"\n :open />\n </div>\n </template>\n </ScalarDropdownMenu>\n </template>\n </ScalarFloating>\n</template>\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ScalarMarkdown.vue.d.ts","sourceRoot":"","sources":["../../../src/components/ScalarMarkdown/ScalarMarkdown.vue"],"names":[],"mappings":"AAgrBA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAGlD;;;;;;;;GAQG;wBACkB,OAAO,YAAY;AAAxC,wBAAyC;AAGzC,QAAA,MAAM,YAAY;;4PA8Fd,CAAC"}
1
+ {"version":3,"file":"ScalarMarkdown.vue.d.ts","sourceRoot":"","sources":["../../../src/components/ScalarMarkdown/ScalarMarkdown.vue"],"names":[],"mappings":"AAsrBA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAIlD;;;;;;;;GAQG;wBACkB,OAAO,YAAY;AAAxC,wBAAyC;AAGzC,QAAA,MAAM,YAAY;;4PAmGd,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"ScalarMarkdown.vue.js","names":[],"sources":["../../../src/components/ScalarMarkdown/ScalarMarkdown.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Scalar Markdown component\n *\n * Renders a markdown string as styled HTML with support for\n * headings, links, code blocks, tables, alerts, and more.\n *\n * @example\n * <ScalarMarkdown value=\"# Hello World\" />\n */\nexport default {}\n</script>\n\n<script setup lang=\"ts\">\nimport {\n type Node,\n htmlFromMarkdown,\n isHeading,\n textFromNode,\n} from '@scalar/code-highlight'\nimport { useBindCx } from '@scalar/use-hooks/useBindCx'\nimport { computed, useTemplateRef } from 'vue'\n\nimport type { ScalarMarkdownProps } from './types'\n\nconst {\n value,\n transform,\n transformType,\n withImages = false,\n withAnchors = false,\n anchorPrefix,\n} = defineProps<ScalarMarkdownProps>()\n\nconst { cx } = useBindCx()\ndefineOptions({ inheritAttrs: false })\n\n// Expose the element ref to the parent component\nconst templateRef = useTemplateRef('div')\ndefineExpose({ el: templateRef })\n\nconst transformHeading = (node: Node) => {\n if (!withAnchors) {\n return transform?.(node) || node\n }\n\n if (!isHeading(node)) {\n return node\n }\n\n const headingText = textFromNode(node)\n\n /** Basic slugify for the heading text */\n const slug = headingText.toLowerCase().replace(/\\s+/g, '-')\n\n /** Return adding anchor prefix if present or just the slug */\n const id = anchorPrefix ? `${anchorPrefix}/description/${slug}` : slug\n\n // Add the id to the heading\n node.data = {\n hProperties: {\n id,\n },\n }\n\n if (transform) {\n return transform(node)\n }\n\n return node\n}\n\nconst html = computed(() => {\n return htmlFromMarkdown(value ?? '', {\n removeTags: withImages ? [] : ['img', 'picture'],\n transform:\n withAnchors && transformType === 'heading' ? transformHeading : transform,\n transformType,\n })\n})\n</script>\n<template>\n <div\n ref=\"div\"\n v-bind=\"cx('markdown', { 'line-clamp-(--markdown-clamp)': !!clamp })\"\n :style=\"{ '--markdown-clamp': clamp }\"\n v-html=\"html\" />\n</template>\n<style>\n@import '@scalar/code-highlight/css/code.css';\n\n.scalar-app {\n /* Base container and variables */\n .markdown {\n --scalar-refs-heading-spacing: 24px;\n --markdown-border: var(--scalar-border-width) solid\n var(--scalar-border-color);\n --markdown-spacing-sm: 12px;\n --markdown-spacing-md: 16px;\n --markdown-line-height: 1.625;\n --markdown-heading-line-height: 1.15;\n\n font-family: var(--scalar-font);\n word-break: break-word;\n line-height: var(--markdown-line-height);\n }\n\n .markdown > * {\n margin-bottom: var(--markdown-spacing-md);\n }\n\n /* margin gets doubled between heading + non heading elements if we don't do this*/\n .markdown > *:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6):last-child {\n margin-bottom: 0;\n }\n /* Headings */\n .markdown h1,\n .markdown h2,\n .markdown h3,\n .markdown h4,\n .markdown h5,\n .markdown h6 {\n display: block;\n font-weight: var(--scalar-bold);\n margin-top: var(--scalar-refs-heading-spacing);\n margin-bottom: var(--markdown-spacing-sm);\n line-height: var(--markdown-heading-line-height, 1.15);\n scroll-margin-top: 1rem;\n }\n .markdown h1 {\n font-size: 1.5rem;\n }\n .markdown h2,\n .markdown h3 {\n font-size: 1.25rem;\n }\n .markdown h4,\n .markdown h5,\n .markdown h6 {\n font-size: 1rem;\n }\n\n /* Text formatting and paragraphs */\n .markdown b,\n .markdown strong {\n font-weight: var(--scalar-bold);\n }\n\n .markdown p {\n color: inherit;\n display: block;\n line-height: var(--markdown-line-height);\n }\n\n /* Images */\n .markdown img {\n /**\n * With `display: block` <a><img></a> will take the whole width.\n *\n * @see https://github.com/scalar/scalar/issues/3961\n */\n display: inline-block;\n overflow: hidden;\n border-radius: var(--scalar-radius-md);\n max-width: 100%;\n }\n\n /* Lists */\n .markdown ul,\n .markdown ol {\n display: flex;\n flex-direction: column;\n padding-left: 1.6em;\n gap: 2px;\n line-height: var(--markdown-line-height);\n }\n\n .markdown li {\n margin-top: 2px;\n padding-left: 7px;\n }\n\n ol > li::marker,\n ol > * > li::marker {\n font: var(--scalar-font);\n font-variant-numeric: tabular-nums;\n font-weight: var(--scalar-semibold);\n white-space: nowrap;\n }\n\n /* Level 1, 4, 7 → decimal */\n .markdown ol {\n list-style-type: decimal;\n }\n /* Level 2, 5, 8 → lower-alpha */\n .markdown ol ol {\n list-style-type: lower-alpha;\n }\n\n .markdown ol ol ol ol,\n .markdown ol ol ol ol ol ol ol {\n list-style-type: decimal;\n }\n\n .markdown ol ol ol ol ol,\n .markdown ol ol ol ol ol ol ol ol {\n list-style-type: lower-alpha;\n }\n\n /* Level 3, 6, 9 → lower-roman */\n .markdown ol ol ol {\n list-style-type: lower-roman;\n }\n\n .markdown ol ol ol ol ol ol,\n .markdown ol ol ol ol ol ol ol ol ol {\n list-style-type: lower-roman;\n }\n\n .markdown ul > li,\n .markdown ul > * > li {\n list-style-type: disc;\n }\n\n /* Tables */\n .markdown table {\n display: table;\n table-layout: fixed;\n overflow-x: auto;\n position: relative;\n width: max-content;\n max-width: 100%;\n margin: 1em 0;\n border: var(--scalar-border-width) solid var(--scalar-border-color);\n border-radius: var(--scalar-radius-md);\n border-spacing: 0;\n }\n\n .markdown tbody,\n .markdown thead {\n vertical-align: middle;\n }\n\n .markdown tbody {\n display: table-row-group;\n }\n\n .markdown thead {\n display: table-header-group;\n }\n\n .markdown tr {\n display: table-row;\n border-color: inherit;\n vertical-align: inherit;\n }\n\n .markdown td,\n .markdown th {\n display: table-cell;\n vertical-align: top;\n min-width: 1em;\n padding: 8.5px 16px;\n line-height: var(--markdown-line-height);\n position: relative;\n /** Break long words in tables */\n word-break: break-word;\n font-size: var(--scalar-small);\n color: var(--scalar-color-1);\n border-right: var(--markdown-border);\n border-bottom: var(--markdown-border);\n }\n\n .markdown td > *,\n .markdown th > * {\n margin-bottom: 0;\n }\n\n .markdown th:empty {\n display: none;\n }\n\n .markdown td:first-of-type,\n .markdown th:first-of-type {\n border-left: none;\n }\n\n .markdown td:last-of-type,\n .markdown th:last-of-type {\n border-right: none;\n }\n\n .markdown tr:last-of-type td {\n border-bottom: none;\n }\n\n .markdown th {\n font-weight: var(--scalar-bold);\n text-align: left;\n border-left-color: transparent;\n background: var(--scalar-background-2);\n }\n\n .markdown th:first-of-type {\n border-top-left-radius: var(--scalar-radius-md);\n }\n\n .markdown th:last-of-type {\n border-top-right-radius: var(--scalar-radius-md);\n }\n\n .markdown tr > [align='left'] {\n text-align: left;\n }\n\n .markdown tr > [align='right'] {\n text-align: right;\n }\n\n .markdown tr > [align='center'] {\n text-align: center;\n }\n\n /* Details */\n .markdown details {\n border: var(--markdown-border);\n border-radius: var(--scalar-radius-xl);\n color: var(--scalar-color-1);\n }\n\n .markdown details > :not(summary) {\n margin: var(--markdown-spacing-md);\n margin-bottom: 0;\n }\n\n .markdown details > p:has(> strong):not(:has(:not(strong))) {\n margin-bottom: 8px;\n }\n\n .markdown details > p:has(> strong):not(:has(:not(strong))) + * {\n margin-top: 0;\n }\n\n .markdown details > table {\n width: calc(100% - calc(var(--markdown-spacing-md) * 2));\n }\n\n .markdown summary {\n border-radius: var(--scalar-radius);\n display: flex;\n align-items: flex-start;\n gap: 8px;\n min-height: 40px;\n padding: 7px 14px;\n position: relative;\n font-weight: var(--scalar-semibold);\n line-height: var(--markdown-line-height);\n cursor: pointer;\n user-select: none;\n }\n\n .markdown summary:hover {\n background-color: var(--scalar-background-2);\n }\n\n .markdown details[open] {\n padding-bottom: var(--markdown-spacing-md);\n }\n\n .markdown details[open] > summary {\n border-bottom: var(--markdown-border);\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n }\n\n .markdown summary::before {\n display: block;\n content: '';\n flex-shrink: 0;\n width: var(--markdown-spacing-md);\n height: var(--markdown-spacing-md);\n background-color: var(--scalar-color-3);\n mask-image: url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 256 256\" fill=\"currentColor\" width=\"1em\" height=\"1em\"><path d=\"M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z\"></path></svg>');\n margin-top: 5px;\n }\n\n .markdown summary:hover::before {\n background-color: var(--scalar-color-1);\n }\n\n .markdown details[open] > summary::before {\n transform: rotate(90deg);\n transition: transform 0.1s ease-in-out;\n }\n\n .markdown details:has(+ details) {\n border-bottom: 0;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n margin-bottom: 0;\n }\n\n .markdown details:has(+ details) + details,\n .markdown details:has(+ details) + details > summary {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n }\n\n /* Links */\n .markdown a {\n --font-color: var(--scalar-link-color, var(--scalar-color-accent));\n --font-visited: var(--scalar-link-color-visited, var(--scalar-color-2));\n text-decoration: var(--scalar-text-decoration);\n\n color: var(--font-color);\n font-weight: var(--scalar-link-font-weight, var(--scalar-semibold));\n text-underline-offset: 0.25rem;\n text-decoration-thickness: 1px;\n text-decoration-color: color-mix(\n in srgb,\n var(--font-color) 30%,\n transparent\n );\n }\n\n .markdown a:hover {\n text-decoration-color: var(--scalar-color-1, currentColor);\n color: var(--scalar-link-color-hover, var(--scalar-color-accent));\n -webkit-text-decoration: var(--scalar-text-decoration-hover);\n text-decoration: var(--scalar-text-decoration-hover);\n }\n\n .markdown a:visited {\n color: var(--font-visited);\n }\n\n /* Text effects and formatting */\n .markdown em {\n font-style: italic;\n }\n\n .markdown sup,\n .markdown sub {\n font-size: var(--scalar-micro);\n font-weight: 450;\n }\n\n .markdown sup {\n vertical-align: super;\n }\n\n .markdown sub {\n vertical-align: sub;\n }\n\n .markdown del {\n text-decoration: line-through;\n }\n\n /* Code blocks and inline code */\n .markdown code {\n font-family: var(--scalar-font-code);\n background-color: var(--scalar-background-2);\n box-shadow: 0 0 0 var(--scalar-border-width) var(--scalar-border-color);\n font-size: var(--scalar-micro);\n border-radius: var(--scalar-radius-md);\n padding: 0 3px;\n }\n\n .markdown .hljs {\n font-size: var(--scalar-small);\n }\n\n .markdown pre code {\n display: block;\n white-space: pre;\n padding: var(--markdown-spacing-sm);\n line-height: 1.5;\n margin: var(--markdown-spacing-sm) 0;\n -webkit-overflow-scrolling: touch;\n overflow-x: auto;\n max-width: 100%;\n min-width: 100px;\n }\n\n /* Horizontal rules */\n .markdown hr {\n border: none;\n border-bottom: var(--markdown-border);\n }\n\n /* Blockquotes */\n .markdown blockquote {\n border-left: 2px solid var(--scalar-border-color);\n padding-left: var(--markdown-spacing-sm);\n }\n\n .markdown blockquote > * {\n margin-bottom: var(--markdown-spacing-sm);\n }\n\n /* Markdown Checklist */\n .markdown li.task-list-item {\n position: relative;\n list-style: none;\n }\n\n .markdown li.task-list-item > input {\n display: inline;\n position: absolute;\n top: 0.225em;\n left: -1.4em;\n appearance: none;\n width: var(--markdown-spacing-md);\n height: var(--markdown-spacing-md);\n border: 1px solid var(--scalar-color-3);\n border-radius: var(--scalar-radius);\n }\n\n .markdown li.task-list-item > input[type='checkbox']:checked {\n background-color: var(--scalar-color-1);\n border-color: var(--scalar-color-1);\n }\n\n .markdown li.task-list-item > input[type='checkbox']::before {\n content: '';\n position: absolute;\n left: 5px;\n top: 1px;\n width: 5px;\n height: 10px;\n border: solid var(--scalar-background-1);\n border-width: 0 1.5px 1.5px 0;\n transform: rotate(45deg);\n opacity: 0;\n }\n\n .markdown li.task-list-item > input[type='checkbox']:checked::before {\n opacity: 1;\n }\n\n /* Markdown Alert */\n .markdown .markdown-alert {\n align-items: stretch;\n border-radius: var(--scalar-radius-md);\n background-color: color-mix(\n in srgb,\n var(--scalar-background-2),\n transparent\n );\n border: var(--markdown-border);\n display: flex;\n gap: var(--markdown-spacing-sm);\n padding: 10px 14px;\n position: relative;\n }\n\n .markdown .markdown-alert .markdown-alert-icon::before {\n content: '';\n display: block;\n width: 18px;\n height: 18px;\n background-color: currentColor;\n flex-shrink: 0;\n margin-top: 3px;\n mask-repeat: no-repeat;\n mask-size: contain;\n mask-position: center;\n }\n\n .markdown .markdown-alert.markdown-alert-note {\n background-color: color-mix(\n in srgb,\n var(--scalar-color-blue),\n transparent 97%\n );\n border: var(--scalar-border-width) solid\n color-mix(in srgb, var(--scalar-color-blue), transparent 50%);\n }\n\n .markdown .markdown-alert.markdown-alert-tip {\n background-color: color-mix(\n in srgb,\n var(--scalar-color-2),\n transparent 97%\n );\n border: var(--scalar-border-width) solid\n color-mix(in srgb, var(--scalar-color-2), transparent 50%);\n }\n\n .markdown .markdown-alert.markdown-alert-note .markdown-alert-icon::before,\n .markdown .markdown-alert.markdown-alert-tip .markdown-alert-icon::before {\n mask-image: url('data:image/svg+xml,<svg data-v-852d534d=\"\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 256 256\" fill=\"currentColor\" class=\"icon-placeholder\"><path d=\"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z\"></path></svg>');\n color: var(--scalar-color-blue);\n }\n\n .markdown .markdown-alert.markdown-alert-important,\n .markdown .markdown-alert.markdown-alert-warning {\n background-color: color-mix(\n in srgb,\n var(--scalar-color-orange),\n transparent 97%\n );\n border: var(--scalar-border-width) solid\n color-mix(in srgb, var(--scalar-color-orange), transparent 50%);\n }\n\n .markdown\n .markdown-alert.markdown-alert-important\n .markdown-alert-icon::before,\n .markdown\n .markdown-alert.markdown-alert-warning\n .markdown-alert-icon::before {\n mask-image: url('data:image/svg+xml,<svg data-v-852d534d=\"\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 256 256\" fill=\"currentColor\" class=\"icon-placeholder\"><path d=\"M236.8,188.09,149.35,36.22h0a24.76,24.76,0,0,0-42.7,0L19.2,188.09a23.51,23.51,0,0,0,0,23.72A24.35,24.35,0,0,0,40.55,224h174.9a24.35,24.35,0,0,0,21.33-12.19A23.51,23.51,0,0,0,236.8,188.09ZM222.93,203.8a8.5,8.5,0,0,1-7.48,4.2H40.55a8.5,8.5,0,0,1-7.48-4.2,7.59,7.59,0,0,1,0-7.72L120.52,44.21a8.75,8.75,0,0,1,15,0l87.45,151.87A7.59,7.59,0,0,1,222.93,203.8ZM120,144V104a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,180Z\"></path></svg>');\n }\n\n .markdown .markdown-alert.markdown-alert-caution {\n background-color: color-mix(\n in srgb,\n var(--scalar-color-red),\n transparent 97%\n );\n border: var(--scalar-border-width) solid\n color-mix(in srgb, var(--scalar-color-red), transparent 50%);\n }\n\n .markdown\n .markdown-alert.markdown-alert-caution\n .markdown-alert-icon::before {\n mask-image: url('data:image/svg+xml,<svg data-v-852d534d=\"\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 256 256\" fill=\"currentColor\" class=\"icon-placeholder\"><path d=\"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm-8-80V80a8,8,0,0,1,16,0v56a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,172Z\"></path></svg>');\n color: var(--scalar-color-red);\n }\n\n .markdown .markdown-alert.markdown-alert-success {\n background-color: color-mix(\n in srgb,\n var(--scalar-color-green),\n transparent 97%\n );\n border: var(--scalar-border-width) solid\n color-mix(in srgb, var(--scalar-color-green), transparent 50%);\n }\n\n .markdown\n .markdown-alert.markdown-alert-success\n .markdown-alert-icon::before {\n mask-image: url('data:image/svg+xml,<svg data-v-852d534d=\"\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 256 256\" fill=\"currentColor\" class=\"icon-placeholder\"><path d=\"M173.66,98.34a8,8,0,0,1,0,11.32l-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35A8,8,0,0,1,173.66,98.34ZM232,128A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z\"></path></svg>');\n color: var(--scalar-color-green);\n }\n\n .markdown .markdown-alert.markdown-alert-note .markdown-alert-icon::before {\n color: var(--scalar-color-blue);\n }\n\n .markdown .markdown-alert.markdown-alert-tip .markdown-alert-icon::before {\n color: var(--scalar-color-2);\n }\n\n .markdown\n .markdown-alert.markdown-alert-important\n .markdown-alert-icon::before {\n color: var(--scalar-color-purple);\n }\n\n .markdown\n .markdown-alert.markdown-alert-warning\n .markdown-alert-icon::before {\n color: var(--scalar-color-orange);\n }\n\n .markdown .markdown-alert .markdown-alert-content {\n margin: 0;\n line-height: var(--markdown-line-height);\n }\n}\n</style>\n"],"mappings":""}
1
+ {"version":3,"file":"ScalarMarkdown.vue.js","names":[],"sources":["../../../src/components/ScalarMarkdown/ScalarMarkdown.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Scalar Markdown component\n *\n * Renders a markdown string as styled HTML with support for\n * headings, links, code blocks, tables, alerts, and more.\n *\n * @example\n * <ScalarMarkdown value=\"# Hello World\" />\n */\nexport default {}\n</script>\n\n<script setup lang=\"ts\">\nimport {\n type Node,\n htmlFromMarkdown,\n isHeading,\n textFromNode,\n} from '@scalar/code-highlight'\nimport { useBindCx } from '@scalar/use-hooks/useBindCx'\nimport { computed, useTemplateRef } from 'vue'\n\nimport type { ScalarMarkdownProps } from './types'\nimport { useMarkdownRenderHooks } from './useMarkdownRenderHooks'\n\nconst {\n value,\n transform,\n transformType,\n withImages = false,\n withAnchors = false,\n anchorPrefix,\n} = defineProps<ScalarMarkdownProps>()\n\nconst { cx } = useBindCx()\ndefineOptions({ inheritAttrs: false })\n\n// Expose the element ref to the parent component\nconst templateRef = useTemplateRef('div')\ndefineExpose({ el: templateRef })\n\nconst transformHeading = (node: Node) => {\n if (!withAnchors) {\n return transform?.(node) || node\n }\n\n if (!isHeading(node)) {\n return node\n }\n\n const headingText = textFromNode(node)\n\n /** Basic slugify for the heading text */\n const slug = headingText.toLowerCase().replace(/\\s+/g, '-')\n\n /** Return adding anchor prefix if present or just the slug */\n const id = anchorPrefix ? `${anchorPrefix}/description/${slug}` : slug\n\n // Add the id to the heading\n node.data = {\n hProperties: {\n id,\n },\n }\n\n if (transform) {\n return transform(node)\n }\n\n return node\n}\n\nconst html = computed(() => {\n return htmlFromMarkdown(value ?? '', {\n removeTags: withImages ? [] : ['img', 'picture'],\n transform:\n withAnchors && transformType === 'heading' ? transformHeading : transform,\n transformType,\n })\n})\nuseMarkdownRenderHooks(\n templateRef,\n () => value ?? '',\n () => html.value,\n)\n</script>\n<template>\n <div\n ref=\"div\"\n v-bind=\"cx('markdown', { 'line-clamp-(--markdown-clamp)': !!clamp })\"\n :style=\"{ '--markdown-clamp': clamp }\"\n v-html=\"html\" />\n</template>\n<style>\n@import '@scalar/code-highlight/css/code.css';\n\n.scalar-app {\n /* Base container and variables */\n .markdown {\n --scalar-refs-heading-spacing: 24px;\n --markdown-border: var(--scalar-border-width) solid\n var(--scalar-border-color);\n --markdown-spacing-sm: 12px;\n --markdown-spacing-md: 16px;\n --markdown-line-height: 1.625;\n --markdown-heading-line-height: 1.15;\n\n font-family: var(--scalar-font);\n word-break: break-word;\n line-height: var(--markdown-line-height);\n }\n\n .markdown > * {\n margin-bottom: var(--markdown-spacing-md);\n }\n\n /* margin gets doubled between heading + non heading elements if we don't do this*/\n .markdown > *:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6):last-child {\n margin-bottom: 0;\n }\n /* Headings */\n .markdown h1,\n .markdown h2,\n .markdown h3,\n .markdown h4,\n .markdown h5,\n .markdown h6 {\n display: block;\n font-weight: var(--scalar-bold);\n margin-top: var(--scalar-refs-heading-spacing);\n margin-bottom: var(--markdown-spacing-sm);\n line-height: var(--markdown-heading-line-height, 1.15);\n scroll-margin-top: 1rem;\n }\n .markdown h1 {\n font-size: 1.5rem;\n }\n .markdown h2,\n .markdown h3 {\n font-size: 1.25rem;\n }\n .markdown h4,\n .markdown h5,\n .markdown h6 {\n font-size: 1rem;\n }\n\n /* Text formatting and paragraphs */\n .markdown b,\n .markdown strong {\n font-weight: var(--scalar-bold);\n }\n\n .markdown p {\n color: inherit;\n display: block;\n line-height: var(--markdown-line-height);\n }\n\n /* Images */\n .markdown img {\n /**\n * With `display: block` <a><img></a> will take the whole width.\n *\n * @see https://github.com/scalar/scalar/issues/3961\n */\n display: inline-block;\n overflow: hidden;\n border-radius: var(--scalar-radius-md);\n max-width: 100%;\n }\n\n /* Lists */\n .markdown ul,\n .markdown ol {\n display: flex;\n flex-direction: column;\n padding-left: 1.6em;\n gap: 2px;\n line-height: var(--markdown-line-height);\n }\n\n .markdown li {\n margin-top: 2px;\n padding-left: 7px;\n }\n\n ol > li::marker,\n ol > * > li::marker {\n font: var(--scalar-font);\n font-variant-numeric: tabular-nums;\n font-weight: var(--scalar-semibold);\n white-space: nowrap;\n }\n\n /* Level 1, 4, 7 → decimal */\n .markdown ol {\n list-style-type: decimal;\n }\n /* Level 2, 5, 8 → lower-alpha */\n .markdown ol ol {\n list-style-type: lower-alpha;\n }\n\n .markdown ol ol ol ol,\n .markdown ol ol ol ol ol ol ol {\n list-style-type: decimal;\n }\n\n .markdown ol ol ol ol ol,\n .markdown ol ol ol ol ol ol ol ol {\n list-style-type: lower-alpha;\n }\n\n /* Level 3, 6, 9 → lower-roman */\n .markdown ol ol ol {\n list-style-type: lower-roman;\n }\n\n .markdown ol ol ol ol ol ol,\n .markdown ol ol ol ol ol ol ol ol ol {\n list-style-type: lower-roman;\n }\n\n .markdown ul > li,\n .markdown ul > * > li {\n list-style-type: disc;\n }\n\n /* Tables */\n .markdown table {\n display: table;\n table-layout: fixed;\n overflow-x: auto;\n position: relative;\n width: max-content;\n max-width: 100%;\n margin: 1em 0;\n border: var(--scalar-border-width) solid var(--scalar-border-color);\n border-radius: var(--scalar-radius-md);\n border-spacing: 0;\n }\n\n .markdown tbody,\n .markdown thead {\n vertical-align: middle;\n }\n\n .markdown tbody {\n display: table-row-group;\n }\n\n .markdown thead {\n display: table-header-group;\n }\n\n .markdown tr {\n display: table-row;\n border-color: inherit;\n vertical-align: inherit;\n }\n\n .markdown td,\n .markdown th {\n display: table-cell;\n vertical-align: top;\n min-width: 1em;\n padding: 8.5px 16px;\n line-height: var(--markdown-line-height);\n position: relative;\n /** Break long words in tables */\n word-break: break-word;\n font-size: var(--scalar-small);\n color: var(--scalar-color-1);\n border-right: var(--markdown-border);\n border-bottom: var(--markdown-border);\n }\n\n .markdown td > *,\n .markdown th > * {\n margin-bottom: 0;\n }\n\n .markdown th:empty {\n display: none;\n }\n\n .markdown td:first-of-type,\n .markdown th:first-of-type {\n border-left: none;\n }\n\n .markdown td:last-of-type,\n .markdown th:last-of-type {\n border-right: none;\n }\n\n .markdown tr:last-of-type td {\n border-bottom: none;\n }\n\n .markdown th {\n font-weight: var(--scalar-bold);\n text-align: left;\n border-left-color: transparent;\n background: var(--scalar-background-2);\n }\n\n .markdown th:first-of-type {\n border-top-left-radius: var(--scalar-radius-md);\n }\n\n .markdown th:last-of-type {\n border-top-right-radius: var(--scalar-radius-md);\n }\n\n .markdown tr > [align='left'] {\n text-align: left;\n }\n\n .markdown tr > [align='right'] {\n text-align: right;\n }\n\n .markdown tr > [align='center'] {\n text-align: center;\n }\n\n /* Details */\n .markdown details {\n border: var(--markdown-border);\n border-radius: var(--scalar-radius-xl);\n color: var(--scalar-color-1);\n }\n\n .markdown details > :not(summary) {\n margin: var(--markdown-spacing-md);\n margin-bottom: 0;\n }\n\n .markdown details > p:has(> strong):not(:has(:not(strong))) {\n margin-bottom: 8px;\n }\n\n .markdown details > p:has(> strong):not(:has(:not(strong))) + * {\n margin-top: 0;\n }\n\n .markdown details > table {\n width: calc(100% - calc(var(--markdown-spacing-md) * 2));\n }\n\n .markdown summary {\n border-radius: var(--scalar-radius);\n display: flex;\n align-items: flex-start;\n gap: 8px;\n min-height: 40px;\n padding: 7px 14px;\n position: relative;\n font-weight: var(--scalar-semibold);\n line-height: var(--markdown-line-height);\n cursor: pointer;\n user-select: none;\n }\n\n .markdown summary:hover {\n background-color: var(--scalar-background-2);\n }\n\n .markdown details[open] {\n padding-bottom: var(--markdown-spacing-md);\n }\n\n .markdown details[open] > summary {\n border-bottom: var(--markdown-border);\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n }\n\n .markdown summary::before {\n display: block;\n content: '';\n flex-shrink: 0;\n width: var(--markdown-spacing-md);\n height: var(--markdown-spacing-md);\n background-color: var(--scalar-color-3);\n mask-image: url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 256 256\" fill=\"currentColor\" width=\"1em\" height=\"1em\"><path d=\"M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z\"></path></svg>');\n margin-top: 5px;\n }\n\n .markdown summary:hover::before {\n background-color: var(--scalar-color-1);\n }\n\n .markdown details[open] > summary::before {\n transform: rotate(90deg);\n transition: transform 0.1s ease-in-out;\n }\n\n .markdown details:has(+ details) {\n border-bottom: 0;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n margin-bottom: 0;\n }\n\n .markdown details:has(+ details) + details,\n .markdown details:has(+ details) + details > summary {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n }\n\n /* Links */\n .markdown a {\n --font-color: var(--scalar-link-color, var(--scalar-color-accent));\n --font-visited: var(--scalar-link-color-visited, var(--scalar-color-2));\n text-decoration: var(--scalar-text-decoration);\n\n color: var(--font-color);\n font-weight: var(--scalar-link-font-weight, var(--scalar-semibold));\n text-underline-offset: 0.25rem;\n text-decoration-thickness: 1px;\n text-decoration-color: color-mix(\n in srgb,\n var(--font-color) 30%,\n transparent\n );\n }\n\n .markdown a:hover {\n text-decoration-color: var(--scalar-color-1, currentColor);\n color: var(--scalar-link-color-hover, var(--scalar-color-accent));\n -webkit-text-decoration: var(--scalar-text-decoration-hover);\n text-decoration: var(--scalar-text-decoration-hover);\n }\n\n .markdown a:visited {\n color: var(--font-visited);\n }\n\n /* Text effects and formatting */\n .markdown em {\n font-style: italic;\n }\n\n .markdown sup,\n .markdown sub {\n font-size: var(--scalar-micro);\n font-weight: 450;\n }\n\n .markdown sup {\n vertical-align: super;\n }\n\n .markdown sub {\n vertical-align: sub;\n }\n\n .markdown del {\n text-decoration: line-through;\n }\n\n /* Code blocks and inline code */\n .markdown code {\n font-family: var(--scalar-font-code);\n background-color: var(--scalar-background-2);\n box-shadow: 0 0 0 var(--scalar-border-width) var(--scalar-border-color);\n font-size: var(--scalar-micro);\n border-radius: var(--scalar-radius-md);\n padding: 0 3px;\n }\n\n .markdown .hljs {\n font-size: var(--scalar-small);\n }\n\n .markdown pre code {\n display: block;\n white-space: pre;\n padding: var(--markdown-spacing-sm);\n line-height: 1.5;\n margin: var(--markdown-spacing-sm) 0;\n -webkit-overflow-scrolling: touch;\n overflow-x: auto;\n max-width: 100%;\n min-width: 100px;\n }\n\n /* Horizontal rules */\n .markdown hr {\n border: none;\n border-bottom: var(--markdown-border);\n }\n\n /* Blockquotes */\n .markdown blockquote {\n border-left: 2px solid var(--scalar-border-color);\n padding-left: var(--markdown-spacing-sm);\n }\n\n .markdown blockquote > * {\n margin-bottom: var(--markdown-spacing-sm);\n }\n\n /* Markdown Checklist */\n .markdown li.task-list-item {\n position: relative;\n list-style: none;\n }\n\n .markdown li.task-list-item > input {\n display: inline;\n position: absolute;\n top: 0.225em;\n left: -1.4em;\n appearance: none;\n width: var(--markdown-spacing-md);\n height: var(--markdown-spacing-md);\n border: 1px solid var(--scalar-color-3);\n border-radius: var(--scalar-radius);\n }\n\n .markdown li.task-list-item > input[type='checkbox']:checked {\n background-color: var(--scalar-color-1);\n border-color: var(--scalar-color-1);\n }\n\n .markdown li.task-list-item > input[type='checkbox']::before {\n content: '';\n position: absolute;\n left: 5px;\n top: 1px;\n width: 5px;\n height: 10px;\n border: solid var(--scalar-background-1);\n border-width: 0 1.5px 1.5px 0;\n transform: rotate(45deg);\n opacity: 0;\n }\n\n .markdown li.task-list-item > input[type='checkbox']:checked::before {\n opacity: 1;\n }\n\n /* Markdown Alert */\n .markdown .markdown-alert {\n align-items: stretch;\n border-radius: var(--scalar-radius-md);\n background-color: color-mix(\n in srgb,\n var(--scalar-background-2),\n transparent\n );\n border: var(--markdown-border);\n display: flex;\n gap: var(--markdown-spacing-sm);\n padding: 10px 14px;\n position: relative;\n }\n\n .markdown .markdown-alert .markdown-alert-icon::before {\n content: '';\n display: block;\n width: 18px;\n height: 18px;\n background-color: currentColor;\n flex-shrink: 0;\n margin-top: 3px;\n mask-repeat: no-repeat;\n mask-size: contain;\n mask-position: center;\n }\n\n .markdown .markdown-alert.markdown-alert-note {\n background-color: color-mix(\n in srgb,\n var(--scalar-color-blue),\n transparent 97%\n );\n border: var(--scalar-border-width) solid\n color-mix(in srgb, var(--scalar-color-blue), transparent 50%);\n }\n\n .markdown .markdown-alert.markdown-alert-tip {\n background-color: color-mix(\n in srgb,\n var(--scalar-color-2),\n transparent 97%\n );\n border: var(--scalar-border-width) solid\n color-mix(in srgb, var(--scalar-color-2), transparent 50%);\n }\n\n .markdown .markdown-alert.markdown-alert-note .markdown-alert-icon::before,\n .markdown .markdown-alert.markdown-alert-tip .markdown-alert-icon::before {\n mask-image: url('data:image/svg+xml,<svg data-v-852d534d=\"\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 256 256\" fill=\"currentColor\" class=\"icon-placeholder\"><path d=\"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z\"></path></svg>');\n color: var(--scalar-color-blue);\n }\n\n .markdown .markdown-alert.markdown-alert-important,\n .markdown .markdown-alert.markdown-alert-warning {\n background-color: color-mix(\n in srgb,\n var(--scalar-color-orange),\n transparent 97%\n );\n border: var(--scalar-border-width) solid\n color-mix(in srgb, var(--scalar-color-orange), transparent 50%);\n }\n\n .markdown\n .markdown-alert.markdown-alert-important\n .markdown-alert-icon::before,\n .markdown\n .markdown-alert.markdown-alert-warning\n .markdown-alert-icon::before {\n mask-image: url('data:image/svg+xml,<svg data-v-852d534d=\"\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 256 256\" fill=\"currentColor\" class=\"icon-placeholder\"><path d=\"M236.8,188.09,149.35,36.22h0a24.76,24.76,0,0,0-42.7,0L19.2,188.09a23.51,23.51,0,0,0,0,23.72A24.35,24.35,0,0,0,40.55,224h174.9a24.35,24.35,0,0,0,21.33-12.19A23.51,23.51,0,0,0,236.8,188.09ZM222.93,203.8a8.5,8.5,0,0,1-7.48,4.2H40.55a8.5,8.5,0,0,1-7.48-4.2,7.59,7.59,0,0,1,0-7.72L120.52,44.21a8.75,8.75,0,0,1,15,0l87.45,151.87A7.59,7.59,0,0,1,222.93,203.8ZM120,144V104a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,180Z\"></path></svg>');\n }\n\n .markdown .markdown-alert.markdown-alert-caution {\n background-color: color-mix(\n in srgb,\n var(--scalar-color-red),\n transparent 97%\n );\n border: var(--scalar-border-width) solid\n color-mix(in srgb, var(--scalar-color-red), transparent 50%);\n }\n\n .markdown\n .markdown-alert.markdown-alert-caution\n .markdown-alert-icon::before {\n mask-image: url('data:image/svg+xml,<svg data-v-852d534d=\"\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 256 256\" fill=\"currentColor\" class=\"icon-placeholder\"><path d=\"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm-8-80V80a8,8,0,0,1,16,0v56a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,172Z\"></path></svg>');\n color: var(--scalar-color-red);\n }\n\n .markdown .markdown-alert.markdown-alert-success {\n background-color: color-mix(\n in srgb,\n var(--scalar-color-green),\n transparent 97%\n );\n border: var(--scalar-border-width) solid\n color-mix(in srgb, var(--scalar-color-green), transparent 50%);\n }\n\n .markdown\n .markdown-alert.markdown-alert-success\n .markdown-alert-icon::before {\n mask-image: url('data:image/svg+xml,<svg data-v-852d534d=\"\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 256 256\" fill=\"currentColor\" class=\"icon-placeholder\"><path d=\"M173.66,98.34a8,8,0,0,1,0,11.32l-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35A8,8,0,0,1,173.66,98.34ZM232,128A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z\"></path></svg>');\n color: var(--scalar-color-green);\n }\n\n .markdown .markdown-alert.markdown-alert-note .markdown-alert-icon::before {\n color: var(--scalar-color-blue);\n }\n\n .markdown .markdown-alert.markdown-alert-tip .markdown-alert-icon::before {\n color: var(--scalar-color-2);\n }\n\n .markdown\n .markdown-alert.markdown-alert-important\n .markdown-alert-icon::before {\n color: var(--scalar-color-purple);\n }\n\n .markdown\n .markdown-alert.markdown-alert-warning\n .markdown-alert-icon::before {\n color: var(--scalar-color-orange);\n }\n\n .markdown .markdown-alert .markdown-alert-content {\n margin: 0;\n line-height: var(--markdown-line-height);\n }\n}\n</style>\n"],"mappings":""}