@muten/shadcn 0.1.1 → 0.1.2

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.
Files changed (39) hide show
  1. package/AGENTS.md +64 -1
  2. package/README.md +7 -0
  3. package/kit.css +2284 -24
  4. package/motion.css +15 -0
  5. package/package.json +3 -2
  6. package/registry/accordion.muten +10 -0
  7. package/registry/agenda.js +72 -24
  8. package/registry/agenda.muten +30 -2
  9. package/registry/band.muten +1 -1
  10. package/registry/bio-play.muten +16 -0
  11. package/registry/booking-play.muten +43 -0
  12. package/registry/button.muten +3 -2
  13. package/registry/chat-play.muten +77 -0
  14. package/registry/day-plan.muten +40 -0
  15. package/registry/device-frame.muten +25 -7
  16. package/registry/gradient-blinds.js +88 -89
  17. package/registry/gradient-blinds.muten +11 -4
  18. package/registry/inbox-play.muten +56 -0
  19. package/registry/input-group.muten +3 -1
  20. package/registry/kit/collections.js +10 -2
  21. package/registry/kit/core.js +519 -99
  22. package/registry/kit/structure.js +3 -3
  23. package/registry/kit-widget.js +1 -1
  24. package/registry/legal-doc.muten +27 -0
  25. package/registry/modal-behavior.js +11 -0
  26. package/registry/modal.muten +1 -1
  27. package/registry/order-play.muten +33 -0
  28. package/registry/permission-matrix.muten +31 -5
  29. package/registry/phone-bar.muten +18 -0
  30. package/registry/phone-shell.muten +1 -1
  31. package/registry/product-play.muten +20 -0
  32. package/registry/screen-tour.js +59 -0
  33. package/registry/screen-tour.muten +10 -0
  34. package/registry/sidebar.muten +9 -0
  35. package/registry/site-footer.muten +37 -0
  36. package/registry/stamp-card.muten +5 -2
  37. package/registry/zoom-control.muten +3 -3
  38. package/registry.json +78 -0
  39. package/scripts/reuse-audit.mjs +37 -0
@@ -1,129 +1,128 @@
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`.
1
+ // GradientBlinds - first ported from React Bits (reactbits.dev), then made calm: a WebGL fragment shader (via `ogl`)
2
+ // lays a gradient that is born on one side of the hero (`from`) and dissolves into the page over a long, eased fall,
3
+ // with soft blinds that only shade it (never down to black), a light that follows the pointer and only adds, and a
4
+ // fine grain. The fall is the canvas's own alpha, so it melts into any page background, light or dark.
5
+ // A Custom (graphics = the vanilla-JS escape); ogl loads via dynamic import() (a Custom is inlined into an IIFE, so a
6
+ // top-level import is illegal). Inputs: colors, from (top | bottom | left | right | center), angle, blindCount,
7
+ // blindMinWidth, strength (how much the blinds shade, 0..1), noise. It draws only while on screen; with reduced
8
+ // motion it is still (no drift, the light stays put). Needs `ogl`.
6
9
  const MAX_COLORS = 8;
10
+ const FROM = { top: 0, bottom: 1, left: 2, right: 3, center: 4 };
7
11
  const hexToRGB = (hex) => {
8
12
  const c = String(hex).replace("#", "").padEnd(6, "0");
9
13
  return [parseInt(c.slice(0, 2), 16) / 255, parseInt(c.slice(2, 4), 16) / 255, parseInt(c.slice(4, 6), 16) / 255];
10
14
  };
11
15
  const prepStops = (stops) => {
12
- const base = (stops && stops.length ? stops : ["#FF9FFC", "#5227FF"]).slice(0, MAX_COLORS);
16
+ const base = (stops.length ? stops : ["#9B80FF", "#6B46F2"]).slice(0, MAX_COLORS);
13
17
  if (base.length === 1) base.push(base[0]);
18
+ const count = base.length;
14
19
  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)) };
20
+ return { arr: base.map(hexToRGB), count };
16
21
  };
17
22
 
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
23
+ const vertex = "attribute vec2 position;attribute vec2 uv;varying vec2 vUv;void main(){vUv=uv;gl_Position=vec4(position,0.0,1.0);}";
24
+ const fragment = `precision mediump float;
39
25
  uniform vec3 iResolution; uniform vec2 iMouse; uniform float iTime;
