@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cl-light-ds/ui",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "ClickLearn Lightning Design System — Astro UI component library.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -10,7 +10,7 @@
10
10
  "exports": {
11
11
  "./globals.css": "./src/styles/globals.css",
12
12
  "./lib/*": "./src/lib/*.ts",
13
- "./components/*": "./src/components/*.ts"
13
+ "./components/*": "./src/components/*/index.ts"
14
14
  },
15
15
  "publishConfig": {
16
16
  "access": "public"
@@ -34,22 +34,24 @@
34
34
  "typescript": "^5.9.3"
35
35
  },
36
36
  "dependencies": {
37
- "@data-slot/accordion": "^0.2.166",
38
- "@data-slot/collapsible": "^0.2.166",
39
- "@data-slot/combobox": "^0.2.166",
40
- "@data-slot/dialog": "^0.2.166",
41
- "@data-slot/dropdown-menu": "^0.2.166",
42
- "@data-slot/hover-card": "^0.2.166",
43
- "@data-slot/navigation-menu": "^0.2.166",
44
- "@data-slot/popover": "^0.2.166",
45
- "@data-slot/radio-group": "^0.2.166",
46
- "@data-slot/select": "^0.2.166",
47
- "@data-slot/slider": "^0.2.166",
48
- "@data-slot/switch": "^0.2.166",
49
- "@data-slot/tabs": "^0.2.166",
50
- "@data-slot/toggle": "^0.2.166",
51
- "@data-slot/toggle-group": "^0.2.166",
52
- "@data-slot/tooltip": "^0.2.166",
37
+ "@data-slot/accordion": "^1.1.2",
38
+ "@data-slot/collapsible": "^1.1.2",
39
+ "@data-slot/combobox": "^1.1.2",
40
+ "@data-slot/command": "^1.1.2",
41
+ "@data-slot/dialog": "^1.1.2",
42
+ "@data-slot/dropdown-menu": "^1.1.2",
43
+ "@data-slot/hover-card": "^1.1.2",
44
+ "@data-slot/navigation-menu": "^1.1.2",
45
+ "@data-slot/popover": "^1.1.2",
46
+ "@data-slot/radio-group": "^1.1.2",
47
+ "@data-slot/select": "^1.1.2",
48
+ "@data-slot/slider": "^1.1.2",
49
+ "@data-slot/switch": "^1.1.2",
50
+ "@data-slot/tabs": "^1.1.2",
51
+ "@data-slot/toast": "^1.1.2",
52
+ "@data-slot/toggle": "^1.1.2",
53
+ "@data-slot/toggle-group": "^1.1.2",
54
+ "@data-slot/tooltip": "^1.1.2",
53
55
  "@lucide/astro": "^1.14.0",
54
56
  "bejamas": "^0.4.1",
55
57
  "class-variance-authority": "^0.7.1",
@@ -24,6 +24,10 @@
24
24
  * <Alert variant="success" icon={CircleCheckIcon}>
25
25
  * <AlertTitle>Payment successful.</AlertTitle>
26
26
  * </Alert>
27
+ *
28
+ * <Alert variant="warning" icon={TriangleAlertIcon}>
29
+ * <AlertTitle>Linked recordings won't be included.</AlertTitle>
30
+ * </Alert>
27
31
  * </div>
28
32
  *
29
33
  * @usage
@@ -45,7 +49,7 @@ import type { AstroComponent } from "@lucide/astro";
45
49
  import { cn } from "@cl-light-ds/ui/lib/utils";
46
50
  import type { HTMLAttributes } from "astro/types";
47
51
 
48
- type AlertVariant = "default" | "destructive" | "success";
52
+ type AlertVariant = "default" | "destructive" | "success" | "warning";
49
53
  type AlertSize = "default" | "large";
50
54
 
51
55
  interface Props extends HTMLAttributes<"div"> {
@@ -77,6 +81,7 @@ const alertVariants = cva(
77
81
  default: "bg-popover",
78
82
  destructive: "bg-card",
79
83
  success: "bg-card",
84
+ warning: "bg-callout-warning border-warning/40",
80
85
  },
81
86
  size: {
82
87
  default: "items-center gap-2 p-2",
@@ -98,6 +103,7 @@ const iconVariants = cva(
98
103
  default: "bg-muted text-foreground",
99
104
  destructive: "bg-destructive text-destructive-foreground",
100
105
  success: "bg-success text-success-foreground",
106
+ warning: "bg-warning text-warning-foreground",
101
107
  },
102
108
  },
103
109
  defaultVariants: {
@@ -157,7 +163,7 @@ const contentVariants = cva("flex flex-1 text-sm", {
157
163
  <button
158
164
  type="button"
159
165
  data-slot="alert-dismiss"
160
- class="flex shrink-0 items-center justify-center gap-0.5 rounded-lg bg-background p-2 [&>svg]:size-4"
166
+ class="flex shrink-0 items-center justify-center gap-0.5 rounded-lg bg-background p-2 group-data-[variant=warning]/alert:bg-transparent [&>svg]:size-4"
161
167
  >
162
168
  <XIcon />
163
169
  <span class="sr-only">Dismiss</span>
@@ -2,4 +2,13 @@
2
2
 
3
3
  All notable changes to the `Alert` component will be documented in this file.
4
4
 
5
+ ## Date 15th Oct 2026
6
+ ## Workspace selection handover (N19, gap G13)
7
+
8
+ ### Variants
9
+
10
+ - Added a `warning` variant (`variant: "default" | "destructive" | "success" | "warning"`). Background `bg-callout-warning` (`--callout-background-warning`, Figma Yellow/50), border `warning` at 40%, icon in a solid `warning` circle with `warning-foreground`. Replaces the workspace-selection screen's hand-styled `Alert` (`bg-badge-warning/10 border-badge-warning/40`).
11
+ - The dismiss button is transparent on `warning`, so no white square shows on the yellow background.
12
+
13
+
5
14
  ## Unreleased
@@ -95,6 +95,10 @@
95
95
  * <Badge slot="badge" variant="secondary" shape="sharp" size="sm">Value</Badge>
96
96
  * </Button>
97
97
  *
98
+ * ### Loading
99
+ *
100
+ * <Button variant="destructive" loading loadingText="Deleting…">Delete workspace</Button>
101
+ *
98
102
  * ### Destructive
99
103
  *
100
104
  * <Button variant="destructive">Click me</Button>
@@ -125,6 +129,7 @@ import type { HTMLAttributes } from "astro/types";
125
129
  import { cva, type VariantProps } from "class-variance-authority";
126
130
 
127
131
  import { cn } from "@cl-light-ds/ui/lib/utils";
132
+ import { Spinner } from "../spinner";
128
133
 
129
134
  const buttonVariants = cva(
130
135
  "group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:border-transparent disabled:bg-muted disabled:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0",
@@ -143,6 +148,7 @@ const buttonVariants = cva(
143
148
  sm: "h-8 gap-1 px-2 text-xs [&_svg]:size-3.5",
144
149
  lg: "h-10 gap-1 px-2.5 text-sm [&_svg]:size-5",
145
150
  icon: "size-9 [&_svg]:size-4",
151
+ "icon-xs": "size-6 [&_svg]:size-3.5",
146
152
  "icon-sm": "size-8 [&_svg]:size-3.5",
147
153
  "icon-lg": "size-10 [&_svg]:size-5",
148
154
  },
@@ -168,6 +174,15 @@ type ButtonOwnProps = ButtonVariantProps & {
168
174
  leadIcon?: boolean;
169
175
  tailIcon?: boolean;
170
176
  badge?: boolean;
177
+ /**
178
+ * Busy: disabled, with a spinner and `loadingText` in the label's place. The
179
+ * label and the loading content share one grid cell, so the width holds.
180
+ * Passing `loading` (true or false) renders that structure; a script can then
181
+ * switch it with `data-loading="true"` plus `disabled`.
182
+ */
183
+ loading?: boolean;
184
+ /** Text beside the spinner while loading, e.g. "Deleting…". Spinner only when omitted. */
185
+ loadingText?: string;
171
186
  };
172
187
 
173
188
  export type Props = (ButtonAsButton | ButtonAsAnchor) & ButtonOwnProps;
@@ -183,9 +198,21 @@ const {
183
198
  leadIcon = hasLeadIconSlot,
184
199
  tailIcon = hasTailIconSlot,
185
200
  badge = hasBadgeSlot,
201
+ loading,
202
+ loadingText,
186
203
  class: className = "",
187
204
  ...rest
188
205
  } = Astro.props as Props;
206
+
207
+ // Loading (N21): only buttons given `loading` or `loadingText` get the stacked label.
208
+ const hasLoading = loading !== undefined || loadingText !== undefined;
209
+ const loadingAttrs = hasLoading
210
+ ? {
211
+ "data-loading": loading ? "true" : "false",
212
+ "aria-busy": loading ? "true" : undefined,
213
+ ...(loading ? (Tag === "a" ? { "aria-disabled": "true" } : { disabled: true }) : {}),
214
+ }
215
+ : {};
189
216
  ---
190
217
 
191
218
  {
@@ -202,32 +229,64 @@ const {
202
229
  data-variant={variant}
203
230
  data-size={size}
204
231
  class={cn(
205
- "focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-chart-1 in-data-[slot=button-group]:hover:bg-background-hover in-data-[slot=button-group]:hover:border-badge-accent aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border border-transparent bg-clip-padding font-semibold aria-invalid:ring-3 ease-out duration-150 active:scale-98",
232
+ "focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-chart-1 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border border-transparent bg-clip-padding font-semibold aria-invalid:ring-3 ease-out duration-150 active:scale-98",
206
233
  buttonVariants({ variant, size }),
207
234
  className,
208
235
  )}
209
236
  {...rest}
237
+ {...loadingAttrs}
210
238
  >
211
239
  {
212
- leadIcon && (
213
- <span data-slot="button-lead-icon" class="inline-flex shrink-0 items-center">
214
- <slot name="lead-icon" />
215
- </span>
216
- )
217
- }
218
- <slot />
219
- {
220
- badge && (
221
- <span data-slot="button-badge" class="inline-flex shrink-0 items-center">
222
- <slot name="badge" />
223
- </span>
224
- )
225
- }
226
- {
227
- tailIcon && (
228
- <span data-slot="button-tail-icon" class="inline-flex shrink-0 items-center">
229
- <slot name="tail-icon" />
240
+ hasLoading ? (
241
+ <span data-slot="button-loading-stack" class="grid">
242
+ <span
243
+ data-slot="button-content"
244
+ class="col-start-1 row-start-1 inline-flex items-center justify-center gap-1 group-data-[loading=true]/button:invisible"
245
+ >
246
+ {leadIcon && (
247
+ <span data-slot="button-lead-icon" class="inline-flex shrink-0 items-center">
248
+ <slot name="lead-icon" />
249
+ </span>
250
+ )}
251
+ <slot />
252
+ {badge && (
253
+ <span data-slot="button-badge" class="inline-flex shrink-0 items-center">
254
+ <slot name="badge" />
255
+ </span>
256
+ )}
257
+ {tailIcon && (
258
+ <span data-slot="button-tail-icon" class="inline-flex shrink-0 items-center">
259
+ <slot name="tail-icon" />
260
+ </span>
261
+ )}
262
+ </span>
263
+ <span
264
+ data-slot="button-loading"
265
+ class="invisible col-start-1 row-start-1 inline-flex items-center justify-center gap-2 group-data-[loading=true]/button:visible"
266
+ >
267
+ <Spinner aria-hidden="true" role="presentation" />
268
+ {loadingText}
269
+ </span>
230
270
  </span>
271
+ ) : (
272
+ <>
273
+ {leadIcon && (
274
+ <span data-slot="button-lead-icon" class="inline-flex shrink-0 items-center">
275
+ <slot name="lead-icon" />
276
+ </span>
277
+ )}
278
+ <slot />
279
+ {badge && (
280
+ <span data-slot="button-badge" class="inline-flex shrink-0 items-center">
281
+ <slot name="badge" />
282
+ </span>
283
+ )}
284
+ {tailIcon && (
285
+ <span data-slot="button-tail-icon" class="inline-flex shrink-0 items-center">
286
+ <slot name="tail-icon" />
287
+ </span>
288
+ )}
289
+ </>
231
290
  )
232
291
  }
233
292
  </Tag>
@@ -2,6 +2,28 @@
2
2
 
3
3
  All notable changes to the `Button` component will be documented in this file.
4
4
 
5
+ ## Date 15th Oct 2026
6
+ ## Workspace selection handover (N21)
7
+
8
+ ### Props
9
+
10
+ - Added `loading` and `loadingText`. While loading the button is disabled (`aria-disabled` on `as="a"`), `aria-busy="true"` and `data-loading="true"`, and shows a `Spinner` with `loadingText` in place of the label. The label and the loading content are stacked in one grid cell (the label goes `invisible`), so the button keeps its width (D178). Without `loadingText` only the spinner shows.
11
+ - Only buttons given `loading` (true or false) or `loadingText` get the extra markup (`button-loading-stack` > `button-content` + `button-loading`); every other button renders exactly as before. A script can switch a `loading={false}` button with `data-loading="true"` plus `disabled`.
12
+ - Storybook: "Loading" preview, with a scripted example.
13
+
14
+ ## Date 15th Oct 2026
15
+ ## Workspace selection handover (C2, C3, C25)
16
+
17
+ ### Sizes
18
+
19
+ - Added `icon-xs` (24px, 14px icon) for small icon buttons such as a notification's dismiss ✕. Was `icon-sm` resized with `size-6`.
20
+
21
+ ### States: inside a `ButtonGroup` (gap G9)
22
+
23
+ - **Removed** the group-wide hover override (`in-data-[slot=button-group]:hover:bg-background-hover hover:border-badge-accent`). Every variant now keeps its own hover inside a group: a primary button hovers `primary/80` (it turned grey), an outline button keeps its `border` (it turned light blue, unlike the same button outside a group).
24
+ - **Figma:** this reverses the grouped Hover state from the 8th Sep alignment (`bg-background-hover` + `accent` border, master symbols node `114:11217`). Agreed with the designer (D71, D152); Figma's Button Group states will be updated to match.
25
+
26
+
5
27
  ## Date 8th Sep 2026
6
28
  ## Figma alignment (node 114-6897, prop table 7185-13418)
7
29
 
@@ -102,14 +102,25 @@ const {
102
102
  } = Astro.props as Props;
103
103
 
104
104
  export const buttonGroupVariants = cva(
105
- "flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-e-lg [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
105
+ [
106
+ "flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10",
107
+ // A primary button beside a separator: a light blue separator, and no transparent end border
108
+ // (it isn't painted, so the page showed through as a white strip).
109
+ "[&>[data-slot=button][data-variant=default]+[data-slot=button-group-separator]]:bg-chart-2",
110
+ // A field joined to a button (search + Search): while the field has focus, one ring goes round
111
+ // both. The field's own ring is off and the button's border takes the ring colour.
112
+ "has-[>[data-slot=input-group]_[data-slot=input-group-control]:focus-visible]:rounded-lg has-[>[data-slot=input-group]_[data-slot=input-group-control]:focus-visible]:ring-3 has-[>[data-slot=input-group]_[data-slot=input-group-control]:focus-visible]:ring-ring/50 transition-shadow",
113
+ "[&>[data-slot=input-group]]:has-[[data-slot=input-group-control]:focus-visible]:ring-0",
114
+ "has-[>[data-slot=input-group]_[data-slot=input-group-control]:focus-visible]:[&>[data-slot=button]]:border-ring",
115
+ "has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-e-lg [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
116
+ ],
106
117
  {
107
118
  variants: {
108
119
  orientation: {
109
120
  horizontal:
110
- "*:data-slot:rounded-e-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-e-lg! [&>[data-slot]~[data-slot]]:border-s-0 [&>[data-slot]~[data-slot]]:rounded-s-none [&>[data-slot]~[data-slot]_[data-slot]]:border-s-0 [&>[data-slot]~[data-slot]_[data-slot]]:rounded-s-none",
121
+ "[&>[data-slot]:has(+[data-slot=button-group-separator])]:border-e-0 *:data-slot:rounded-e-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-e-lg! [&>[data-slot]~[data-slot]]:border-s-0 [&>[data-slot]~[data-slot]]:rounded-s-none [&>[data-slot]~[data-slot]_[data-slot]]:border-s-0 [&>[data-slot]~[data-slot]_[data-slot]]:rounded-s-none",
111
122
  vertical:
112
- "flex-col *:data-slot:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-lg! [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0 [&>[data-slot]~[data-slot]_[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]_[data-slot]]:border-t-0",
123
+ "flex-col [&>[data-slot]:has(+[data-slot=button-group-separator])]:border-b-0 *:data-slot:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-lg! [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0 [&>[data-slot]~[data-slot]_[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]_[data-slot]]:border-t-0",
113
124
  },
114
125
  },
115
126
  defaultVariants: {
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to the `ButtonGroup` component will be documented in this file.
4
4
 
5
+ ## Date 15th Oct 2026
6
+ ## Workspace selection handover (C2, C4)
7
+
8
+ ### Split button (C2)
9
+
10
+ - A button followed by a `ButtonGroupSeparator` has no end border (no bottom border when vertical). Its transparent border isn't painted (`bg-clip-padding`), so the page showed through as a white strip beside the separator.
11
+ - A separator right after a primary (`variant="default"`) button is `chart-2` (light blue) instead of `input` grey.
12
+
13
+ ### Field joined to a button (C4)
14
+
15
+ - When a `ButtonGroup` holds an `InputGroup` and its field has keyboard focus, one ring (`ring-3 ring-ring/50`) goes round the whole group: the field's own ring is off and the buttons' border turns `ring`. Tab on to the button and it shows its own ring alone.
16
+
17
+ ### Storybook
18
+
19
+ - Added "Split button" and "Field joined to a button" previews.
20
+
21
+
5
22
  ## Date 8th Sep 2026
6
23
  ## Figma alignment (node 114-9882)
7
24
 
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ All notable changes to the `Callout` component will be documented in this file.
4
+
5
+ ## 2026-10-06 — Initial implementation (ClickLearn Lightning Design Library, node `7650-5947`)
6
+
7
+ Figma documents Callout as a single component set (`7656:12714`) with one property, `Type = Default | Info | Warning | Danger`.
8
+
9
+ ### Added
10
+
11
+ - `Callout`, `CalloutTitle`, `CalloutDescription`.
12
+ - `variant: "default" | "info" | "warning" | "danger"` — maps 1:1 to Figma's `Type` (Figma's "Danger" kept as `danger`, its background token is `callout-background-error`).
13
+ - Default icon per variant, rendered in a 30px bordered white box, matching Figma: `Lightbulb` (foreground), `Info` (primary), `CircleAlert` (warning), `ShieldAlert` (destructive). Override with `icon={…}` or hide with `icon={false}`.
14
+ - `actions` named slot — right-aligned row under the copy. Figma's actions are small buttons, so pass `<Button size="sm">` (use `variant="destructive"` for the Danger primary action, as in Figma).
15
+ - `dismissible` (default `true`, Figma always shows the X). Visual only, same as `Alert`.
16
+ - `role="note"` — callouts are static, in-flow content, unlike `Alert`'s `role="alert"`.
17
+
18
+ ### New tokens (`globals.css`)
19
+
20
+ | Token | Light | Dark | Figma variable |
21
+ |---|---|---|---|
22
+ | `--warning` / `bg-warning`, `text-warning` | `#eab308` | — | `badge-warning-backgground` |
23
+ | `--warning-foreground` | white | — | — |
24
+ | `--callout-background-info` / `bg-callout-info` | `var(--primary-50)` (`#ebf3fc`) | primary-500 15% on background | `callout-background-info` |
25
+ | `--callout-background-warning` / `bg-callout-warning` | `#fefce8` | `#eab308` 12% on background | `callout-background-warning` |
26
+ | `--callout-background-error` / `bg-callout-error` | `#fef2f2` | red-600 15% on background | `callout-background-error` |
27
+
28
+ ### Notes
29
+
30
+ - Default variant uses `bg-popover` + `border-border`; tinted variants use a transparent border so all four keep identical dimensions.
31
+ - Radius is `rounded-md` (6px) and padding `p-3` per Figma's `radius/rounded-md` and `padding/p-3`.
32
+ - Dark values are not defined in Figma; they are tinted mixes chosen to keep contrast with `foreground`.
@@ -0,0 +1,147 @@
1
+ ---
2
+ /**
3
+ * @component Callout
4
+ * @title Callout
5
+ * @description An inline, in-flow message that highlights a tip, update, warning or problem, with optional actions.
6
+ * @figmaUrl https://www.figma.com/design/qXkzdqZ3QPQqgckU3AwIAH/ClickLearn-Lightning-Design-Library?node-id=7650-5947
7
+ * @status stable
8
+ * @since 0.4.0
9
+ *
10
+ * @preview
11
+ *
12
+ * <Callout variant="info" class="max-w-md">
13
+ * <CalloutTitle>Bookmark your organisation link</CalloutTitle>
14
+ * <CalloutDescription>Save your organisation URL to sign in directly next time.</CalloutDescription>
15
+ * <Fragment slot="actions">
16
+ * <Button size="sm">View updates</Button>
17
+ * <Button size="sm" variant="outline">Not now</Button>
18
+ * </Fragment>
19
+ * </Callout>
20
+ *
21
+ * @usage
22
+ *
23
+ * ```astro nocollapse
24
+ * ---
25
+ * import { Callout, CalloutTitle, CalloutDescription } from '@cl-light-ds/ui/components/callout';
26
+ * import { Button } from '@cl-light-ds/ui/components/button';
27
+ * ---
28
+ * <Callout variant="warning">
29
+ * <CalloutTitle>Your session is about to expire</CalloutTitle>
30
+ * <CalloutDescription>Save your work or extend the session.</CalloutDescription>
31
+ * <Fragment slot="actions">
32
+ * <Button size="sm">Extend session</Button>
33
+ * <Button size="sm" variant="outline">Save work</Button>
34
+ * </Fragment>
35
+ * </Callout>
36
+ * ```
37
+ */
38
+ import { cva } from "class-variance-authority";
39
+ import { CircleAlert, Info, Lightbulb, ShieldAlert, X as XIcon } from "@lucide/astro";
40
+ import type { AstroComponent } from "@lucide/astro";
41
+ import { cn } from "@cl-light-ds/ui/lib/utils";
42
+ import type { HTMLAttributes } from "astro/types";
43
+
44
+ type CalloutVariant = "default" | "info" | "warning" | "danger";
45
+
46
+ interface Props extends HTMLAttributes<"div"> {
47
+ variant?: CalloutVariant;
48
+ /** Icon component; defaults to the variant's icon. Pass `false` to hide it. */
49
+ icon?: AstroComponent | false;
50
+ dismissible?: boolean;
51
+ class?: string;
52
+ }
53
+
54
+ const defaultIcons: Record<CalloutVariant, AstroComponent> = {
55
+ default: Lightbulb,
56
+ info: Info,
57
+ warning: CircleAlert,
58
+ danger: ShieldAlert,
59
+ };
60
+
61
+ const {
62
+ class: className = "",
63
+ variant = "default",
64
+ icon,
65
+ dismissible = true,
66
+ ...rest
67
+ } = Astro.props as Props;
68
+
69
+ const Icon = icon === false ? undefined : (icon ?? defaultIcons[variant]);
70
+ const hasActions = Astro.slots.has("actions");
71
+
72
+ const calloutVariants = cva(
73
+ "group/callout relative flex w-full items-start gap-2 rounded-md border p-3",
74
+ {
75
+ variants: {
76
+ variant: {
77
+ default: "border-border bg-popover",
78
+ info: "border-transparent bg-callout-info",
79
+ warning: "border-transparent bg-callout-warning",
80
+ danger: "border-transparent bg-callout-error",
81
+ },
82
+ },
83
+ defaultVariants: {
84
+ variant: "default",
85
+ },
86
+ },
87
+ );
88
+
89
+ const iconVariants = cva(
90
+ "flex size-7.5 shrink-0 items-center justify-center rounded-lg border border-border bg-background [&>svg]:size-[15px]",
91
+ {
92
+ variants: {
93
+ variant: {
94
+ default: "text-foreground",
95
+ info: "text-primary",
96
+ warning: "text-warning",
97
+ danger: "text-destructive",
98
+ },
99
+ },
100
+ defaultVariants: {
101
+ variant: "default",
102
+ },
103
+ },
104
+ );
105
+ ---
106
+
107
+ <div
108
+ {...rest}
109
+ data-slot="callout"
110
+ data-variant={variant}
111
+ role="note"
112
+ class={cn(calloutVariants({ variant }), className)}
113
+ >
114
+ <div data-slot="callout-message" class="flex min-w-0 flex-1 items-start gap-3">
115
+ {
116
+ Icon && (
117
+ <div data-slot="callout-icon" class={iconVariants({ variant })}>
118
+ <Icon aria-hidden="true" />
119
+ </div>
120
+ )
121
+ }
122
+ <div data-slot="callout-content" class="flex min-w-0 flex-1 flex-col gap-3 pt-0.5">
123
+ <div class="flex flex-col gap-1">
124
+ <slot />
125
+ </div>
126
+ {
127
+ hasActions && (
128
+ <div data-slot="callout-actions" class="flex w-full items-center justify-end gap-2">
129
+ <slot name="actions" />
130
+ </div>
131
+ )
132
+ }
133
+ </div>
134
+ </div>
135
+ {
136
+ dismissible && (
137
+ <button
138
+ type="button"
139
+ data-slot="callout-dismiss"
140
+ class="flex shrink-0 items-center justify-center rounded-lg p-2 text-muted-foreground [&>svg]:size-4"
141
+ >
142
+ <XIcon aria-hidden="true" />
143
+ <span class="sr-only">Dismiss</span>
144
+ </button>
145
+ )
146
+ }
147
+ </div>
@@ -0,0 +1,16 @@
1
+ ---
2
+ import { cn } from "@cl-light-ds/ui/lib/utils";
3
+ import type { HTMLAttributes } from "astro/types";
4
+
5
+ interface Props extends HTMLAttributes<"div"> {}
6
+
7
+ const { class: className = "", ...rest } = Astro.props as Props;
8
+ ---
9
+
10
+ <div
11
+ {...rest}
12
+ data-slot="callout-description"
13
+ class={cn("text-sm leading-5 text-muted-foreground", className)}
14
+ >
15
+ <slot />
16
+ </div>
@@ -0,0 +1,16 @@
1
+ ---
2
+ import { cn } from "@cl-light-ds/ui/lib/utils";
3
+ import type { HTMLAttributes } from "astro/types";
4
+
5
+ interface Props extends HTMLAttributes<"div"> {}
6
+
7
+ const { class: className = "", ...rest } = Astro.props as Props;
8
+ ---
9
+
10
+ <div
11
+ {...rest}
12
+ data-slot="callout-title"
13
+ class={cn("text-sm leading-5 font-semibold text-foreground", className)}
14
+ >
15
+ <slot />
16
+ </div>
@@ -0,0 +1,3 @@
1
+ export { default as Callout } from "./Callout.astro";
2
+ export { default as CalloutTitle } from "./CalloutTitle.astro";
3
+ export { default as CalloutDescription } from "./CalloutDescription.astro";
@@ -2,6 +2,15 @@
2
2
 
3
3
  All notable changes to the `Card` component will be documented in this file.
4
4
 
5
+ ## Date 15th Oct 2026
6
+ ## Workspace selection handover (C14)
7
+
8
+ ### Added
9
+
10
+ - `interactive` prop (default `false`): for cards that open something, the card lifts 4px (`-translate-y-1`) with `shadow-lg` on hover and on keyboard focus, on the card itself (`as="a"`) or inside it (`has-focus-visible`, for a stretched link with other controls beside it). Only keyboard focus lifts it, so a mouse click on a control in the card doesn't leave it raised. No lift with `prefers-reduced-motion` (the shadow still changes). Sets `data-interactive`.
11
+ - Opt-in rather than always on: most cards are static, and lifting them would suggest they can be clicked.
12
+ - Storybook: an "Interactive" preview.
13
+
5
14
  ## Date 9th Sep 2026
6
15
  ## Figma alignment (node 54-1310, prop table 7350-1279)
7
16
 
@@ -102,6 +102,16 @@
102
102
  * Pass `as="a"` (with `href`) to render the whole card as a real, natively focusable anchor — this is how to make Figma's Focus state reachable via keyboard, since a plain `<div>` can't receive `:focus-visible`.
103
103
  *
104
104
  * <Card as="a" href="#">...</Card>
105
+ *
106
+ * ### Interactive (hover lift)
107
+ *
108
+ * `interactive` is for cards that open something (the whole card is a link,
109
+ * either `as="a"` or a stretched link inside it): the card lifts 4px with a
110
+ * larger shadow on hover and while it, or anything in it, has keyboard focus.
111
+ * No lift with reduced motion (the shadow still changes). Off by default, so
112
+ * static cards don't suggest they can be clicked.
113
+ *
114
+ * <Card as="a" href="/workspace" interactive>...</Card>
105
115
  */
106
116
 
107
117
  import { cva } from "class-variance-authority";
@@ -124,6 +134,8 @@ type CardOwnProps = {
124
134
  variant?: CardVariant;
125
135
  size?: CardSize;
126
136
  orientation?: CardOrientation;
137
+ /** Lift 4px with a larger shadow on hover and keyboard focus, for cards that open something. */
138
+ interactive?: boolean;
127
139
  };
128
140
 
129
141
  export type Props = (CardAsDiv | CardAsAnchor) & CardOwnProps;
@@ -134,9 +146,17 @@ const {
134
146
  variant = "default",
135
147
  size = "default",
136
148
  orientation = "vertical",
149
+ interactive = false,
137
150
  ...rest
138
151
  } = Astro.props as Props;
139
152
 
153
+ /**
154
+ * C14 — the hover lift. Keyboard focus only (`focus-visible`, on the card or
155
+ * inside it): a mouse click on a control in the card doesn't keep it lifted.
156
+ */
157
+ const interactiveClasses =
158
+ "transition-[translate,box-shadow] duration-200 hover:shadow-lg focus-visible:shadow-lg has-focus-visible:shadow-lg motion-safe:hover:-translate-y-1 motion-safe:focus-visible:-translate-y-1 motion-safe:has-focus-visible:-translate-y-1";
159
+
140
160
  const cardVariants = cva(
141
161
  "group/card relative flex overflow-clip rounded-lg text-sm text-card-foreground outline-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-chart-1",
142
162
  {
@@ -185,11 +205,13 @@ const cardVariants = cva(
185
205
  data-variant={variant}
186
206
  data-size={size}
187
207
  data-orientation={orientation}
208
+ data-interactive={interactive ? "" : undefined}
188
209
  class={cn(
189
210
  cardVariants({ variant, size, orientation }),
190
211
  "*:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg",
191
212
  variant === "overlay" &&
192
213
  "[&_[data-slot=card-media]]:absolute [&_[data-slot=card-media]]:inset-0 [&_[data-slot=card-media]]:z-0 [&_[data-slot=card-media]]:rounded-lg [&_[data-slot=card-media]]:p-0 [&_[data-slot=card-media]_img]:rounded-lg",
214
+ interactive && interactiveClasses,
193
215
  className,
194
216
  )}
195
217
  >