@epicenter/ui
v0.1.0
Published
Svelte 5 UI components for Epicenter apps. Built with shadcn-svelte and Tailwind CSS.
Downloads
77
Maintainers
Readme
UI Package - shadcn-svelte Management Guide
This guide explains how we manage shadcn-svelte components in our monorepo setup, including our custom configuration and best practices.
Component Library Overview
This UI package contains a combination of:
- shadcn-svelte components (core design system components)
- shadcn-svelte-extras components (additional utility components)
Dialog vs Modal Usage Guidelines
We use different component types based on the interaction pattern:
Use Dialog + AlertDialog for:
- Confirmations and simple yes/no prompts
- Display-only content (viewing information)
- Simple action confirmations (delete, cancel, etc.)
- Non-interactive content presentation
Use Modal for:
- Forms with user input (text fields, dropdowns, etc.)
- Complex interactions requiring typing
- Multi-step workflows with form data
- Any component where users need to input data
Decision Rule: If the user needs to type or input data → use Modal. Otherwise, use Dialog/AlertDialog.
Examples:
ConfirmationDialog✅ (just yes/no buttons)CreateWorkspaceModal✅ (multiple form inputs)EditRecordingModal✅ (text inputs and editing)DeleteWorkspaceButton✅ (uses AlertDialog for confirmation)
Key Differences from Standard shadcn-svelte
1. Path Aliases Configuration
Our components.json uses a flattened structure with the # symbol mapping directly to the src folder:
{
"aliases": {
"components": "#",
"utils": "#/utils",
"ui": "#",
"hooks": "#/hooks",
"lib": "#/lib"
}
}What this means:
#maps to./src(configured via Node.js subpath imports inpackage.json)#/buttonresolves to./src/button- All components and utils live directly in the
srcfolder (no nestedcomponentsdirectory)
Why We Use # Instead of @
We switched from @ to # for path aliases because of how Node.js handles module resolution in monorepos. The # symbol is the standard prefix for Node.js subpath imports, which provides better compatibility with build tools and TypeScript in monorepo environments.
This change was necessary to avoid conflicts with package name conventions (which often use @ for scoped packages) and to ensure proper module resolution across different tools in our build pipeline. For more context on this issue, see the Turborepo discussion on module resolution.
2. Package Exports Structure
Our package.json uses a pattern-based export system with subpath imports:
{
"exports": {
"./*": "./src/*/index.ts",
"./utils": "./src/utils.ts",
"./app.css": "./src/app.css"
},
"imports": {
"#*": ["./src/*", "./src/*.ts", "./src/*/index.ts"]
}
}The imports field enables the # prefix for internal module resolution, allowing multiple resolution patterns for flexibility.
This allows consumers to import components like:
import { Button } from '@epicenter/ui/button';
import { cn } from '@epicenter/ui/utils';3. Styling Override Pattern
When extending shadcn-svelte components with custom styles, we use a specific pattern that separates base styles from our overrides:
<SelectPrimitive.Content
class={cn(
'base-shadcn-styles-here',
// Custom override: prevents dropdown from expanding
'max-w-min',
className,
)}
/>Why use separate arguments in cn()?
- Clear separation: First argument contains shadcn's base styles, second argument contains our overrides
- Better diffs: When updating shadcn components, changes to base styles appear in the first argument, making it obvious what shadcn changed vs. what we customized
- Comments: We can add comments above our overrides explaining why they're needed
- Easier updates: During
shadcn-svelteupdates, if our override (second argument) disappears in the diff, we know we need to re-apply it
This pattern makes component updates much clearer: shadcn's style updates show in the first cn() argument, while our customizations remain visually separate in subsequent arguments.
Component Management Workflow
Adding New Components
# Add a new component
bunx shadcn-svelte@latest add dialog
# The component will be added to packages/ui/src/dialog/Updating Components
Run the add command with the
--overwriteflag:bunx shadcn-svelte@latest add dialog --overwriteReview the diff carefully, especially:
- Custom style overrides (marked with comments)
- Import path changes
- Any custom props or functionality
Import Path Convention
Always use the #/ alias for internal imports within the UI package:
// ❌ Don't use relative imports
import { Button } from '../button';
// ✅ Do use # alias
import { Button } from '#/button';Directory Structure
packages/ui/
├── src/
│ ├── accordion/
│ │ ├── accordion.svelte
│ │ ├── accordion-content.svelte
│ │ ├── accordion-item.svelte
│ │ ├── accordion-trigger.svelte
│ │ └── index.ts
│ ├── button/
│ │ ├── button.svelte
│ │ └── index.ts
│ ├── utils.ts
│ └── app.css
├── components.json
├── package.json
└── tsconfig.jsonBest Practices
- Keep Components Pure: Don't add business logic to UI components
- Use Barrel Exports: Each component folder should have an
index.ts - Document Overrides: Always comment custom style additions
- Test After Updates: Verify components work after shadcn updates
- Consistent Imports: Use
#/alias throughout the package
TypeScript Path Resolution
The # symbol is configured in tsconfig.json:
{
"compilerOptions": {
"paths": {
"#": ["./src"],
"#/*": ["./src/*"]
}
}
}This configuration:
- Maps
#to thesrcdirectory - Enables autocomplete in your IDE
- Ensures consistent import paths
- Works with both TypeScript and bundlers
- Aligns with the Node.js subpath imports defined in
package.json
Troubleshooting
Import Resolution Issues
If imports aren't resolving:
- Check
tsconfig.jsonpaths configuration - Ensure your IDE recognizes the TypeScript config
- Restart the TypeScript language server
Style Conflicts
If custom styles aren't applying:
- Check the order in the
cn()function - Ensure custom styles come after base styles
- Use more specific selectors if needed
Component Updates
When updating breaks functionality:
- Check the shadcn-svelte changelog
- Review our custom overrides
- Test thoroughly before committing