40
- uniform float uAngle, uNoise, uBlindCount, uSpotlightRadius, uSpotlightSoftness, uSpotlightOpacity, uMirror, uDistort, uShineFlip;
26
+ uniform float uAngle, uNoise, uBlindCount, uStrength, uFrom;
41
27
  uniform vec3 uColor0,uColor1,uColor2,uColor3,uColor4,uColor5,uColor6,uColor7; uniform int uColorCount;
42
28
  varying vec2 vUv;
43
29
  float rand(vec2 co){ return fract(sin(dot(co, vec2(12.9898,78.233))) * 43758.5453); }
44
30
  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);
31
+ vec3 gradient(float t){
32
+ float scaled = clamp(t,0.0,1.0) * float(uColorCount-1); float seg = floor(scaled); float f = smoothstep(0.0,1.0,fract(scaled));
48
33
  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;
34
+ if(seg<2.0) return mix(uColor1,uColor2,f);
35
+ if(seg<3.0) return mix(uColor2,uColor3,f);
36
+ if(seg<4.0) return mix(uColor3,uColor4,f);
37
+ if(seg<5.0) return mix(uColor4,uColor5,f);
38
+ if(seg<6.0) return mix(uColor5,uColor6,f);
39
+ return mix(uColor6,uColor7,f);
57
40
  }
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);
41
+ // how far a point is from the side the colour is born on: 0 there, 1 at the far side
42
+ float away(vec2 uv){
43
+ if(uFrom<0.5) return 1.0-uv.y;
44
+ if(uFrom<1.5) return uv.y;
45
+ if(uFrom<2.5) return uv.x;
46
+ if(uFrom<3.5) return 1.0-uv.x;
47
+ vec2 d = (uv-0.5)*vec2(iResolution.x/iResolution.y,1.0); return clamp(length(d)*0.9,0.0,1.0);
75
48
  }
76
- void main(){ vec4 color; mainImage(color, vUv*iResolution.xy); gl_FragColor = color; }`;
49
+ void main(){
50
+ vec2 uv0 = vUv;
51
+ float aspect = iResolution.x / iResolution.y;
52
+ 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;
53
+ float t = uv.x + iTime*0.012;
54
+ float wave = fract(t*0.5)*2.0; wave = wave > 1.0 ? 2.0-wave : wave; // there and back, so the colours never jump
55
+ vec3 col = gradient(wave);
56
+ // blinds: a rounded light-to-shade profile per blind, shading the colour by at most uStrength
57
+ float stripe = fract(t * uBlindCount);
58
+ float shade = 0.5 - 0.5*cos(stripe*6.28318);
59
+ col *= 1.0 - uStrength*shade;
60
+ // the light under the pointer: a wide, soft lift
61
+ vec2 m = iMouse / iResolution.xy; vec2 dm = (uv0-m)*vec2(aspect,1.0);
62
+ col += exp(-dot(dm,dm)*3.5) * 0.18;
63
+ col += (rand(gl_FragCoord.xy + floor(iTime*24.0)) - 0.5) * uNoise;
64
+ // the fall into the page: full near the side it comes from, gone by the far side, eased so there is no edge
65
+ float a = 1.0 - smoothstep(0.0, 1.0, away(uv0));
66
+ a = a*a*(3.0-2.0*a);
67
+ gl_FragColor = vec4(col*a, a);
68
+ }`;
69
+
70
+ export function mount(el, inputs) {
71
+ const num = (v, d) => { const n = Number(v); return Number.isFinite(n) ? n : d; };
72
+ const I = inputs || {};
73
+ const colors = String(I.colors || "").split(",").map((s) => s.trim()).filter(Boolean);
74
+ const from = FROM[I.from] ?? FROM.top;
75
+ const angle = num(I.angle, -25), noise = num(I.noise, 0.035), strength = num(I.strength, 0.22);
76
+ const blindCount = num(I.blindCount, 12), blindMinWidth = num(I.blindMinWidth, 80);
77
+ const still = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
78
+
79
+ import("ogl").then(({ Renderer, Program, Mesh, Triangle }) => {
80
+ const renderer = new Renderer({ dpr: Math.min(2, window.devicePixelRatio || 1), alpha: true, premultipliedAlpha: true });
81
+ const gl = renderer.gl;
82
+ const canvas = gl.canvas;
83
+ canvas.style.cssText = "position:absolute;inset:0;width:100%;height:100%;display:block";
84
+ el.appendChild(canvas);
77
85
 
78
86
  const { arr, count } = prepStops(colors);
79
87
  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 },
88
+ iResolution: { value: [1, 1, 1] }, iMouse: { value: [0, 0] }, iTime: { value: 0 },
89
+ uAngle: { value: (angle * Math.PI) / 180 }, uNoise: { value: noise }, uStrength: { value: strength },
90
+ uBlindCount: { value: blindCount }, uFrom: { value: from }, uColorCount: { value: count },
86
91
  uColor0: { value: arr[0] }, uColor1: { value: arr[1] }, uColor2: { value: arr[2] }, uColor3: { value: arr[3] },
87
92
  uColor4: { value: arr[4] }, uColor5: { value: arr[5] }, uColor6: { value: arr[6] }, uColor7: { value: arr[7] },
88
- uColorCount: { value: count },
89
93
  };
90
- const program = new Program(gl, { vertex, fragment, uniforms });
91
- const mesh = new Mesh(gl, { geometry: new Triangle(gl), program });
94
+ const mesh = new Mesh(gl, { geometry: new Triangle(gl), program: new Program(gl, { vertex, fragment, uniforms, transparent: true }) });
92
95
 
93
- let first = true;
96
+ let tx = 0, ty = 0, last = 0, seen = true, placed = false;
97
+ const draw = () => { try { renderer.render({ scene: mesh }); } catch (_) { /* context lost */ } };
94
98
  const resize = () => {
95
99
  const rect = el.getBoundingClientRect();
96
100
  renderer.setSize(rect.width || 1, rect.height || 1);
97
101
  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; }
102
+ uniforms.uBlindCount.value = Math.max(1, Math.min(blindCount, Math.floor((rect.width || 1) / blindMinWidth)));
103
+ if (!placed) { placed = true; tx = gl.drawingBufferWidth / 2; ty = gl.drawingBufferHeight * 0.7; uniforms.iMouse.value = [tx, ty]; }
104
+ draw();
101
105
  };
