@muten/shadcn 0.0.4 → 0.0.7
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/AGENTS.md +116 -0
- package/README.md +10 -4
- package/globals.css +44 -8
- package/motion.css +49 -0
- package/package.json +7 -2
- package/registry/bento.muten +9 -0
- package/registry/button-group.muten +4 -0
- package/registry/button.muten +8 -0
- package/registry/feature-rows.muten +8 -0
- package/registry/input-group.muten +4 -0
- package/registry/input.muten +7 -0
- package/registry/kbd.muten +3 -0
- package/registry/link-btn.muten +7 -0
- package/registry/navpill.muten +10 -0
- package/registry/skeleton.muten +4 -0
- package/registry/stat-band.muten +9 -0
- package/registry.json +78 -32
- package/themes/bloom.css +53 -0
- package/themes/editorial.css +65 -0
- package/themes/noir.css +54 -0
- package/registry/chart.js +0 -34
- package/registry/chart.muten +0 -7
- package/registry/checkbox.muten +0 -8
- package/registry/select.muten +0 -21
package/AGENTS.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# @muten/shadcn - component catalog (for an AI assembling a muten app)
|
|
2
|
+
|
|
3
|
+
## Setup (once): `plugins { shadcn {} }` in muten.config + `@import "@muten/shadcn/globals.css";` in src/styles.css. Then everything below is usable directly; `muten add <name>` only ejects a source.
|
|
4
|
+
|
|
5
|
+
> **Native primitives always win — for a checkbox / select / number / range / date / chart, use the native muten
|
|
6
|
+
> primitive, NOT a plugin part.** muten's core now ships these as primitives, so this plugin does NOT: write
|
|
7
|
+
> `Checkbox bind(agree)`, `Select bind(theme) options(System, Light, Dark) class("native-select")` (the `.native-select`
|
|
8
|
+
> class gives the native `<select>` the shadcn look), `Number bind(qty)`, `Range bind(vol)`, `Date bind(due)`,
|
|
9
|
+
> `Chart @data kind(bar) x(label) y(value)`. A part named the same as a primitive is unreachable (the primitive
|
|
10
|
+
> shadows it), so those parts were removed. For a *searchable* dropdown use the `Combobox` part.
|
|
11
|
+
> **The exact props of every part are in the header comment of `node_modules/@muten/shadcn/registry/<name>.muten` —
|
|
12
|
+
> read it before calling (the one-liners below are the catalog, not the API).**
|
|
13
|
+
|
|
14
|
+
## PLUG-AND-PLAY (zero class props)
|
|
15
|
+
- **Btn**(variant, onClick) { slot } · **LinkBtn**(to, variant) { slot } · **Input**(value, placeholder)
|
|
16
|
+
- **DataTable** @rows columns(a, b) - STYLED BY DEFAULT, no class needed.
|
|
17
|
+
- + every Part / Custom below.
|
|
18
|
+
|
|
19
|
+
## Parts (62) - imported by plugins {}
|
|
20
|
+
|
|
21
|
+
- **Card** - A surface with CardHeader / CardTitle / CardDescription / CardContent / CardFooter sub-components. · +CardHeader, CardTitle, CardDescription, CardContent, CardFooter
|
|
22
|
+
- **Badge** - A small status descriptor. variant: default | secondary | destructive | outline.
|
|
23
|
+
- **Alert** - A callout with AlertTitle / AlertDescription. variant: default | destructive. · +AlertTitle, AlertDescription
|
|
24
|
+
- **Avatar** - A user's initials in a circle (AvatarFallback).
|
|
25
|
+
- **Switch** - A toggle; the page owns the bool + the action.
|
|
26
|
+
- **Spinner** - A loading indicator (animate-spin loader).
|
|
27
|
+
- **Progress** - A determinate progress bar; value is 0-100.
|
|
28
|
+
- **Separator** - A thin divider line.
|
|
29
|
+
- **Empty** - An empty-state placeholder (title + description in a dashed border).
|
|
30
|
+
- **Breadcrumb** - A breadcrumb trail with BreadcrumbLink / BreadcrumbSeparator / BreadcrumbPage. · +BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator
|
|
31
|
+
- **Pagination** - Page navigation with PaginationLink / Previous / Next / Ellipsis. · +PaginationLink, PaginationPrevious, PaginationNext, PaginationEllipsis
|
|
32
|
+
- **Field** - A labelled form control: FieldLabel / FieldDescription / FieldError. · +FieldLabel, FieldDescription, FieldError
|
|
33
|
+
- **Item** - A bordered row: ItemContent / ItemTitle / ItemDescription. · +ItemContent, ItemTitle, ItemDescription
|
|
34
|
+
- **Toggle** - A two-state button; the page owns the bool + the toggle action.
|
|
35
|
+
- **ToggleGroup** - Single-select toolbar with ToggleGroupItem; page owns a text state. · +ToggleGroupItem
|
|
36
|
+
- **RadioGroup** - Single choice with RadioGroupItem; page owns a text state. · +RadioGroupItem
|
|
37
|
+
- **Collapsible** - A trigger + content shown when open; page owns the bool.
|
|
38
|
+
- **Accordion** - Stacked expandable sections with AccordionItem; page owns a bool per item. · +AccordionItem
|
|
39
|
+
- **TabsList** - Tab bar (TabsList/TabsTrigger) + TabsContent panels; page owns the active-tab text. · +TabsTrigger, TabsContent
|
|
40
|
+
- **Dialog** - A modal with DialogTitle/Description/Footer; page owns the open bool. Backdrop click closes. · +DialogTitle, DialogDescription, DialogFooter
|
|
41
|
+
- **Tooltip** - A hover tooltip (pure CSS, no state). The slot is the trigger.
|
|
42
|
+
- **DropdownMenu** - A menu below a trigger with DropdownItem/Label/Separator; page owns the open bool. · +DropdownItem, DropdownLabel, DropdownSeparator
|
|
43
|
+
- **Popover** - A floating panel below a trigger; page owns the open bool.
|
|
44
|
+
- **AlertDialog** - A modal requiring an explicit choice (no backdrop dismiss); page owns the open bool. · +AlertDialogTitle, AlertDialogDescription, AlertDialogFooter
|
|
45
|
+
- **Sheet** - A panel that slides in from the right; page owns the open bool. · +SheetTitle, SheetDescription
|
|
46
|
+
- **Drawer** - A panel that slides up from the bottom; page owns the open bool.
|
|
47
|
+
- **HoverCard** - Richer content shown on hover (pure CSS, no state).
|
|
48
|
+
- **ScrollArea** - A scrollable region with styled scrollbars.
|
|
49
|
+
- **Message** - Chat Message + Bubble + Marker (variant: sent | received). · +Bubble, Marker
|
|
50
|
+
- **MessageScroller** - A scrollable column of chat messages.
|
|
51
|
+
- **Attachment** - A file attachment chip (icon + name + size).
|
|
52
|
+
- **Menubar** - A bar of menus (File/Edit/View); page owns which is open via a text state. · +MenubarMenu, MenubarItem, MenubarSeparator
|
|
53
|
+
- **NavigationMenu** - A nav bar with hover-revealed panels (pure CSS, no state). · +NavigationMenuItem, NavigationMenuLink
|
|
54
|
+
- **Combobox** - A Select with a search box; page filters items with `each … where`. · +ComboboxItem
|
|
55
|
+
- **Command** - A command palette (search + grouped CommandItem list). · +CommandList, CommandGroup, CommandItem, CommandShortcut
|
|
56
|
+
- **SegmentedControl** - A single-select toolbar (SegmentedItem); page owns a text state. · +SegmentedItem, SegmentedTabs, IconTab
|
|
57
|
+
- **VectorField** - Paired NumberFields (X/Y) for position or size.
|
|
58
|
+
- **Toolbar** - A grouped button bar with ToolbarGroup / ToolbarSeparator. · +ToolbarGroup, ToolbarSeparator
|
|
59
|
+
- **StatusBar** - A thin bottom bar with StatusItem / StatusSpacer. · +StatusItem, StatusSpacer
|
|
60
|
+
- **ZoomControl** - A - 100% + zoom widget; page owns the zoom number.
|
|
61
|
+
- **SplitButton** - A main action + a caret dropdown; page owns the open bool.
|
|
62
|
+
- **SwatchPalette** - A grid of selectable color swatches (Swatch); page owns the selected hex + colors. · +Swatch
|
|
63
|
+
- **AppSidebar** - AppSidebar - a collapsible nav panel (SidebarHeader / SidebarGroup / SidebarItem). Named AppSidebar so it never shadows muten's core Sidebar <aside> primitive. · +SidebarHeader, SidebarGroup, SidebarGroupLabel, SidebarItem
|
|
64
|
+
- **Inspector** - Property grid: InspectorSection (collapsible) / InspectorRow (label : value slot). · +InspectorSection, InspectorRow
|
|
65
|
+
- **CommandPalette** - A top-anchored modal holding a Command; page owns the open bool.
|
|
66
|
+
- **Stepper** - Horizontal or vertical step progress indicator; done steps show a check, the current step is ringed (parts: Stepper, StepperVertical, Step). · +StepperVertical, Step
|
|
67
|
+
- **Timeline** - Vertical event timeline: a dot on a connector line + title/time/description per item (parts: Timeline, TimelineItem). · +TimelineItem
|
|
68
|
+
- **Pill** - A hero eyebrow chip (rounded-full, bigger than Badge); slot-based for an icon + text.
|
|
69
|
+
- **FeatureGrid** - BLOCK: responsive grid of feature cards (icon + title + copy). Parts: FeatureGrid, FeatureCard. · +FeatureCard
|
|
70
|
+
- **Pricing** - BLOCK: a row of pricing tiers (name/price/features/CTA), featured highlight. Parts: Pricing, PricingTier, PricingFeature. · +PricingTier, PricingTierFeatured, PricingFeature
|
|
71
|
+
- **LogoCloud** - BLOCK: a 'trusted by' strip of logos, muted + grayscale until hover. Parts: LogoCloud, LogoCloudItem. · +LogoCloudItem
|
|
72
|
+
- **NavPill** - BLOCK: the floating glass navbar (sticky wrapper + glass pill + scroll animation baked in). Drop logo/links/CTA inside.
|
|
73
|
+
- **StatBand** - BLOCK: a divided row of KPI stats. Parts: StatBand, Stat(label) - value goes in the slot (Span or Custom CountUp). · +Stat
|
|
74
|
+
- **FeatureRows** - BLOCK: numbered feature/schedule rows with side gutters (12-col grid inside). Parts: FeatureRows, FeatureRow. · +FeatureRow
|
|
75
|
+
- **BentoGallery** - BLOCK: fixed-row-height tiling gallery; cells pick their col-span. Parts: BentoGallery, BentoCell. · +BentoCell
|
|
76
|
+
- **ButtonGroup** - A segmented row of buttons that share borders. Put btn-outline buttons inside.
|
|
77
|
+
- **InputGroup** - An input with a leading icon/addon in one bordered field.
|
|
78
|
+
- **Skeleton** - A loading placeholder (pulse). Add size via class: Skeleton class("h-4 w-32").
|
|
79
|
+
- **Kbd** - A keyboard key hint. Kbd { Span "Ctrl" }.
|
|
80
|
+
- **Btn** - Plug-and-play button (part Btn - Button is a primitive). `Btn(variant: "default", onClick: save) { Span "Save" }`. variant: default|secondary|outline|ghost|destructive|link.
|
|
81
|
+
- **Input** - Plug-and-play text input (part Input wraps SearchField). `Input(value: q, placeholder: "...")` - value is two-way bound. For typed/validated fields use a Form.
|
|
82
|
+
- **LinkBtn** - A navigation link styled as a button. `LinkBtn(to: "/signup", variant: "default") { Span "Get started" }`.
|
|
83
|
+
|
|
84
|
+
## Customs (20) - importable; some need a dep
|
|
85
|
+
|
|
86
|
+
- **Slider** (`slider`) - A draggable range slider (Custom). Page owns the value number.
|
|
87
|
+
- **InputOtp** (`input-otp`) - N single-character boxes synced to one value (Custom). Page owns the value text.
|
|
88
|
+
- **Calendar** (`calendar`) - A month-grid date picker (Custom). Page owns the selected ISO date text.
|
|
89
|
+
- **Toaster** (`toaster`) - Fixed toast notifications (Custom). Page bumps a trigger counter to fire one.
|
|
90
|
+
- **Carousel** (`carousel`) - A slide carousel over a list (Custom). Page owns the slides list.
|
|
91
|
+
- **GradientBlinds** (`gradient-blinds`) · **needs npm: ogl** - Animated WebGL gradient + diagonal-blinds hero background that follows the cursor and fades into the page (needs ogl).
|
|
92
|
+
- **Resizable** (`resizable`) - Two panels with a draggable splitter (Custom).
|
|
93
|
+
- **ContextMenu** (`context-menu`) - Right-click an area to open a menu of items (Custom).
|
|
94
|
+
- **NumberField** (`number-field`) - A numeric input with -/+ steppers, min/max/step (Custom).
|
|
95
|
+
- **FileInput** (`file-input`) - A click/drag-drop file dropzone (Custom); emits file names.
|
|
96
|
+
- **ColorPicker** (`color-picker`) - A swatch + saturation/value + hue popover (Custom); page owns the hex.
|
|
97
|
+
- **TreeView** (`tree-view`) - A nested folder/file tree (Custom): expandable, selectable, icons + indent.
|
|
98
|
+
- **CanvasHost** (`canvas-viewport`) - A <canvas> with pan/zoom (Custom) + a muten toolbar slot floating on top.
|
|
99
|
+
- **TagInput** (`tag-input`) - Chips you add by typing + Enter (Custom); page owns a comma-joined text.
|
|
100
|
+
- **RangeSlider** (`range-slider`) - A two-thumb range slider (Custom); page owns the low + high numbers.
|
|
101
|
+
- **SortableList** (`sortable-list`) - Drag rows to reorder (Custom); page owns a comma-joined order.
|
|
102
|
+
- **FolderPicker** (`folder-picker`) - Connect a local folder via showDirectoryPicker (Custom); emits the name.
|
|
103
|
+
- **Hotkey** (`hotkey`) - A global keyboard shortcut (Cmd/Ctrl+key), invisible (Custom).
|
|
104
|
+
- **Code** (`code`) - Syntax-highlighted code block that understands muten (shared highlight.js, same grammar as the VS Code extension).
|
|
105
|
+
- **CountUp** (`count-up`) - Animate a formatted number (2.4M / 94% / 40ms / 99.99%) from 0 when it scrolls into view; lands on the exact target. Custom. Styled form (takes the class): `Custom CountUp inputs(to: "2.4M") class("text-4xl font-bold")`. For stat bands + hero metrics.
|
|
106
|
+
|
|
107
|
+
## Class recipes (4) - style a primitive (Table is default-styled; typography/textarea/motion)
|
|
108
|
+
|
|
109
|
+
- **textarea** - class("textarea") · label: class("label") · native select: class("native-select")
|
|
110
|
+
- **typography** - Title "Docs" h1 class("prose-h1") (prose-h1..h4 · prose-p|lead|large|small|muted|blockquote|code|list)
|
|
111
|
+
- **table** - DataTable @rows columns(name, email, status) - STYLED BY DEFAULT (no class needed). `class("data-table")` still works as an explicit alias. Raw cells; for badge/formatted cells build with each + Stack.
|
|
112
|
+
- **motion** - Page animations, all CSS + reduced-motion safe. Entrance: animate-fade-up|fade-in|scale-in|slide-in (+ d1..d6 stagger). Scroll: reveal|reveal-left|reveal-scale, and nav-scroll on a navbar. Hover/press: hover-lift, hover-glow, arrow-hover (nudges a trailing Icon), link-underline, press, float.
|
|
113
|
+
|
|
114
|
+
### Gotcha: a part can not be named after a core primitive — the **primitive wins** and any same-named plugin part is unreachable on import. The primitives include `Button/Link/Form/Image/SearchField/DataTable/Icon/Text/Title/Span` (→ this plugin uses `Btn`/`Input`/`LinkBtn`) AND `Select/Checkbox/Number/Range/Date/Chart` (→ use those primitives directly, see the note at the top). If you write a part call and the oracle says `missing-prop: X is missing the required "bind"`, X is a primitive shadowing the part — switch to the primitive's API.
|
|
115
|
+
|
|
116
|
+
### Gotcha: a **part takes NO trailing modifiers** — `Part(...) class(...)`, `Part(...) disabled when x`, `Part(...) on(...)` and `Part(...) aria(...)` are all syntax errors (a part call ends at its `)`/`{}`). When you need `class`/`disabled when`/`on`/`aria` on a control, use the **native primitive** (`Button "Save" -> save disabled when not valid class("btn btn-default")`), not the `Btn(...)` part.
|
package/README.md
CHANGED
|
@@ -94,19 +94,25 @@ Display: **card** (Card / CardHeader / CardTitle / CardDescription / CardContent
|
|
|
94
94
|
**alert** (Alert / AlertTitle / AlertDescription), **avatar**, **spinner**, **progress**, **empty**,
|
|
95
95
|
**breadcrumb**, **pagination**, **field**, **item**.
|
|
96
96
|
|
|
97
|
-
Stateful (the page owns the state, the part receives it - see the pattern below): **switch**,
|
|
97
|
+
Stateful (the page owns the state, the part receives it - see the pattern below): **switch**,
|
|
98
98
|
**toggle**, **toggle-group**, **radio-group**, **collapsible**, **accordion**, **tabs**.
|
|
99
99
|
|
|
100
100
|
Overlays (the page owns an `open` bool): **dialog**, **alert-dialog**, **tooltip** (pure hover, no state),
|
|
101
|
-
**dropdown-menu**, **popover**, **sheet**, **drawer**, **hover-card** (hover), **
|
|
101
|
+
**dropdown-menu**, **popover**, **sheet**, **drawer**, **hover-card** (hover), **scroll-area**,
|
|
102
102
|
**menubar**, **navigation-menu** (hover), **combobox**, **command**.
|
|
103
103
|
|
|
104
|
+
> **Not here on purpose:** checkbox / select / number / range / date / chart are **native muten primitives** now,
|
|
105
|
+
> so this plugin ships no part for them (a same-named part would be shadowed by the primitive). Use `Checkbox bind(x)`,
|
|
106
|
+
> `Select bind(x) options(a, b) class("native-select")`, `Number`/`Range`/`Date bind(x)`, `Chart @data kind(bar) …`.
|
|
107
|
+
> For a *searchable* dropdown, use **combobox**.
|
|
108
|
+
|
|
104
109
|
Chat: **message** (Message / Bubble / Marker), **message-scroller**, **attachment**.
|
|
105
110
|
|
|
106
111
|
### Custom (eject-only - `muten add` copies a host `.js` into `src/components/`)
|
|
107
112
|
|
|
108
|
-
The genuinely interactive 20%: **
|
|
109
|
-
|
|
113
|
+
The genuinely interactive 20%: **input-otp**, **toaster**, **carousel**, **resizable**, **context-menu** (+ more,
|
|
114
|
+
see AGENTS.md). Each is a thin muten part over a small vanilla-JS host you own and can edit. (A chart / slider /
|
|
115
|
+
date picker is **native** — `Chart`/`Range`/`Date` — not a Custom; don't `muten add` those.)
|
|
110
116
|
|
|
111
117
|
`calendar` ships all the shadcn variants: `mode` (`single` / `range` / `multiple`), `months` (1-2 side by side),
|
|
112
118
|
`caption` (`label` / `dropdown`). `selected` encodes the value (`"yyyy-mm-dd"`, `"from/to"`, or `"d1,d2,..."`).
|
package/globals.css
CHANGED
|
@@ -175,6 +175,7 @@
|
|
|
175
175
|
raw <svg> child - muten renders icons as iconify-icon). Icon sizing is the `.btn svg` rule below. */
|
|
176
176
|
.btn { @apply inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium leading-none transition-all shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] active:scale-[0.97] aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 disabled:pointer-events-none disabled:opacity-50 h-8 px-3; }
|
|
177
177
|
.btn-default { @apply bg-primary text-primary-foreground shadow-xs hover:bg-primary/90 active:bg-primary/80; }
|
|
178
|
+
.btn-primary { @apply bg-primary text-primary-foreground shadow-xs hover:bg-primary/90 active:bg-primary/80; } /* alias of btn-default — tolerate the near-universal `btn-primary` idiom */
|
|
178
179
|
.btn-destructive { @apply bg-destructive text-white shadow-xs hover:bg-destructive/90 active:bg-destructive/80 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60; }
|
|
179
180
|
.btn-outline { @apply border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground active:bg-accent/70 dark:bg-input/30 dark:border-input dark:hover:bg-input/50 dark:active:bg-input/70; }
|
|
180
181
|
.btn-secondary { @apply bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80 active:bg-secondary/70; }
|
|
@@ -197,6 +198,20 @@
|
|
|
197
198
|
.label { @apply text-sm font-medium leading-none; }
|
|
198
199
|
.native-select { @apply flex h-8 w-full items-center rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive dark:bg-input/30 disabled:cursor-not-allowed disabled:opacity-50; }
|
|
199
200
|
|
|
201
|
+
/* ---- native primitives styled shadcn-flavored (muten core ships Checkbox/Number/Range/Date/Chart) ---- */
|
|
202
|
+
.mu-checkbox { @apply inline-flex items-center gap-2 text-sm; }
|
|
203
|
+
.mu-checkbox input, .mu-field-check { @apply size-4 rounded-[4px] border-input; accent-color: var(--primary); }
|
|
204
|
+
.mu-number, .mu-date { @apply flex h-8 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 dark:bg-input/30; }
|
|
205
|
+
.mu-range { @apply w-full; accent-color: var(--primary); }
|
|
206
|
+
.mu-chart-bar, .mu-chart-dot, .mu-chart-slice { fill: var(--primary); }
|
|
207
|
+
.mu-chart-slice { stroke: var(--card); stroke-width: 2.5; }
|
|
208
|
+
.mu-chart-dot { stroke: var(--card); stroke-width: 2; }
|
|
209
|
+
.mu-chart-line { fill: none; stroke: var(--primary); stroke-width: 2.25; stroke-linecap: round; stroke-linejoin: round; }
|
|
210
|
+
.mu-chart-area { stroke: none; } /* fill comes from the inline accent→transparent gradient __chart draws */
|
|
211
|
+
.mu-chart-grid { stroke: var(--border); stroke-opacity: 0.55; }
|
|
212
|
+
.mu-chart-tick, .mu-chart-xlabel { fill: var(--muted-foreground); font-size: 10px; font-variant-numeric: tabular-nums; }
|
|
213
|
+
.mu-chart-total { fill: var(--foreground); font-size: 15px; font-weight: 650; font-variant-numeric: tabular-nums; }
|
|
214
|
+
|
|
200
215
|
/* ---- Card ---- */
|
|
201
216
|
.card { @apply bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm; }
|
|
202
217
|
.card-header { @apply grid auto-rows-min items-start gap-1.5 px-6; }
|
|
@@ -435,12 +450,14 @@
|
|
|
435
450
|
.command-shortcut { @apply ml-auto text-xs tracking-widest text-muted-foreground; }
|
|
436
451
|
|
|
437
452
|
/* ---- Table - style muten's DataTable to look shadcn: DataTable @rows columns(...) class("data-table") ---- */
|
|
438
|
-
.
|
|
439
|
-
|
|
440
|
-
.data-table
|
|
441
|
-
.data-table
|
|
442
|
-
.data-table
|
|
443
|
-
.data-table
|
|
453
|
+
/* .mu-datatable is the base class the DataTable primitive always gets, so a bare `DataTable @rows columns(...)`
|
|
454
|
+
is styled out of the box (no class needed). Safe: DataTable is a leaf, never reused with another class. */
|
|
455
|
+
.data-table, .mu-datatable { @apply w-full caption-bottom text-sm; }
|
|
456
|
+
.data-table thead tr, .mu-datatable thead tr { @apply border-b; }
|
|
457
|
+
.data-table th, .mu-datatable th { @apply h-11 px-3 text-left align-middle font-medium text-muted-foreground whitespace-nowrap; }
|
|
458
|
+
.data-table tbody tr, .mu-datatable tbody tr { @apply border-b transition-colors duration-200 hover:bg-muted/50; }
|
|
459
|
+
.data-table tbody tr:last-child, .mu-datatable tbody tr:last-child { @apply border-0; }
|
|
460
|
+
.data-table td, .mu-datatable td { @apply p-3 align-middle whitespace-nowrap; }
|
|
444
461
|
|
|
445
462
|
/* ---- Slider (Custom; the host JS builds .slider-range + .slider-thumb inside .slider) ---- */
|
|
446
463
|
.slider { @apply relative h-2 w-full rounded-full bg-primary/20 cursor-pointer select-none; touch-action: none; }
|
|
@@ -715,6 +732,12 @@
|
|
|
715
732
|
.pricing-features { @apply flex flex-col gap-2.5 flex-1; }
|
|
716
733
|
.pricing-feature { @apply flex flex-row items-center gap-2 text-sm; }
|
|
717
734
|
|
|
735
|
+
/* ---- StatBand: a divided row of KPI stats (value + label), uniform, 2-up on mobile ---- */
|
|
736
|
+
.stat-band { @apply grid grid-cols-4 gap-px border-y border-border bg-border overflow-hidden max-sm:grid-cols-2; }
|
|
737
|
+
.stat { @apply flex flex-col items-center gap-1 bg-background px-6 py-10 text-center; }
|
|
738
|
+
.stat-value { @apply text-4xl font-bold tracking-tight; }
|
|
739
|
+
.stat-label { @apply text-sm text-muted-foreground; }
|
|
740
|
+
|
|
718
741
|
/* ---- LogoCloud: the "trusted by" strip - a wrapping row of logos, muted until hover. GENERIC: an item holds
|
|
719
742
|
ANY logo type - an <img>, an inline SVG/Icon, or a text wordmark - and each is normalized to the same height. ---- */
|
|
720
743
|
.logo-cloud { @apply flex flex-row flex-wrap items-center justify-center gap-x-10 gap-y-6; }
|
|
@@ -738,6 +761,16 @@
|
|
|
738
761
|
(the classic "call to action" micro-interaction). Respects reduced motion via the global rule below. ---- */
|
|
739
762
|
.arrow-hover .mu-icon { @apply transition-transform duration-200; }
|
|
740
763
|
.arrow-hover:hover .mu-icon:last-child { translate: 3px 0; }
|
|
764
|
+
|
|
765
|
+
/* ---- Touch targets. These components are sized for a mouse (h-8 = 32px); Apple HIG asks for >=44px and
|
|
766
|
+
Material for >=48dp before a finger hits reliably. `pointer: coarse` is the browser telling us there IS a
|
|
767
|
+
finger, so the SAME app adapts on a phone with no mobile build, no second plugin and no decision for the
|
|
768
|
+
author to make. Inside @layer components on purpose: a class("h-14") on the Button still wins.
|
|
769
|
+
Deliberately NOT everything h-8: `btn-xs`/`switch` are small by intent, and badges/avatars aren't targets. ---- */
|
|
770
|
+
@media (pointer: coarse) {
|
|
771
|
+
.btn, .input, .toggle, .select-trigger, .native-select { @apply h-11; }
|
|
772
|
+
.btn-sm { @apply h-10; }
|
|
773
|
+
}
|
|
741
774
|
}
|
|
742
775
|
|
|
743
776
|
@keyframes progress-indeterminate { 0% { left: -40%; } 100% { left: 100%; } }
|
|
@@ -754,8 +787,11 @@
|
|
|
754
787
|
@keyframes mu-slide-in { from { opacity: 0; translate: -1.5rem 0; } to { opacity: 1; translate: 0 0; } }
|
|
755
788
|
@keyframes mu-reveal-scale{ from { opacity: 0; scale: 0.92; translate: 0 1rem; } to { opacity: 1; scale: 1; translate: 0 0; } }
|
|
756
789
|
/* nav pill: airy + shadowless at the top, settles + deepens its shadow once you scroll, and reverses smoothly
|
|
757
|
-
when you return to the top (scroll-position driven, so "back to top" is free).
|
|
758
|
-
|
|
790
|
+
when you return to the top (scroll-position driven, so "back to top" is free).
|
|
791
|
+
It scrubs the SHADOW ONLY. It used to scrub `scale: 1.015 → 0.98` as well, which resized the navbar by 3.5% of
|
|
792
|
+
its own width on every scroll — a visible jump on each landing, and blurred nav text for the whole animation.
|
|
793
|
+
A navbar is furniture: it may deepen, it must never change size. */
|
|
794
|
+
@keyframes mu-nav-scroll { from { box-shadow: 0 4px 12px -6px rgb(0 0 0 / 0.10); } to { box-shadow: 0 16px 36px -10px rgb(0 0 0 / 0.30); } }
|
|
759
795
|
@keyframes mu-float { 0%, 100% { translate: 0 0; } 50% { translate: 0 -6px; } }
|
|
760
796
|
|
|
761
797
|
@layer utilities {
|
package/motion.css
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/* @muten/shadcn/motion.css — a small, tasteful set of entrance + hover animations, as CLASSES the AI can just
|
|
2
|
+
apply (`class("animate-rise")`). CSS-only, plays once on mount, and fully disabled under reduced-motion.
|
|
3
|
+
Import after globals: @import "@muten/shadcn/globals.css"; @import "@muten/shadcn/motion.css";
|
|
4
|
+
|
|
5
|
+
Vocabulary:
|
|
6
|
+
animate-fade-in — fade in
|
|
7
|
+
animate-rise — fade + slide up (the default for hero copy, cards, sections)
|
|
8
|
+
animate-scale-in — fade + subtle scale (badges, images, pop-ins)
|
|
9
|
+
animate-blur-in — fade + un-blur (editorial hero images / big type)
|
|
10
|
+
stagger — on a CONTAINER: its direct children rise in sequence (feature grids, lists)
|
|
11
|
+
hover-lift — on a card/link: lifts + deepens shadow on hover
|
|
12
|
+
hover-zoom — on an image WRAPPER (add `overflow-hidden`): the <img> scales gently on hover
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
@keyframes mu-fade-in { from { opacity: 0; } to { opacity: 1; } }
|
|
16
|
+
@keyframes mu-rise { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }
|
|
17
|
+
@keyframes mu-scale-in{ from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: none; } }
|
|
18
|
+
@keyframes mu-blur-in { from { opacity: 0; filter: blur(12px); transform: scale(1.02); } to { opacity: 1; filter: blur(0); transform: none; } }
|
|
19
|
+
|
|
20
|
+
.animate-fade-in { animation: mu-fade-in 0.6s var(--mu-ease, cubic-bezier(0.16,1,0.3,1)) both; }
|
|
21
|
+
.animate-rise { animation: mu-rise 0.7s var(--mu-ease, cubic-bezier(0.16,1,0.3,1)) both; }
|
|
22
|
+
.animate-scale-in { animation: mu-scale-in 0.6s var(--mu-ease, cubic-bezier(0.16,1,0.3,1)) both; }
|
|
23
|
+
.animate-blur-in { animation: mu-blur-in 0.9s var(--mu-ease, cubic-bezier(0.16,1,0.3,1)) both; }
|
|
24
|
+
|
|
25
|
+
/* Stagger: children rise in, each ~80ms after the last (up to 12; more just share the last delay). */
|
|
26
|
+
.stagger > * { animation: mu-rise 0.7s var(--mu-ease, cubic-bezier(0.16,1,0.3,1)) both; }
|
|
27
|
+
.stagger > *:nth-child(1) { animation-delay: 0.00s; }
|
|
28
|
+
.stagger > *:nth-child(2) { animation-delay: 0.08s; }
|
|
29
|
+
.stagger > *:nth-child(3) { animation-delay: 0.16s; }
|
|
30
|
+
.stagger > *:nth-child(4) { animation-delay: 0.24s; }
|
|
31
|
+
.stagger > *:nth-child(5) { animation-delay: 0.32s; }
|
|
32
|
+
.stagger > *:nth-child(6) { animation-delay: 0.40s; }
|
|
33
|
+
.stagger > *:nth-child(7) { animation-delay: 0.48s; }
|
|
34
|
+
.stagger > *:nth-child(8) { animation-delay: 0.56s; }
|
|
35
|
+
.stagger > *:nth-child(9) { animation-delay: 0.64s; }
|
|
36
|
+
.stagger > *:nth-child(10) { animation-delay: 0.72s; }
|
|
37
|
+
.stagger > *:nth-child(11) { animation-delay: 0.80s; }
|
|
38
|
+
.stagger > *:nth-child(n+12) { animation-delay: 0.88s; }
|
|
39
|
+
|
|
40
|
+
/* Hover affordances. */
|
|
41
|
+
.hover-lift { transition: transform var(--mu-motion, 200ms) var(--mu-ease, cubic-bezier(0.16,1,0.3,1)), box-shadow var(--mu-motion, 200ms) ease; }
|
|
42
|
+
.hover-lift:hover { transform: translateY(-4px); box-shadow: var(--shadow-soft, 0 12px 32px -12px rgb(0 0 0 / 0.25)); }
|
|
43
|
+
.hover-zoom img { transition: transform 0.5s var(--mu-ease, cubic-bezier(0.16,1,0.3,1)); }
|
|
44
|
+
.hover-zoom:hover img { transform: scale(1.05); }
|
|
45
|
+
|
|
46
|
+
@media (prefers-reduced-motion: reduce) {
|
|
47
|
+
.animate-fade-in, .animate-rise, .animate-scale-in, .animate-blur-in, .stagger > * { animation: none !important; }
|
|
48
|
+
.hover-lift, .hover-zoom img { transition: none !important; }
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muten/shadcn",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "shadcn-style component registry for muten - own-the-source parts you add with `muten add`.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -15,10 +15,15 @@
|
|
|
15
15
|
"registry",
|
|
16
16
|
"registry.json",
|
|
17
17
|
"globals.css",
|
|
18
|
-
"
|
|
18
|
+
"motion.css",
|
|
19
|
+
"themes",
|
|
20
|
+
"README.md",
|
|
21
|
+
"AGENTS.md"
|
|
19
22
|
],
|
|
20
23
|
"exports": {
|
|
21
24
|
"./globals.css": "./globals.css",
|
|
25
|
+
"./motion.css": "./motion.css",
|
|
26
|
+
"./themes/*": "./themes/*",
|
|
22
27
|
"./registry.json": "./registry.json",
|
|
23
28
|
"./registry/*": "./registry/*"
|
|
24
29
|
},
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# BentoGallery - a tiling gallery on a fixed row-height grid, so cells of different column widths always line up.
|
|
2
|
+
# Each BentoCell picks its own col-span (and bg / content) via class:
|
|
3
|
+
# BentoGallery {
|
|
4
|
+
# BentoCell class("col-span-4 bg-muted") { Span "01" }
|
|
5
|
+
# BentoCell class("col-span-2 bg-accent") { Span "02" }
|
|
6
|
+
# }
|
|
7
|
+
# shadcn-for-muten · block · no deps · uses .bento-gallery / .bento-cell from globals.css.
|
|
8
|
+
part BentoGallery() { Stack class("bento-gallery") { slot } }
|
|
9
|
+
part BentoCell() { Stack class("bento-cell") { slot } }
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# ButtonGroup - segmented row of buttons that share borders. Put `btn btn-outline` buttons inside:
|
|
2
|
+
# ButtonGroup { Button "Bold" -> b class("btn btn-outline") Button "Italic" -> i class("btn btn-outline") }
|
|
3
|
+
# shadcn-for-muten · part · no deps · uses .btn-group from globals.css.
|
|
4
|
+
part ButtonGroup() { Stack class("btn-group") { slot } }
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Button (part name Btn - `Button` is a muten primitive, so a part can't reuse that name) - a plug-and-play
|
|
2
|
+
# button. No class props: pass a `variant` + your action, put the label/icon in the slot.
|
|
3
|
+
# Btn(variant: "default", onClick: save) { Span "Save" }
|
|
4
|
+
# Btn(variant: "outline", onClick: cancel) { Icon "lucide:x" class("size-4") Span "Cancel" }
|
|
5
|
+
# variant: default | secondary | outline | ghost | destructive | link. For a link that looks like a button use
|
|
6
|
+
# `Link "..." -> "/path" class("btn btn-default")`; for sizes add btn-sm|btn-lg on the primitive.
|
|
7
|
+
# shadcn-for-muten · part · no deps · wraps the Button primitive + the .btn recipe.
|
|
8
|
+
part Btn(variant: text, onClick: action) { Button -> $onClick class("btn btn-{$variant}") { slot } }
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# FeatureRows - a divided list of numbered feature/schedule rows, each with its own side gutters so the index
|
|
2
|
+
# never kisses the edge. A FeatureRow is a 12-col grid (index · title · copy); it stacks on mobile.
|
|
3
|
+
# FeatureRows {
|
|
4
|
+
# FeatureRow { Span "01" class("col-span-1") Title "One command" h3 class("col-span-4") Text "..." class("col-span-7") }
|
|
5
|
+
# }
|
|
6
|
+
# shadcn-for-muten · block · no deps · uses .feature-rows / .feature-row from globals.css.
|
|
7
|
+
part FeatureRows() { Stack class("feature-rows") { slot } }
|
|
8
|
+
part FeatureRow() { Stack class("feature-row") { slot } }
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# InputGroup - an input with a leading icon / addon in one bordered field:
|
|
2
|
+
# InputGroup { Icon "lucide:search" SearchField bind(q) "Search..." class("input-group-control") }
|
|
3
|
+
# shadcn-for-muten · part · no deps · uses .input-group from globals.css.
|
|
4
|
+
part InputGroup() { Stack class("input-group") { slot } }
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Input (part name Input - muten's text-input primitive is `SearchField`, so this wraps it) - a plug-and-play
|
|
2
|
+
# text input. No class props: pass a state to `value` (two-way bound) + a placeholder.
|
|
3
|
+
# state { email = "" : text }
|
|
4
|
+
# Input(value: email, placeholder: "you@example.com")
|
|
5
|
+
# For typed fields (email/number/password/date) or validation, use a `Form` over an entity instead.
|
|
6
|
+
# shadcn-for-muten · part · no deps · wraps SearchField + the .input recipe.
|
|
7
|
+
part Input(value: text, placeholder: text) { SearchField bind($value) "{$placeholder}" class("input") }
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# LinkBtn - a navigation link that looks like a button (plug-and-play, no class props). For CTAs that go to a
|
|
2
|
+
# route. Put the label/icon in the slot.
|
|
3
|
+
# LinkBtn(to: "/signup", variant: "default") { Span "Get started" }
|
|
4
|
+
# LinkBtn(to: "/docs", variant: "outline") { Span "Read the docs" Icon "lucide:arrow-right" class("size-4") }
|
|
5
|
+
# variant: default | secondary | outline | ghost | destructive | link.
|
|
6
|
+
# shadcn-for-muten · part · no deps · wraps the Link primitive + the .btn recipe.
|
|
7
|
+
part LinkBtn(to: text, variant: text) { Link -> "{$to}" class("btn btn-{$variant}") { slot } }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# NavPill - the floating glass navbar, batteries included: a sticky + padded wrapper, the glass pill, AND the
|
|
2
|
+
# scroll animation (settles + returns to rest at the top). Just drop your logo / links / CTA inside; it lays them
|
|
3
|
+
# out with space-between (logo left, the rest right).
|
|
4
|
+
# NavPill {
|
|
5
|
+
# Stack class("flex flex-row items-center gap-2") { Icon "lucide:hexagon" class("size-5 text-primary") Span "Acme" class("font-bold") }
|
|
6
|
+
# Stack class("flex flex-row items-center gap-6 text-sm text-muted-foreground max-md:hidden") { Link "Features" -> "/" class("link-underline") Link "Pricing" -> "/" class("link-underline") }
|
|
7
|
+
# Link "Get started" -> "/" class("btn btn-default btn-sm press")
|
|
8
|
+
# }
|
|
9
|
+
# shadcn-for-muten · part · no deps · uses .nav-pill + nav-scroll from globals.css.
|
|
10
|
+
part NavPill() { Stack class("sticky top-0 z-50 px-4 pt-4") { Header class("nav-pill nav-scroll mx-auto w-full max-w-5xl") { slot } } }
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# Skeleton - a loading placeholder. Add the size utilities via class (paramless part, so class() works):
|
|
2
|
+
# Skeleton class("h-4 w-32") · Skeleton class("size-10 rounded-full")
|
|
3
|
+
# shadcn-for-muten · part · no deps · uses .skeleton (pulse) from globals.css.
|
|
4
|
+
part Skeleton() { Stack class("skeleton") { slot } }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# StatBand - a divided row of stats (KPIs), uniform + responsive (2-up on mobile). Each Stat holds its value in
|
|
2
|
+
# the slot (a Span, or a `Custom CountUp` for the count-up animation) plus a label:
|
|
3
|
+
# StatBand {
|
|
4
|
+
# Stat(label: "trees planted") { Custom CountUp inputs(to: "2.4M") class("stat-value") }
|
|
5
|
+
# Stat(label: "countries") { Span "58" class("stat-value") }
|
|
6
|
+
# }
|
|
7
|
+
# shadcn-for-muten · block · no deps · uses .stat-band / .stat / .stat-value / .stat-label from globals.css.
|
|
8
|
+
part StatBand() { Stack class("stat-band") { slot } }
|
|
9
|
+
part Stat(label: text) { Stack class("stat") { slot Span "{$label}" class("stat-label") } }
|
package/registry.json
CHANGED
|
@@ -94,13 +94,6 @@
|
|
|
94
94
|
"description": "A bordered row: ItemContent / ItemTitle / ItemDescription.",
|
|
95
95
|
"deps": []
|
|
96
96
|
},
|
|
97
|
-
{
|
|
98
|
-
"name": "checkbox",
|
|
99
|
-
"part": "Checkbox",
|
|
100
|
-
"file": "registry/checkbox.muten",
|
|
101
|
-
"description": "A checkbox; the page owns the bool + the toggle action.",
|
|
102
|
-
"deps": []
|
|
103
|
-
},
|
|
104
97
|
{
|
|
105
98
|
"name": "toggle",
|
|
106
99
|
"part": "Toggle",
|
|
@@ -199,13 +192,6 @@
|
|
|
199
192
|
"description": "Richer content shown on hover (pure CSS, no state).",
|
|
200
193
|
"deps": []
|
|
201
194
|
},
|
|
202
|
-
{
|
|
203
|
-
"name": "select",
|
|
204
|
-
"part": "Select",
|
|
205
|
-
"file": "registry/select.muten",
|
|
206
|
-
"description": "A value picker with SelectItem; page owns the value text + open bool.",
|
|
207
|
-
"deps": []
|
|
208
|
-
},
|
|
209
195
|
{
|
|
210
196
|
"name": "scroll-area",
|
|
211
197
|
"part": "ScrollArea",
|
|
@@ -302,14 +288,6 @@
|
|
|
302
288
|
"description": "A slide carousel over a list (Custom). Page owns the slides list.",
|
|
303
289
|
"deps": []
|
|
304
290
|
},
|
|
305
|
-
{
|
|
306
|
-
"name": "chart",
|
|
307
|
-
"part": "Chart",
|
|
308
|
-
"file": "registry/chart.muten",
|
|
309
|
-
"component": "Chart",
|
|
310
|
-
"description": "An SVG bar chart over a list of { label, value } (Custom).",
|
|
311
|
-
"deps": []
|
|
312
|
-
},
|
|
313
291
|
{
|
|
314
292
|
"name": "gradient-blinds",
|
|
315
293
|
"part": "GradientBlinds",
|
|
@@ -545,22 +523,90 @@
|
|
|
545
523
|
"component": "CountUp",
|
|
546
524
|
"description": "Animate a formatted number (2.4M / 94% / 40ms / 99.99%) from 0 when it scrolls into view; lands on the exact target. Custom. Styled form (takes the class): `Custom CountUp inputs(to: \"2.4M\") class(\"text-4xl font-bold\")`. For stat bands + hero metrics.",
|
|
547
525
|
"deps": []
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
"name": "navpill",
|
|
529
|
+
"part": "NavPill",
|
|
530
|
+
"file": "registry/navpill.muten",
|
|
531
|
+
"description": "BLOCK: the floating glass navbar (sticky wrapper + glass pill + scroll animation baked in). Drop logo/links/CTA inside.",
|
|
532
|
+
"deps": []
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
"name": "stat-band",
|
|
536
|
+
"part": "StatBand",
|
|
537
|
+
"file": "registry/stat-band.muten",
|
|
538
|
+
"description": "BLOCK: a divided row of KPI stats. Parts: StatBand, Stat(label) - value goes in the slot (Span or Custom CountUp).",
|
|
539
|
+
"deps": []
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
"name": "feature-rows",
|
|
543
|
+
"part": "FeatureRows",
|
|
544
|
+
"file": "registry/feature-rows.muten",
|
|
545
|
+
"description": "BLOCK: numbered feature/schedule rows with side gutters (12-col grid inside). Parts: FeatureRows, FeatureRow.",
|
|
546
|
+
"deps": []
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
"name": "bento",
|
|
550
|
+
"part": "BentoGallery",
|
|
551
|
+
"file": "registry/bento.muten",
|
|
552
|
+
"description": "BLOCK: fixed-row-height tiling gallery; cells pick their col-span. Parts: BentoGallery, BentoCell.",
|
|
553
|
+
"deps": []
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
"name": "button-group",
|
|
557
|
+
"part": "ButtonGroup",
|
|
558
|
+
"file": "registry/button-group.muten",
|
|
559
|
+
"description": "A segmented row of buttons that share borders. Put btn-outline buttons inside.",
|
|
560
|
+
"deps": []
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
"name": "input-group",
|
|
564
|
+
"part": "InputGroup",
|
|
565
|
+
"file": "registry/input-group.muten",
|
|
566
|
+
"description": "An input with a leading icon/addon in one bordered field.",
|
|
567
|
+
"deps": []
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
"name": "skeleton",
|
|
571
|
+
"part": "Skeleton",
|
|
572
|
+
"file": "registry/skeleton.muten",
|
|
573
|
+
"description": "A loading placeholder (pulse). Add size via class: Skeleton class(\"h-4 w-32\").",
|
|
574
|
+
"deps": []
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
"name": "kbd",
|
|
578
|
+
"part": "Kbd",
|
|
579
|
+
"file": "registry/kbd.muten",
|
|
580
|
+
"description": "A keyboard key hint. Kbd { Span \"Ctrl\" }.",
|
|
581
|
+
"deps": []
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
"name": "button",
|
|
585
|
+
"part": "Btn",
|
|
586
|
+
"file": "registry/button.muten",
|
|
587
|
+
"description": "Plug-and-play button (part Btn - Button is a primitive). `Btn(variant: \"default\", onClick: save) { Span \"Save\" }`. variant: default|secondary|outline|ghost|destructive|link.",
|
|
588
|
+
"deps": []
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
"name": "input",
|
|
592
|
+
"part": "Input",
|
|
593
|
+
"file": "registry/input.muten",
|
|
594
|
+
"description": "Plug-and-play text input (part Input wraps SearchField). `Input(value: q, placeholder: \"...\")` - value is two-way bound. For typed/validated fields use a Form.",
|
|
595
|
+
"deps": []
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
"name": "link-btn",
|
|
599
|
+
"part": "LinkBtn",
|
|
600
|
+
"file": "registry/link-btn.muten",
|
|
601
|
+
"description": "A navigation link styled as a button. `LinkBtn(to: \"/signup\", variant: \"default\") { Span \"Get started\" }`.",
|
|
602
|
+
"deps": []
|
|
548
603
|
}
|
|
549
604
|
],
|
|
550
605
|
"classComponents": {
|
|
551
606
|
"note": "Size/spacing-driven primitives ship as classes in globals.css (no `muten add` needed once you @import it). Style the muten primitive with these:",
|
|
552
|
-
"button": "Button \"Save\" -> save class(\"btn btn-default\") (variants: btn-default|btn-secondary|btn-outline|btn-ghost|btn-destructive|btn-link · sizes: btn-sm|btn-lg|btn-icon)",
|
|
553
|
-
"input": "SearchField bind(q) class(\"input\")",
|
|
554
607
|
"textarea": "class(\"textarea\") · label: class(\"label\") · native select: class(\"native-select\")",
|
|
555
|
-
"skeleton": "Stack class(\"skeleton h-4 w-32\") (add the size utilities)",
|
|
556
|
-
"kbd": "Span \"Ctrl\" class(\"kbd\")",
|
|
557
608
|
"typography": "Title \"Docs\" h1 class(\"prose-h1\") (prose-h1..h4 · prose-p|lead|large|small|muted|blockquote|code|list)",
|
|
558
|
-
"
|
|
559
|
-
"input-group": "Stack class(\"input-group\") { Icon \"lucide:search\" SearchField bind(q) class(\"input-group-control\") }",
|
|
560
|
-
"nav-pill": "Header class(\"nav-pill\") - the floating glass navbar; wrap in Stack class(\"sticky top-0 z-50 px-4 pt-4\"). Add `nav-scroll` so it settles + deepens its shadow on scroll and returns to rest at the top.",
|
|
561
|
-
"feature-rows": "Stack class(\"feature-rows\") { Stack class(\"feature-row\") { Span \"01\" ... } } - numbered feature/schedule rows with baked side gutters (grid-cols-12 inside, stacks on mobile).",
|
|
562
|
-
"bento-gallery": "Stack class(\"bento-gallery\") { Stack class(\"bento-cell col-span-4\") {} Stack class(\"bento-cell col-span-2\") {} } - a fixed-row-height tiling gallery; children pick their col-span so every row lines up.",
|
|
563
|
-
"logo-cloud": "LogoCloud { LogoCloudItem { Span \"Acme\" } LogoCloudItem { Icon \"lucide:hexagon\" Span \"Northwind\" } } - a 'trusted by' strip, muted+grayscale until hover; accepts any logo type (image / icon / wordmark).",
|
|
609
|
+
"table": "DataTable @rows columns(name, email, status) - STYLED BY DEFAULT (no class needed). `class(\"data-table\")` still works as an explicit alias. Raw cells; for badge/formatted cells build with each + Stack.",
|
|
564
610
|
"motion": "Page animations, all CSS + reduced-motion safe. Entrance: animate-fade-up|fade-in|scale-in|slide-in (+ d1..d6 stagger). Scroll: reveal|reveal-left|reveal-scale, and nav-scroll on a navbar. Hover/press: hover-lift, hover-glow, arrow-hover (nudges a trailing Icon), link-underline, press, float."
|
|
565
611
|
}
|
|
566
612
|
}
|
package/themes/bloom.css
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/* @muten/shadcn/themes/bloom.css — "Bloom": soft, friendly, rounded. A rounded display (Quicksand) + Inter, gentle
|
|
2
|
+
pastel tints (lavender paper, violet accent), large radius, airy soft shadows. Import AFTER globals.css.
|
|
3
|
+
@import "@muten/shadcn/globals.css"; @import "@muten/shadcn/themes/bloom.css"; */
|
|
4
|
+
|
|
5
|
+
@import url("https://fonts.googleapis.com/css2?family=Quicksand:wght@500;600;700&family=Inter:wght@400;500;600&display=swap");
|
|
6
|
+
@theme {
|
|
7
|
+
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
|
8
|
+
--font-serif: "Quicksand", ui-rounded, ui-sans-serif, system-ui, sans-serif; /* `font-serif` on headings → rounded display */
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
:root {
|
|
12
|
+
--radius: 1rem; /* pillowy */
|
|
13
|
+
--background: oklch(0.99 0.008 310);
|
|
14
|
+
--foreground: oklch(0.30 0.04 300);
|
|
15
|
+
--card: oklch(1 0.004 310);
|
|
16
|
+
--card-foreground: oklch(0.30 0.04 300);
|
|
17
|
+
--popover: oklch(1 0.004 310);
|
|
18
|
+
--popover-foreground: oklch(0.30 0.04 300);
|
|
19
|
+
--primary: oklch(0.62 0.19 300); /* soft violet */
|
|
20
|
+
--primary-foreground: oklch(0.99 0.008 310);
|
|
21
|
+
--secondary: oklch(0.95 0.03 310);
|
|
22
|
+
--secondary-foreground: oklch(0.40 0.06 300);
|
|
23
|
+
--muted: oklch(0.955 0.022 315);
|
|
24
|
+
--muted-foreground: oklch(0.55 0.05 300);
|
|
25
|
+
--accent: oklch(0.93 0.04 340); /* rose tint */
|
|
26
|
+
--accent-foreground: oklch(0.40 0.08 340);
|
|
27
|
+
--destructive: oklch(0.60 0.20 20);
|
|
28
|
+
--border: oklch(0.91 0.025 315);
|
|
29
|
+
--input: oklch(0.91 0.025 315);
|
|
30
|
+
--ring: oklch(0.62 0.19 300);
|
|
31
|
+
}
|
|
32
|
+
/* bridge both selectors: muten sets `data-theme="dark"`, shadcn keys off the `.dark` class (globals.css does the same). */
|
|
33
|
+
.dark, [data-theme="dark"] {
|
|
34
|
+
--background: oklch(0.21 0.03 300);
|
|
35
|
+
--foreground: oklch(0.95 0.02 315);
|
|
36
|
+
--card: oklch(0.25 0.035 300);
|
|
37
|
+
--card-foreground: oklch(0.95 0.02 315);
|
|
38
|
+
--popover: oklch(0.25 0.035 300);
|
|
39
|
+
--popover-foreground: oklch(0.95 0.02 315);
|
|
40
|
+
--primary: oklch(0.72 0.17 300);
|
|
41
|
+
--primary-foreground: oklch(0.20 0.03 300);
|
|
42
|
+
--secondary: oklch(0.30 0.035 300);
|
|
43
|
+
--secondary-foreground: oklch(0.95 0.02 315);
|
|
44
|
+
--muted: oklch(0.30 0.035 300);
|
|
45
|
+
--muted-foreground: oklch(0.75 0.03 310);
|
|
46
|
+
--accent: oklch(0.34 0.05 340);
|
|
47
|
+
--accent-foreground: oklch(0.95 0.02 315);
|
|
48
|
+
--destructive: oklch(0.68 0.18 20);
|
|
49
|
+
--border: oklch(0.33 0.035 300);
|
|
50
|
+
--input: oklch(0.35 0.035 300);
|
|
51
|
+
--ring: oklch(0.72 0.17 300);
|
|
52
|
+
}
|
|
53
|
+
@theme { --shadow-soft: 0 2px 8px oklch(0.5 0.1 300 / 0.06), 0 16px 40px -16px oklch(0.5 0.15 300 / 0.20); }
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/* @muten/shadcn/themes/editorial.css — "Editorial": a warm, quiet, luxury look. A display serif (Fraunces) + a
|
|
2
|
+
clean sans (Inter), sand/stone neutrals with a terracotta accent, generous radius, soft warm shadows.
|
|
3
|
+
Import AFTER globals.css: @import "@muten/shadcn/globals.css"; @import "@muten/shadcn/themes/editorial.css";
|
|
4
|
+
Works in light (default) and dark. `font-serif` now renders Fraunces, so headings feel editorial for free. */
|
|
5
|
+
|
|
6
|
+
@import url("https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600;9..144,700&family=Inter:wght@400;500;600&display=swap");
|
|
7
|
+
|
|
8
|
+
/* Tailwind picks these up: `font-sans` → Inter, `font-serif` → Fraunces (use it on headings). */
|
|
9
|
+
@theme {
|
|
10
|
+
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
|
11
|
+
--font-serif: "Fraunces", ui-serif, Georgia, "Times New Roman", serif;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* LIGHT — warm sand paper, deep-ink text, terracotta as the single accent (primary = buttons + `text-primary`). */
|
|
15
|
+
:root {
|
|
16
|
+
--radius: 0.625rem;
|
|
17
|
+
--background: oklch(0.985 0.007 83);
|
|
18
|
+
--foreground: oklch(0.24 0.017 55);
|
|
19
|
+
--card: oklch(0.997 0.004 83);
|
|
20
|
+
--card-foreground: oklch(0.24 0.017 55);
|
|
21
|
+
--popover: oklch(0.997 0.004 83);
|
|
22
|
+
--popover-foreground: oklch(0.24 0.017 55);
|
|
23
|
+
--primary: oklch(0.62 0.115 46); /* terracotta */
|
|
24
|
+
--primary-foreground: oklch(0.99 0.006 83);
|
|
25
|
+
--secondary: oklch(0.955 0.011 80);
|
|
26
|
+
--secondary-foreground: oklch(0.30 0.02 50);
|
|
27
|
+
--muted: oklch(0.955 0.011 80);
|
|
28
|
+
--muted-foreground: oklch(0.52 0.018 60);
|
|
29
|
+
--accent: oklch(0.94 0.016 74); /* subtle warm hover bg (stays neutral, per shadcn) */
|
|
30
|
+
--accent-foreground: oklch(0.30 0.02 50);
|
|
31
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
32
|
+
--border: oklch(0.905 0.012 76);
|
|
33
|
+
--input: oklch(0.905 0.012 76);
|
|
34
|
+
--ring: oklch(0.62 0.115 46);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* DARK — warm near-black, cream text, terracotta glows. */
|
|
38
|
+
/* bridge both selectors: muten sets `data-theme="dark"`, shadcn keys off the `.dark` class (globals.css does the same). */
|
|
39
|
+
.dark, [data-theme="dark"] {
|
|
40
|
+
--background: oklch(0.195 0.012 58);
|
|
41
|
+
--foreground: oklch(0.955 0.008 83);
|
|
42
|
+
--card: oklch(0.235 0.014 58);
|
|
43
|
+
--card-foreground: oklch(0.955 0.008 83);
|
|
44
|
+
--popover: oklch(0.235 0.014 58);
|
|
45
|
+
--popover-foreground: oklch(0.955 0.008 83);
|
|
46
|
+
--primary: oklch(0.70 0.128 47); /* terracotta, brighter for dark */
|
|
47
|
+
--primary-foreground: oklch(0.20 0.012 58);
|
|
48
|
+
--secondary: oklch(0.28 0.014 58);
|
|
49
|
+
--secondary-foreground: oklch(0.955 0.008 83);
|
|
50
|
+
--muted: oklch(0.28 0.014 58);
|
|
51
|
+
--muted-foreground: oklch(0.72 0.013 72);
|
|
52
|
+
--accent: oklch(0.30 0.015 58);
|
|
53
|
+
--accent-foreground: oklch(0.955 0.008 83);
|
|
54
|
+
--destructive: oklch(0.65 0.19 25);
|
|
55
|
+
--border: oklch(0.31 0.014 58);
|
|
56
|
+
--input: oklch(0.33 0.014 58);
|
|
57
|
+
--ring: oklch(0.70 0.128 47);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Editorial polish that the token vars alone can't express. */
|
|
61
|
+
body { font-feature-settings: "liga" 1, "calt" 1; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
|
|
62
|
+
/* Fraunces headings: a touch of optical weight + tight tracking reads as "designed", not default-serif. */
|
|
63
|
+
h1, h2, h3, .font-serif { letter-spacing: -0.01em; }
|
|
64
|
+
/* soft, warm-tinted elevation (shadcn ships flat) — reach for it on cards/hero via `shadow-soft`. */
|
|
65
|
+
@theme { --shadow-soft: 0 1px 2px oklch(0.4 0.03 60 / 0.04), 0 8px 24px -12px oklch(0.4 0.05 60 / 0.14); }
|
package/themes/noir.css
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/* @muten/shadcn/themes/noir.css — "Noir": crisp, high-contrast, mono-minimal. A grotesk display (Space Grotesk) +
|
|
2
|
+
Inter, near-monochrome cool neutrals, sharp corners, one stark ink accent. Import AFTER globals.css.
|
|
3
|
+
@import "@muten/shadcn/globals.css"; @import "@muten/shadcn/themes/noir.css"; */
|
|
4
|
+
|
|
5
|
+
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Inter:wght@400;500;600&display=swap");
|
|
6
|
+
@theme {
|
|
7
|
+
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
|
8
|
+
--font-serif: "Space Grotesk", ui-sans-serif, system-ui, sans-serif; /* `font-serif` on headings → grotesk display */
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
:root {
|
|
12
|
+
--radius: 0.25rem; /* sharp */
|
|
13
|
+
--background: oklch(0.995 0 0);
|
|
14
|
+
--foreground: oklch(0.16 0.004 260);
|
|
15
|
+
--card: oklch(1 0 0);
|
|
16
|
+
--card-foreground: oklch(0.16 0.004 260);
|
|
17
|
+
--popover: oklch(1 0 0);
|
|
18
|
+
--popover-foreground: oklch(0.16 0.004 260);
|
|
19
|
+
--primary: oklch(0.16 0.004 260); /* ink */
|
|
20
|
+
--primary-foreground: oklch(0.99 0 0);
|
|
21
|
+
--secondary: oklch(0.965 0.002 260);
|
|
22
|
+
--secondary-foreground: oklch(0.16 0.004 260);
|
|
23
|
+
--muted: oklch(0.965 0.002 260);
|
|
24
|
+
--muted-foreground: oklch(0.48 0.006 260);
|
|
25
|
+
--accent: oklch(0.95 0.003 260);
|
|
26
|
+
--accent-foreground: oklch(0.16 0.004 260);
|
|
27
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
28
|
+
--border: oklch(0.90 0.004 260);
|
|
29
|
+
--input: oklch(0.90 0.004 260);
|
|
30
|
+
--ring: oklch(0.16 0.004 260);
|
|
31
|
+
}
|
|
32
|
+
/* bridge both selectors: muten sets `data-theme="dark"`, shadcn keys off the `.dark` class (globals.css does the same). */
|
|
33
|
+
.dark, [data-theme="dark"] {
|
|
34
|
+
--background: oklch(0.145 0.004 260);
|
|
35
|
+
--foreground: oklch(0.98 0 0);
|
|
36
|
+
--card: oklch(0.185 0.005 260);
|
|
37
|
+
--card-foreground: oklch(0.98 0 0);
|
|
38
|
+
--popover: oklch(0.185 0.005 260);
|
|
39
|
+
--popover-foreground: oklch(0.98 0 0);
|
|
40
|
+
--primary: oklch(0.98 0 0); /* stark white on black */
|
|
41
|
+
--primary-foreground: oklch(0.16 0.004 260);
|
|
42
|
+
--secondary: oklch(0.24 0.006 260);
|
|
43
|
+
--secondary-foreground: oklch(0.98 0 0);
|
|
44
|
+
--muted: oklch(0.24 0.006 260);
|
|
45
|
+
--muted-foreground: oklch(0.68 0.006 260);
|
|
46
|
+
--accent: oklch(0.26 0.006 260);
|
|
47
|
+
--accent-foreground: oklch(0.98 0 0);
|
|
48
|
+
--destructive: oklch(0.65 0.19 25);
|
|
49
|
+
--border: oklch(0.27 0.006 260);
|
|
50
|
+
--input: oklch(0.30 0.006 260);
|
|
51
|
+
--ring: oklch(0.98 0 0);
|
|
52
|
+
}
|
|
53
|
+
.font-serif, h1, h2, h3 { letter-spacing: -0.02em; }
|
|
54
|
+
@theme { --shadow-soft: 0 1px 2px oklch(0 0 0 / 0.05), 0 12px 28px -14px oklch(0 0 0 / 0.18); }
|
package/registry/chart.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
// Chart host component (muten Custom). inputs: { data } (a list of { label, value }). Renders an SVG bar chart.
|
|
2
|
-
// Edit this file for line/area/pie variants; this default is a clean bar chart driven by the page's data.
|
|
3
|
-
export function mount(el, inputs, handlers) {
|
|
4
|
-
const NS = 'http://www.w3.org/2000/svg';
|
|
5
|
-
const render = (data) => {
|
|
6
|
-
el.innerHTML = '';
|
|
7
|
-
const items = Array.isArray(data) ? data : [];
|
|
8
|
-
const W = 480, H = 200, pad = 24, gap = 10;
|
|
9
|
-
const max = Math.max(1, ...items.map((d) => Number(d.value) || 0));
|
|
10
|
-
const bw = items.length ? (W - pad * 2 - gap * (items.length - 1)) / items.length : 0;
|
|
11
|
-
const svg = document.createElementNS(NS, 'svg');
|
|
12
|
-
svg.setAttribute('viewBox', '0 0 ' + W + ' ' + H);
|
|
13
|
-
svg.setAttribute('class', 'chart-svg');
|
|
14
|
-
items.forEach((d, i) => {
|
|
15
|
-
const v = Number(d.value) || 0;
|
|
16
|
-
const bh = (v / max) * (H - pad * 2);
|
|
17
|
-
const x = pad + i * (bw + gap);
|
|
18
|
-
const y = H - pad - bh;
|
|
19
|
-
const rect = document.createElementNS(NS, 'rect');
|
|
20
|
-
rect.setAttribute('x', x); rect.setAttribute('y', y);
|
|
21
|
-
rect.setAttribute('width', bw); rect.setAttribute('height', bh);
|
|
22
|
-
rect.setAttribute('rx', 4); rect.setAttribute('class', 'chart-bar');
|
|
23
|
-
svg.appendChild(rect);
|
|
24
|
-
const t = document.createElementNS(NS, 'text');
|
|
25
|
-
t.setAttribute('x', x + bw / 2); t.setAttribute('y', H - pad + 14);
|
|
26
|
-
t.setAttribute('text-anchor', 'middle'); t.setAttribute('class', 'chart-label');
|
|
27
|
-
t.textContent = d.label == null ? '' : String(d.label);
|
|
28
|
-
svg.appendChild(t);
|
|
29
|
-
});
|
|
30
|
-
el.appendChild(svg);
|
|
31
|
-
};
|
|
32
|
-
render(inputs.data);
|
|
33
|
-
return (n) => render(n.data);
|
|
34
|
-
}
|
package/registry/chart.muten
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
# Chart - an SVG bar chart over a list of { label, value }. The PAGE owns the data:
|
|
2
|
-
# entity Metric { label text value number } state { data = query data : list<Metric> } mock { data: [ … ] }
|
|
3
|
-
# Chart(data: data)
|
|
4
|
-
# shadcn-for-muten · Custom · host JS is src/components/Chart.js (edit it for line/area/pie variants).
|
|
5
|
-
part Chart(data: list) {
|
|
6
|
-
Custom Chart inputs(data: $data) class("chart")
|
|
7
|
-
}
|
package/registry/checkbox.muten
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
# Checkbox - the PAGE owns the bool + the action that flips it (container/presentational):
|
|
2
|
-
# state { agree = false : bool } action toggle mutates agree { agree.toggle() } Checkbox(checked: agree, onToggle: toggle)
|
|
3
|
-
# shadcn-for-muten · part · no deps · the check always renders and scales/fades in via .checkbox-icon-on.
|
|
4
|
-
part Checkbox(checked: bool, onToggle: action) {
|
|
5
|
-
Button -> $onToggle aria(checked: $checked) class("checkbox", "checkbox-checked" when $checked) {
|
|
6
|
-
Icon "lucide:check" class("checkbox-icon", "checkbox-icon-on" when $checked)
|
|
7
|
-
}
|
|
8
|
-
}
|
package/registry/select.muten
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# Select - a dropdown that shows the chosen value. The PAGE owns the value (text) + the open bool + actions:
|
|
2
|
-
# state { fruit = "Apple" : text open = false : bool } action setFruit(v: text) mutates fruit { fruit.set(v) … }
|
|
3
|
-
# Select(value: fruit, open: open, onToggle: t) { SelectItem(value: "Apple", current: fruit, label: "Apple", onSelect: setFruit) … }
|
|
4
|
-
# Each SelectItem's action should set the value AND close. The backdrop catches outside clicks.
|
|
5
|
-
# shadcn-for-muten · part · no deps · styles via the semantic .select* classes in globals.css.
|
|
6
|
-
part Select(value: text, open: bool, onToggle: action) {
|
|
7
|
-
Stack class("select-wrap") {
|
|
8
|
-
Button -> $onToggle aria(expanded: $open) class("select-trigger") {
|
|
9
|
-
Span "{$value}"
|
|
10
|
-
Icon "lucide:chevrons-up-down" class("size-4 opacity-50")
|
|
11
|
-
}
|
|
12
|
-
when $open { Button -> $onToggle class("select-backdrop") }
|
|
13
|
-
Stack class("select-content", "select-content-open" when $open) { slot }
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
part SelectItem(value: text, current: text, label: text, onSelect: action) {
|
|
17
|
-
Button -> $onSelect($value) class("select-item") {
|
|
18
|
-
Span "{$label}"
|
|
19
|
-
when $value == $current { Icon "lucide:check" class("size-4") }
|
|
20
|
-
}
|
|
21
|
-
}
|