@cl-light-ds/ui 0.4.0 → 0.6.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 (127) hide show
  1. package/package.json +19 -17
  2. package/src/components/accordion/AccordionTrigger.astro +5 -1
  3. package/src/components/accordion/CHANGELOG.md +9 -0
  4. package/src/components/alert/Alert.astro +42 -3
  5. package/src/components/alert/CHANGELOG.md +19 -0
  6. package/src/components/button/Button.astro +78 -19
  7. package/src/components/button/CHANGELOG.md +22 -0
  8. package/src/components/button-group/ButtonGroup.astro +14 -3
  9. package/src/components/button-group/CHANGELOG.md +17 -0
  10. package/src/components/card/CHANGELOG.md +9 -0
  11. package/src/components/card/Card.astro +22 -0
  12. package/src/components/collapsible/CHANGELOG.md +9 -0
  13. package/src/components/collapsible/CollapsibleTrigger.astro +5 -1
  14. package/src/components/combobox/CHANGELOG.md +27 -0
  15. package/src/components/combobox/Combobox.astro +18 -1
  16. package/src/components/combobox/ComboboxTrigger.astro +32 -2
  17. package/src/components/command-search/CHANGELOG.md +57 -0
  18. package/src/components/command-search/CommandSearch.astro +594 -0
  19. package/src/components/command-search/CommandSearchGroup.astro +43 -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 +50 -0
  25. package/src/components/dialog/Dialog.astro +14 -3
  26. package/src/components/dialog/DialogClose.astro +28 -12
  27. package/src/components/dialog/DialogContent.astro +19 -1
  28. package/src/components/dialog/DialogTrigger.astro +5 -1
  29. package/src/components/dropdown-menu/CHANGELOG.md +32 -0
  30. package/src/components/dropdown-menu/DropdownMenu.astro +13 -3
  31. package/src/components/dropdown-menu/DropdownMenuRadioGroup.astro +17 -2
  32. package/src/components/dropdown-menu/DropdownMenuTrigger.astro +7 -1
  33. package/src/components/dropdown-menu/dropdown-menu-item-variants.ts +1 -1
  34. package/src/components/empty-state/CHANGELOG.md +18 -0
  35. package/src/components/empty-state/EmptyState.astro +157 -0
  36. package/src/components/empty-state/illustrations/AllCaughtUp.astro +52 -0
  37. package/src/components/empty-state/illustrations/EmptyWorkspaces.astro +56 -0
  38. package/src/components/empty-state/illustrations/LoadError.astro +61 -0
  39. package/src/components/empty-state/illustrations/NoResults.astro +60 -0
  40. package/src/components/empty-state/illustrations/NothingMatches.astro +66 -0
  41. package/src/components/empty-state/illustrations/index.ts +27 -0
  42. package/src/components/empty-state/index.ts +7 -0
  43. package/src/components/favorite-toggle/CHANGELOG.md +13 -0
  44. package/src/components/favorite-toggle/FavoriteToggle.astro +94 -0
  45. package/src/components/favorite-toggle/index.ts +1 -0
  46. package/src/components/field/CHANGELOG.md +18 -0
  47. package/src/components/field/Field.astro +18 -0
  48. package/src/components/field/FieldError.astro +13 -1
  49. package/src/components/field/FieldGroup.astro +12 -3
  50. package/src/components/hover-card/CHANGELOG.md +9 -0
  51. package/src/components/hover-card/HoverCardTrigger.astro +5 -1
  52. package/src/components/input-group/CHANGELOG.md +7 -0
  53. package/src/components/input-group/InputGroup.astro +2 -0
  54. package/src/components/loading-overlay/CHANGELOG.md +25 -0
  55. package/src/components/loading-overlay/LoadingOverlay.astro +179 -0
  56. package/src/components/loading-overlay/index.ts +8 -0
  57. package/src/components/notifications-menu/CHANGELOG.md +29 -0
  58. package/src/components/notifications-menu/NotificationsMenu.astro +375 -0
  59. package/src/components/notifications-menu/NotificationsMenuItem.astro +130 -0
  60. package/src/components/notifications-menu/all-caught-up.svg +28 -0
  61. package/src/components/notifications-menu/index.ts +3 -0
  62. package/src/components/notifications-menu/types.ts +32 -0
  63. package/src/components/option-card/CHANGELOG.md +26 -0
  64. package/src/components/option-card/OptionCard.astro +143 -0
  65. package/src/components/option-card/index.ts +1 -0
  66. package/src/components/organisms/top-bar/CHANGELOG.md +27 -0
  67. package/src/components/organisms/top-bar/TopBar.astro +257 -0
  68. package/src/components/organisms/top-bar/index.ts +2 -0
  69. package/src/components/organisms/top-bar/types.ts +30 -0
  70. package/src/components/organisms/workspace/CHANGELOG.md +80 -0
  71. package/src/components/organisms/workspace/OrganisationOverview.astro +62 -0
  72. package/src/components/organisms/workspace/RecentWorkspaceCard.astro +235 -0
  73. package/src/components/organisms/workspace/RecentWorkspaces.astro +93 -0
  74. package/src/components/organisms/workspace/WorkspaceAvatar.astro +144 -0
  75. package/src/components/organisms/workspace/WorkspaceCard.astro +172 -0
  76. package/src/components/organisms/workspace/WorkspaceGlyph.astro +62 -0
  77. package/src/components/organisms/workspace/WorkspaceIcon.astro +42 -0
  78. package/src/components/organisms/workspace/WorkspaceIconPicker.astro +288 -0
  79. package/src/components/organisms/workspace/WorkspaceIconPickerGlyph.astro +44 -0
  80. package/src/components/organisms/workspace/icons.ts +98 -0
  81. package/src/components/organisms/workspace/index.ts +35 -0
  82. package/src/components/organisms/workspace/labels.ts +54 -0
  83. package/src/components/organisms/workspace/recent-card-rings-strong.png +0 -0
  84. package/src/components/organisms/workspace/types.ts +30 -0
  85. package/src/components/popover/CHANGELOG.md +14 -0
  86. package/src/components/popover/PopoverClose.astro +2 -1
  87. package/src/components/popover/PopoverTrigger.astro +7 -1
  88. package/src/components/profile-menu/CHANGELOG.md +22 -0
  89. package/src/components/profile-menu/ProfileMenu.astro +236 -0
  90. package/src/components/profile-menu/index.ts +2 -0
  91. package/src/components/profile-menu/types.ts +27 -0
  92. package/src/components/search-field/CHANGELOG.md +14 -0
  93. package/src/components/search-field/SearchField.astro +163 -0
  94. package/src/components/search-field/index.ts +1 -0
  95. package/src/components/select/CHANGELOG.md +8 -0
  96. package/src/components/select/SelectTrigger.astro +1 -1
  97. package/src/components/table/CHANGELOG.md +15 -0
  98. package/src/components/table/TableCell.astro +6 -0
  99. package/src/components/table/TableEditableCell.astro +2 -2
  100. package/src/components/table/TableHead.astro +5 -1
  101. package/src/components/table/TablePagination.astro +4 -4
  102. package/src/components/table/TableRow.astro +3 -1
  103. package/src/components/table/table-cell-variants.ts +2 -0
  104. package/src/components/tabs/CHANGELOG.md +16 -0
  105. package/src/components/tabs/Tabs.astro +26 -0
  106. package/src/components/tabs/TabsList.astro +30 -3
  107. package/src/components/toast/CHANGELOG.md +32 -0
  108. package/src/components/toast/Toast.astro +145 -0
  109. package/src/components/toast/Toaster.astro +322 -0
  110. package/src/components/toast/index.ts +9 -0
  111. package/src/components/toggle/CHANGELOG.md +7 -0
  112. package/src/components/toggle-group/CHANGELOG.md +11 -0
  113. package/src/components/toggle-group/ToggleGroup.astro +19 -2
  114. package/src/components/toggle-group/ToggleGroupItem.astro +10 -0
  115. package/src/components/tooltip/CHANGELOG.md +26 -0
  116. package/src/components/tooltip/Tooltip.astro +31 -3
  117. package/src/components/tooltip/TooltipTrigger.astro +5 -1
  118. package/src/lib/command-search.ts +78 -0
  119. package/src/lib/dialog-focus.ts +73 -0
  120. package/src/lib/dropdown-menu-radio-groups.ts +79 -0
  121. package/src/lib/loading-overlay.ts +174 -0
  122. package/src/lib/scroll-lock.ts +97 -0
  123. package/src/lib/toast.ts +127 -0
  124. package/src/lib/toggle-shared.ts +9 -1
  125. package/src/lib/workspace-palette.ts +119 -0
  126. package/src/styles/CHANGELOG.md +35 -0
  127. package/src/styles/globals.css +109 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cl-light-ds/ui",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "ClickLearn Lightning Design System — Astro UI component library.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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",