102
- let tx = 0, ty = 0, last = 0;
103
106
  resize();
104
107
  try { new ResizeObserver(resize).observe(el); } catch (_) {}
108
+ try { new IntersectionObserver(([e]) => { seen = e.isIntersecting; }).observe(el); } catch (_) {}
105
109
 
106
- // listen on window so the spotlight tracks the cursor even through the hero content overlaid on top.
110
+ if (still) return;
111
+ // on window, so the light follows the pointer even over the hero's content
107
112
  window.addEventListener("pointermove", (e) => {
108
- const rect = canvas.getBoundingClientRect();
109
- const scale = renderer.dpr || 1;
113
+ const rect = canvas.getBoundingClientRect(), scale = renderer.dpr || 1;
110
114
  tx = (e.clientX - rect.left) * scale;
111
115
  ty = (rect.height - (e.clientY - rect.top)) * scale;
112
- if (damp <= 0) uniforms.iMouse.value = [tx, ty];
113
- });
114
-
115
- const loop = (t) => {
116
+ }, { passive: true });
117
+ const loop = (time) => {
116
118
  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 */ }
119
+ if (!seen || document.hidden) { last = time; return; }
120
+ const dt = last ? (time - last) / 1000 : 0; last = time;
121
+ const ease = 1 - Math.exp(-dt / 0.35), cur = uniforms.iMouse.value;
122
+ cur[0] += (tx - cur[0]) * ease; cur[1] += (ty - cur[1]) * ease;
123
+ uniforms.iTime.value = time * 0.001;
124
+ draw();
126
125
  };
127
126
  requestAnimationFrame(loop);
128
- }).catch(() => { /* no ogl - the hero just has a flat background */ });
127
+ }).catch(() => { /* no ogl: the hero keeps its flat background */ });
129
128
  }
