@phatvu/web-component-poc 1.0.5 → 1.0.7

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 (55) hide show
  1. package/dist/cjs/fast-button.cjs.entry.js +46 -0
  2. package/dist/cjs/{fast-button_4.cjs.entry.js → fast-carousel.cjs.entry.js} +1 -231
  3. package/dist/cjs/fast-input_4.cjs.entry.js +499 -0
  4. package/dist/cjs/{index-B2BTpdbN.js → index-BEvZs91D.js} +2 -2
  5. package/dist/cjs/job-card.cjs.entry.js +138 -0
  6. package/dist/cjs/loader.cjs.js +2 -2
  7. package/dist/cjs/web-component-poc.cjs.js +2 -2
  8. package/dist/collection/collection-manifest.json +4 -1
  9. package/dist/collection/components/button/button.js +2 -2
  10. package/dist/collection/components/fast-input/fast-input.css +55 -0
  11. package/dist/collection/components/fast-input/fast-input.js +335 -0
  12. package/dist/collection/components/job-card/job-card.css +247 -0
  13. package/dist/collection/components/job-card/job-card.js +435 -0
  14. package/dist/collection/components/jobs-item/jobs-item.js +5 -5
  15. package/dist/collection/components/jobs-list-only/jobs-list-only.js +185 -8
  16. package/dist/collection/components/jobs-list-reactive/jobs-list-reactive.css +8 -0
  17. package/dist/collection/components/jobs-list-reactive/jobs-list-reactive.js +203 -0
  18. package/dist/components/fast-button.js +1 -1
  19. package/dist/components/fast-carousel.js +1 -1
  20. package/dist/components/fast-input.d.ts +11 -0
  21. package/dist/components/fast-input.js +1 -0
  22. package/dist/components/index.js +1 -1
  23. package/dist/components/job-card.d.ts +11 -0
  24. package/dist/components/job-card.js +1 -0
  25. package/dist/components/jobs-item.js +1 -1
  26. package/dist/components/jobs-list-only.js +1 -1
  27. package/dist/components/jobs-list-reactive.d.ts +11 -0
  28. package/dist/components/jobs-list-reactive.js +1 -0
  29. package/dist/components/{p-ClQDwJJB.js → p-DQiaLjLf.js} +1 -1
  30. package/dist/esm/fast-button.entry.js +44 -0
  31. package/dist/esm/{fast-button_4.entry.js → fast-carousel.entry.js} +2 -229
  32. package/dist/esm/fast-input_4.entry.js +494 -0
  33. package/dist/esm/{index-Dk5CvWmb.js → index-C_ZLQIpp.js} +2 -2
  34. package/dist/esm/job-card.entry.js +136 -0
  35. package/dist/esm/loader.js +3 -3
  36. package/dist/esm/web-component-poc.js +3 -3
  37. package/dist/types/components/fast-input/fast-input.d.ts +37 -0
  38. package/dist/types/components/job-card/job-card.d.ts +93 -0
  39. package/dist/types/components/jobs-item/jobs-item.d.ts +2 -2
  40. package/dist/types/components/jobs-list-only/jobs-list-only.d.ts +24 -2
  41. package/dist/types/components/jobs-list-reactive/jobs-list-reactive.d.ts +26 -0
  42. package/dist/types/components.d.ts +469 -7
  43. package/dist/types/mock/jobs-list-only.mock.d.ts +2 -2
  44. package/dist/types/types/jobs-list.d.ts +6 -2
  45. package/dist/web-component-poc/p-618fba28.entry.js +1 -0
  46. package/dist/web-component-poc/p-7d45772f.entry.js +1 -0
  47. package/dist/web-component-poc/p-bef7c8e2.entry.js +1 -0
  48. package/dist/web-component-poc/p-cfb9aed9.entry.js +1 -0
  49. package/dist/web-component-poc/web-component-poc.esm.js +1 -1
  50. package/hydrate/index.js +534 -6
  51. package/hydrate/index.mjs +534 -6
  52. package/package.json +9 -1
  53. package/dist/web-component-poc/p-df843533.entry.js +0 -1
  54. /package/dist/components/{p-UM9TUfe3.js → p-BiaJAQXY.js} +0 -0
  55. /package/dist/web-component-poc/{p-Dk5CvWmb.js → p-C_ZLQIpp.js} +0 -0
package/hydrate/index.mjs CHANGED
@@ -130,7 +130,7 @@ function hydrateFactory($stencilWindow, $stencilHydrateOpts, $stencilHydrateResu
130
130
 
131
131
 
132
132
  const NAMESPACE = 'web-component-poc';
133
- const BUILD = /* web-component-poc */ { hotModuleReplacement: false, hydratedSelectorName: "hydrated", prop: true, propChangeCallback: false, shadowDom: false, slotRelocation: true, updatable: true};
133
+ const BUILD = /* web-component-poc */ { hotModuleReplacement: false, hydratedSelectorName: "hydrated", propChangeCallback: false, shadowDom: false, slotRelocation: true, state: true, updatable: true};
134
134
 
135
135
  /*
136
136
  Stencil Hydrate Platform v4.43.2 | MIT Licensed | https://stenciljs.com
@@ -4505,7 +4505,7 @@ var registerInstance = (lazyInstance, hostRef) => {
4505
4505
  if (!hostRef) return void 0;
4506
4506
  lazyInstance.__stencil__getHostRef = () => hostRef;
4507
4507
  hostRef.$lazyInstance$ = lazyInstance;
4508
- if (hostRef.$cmpMeta$.$flags$ & 512 /* hasModernPropertyDecls */ && (BUILD.prop)) {
4508
+ if (hostRef.$cmpMeta$.$flags$ & 512 /* hasModernPropertyDecls */ && (BUILD.state)) {
4509
4509
  reWireGetterSetter(lazyInstance, hostRef);
4510
4510
  }
4511
4511
  return hostRef;
@@ -6505,6 +6505,334 @@ class CustomButton {
6505
6505
  }; }
6506
6506
  }
6507
6507
 
