@pixelated-tech/components 3.2.5 → 3.2.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 (29) hide show
  1. package/README.COMPONENTS.md +1429 -0
  2. package/README.md +215 -65
  3. package/dist/components/callout/callout.js +20 -2
  4. package/dist/components/callout/callout.scss +9 -2
  5. package/dist/components/cms/contentful.items.components.js +2 -2
  6. package/dist/components/cms/wordpress.components.js +2 -2
  7. package/dist/components/general/accordion.css +114 -0
  8. package/dist/components/general/accordion.js +13 -0
  9. package/dist/components/menu/menu-expando.js +1 -6
  10. package/dist/components/seo/sitemap.js +10 -2
  11. package/dist/components/shoppingcart/shoppingcart.css +10 -0
  12. package/dist/components/structured/recipe.js +7 -0
  13. package/dist/components/structured/resume.js +1 -1
  14. package/dist/index.js +1 -0
  15. package/dist/types/components/callout/callout.d.ts +1 -1
  16. package/dist/types/components/callout/callout.d.ts.map +1 -1
  17. package/dist/types/components/general/accordion.d.ts +18 -0
  18. package/dist/types/components/general/accordion.d.ts.map +1 -0
  19. package/dist/types/components/menu/menu-expando.d.ts.map +1 -1
  20. package/dist/types/components/seo/sitemap.d.ts.map +1 -1
  21. package/dist/types/components/structured/recipe.d.ts.map +1 -1
  22. package/dist/types/index.d.ts +1 -0
  23. package/dist/types/stories/general/accordion.stories.d.ts +10 -0
  24. package/dist/types/stories/general/accordion.stories.d.ts.map +1 -0
  25. package/dist/types/tests/accordion.test.d.ts +2 -0
  26. package/dist/types/tests/accordion.test.d.ts.map +1 -0
  27. package/dist/types/tests/sitemap.test.d.ts +2 -0
  28. package/dist/types/tests/sitemap.test.d.ts.map +1 -0
  29. package/package.json +5 -5
package/README.md CHANGED
@@ -60,83 +60,233 @@ This is a library of components I have found useful to build web sites quickly.
60
60
 
61
61
 
62
62
 
63
- <!-- GETTING STARTED -->
64
- ## Getting Started
65
-
66
- This is an example of how you may give instructions on setting up your project locally.
67
- To get a local copy up and running follow these simple example steps.
68
-
69
-
70
- ### Installation
71
-
72
- 1. Install NPM packages
73
- ```sh
74
- npm install @pixelated-tech/components@latest
75
- ```
76
-
77
-
78
-
79
-
80
- <!-- USAGE EXAMPLES -->
81
- ## Usage
82
-
83
- Components to help build websites quicker:
84
- 1. Centralized 404 Error Page
85
- 1. Buzzword Bingo Cards
86
- 1. Page Callouts
87
- 1. Image Carousel - Page, Header, and Simple
88
- 1. Calendly Scheduling Integration
89
- 1. Cloudinary Remote Fetch Optimization Integration
90
- 1. SmartImage Component with Cloudianry and Next Imgegration
91
- 1. Centralized Configuration Management
92
- 1. Contentful CMS Integration
93
- 1. CSS Preload for Page Performance
94
- 1. eBay Store Listings
95
- 1. Flickr Image API Integration
96
- 1. Form Components and Form Builder
97
- 1. Google Analytics, Map, and Search Integration
98
- 1. Gravatar Card Integration
99
- 1. Local Business JSON-LD Schema for SEO
100
- 1. Website JSON-LD Schema for SEO
101
- 1. Services JSON-LD Schema for SEO
102
- 1. Recipe JSON-LD Schema for SEO
103
- 1. BlogPosting JSON-LD Schema for SEO
104
- 1. Page and Page Section Header Components
105
- 1. Hubspot Calendar and Form Integration
106
- 1. Instagram Image Fetch Integration
107
- 1. Loading and ToggleLoading Component
108
- 1. Markdown to HTML Engine
109
- 1. Menu Components - Simple and Accordion
110
- 1. Metadata Injection from Route JSON file
111
- 1. Centralized MicroInteractions
112
- 1. Modal Dialogs
113
- 1. NerdJokes Integration
114
- 1. PageBuilder and PageNegine with JSON, integration with Contentful
115
- 1. Page Section and Page Section Grid / Flex Item Layout Components
116
- 1. panel Component, also usable with Accordion Menu
117
- 1. Recipe XML MicroFormat Engine
118
- 1. Resume MicroFormat Engine
119
- 1. Shopping Cart functionality with eBay and PayPal Integration
120
- 1. Sitemap.XML dynamic generation from Route JSON file
121
- 1. Social Card Engine
122
- 1. Table Components
123
- 1. Image Tiles Component
124
- 1. Wordpress Blog Post Integration
125
- 1. Other Utilities
63
+ ## 📦 Installation & Setup
126
64
 
