@inploi/plugin-job-search 0.17.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2204,7 +2204,7 @@ const AlertsButton = ({
2204
2204
  }) => {
2205
2205
  const [plugin, setPlugin] = h();
2206
2206
  y(() => {
2207
- Promise.resolve().then(() => require("./plugin-job-alerts-ee47a7fb.cjs")).then((m2) => setPlugin(m2.jobAlertsPlugin()({ analytics, apiClient, logger })));
2207
+ Promise.resolve().then(() => require("./plugin-job-alerts-e8471004.cjs")).then((m2) => setPlugin(m2.jobAlertsPlugin()({ analytics, apiClient, logger })));
2208
2208
  }, [analytics, apiClient, logger]);
2209
2209
  y(() => {
2210
2210
  plugin == null ? void 0 : plugin.render({
@@ -3942,7 +3942,7 @@ const FilterContentActive = ({
3942
3942
  ) });
3943
3943
  case "revalidating":
3944
3944
  case "loaded": {
3945
- const allSuggestions = sortResults(unfilteredQuery.data[property.key] ?? [], property.sortSuggestions);
3945
+ const allSuggestions = unfilteredQuery.data[property.key] ?? [];
3946
3946
  const filteredSuggestions = search && search.length > 1 ? allSuggestions.filter((s2) => s2.label.toLowerCase().includes(search.toLowerCase())) : allSuggestions;
3947
3947
  const suggestions = [];
3948
3948
  for (const suggestion of filteredSuggestions) {
@@ -3965,7 +3965,7 @@ const FilterContentActive = ({
3965
3965
  const noEnabledOptions = !suggestions.find((suggestion) => !suggestion.disabled);
3966
3966
  if (noEnabledOptions)
3967
3967
  return /* @__PURE__ */ u$1("p", { class: "font-style-tag text-style-tag font-weight-tag border-px w-full border-transparent px-4 py-2 text-center", children: "No filter options available." });
3968
- const isLongList = suggestions.length > SUGGESTIONS_COUNT_THRESHOLD || suggestions.some((suggestion) => suggestion.label.length > 24);
3968
+ const isLongList = allSuggestions.length > SUGGESTIONS_COUNT_THRESHOLD || allSuggestions.some((suggestion) => suggestion.label.length > 24);
3969
3969
  return /* @__PURE__ */ u$1("div", { class: "pointer-fine:px-4", children: /* @__PURE__ */ u$1(
3970
3970
  "ul",
3971
3971
  {
@@ -3973,7 +3973,7 @@ const FilterContentActive = ({
3973
3973
  "--maxHeight": TAG_HEIGHT * (LINE_COUNT + 0.5) + 2 * PADDING_Y + "px"
3974
3974
  },
3975
3975
  class: suggestionsListClass({ type: isLongList ? "long" : "short" }),
3976
- children: suggestions.map((suggestion) => {
3976
+ children: sortResults(suggestions, property.sortSuggestions).map((suggestion) => {
3977
3977
  const active = selectedValues.includes(suggestion.value);
3978
3978
  return /* @__PURE__ */ u$1("li", { class: suggestionItemClass({ type: isLongList ? "line" : "pill" }), children: /* @__PURE__ */ u$1(
3979
3979
  "button",
@@ -4205,6 +4205,36 @@ const useFilterKeyboardFocus = (activeKey, ref) => {
4205
4205
  };
4206
4206
  function noop() {
4207
4207
  }
4208
+ const mergeFilterValues = (subject, mergeWith) => {
4209
+ var _a, _b, _c;
4210
+ switch (mergeWith.select) {
4211
+ case "many":
4212
+ (_a = mergeWith.value) == null ? void 0 : _a.forEach((val) => {
4213
+ var _a2, _b2;
4214
+ switch (subject.select) {
4215
+ case "many":
4216
+ if (!((_a2 = subject.value) == null ? void 0 : _a2.includes(val)))
4217
+ (_b2 = subject.value) == null ? void 0 : _b2.push(val);
4218
+ break;
4219
+ case "one":
4220
+ subject.value = val;
4221
+ }
4222
+ });
4223
+ break;
4224
+ case "one":
4225
+ if (!mergeWith.value)
4226
+ break;
4227
+ switch (subject.select) {
4228
+ case "many":
4229
+ if (!((_b = subject.value) == null ? void 0 : _b.includes(mergeWith.value)))
4230
+ (_c = subject.value) == null ? void 0 : _c.push(mergeWith.value);
4231
+ break;
4232
+ case "one":
4233
+ subject.value = mergeWith.value;
4234
+ }
4235
+ break;
4236
+ }
4237
+ };
4208
4238
  const nextStateFromValue = ({
4209
4239
  property,
4210
4240
  value,
@@ -4245,10 +4275,7 @@ const FilterSection = ({
4245
4275
  var _a;
4246
4276
  const [activeKey, setActiveKey] = h((_a = properties[0]) == null ? void 0 : _a.key);
4247
4277
  useFilterKeyboardFocus(activeKey, parentRef);
4248
- const defaultFilters = T$1(
4249
- () => getFilterDefaults(properties.map(({ value: _2, ...prop }) => prop)),
4250
- [properties]
4251
- );
4278
+ const defaultFilters = T$1(() => getFilterDefaults(properties, "value"), [properties]);
4252
4279
  const unfilteredQuery = getQueryState(
4253
4280
  useSWR(
4254
4281
  cacheKey({ filter: defaultFilters, baseKey: "filters" }),
@@ -4279,11 +4306,14 @@ const FilterSection = ({
4279
4306
  return /* @__PURE__ */ u$1("aside", { children: /* @__PURE__ */ u$1("div", { role: "tablist", class: "top-filters ijs-filters-section sticky flex flex-col gap-2 px-4 md:px-0", children: properties.filter((property) => property.hidden !== true).map((property) => {
4280
4307
  const active = activeKey === property.key;
4281
4308
  const value = appliedFilters[property.key] ?? { select: property.select, value: void 0 };
4309
+ const propertyDefault = defaultFilters[property.key];
4310
+ if (propertyDefault)
4311
+ mergeFilterValues(value, propertyDefault);
4282
4312
  const onValueChange = (value2) => {
4283
4313
  const { newActiveKey, newValue } = nextStateFromValue({
4284
4314
  property,
4285
4315
  activeKey,
4286
- value: value2,
4316
+ value: value2 ?? propertyDefault,
4287
4317
  properties,
4288
4318
  appliedFilters
4289
4319
  });
@@ -5010,17 +5040,18 @@ const ResultsPage = ({
5010
5040
  exhaustive(state);
5011
5041
  }
5012
5042
  };
5013
- const getFilterDefaults = (properties) => properties.reduce(
5043
+ const getFilterDefaults = (properties, field = "value") => properties.reduce(
5014
5044
  (acc, property) => {
5015
- var _a;
5016
5045
  switch (property.select) {
5017
5046
  case "one": {
5018
- const value = typeof property.value === "string" ? property.value : (_a = property.value) == null ? void 0 : _a[0];
5047
+ const defaultValue = property[field];
5048
+ const value = typeof defaultValue === "string" ? defaultValue : defaultValue == null ? void 0 : defaultValue[0];
5019
5049
  acc[property.key] = { select: property.select, value };
5020
5050
  break;
5021
5051
  }
5022
5052
  case "many": {
5023
- const value = typeof property.value === "string" ? [property.value] : Array.isArray(property.value) ? property.value : [];
5053
+ const defaultValue = property[field];
5054
+ const value = typeof defaultValue === "string" ? [defaultValue] : Array.isArray(defaultValue) ? defaultValue : [];
5024
5055
  acc[property.key] = {
5025
5056
  select: property.select,
5026
5057
  value
@@ -5047,7 +5078,7 @@ const JobSearch = ({
5047
5078
  const appRef = A$1(null);
5048
5079
  const [appliedFilters, setAppliedFilters] = h(() => ({
5049
5080
  default: true,
5050
- filters: getFilterDefaults(properties)
5081
+ filters: getFilterDefaults(properties, "defaultValue")
5051
5082
  }));
5052
5083
  return /* @__PURE__ */ u$1("div", { ref: appRef, class: className, id: JOB_SEARCH_SCOPE_ELEMENT_ID, children: /* @__PURE__ */ u$1("div", { class: "ijs-app bg-app rounded-app font-family-default font-weight-default font-style-default mx-auto flex min-h-screen max-w-screen-lg flex-col gap-4 py-4 md:grid md:grid-cols-3 md:px-4", children: [
5053
5084
  /* @__PURE__ */ u$1(ErrorBoundary, { Fallback: ErrorView, children: /* @__PURE__ */ u$1("div", { class: "flex flex-col gap-2", children: [
@@ -5236,35 +5267,38 @@ const match = (subject) => {
5236
5267
  return run(subject);
5237
5268
  };
5238
5269
  const [black, white, transparent] = ["oklch(0 0 0)", "oklch(100 0 0)", "transparent"];
5239
- const tokens = designTokens().core(({ theme }) => ({
5240
- color: {
5241
- black,
5242
- white,
5243
- lowest: theme.mode === "light" ? white : black,
5244
- highest: theme.mode === "light" ? black : white,
5245
- ...generatePalette({
5246
- prefix: "a-",
5247
- hue: theme.accent.hue,
5248
- chroma: theme.accent.chroma,
5249
- mode: theme.mode,
5250
- transform: sameAsInput
5251
- }),
5252
- ...generatePalette({
5253
- prefix: "n-",
5254
- hue: theme.accent.hue,
5255
- chroma: 0.05,
5256
- mode: theme.mode,
5257
- transform: sameAsInput
5258
- }),
5259
- ...generatePalette({
5260
- prefix: "m-",
5261
- hue: 140,
5262
- chroma: theme.accent.chroma,
5263
- mode: theme.mode,
5264
- transform: sameAsInput
5265
- })
5266
- }
5267
- })).semantic(({ core, theme, raw }) => {
5270
+ const tokens = designTokens().core(({ theme }) => {
5271
+ var _a, _b;
5272
+ return {
5273
+ color: {
5274
+ black,
5275
+ white,
5276
+ lowest: theme.mode === "light" ? white : black,
5277
+ highest: theme.mode === "light" ? black : white,
5278
+ ...generatePalette({
5279
+ prefix: "a-",
5280
+ hue: theme.accent.hue,
5281
+ chroma: theme.accent.chroma,
5282
+ mode: theme.mode,
5283
+ transform: sameAsInput
5284
+ }),
5285
+ ...generatePalette({
5286
+ prefix: "n-",
5287
+ hue: ((_a = theme.neutral) == null ? void 0 : _a.hue) ?? theme.accent.hue,
5288
+ chroma: ((_b = theme.neutral) == null ? void 0 : _b.chroma) ?? 0.05,
5289
+ mode: theme.mode,
5290
+ transform: sameAsInput
5291
+ }),
5292
+ ...generatePalette({
5293
+ prefix: "m-",
5294
+ hue: 140,
5295
+ chroma: theme.accent.chroma,
5296
+ mode: theme.mode,
5297
+ transform: sameAsInput
5298
+ })
5299
+ }
5300
+ };
5301
+ }).semantic(({ core, theme, raw }) => {
5268
5302
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n2, _o, _p, _q, _r, _s, _t, _u;
5269
5303
  const withOpacity = (token, opacity) => raw.core.color[token].replace(")", ` / ${opacity})`);
5270
5304
  const { typography, highlights } = theme;
package/dist/index.js CHANGED
@@ -2202,7 +2202,7 @@ const AlertsButton = ({
2202
2202
  }) => {
2203
2203
  const [plugin, setPlugin] = h();
2204
2204
  y(() => {
2205
- import("./plugin-job-alerts-ddf07a02.js").then((m2) => setPlugin(m2.jobAlertsPlugin()({ analytics, apiClient, logger })));
2205
+ import("./plugin-job-alerts-2ce58f09.js").then((m2) => setPlugin(m2.jobAlertsPlugin()({ analytics, apiClient, logger })));
2206
2206
  }, [analytics, apiClient, logger]);
2207
2207
  y(() => {
2208
2208
  plugin == null ? void 0 : plugin.render({
@@ -3940,7 +3940,7 @@ const FilterContentActive = ({
3940
3940
  ) });
3941
3941
  case "revalidating":
3942
3942
  case "loaded": {
3943
- const allSuggestions = sortResults(unfilteredQuery.data[property.key] ?? [], property.sortSuggestions);
3943
+ const allSuggestions = unfilteredQuery.data[property.key] ?? [];
3944
3944
  const filteredSuggestions = search && search.length > 1 ? allSuggestions.filter((s2) => s2.label.toLowerCase().includes(search.toLowerCase())) : allSuggestions;
3945
3945
  const suggestions = [];
3946
3946
  for (const suggestion of filteredSuggestions) {
@@ -3963,7 +3963,7 @@ const FilterContentActive = ({
3963
3963
  const noEnabledOptions = !suggestions.find((suggestion) => !suggestion.disabled);
3964
3964
  if (noEnabledOptions)
3965
3965
  return /* @__PURE__ */ u$1("p", { class: "font-style-tag text-style-tag font-weight-tag border-px w-full border-transparent px-4 py-2 text-center", children: "No filter options available." });
3966
- const isLongList = suggestions.length > SUGGESTIONS_COUNT_THRESHOLD || suggestions.some((suggestion) => suggestion.label.length > 24);
3966
+ const isLongList = allSuggestions.length > SUGGESTIONS_COUNT_THRESHOLD || allSuggestions.some((suggestion) => suggestion.label.length > 24);
3967
3967
  return /* @__PURE__ */ u$1("div", { class: "pointer-fine:px-4", children: /* @__PURE__ */ u$1(
3968
3968
  "ul",
3969
3969
  {
@@ -3971,7 +3971,7 @@ const FilterContentActive = ({
3971
3971
  "--maxHeight": TAG_HEIGHT * (LINE_COUNT + 0.5) + 2 * PADDING_Y + "px"
3972
3972
  },
3973
3973
  class: suggestionsListClass({ type: isLongList ? "long" : "short" }),
3974
- children: suggestions.map((suggestion) => {
3974
+ children: sortResults(suggestions, property.sortSuggestions).map((suggestion) => {
3975
3975
  const active = selectedValues.includes(suggestion.value);
3976
3976
  return /* @__PURE__ */ u$1("li", { class: suggestionItemClass({ type: isLongList ? "line" : "pill" }), children: /* @__PURE__ */ u$1(
3977
3977
  "button",
@@ -4203,6 +4203,36 @@ const useFilterKeyboardFocus = (activeKey, ref) => {
4203
4203
  };
4204
4204
  function noop() {
4205
4205
  }
4206
+ const mergeFilterValues = (subject, mergeWith) => {
4207
+ var _a, _b, _c;
4208
+ switch (mergeWith.select) {
4209
+ case "many":
4210
+ (_a = mergeWith.value) == null ? void 0 : _a.forEach((val) => {
4211
+ var _a2, _b2;
4212
+ switch (subject.select) {
4213
+ case "many":
4214
+ if (!((_a2 = subject.value) == null ? void 0 : _a2.includes(val)))
4215
+ (_b2 = subject.value) == null ? void 0 : _b2.push(val);
4216
+ break;
4217
+ case "one":
4218
+ subject.value = val;
4219
+ }
4220
+ });
4221
+ break;
4222
+ case "one":
4223
+ if (!mergeWith.value)
4224
+ break;
4225
+ switch (subject.select) {
4226
+ case "many":
4227
+ if (!((_b = subject.value) == null ? void 0 : _b.includes(mergeWith.value)))
4228
+ (_c = subject.value) == null ? void 0 : _c.push(mergeWith.value);
4229
+ break;
4230
+ case "one":
4231
+ subject.value = mergeWith.value;
4232
+ }
4233
+ break;
4234
+ }
4235
+ };
4206
4236
  const nextStateFromValue = ({
4207
4237
  property,
4208
4238
  value,
@@ -4243,10 +4273,7 @@ const FilterSection = ({
4243
4273
  var _a;
4244
4274
  const [activeKey, setActiveKey] = h((_a = properties[0]) == null ? void 0 : _a.key);
4245
4275
  useFilterKeyboardFocus(activeKey, parentRef);
4246
- const defaultFilters = T$1(
4247
- () => getFilterDefaults(properties.map(({ value: _2, ...prop }) => prop)),
4248
- [properties]
4249
- );
4276
+ const defaultFilters = T$1(() => getFilterDefaults(properties, "value"), [properties]);
4250
4277
  const unfilteredQuery = getQueryState(
4251
4278
  useSWR(
4252
4279
  cacheKey({ filter: defaultFilters, baseKey: "filters" }),
@@ -4277,11 +4304,14 @@ const FilterSection = ({
4277
4304
  return /* @__PURE__ */ u$1("aside", { children: /* @__PURE__ */ u$1("div", { role: "tablist", class: "top-filters ijs-filters-section sticky flex flex-col gap-2 px-4 md:px-0", children: properties.filter((property) => property.hidden !== true).map((property) => {
4278
4305
  const active = activeKey === property.key;
4279
4306
  const value = appliedFilters[property.key] ?? { select: property.select, value: void 0 };
4307
+ const propertyDefault = defaultFilters[property.key];
4308
+ if (propertyDefault)
4309
+ mergeFilterValues(value, propertyDefault);
4280
4310
  const onValueChange = (value2) => {
4281
4311
  const { newActiveKey, newValue } = nextStateFromValue({
4282
4312
  property,
4283
4313
  activeKey,
4284
- value: value2,
4314
+ value: value2 ?? propertyDefault,
4285
4315
  properties,
4286
4316
  appliedFilters
4287
4317
  });
@@ -5008,17 +5038,18 @@ const ResultsPage = ({
5008
5038
  exhaustive(state);
5009
5039
  }
5010
5040
  };
5011
- const getFilterDefaults = (properties) => properties.reduce(
5041
+ const getFilterDefaults = (properties, field = "value") => properties.reduce(
5012
5042
  (acc, property) => {
5013
- var _a;
5014
5043
  switch (property.select) {
5015
5044
  case "one": {
5016
- const value = typeof property.value === "string" ? property.value : (_a = property.value) == null ? void 0 : _a[0];
5045
+ const defaultValue = property[field];
5046
+ const value = typeof defaultValue === "string" ? defaultValue : defaultValue == null ? void 0 : defaultValue[0];
5017
5047
  acc[property.key] = { select: property.select, value };
5018
5048
  break;
5019
5049
  }
5020
5050
  case "many": {
5021
- const value = typeof property.value === "string" ? [property.value] : Array.isArray(property.value) ? property.value : [];
5051
+ const defaultValue = property[field];
5052
+ const value = typeof defaultValue === "string" ? [defaultValue] : Array.isArray(defaultValue) ? defaultValue : [];
5022
5053
  acc[property.key] = {
5023
5054
  select: property.select,
5024
5055
  value
@@ -5045,7 +5076,7 @@ const JobSearch = ({
5045
5076
  const appRef = A$1(null);
5046
5077
  const [appliedFilters, setAppliedFilters] = h(() => ({
5047
5078
  default: true,
5048
- filters: getFilterDefaults(properties)
5079
+ filters: getFilterDefaults(properties, "defaultValue")
5049
5080
  }));
5050
5081
  return /* @__PURE__ */ u$1("div", { ref: appRef, class: className, id: JOB_SEARCH_SCOPE_ELEMENT_ID, children: /* @__PURE__ */ u$1("div", { class: "ijs-app bg-app rounded-app font-family-default font-weight-default font-style-default mx-auto flex min-h-screen max-w-screen-lg flex-col gap-4 py-4 md:grid md:grid-cols-3 md:px-4", children: [
5051
5082
  /* @__PURE__ */ u$1(ErrorBoundary, { Fallback: ErrorView, children: /* @__PURE__ */ u$1("div", { class: "flex flex-col gap-2", children: [
@@ -5234,35 +5265,38 @@ const match = (subject) => {
5234
5265
  return run(subject);
5235
5266
  };
5236
5267
  const [black, white, transparent] = ["oklch(0 0 0)", "oklch(100 0 0)", "transparent"];
5237
- const tokens = designTokens().core(({ theme }) => ({
5238
- color: {
5239
- black,
5240
- white,
5241
- lowest: theme.mode === "light" ? white : black,
5242
- highest: theme.mode === "light" ? black : white,
5243
- ...generatePalette({
5244
- prefix: "a-",
5245
- hue: theme.accent.hue,
5246
- chroma: theme.accent.chroma,
5247
- mode: theme.mode,
5248
- transform: sameAsInput
5249
- }),
5250
- ...generatePalette({
5251
- prefix: "n-",
5252
- hue: theme.accent.hue,
5253
- chroma: 0.05,
5254
- mode: theme.mode,
5255
- transform: sameAsInput
5256
- }),
5257
- ...generatePalette({
5258
- prefix: "m-",
5259
- hue: 140,
5260
- chroma: theme.accent.chroma,
5261
- mode: theme.mode,
5262
- transform: sameAsInput
5263
- })
5264
- }
5265
- })).semantic(({ core, theme, raw }) => {
5268
+ const tokens = designTokens().core(({ theme }) => {
5269
+ var _a, _b;
5270
+ return {
5271
+ color: {
5272
+ black,
5273
+ white,
5274
+ lowest: theme.mode === "light" ? white : black,
5275
+ highest: theme.mode === "light" ? black : white,
5276
+ ...generatePalette({
5277
+ prefix: "a-",
5278
+ hue: theme.accent.hue,
5279
+ chroma: theme.accent.chroma,
5280
+ mode: theme.mode,
5281
+ transform: sameAsInput
5282
+ }),
5283
+ ...generatePalette({
5284
+ prefix: "n-",
5285
+ hue: ((_a = theme.neutral) == null ? void 0 : _a.hue) ?? theme.accent.hue,
5286
+ chroma: ((_b = theme.neutral) == null ? void 0 : _b.chroma) ?? 0.05,
5287
+ mode: theme.mode,
5288
+ transform: sameAsInput
5289
+ }),
5290
+ ...generatePalette({
5291
+ prefix: "m-",
5292
+ hue: 140,
5293
+ chroma: theme.accent.chroma,
5294
+ mode: theme.mode,
5295
+ transform: sameAsInput
5296
+ })
5297
+ }
5298
+ };
5299
+ }).semantic(({ core, theme, raw }) => {
5266
5300
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n2, _o, _p, _q, _r, _s, _t, _u;
5267
5301
  const withOpacity = (token, opacity) => raw.core.color[token].replace(")", ` / ${opacity})`);
5268
5302
  const { typography, highlights } = theme;
@@ -31,8 +31,10 @@ export type FilterProperty = {
31
31
  * You can use this property to hard-code searches without allowing the user to modify or view them.
32
32
  */
33
33
  hidden?: boolean;
34
- /** The initial value for the filter. */
34
+ /** The **fixed** value for the filter. Cannot be removed or changed. */
35
35
  value?: string | string[];
36
+ /** The **default** value for the filter. Can be removed or changed, but defaults to it during initial load. */
37
+ defaultValue?: string | string[];
36
38
  /** Whether users can search from the available results.
37
39
  * @default false
38
40
  */
@@ -83,6 +85,12 @@ export type JobSearchPluginParams = {
83
85
  /** Float between 0 and 2. Acts as a multiplier to the original generated palette. */
84
86
  chroma: number;
85
87
  };
88
+ neutral?: {
89
+ /** Number between 0 and 360. Number above or below range will loop. */
90
+ hue: number;
91
+ /** Float between 0 and 2. Acts as a multiplier to the original generated palette. */
92
+ chroma: number;
93
+ };
86
94
  typography: Partial<FontSettings> & Partial<Record<TextStyle, Partial<FontSettings>>>;
87
95
  /** Applies to the filter section on desktop. Override to ensure the filters are stuck to the top of the viewport,
88
96
  * but not obscured by the header. This is useful when the header is sticky on your website.
@@ -195,14 +203,13 @@ export declare const jobSearchPlugin: () => ({ apiClient, logger, analytics }: {
195
203
  id: number;
196
204
  createdAt: string | null;
197
205
  company: {
198
- /** Whether users can search from the available results.
199
- * @default false
200
- */
201
206
  name: string;
202
207
  logo: string | undefined;
203
208
  };
204
209
  employmentType: string;
205
- locationType: "LOCATION" | "REMOTE" | "HYBRID";
210
+ locationType: "LOCATION" | "REMOTE" | "HYBRID"; /** Whether users can search from the available results.
211
+ * @default false
212
+ */
206
213
  pay: string | undefined;
207
214
  description: string;
208
215
  title: string;