@a4ui/core 0.13.0 → 0.13.1

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.
@@ -8,23 +8,23 @@ export interface StackNotification {
8
8
  export interface NotificationStackProps {
9
9
  /** Newest first. The parent owns this array. */
10
10
  items: StackNotification[];
11
- /** How many cards are visible before the rest collapse behind. @default 3 */
11
+ /** How many cards peek when collapsed. @default 3 */
12
12
  max?: number;
13
13
  /** Fired when a card is dismissed; the parent should drop it from `items`. */
14
14
  onDismiss?: (id: string | number) => void;
15
15
  class?: string;
16
16
  }
17
17
  /**
18
- * Stacked notifications. Cards behind the front one are offset down and scaled,
19
- * so they peek from the bottom edge; removing the front card promotes the rest
20
- * with a smooth transition (skipped under reduced motion). New cards fade/slide
21
- * in via `animateIn`.
18
+ * Stacked notifications. Collapsed, cards behind the front one are offset down
19
+ * and scaled so they peek from the bottom edge; click the stack to expand every
20
+ * notification into a scrollable list, and "Show less" to collapse. Removing the
21
+ * front card promotes the rest; new cards fade/slide in via `animateIn`.
22
22
  *
23
23
  * @example
24
24
  * ```tsx
25
- * const [items, setItems] = createStore<StackNotification[]>([])
25
+ * const [items, setItems] = createSignal<StackNotification[]>([])
26
26
  * <NotificationStack
27
- * items={items}
27
+ * items={items()}
28
28
  * onDismiss={(id) => setItems((xs) => xs.filter((x) => x.id !== id))}
29
29
  * />
30
30
  * ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a4ui/core",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "A4ui — Spatial Glass design system & component library for SolidJS (Kobalte behavior + Tailwind glass tokens + motion).",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  // import '@a4ui/core/styles.css'
9
9
  // import { Button, Card, Modal } from '@a4ui/core'
10
10
 
11
- export const A4UI_VERSION = '0.13.0'
11
+ export const A4UI_VERSION = '0.13.1'
12
12
 
13
13
  // Helpers (src/lib) — generic, framework-level utilities.
14
14
  export { cn } from './lib/cn'
@@ -1,8 +1,9 @@
1
- // A stacked-notifications widget (phone lock-screen style): the newest card is
2
- // full-size on top and older cards peek out behind it, scaled down and offset.
3
- // Presentational + controlled the parent owns `items` (newest first) and
4
- // removes a card when `onDismiss` fires; the stack re-flows with a CSS transition.
5
- import { For, type JSX } from 'solid-js'
1
+ // A stacked-notifications widget (phone lock-screen style): collapsed, the
2
+ // newest card is full-size on top and older ones peek out behind it, scaled
3
+ // down and offset. Click the stack (or "Show all") to EXPAND it into a full,
4
+ // scrollable list; "Show less" collapses it back. Presentational + controlled
5
+ // the parent owns `items` (newest first) and removes a card on `onDismiss`.
6
+ import { createSignal, For, Show, type JSX } from 'solid-js'
6
7
 
7
8
  import { cn } from '../lib/cn'
8
9
  import { animateIn, motionReduced } from '../lib/motion'
@@ -17,7 +18,7 @@ export interface StackNotification {
17
18
  export interface NotificationStackProps {
18
19
  /** Newest first. The parent owns this array. */
19
20
  items: StackNotification[]
20
- /** How many cards are visible before the rest collapse behind. @default 3 */
21
+ /** How many cards peek when collapsed. @default 3 */
21
22
  max?: number
22
23
  /** Fired when a card is dismissed; the parent should drop it from `items`. */
23
24
  onDismiss?: (id: string | number) => void
@@ -25,80 +26,114 @@ export interface NotificationStackProps {
25
26
  }
26
27
 
27
28
  /**
28
- * Stacked notifications. Cards behind the front one are offset down and scaled,
29
- * so they peek from the bottom edge; removing the front card promotes the rest
30
- * with a smooth transition (skipped under reduced motion). New cards fade/slide
31
- * in via `animateIn`.
29
+ * Stacked notifications. Collapsed, cards behind the front one are offset down
30
+ * and scaled so they peek from the bottom edge; click the stack to expand every
31
+ * notification into a scrollable list, and "Show less" to collapse. Removing the
32
+ * front card promotes the rest; new cards fade/slide in via `animateIn`.
32
33
  *
33
34
  * @example
34
35
  * ```tsx
35
- * const [items, setItems] = createStore<StackNotification[]>([])
36
+ * const [items, setItems] = createSignal<StackNotification[]>([])
36
37
  * <NotificationStack
37
- * items={items}
38
+ * items={items()}
38
39
  * onDismiss={(id) => setItems((xs) => xs.filter((x) => x.id !== id))}
39
40
  * />
40
41
  * ```
41
42
  */
42
43
  export function NotificationStack(props: NotificationStackProps): JSX.Element {
43
44
  const max = () => props.max ?? 3
45
+ const [expanded, setExpanded] = createSignal(false)
46
+ const canExpand = () => props.items.length > 1
44
47
 
45
48
  return (
46
- <div
47
- role="log"
48
- aria-live="polite"
49
- class={cn('relative w-full max-w-sm', props.class)}
50
- // reserve room for the front card plus the peek of those behind it
51
- style={{ 'min-height': `${88 + (Math.min(props.items.length, max()) - 1) * 10}px` }}
52
- >
53
- <For each={props.items}>
54
- {(item, i) => {
55
- const hidden = () => i() > max() - 1
56
- return (
57
- <div
58
- ref={(el) => animateIn(el, { y: -8 })}
59
- class={cn(
60
- 'card absolute inset-x-0 top-0 rounded-xl border border-border bg-card p-3 shadow-lg',
61
- !motionReduced() && 'transition-[transform,opacity] duration-300 ease-out',
62
- )}
63
- style={{
64
- transform: `translateY(${i() * 10}px) scale(${1 - i() * 0.05})`,
65
- opacity: hidden() ? 0 : 1 - i() * 0.15,
66
- 'z-index': props.items.length - i(),
67
- 'pointer-events': hidden() ? 'none' : 'auto',
68
- 'transform-origin': 'top center',
69
- }}
70
- >
71
- <div class="flex items-start gap-2.5">
72
- {item.icon && <span class="mt-0.5 shrink-0 text-primary">{item.icon}</span>}
73
- <div class="min-w-0 flex-1">
74
- <p class="truncate text-sm font-medium text-foreground">{item.title}</p>
75
- {item.description && (
76
- <p class="mt-0.5 line-clamp-2 text-sm text-muted-foreground">{item.description}</p>
77
- )}
78
- </div>
79
- <button
80
- type="button"
81
- aria-label={`Dismiss ${item.title}`}
82
- onClick={() => props.onDismiss?.(item.id)}
83
- class="shrink-0 rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
84
- >
85
- <svg
86
- viewBox="0 0 24 24"
87
- class="h-3.5 w-3.5"
88
- fill="none"
89
- stroke="currentColor"
90
- stroke-width="2.5"
91
- stroke-linecap="round"
92
- aria-hidden="true"
49
+ <div role="log" aria-live="polite" class={cn('w-full max-w-sm', props.class)}>
50
+ <div
51
+ class={cn(
52
+ expanded()
53
+ ? 'max-h-[60vh] space-y-2 overflow-y-auto pr-1'
54
+ : cn('relative', canExpand() && 'cursor-pointer'),
55
+ )}
56
+ style={
57
+ expanded()
58
+ ? undefined
59
+ : // reserve room for the front card plus the peek of those behind it
60
+ { 'min-height': `${88 + (Math.min(props.items.length, max()) - 1) * 10}px` }
61
+ }
62
+ onClick={() => {
63
+ if (!expanded() && canExpand()) setExpanded(true)
64
+ }}
65
+ >
66
+ <For each={props.items}>
67
+ {(item, i) => {
68
+ const collapsedHidden = () => !expanded() && i() > max() - 1
69
+ return (
70
+ <div
71
+ ref={(el) => animateIn(el, { y: -8 })}
72
+ class={cn(
73
+ 'card rounded-xl border border-border bg-card p-3 shadow-lg',
74
+ expanded() ? 'relative' : 'absolute inset-x-0 top-0',
75
+ !motionReduced() && !expanded() && 'transition-[transform,opacity] duration-300 ease-out',
76
+ )}
77
+ style={
78
+ expanded()
79
+ ? undefined
80
+ : {
81
+ transform: `translateY(${i() * 10}px) scale(${1 - i() * 0.05})`,
82
+ opacity: collapsedHidden() ? 0 : 1 - i() * 0.15,
83
+ 'z-index': props.items.length - i(),
84
+ 'pointer-events': collapsedHidden() ? 'none' : 'auto',
85
+ 'transform-origin': 'top center',
86
+ }
87
+ }
88
+ >
89
+ <div class="flex items-start gap-2.5">
90
+ {item.icon && <span class="mt-0.5 shrink-0 text-primary">{item.icon}</span>}
91
+ <div class="min-w-0 flex-1">
92
+ <p class={cn('text-sm font-medium text-foreground', !expanded() && 'truncate')}>
93
+ {item.title}
94
+ </p>
95
+ {item.description && (
96
+ <p class="mt-0.5 line-clamp-2 text-sm text-muted-foreground">{item.description}</p>
97
+ )}
98
+ </div>
99
+ <button
100
+ type="button"
101
+ aria-label={`Dismiss ${item.title}`}
102
+ onClick={(e) => {
103
+ e.stopPropagation()
104
+ props.onDismiss?.(item.id)
105
+ }}
106
+ class="shrink-0 rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
93
107
  >
94
- <path d="M6 6l12 12M18 6L6 18" />
95
- </svg>
96
- </button>
108
+ <svg
109
+ viewBox="0 0 24 24"
110
+ class="h-3.5 w-3.5"
111
+ fill="none"
112
+ stroke="currentColor"
113
+ stroke-width="2.5"
114
+ stroke-linecap="round"
115
+ aria-hidden="true"
116
+ >
117
+ <path d="M6 6l12 12M18 6L6 18" />
118
+ </svg>
119
+ </button>
120
+ </div>
97
121
  </div>
98
- </div>
99
- )
100
- }}
101
- </For>
122
+ )
123
+ }}
124
+ </For>
125
+ </div>
126
+
127
+ <Show when={canExpand()}>
128
+ <button
129
+ type="button"
130
+ aria-expanded={expanded()}
131
+ onClick={() => setExpanded((v) => !v)}
132
+ class="mt-2 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground"
133
+ >
134
+ {expanded() ? 'Show less' : `Show all ${props.items.length}`}
135
+ </button>
136
+ </Show>
102
137
  </div>
103
138
  )
104
139
  }