minka-ds 0.3.11 → 0.4.0
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/package.json +3 -2
- package/src/components/ui/avatar.tsx +47 -0
- package/src/components/ui/input-otp.tsx +106 -0
- package/src/components/ui/sidebar.tsx +34 -0
- package/src/index.ts +5 -0
- package/src/textures/brand-texture.tsx +60 -0
- package/src/textures/full-frame.tsx +7 -0
- package/src/textures/full-height-m.tsx +212 -0
- package/src/textures/full-height-s.tsx +32 -0
- package/src/textures/full-width-l.tsx +557 -0
- package/src/textures/full-width-m.tsx +7 -0
- package/src/textures/full-width-s.tsx +791 -0
- package/src/textures/full-width-xl.tsx +978 -0
- package/src/textures/index.ts +48 -0
- package/src/textures/logo-icon.tsx +29 -0
- package/src/textures/logo-primary.tsx +30 -0
- package/src/textures/pattern-m.tsx +555 -0
- package/src/textures/pattern-s.tsx +7 -0
- package/tokens/semantic.css +18 -0
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minka-ds",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Minka product design system — tokenized component library",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
7
7
|
"src",
|
|
8
|
-
"tokens"
|
|
8
|
+
"tokens",
|
|
9
|
+
"!src/textures/*.svg"
|
|
9
10
|
],
|
|
10
11
|
"exports": {
|
|
11
12
|
".": "./src/index.ts",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "../../lib/utils"
|
|
3
|
+
|
|
4
|
+
const SIZE: Record<NonNullable<AvatarProps["size"]>, string> = {
|
|
5
|
+
sm: "size-7 text-caption-serif",
|
|
6
|
+
md: "size-9 text-body-sm-serif",
|
|
7
|
+
lg: "size-12 text-body-serif",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface AvatarProps {
|
|
11
|
+
/** Image URL. When absent, initials (or a fallback) are shown. */
|
|
12
|
+
src?: string
|
|
13
|
+
/** Full name — used for alt text and to derive initials. */
|
|
14
|
+
name?: string
|
|
15
|
+
/** Explicit initials override; otherwise derived from `name`. */
|
|
16
|
+
initials?: string
|
|
17
|
+
size?: "sm" | "md" | "lg"
|
|
18
|
+
/** Background for the initials state. Defaults to a brand color. */
|
|
19
|
+
background?: string
|
|
20
|
+
className?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function deriveInitials(name?: string): string {
|
|
24
|
+
if (!name) return "?"
|
|
25
|
+
return name.trim().split(/\s+/).map(p => p[0]).join("").slice(0, 2).toUpperCase()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function Avatar({ src, name, initials, size = "md", background, className }: AvatarProps) {
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
data-slot="avatar"
|
|
32
|
+
className={cn(
|
|
33
|
+
"shrink-0 rounded-full flex items-center justify-center overflow-hidden text-[var(--color-text-inverse)]",
|
|
34
|
+
SIZE[size],
|
|
35
|
+
className
|
|
36
|
+
)}
|
|
37
|
+
style={{ background: src ? undefined : (background ?? "var(--color-brand-blue)") }}
|
|
38
|
+
>
|
|
39
|
+
{src
|
|
40
|
+
? <img src={src} alt={name ?? ""} className="size-full object-cover" />
|
|
41
|
+
: <span className="leading-none">{initials ?? deriveInitials(name)}</span>}
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { Avatar }
|
|
47
|
+
export type { AvatarProps }
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../lib/utils"
|
|
5
|
+
|
|
6
|
+
interface InputOTPProps {
|
|
7
|
+
/** Number of digit boxes. */
|
|
8
|
+
length?: number
|
|
9
|
+
value: string
|
|
10
|
+
onChange: (value: string) => void
|
|
11
|
+
/** Fires when all boxes are filled. */
|
|
12
|
+
onComplete?: (value: string) => void
|
|
13
|
+
invalid?: boolean
|
|
14
|
+
disabled?: boolean
|
|
15
|
+
autoFocus?: boolean
|
|
16
|
+
className?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function InputOTP({
|
|
20
|
+
length = 6,
|
|
21
|
+
value,
|
|
22
|
+
onChange,
|
|
23
|
+
onComplete,
|
|
24
|
+
invalid,
|
|
25
|
+
disabled,
|
|
26
|
+
autoFocus,
|
|
27
|
+
className,
|
|
28
|
+
}: InputOTPProps) {
|
|
29
|
+
const refs = React.useRef<(HTMLInputElement | null)[]>([])
|
|
30
|
+
const digits = Array.from({ length }, (_, i) => value[i] ?? "")
|
|
31
|
+
|
|
32
|
+
function setAt(index: number, char: string) {
|
|
33
|
+
const next = digits.slice()
|
|
34
|
+
next[index] = char
|
|
35
|
+
const joined = next.join("").slice(0, length)
|
|
36
|
+
onChange(joined)
|
|
37
|
+
if (joined.length === length) onComplete?.(joined)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function handleChange(index: number, raw: string) {
|
|
41
|
+
const char = raw.replace(/\D/g, "").slice(-1) // keep last typed digit
|
|
42
|
+
if (!char) return
|
|
43
|
+
setAt(index, char)
|
|
44
|
+
if (index < length - 1) refs.current[index + 1]?.focus()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function handleKeyDown(index: number, e: React.KeyboardEvent<HTMLInputElement>) {
|
|
48
|
+
if (e.key === "Backspace") {
|
|
49
|
+
e.preventDefault()
|
|
50
|
+
if (digits[index]) {
|
|
51
|
+
setAt(index, "")
|
|
52
|
+
} else if (index > 0) {
|
|
53
|
+
refs.current[index - 1]?.focus()
|
|
54
|
+
setAt(index - 1, "")
|
|
55
|
+
}
|
|
56
|
+
} else if (e.key === "ArrowLeft" && index > 0) {
|
|
57
|
+
e.preventDefault()
|
|
58
|
+
refs.current[index - 1]?.focus()
|
|
59
|
+
} else if (e.key === "ArrowRight" && index < length - 1) {
|
|
60
|
+
e.preventDefault()
|
|
61
|
+
refs.current[index + 1]?.focus()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function handlePaste(e: React.ClipboardEvent) {
|
|
66
|
+
e.preventDefault()
|
|
67
|
+
const pasted = e.clipboardData.getData("text").replace(/\D/g, "").slice(0, length)
|
|
68
|
+
if (!pasted) return
|
|
69
|
+
onChange(pasted)
|
|
70
|
+
if (pasted.length === length) onComplete?.(pasted)
|
|
71
|
+
refs.current[Math.min(pasted.length, length - 1)]?.focus()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div data-slot="input-otp" className={cn("flex items-center gap-2", className)} onPaste={handlePaste}>
|
|
76
|
+
{digits.map((d, i) => (
|
|
77
|
+
<input
|
|
78
|
+
key={i}
|
|
79
|
+
ref={el => { refs.current[i] = el }}
|
|
80
|
+
type="text"
|
|
81
|
+
inputMode="numeric"
|
|
82
|
+
autoComplete={i === 0 ? "one-time-code" : "off"}
|
|
83
|
+
maxLength={1}
|
|
84
|
+
value={d}
|
|
85
|
+
disabled={disabled}
|
|
86
|
+
autoFocus={autoFocus && i === 0}
|
|
87
|
+
aria-invalid={invalid || undefined}
|
|
88
|
+
aria-label={`Digit ${i + 1}`}
|
|
89
|
+
onChange={e => handleChange(i, e.target.value)}
|
|
90
|
+
onKeyDown={e => handleKeyDown(i, e)}
|
|
91
|
+
onFocus={e => e.target.select()}
|
|
92
|
+
className={cn(
|
|
93
|
+
"size-11 text-center text-heading-4 [border-radius:var(--radius-input)] border bg-[var(--color-bg-raised)] outline-none transition-[color,box-shadow]",
|
|
94
|
+
"border-[var(--color-border-default)]",
|
|
95
|
+
"focus-visible:border-[var(--color-border-focus)] focus-visible:ring-[3px] focus-visible:ring-[var(--color-border-focus)]/50",
|
|
96
|
+
"aria-invalid:border-[var(--color-border-error)] aria-invalid:ring-[3px] aria-invalid:ring-[var(--color-border-error)]/20",
|
|
97
|
+
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
98
|
+
)}
|
|
99
|
+
/>
|
|
100
|
+
))}
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export { InputOTP }
|
|
106
|
+
export type { InputOTPProps }
|
|
@@ -7,6 +7,7 @@ import { Slot } from "radix-ui"
|
|
|
7
7
|
|
|
8
8
|
import { useIsMobile } from "../../hooks/use-mobile"
|
|
9
9
|
import { cn } from "../../lib/utils"
|
|
10
|
+
import { Avatar } from "./avatar"
|
|
10
11
|
import { Button } from "./button"
|
|
11
12
|
import { Input } from "./input"
|
|
12
13
|
import { Separator } from "./separator"
|
|
@@ -368,6 +369,38 @@ function SidebarSeparator({
|
|
|
368
369
|
)
|
|
369
370
|
}
|
|
370
371
|
|
|
372
|
+
// Footer user block: avatar + name/role, with an optional trailing action
|
|
373
|
+
// (e.g. a kebab dropdown trigger). Composed for the sidebar footer.
|
|
374
|
+
function SidebarUser({
|
|
375
|
+
name,
|
|
376
|
+
role,
|
|
377
|
+
avatarSrc,
|
|
378
|
+
avatarBackground,
|
|
379
|
+
action,
|
|
380
|
+
className,
|
|
381
|
+
}: {
|
|
382
|
+
name: string
|
|
383
|
+
role?: string
|
|
384
|
+
avatarSrc?: string
|
|
385
|
+
avatarBackground?: string
|
|
386
|
+
action?: React.ReactNode
|
|
387
|
+
className?: string
|
|
388
|
+
}) {
|
|
389
|
+
return (
|
|
390
|
+
<div
|
|
391
|
+
data-slot="sidebar-user"
|
|
392
|
+
className={cn("flex items-center gap-2.5 px-2 py-1.5", className)}
|
|
393
|
+
>
|
|
394
|
+
<Avatar name={name} src={avatarSrc} background={avatarBackground} />
|
|
395
|
+
<div className="flex flex-col gap-0.5 flex-1 min-w-0">
|
|
396
|
+
<span className="text-body-sm text-[var(--color-text-default)] truncate">{name}</span>
|
|
397
|
+
{role && <span className="text-caption-light text-[var(--color-text-muted)] truncate">{role}</span>}
|
|
398
|
+
</div>
|
|
399
|
+
{action}
|
|
400
|
+
</div>
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
|
|
371
404
|
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
372
405
|
return (
|
|
373
406
|
<div
|
|
@@ -722,5 +755,6 @@ export {
|
|
|
722
755
|
SidebarRail,
|
|
723
756
|
SidebarSeparator,
|
|
724
757
|
SidebarTrigger,
|
|
758
|
+
SidebarUser,
|
|
725
759
|
useSidebar,
|
|
726
760
|
}
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { usePlatform } from "./hooks/use-platform"
|
|
|
6
6
|
|
|
7
7
|
// Components
|
|
8
8
|
export * from "./components/ui/alert"
|
|
9
|
+
export * from "./components/ui/avatar"
|
|
9
10
|
export * from "./components/ui/badge"
|
|
10
11
|
export * from "./components/ui/breadcrumb"
|
|
11
12
|
export * from "./components/ui/calendar"
|
|
@@ -26,6 +27,7 @@ export * from "./components/ui/filter-combobox"
|
|
|
26
27
|
export * from "./components/ui/search-bar"
|
|
27
28
|
export * from "./components/ui/input"
|
|
28
29
|
export * from "./components/ui/input-group"
|
|
30
|
+
export * from "./components/ui/input-otp"
|
|
29
31
|
export * from "./components/ui/kbd"
|
|
30
32
|
export * from "./components/ui/label"
|
|
31
33
|
export * from "./components/ui/pagination"
|
|
@@ -41,3 +43,6 @@ export * from "./components/ui/tab-count"
|
|
|
41
43
|
export * from "./components/ui/tabs"
|
|
42
44
|
export * from "./components/ui/textarea"
|
|
43
45
|
export * from "./components/ui/tooltip"
|
|
46
|
+
|
|
47
|
+
// Brand textures + logo
|
|
48
|
+
export * from "./textures"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "../lib/utils"
|
|
3
|
+
import { TEXTURES, type TextureName } from "./index"
|
|
4
|
+
|
|
5
|
+
/** The five sanctioned brand pairs. */
|
|
6
|
+
export type BrandPair =
|
|
7
|
+
| "yellow-darkforest"
|
|
8
|
+
| "rose-coral"
|
|
9
|
+
| "blue-navy"
|
|
10
|
+
| "beige-bronze"
|
|
11
|
+
| "gray-black"
|
|
12
|
+
|
|
13
|
+
export interface BrandTextureProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
14
|
+
/** Which texture motif to render. */
|
|
15
|
+
name: TextureName
|
|
16
|
+
/** Which brand pair colors it. */
|
|
17
|
+
pair: BrandPair
|
|
18
|
+
/**
|
|
19
|
+
* Swap which member is ink vs background. By default the pair's dark member
|
|
20
|
+
* is the ink (shapes) and the light member is the background.
|
|
21
|
+
*/
|
|
22
|
+
reverse?: boolean
|
|
23
|
+
/**
|
|
24
|
+
* How the motif fills the box. `cover` (default) crops to fill — right for
|
|
25
|
+
* edge-to-edge patterns; `contain` fits the whole motif without cropping —
|
|
26
|
+
* right for the logo or any motif that must stay intact.
|
|
27
|
+
*/
|
|
28
|
+
fit?: "cover" | "contain"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Renders a brand texture colored by a sanctioned pair. Sets --texture-ink and
|
|
33
|
+
* --texture-bg on a wrapper; the inlined SVG reads them. The wrapper clips the
|
|
34
|
+
* texture, so size it via className (e.g. `h-40 w-full`).
|
|
35
|
+
*/
|
|
36
|
+
function BrandTexture({ name, pair, reverse, fit = "cover", className, style, ...props }: BrandTextureProps) {
|
|
37
|
+
const Texture = TEXTURES[name]
|
|
38
|
+
const light = `var(--color-pair-${pair}-light)`
|
|
39
|
+
const dark = `var(--color-pair-${pair}-dark)`
|
|
40
|
+
|
|
41
|
+
const vars = {
|
|
42
|
+
"--texture-ink": reverse ? light : dark,
|
|
43
|
+
"--texture-bg": reverse ? dark : light,
|
|
44
|
+
backgroundColor: "var(--texture-bg)",
|
|
45
|
+
} as React.CSSProperties
|
|
46
|
+
|
|
47
|
+
const par = fit === "contain" ? "xMidYMid meet" : "xMidYMid slice"
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
className={cn("relative overflow-hidden", className)}
|
|
52
|
+
style={{ ...vars, ...style }}
|
|
53
|
+
{...props}
|
|
54
|
+
>
|
|
55
|
+
<Texture className="absolute inset-0 size-full" preserveAspectRatio={par} />
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { BrandTexture }
|