@@ -10,7 +10,11 @@ type IndicatorIcon = "plus-minus" | "chevron";
10
10
 
11
11
  interface Props extends HTMLAttributes<"button"> {
12
12
  class?: string;
13
- /** Radix-like: render wrapper so the child can be the interactive element */
13
+ /**
14
+ * Radix-like: render wrapper so the child can be the interactive element.
15
+ * The wrapper is not a button, so the child sets its own type: give a
16
+ * `<button>`/`<Button>` child `type="button"` inside a `<form>` or it submits it.
17
+ */
14
18
  asChild?: boolean;
15
19
  /** Leading icon rendered before the label (e.g. a lucide-astro icon). */
16
20
  icon?: AstroComponent;
@@ -2,6 +2,15 @@
2
2
 
3
3
  All notable changes to the `Accordion` component family (`Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent`) are documented in this file.
4
4
 
5
+ ## Date 15th Oct 2026
6
+ ## Handover gaps: triggers don't submit forms (C29, gap G18)
7
+
8
+ ### Docs
9
+
10
+ - `AccordionTrigger` already renders `type="button"` unless you pass a `type`; no change.
11
+ - **`asChild`:** the wrapper is a `<div>`, not a button, so it can't set a type. Inside a `<form>`, give your child `type="button"` (now in the prop's JSDoc).
12
+
13
+
5
14
  ## 2026-09-08 — Figma alignment (ClickLearn Lightning Design Library, node `133-7547`)
6
15
 
7
16
  Aligned the component visually and functionally with Figma, while preserving the existing 4-part structure and all pre-existing props.
@@ -24,8 +24,26 @@
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
  *
33
+ * ### Destructive as a tint
34
+ *
35
+ * `appearance="tint"` on the destructive variant draws it as a plain light-red
36
+ * note: `destructive` at 10% behind it, no border, the title in `destructive`
37
+ * and the description in `destructive` at 80% (D65). For a warning inside a
38
+ * dialog (delete), where the white card with a red icon reads too loud. Only
39
+ * the destructive variant has a tint (`warning` is always one); on the others
40
+ * `appearance` does nothing.
41
+ *
42
+ * <Alert variant="destructive" appearance="tint" size="large" dismissible={false}>
43
+ * <AlertTitle>This can't be undone</AlertTitle>
44
+ * <AlertDescription>Deleted workspaces and their recordings can't be restored.</AlertDescription>
45
+ * </Alert>
46
+ *
29
47
  * @usage
30
48
  *
31
49
  * ```astro nocollapse
@@ -45,12 +63,15 @@ import type { AstroComponent } from "@lucide/astro";
45
63
  import { cn } from "@cl-light-ds/ui/lib/utils";
46
64
  import type { HTMLAttributes } from "astro/types";
47
65
 
48
- type AlertVariant = "default" | "destructive" | "success";
66
+ type AlertVariant = "default" | "destructive" | "success" | "warning";
49
67
  type AlertSize = "default" | "large";
68
+ type AlertAppearance = "default" | "tint";
50
69
 
51
70
  interface Props extends HTMLAttributes<"div"> {
52
71
  variant?: AlertVariant;
53
72
  size?: AlertSize;
73
+ /** `"tint"`: the destructive variant as a light-red note, no border (C18, D65). */
74
+ appearance?: AlertAppearance;
54
75
  icon?: AstroComponent;
55
76
  dismissible?: boolean;
56
77
  class?: string;
@@ -60,6 +81,7 @@ const {
60
81
  class: className = "",
61
82
  variant = "default",
62
83
  size = "default",
84
+ appearance = "default",
63
85
  icon,
64
86
  dismissible = true,
65
87
  ...rest
@@ -77,15 +99,30 @@ const alertVariants = cva(
77
99
  default: "bg-popover",
78
100
  destructive: "bg-card",
79
101
  success: "bg-card",
102
+ warning: "bg-callout-warning border-warning/40",
80
103
  },
81
104
  size: {
82
105
  default: "items-center gap-2 p-2",
83
106
  large: "items-start gap-4 p-4",
84
107
  },
108
+ appearance: {
109
+ default: "",
110
+ tint: "",
111
+ },
85
112
  },
113
+ compoundVariants: [
114
+ // C18, D65 — a plain light-red note: no border, red title, red text at 80%.
115
+ {
116
+ variant: "destructive",
117
+ appearance: "tint",
118
+ class:
119
+ "bg-destructive/10 border-transparent [&_[data-slot=alert-title]]:text-destructive [&_[data-slot=alert-description]]:text-destructive/80",
120
+ },
121
+ ],
86
122
  defaultVariants: {
87
123
  variant: "default",
88
124
  size: "default",
125
+ appearance: "default",
89
126
  },
90
127
  },
91
128
  );
@@ -98,6 +135,7 @@ const iconVariants = cva(
98
135
  default: "bg-muted text-foreground",
99
136
  destructive: "bg-destructive text-destructive-foreground",
100
137
  success: "bg-success text-success-foreground",
138
+ warning: "bg-warning text-warning-foreground",
101
139
  },
102
140
  },
103
141
  defaultVariants: {
@@ -124,8 +162,9 @@ const contentVariants = cva("flex flex-1 text-sm", {
124
162
  data-slot="alert"
125
163
  data-variant={variant}
126
164
  data-size={size}
165
+ data-appearance={appearance}
127
166
  role="alert"
128
- class={cn(alertVariants({ variant, size }), className)}
167
+ class={cn(alertVariants({ variant, size, appearance }), className)}
129
168
  >
130
169
  {
131
170
  hasIcon && (
@@ -157,7 +196,7 @@ const contentVariants = cva("flex flex-1 text-sm", {
157
196
  <button
158
197
  type="button"
159
198
  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"
199
+ class="flex shrink-0 items-center justify-center gap-0.5 rounded-lg bg-background p-2 group-data-[variant=warning]/alert:bg-transparent group-data-[variant=destructive]/alert:group-data-[appearance=tint]/alert:bg-transparent [&>svg]:size-4"
161
200
  >
162
201
  <XIcon />
163
202
  <span class="sr-only">Dismiss</span>
@@ -2,4 +2,23 @@
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 (C18)
7
+
8
+ ### Added
9
+
10
+ - `appearance` prop (`"default" | "tint"`, default `"default"`), sets `data-appearance`. `variant="destructive" appearance="tint"` draws the destructive alert as a plain light-red note (D65): background `destructive` at 10%, no border, `AlertTitle` in `destructive` and `AlertDescription` in `destructive` at 80%; the dismiss button is transparent, as on `warning`. Replaces the workspace-selection delete dialog's `class="bg-destructive/10 border-transparent"` and its title/description colour classes.
11
+ - Uses `destructive` at 10% rather than `callout-error` (Red/50): D65 specifies the destructive red at 10%, which is a little stronger than Red/50, and the title and description in the destructive red (not `callout-foreground-error`), so the tint matches the reference exactly.
12
+ - Opt-in: the destructive alert's default look (white card, border, red icon) is unchanged. Only the destructive variant has a tint (`warning` is always tinted); on the other variants `appearance` does nothing.
13
+ - Storybook: a "Destructive as a tint" preview.
14
+
15
+ ## Date 15th Oct 2026
16
+ ## Workspace selection handover (N19, gap G13)
17
+
18
+ ### Variants
19
+
20
+ - 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`).
21
+ - The dismiss button is transparent on `warning`, so no white square shows on the yellow background.
22
+
23
+
5
24
  ## 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
 
