@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
@@ -101,6 +101,18 @@
101
101
  * <TabsContent value="reports">Reports content.</TabsContent>
102
102
  * </Tabs>
103
103
  *
104
+ * ### Bordered
105
+ *
106
+ * `bordered` on `TabsList` adds a 1px `border`-token outline round the list,
107
+ * for a list on a light grey page beside bordered controls. Off by default.
108
+ *
109
+ * <Tabs defaultValue="all">
110
+ * <TabsList bordered>
111
+ * <TabsTrigger value="all">All</TabsTrigger>
112
+ * <TabsTrigger value="favourites">Favourites</TabsTrigger>
113
+ * </TabsList>
114
+ * </Tabs>
115
+ *
104
116
  * ### Vertical
105
117
  *
106
118
  * <Tabs defaultValue="profile" orientation="vertical" class="w-full max-w-xl">
@@ -303,4 +315,18 @@ const {
303
315
  document
304
316
  .querySelectorAll('[data-slot="tabs"]')
305
317
  .forEach((el) => createTabs(el));
318
+
319
+ // Along the list (left when horizontal, top when vertical) the runtime measures
320
+ // the active tab from inside the list's border, then subtracts the border width
321
+ // again, so on a list with a real border the indicator sits off by that width.
322
+ // Record the border widths; the indicator adds them back on that axis.
323
+ const syncBorders = (list: HTMLElement) => {
324
+ list.style.setProperty("--tabs-list-border-l", `${list.clientLeft}px`);
325
+ list.style.setProperty("--tabs-list-border-t", `${list.clientTop}px`);
326
+ };
327
+ const borders = new ResizeObserver((entries) => entries.forEach((e) => syncBorders(e.target as HTMLElement)));
328
+ document.querySelectorAll<HTMLElement>('[data-slot="tabs-list"]').forEach((list) => {
329
+ syncBorders(list);
330
+ borders.observe(list);
331
+ });
306
332
  </script>
@@ -1,4 +1,17 @@
1
1
  ---
2
+ /**
3
+ * TabsList — the row (or column) of tab triggers.
4
+ *
5
+ * `bordered` (default `false`) adds a 1px `border`-token outline round the
6
+ * list, for a list sitting on a light grey page beside bordered controls
7
+ * (search field, buttons), where the muted fill alone blends in (C11, D104).
8
+ * The list keeps its height: the border sits inside it.
9
+ *
10
+ * Drawn as an inset ring with 1px more padding, not a CSS border: the
11
+ * runtime positions the indicator from `offsetLeft` and then subtracts the
12
+ * list's `clientLeft` (its border width) again, so a real border shifts the
13
+ * indicator 1px off its tab. Same geometry as `border` + the usual padding.
14
+ */
2
15
  import { cva, type VariantProps } from "class-variance-authority";
3
16
  import { cn } from "@cl-light-ds/ui/lib/utils";
4
17
  import type { HTMLAttributes } from "astro/types";
@@ -14,27 +27,40 @@ export const tabsListVariants = cva(
14
27
  "line-animated":
15
28
  "rounded-none gap-1 bg-transparent p-0",
16
29
  },
30
+ bordered: {
31
+ true: "ring-1 ring-inset ring-border",
32
+ false: "",
33
+ },
17
34
  size: {
18
35
  default: "group-data-horizontal/tabs:h-9",
19
36
  sm: "group-data-horizontal/tabs:h-8",
20
37
  lg: "group-data-horizontal/tabs:h-10",
21
38
  },
22
39
  },
40
+ compoundVariants: [
41
+ // 1px more padding where the border would be (C11).
42
+ { bordered: true, variant: "indicator", class: "p-[3px]" },
43
+ { bordered: true, variant: "default", class: "p-[4px]" },
44
+ ],
23
45
  defaultVariants: {
24
46
  variant: "indicator",
25
47
  size: "default",
48
+ bordered: false,
26
49
  },
27
50
  },
28
51
  );
29
52
 
30
53
  interface Props
31
54
  extends VariantProps<typeof tabsListVariants>, HTMLAttributes<"div"> {
55
+ /** A 1px `border`-token outline round the list (C11). Off by default. */
56
+ bordered?: boolean;
32
57
  class?: string;
33
58
  }
34
59
 
