@muten/shadcn 0.2.12 → 0.2.13
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/kit.css +47 -0
- package/package.json +1 -1
- package/registry/badge.muten +9 -4
- package/registry/sidebar.muten +24 -0
- package/registry.json +1 -1
package/kit.css
CHANGED
|
@@ -5560,3 +5560,50 @@ a.cx-bc-item:hover { color: var(--foreground); }
|
|
|
5560
5560
|
.style-nova .cn-alert > .cn-alert-action { grid-column: 1 / -1; grid-row: auto; justify-self: start; margin: 8px 0 0; }
|
|
5561
5561
|
.style-nova .cn-alert:has(> :is(svg, .mu-icon)) > .cn-alert-action { grid-column: 2; }
|
|
5562
5562
|
}
|
|
5563
|
+
|
|
5564
|
+
/* ═════ SIDEBAR SUB · the level below an item (SidebarSub, SidebarSubLink) ═════
|
|
5565
|
+
Indented under its parent with a hairline on the left; an item holds an optional status dot or a spinning ring, the
|
|
5566
|
+
name, a hint in words that opens on hover (always shown where there is no hover), and a count. */
|
|
5567
|
+
.cx-sb-sub { display: flex; flex-direction: column; gap: 1px; margin: 2px 0 4px 15px; padding-left: 10px; border-left: 1px solid var(--border); min-width: 0; }
|
|
5568
|
+
.cx-sidebar[data-collapsed="true"] .cx-sb-sub { display: none; }
|
|
5569
|
+
.cx-sb-subitem { position: relative; display: flex; align-items: center; gap: 8px; min-height: 30px; padding: 0 8px; border-radius: calc(var(--radius) * .7); color: var(--muted-foreground); font-size: 13.5px; text-decoration: none; white-space: nowrap; min-width: 0; transition: background-color .15s ease, color .15s ease; }
|
|
5570
|
+
.cx-sb-subitem:hover { background: var(--accent); color: var(--foreground); }
|
|
5571
|
+
.cx-sb-subitem[data-active="true"] { background: var(--selected-soft); color: var(--selected); font-weight: 500; }
|
|
5572
|
+
.cx-sb-subitem:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; }
|
|
5573
|
+
.cx-sb-subitem > .mu-icon { width: 15px; height: 15px; flex: none; }
|
|
5574
|
+
.cx-sb-subitem > .cx-sb-count { margin-left: auto; }
|
|
5575
|
+
.cx-sb-sub > .cx-sb-subitem[data-tone] { color: var(--foreground); font-size: 14px; }
|
|
5576
|
+
.cx-sb-subitem:has(+ .cx-sb-sub) { font-weight: 600; }
|
|
5577
|
+
.cx-sb-dot { width: 8px; height: 8px; border-radius: 99px; flex: none; background: var(--muted-foreground); }
|
|
5578
|
+
.cx-sb-subitem[data-tone="ok"] > .cx-sb-dot { background: var(--success); }
|
|
5579
|
+
.cx-sb-subitem[data-tone="wait"] > .cx-sb-dot { background: var(--warning); }
|
|
5580
|
+
.cx-sb-subitem[data-tone="bad"] > .cx-sb-dot { background: var(--destructive); }
|
|
5581
|
+
.cx-sb-ring { width: 12px; height: 12px; flex: none; box-sizing: border-box; border-radius: 99px; border: 2px solid var(--warning); border-right-color: transparent; animation: cx-sb-spin .7s linear infinite; }
|
|
5582
|
+
.cx-sb-hint { margin-left: auto; font-size: 12px; color: var(--warning); opacity: 0; transform: translateX(4px); transition: opacity .15s ease, transform .2s var(--ease-out); flex: none; }
|
|
5583
|
+
.cx-sb-subitem:hover > .cx-sb-hint, .cx-sb-subitem:focus-visible > .cx-sb-hint { opacity: 1; transform: none; }
|
|
5584
|
+
/* with a mouse the words take the name's place on hover (the name fades, they fade in where it was): the row never
|
|
5585
|
+
changes size and nothing shows cut. Without hover they sit after the name. */
|
|
5586
|
+
@media (hover: hover) {
|
|
5587
|
+
.cx-sb-subitem { --hint-x: 28px; }
|
|
5588
|
+
.cx-sb-subitem:has(> .cx-sb-dot) { --hint-x: 24px; }
|
|
5589
|
+
.cx-sb-hint { position: absolute; left: var(--hint-x); right: 8px; top: 50%; translate: 0 -50%; margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13.5px; pointer-events: none; }
|
|
5590
|
+
.cx-sb-label { transition: opacity .15s ease; }
|
|
5591
|
+
.cx-sb-subitem:hover > .cx-sb-label:has(+ .cx-sb-hint), .cx-sb-subitem:focus-visible > .cx-sb-label:has(+ .cx-sb-hint) { opacity: 0; }
|
|
5592
|
+
}
|
|
5593
|
+
@media (hover: none) { .cx-sb-hint { opacity: 1; transform: none; } }
|
|
5594
|
+
|
|
5595
|
+
/* ═════ TONE BADGE · busy, compact and lg (ToneBadge) ═════
|
|
5596
|
+
busy swaps the dot for a ring that spins; compact keeps only the dot or the ring and opens the words beside it on
|
|
5597
|
+
hover or focus (their column grows from 0fr, so nothing jumps); without hover they always show. lg sits beside a
|
|
5598
|
+
page's title. */
|
|
5599
|
+
.cn-badge[class*="cx-tone-"] .cx-tone-words { display: grid; grid-template-columns: 1fr; overflow: hidden; }
|
|
5600
|
+
.cn-badge[class*="cx-tone-"] .cx-tone-words > span { min-width: 0; white-space: nowrap; }
|
|
5601
|
+
.cn-badge[data-size="lg"] { height: 28px; padding: 0 12px; gap: 8px; font-size: 14px; font-weight: 600; border: 1px solid color-mix(in oklch, var(--tone, var(--muted-foreground)) 30%, transparent); }
|
|
5602
|
+
.cn-badge[data-size="lg"] .cx-dot { width: 8px; height: 8px; }
|
|
5603
|
+
.cx-tone-ring { width: 12px; height: 12px; flex: none; box-sizing: border-box; border-radius: 99px; border: 2px solid var(--tone, currentColor); border-right-color: transparent; animation: cx-sb-spin .7s linear infinite; }
|
|
5604
|
+
.cn-badge[data-compact="true"] { gap: 0; padding-inline: 8px; transition: gap .2s var(--ease-out), padding .2s var(--ease-out); }
|
|
5605
|
+
.cn-badge[data-compact="true"] .cx-tone-words { grid-template-columns: 0fr; opacity: 0; transition: grid-template-columns .2s var(--ease-out), opacity .15s ease; }
|
|
5606
|
+
.cn-badge[data-compact="true"]:hover, .cn-badge[data-compact="true"]:focus-within { gap: 8px; padding-inline: 12px; }
|
|
5607
|
+
.cn-badge[data-compact="true"]:hover .cx-tone-words, .cn-badge[data-compact="true"]:focus-within .cx-tone-words { grid-template-columns: 1fr; opacity: 1; }
|
|
5608
|
+
@media (hover: none) { .cn-badge[data-compact="true"] { gap: 8px; padding-inline: 12px; } .cn-badge[data-compact="true"] .cx-tone-words { grid-template-columns: 1fr; opacity: 1; } }
|
|
5609
|
+
@media (prefers-reduced-motion: reduce) { .cx-sb-ring, .cx-tone-ring { animation-duration: 1.6s; } .cn-badge[data-compact="true"], .cn-badge[data-compact="true"] .cx-tone-words, .cx-sb-hint { transition: none; } }
|
package/package.json
CHANGED
package/registry/badge.muten
CHANGED
|
@@ -19,10 +19,15 @@ part StatusBadge(label: text, tone: text = "") { Span "{$label}" class("cn-badge
|
|
|
19
19
|
# ToneBadge - a state with its tone, tinted, with or without a dot: new | ok | wait | bad | pro (PRO uses --selected).
|
|
20
20
|
# The word is the tone mixed toward the ink, so it reads on its tint in light and dark.
|
|
21
21
|
# ToneBadge(label: "Confirmed", tone: "ok") ToneBadge(label: "Pending", tone: "wait", dot: "dot")
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
# busy: a ring that spins instead of the dot (something on its way). compact: only the dot or the ring, the words open
|
|
23
|
+
# beside it on hover or focus (where there is no hover they always show; a screen reader always reads them).
|
|
24
|
+
# size "lg": the state beside a page's title.
|
|
25
|
+
# ToneBadge(label: "Building v24", tone: "wait", busy: true, compact: true, size: "lg")
|
|
26
|
+
part ToneBadge(label: text, tone: text, dot: text = "", busy: bool = false, compact: bool = false, size: text = "") {
|
|
27
|
+
Stack class("cn-badge group/badge cx-tone-{$tone}") data(slot: "badge", busy: $busy, compact: $compact, size: $size) {
|
|
28
|
+
when $busy { Span "" class("cx-tone-ring") aria(hidden: "true") }
|
|
29
|
+
when not $busy and $dot != "" { Span "" class("cx-dot") }
|
|
30
|
+
Stack class("cx-tone-words") { Span "{$label}" }
|
|
26
31
|
}
|
|
27
32
|
}
|
|
28
33
|
# Chip - a value the person can take away with its ×; it fades out (no jump). The page owns the list.
|
package/registry/sidebar.muten
CHANGED
|
@@ -9,6 +9,17 @@
|
|
|
9
9
|
# SidebarLink is the same item as a link to a route: the app marks where you are by itself (no `current` to keep), also
|
|
10
10
|
# below it (on /schedule/42 the “Appointments” link stays marked). `match` lists other pages it stands for ("/a /b").
|
|
11
11
|
# SidebarLink(label: "Appointments", to: "/schedule", count: "6") { Icon "lucide:calendar-days" }
|
|
12
|
+
# SidebarSub is the level below an item (shadcn's SidebarMenuSub): indented, with a line on its left. Nest one inside
|
|
13
|
+
# another for one more level. SidebarSubLink is an item in it; the app says which one is `active` (a tab, a query: a
|
|
14
|
+
# route alone cannot tell), `tone` puts a status dot before it (ok · wait · bad · idle), `busy` a ring that spins
|
|
15
|
+
# instead, and `hint` says that state in words: on hover beside the name, always for a screen reader. `active` and
|
|
16
|
+
# `busy` are "true" or anything else (text, so a row's own call can fill them: "{isOpen(s.name)}"). An item with its
|
|
17
|
+
# own level open below it is set in bold.
|
|
18
|
+
# SidebarLink(label: "Services", to: "/services") { Icon "lucide:layers" }
|
|
19
|
+
# SidebarSub(label: "Your services") {
|
|
20
|
+
# SidebarSubLink(label: "api", to: "/services/api", tone: "ok", busy: "{building}", hint: "Building v24")
|
|
21
|
+
# SidebarSub(label: "api") { SidebarSubLink(label: "Logs", to: "/services/api?tab=logs", active: "{tab == "logs"}") { Icon "lucide:terminal" } }
|
|
22
|
+
# }
|
|
12
23
|
# The name AppSidebar avoids muten's own Sidebar primitive. shadcn-for-muten · parts · no deps · Nova (.cn-sidebar-*) + kit.css.
|
|
13
24
|
part AppSidebar(collapsed: bool, label: text = "Menu") {
|
|
14
25
|
Nav class("cx-sidebar") data(slot: "sidebar", collapsed: $collapsed) aria(label: $label) { Stack class("cn-sidebar-inner cx-sb-inner") data(slot: "sidebar-inner") { slot } }
|
|
@@ -40,3 +51,16 @@ part SidebarLink(label: text, to: text, count: text = "", match: text = "") {
|
|
|
40
51
|
Span "{$count}" class("cn-sidebar-menu-badge cx-sb-count") data(slot: "sidebar-menu-badge")
|
|
41
52
|
}
|
|
42
53
|
}
|
|
54
|
+
part SidebarSub(label: text = "") {
|
|
55
|
+
Stack class("cn-sidebar-menu-sub cx-sb-sub") data(slot: "sidebar-menu-sub") aria(role: "group", label: $label) { slot }
|
|
56
|
+
}
|
|
57
|
+
part SidebarSubLink(label: text, to: text, active: text = "", count: text = "", tone: text = "", busy: text = "", hint: text = "") {
|
|
58
|
+
Link -> "{$to}" class("cn-sidebar-menu-sub-button cx-sb-subitem") data(slot: "sidebar-menu-sub-button", active: "{$active == "true"}", tone: $tone, busy: "{$busy == "true"}", name: $label) aria(current: "{$active == "true" ? "page" : "false"}", label: "{$label}{$hint == "" ? "" : ", {$hint}"}") {
|
|
59
|
+
when $busy == "true" { Span "" class("cx-sb-ring") aria(hidden: "true") }
|
|
60
|
+
when $busy != "true" and $tone != "" { Span "" class("cx-sb-dot") aria(hidden: "true") }
|
|
61
|
+
slot
|
|
62
|
+
Span "{$label}" class("cx-sb-label")
|
|
63
|
+
when $hint != "" { Span "{$hint}" class("cx-sb-hint") aria(hidden: "true") }
|
|
64
|
+
when $count != "" { Span "{$count}" class("cn-sidebar-menu-badge cx-sb-count") data(slot: "sidebar-menu-badge") }
|
|
65
|
+
}
|
|
66
|
+
}
|
package/registry.json
CHANGED
|
@@ -936,7 +936,7 @@
|
|
|
936
936
|
"deps": [],
|
|
937
937
|
"part": "AppSidebar",
|
|
938
938
|
"file": "registry/sidebar.muten",
|
|
939
|
-
"description": "AppSidebar - a collapsible nav panel (SidebarHeader / SidebarGroup / SidebarItem). Named AppSidebar so it never shadows muten's core Sidebar <aside> primitive."
|
|
939
|
+
"description": "AppSidebar - a collapsible nav panel (SidebarHeader / SidebarGroup / SidebarItem / SidebarLink), with SidebarSub / SidebarSubLink for the level below an item (active, a status dot or spinning ring, a hint on hover). Named AppSidebar so it never shadows muten's core Sidebar <aside> primitive."
|
|
940
940
|
},
|
|
941
941
|
{
|
|
942
942
|
"name": "site-footer",
|