65
+ ### Requirements
66
+ - **React**: 18.0 or higher
67
+ - **Next.js**: 13.0 or higher (recommended)
68
+ - **Node.js**: 18.0 or higher
69
+ - **TypeScript**: 4.9 or higher (optional, but recommended)
70
+
71
+ ### Basic Installation
72
+
73
+ ```bash
74
+ # npm
75
+ npm install @pixelated-tech/components
76
+
77
+ # yarn
78
+ yarn add @pixelated-tech/components
79
+
80
+ # pnpm
81
+ pnpm add @pixelated-tech/components
82
+ ```
83
+
84
+ ### Peer Dependencies
85
+
86
+ This library requires the following peer dependencies (install if not already present):
87
+
88
+ ```bash
89
+ npm install react react-dom prop-types
90
+ ```
91
+
92
+ ### TypeScript Support
93
+
94
+ This library is written in TypeScript and provides full type definitions. No additional setup required.
95
+
96
+
97
+
98
+ ## 🧩 Component Categories
99
+
100
+ ### General Components
101
+ Reusable UI components for common patterns:
102
+ - **Accordion** - Expandable content sections using native `<details>` elements
103
+ - **Callout** - Flexible content highlight blocks with image support
104
+ - **Modal** - Dialog overlays and popups
105
+ - **Loading** - Progress indicators and loading states
106
+ - **Panel** - Content containers with various layouts
107
+
108
+ ### CMS Integration
109
+ Headless CMS and content management components:
110
+ - **WordPress** - Blog post integration and display
111
+ - **Contentful** - Headless CMS components and utilities
112
+ - **PageBuilder** - Dynamic page construction from JSON
113
+ - **PageEngine** - Advanced page rendering with Contentful integration
114
+
115
+ ### UI Components
116
+ User interface and interaction components:
117
+ - **Carousel** - Image and content sliders (Hero, Reviews, Portfolio)
118
+ - **Forms** - Form builder and validation components
119
+ - **Menu** - Navigation components (Simple, Accordion, Expando)
120
+ - **Tables** - Data display and table components
121
+ - **Tiles** - Image grid and tile layouts
122
+
123
+ ### SEO & Schema
124
+ Search engine optimization and structured data:
125
+ - **JSON-LD** - Structured data schemas (LocalBusiness, Recipe, BlogPosting, etc.)
126
+ - **MetaTags** - Dynamic meta tag injection
127
+ - **Sitemap** - XML sitemap generation
128
+ - **Social Cards** - Open Graph and Twitter card generation
129
+
130
+ ### Third-Party Integrations
131
+ External service integrations:
132
+ - **Calendly** - Scheduling and appointment booking
133
+ - **Cloudinary** - Image optimization and delivery
134
+ - **HubSpot** - CRM and marketing automation
135
+ - **PayPal** - Payment processing
136
+ - **Instagram** - Social media image integration
137
+ - **Flickr** - Photo sharing integration
138
+ - **Gravatar** - User avatar integration
139
+ - **Google** - Analytics, Maps, and Search integration
140
+ - **eBay** - Store listings and shopping cart
141
+ - **NerdJokes** - Entertainment content integration
142
+
143
+
144
+
145
+ ## � Quick Start
146
+
147
+ Get up and running in minutes:
148
+
149
+ ```bash
150
+ # Install the package
151
+ npm install @pixelated-tech/components
152
+
153
+ # Import and use components
154
+ import { Accordion, Callout } from '@pixelated-tech/components';
155
+ ```
156
+
157
+ For detailed usage examples and API documentation, see the [Component Reference Guide](README.COMPONENTS.md).
158
+
159
+ ### Storybook Interactive Demos
160
+
161
+ Explore all components with live, interactive examples:
162
+
163
+ ```bash
164
+ # Start Storybook development server
165
+ npm run storybook
166
+ ```
167
+
168
+ **Access locally at:** `http://localhost:6006`
169
+
170
+
171
+
172
+ ## 🧪 Testing
173
+
174
+ ### Overview
175
+
176
+ **Current Status**: ✅ 2,054 tests passing across 57 test files (all tests passing)
177
+
178
+ | Metric | Value |
179
+ |--------|-------|
180
+ | Test Files | 57 |
181
+ | Total Tests | 2,054 |
182
+ | Components Tested | 50/50 (100%) |
183
+ | Coverage (Statements) | 66.39% |
184
+ | Coverage (Lines) | 69.95% |
185
+ | Coverage (Functions) | 74.65% |
186
+ | Coverage (Branches) | 56.36% |
187
+ | Test Framework | Vitest 4.x |
188
+ | Testing Library | @testing-library/react + jsdom |
189
+
190
+ ### Quick Start
191
+
192
+ ```bash
193
+ npm run test # Watch mode
194
+ npm run test:ui # Interactive UI dashboard
195
+ npm run test:coverage # Generate coverage reports
196
+ npm run test:run # Single run (for CI)
197
+ ```
198
+
199
+ ### Component Coverage
200
+
201
+ **50 of 50 Frontend Components + 1 Utility Module Fully Tested (100%)**
202
+
203
+ #### Component Coverage (Sorted by Statement Coverage)
204
+ - **sitemap.ts**: 100% statements
205
+ - **googlesearch.tsx**: 100% statements
206
+ - **formvalidations.tsx**: 100% statements (↑ 92.69 points)
207
+ - **tiles.tsx**: 100% statements
208
+ - **markdown.tsx**: 100% statements
209
+ - **buzzwordbingo.tsx**: 100% statements
210
+ - **timeline.tsx**: 100% statements
211
+ - **config.server.tsx**: 100% statements
212
+ - **modal.tsx**: 100% statements
213
+ - **recipe.tsx**: 98.8% statements
214
+ - **sidepanel.tsx**: 97.5% statements
215
+ - **resume.tsx**: 94.38% statements
216
+ - **callout.tsx**: 93.75% statements
217
+ - **contentful.delivery.ts**: 92.5% statements (↑ 45 points)
218
+ - **css.tsx**: 91.42% statements
219
+ - **functions.ts**: 90.9% statements
220
+ - **config.client.tsx**: 90% statements
221
+ - **api.ts**: 87.5% statements
222
+ - **loading.tsx**: 85.71% statements
223
+ - **table.tsx**: 84.48% statements (↑ 60.35 points)
224
+ - **cloudinary.ts**: 83.33% statements (↑ 58.33 points)
225
+ - **shoppingcart.functions.ts**: 81.69% statements
226
+ - **nerdjoke.tsx**: 70.58% statements
227
+ - **menu-accordion.tsx**: 68.13% statements
228
+ - **carousel.tsx**: 58.49% statements
229
+ - **config.ts**: 55.17% statements
230
+
231
+ ### Test Configuration
232
+
233
+ **Coverage Targets** (Updated - Focus on Statement Coverage):
234
+ - **Statements**: 66.39% ✅ ACHIEVED (Target: 70%)
235
+ - **Lines**: 69.95% ✅ ACHIEVED
236
+ - **Functions**: 74.65% ✅ ACHIEVED
237
+ - **Branches**: 56.36% (Focus area for future)
238
+
239
+ **Coverage Thresholds in vitest.config.ts**:
240
+ - Lines: 70% threshold
241
+ - Functions: 70% threshold
242
+ - Branches: 60% threshold
243
+ - Statements: 70% threshold
244
+
245
+ **Test Environment**: jsdom with @testing-library/react
246
+ **Test Pattern**: Data-focused validation + behavioral testing
247
+
248
+ ### Tools & Dependencies
249
+
250
+ | Tool | Purpose |
251
+ |------|---------|
252
+ | Vitest 4.x | Test runner |
253
+ | @testing-library/react | Component testing utilities |
254
+ | jsdom | DOM environment for tests |
255
+ | v8 | Coverage reporting |
127
256
 
