minka-ds 0.3.10 → 0.3.12

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minka-ds",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "description": "Minka product design system — tokenized component library",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -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 }
@@ -29,32 +29,37 @@ export function DateTimeRangePicker({
29
29
  const range: DateRange | undefined =
30
30
  value?.from ? { from: value.from, to: value.to } : undefined
31
31
 
32
- // A range is "complete" once from and to differ (or a real end was picked).
33
- const isComplete = Boolean(value?.from && value?.to && value.from.getTime() !== value.to.getTime())
32
+ // `anchor` is the source of truth for selection phase:
33
+ // anchor === null no active pick (nothing selected, or a complete range)
34
+ // anchor !== null → first date is set, waiting for the second click
35
+ // This avoids the ambiguity of inferring phase from `from === to`.
36
+ const [anchor, setAnchor] = React.useState<Date | null>(null)
34
37
 
35
- function handleRangeSelect(
36
- selected: DateRange | undefined,
37
- selectedDay: Date,
38
- ) {
39
- // If a complete range already exists, any click starts a brand-new range
40
- // anchored on the clicked day — instead of extending the old one. This also
41
- // frees the user from the max-range cap that was anchored on the old `from`.
42
- if (isComplete) {
43
- onChange({
44
- from: selectedDay,
45
- to: selectedDay,
46
- startTime: value?.startTime ?? "",
47
- endTime: value?.endTime ?? "",
48
- })
38
+ function handleDay(day: Date) {
39
+ const startTime = value?.startTime ?? ""
40
+ const endTime = value?.endTime ?? ""
41
+
42
+ // Picking the second date.
43
+ if (anchor) {
44
+ const spanMs = Math.abs(day.getTime() - anchor.getTime())
45
+ const withinCap = maxRangeDays == null || spanMs <= maxRangeDays * 86_400_000
46
+ if (withinCap) {
47
+ // Complete the range (order endpoints; can extend backward or forward).
48
+ const from = day < anchor ? day : anchor
49
+ const to = day < anchor ? anchor : day
50
+ setAnchor(null)
51
+ onChange({ from, to, startTime, endTime })
52
+ } else {
53
+ // Outside the cap → treat as a fresh start anchored on the clicked day.
54
+ setAnchor(day)
55
+ onChange({ from: day, to: day, startTime, endTime })
56
+ }
49
57
  return
50
58
  }
51
- if (!selected?.from) { onChange(null); return }
52
- onChange({
53
- from: selected.from,
54
- to: selected.to ?? selected.from,
55
- startTime: value?.startTime ?? "",
56
- endTime: value?.endTime ?? "",
57
- })
59
+
60
+ // No active pick (fresh, or restarting from a complete range).
61
+ setAnchor(day)
62
+ onChange({ from: day, to: day, startTime, endTime })
58
63
  }
59
64
 
60
65
  function handleStartTime(e: React.ChangeEvent<HTMLInputElement>) {
@@ -67,14 +72,6 @@ export function DateTimeRangePicker({
67
72
  onChange({ ...value, endTime: e.target.value })
68
73
  }
69
74
 
70
- // Only cap the calendar while the user is picking the second date (from set,
71
- // range not yet complete). Once complete, all dates stay clickable so a fresh
72
- // click elsewhere can start a new range.
73
- const disabledAfter =
74
- maxRangeDays && range?.from && !isComplete
75
- ? { after: new Date(range.from.getTime() + maxRangeDays * 86_400_000) }
76
- : undefined
77
-
78
75
  return (
79
76
  <div className={cn(
80
77
  "[border-radius:var(--radius-card)] border border-[var(--color-border-default)] bg-[var(--color-bg-raised)] overflow-hidden w-fit",
@@ -85,8 +82,17 @@ export function DateTimeRangePicker({
85
82
  numberOfMonths={1}
86
83
  captionLayout="label"
87
84
  selected={range}
88
- onSelect={handleRangeSelect}
89
- disabled={disabledAfter}
85
+ onSelect={(_, selectedDay) => handleDay(selectedDay)}
86
+ // While picking the second date, soften days outside the ±maxRangeDays
87
+ // window — a visual hint of the recommended span. They stay clickable
88
+ // (clicking one re-anchors) and hover still works; this is a hint, not
89
+ // a block.
90
+ modifiers={
91
+ anchor && maxRangeDays != null
92
+ ? { outOfRange: (d: Date) => Math.abs(d.getTime() - anchor.getTime()) > maxRangeDays * 86_400_000 }
93
+ : undefined
94
+ }
95
+ modifiersClassNames={{ outOfRange: "text-[var(--color-text-hint)]" }}
90
96
  />
91
97
  <div className="border-t border-[var(--color-border-default)] px-4 py-3 flex flex-col gap-3">
92
98
  <div className="flex flex-col gap-1.5">
@@ -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"