@@ -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
  >
@@ -2,4 +2,13 @@
2
2
 
3
3
  All notable changes to the `Collapsible` component will be documented in this file.
4
4
 
5
+ ## Date 15th Oct 2026
6
+ ## Handover gaps: triggers don't submit forms (C29, gap G18)
7
+
8
+ ### Docs
9
+
10
+ - `CollapsibleTrigger` already renders `type="button"` unless you pass a `type`; no change.
11
+ - **`asChild`:** the wrapper is a `<div>`, not a button, so it can't set a type. Inside a `<form>`, give your child `type="button"` (now in the prop's JSDoc).
12
+
13
+
5
14
  ## Unreleased
@@ -4,7 +4,11 @@ import type { HTMLAttributes } from "astro/types";
4
4
 
5
5
  interface Props extends HTMLAttributes<"button"> {
6
6
  class?: string;
7
- /** Radix-like: render wrapper so the child can be the interactive element */
7
+ /**
8
+ * Radix-like: render wrapper so the child can be the interactive element.
9
+ * The wrapper is not a button, so the child sets its own type: give a
10
+ * `<button>`/`<Button>` child `type="button"` inside a `<form>` or it submits it.
11
+ */
8
12
  asChild?: boolean;
9
13
  }
10
14
 
@@ -2,6 +2,33 @@
2
2
 