6508
+ const fastInputCss = () => `.fast-input{position:relative;display:inline-flex;gap:0.5rem}.fast-input__field{flex:1;padding:0.5rem 0.75rem;font-size:1rem;border:1px solid #ccc;border-radius:4px}.fast-input__button{padding:0.5rem 1rem;font-size:1rem;cursor:pointer;background:#0070f3;color:#fff;border:none;border-radius:4px}.fast-input__dropdown{position:absolute;top:100%;left:0;right:0;margin:0;padding:0;list-style:none;background:#fff;border:1px solid #ccc;border-top:none;border-radius:0 0 4px 4px;z-index:100;max-height:200px;overflow-y:auto}.fast-input__dropdown-item{padding:0.5rem 0.75rem;cursor:pointer}.fast-input__dropdown-item:hover{background:#f0f0f0}.fast-input__dropdown-loading{padding:0.5rem 0.75rem;color:#999;font-style:italic}`;
6509
+
6510
+ class FastInput {
6511
+ constructor(hostRef) {
6512
+ registerInstance(this, hostRef);
6513
+ this.searchExecuted = createEvent(this, "searchExecuted");
6514
+ this.inputChanged = createEvent(this, "inputChanged");
6515
+ }
6516
+ placeholder = 'Search...';
6517
+ value = '';
6518
+ paramName = 'keyword';
6519
+ enableAutocomplete = false;
6520
+ autocompleteUrl = '/api/jobs/autocomplete';
6521
+ targetPath;
6522
+ debounceMs = 300;
6523
+ minChars = 3;
6524
+ searchExecuted;
6525
+ inputChanged;
6526
+ inputValue = '';
6527
+ suggestions = [];
6528
+ showDropdown = false;
6529
+ autocompleteLoading = false;
6530
+ debounceTimer;
6531
+ popstateHandler;
6532
+ connectedCallback() {
6533
+ const urlValue = this.getUrlParam();
6534
+ this.inputValue = urlValue !== null ? urlValue : this.value;
6535
+ this.popstateHandler = () => {
6536
+ this.inputValue = this.getUrlParam() ?? '';
6537
+ };
6538
+ window.addEventListener('popstate', this.popstateHandler);
6539
+ }
6540
+ disconnectedCallback() {
6541
+ window.removeEventListener('popstate', this.popstateHandler);
6542
+ clearTimeout(this.debounceTimer);
6543
+ }
6544
+ getUrlParam() {
6545
+ const params = new URLSearchParams(window.location.search);
6546
+ return params.get(this.paramName);
6547
+ }
6548
+ updateUrlParam(value) {
6549
+ const params = new URLSearchParams(window.location.search);
6550
+ if (value) {
6551
+ params.set(this.paramName, value);
6552
+ }
6553
+ else {
6554
+ params.delete(this.paramName);
6555
+ }
6556
+ const qs = params.toString();
6557
+ const newUrl = qs
6558
+ ? `${window.location.pathname}?${qs}`
6559
+ : window.location.pathname;
6560
+ history.pushState({}, '', newUrl);
6561
+ }
6562
+ submit() {
6563
+ this.updateUrlParam(this.inputValue);
6564
+ document.dispatchEvent(new CustomEvent('search-executed', {
6565
+ detail: { keyword: this.inputValue },
6566
+ bubbles: true,
6567
+ composed: true,
6568
+ }));
6569
+ this.searchExecuted.emit({ keyword: this.inputValue });
6570
+ this.showDropdown = false;
6571
+ }
6572
+ handleInput = (e) => {
6573
+ const value = e.target.value;
6574
+ this.inputValue = value;
6575
+ this.inputChanged.emit({ value });
6576
+ if (this.enableAutocomplete) {
6577
+ this.scheduleAutocomplete(value);
6578
+ }
6579
+ };
6580
+ handleKeydown = (e) => {
6581
+ if (e.key === 'Enter') {
6582
+ this.submit();
6583
+ }
6584
+ else if (e.key === 'Escape') {
6585
+ this.showDropdown = false;
6586
+ }
6587
+ };
6588
+ handleBlur = () => {
6589
+ this.showDropdown = false;
6590
+ };
6591
+ scheduleAutocomplete(value) {
6592
+ clearTimeout(this.debounceTimer);
6593
+ if (value.length < this.minChars) {
6594
+ this.showDropdown = false;
6595
+ return;
6596
+ }
6597
+ this.debounceTimer = setTimeout(() => this.fetchSuggestions(value), this.debounceMs);
6598
+ }
6599
+ async fetchSuggestions(keyword) {
6600
+ if (!this.targetPath) {
6601
+ console.warn('[fast-input] target-path is required for autocomplete');
6602
+ return;
6603
+ }
6604
+ this.autocompleteLoading = true;
6605
+ this.showDropdown = true;
6606
+ try {
6607
+ const res = await fetch(this.autocompleteUrl, {
6608
+ method: 'POST',
6609
+ headers: { 'Content-Type': 'application/json' },
6610
+ body: JSON.stringify({ keyword, target_path: this.targetPath }),
6611
+ });
6612
+ if (!res.ok)
6613
+ throw new Error('autocomplete request failed');
6614
+ const data = await res.json();
6615
+ this.suggestions = data;
6616
+ }
6617
+ catch {
6618
+ this.showDropdown = false;
6619
+ this.suggestions = [];
6620
+ }
6621
+ finally {
6622
+ this.autocompleteLoading = false;
6623
+ }
6624
+ }
6625
+ selectSuggestion(title) {
6626
+ this.inputValue = title;
6627
+ this.showDropdown = false;
6628
+ this.submit();
6629
+ }
6630
+ render() {
6631
+ return (hAsync("div", { key: '3a9d31c7b109205600addc326d63979585f10bcd', class: "fast-input" }, hAsync("input", { key: '8f238fe9e002f367d4939616be8c06d938d76045', type: "text", class: "fast-input__field", placeholder: this.placeholder, value: this.inputValue, onInput: this.handleInput, onKeyDown: this.handleKeydown, onBlur: this.handleBlur }), hAsync("button", { key: '7b7404f13432750ece669da8ce68be15179921de', class: "fast-input__button", type: "button", onClick: () => this.submit() }, "Search"), this.enableAutocomplete && this.showDropdown && (hAsync("ul", { key: '1438bacadc21c183842a8bdaa3f336bffb152e14', class: "fast-input__dropdown" }, this.autocompleteLoading ? (hAsync("li", { class: "fast-input__dropdown-loading" }, "Loading...")) : (this.suggestions.map(s => (hAsync("li", { class: "fast-input__dropdown-item", onMouseDown: e => { e.preventDefault(); }, onClick: () => this.selectSuggestion(s.title) }, s.title))))))));
6632
+ }
6633
+ static get style() { return fastInputCss(); }
6634
+ static get cmpMeta() { return {
6635
+ "$flags$": 512,
6636
+ "$tagName$": "fast-input",
6637
+ "$members$": {
6638
+ "placeholder": [1],
6639
+ "value": [1],
6640
+ "paramName": [1, "param-name"],
6641
+ "enableAutocomplete": [4, "enable-autocomplete"],
6642
+ "autocompleteUrl": [1, "autocomplete-url"],
6643
+ "targetPath": [1, "target-path"],
6644
+ "debounceMs": [2, "debounce-ms"],
6645
+ "minChars": [2, "min-chars"],
6646
+ "inputValue": [32],
6647
+ "suggestions": [32],
6648
+ "showDropdown": [32],
6649
+ "autocompleteLoading": [32]
6650
+ },
6651
+ "$listeners$": undefined,
6652
+ "$lazyBundleId$": "-",
6653
+ "$attrsToReflect$": []
6654
+ }; }
6655
+ }
6656
+
6657
+ const jobCardCss = () => `.job-card{display:block;padding:16px;border:1px solid #e0e0e0;border-radius:8px;background-color:#fff;box-shadow:0 2px 4px rgba(0, 0, 0, 0.08);transition:box-shadow 0.2s ease, border-color 0.2s ease}.job-card:hover{box-shadow:0 4px 8px rgba(0, 0, 0, 0.12);border-color:#d0d0d0}.job-card__header{margin-bottom:12px}.job-card__title{margin:0;font-size:18px;font-weight:700;display:flex;align-items:center;flex-wrap:wrap;gap:8px}.job-card__title--link{text-decoration:none;color:#1f9755;transition:color 0.2s ease}.job-card__title--link:hover{text-decoration:underline;color:#1a7a43}.job-card__reference{font-size:0.875em;color:#666;background-color:#f5f5f5;padding:2px 6px;border-radius:3px}.job-card__reference--empty{display:none}.job-card__remote{background:#e8f5e9;color:#2e7d32;border-radius:100px;padding:4px 12px;text-transform:uppercase;font-size:11px;font-weight:700;line-height:1.5;white-space:nowrap}.job-card__remote--empty{display:none}.job-card__distance{display:inline-flex;align-items:center;gap:4px;margin-top:6px;font-size:13px;font-weight:500;color:#555}.job-card__distance--icon{display:inline-flex;align-items:center}.job-card__distance--icon svg{width:16px;height:16px;color:#1f9755}.job-card__content{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.job-card__info{flex:1;display:flex;flex-direction:column;gap:8px}.job-card__street,.job-card__brand,.job-card__employment-type{display:flex;align-items:center;flex-wrap:wrap;gap:4px 6px;font-size:14px}.job-card__street--empty,.job-card__brand--empty,.job-card__employment-type--empty{color:#999}.job-card__street--icon,.job-card__brand--icon,.job-card__employment-type--icon{display:inline-flex;align-items:center;flex-shrink:0}.job-card__street--icon svg,.job-card__brand--icon svg,.job-card__employment-type--icon svg{width:16px;height:16px;color:#666}.job-card__street--label,.job-card__brand--label,.job-card__employment-type--label{color:#333}.job-card__street--label__wrapper{display:flex;align-items:center;gap:6px}.job-card__street--more-locations__wrapper{display:flex;align-items:center;gap:2px;font-size:12px;margin-left:2px}.job-card__street--amount{font-weight:600;color:#1f9755}.job-card__street--more-locations{color:#999}.job-card__apply{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:10px 16px;background-color:#198754;color:#fff;border-radius:4px;text-decoration:none;font-weight:600;font-size:14px;transition:background-color 0.2s ease, transform 0.1s ease;white-space:nowrap;flex-shrink:0}.job-card__apply:hover{background-color:#1a6f47;transform:translateY(-1px)}.job-card__apply:active{transform:translateY(0)}.job-card__apply--icon{display:inline-flex;align-items:center}.job-card__apply--icon svg{width:14px;height:14px}@media (max-width: 768px){.job-card{padding:12px}.job-card__content{flex-direction:column;gap:10px}.job-card__apply{width:100%;justify-content:center}.job-card__title{font-size:16px}}@media (max-width: 480px){.job-card{padding:10px}.job-card__title{font-size:15px}.job-card__distance{font-size:12px}.job-card__street,.job-card__brand,.job-card__employment-type{font-size:13px}}`;
6658
+
6659
+ /**
6660
+ * Helper function to get formatted location label from location object
6661
+ */
6662
+ function getLocationLabel$1(loc) {
6663
+ if (loc.cityStateAbbr)
6664
+ return loc.cityStateAbbr;
6665
+ const parts = [loc.streetAddress, loc.city, loc.stateAbbr || loc.state, loc.countryAbbr || loc.country].filter(Boolean);
6666
+ return parts.join(', ') || loc.locationText || '';
6667
+ }
6668
+ /**
6669
+ * Helper function to get the first location from job locations
6670
+ */
6671
+ function getFirstLocation$1(job) {
6672
+ const locs = job.locations;
6673
+ if (!locs?.length)
6674
+ return undefined;
6675
+ return locs[0];
6676
+ }
6677
+ /**
6678
+ * JobCard Component
6679
+ *
6680
+ * A reusable card component for displaying job information based on the React JobItem component.
6681
+ * Features include:
6682
+ * - Job title with apply link
6683
+ * - Location with distance
6684
+ * - Brand/company name
6685
+ * - Employment type
6686
+ * - Reference number (optional)
6687
+ * - Remote indicator (optional)
6688
+ * - Multi-location support
6689
+ * - Custom extra fields
6690
+ *
6691
+ * @example
6692
+ * ```html
6693
+ * <job-card
6694
+ * .job={jobData}
6695
+ * applyButtonText="Apply"
6696
+ * showBrand="true"
6697
+ * showEmploymentType="true"
6698
+ * ></job-card>
6699
+ * ```
6700
+ */
6701
+ class JobCard {
6702
+ constructor(hostRef) {
6703
+ registerInstance(this, hostRef);
6704
+ }
6705
+ /**
6706
+ * The job data object to display. Accepts either a Job object or a JSON string.
6707
+ */
6708
+ job;
6709
+ /**
6710
+ * Index of the job in a list (used for accessibility)
6711
+ */
6712
+ index = 0;
6713
+ /**
6714
+ * Text for the apply button
6715
+ */
6716
+ applyButtonText = 'Apply Now';
6717
+ /**
6718
+ * Whether to show the brand/company name
6719
+ */
6720
+ showBrand = true;
6721
+ /**
6722
+ * Whether to show the reference number
6723
+ */
6724
+ showReference = false;
6725
+ /**
6726
+ * Whether to show employment type
6727
+ */
6728
+ showEmploymentType = true;
6729
+ /**
6730
+ * Text shown for multiple locations
6731
+ */
6732
+ multiLocationText = 'More locations';
6733
+ /**
6734
+ * Text shown for remote jobs
6735
+ */
6736
+ remoteLocationText = 'Remote';
6737
+ /**
6738
+ * Whether to show distances in kilometers instead of miles
6739
+ */
6740
+ enableKilometers = false;
6741
+ /**
6742
+ * Whether to show commute time
6743
+ */
6744
+ showCommuteTime = false;
6745
+ /**
6746
+ * Format string for street address (not used in base web component)
6747
+ */
6748
+ streetFormat = '{street}, {city_state_abbr}';
6749
+ /** Extra CSS class on the root inner element (avoid prop name "className", reserved on HTMLElement). */
6750
+ rootClass = '';
6751
+ /**
6752
+ * Custom extra fields configuration
6753
+ */
6754
+ extraFieldsConfig = [];
6755
+ /**
6756
+ * Format distance with unit
6757
+ */
6758
+ formatDistance(distance) {
6759
+ const value = this.enableKilometers ? distance * 1.60934 : distance;
6760
+ const unit = this.enableKilometers ? 'Km' : 'Miles';
6761
+ const str = `${value.toFixed(1)} ${unit}`.replace('.0', '');
6762
+ return str;
6763
+ }
6764
+ /**
6765
+ * Is Empty utility
6766
+ */
6767
+ isEmpty(value) {
6768
+ if (value === null || value === undefined || value === '') {
6769
+ return true;
6770
+ }
6771
+ if (Array.isArray(value) && value.length === 0) {
6772
+ return true;
6773
+ }
6774
+ return false;
6775
+ }
6776
+ /**
6777
+ * Parse job data from prop - handles both object and JSON string
6778
+ */
6779
+ getJobData() {
6780
+ if (!this.job)
6781
+ return null;
6782
+ if (typeof this.job === 'string') {
6783
+ try {
6784
+ return JSON.parse(this.job);
6785
+ }
6786
+ catch {
6787
+ console.warn('job-card: Failed to parse job JSON string');
6788
+ return null;
6789
+ }
6790
+ }
6791
+ return this.job;
6792
+ }
6793
+ render() {
6794
+ const job = this.getJobData();
6795
+ if (!job)
6796
+ return null;
6797
+ const firstLoc = getFirstLocation$1(job);
6798
+ const locationLabel = firstLoc ? getLocationLabel$1(firstLoc) : '';
6799
+ const distance = firstLoc?.distance ?? 0;
6800
+ const distanceLabel = distance > 0 ? this.formatDistance(distance) : '';
6801
+ const applyHref = job.applyURL ||
6802
+ (job.originalURL ? `${typeof window !== 'undefined' ? window.location.origin : ''}${job.originalURL}` : '#');
6803
+ const applyLabel = `${this.applyButtonText}, ${job.title || ''}`;
6804
+ const locs = job.locations ?? [];
6805
+ const hasMultipleLocations = locs.length > 1;
6806
+ return (hAsync("div", { class: `job-card ${this.rootClass}`.trim() }, hAsync("div", { class: "job-card__header" }, hAsync("h3", { class: "job-card__title" }, hAsync("a", { class: "job-card__title--link", href: applyHref, target: "_blank", rel: "noopener noreferrer" }, job.title || ''), this.showReference && (hAsync("span", { class: `job-card__reference ${job.reference ? '' : 'job-card__reference--empty'}` }, job.reference || '')), job.isRemote && (hAsync("span", { class: this.remoteLocationText ? 'job-card__remote' : 'job-card__remote job-card__remote--empty' }, this.remoteLocationText))), distanceLabel && (hAsync("div", { class: "job-card__distance" }, hAsync("span", { class: "job-card__distance--icon" }, hAsync("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.5" }, hAsync("path", { "stroke-linecap": "round", "stroke-linejoin": "round", d: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18z" }), hAsync("path", { "stroke-linecap": "round", "stroke-linejoin": "round", d: "M12 8v4l2 2" }))), hAsync("span", { class: "job-card__distance--label" }, distanceLabel)))), hAsync("div", { class: "job-card__content" }, hAsync("div", { class: "job-card__info" }, hAsync("div", { class: locs.length ? 'job-card__street' : 'job-card__street job-card__street--empty' }, hAsync("div", { class: "job-card__street--label__wrapper" }, hAsync("span", { class: "job-card__street--icon" }, hAsync("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.5" }, hAsync("path", { "stroke-linecap": "round", "stroke-linejoin": "round", d: "M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0z" }), hAsync("path", { "stroke-linecap": "round", "stroke-linejoin": "round", d: "M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0z" }))), hAsync("span", { class: "job-card__street--label" }, locationLabel || '—')), hasMultipleLocations && (hAsync("div", { class: "job-card__street--more-locations__wrapper" }, hAsync("span", { class: "job-card__street--amount" }, "+", locs.length - 1), hAsync("span", { class: "job-card__street--more-locations" }, this.multiLocationText)))), this.showBrand && (hAsync("div", { class: job.brandName ? 'job-card__brand' : 'job-card__brand job-card__brand--empty' }, hAsync("span", { class: "job-card__brand--icon" }, hAsync("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.5" }, hAsync("path", { "stroke-linecap": "round", "stroke-linejoin": "round", d: "M2.25 21h19.5m-18-18v18m10.5-18v18m6-13.5V21M6.75 6.75h.75m-.75 3h.75m-.75 3h.75m3-6h.75m-.75 3h.75m-.75 3h.75M6.75 21v-3.375c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21M3 3h12m-.75 4.5H21m-3.75 3.75h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008z" }))), hAsync("span", { class: "job-card__brand--label" }, job.brandName || '—'))), this.showEmploymentType && (hAsync("div", { class: job.employmentType?.length
6807
+ ? 'job-card__employment-type'
6808
+ : 'job-card__employment-type job-card__employment-type--empty' }, hAsync("span", { class: "job-card__employment-type--icon" }, hAsync("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.5" }, hAsync("path", { "stroke-linecap": "round", "stroke-linejoin": "round", d: "M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0z" }))), job.employmentType?.length ? (job.employmentType?.map((type) => (hAsync("span", { key: type, class: "job-card__employment-type--label" }, type)))) : (hAsync("span", { class: "job-card__employment-type--label" }, "\u2014")))), (job.jobCardExtraFields ?? []).length > 0 &&
6809
+ job.jobCardExtraFields?.map((field, idx) => (hAsync("div", { key: idx, class: !this.isEmpty(field.value) ? `${field.classname}` : `${field.classname}--empty` }, Array.isArray(field.value) ? (field.value.map((item, itemIdx) => (hAsync("span", { key: itemIdx, class: `${field.classname}--label` }, item)))) : (hAsync("span", { class: `${field.classname}--label` }, field.value)))))), hAsync("a", { class: "job-card__apply", href: applyHref, target: "_blank", rel: "noopener noreferrer", "aria-label": applyLabel }, hAsync("span", { class: "job-card__apply--label" }, this.applyButtonText), hAsync("span", { class: "job-card__apply--icon" }, hAsync("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, hAsync("path", { "stroke-linecap": "round", "stroke-linejoin": "round", d: "M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" })))))));
6810
+ }
6811
+ static get style() { return jobCardCss(); }
6812
+ static get cmpMeta() { return {
6813
+ "$flags$": 512,
6814
+ "$tagName$": "job-card",
6815
+ "$members$": {
6816
+ "job": [1],
6817
+ "index": [2],
6818
+ "applyButtonText": [1, "apply-button-text"],
6819
+ "showBrand": [4, "show-brand"],
6820
+ "showReference": [4, "show-reference"],
6821
+ "showEmploymentType": [4, "show-employment-type"],
6822
+ "multiLocationText": [1, "multi-location-text"],
6823
+ "remoteLocationText": [1, "remote-location-text"],
6824
+ "enableKilometers": [4, "enable-kilometers"],
6825
+ "showCommuteTime": [4, "show-commute-time"],
6826
+ "streetFormat": [1, "street-format"],
6827
+ "rootClass": [1, "root-class"],
6828
+ "extraFieldsConfig": [16]
6829
+ },
6830
+ "$listeners$": undefined,
6831
+ "$lazyBundleId$": "-",
6832
+ "$attrsToReflect$": []
6833
+ }; }
6834
+ }
6835
+
6508
6836
  const jobsItemCss = () => `.results-list__item{list-style:none;padding:10px 0;border-bottom:1px solid #ddd;margin:15px 0;display:inline-block;width:100%;position:relative}.results-list__item:last-child{border-bottom:none}.results-list__item-header{margin:10px 0;font-size:18px;font-weight:700;display:flex;flex-direction:column}.results-list__item-title{margin:0}.results-list__item-title--link{text-decoration:none;color:#1f9755}.results-list__item-title--link:hover{text-decoration:underline}.reference{margin-left:8px;font-size:0.9em;color:#666}.reference.empty{display:none}.remote{background:#f3f3f3;color:#808285;border-radius:100px;padding:6px 16px;text-transform:uppercase;font-size:12px;font-weight:700;line-height:24px;margin-left:8px}.remote--empty{display:none}.results-list__item-distance{display:inline-flex;align-items:center;gap:4px;margin-top:4px;font-size:14px;font-weight:400}.results-list__item-distance--icon{display:inline-flex}.results-list__item-distance--icon svg{width:16px;height:16px}.results-list__item-content{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-top:8px}.results-list__item-info{flex:1}.results-list__item-street,.results-list__item-brand,.results-list__item-employment-type{margin:10px 0;display:flex;flex-wrap:wrap;align-items:center;gap:4px 8px}.results-list__item-street--empty,.results-list__item-brand--empty,.results-list__item-employment-type--empty{color:#999}.results-list__item-street--icon,.results-list__item-brand--icon,.results-list__item-employment-type--icon{margin-right:6px;display:inline-flex}.results-list__item-street--icon svg,.results-list__item-brand--icon svg,.results-list__item-employment-type--icon svg{width:16px;height:16px}.results-list__item-street--more-locations__wrapper{margin-left:8px}.results-list__item-street--amount{font-weight:600}.results-list__item-apply{margin:10px 0;padding:10px 20px;display:inline-flex;align-items:center;gap:8px;background-color:#198754;color:#fff;border-radius:3px;text-decoration:none;font-weight:600;flex-shrink:0}.results-list__item-apply:hover{background-color:#1f9755;color:#fff}.results-list__item-apply--icon svg{width:14px;height:14px}`;
6509
6837
 
6510
6838
  function getLocationLabel(loc) {
@@ -6648,6 +6976,7 @@ const defaultNoResultsLine2 = 'Please refine your keywords in the search bar abo
6648
6976
  class JobsListOnly {
6649
6977
  constructor(hostRef) {
6650
6978
  registerInstance(this, hostRef);
6979
+ this.fetchComplete = createEvent(this, "fetchComplete");
6651
6980
  }
6652
6981
  /**
6653
6982
  * When "true", use built-in mock data (for local/dev/docs). Otherwise use `jobs` from API/parent.
@@ -6671,16 +7000,84 @@ class JobsListOnly {
6671
7000
  enableKilometers = false;
6672
7001
  /** Extra CSS class on the root element (avoid prop name "class" / "classname" reserved). */
6673
7002
  rootClass = '';
7003
+ /** Template string for count display. Tokens: {count} = jobs on page, {total} = total from API. */
7004
+ showCountText = '';
6674
7005
  showSuggestions = false;
6675
7006
  clearResultSuggestionsTitleText = 'Suggestions';
6676
7007
  clearResultSuggestionsLine1 = 'Try different keywords';
6677
7008
  clearResultSuggestionsLine2 = 'Make sure everything is spelled correctly';
6678
7009
  clearResultSuggestionsLine3 = 'Try other locations';
6679
7010
  clearResultSuggestionsLine4 = '';
7011
+ /** When true, component manages its own data fetching */
7012
+ autoFetch = false;
7013
+ /** Jobs search endpoint */
7014
+ apiUrl = '/api/get-jobs';
7015
+ /** Comma-separated URL param names to watch and forward to the API */
7016
+ watchParams = 'keyword';
7017
+ fetchedJobs = [];
7018
+ fetchedTotal = 0;
7019
+ fetchLoading = false;
7020
+ fetchComplete;
7021
+ searchExecutedHandler;
7022
+ popstateHandler;
7023
+ connectedCallback() {
7024
+ if (this.autoFetch) {
7025
+ this.fetchJobs();
7026
+ this.searchExecutedHandler = () => this.fetchJobs();
7027
+ this.popstateHandler = () => this.fetchJobs();
7028
+ document.addEventListener('search-executed', this.searchExecutedHandler);
7029
+ window.addEventListener('popstate', this.popstateHandler);
7030
+ }
7031
+ }
7032
+ disconnectedCallback() {
7033
+ if (this.autoFetch) {
7034
+ document.removeEventListener('search-executed', this.searchExecutedHandler);
7035
+ window.removeEventListener('popstate', this.popstateHandler);
7036
+ }
7037
+ }
7038
+ async fetchJobs() {
7039
+ this.fetchLoading = true;
7040
+ const params = new URLSearchParams(window.location.search);
7041
+ const watchList = this.watchParams.split(',').map(p => p.trim()).filter(Boolean);
7042
+ const query = new URLSearchParams();
7043
+ for (const key of watchList) {
7044
+ const val = params.get(key);
7045
+ if (val !== null)
7046
+ query.set(key, val);
7047
+ }
7048
+ const url = `${this.apiUrl}?${query.toString()}`;
7049
+ try {
7050
+ const res = await fetch(url, {
7051
+ method: 'POST',
7052
+ headers: { 'Content-Type': 'application/json' },
7053
+ body: JSON.stringify({ disable_switch_search_mode: false }),
7054
+ });
7055
+ if (!res.ok)
7056
+ throw new Error('fetch failed');
7057
+ const data = await res.json();
7058
+ this.fetchedJobs = data.jobs;
7059
+ this.fetchedTotal = data.totalJob;
7060
+ this.fetchComplete.emit({ jobs: data.jobs, totalJob: data.totalJob });
7061
+ }
7062
+ catch {
7063
+ // preserve stale data, just stop loading
7064
+ }
7065
+ finally {
7066
+ this.fetchLoading = false;
7067
+ }
7068
+ }
7069
+ renderCountText(count, total) {
7070
+ return this.showCountText
7071
+ .replace('{count}', String(count))
7072
+ .replace('{total}', String(total));
7073
+ }
6680
7074
  getJobsArray() {
6681
7075
  if (this.mockData) {
6682
7076
  return mockJobsListOnly;
6683
7077
  }
7078
+ if (this.autoFetch) {
7079
+ return this.fetchedJobs;
7080
+ }
6684
7081
  const j = this.jobs;
6685
7082
  if (Array.isArray(j))
6686
7083
  return j;
@@ -6700,11 +7097,15 @@ class JobsListOnly {
6700
7097
  }
6701
7098
  render() {
6702
7099
  const jobsArray = this.getJobsArray();
6703
- const loading = this.mockData ? false : this.loading;
6704
- const totalJob = this.mockData ? jobsArray.length : (this.totalJob || jobsArray.length);
7100
+ const loading = this.mockData ? false : (this.autoFetch ? this.fetchLoading : this.loading);
7101
+ const totalJob = this.mockData
7102
+ ? jobsArray.length
7103
+ : this.autoFetch
7104
+ ? this.fetchedTotal
7105
+ : (this.totalJob || jobsArray.length);
6705
7106
  const showNoResults = !loading && totalJob === 0 && !this.showSuggestions;
6706
7107
  const showSuggestionsBlock = !loading && totalJob === 0 && this.showSuggestions;
6707
- return (hAsync("div", { key: '9e69d40c649b4f16617f522c89bc87d4cbffe1e3', class: `jobs-list-root ${this.rootClass}`.trim() }, hAsync("div", { key: '7daf1c09dd1d5553a335f3c81c894a331964ee96', class: "results-container" }, hAsync("div", { key: '9ab3db2b834359a64faee61f8c3dc2daca8211d9', class: loading ? 'loader' : 'loader hide', "aria-hidden": !loading }), totalJob > 0 && (hAsync("div", { key: 'c729ccdf715105b9fbc7af0a1f148c82535772c1', class: "card" }, hAsync("ul", { key: 'b0c5b6baccf7b94819cc2ad9ab970981a5294b42', class: "results-list front" }, jobsArray.map((job, index) => this.renderJobItem(job, index))))), showNoResults && (hAsync("div", { key: '8f1684ddde51474e9c1699f93fb01c56e5406a51', class: "share-jobs__no-results" }, hAsync("h2", { key: '2e3bc4b6b7647e3939588102d659e777207e46e9' }, this.noResultsLine1), hAsync("h3", { key: 'f4881f19ad5db8fdd47aa4d31e1e0435f08da1ab' }, this.noResultsLine2))), showSuggestionsBlock && (hAsync("div", { key: 'ed34907fd90f28571786045f21525895cef6e98a', class: "card primary-color" }, hAsync("h4", { key: '33e96147f1b427df874656c1e0647af1d97fea29', class: "result-suggestions-title" }, this.clearResultSuggestionsTitleText, ":"), hAsync("ul", { key: '570fd8e5068f1a7c809fad47347fe5f469da8bc6', class: "results-list front" }, hAsync("li", { key: 'b5d22dfb448d2888dc010d6e494d1834d3d60034', class: "result-suggestions-line" }, this.clearResultSuggestionsLine1), hAsync("li", { key: '77c1e8bbe9ab238e4f05fdbaf43551aef4f2a36e', class: "result-suggestions-line" }, this.clearResultSuggestionsLine2), hAsync("li", { key: '0bd9503bf686213ae087f74a7b5f2cc62f9c96b7', class: "result-suggestions-line" }, this.clearResultSuggestionsLine3), this.clearResultSuggestionsLine4 && (hAsync("li", { key: '961b2904347e1c5fc6b355a069790f0f2997917f', class: "result-suggestions-line" }, this.clearResultSuggestionsLine4))))))));
7108
+ return (hAsync("div", { key: '1116855473d28d650641b9d962243bfcdcb434ec', class: `jobs-list-root ${this.rootClass}`.trim() }, hAsync("div", { key: 'fcef04f1da9ad4e150af9f59921688f5781d9d43', class: "results-container" }, this.autoFetch && this.fetchLoading && (hAsync("div", { key: '75b157c82c89691c7ce73d12ea0144b3b45485c3', class: "jobs-list-only__loading" }, "Loading...")), hAsync("div", { key: 'b09f9879e10ee4a93e32177611912da5f19f3526', class: loading ? 'loader' : 'loader hide', "aria-hidden": !loading }), totalJob > 0 && this.showCountText && (hAsync("p", { key: 'd10f800fb0a33d82531d5f1728bac4ceba2ed577', class: "jobs-list-only__count" }, this.renderCountText(jobsArray.length, totalJob))), totalJob > 0 && (hAsync("div", { key: '18153ed1338bd48f7be4f043b11ce15e3271f27b', class: "card" }, hAsync("ul", { key: '766e128b1fd5adb456530ae39e92ba8eb0b5d6cf', class: "results-list front" }, jobsArray.map((job, index) => this.renderJobItem(job, index))))), showNoResults && (hAsync("div", { key: 'ed6f3d2bd2bbedabd6e69d508ea1425580e6941f', class: "share-jobs__no-results" }, hAsync("h2", { key: '2302656e33340c69e84cb949afb7256b8f35f440' }, this.noResultsLine1), hAsync("h3", { key: '1c7e6642441a96c04ee26883fdec4f81b0fe6cec' }, this.noResultsLine2))), showSuggestionsBlock && (hAsync("div", { key: 'be7af85f64455918545e88952ca6ff00f0a970c5', class: "card primary-color" }, hAsync("h4", { key: '2f63deb8131190eff882308544b15f767b6f3edc', class: "result-suggestions-title" }, this.clearResultSuggestionsTitleText, ":"), hAsync("ul", { key: 'cb18daaa9e2c2c442c5b906ed370dcd653b5262d', class: "results-list front" }, hAsync("li", { key: 'ff1d5c6518b75c0daa35b43df7162b0dfecde25e', class: "result-suggestions-line" }, this.clearResultSuggestionsLine1), hAsync("li", { key: '4099fd7bf8dcf114eca28702a498ab0938f5de46', class: "result-suggestions-line" }, this.clearResultSuggestionsLine2), hAsync("li", { key: 'fb65b54c3c0b14bc58112977eb4c7c56c1246a45', class: "result-suggestions-line" }, this.clearResultSuggestionsLine3), this.clearResultSuggestionsLine4 && (hAsync("li", { key: '10f745e74cf68a2b1c42e6f49f810a8b59eb27b6', class: "result-suggestions-line" }, this.clearResultSuggestionsLine4))))))));
6708
7109
  }
6709
7110
  static get style() { return jobsListOnlyCss(); }
6710
7111
  static get cmpMeta() { return {
@@ -6726,12 +7127,136 @@ class JobsListOnly {
6726
7127
  "remoteLocationText": [1, "remote-location-text"],
6727
7128
  "enableKilometers": [4, "enable-kilometers"],
6728
7129
  "rootClass": [1, "root-class"],
7130
+ "showCountText": [1, "show-count-text"],
6729
7131
  "showSuggestions": [4, "show-suggestions"],
6730
7132
  "clearResultSuggestionsTitleText": [1, "clear-result-suggestions-title-text"],
6731
7133
  "clearResultSuggestionsLine1": [1, "clear-result-suggestions-line-1"],
6732
7134
  "clearResultSuggestionsLine2": [1, "clear-result-suggestions-line-2"],
6733
7135
  "clearResultSuggestionsLine3": [1, "clear-result-suggestions-line-3"],
6734
- "clearResultSuggestionsLine4": [1, "clear-result-suggestions-line-4"]
7136
+ "clearResultSuggestionsLine4": [1, "clear-result-suggestions-line-4"],
7137
+ "autoFetch": [4, "auto-fetch"],
7138
+ "apiUrl": [1, "api-url"],
7139
+ "watchParams": [1, "watch-params"],
7140
+ "fetchedJobs": [32],
7141
+ "fetchedTotal": [32],
7142
+ "fetchLoading": [32]
7143
+ },
7144
+ "$listeners$": undefined,
7145
+ "$lazyBundleId$": "-",
7146
+ "$attrsToReflect$": []
7147
+ }; }
7148
+ }
7149
+
7150
+ const jobsListReactiveCss = () => `jobs-list-reactive{display:block}jobs-list-reactive.loading{opacity:0.6;pointer-events:none}`;
7151
+
7152
+ class JobsListReactive {
7153
+ constructor(hostRef) {
7154
+ registerInstance(this, hostRef);
7155
+ this.fetchComplete = createEvent(this, "fetchComplete");
7156
+ }
7157
+ get el() { return getElement(this); }
7158
+ /** Jobs search endpoint */
7159
+ apiUrl = '/api/get-jobs';
7160
+ /** Comma-separated URL param names to watch and forward to the API */
7161
+ watchParams = 'keyword,location_name';
7162
+ /** CSS class added to container while fetching */
7163
+ loadingClass = 'loading';
7164
+ isLoading = false;
7165
+ fetchComplete;
7166
+ templateEl = null;
7167
+ searchExecutedHandler;
7168
+ popstateHandler;
7169
+ connectedCallback() {
7170
+ this.templateEl = this.el.querySelector('template');
7171
+ this.searchExecutedHandler = () => this.fetchJobs();
7172
+ this.popstateHandler = () => this.fetchJobs();
7173
+ document.addEventListener('search-executed', this.searchExecutedHandler);
7174
+ window.addEventListener('popstate', this.popstateHandler);
7175
+ }
7176
+ disconnectedCallback() {
7177
+ document.removeEventListener('search-executed', this.searchExecutedHandler);
7178
+ window.removeEventListener('popstate', this.popstateHandler);
7179
+ }
7180
+ buildQueryString() {
7181
+ const urlParams = new URLSearchParams(window.location.search);
7182
+ const watchList = this.watchParams.split(',').map(p => p.trim()).filter(Boolean);
7183
+ const query = new URLSearchParams();
7184
+ for (const key of watchList) {
7185
+ const val = urlParams.get(key);
7186
+ if (val !== null && val !== '') {
7187
+ query.set(key, val);
7188
+ }
7189
+ }
7190
+ return query.toString();
7191
+ }
7192
+ async fetchJobs() {
7193
+ this.isLoading = true;
7194
+ this.el.classList.add(this.loadingClass);
7195
+ const queryString = this.buildQueryString();
7196
+ const url = queryString ? `${this.apiUrl}?${queryString}` : this.apiUrl;
7197
+ try {
7198
+ const res = await fetch(url, {
7199
+ method: 'POST',
7200
+ headers: { 'Content-Type': 'application/json' },
7201
+ body: JSON.stringify({ disable_switch_search_mode: false }),
7202
+ });
7203
+ if (!res.ok)
7204
+ throw new Error('fetch failed');
7205
+ const data = await res.json();
7206
+ this.renderJobs(data.jobs);
7207
+ this.updateCountElements(data.jobs.length, data.totalJob);
7208
+ this.fetchComplete.emit({ jobs: data.jobs, totalJob: data.totalJob });
7209
+ }
7210
+ catch {
7211
+ // Preserve stale data on error
7212
+ }
7213
+ finally {
7214
+ this.isLoading = false;
7215
+ this.el.classList.remove(this.loadingClass);
7216
+ }
7217
+ }
7218
+ renderJobs(jobs) {
7219
+ if (!this.templateEl)
7220
+ return;
7221
+ // Remove all children except the template
7222
+ const children = Array.from(this.el.children);
7223
+ for (const child of children) {
7224
+ if (child !== this.templateEl) {
7225
+ child.remove();
7226
+ }
7227
+ }
7228
+ // Clone template and render each job
7229
+ for (const job of jobs) {
7230
+ const clone = this.templateEl.content.cloneNode(true);
7231
+ const jobCard = clone.querySelector('job-card');
7232
+ if (jobCard) {
7233
+ jobCard.setAttribute('job', JSON.stringify(job));
7234
+ }
7235
+ this.el.appendChild(clone);
7236
+ }
7237
+ }
7238
+ updateCountElements(count, total) {
7239
+ const countEls = document.querySelectorAll('[data-job-count]');
7240
+ const totalEls = document.querySelectorAll('[data-job-total]');
7241
+ countEls.forEach(el => {
7242
+ el.textContent = String(count);
7243
+ });
7244
+ totalEls.forEach(el => {
7245
+ el.textContent = String(total);
7246
+ });
7247
+ }
7248
+ render() {
7249
+ return hAsync("slot", { key: '30a6fe9727eb877b6aafb99072c40811df121ba6' });
7250
+ }
7251
+ static get style() { return jobsListReactiveCss(); }
7252
+ static get cmpMeta() { return {
7253
+ "$flags$": 772,
7254
+ "$tagName$": "jobs-list-reactive",
7255
+ "$members$": {
7256
+ "apiUrl": [1, "api-url"],
7257
+ "watchParams": [1, "watch-params"],
7258
+ "loadingClass": [1, "loading-class"],
7259
+ "isLoading": [32]
6735
7260
  },
6736
7261
  "$listeners$": undefined,
6737
7262
  "$lazyBundleId$": "-",
@@ -6742,8 +7267,11 @@ class JobsListOnly {
6742
7267
  registerComponents([
6743
7268
  AppCarousel,
6744
7269
  CustomButton,
7270
+ FastInput,
7271
+ JobCard,
6745
7272
  JobsItem,
6746
7273
  JobsListOnly,
7274
+ JobsListReactive,
6747
7275
  ]);
6748
7276
 
6749
7277
  exports.hydrateApp = hydrateApp;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phatvu/web-component-poc",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Stencil Component Starter",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.js",
@@ -32,6 +32,10 @@
32
32
  "require": "./loader/index.cjs",
33
33
  "types": "./loader/index.d.ts"
34
34
  },
35
+ "./fast-input": {
36
+ "import": "./dist/components/fast-input.js",
37
+ "types": "./dist/components/fast-input.d.ts"
38
+ },
35
39
  "./jobs-list-only": {
36
40
  "import": "./dist/components/jobs-list-only.js",
37
41
  "types": "./dist/components/jobs-list-only.d.ts"
@@ -39,6 +43,10 @@
39
43
  "./jobs-item": {
40
44
  "import": "./dist/components/jobs-item.js",
41
45
  "types": "./dist/components/jobs-item.d.ts"
46
+ },
47
+ "./job-card": {
48
+ "import": "./dist/components/job-card.js",
49
+ "types": "./dist/components/job-card.d.ts"
42
50
  }
43
51
  },
44
52
  "repository": {