fix: initial commit
This commit is contained in:
211
js/components/book-card.js
Normal file
211
js/components/book-card.js
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Book Card Component
|
||||
* Reusable card displaying book thumbnail, title, author, and price
|
||||
*/
|
||||
class BookCard extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['title', 'author', 'price', 'image', 'href', 'rating'];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
attributeChangedCallback() {
|
||||
if (this.shadowRoot) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
get title() {
|
||||
return this.getAttribute('title') || 'Book Title';
|
||||
}
|
||||
|
||||
get author() {
|
||||
return this.getAttribute('author') || 'Author Name';
|
||||
}
|
||||
|
||||
get price() {
|
||||
return this.getAttribute('price') || '$0.00';
|
||||
}
|
||||
|
||||
get image() {
|
||||
return this.getAttribute('image') || '';
|
||||
}
|
||||
|
||||
get href() {
|
||||
return this.getAttribute('href') || 'book.html';
|
||||
}
|
||||
|
||||
get rating() {
|
||||
return parseFloat(this.getAttribute('rating')) || 0;
|
||||
}
|
||||
|
||||
renderStars(rating) {
|
||||
const fullStars = Math.floor(rating);
|
||||
const hasHalf = rating % 1 >= 0.5;
|
||||
const emptyStars = 5 - fullStars - (hasHalf ? 1 : 0);
|
||||
|
||||
let stars = '';
|
||||
|
||||
// Full stars
|
||||
for (let i = 0; i < fullStars; i++) {
|
||||
stars += `<svg class="star star-full" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
// Half star
|
||||
if (hasHalf) {
|
||||
stars += `<svg class="star star-half" viewBox="0 0 24 24">
|
||||
<defs>
|
||||
<linearGradient id="halfGrad">
|
||||
<stop offset="50%" stop-color="currentColor"/>
|
||||
<stop offset="50%" stop-color="transparent"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="url(#halfGrad)" stroke="currentColor" stroke-width="1" d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
// Empty stars
|
||||
for (let i = 0; i < emptyStars; i++) {
|
||||
stars += `<svg class="star star-empty" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
return stars;
|
||||
}
|
||||
|
||||
render() {
|
||||
// Generate placeholder image if none provided
|
||||
const imageHtml = this.image
|
||||
? `<img src="${this.image}" alt="${this.title}" class="book-image">`
|
||||
: `<div class="book-image placeholder">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25" />
|
||||
</svg>
|
||||
</div>`;
|
||||
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
background-color: var(--color-background, #ffffff);
|
||||
border-radius: var(--radius-lg, 0.75rem);
|
||||
overflow: hidden;
|
||||
transition: transform var(--transition-fast, 150ms ease),
|
||||
box-shadow var(--transition-fast, 150ms ease);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md, 0 4px 6px -1px rgb(0 0 0 / 0.1));
|
||||
}
|
||||
|
||||
.card:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.image-container {
|
||||
position: relative;
|
||||
aspect-ratio: 3 / 4;
|
||||
background-color: var(--color-background-tertiary, #f1f5f9);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.book-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.book-image.placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-light, #64748b);
|
||||
}
|
||||
|
||||
.book-image.placeholder svg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: var(--spacing-sm, 0.5rem);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--font-size-sm, 0.875rem);
|
||||
font-weight: var(--font-weight-semibold, 600);
|
||||
color: var(--color-text, #1e293b);
|
||||
margin-bottom: var(--spacing-xs, 0.25rem);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
line-height: var(--line-height-tight, 1.25);
|
||||
}
|
||||
|
||||
.author {
|
||||
font-size: var(--font-size-xs, 0.75rem);
|
||||
color: var(--color-text-light, #64748b);
|
||||
margin-bottom: var(--spacing-xs, 0.25rem);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.rating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
margin-bottom: var(--spacing-xs, 0.25rem);
|
||||
}
|
||||
|
||||
.star {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
color: var(--color-accent, #f59e0b);
|
||||
}
|
||||
|
||||
.star-empty {
|
||||
color: var(--color-border, #e2e8f0);
|
||||
}
|
||||
|
||||
.price {
|
||||
font-size: var(--font-size-base, 1rem);
|
||||
font-weight: var(--font-weight-bold, 700);
|
||||
color: var(--color-primary, #2563eb);
|
||||
}
|
||||
</style>
|
||||
<a href="${this.href}" class="card">
|
||||
<div class="image-container">
|
||||
${imageHtml}
|
||||
</div>
|
||||
<div class="content">
|
||||
<h3 class="title">${this.title}</h3>
|
||||
<p class="author">${this.author}</p>
|
||||
${this.rating > 0 ? `<div class="rating">${this.renderStars(this.rating)}</div>` : ''}
|
||||
<p class="price">${this.price}</p>
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('book-card', BookCard);
|
||||
113
js/components/horizontal-scroll-nav.js
Normal file
113
js/components/horizontal-scroll-nav.js
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Horizontal Scroll Nav Component
|
||||
* Pill-style category buttons in a horizontal scroller
|
||||
*/
|
||||
class HorizontalScrollNav extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.categories = [
|
||||
{ id: 'all', label: 'All', active: true },
|
||||
{ id: 'fiction', label: 'Fiction', active: false },
|
||||
{ id: 'non-fiction', label: 'Non-Fiction', active: false },
|
||||
{ id: 'mystery', label: 'Mystery', active: false },
|
||||
{ id: 'romance', label: 'Romance', active: false },
|
||||
{ id: 'sci-fi', label: 'Sci-Fi', active: false },
|
||||
{ id: 'biography', label: 'Biography', active: false },
|
||||
{ id: 'history', label: 'History', active: false },
|
||||
];
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
this.addEventListeners();
|
||||
}
|
||||
|
||||
addEventListeners() {
|
||||
const buttons = this.shadowRoot.querySelectorAll('.nav-pill');
|
||||
buttons.forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
this.setActiveCategory(e.target.dataset.id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setActiveCategory(id) {
|
||||
this.categories = this.categories.map(cat => ({
|
||||
...cat,
|
||||
active: cat.id === id
|
||||
}));
|
||||
this.render();
|
||||
this.addEventListeners();
|
||||
|
||||
// Dispatch custom event for parent components
|
||||
this.dispatchEvent(new CustomEvent('category-change', {
|
||||
detail: { categoryId: id },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
render() {
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm, 0.5rem);
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
padding: var(--spacing-xs, 0.25rem) 0;
|
||||
}
|
||||
|
||||
.nav-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-pill {
|
||||
flex-shrink: 0;
|
||||
padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
|
||||
font-size: var(--font-size-sm, 0.875rem);
|
||||
font-weight: var(--font-weight-medium, 500);
|
||||
color: var(--color-text-light, #64748b);
|
||||
background-color: var(--color-background-tertiary, #f1f5f9);
|
||||
border: none;
|
||||
border-radius: var(--radius-full, 9999px);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast, 150ms ease);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-pill:hover {
|
||||
background-color: var(--color-border, #e2e8f0);
|
||||
}
|
||||
|
||||
.nav-pill.active {
|
||||
color: var(--color-text-inverse, #ffffff);
|
||||
background-color: var(--color-primary, #2563eb);
|
||||
}
|
||||
|
||||
.nav-pill.active:hover {
|
||||
background-color: var(--color-primary-dark, #1d4ed8);
|
||||
}
|
||||
</style>
|
||||
<nav class="nav-container" role="navigation" aria-label="Book categories">
|
||||
${this.categories.map(cat => `
|
||||
<button
|
||||
class="nav-pill ${cat.active ? 'active' : ''}"
|
||||
data-id="${cat.id}"
|
||||
${cat.active ? 'aria-current="true"' : ''}
|
||||
>
|
||||
${cat.label}
|
||||
</button>
|
||||
`).join('')}
|
||||
</nav>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('horizontal-scroll-nav', HorizontalScrollNav);
|
||||
98
js/components/search-bar.js
Normal file
98
js/components/search-bar.js
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Search Bar Component
|
||||
* Search input with icon
|
||||
*/
|
||||
class SearchBar extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
this.addEventListeners();
|
||||
}
|
||||
|
||||
addEventListeners() {
|
||||
const input = this.shadowRoot.querySelector('.search-input');
|
||||
const form = this.shadowRoot.querySelector('.search-form');
|
||||
|
||||
form.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
this.dispatchEvent(new CustomEvent('search', {
|
||||
detail: { query: input.value },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
});
|
||||
|
||||
input.addEventListener('input', (e) => {
|
||||
this.dispatchEvent(new CustomEvent('search-input', {
|
||||
detail: { query: e.target.value },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: var(--spacing-md, 1rem);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--color-text-light, #64748b);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
|
||||
padding-left: calc(var(--spacing-md, 1rem) + 20px + var(--spacing-sm, 0.5rem));
|
||||
font-size: var(--font-size-sm, 0.875rem);
|
||||
color: var(--color-text, #1e293b);
|
||||
background-color: var(--color-background-tertiary, #f1f5f9);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-lg, 0.75rem);
|
||||
outline: none;
|
||||
transition: all var(--transition-fast, 150ms ease);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: var(--color-text-light, #64748b);
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
background-color: var(--color-background, #ffffff);
|
||||
border-color: var(--color-primary, #2563eb);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
</style>
|
||||
<form class="search-form" role="search">
|
||||
<svg class="search-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
||||
</svg>
|
||||
<input
|
||||
type="search"
|
||||
class="search-input"
|
||||
placeholder="Search books, authors..."
|
||||
aria-label="Search books and authors"
|
||||
>
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('search-bar', SearchBar);
|
||||
35
js/components/site-content.js
Normal file
35
js/components/site-content.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Site Content Component
|
||||
* Main content area wrapper with slot for page content
|
||||
*/
|
||||
class SiteContent extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: var(--spacing-md, 1rem);
|
||||
}
|
||||
</style>
|
||||
<main class="content">
|
||||
<slot></slot>
|
||||
</main>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('site-content', SiteContent);
|
||||
111
js/components/site-footer.js
Normal file
111
js/components/site-footer.js
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Site Footer Component
|
||||
* Footer with navigation links, social icons, and copyright
|
||||
*/
|
||||
class SiteFooter extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
background-color: var(--color-background-secondary, #f8fafc);
|
||||
border-top: 1px solid var(--color-border, #e2e8f0);
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: var(--spacing-lg, 1.5rem) var(--spacing-md, 1rem);
|
||||
}
|
||||
|
||||
.footer-nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-md, 1rem);
|
||||
margin-bottom: var(--spacing-lg, 1.5rem);
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
font-size: var(--font-size-sm, 0.875rem);
|
||||
color: var(--color-text-light, #64748b);
|
||||
text-decoration: none;
|
||||
transition: color var(--transition-fast, 150ms ease);
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
color: var(--color-primary, #2563eb);
|
||||
}
|
||||
|
||||
.social-icons {
|
||||
display: flex;
|
||||
gap: var(--spacing-md, 1rem);
|
||||
margin-bottom: var(--spacing-lg, 1.5rem);
|
||||
}
|
||||
|
||||
.social-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: var(--color-text-light, #64748b);
|
||||
background-color: var(--color-background, #ffffff);
|
||||
border-radius: var(--radius-full, 9999px);
|
||||
transition: all var(--transition-fast, 150ms ease);
|
||||
}
|
||||
|
||||
.social-icon:hover {
|
||||
color: var(--color-primary, #2563eb);
|
||||
background-color: var(--color-background-tertiary, #f1f5f9);
|
||||
}
|
||||
|
||||
.social-icon svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.copyright {
|
||||
font-size: var(--font-size-xs, 0.75rem);
|
||||
color: var(--color-text-light, #64748b);
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
<footer class="footer">
|
||||
<nav class="footer-nav">
|
||||
<a href="#" class="footer-link">About Us</a>
|
||||
<a href="#" class="footer-link">Contact</a>
|
||||
<a href="#" class="footer-link">FAQ</a>
|
||||
<a href="#" class="footer-link">Privacy Policy</a>
|
||||
<a href="#" class="footer-link">Terms of Service</a>
|
||||
</nav>
|
||||
<div class="social-icons">
|
||||
<a href="#" class="social-icon" aria-label="Facebook">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" class="social-icon" aria-label="Twitter">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" class="social-icon" aria-label="Instagram">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<p class="copyright">© 2026 BookStore. All rights reserved.</p>
|
||||
</footer>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('site-footer', SiteFooter);
|
||||
41
js/components/site-header.js
Normal file
41
js/components/site-header.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Site Header Component
|
||||
* Sticky header container that holds top-bar, navigation, and search
|
||||
*/
|
||||
class SiteHeader extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background-color: var(--color-background, #ffffff);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm, 0.5rem);
|
||||
padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
|
||||
border-bottom: 1px solid var(--color-border, #e2e8f0);
|
||||
}
|
||||
</style>
|
||||
<header class="header">
|
||||
<slot></slot>
|
||||
</header>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('site-header', SiteHeader);
|
||||
110
js/components/top-bar.js
Normal file
110
js/components/top-bar.js
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Top Bar Component
|
||||
* Contains logo, menu icon, and cart icon
|
||||
*/
|
||||
class TopBar extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: var(--font-size-xl, 1.25rem);
|
||||
font-weight: var(--font-weight-bold, 700);
|
||||
color: var(--color-text, #1e293b);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm, 0.5rem);
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--radius-md, 0.5rem);
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--color-text, #1e293b);
|
||||
transition: background-color var(--transition-fast, 150ms ease);
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
background-color: var(--color-background-tertiary, #f1f5f9);
|
||||
}
|
||||
|
||||
.icon-button:active {
|
||||
background-color: var(--color-border, #e2e8f0);
|
||||
}
|
||||
|
||||
.icon-button svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.cart-badge {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
font-size: var(--font-size-xs, 0.75rem);
|
||||
font-weight: var(--font-weight-semibold, 600);
|
||||
color: var(--color-text-inverse, #ffffff);
|
||||
background-color: var(--color-primary, #2563eb);
|
||||
border-radius: var(--radius-full, 9999px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
<div class="top-bar">
|
||||
<a href="index.html" class="logo">BookStore</a>
|
||||
<div class="actions">
|
||||
<button class="icon-button" aria-label="Menu">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="icon-button cart-badge" aria-label="Shopping cart">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 0 0-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 0 0-16.536-1.84M7.5 14.25 5.106 5.272M6 20.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm12.75 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" />
|
||||
</svg>
|
||||
<span class="badge">2</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('top-bar', TopBar);
|
||||
Reference in New Issue
Block a user