@mrintel/villain-ui 0.8.2 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -3
- package/dist/components/buttons/Button.svelte +1 -1
- package/dist/components/buttons/FloatingActionButton.svelte +1 -1
- package/dist/components/buttons/IconButton.svelte +2 -2
- package/dist/components/buttons/LinkButton.svelte +1 -1
- package/dist/components/buttons/buttonClasses.js +7 -7
- package/dist/components/cards/Card.svelte +43 -11
- package/dist/components/cards/Card.svelte.d.ts +11 -0
- package/dist/components/cards/Panel.svelte +44 -5
- package/dist/components/cards/Panel.svelte.d.ts +7 -0
- package/dist/components/cards/SectionHeader.svelte +2 -2
- package/dist/components/data/Avatar.svelte +1 -1
- package/dist/components/data/Badge.svelte +3 -3
- package/dist/components/data/CalendarGrid.svelte +6 -4
- package/dist/components/data/CalendarGrid.svelte.d.ts +3 -0
- package/dist/components/data/CodeBlock.svelte +1 -1
- package/dist/components/data/List.svelte +1 -1
- package/dist/components/data/Pagination.svelte +3 -3
- package/dist/components/data/Stat.svelte +3 -5
- package/dist/components/data/Table.svelte +12 -14
- package/dist/components/data/Tag.svelte +1 -1
- package/dist/components/data/WeekHeatmap.svelte +2 -2
- package/dist/components/forms/Checkbox.svelte +1 -1
- package/dist/components/forms/DatePicker.svelte +3 -1
- package/dist/components/forms/DateTimePicker.svelte +3 -1
- package/dist/components/forms/FileUpload.svelte +1 -1
- package/dist/components/forms/FormSection.svelte +21 -0
- package/dist/components/forms/FormSection.svelte.d.ts +14 -0
- package/dist/components/forms/InputGroup.svelte +1 -1
- package/dist/components/forms/RadioGroup.svelte +0 -1
- package/dist/components/forms/RangeSlider.svelte +2 -6
- package/dist/components/forms/SelectMenu.svelte +4 -3
- package/dist/components/forms/Switch.svelte +1 -1
- package/dist/components/forms/TimePicker.svelte +1 -1
- package/dist/components/forms/TimePickerGrid.svelte +0 -3
- package/dist/components/forms/formClasses.d.ts +2 -2
- package/dist/components/forms/formClasses.js +2 -2
- package/dist/components/forms/index.d.ts +1 -0
- package/dist/components/forms/index.js +1 -0
- package/dist/components/navigation/Breadcrumbs.svelte +3 -3
- package/dist/components/navigation/ContextMenu.svelte +2 -2
- package/dist/components/navigation/DropdownMenu.svelte +2 -2
- package/dist/components/navigation/Menu.svelte +2 -2
- package/dist/components/navigation/Navbar.svelte +10 -2
- package/dist/components/navigation/Sidebar.svelte +6 -9
- package/dist/components/navigation/Tabs.svelte +2 -2
- package/dist/components/overlays/Alert.svelte +1 -1
- package/dist/components/overlays/CommandPalette.svelte +2 -2
- package/dist/components/overlays/Drawer.svelte +5 -5
- package/dist/components/overlays/Dropdown.svelte +1 -1
- package/dist/components/overlays/Modal.svelte +2 -2
- package/dist/components/overlays/Popover.svelte +1 -1
- package/dist/components/overlays/ProgressBar.svelte +3 -3
- package/dist/components/overlays/SkeletonLoader.svelte +2 -2
- package/dist/components/overlays/Spinner.svelte +1 -1
- package/dist/components/overlays/Toast.svelte +2 -2
- package/dist/components/overlays/Tooltip.svelte +1 -1
- package/dist/components/typography/Code.svelte +1 -1
- package/dist/components/utilities/Carousel.svelte +4 -4
- package/dist/components/utilities/Collapse.svelte +1 -1
- package/dist/components/utilities/Hero.svelte +3 -2
- package/dist/components/utilities/SystemConsole.svelte +1 -1
- package/dist/components/utilities/SystemInterface.svelte +1 -2
- package/dist/components/wizard/StepForm.svelte +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/theme.css +207 -190
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -3,16 +3,17 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@mrintel/villain-ui)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
An opinionated Svelte 5 design system built around **Modern Villain** design: square geometry, crisp 1px borders, heavy editorial typography, and disciplined use of accent color. Onyx Black surfaces with a Royal Purple accent. Engineered, not decorative.
|
|
7
7
|
|
|
8
8
|
## ✨ Features
|
|
9
9
|
|
|
10
10
|
- **🚀 Svelte 5 with Runes** - Built on the latest Svelte 5 reactivity system with full TypeScript support
|
|
11
|
-
-
|
|
11
|
+
- **⬛ Modern Villain** - Square geometry (0-8px radii), border-driven hierarchy, minimal shadow, glow reserved for primary CTAs and active states
|
|
12
|
+
- **🔤 Editorial Typography** - Archivo display (weight 900 headlines) with IBM Plex Mono for all system text: buttons, navigation, labels, metadata
|
|
12
13
|
- **🎭 Tailwind CSS v4** - Powered by the latest Tailwind with CSS variable theming
|
|
13
14
|
- **🌳 Tree-Shakeable** - Import only what you need for optimal bundle size
|
|
14
15
|
- **🎯 Fully Typed** - Strict TypeScript mode with complete type definitions
|
|
15
|
-
- **🎬
|
|
16
|
+
- **🎬 Mechanical Motion** - Fade, precision slide, and 0.98→1 scale; no bounce, no float
|
|
16
17
|
- **🔧 Highly Customizable** - Theme via CSS variables without touching component code
|
|
17
18
|
- **♿ Accessible** - ARIA-compliant components following WAI-ARIA best practices
|
|
18
19
|
|
|
@@ -56,6 +57,54 @@ This explicit import strategy gives you full control over styling and allows you
|
|
|
56
57
|
- Conditionally load themes
|
|
57
58
|
- Override theme variables before or after the default theme loads
|
|
58
59
|
|
|
60
|
+
### Load Fonts
|
|
61
|
+
|
|
62
|
+
The library references fonts by name but does not bundle them - your app loads them. Add to your `app.html` / `index.html`:
|
|
63
|
+
|
|
64
|
+
```html
|
|
65
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
66
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
67
|
+
<link href="https://fonts.googleapis.com/css2?family=Archivo:wght@400..900&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Required fonts:
|
|
71
|
+
|
|
72
|
+
| Font | Role |
|
|
73
|
+
|------|------|
|
|
74
|
+
| **Archivo** (400-900) | Display headlines and body copy |
|
|
75
|
+
| **IBM Plex Mono** (400-600) | System text: buttons, navigation, labels, metadata, tables |
|
|
76
|
+
|
|
77
|
+
If the fonts are not loaded, the library falls back to the browser's default `sans-serif` / `monospace`.
|
|
78
|
+
|
|
79
|
+
### Customizing the Accent Color
|
|
80
|
+
|
|
81
|
+
Royal Purple is the default accent. Every accent-derived color in the library (overlays, borders, focus rings, scrollbars, glows) is computed from three CSS variables via `color-mix`, so rebranding is a single override after the theme import:
|
|
82
|
+
|
|
83
|
+
```css
|
|
84
|
+
/* your app css, after importing theme.css */
|
|
85
|
+
:root {
|
|
86
|
+
--color-accent: #c8102e; /* primary accent */
|
|
87
|
+
--color-accent-soft: #e4304c; /* hover / lighter variant */
|
|
88
|
+
--color-accent-dark: #8f0b21; /* pressed / darker variant */
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
No component changes needed - the entire system re-tints.
|
|
93
|
+
|
|
94
|
+
### Display Themes
|
|
95
|
+
|
|
96
|
+
Archivo is the default display font. Two alternate display modes are available by setting `data-display` on any ancestor element (typically `<html>` or `<body>`):
|
|
97
|
+
|
|
98
|
+
```html
|
|
99
|
+
<!-- Luxury editorial (italic accent words welcome) -->
|
|
100
|
+
<html data-display="bodoni">
|
|
101
|
+
|
|
102
|
+
<!-- Condensed performance aesthetic -->
|
|
103
|
+
<html data-display="oswald">
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Load the matching font when using a display mode: `Bodoni Moda` (400-900, incl. italic) or `Oswald` (400-700).
|
|
107
|
+
|
|
59
108
|
## 🚀 Quick Start
|
|
60
109
|
|
|
61
110
|
### SvelteKit Setup
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">import { variantClasses, sizeClasses, disabledClasses } from './buttonClasses';
|
|
2
2
|
let { variant = 'primary', size = 'md', disabled = false, type = 'button', onclick, iconBefore, iconAfter, class: className = '', children, ...restProps } = $props();
|
|
3
|
-
const baseClasses = 'inline-flex items-center justify-center rounded-[var(--radius-
|
|
3
|
+
const baseClasses = 'inline-flex items-center justify-center rounded-[var(--radius-sm)] font-[var(--font-body)] transition-all duration-300 ease-[var(--ease-luxe)] cursor-pointer gap-2';
|
|
4
4
|
const classes = $derived(`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${disabled ? disabledClasses : ''} ${className}`);
|
|
5
5
|
</script>
|
|
6
6
|
|
|
@@ -6,7 +6,7 @@ const positionClasses = {
|
|
|
6
6
|
'top-right': 'top-6 right-6',
|
|
7
7
|
'top-left': 'top-6 left-6'
|
|
8
8
|
};
|
|
9
|
-
const baseClasses = 'fixed z-[var(--z-50)] flex items-center justify-center rounded-[var(--radius-
|
|
9
|
+
const baseClasses = 'fixed z-[var(--z-50)] flex items-center justify-center rounded-[var(--radius-sm)] bg-[var(--color-accent)] text-[var(--color-text)] accent-glow hover-lift shadow-[var(--shadow-deep)] transition-all duration-300 ease-[var(--ease-luxe)] cursor-pointer';
|
|
10
10
|
</script>
|
|
11
11
|
|
|
12
12
|
<button
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">import { variantClasses, disabledClasses } from './buttonClasses';
|
|
2
|
-
let { variant = 'ghost', size = 'md', shape = '
|
|
2
|
+
let { variant = 'ghost', size = 'md', shape = 'square', disabled = false, type = 'button', ariaLabel, onclick, children, class: className = '' } = $props();
|
|
3
3
|
const iconSizeClasses = {
|
|
4
4
|
sm: 'w-10 h-10 text-sm',
|
|
5
5
|
md: 'w-12 h-12 text-base',
|
|
@@ -7,7 +7,7 @@ const iconSizeClasses = {
|
|
|
7
7
|
};
|
|
8
8
|
const shapeClasses = {
|
|
9
9
|
circle: 'rounded-[var(--radius-pill)]',
|
|
10
|
-
square: 'rounded-[var(--radius-
|
|
10
|
+
square: 'rounded-[var(--radius-sm)]'
|
|
11
11
|
};
|
|
12
12
|
const baseClasses = 'inline-flex items-center justify-center font-[var(--font-body)] transition-all duration-300 ease-[var(--ease-luxe)] cursor-pointer hover-lift';
|
|
13
13
|
</script>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">import { variantClasses, sizeClasses, disabledClasses } from './buttonClasses';
|
|
2
2
|
let { href, variant = 'primary', size = 'md', disabled = false, target, rel, children, iconBefore, iconAfter, class: className = '' } = $props();
|
|
3
|
-
const baseClasses = 'inline-flex items-center justify-center rounded-[var(--radius-
|
|
3
|
+
const baseClasses = 'inline-flex items-center justify-center rounded-[var(--radius-sm)] font-[var(--font-body)] transition-all duration-300 ease-[var(--ease-luxe)] no-underline gap-2';
|
|
4
4
|
const computedRel = $derived(target === '_blank' ? (rel || 'noopener noreferrer') : rel);
|
|
5
5
|
</script>
|
|
6
6
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
export const variantClasses = {
|
|
2
|
-
primary: 'bg-[var(--color-accent)] text-[var(--color-text)] accent-
|
|
3
|
-
secondary: 'bg-transparent text-[var(--color-text)] border border-[var(--color-border-strong)] hover:border-[var(--color-accent)] hover-lift',
|
|
4
|
-
ghost: 'bg-transparent text-[var(--color-text)] hover:bg-[var(--color-base-2)] hover-lift',
|
|
5
|
-
danger: 'bg-[var(--color-error)] text-[var(--color-text)]
|
|
2
|
+
primary: 'bg-[var(--color-accent)] text-[var(--color-text)] border border-transparent machined hover:bg-[var(--color-accent-soft)] hover-lift active:translate-y-px',
|
|
3
|
+
secondary: 'bg-transparent text-[var(--color-text)] border border-[var(--color-border-strong)] hover:border-[var(--color-accent)] hover-lift active:translate-y-px',
|
|
4
|
+
ghost: 'bg-transparent text-[var(--color-text)] border border-transparent hover:bg-[var(--color-base-2)] hover-lift active:translate-y-px',
|
|
5
|
+
danger: 'bg-[var(--color-error)] text-[var(--color-text)] border border-transparent machined hover-lift active:translate-y-px'
|
|
6
6
|
};
|
|
7
7
|
export const sizeClasses = {
|
|
8
|
-
sm: 'px-3 py-
|
|
9
|
-
md: 'px-
|
|
10
|
-
lg: 'px-
|
|
8
|
+
sm: 'px-3.5 py-2 text-xs font-mono font-medium uppercase tracking-[0.08em]',
|
|
9
|
+
md: 'px-5 py-2.5 text-sm font-mono font-medium uppercase tracking-[0.08em]',
|
|
10
|
+
lg: 'px-6 py-3 text-base font-mono font-semibold uppercase tracking-[0.08em]'
|
|
11
11
|
};
|
|
12
12
|
export const disabledClasses = 'opacity-50 cursor-not-allowed pointer-events-none';
|
|
13
13
|
export const fabSizeClasses = {
|
|
@@ -1,29 +1,43 @@
|
|
|
1
|
-
<script lang="ts">let { padding = 'md', href, target = '_self', rel, onclick, selected = false, disabled = false, class: className = '', children, header, footer, iconBefore, ...restProps } = $props();
|
|
1
|
+
<script lang="ts">let { padding = 'md', variant = 'default', eyebrow, actions, href, target = '_self', rel, onclick, selected = false, disabled = false, class: className = '', children, header, footer, iconBefore, ...restProps } = $props();
|
|
2
2
|
// Compute rel attribute: use explicit rel if provided, otherwise default to 'noopener noreferrer' for target='_blank'
|
|
3
3
|
const relValue = $derived(rel ?? (target === '_blank' ? 'noopener noreferrer' : undefined));
|
|
4
4
|
const paddingClasses = {
|
|
5
5
|
none: 'p-0',
|
|
6
|
-
sm: 'p-
|
|
7
|
-
md: 'p-
|
|
8
|
-
lg: 'p-
|
|
6
|
+
sm: 'p-5',
|
|
7
|
+
md: 'p-8',
|
|
8
|
+
lg: 'p-10'
|
|
9
9
|
};
|
|
10
10
|
const isInteractive = $derived(!!href || !!onclick);
|
|
11
|
-
const
|
|
11
|
+
const hasCommandBar = $derived(variant === 'command' && (!!eyebrow || !!actions));
|
|
12
|
+
const surfaceClasses = $derived(variant === 'metric' ? 'panel-spectral' : 'panel-raised');
|
|
13
|
+
// Command bar is full-bleed: root loses padding, content area gets it back
|
|
14
|
+
const rootPadding = $derived(hasCommandBar ? 'p-0' : paddingClasses[padding]);
|
|
15
|
+
const baseClasses = $derived(`${surfaceClasses} rounded-[var(--radius-sm)] transition-all duration-300 ease-[var(--ease-luxe)]` +
|
|
16
|
+
`${hasCommandBar ? ' overflow-hidden' : ''}` +
|
|
12
17
|
`${href ? ' no-underline' : ''}` +
|
|
13
18
|
`${isInteractive ? ' cursor-pointer hover-lift' : ''}` +
|
|
14
19
|
`${selected ? ' ring-2 ring-accent ring-offset-2 ring-offset-base-0' : ''}` +
|
|
15
20
|
`${disabled ? ' opacity-50 pointer-events-none' : ''}`);
|
|
21
|
+
const eyebrowClasses = 'font-[var(--font-mono)] text-xs uppercase tracking-[0.08em] text-[var(--color-text-soft)]';
|
|
16
22
|
export {};
|
|
17
23
|
</script>
|
|
18
24
|
|
|
19
|
-
{#snippet
|
|
25
|
+
{#snippet cardBody()}
|
|
20
26
|
{#if iconBefore}
|
|
21
27
|
<div class="card-icon">
|
|
22
28
|
{@render iconBefore()}
|
|
23
29
|
</div>
|
|
24
30
|
{/if}
|
|
31
|
+
{#if !hasCommandBar && eyebrow}
|
|
32
|
+
<div class="{eyebrowClasses} mb-3 flex items-center justify-between gap-4">
|
|
33
|
+
{eyebrow}
|
|
34
|
+
{#if actions}
|
|
35
|
+
<span class="flex items-center gap-2 normal-case tracking-normal">{@render actions()}</span>
|
|
36
|
+
{/if}
|
|
37
|
+
</div>
|
|
38
|
+
{/if}
|
|
25
39
|
{#if header}
|
|
26
|
-
<div class="
|
|
40
|
+
<div class="mb-5">
|
|
27
41
|
{@render header()}
|
|
28
42
|
</div>
|
|
29
43
|
{/if}
|
|
@@ -33,22 +47,40 @@ export {};
|
|
|
33
47
|
</div>
|
|
34
48
|
|
|
35
49
|
{#if footer}
|
|
36
|
-
<div class="
|
|
50
|
+
<div class="mt-5">
|
|
37
51
|
{@render footer()}
|
|
38
52
|
</div>
|
|
39
53
|
{/if}
|
|
40
54
|
{/snippet}
|
|
41
55
|
|
|
56
|
+
{#snippet cardContent()}
|
|
57
|
+
{#if hasCommandBar}
|
|
58
|
+
<div class="flex items-center justify-between gap-4 px-5 py-3 border-b border-[var(--color-border)] bg-[var(--color-neutral-overlay-2)]">
|
|
59
|
+
<span class={eyebrowClasses}>{eyebrow}</span>
|
|
60
|
+
{#if actions}
|
|
61
|
+
<div class="flex items-center gap-2">
|
|
62
|
+
{@render actions()}
|
|
63
|
+
</div>
|
|
64
|
+
{/if}
|
|
65
|
+
</div>
|
|
66
|
+
<div class={paddingClasses[padding]}>
|
|
67
|
+
{@render cardBody()}
|
|
68
|
+
</div>
|
|
69
|
+
{:else}
|
|
70
|
+
{@render cardBody()}
|
|
71
|
+
{/if}
|
|
72
|
+
{/snippet}
|
|
73
|
+
|
|
42
74
|
{#if href}
|
|
43
|
-
<a {href} {target} rel={relValue} class="{baseClasses} {
|
|
75
|
+
<a {href} {target} rel={relValue} class="{baseClasses} {rootPadding} {className}" aria-disabled={disabled || undefined} {...restProps}>
|
|
44
76
|
{@render cardContent()}
|
|
45
77
|
</a>
|
|
46
78
|
{:else if onclick}
|
|
47
|
-
<button type="button" {onclick} {disabled} aria-pressed={selected} class="{baseClasses} {
|
|
79
|
+
<button type="button" {onclick} {disabled} aria-pressed={selected} class="{baseClasses} {rootPadding} {className} text-left w-full" {...restProps}>
|
|
48
80
|
{@render cardContent()}
|
|
49
81
|
</button>
|
|
50
82
|
{:else}
|
|
51
|
-
<div class="{baseClasses} {
|
|
83
|
+
<div class="{baseClasses} {rootPadding} {className}" {...restProps}>
|
|
52
84
|
{@render cardContent()}
|
|
53
85
|
</div>
|
|
54
86
|
{/if}
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
export interface Props {
|
|
3
3
|
padding?: 'none' | 'sm' | 'md' | 'lg';
|
|
4
|
+
/**
|
|
5
|
+
* Pattern variant:
|
|
6
|
+
* - default: raised content card
|
|
7
|
+
* - command: card with an integrated command bar (eyebrow + actions strip)
|
|
8
|
+
* - metric: recessed instrument surface for numbers/readouts
|
|
9
|
+
*/
|
|
10
|
+
variant?: 'default' | 'command' | 'metric';
|
|
11
|
+
/** Mono uppercase system label. In command variant it lives in the command bar. */
|
|
12
|
+
eyebrow?: string;
|
|
13
|
+
/** Actions rendered in the command bar (command variant) or top-right of content. */
|
|
14
|
+
actions?: Snippet;
|
|
4
15
|
href?: string;
|
|
5
16
|
target?: '_blank' | '_self' | '_parent' | '_top';
|
|
6
17
|
rel?: string;
|
|
@@ -1,18 +1,57 @@
|
|
|
1
|
-
<script lang="ts">let { padding = 'md', rounded = true, variant = 'default', class: className = '', children, ...restProps } = $props();
|
|
1
|
+
<script lang="ts">let { padding = 'md', rounded = true, variant = 'default', title, status, statusVariant = 'neutral', actions, class: className = '', children, ...restProps } = $props();
|
|
2
2
|
const paddingClasses = {
|
|
3
3
|
none: 'p-0',
|
|
4
4
|
sm: 'p-5',
|
|
5
5
|
md: 'p-8',
|
|
6
6
|
lg: 'p-10'
|
|
7
7
|
};
|
|
8
|
+
const statusColors = {
|
|
9
|
+
success: 'var(--color-success)',
|
|
10
|
+
warning: 'var(--color-warning)',
|
|
11
|
+
error: 'var(--color-error)',
|
|
12
|
+
info: 'var(--color-info)',
|
|
13
|
+
neutral: 'var(--color-text-muted)'
|
|
14
|
+
};
|
|
8
15
|
const glassClasses = $derived(variant === 'glass'
|
|
9
16
|
? 'glass-panel bg-[var(--color-accent-overlay-5)] border-[var(--color-border-glow)]'
|
|
10
17
|
: 'panel-raised');
|
|
11
|
-
const
|
|
18
|
+
const hasCommandBar = $derived(!!title || !!status || !!actions);
|
|
19
|
+
const roundedClasses = $derived(rounded ? 'rounded-[var(--radius-sm)]' : '');
|
|
12
20
|
const baseClasses = 'transition-all duration-300 ease-[var(--ease-luxe)]';
|
|
13
21
|
export {};
|
|
14
22
|
</script>
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
{
|
|
18
|
-
|
|
24
|
+
{#if hasCommandBar}
|
|
25
|
+
<div class="{baseClasses} {glassClasses} {roundedClasses} overflow-hidden {className}" {...restProps}>
|
|
26
|
+
<div class="flex items-center justify-between gap-4 px-5 py-3 border-b border-[var(--color-border)] bg-[var(--color-neutral-overlay-2)]">
|
|
27
|
+
{#if title}
|
|
28
|
+
<span class="font-[var(--font-mono)] text-xs uppercase tracking-[0.08em] text-[var(--color-text)]">{title}</span>
|
|
29
|
+
{:else}
|
|
30
|
+
<span></span>
|
|
31
|
+
{/if}
|
|
32
|
+
<div class="flex items-center gap-4">
|
|
33
|
+
{#if status}
|
|
34
|
+
<span
|
|
35
|
+
class="inline-flex items-center gap-2 font-[var(--font-mono)] text-xs uppercase tracking-[0.08em]"
|
|
36
|
+
style="color: {statusColors[statusVariant]};"
|
|
37
|
+
>
|
|
38
|
+
<span class="w-2 h-2 inline-block" style="background: {statusColors[statusVariant]};"></span>
|
|
39
|
+
{status}
|
|
40
|
+
</span>
|
|
41
|
+
{/if}
|
|
42
|
+
{#if actions}
|
|
43
|
+
<div class="flex items-center gap-2">
|
|
44
|
+
{@render actions()}
|
|
45
|
+
</div>
|
|
46
|
+
{/if}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div class={paddingClasses[padding]}>
|
|
50
|
+
{@render children?.()}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
{:else}
|
|
54
|
+
<div class="{baseClasses} {glassClasses} {roundedClasses} {paddingClasses[padding]} {className}" {...restProps}>
|
|
55
|
+
{@render children?.()}
|
|
56
|
+
</div>
|
|
57
|
+
{/if}
|
|
@@ -3,6 +3,13 @@ interface Props {
|
|
|
3
3
|
padding?: 'none' | 'sm' | 'md' | 'lg';
|
|
4
4
|
rounded?: boolean;
|
|
5
5
|
variant?: 'default' | 'glass';
|
|
6
|
+
/** Mono uppercase title rendered in an integrated command bar. */
|
|
7
|
+
title?: string;
|
|
8
|
+
/** System status shown in the command bar (with a colored indicator). */
|
|
9
|
+
status?: string;
|
|
10
|
+
statusVariant?: 'success' | 'warning' | 'error' | 'info' | 'neutral';
|
|
11
|
+
/** Actions rendered on the right side of the command bar. */
|
|
12
|
+
actions?: Snippet;
|
|
6
13
|
class?: string;
|
|
7
14
|
children?: Snippet;
|
|
8
15
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script lang="ts">let { variant = 'default', title, level = 2, glow =
|
|
1
|
+
<script lang="ts">let { variant = 'default', title, level = 2, glow = false, subtitle, align = 'left', children } = $props();
|
|
2
2
|
const variantClasses = {
|
|
3
3
|
default: '',
|
|
4
4
|
accent: 'text-accent',
|
|
@@ -12,7 +12,7 @@ const subtitleClasses = $derived(`mt-2 font-[var(--font-body)] text-[length:var(
|
|
|
12
12
|
export {};
|
|
13
13
|
</script>
|
|
14
14
|
|
|
15
|
-
<div class="mb-
|
|
15
|
+
<div class="mb-12">
|
|
16
16
|
<svelte:element this={headingTag} class={headingClasses}>
|
|
17
17
|
{#if title}
|
|
18
18
|
{title}
|
|
@@ -14,7 +14,7 @@ const fontSizes = {
|
|
|
14
14
|
lg: '1.25rem',
|
|
15
15
|
xl: '1.5rem'
|
|
16
16
|
};
|
|
17
|
-
const classes = $derived(`${sizeClasses[size]} rounded-
|
|
17
|
+
const classes = $derived(`${sizeClasses[size]} rounded-[var(--radius-sm)] overflow-hidden inline-flex items-center justify-center transition-all duration-300 ${glow ? 'border-2 border-[var(--color-accent)] accent-glow' : ''}`);
|
|
18
18
|
const fallbackText = $derived(fallback || alt.charAt(0).toUpperCase());
|
|
19
19
|
function handleImageError() {
|
|
20
20
|
imageError = true;
|
|
@@ -17,10 +17,10 @@ const hoverClasses = {
|
|
|
17
17
|
feature: 'hover:bg-[var(--color-accent-overlay-30)] hover:shadow-[var(--shadow-accent-glow)]'
|
|
18
18
|
};
|
|
19
19
|
const sizeClasses = {
|
|
20
|
-
sm: 'px-2 py-
|
|
21
|
-
md: 'px-3 py-1 text-[0.75rem] gap-1.5'
|
|
20
|
+
sm: 'px-2.5 py-1 text-[var(--text-xs)] gap-1',
|
|
21
|
+
md: 'px-3.5 py-1.5 text-[0.75rem] gap-1.5'
|
|
22
22
|
};
|
|
23
|
-
const classes = $derived(`inline-flex items-center justify-center rounded-[var(--radius-
|
|
23
|
+
const classes = $derived(`inline-flex items-center justify-center rounded-[var(--radius-sm)] border-[1.5px] font-[var(--font-mono)] font-medium transition-all duration-[var(--duration-300)] ease-[var(--ease-sharp)] ${variantClasses[variant]} ${sizeClasses[size]} ${hover ? hoverClasses[variant] : ''} ${glow ? 'accent-glow' : ''} ${uppercase ? 'uppercase tracking-wider' : ''} ${className}`.trim());
|
|
24
24
|
</script>
|
|
25
25
|
|
|
26
26
|
<span class={classes} {...restProps}>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">import SelectMenu from '../forms/SelectMenu.svelte';
|
|
2
|
-
let { month = $bindable(new Date()), events = [], selectedDate = $bindable(undefined), onDateSelect, onMonthChange, renderCell, weekStartsOn = 0, showWeekNumbers = false, highlightToday = true, class: className = '' } = $props();
|
|
2
|
+
let { month = $bindable(new Date()), events = [], selectedDate = $bindable(undefined), onDateSelect, onMonthChange, renderCell, weekStartsOn = 0, showWeekNumbers = false, highlightToday = true, minDate, maxDate, class: className = '' } = $props();
|
|
3
3
|
const today = new Date();
|
|
4
4
|
today.setHours(0, 0, 0, 0);
|
|
5
5
|
// Get calendar days for the current month
|
|
@@ -156,11 +156,14 @@ const monthOptions = monthNames.map((name, index) => ({
|
|
|
156
156
|
value: String(index),
|
|
157
157
|
label: name
|
|
158
158
|
}));
|
|
159
|
-
// Generate year options (
|
|
159
|
+
// Generate year options: 100 years back (birth dates), 50 forward,
|
|
160
|
+
// clamped to minDate/maxDate when provided.
|
|
160
161
|
const yearOptions = $derived.by(() => {
|
|
161
162
|
const current = new Date().getFullYear();
|
|
163
|
+
const start = minDate ? minDate.getFullYear() : current - 100;
|
|
164
|
+
const end = maxDate ? maxDate.getFullYear() : current + 50;
|
|
162
165
|
const options = [];
|
|
163
|
-
for (let y =
|
|
166
|
+
for (let y = start; y <= end; y++) {
|
|
164
167
|
options.push({ value: String(y), label: String(y) });
|
|
165
168
|
}
|
|
166
169
|
return options;
|
|
@@ -438,7 +441,6 @@ const variantColors = {
|
|
|
438
441
|
background: var(--color-accent);
|
|
439
442
|
border-color: var(--color-accent);
|
|
440
443
|
color: var(--color-text);
|
|
441
|
-
box-shadow: 0 0 20px var(--color-accent-glow);
|
|
442
444
|
}
|
|
443
445
|
|
|
444
446
|
.calendar-cell.is-selected .day-number {
|
|
@@ -18,6 +18,9 @@ interface Props {
|
|
|
18
18
|
weekStartsOn?: 0 | 1;
|
|
19
19
|
showWeekNumbers?: boolean;
|
|
20
20
|
highlightToday?: boolean;
|
|
21
|
+
/** Clamp the year selector (e.g. from a DatePicker's min/max). */
|
|
22
|
+
minDate?: Date;
|
|
23
|
+
maxDate?: Date;
|
|
21
24
|
class?: string;
|
|
22
25
|
}
|
|
23
26
|
declare const CalendarGrid: import("svelte").Component<Props, {}, "month" | "selectedDate">;
|
|
@@ -17,7 +17,7 @@ async function copyCode() {
|
|
|
17
17
|
}
|
|
18
18
|
</script>
|
|
19
19
|
|
|
20
|
-
<div class="panel-raised rounded-[var(--radius-
|
|
20
|
+
<div class="panel-raised rounded-[var(--radius-sm)] overflow-hidden relative">
|
|
21
21
|
{#if filename || showCopy}
|
|
22
22
|
<div
|
|
23
23
|
class="px-4 py-2 border-b border-border flex items-center justify-between"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">let { variant = 'plain', hoverable = false, items, children } = $props();
|
|
2
2
|
const containerClasses = $derived(variant === 'bordered'
|
|
3
|
-
? 'panel-raised rounded-[var(--radius-
|
|
3
|
+
? 'panel-raised rounded-[var(--radius-sm)] border border-[var(--color-border)]'
|
|
4
4
|
: '');
|
|
5
5
|
const listClasses = $derived(`list-none ${variant} ${hoverable ? 'hoverable' : ''}`);
|
|
6
6
|
export {};
|
|
@@ -52,7 +52,7 @@ function goToPage(page) {
|
|
|
52
52
|
<nav role="navigation" aria-label="Pagination" class="flex items-center gap-2">
|
|
53
53
|
<!-- Previous Button -->
|
|
54
54
|
<button
|
|
55
|
-
class="flex items-center gap-2 px-3 py-2 rounded-
|
|
55
|
+
class="flex items-center gap-2 px-3 py-2 rounded-none border border-[var(--color-border)] bg-transparent transition-all duration-300 font-[var(--font-mono)] hover:bg-[var(--color-secondary-overlay-10)] hover:border-[var(--color-accent)] disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:border-[var(--color-border)]"
|
|
56
56
|
onclick={() => goToPage(currentPage - 1)}
|
|
57
57
|
disabled={currentPage === 1}
|
|
58
58
|
aria-disabled={currentPage === 1}
|
|
@@ -71,7 +71,7 @@ function goToPage(page) {
|
|
|
71
71
|
<span class="px-3 py-2 text-[var(--color-text-muted)]">...</span>
|
|
72
72
|
{:else}
|
|
73
73
|
<button
|
|
74
|
-
class="px-3 py-2 rounded-
|
|
74
|
+
class="px-3 py-2 rounded-none transition-all duration-300 font-[var(--font-mono)]"
|
|
75
75
|
class:active={page === currentPage}
|
|
76
76
|
class:inactive={page !== currentPage}
|
|
77
77
|
onclick={() => goToPage(page)}
|
|
@@ -84,7 +84,7 @@ function goToPage(page) {
|
|
|
84
84
|
|
|
85
85
|
<!-- Next Button -->
|
|
86
86
|
<button
|
|
87
|
-
class="flex items-center gap-2 px-3 py-2 rounded-
|
|
87
|
+
class="flex items-center gap-2 px-3 py-2 rounded-none border border-[var(--color-border)] bg-transparent transition-all duration-300 font-[var(--font-mono)] hover:bg-[var(--color-secondary-overlay-10)] hover:border-[var(--color-accent)] disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:border-[var(--color-border)]"
|
|
88
88
|
onclick={() => goToPage(currentPage + 1)}
|
|
89
89
|
disabled={currentPage === totalPages}
|
|
90
90
|
aria-disabled={currentPage === totalPages}
|
|
@@ -6,7 +6,6 @@ const trendColors = {
|
|
|
6
6
|
neutral: 'var(--color-text-muted)'
|
|
7
7
|
};
|
|
8
8
|
const trendColor = $derived(trend ? trendColors[trend] : 'var(--color-text-muted)');
|
|
9
|
-
const showGlow = $derived(trend === 'up');
|
|
10
9
|
// Count-up: animate toward the current numeric value; re-runs when value changes.
|
|
11
10
|
let animatedValue = $state(0);
|
|
12
11
|
$effect(() => {
|
|
@@ -37,13 +36,13 @@ const displayValue = $derived(countUp && typeof value === 'number'
|
|
|
37
36
|
: value);
|
|
38
37
|
</script>
|
|
39
38
|
|
|
40
|
-
<div class="panel-spectral rounded-[var(--radius-
|
|
39
|
+
<div class="panel-spectral rounded-[var(--radius-sm)] p-6">
|
|
41
40
|
<!-- Top Row: Icon & Label -->
|
|
42
41
|
<div class="flex items-start justify-between mb-4">
|
|
43
42
|
<div class="flex-1">
|
|
44
43
|
<div
|
|
45
44
|
class="uppercase tracking-wider"
|
|
46
|
-
style="color: var(--color-text-soft); font-family: var(--font-
|
|
45
|
+
style="color: var(--color-text-soft); font-family: var(--font-mono); font-size: 0.75rem; font-weight: 500;"
|
|
47
46
|
>
|
|
48
47
|
{label}
|
|
49
48
|
</div>
|
|
@@ -59,8 +58,7 @@ const displayValue = $derived(countUp && typeof value === 'number'
|
|
|
59
58
|
<!-- Value Display -->
|
|
60
59
|
<div
|
|
61
60
|
class="mb-2"
|
|
62
|
-
|
|
63
|
-
style="color: var(--color-text); font-family: var(--font-heading); font-size: 2.5rem; font-weight: 700; line-height: 1.2;"
|
|
61
|
+
style="color: var(--color-text); font-family: var(--font-heading); font-size: 2.5rem; font-weight: 800; line-height: 1.2;"
|
|
64
62
|
>
|
|
65
63
|
{displayValue}
|
|
66
64
|
</div>
|
|
@@ -87,7 +87,7 @@ function notifySelectionChange() {
|
|
|
87
87
|
}
|
|
88
88
|
</script>
|
|
89
89
|
|
|
90
|
-
<div class="panel-raised rounded-
|
|
90
|
+
<div class="panel-raised rounded-none overflow-hidden {className}" {...restProps}>
|
|
91
91
|
<table class="mrdv-table w-full" class:striped class:hoverable class:compact class:sticky-header={stickyHeader}>
|
|
92
92
|
{#if isDynamicMode}
|
|
93
93
|
<!-- Dynamic mode: render from columns and data -->
|
|
@@ -265,15 +265,21 @@ function notifySelectionChange() {
|
|
|
265
265
|
|
|
266
266
|
.mrdv-table :global(thead th) {
|
|
267
267
|
color: var(--color-text);
|
|
268
|
-
font-
|
|
268
|
+
font-family: var(--font-mono);
|
|
269
|
+
font-weight: 500;
|
|
269
270
|
text-transform: uppercase;
|
|
270
271
|
letter-spacing: 0.05em;
|
|
271
272
|
text-align: left;
|
|
272
273
|
}
|
|
273
274
|
|
|
275
|
+
/* Dense mode: information density over spaciousness */
|
|
274
276
|
.mrdv-table.compact :global(th),
|
|
275
277
|
.mrdv-table.compact :global(td) {
|
|
276
|
-
padding: 0.
|
|
278
|
+
padding: 0.375rem 0.75rem;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.mrdv-table.compact :global(td) {
|
|
282
|
+
font-size: var(--text-sm);
|
|
277
283
|
}
|
|
278
284
|
|
|
279
285
|
.mrdv-table:not(.compact) :global(th),
|
|
@@ -292,11 +298,7 @@ function notifySelectionChange() {
|
|
|
292
298
|
}
|
|
293
299
|
|
|
294
300
|
.mrdv-table.hoverable :global(tbody tr:hover) {
|
|
295
|
-
background: var(--color-accent-overlay-
|
|
296
|
-
transform: translateY(-1px);
|
|
297
|
-
box-shadow:
|
|
298
|
-
0 2px 8px var(--color-shadow-overlay-20),
|
|
299
|
-
var(--shadow-accent-glow);
|
|
301
|
+
background: var(--color-accent-overlay-10);
|
|
300
302
|
}
|
|
301
303
|
|
|
302
304
|
.mrdv-table.striped :global(tbody tr:nth-child(even)) {
|
|
@@ -304,11 +306,7 @@ function notifySelectionChange() {
|
|
|
304
306
|
}
|
|
305
307
|
|
|
306
308
|
.mrdv-table.striped.hoverable :global(tbody tr:nth-child(even):hover) {
|
|
307
|
-
background: var(--color-accent-overlay-
|
|
308
|
-
transform: translateY(-1px);
|
|
309
|
-
box-shadow:
|
|
310
|
-
0 2px 8px var(--color-shadow-overlay-20),
|
|
311
|
-
var(--shadow-accent-glow);
|
|
309
|
+
background: var(--color-accent-overlay-10);
|
|
312
310
|
}
|
|
313
311
|
|
|
314
312
|
.mrdv-table :global(th),
|
|
@@ -324,7 +322,7 @@ function notifySelectionChange() {
|
|
|
324
322
|
}
|
|
325
323
|
|
|
326
324
|
.mrdv-table th.sortable:hover {
|
|
327
|
-
background: var(--color-accent-overlay-
|
|
325
|
+
background: var(--color-accent-overlay-10);
|
|
328
326
|
color: var(--color-accent);
|
|
329
327
|
}
|
|
330
328
|
|
|
@@ -11,7 +11,7 @@ const sizeClasses = {
|
|
|
11
11
|
md: 'px-3 py-1.5 text-sm gap-2',
|
|
12
12
|
lg: 'px-4 py-2 text-base gap-2'
|
|
13
13
|
};
|
|
14
|
-
const classes = $derived(`inline-flex items-center rounded-[var(--radius-
|
|
14
|
+
const classes = $derived(`inline-flex items-center rounded-[var(--radius-sm)] border font-[var(--font-mono)] font-medium transition-all duration-150 ${sizeClasses[size]} ${variantClasses[variant]}`);
|
|
15
15
|
function handleDismiss() {
|
|
16
16
|
onDismissCallback?.();
|
|
17
17
|
}
|
|
@@ -122,7 +122,7 @@ const hideTip = () => (tip = null);
|
|
|
122
122
|
.heatmap-cell {
|
|
123
123
|
height: var(--heatmap-cell-size, 1.5rem);
|
|
124
124
|
min-width: var(--heatmap-cell-size, 1.5rem);
|
|
125
|
-
border-radius:
|
|
125
|
+
border-radius: 0;
|
|
126
126
|
border: 0;
|
|
127
127
|
padding: 0;
|
|
128
128
|
}
|
|
@@ -134,7 +134,7 @@ const hideTip = () => (tip = null);
|
|
|
134
134
|
pointer-events: none;
|
|
135
135
|
white-space: nowrap;
|
|
136
136
|
padding: 0.4rem 0.75rem;
|
|
137
|
-
border-radius: var(--radius-
|
|
137
|
+
border-radius: var(--radius-sm);
|
|
138
138
|
font-size: 0.8rem;
|
|
139
139
|
color: var(--color-text);
|
|
140
140
|
}
|
|
@@ -12,7 +12,7 @@ const hasError = $derived(error || !!validationMessage);
|
|
|
12
12
|
{disabled}
|
|
13
13
|
bind:checked
|
|
14
14
|
onchange={onchange}
|
|
15
|
-
class="w-6 h-6 rounded-sm border-2 bg-transparent appearance-none transition-all duration-200 ease-luxe cursor-pointer checked:bg-accent checked:border-accent
|
|
15
|
+
class="w-6 h-6 rounded-sm border-2 bg-transparent appearance-none transition-all duration-200 ease-luxe cursor-pointer checked:bg-accent checked:border-accent focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-2 focus:ring-offset-base-1 relative {hasError ? 'border-error' : 'border-[var(--color-border-bright)]'} {disabled ? 'cursor-not-allowed' : ''}"
|
|
16
16
|
/>
|
|
17
17
|
{#if iconBefore}
|
|
18
18
|
<span class="inline-flex items-center justify-center text-text-soft">
|
|
@@ -246,7 +246,7 @@ const errorClasses = $derived(hasError ? 'error-state' : '');
|
|
|
246
246
|
role="dialog"
|
|
247
247
|
aria-modal="true"
|
|
248
248
|
aria-label="Choose date"
|
|
249
|
-
class="calendar-dropdown {variant === 'glass' ? 'glass-panel' : 'panel-floating'} rounded-
|
|
249
|
+
class="calendar-dropdown {variant === 'glass' ? 'glass-panel' : 'panel-floating'} rounded-[var(--radius-sm)] overflow-hidden"
|
|
250
250
|
onclick={(e) => e.stopPropagation()}
|
|
251
251
|
>
|
|
252
252
|
<CalendarGrid
|
|
@@ -254,6 +254,8 @@ const errorClasses = $derived(hasError ? 'error-state' : '');
|
|
|
254
254
|
selectedDate={selectedDate}
|
|
255
255
|
onDateSelect={handleDateSelect}
|
|
256
256
|
highlightToday={true}
|
|
257
|
+
minDate={minDate}
|
|
258
|
+
maxDate={maxDate}
|
|
257
259
|
class="calendar-picker"
|
|
258
260
|
/>
|
|
259
261
|
</div>
|