@@ -1,6 +1,13 @@
1
- # GradientBlinds - an animated gradient behind a hero: diagonal blinds and a light that follows the pointer, drawn on
2
- # the GPU (WebGL, through `ogl`: npm i ogl). Make it the first child of the hero and it fills the hero, fading out
3
- # at the bottom into the page; the content goes on top. colors: hex stops separated by commas.
1
+ # GradientBlinds - an animated gradient behind a hero, drawn on the GPU (WebGL, through `ogl`: npm i ogl): the colour is
2
+ # born on one side (`from`) and melts into the page over a long, eased fall, with soft diagonal blinds, a light that
3
+ # follows the pointer and a fine grain. Make it the first child of the hero (`cx-hero-blinds`) and it fills it; the
4
+ # content goes on top. It melts into any page background, light or dark, and is still under reduced motion.
4
5
  # Stack class("cx-hero-blinds") { GradientBlinds(colors: "#9B80FF,#6B46F2") Stack class("…") { … } }
6
+ # GradientBlinds(colors: "#FFB38A,#FF6F91,#9B80FF", from: "left", angle: 30, strength: 0.4, blinds: 8)
7
+ # colors: hex stops separated by commas. from: top | bottom | left | right | center. angle: the blinds' tilt in degrees
8
+ # (-25). strength: how much they shade, 0 (none) to 1 (0.22). blinds: how many at most (12; fewer on a narrow hero).
9
+ # noise: the grain, 0 to 0.2 (0.035).
5
10
  # shadcn-for-muten · Custom · needs ogl · host JS gradient-blinds.js.
6
- part GradientBlinds(colors: text = "#9B80FF,#6B46F2") { Custom GradientBlinds inputs(colors: $colors) class("cx-blinds") }
11
+ part GradientBlinds(colors: text = "#9B80FF,#6B46F2", from: text = "top", angle: number = -25, strength: number = 0.22, blinds: number = 12, noise: number = 0.035) {
12
+ Custom GradientBlinds inputs(colors: $colors, from: $from, angle: $angle, strength: $strength, blindCount: $blinds, noise: $noise) class("cx-blinds")
13
+ }
@@ -0,0 +1,56 @@
1
+ # InboxPlay - a messages list shown, not used: the shop's inbox as the channel's app draws it (skin: instagram |
2
+ # whatsapp), with the unread ones in bold and their dot. For a landing, beside «nobody answered them»; the real inbox
3
+ # is Messaging. The rows are a picture (aria-hidden); `label` says what it shows.
4
+ # InboxPlay(title: "brumabarberia", tabs: "Principal", requests: "Solicitudes (12)", label: "…") {
5
+ # InboxPlayRow(name: "lau.mtz", text: "precio??", time: "2 h", unread: "yes")
6
+ # InboxPlayRow(name: "carlos.r", text: "Visto", time: "1 d", photo: "/face.webp")
7
+ # }
8
+ # search (a placeholder) draws the app's search bar; InboxPlayNotes, InboxPlayTabs and InboxPlayFilters go in the list
9
+ # in that order. A row takes ring (yes: a story ring), badge (WhatsApp's unread count), tag (who answered: «Asistente»);
10
+ # and Instagram rows show the camera at the end.
11
+ # shadcn-for-muten · part · no deps · kit.css (.cx-ip).
12
+ part InboxPlay(title: text, tabs: text = "", requests: text = "", skin: text = "instagram", label: text = "", search: text = "") {
13
+ Stack class("cx-ip cx-play") data(skin: $skin) aria(role: "img", label: $label) {
14
+ Stack class("cx-ip-head") aria(hidden: true) {
15
+ Span "{$title}" class("cx-ip-t")
16
+ Icon "lucide:chevron-down"
17
+ Stack class("cx-ip-ic") { Icon "lucide:square-pen" }
18
+ }
19
+ when $search != "" { Stack class("cx-ip-search") aria(hidden: true) { Icon "lucide:search" Span "{$search}" } }
20
+ when $tabs != "" { Stack class("cx-ip-tabs") aria(hidden: true) { Span "{$tabs}" data(on: "yes") Span "{$requests}" } }
21
+ Stack class("cx-ip-list") aria(hidden: true) { slot }
22
+ }
23
+ }
24
+ part InboxPlayRow(name: text, text: text, time: text = "", photo: text = "", unread: text = "", ring: text = "", badge: text = "", tag: text = "") {
25
+ Stack class("cx-ip-row") data(unread: $unread, ring: $ring) {
26
+ Stack class("cx-ip-avw") {
27
+ when $photo != "" { Image "{$photo}" alt("") class("cx-ip-av") }
28
+ when $photo == "" { Span "{initial($name)}" class("cx-ip-av cx-ip-init") }
29
+ }
30
+ Stack class("cx-ip-tx") {
31
+ Stack class("cx-ip-top") { Span "{$name}" class("cx-ip-n") Span "{$time}" class("cx-ip-tr") }
32
+ Stack class("cx-ip-l") {
33
+ when $tag != "" { Span "{$tag}" class("cx-ip-tag") }
34
+ Span "{$text}" class("cx-ip-m")
35
+ Span "· {$time}" class("cx-ip-time")
36
+ when $badge != "" { Span "{$badge}" class("cx-ip-badge") }
37
+ }
38
+ }
39
+ Span "" class("cx-ip-dot")
40
+ Stack class("cx-ip-cam") { Icon "lucide:camera" }
41
+ }
42
+ }
43
+ # Instagram's notes over the list: a face with its short note above it.
44
+ part InboxPlayNotes() { Stack class("cx-ip-notes") { slot } }
45
+ part InboxPlayNote(name: text, note: text = "", photo: text = "") {
46
+ Stack class("cx-ip-note") {
47
+ when $note != "" { Span "{$note}" class("cx-ip-bub") }
48
+ when $photo != "" { Image "{$photo}" alt("") class("cx-ip-av") }
49
+ when $photo == "" { Span "{initial($name)}" class("cx-ip-av cx-ip-init") }
50
+ Span "{$name}" class("cx-ip-nn")
51
+ }
52
+ }
53
+ # Tabs as the app draws them (Instagram: Principal · General … Solicitudes) and WhatsApp's filter chips.
54
+ part InboxPlayTabs(primary: text, general: text = "", requests: text = "") { Stack class("cx-ip-tabs") { Span "{$primary}" data(on: "yes") when $general != "" { Span "{$general}" } when $requests != "" { Span "{$requests}" class("cx-ip-req") } } }
55
+ part InboxPlayFilters() { Stack class("cx-ip-filters") { slot } }
56
+ part InboxPlayFilter(label: text, on: text = "") { Span "{$label}" class("cx-ip-filter") data(on: $on) }
@@ -9,7 +9,9 @@
9
9
  # shadcn-for-muten · part · no deps · Nova (.cn-input-group).