35
60
  const {
36
61
  variant = "indicator",
37
62
  size = "default",
63
+ bordered = false,
38
64
  class: className = "",
39
65
  ...rest
40
66
  } = Astro.props as Props;
@@ -45,14 +71,15 @@ const {
45
71
  role="tablist"
46
72
  data-slot="tabs-list"
47
73
  data-variant={variant}
48
- class={cn("", tabsListVariants({ variant, size }), className)}
74
+ data-bordered={bordered ? "" : undefined}
75
+ class={cn("", tabsListVariants({ variant, size, bordered }), className)}
49
76
  >
50
77
  {
51
78
  variant === "indicator" && (
52
79
  <div
53
80
  aria-hidden="true"
54
81
  data-slot="tabs-indicator"
55
- class="gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg:not([class*='size-'])]:size-4 pointer-events-none absolute top-(--active-tab-top) left-(--active-tab-left) z-0 h-(--active-tab-height) w-(--active-tab-width) bg-background shadow-sm transition-all duration-150 ease dark:border-input dark:bg-input/30"
82
+ class="gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg:not([class*='size-'])]:size-4 pointer-events-none absolute top-(--active-tab-top) left-(--active-tab-left) group-data-[orientation=horizontal]/tabs:left-[calc(var(--active-tab-left)+var(--tabs-list-border-l,0px))] group-data-[orientation=vertical]/tabs:top-[calc(var(--active-tab-top)+var(--tabs-list-border-t,0px))] z-0 h-(--active-tab-height) w-(--active-tab-width) bg-background shadow-sm transition-all duration-150 ease dark:border-input dark:bg-input/30"
56
83
  />
57
84
  )
58
85
  }
@@ -61,7 +88,7 @@ const {
61
88
  <div
62
89
  aria-hidden="true"
63
90
  data-slot="tabs-indicator"
64
- class="pointer-events-none absolute z-0 rounded-full bg-foreground transition-all duration-150 ease group-data-[orientation=horizontal]/tabs:left-[var(--active-tab-left)] group-data-[orientation=horizontal]/tabs:top-[calc(var(--active-tab-top)+var(--active-tab-height)-2px)] group-data-[orientation=horizontal]/tabs:h-0.5 group-data-[orientation=horizontal]/tabs:w-[var(--active-tab-width)] group-data-[orientation=vertical]/tabs:top-[var(--active-tab-top)] group-data-[orientation=vertical]/tabs:left-[calc(var(--active-tab-left)+var(--active-tab-width)-2px)] group-data-[orientation=vertical]/tabs:h-[var(--active-tab-height)] group-data-[orientation=vertical]/tabs:w-0.5"
91
+ class="pointer-events-none absolute z-0 rounded-full bg-foreground transition-all duration-150 ease group-data-[orientation=horizontal]/tabs:left-[calc(var(--active-tab-left)+var(--tabs-list-border-l,0px))] group-data-[orientation=horizontal]/tabs:top-[calc(var(--active-tab-top)+var(--active-tab-height)-2px)] group-data-[orientation=horizontal]/tabs:h-0.5 group-data-[orientation=horizontal]/tabs:w-[var(--active-tab-width)] group-data-[orientation=vertical]/tabs:top-[calc(var(--active-tab-top)+var(--tabs-list-border-t,0px))] group-data-[orientation=vertical]/tabs:left-[calc(var(--active-tab-left)+var(--active-tab-width)-2px)] group-data-[orientation=vertical]/tabs:h-[var(--active-tab-height)] group-data-[orientation=vertical]/tabs:w-0.5"
65
92
  />
66
93
  )
67
94
  }
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ All notable changes to the `Toast` component family (`Toaster`, `Toast`, and the `toast()` API in `@cl-light-ds/ui/lib/toast`) are documented in this file.
4
+
5
+ ## Date 15th Oct 2026
6
+ ## Workspace selection handover (N9)
7
+
8
+ New component, replacing the workspace-selection screen's hand-composed `Toast.astro`, the toast stack in `TopBar.astro` and `restackToasts` (gap G14).
9
+
10
+ ### `Toaster`
11
+
12
+ - Built on `@data-slot/toast` (`createToast`): the runtime renders toasts from a `<template>`, keeps their order, sets `role`/`aria-live` per kind (error → `alert`, assertive; others → `status`, polite), pauses timers on hover and focus, unfurls on hover or focus within (`data-expanded`), supports swipe to dismiss and measures heights (`--toast-*` tokens).
13
+ - Placement (D160): fixed under the top bar's right end. Props `top` (where the bar ends, default 41px), `offset` (gap, default 0.5rem), `right` (inset, default 1rem, 1.5rem from md). The right edge adds `var(--data-slot-scrollbar-width, 0px)` so the stack stays put while a dialog hides the page scrollbar.
14
+ - Stacking (D161, D164, D167): the newest in front, two more peeking out 10px below, 5% smaller each and blank; a fourth and beyond hidden while stacked. Unfurled: a list 12px apart, each at its own height, with a softer shadow (`shadow-xs`). All moves take 300ms.
15
+ - Clear all (D170–D172, C23): with two or more toasts, unfurling shows a link button above the list, flush under the bar (the gap closes), no hover background, underline only. Label via `clearAllLabel`.
16
+ - Toasts stay until closed by default (`duration={0}`, D166); `limit` (default 5) caps how many are kept visible.
17
+ - Entrance (D160): slides in 2rem from the right and fades; slides back out on close.
18
+ - Attention (D164, D175): once in (300ms), a ring ripples out three times (info blue, success green) or the card shakes 12px, seven swings (error). Only the newest toast. Skipped with `prefers-reduced-motion`, which also drops the slide (fade only).
19
+
20
+ ### `Toast`
21
+
22
+ - Kinds `info` (blue `Info` icon), `success` (green `CircleCheck`), `error` (`callout-background-error` tint, `destructive/40` border, red `CircleX` icon and title, foreground text). Icon and ✕ at the top, outline action button centred on the right (D168, D169).
23
+ - Usable on its own as a static toast (docs, previews).
24
+
25
+ ### `toast()` (`@cl-light-ds/ui/lib/toast`)
26
+
27
+ - `toast({ kind, title, description, action: { label, onClick, href, download }, duration, id })` returns the id; `toast.update`, `toast.dismiss`, `toast.clear`. Wraps the runtime's inbound `toast:*` events; calls made before the Toaster is bound are queued.
28
+ - `action.href` (+ `download`) opens or downloads a file: added on top of the runtime, whose action is a plain button.
29
+
30
+ ### Storybook
31
+
32
+ - "Toast" section: static kinds, live buttons per kind, a stack of three, and Clear all.
@@ -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`.
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable changes to the `ToggleGroup` component will be documented in this file.
4
4
 
5
+ ## Date 15th Oct 2026
6
+ ## Workspace selection handover (C26)
7
+
8
+ ### Added
9
+
10
+ - `variant="segmented"` (`variant: "default" | "outline" | "segmented"`): the outline look (shared borders, shadow) with the pressed item filled `muted` in `foreground` text, and kept `muted` on hover. For a view switcher (card/list), where one item is always the current view. Replaces the workspace-selection screen's `group-data-[view=…]` classes on each item.
11
+ - The fill follows the runtime's own pressed state (`aria-pressed`, set by `@data-slot/toggle-group`), not app classes: set it with `defaultValue` or the `toggle-group:set` event. The pressed colours use a plain ancestor selector (`[data-slot=toggle-group][data-variant=segmented] …[aria-pressed=true]`), which outranks the shared pressed and hover backgrounds; Tailwind's `group-data-*` wraps the group in `:where()` and would only tie with them.
12
+ - Added to the shared `TOGGLE_VARIANTS` / `toggleVariants` (`lib/toggle-shared.ts`) so the controller gives items the outline borders; a standalone `Toggle` accepts it too (outline with a `muted` pressed state), though it's made for groups.
13
+ - Opt-in: `default` and `outline` groups are unchanged.
14
+ - Storybook: a "Segmented" preview beside the outline one.
15
+
5
16
  ## 2026-09-18 — Figma alignment (ClickLearn Lightning Design Library, node `5455-20808`)
6
17
 
7
18
  Figma documents ToggleGroup across four pages: States (Default, Hover, Pressed, Focus Default, Focus Pressed, Disabled), Sizes (Small 28px, Medium 32px, Large 36px — matching `Toggle`'s own scale), Items (Lead/Middle/Tail item positions × sizes × states), and the top-level Variants page (Small/Medium/Large).