@cl-light-ds/ui 0.3.0 → 0.5.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.
Files changed (103) hide show
  1. package/package.json +20 -18
  2. package/src/components/alert/Alert.astro +8 -2
  3. package/src/components/alert/CHANGELOG.md +9 -0
  4. package/src/components/button/Button.astro +78 -19
  5. package/src/components/button/CHANGELOG.md +22 -0
  6. package/src/components/button-group/ButtonGroup.astro +14 -3
  7. package/src/components/button-group/CHANGELOG.md +17 -0
  8. package/src/components/callout/CHANGELOG.md +32 -0
  9. package/src/components/callout/Callout.astro +147 -0
  10. package/src/components/callout/CalloutDescription.astro +16 -0
  11. package/src/components/callout/CalloutTitle.astro +16 -0
  12. package/src/components/callout/index.ts +3 -0
  13. package/src/components/card/CHANGELOG.md +9 -0
  14. package/src/components/card/Card.astro +22 -0
  15. package/src/components/combobox/CHANGELOG.md +14 -0
  16. package/src/components/combobox/ComboboxTrigger.astro +31 -1
  17. package/src/components/command-search/CHANGELOG.md +27 -0
  18. package/src/components/command-search/CommandSearch.astro +515 -0
  19. package/src/components/command-search/CommandSearchGroup.astro +32 -0
  20. package/src/components/command-search/CommandSearchItem.astro +64 -0
  21. package/src/components/command-search/classes.ts +18 -0
  22. package/src/components/command-search/index.ts +12 -0
  23. package/src/components/command-search/nothing-matches.svg +42 -0
  24. package/src/components/dialog/CHANGELOG.md +13 -0
  25. package/src/components/dialog/DialogClose.astro +26 -12
  26. package/src/components/dropdown-menu/CHANGELOG.md +7 -0
  27. package/src/components/dropdown-menu/dropdown-menu-item-variants.ts +1 -1
  28. package/src/components/empty-state/CHANGELOG.md +18 -0
  29. package/src/components/empty-state/EmptyState.astro +157 -0
  30. package/src/components/empty-state/illustrations/AllCaughtUp.astro +52 -0
  31. package/src/components/empty-state/illustrations/EmptyWorkspaces.astro +56 -0
  32. package/src/components/empty-state/illustrations/LoadError.astro +61 -0
  33. package/src/components/empty-state/illustrations/NoResults.astro +60 -0
  34. package/src/components/empty-state/illustrations/NothingMatches.astro +66 -0
  35. package/src/components/empty-state/illustrations/index.ts +27 -0
  36. package/src/components/empty-state/index.ts +7 -0
  37. package/src/components/favorite-toggle/CHANGELOG.md +13 -0
  38. package/src/components/favorite-toggle/FavoriteToggle.astro +94 -0
  39. package/src/components/favorite-toggle/index.ts +1 -0
  40. package/src/components/input/CHANGELOG.md +15 -0
  41. package/src/components/input/InputOTP.astro +211 -0
  42. package/src/components/input/index.ts +1 -1
  43. package/src/components/input-group/CHANGELOG.md +7 -0
  44. package/src/components/input-group/InputGroup.astro +2 -0
  45. package/src/components/loading-overlay/CHANGELOG.md +25 -0
  46. package/src/components/loading-overlay/LoadingOverlay.astro +179 -0
  47. package/src/components/loading-overlay/index.ts +8 -0
  48. package/src/components/logo/CHANGELOG.md +17 -0
  49. package/src/components/logo/Logo.astro +90 -0
  50. package/src/components/logo/index.ts +3 -0
  51. package/src/components/logo/logo-paths.ts +57 -0
  52. package/src/components/notifications-menu/CHANGELOG.md +22 -0
  53. package/src/components/notifications-menu/NotificationsMenu.astro +374 -0
  54. package/src/components/notifications-menu/NotificationsMenuItem.astro +129 -0
  55. package/src/components/notifications-menu/all-caught-up.svg +28 -0
  56. package/src/components/notifications-menu/index.ts +3 -0
  57. package/src/components/notifications-menu/types.ts +32 -0
  58. package/src/components/option-card/CHANGELOG.md +16 -0
  59. package/src/components/option-card/OptionCard.astro +135 -0
  60. package/src/components/option-card/index.ts +1 -0
  61. package/src/components/organisms/top-bar/CHANGELOG.md +21 -0
  62. package/src/components/organisms/top-bar/TopBar.astro +264 -0
  63. package/src/components/organisms/top-bar/index.ts +2 -0
  64. package/src/components/organisms/top-bar/types.ts +30 -0
  65. package/src/components/organisms/workspace/CHANGELOG.md +73 -0
  66. package/src/components/organisms/workspace/OrganisationOverview.astro +62 -0
  67. package/src/components/organisms/workspace/RecentWorkspaceCard.astro +235 -0
  68. package/src/components/organisms/workspace/RecentWorkspaces.astro +93 -0
  69. package/src/components/organisms/workspace/WorkspaceAvatar.astro +144 -0
  70. package/src/components/organisms/workspace/WorkspaceCard.astro +172 -0
  71. package/src/components/organisms/workspace/WorkspaceGlyph.astro +62 -0
  72. package/src/components/organisms/workspace/WorkspaceIcon.astro +42 -0
  73. package/src/components/organisms/workspace/WorkspaceIconPicker.astro +288 -0
  74. package/src/components/organisms/workspace/WorkspaceIconPickerGlyph.astro +44 -0
  75. package/src/components/organisms/workspace/icons.ts +98 -0
  76. package/src/components/organisms/workspace/index.ts +35 -0
  77. package/src/components/organisms/workspace/labels.ts +54 -0
  78. package/src/components/organisms/workspace/recent-card-rings-strong.png +0 -0
  79. package/src/components/organisms/workspace/types.ts +30 -0
  80. package/src/components/profile-menu/CHANGELOG.md +22 -0
  81. package/src/components/profile-menu/ProfileMenu.astro +236 -0
  82. package/src/components/profile-menu/index.ts +2 -0
  83. package/src/components/profile-menu/types.ts +27 -0
  84. package/src/components/search-field/CHANGELOG.md +14 -0
  85. package/src/components/search-field/SearchField.astro +163 -0
  86. package/src/components/search-field/index.ts +1 -0
  87. package/src/components/table/CHANGELOG.md +9 -0
  88. package/src/components/table/TableCell.astro +6 -0
  89. package/src/components/table/TableHead.astro +5 -1
  90. package/src/components/table/TableRow.astro +3 -1
  91. package/src/components/table/table-cell-variants.ts +2 -0
  92. package/src/components/toast/CHANGELOG.md +32 -0
  93. package/src/components/toast/Toast.astro +145 -0
  94. package/src/components/toast/Toaster.astro +322 -0
  95. package/src/components/toast/index.ts +9 -0
  96. package/src/components/toggle/CHANGELOG.md +7 -0
  97. package/src/lib/command-search.ts +73 -0
  98. package/src/lib/loading-overlay.ts +174 -0
  99. package/src/lib/toast.ts +127 -0
  100. package/src/lib/toggle-shared.ts +4 -1
  101. package/src/lib/workspace-palette.ts +119 -0
  102. package/src/styles/CHANGELOG.md +22 -0
  103. package/src/styles/globals.css +100 -1
@@ -0,0 +1,145 @@
1
+ ---
2
+ /**
3
+ * @component Toast
4
+ * @title Toast
5
+ * @description One toast: an icon, a title and a line of text, an optional action and a ✕. `Toaster` uses it as the template for every toast it shows; on its own it renders a static toast (docs, screenshots).
6
+ *
7
+ * @preview
8
+ *
9
+ * <ol class="flex w-96 max-w-full list-none flex-col gap-3 p-0">
10
+ * <Toast kind="info" title="Export started" description="We'll let you know when Acme Finance is ready." />
11
+ * <Toast kind="success" title="Export ready" description="Acme Finance export.zip" action="Download" />
12
+ * <Toast kind="error" title="Couldn't open workspace" description="Something went wrong opening Acme Finance." />
13
+ * </ol>
14
+ *
15
+ * @usage
16
+ *
17
+ * ```astro nocollapse
18
+ * ---
19
+ * import { Toast } from '@cl-light-ds/ui/components/toast';
20
+ * ---
21
+ *
22
+ * <ol class="list-none p-0">
23
+ * <Toast kind="success" title="Export ready" description="Acme.zip" action="Download" />
24
+ * </ol>
25
+ * ```
26
+ *
27
+ * Kinds: `info` (blue icon), `success` (green icon), `error` (light red tint,
28
+ * red border, red icon and title). The icon and the ✕ sit at the top; the
29
+ * action (an outline button) is centred on the right (D168, D169).
30
+ *
31
+ * Inside a `Toaster`, `@data-slot/toast` fills `toast-title`,
32
+ * `toast-description` and `toast-action`, and sets `data-type` (the kind),
33
+ * `data-front`, `data-expanded` and `data-state` on the item. Once a toast is
34
+ * in, the Toaster sets `data-attention="on"` for 2.4s: a ring ripples out
35
+ * three times (info, success) or the card shakes (error) (D164, D175).
36
+ */
37
+ import { CircleCheck, CircleX, Info, X } from "@lucide/astro";
38
+ import { cn } from "@cl-light-ds/ui/lib/utils";
39
+ import type { HTMLAttributes } from "astro/types";
40
+
41
+ import Button from "../button/Button.astro";
42
+
43
+ interface Props extends HTMLAttributes<"li"> {
44
+ kind?: "info" | "success" | "error";
45
+ title?: string;
46
+ description?: string;
47
+ /** The action button's label; no action when left out. */
48
+ action?: string;
49
+ /** Accessible name of the ✕. */
50
+ closeLabel?: string;
51
+ class?: string;
52
+ }
53
+
54
+ const {
55
+ kind = "info",
56
+ title = "",
57
+ description = "",
58
+ action = "",
59
+ closeLabel = "Close",
60
+ class: className = "",
61
+ ...rest
62
+ } = Astro.props as Props;
63
+
64
+ const error = kind === "error";
65
+ ---
66
+
67
+ <li
68
+ data-slot="toast-item"
69
+ data-type={kind}
70
+ role={error ? "alert" : "status"}
71
+ aria-live={error ? "assertive" : "polite"}
72
+ aria-atomic="true"
73
+ class={cn("group/toast list-none", className)}
74
+ {...rest}
75
+ >
76
+ {/* D164, D175 — the ripple: three rings in the kind's colour (not on errors). */}
77
+ <span
78
+ data-slot="toast-ripple"
79
+ aria-hidden="true"
80
+ class="ring-primary group-data-[type=success]/toast:ring-success pointer-events-none absolute inset-0 rounded-lg opacity-0 ring-2 group-data-[type=error]/toast:hidden group-data-[attention=on]/toast:animate-out group-data-[attention=on]/toast:fade-out group-data-[attention=on]/toast:zoom-out-105 group-data-[attention=on]/toast:fill-mode-forwards group-data-[attention=on]/toast:repeat-3 group-data-[attention=on]/toast:animation-duration-700 group-data-[attention=on]/toast:opacity-100 motion-reduce:hidden"
81
+ ></span>
82
+ <div
83
+ data-slot="toast-card"
84
+ class="bg-popover text-popover-foreground group-data-[type=error]/toast:bg-callout-error group-data-[type=error]/toast:border-destructive/40 relative h-full overflow-hidden rounded-lg border p-4 pe-10 shadow-md transition-shadow group-data-[expanded=true]/toast:shadow-xs group-data-[attention=on]/toast:group-data-[type=error]/toast:animate-in group-data-[attention=on]/toast:group-data-[type=error]/toast:slide-in-from-left-3 group-data-[attention=on]/toast:group-data-[type=error]/toast:direction-alternate group-data-[attention=on]/toast:group-data-[type=error]/toast:repeat-7 group-data-[attention=on]/toast:group-data-[type=error]/toast:animation-duration-75 motion-reduce:animate-none!"
85
+ >
86
+ <div data-slot="toast-body" class="flex items-start gap-3">
87
+ <Info
88
+ aria-hidden="true"
89
+ class="text-primary mt-0.5 size-5 shrink-0 group-data-[type=error]/toast:hidden group-data-[type=success]/toast:hidden"
90
+ />
91
+ <CircleCheck
92
+ aria-hidden="true"
93
+ class="text-success mt-0.5 hidden size-5 shrink-0 group-data-[type=success]/toast:block"
94
+ />
95
+ <CircleX
96
+ aria-hidden="true"
97
+ class="text-destructive mt-0.5 hidden size-5 shrink-0 group-data-[type=error]/toast:block"
98
+ />
99
+ <div class="flex min-w-0 flex-1 flex-col gap-1">
100
+ <p
101
+ data-slot="toast-title"
102
+ class="text-sm font-semibold group-data-[type=error]/toast:text-destructive"
103
+ >
104
+ {title}
105
+ </p>
106
+ <p
107
+ data-slot="toast-description"
108
+ class="text-muted-foreground text-sm group-data-[type=error]/toast:text-foreground"
109
+ hidden={!description}
110
+ >
111
+ {description}
112
+ </p>
113
+ </div>
114
+ <Button
115
+ data-slot="toast-action"
116
+ type="button"
117
+ size="sm"
118
+ variant="outline"
119
+ class="shrink-0 self-center"
120
+ hidden={!action}
121
+ >
122
+ {action}
123
+ </Button>
124
+ </div>
125
+ <Button
126
+ data-slot="toast-close"
127
+ type="button"
128
+ variant="ghost"
129
+ size="icon-sm"
130
+ aria-label={closeLabel}
131
+ class="group-data-[type=error]/toast:hover:bg-destructive/10 absolute top-2 right-2"
132
+ >
133
+ <X />
134
+ </Button>
135
+ </div>
136
+ </li>
137
+
138
+ <style is:global>
139
+ /* In a layer, so the Toaster's stack rules (more specific) take over inside it. */
140
+ @layer components {
141
+ [data-slot="toast-item"] {
142
+ position: relative;
143
+ }
144
+ }
145
+ </style>
@@ -0,0 +1,322 @@
1
+ ---
2
+ /**
3
+ * @component Toaster
4
+ * @title Toaster
5
+ * @description The toast stack: a live region under the top bar's right end where toasts slide in from the right, stack (the newest in front, two peeking out below) and unfurl into a list with Clear all on hover or keyboard focus.
6
+ *
7
+ * @preview
8
+ *
9
+ * <Toaster />
10
+ * <Button variant="outline" data-demo-toast="success">Show a toast</Button>
11
+ *
12
+ * @usage
13
+ *
14
+ * ```astro nocollapse
15
+ * ---
16
+ * import { Toaster } from '@cl-light-ds/ui/components/toast';
17
+ * ---
18
+ *
19
+ * <!-- once per page, e.g. in the layout -->
20
+ * <Toaster top="41px" />
21
+ *
22
+ * <script>
23
+ * import { toast } from '@cl-light-ds/ui/lib/toast';
24
+ *
25
+ * toast({ kind: 'success', title: 'Export ready', description: 'Acme Finance export.zip',
26
+ * action: { label: 'Download', href: '/exports/acme.zip', download: true } });
27
+ * </script>
28
+ * ```
29
+ *
30
+ * @api
31
+ *
32
+ * ### JS (`@cl-light-ds/ui/lib/toast`)
33
+ *
34
+ * | Function | Description |
35
+ * |----------|-------------|
36
+ * | `toast({ kind, title, description, action, duration, id, closeLabel, toaster })` | Show a toast, returns its id |
37
+ * | `toast.update(id, patch)` | Change a showing toast |
38
+ * | `toast.dismiss(id)` | Close one |
39
+ * | `toast.clear()` | Close all |
40
+ *
41
+ * ### Events (on the `[data-slot="toast"]` root, from `@data-slot/toast`)
42
+ *
43
+ * Inbound: `toast:show` (`{ title, description, type, action, duration, id }`), `toast:update`, `toast:dismiss`, `toast:clear`.
44
+ * Outbound: `toast:change` (`{ id, action: "show" | "dismiss" }`), `toast:action` (`{ id, value }`), and `toaster:ready` once bound.
45
+ *
46
+ * ### Placement
47
+ *
48
+ * Fixed; `top` is where the top bar ends (default 41px, the top bar's height), `offset` the gap under it, `right` the inset from the
49
+ * window's right edge (plus `--data-slot-scrollbar-width` while a dialog hides the scrollbar). Unfurled with two or more toasts the
50
+ * gap closes so Clear all sits flush under the bar (D172).
51
+ */
52
+ import { cn } from "@cl-light-ds/ui/lib/utils";
53
+ import type { HTMLAttributes } from "astro/types";
54
+
55
+ import Button from "../button/Button.astro";
56
+ import Toast from "./Toast.astro";
57
+
58
+ interface Props extends HTMLAttributes<"ol"> {
59
+ /** Where the bar above ends (CSS length). Default 41px. */
60
+ top?: string;
61
+ /** Gap between the bar and the stack (CSS length). Default 0.5rem. */
62
+ offset?: string;
63
+ /** Inset from the window's right edge (CSS length). Default 1rem, 1.5rem from md. */
64
+ right?: string;
65
+ /** ms before a toast closes by itself; 0 = stays until closed (D166). */
66
+ duration?: number;
67
+ /** Toasts kept visible at once; older ones wait, hidden, until others close. */
68
+ limit?: number;
69
+ /** Accessible name of the region. */
70
+ label?: string;
71
+ clearAllLabel?: string;
72
+ closeLabel?: string;
73
+ /** Root id, to target this Toaster from `toast({ toaster })`. */
74
+ id?: string;
75
+ class?: string;
76
+ }
77
+
78
+ const {
79
+ top,
80
+ offset,
81
+ right,
82
+ duration = 0,
83
+ limit = 5,
84
+ label = "Notifications",
85
+ clearAllLabel = "Clear all",
86
+ closeLabel = "Close",
87
+ id,
88
+ class: className = "",
89
+ style,
90
+ ...rest
91
+ } = Astro.props as Props;
92
+
93
+ const vars = [
94
+ top && `--toaster-top: ${top}`,
95
+ offset && `--toaster-offset: ${offset}`,
96
+ right && `--toaster-right: ${right}`,
97
+ ]
98
+ .filter(Boolean)
99
+ .join("; ");
100
+ ---
101
+
102
+ <div
103
+ data-slot="toast"
104
+ data-position="top-right"
105
+ data-duration={duration}
106
+ data-limit={limit}
107
+ id={id}
108
+ class="contents"
109
+ >
110
+ <template data-slot="toast-template">
111
+ <Toast closeLabel={closeLabel} />
112
+ </template>
113
+ <ol
114
+ data-slot="toast-viewport"
115
+ role="region"
116
+ aria-label={label}
117
+ class={cn("group/toaster", className)}
118
+ style={[vars, style].filter(Boolean).join("; ") || undefined}
119
+ {...rest}
120
+ >
121
+ {/* D171, D172, C23 — above the unfurled list: a link button, no hover background, only the underline. */}
122
+ <li data-slot="toaster-clear-all">
123
+ <Button
124
+ type="button"
125
+ variant="link"
126
+ size="sm"
127
+ class="bg-background hover:bg-background rounded-none px-2"
128
+ data-toaster-clear
129
+ >
130
+ {clearAllLabel}
131
+ </Button>
132
+ </li>
133
+ </ol>
134
+ </div>
135
+
136
+ <style is:global>
137
+ /*
138
+ * Stack geometry on top of @data-slot/toast's tokens (--toast-index,
139
+ * --toast-offset, --toast-initial-height, --toast-front-height,
140
+ * --toast-count, --toast-expanded-stack-size). D161, D164, D167: the newest
141
+ * in front, two more peeking out 10px below it, 5% smaller each and blank;
142
+ * unfurled, one under the other 12px apart at their own heights; 300ms.
143
+ */
144
+ @layer components {
145
+ [data-slot="toast-viewport"] {
146
+ --toast-gap: 12px;
147
+ --toast-collapsed-peek: 10px;
148
+ --toaster-slide: 2rem;
149
+ --toaster-band: 0px;
150
+ --toaster-lift: 0px;
151
+ position: fixed;
152
+ z-index: 50;
153
+ top: calc(
154
+ var(--toaster-top, 41px) + var(--toaster-offset, 0.5rem) -
155
+ var(--toaster-lift)
156
+ );
157
+ right: calc(
158
+ var(--toaster-right, 1rem) + var(--data-slot-scrollbar-width, 0px)
159
+ );
160
+ width: min(24rem, calc(100vw - 2 * var(--toaster-right, 1rem)));
161
+ height: calc(
162
+ var(--toast-front-height, 0px) +
163
+ min(var(--toast-count, 0) - 1, 2) * var(--toast-collapsed-peek)
164
+ );
165
+ margin: 0;
166
+ padding: 0;
167
+ list-style: none;
168
+ transition:
169
+ top 300ms ease-out,
170
+ height 300ms ease-out;
171
+ }
172
+ @media (min-width: 48rem) {
173
+ [data-slot="toast-viewport"] {
174
+ --toaster-right: 1.5rem;
175
+ }
176
+ }
177
+ [data-slot="toast-viewport"][data-expanded] {
178
+ height: calc(
179
+ var(--toast-expanded-stack-size, 0px) + var(--toaster-band)
180
+ );
181
+ }
182
+ /* D170–D172 — two or more, unfurled: room for Clear all, flush under the bar. */
183
+ [data-slot="toast-viewport"][data-expanded]:has(
184
+ > [data-slot="toast-item"][data-front="false"][data-removed="false"]
185
+ ) {
186
+ --toaster-band: 2rem;
187
+ --toaster-lift: var(--toaster-offset, 0.5rem);
188
+ }
189
+ [data-slot="toaster-clear-all"] {
190
+ display: none;
191
+ position: absolute;
192
+ top: 0;
193
+ right: 0;
194
+ z-index: 1000;
195
+ }
196
+ [data-slot="toast-viewport"][data-expanded]:has(
197
+ > [data-slot="toast-item"][data-front="false"][data-removed="false"]
198
+ )
199
+ > [data-slot="toaster-clear-all"] {
200
+ display: block;
201
+ }
202
+
203
+ [data-slot="toast-viewport"] > [data-slot="toast-item"] {
204
+ --toast-y: min(
205
+ calc(var(--toast-index, 0) * var(--toast-collapsed-peek)),
206
+ calc(2 * var(--toast-collapsed-peek))
207
+ );
208
+ --toast-s: calc(1 - min(var(--toast-index, 0), 3) * 0.05);
209
+ position: absolute;
210
+ inset-inline: 0;
211
+ top: 0;
212
+ height: var(--toast-initial-height, auto);
213
+ transform-origin: bottom;
214
+ translate: var(--toaster-slide) var(--toast-y);
215
+ scale: var(--toast-s);
216
+ opacity: 0;
217
+ transition:
218
+ translate 300ms ease-out,
219
+ scale 300ms ease-out,
220
+ opacity 300ms ease-out,
221
+ height 300ms ease-out;
222
+ }
223
+ [data-slot="toast-viewport"] > [data-slot="toast-item"][data-expanded="true"] {
224
+ --toast-y: calc(var(--toast-offset, 0px) + var(--toaster-band));
225
+ --toast-s: 1;
226
+ }
227
+ [data-slot="toast-viewport"]
228
+ > [data-slot="toast-item"][data-expanded="false"][data-front="false"] {
229
+ height: var(--toast-front-height);
230
+ }
231
+ /* In: slid home; a fourth one and beyond stay out of sight while stacked. */
232
+ [data-slot="toast-viewport"] > [data-slot="toast-item"][data-mounted="true"] {
233
+ translate: 0 var(--toast-y);
234
+ opacity: clamp(0, 3 - var(--toast-index, 0), 1);
235
+ }
236
+ [data-slot="toast-viewport"]
237
+ > [data-slot="toast-item"][data-mounted="true"][data-expanded="true"] {
238
+ opacity: 1;
239
+ }
240
+ [data-slot="toast-viewport"] > [data-slot="toast-item"][data-swiping="true"] {
241
+ translate: var(--toast-swipe-amount-x, 0px) var(--toast-y);
242
+ transition: none;
243
+ }
244
+ /* Out: back to the right, fading (or off where it was swiped). */
245
+ [data-slot="toast-viewport"]
246
+ > [data-slot="toast-item"][data-mounted][data-removed="true"],
247
+ [data-slot="toast-viewport"]
248
+ > [data-slot="toast-item"][data-mounted][data-visible="false"] {
249
+ translate: var(--toaster-slide) var(--toast-y);
250
+ opacity: 0;
251
+ }
252
+ [data-slot="toast-viewport"]
253
+ > [data-slot="toast-item"][data-mounted][data-swipe-out="true"] {
254
+ translate: var(--toast-swipe-end-x, 0px) var(--toast-y);
255
+ }
256
+ /* Stacked: the ones behind are blank. */
257
+ [data-slot="toast-viewport"]
258
+ > [data-slot="toast-item"]
259
+ :is([data-slot="toast-body"], [data-slot="toast-close"]) {
260
+ transition: opacity 300ms ease-out;
261
+ }
262
+ [data-slot="toast-viewport"]
263
+ > [data-slot="toast-item"][data-expanded="false"][data-front="false"]
264
+ :is([data-slot="toast-body"], [data-slot="toast-close"]) {
265
+ opacity: 0;
266
+ }
267
+
268
+ @media (prefers-reduced-motion: reduce) {
269
+ [data-slot="toast-viewport"] {
270
+ --toaster-slide: 0px;
271
+ transition: none;
272
+ }
273
+ [data-slot="toast-viewport"] > [data-slot="toast-item"] {
274
+ transition: opacity 150ms linear;
275
+ }
276
+ }
277
+ }
278
+ </style>
279
+
280
+ <script>
281
+ import { createToast } from "@data-slot/toast";
282
+
283
+ /** D164 — the attention effect starts once the slide in (300ms) is done. */
284
+ const IN_MS = 300;
285
+ /** D175 — three 700ms ripples (or seven 75ms swings each way). */
286
+ const ATTENTION_MS = 2400;
287
+ const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
288
+
289
+ document.querySelectorAll<HTMLElement>('[data-slot="toast"]').forEach((root) => {
290
+ if (root.dataset.toasterReady === "true") return;
291
+ const viewport = root.querySelector<HTMLElement>('[data-slot="toast-viewport"]');
292
+ if (!viewport) return;
293
+
294
+ const item = (id: string) =>
295
+ viewport.querySelector<HTMLElement>(
296
+ `[data-slot="toast-item"][data-id="${CSS.escape(id)}"]`,
297
+ );
298
+
299
+ const controller = createToast(root, {
300
+ onShow: (id) => {
301
+ // Only the newest draws the eye.
302
+ viewport
303
+ .querySelectorAll<HTMLElement>('[data-slot="toast-item"][data-attention]')
304
+ .forEach((el) => el.removeAttribute("data-attention"));
305
+ if (reducedMotion.matches) return;
306
+ window.setTimeout(() => {
307
+ const el = item(id);
308
+ if (!el || el.dataset.state !== "open" || el.dataset.front !== "true") return;
309
+ el.dataset.attention = "on";
310
+ window.setTimeout(() => el.removeAttribute("data-attention"), ATTENTION_MS);
311
+ }, IN_MS);
312
+ },
313
+ });
314
+
315
+ viewport
316
+ .querySelector<HTMLElement>("[data-toaster-clear]")
317
+ ?.addEventListener("click", () => controller.dismissAll());
318
+
319
+ root.dataset.toasterReady = "true";
320
+ root.dispatchEvent(new CustomEvent("toaster:ready"));
321
+ });
322
+ </script>
@@ -0,0 +1,9 @@
1
+ export { default as Toast } from "./Toast.astro";
2
+ export { default as Toaster } from "./Toaster.astro";
3
+ export { toast } from "../../lib/toast";
4
+ export type {
5
+ ToastKind,
6
+ ToastShowOptions,
7
+ ToastUpdateOptions,
8
+ ToastActionOptions,
9
+ } from "../../lib/toast";
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to the `Toggle` component will be documented in this file.
4
4
 