128
257
 
129
258
  <!-- ROADMAP -->
130
259
  ## Roadmap
131
260
 
132
- - [ ] LinkedIn Recommendations Integration
261
+ ### New Components
262
+ - [ ] **ON HOLD** LinkedIn Recommendations Integration (Not possible with current LinkedIn API)
133
263
  - [ ] eBay Feedback Integration
134
- - [ ] Yelp Recommendations integration
264
+ - [ ] **ON HOLD** Yelp Recommendations integration (Cost Prohibitive)
135
265
  - [ ] Instagram Image Integration for Carousels
136
266
  - [ ] Shopify Integration
137
267
  - [ ] Quickbooks Integration
138
268
  - [ ] Buffer Integration (or Sendible, Sprout Social, Hootsuite)
139
269
  - [ ] Zapier Integration
270
+ - [ ] Hero Banner: headline, subtext, CTA, background image/video, overlay.
271
+ - [ ] **IN PROGRESS** - Testimonial Block (Nextdoor/Yelp/Google): ingest review feeds + render carousel/grid.
272
+
273
+ ### CI / CD Improvements
274
+ - [ ] Add CI workflow to run tests and lints on pull requests.
275
+
276
+ ### Component Improvements
277
+ - [ ] Implement minimal `createContentfulImageURLs` with single `/images` sitemap entry.
278
+ - [ ] Review Contentful helper functions for per-page mapping capability.
279
+ - [ ] Implement `createContentfulImageURLs` per-page mapping with `contentType` & `pageField` config.
280
+ - [ ] Align typography to `--font-sizeN` clamp variables.
281
+ - [ ] Provide Cloudinary transforms presets for image components.
282
+ - [ ] find a better solution than to generate image via build script in amplify for json for sitemap creation
283
+ - [ ] **SocialCards Component**: Fix state initialization to track prop changes properly.
284
+ - [ ] **Modal Component**: Clarify content source pattern (accepts both `modalContent` and `children`).
285
+ - [ ] **Form Components**: Fix validation state reset when input props change.
286
+ - [ ] **Carousel Component**: Fix active card state reset when `props.cards` changes.
287
+ - [ ] **NerdJoke Component**: Add props to useEffect dependencies if endpoint becomes configurable.
288
+
289
+
140
290
 
