@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.
- package/package.json +20 -18
- package/src/components/alert/Alert.astro +8 -2
- package/src/components/alert/CHANGELOG.md +9 -0
- package/src/components/button/Button.astro +78 -19
- package/src/components/button/CHANGELOG.md +22 -0
- package/src/components/button-group/ButtonGroup.astro +14 -3
- package/src/components/button-group/CHANGELOG.md +17 -0
- package/src/components/callout/CHANGELOG.md +32 -0
- package/src/components/callout/Callout.astro +147 -0
- package/src/components/callout/CalloutDescription.astro +16 -0
- package/src/components/callout/CalloutTitle.astro +16 -0
- package/src/components/callout/index.ts +3 -0
- package/src/components/card/CHANGELOG.md +9 -0
- package/src/components/card/Card.astro +22 -0
- package/src/components/combobox/CHANGELOG.md +14 -0
- package/src/components/combobox/ComboboxTrigger.astro +31 -1
- package/src/components/command-search/CHANGELOG.md +27 -0
- package/src/components/command-search/CommandSearch.astro +515 -0
- package/src/components/command-search/CommandSearchGroup.astro +32 -0
- package/src/components/command-search/CommandSearchItem.astro +64 -0
- package/src/components/command-search/classes.ts +18 -0
- package/src/components/command-search/index.ts +12 -0
- package/src/components/command-search/nothing-matches.svg +42 -0
- package/src/components/dialog/CHANGELOG.md +13 -0
- package/src/components/dialog/DialogClose.astro +26 -12
- package/src/components/dropdown-menu/CHANGELOG.md +7 -0
- package/src/components/dropdown-menu/dropdown-menu-item-variants.ts +1 -1
- package/src/components/empty-state/CHANGELOG.md +18 -0
- package/src/components/empty-state/EmptyState.astro +157 -0
- package/src/components/empty-state/illustrations/AllCaughtUp.astro +52 -0
- package/src/components/empty-state/illustrations/EmptyWorkspaces.astro +56 -0
- package/src/components/empty-state/illustrations/LoadError.astro +61 -0
- package/src/components/empty-state/illustrations/NoResults.astro +60 -0
- package/src/components/empty-state/illustrations/NothingMatches.astro +66 -0
- package/src/components/empty-state/illustrations/index.ts +27 -0
- package/src/components/empty-state/index.ts +7 -0
- package/src/components/favorite-toggle/CHANGELOG.md +13 -0
- package/src/components/favorite-toggle/FavoriteToggle.astro +94 -0
- package/src/components/favorite-toggle/index.ts +1 -0
- package/src/components/input/CHANGELOG.md +15 -0
- package/src/components/input/InputOTP.astro +211 -0
- package/src/components/input/index.ts +1 -1
- package/src/components/input-group/CHANGELOG.md +7 -0
- package/src/components/input-group/InputGroup.astro +2 -0
- package/src/components/loading-overlay/CHANGELOG.md +25 -0
- package/src/components/loading-overlay/LoadingOverlay.astro +179 -0
- package/src/components/loading-overlay/index.ts +8 -0
- package/src/components/logo/CHANGELOG.md +17 -0
- package/src/components/logo/Logo.astro +90 -0
- package/src/components/logo/index.ts +3 -0
- package/src/components/logo/logo-paths.ts +57 -0
- package/src/components/notifications-menu/CHANGELOG.md +22 -0
- package/src/components/notifications-menu/NotificationsMenu.astro +374 -0
- package/src/components/notifications-menu/NotificationsMenuItem.astro +129 -0
- package/src/components/notifications-menu/all-caught-up.svg +28 -0
- package/src/components/notifications-menu/index.ts +3 -0
- package/src/components/notifications-menu/types.ts +32 -0
- package/src/components/option-card/CHANGELOG.md +16 -0
- package/src/components/option-card/OptionCard.astro +135 -0
- package/src/components/option-card/index.ts +1 -0
- package/src/components/organisms/top-bar/CHANGELOG.md +21 -0
- package/src/components/organisms/top-bar/TopBar.astro +264 -0
- package/src/components/organisms/top-bar/index.ts +2 -0
- package/src/components/organisms/top-bar/types.ts +30 -0
- package/src/components/organisms/workspace/CHANGELOG.md +73 -0
- package/src/components/organisms/workspace/OrganisationOverview.astro +62 -0
- package/src/components/organisms/workspace/RecentWorkspaceCard.astro +235 -0
- package/src/components/organisms/workspace/RecentWorkspaces.astro +93 -0
- package/src/components/organisms/workspace/WorkspaceAvatar.astro +144 -0
- package/src/components/organisms/workspace/WorkspaceCard.astro +172 -0
- package/src/components/organisms/workspace/WorkspaceGlyph.astro +62 -0
- package/src/components/organisms/workspace/WorkspaceIcon.astro +42 -0
- package/src/components/organisms/workspace/WorkspaceIconPicker.astro +288 -0
- package/src/components/organisms/workspace/WorkspaceIconPickerGlyph.astro +44 -0
- package/src/components/organisms/workspace/icons.ts +98 -0
- package/src/components/organisms/workspace/index.ts +35 -0
- package/src/components/organisms/workspace/labels.ts +54 -0
- package/src/components/organisms/workspace/recent-card-rings-strong.png +0 -0
- package/src/components/organisms/workspace/types.ts +30 -0
- package/src/components/profile-menu/CHANGELOG.md +22 -0
- package/src/components/profile-menu/ProfileMenu.astro +236 -0
- package/src/components/profile-menu/index.ts +2 -0
- package/src/components/profile-menu/types.ts +27 -0
- package/src/components/search-field/CHANGELOG.md +14 -0
- package/src/components/search-field/SearchField.astro +163 -0
- package/src/components/search-field/index.ts +1 -0
- package/src/components/table/CHANGELOG.md +9 -0
- package/src/components/table/TableCell.astro +6 -0
- package/src/components/table/TableHead.astro +5 -1
- package/src/components/table/TableRow.astro +3 -1
- package/src/components/table/table-cell-variants.ts +2 -0
- package/src/components/toast/CHANGELOG.md +32 -0
- package/src/components/toast/Toast.astro +145 -0
- package/src/components/toast/Toaster.astro +322 -0
- package/src/components/toast/index.ts +9 -0
- package/src/components/toggle/CHANGELOG.md +7 -0
- package/src/lib/command-search.ts +73 -0
- package/src/lib/loading-overlay.ts +174 -0
- package/src/lib/toast.ts +127 -0
- package/src/lib/toggle-shared.ts +4 -1
- package/src/lib/workspace-palette.ts +119 -0
- package/src/styles/CHANGELOG.md +22 -0
- package/src/styles/globals.css +100 -1
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* @component LoadingOverlay
|
|
4
|
+
* @title Loading Overlay
|
|
5
|
+
* @description The global loading pattern: the whole screen dimmed and blurred, a 48px spinner, a 30px title and status lines that change as the work goes on. It can show what comes next behind the dimming, then fade away to reveal it.
|
|
6
|
+
*
|
|
7
|
+
* @preview
|
|
8
|
+
*
|
|
9
|
+
* <LoadingOverlay
|
|
10
|
+
* id="demo-overlay"
|
|
11
|
+
* title="Opening workspace…"
|
|
12
|
+
* lines={["Loading your recordings…", "Fetching guides and assets…", "Getting your workspace ready…"]}
|
|
13
|
+
* />
|
|
14
|
+
* <Button data-demo-overlay>Show for 4 seconds</Button>
|
|
15
|
+
*
|
|
16
|
+
* @usage
|
|
17
|
+
*
|
|
18
|
+
* ```astro nocollapse
|
|
19
|
+
* ---
|
|
20
|
+
* import { LoadingOverlay } from '@cl-light-ds/ui/components/loading-overlay';
|
|
21
|
+
* ---
|
|
22
|
+
*
|
|
23
|
+
* <LoadingOverlay title="Creating workspace…" lines={["Creating scaffolding…", "Setting up roles and permissions…"]} />
|
|
24
|
+
*
|
|
25
|
+
* <script>
|
|
26
|
+
* import { showLoadingOverlay, revealLoadingOverlay } from '@cl-light-ds/ui/lib/loading-overlay';
|
|
27
|
+
*
|
|
28
|
+
* showLoadingOverlay();
|
|
29
|
+
* await createWorkspace();
|
|
30
|
+
* await revealLoadingOverlay();
|
|
31
|
+
* </script>
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @api
|
|
35
|
+
*
|
|
36
|
+
* ### JS (`@cl-light-ds/ui/lib/loading-overlay`)
|
|
37
|
+
*
|
|
38
|
+
* | Function | Description |
|
|
39
|
+
* |----------|-------------|
|
|
40
|
+
* | `showLoadingOverlay({ title, lines, interval, loop, behind, overlay })` | Show it (or restart with new text) |
|
|
41
|
+
* | `setLoadingOverlayBehind(show)` | Fade the `behind` slot in or out under the dimming |
|
|
42
|
+
* | `revealLoadingOverlay({ duration })` | Fade the dimming and text away, then hide; returns a Promise |
|
|
43
|
+
* | `hideLoadingOverlay()` | Hide at once |
|
|
44
|
+
*
|
|
45
|
+
* ### Events (on the overlay)
|
|
46
|
+
*
|
|
47
|
+
* Inbound: `loading-overlay:show` (detail: show options), `loading-overlay:hide`, `loading-overlay:reveal` (`{ duration }`), `loading-overlay:behind` (`{ show }`).
|
|
48
|
+
* Outbound: `loading-overlay:change` (`{ open }`).
|
|
49
|
+
*
|
|
50
|
+
* ### Data attributes
|
|
51
|
+
*
|
|
52
|
+
* `data-reveal` (`on` | `off`), `data-behind` (`on` | `off`), `aria-busy` while running.
|
|
53
|
+
*/
|
|
54
|
+
import { cn } from "@cl-light-ds/ui/lib/utils";
|
|
55
|
+
import type { HTMLAttributes } from "astro/types";
|
|
56
|
+
|
|
57
|
+
import Spinner from "../spinner/Spinner.astro";
|
|
58
|
+
|
|
59
|
+
interface Props extends HTMLAttributes<"div"> {
|
|
60
|
+
/** The big title, e.g. "Opening workspace…". */
|
|
61
|
+
title?: string;
|
|
62
|
+
/** Status lines shown in turn under the title. */
|
|
63
|
+
lines?: string[];
|
|
64
|
+
/** ms between status lines (D135: 0.9s). */
|
|
65
|
+
interval?: number;
|
|
66
|
+
/** Start again after the last line instead of staying on it. */
|
|
67
|
+
loop?: boolean;
|
|
68
|
+
/** Render it shown (static screens, docs). Otherwise hidden until shown from JS. */
|
|
69
|
+
open?: boolean;
|
|
70
|
+
class?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const {
|
|
74
|
+
title = "Loading…",
|
|
75
|
+
lines = [],
|
|
76
|
+
interval = 900,
|
|
77
|
+
loop = false,
|
|
78
|
+
open = false,
|
|
79
|
+
class: className = "",
|
|
80
|
+
...rest
|
|
81
|
+
} = Astro.props as Props;
|
|
82
|
+
|
|
83
|
+
const hasBehind = Astro.slots.has("behind");
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
<div
|
|
87
|
+
data-slot="loading-overlay"
|
|
88
|
+
data-lines={JSON.stringify(lines)}
|
|
89
|
+
data-interval={interval}
|
|
90
|
+
data-loop={loop ? "true" : "false"}
|
|
91
|
+
data-reveal="off"
|
|
92
|
+
data-behind="off"
|
|
93
|
+
aria-busy={open ? "true" : undefined}
|
|
94
|
+
tabindex="-1"
|
|
95
|
+
hidden={!open}
|
|
96
|
+
class={cn(
|
|
97
|
+
"group/overlay text-background fixed inset-0 z-50 flex flex-col items-center justify-center gap-5 outline-none",
|
|
98
|
+
className,
|
|
99
|
+
)}
|
|
100
|
+
{...rest}
|
|
101
|
+
>
|
|
102
|
+
{
|
|
103
|
+
/* D135, D191 — what comes next, behind the dimming; fades in on `data-behind="on"`. Not interactive. */
|
|
104
|
+
hasBehind && (
|
|
105
|
+
<div
|
|
106
|
+
data-slot="loading-overlay-behind"
|
|
107
|
+
aria-hidden="true"
|
|
108
|
+
inert
|
|
109
|
+
class="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-700 group-data-[behind=on]/overlay:opacity-100 motion-reduce:transition-none"
|
|
110
|
+
>
|
|
111
|
+
<slot name="behind" />
|
|
112
|
+
</div>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
{/* D117 — the dimming; D143 — fades away on reveal. */}
|
|
116
|
+
<div
|
|
117
|
+
data-slot="loading-overlay-backdrop"
|
|
118
|
+
aria-hidden="true"
|
|
119
|
+
class="bg-foreground/60 absolute inset-0 backdrop-blur-sm transition-opacity duration-700 group-data-[reveal=on]/overlay:opacity-0 motion-reduce:transition-none"
|
|
120
|
+
>
|
|
121
|
+
</div>
|
|
122
|
+
<Spinner
|
|
123
|
+
aria-hidden="true"
|
|
124
|
+
role="presentation"
|
|
125
|
+
class="relative size-12 transition-opacity duration-500 group-data-[reveal=on]/overlay:opacity-0 motion-reduce:transition-none"
|
|
126
|
+
/>
|
|
127
|
+
<div
|
|
128
|
+
role="status"
|
|
129
|
+
aria-live="polite"
|
|
130
|
+
class="relative flex flex-col items-center gap-1 px-6 text-center transition-opacity duration-500 group-data-[reveal=on]/overlay:opacity-0 motion-reduce:transition-none"
|
|
131
|
+
>
|
|
132
|
+
<p data-slot="loading-overlay-title" class="font-heading text-3xl font-semibold tracking-tight">
|
|
133
|
+
{title}
|
|
134
|
+
</p>
|
|
135
|
+
<p
|
|
136
|
+
data-slot="loading-overlay-status"
|
|
137
|
+
class="text-background/80 text-base"
|
|
138
|
+
hidden={lines.length === 0}
|
|
139
|
+
>
|
|
140
|
+
{lines[0]}
|
|
141
|
+
</p>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<script>
|
|
146
|
+
import {
|
|
147
|
+
hideLoadingOverlay,
|
|
148
|
+
revealLoadingOverlay,
|
|
149
|
+
setLoadingOverlayBehind,
|
|
150
|
+
showLoadingOverlay,
|
|
151
|
+
type LoadingOverlayShowOptions,
|
|
152
|
+
} from "@cl-light-ds/ui/lib/loading-overlay";
|
|
153
|
+
|
|
154
|
+
document.querySelectorAll<HTMLElement>('[data-slot="loading-overlay"]').forEach((root) => {
|
|
155
|
+
if (root.dataset.wired === "true") return;
|
|
156
|
+
root.dataset.wired = "true";
|
|
157
|
+
root.addEventListener("loading-overlay:show", (event) =>
|
|
158
|
+
showLoadingOverlay({
|
|
159
|
+
...((event as CustomEvent<LoadingOverlayShowOptions>).detail ?? {}),
|
|
160
|
+
overlay: root,
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
root.addEventListener("loading-overlay:hide", () => hideLoadingOverlay(root));
|
|
164
|
+
root.addEventListener("loading-overlay:reveal", (event) =>
|
|
165
|
+
revealLoadingOverlay({
|
|
166
|
+
...((event as CustomEvent<{ duration?: number }>).detail ?? {}),
|
|
167
|
+
overlay: root,
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
root.addEventListener("loading-overlay:behind", (event) =>
|
|
171
|
+
setLoadingOverlayBehind(
|
|
172
|
+
(event as CustomEvent<{ show?: boolean }>).detail?.show ?? true,
|
|
173
|
+
root,
|
|
174
|
+
),
|
|
175
|
+
);
|
|
176
|
+
// Rendered open: run its status lines.
|
|
177
|
+
if (!root.hidden) showLoadingOverlay({ overlay: root });
|
|
178
|
+
});
|
|
179
|
+
</script>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { default as LoadingOverlay } from "./LoadingOverlay.astro";
|
|
2
|
+
export {
|
|
3
|
+
showLoadingOverlay,
|
|
4
|
+
hideLoadingOverlay,
|
|
5
|
+
revealLoadingOverlay,
|
|
6
|
+
setLoadingOverlayBehind,
|
|
7
|
+
} from "../../lib/loading-overlay";
|
|
8
|
+
export type { LoadingOverlayShowOptions } from "../../lib/loading-overlay";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `Logo` component will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## 2026-10-06 — Initial implementation
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `Logo` — inline SVG ClickLearn logo. Props: `variant` (`full` | `mark` | `wordmark`), `tone` (`color` | `dark` | `white`), `size` (`sm` 24px | `default` 32px | `lg` 48px | `xl` 64px height, width auto), `title` (accessible name; `""` marks it decorative).
|
|
10
|
+
- `logoSvg(variant, tone)` — returns standalone SVG markup for the same artwork (used for the downloads on the Foundations → Logos page).
|
|
11
|
+
- `LOGO_COLORS` — mark `#3785E0` (`--primary-500`), wordmark `#112843` (`--primary-950`).
|
|
12
|
+
|
|
13
|
+
### Notes
|
|
14
|
+
|
|
15
|
+
- Artwork taken from the logo previously inlined in the storybook sidebar. The Figma "ClickLearn Logo" component set (library key `87ef4404…`) was not reachable through the Figma API at the time of writing; align variants/tones with it once a node link is available.
|
|
16
|
+
- Fills are fixed brand hex values (not theme tokens) so the logo stays on-brand regardless of theme; use `tone="white"` on dark or brand-coloured surfaces.
|
|
17
|
+
- No `clipPath`/`id`s are used, so multiple logos on a page never clash.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* @component Logo
|
|
4
|
+
* @title Logo
|
|
5
|
+
* @description The ClickLearn logo as an inline SVG: full lockup, mark only or wordmark only, in brand colour, dark or white.
|
|
6
|
+
* @status stable
|
|
7
|
+
* @since 0.4.0
|
|
8
|
+
*
|
|
9
|
+
* @preview
|
|
10
|
+
*
|
|
11
|
+
* <Logo />
|
|
12
|
+
*
|
|
13
|
+
* @usage
|
|
14
|
+
*
|
|
15
|
+
* ```astro nocollapse
|
|
16
|
+
* ---
|
|
17
|
+
* import { Logo } from '@cl-light-ds/ui/components/logo';
|
|
18
|
+
* ---
|
|
19
|
+
* <Logo variant="full" tone="color" size="default" />
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
import { cva } from "class-variance-authority";
|
|
23
|
+
import { cn } from "@cl-light-ds/ui/lib/utils";
|
|
24
|
+
import type { HTMLAttributes } from "astro/types";
|
|
25
|
+
import {
|
|
26
|
+
LOGO_VIEWBOX,
|
|
27
|
+
MARK_PATHS,
|
|
28
|
+
WORDMARK_PATHS,
|
|
29
|
+
logoFills,
|
|
30
|
+
type LogoTone,
|
|
31
|
+
type LogoVariant,
|
|
32
|
+
} from "./logo-paths";
|
|
33
|
+
|
|
34
|
+
type LogoSize = "sm" | "default" | "lg" | "xl";
|
|
35
|
+
|
|
36
|
+
interface Props extends HTMLAttributes<"svg"> {
|
|
37
|
+
variant?: LogoVariant;
|
|
38
|
+
tone?: LogoTone;
|
|
39
|
+
size?: LogoSize;
|
|
40
|
+
/** Accessible name. Pass an empty string when the logo is decorative (e.g. next to visible "ClickLearn" text). */
|
|
41
|
+
title?: string;
|
|
42
|
+
class?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const {
|
|
46
|
+
class: className = "",
|
|
47
|
+
variant = "full",
|
|
48
|
+
tone = "color",
|
|
49
|
+
size = "default",
|
|
50
|
+
title = "ClickLearn",
|
|
51
|
+
...rest
|
|
52
|
+
} = Astro.props as Props;
|
|
53
|
+
|
|
54
|
+
const logoVariants = cva("w-auto shrink-0", {
|
|
55
|
+
variants: {
|
|
56
|
+
size: {
|
|
57
|
+
sm: "h-6",
|
|
58
|
+
default: "h-8",
|
|
59
|
+
lg: "h-12",
|
|
60
|
+
xl: "h-16",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
defaultVariants: {
|
|
64
|
+
size: "default",
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const fills = logoFills(tone);
|
|
69
|
+
const showMark = variant !== "wordmark";
|
|
70
|
+
const showWordmark = variant !== "mark";
|
|
71
|
+
const decorative = title === "";
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
<svg
|
|
75
|
+
{...rest}
|
|
76
|
+
data-slot="logo"
|
|
77
|
+
data-variant={variant}
|
|
78
|
+
data-tone={tone}
|
|
79
|
+
viewBox={LOGO_VIEWBOX[variant]}
|
|
80
|
+
fill="none"
|
|
81
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
82
|
+
role={decorative ? undefined : "img"}
|
|
83
|
+
aria-label={decorative ? undefined : title}
|
|
84
|
+
aria-hidden={decorative ? "true" : undefined}
|
|
85
|
+
class={cn(logoVariants({ size }), className)}
|
|
86
|
+
>
|
|
87
|
+
{!decorative && <title>{title}</title>}
|
|
88
|
+
{showWordmark && WORDMARK_PATHS.map((d) => <path d={d} fill={fills.wordmark} />)}
|
|
89
|
+
{showMark && MARK_PATHS.map((d) => <path d={d} fill={fills.mark} />)}
|
|
90
|
+
</svg>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClickLearn logo artwork. Single source for the `Logo` component and for
|
|
3
|
+
* generating downloadable SVG files.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type LogoVariant = "full" | "mark" | "wordmark";
|
|
7
|
+
export type LogoTone = "color" | "dark" | "white";
|
|
8
|
+
|
|
9
|
+
/** Brand colours of the logo artwork (mark = primary-500, wordmark = primary-950). */
|
|
10
|
+
export const LOGO_COLORS = {
|
|
11
|
+
mark: "#3785E0",
|
|
12
|
+
wordmark: "#112843",
|
|
13
|
+
white: "#FFFFFF",
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
/** Paths for the speech-bubble mark, in the full lockup's coordinate space. */
|
|
17
|
+
export const MARK_PATHS = [
|
|
18
|
+
"M54.4204 27.2102C39.3935 27.2102 27.2102 39.3936 27.2102 54.4204C27.2102 69.4472 39.3935 81.6306 54.4204 81.6306C69.4472 81.6306 81.6306 69.4472 81.6306 54.4204C81.6306 39.3936 69.4472 27.2102 54.4204 27.2102ZM54.4204 68.0255C46.9172 68.0255 40.8153 61.9236 40.8153 54.4204C40.8153 46.9172 46.9172 40.8153 54.4204 40.8153C61.9236 40.8153 68.0255 46.9172 68.0255 54.4204C68.0255 61.9236 61.9236 68.0255 54.4204 68.0255Z",
|
|
19
|
+
"M54.4204 0C24.3633 0 0 24.3633 0 54.4204C0 79.8291 17.4104 101.169 40.9529 107.162C48.2346 109.015 54.4204 114.932 54.4204 122.446C77.8586 122.446 108.841 95.4465 108.841 54.4204C108.841 24.3633 84.4774 0 54.4204 0ZM54.4204 95.2357C31.9141 95.2357 13.6051 76.9266 13.6051 54.4204C13.6051 31.9142 31.9141 13.6051 54.4204 13.6051C76.9266 13.6051 95.2357 31.9142 95.2357 54.4204C95.2357 76.9266 76.9266 95.2357 54.4204 95.2357Z",
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
/** Paths for the "ClickLearn" wordmark, in the full lockup's coordinate space. */
|
|
23
|
+
export const WORDMARK_PATHS = [
|
|
24
|
+
"M136.051 54.4202C136.051 73.1851 151.22 88.433 170.002 88.433C176.325 88.433 182.192 86.8106 187.07 83.8311V69.7566C183.186 74.0864 177.227 76.7938 170.274 76.7938C157.812 76.7938 148.241 67.0492 148.241 54.4202C148.241 41.7913 157.812 32.0467 170.274 32.0467C177.227 32.0467 183.189 34.7541 187.07 39.0839V25.0094C182.192 22.0333 176.325 20.4075 170.002 20.4075C151.22 20.4075 136.051 35.6554 136.051 54.4202Z",
|
|
25
|
+
"M227.079 40.8152H215.175V87.0725H227.079V40.8152Z",
|
|
26
|
+
"M233.307 64.1139C233.307 78.0897 244.643 88.433 257.888 88.433C261.969 88.433 265.779 87.5248 268.68 85.8922V74.4572C265.867 76.7258 262.874 77.9945 258.612 77.9945C251.174 77.9945 244.735 72.2769 244.735 64.1105C244.735 55.944 251.174 50.3183 258.612 50.3183C262.874 50.3183 265.867 51.5904 268.68 53.7672V42.3321C265.779 40.6995 261.969 39.7914 257.888 39.7914C244.647 39.7914 233.307 50.138 233.307 64.1139Z",
|
|
27
|
+
"M384.334 39.7948C371.654 39.7948 360.514 50.2265 360.514 64.2057C360.514 78.1849 371.654 88.433 386.235 88.433C392.031 88.433 398.371 86.9806 402.99 83.8038V73.2769C399.003 76.5421 393.119 78.4502 387.684 78.4502C379.17 78.4502 373.011 74.3653 371.742 67.1988H406.34C406.432 66.1989 406.432 65.2907 406.432 64.3826C406.432 50.5904 399.007 39.7914 384.334 39.7914V39.7948ZM372.106 59.2126C373.735 53.223 378.354 49.4102 384.15 49.4102C390.946 49.4102 394.656 54.3148 395.201 59.2126H372.106Z",
|
|
28
|
+
"M193.872 87.0725H205.777V19.047C201.355 20.4075 198.634 20.7476 193.872 20.7476V87.0725Z",
|
|
29
|
+
"M317.614 40.8152H303.523L287.387 59.2603V19.047C282.965 20.4075 280.244 20.7476 275.483 20.7476V87.0725H287.387V66.09L304.155 87.0725H318.339L298.013 62.318L317.614 40.8152Z",
|
|
30
|
+
"M471.103 40.873H459.151V87.0725H471.056V64.5969C471.056 55.6311 476.447 51.9203 483.331 51.9203C484.236 51.9203 485.025 51.9475 486.021 52.0394V40.0873C478.879 39.4546 473.641 43.5906 471.103 48.0293V40.873Z",
|
|
31
|
+
"M517.004 39.7948C511.096 39.7948 506.279 42.5906 503.824 46.2912V40.1995H491.824V87.0725H503.728V60.6378C503.728 55.1345 507.184 50.5326 512.912 50.5326C518.64 50.5326 521.755 55.1345 521.755 60.5493V87.0759H534V57.393C534 47.1075 527.728 39.7982 517.004 39.7982V39.7948Z",
|
|
32
|
+
"M430.645 39.7948C424.584 39.7948 418.55 41.5396 414.931 43.5362V54.0801C418.642 51.8999 423.224 50.138 428.111 50.138C435.081 50.138 439.063 53.7706 439.063 58.4882V61.6888C437.162 59.6922 432.819 57.8521 427.57 57.8521C417.884 57.8521 409.829 63.9336 409.829 72.9164C409.829 82.4467 417.884 88.433 427.207 88.433C432.999 88.433 437.363 86.1916 439.08 84.0113V87.0725H450.645V57.9406C450.645 46.5973 443.043 39.7948 430.645 39.7948ZM429.921 79.5387C425.217 79.5387 420.782 77.1782 420.782 72.9164C420.782 68.6546 425.217 66.1989 429.921 66.1989C434.625 66.1989 439.244 68.6478 439.244 72.9164C439.244 77.185 434.536 79.5387 429.921 79.5387Z",
|
|
33
|
+
"M221.083 33.2065C224.84 33.2065 227.885 30.1609 227.885 26.404C227.885 22.647 224.84 19.6014 221.083 19.6014C217.326 19.6014 214.28 22.647 214.28 26.404C214.28 30.1609 217.326 33.2065 221.083 33.2065Z",
|
|
34
|
+
"M333.984 22.4483C329.563 23.8088 326.842 24.1489 322.08 24.1489V87.0725H359.494V76.8687H333.984V22.4483Z",
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
/** viewBox per variant — crops the shared coordinate space. */
|
|
38
|
+
export const LOGO_VIEWBOX: Record<LogoVariant, string> = {
|
|
39
|
+
full: "0 0 534 122.446",
|
|
40
|
+
mark: "0 0 108.841 122.446",
|
|
41
|
+
wordmark: "136.051 19.047 397.949 69.386",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function logoFills(tone: LogoTone) {
|
|
45
|
+
if (tone === "white") return { mark: LOGO_COLORS.white, wordmark: LOGO_COLORS.white };
|
|
46
|
+
if (tone === "dark") return { mark: LOGO_COLORS.wordmark, wordmark: LOGO_COLORS.wordmark };
|
|
47
|
+
return { mark: LOGO_COLORS.mark, wordmark: LOGO_COLORS.wordmark };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Standalone SVG markup, e.g. for download links. */
|
|
51
|
+
export function logoSvg(variant: LogoVariant, tone: LogoTone = "color"): string {
|
|
52
|
+
const fills = logoFills(tone);
|
|
53
|
+
const [, , width, height] = LOGO_VIEWBOX[variant].split(" ").map(Number);
|
|
54
|
+
const mark = variant === "wordmark" ? [] : MARK_PATHS.map((d) => `<path d="${d}" fill="${fills.mark}"/>`);
|
|
55
|
+
const word = variant === "mark" ? [] : WORDMARK_PATHS.map((d) => `<path d="${d}" fill="${fills.wordmark}"/>`);
|
|
56
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${LOGO_VIEWBOX[variant]}" width="${width}" height="${height}" fill="none"><title>ClickLearn</title>${[...word, ...mark].join("")}</svg>`;
|
|
57
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `NotificationsMenu` component family (`NotificationsMenu`, `NotificationsMenuItem`) are documented in this file.
|
|
4
|
+
|
|
5
|
+
## Date 15th Oct 2026
|
|
6
|
+
## Workspace selection handover (N10)
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- `NotificationsMenu`: a ghost `icon-sm` bell (`PopoverTrigger`) with a red `destructive` dot while any notification is unseen, opening a 360px (`w-90`) `PopoverContent` (`align="end"`, `sideOffset` 6): a header with the title and a link-style **Clear all**, a separator, a scrolling list (max 320px), and an empty state.
|
|
11
|
+
- `NotificationsMenuItem`: avatar (DS `Avatar size="sm"`, initials or image) or a 24px icon chip (`tone`: `default` / `success` / `warning` / `destructive`), title, text, a "context · time" line (`<time>` when `datetime` is given), a blue `primary` "new" dot centred on the right (D154, no tint, D162) with sr-only "(new)", and a ✕ (`Button` ghost `icon-xs`, C25) shown on hover / focus-within (always on coarse pointers). Default slot for extra content (e.g. a Download button); `media` slot to replace the avatar / icon.
|
|
12
|
+
- Items come from the `notifications` prop (JSON-serialisable data) and/or slotted `NotificationsMenuItem`s.
|
|
13
|
+
- Seen logic (D141, D165): an item counts as seen once `seenThreshold` (60%) of it has been in view in the open list, or at once when hovered or keyboard-focused. Hovered / focused items lose their dot immediately; scrolled-to ones when the popover closes. The bell's dot and accessible name ("Notifications — {count} new") stay in sync.
|
|
14
|
+
- Empty state (D203): the designer's `all-caught-up.svg` (144×104) with "You're all caught up". Copy, illustration (`emptyIllustration`, or `false`) and the whole block (`empty` slot) are overridable.
|
|
15
|
+
- Events on the root: `notifications:dismiss` `{ id }` and `notifications:clear` `{ ids }` (both cancelable), `notifications:seen` `{ ids }`, `notifications:change` `{ total, unseen }`. Inbound `notifications:add` `{ item, position }`; any other DOM change to the list is picked up by a `MutationObserver`.
|
|
16
|
+
- After a dismiss, focus moves to the next (or previous) item's ✕, else to the popover.
|
|
17
|
+
|
|
18
|
+
### Generalised from the reference (cl-design `NotificationsPopover.astro`)
|
|
19
|
+
|
|
20
|
+
- "workspace · time" → `context` + `time`; all copy (title, labels, empty state) is props, with the reference's English as defaults except the empty description, which is now generic.
|
|
21
|
+
- Screen-specific export/duplicate templates are not part of the component: render a `NotificationsMenuItem` (e.g. in a `<template>`) and add it with `notifications:add`.
|
|
22
|
+
- The bell is the trigger itself (`PopoverTrigger` renders the `Button`), so `aria-expanded` / `aria-controls` land on it, not on a wrapper.
|