26 changed files with 2766 additions and 125 deletions
@ -1,12 +1,29 @@ |
|||||||
export { default as AThemeToggleMini } from "./primitives/AThemeToggleMini.svelte"; |
// Alexandria Component Library - Main Export File
|
||||||
|
|
||||||
|
// Primitive Components
|
||||||
export { default as AAlert } from "./primitives/AAlert.svelte"; |
export { default as AAlert } from "./primitives/AAlert.svelte"; |
||||||
|
export { default as ADetails } from "./primitives/ADetails.svelte"; |
||||||
|
export { default as AInput } from "./primitives/AInput.svelte"; |
||||||
|
export { default as ANostrBadge } from "./primitives/ANostrBadge.svelte"; |
||||||
|
export { default as ANostrBadgeRow } from "./primitives/ANostrBadgeRow.svelte"; |
||||||
|
export { default as ANostrUser } from "./primitives/ANostrUser.svelte"; |
||||||
export { default as APagination } from "./primitives/APagination.svelte"; |
export { default as APagination } from "./primitives/APagination.svelte"; |
||||||
|
export { default as AThemeToggleMini } from "./primitives/AThemeToggleMini.svelte"; |
||||||
|
|
||||||
|
// Navigation Components
|
||||||
export { default as ANavbar } from "./nav/ANavbar.svelte"; |
export { default as ANavbar } from "./nav/ANavbar.svelte"; |
||||||
export { default as AFooter } from "./nav/AFooter.svelte"; |
export { default as AFooter } from "./nav/AFooter.svelte"; |
||||||
|
|
||||||
|
// Form Components
|
||||||
export { default as ACommentForm } from "./forms/ACommentForm.svelte"; |
export { default as ACommentForm } from "./forms/ACommentForm.svelte"; |
||||||
export { default as AMarkupForm } from "./forms/AMarkupForm.svelte"; |
export { default as AMarkupForm } from "./forms/AMarkupForm.svelte"; |
||||||
|
export { default as ASearchForm } from "./forms/ASearchForm.svelte"; |
||||||
export { default as ATextareaWithPreview } from "./forms/ATextareaWithPreview.svelte"; |
export { default as ATextareaWithPreview } from "./forms/ATextareaWithPreview.svelte"; |
||||||
|
|
||||||
|
// Card Components
|
||||||
|
export { default as AEventPreview } from "./cards/AEventPreview.svelte"; |
||||||
export { default as AProfilePreview } from "./cards/AProfilePreview.svelte"; |
export { default as AProfilePreview } from "./cards/AProfilePreview.svelte"; |
||||||
|
|
||||||
|
// Reader Components
|
||||||
|
export { default as ATechBlock } from "./reader/ATechBlock.svelte"; |
||||||
|
export { default as ATechToggle } from "./reader/ATechToggle.svelte"; |
||||||
|
|||||||
@ -0,0 +1,368 @@ |
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import fs from 'fs'; |
||||||
|
import path from 'path'; |
||||||
|
import { fileURLToPath } from 'url'; |
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url); |
||||||
|
const __dirname = path.dirname(__filename); |
||||||
|
|
||||||
|
/** |
||||||
|
* @typedef {Object} PropDefinition |
||||||
|
* @property {string} name |
||||||
|
* @property {string[]} type |
||||||
|
* @property {string | null | undefined} default |
||||||
|
* @property {string} description |
||||||
|
* @property {boolean} required |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* @typedef {Object} ExampleDefinition |
||||||
|
* @property {string} name |
||||||
|
* @property {string} code |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* @typedef {Object} ComponentDefinition |
||||||
|
* @property {string} name |
||||||
|
* @property {string} description |
||||||
|
* @property {string} category |
||||||
|
* @property {PropDefinition[]} props |
||||||
|
* @property {string[]} events |
||||||
|
* @property {string[]} slots |
||||||
|
* @property {ExampleDefinition[]} examples |
||||||
|
* @property {string[]} features |
||||||
|
* @property {string[]} accessibility |
||||||
|
* @property {string} since |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* Parse TSDoc comments from Svelte component files |
||||||
|
*/ |
||||||
|
class ComponentParser { |
||||||
|
constructor() { |
||||||
|
/** @type {ComponentDefinition[]} */ |
||||||
|
this.components = []; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Extract TSDoc block from script content |
||||||
|
* @param {string} content |
||||||
|
* @returns {string | null} |
||||||
|
*/ |
||||||
|
extractTSDoc(content) { |
||||||
|
const scriptMatch = content.match(/<script[^>]*>([\s\S]*?)<\/script>/); |
||||||
|
if (!scriptMatch) return null; |
||||||
|
|
||||||
|
const scriptContent = scriptMatch[1]; |
||||||
|
const tsDocMatch = scriptContent.match(/\/\*\*\s*([\s\S]*?)\*\//); |
||||||
|
if (!tsDocMatch) return null; |
||||||
|
|
||||||
|
return tsDocMatch[1]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Parse TSDoc content into structured data |
||||||
|
* @param {string} tsDocContent |
||||||
|
* @returns {ComponentDefinition} |
||||||
|
*/ |
||||||
|
parseTSDoc(tsDocContent) { |
||||||
|
const lines = tsDocContent |
||||||
|
.split("\n") |
||||||
|
.map((line) => line.replace(/^\s*\*\s?/, "").trim()); |
||||||
|
|
||||||
|
/** @type {ComponentDefinition} */ |
||||||
|
const component = { |
||||||
|
name: "", |
||||||
|
description: "", |
||||||
|
category: "", |
||||||
|
props: [], |
||||||
|
events: [], |
||||||
|
slots: [], |
||||||
|
examples: [], |
||||||
|
features: [], |
||||||
|
accessibility: [], |
||||||
|
since: "1.0.0", // Default version
|
||||||
|
}; |
||||||
|
|
||||||
|
let currentSection = "description"; |
||||||
|
let currentExample = ""; |
||||||
|
let inCodeBlock = false; |
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) { |
||||||
|
const line = lines[i]; |
||||||
|
|
||||||
|
// Skip empty lines
|
||||||
|
if (!line) continue; |
||||||
|
|
||||||
|
// Handle @tags
|
||||||
|
if (line.startsWith("@fileoverview")) { |
||||||
|
const nameMatch = line.match(/@fileoverview\s+(\w+)\s+Component/); |
||||||
|
if (nameMatch) { |
||||||
|
component.name = nameMatch[1]; |
||||||
|
} |
||||||
|
const descMatch = lines |
||||||
|
.slice(i + 1) |
||||||
|
.find((l) => l && !l.startsWith("@")) |
||||||
|
?.trim(); |
||||||
|
if (descMatch) { |
||||||
|
component.description = descMatch; |
||||||
|
} |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (line.startsWith("@category")) { |
||||||
|
component.category = line.replace("@category", "").trim(); |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (line.startsWith("@prop")) { |
||||||
|
const prop = this.parseProp(line); |
||||||
|
if (prop) component.props.push(prop); |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (line.startsWith("@example")) { |
||||||
|
currentSection = "example"; |
||||||
|
currentExample = line.replace("@example", "").trim(); |
||||||
|
if (currentExample) { |
||||||
|
currentExample += "\n"; |
||||||
|
} |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (line.startsWith("@features")) { |
||||||
|
currentSection = "features"; |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (line.startsWith("@accessibility")) { |
||||||
|
currentSection = "accessibility"; |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (line.startsWith("@since")) { |
||||||
|
component.since = line.replace("@since", "").trim(); |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
// Handle content based on current section
|
||||||
|
if (currentSection === "example") { |
||||||
|
if (line === "```svelte" || line === "```") { |
||||||
|
inCodeBlock = !inCodeBlock; |
||||||
|
if (!inCodeBlock && currentExample.trim()) { |
||||||
|
component.examples.push({ |
||||||
|
name: currentExample.split("\n")[0] || "Example", |
||||||
|
code: currentExample.trim(), |
||||||
|
}); |
||||||
|
currentExample = ""; |
||||||
|
} |
||||||
|
continue; |
||||||
|
} |
||||||
|
if (inCodeBlock) { |
||||||
|
currentExample += line + "\n"; |
||||||
|
} else if (line.startsWith("@")) { |
||||||
|
// New section started
|
||||||
|
i--; // Reprocess this line
|
||||||
|
currentSection = "description"; |
||||||
|
} else if (line && !line.startsWith("```")) { |
||||||
|
currentExample = line + "\n"; |
||||||
|
} |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (currentSection === "features" && line.startsWith("-")) { |
||||||
|
component.features.push(line.substring(1).trim()); |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (currentSection === "accessibility" && line.startsWith("-")) { |
||||||
|
component.accessibility.push(line.substring(1).trim()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return component; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Parse a @prop line into structured prop data |
||||||
|
* @param {string} propLine |
||||||
|
* @returns {PropDefinition | null} |
||||||
|
*/ |
||||||
|
parseProp(propLine) { |
||||||
|
// First, extract the type by finding balanced braces
|
||||||
|
const propMatch = propLine.match(/@prop\s+\{/); |
||||||
|
if (!propMatch || propMatch.index === undefined) return null; |
||||||
|
|
||||||
|
// Find the closing brace for the type
|
||||||
|
let braceCount = 1; |
||||||
|
let typeEndIndex = propMatch.index + propMatch[0].length; |
||||||
|
const lineAfterType = propLine.substring(typeEndIndex); |
||||||
|
|
||||||
|
for (let i = 0; i < lineAfterType.length; i++) { |
||||||
|
if (lineAfterType[i] === "{") braceCount++; |
||||||
|
if (lineAfterType[i] === "}") braceCount--; |
||||||
|
if (braceCount === 0) { |
||||||
|
typeEndIndex += i; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const typeStr = propLine |
||||||
|
.substring(propMatch.index + propMatch[0].length, typeEndIndex) |
||||||
|
.trim(); |
||||||
|
const restOfLine = propLine.substring(typeEndIndex + 1).trim(); |
||||||
|
|
||||||
|
// Parse the rest: [name=default] or name - description
|
||||||
|
const restMatch = restOfLine.match( |
||||||
|
/(\[?)([^[\]\s=-]+)(?:=([^\]]*))?]?\s*-?\s*(.*)/, |
||||||
|
); |
||||||
|
|
||||||
|
if (!restMatch) return null; |
||||||
|
|
||||||
|
const [, isOptional, name, defaultValue, description] = restMatch; |
||||||
|
|
||||||
|
// Parse type - handle union types like "xs" | "s" | "m" | "l"
|
||||||
|
let type = [typeStr.trim()]; |
||||||
|
if (typeStr.includes("|") && !typeStr.includes("<")) { |
||||||
|
type = typeStr.split("|").map((t) => t.trim().replace(/"/g, "")); |
||||||
|
} else if (typeStr.includes('"') && !typeStr.includes("<")) { |
||||||
|
// Handle quoted literal types
|
||||||
|
const literals = typeStr.match(/"[^"]+"/g); |
||||||
|
if (literals) { |
||||||
|
type = literals.map((l) => l.replace(/"/g, "")); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
name: name.trim(), |
||||||
|
type: type, |
||||||
|
default: defaultValue |
||||||
|
? defaultValue.trim() |
||||||
|
: isOptional |
||||||
|
? undefined |
||||||
|
: null, |
||||||
|
description: description.trim(), |
||||||
|
required: !isOptional, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Process a single Svelte file |
||||||
|
* @param {string} filePath |
||||||
|
* @returns {ComponentDefinition | null} |
||||||
|
*/ |
||||||
|
processFile(filePath) { |
||||||
|
try { |
||||||
|
const content = fs.readFileSync(filePath, "utf-8"); |
||||||
|
const tsDocContent = this.extractTSDoc(content); |
||||||
|
|
||||||
|
if (!tsDocContent) { |
||||||
|
console.warn(`No TSDoc found in ${filePath}`); |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
const component = this.parseTSDoc(tsDocContent); |
||||||
|
|
||||||
|
// If no name was extracted, use filename
|
||||||
|
if (!component.name) { |
||||||
|
component.name = path.basename(filePath, ".svelte"); |
||||||
|
} |
||||||
|
|
||||||
|
return component; |
||||||
|
} catch (error) { |
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error); |
||||||
|
console.error(`Error processing ${filePath}:`, errorMessage); |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Process all Svelte files in a directory recursively |
||||||
|
* @param {string} dirPath |
||||||
|
*/ |
||||||
|
processDirectory(dirPath) { |
||||||
|
const items = fs.readdirSync(dirPath); |
||||||
|
|
||||||
|
for (const item of items) { |
||||||
|
const itemPath = path.join(dirPath, item); |
||||||
|
const stat = fs.statSync(itemPath); |
||||||
|
|
||||||
|
if (stat.isDirectory()) { |
||||||
|
this.processDirectory(itemPath); |
||||||
|
} else if (item.endsWith(".svelte")) { |
||||||
|
const component = this.processFile(itemPath); |
||||||
|
if (component) { |
||||||
|
this.components.push(component); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Generate the final JSON output |
||||||
|
*/ |
||||||
|
generateOutput() { |
||||||
|
// Sort components by category and name
|
||||||
|
this.components.sort((a, b) => { |
||||||
|
if (a.category !== b.category) { |
||||||
|
return a.category.localeCompare(b.category); |
||||||
|
} |
||||||
|
return a.name.localeCompare(b.name); |
||||||
|
}); |
||||||
|
|
||||||
|
return { |
||||||
|
library: "Alexandria Component Library", |
||||||
|
version: "1.0.0", |
||||||
|
generated: new Date().toISOString(), |
||||||
|
totalComponents: this.components.length, |
||||||
|
categories: [...new Set(this.components.map((c) => c.category))].sort(), |
||||||
|
components: this.components, |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Main execution |
||||||
|
*/ |
||||||
|
function main() { |
||||||
|
const parser = new ComponentParser(); |
||||||
|
const aFolderPath = __dirname; |
||||||
|
|
||||||
|
console.log('Parsing Alexandria components...'); |
||||||
|
console.log(`Source directory: ${aFolderPath}`); |
||||||
|
|
||||||
|
if (!fs.existsSync(aFolderPath)) { |
||||||
|
console.error(`Directory not found: ${aFolderPath}`); |
||||||
|
process.exit(1); |
||||||
|
} |
||||||
|
|
||||||
|
// Process all components
|
||||||
|
parser.processDirectory(aFolderPath); |
||||||
|
|
||||||
|
// Generate output
|
||||||
|
const output = parser.generateOutput(); |
||||||
|
|
||||||
|
// Write to file in the same directory (/a folder)
|
||||||
|
const outputPath = path.join(__dirname, 'alexandria-components.json'); |
||||||
|
fs.writeFileSync(outputPath, JSON.stringify(output, null, 2)); |
||||||
|
|
||||||
|
console.log(`\n✅ Successfully parsed ${output.totalComponents} components`); |
||||||
|
console.log(`📁 Categories: ${output.categories.join(', ')}`); |
||||||
|
console.log(`💾 Output saved to: ${outputPath}`); |
||||||
|
|
||||||
|
// Print summary
|
||||||
|
console.log('\n📊 Component Summary:'); |
||||||
|
/** @type {Record<string, number>} */ |
||||||
|
const categoryCounts = {}; |
||||||
|
output.components.forEach(c => { |
||||||
|
categoryCounts[c.category] = (categoryCounts[c.category] || 0) + 1; |
||||||
|
}); |
||||||
|
|
||||||
|
Object.entries(categoryCounts).forEach(([category, count]) => { |
||||||
|
console.log(` ${category}: ${count} components`); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
// Run the script
|
||||||
|
main(); |
||||||
@ -0,0 +1,266 @@ |
|||||||
|
@layer components { |
||||||
|
/* ======================================== |
||||||
|
Base Card Styles |
||||||
|
======================================== */ |
||||||
|
|
||||||
|
/* Main card leather theme */ |
||||||
|
.card-leather { |
||||||
|
@apply shadow-none text-primary-1000 border-s-4 bg-highlight |
||||||
|
border-primary-200 has-[:hover]:border-primary-700; |
||||||
|
@apply dark:bg-primary-1000 dark:border-primary-800 |
||||||
|
dark:has-[:hover]:bg-primary-950 dark:has-[:hover]:border-primary-500; |
||||||
|
} |
||||||
|
|
||||||
|
.card-leather h1, |
||||||
|
.card-leather h2, |
||||||
|
.card-leather h3, |
||||||
|
.card-leather h4, |
||||||
|
.card-leather h5, |
||||||
|
.card-leather h6 { |
||||||
|
@apply text-gray-900 hover:text-primary-600 dark:text-gray-100 |
||||||
|
dark:hover:text-primary-400; |
||||||
|
} |
||||||
|
|
||||||
|
.card-leather .font-thin { |
||||||
|
@apply text-gray-900 hover:text-primary-700 dark:text-gray-100 |
||||||
|
dark:hover:text-primary-300; |
||||||
|
} |
||||||
|
|
||||||
|
/* Main card leather (used in profile previews) */ |
||||||
|
.main-leather { |
||||||
|
@apply bg-primary-50 dark:bg-primary-1000 text-gray-900 dark:text-gray-100; |
||||||
|
} |
||||||
|
|
||||||
|
/* ======================================== |
||||||
|
Responsive Card Styles |
||||||
|
======================================== */ |
||||||
|
|
||||||
|
.responsive-card { |
||||||
|
@apply w-full min-w-0 overflow-hidden; |
||||||
|
} |
||||||
|
|
||||||
|
.responsive-card-content { |
||||||
|
@apply break-words overflow-hidden; |
||||||
|
} |
||||||
|
|
||||||
|
/* ======================================== |
||||||
|
Article Box Styles (Blog & Publication Cards) |
||||||
|
======================================== */ |
||||||
|
|
||||||
|
.ArticleBox { |
||||||
|
@apply shadow-none text-primary-1000 border-s-4 bg-highlight |
||||||
|
border-primary-200 has-[:hover]:border-primary-700; |
||||||
|
@apply dark:bg-primary-1000 dark:border-primary-800 |
||||||
|
dark:has-[:hover]:bg-primary-950 dark:has-[:hover]:border-primary-500; |
||||||
|
} |
||||||
|
|
||||||
|
.ArticleBox h1, |
||||||
|
.ArticleBox h2, |
||||||
|
.ArticleBox h3, |
||||||
|
.ArticleBox h4, |
||||||
|
.ArticleBox h5, |
||||||
|
.ArticleBox h6 { |
||||||
|
@apply text-gray-900 hover:text-primary-600 dark:text-gray-100 |
||||||
|
dark:hover:text-primary-400; |
||||||
|
} |
||||||
|
|
||||||
|
.ArticleBox .font-thin { |
||||||
|
@apply text-gray-900 hover:text-primary-700 dark:text-gray-100 |
||||||
|
dark:hover:text-primary-300; |
||||||
|
} |
||||||
|
|
||||||
|
/* Article box image transitions */ |
||||||
|
.ArticleBox.grid .ArticleBoxImage { |
||||||
|
@apply max-h-0; |
||||||
|
transition: max-height 0.5s ease; |
||||||
|
} |
||||||
|
|
||||||
|
.ArticleBox.grid.active .ArticleBoxImage { |
||||||
|
@apply max-h-40; |
||||||
|
} |
||||||
|
|
||||||
|
/* ======================================== |
||||||
|
Event Preview Card Styles |
||||||
|
======================================== */ |
||||||
|
|
||||||
|
/* Event preview card hover state */ |
||||||
|
.event-preview-card { |
||||||
|
@apply hover:bg-highlight dark:bg-primary-900/70 bg-primary-50 |
||||||
|
dark:hover:bg-primary-800 border-primary-400 border-s-4 |
||||||
|
transition-colors cursor-pointer |
||||||
|
focus:outline-none focus:ring-2 focus:ring-primary-500 shadow-none; |
||||||
|
} |
||||||
|
|
||||||
|
/* Event metadata badges */ |
||||||
|
.event-kind-badge { |
||||||
|
@apply text-[10px] px-1.5 py-0.5 rounded |
||||||
|
bg-gray-200 dark:bg-gray-700 |
||||||
|
text-gray-700 dark:text-gray-300; |
||||||
|
} |
||||||
|
|
||||||
|
.event-label { |
||||||
|
@apply text-xs uppercase tracking-wide |
||||||
|
text-gray-500 dark:text-gray-400; |
||||||
|
} |
||||||
|
|
||||||
|
/* Community badge */ |
||||||
|
.community-badge { |
||||||
|
@apply inline-flex items-center gap-1 |
||||||
|
text-[10px] px-1.5 py-0.5 rounded |
||||||
|
bg-yellow-100 dark:bg-yellow-900 |
||||||
|
text-yellow-700 dark:text-yellow-300; |
||||||
|
} |
||||||
|
|
||||||
|
/* ======================================== |
||||||
|
Profile Card Styles |
||||||
|
======================================== */ |
||||||
|
|
||||||
|
/* Profile verification badge (NIP-05) */ |
||||||
|
.profile-nip05-badge { |
||||||
|
@apply px-2 py-0.5 !mb-0 rounded |
||||||
|
bg-green-100 dark:bg-green-900 |
||||||
|
text-green-700 dark:text-green-300 text-xs; |
||||||
|
} |
||||||
|
|
||||||
|
/* Community status indicator */ |
||||||
|
.community-status-indicator { |
||||||
|
@apply flex-shrink-0 w-4 h-4 |
||||||
|
bg-yellow-100 dark:bg-yellow-900 |
||||||
|
rounded-full flex items-center justify-center; |
||||||
|
} |
||||||
|
|
||||||
|
.community-status-icon { |
||||||
|
@apply w-3 h-3 |
||||||
|
text-yellow-600 dark:text-yellow-400; |
||||||
|
} |
||||||
|
|
||||||
|
/* User list status indicator (heart) */ |
||||||
|
.user-list-indicator { |
||||||
|
@apply flex-shrink-0 w-4 h-4 |
||||||
|
bg-red-100 dark:bg-red-900 |
||||||
|
rounded-full flex items-center justify-center; |
||||||
|
} |
||||||
|
|
||||||
|
.user-list-icon { |
||||||
|
@apply w-3 h-3 |
||||||
|
text-red-600 dark:text-red-400; |
||||||
|
} |
||||||
|
|
||||||
|
/* ======================================== |
||||||
|
Card Content Styles |
||||||
|
======================================== */ |
||||||
|
|
||||||
|
/* Card content sections */ |
||||||
|
.card-header { |
||||||
|
@apply flex items-start w-full p-4; |
||||||
|
} |
||||||
|
|
||||||
|
.card-body { |
||||||
|
@apply px-4 pb-3 flex flex-col gap-2; |
||||||
|
} |
||||||
|
|
||||||
|
.card-footer { |
||||||
|
@apply px-4 pt-2 pb-3 |
||||||
|
border-t border-primary-200 dark:border-primary-700 |
||||||
|
flex items-center gap-2 flex-wrap; |
||||||
|
} |
||||||
|
|
||||||
|
/* Card content text styles */ |
||||||
|
.card-summary { |
||||||
|
@apply text-sm text-primary-900 dark:text-primary-200 line-clamp-2; |
||||||
|
} |
||||||
|
|
||||||
|
.card-content { |
||||||
|
@apply text-sm text-gray-800 dark:text-gray-200 |
||||||
|
line-clamp-3 break-words mb-4; |
||||||
|
} |
||||||
|
|
||||||
|
.card-about { |
||||||
|
@apply text-sm text-gray-700 dark:text-gray-300 line-clamp-3; |
||||||
|
} |
||||||
|
|
||||||
|
/* Deferral link styling */ |
||||||
|
.deferral-link { |
||||||
|
@apply underline |
||||||
|
text-primary-700 dark:text-primary-400 |
||||||
|
hover:text-primary-900 dark:hover:text-primary-200 |
||||||
|
break-all cursor-pointer; |
||||||
|
} |
||||||
|
|
||||||
|
/* ======================================== |
||||||
|
Tags and Badges |
||||||
|
======================================== */ |
||||||
|
|
||||||
|
.tags span { |
||||||
|
@apply bg-primary-50 text-primary-800 |
||||||
|
text-sm font-medium me-2 px-2.5 py-0.5 |
||||||
|
rounded-sm |
||||||
|
dark:bg-primary-900 dark:text-primary-200; |
||||||
|
} |
||||||
|
|
||||||
|
/* ======================================== |
||||||
|
Card Image Styles |
||||||
|
======================================== */ |
||||||
|
|
||||||
|
.card-image-container { |
||||||
|
@apply w-full bg-primary-200 dark:bg-primary-800 relative; |
||||||
|
} |
||||||
|
|
||||||
|
.card-banner { |
||||||
|
@apply w-full h-60 object-cover; |
||||||
|
} |
||||||
|
|
||||||
|
.card-avatar-container { |
||||||
|
@apply absolute w-fit top-[-56px]; |
||||||
|
} |
||||||
|
|
||||||
|
/* ======================================== |
||||||
|
Utility Classes for Cards |
||||||
|
======================================== */ |
||||||
|
|
||||||
|
/* Prose styling within cards - extends prose class when applied */ |
||||||
|
.card-prose { |
||||||
|
@apply max-w-none text-gray-900 dark:text-gray-100 |
||||||
|
break-words min-w-0; |
||||||
|
overflow-wrap: anywhere; |
||||||
|
} |
||||||
|
|
||||||
|
/* Card metadata grid */ |
||||||
|
.card-metadata-grid { |
||||||
|
@apply grid grid-cols-1 gap-y-2; |
||||||
|
} |
||||||
|
|
||||||
|
.card-metadata-label { |
||||||
|
@apply font-semibold min-w-[120px] flex-shrink-0; |
||||||
|
} |
||||||
|
|
||||||
|
.card-metadata-value { |
||||||
|
@apply min-w-0 break-words; |
||||||
|
} |
||||||
|
|
||||||
|
/* ======================================== |
||||||
|
Interactive Card States |
||||||
|
======================================== */ |
||||||
|
|
||||||
|
/* Clickable card states */ |
||||||
|
.card-clickable { |
||||||
|
@apply cursor-pointer transition-colors |
||||||
|
focus:outline-none focus:ring-2 focus:ring-primary-500; |
||||||
|
} |
||||||
|
|
||||||
|
.card-clickable:hover { |
||||||
|
@apply bg-primary-100 dark:bg-primary-800; |
||||||
|
} |
||||||
|
|
||||||
|
/* ======================================== |
||||||
|
Skeleton Loader for Cards |
||||||
|
======================================== */ |
||||||
|
|
||||||
|
.skeleton-leather div { |
||||||
|
@apply bg-primary-100 dark:bg-primary-800; |
||||||
|
} |
||||||
|
|
||||||
|
.skeleton-leather { |
||||||
|
@apply h-48; |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue