Browse Source

More docs and some styles refactoring

master
Nuša Pukšič 5 months ago committed by buttercat1791
parent
commit
df8fc381b5
  1. 56
      src/app.css
  2. 938
      src/lib/a/AGENTS.md
  3. 138
      src/lib/a/README.md
  4. 106
      src/lib/a/cards/AEventPreview.svelte
  5. 99
      src/lib/a/cards/AProfilePreview.svelte
  6. 53
      src/lib/a/forms/ACommentForm.svelte
  7. 61
      src/lib/a/forms/AMarkupForm.svelte
  8. 66
      src/lib/a/forms/ASearchForm.svelte
  9. 66
      src/lib/a/forms/ATextareaWithPreview.svelte
  10. 19
      src/lib/a/index.ts
  11. 42
      src/lib/a/nav/AFooter.svelte
  12. 54
      src/lib/a/nav/ANavbar.svelte
  13. 368
      src/lib/a/parse-components.js
  14. 50
      src/lib/a/primitives/AAlert.svelte
  15. 51
      src/lib/a/primitives/ADetails.svelte
  16. 51
      src/lib/a/primitives/AInput.svelte
  17. 56
      src/lib/a/primitives/ANostrBadge.svelte
  18. 51
      src/lib/a/primitives/ANostrBadgeRow.svelte
  19. 77
      src/lib/a/primitives/ANostrUser.svelte
  20. 66
      src/lib/a/primitives/APagination.svelte
  21. 43
      src/lib/a/primitives/AThemeToggleMini.svelte
  22. 43
      src/lib/a/reader/ATechBlock.svelte
  23. 26
      src/lib/a/reader/ATechToggle.svelte
  24. 40
      src/lib/components/cards/ProfileHeader.svelte
  25. 266
      src/styles/a/cards.css
  26. 5
      src/styles/a/forms.css

56
src/app.css

@ -9,6 +9,8 @@ @@ -9,6 +9,8 @@
@import "./styles/visualize.css";
@import "./styles/asciidoc.css";
@import "theme-tokens.css";
@import "./styles/a/cards.css";
@import "./styles/a/forms.css";
@import "./styles/a/primitives.css";
@layer theme, base, components, utilities;
@ -183,28 +185,6 @@ @@ -183,28 +185,6 @@
@apply border border-primary-700;
}
div.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;
}
div.card-leather h1,
div.card-leather h2,
div.card-leather h3,
div.card-leather h4,
div.card-leather h5,
div.card-leather h6 {
@apply text-gray-900 hover:text-primary-600 dark:text-gray-100
dark:hover:text-primary-400;
}
div.card-leather .font-thin {
@apply text-gray-900 hover:text-primary-700 dark:text-gray-100
dark:hover:text-primary-300;
}
main {
@apply max-w-full flex;
}
@ -245,15 +225,6 @@ @@ -245,15 +225,6 @@
@apply 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;
}
h1.h-leather {
@apply text-4xl font-bold;
}
@ -323,14 +294,6 @@ @@ -323,14 +294,6 @@
dark:hover:text-primary-400;
}
div.skeleton-leather div {
@apply bg-primary-100 dark:bg-primary-800;
}
div.skeleton-leather {
@apply h-48;
}
div.textarea-leather {
@apply bg-primary-50 dark:bg-primary-1000;
}
@ -453,21 +416,6 @@ @@ -453,21 +416,6 @@
dark:hover:text-primary-400;
}
/* Card with transition */
.ArticleBox.grid .ArticleBoxImage {
@apply max-h-0;
transition: max-height 0.5s ease;
}
.ArticleBox.grid.active .ArticleBoxImage {
@apply max-h-40;
}
.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;
}
.npub-badge {
@apply inline-flex space-x-1 items-center text-primary-600
dark:text-primary-500 hover:underline me-2 px-2 py-0.5 rounded-sm border

938
src/lib/a/AGENTS.md

@ -0,0 +1,938 @@ @@ -0,0 +1,938 @@
# Alexandria Component Library - AI Agent Guide
**Version:** 1.0.0
**Last Updated:** October 4, 2025
This guide provides comprehensive instructions for AI agents working with the Alexandria UI component library. Following these guidelines ensures consistency, maintainability, and proper integration with the existing codebase.
---
## Core Principles
### 1. Always Check Before Creating
Before creating any new UI component, **you must**:
1. Search the existing component library in `src/lib/a/`
2. Review `alexandria-components.json` for available components
3. Check if an existing component can be reused or extended
4. Only create new components when absolutely necessary
### 2. Use alexandria-components.json as Your Reference
The `alexandria-components.json` file is the **single source of truth** for all available components. It contains:
- Complete component inventory (18 components across 5 categories)
- Full prop definitions with types and defaults
- Usage examples and patterns
- Features and accessibility information
**Always consult this file first** when selecting components.
### 3. Maintain TSDoc Documentation Standards
All components use **TSDoc format** for documentation. You must maintain this standard when creating or modifying components.
---
## Component Inventory
### Available Categories
1. **Primitives** (8 components) - Basic UI building blocks
2. **Navigation** (2 components) - App navigation elements
3. **Forms** (4 components) - Input and editing interfaces
4. **Cards** (2 components) - Content display cards
5. **Reader** (2 components) - Technical content controls
---
## Component Selection Workflow
### Step 1: Identify the Need
Determine what UI functionality is required:
- User input? → Check **Forms** category
- Display content? → Check **Cards** category
- Navigation? → Check **Navigation** category
- Basic UI element? → Check **Primitives** category
- Technical content toggle? → Check **Reader** category
### Step 2: Search alexandria-components.json
For example: Finding a component for user profiles
- Search for: "profile", "user", or relevant keywords
- Result: AProfilePreview and ANostrUser are available
### Step 3: Review Component Props
Check the component's props in `alexandria-components.json`:
```json
{
"name": "AEventPreview",
"props": [
{
"name": "event",
"type": ["NDKEvent"],
"required": true,
"description": "The nostr event to display (required)"
},
{
"name": "showContent",
"type": ["boolean"],
"description": "Whether to show event content",
"required": false
}
// ... more props
]
}
```
### Step 4: Review Examples
Check the `examples` array in `alexandria-components.json` for usage patterns.
---
## TSDoc Documentation Standard
### Required Documentation Structure
Every component **must** include TSDoc comments following this exact format:
````typescript
/**
* @fileoverview ComponentName Component - Alexandria
*
* A brief description of what the component does and its primary purpose.
* Can span multiple lines for detailed explanation.
*
* @component
* @category [Primitives|Navigation|Forms|Cards|Reader]
*
* @prop {type} [propName] - Prop description (if optional)
* @prop {type} propName - Prop description (if required)
* @prop {type} [propName=defaultValue] - Prop with default value
*
* @example
* ```svelte
* <ComponentName requiredProp={value} optionalProp={value} ></ComponentName>
* ```
*
*
* @features
* - Feature 1
* - Feature 2
* - Feature 3
*
* @accessibility
* - Accessibility feature 1
* - ARIA compliance notes
* - Keyboard navigation details
*/
````
### TSDoc Tags Reference
| Tag | Purpose | Required | Example |
|-----|---------|----------|---------|
| `@fileoverview` | Component name and description | ✅ Yes | `@fileoverview AAlert Component - Alexandria` |
| `@component` | Marks file as Svelte component | ✅ Yes | `@component` |
| `@category` | Component category | ✅ Yes | `@category Primitives` |
| `@prop` | Define component properties | ✅ Yes | `@prop {string} [color] - Alert color theme` |
| `@example` | Usage examples with code | ✅ Yes | See format above |
| `@features` | List key functionality | ✅ Yes | `- Responsive layout` |
| `@accessibility` | Accessibility notes | ✅ Yes | `- ARIA compliant` |
| `@since` | Version introduced | ⚠ Optional | `@since 1.0.0` |
---
## Component Import Patterns
### Import
```typescript
import { AAlert, AEventPreview, AMarkupForm } from '$lib/a';
```
---
## Creating New Components
### When to Create a New Component
**DO create a new component when:**
- No existing component provides the required functionality
- The component will be reused in 3+ places
- It represents a distinct, standalone UI pattern
- It follows the Alexandria design system
**DON'T create a new component when:**
- An existing component can be configured to meet needs
- It's only used once (keep in parent component)
- It's too generic (use Flowbite components directly)
- It doesn't fit the Alexandria design system
### New Component Checklist
- [ ] **Verify** no existing component can be used
- [ ] **Choose** appropriate category (Primitives/Navigation/Forms/Cards/Reader)
- [ ] **Name** following convention: `A[ComponentName].svelte`
- [ ] **Add TSDoc** documentation following standard format
- [ ] **Create** component file in correct category folder
- [ ] **Add styles** to appropriate `/src/styles/a/` CSS file (or create new file if needed)
- [ ] **Import new CSS file** in `app.css` if created
- [ ] **Export** component in `index.ts`
- [ ] **Update** `alexandria-components.json` (run parse script)
- [ ] **Test** component functionality
- [ ] **Validate** accessibility compliance
### Component Template
````svelte
<script lang="ts">
/**
* @fileoverview AYourComponent Component - Alexandria
*
* Brief description of what this component does and why it exists.
*
* @component
* @category [Primitives|Navigation|Forms|Cards|Reader]
*
* @prop {type} [propName] - Description
*
* @example
* ```svelte
* <AYourComponent prop={value} />
* ```
*
* @features
* - Feature description
*
* @accessibility
* - Accessibility feature
*/
import { /* Flowbite components */ } from "flowbite-svelte";
let {
propName = $bindable("default"),
// ... other props
} = $props<{
propName?: string;
}>();
// Component logic
</script>
<!-- Component template -->
<div class="themed-container">
<!-- Component markup -->
</div>
````
---
## Updating alexandria-components.json
### When to Update
You **must** update `alexandria-components.json` whenever:
- A new component is created
- Component props are added, modified, or removed
- Component examples are updated
- Component features or accessibility notes change
- Component documentation is enhanced
### How to Update
1. **Make your changes** to component TSDoc comments
2. **Run the parser script**:
```bash
cd src/lib/a
node parse-components.js
```
3. **Verify** the generated JSON is valid
4. **Commit** both the component file and updated JSON
### Parser Script Location
- **File:** `src/lib/a/parse-components.js`
- **Purpose:** Extracts TSDoc from all `.svelte` files in the library
- **Output:** `alexandria-components.json`
### Manual Updates (When Necessary)
If the parser doesn't capture something correctly:
1. First, try to fix the TSDoc format
2. Re-run the parser
3. Only manually edit JSON as last resort
4. Document any manual changes in comments
---
## Design System Integration
### Theme Compatibility
All Alexandria components support:
- Light and dark themes
- "Leather" aesthetic (warm browns, tans)
- Flowbite-based styling
- Tailwind CSS utilities
### Styling Guidelines
**DO:**
- Use Tailwind CSS classes
- Leverage Flowbite Svelte components as base
- Follow existing color patterns in library
- Ensure responsive design (mobile-first)
- Test in both light and dark modes
**DON'T:**
- Add arbitrary custom CSS without justification
- Override Flowbite's accessibility features
- Hard-code colors (use theme tokens)
- Create layout inconsistencies
### Component Styling Pattern
```svelte
<!-- Use Flowbite component as base -->
<Alert color="info" class="leather-theme">
<!-- Alexandria-specific theming applied via class -->
</Alert>
```
---
## Styling Architecture (CRITICAL)
### **Mandatory Styling Rules**
**ALL custom styles for Alexandria components MUST go in the `/src/styles/a/` folder.**
**DO NOT add `<style>` blocks inside component `.svelte` files unless absolutely necessary for component-scoped styles that cannot be reused.**
### Directory Structure
```
src/
├── styles/
│ └── a/
│ ├── cards.css ← Styles for Card components
│ ├── forms.css ← Styles for Form components
│ ├── primitives.css ← Styles for Primitive components
│ ├── nav.css ← Styles for Navigation components (create if needed)
│ └── reader.css ← Styles for Reader components (create if needed)
├── app.css ← Main CSS file that imports all styles
└── lib/
└── a/
├── cards/ ← Component files (minimal/no styles)
├── forms/ ← Component files (minimal/no styles)
├── primitives/ ← Component files (minimal/no styles)
├── nav/ ← Component files (minimal/no styles)
└── reader/ ← Component files (minimal/no styles)
```
### Styling Workflow
#### 1. **Determine Component Category**
Identify which category your component belongs to:
- `primitives/``styles/a/primitives.css`
- `forms/``styles/a/forms.css`
- `cards/``styles/a/cards.css`
- `nav/``styles/a/nav.css`
- `reader/``styles/a/reader.css`
#### 2. **Check if Style File Exists**
Before adding styles, verify the corresponding CSS file exists in `src/styles/a/`:
**Existing files:**
- ✅ `styles/a/cards.css`
- ✅ `styles/a/forms.css`
- ✅ `styles/a/primitives.css`
**Files to create when needed:**
- ⚠ `styles/a/nav.css` (create for navigation components)
- ⚠ `styles/a/reader.css` (create for reader components)
#### 3. **Create New Style File (If Needed)**
If the CSS file doesn't exist for your category:
**Step A: Create the file**
```bash
# Create the new CSS file
type nul > src\styles\a\nav.css
```
**Step B: Add the CSS structure**
```css
/* src/styles/a/nav.css */
/* Alexandria Navigation Component Styles */
@layer components {
/* ANavbar styles */
.navbar-alexandria {
@apply /* your Tailwind classes */;
}
/* AFooter styles */
.footer-alexandria {
@apply /* your Tailwind classes */;
}
}
```
**Step C: Import in app.css**
Add the import to `src/app.css` in the correct location, after existing imports.
#### 4. **Add Component Styles**
Add your styles to the appropriate file using the `@layer components` directive:
```css
/* Example: src/styles/a/primitives.css */
@layer components {
/* AAlert component styles */
.alert-leather {
@apply border border-s-4;
@apply bg-primary-50 dark:bg-primary-1000;
@apply text-gray-900 dark:text-gray-100;
}
/* AInput component styles */
.input-alexandria {
@apply bg-primary-50 dark:bg-primary-1000;
@apply border-s-4 border-primary-200;
@apply focus:border-primary-600 dark:focus:border-primary-400;
}
/* ANostrBadge component styles */
.badge-alexandria {
@apply inline-flex space-x-1 items-center;
@apply text-primary-600 dark:text-primary-500;
@apply border border-primary-600 dark:border-primary-500;
}
}
```
#### 5. **Use Classes in Components**
Reference the CSS classes in your Svelte components:
```svelte
<script lang="ts">
/**
* @fileoverview AAlert Component - Alexandria
* ...TSDoc
*/
import { Alert } from "flowbite-svelte";
let { color = "info", dismissable = false } = $props();
</script>
<!-- Use the CSS class from styles/a/primitives.css -->
<Alert {color} {dismissable} class="alert-leather">
{@render children?.()}
</Alert>
```
### CSS Organization Best Practices
#### Naming Conventions
- Use descriptive, component-specific class names
- Prefix with component name or category
- Use `-leather` or `-alexandria` suffix for themed styles
- Examples:
- `.alert-leather`
- `.card-alexandria`
- `.navbar-main-leather`
- `.form-input-alexandria`
#### File Structure Within CSS Files
Organize styles by component within each file:
```css
/* src/styles/a/forms.css */
@layer components {
/* ========================================
* AMarkupForm Component
* ======================================== */
.markup-form-container {
@apply /* styles */;
}
.markup-form-header {
@apply /* styles */;
}
/* ========================================
* ACommentForm Component
* ======================================== */
.comment-form-wrapper {
@apply /* styles */;
}
.comment-form-textarea {
@apply /* styles */;
}
/* ========================================
* ATextareaWithPreview Component
* ======================================== */
.textarea-preview-container {
@apply /* styles */;
}
.textarea-toolbar {
@apply /* styles */;
}
}
```
#### Use Tailwind @apply Directive
Prefer `@apply` with Tailwind utilities over custom CSS:
```css
/* ✅ GOOD: Using Tailwind utilities */
.card-leather {
@apply shadow-none text-primary-1000 border-s-4 bg-highlight;
@apply border-primary-200 has-[:hover]:border-primary-700;
@apply dark:bg-primary-1000 dark:border-primary-800;
}
/* ❌ AVOID: Custom CSS properties */
.card-leather {
box-shadow: none;
color: #1a1a1a;
border-left: 4px solid #e5d5c5;
background: #f9f6f1;
}
```
### When Component-Scoped Styles Are Acceptable
Use `<style>` blocks in `.svelte` files ONLY when:
1. **Truly unique** - Style is used nowhere else and never will be
2. **Animation keyframes** - Component-specific animations
3. **CSS variables** - Component-specific custom properties
4. **Pseudo-elements** - Complex ::before/::after that can't use Tailwind
```svelte
<!-- Acceptable use of component-scoped styles -->
<style>
/* Unique animation for this component only */
@keyframes pulse-custom {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.loading-indicator {
animation: pulse-custom 2s infinite;
}
</style>
```
### Checklist for Adding Styles
- [ ] Identified component category (primitives/forms/cards/nav/reader)
- [ ] Checked if corresponding CSS file exists in `src/styles/a/`
- [ ] Created new CSS file if needed (e.g., `nav.css`, `reader.css`)
- [ ] Added import to `src/app.css` for new CSS file
- [ ] Used `@layer components` directive
- [ ] Followed naming conventions (`-leather` or `-alexandria` suffix)
- [ ] Used Tailwind `@apply` directive instead of custom CSS
- [ ] Added comments to organize styles by component
- [ ] Tested in both light and dark themes
- [ ] Verified no `<style>` block in component (unless necessary)
### Example: Adding Styles for ANavbar (Navigation Component)
**Step 1:** Check if `styles/a/nav.css` exists (it doesn't)
**Step 2:** Create the file
```bash
type nul > src\styles\a\nav.css
```
**Step 3:** Add styles to the new file
```css
/* src/styles/a/nav.css */
/* Alexandria Navigation Component Styles */
@layer components {
/* ========================================
* ANavbar Component
* ======================================== */
.navbar-alexandria-main {
@apply bg-primary-50 dark:bg-primary-1000 z-10;
@apply border-b border-primary-200 dark:border-primary-800;
}
.navbar-alexandria-main svg {
@apply fill-gray-900 hover:fill-primary-600;
@apply dark:fill-gray-100 dark:hover:fill-primary-400;
}
.navbar-alexandria-main h1,
.navbar-alexandria-main h2 {
@apply text-gray-900 hover:text-primary-600;
@apply dark:text-gray-100 dark:hover:text-primary-400;
}
/* ========================================
* AFooter Component
* ======================================== */
.footer-alexandria {
@apply bg-primary-100 dark:bg-primary-950;
@apply border-t border-primary-200 dark:border-primary-800;
}
}
```
**Step 4:** Import in app.css
```css
/* src/app.css */
@import "./styles/a/cards.css";
@import "./styles/a/forms.css";
@import "./styles/a/primitives.css";
@import "./styles/a/nav.css"; /* ← NEW */
```
**Step 5:** Use in component
```svelte
<!-- src/lib/a/nav/ANavbar.svelte -->
<script lang="ts">
/**
* @fileoverview ANavbar Component - Alexandria
* ...
*/
import { Navbar } from "flowbite-svelte";
</script>
<Navbar class="navbar-alexandria-main">
<!-- Navbar content -->
</Navbar>
```
---
## Testing Requirements
### Component Testing
When creating or modifying components:
1. **Visual Testing**
- Test in light and dark themes
- Test on mobile and desktop viewports
- Verify responsive behavior
2. **Functional Testing**
- Test all prop combinations
- Verify event handlers work
- Test edge cases (empty data, long text, etc.)
3. **Accessibility Testing**
- Keyboard navigation
- Screen reader compatibility
- ARIA attributes present
- Color contrast ratios
4. **Integration Testing**
- Test within actual application pages
- Verify imports work correctly
- Check for console errors
---
## Common Patterns & Best Practices
### Bindable Props
Use `$bindable` for two-way data binding:
```typescript
let {
value = $bindable(""),
isOpen = $bindable(false)
} = $props<{
value?: string;
isOpen?: boolean;
}>();
```
### Event Handlers
Use optional function props for callbacks:
```typescript
let {
onSubmit = async () => {},
onClick
} = $props<{
onSubmit?: (data: string) => Promise<void>;
onClick?: () => void;
}>();
```
### Conditional Rendering
```svelte
{#if showContent}
<div>Content</div>
{/if}
```
### Snippets (Svelte 5)
For flexible content slots:
```typescript
@prop {snippet} children - Main content (required)
@prop {snippet} [title] - Optional title section
```
```svelte
{#snippet children()}
Content goes here
{/snippet}
```
---
## Common Mistakes to Avoid
### DON'T: Create duplicate components
```typescript
// Bad: Creating new component without checking
export default AUserCard.svelte // AProfilePreview already exists!
```
### DO: Use existing components
```typescript
// Good: Use existing component
import { AProfilePreview } from '$lib/a';
```
### DON'T: Skip TSDoc documentation
```svelte
<!-- Bad: No documentation -->
<script>
let { prop } = $props();
</script>
```
### DO: Add complete TSDoc
```svelte
<!-- Good: Complete documentation -->
<script>
/**
* @fileoverview AComponent - Alexandria
* ... complete TSDoc
*/
let { prop } = $props();
</script>
```
### DON'T: Forget to update JSON
```bash
# Bad: Only editing component, not updating JSON
# (Other agents won't know about your changes!)
```
### DO: Always run the parser
```bash
# Good: Update JSON after changes
cd src/lib/a
node parse-components.js
```
### DON'T: Use generic component names
```typescript
// Bad: Could be anything
export UserDisplay.svelte
```
### DO: Follow naming convention
```typescript
// Good: Clearly part of Alexandria library
export ANostrUser.svelte
```
---
## Integration with Application
### Directory Structure
```
src/lib/a/
├── AGENTS.md ← This file
├── README.md ← User documentation
├── alexandria-components.json ← Component registry (auto-generated)
├── index.ts ← Main export file
├── parse-components.js ← JSON generator script
├── primitives/ ← Basic UI elements
├── navigation/ ← Nav components
├── forms/ ← Input/form components
├── cards/ ← Content cards
└── reader/ ← Reader-specific components
```
### Adding Component to Library
1. **Create component** in appropriate category folder
2. **Add export** to `index.ts`:
```typescript
export { default as AYourComponent } from "./category/AYourComponent.svelte";
```
3. **Update JSON** by running parser script
4. **Verify import** works in application
---
## Examples: Component Selection Scenarios
### Scenario 1: Displaying User Information
**Need:** Show user profile with avatar and bio
**Process:**
1. Check `alexandria-components.json` for "user" or "profile"
2. Find: `ANostrUser` (compact) and `AProfilePreview` (full card)
3. Choose based on needs:
- Compact display in list: `ANostrUser`
- Full profile card: `AProfilePreview`
**Solution:**
```svelte
<AProfilePreview
{npub}
showBio={true}
showBadges={true}
/>
```
### Scenario 2: Creating a Comment Form
**Need:** Allow users to write and submit comments
**Process:**
1. Check `alexandria-components.json` under "Forms"
2. Find: `ACommentForm` - "Comment creation with markup support"
3. Review props: `content`, `placeholder`, `onSubmit`
**Solution:**
```svelte
<ACommentForm
bind:content={commentText}
placeholder="Write your comment..."
onSubmit={handleCommentSubmit}
/>
```
### Scenario 3: Showing Event Cards
**Need:** Display nostr events in a feed
**Process:**
1. Check `alexandria-components.json` under "Cards"
2. Find: `AEventPreview` - "Event preview cards with metadata and actions"
3. Review configurable options: `showContent`, `truncateContentAt`, etc.
**Solution:**
```svelte
{#each events as event}
<AEventPreview
{event}
showContent={true}
truncateContentAt={200}
onSelect={handleEventSelect}
/>
{/each}
```
### Scenario 4: Alert/Notification
**Need:** Show success message after save
**Process:**
1. Check "Primitives" category
2. Find: `AAlert` - "Themed alert messages with dismissal options"
3. Review color options and dismissable prop
**Solution:**
```svelte
{#if saveSuccessful}
<AAlert color="success" dismissable={true}>
{#snippet children()}Your changes have been saved.{/snippet}
</AAlert>
{/if}
```
---
## Troubleshooting
### Component Not Found
1. Verify component exists in `alexandria-components.json`
2. Check import statement syntax
3. Ensure component is exported in `index.ts`
4. Check file path matches category structure
### Props Not Working
1. Check prop name spelling in `alexandria-components.json`
2. Verify prop type matches expected type
3. Check if prop is bindable (use `bind:` prefix if needed)
4. Review component TSDoc for usage examples
### Styling Issues
1. Verify theme mode (light/dark) is set correctly
2. Check if custom classes conflict with Flowbite
3. Ensure Tailwind classes are being applied
4. Test in isolation to identify conflicts
### Documentation Out of Sync
1. Run parser script: `node parse-components.js`
2. Verify TSDoc format is correct
3. Check for syntax errors in TSDoc comments
4. Manually review generated JSON
---
## Additional Resources
### Related Files
- **README.md**: User-facing library documentation
- **index.ts**: Component export definitions
- **parse-components.js**: TSDoc extraction script
- **alexandria-components.json**: Generated component registry
### External Documentation
- [Flowbite Svelte](https://flowbite-svelte.com/) - Base component library
- [Tailwind CSS](https://tailwindcss.com/) - Styling framework
- [TSDoc](https://tsdoc.org/) - Documentation standard
- [Svelte 5 Docs](https://svelte.dev/docs) - Svelte framework
### Nostr-Specific
- [NDK Documentation](https://github.com/nostr-dev-kit/ndk) - Nostr Development Kit
- [NIPs](https://github.com/nostr-protocol/nips) - Nostr Implementation Possibilities
---
## Quick Checklist for AI Agents
Before creating or modifying any UI component:
- [ ] Searched `alexandria-components.json` for existing components
- [ ] Reviewed component props and examples
- [ ] Verified no existing component meets the need
- [ ] Chosen appropriate category for new component
- [ ] Named component with `A` prefix (e.g., `AComponentName`)
- [ ] Added complete TSDoc documentation
- [ ] Followed TSDoc format exactly as documented
- [ ] Included all required tags (@fileoverview, @component, @category, @prop, @example, @features, @accessibility)
- [ ] Added component export to `index.ts`
- [ ] Ran `node parse-components.js` to update JSON
- [ ] Tested component in light and dark themes
- [ ] Verified accessibility compliance
- [ ] Checked for TypeScript/linting errors
---
## Summary
1. **Always search first** - Check `alexandria-components.json` before creating anything
2. **Use TSDoc religiously** - Every component must have complete TSDoc documentation
3. **Update the JSON** - Run parser script after any component changes
4. **Follow naming conventions** - `A` prefix, PascalCase, descriptive names
5. **Maintain consistency** - Match existing patterns and style
7. **Don't reinvent** - Reuse existing components whenever possible
**Remember:** The goal is to maintain a cohesive, well-documented, and easily discoverable
component library that serves the entire Alexandria application ecosystem.
---
**Questions or Issues?**
Refer to this guide, review existing components for patterns, or check the main README.md for additional context.

138
src/lib/a/README.md

@ -1,11 +1,133 @@ @@ -1,11 +1,133 @@
# Component Library
# Alexandria Component Library
This folder contains a component library. The idea is to have project-scoped
reusable components that centralize theming and style rules, so that main pages
and layouts focus on the functionalities.
A comprehensive, project-scoped component library for the Alexandria nostr application. All components are built on Flowbite Svelte and Tailwind CSS, providing consistent theming and accessibility across the application.
All components are based on Flowbite Svelte components, which are built on top
of Tailwind CSS.
> **For AI Agents & Detailed Guidelines:** See [AGENTS.md](./AGENTS.md) for complete workflow instructions, styling architecture, and component creation guidelines.
Keeping all the styles in one place allows us to easily change the look and feel
of the application by switching themes.
## Quick Start
```typescript
// Import components from the library
import { AAlert, AEventPreview, AMarkupForm } from '$lib/a';
// Use in your Svelte components
<AAlert color="success" dismissable={true}>
{#snippet children()}Your changes have been saved!{/snippet}
</AAlert>
```
## Component Categories
### 🧱 Primitives (8)
Basic building blocks: `AAlert`, `ADetails`, `AInput`, `ANostrBadge`, `ANostrBadgeRow`, `ANostrUser`, `APagination`, `AThemeToggleMini`
### 🧭 Navigation (2)
App navigation: `ANavbar`, `AFooter`
### 📝 Forms (4)
Input interfaces: `ACommentForm`, `AMarkupForm`, `ASearchForm`, `ATextareaWithPreview`
### 🃏 Cards (2)
Content display: `AEventPreview`, `AProfilePreview`
### 👁 Reader (2)
Technical content controls: `ATechBlock`, `ATechToggle`
## Component Reference
All components are documented in `alexandria-components.json`. This file contains:
- Complete prop definitions with types and defaults
- Usage examples and patterns
- Features and accessibility information
**View component details:**
```bash
# Generate/update the component reference
cd src/lib/a
node parse-components.js
```
## Usage Examples
### Display a user profile
```svelte
<ANostrUser
{npub}
{profile}
size="lg"
showBadges={true}
href="/profile/{npub}"
/>
```
### Show an event card
```svelte
<AEventPreview
{event}
label="Article"
showContent={true}
actions={[{label: "View", onClick: handleView}]}
/>
```
### Rich text editor with preview
```svelte
<ATextareaWithPreview
bind:value={content}
parser={parseMarkup}
previewSnippet={markupRenderer}
placeholder="Write your content..."
/>
```
### Alert notification
```svelte
{#if saveSuccessful}
<AAlert color="success" dismissable={true}>
{#snippet children()}Your changes have been saved.{/snippet}
</AAlert>
{/if}
```
## Key Features
- ✅ **Consistent theming** - Automatic light/dark mode support
- ✅ **Accessibility first** - ARIA attributes, keyboard navigation, screen reader friendly
- ✅ **TypeScript support** - Full type definitions for all props
- ✅ **TSDoc documented** - Machine-readable documentation for AI tools
- ✅ **Flexible APIs** - Sensible defaults with extensive customization options
## Documentation
All components follow TSDoc format with these tags:
- `@fileoverview` - Component description
- `@category` - Component category
- `@prop` - Property definitions with types
- `@example` - Usage examples
- `@features` - Key functionality
- `@accessibility` - Accessibility notes
The `parse-components.js` script extracts this documentation into `alexandria-components.json` for automated tooling and AI agents.
## Contributing
When adding components:
1. Follow the `A[ComponentName]` naming convention
2. Add complete TSDoc documentation
3. Place in the appropriate category folder
4. Export from `index.ts`
5. Run `node parse-components.js` to update the JSON reference
**See [AGENTS.md](./AGENTS.md) for detailed guidelines on:**
- Component creation workflow
- Styling architecture (mandatory `/src/styles/a/` folder structure)
- TSDoc documentation standards
- Testing requirements
- Common patterns and best practices
## Resources
- [Flowbite Svelte](https://flowbite-svelte.com/) - Base component library
- [Tailwind CSS](https://tailwindcss.com/) - Styling framework
- [TSDoc](https://tsdoc.org/) - Documentation standard
- [Svelte 5](https://svelte.dev/docs) - Framework documentation

106
src/lib/a/cards/AEventPreview.svelte

@ -1,4 +1,78 @@ @@ -1,4 +1,78 @@
<script lang="ts">
/**
* @fileoverview AEventPreview Component - Alexandria
*
* A card component for displaying nostr event previews with configurable display options.
* Shows event metadata, content, author information, and action buttons.
*
* @component
* @category Cards
*
* @prop {NDKEvent} event - The nostr event to display (required)
* @prop {string} [label=""] - Optional label/category for the event
* @prop {boolean} [community=false] - Whether this is a community event
* @prop {number} [truncateContentAt=200] - Character limit for content truncation
* @prop {boolean} [showKind=true] - Whether to show event kind
* @prop {boolean} [showSummary=true] - Whether to show event summary
* @prop {boolean} [showDeferralNaddr=true] - Whether to show deferral naddr
* @prop {boolean} [showPublicationLink=true] - Whether to show publication link
* @prop {boolean} [showContent=true] - Whether to show event content
* @prop {Array<{label: string, onClick: (ev: NDKEvent) => void, variant?: string}>} [actions] - Action buttons
* @prop {(ev: NDKEvent) => void} [onSelect] - Callback when event is selected
* @prop {(naddr: string, ev: NDKEvent) => void} [onDeferralClick] - Callback for deferral clicks
*
* @example
* ```svelte
* <AEventPreview
* {event}
* label="Article"
* showContent={true}
* actions={[{label: "View", onClick: handleView}]}
* />
* ```
*
* @example Basic event preview
* ```svelte
* <AEventPreview {event} />
* ```
*
* @example Community event with actions
* ```svelte
* <AEventPreview
* {event}
* community={true}
* actions={[
* {label: "Reply", onClick: handleReply},
* {label: "Share", onClick: handleShare, variant: "light"}
* ]}
* />
* ```
*
* @example Minimal preview without content
* ```svelte
* <AEventPreview
* {event}
* showContent={false}
* showSummary={false}
* truncateContentAt={100}
* />
* ```
*
* @features
* - Responsive card layout with author badges
* - Content truncation with "show more" functionality
* - Publication links and metadata display
* - Configurable action buttons
* - Community event highlighting
* - Event kind and summary display
*
* @accessibility
* - Semantic card structure
* - Keyboard accessible action buttons
* - Screen reader friendly metadata
* - Proper heading hierarchy
*/
import { Card, Button } from "flowbite-svelte";
import ViewPublicationLink from "$lib/components/util/ViewPublicationLink.svelte";
import { userBadge } from "$lib/snippets/UserSnippets.svelte";
@ -122,7 +196,7 @@ @@ -122,7 +196,7 @@
</script>
<Card
class="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"
class="event-preview-card"
role="group"
tabindex="0"
aria-label="Event preview"
@ -131,26 +205,22 @@ @@ -131,26 +205,22 @@
size="xl"
>
<!-- Header -->
<div class="flex items-start w-full p-4">
<div class="card-header">
<!-- Meta -->
<div class="flex flex-row w-full gap-3 items-center min-w-0">
{#if label}
<span
class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400"
>
<span class="event-label">
{label}
</span>
{/if}
{#if showKind}
<span
class="text-[10px] px-1.5 py-0.5 rounded bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300"
>
<span class="event-kind-badge">
Kind {event.kind}
</span>
{/if}
{#if community}
<span
class="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"
class="community-badge"
title="Has posted to the community"
>
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24">
@ -172,16 +242,14 @@ @@ -172,16 +242,14 @@
</div>
<!-- Body -->
<div class="px-4 pb-3 flex flex-col gap-2">
<div class="card-body">
{#if event.kind === 0 && profileData?.about}
<div class="text-sm text-gray-700 dark:text-gray-300 line-clamp-3">
<div class="card-about">
{clippedContent(profileData.about)}
</div>
{:else}
{#if summary}
<div
class="text-sm text-primary-900 dark:text-primary-200 line-clamp-2"
>
<div class="card-summary">
{summary}
</div>
{/if}
@ -189,7 +257,7 @@ @@ -189,7 +257,7 @@
<div class="text-xs text-primary-800 dark:text-primary-300">
Read
<span
class="underline text-primary-700 dark:text-primary-400 hover:text-primary-900 dark:hover:text-primary-200 break-all cursor-pointer"
class="deferral-link"
role="button"
tabindex="0"
onclick={handleDeferralClick}
@ -206,9 +274,7 @@ @@ -206,9 +274,7 @@
{/if}
{#if showContent && event.content}
<div
class="text-sm text-gray-800 dark:text-gray-200 line-clamp-3 break-words mb-4"
>
<div class="card-content">
{clippedContent(event.content)}
</div>
{/if}
@ -217,9 +283,7 @@ @@ -217,9 +283,7 @@
<!-- Footer / Actions -->
{#if showPublicationLink && event.kind !== 0}
<div
class="px-4 pt-2 pb-3 border-t border-primary-200 dark:border-primary-700 flex items-center gap-2 flex-wrap"
>
<div class="card-footer">
<ViewPublicationLink {event} />
</div>
{/if}

99
src/lib/a/cards/AProfilePreview.svelte

@ -1,4 +1,80 @@ @@ -1,4 +1,80 @@
<script lang="ts">
/**
* @fileoverview AProfilePreview Component - Alexandria
*
* A comprehensive profile card component for displaying nostr user profiles.
* Shows avatar, banner, name, bio, NIP-05 verification, lightning address, and user status indicators.
*
* @component
* @category Cards
*
* @prop {NDKEvent} event - The nostr event (kind 0 profile) to display (required)
* @prop {UserLite} [user] - User object containing npub identifier
* @prop {Profile} profile - User profile metadata (required)
* @prop {boolean} [loading=false] - Whether the profile is currently loading
* @prop {string} [error=null] - Error message if profile loading failed
* @prop {boolean} [isOwn=false] - Whether this is the current user's own profile
* @prop {Record<string, boolean>} [communityStatusMap=false] - Map of pubkey to community membership status
*
* @example
* ```svelte
* <AProfilePreview
* {event}
* user={{npub}}
* {profile}
* />
* ```
*
* @example Own profile with actions
* ```svelte
* <AProfilePreview
* {event}
* {profile}
* isOwn={true}
* />
* ```
*
* @example Loading state
* ```svelte
* <AProfilePreview
* {event}
* {profile}
* loading={true}
* />
* ```
*
* @example With error handling
* ```svelte
* <AProfilePreview
* {event}
* {profile}
* error={errorMessage}
* />
* ```
*
* @features
* - Banner image with fallback color generation
* - Avatar display with proper sizing
* - NIP-05 verification badge display
* - Community membership indicator (star icon)
* - User list membership indicator (heart icon)
* - Lightning address (lud16) with QR code modal
* - Multiple identifier formats (npub, nprofile, nevent)
* - Copy to clipboard functionality for identifiers
* - Website link display
* - Bio/about text with markup rendering
* - Own profile actions (notifications, my notes)
* - Loading and error states
*
* @accessibility
* - Semantic profile structure with proper headings
* - Keyboard accessible action buttons and dropdowns
* - Screen reader friendly verification status badges
* - Proper modal focus management for QR code
* - Alt text for images
* - ARIA labels for status indicators
*/
import {
Card,
Heading,
@ -203,12 +279,12 @@ @@ -203,12 +279,12 @@
class="main-leather p-0 overflow-hidden rounded-lg border border-primary-200 dark:border-primary-700"
>
{#if props.profile?.banner}
<div class="w-full bg-primary-200 dark:bg-primary-800 relative">
<div class="card-image-container">
<LazyImage
src={props.profile.banner}
alt="Profile banner"
eventId={props.event.id}
className="w-full h-60 object-cover"
className="card-banner"
/>
</div>
{:else}
@ -223,7 +299,7 @@ @@ -223,7 +299,7 @@
size="xl"
src={props.profile?.picture ?? null}
alt="Avatar"
class="absolute w-fit top-[-56px]"
class="card-avatar-container"
/>
<div class="flex flex-col gap-3 mt-14">
@ -235,18 +311,15 @@ @@ -235,18 +311,15 @@
{#if props.event}
<div class="flex items-center gap-2 min-w-0">
{#if props.profile?.nip05}
<span
class="px-2 py-0.5 !mb-0 rounded bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 text-xs"
>{props.profile.nip05}</span
>
<span class="profile-nip05-badge">{props.profile.nip05}</span>
{/if}
{#if communityStatus === true}
<div
class="flex-shrink-0 w-4 h-4 bg-yellow-100 dark:bg-yellow-900 rounded-full flex items-center justify-center"
class="community-status-indicator"
title="Has posted to the community"
>
<svg
class="w-3 h-3 text-yellow-600 dark:text-yellow-400"
class="community-status-icon"
fill="currentColor"
viewBox="0 0 24 24"
><path
@ -257,11 +330,11 @@ @@ -257,11 +330,11 @@
{/if}
{#if isInUserLists === true}
<div
class="flex-shrink-0 w-4 h-4 bg-red-100 dark:bg-red-900 rounded-full flex items-center justify-center"
class="user-list-indicator"
title="In your lists (follows, etc.)"
>
<svg
class="w-3 h-3 text-red-600 dark:text-red-400"
class="user-list-icon"
fill="currentColor"
viewBox="0 0 24 24"
><path
@ -275,9 +348,7 @@ @@ -275,9 +348,7 @@
</div>
{#if props.profile?.about}
<div
class="prose dark:prose-invert max-w-none text-gray-900 dark:text-gray-100 break-words overflow-wrap-anywhere min-w-0"
>
<div class="prose dark:prose-invert card-prose">
{@render basicMarkup(props.profile.about, ndk)}
</div>
{/if}

53
src/lib/a/forms/ACommentForm.svelte

@ -1,4 +1,57 @@ @@ -1,4 +1,57 @@
<script lang="ts">
/**
* @fileoverview ACommentForm Component - Alexandria
*
* A form component for creating and editing comments with markup support and preview functionality.
* Integrates with ATextareaWithPreview to provide rich text editing capabilities.
*
* @component
* @category Forms
*
* @prop {string} [content=""] - The comment content text (bindable)
* @prop {any} [extensions] - Additional extensions for markup processing
* @prop {boolean} [isSubmitting=false] - Whether form is currently submitting
* @prop {(content: string) => Promise<void>} [onSubmit] - Callback when form is submitted
*
* @example
* ```svelte
* <ACommentForm
* bind:content={commentText}
* {isSubmitting}
* onSubmit={handleCommentSubmit}
* />
* ```
*
* @example Basic comment form
* ```svelte
* <ACommentForm bind:content={comment} onSubmit={postComment} />
* ```
*
* @example Comment form with custom extensions
* ```svelte
* <ACommentForm
* bind:content={replyText}
* extensions={customMarkupExtensions}
* isSubmitting={posting}
* onSubmit={handleReply}
* />
* ```
*
* @features
* - Rich text editing with markdown-like syntax
* - Live preview of formatted content
* - Clear form functionality
* - Remove formatting option
* - Submit handling with loading states
* - Integration with user authentication
*
* @accessibility
* - Proper form labels and structure
* - Keyboard accessible controls
* - Screen reader friendly
* - Clear form validation feedback
*/
import { Button, Label } from "flowbite-svelte";
import { userStore } from "$lib/stores/userStore.ts";
import { parseBasicMarkup } from "$lib/utils/markup/basicMarkupParser.ts";

61
src/lib/a/forms/AMarkupForm.svelte

@ -1,4 +1,65 @@ @@ -1,4 +1,65 @@
<script lang="ts">
/**
* @fileoverview AMarkupForm Component - Alexandria
*
* A comprehensive form component for creating content with subject/title and rich markup body.
* Provides advanced markup editing with preview, confirmation dialogs, and form management.
*
* @component
* @category Forms
*
* @prop {string} [subject=""] - The content title/subject (bindable)
* @prop {string} [content=""] - The main content body (bindable)
* @prop {boolean} [isSubmitting=false] - Whether form is currently submitting
* @prop {number} [clearSignal=0] - Signal to clear form (increment to trigger clear)
* @prop {(subject: string, content: string) => Promise<void>} [onSubmit] - Submit callback
*
* @example
* ```svelte
* <AMarkupForm
* bind:subject={title}
* bind:content={body}
* {isSubmitting}
* onSubmit={handlePublish}
* />
* ```
*
* @example Basic markup form
* ```svelte
* <AMarkupForm
* bind:subject={articleTitle}
* bind:content={articleContent}
* onSubmit={publishArticle}
* />
* ```
*
* @example Form with clear signal control
* ```svelte
* <AMarkupForm
* bind:subject={title}
* bind:content={body}
* clearSignal={resetCounter}
* isSubmitting={publishing}
* onSubmit={handleSubmit}
* />
* ```
*
* @features
* - Subject/title input field
* - Advanced markup editor with preview
* - Clear form functionality with confirmation dialog
* - Form validation and submission states
* - Integration with advanced markup parser
* - Responsive layout with proper spacing
*
* @accessibility
* - Proper form labels and structure
* - Keyboard accessible controls
* - Screen reader friendly
* - Modal dialogs with focus management
* - Clear form validation feedback
*/
import { Label, Input, Button, Modal } from "flowbite-svelte";
import { parseAdvancedmarkup } from "$lib/utils/markup/advancedMarkupParser";
import { ATextareaWithPreview } from "$lib/a/index.ts";

66
src/lib/a/forms/ASearchForm.svelte

@ -1,4 +1,70 @@ @@ -1,4 +1,70 @@
<script lang="ts">
/**
* @fileoverview ASearchForm Component - Alexandria
*
* A search form component with loading states, keyboard handling, and flexible callback system.
* Provides a standardized search interface with clear functionality and user feedback.
*
* @component
* @category Forms
*
* @prop {string} searchQuery - The current search query text (bindable)
* @prop {boolean} searching - Whether a search is currently in progress
* @prop {boolean} loading - Whether data is being loaded
* @prop {boolean} isUserEditing - Whether user is actively editing the query (bindable)
* @prop {string} [placeholder] - Placeholder text for the search input
* @prop {(args: {clearInput: boolean, queryOverride?: string}) => void} [search] - Search callback
* @prop {() => void} [clear] - Clear callback
*
* @example
* ```svelte
* <ASearchForm
* bind:searchQuery={query}
* {searching}
* {loading}
* bind:isUserEditing={editing}
* search={handleSearch}
* clear={handleClear}
* />
* ```
*
* @example Basic search form
* ```svelte
* <ASearchForm
* bind:searchQuery={searchTerm}
* searching={isSearching}
* search={performSearch}
* clear={clearResults}
* />
* ```
*
* @example Custom placeholder and editing tracking
* ```svelte
* <ASearchForm
* bind:searchQuery={query}
* bind:isUserEditing={userTyping}
* searching={searching}
* placeholder="Search events, users, topics..."
* search={handleEventSearch}
* clear={resetSearch}
* />
* ```
*
* @features
* - Enter key triggers search
* - Loading spinner during operations
* - Clear button functionality
* - User editing state tracking
* - Flexible callback system
* - Accessible search interface
*
* @accessibility
* - Keyboard accessible (Enter to search)
* - Screen reader friendly with proper labels
* - Loading states clearly communicated
* - Focus management
*/
import { Button, Search, Spinner } from "flowbite-svelte";
// AI-NOTE: 2025-08-16 - This component centralizes search form behavior.

66
src/lib/a/forms/ATextareaWithPreview.svelte

@ -1,4 +1,70 @@ @@ -1,4 +1,70 @@
<script lang="ts">
/**
* @fileoverview ATextareaWithPreview Component - Alexandria
*
* A rich text editor with toolbar and live preview functionality for markup content.
* Provides formatting tools, preview toggle, and extensible parsing system.
*
* @component
* @category Forms
*
* @prop {string} value - The textarea content (bindable)
* @prop {string} [id="editor"] - HTML id for the textarea element
* @prop {string} [label=""] - Label text for the textarea
* @prop {number} [rows=10] - Number of textarea rows
* @prop {string} [placeholder=""] - Placeholder text
* @prop {(text: string, extensions?: any) => Promise<string>} parser - Async markup parser function
* @prop {snippet} previewSnippet - Svelte snippet for rendering preview content
* @prop {any} [previewArg] - Additional argument passed to preview snippet
* @prop {any} [extensions] - Extensions passed to the parser
*
* @example
* ```svelte
* <ATextareaWithPreview
* bind:value={content}
* {parser}
* {previewSnippet}
* previewArg={ndk}
* />
* ```
*
* @example Basic markup editor
* ```svelte
* <ATextareaWithPreview
* bind:value={content}
* parser={parseBasicMarkup}
* previewSnippet={basicMarkup}
* placeholder="Write your content..."
* />
* ```
*
* @example Advanced editor with extensions
* ```svelte
* <ATextareaWithPreview
* bind:value={articleContent}
* parser={parseAdvancedMarkup}
* previewSnippet={advancedMarkup}
* previewArg={ndkInstance}
* extensions={customExtensions}
* rows={15}
* />
* ```
*
* @features
* - Rich formatting toolbar (bold, italic, strikethrough, etc.)
* - Live preview toggle with eye icon
* - Support for links, images, quotes, lists
* - Hashtag and mention insertion
* - Extensible parser system
* - Keyboard shortcuts for formatting
*
* @accessibility
* - Proper form labels and ARIA attributes
* - Keyboard accessible toolbar buttons
* - Screen reader friendly with descriptive labels
* - Focus management between edit and preview modes
*/
import {
Textarea,
Toolbar,

19
src/lib/a/index.ts

@ -1,12 +1,29 @@ @@ -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 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 AThemeToggleMini } from "./primitives/AThemeToggleMini.svelte";
// Navigation Components
export { default as ANavbar } from "./nav/ANavbar.svelte";
export { default as AFooter } from "./nav/AFooter.svelte";
// Form Components
export { default as ACommentForm } from "./forms/ACommentForm.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";
// Card Components
export { default as AEventPreview } from "./cards/AEventPreview.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";

42
src/lib/a/nav/AFooter.svelte

@ -1,4 +1,46 @@ @@ -1,4 +1,46 @@
<script>
/**
* @fileoverview AFooter Component - Alexandria
*
* A standardized footer component with copyright information and navigation links.
* Uses Flowbite's Footer components with Alexandria-specific styling and content.
* This component has no props - it renders a fixed footer structure.
*
* @component
* @category Navigation
*
* @example
* ```svelte
* <AFooter />
* ```
*
* @example Place at bottom of layout
* ```svelte
* <main>
* <!-- page content -->
* </main>
* <AFooter />
* ```
*
* @features
* - Copyright notice with GitCitadel attribution
* - Navigation links to About and Contact pages
* - Responsive layout that adapts to screen size
* - Consistent styling with Alexandria theme
* - Links to creator's nostr profile
*
* @accessibility
* - Semantic footer structure
* - Keyboard accessible navigation links
* - Screen reader friendly with proper link text
* - Responsive design for various screen sizes
*
* @integration
* - Typically placed at the bottom of page layouts
* - Uses Flowbite Footer components for consistency
* - Matches Alexandria's overall design system
*/
import {
Footer,
FooterCopyright,

54
src/lib/a/nav/ANavbar.svelte

@ -1,4 +1,56 @@ @@ -1,4 +1,56 @@
<script lang="ts">
/**
* @fileoverview ANavbar Component - Alexandria
*
* The main navigation bar component with responsive menu, user profile, and theme controls.
* Provides primary navigation for the Alexandria application with mega menu functionality.
* This component has no props - it renders a fixed navigation structure.
*
* @component
* @category Navigation
*
* @example
* ```svelte
* <ANavbar />
* ```
*
* @example Place at top of main layout
* ```svelte
* <ANavbar />
* <main>
* <!-- page content -->
* </main>
* ```
*
* @features
* - Responsive hamburger menu for mobile devices
* - Mega menu with categorized navigation items
* - User profile integration with sign-in/out functionality
* - Dark mode toggle
* - Brand logo and home link
* - Organized menu sections (Browse, Create, Learn, etc.)
* - Helpful descriptions for each navigation item
*
* @navigation
* - Browse: Publications, Events, Visualize
* - Create: Compose notes, Publish events
* - Learn: Getting Started, Relay Status
* - Profile: User-specific actions and settings
*
* @accessibility
* - Semantic navigation structure with proper ARIA attributes
* - Keyboard accessible menu items and dropdowns
* - Screen reader friendly with descriptive labels
* - Focus management for mobile menu
* - Proper heading hierarchy
*
* @integration
* - Uses Flowbite Navbar components for consistency
* - Integrates with Alexandria's theme system
* - Connects to user authentication state
* - Responsive design adapts to all screen sizes
*/
import {
DarkMode,
Navbar,
@ -80,7 +132,7 @@ @@ -80,7 +132,7 @@
class="text-primary-800 ms-2 inline h-6 w-6 dark:text-white"
/>
</NavLi>
<MegaMenu full items={menu2}>
<MegaMenu items={menu2}>
{#snippet children({ item })}
<a
href={item.href}

368
src/lib/a/parse-components.js

@ -0,0 +1,368 @@ @@ -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();

50
src/lib/a/primitives/AAlert.svelte

@ -1,4 +1,54 @@ @@ -1,4 +1,54 @@
<script lang="ts">
/**
* @fileoverview AAlert Component - Alexandria
*
* A themed alert component based on Flowbite's Alert with consistent styling.
* Provides notifications, warnings, and informational messages with optional dismissal.
*
* @component
* @category Primitives
*
* @prop {string} [color] - Alert color theme (success, warning, error, info, etc.)
* @prop {boolean} [dismissable] - Whether alert can be dismissed by user
* @prop {snippet} children - Main alert content (required)
* @prop {snippet} [title] - Optional title section
* @prop {string} [classes] - Additional CSS classes to apply
*
* @example
* ```svelte
* <AAlert color="success" dismissable={true}>
* {#snippet title()}Success!{/snippet}
* {#snippet children()}Your changes have been saved.{/snippet}
* </AAlert>
* ```
*
* @example Simple alert
* ```svelte
* <AAlert color="info">
* {#snippet children()}This is an informational message.{/snippet}
* </AAlert>
* ```
*
* @example Alert with title and custom classes
* ```svelte
* <AAlert color="warning" classes="mt-4" dismissable={true}>
* {#snippet title()}Warning{/snippet}
* {#snippet children()}Please check your input.{/snippet}
* </AAlert>
* ```
*
* @features
* - Consistent "leather" theme styling
* - Built on Flowbite Alert component
* - Support for custom colors and dismissal
* - Flexible content with title and body sections
*
* @accessibility
* - Inherits Flowbite's accessibility features
* - Proper ARIA attributes for alerts
* - Keyboard accessible dismiss button when dismissable
*/
import { Alert } from "flowbite-svelte";
let { color, dismissable, children, title, classes } = $props<{

51
src/lib/a/primitives/ADetails.svelte

@ -1,4 +1,55 @@ @@ -1,4 +1,55 @@
<script lang="ts">
/**
* @fileoverview ADetails Component - Alexandria
*
* A collapsible details/summary element with enhanced styling and tech-aware functionality.
* Integrates with the techStore to automatically hide technical details based on user preference.
*
* @component
* @category Primitives
*
* @prop {string} [summary=""] - The summary text shown in the collapsible header
* @prop {boolean} [tech=false] - Whether this contains technical content (affects visibility)
* @prop {boolean} [defaultOpen=false] - Whether details should be open by default
* @prop {boolean} [forceHide=false] - Force hide content even when tech mode is on
* @prop {string} [class=""] - Additional CSS classes to apply
* @prop {snippet} children - The content to show/hide in the details body (required, via default slot)
*
* @example
* ```svelte
* <ADetails summary="Event Details" tech={true}>
* <p>Technical event information here...</p>
* </ADetails>
* ```
*
* @example Regular details block
* ```svelte
* <ADetails summary="More Information">
* <p>Additional content here...</p>
* </ADetails>
* ```
*
* @example Technical details with custom styling
* ```svelte
* <ADetails summary="Raw Event Data" tech={true} class="border-red-200">
* <pre>{JSON.stringify(event, null, 2)}</pre>
* </ADetails>
* ```
*
* @features
* - Respects global techStore setting for tech content
* - Animated chevron icon indicates open/closed state
* - "Technical" badge for tech-related details
* - Consistent themed styling with hover effects
* - Auto-closes tech details when techStore is disabled
*
* @accessibility
* - Uses semantic HTML details/summary elements
* - Keyboard accessible (Enter/Space to toggle)
* - Screen reader friendly with proper labeling
* - Clear visual indicators for state changes
*/
import { showTech } from "$lib/stores/techStore";
let {
summary = "",

51
src/lib/a/primitives/AInput.svelte

@ -1,5 +1,54 @@ @@ -1,5 +1,54 @@
<script lang="ts">
let { value = "", class: className = "", placeholder = "" } = $props();
/**
* @fileoverview AInput Component - Alexandria
*
* A styled input field with consistent theming and focus states.
* Provides a standardized text input with Alexandria's design system.
*
* @component
* @category Primitives
*
* @prop {string} value - The input value (bindable)
* @prop {string} [class=""] - Additional CSS classes to apply
* @prop {string} [placeholder=""] - Placeholder text for the input
*
* @example
* ```svelte
* <AInput bind:value={searchQuery} placeholder="Enter search term..." />
* ```
*
* @example Basic input
* ```svelte
* <AInput bind:value={name} placeholder="Your name" />
* ```
*
* @example Custom styled input
* ```svelte
* <AInput
* bind:value={email}
* placeholder="Email address"
* class="max-w-md border-blue-300"
* />
* ```
*
* @features
* - Consistent themed styling with focus rings
* - Full width by default with customizable classes
* - Smooth focus transitions and hover effects
* - Integrates with Alexandria's color system
*
* @accessibility
* - Proper focus management with visible focus rings
* - Keyboard accessible
* - Supports all standard input attributes
* - Screen reader compatible
*/
let { value = $bindable(""), class: className = "", placeholder = "" } = $props<{
value?: string;
class?: string;
placeholder?: string;
}>();
</script>
<input

56
src/lib/a/primitives/ANostrBadge.svelte

@ -1,7 +1,59 @@ @@ -1,7 +1,59 @@
<script lang="ts">
/**
* @fileoverview ANostrBadge Component - Alexandria
*
* Displays a nostr badge (NIP-58) with image or fallback text representation.
* Shows badge thumbnails with proper sizing and accessibility features.
*
* @component
* @category Primitives
*
* @prop {DisplayBadge} badge - Badge object containing title, thumbUrl, etc. (required)
* @prop {"xs" | "s" | "m" | "l"} [size="s"] - Badge size (xs: 16px, s: 24px, m: 32px, l: 48px)
*
* @example
* ```svelte
* <ANostrBadge {badge} size="m" />
* ```
*
* @example Badge with image
* ```svelte
* <ANostrBadge badge={{title: "Developer", thumbUrl: "/badge.png"}} size="l" />
* ```
*
* @example Badge without image (shows first letter)
* ```svelte
* <ANostrBadge badge={{title: "Contributor"}} size="s" />
* ```
*
* @example In a list of badges
* ```svelte
* {#each userBadges as badge}
* <ANostrBadge {badge} size="xs" />
* {/each}
* ```
*
* @typedef {Object} DisplayBadge
* @property {string} title - Badge title
* @property {string} [thumbUrl] - Optional thumbnail URL
*
* @features
* - Displays badge thumbnail image when available
* - Fallback to first letter of title when no image
* - Multiple size options for different contexts
* - Lazy loading for performance
* - Proper aspect ratio and object-fit
*
* @accessibility
* - Alt text for badge images
* - Title attribute for hover information
* - Proper semantic structure
* - Loading and decoding optimizations
*/
import type { DisplayBadge } from "$lib/nostr/nip58";
export let badge: DisplayBadge;
export let size: "xs" | "s" | "m" | "l" = "s";
let { badge, size = "s" }: { badge: DisplayBadge; size?: "xs" | "s" | "m" | "l" } = $props();
const px = { xs: 16, s: 24, m: 32, l: 48 }[size];
</script>

51
src/lib/a/primitives/ANostrBadgeRow.svelte

@ -1,9 +1,54 @@ @@ -1,9 +1,54 @@
<script lang="ts">
/**
* @fileoverview ANostrBadgeRow Component - Alexandria
*
* Displays a horizontal row of nostr badges with optional limit and overflow indicator.
* Uses ANostrBadge components to render individual badges in a flex layout.
*
* @component
* @category Primitives
*
* @prop {DisplayBadge[]} [badges=[]] - Array of badge objects to display
* @prop {"xs" | "s" | "m" | "l"} [size="s"] - Size for all badges in the row
* @prop {number} [limit=6] - Maximum number of badges to show before truncating
*
* @example
* ```svelte
* <ANostrBadgeRow badges={userBadges} size="m" limit={4} />
* ```
*
* @example Show all badges
* ```svelte
* <ANostrBadgeRow badges={allBadges} limit={999} />
* ```
*
* @example Limited display with small badges
* ```svelte
* <ANostrBadgeRow badges={userBadges} size="xs" limit={3} />
* ```
*
* @example Profile header with medium badges
* ```svelte
* <ANostrBadgeRow badges={profileBadges} size="m" limit={5} />
* ```
*
* @features
* - Responsive flex layout with wrapping
* - Configurable display limit with overflow counter
* - Consistent spacing between badges
* - Shows "+N" indicator when badges exceed limit
* - Uses badge.def.id as key for efficient rendering
*
* @accessibility
* - Inherits accessibility from ANostrBadge components
* - Clear visual hierarchy with proper spacing
* - Overflow indicator provides context about hidden badges
*/
import type { DisplayBadge } from "$lib/nostr/nip58";
import ANostrBadge from "./ANostrBadge.svelte";
export let badges: DisplayBadge[] = [];
export let size: "xs" | "s" | "m" | "l" = "s";
export let limit: number = 6;
let { badges = [], size = "s", limit = 6 }: { badges?: DisplayBadge[]; size?: "xs" | "s" | "m" | "l"; limit?: number } = $props();
const shown = () => badges.slice(0, limit);
</script>

77
src/lib/a/primitives/ANostrUser.svelte

@ -1,4 +1,81 @@ @@ -1,4 +1,81 @@
<script lang="ts">
/**
* @fileoverview ANostrUser Component - Alexandria
*
* Displays a nostr user with avatar, display name, npub, NIP-05 verification, and badges.
* Provides a comprehensive user representation with configurable display options.
*
* @component
* @category Primitives
*
* @prop {string} npub - The user's npub (required)
* @prop {string} [pubkey] - The user's public key (for NIP-05 verification)
* @prop {NostrProfile} [profile] - User profile metadata
* @prop {"sm" | "md" | "lg"} [size="md"] - Component size variant
* @prop {boolean} [showNpub=true] - Whether to show the shortened npub
* @prop {boolean} [showBadges=true] - Whether to display user badges
* @prop {boolean} [verifyNip05=true] - Whether to verify NIP-05 identifier
* @prop {boolean} [nip05Verified] - Override NIP-05 verification status
* @prop {DisplayBadge[] | null} [nativeBadges] - User's badges to display
* @prop {number} [badgeLimit=6] - Maximum badges to show
* @prop {string} [href] - Optional link URL (makes component clickable)
* @prop {string} [class=""] - Additional CSS classes
*
* @example
* ```svelte
* <ANostrUser
* {npub}
* {pubkey}
* {profile}
* size="lg"
* showBadges={true}
* />
* ```
*
* @example Basic user display
* ```svelte
* <ANostrUser {npub} {profile} />
* ```
*
* @example Large user card with all features
* ```svelte
* <ANostrUser
* {npub}
* {pubkey}
* {profile}
* size="lg"
* nativeBadges={userBadges}
* href="/profile/{npub}"
* />
* ```
*
* @example Compact user mention
* ```svelte
* <ANostrUser
* {npub}
* size="sm"
* showNpub={false}
* showBadges={false}
* />
* ```
*
* @features
* - Avatar display with fallback
* - Display name from profile or npub
* - NIP-05 verification with visual indicator
* - Badge integration via ANostrBadgeRow
* - Configurable sizing and display options
* - Optional linking capability
* - Loading states for verification
*
* @accessibility
* - Semantic user representation
* - Alt text for avatars
* - Screen reader friendly verification status
* - Keyboard accessible when linked
* - Proper focus management
*/
import type { NostrProfile } from "$lib/nostr/types";
import type { DisplayBadge } from "$lib/nostr/nip58";
import ANostrBadgeRow from "./ANostrBadgeRow.svelte";

66
src/lib/a/primitives/APagination.svelte

@ -1,4 +1,70 @@ @@ -1,4 +1,70 @@
<script lang="ts">
/**
* @fileoverview APagination Component - Alexandria
*
* A pagination component for navigating through multiple pages of content.
* Provides previous/next navigation with page information and item counts.
*
* @component
* @category Primitives
*
* @prop {number} currentPage - Current page number (1-based, bindable)
* @prop {number} totalPages - Total number of pages available
* @prop {boolean} hasNextPage - Whether there is a next page available
* @prop {boolean} hasPreviousPage - Whether there is a previous page available
* @prop {number} [totalItems=0] - Total number of items across all pages
* @prop {string} [itemsLabel="items"] - Label for items (e.g., "posts", "events")
* @prop {string} [className=""] - Additional CSS classes to apply
*
* @example
* ```svelte
* <APagination
* bind:currentPage={page}
* totalPages={10}
* hasNextPage={page < 10}
* hasPreviousPage={page > 1}
* totalItems={100}
* itemsLabel="events"
* />
* ```
*
* @example Basic pagination
* ```svelte
* <APagination
* bind:currentPage={currentPage}
* totalPages={Math.ceil(totalEvents / pageSize)}
* hasNextPage={currentPage < totalPages}
* hasPreviousPage={currentPage > 1}
* />
* ```
*
* @example With custom item labels and styling
* ```svelte
* <APagination
* bind:currentPage={page}
* totalPages={pageCount}
* hasNextPage={hasNext}
* hasPreviousPage={hasPrev}
* totalItems={eventCount}
* itemsLabel="nostr events"
* className="border-2 border-primary"
* />
* ```
*
* @features
* - Bindable current page for reactive updates
* - Previous/Next button navigation
* - Page information display with item counts
* - Disabled state for unavailable navigation
* - Only renders when totalPages > 1
*
* @accessibility
* - Keyboard accessible buttons
* - Disabled buttons have proper cursor and opacity
* - Clear page information for screen readers
* - Semantic button elements
*/
type Props = {
currentPage: number;
totalPages: number;

43
src/lib/a/primitives/AThemeToggleMini.svelte

@ -1,4 +1,47 @@ @@ -1,4 +1,47 @@
<script lang="ts">
/**
* @fileoverview AThemeToggleMini Component - Alexandria
*
* A compact theme selector dropdown that allows users to switch between available themes.
* Integrates with the themeStore to persist and apply theme changes across the app.
* This component has no props - it manages its own state internally.
*
* @component
* @category Primitives
*
* @example
* ```svelte
* <AThemeToggleMini />
* ```
*
* @example Place in navigation or settings area
* ```svelte
* <div class="flex items-center gap-4">
* <span>Appearance:</span>
* <AThemeToggleMini />
* </div>
* ```
*
* @features
* - Dropdown with radio buttons for theme selection
* - Automatic persistence via themeStore
* - Shows current theme in button label
* - Available themes: Light, Ocean, Forest
* - Instant theme application on selection
*
* @accessibility
* - Keyboard accessible dropdown navigation
* - Radio buttons for clear selection state
* - Screen reader friendly with proper labels
* - Focus management within dropdown
*
* @integration
* - Works with Alexandria's theme system
* - Automatically applies CSS custom properties
* - Persists selection in localStorage
* - Updates all themed components instantly
*/
import { ChevronDownOutline } from "flowbite-svelte-icons";
import { Button, Dropdown, DropdownGroup, Radio } from "flowbite-svelte";
import { onMount } from "svelte";

43
src/lib/a/reader/ATechBlock.svelte

@ -1,4 +1,47 @@ @@ -1,4 +1,47 @@
<script lang="ts">
/**
* @fileoverview ATechBlock Component - Alexandria
*
* A collapsible container for technical details that can be shown/hidden based on user preference.
* Works with the ATechToggle component and techStore to manage visibility of developer information.
*
* @component
* @category Reader
*
* @prop {string} [title="Technical details"] - The title shown when block is hidden
* @prop {string} [className=""] - Additional CSS classes to apply
* @prop {snippet} content - The technical content to show/hide (required)
*
* @example
* ```svelte
* <ATechBlock title="Raw Event Data">
* {#snippet content()}
* <pre>{JSON.stringify(event, null, 2)}</pre>
* {/snippet}
* </ATechBlock>
* ```
*
* @example Custom title and styling
* ```svelte
* <ATechBlock title="Event JSON" className="border-red-200">
* {#snippet content()}
* <code>{eventData}</code>
* {/snippet}
* </ATechBlock>
* ```
*
* @features
* - Respects global showTech setting from techStore
* - Individual reveal button when globally hidden
* - Accessible with proper ARIA attributes
* - Useful for hiding nostr event data, debug info, etc.
*
* @accessibility
* - Keyboard accessible reveal button
* - Screen reader friendly with descriptive labels
* - Proper semantic HTML structure
*/
import { showTech } from "$lib/stores/techStore.ts";
let revealed = $state(false);
let { title = "Technical details", className = "", content } = $props();

26
src/lib/a/reader/ATechToggle.svelte

@ -1,4 +1,30 @@ @@ -1,4 +1,30 @@
<script lang="ts">
/**
* @fileoverview ATechToggle Component - Alexandria
*
* A toggle switch that controls the visibility of technical details throughout the app.
* Works in conjunction with ATechBlock components to show/hide nostr-specific developer information.
*
* @component
* @category Reader
*
* @example
* ```svelte
* <ATechToggle />
* ```
*
* @features
* - Persists setting in localStorage via techStore
* - Accessible with ARIA labels and keyboard navigation
* - Automatically updates all ATechBlock components when toggled
* - Useful for nostr developers who want to see raw event data and technical details
*
* @accessibility
* - Uses proper ARIA labeling
* - Keyboard accessible (Space/Enter to toggle)
* - Screen reader friendly with descriptive label
*/
import { showTech } from "$lib/stores/techStore.ts";
import { Toggle, P } from "flowbite-svelte";
let label = "Show technical details";

40
src/lib/components/cards/ProfileHeader.svelte

@ -156,11 +156,11 @@ @@ -156,11 +156,11 @@
</div>
{#if communityStatus === true}
<div
class="flex-shrink-0 w-4 h-4 bg-yellow-100 dark:bg-yellow-900 rounded-full flex items-center justify-center"
class="community-status-indicator"
title="Has posted to the community"
>
<svg
class="w-3 h-3 text-yellow-600 dark:text-yellow-400"
class="community-status-icon"
fill="currentColor"
viewBox="0 0 24 24"
>
@ -174,11 +174,11 @@ @@ -174,11 +174,11 @@
{/if}
{#if isInUserLists === true}
<div
class="flex-shrink-0 w-4 h-4 bg-red-100 dark:bg-red-900 rounded-full flex items-center justify-center"
class="user-list-indicator"
title="In your lists (follows, etc.)"
>
<svg
class="w-3 h-3 text-red-600 dark:text-red-400"
class="user-list-icon"
fill="currentColor"
viewBox="0 0 24 24"
>
@ -194,24 +194,24 @@ @@ -194,24 +194,24 @@
</div>
<div class="min-w-0">
<div class="mt-2 flex flex-col gap-4">
<dl class="grid grid-cols-1 gap-y-2">
<dl class="card-metadata-grid">
{#if profile.name}
<div class="flex gap-2 min-w-0">
<dt class="font-semibold min-w-[120px] flex-shrink-0">Name:</dt>
<dd class="min-w-0 break-words">{profile.name}</dd>
<dt class="card-metadata-label">Name:</dt>
<dd class="card-metadata-value">{profile.name}</dd>
</div>
{/if}
{#if profile.displayName}
<div class="flex gap-2 min-w-0">
<dt class="font-semibold min-w-[120px] flex-shrink-0">Display Name:</dt>
<dd class="min-w-0 break-words">{profile.displayName}</dd>
<dt class="card-metadata-label">Display Name:</dt>
<dd class="card-metadata-value">{profile.displayName}</dd>
</div>
{/if}
{#if profile.about}
<div class="flex gap-2 min-w-0">
<dt class="font-semibold min-w-[120px] flex-shrink-0">About:</dt>
<dd class="min-w-0 break-words">
<div class="prose dark:prose-invert max-w-none text-gray-900 dark:text-gray-100 break-words overflow-wrap-anywhere min-w-0">
<dt class="card-metadata-label">About:</dt>
<dd class="card-metadata-value">
<div class="prose dark:prose-invert card-prose">
{@render basicMarkup(profile.about, ndk)}
</div>
</dd>
@ -219,8 +219,8 @@ @@ -219,8 +219,8 @@
{/if}
{#if profile.website}
<div class="flex gap-2 min-w-0">
<dt class="font-semibold min-w-[120px] flex-shrink-0">Website:</dt>
<dd class="min-w-0 break-all">
<dt class="card-metadata-label">Website:</dt>
<dd class="card-metadata-value">
<a
href={profile.website}
class="underline text-primary-700 dark:text-primary-200"
@ -231,8 +231,8 @@ @@ -231,8 +231,8 @@
{/if}
{#if profile.lud16}
<div class="flex items-center gap-2 mt-4 min-w-0">
<dt class="font-semibold min-w-[120px] flex-shrink-0">Lightning:</dt>
<dd class="min-w-0 break-all">
<dt class="card-metadata-label">Lightning:</dt>
<dd class="card-metadata-value">
<Button
class="btn-leather"
color="primary"
@ -244,14 +244,14 @@ @@ -244,14 +244,14 @@
{/if}
{#if profile.nip05}
<div class="flex gap-2 min-w-0">
<dt class="font-semibold min-w-[120px] flex-shrink-0">NIP-05:</dt>
<dd class="min-w-0 break-all">{profile.nip05}</dd>
<dt class="card-metadata-label">NIP-05:</dt>
<dd class="card-metadata-value">{profile.nip05}</dd>
</div>
{/if}
{#each identifiers as id}
<div class="flex gap-2 min-w-0">
<dt class="font-semibold min-w-[120px] flex-shrink-0">{id.label}:</dt>
<dd class="min-w-0 break-all">
<dt class="card-metadata-label">{id.label}:</dt>
<dd class="card-metadata-value">
{#if id.link}
<button
class="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 underline hover:no-underline transition-colors"

266
src/styles/a/cards.css

@ -0,0 +1,266 @@ @@ -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;
}
}

5
src/styles/a/forms.css

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
@layer components {
/* ========================================
Base Form Styles
======================================== */
}
Loading…
Cancel
Save