3
3
  All notable changes to the `Combobox` component family (`Combobox`, `ComboboxInput`, `ComboboxContent`, `ComboboxList`, `ComboboxItem`, `ComboboxGroup`, `ComboboxLabel`, `ComboboxSeparator`, `ComboboxEmpty`, `ComboboxTrigger`, `ComboboxValue`) are documented in this file.
4
4
 
5
+ ## Date 15th Oct 2026
6
+ ## Workspace selection handover (C7, gap G17)
7
+
8
+ - In "search in popup" mode, focus goes back to the trigger when the list closes (a pick or Escape), as with a native select. It dropped to the page, and out of a surrounding dialog. A workaround for a runtime bug (bejamas/data-slot).
9
+
10
+
11
+ ## Date 15th Oct 2026
12
+ ## Handover gaps: triggers don't submit forms (C29, gap G18)
13
+
14
+ ### Fixed
15
+
16
+ - **`ComboboxTrigger`: a `type` you pass now wins.** It always rendered `type="button"`, but after your attributes, so it overrode yours. The default is unchanged.
17
+
18
+
19
+ ## Workspace selection handover (C6; gaps G12, G17)
20
+
21
+ ### `ComboboxTrigger` (C6)
22
+
23
+ - **Styled as a field:** the Input's border, `bg-card`, shadow and focus ring, sized by its `size` prop (`sm` 32px, `default` 36px, `lg` 40px; padding and text as `Input`). It was unstyled and `size` did nothing.
24
+ - **In the Tab order:** `tabindex` defaults to `0`. The runtime takes a trigger without one out of the Tab order, so the "search in popup" field couldn't be reached by keyboard. Pass `tabindex={-1}` to opt out.
25
+ - **Named by its label:** the runtime names every trigger "Toggle". When the trigger has an `id` and a `<label for>` points at it, it gets `aria-labelledby` the label and its value ("Language English"). An author's own `aria-labelledby` is left alone.
26
+
27
+ ### Storybook
28
+
29
+ - "Search in popup, labelled, in sizes" preview.
30
+
31
+
5
32
  ## 2026-09-08 — Figma alignment (ClickLearn Lightning Design Library, node `5455-12863`)
6
33
 
7
34
  Styling-only pass — no props, components, or behavior were added or removed. The implementation (including the shared `InputGroup`/`InputGroupInput` it delegates trigger styling to) was already close to Figma; this fixed the remaining concrete mismatches found by comparing computed styles against the Figma spec.
@@ -346,8 +346,25 @@ const {
346
346
  <script>
347
347
  import { createCombobox } from "@data-slot/combobox";
348
348
 
349
- document.querySelectorAll('[data-slot="combobox"]').forEach((el) => {
349
+ document.querySelectorAll<HTMLElement>('[data-slot="combobox"]').forEach((el) => {
350
+ // Found before the runtime starts: it moves the content to <body> when it opens.
351
+ const content = el.querySelector('[data-slot="combobox-content"]');
352
+ const trigger = Array.from(el.querySelectorAll<HTMLElement>('[data-slot="combobox-trigger"]')).find(
353
+ (t) => !content?.contains(t),
354
+ );
355
+
350
356
  createCombobox(el);
357
+
358
+ // "Search in popup": the search input lives in the list, which hides on a
359
+ // pick or Escape, so focus would drop to the page (and out of a dialog).
360
+ // Put it back on the trigger, as a native select does.
361
+ el.addEventListener("combobox:open-change", (event) => {
362
+ if ((event as CustomEvent<{ open: boolean }>).detail.open || !trigger) return;
363
+ requestAnimationFrame(() => {
364
+ const at = document.activeElement;
365
+ if (!at || at === document.body || content?.contains(at)) trigger.focus();
366
+ });
367
+ });
351
368
  });
352
369
 
353
370
  // Clear button delegation