@cl-light-ds/ui 0.4.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 +19 -17
- 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/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-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/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 +87 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Controller for the `LoadingOverlay` component
|
|
3
|
+
* (`@cl-light-ds/ui/components/loading-overlay`). Works on the overlay's markup
|
|
4
|
+
* directly, so it can be called from any script, before or after the
|
|
5
|
+
* component's own script has run.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { showLoadingOverlay, revealLoadingOverlay, hideLoadingOverlay } from "@cl-light-ds/ui/lib/loading-overlay";
|
|
9
|
+
*
|
|
10
|
+
* showLoadingOverlay({ title: "Opening workspace…", lines: ["Loading your recordings…", "Fetching guides and assets…"] });
|
|
11
|
+
* await work();
|
|
12
|
+
* await revealLoadingOverlay(); // fade the dimming away to what is behind it
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* The same is available as events on the overlay element:
|
|
16
|
+
* `loading-overlay:show` (detail: the show options), `loading-overlay:hide`,
|
|
17
|
+
* `loading-overlay:reveal` (detail: `{ duration }`), `loading-overlay:behind`
|
|
18
|
+
* (detail: `{ show }`). It fires `loading-overlay:change` (`{ open }`).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface LoadingOverlayShowOptions {
|
|
22
|
+
/** Big title, e.g. "Opening workspace…". Defaults to the component's `title`. */
|
|
23
|
+
title?: string;
|
|
24
|
+
/** Status lines shown in turn under the title. Defaults to the component's `lines`. */
|
|
25
|
+
lines?: string[];
|
|
26
|
+
/** ms between status lines. Defaults to the component's `interval` (900). */
|
|
27
|
+
interval?: number;
|
|
28
|
+
/** Start again after the last line instead of staying on it. */
|
|
29
|
+
loop?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Fade in the `behind` slot's content under the dimming: `true` at once, a
|
|
32
|
+
* number of ms to wait first, `false` not at all (default).
|
|
33
|
+
*/
|
|
34
|
+
behind?: boolean | number;
|
|
35
|
+
/** Which overlay: its id or element. Defaults to the first on the page. */
|
|
36
|
+
overlay?: string | HTMLElement;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type Target = string | HTMLElement | undefined;
|
|
40
|
+
|
|
41
|
+
interface RunState {
|
|
42
|
+
lineTimer?: number;
|
|
43
|
+
behindTimer?: number;
|
|
44
|
+
hideTimer?: number;
|
|
45
|
+
returnFocus?: HTMLElement | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const ROOT = '[data-slot="loading-overlay"]';
|
|
49
|
+
const runs = new WeakMap<HTMLElement, RunState>();
|
|
50
|
+
|
|
51
|
+
const findOverlay = (overlay: Target): HTMLElement | null => {
|
|
52
|
+
if (typeof document === "undefined") return null;
|
|
53
|
+
if (overlay instanceof HTMLElement) return overlay;
|
|
54
|
+
if (typeof overlay === "string") return document.getElementById(overlay);
|
|
55
|
+
return document.querySelector<HTMLElement>(ROOT);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const part = (root: HTMLElement, name: string) =>
|
|
59
|
+
root.querySelector<HTMLElement>(`[data-slot="loading-overlay-${name}"]`);
|
|
60
|
+
|
|
61
|
+
const readLines = (root: HTMLElement): string[] => {
|
|
62
|
+
try {
|
|
63
|
+
const lines = JSON.parse(root.dataset.lines ?? "[]");
|
|
64
|
+
return Array.isArray(lines) ? lines.map(String) : [];
|
|
65
|
+
} catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const stop = (root: HTMLElement) => {
|
|
71
|
+
const run = runs.get(root);
|
|
72
|
+
if (!run) return run;
|
|
73
|
+
window.clearInterval(run.lineTimer);
|
|
74
|
+
window.clearTimeout(run.behindTimer);
|
|
75
|
+
window.clearTimeout(run.hideTimer);
|
|
76
|
+
return run;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const emit = (root: HTMLElement, open: boolean) =>
|
|
80
|
+
root.dispatchEvent(new CustomEvent("loading-overlay:change", { detail: { open } }));
|
|
81
|
+
|
|
82
|
+
/** Show the overlay (or restart it with new text). */
|
|
83
|
+
export function showLoadingOverlay(options: LoadingOverlayShowOptions = {}) {
|
|
84
|
+
const root = findOverlay(options.overlay);
|
|
85
|
+
if (!root) return;
|
|
86
|
+
const wasOpen = !root.hidden;
|
|
87
|
+
const previous = stop(root);
|
|
88
|
+
const run: RunState = {
|
|
89
|
+
returnFocus: wasOpen
|
|
90
|
+
? previous?.returnFocus
|
|
91
|
+
: (document.activeElement as HTMLElement | null),
|
|
92
|
+
};
|
|
93
|
+
runs.set(root, run);
|
|
94
|
+
|
|
95
|
+
const title = part(root, "title");
|
|
96
|
+
const status = part(root, "status");
|
|
97
|
+
if (title && options.title !== undefined) title.textContent = options.title;
|
|
98
|
+
const lines = options.lines ?? readLines(root);
|
|
99
|
+
const interval = options.interval ?? (Number(root.dataset.interval) || 900);
|
|
100
|
+
const loop = options.loop ?? root.dataset.loop === "true";
|
|
101
|
+
let at = 0;
|
|
102
|
+
if (status) {
|
|
103
|
+
status.textContent = lines[0] ?? "";
|
|
104
|
+
status.hidden = lines.length === 0;
|
|
105
|
+
}
|
|
106
|
+
if (lines.length > 1) {
|
|
107
|
+
run.lineTimer = window.setInterval(() => {
|
|
108
|
+
at = loop ? (at + 1) % lines.length : Math.min(at + 1, lines.length - 1);
|
|
109
|
+
if (status) status.textContent = lines[at];
|
|
110
|
+
}, interval);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
root.dataset.reveal = "off";
|
|
114
|
+
root.dataset.behind = "off";
|
|
115
|
+
const behind = options.behind ?? false;
|
|
116
|
+
if (behind !== false) {
|
|
117
|
+
run.behindTimer = window.setTimeout(
|
|
118
|
+
() => setLoadingOverlayBehind(true, root),
|
|
119
|
+
behind === true ? 0 : behind,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
root.hidden = false;
|
|
124
|
+
root.setAttribute("aria-busy", "true");
|
|
125
|
+
// Keyboard focus moves off whatever is now covered.
|
|
126
|
+
root.focus({ preventScroll: true });
|
|
127
|
+
if (!wasOpen) emit(root, true);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Fade the `behind` slot's content in (or out) under the dimming. */
|
|
131
|
+
export function setLoadingOverlayBehind(show: boolean, overlay?: Target) {
|
|
132
|
+
const root = findOverlay(overlay);
|
|
133
|
+
if (root && !root.hidden) root.dataset.behind = show ? "on" : "off";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Hide the overlay at once. */
|
|
137
|
+
export function hideLoadingOverlay(overlay?: Target) {
|
|
138
|
+
const root = findOverlay(overlay);
|
|
139
|
+
if (!root || root.hidden) return;
|
|
140
|
+
const run = stop(root);
|
|
141
|
+
runs.delete(root);
|
|
142
|
+
root.hidden = true;
|
|
143
|
+
root.removeAttribute("aria-busy");
|
|
144
|
+
root.dataset.reveal = "off";
|
|
145
|
+
root.dataset.behind = "off";
|
|
146
|
+
if (root.contains(document.activeElement) && run?.returnFocus?.isConnected)
|
|
147
|
+
run.returnFocus.focus({ preventScroll: true });
|
|
148
|
+
emit(root, false);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Fade the dimming, spinner and text away, revealing what is behind (the
|
|
153
|
+
* page, or the `behind` slot), then hide. Resolves once hidden.
|
|
154
|
+
*/
|
|
155
|
+
export function revealLoadingOverlay(
|
|
156
|
+
options: { duration?: number; overlay?: Target } = {},
|
|
157
|
+
): Promise<void> {
|
|
158
|
+
const root = findOverlay(options.overlay);
|
|
159
|
+
if (!root || root.hidden) return Promise.resolve();
|
|
160
|
+
const run = runs.get(root) ?? {};
|
|
161
|
+
window.clearInterval(run.lineTimer);
|
|
162
|
+
window.clearTimeout(run.hideTimer);
|
|
163
|
+
runs.set(root, run);
|
|
164
|
+
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
165
|
+
const duration = reduced ? 0 : (options.duration ?? 700);
|
|
166
|
+
root.dataset.reveal = "on";
|
|
167
|
+
root.removeAttribute("aria-busy");
|
|
168
|
+
return new Promise((resolve) => {
|
|
169
|
+
run.hideTimer = window.setTimeout(() => {
|
|
170
|
+
hideLoadingOverlay(root);
|
|
171
|
+
resolve();
|
|
172
|
+
}, duration);
|
|
173
|
+
});
|
|
174
|
+
}
|
package/src/lib/toast.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Imperative API for the `Toaster` component (`@cl-light-ds/ui/components/toast`).
|
|
3
|
+
*
|
|
4
|
+
* `Toaster` binds `@data-slot/toast` to its root; this module talks to that
|
|
5
|
+
* root through the runtime's inbound events (`toast:show`, `toast:update`,
|
|
6
|
+
* `toast:dismiss`, `toast:clear`), so it never needs the controller itself and
|
|
7
|
+
* works from any bundled script on the page.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { toast } from "@cl-light-ds/ui/lib/toast";
|
|
11
|
+
*
|
|
12
|
+
* const id = toast({ kind: "success", title: "Export ready", description: "Acme.zip", action: { label: "Download", href: "/acme.zip", download: true } });
|
|
13
|
+
* toast.dismiss(id);
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Calls made before the Toaster's script has run are queued and replayed once
|
|
17
|
+
* it is ready (`toaster:ready`).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export type ToastKind = "info" | "success" | "error";
|
|
21
|
+
|
|
22
|
+
export interface ToastActionOptions {
|
|
23
|
+
/** The action button's label, e.g. "Download". */
|
|
24
|
+
label: string;
|
|
25
|
+
/** Runs on click. Call `event.preventDefault()` to keep the toast open. */
|
|
26
|
+
onClick?: (event: { readonly defaultPrevented: boolean; preventDefault(): void }) => void;
|
|
27
|
+
/** Sent with the root's `toast:action` event. */
|
|
28
|
+
value?: string;
|
|
29
|
+
/** Opens (or downloads) this URL on click. */
|
|
30
|
+
href?: string;
|
|
31
|
+
/** With `href`: download the file; a string names the saved file. */
|
|
32
|
+
download?: boolean | string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ToastShowOptions {
|
|
36
|
+
/** Reuse an id to replace a toast in place. Generated when left out. */
|
|
37
|
+
id?: string;
|
|
38
|
+
/** `info` (blue icon, default), `success` (green), `error` (red tint, shakes). */
|
|
39
|
+
kind?: ToastKind;
|
|
40
|
+
title: string;
|
|
41
|
+
description?: string;
|
|
42
|
+
/** ms before it closes by itself; `0` = stays until closed. Defaults to the Toaster's `duration`. */
|
|
43
|
+
duration?: number;
|
|
44
|
+
action?: ToastActionOptions;
|
|
45
|
+
/** `false` hides nothing but disables swipe-to-dismiss. */
|
|
46
|
+
dismissible?: boolean;
|
|
47
|
+
/** Accessible name of the ✕ (default "Close"). */
|
|
48
|
+
closeLabel?: string;
|
|
49
|
+
/** Which Toaster: its id or element. Defaults to the first on the page. */
|
|
50
|
+
toaster?: string | HTMLElement;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type ToastUpdateOptions = Partial<Omit<ToastShowOptions, "id" | "toaster">> & {
|
|
54
|
+
toaster?: string | HTMLElement;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const ROOT = '[data-slot="toast"]';
|
|
58
|
+
const KINDS: readonly ToastKind[] = ["info", "success", "error"];
|
|
59
|
+
let counter = 0;
|
|
60
|
+
|
|
61
|
+
const findRoot = (toaster?: string | HTMLElement): HTMLElement | null => {
|
|
62
|
+
if (typeof document === "undefined") return null;
|
|
63
|
+
if (toaster instanceof HTMLElement) return toaster;
|
|
64
|
+
if (typeof toaster === "string") return document.getElementById(toaster);
|
|
65
|
+
return document.querySelector<HTMLElement>(ROOT);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Send an inbound event, waiting for the Toaster to be bound if needed. */
|
|
69
|
+
const send = (root: HTMLElement | null, name: string, detail?: unknown) => {
|
|
70
|
+
if (!root) {
|
|
71
|
+
console.warn(`[toast] No <Toaster /> found on the page for "${name}".`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const fire = () => root.dispatchEvent(new CustomEvent(name, { detail }));
|
|
75
|
+
if (root.dataset.toasterReady === "true") fire();
|
|
76
|
+
else root.addEventListener("toaster:ready", fire, { once: true });
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const toRuntimeAction = (action: ToastActionOptions) => ({
|
|
80
|
+
label: action.label,
|
|
81
|
+
value: action.value,
|
|
82
|
+
onClick: (event: { readonly defaultPrevented: boolean; preventDefault(): void }) => {
|
|
83
|
+
if (action.href) {
|
|
84
|
+
const link = document.createElement("a");
|
|
85
|
+
link.href = action.href;
|
|
86
|
+
if (action.download)
|
|
87
|
+
link.download = typeof action.download === "string" ? action.download : "";
|
|
88
|
+
link.rel = "noopener";
|
|
89
|
+
link.click();
|
|
90
|
+
}
|
|
91
|
+
action.onClick?.(event);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const toRuntime = (options: Partial<ToastShowOptions>) => {
|
|
96
|
+
const detail: Record<string, unknown> = {};
|
|
97
|
+
if (options.title !== undefined) detail.title = options.title;
|
|
98
|
+
if (options.description !== undefined) detail.description = options.description;
|
|
99
|
+
if (options.kind !== undefined)
|
|
100
|
+
detail.type = KINDS.includes(options.kind) ? options.kind : "info";
|
|
101
|
+
if (options.duration !== undefined) detail.duration = options.duration;
|
|
102
|
+
if (options.dismissible !== undefined) detail.dismissible = options.dismissible;
|
|
103
|
+
if (options.closeLabel !== undefined) detail.closeButtonAriaLabel = options.closeLabel;
|
|
104
|
+
if (options.action !== undefined) detail.action = toRuntimeAction(options.action);
|
|
105
|
+
return detail;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** Show a toast; returns its id. */
|
|
109
|
+
export function toast(options: ToastShowOptions): string {
|
|
110
|
+
const id = options.id ?? `toast-${Date.now().toString(36)}-${(counter += 1)}`;
|
|
111
|
+
send(findRoot(options.toaster), "toast:show", {
|
|
112
|
+
...toRuntime({ kind: "info", ...options }),
|
|
113
|
+
id,
|
|
114
|
+
});
|
|
115
|
+
return id;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Change a toast that is still showing. */
|
|
119
|
+
toast.update = (id: string, patch: ToastUpdateOptions) =>
|
|
120
|
+
send(findRoot(patch.toaster), "toast:update", { ...toRuntime(patch), id });
|
|
121
|
+
|
|
122
|
+
/** Close one toast. */
|
|
123
|
+
toast.dismiss = (id: string, toaster?: string | HTMLElement) =>
|
|
124
|
+
send(findRoot(toaster), "toast:dismiss", { id });
|
|
125
|
+
|
|
126
|
+
/** Close every toast (what "Clear all" does). */
|
|
127
|
+
toast.clear = (toaster?: string | HTMLElement) => send(findRoot(toaster), "toast:clear");
|
package/src/lib/toggle-shared.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
2
|
|
|
3
|
-
export const TOGGLE_VARIANTS = ["default", "outline"] as const;
|
|
3
|
+
export const TOGGLE_VARIANTS = ["default", "outline", "ghost"] as const;
|
|
4
4
|
export const TOGGLE_SIZES = ["xs", "default", "sm", "lg"] as const;
|
|
5
5
|
|
|
6
6
|
export type ToggleVariant = (typeof TOGGLE_VARIANTS)[number];
|
|
@@ -13,6 +13,9 @@ export const toggleVariants = cva(
|
|
|
13
13
|
variant: {
|
|
14
14
|
default: "bg-transparent",
|
|
15
15
|
outline: "border-input hover:bg-background-hover border bg-transparent shadow-xs",
|
|
16
|
+
// Pressed shows in the content only (e.g. a filled star): no pressed
|
|
17
|
+
// background, the normal hover background on hover.
|
|
18
|
+
ghost: "bg-transparent aria-pressed:bg-transparent aria-pressed:hover:bg-background-hover",
|
|
16
19
|
},
|
|
17
20
|
size: {
|
|
18
21
|
xs: "h-7 min-w-7 px-1",
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace palette: the 12 decorative hues a workspace can be given, mapped to
|
|
3
|
+
* the Tailwind classes each coloured part uses. Backed by the `--workspace-*`
|
|
4
|
+
* tokens in globals.css.
|
|
5
|
+
*
|
|
6
|
+
* Literal class strings, so Tailwind generates them.
|
|
7
|
+
*
|
|
8
|
+
* - `solid` avatar background (white icon on it)
|
|
9
|
+
* - `text` the icon colour on a white or tinted avatar
|
|
10
|
+
* - `tint` the soft avatar background
|
|
11
|
+
* - `gradient` the large recent card: the hue at 50% to its deep shade at 50%,
|
|
12
|
+
* over a neutral grey, so it reads washed out rather than vivid.
|
|
13
|
+
* Add a direction yourself, e.g. `bg-linear-to-br`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const WORKSPACE_COLORS = [
|
|
17
|
+
"purple",
|
|
18
|
+
"red",
|
|
19
|
+
"blue",
|
|
20
|
+
"emerald",
|
|
21
|
+
"orange",
|
|
22
|
+
"teal",
|
|
23
|
+
"indigo",
|
|
24
|
+
"amber",
|
|
25
|
+
"pink",
|
|
26
|
+
"sky",
|
|
27
|
+
"lime",
|
|
28
|
+
"slate",
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
export type WorkspaceColor = (typeof WORKSPACE_COLORS)[number];
|
|
32
|
+
|
|
33
|
+
/** The colour a new workspace starts with. */
|
|
34
|
+
export const DEFAULT_WORKSPACE_COLOR: WorkspaceColor = "blue";
|
|
35
|
+
|
|
36
|
+
export interface WorkspaceSwatch {
|
|
37
|
+
solid: string;
|
|
38
|
+
text: string;
|
|
39
|
+
tint: string;
|
|
40
|
+
gradient: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const workspacePalette: Record<WorkspaceColor, WorkspaceSwatch> = {
|
|
44
|
+
purple: {
|
|
45
|
+
solid: "bg-workspace-purple",
|
|
46
|
+
text: "text-workspace-purple",
|
|
47
|
+
tint: "bg-workspace-purple-tint",
|
|
48
|
+
gradient: "bg-workspace-gradient-base from-workspace-purple/50 to-workspace-purple-deep/50",
|
|
49
|
+
},
|
|
50
|
+
red: {
|
|
51
|
+
solid: "bg-workspace-red",
|
|
52
|
+
text: "text-workspace-red",
|
|
53
|
+
tint: "bg-workspace-red-tint",
|
|
54
|
+
gradient: "bg-workspace-gradient-base from-workspace-red/50 to-workspace-red-deep/50",
|
|
55
|
+
},
|
|
56
|
+
blue: {
|
|
57
|
+
solid: "bg-workspace-blue",
|
|
58
|
+
text: "text-workspace-blue",
|
|
59
|
+
tint: "bg-workspace-blue-tint",
|
|
60
|
+
gradient: "bg-workspace-gradient-base from-workspace-blue/50 to-workspace-blue-deep/50",
|
|
61
|
+
},
|
|
62
|
+
emerald: {
|
|
63
|
+
solid: "bg-workspace-emerald",
|
|
64
|
+
text: "text-workspace-emerald",
|
|
65
|
+
tint: "bg-workspace-emerald-tint",
|
|
66
|
+
gradient: "bg-workspace-gradient-base from-workspace-emerald/50 to-workspace-emerald-deep/50",
|
|
67
|
+
},
|
|
68
|
+
orange: {
|
|
69
|
+
solid: "bg-workspace-orange",
|
|
70
|
+
text: "text-workspace-orange",
|
|
71
|
+
tint: "bg-workspace-orange-tint",
|
|
72
|
+
gradient: "bg-workspace-gradient-base from-workspace-orange/50 to-workspace-orange-deep/50",
|
|
73
|
+
},
|
|
74
|
+
teal: {
|
|
75
|
+
solid: "bg-workspace-teal",
|
|
76
|
+
text: "text-workspace-teal",
|
|
77
|
+
tint: "bg-workspace-teal-tint",
|
|
78
|
+
gradient: "bg-workspace-gradient-base from-workspace-teal/50 to-workspace-teal-deep/50",
|
|
79
|
+
},
|
|
80
|
+
indigo: {
|
|
81
|
+
solid: "bg-workspace-indigo",
|
|
82
|
+
text: "text-workspace-indigo",
|
|
83
|
+
tint: "bg-workspace-indigo-tint",
|
|
84
|
+
gradient: "bg-workspace-gradient-base from-workspace-indigo/50 to-workspace-indigo-deep/50",
|
|
85
|
+
},
|
|
86
|
+
amber: {
|
|
87
|
+
solid: "bg-workspace-amber",
|
|
88
|
+
text: "text-workspace-amber",
|
|
89
|
+
tint: "bg-workspace-amber-tint",
|
|
90
|
+
gradient: "bg-workspace-gradient-base from-workspace-amber/50 to-workspace-amber-deep/50",
|
|
91
|
+
},
|
|
92
|
+
pink: {
|
|
93
|
+
solid: "bg-workspace-pink",
|
|
94
|
+
text: "text-workspace-pink",
|
|
95
|
+
tint: "bg-workspace-pink-tint",
|
|
96
|
+
gradient: "bg-workspace-gradient-base from-workspace-pink/50 to-workspace-pink-deep/50",
|
|
97
|
+
},
|
|
98
|
+
sky: {
|
|
99
|
+
solid: "bg-workspace-sky",
|
|
100
|
+
text: "text-workspace-sky",
|
|
101
|
+
tint: "bg-workspace-sky-tint",
|
|
102
|
+
gradient: "bg-workspace-gradient-base from-workspace-sky/50 to-workspace-sky-deep/50",
|
|
103
|
+
},
|
|
104
|
+
lime: {
|
|
105
|
+
solid: "bg-workspace-lime",
|
|
106
|
+
text: "text-workspace-lime",
|
|
107
|
+
tint: "bg-workspace-lime-tint",
|
|
108
|
+
gradient: "bg-workspace-gradient-base from-workspace-lime/50 to-workspace-lime-deep/50",
|
|
109
|
+
},
|
|
110
|
+
slate: {
|
|
111
|
+
solid: "bg-workspace-slate",
|
|
112
|
+
text: "text-workspace-slate",
|
|
113
|
+
tint: "bg-workspace-slate-tint",
|
|
114
|
+
gradient: "bg-workspace-gradient-base from-workspace-slate/50 to-workspace-slate-deep/50",
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** The brand gradient (primary to its deep blue), for brand-toned surfaces. Add a direction yourself. */
|
|
119
|
+
export const brandGradient = "from-primary to-chart-4";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the design tokens in `globals.css` will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## Date 15th Oct 2026
|
|
6
|
+
## Workspace selection handover (N1, N8, N19, C17)
|
|
7
|
+
|
|
8
|
+
### Feedback colours (gaps G7, G13)
|
|
9
|
+
|
|
10
|
+
- Uses the existing `--warning`, `--warning-foreground` and `--callout-background-*` tokens (utilities `*-warning`, `*-callout-info/-warning/-error`).
|
|
11
|
+
- Added `--callout-foreground-warning` (Yellow/800) and `--callout-foreground-error` (Red/700); Yellow/300 and Red/300 in dark mode: darker text that reads on the callout backgrounds. **Not in Figma yet.**
|
|
12
|
+
|
|
13
|
+
### Favourite (gap G3)
|
|
14
|
+
|
|
15
|
+
- `--favorite` (Yellow/500, utility `*-favorite`): the starred colour. Separate from `badge-warning` and `warning`, which were being borrowed for it. **Not in Figma yet.**
|
|
16
|
+
|
|
17
|
+
### Workspace palette (gap G4)
|
|
18
|
+
|
|
19
|
+
- 12 decorative hues for workspace avatars and cards: purple, red, blue, emerald, orange, teal, indigo, amber, pink, sky, lime, slate. Each has `--workspace-{hue}` (600: solid background and icon colour), `--workspace-{hue}-tint` (100: soft background) and `--workspace-{hue}-deep` (800: gradient end). `--workspace-gradient-base` (Zinc/600) sits under the large card's gradient. **Not in Figma yet.**
|
|
20
|
+
- `lib/workspace-palette.ts` maps each hue to its class strings (`solid`, `text`, `tint`, `gradient`), plus `WORKSPACE_COLORS`, `DEFAULT_WORKSPACE_COLOR` (`blue`) and `brandGradient` (`from-primary to-chart-4`). It's a drop-in replacement for the screen's `palette.ts`.
|
|
21
|
+
- No dark-mode values for the palette yet.
|
|
22
|
+
- Storybook: a "Workspace palette" section on Foundations › Colors.
|
package/src/styles/globals.css
CHANGED
|
@@ -42,6 +42,46 @@
|
|
|
42
42
|
--color-callout-info: var(--callout-background-info);
|
|
43
43
|
--color-callout-warning: var(--callout-background-warning);
|
|
44
44
|
--color-callout-error: var(--callout-background-error);
|
|
45
|
+
--color-callout-foreground-warning: var(--callout-foreground-warning);
|
|
46
|
+
--color-callout-foreground-error: var(--callout-foreground-error);
|
|
47
|
+
--color-favorite: var(--favorite);
|
|
48
|
+
--color-workspace-gradient-base: var(--workspace-gradient-base);
|
|
49
|
+
--color-workspace-purple: var(--workspace-purple);
|
|
50
|
+
--color-workspace-purple-tint: var(--workspace-purple-tint);
|
|
51
|
+
--color-workspace-purple-deep: var(--workspace-purple-deep);
|
|
52
|
+
--color-workspace-red: var(--workspace-red);
|
|
53
|
+
--color-workspace-red-tint: var(--workspace-red-tint);
|
|
54
|
+
--color-workspace-red-deep: var(--workspace-red-deep);
|
|
55
|
+
--color-workspace-blue: var(--workspace-blue);
|
|
56
|
+
--color-workspace-blue-tint: var(--workspace-blue-tint);
|
|
57
|
+
--color-workspace-blue-deep: var(--workspace-blue-deep);
|
|
58
|
+
--color-workspace-emerald: var(--workspace-emerald);
|
|
59
|
+
--color-workspace-emerald-tint: var(--workspace-emerald-tint);
|
|
60
|
+
--color-workspace-emerald-deep: var(--workspace-emerald-deep);
|
|
61
|
+
--color-workspace-orange: var(--workspace-orange);
|
|
62
|
+
--color-workspace-orange-tint: var(--workspace-orange-tint);
|
|
63
|
+
--color-workspace-orange-deep: var(--workspace-orange-deep);
|
|
64
|
+
--color-workspace-teal: var(--workspace-teal);
|
|
65
|
+
--color-workspace-teal-tint: var(--workspace-teal-tint);
|
|
66
|
+
--color-workspace-teal-deep: var(--workspace-teal-deep);
|
|
67
|
+
--color-workspace-indigo: var(--workspace-indigo);
|
|
68
|
+
--color-workspace-indigo-tint: var(--workspace-indigo-tint);
|
|
69
|
+
--color-workspace-indigo-deep: var(--workspace-indigo-deep);
|
|
70
|
+
--color-workspace-amber: var(--workspace-amber);
|
|
71
|
+
--color-workspace-amber-tint: var(--workspace-amber-tint);
|
|
72
|
+
--color-workspace-amber-deep: var(--workspace-amber-deep);
|
|
73
|
+
--color-workspace-pink: var(--workspace-pink);
|
|
74
|
+
--color-workspace-pink-tint: var(--workspace-pink-tint);
|
|
75
|
+
--color-workspace-pink-deep: var(--workspace-pink-deep);
|
|
76
|
+
--color-workspace-sky: var(--workspace-sky);
|
|
77
|
+
--color-workspace-sky-tint: var(--workspace-sky-tint);
|
|
78
|
+
--color-workspace-sky-deep: var(--workspace-sky-deep);
|
|
79
|
+
--color-workspace-lime: var(--workspace-lime);
|
|
80
|
+
--color-workspace-lime-tint: var(--workspace-lime-tint);
|
|
81
|
+
--color-workspace-lime-deep: var(--workspace-lime-deep);
|
|
82
|
+
--color-workspace-slate: var(--workspace-slate);
|
|
83
|
+
--color-workspace-slate-tint: var(--workspace-slate-tint);
|
|
84
|
+
--color-workspace-slate-deep: var(--workspace-slate-deep);
|
|
45
85
|
--color-badge-success: var(--badge-success);
|
|
46
86
|
--color-badge-success-foreground: var(--badge-success-foreground);
|
|
47
87
|
--color-badge-warning: var(--badge-warning);
|
|
@@ -114,6 +154,51 @@
|
|
|
114
154
|
--callout-background-info: var(--primary-50);
|
|
115
155
|
--callout-background-warning: #fefce8;
|
|
116
156
|
--callout-background-error: #fef2f2;
|
|
157
|
+
/* Darker text that reads on the callout backgrounds. */
|
|
158
|
+
--callout-foreground-warning: var(--color-yellow-800);
|
|
159
|
+
--callout-foreground-error: var(--color-red-700);
|
|
160
|
+
/* Favourite / starred state. Separate from the badge and warning yellows so they can change independently. */
|
|
161
|
+
--favorite: var(--color-yellow-500);
|
|
162
|
+
/* Workspace palette: 12 decorative hues for workspace avatars and cards.
|
|
163
|
+
{hue} = solid background / icon colour, {hue}-tint = soft background, {hue}-deep = gradient end.
|
|
164
|
+
The large card gradient is `{hue}/50 -> {hue}-deep/50` over `workspace-gradient-base`. */
|
|
165
|
+
--workspace-gradient-base: var(--color-zinc-600);
|
|
166
|
+
--workspace-purple: var(--color-purple-600);
|
|
167
|
+
--workspace-purple-tint: var(--color-purple-100);
|
|
168
|
+
--workspace-purple-deep: var(--color-purple-800);
|
|
169
|
+
--workspace-red: var(--color-red-600);
|
|
170
|
+
--workspace-red-tint: var(--color-red-100);
|
|
171
|
+
--workspace-red-deep: var(--color-red-800);
|
|
172
|
+
--workspace-blue: var(--color-blue-600);
|
|
173
|
+
--workspace-blue-tint: var(--color-blue-100);
|
|
174
|
+
--workspace-blue-deep: var(--color-blue-800);
|
|
175
|
+
--workspace-emerald: var(--color-emerald-600);
|
|
176
|
+
--workspace-emerald-tint: var(--color-emerald-100);
|
|
177
|
+
--workspace-emerald-deep: var(--color-emerald-800);
|
|
178
|
+
--workspace-orange: var(--color-orange-600);
|
|
179
|
+
--workspace-orange-tint: var(--color-orange-100);
|
|
180
|
+
--workspace-orange-deep: var(--color-orange-800);
|
|
181
|
+
--workspace-teal: var(--color-teal-600);
|
|
182
|
+
--workspace-teal-tint: var(--color-teal-100);
|
|
183
|
+
--workspace-teal-deep: var(--color-teal-800);
|
|
184
|
+
--workspace-indigo: var(--color-indigo-600);
|
|
185
|
+
--workspace-indigo-tint: var(--color-indigo-100);
|
|
186
|
+
--workspace-indigo-deep: var(--color-indigo-800);
|
|
187
|
+
--workspace-amber: var(--color-amber-600);
|
|
188
|
+
--workspace-amber-tint: var(--color-amber-100);
|
|
189
|
+
--workspace-amber-deep: var(--color-amber-800);
|
|
190
|
+
--workspace-pink: var(--color-pink-600);
|
|
191
|
+
--workspace-pink-tint: var(--color-pink-100);
|
|
192
|
+
--workspace-pink-deep: var(--color-pink-800);
|
|
193
|
+
--workspace-sky: var(--color-sky-600);
|
|
194
|
+
--workspace-sky-tint: var(--color-sky-100);
|
|
195
|
+
--workspace-sky-deep: var(--color-sky-800);
|
|
196
|
+
--workspace-lime: var(--color-lime-600);
|
|
197
|
+
--workspace-lime-tint: var(--color-lime-100);
|
|
198
|
+
--workspace-lime-deep: var(--color-lime-800);
|
|
199
|
+
--workspace-slate: var(--color-slate-600);
|
|
200
|
+
--workspace-slate-tint: var(--color-slate-100);
|
|
201
|
+
--workspace-slate-deep: var(--color-slate-800);
|
|
117
202
|
--badge-success: var(--color-green-700);
|
|
118
203
|
--badge-success-foreground: var(--color-white);
|
|
119
204
|
--badge-warning: var(--color-yellow-500);
|
|
@@ -173,6 +258,8 @@
|
|
|
173
258
|
--callout-background-info: color-mix(in oklab, var(--primary-500) 15%, var(--background));
|
|
174
259
|
--callout-background-warning: color-mix(in oklab, #eab308 12%, var(--background));
|
|
175
260
|
--callout-background-error: color-mix(in oklab, var(--color-red-600) 15%, var(--background));
|
|
261
|
+
--callout-foreground-warning: var(--color-yellow-300);
|
|
262
|
+
--callout-foreground-error: var(--color-red-300);
|
|
176
263
|
--border: oklch(1 0 0 / 10%);
|
|
177
264
|
--input: oklch(1 0 0 / 15%);
|
|
178
265
|
--ring: oklch(0.556 0 0);
|