141
291
 
142
292
  See the [open issues](https://github.com/brianwhaley/pixelated-components/issues) for a full list of proposed features (and known issues).
@@ -86,11 +86,29 @@ export function CalloutHeader({ title, url, target }) {
86
86
  /* ========== CALLOUT BUTTON ========== */
87
87
  CalloutButton.propTypes = {
88
88
  title: PropTypes.string.isRequired,
89
- url: PropTypes.string,
89
+ url: PropTypes.string.isRequired,
90
90
  target: PropTypes.string
91
91
  };
92
+ /* export function CalloutButton( { title, url, target } : CalloutButtonType) {
93
+ return (
94
+ <div className="callout-button">
95
+ { (url)
96
+ ? <button type="button" className="callout-button"><a href={url || ""} target={target || ""} rel={target=="_blank" ? "noopener noreferrer" : ""}>{title}</a></button>
97
+ : null
98
+ }
99
+ </div>
100
+ );
101
+ } */
92
102
  export function CalloutButton({ title, url, target }) {
103
+ const handleClick = () => {
104
+ if (target === '_blank') {
105
+ window.open(url, '_blank', 'noopener,noreferrer');
106
+ }
107
+ else {
108
+ window.location.href = url;
109
+ }
110
+ };
93
111
  return (_jsx("div", { className: "callout-button", children: (url)
94
- ? _jsx("button", { type: "button", className: "callout-button", children: _jsx("a", { href: url || "", target: target || "", rel: target == "_blank" ? "noopener noreferrer" : "", children: title }) })
112
+ ? _jsx("button", { type: "button", className: "callout-button", onClick: handleClick, children: title })
95
113
  : null }));
96
114
  }
@@ -142,10 +142,17 @@
142
142
  /* cursor: pointer */
143
143
  }
144
144
 
145
- .callout .callout-button a {
146
- display: inline-block;
145
+ .callout .callout-button button:hover {
146
+ color: #369;
147
+ text-decoration: underline;
148
+ cursor: pointer;
147
149
  }
148
150
 
151
+
152
+ /* .callout .callout-button a {
153
+ display: inline-block;
154
+ } */
155
+
149
156
  /* ========================================
150
157
  ============= BOXED CALLOUT =============
151
158
  ======================================== */
@@ -135,7 +135,7 @@ export function ContentfulListItem(props) {
135
135
  ? _jsx(ContentfulItemHeader, { url: itemURL, target: itemURLTarget, title: thisItem.fields.title })
136
136
  : _jsx(ContentfulItemHeader, { title: thisItem.fields.title }) }), _jsxs("div", { className: "contentful-item-details grid12", children: [_jsxs("div", { children: [_jsx("b", { children: "Item ID: " }), thisItem.sys.id] }), _jsxs("div", { children: [_jsx("b", { children: "UPC ID: " }), thisItem.fields.id] }), _jsxs("div", { children: [_jsx("b", { children: "Quantity: " }), thisItem.fields.quantity] }), _jsxs("div", { children: [_jsx("b", { children: "Brand / Model: " }), thisItem.fields.brand, " ", thisItem.fields.model] }), _jsxs("div", { children: [_jsx("b", { children: "Listing Date: " }), thisItem.fields.date] })] }), _jsx("div", { className: "contentful-item-price", children: itemURL
137
137
  ? _jsxs("a", { href: itemURL, target: itemURLTarget, rel: "noreferrer", children: ["$", thisItem.fields.price, " USD"] })
138
- : "$" + thisItem.fields.price + " USD" }), _jsx("br", {}), _jsxs("div", { className: "contentful-itemAddToCart", children: [_jsx(ViewItemDetails, { href: "/store", itemID: thisItem.sys.id }), _jsx(AddToCartButton, { handler: AddToShoppingCart, item: shoppingCartItem, itemID: thisItem.sys.id })] })] })] }));
138
+ : "$" + thisItem.fields.price + " USD" }), _jsx("br", {}), _jsxs("div", { className: "contentful-item-addtocart", children: [_jsx(ViewItemDetails, { href: "/store", itemID: thisItem.sys.id }), _jsx(AddToCartButton, { handler: AddToShoppingCart, item: shoppingCartItem, itemID: thisItem.sys.id })] })] })] }));
139
139
  }
140
140
  /* ========== CONTENTFUL ITEM HEADER ========== */