5
+ ## Date 15th Oct 2026
6
+ ## Workspace selection handover (C12)
7
+
8
+ - Added `variant="ghost"`: no pressed background (the content shows the state, e.g. a filled `favorite` star); the normal hover background on hover, pressed or not. `default` and `outline` are unchanged, since `ToggleGroup` shows its selection with the pressed background.
9
+ - Storybook: "Ghost" preview with the favourite star.
10
+
11
+
5
12
  ## 2026-09-18 — Figma alignment (ClickLearn Lightning Design Library, node `5455-19034`)
6
13
 
7
14
  Figma documents Toggle across four pages: Contents (Default, Clean, Tail Icon, Complex), States (Default, Hover, Pressed, Focus Default, Focus Pressed, Disabled), and Sizes (Small 28px, Medium 32px, Large 36px). Sizing/variant fixes live in the shared `toggleVariants` (`packages/ui/src/lib/toggle-shared.ts`), used by both `Toggle` and `ToggleGroup`.
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Helpers for the `CommandSearch` component
3
+ * (`@cl-light-ds/ui/components/command-search`).
4
+ *
5
+ * ```ts
6
+ * import { setCommandSearchResults } from "@cl-light-ds/ui/lib/command-search";
7
+ *
8
+ * // mode="remote": search on your side, then hand the results back.
9
+ * palette.addEventListener("command-search:search", async (event) => {
10
+ * const { query } = event.detail;
11
+ * setCommandSearchResults(palette, await api.search(query), { query });
12
+ * });
13
+ * ```
14
+ */
15
+
16
+ export interface CommandSearchResultItem {
17
+ /** Unique value, sent with `command-search:select`. */
18
+ value: string;
19
+ /** The row's text. */
20
+ label: string;
21
+ /** Muted text on the right, e.g. the workspace it is in. */
22
+ description?: string;
23
+ /** Navigate here when chosen. */
24
+ href?: string;
25
+ disabled?: boolean;
26
+ }
27
+
28
+ export interface CommandSearchResultGroup {
29
+ heading: string;
30
+ items: CommandSearchResultItem[];
31
+ /** Total matches, when more exist than `items` holds. Defaults to `items.length`. */
32
+ count?: number;
33
+ }
34
+
35
+ type Target = string | HTMLElement | undefined;
36
+
37
+ const findRoot = (target: Target): HTMLElement | null => {
38
+ if (typeof document === "undefined") return null;
39
+ if (target instanceof HTMLElement) return target;
40
+ if (typeof target === "string") return document.getElementById(target);
41
+ return document.querySelector<HTMLElement>("[data-command-search]");
42
+ };
43
+
44
+ /** Open the palette (empty, focused). Defaults to the first on the page. */
45
+ export function openCommandSearch(target?: Target) {
46
+ findRoot(target)?.dispatchEvent(
47
+ new CustomEvent("dialog:set", { detail: { open: true } }),
48
+ );
49
+ }
50
+
51
+ /** Close the palette. */
52
+ export function closeCommandSearch(target?: Target) {
53
+ findRoot(target)?.dispatchEvent(
54
+ new CustomEvent("dialog:set", { detail: { open: false } }),
55
+ );
56
+ }
57
+
58
+ /**
59
+ * End a search in `mode="remote"`: render these groups as the results (or,
60
+ * with no groups, keep the rows you have put in the list yourself). `query`
61
+ * guards against stale answers: results for an older query are ignored.
62
+ */
63
+ export function setCommandSearchResults(
64
+ target: Target,
65
+ groups?: CommandSearchResultGroup[],
66
+ options: { query?: string } = {},
67
+ ) {
68
+ findRoot(target)?.dispatchEvent(
69
+ new CustomEvent("command-search:results", {
70
+ detail: { groups, query: options.query },
71
+ }),
72
+ );
73
+ }