10
10
  part InputGroup(size: text = "default", variant: text = "default") { Stack class("cn-input-group group/input-group") data(slot: "input-group", size: $size, variant: $variant) aria(role: "group") { slot } }
11
11
  part InputAddon(align: text = "inline-start") { Stack class("cn-input-group-addon cn-input-group-addon-align-{$align}") data(slot: "input-group-addon", align: $align) { slot } }
12
- part GroupInput(value: text, placeholder: text = "") { SearchField bind($value) "{$placeholder}" class("cn-input cn-input-group-input cx-num") data(slot: "input-group-control") }
12
+ # GroupInput - the field inside an InputGroup. `label` names it for a screen reader when the group shows no label of its own
13
+ # (a literal: an interpolated arg cannot reach aria(); without it the placeholder names the field).
14
+ part GroupInput(value: text, placeholder: text = "", label: text = "") { SearchField bind($value) "{$placeholder}" class("cn-input cn-input-group-input cx-num") data(slot: "input-group-control") aria(label: $label) }
13
15
  part SearchInput(value: text, placeholder: text = "Buscar", onClear: action, size: text = "default", variant: text = "default") {
14
16
  Stack class("cn-input-group group/input-group") data(slot: "input-group", size: $size, variant: $variant) aria(role: "group") {
15
17
  Stack class("cn-input-group-addon cn-input-group-addon-align-inline-start") data(slot: "input-group-addon", align: "inline-start") { Icon "lucide:search" }
@@ -115,7 +115,8 @@ export function carouselBehavior(root, i, core) {
115
115
  const thumbOf = (k) => { const media = cards[k].querySelector('img, [role=img], .cx-gallery-img'); const bg = media ? getComputedStyle(media).backgroundImage : ''; return media?.tagName === 'IMG' ? `<img src="${media.src}" alt="">` : `<span class="cx-thumb-fill" style="background-image:${bg}"></span>`; };
116
116
  const paint = () => {
117
117
  const n = pages(), cur = current();
118
- if (indicator === 'dots') { if (ind.childElementCount !== n) ind.innerHTML = Array.from({ length: n }, (_, k) => ui.button({ variant: 'ghost', size: 'icon-xs', cls: 'cx-dotb', attrs: `role="tab" data-go="${k}" aria-label="Ir a ${k + 1} de ${n}"`, label: '<span class="cx-dot-mark"></span>' })).join(''); $$('[data-go]', ind).forEach((d, k) => d.setAttribute('aria-selected', k === cur)); }
118
+ // the dots are tabs, so their holder is the tablist (a tab without one fails the a11y check)
119
+ if (indicator === 'dots') { ind.setAttribute('role', 'tablist'); ind.setAttribute('aria-label', 'Diapositivas'); if (ind.childElementCount !== n) ind.innerHTML = Array.from({ length: n }, (_, k) => ui.button({ variant: 'ghost', size: 'icon-xs', cls: 'cx-dotb', attrs: `role="tab" data-go="${k}" aria-label="Ir a ${k + 1} de ${n}"`, label: '<span class="cx-dot-mark"></span>' })).join(''); $$('[data-go]', ind).forEach((d, k) => d.setAttribute('aria-selected', k === cur)); }
119
120
  if (indicator === 'progress') ind.innerHTML = `<div class="cx-hstack" style="gap:10px;flex-wrap:nowrap">${ui.progress(((cur + 1) / n) * 100, `Diapositiva ${cur + 1} de ${n}`)}${ui.text(`${cur + 1} / ${n}`, 'caption')}</div>`;
120
121
  if (indicator === 'count') ind.innerHTML = ui.text(`${cur + 1} de ${n}`, 'caption');
121
122
  if (indicator === 'thumbs') { if (!ind.childElementCount) ind.innerHTML = `<div class="cx-hstack" role="tablist" aria-label="Miniaturas">${cards.map((_, k) => ui.button({ variant: 'ghost', size: 'icon-lg', cls: 'cx-thumb', attrs: `role="tab" data-go="${k}" aria-label="Foto ${k + 1}"`, label: thumbOf(k) })).join('')}</div>`; $$('[data-go]', ind).forEach((d, k) => { d.setAttribute('aria-selected', k === cur); d.setAttribute('aria-pressed', k === cur); }); }
@@ -135,7 +136,14 @@ export function carouselBehavior(root, i, core) {
135
136
  let drag = null;
136
137
  track.addEventListener('pointerdown', (e) => { if (e.pointerType !== 'mouse' || e.button > 0) return; drag = { x: e.clientX, left: track.scrollLeft, moved: false }; track.classList.add('dragging'); });
137
138
  addEventListener('pointermove', (e) => { if (!drag) return; const dx = e.clientX - drag.x; if (Math.abs(dx) > 4) drag.moved = true; track.scrollLeft = drag.left - dx; });
138
- addEventListener('pointerup', () => { if (!drag) return; const moved = drag.moved; drag = null; track.classList.remove('dragging'); if (moved) track.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); }, { capture: true, once: true }); });
139
+ // A drag lets go between two pages (snapping is off while dragging): settle on the nearest one, as a swipe does,
140
+ // or the track rests half-way and the dots and arrows disagree about where it is.
141
+ const nearest = () => {
142
+ if (center) return current();
143
+ if (track.scrollLeft >= track.scrollWidth - track.clientWidth - 4) return pages() - 1;
144
+ return Math.max(0, Math.min(pages() - 1, Math.round(track.scrollLeft / (step() * perPage()))));
145
+ };
146
+ addEventListener('pointerup', () => { if (!drag) return; const moved = drag.moved; drag = null; track.classList.remove('dragging'); if (moved) { go(nearest()); track.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); }, { capture: true, once: true }); } });
139
147
  let playing = !!autoplay && !matchMedia('(prefers-reduced-motion: reduce)').matches, hold = false;
140
148
  if (autoplay) {
141
149
  if (!playing) { const b = $('[data-car="play"]', root); b.innerHTML = ui.icon('play'); b.setAttribute('aria-label', 'Reproducir'); }