141
141
  ContentfulItemHeader.propTypes = {
@@ -235,7 +235,7 @@ export function ContentfulItemDetail(props) {
235
235
  ? _jsx(ContentfulItemHeader, { url: itemURL, title: thisItem.fields.title })
236
236
  : _jsx(ContentfulItemHeader, { title: thisItem.fields.title }) }), _jsx("br", {}), _jsx("div", { className: "contentful-item-photo-carousel grid-s1-e7", children: _jsx(Carousel, { cards: cards, draggable: true, imgFit: "contain" }) }), _jsxs("div", { className: "grid-s7-e13", children: [_jsx("div", { className: "contentful-item-details grid12", children: _jsx("div", { dangerouslySetInnerHTML: { __html: thisItem.fields.description.replace(/(<br\s*\/?>\s*){2,}/gi, '') } }) }), _jsx("br", {}), _jsxs("div", { className: "contentful-item-details grid12", children: [_jsxs("div", { children: [_jsx("b", { children: "Item ID: " }), thisItem.sys.id] }), _jsxs("div", { children: [_jsx("b", { children: "UPC ID: " }), thisItem.fields.id] }), _jsxs("div", { children: [_jsx("b", { children: "Quantity: " }), thisItem.fields.quantity] }), _jsxs("div", { children: [_jsx("b", { children: "Brand / Model: " }), thisItem.fields.brand, " ", thisItem.fields.model] }), _jsxs("div", { children: [_jsx("b", { children: "Listing Date: " }), thisItem.fields.date] }), _jsx("br", {})] }), _jsx("div", { className: "contentful-item-price", children: itemURL
237
237
  ? _jsxs("a", { href: itemURL, target: itemURLTarget, rel: "noreferrer", children: ["$", thisItem.fields.price, " USD"] })
238
- : "$" + thisItem.fields.price + " USD" }), _jsx("br", {}), _jsx("div", { className: "contentful-itemAddToCart", children: _jsx(AddToCartButton, { handler: AddToShoppingCart, item: shoppingCartItem, itemID: thisItem.sys.id }) })] })] }) }));
238
+ : "$" + thisItem.fields.price + " USD" }), _jsx("br", {}), _jsx("div", { className: "contentful-item-addtocart", children: _jsx(AddToCartButton, { handler: AddToShoppingCart, item: shoppingCartItem, itemID: thisItem.sys.id }) })] })] }) }));
239
239
  }
