@muten/shadcn 0.0.3 → 0.0.5
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 +108 -0
- package/globals.css +395 -90
- package/package.json +3 -2
- package/registry/bento.muten +9 -0
- package/registry/button-group.muten +4 -0
- package/registry/button.muten +8 -0
- package/registry/code.js +14 -0
- package/registry/code.muten +7 -0
- package/registry/count-up.js +36 -0
- package/registry/count-up.muten +10 -0
- package/registry/feature-grid.muten +14 -0
- package/registry/feature-rows.muten +8 -0
- package/registry/gradient-blinds.js +129 -0
- package/registry/gradient-blinds.muten +13 -0
- package/registry/highlight.js +47 -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/logo-cloud.muten +11 -0
- package/registry/navpill.muten +10 -0
- package/registry/pill.muten +5 -0
- package/registry/pricing.muten +25 -0
- package/registry/segmented-control.muten +6 -0
- package/registry/sidebar.muten +5 -4
- package/registry/skeleton.muten +4 -0
- package/registry/stat-band.muten +9 -0
- package/registry/stepper.muten +17 -0
- package/registry/timeline.muten +16 -0
- package/registry.json +622 -71
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muten/shadcn",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
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,7 +15,8 @@
|
|
|
15
15
|
"registry",
|
|
16
16
|
"registry.json",
|
|
17
17
|
"globals.css",
|
|
18
|
-
"README.md"
|
|
18
|
+
"README.md",
|
|
19
|
+
"AGENTS.md"
|
|
19
20
|
],
|
|
20
21
|
"exports": {
|
|
21
22
|
"./globals.css": "./globals.css",
|
|
@@ -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 } }
|
package/registry/code.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Code block host (muten Custom). inputs: { code, lang }. Highlights with the muten grammar via the shared
|
|
2
|
+
// highlight.js (lazy-loaded, so the token rules live in one place). See code.muten.
|
|
3
|
+
export function mount(el, inputs) {
|
|
4
|
+
const code = String(inputs.code == null ? "" : inputs.code);
|
|
5
|
+
const lang = inputs.lang == null ? "muten" : String(inputs.lang);
|
|
6
|
+
el.className = "rounded-lg border bg-muted/40 px-4 py-3 font-mono text-foreground overflow-x-auto";
|
|
7
|
+
const pre = document.createElement("pre");
|
|
8
|
+
pre.className = "m-0 text-sm leading-relaxed";
|
|
9
|
+
pre.textContent = code; // plain until the highlighter loads (progressive)
|
|
10
|
+
el.appendChild(pre);
|
|
11
|
+
import("@muten/shadcn/registry/highlight.js")
|
|
12
|
+
.then((m) => { pre.innerHTML = m.highlight(code, lang); })
|
|
13
|
+
.catch(() => { /* keep the plain text */ });
|
|
14
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Code - a syntax-highlighted code block. `lang` defaults to "muten" (the muten grammar, matching the VS Code
|
|
2
|
+
# extension); pass another lang to render plain. NOTE: muten strings can't hold `{` or `"`, so feed muten source
|
|
3
|
+
# from a .md file (a Markdown component) or a .js snippet, not an inline muten string.
|
|
4
|
+
# shadcn-for-muten · Custom · host JS is src/components/Code.js (copied by `muten add code`); shares highlight.js.
|
|
5
|
+
part Code(code: text, lang: text) {
|
|
6
|
+
Custom Code inputs(code: $code, lang: $lang) class("code-block")
|
|
7
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// CountUp - a muten Custom that animates a formatted stat from 0 to its target the first time it scrolls into
|
|
2
|
+
// view. inputs: { to } - the final display string (e.g. "2.4M", "94%", "120K", "58", "99.99%", "40ms", "12B").
|
|
3
|
+
// It splits an optional prefix + number (decimals/commas ok) + suffix, animates the number, then restores the
|
|
4
|
+
// EXACT target string so formatting like "2.4M" is preserved. Pure vanilla JS - this is muten's 20% escape for
|
|
5
|
+
// something CSS can't do (animating a formatted number). Respects prefers-reduced-motion.
|
|
6
|
+
export function mount(el, inputs) {
|
|
7
|
+
const target = String(inputs && inputs.to != null ? inputs.to : '0');
|
|
8
|
+
const m = target.match(/^([^\d]*)([\d.,]+)(.*)$/);
|
|
9
|
+
if (!m) { el.textContent = target; return; }
|
|
10
|
+
const prefix = m[1], numStr = m[2], suffix = m[3];
|
|
11
|
+
const end = parseFloat(numStr.replace(/,/g, ''));
|
|
12
|
+
const decimals = (numStr.split('.')[1] || '').length;
|
|
13
|
+
const grouped = numStr.indexOf(',') !== -1;
|
|
14
|
+
const fmt = (v) => prefix + (decimals ? v.toFixed(decimals) : (grouped ? Math.round(v).toLocaleString() : String(Math.round(v)))) + suffix;
|
|
15
|
+
|
|
16
|
+
const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
17
|
+
if (reduce || !('IntersectionObserver' in window) || !isFinite(end)) { el.textContent = target; return; }
|
|
18
|
+
|
|
19
|
+
el.textContent = fmt(0);
|
|
20
|
+
let started = false;
|
|
21
|
+
const run = () => {
|
|
22
|
+
if (started) return; started = true;
|
|
23
|
+
const dur = 1400, t0 = performance.now();
|
|
24
|
+
const tick = (now) => {
|
|
25
|
+
const p = Math.min(1, (now - t0) / dur);
|
|
26
|
+
const eased = 1 - Math.pow(1 - p, 3); // ease-out cubic
|
|
27
|
+
if (p < 1) { el.textContent = fmt(end * eased); requestAnimationFrame(tick); }
|
|
28
|
+
else el.textContent = target; // land on the exact target (keeps "2.4M"/"99.99%" formatting)
|
|
29
|
+
};
|
|
30
|
+
requestAnimationFrame(tick);
|
|
31
|
+
};
|
|
32
|
+
const io = new IntersectionObserver((entries) => {
|
|
33
|
+
for (const e of entries) if (e.isIntersecting) { run(); io.disconnect(); }
|
|
34
|
+
}, { threshold: 0.5 });
|
|
35
|
+
io.observe(el);
|
|
36
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# CountUp - animate a formatted stat from 0 to its target when it scrolls into view (Custom, keeps the exact
|
|
2
|
+
# formatting: 2.4M / 94% / 40ms / 99.99% / 120K). For a STYLED number use the Custom form directly (it takes the
|
|
3
|
+
# class - a part with params can't), which is what stat bands and hero metrics want:
|
|
4
|
+
# Stack class("items-center gap-1") {
|
|
5
|
+
# Custom CountUp inputs(to: "2.4M") class("text-4xl font-bold text-primary")
|
|
6
|
+
# Span "trees planted" class("text-sm text-muted-foreground")
|
|
7
|
+
# }
|
|
8
|
+
# The `CountUp(to: ...)` part below is a thin convenience for the unstyled/inherit case (wrap it in a styled Stack).
|
|
9
|
+
# shadcn-for-muten · Custom · no deps · host JS: src/components/CountUp.js.
|
|
10
|
+
part CountUp(to: text) { Custom CountUp inputs(to: $to) }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# FeatureGrid - a responsive grid of feature cards. FeatureCard takes title + desc and a slot for the icon:
|
|
2
|
+
# FeatureGrid {
|
|
3
|
+
# FeatureCard(title: "Locatable", desc: "Every screen has a name and a place.") { Icon "lucide:scan-text" class("size-5") }
|
|
4
|
+
# …
|
|
5
|
+
# }
|
|
6
|
+
# shadcn-for-muten · block · no deps · styles via the semantic .feature-* classes in globals.css.
|
|
7
|
+
part FeatureGrid() { Stack class("feature-grid") { slot } }
|
|
8
|
+
part FeatureCard(title: text, desc: text) {
|
|
9
|
+
Stack class("feature-card") {
|
|
10
|
+
Stack class("feature-card-icon") { slot }
|
|
11
|
+
Title "{$title}" h3 class("feature-card-title")
|
|
12
|
+
Text "{$desc}" class("feature-card-desc")
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -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,129 @@
|
|
|
1
|
+
// GradientBlinds - ported from React Bits (reactbits.dev) to a muten Custom. A WebGL fragment shader (via `ogl`)
|
|
2
|
+
// draws diagonal blinds over an animated gradient with a spotlight that FOLLOWS THE MOUSE + grain. Faithful and
|
|
3
|
+
// GPU-fast. A Custom (graphics = the vanilla-JS escape); ogl loads via dynamic import() (a Custom is inlined into
|
|
4
|
+
// an IIFE, so a top-level import is illegal). Inputs mirror the React props: colors, angle, noise, blindCount,
|
|
5
|
+
// blindMinWidth, spotlightRadius/Softness/Opacity, mouseDampening, distort, shineDirection, mirror. Needs `ogl`.
|
|
6
|
+
const MAX_COLORS = 8;
|
|
7
|
+
const hexToRGB = (hex) => {
|
|
8
|
+
const c = String(hex).replace("#", "").padEnd(6, "0");
|
|
9
|
+
return [parseInt(c.slice(0, 2), 16) / 255, parseInt(c.slice(2, 4), 16) / 255, parseInt(c.slice(4, 6), 16) / 255];
|
|
10
|
+
};
|
|
11
|
+
const prepStops = (stops) => {
|
|
12
|
+
const base = (stops && stops.length ? stops : ["#FF9FFC", "#5227FF"]).slice(0, MAX_COLORS);
|
|
13
|
+
if (base.length === 1) base.push(base[0]);
|
|
14
|
+
while (base.length < MAX_COLORS) base.push(base[base.length - 1]);
|
|
15
|
+
return { arr: base.map(hexToRGB), count: Math.max(2, Math.min(MAX_COLORS, stops && stops.length ? stops.length : 2)) };
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function mount(el, inputs) {
|
|
19
|
+
const num = (v, d) => { const n = Number(v); return Number.isFinite(n) ? n : d; };
|
|
20
|
+
const I = inputs || {};
|
|
21
|
+
const colors = (I.colors ? String(I.colors) : "#FF9FFC,#5227FF").split(",").map((s) => s.trim()).filter(Boolean);
|
|
22
|
+
const angle = num(I.angle, -25), noise = num(I.noise, 0.22);
|
|
23
|
+
const blindCount = num(I.blindCount, 16), blindMinWidth = num(I.blindMinWidth, 55);
|
|
24
|
+
const spotRadius = num(I.spotlightRadius, 0.6), spotSoft = num(I.spotlightSoftness, 1), spotOpacity = num(I.spotlightOpacity, 0.75);
|
|
25
|
+
const damp = num(I.mouseDampening, 0.15), distort = num(I.distort, 0);
|
|
26
|
+
const shineFlip = I.shineDirection === "right" ? 1 : 0, mirror = I.mirror ? 1 : 0;
|
|
27
|
+
|
|
28
|
+
import("ogl").then(({ Renderer, Program, Mesh, Triangle }) => {
|
|
29
|
+
const renderer = new Renderer({ dpr: Math.min(2, window.devicePixelRatio || 1), alpha: true, antialias: true });
|
|
30
|
+
const gl = renderer.gl;
|
|
31
|
+
const canvas = gl.canvas;
|
|
32
|
+
canvas.style.cssText = "position:absolute;inset:0;width:100%;height:100%;display:block;-webkit-mask-image:linear-gradient(to bottom,#000 42%,transparent 94%);mask-image:linear-gradient(to bottom,#000 42%,transparent 94%)";
|
|
33
|
+
el.appendChild(canvas);
|
|
34
|
+
|
|
35
|
+
const vertex = "attribute vec2 position;attribute vec2 uv;varying vec2 vUv;void main(){vUv=uv;gl_Position=vec4(position,0.0,1.0);}";
|
|
36
|
+
const fragment = `#ifdef GL_ES
|
|
37
|
+
precision mediump float;
|
|
38
|
+
#endif
|
|
39
|
+
uniform vec3 iResolution; uniform vec2 iMouse; uniform float iTime;
|
|
40
|
+
uniform float uAngle, uNoise, uBlindCount, uSpotlightRadius, uSpotlightSoftness, uSpotlightOpacity, uMirror, uDistort, uShineFlip;
|
|
41
|
+
uniform vec3 uColor0,uColor1,uColor2,uColor3,uColor4,uColor5,uColor6,uColor7; uniform int uColorCount;
|
|
42
|
+
varying vec2 vUv;
|
|
43
|
+
float rand(vec2 co){ return fract(sin(dot(co, vec2(12.9898,78.233))) * 43758.5453); }
|
|
44
|
+
vec2 rotate2D(vec2 p, float a){ float c=cos(a), s=sin(a); return mat2(c,-s,s,c)*p; }
|
|
45
|
+
vec3 getGradientColor(float t){
|
|
46
|
+
float tt=clamp(t,0.0,1.0); int count=uColorCount; if(count<2) count=2;
|
|
47
|
+
float scaled=tt*float(count-1); float seg=floor(scaled); float f=fract(scaled);
|
|
48
|
+
if(seg<1.0) return mix(uColor0,uColor1,f);
|
|
49
|
+
if(seg<2.0 && count>2) return mix(uColor1,uColor2,f);
|
|
50
|
+
if(seg<3.0 && count>3) return mix(uColor2,uColor3,f);
|
|
51
|
+
if(seg<4.0 && count>4) return mix(uColor3,uColor4,f);
|
|
52
|
+
if(seg<5.0 && count>5) return mix(uColor4,uColor5,f);
|
|
53
|
+
if(seg<6.0 && count>6) return mix(uColor5,uColor6,f);
|
|
54
|
+
if(seg<7.0 && count>7) return mix(uColor6,uColor7,f);
|
|
55
|
+
if(count>7) return uColor7; if(count>6) return uColor6; if(count>5) return uColor5;
|
|
56
|
+
if(count>4) return uColor4; if(count>3) return uColor3; if(count>2) return uColor2; return uColor1;
|
|
57
|
+
}
|
|
58
|
+
void mainImage(out vec4 fragColor, in vec2 fragCoord){
|
|
59
|
+
vec2 uv0 = fragCoord.xy / iResolution.xy;
|
|
60
|
+
float aspect = iResolution.x / iResolution.y;
|
|
61
|
+
vec2 p = uv0*2.0-1.0; p.x*=aspect; vec2 pr = rotate2D(p, uAngle); pr.x/=aspect; vec2 uv = pr*0.5+0.5;
|
|
62
|
+
vec2 uvMod = uv;
|
|
63
|
+
if(uDistort>0.0){ float a=uvMod.y*6.0, b=uvMod.x*6.0, w=0.01*uDistort; uvMod.x+=sin(a)*w; uvMod.y+=cos(b)*w; }
|
|
64
|
+
float t = uvMod.x; if(uMirror>0.5){ t = 1.0 - abs(1.0 - 2.0*fract(t)); }
|
|
65
|
+
vec3 base = getGradientColor(t);
|
|
66
|
+
vec2 offset = vec2(iMouse.x/iResolution.x, iMouse.y/iResolution.y);
|
|
67
|
+
float d = length(uv0-offset); float r = max(uSpotlightRadius, 1e-4); float dn = d/r;
|
|
68
|
+
float spot = (1.0 - 2.0*pow(dn, uSpotlightSoftness)) * uSpotlightOpacity;
|
|
69
|
+
vec3 cir = vec3(spot);
|
|
70
|
+
float stripe = fract(uvMod.x * max(uBlindCount,1.0)); if(uShineFlip>0.5) stripe = 1.0-stripe;
|
|
71
|
+
vec3 ran = vec3(stripe);
|
|
72
|
+
vec3 col = cir + base - ran;
|
|
73
|
+
col += (rand(gl_FragCoord.xy + iTime) - 0.5) * uNoise;
|
|
74
|
+
fragColor = vec4(col, 1.0);
|
|
75
|
+
}
|
|
76
|
+
void main(){ vec4 color; mainImage(color, vUv*iResolution.xy); gl_FragColor = color; }`;
|
|
77
|
+
|
|
78
|
+
const { arr, count } = prepStops(colors);
|
|
79
|
+
const uniforms = {
|
|
80
|
+
iResolution: { value: [gl.drawingBufferWidth, gl.drawingBufferHeight, 1] },
|
|
81
|
+
iMouse: { value: [0, 0] }, iTime: { value: 0 },
|
|
82
|
+
uAngle: { value: (angle * Math.PI) / 180 }, uNoise: { value: noise },
|
|
83
|
+
uBlindCount: { value: Math.max(1, blindCount) },
|
|
84
|
+
uSpotlightRadius: { value: spotRadius }, uSpotlightSoftness: { value: spotSoft }, uSpotlightOpacity: { value: spotOpacity },
|
|
85
|
+
uMirror: { value: mirror }, uDistort: { value: distort }, uShineFlip: { value: shineFlip },
|
|
86
|
+
uColor0: { value: arr[0] }, uColor1: { value: arr[1] }, uColor2: { value: arr[2] }, uColor3: { value: arr[3] },
|
|
87
|
+
uColor4: { value: arr[4] }, uColor5: { value: arr[5] }, uColor6: { value: arr[6] }, uColor7: { value: arr[7] },
|
|
88
|
+
uColorCount: { value: count },
|
|
89
|
+
};
|
|
90
|
+
const program = new Program(gl, { vertex, fragment, uniforms });
|
|
91
|
+
const mesh = new Mesh(gl, { geometry: new Triangle(gl), program });
|
|
92
|
+
|
|
93
|
+
let first = true;
|
|
94
|
+
const resize = () => {
|
|
95
|
+
const rect = el.getBoundingClientRect();
|
|
96
|
+
renderer.setSize(rect.width || 1, rect.height || 1);
|
|
97
|
+
uniforms.iResolution.value = [gl.drawingBufferWidth, gl.drawingBufferHeight, 1];
|
|
98
|
+
const maxByMin = blindMinWidth > 0 ? Math.max(1, Math.floor((rect.width || 1) / blindMinWidth)) : blindCount;
|
|
99
|
+
uniforms.uBlindCount.value = Math.max(1, blindCount ? Math.min(blindCount, maxByMin) : maxByMin);
|
|
100
|
+
if (first) { first = false; const cx = gl.drawingBufferWidth / 2, cy = gl.drawingBufferHeight / 2; uniforms.iMouse.value = [cx, cy]; tx = cx; ty = cy; }
|
|
101
|
+
};
|
|
102
|
+
let tx = 0, ty = 0, last = 0;
|
|
103
|
+
resize();
|
|
104
|
+
try { new ResizeObserver(resize).observe(el); } catch (_) {}
|
|
105
|
+
|
|
106
|
+
// listen on window so the spotlight tracks the cursor even through the hero content overlaid on top.
|
|
107
|
+
window.addEventListener("pointermove", (e) => {
|
|
108
|
+
const rect = canvas.getBoundingClientRect();
|
|
109
|
+
const scale = renderer.dpr || 1;
|
|
110
|
+
tx = (e.clientX - rect.left) * scale;
|
|
111
|
+
ty = (rect.height - (e.clientY - rect.top)) * scale;
|
|
112
|
+
if (damp <= 0) uniforms.iMouse.value = [tx, ty];
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const loop = (t) => {
|
|
116
|
+
requestAnimationFrame(loop);
|
|
117
|
+
uniforms.iTime.value = t * 0.001;
|
|
118
|
+
if (damp > 0) {
|
|
119
|
+
if (!last) last = t;
|
|
120
|
+
const dt = (t - last) / 1000; last = t;
|
|
121
|
+
let factor = 1 - Math.exp(-dt / Math.max(1e-4, damp)); if (factor > 1) factor = 1;
|
|
122
|
+
const cur = uniforms.iMouse.value;
|
|
123
|
+
cur[0] += (tx - cur[0]) * factor; cur[1] += (ty - cur[1]) * factor;
|
|
124
|
+
}
|
|
125
|
+
try { renderer.render({ scene: mesh }); } catch (e) { /* context lost */ }
|
|
126
|
+
};
|
|
127
|
+
requestAnimationFrame(loop);
|
|
128
|
+
}).catch(() => { /* no ogl - the hero just has a flat background */ });
|
|
129
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# GradientBlinds - an animated WebGL gradient with diagonal blinds and a spotlight that follows the cursor
|
|
2
|
+
# (ported from React Bits). Use it as a hero background: make it the first child of a
|
|
3
|
+
# `relative overflow-hidden` box and it fills the box, then fades out at the bottom to blend into the page.
|
|
4
|
+
# Stack class("relative overflow-hidden") {
|
|
5
|
+
# GradientBlinds(colors: "#FF9FFC,#5227FF")
|
|
6
|
+
# Section class("relative z-10 ...") { ...hero content... }
|
|
7
|
+
# }
|
|
8
|
+
# `colors` = a comma-separated list of hex stops. Angle, blind count, spotlight and grain are tuned defaults in
|
|
9
|
+
# the ejected host JS - edit src/components/GradientBlinds.js to change them.
|
|
10
|
+
# shadcn-for-muten · Custom · needs `ogl` (npm i ogl). Host JS: src/components/GradientBlinds.js.
|
|
11
|
+
part GradientBlinds(colors: text) {
|
|
12
|
+
Custom GradientBlinds inputs(colors: $colors) class("absolute inset-0 z-0 pointer-events-none opacity-75")
|
|
13
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// A muten-aware syntax highlighter for the browser - the same token classes the muten VS Code grammar uses
|
|
2
|
+
// (keywords, primitives, types, modifiers, methods, components, strings, params, refs, numbers, operators,
|
|
3
|
+
// comments). `highlight(code, lang)` returns HTML with .tk-* spans; the colors live in globals.css so they
|
|
4
|
+
// follow the theme. Non-muten languages are returned escaped (no highlight). Shared by the Code Custom + docs.
|
|
5
|
+
const KEYWORDS = "screen entity state store const theme get effect action mutates mock sources api meta routes shell guard else part param query every live persist post put delete body into if when each as where by with and or not contains use from".split(" ");
|
|
6
|
+
const PRIMITIVES = "Stack Header Nav Sidebar Footer Page Section Article List Details Text Title Span Image Icon Video SearchField DataTable RowAction Button Form Link slot When Each Custom".split(" ");
|
|
7
|
+
const MODIFIERS = "bind submit where columns class alt inputs on aria style".split(" ");
|
|
8
|
+
const METHODS = "push remove patch reset toggle set create update delete refetch".split(" ");
|
|
9
|
+
const TYPES = "text email string number bool uuid list".split(" ");
|
|
10
|
+
|
|
11
|
+
const grp = (a) => "(?:" + a.join("|") + ")";
|
|
12
|
+
// Ordered like the grammar: first match wins, specific before generic. Each: [regex source, token class].
|
|
13
|
+
const RULES = [
|
|
14
|
+
["#[^\\n]*", "comment"],
|
|
15
|
+
['"(?:[^"\\\\]|\\\\.)*"', "string"],
|
|
16
|
+
["\\$[A-Za-z_]\\w*", "variable"],
|
|
17
|
+
["@[A-Za-z_]\\w*", "variable"],
|
|
18
|
+
["\\b" + grp(KEYWORDS) + "\\b", "keyword"],
|
|
19
|
+
["\\b(?:true|false)\\b", "constant"],
|
|
20
|
+
["\\b" + grp(TYPES) + "\\b", "type"],
|
|
21
|
+
["\\b" + grp(PRIMITIVES) + "\\b", "primitive"],
|
|
22
|
+
["\\b" + grp(MODIFIERS) + "\\b", "modifier"],
|
|
23
|
+
["\\." + grp(METHODS) + "\\b", "method"],
|
|
24
|
+
["\\b[A-Z][A-Za-z0-9_]*\\b", "component"],
|
|
25
|
+
["-?\\b\\d+(?:\\.\\d+)?\\b", "number"],
|
|
26
|
+
["->|<-|==|!=|<=|>=|[-+*/<>=|?]", "operator"],
|
|
27
|
+
];
|
|
28
|
+
const MASTER = new RegExp(RULES.map((r) => "(" + r[0] + ")").join("|"), "g");
|
|
29
|
+
const CLASSES = RULES.map((r) => r[1]);
|
|
30
|
+
|
|
31
|
+
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
32
|
+
|
|
33
|
+
export function highlight(code, lang) {
|
|
34
|
+
const src = String(code == null ? "" : code);
|
|
35
|
+
if (lang && lang !== "muten" && lang !== "store") return esc(src);
|
|
36
|
+
let out = "", last = 0, m;
|
|
37
|
+
MASTER.lastIndex = 0;
|
|
38
|
+
while ((m = MASTER.exec(src))) {
|
|
39
|
+
if (m.index > last) out += esc(src.slice(last, m.index));
|
|
40
|
+
let cls = "variable";
|
|
41
|
+
for (let i = 1; i < m.length; i++) { if (m[i] !== undefined) { cls = CLASSES[i - 1]; break; } }
|
|
42
|
+
out += '<span class="tk-' + cls + '">' + esc(m[0]) + "</span>";
|
|
43
|
+
last = m.index + m[0].length;
|
|
44
|
+
}
|
|
45
|
+
if (last < src.length) out += esc(src.slice(last));
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
@@ -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,11 @@
|
|
|
1
|
+
# LogoCloud - the "trusted by" strip: a wrapping row of logos, muted + grayscale until hover. GENERIC - a
|
|
2
|
+
# LogoCloudItem holds ANY logo type: an Image, an Icon, or a plain text wordmark (Span). All normalize to one
|
|
3
|
+
# height. Give it a heading above via a plain Text.
|
|
4
|
+
# LogoCloud {
|
|
5
|
+
# LogoCloudItem { Image "~/logos/acme.svg" alt("Acme") } # image logo
|
|
6
|
+
# LogoCloudItem { Icon "simple-icons:vercel" } # icon logo
|
|
7
|
+
# LogoCloudItem { Span "Globex" class("text-lg font-bold tracking-tight") } # text wordmark
|
|
8
|
+
# }
|
|
9
|
+
# shadcn-for-muten · block · no deps · styles via .logo-cloud / .logo-cloud-item in globals.css.
|
|
10
|
+
part LogoCloud() { Stack class("logo-cloud") { slot } }
|
|
11
|
+
part LogoCloudItem() { Stack class("logo-cloud-item") { 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,5 @@
|
|
|
1
|
+
# Pill - a hero eyebrow chip (bigger than a Badge, rounded-full). Slot-based so you drop an icon + text:
|
|
2
|
+
# Pill { Icon "lucide:sparkles" class("size-4") Span "For freelancers, studios and agencies" }
|
|
3
|
+
# Tint it per brand with extra classes: Pill class("bg-blue-50 border-blue-100 text-blue-700").
|
|
4
|
+
# shadcn-for-muten · part · no deps · styles via the semantic .pill class in globals.css.
|
|
5
|
+
part Pill() { Stack class("pill") { slot } }
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Pricing - a row of tiers. PricingTier / PricingTierFeatured take name/price/period/description; the slot holds
|
|
2
|
+
# the feature list (PricingFeature items) + the CTA. Use PricingTierFeatured to highlight the recommended plan.
|
|
3
|
+
# Pricing {
|
|
4
|
+
# PricingTierFeatured(name: "Pro", price: "$20", period: "/mo", description: "for pro sites") {
|
|
5
|
+
# Stack class("pricing-features") { PricingFeature { Span "Image optimization" } PricingFeature { Span "Bot protection" } }
|
|
6
|
+
# Button "See Pro plan" -> choose class("btn btn-default w-full mt-auto")
|
|
7
|
+
# }
|
|
8
|
+
# }
|
|
9
|
+
# shadcn-for-muten · block · no deps · styles via the semantic .pricing-* classes in globals.css.
|
|
10
|
+
part Pricing() { Stack class("pricing-grid") { slot } }
|
|
11
|
+
part PricingTier(name: text, price: text, period: text, description: text) {
|
|
12
|
+
Stack class("pricing-tier") {
|
|
13
|
+
Stack class("gap-1") { Span "{$name}" class("pricing-name") Span "{$description}" class("pricing-desc") }
|
|
14
|
+
Stack class("flex flex-row items-baseline gap-1.5") { Span "{$price}" class("pricing-price") Span "{$period}" class("pricing-period") }
|
|
15
|
+
slot
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
part PricingTierFeatured(name: text, price: text, period: text, description: text) {
|
|
19
|
+
Stack class("pricing-tier pricing-tier-featured") {
|
|
20
|
+
Stack class("gap-1") { Span "{$name}" class("pricing-name") Span "{$description}" class("pricing-desc") }
|
|
21
|
+
Stack class("flex flex-row items-baseline gap-1.5") { Span "{$price}" class("pricing-price") Span "{$period}" class("pricing-period") }
|
|
22
|
+
slot
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
part PricingFeature() { Stack class("pricing-feature") { Icon "lucide:check" class("size-4 text-primary shrink-0") slot } }
|
|
@@ -6,3 +6,9 @@ part SegmentedControl() { Stack class("segmented") { slot } }
|
|
|
6
6
|
part SegmentedItem(value: text, current: text, label: text, onSelect: action) {
|
|
7
7
|
Button "{$label}" -> $onSelect($value) aria(pressed: $value == $current) class("segmented-item", "segmented-item-active" when $value == $current)
|
|
8
8
|
}
|
|
9
|
+
# Full-width tab bar with icon + label (Forms · Scheduling · PDF …). IconTab is slot-based so you drop an
|
|
10
|
+
# Icon + Span inside: SegmentedTabs { IconTab(value: "forms", current: tab, onSelect: setTab) { Icon "lucide:file-text" Span "Forms" } … }
|
|
11
|
+
part SegmentedTabs() { Stack class("segmented segmented-fill") { slot } }
|
|
12
|
+
part IconTab(value: text, current: text, onSelect: action) {
|
|
13
|
+
Button -> $onSelect($value) aria(pressed: $value == $current) class("segmented-item", "segmented-item-active" when $value == $current) { slot }
|
|
14
|
+
}
|
package/registry/sidebar.muten
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
#
|
|
1
|
+
# AppSidebar - a collapsible nav panel (named AppSidebar, NOT Sidebar, so it never shadows muten's core `Sidebar`
|
|
2
|
+
# <aside> primitive). The PAGE owns a collapsed bool + the active item:
|
|
2
3
|
# state { collapsed = false : bool nav = "sprites" : text } action setNav(v: text) mutates nav { nav.set(v) }
|
|
3
|
-
#
|
|
4
|
+
# AppSidebar(collapsed: collapsed) {
|
|
4
5
|
# SidebarHeader { Span "Studio" }
|
|
5
6
|
# SidebarGroup { SidebarGroupLabel(label: "Assets")
|
|
6
7
|
# SidebarItem(label: "Sprites", value: "sprites", current: nav, onSelect: setNav) { Icon "lucide:image" class("size-4 shrink-0") } … }
|
|
7
8
|
# }
|
|
8
9
|
# shadcn-for-muten · part · no deps · styles via the semantic .sidebar* classes in globals.css.
|
|
9
|
-
part
|
|
10
|
+
part AppSidebar(collapsed: bool) { Stack class("sidebar", "sidebar-collapsed" when $collapsed) { slot } }
|
|
10
11
|
part SidebarHeader() { Stack class("sidebar-header") { slot } }
|
|
11
12
|
part SidebarGroup() { Stack class("sidebar-group") { slot } }
|
|
12
13
|
part SidebarGroupLabel(label: text) { Span "{$label}" class("sidebar-group-label") }
|
|
13
14
|
part SidebarItem(label: text, value: text, current: text, onSelect: action) {
|
|
14
15
|
Button -> $onSelect($value) aria(pressed: $value == $current) class("sidebar-item", "sidebar-item-active" when $value == $current) {
|
|
15
16
|
slot
|
|
16
|
-
Span "{$label}" class("truncate")
|
|
17
|
+
Span "{$label}" class("sidebar-item-label truncate")
|
|
17
18
|
}
|
|
18
19
|
}
|
|
@@ -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") } }
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Stepper - a horizontal (default) or vertical progress indicator. Wrap the steps in Stepper (or
|
|
2
|
+
# StepperVertical) and render each with the Step part; state is "done" | "active" | "upcoming".
|
|
3
|
+
# state { steps = query steps : list<StepItem> }
|
|
4
|
+
# Stepper { each steps as s { Step(num: s.num, label: s.label, state: s.state) } }
|
|
5
|
+
# A completed step shows a check (the number hides); the current step is ringed. Own it with `muten add stepper`.
|
|
6
|
+
# shadcn-for-muten · part · no deps · styles via .stepper / .stepper-step* in globals.css.
|
|
7
|
+
part Stepper() { Stack class("stepper") { slot } }
|
|
8
|
+
part StepperVertical() { Stack class("stepper stepper-vertical") { slot } }
|
|
9
|
+
part Step(num: text, label: text, state: text) {
|
|
10
|
+
Stack class("stepper-step stepper-step-{$state}") {
|
|
11
|
+
Stack class("stepper-indicator") {
|
|
12
|
+
when $state == "done" { Icon "lucide:check" class("size-4") }
|
|
13
|
+
Span "{$num}" class("stepper-num")
|
|
14
|
+
}
|
|
15
|
+
Span "{$label}" class("stepper-label")
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Timeline - a vertical list of events, each a dot on a connector line + content (title, time, description).
|
|
2
|
+
# state { events = query events : list<Event> }
|
|
3
|
+
# Timeline { each events as e { TimelineItem(title: e.title, time: e.time, desc: e.desc) } }
|
|
4
|
+
# Own it with `muten add timeline`.
|
|
5
|
+
# shadcn-for-muten · part · no deps · styles via .timeline / .timeline-* in globals.css.
|
|
6
|
+
part Timeline() { Stack class("timeline") { slot } }
|
|
7
|
+
part TimelineItem(title: text, time: text, desc: text) {
|
|
8
|
+
Stack class("timeline-item") {
|
|
9
|
+
Stack class("timeline-dot")
|
|
10
|
+
Stack class("timeline-content") {
|
|
11
|
+
Span "{$time}" class("timeline-time")
|
|
12
|
+
Span "{$title}" class("timeline-title")
|
|
13
|
+
Span "{$desc}" class("timeline-desc")
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|