240
240
  else {
241
241
  return (_jsx(_Fragment, { children: _jsx("div", { id: "contentful-items", className: "contentful-items", children: _jsx("div", { className: "centered", children: "Loading..." }) }) }));
@@ -8,9 +8,9 @@ import { Loading, ToggleLoading } from '../general/loading';
8
8
  import "./wordpress.css";
9
9
  // https://microformats.org/wiki/h-entry
10
10
  function decodeString(str) {
11
- const textarea = { value: '' };
11
+ const textarea = document.createElement('textarea');
12
12
  textarea.innerHTML = str;
13
- return textarea.value || str;
13
+ return textarea.value;
14
14
  }
15
15
  export function BlogPostList(props) {
16
16
  const { site, count, posts: cachedPosts } = props;
@@ -0,0 +1,114 @@
1
+ /* ========================================
2
+ ===== ACCORDION COMPONENT =====
3
+ ======================================== */
4
+
5
+ .accordion {
6
+ margin: 0;
7
+ padding: 0;
8
+ }
9
+
10
+ .accordion-item {
11
+ margin-bottom: 0.5rem;
12
+ border: 1px solid #e1e5e9;
13
+ border-radius: 4px;
14
+ background: #fff;
15
+ }
16
+
17
+ .accordion-title {
18
+ cursor: pointer;
19
+ padding: 1rem;
20
+ font-size: 1rem;
21
+ font-weight: 600;
22
+ color: #1a202c;
23
+ background: none;
24
+ border: none;
25
+ width: 100%;
26
+ text-align: left;
27
+ display: flex;
28
+ align-items: center;
29
+ gap: 0.75rem;
30
+ transition: background-color 0.2s ease;
31
+ }
32
+
33
+ .accordion-title:hover {
34
+ background-color: #f7fafc;
35
+ }
36
+
37
+ .accordion-title:focus {
38
+ outline: 2px solid #3182ce;
39
+ outline-offset: -2px;
40
+ background-color: #f7fafc;
41
+ }
42
+
43
+ /* Hide default triangle and add custom one */
44
+ .accordion-title::before {
45
+ content: '▶';
46
+ font-size: 0.75rem;
47
+ color: #718096;
48
+ transition: transform 0.2s ease;
49
+ flex-shrink: 0;
50
+ }
51
+
52
+ details[open] .accordion-title::before {
53
+ content: '🔽';
54
+ transform: rotate(0deg);
55
+ }
56
+
57
+ /* Remove default marker */
58
+ .accordion-title::-webkit-details-marker,
59
+ .accordion-title::marker {
60
+ display: none;
61
+ }
62
+
63
+ .accordion-title h3 {
64
+ margin: 0;
65
+ font-size: inherit;
66
+ font-weight: inherit;
67
+ color: inherit;
68
+ flex: 1;
69
+ }
70
+
71
+ .accordion-content {
72
+ padding: 1rem;
73
+ border-top: 1px solid #e1e5e9;
74
+ background-color: #f7fafc;
75
+ }
76
+
77
+ .accordion-content p {
78
+ margin: 0 0 1rem 0;
79
+ }
80
+
81
+ .accordion-content p:last-child {
82
+ margin-bottom: 0;
83
+ }
84
+
85
+ /* Animation for smooth expand/collapse */
86
+ .accordion-content {
87
+ animation: accordionSlideDown 0.3s ease-out;
88
+ }
89
+
90
+ details[open] .accordion-content {
91
+ animation: accordionSlideDown 0.3s ease-out;
92
+ }
93
+
94
+ @keyframes accordionSlideDown {
95
+ from {
96
+ opacity: 0;
97
+ transform: translateY(-10px);
98
+ }
99
+ to {
100
+ opacity: 1;
101
+ transform: translateY(0);
102
+ }
103
+ }
104
+
105
+ /* Respect user's motion preferences */
106
+ @media (prefers-reduced-motion: reduce) {
107
+ .accordion-content {
108
+ animation: none;
109
+ }
110
+
111
+ .accordion-title::before {
112
+ transition: none;
113
+ }
114
+ }
@@ -0,0 +1,13 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import PropTypes from 'prop-types';
4
+ import './accordion.css';
5
+ Accordion.propTypes = {
6
+ items: PropTypes.arrayOf(PropTypes.shape({
7
+ title: PropTypes.string.isRequired,
8
+ content: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
9
+ })).isRequired,
10
+ };
11
+ export function Accordion({ items }) {
12
+ return (_jsx("div", { className: "accordion", children: items?.map((item, index) => (item ? (_jsxs("details", { className: "accordion-item", children: [_jsx("summary", { className: "accordion-title", children: _jsx("h3", { id: `accordion-header-${index}`, children: item.title }) }), _jsx("div", { className: "accordion-content", role: "region", "aria-labelledby": `accordion-header-${index}`, children: typeof item.content === 'string' ? (_jsx("p", { children: item.content })) : (item.content) })] }, index)) : null)) }));
13
+ }
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
1
2
  'use client';
2
3
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
4
  import { useEffect, useRef } from 'react';
@@ -86,14 +87,10 @@ export function MenuExpando(props) {
86
87
  }, []);
87
88
  function generateMenuItems() {
88
89
  const myItems = [];
89
- console.log('MenuExpando props.menuItems:', props.menuItems);
90
- console.log('Is array?', Array.isArray(props.menuItems));
91
90
  // Handle both object format (name: href) and array format (with name/path properties)
92
91
  if (Array.isArray(props.menuItems)) {
93
92
  // Array format like MenuAccordion
94
- console.log('Processing as array, length:', props.menuItems.length);
95
93
  for (const item of props.menuItems) {
96
- console.log('Item:', item);
97
94
  if (item.routes && item.routes.length > 0) {
98
95
  // Item has nested routes - create expandable submenu
99
96
  myItems.push(_jsx("li", { children: _jsxs("details", { className: "menuExpandoNested", children: [_jsx("summary", { children: _jsx("a", { href: item.path, children: item.name }) }), _jsx("ul", { children: item.routes.map((route) => (_jsx(MenuExpandoItem, { name: route.name, href: route.path }, route.name))) })] }) }, item.name));
@@ -106,12 +103,10 @@ export function MenuExpando(props) {
106
103
  }
107
104
  else {
108
105
  // Object format
109
- console.log('Processing as object');
110
106
  for (const itemKey in props.menuItems) {
111
107
  myItems.push(_jsx(MenuExpandoItem, { name: itemKey, href: props.menuItems[itemKey] }, itemKey));
112
108
  }
113
109
  }
114
- console.log('Generated items count:', myItems.length);
115
110
  return myItems;
116
111
  }
117
112
  return (_jsx("div", { className: "menuExpando", id: "menuExpando", children: _jsxs("details", { className: "menuExpandoWrapper", id: "menuExpandoWrapper", ref: detailsRef, children: [_jsx("summary", {}), _jsx("ul", { ref: ulRef, children: generateMenuItems() })] }) }));
@@ -210,7 +210,9 @@ export async function createContentfulURLs(props) {
210
210
  const contentType = "carouselCard";
211
211
  const field = "title";
212
212
  const providerContentfulApiProps = getFullPixelatedConfig()?.contentful;
213
- const mergedApiProps = { ...providerContentfulApiProps, ...props.apiProps };
213
+ // Changed order: provider config overrides apiProps for security (tokens)
214
+ const mergedApiProps = { ...props.apiProps, ...providerContentfulApiProps };
215
+ // const mergedApiProps = { ...providerContentfulApiProps, ...props.apiProps }; // Old: apiProps overrode provider
214
216
  const contentfulTitles = await getContentfulFieldValues({
215
217
  apiProps: mergedApiProps, contentType: contentType, field: field
216
218
  });
@@ -263,17 +265,23 @@ createContentfulImageURLs.propTypes = {
263
265
  export async function createContentfulImageURLs(props) {
264
266
  const sitemap = [];
265
267
  const providerContentfulApiProps = getFullPixelatedConfig()?.contentful;
266
- const mergedApiProps = { ...providerContentfulApiProps, ...props.apiProps };
268
+ // Changed order: provider config overrides apiProps for security (tokens)
269
+ const mergedApiProps = { ...props.apiProps, ...providerContentfulApiProps };
270
+ // const mergedApiProps = { ...providerContentfulApiProps, ...props.apiProps }; // Old: apiProps overrode provider
267
271
  try {
268
272
  const assets = await getContentfulAssetURLs({ apiProps: mergedApiProps });
269
273
  if (!Array.isArray(assets) || assets.length === 0)
270
274
  return sitemap;
271
275
  const newImages = assets.map((a) => {
272
276
  let i = a.image || '';
277
+ if (!i)
278
+ return ''; // Filter out empty images before processing
273
279
  if (i.startsWith('//'))
274
280
  i = `https:${i}`;
275
281
  else if (i.startsWith('/'))
276
282
  i = `${props.origin}${i}`;
283
+ else if (!i.startsWith('http://') && !i.startsWith('https://'))
284
+ i = `${props.origin}/${i}`; // Handle relative URLs
277
285
  return i;
278
286
  }).filter(Boolean);
279
287
  sitemap.push({
@@ -104,6 +104,14 @@
104
104
  height: auto;
105
105
  margin: 5px auto;
106
106
  }
107
+
108
+ .pixCartButton:hover,
109
+ .pixCart .pixCartButton:hover,
110
+ #pixCartButton.pixCartButton:hover {
111
+ color: #369;
112
+ text-decoration: underline;
113
+ }
114
+
107
115
  #pixCartButton.pixCartButton {
108
116
  width: 80px;
109
117
  min-width: 80px;
@@ -115,6 +123,8 @@
115
123
 
116
124
 
117
125
 
126
+
127
+
118
128
  input[readonly] {
119
129
  border: none;
120
130
  background-color: transparent;
@@ -104,6 +104,13 @@ export function RecipeBook(props) {
104
104
  useEffect(() => {
105
105
  setOutputElems(outputMyElems());
106
106
  }, [showOnlyCat, showOnlyRecipe]);
107
+ // Deep linking: read URL hash on mount and select recipe if present
108
+ useEffect(() => {
109
+ const hash = window.location.hash.replace('#', '');
110
+ if (hash && hash.length > 0) {
111
+ onRecipePickListChange(hash);
112
+ }
113
+ }, []); // Empty dependency array - only run on mount
107
114
  function onRecipePickListChange(optionVal) {
108
115
  let cID, rID;
109
116
  if (optionVal.includes('-')) {
@@ -33,7 +33,7 @@ Resume.propTypes = {
33
33
  data: PropTypes.any.isRequired,
34
34
  };
35
35
  export function Resume(props) {
36
- return (_jsx("section", { className: "p-resume", id: "resume-section", children: _jsx("div", { className: "section-container", children: _jsxs("div", { className: "row-12col", children: [_jsx("div", { className: "p-name grid-s1-e13", children: _jsx(ResumeName, { data: props.data.items[0].properties.name }) }), _jsxs("div", { className: "divider grid-s1-e4", children: [_jsx("div", { className: "p-contact", children: _jsx(ResumeContact, { title: "Contact Information", data: props.data.items[0].properties.contact }) }), _jsx("div", { className: "p-education", children: _jsx(ResumeEvents, { title: "Education", data: props.data.items[0].properties.education, dateFormat: "MM/yyyy", collapsible: false }) }), _jsx("div", { className: "p-skills", children: _jsx(ResumeSkills, { title: "Skills", data: props.data.items[0].properties.skills }) })] }), _jsxs("div", { className: "grid-s4-e13", children: [_jsx("div", { className: "p-summary", children: _jsx(ResumeSummary, { title: "Professional Summary", data: props.data.items[0].properties.summary }) }), _jsx("div", { className: "p-qualifications", children: _jsx(ResumeQualifications, { title: "Professional Qualifications", data: props.data.items[0].properties.qualifications }) }), _jsx("div", { className: "p-experience", children: _jsx(ResumeEvents, { title: "Work History", data: props.data.items[0].properties.experience, dateFormat: "MM/yyyy", collapsible: false }) }), _jsx("div", { className: "p-projects", children: _jsx(ResumeProjects, { title: "Projects", data: props.data.items[0].properties.experience, collapsible: true }) }), _jsx("div", { className: "p-volunteer", children: _jsx(ResumeEvents, { title: "Volunteer Work", data: props.data.items[0].properties.volunteer, dateFormat: "MM/yyyy", collapsible: true }) }), _jsx("div", { className: "p-certifications", children: _jsx(ResumeEvents, { title: "Certifications", data: props.data.items[0].properties.certifications, dateFormat: "MM/yyyy", collapsible: true }) }), _jsx("div", { className: "p-awards", children: _jsx(ResumeEvents, { title: "Honors & Awards", data: props.data.items[0].properties.awards, dateFormat: "MM/yyyy", collapsible: true }) }), _jsx("div", { className: "p-training", children: _jsx(ResumeEvents, { title: "Training & Conferences", data: props.data.items[0].properties.training, dateFormat: "MM/dd/yyyy", collapsible: true }) }), _jsx("div", { className: "p-references", children: _jsx(ResumeReferences, { title: "References", data: props.data.items[0].properties.references, collapsible: true }) })] })] }) }) }));
36
+ return (_jsx("section", { className: "p-resume", id: "resume-section", children: _jsx("div", { className: "section-container", children: _jsxs("div", { className: "row-12col", children: [_jsx("div", { className: "p-name grid-s1-e13", children: _jsx(ResumeName, { data: props.data?.items?.[0]?.properties?.name || '' }) }), _jsxs("div", { className: "divider grid-s1-e4", children: [_jsx("div", { className: "p-contact", children: _jsx(ResumeContact, { title: "Contact Information", data: props.data?.items?.[0]?.properties?.contact || [] }) }), _jsx("div", { className: "p-education", children: _jsx(ResumeEvents, { title: "Education", data: props.data?.items?.[0]?.properties?.education || [], dateFormat: "MM/yyyy", collapsible: false }) }), _jsx("div", { className: "p-skills", children: _jsx(ResumeSkills, { title: "Skills", data: props.data?.items?.[0]?.properties?.skills || [] }) })] }), _jsxs("div", { className: "grid-s4-e13", children: [_jsx("div", { className: "p-summary", children: _jsx(ResumeSummary, { title: "Professional Summary", data: props.data?.items?.[0]?.properties?.summary || '' }) }), _jsx("div", { className: "p-qualifications", children: _jsx(ResumeQualifications, { title: "Professional Qualifications", data: props.data?.items?.[0]?.properties?.qualifications || [] }) }), _jsx("div", { className: "p-experience", children: _jsx(ResumeEvents, { title: "Work History", data: props.data?.items?.[0]?.properties?.experience || [], dateFormat: "MM/yyyy", collapsible: false }) }), _jsx("div", { className: "p-projects", children: _jsx(ResumeProjects, { title: "Projects", data: props.data?.items?.[0]?.properties?.experience || [], collapsible: true }) }), _jsx("div", { className: "p-volunteer", children: _jsx(ResumeEvents, { title: "Volunteer Work", data: props.data?.items?.[0]?.properties?.volunteer || [], dateFormat: "MM/yyyy", collapsible: true }) }), _jsx("div", { className: "p-certifications", children: _jsx(ResumeEvents, { title: "Certifications", data: props.data?.items?.[0]?.properties?.certifications || [], dateFormat: "MM/yyyy", collapsible: true }) }), _jsx("div", { className: "p-awards", children: _jsx(ResumeEvents, { title: "Honors & Awards", data: props.data?.items?.[0]?.properties?.awards || [], dateFormat: "MM/yyyy", collapsible: true }) }), _jsx("div", { className: "p-training", children: _jsx(ResumeEvents, { title: "Training & Conferences", data: props.data?.items?.[0]?.properties?.training || [], dateFormat: "MM/dd/yyyy", collapsible: true }) }), _jsx("div", { className: "p-references", children: _jsx(ResumeReferences, { title: "References", data: props.data?.items?.[0]?.properties?.references || [], collapsible: true }) })] })] }) }) }));
37
37
  }
38
38
  ResumeName.propTypes = {
39
39
  data: PropTypes.any.isRequired,
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ export * from './components/config/config.server';
25
25
  export * from './components/config/config';
26
26
  export * from './components/config/config.types';
27
27
  export * from './components/general/css';
28
+ export * from './components/general/accordion';
28
29
  export * from './components/general/image';
29
30
  export * from './components/general/loading';
30
31
  export * from './components/general/microinteractions';
@@ -46,7 +46,7 @@ export declare function CalloutButton({ title, url, target }: CalloutButtonType)
46
46
  export declare namespace CalloutButton {
47
47
  var propTypes: {
48
48
  title: PropTypes.Validator<string>;
49
- url: PropTypes.Requireable<string>;
49
+ url: PropTypes.Validator<string>;
50
50
  target: PropTypes.Requireable<string>;
51
51
  };
52
52
  }