@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,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* @component WorkspaceGlyph
|
|
4
|
+
* @title Workspace Glyph
|
|
5
|
+
* @description Organism part. What a workspace avatar shows: one of its placeholder icons, or the first letter of its name when the glyph is `initial`. The letter is centred in a box the size of the icon, so it sits in the middle of the avatar at every size.
|
|
6
|
+
*
|
|
7
|
+
* @usage
|
|
8
|
+
*
|
|
9
|
+
* ```astro nocollapse
|
|
10
|
+
* ---
|
|
11
|
+
* import { WorkspaceGlyph } from '@cl-light-ds/ui/components/organisms/workspace';
|
|
12
|
+
* ---
|
|
13
|
+
*
|
|
14
|
+
* <WorkspaceGlyph icon="initial" name="Acme Logistics" class="size-5 text-base" />
|
|
15
|
+
* <WorkspaceGlyph icon="truck" class="size-5" />
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* `class` sizes the icon, or the letter's box and its font size (give both a
|
|
19
|
+
* `size-*` and a `text-*`). Colour comes from `currentColor`.
|
|
20
|
+
*
|
|
21
|
+
* Hooks for scripts (e.g. an icon picker that swaps the glyph as you pick):
|
|
22
|
+
* `data-glyph` is the current glyph name, `data-slot="workspace-glyph-initial"`
|
|
23
|
+
* holds the letter.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { HTMLAttributes } from "astro/types";
|
|
27
|
+
import { cn } from "@cl-light-ds/ui/lib/utils";
|
|
28
|
+
import { DEFAULT_WORKSPACE_ICON, workspaceInitial, type WorkspaceGlyphName } from "./icons";
|
|
29
|
+
import WorkspaceIcon from "./WorkspaceIcon.astro";
|
|
30
|
+
|
|
31
|
+
interface Props extends Omit<HTMLAttributes<"span">, "class"> {
|
|
32
|
+
/** An icon name, or `"initial"` for the first letter of `name`. */
|
|
33
|
+
icon: WorkspaceGlyphName | (string & {});
|
|
34
|
+
/** The workspace name, for the initial. With no name the initial falls back to the default icon. */
|
|
35
|
+
name?: string;
|
|
36
|
+
/** Size of the icon, or of the letter's box (add a `text-*` for the letter). */
|
|
37
|
+
class?: string;
|
|
38
|
+
/** Lucide stroke width for icons. */
|
|
39
|
+
strokeWidth?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { icon, name = "", class: className = "", strokeWidth, ...rest } = Astro.props as Props;
|
|
43
|
+
const initial = workspaceInitial(name);
|
|
44
|
+
const showInitial = icon === "initial" && initial !== "";
|
|
45
|
+
const iconName = icon === "initial" ? DEFAULT_WORKSPACE_ICON : icon;
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
<span {...rest} data-slot="workspace-glyph" data-glyph={icon} class="flex items-center justify-center">
|
|
49
|
+
{
|
|
50
|
+
showInitial ? (
|
|
51
|
+
<span
|
|
52
|
+
data-slot="workspace-glyph-initial"
|
|
53
|
+
class={cn("flex size-4 items-center justify-center text-sm leading-none font-semibold", className)}
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
>
|
|
56
|
+
{initial}
|
|
57
|
+
</span>
|
|
58
|
+
) : (
|
|
59
|
+
<WorkspaceIcon name={iconName} class={className} strokeWidth={strokeWidth} />
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
</span>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* @component WorkspaceIcon
|
|
4
|
+
* @title Workspace Icon
|
|
5
|
+
* @description Organism part. One of the 27 workspace placeholder Lucide icons, by name. Unknown names fall back to a building. Always decorative.
|
|
6
|
+
*
|
|
7
|
+
* @usage
|
|
8
|
+
*
|
|
9
|
+
* ```astro nocollapse
|
|
10
|
+
* ---
|
|
11
|
+
* import { WorkspaceIcon } from '@cl-light-ds/ui/components/organisms/workspace';
|
|
12
|
+
* ---
|
|
13
|
+
*
|
|
14
|
+
* <WorkspaceIcon name="rocket" class="size-5" />
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { cn } from "@cl-light-ds/ui/lib/utils";
|
|
19
|
+
import { resolveWorkspaceIcon, type WorkspaceIconName } from "./icons";
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
/** Icon name from `WORKSPACE_ICONS`. Unknown names show the building. */
|
|
23
|
+
name: WorkspaceIconName | (string & {});
|
|
24
|
+
/** Lucide stroke width; thinner reads better at large sizes. */
|
|
25
|
+
strokeWidth?: number;
|
|
26
|
+
class?: string;
|
|
27
|
+
/** Other SVG attributes pass through to the icon. */
|
|
28
|
+
[attribute: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { name, strokeWidth = 2, class: className = "", ...rest } = Astro.props as Props;
|
|
32
|
+
const Icon = resolveWorkspaceIcon(name);
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
<Icon
|
|
36
|
+
{...rest}
|
|
37
|
+
data-slot="workspace-icon"
|
|
38
|
+
data-icon={name}
|
|
39
|
+
class={cn("size-4 shrink-0", className)}
|
|
40
|
+
stroke-width={strokeWidth}
|
|
41
|
+
aria-hidden="true"
|
|
42
|
+
/>
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* @component WorkspaceIconPicker
|
|
4
|
+
* @title Workspace Icon Picker
|
|
5
|
+
* @description Organism. Choose a workspace's colour and icon: a 64px avatar button opens a popover with the 12 palette colours, then the name's initial and the 27 placeholder icons. The avatar follows the choice as you pick.
|
|
6
|
+
*
|
|
7
|
+
* @preview
|
|
8
|
+
*
|
|
9
|
+
* <WorkspaceIconPicker name="Acme Logistics" color="blue" icon="initial" />
|
|
10
|
+
*
|
|
11
|
+
* @usage
|
|
12
|
+
*
|
|
13
|
+
* ```astro nocollapse
|
|
14
|
+
* ---
|
|
15
|
+
* import { WorkspaceIconPicker } from '@cl-light-ds/ui/components/organisms/workspace';
|
|
16
|
+
* ---
|
|
17
|
+
*
|
|
18
|
+
* <form>
|
|
19
|
+
* <WorkspaceIconPicker name="" color="blue" icon="initial" colorName="color" iconName="icon" />
|
|
20
|
+
* <input name="name" data-workspace-name />
|
|
21
|
+
* </form>
|
|
22
|
+
*
|
|
23
|
+
* <script>
|
|
24
|
+
* const picker = document.querySelector('[data-slot="workspace-icon-picker"]');
|
|
25
|
+
* picker.addEventListener("workspace-icon-picker:change", (e) => console.log(e.detail)); // { color, icon }
|
|
26
|
+
* // Keep the initial in step with the name as it is typed:
|
|
27
|
+
* document.querySelector("[data-workspace-name]").addEventListener("input", (e) =>
|
|
28
|
+
* picker.dispatchEvent(new CustomEvent("workspace-icon-picker:set", { detail: { name: e.target.value } })),
|
|
29
|
+
* );
|
|
30
|
+
* </script>
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* Colours are round swatches (the tint with a dot of the colour), ringed when
|
|
34
|
+
* chosen; icons are tiles, on the chosen colour's tint with a ring when
|
|
35
|
+
* chosen. Every swatch and tile is a button with `aria-pressed`. The popover
|
|
36
|
+
* stays open while you pick; Escape or a click outside closes it and returns
|
|
37
|
+
* focus to the avatar (popover runtime).
|
|
38
|
+
*
|
|
39
|
+
* With `colorName` / `iconName`, hidden inputs carry the choice in a form.
|
|
40
|
+
*
|
|
41
|
+
* @api
|
|
42
|
+
*
|
|
43
|
+
* ### Events (on the `[data-slot="workspace-icon-picker"]` root)
|
|
44
|
+
*
|
|
45
|
+
* | Event | Detail | Description |
|
|
46
|
+
* |-------|--------|-------------|
|
|
47
|
+
* | `workspace-icon-picker:change` | `{ color, icon }` | Fired (bubbling) when a colour or icon is picked |
|
|
48
|
+
* | `workspace-icon-picker:set` | `{ color?, icon?, name? }` | Dispatch to set the colour, icon, or the name the initial is taken from. Doesn't fire `change` |
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import type { HTMLAttributes } from "astro/types";
|
|
52
|
+
import { cn } from "@cl-light-ds/ui/lib/utils";
|
|
53
|
+
import {
|
|
54
|
+
DEFAULT_WORKSPACE_COLOR,
|
|
55
|
+
WORKSPACE_COLORS,
|
|
56
|
+
workspacePalette,
|
|
57
|
+
type WorkspaceColor,
|
|
58
|
+
} from "@cl-light-ds/ui/lib/workspace-palette";
|
|
59
|
+
import { Popover, PopoverContent, PopoverTrigger } from "../../popover";
|
|
60
|
+
import { Separator } from "../../separator";
|
|
61
|
+
import { WORKSPACE_GLYPHS, workspaceGlyphLabel, type WorkspaceGlyphName } from "./icons";
|
|
62
|
+
import WorkspaceAvatar from "./WorkspaceAvatar.astro";
|
|
63
|
+
import WorkspaceIconPickerGlyph from "./WorkspaceIconPickerGlyph.astro";
|
|
64
|
+
|
|
65
|
+
interface Props extends Omit<HTMLAttributes<"div">, "class"> {
|
|
66
|
+
/** The workspace name, for the initial. Can change later with `workspace-icon-picker:set`. */
|
|
67
|
+
name?: string;
|
|
68
|
+
/** The chosen colour. Defaults to blue. */
|
|
69
|
+
color?: WorkspaceColor;
|
|
70
|
+
/** The chosen glyph: an icon name, or `"initial"` (default). */
|
|
71
|
+
icon?: WorkspaceGlyphName;
|
|
72
|
+
/** Name of a hidden input carrying the colour, for forms. */
|
|
73
|
+
colorName?: string;
|
|
74
|
+
/** Name of a hidden input carrying the icon, for forms. */
|
|
75
|
+
iconName?: string;
|
|
76
|
+
/** Accessible name of the avatar button. */
|
|
77
|
+
label?: string;
|
|
78
|
+
/** Caps heading and group name over the colours. */
|
|
79
|
+
colorLabel?: string;
|
|
80
|
+
/** Caps heading and group name over the icons. */
|
|
81
|
+
iconLabel?: string;
|
|
82
|
+
/** Popover alignment against the avatar. */
|
|
83
|
+
align?: "start" | "center" | "end";
|
|
84
|
+
class?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const {
|
|
88
|
+
name = "",
|
|
89
|
+
color = DEFAULT_WORKSPACE_COLOR,
|
|
90
|
+
icon = "initial",
|
|
91
|
+
colorName,
|
|
92
|
+
iconName,
|
|
93
|
+
label = "Choose an icon and colour",
|
|
94
|
+
colorLabel = "Colour",
|
|
95
|
+
iconLabel = "Icon",
|
|
96
|
+
align = "start",
|
|
97
|
+
class: className = "",
|
|
98
|
+
...rest
|
|
99
|
+
} = Astro.props as Props;
|
|
100
|
+
|
|
101
|
+
const swatch = workspacePalette[color] ?? workspacePalette[DEFAULT_WORKSPACE_COLOR];
|
|
102
|
+
const colorTitle = (c: string) => c.charAt(0).toUpperCase() + c.slice(1);
|
|
103
|
+
const tile =
|
|
104
|
+
"flex size-9 cursor-pointer items-center justify-center outline-none transition-[color,background-color,box-shadow] focus-visible:ring-3 focus-visible:ring-chart-1";
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
<div
|
|
108
|
+
{...rest}
|
|
109
|
+
data-slot="workspace-icon-picker"
|
|
110
|
+
data-color={color}
|
|
111
|
+
data-icon={icon}
|
|
112
|
+
data-name={name}
|
|
113
|
+
class={cn("inline-flex", className)}
|
|
114
|
+
>
|
|
115
|
+
<Popover class="flex">
|
|
116
|
+
<PopoverTrigger
|
|
117
|
+
variant="ghost"
|
|
118
|
+
size="icon"
|
|
119
|
+
aria-label={label}
|
|
120
|
+
class="size-16 rounded-xl p-0 hover:bg-transparent hover:opacity-90 [&_svg]:size-full"
|
|
121
|
+
data-workspace-icon-picker-trigger
|
|
122
|
+
>
|
|
123
|
+
<WorkspaceAvatar color={color} size="xl" shape="square">
|
|
124
|
+
<WorkspaceIconPickerGlyph icon={icon} name={name} class="size-7 text-2xl" />
|
|
125
|
+
</WorkspaceAvatar>
|
|
126
|
+
</PopoverTrigger>
|
|
127
|
+
|
|
128
|
+
<PopoverContent align={align} class="w-80 max-w-[calc(100vw-1rem)] gap-3" data-workspace-icon-picker-content>
|
|
129
|
+
<p class="text-xs font-semibold tracking-wide text-muted-foreground uppercase" aria-hidden="true">
|
|
130
|
+
{colorLabel}
|
|
131
|
+
</p>
|
|
132
|
+
<div role="group" aria-label={colorLabel} class="grid grid-cols-6 gap-1">
|
|
133
|
+
{
|
|
134
|
+
WORKSPACE_COLORS.map((c) => (
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
class={cn(tile, "rounded-full hover:bg-background-hover aria-pressed:ring-2 aria-pressed:ring-ring")}
|
|
138
|
+
aria-label={colorTitle(c)}
|
|
139
|
+
aria-pressed={c === color ? "true" : "false"}
|
|
140
|
+
data-color-option={c}
|
|
141
|
+
>
|
|
142
|
+
<span class={cn("flex size-7 items-center justify-center rounded-full", workspacePalette[c].tint)}>
|
|
143
|
+
<span class={cn("size-3 rounded-full", workspacePalette[c].solid)} />
|
|
144
|
+
</span>
|
|
145
|
+
</button>
|
|
146
|
+
))
|
|
147
|
+
}
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<Separator />
|
|
151
|
+
|
|
152
|
+
<p class="text-xs font-semibold tracking-wide text-muted-foreground uppercase" aria-hidden="true">
|
|
153
|
+
{iconLabel}
|
|
154
|
+
</p>
|
|
155
|
+
<div role="group" aria-label={iconLabel} class="grid grid-cols-7 gap-1">
|
|
156
|
+
{
|
|
157
|
+
WORKSPACE_GLYPHS.map((g) => (
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
class={cn(
|
|
161
|
+
tile,
|
|
162
|
+
"rounded-md not-aria-pressed:text-foreground not-aria-pressed:hover:bg-background-hover aria-pressed:ring-1 aria-pressed:ring-ring",
|
|
163
|
+
g === icon && [swatch.tint, swatch.text],
|
|
164
|
+
)}
|
|
165
|
+
aria-label={workspaceGlyphLabel(g)}
|
|
166
|
+
aria-pressed={g === icon ? "true" : "false"}
|
|
167
|
+
data-icon-option={g}
|
|
168
|
+
>
|
|
169
|
+
<WorkspaceIconPickerGlyph icon={g} name={name} class="size-5 text-base" />
|
|
170
|
+
</button>
|
|
171
|
+
))
|
|
172
|
+
}
|
|
173
|
+
</div>
|
|
174
|
+
</PopoverContent>
|
|
175
|
+
</Popover>
|
|
176
|
+
|
|
177
|
+
{colorName && <input type="hidden" name={colorName} value={color} data-workspace-icon-picker-color />}
|
|
178
|
+
{iconName && <input type="hidden" name={iconName} value={icon} data-workspace-icon-picker-icon />}
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<script>
|
|
182
|
+
import { workspacePalette, type WorkspaceColor } from "@cl-light-ds/ui/lib/workspace-palette";
|
|
183
|
+
|
|
184
|
+
const wired = new WeakSet<Element>();
|
|
185
|
+
const initialOf = (name: string) => ([...name.trim()][0] ?? "").toLocaleUpperCase();
|
|
186
|
+
const classesOf = (color: string) => {
|
|
187
|
+
const s = workspacePalette[color as WorkspaceColor];
|
|
188
|
+
return s ? [s.tint, s.text] : [];
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
function wire(root: HTMLElement) {
|
|
192
|
+
if (wired.has(root)) return;
|
|
193
|
+
wired.add(root);
|
|
194
|
+
|
|
195
|
+
// Hold references now: the popover portals its content to <body> while open.
|
|
196
|
+
const content = root.querySelector<HTMLElement>("[data-workspace-icon-picker-content]");
|
|
197
|
+
const avatar = root.querySelector<HTMLElement>('[data-slot="workspace-avatar"]');
|
|
198
|
+
if (!content || !avatar) return;
|
|
199
|
+
const colorButtons = Array.from(content.querySelectorAll<HTMLElement>("[data-color-option]"));
|
|
200
|
+
const iconButtons = Array.from(content.querySelectorAll<HTMLElement>("[data-icon-option]"));
|
|
201
|
+
const colorInput = root.querySelector<HTMLInputElement>("[data-workspace-icon-picker-color]");
|
|
202
|
+
const iconInput = root.querySelector<HTMLInputElement>("[data-workspace-icon-picker-icon]");
|
|
203
|
+
|
|
204
|
+
let color = root.dataset.color ?? "blue";
|
|
205
|
+
let icon = root.dataset.icon ?? "initial";
|
|
206
|
+
|
|
207
|
+
const pressedTile = () => iconButtons.find((b) => b.dataset.iconOption === icon);
|
|
208
|
+
|
|
209
|
+
const setColor = (next: string) => {
|
|
210
|
+
if (!workspacePalette[next as WorkspaceColor]) return;
|
|
211
|
+
const from = classesOf(color);
|
|
212
|
+
const to = classesOf(next);
|
|
213
|
+
for (const el of [avatar, pressedTile()]) {
|
|
214
|
+
if (!el) continue;
|
|
215
|
+
el.classList.remove(...from);
|
|
216
|
+
el.classList.add(...to);
|
|
217
|
+
}
|
|
218
|
+
avatar.dataset.color = next;
|
|
219
|
+
color = next;
|
|
220
|
+
root.dataset.color = next;
|
|
221
|
+
for (const b of colorButtons) b.setAttribute("aria-pressed", String(b.dataset.colorOption === next));
|
|
222
|
+
if (colorInput) colorInput.value = next;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const setIcon = (next: string) => {
|
|
226
|
+
const button = iconButtons.find((b) => b.dataset.iconOption === next);
|
|
227
|
+
if (!button) return;
|
|
228
|
+
pressedTile()?.classList.remove(...classesOf(color));
|
|
229
|
+
icon = next;
|
|
230
|
+
root.dataset.icon = next;
|
|
231
|
+
button.classList.add(...classesOf(color));
|
|
232
|
+
for (const b of iconButtons) b.setAttribute("aria-pressed", String(b === button));
|
|
233
|
+
// Copy the tile's glyph into the avatar, keeping the avatar's glyph size.
|
|
234
|
+
const source = button.querySelector<HTMLElement>('[data-slot="workspace-icon-picker-glyph"]');
|
|
235
|
+
const target = avatar.querySelector<HTMLElement>('[data-slot="workspace-icon-picker-glyph"]');
|
|
236
|
+
if (source && target) {
|
|
237
|
+
const copy = source.cloneNode(true) as HTMLElement;
|
|
238
|
+
copy.className = target.className;
|
|
239
|
+
target.replaceWith(copy);
|
|
240
|
+
}
|
|
241
|
+
if (iconInput) iconInput.value = next;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
/** The initial follows the name; with no name, the fallback icon shows. */
|
|
245
|
+
const setName = (name: string) => {
|
|
246
|
+
const letter = initialOf(name);
|
|
247
|
+
root.dataset.name = name;
|
|
248
|
+
for (const scope of [avatar, content]) {
|
|
249
|
+
for (const el of scope.querySelectorAll<HTMLElement>('[data-slot="workspace-icon-picker-initial"]')) {
|
|
250
|
+
el.textContent = letter;
|
|
251
|
+
el.hidden = !letter;
|
|
252
|
+
}
|
|
253
|
+
for (const el of scope.querySelectorAll<HTMLElement>('[data-slot="workspace-icon-picker-fallback"]')) {
|
|
254
|
+
el.hidden = Boolean(letter);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const emitChange = () =>
|
|
260
|
+
root.dispatchEvent(
|
|
261
|
+
new CustomEvent("workspace-icon-picker:change", { detail: { color, icon }, bubbles: true }),
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
for (const b of colorButtons) {
|
|
265
|
+
b.addEventListener("click", () => {
|
|
266
|
+
if (b.dataset.colorOption === color) return;
|
|
267
|
+
setColor(b.dataset.colorOption!);
|
|
268
|
+
emitChange();
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
for (const b of iconButtons) {
|
|
272
|
+
b.addEventListener("click", () => {
|
|
273
|
+
if (b.dataset.iconOption === icon) return;
|
|
274
|
+
setIcon(b.dataset.iconOption!);
|
|
275
|
+
emitChange();
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
root.addEventListener("workspace-icon-picker:set", (event) => {
|
|
280
|
+
const detail = (event as CustomEvent<{ color?: string; icon?: string; name?: string }>).detail ?? {};
|
|
281
|
+
if (detail.name !== undefined) setName(detail.name);
|
|
282
|
+
if (detail.color && detail.color !== color) setColor(detail.color);
|
|
283
|
+
if (detail.icon && detail.icon !== icon) setIcon(detail.icon);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
document.querySelectorAll<HTMLElement>('[data-slot="workspace-icon-picker"]').forEach(wire);
|
|
288
|
+
</script>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Internal to `WorkspaceIconPicker`: the glyph on a tile and on the avatar.
|
|
4
|
+
* Unlike `WorkspaceGlyph`, the initial always renders both the letter and the
|
|
5
|
+
* fallback icon (one of them `hidden`), so the picker can follow a name that
|
|
6
|
+
* is typed in after the page loads. `class` sizes the box (`size-*`) and the
|
|
7
|
+
* letter (`text-*`); the icon fills the box.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { cn } from "@cl-light-ds/ui/lib/utils";
|
|
11
|
+
import { DEFAULT_WORKSPACE_ICON, workspaceInitial } from "./icons";
|
|
12
|
+
import WorkspaceIcon from "./WorkspaceIcon.astro";
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
icon: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { icon, name = "", class: className = "" } = Astro.props as Props;
|
|
21
|
+
const letter = workspaceInitial(name);
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<span
|
|
25
|
+
data-slot="workspace-icon-picker-glyph"
|
|
26
|
+
data-glyph={icon}
|
|
27
|
+
class={cn("flex shrink-0 items-center justify-center", className)}
|
|
28
|
+
aria-hidden="true"
|
|
29
|
+
>
|
|
30
|
+
{
|
|
31
|
+
icon === "initial" ? (
|
|
32
|
+
<>
|
|
33
|
+
<span data-slot="workspace-icon-picker-initial" class="leading-none font-semibold" hidden={!letter}>
|
|
34
|
+
{letter}
|
|
35
|
+
</span>
|
|
36
|
+
<span data-slot="workspace-icon-picker-fallback" class="flex size-full" hidden={Boolean(letter)}>
|
|
37
|
+
<WorkspaceIcon name={DEFAULT_WORKSPACE_ICON} class="size-full" />
|
|
38
|
+
</span>
|
|
39
|
+
</>
|
|
40
|
+
) : (
|
|
41
|
+
<WorkspaceIcon name={icon} class="size-full" />
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
</span>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BookOpen,
|
|
3
|
+
Briefcase,
|
|
4
|
+
Building2,
|
|
5
|
+
Factory,
|
|
6
|
+
FlaskConical,
|
|
7
|
+
Globe,
|
|
8
|
+
GraduationCap,
|
|
9
|
+
HardHat,
|
|
10
|
+
Headset,
|
|
11
|
+
HeartPulse,
|
|
12
|
+
Landmark,
|
|
13
|
+
Leaf,
|
|
14
|
+
Lightbulb,
|
|
15
|
+
Mountain,
|
|
16
|
+
Package,
|
|
17
|
+
Plane,
|
|
18
|
+
Rocket,
|
|
19
|
+
Scale,
|
|
20
|
+
Server,
|
|
21
|
+
ShieldCheck,
|
|
22
|
+
ShoppingCart,
|
|
23
|
+
Store,
|
|
24
|
+
TrendingUp,
|
|
25
|
+
Truck,
|
|
26
|
+
Users,
|
|
27
|
+
Wallet,
|
|
28
|
+
Wrench,
|
|
29
|
+
type AstroComponent,
|
|
30
|
+
} from "@lucide/astro";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The workspace placeholder icon set: 27 Lucide icons a workspace can be given
|
|
34
|
+
* until it has a logo. String names, so backend data can store them. The order
|
|
35
|
+
* is the icon picker's order.
|
|
36
|
+
*/
|
|
37
|
+
export const workspaceIcons = {
|
|
38
|
+
"building-2": Building2,
|
|
39
|
+
briefcase: Briefcase,
|
|
40
|
+
users: Users,
|
|
41
|
+
"graduation-cap": GraduationCap,
|
|
42
|
+
"book-open": BookOpen,
|
|
43
|
+
landmark: Landmark,
|
|
44
|
+
wallet: Wallet,
|
|
45
|
+
"trending-up": TrendingUp,
|
|
46
|
+
"shopping-cart": ShoppingCart,
|
|
47
|
+
store: Store,
|
|
48
|
+
truck: Truck,
|
|
49
|
+
package: Package,
|
|
50
|
+
factory: Factory,
|
|
51
|
+
"hard-hat": HardHat,
|
|
52
|
+
wrench: Wrench,
|
|
53
|
+
server: Server,
|
|
54
|
+
headset: Headset,
|
|
55
|
+
"shield-check": ShieldCheck,
|
|
56
|
+
scale: Scale,
|
|
57
|
+
"heart-pulse": HeartPulse,
|
|
58
|
+
"flask-conical": FlaskConical,
|
|
59
|
+
globe: Globe,
|
|
60
|
+
plane: Plane,
|
|
61
|
+
leaf: Leaf,
|
|
62
|
+
rocket: Rocket,
|
|
63
|
+
lightbulb: Lightbulb,
|
|
64
|
+
mountain: Mountain,
|
|
65
|
+
} satisfies Record<string, AstroComponent>;
|
|
66
|
+
|
|
67
|
+
export type WorkspaceIconName = keyof typeof workspaceIcons;
|
|
68
|
+
|
|
69
|
+
/** The 27 icon names, in picker order. */
|
|
70
|
+
export const WORKSPACE_ICONS = Object.keys(workspaceIcons) as WorkspaceIconName[];
|
|
71
|
+
|
|
72
|
+
/** What a workspace avatar can show: one of the icons, or the first letter of the name. */
|
|
73
|
+
export type WorkspaceGlyphName = WorkspaceIconName | "initial";
|
|
74
|
+
|
|
75
|
+
/** Every glyph an icon picker offers: the initial first, then the 27 icons. */
|
|
76
|
+
export const WORKSPACE_GLYPHS: WorkspaceGlyphName[] = ["initial", ...WORKSPACE_ICONS];
|
|
77
|
+
|
|
78
|
+
/** The icon for unknown names, and for an initial with no name to take it from. */
|
|
79
|
+
export const DEFAULT_WORKSPACE_ICON: WorkspaceIconName = "building-2";
|
|
80
|
+
|
|
81
|
+
/** The Lucide component for an icon name; unknown names fall back to the building. */
|
|
82
|
+
export const resolveWorkspaceIcon = (name: string | undefined): AstroComponent =>
|
|
83
|
+
workspaceIcons[name as WorkspaceIconName] ?? workspaceIcons[DEFAULT_WORKSPACE_ICON];
|
|
84
|
+
|
|
85
|
+
/** The first letter of a workspace name, upper-cased ("" when the name is blank). */
|
|
86
|
+
export const workspaceInitial = (name: string | undefined): string =>
|
|
87
|
+
([...(name ?? "").trim()][0] ?? "").toLocaleUpperCase();
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A readable label for a glyph, e.g. an icon picker's `aria-label`:
|
|
91
|
+
* "shield-check" → "Shield check", "building-2" → "Building",
|
|
92
|
+
* "initial" → "First letter of the name".
|
|
93
|
+
*/
|
|
94
|
+
export const workspaceGlyphLabel = (name: WorkspaceGlyphName): string => {
|
|
95
|
+
if (name === "initial") return "First letter of the name";
|
|
96
|
+
const words = name.replace(/-\d+$/, "").replace(/-/g, " ");
|
|
97
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
98
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export { default as OrganisationOverview } from "./OrganisationOverview.astro";
|
|
2
|
+
export { default as WorkspaceCard } from "./WorkspaceCard.astro";
|
|
3
|
+
export { default as RecentWorkspaceCard } from "./RecentWorkspaceCard.astro";
|
|
4
|
+
export { default as RecentWorkspaces } from "./RecentWorkspaces.astro";
|
|
5
|
+
export { default as WorkspaceIconPicker } from "./WorkspaceIconPicker.astro";
|
|
6
|
+
export { default as WorkspaceAvatar } from "./WorkspaceAvatar.astro";
|
|
7
|
+
export { workspaceAvatarVariants } from "./WorkspaceAvatar.astro";
|
|
8
|
+
export { default as WorkspaceGlyph } from "./WorkspaceGlyph.astro";
|
|
9
|
+
export { default as WorkspaceIcon } from "./WorkspaceIcon.astro";
|
|
10
|
+
export {
|
|
11
|
+
DEFAULT_WORKSPACE_ICON,
|
|
12
|
+
WORKSPACE_GLYPHS,
|
|
13
|
+
WORKSPACE_ICONS,
|
|
14
|
+
resolveWorkspaceIcon,
|
|
15
|
+
workspaceGlyphLabel,
|
|
16
|
+
workspaceIcons,
|
|
17
|
+
workspaceInitial,
|
|
18
|
+
type WorkspaceGlyphName,
|
|
19
|
+
type WorkspaceIconName,
|
|
20
|
+
} from "./icons";
|
|
21
|
+
export {
|
|
22
|
+
DEFAULT_WORKSPACE_COLOR,
|
|
23
|
+
WORKSPACE_COLORS,
|
|
24
|
+
brandGradient,
|
|
25
|
+
workspacePalette,
|
|
26
|
+
type WorkspaceColor,
|
|
27
|
+
type WorkspaceSwatch,
|
|
28
|
+
} from "@cl-light-ds/ui/lib/workspace-palette";
|
|
29
|
+
export * from "./types";
|
|
30
|
+
export {
|
|
31
|
+
defaultWorkspaceCardLabels,
|
|
32
|
+
fillLabel,
|
|
33
|
+
workspaceCardTexts,
|
|
34
|
+
type WorkspaceCardLabels,
|
|
35
|
+
} from "./labels";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The copy on workspace cards (`WorkspaceCard`, `RecentWorkspaceCard`,
|
|
3
|
+
* `RecentWorkspaces`), as templates so it can be translated. `{time}`,
|
|
4
|
+
* `{name}` and `{count}` are filled in.
|
|
5
|
+
*/
|
|
6
|
+
export interface WorkspaceCardLabels {
|
|
7
|
+
/** "Opened {time}", e.g. "Opened 2 hours ago". */
|
|
8
|
+
opened: string;
|
|
9
|
+
/** "Last edited by {name}". */
|
|
10
|
+
lastEditedBy: string;
|
|
11
|
+
/** "{count} recordings". */
|
|
12
|
+
recordings: string;
|
|
13
|
+
/** "{count} recording", for exactly one. */
|
|
14
|
+
recording: string;
|
|
15
|
+
/** The star's accessible name: "Favourite {name}". */
|
|
16
|
+
favorite: string;
|
|
17
|
+
/** The large recent card's caps line: "Jump back in". */
|
|
18
|
+
jumpBackIn: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const defaultWorkspaceCardLabels: WorkspaceCardLabels = {
|
|
22
|
+
opened: "Opened {time}",
|
|
23
|
+
lastEditedBy: "Last edited by {name}",
|
|
24
|
+
recordings: "{count} recordings",
|
|
25
|
+
recording: "{count} recording",
|
|
26
|
+
favorite: "Favourite {name}",
|
|
27
|
+
jumpBackIn: "Jump back in",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Fills `{key}` placeholders in a template. */
|
|
31
|
+
export const fillLabel = (template: string, values: Record<string, string | number>): string =>
|
|
32
|
+
template.replace(/\{(\w+)\}/g, (match, key: string) => (key in values ? String(values[key]) : match));
|
|
33
|
+
|
|
34
|
+
/** The card texts for one workspace; parts whose data is missing are `""`. */
|
|
35
|
+
export function workspaceCardTexts(
|
|
36
|
+
data: { name: string; opened?: string; lastEditedBy?: string; recordings?: number | string },
|
|
37
|
+
labels: Partial<WorkspaceCardLabels> = {},
|
|
38
|
+
) {
|
|
39
|
+
const l = { ...defaultWorkspaceCardLabels, ...labels };
|
|
40
|
+
const { name, opened, lastEditedBy, recordings } = data;
|
|
41
|
+
const recordingsText =
|
|
42
|
+
recordings === undefined || recordings === ""
|
|
43
|
+
? ""
|
|
44
|
+
: typeof recordings === "number"
|
|
45
|
+
? fillLabel(recordings === 1 ? l.recording : l.recordings, { count: recordings })
|
|
46
|
+
: recordings;
|
|
47
|
+
return {
|
|
48
|
+
opened: opened ? fillLabel(l.opened, { time: opened }) : "",
|
|
49
|
+
lastEditedBy: lastEditedBy ? fillLabel(l.lastEditedBy, { name: lastEditedBy }) : "",
|
|
50
|
+
recordings: recordingsText,
|
|
51
|
+
favorite: fillLabel(l.favorite, { name }),
|
|
52
|
+
jumpBackIn: l.jumpBackIn,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { WorkspaceColor } from "@cl-light-ds/ui/lib/workspace-palette";
|
|
2
|
+
|
|
3
|
+
/** `WorkspaceAvatar` colour treatments. */
|
|
4
|
+
export type WorkspaceAvatarTone = "soft" | "solid" | "inverse" | "brand";
|
|
5
|
+
|
|
6
|
+
/** `WorkspaceAvatar` sizes: 32 / 40 / 56 / 64px. */
|
|
7
|
+
export type WorkspaceAvatarSize = "sm" | "default" | "lg" | "xl";
|
|
8
|
+
|
|
9
|
+
/** `WorkspaceAvatar` shapes: round, or a rounded square. */
|
|
10
|
+
export type WorkspaceAvatarShape = "circle" | "square";
|
|
11
|
+
|
|
12
|
+
/** `RecentWorkspaceCard` sizes: large (gradient), medium (tall), small (two stacked), extra small (one row). */
|
|
13
|
+
export type RecentWorkspaceCardSize = "lg" | "md" | "sm" | "xs";
|
|
14
|
+
|
|
15
|
+
/** The large recent card's background: the workspace's own gradient, or the brand gradient. */
|
|
16
|
+
export type RecentWorkspaceTone = "workspace" | "brand";
|
|
17
|
+
|
|
18
|
+
/** One workspace for `RecentWorkspaces`. Other keys (e.g. `data-*`) go on its card's link. */
|
|
19
|
+
export interface RecentWorkspace {
|
|
20
|
+
name: string;
|
|
21
|
+
href: string;
|
|
22
|
+
color?: WorkspaceColor;
|
|
23
|
+
/** A placeholder icon name, or `"initial"`. */
|
|
24
|
+
icon?: string;
|
|
25
|
+
/** When it was last opened, e.g. "2 hours ago" ("Opened 2 hours ago"). */
|
|
26
|
+
opened?: string;
|
|
27
|
+
lastEditedBy?: string;
|
|
28
|
+
recordings?: number | string;
|
|
29
|
+
[attribute: `data-${string}`]: string | undefined;
|
|
30
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `ProfileMenu` component are documented in this file.
|
|
4
|
+
|
|
5
|
+
## Date 15th Oct 2026
|
|
6
|
+
## Workspace selection handover (N13)
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- `ProfileMenu`, built on `DropdownMenu`:
|
|
11
|
+
- **Trigger (C15, D103):** the user's `Avatar` (32px) is the trigger: a chrome-less round `<button data-slot="dropdown-menu-trigger">` around it, with a `ring-2 ring-ring` focus ring and no hover ring. Because the button *is* the runtime's trigger (not an `asChild` wrapper), `aria-haspopup`, `aria-expanded` and Esc focus return land on it, as D103 asked frontend to do.
|
|
12
|
+
- **Panel:** `w-64`, `align="end"`, opens **6px** below the avatar (`sideOffset` 6, C28, D206) instead of the menus' default 4px. Scroll lock is off by default (`lockScroll`), as in the reference.
|
|
13
|
+
- **Header (D180):** avatar, name (semibold, truncates) and email (muted). No role line. `header` slot to replace it.
|
|
14
|
+
- **Upgrade:** a full-width `sm` primary button whose label comes from `plan` looked up in `upgradeLabels` — defaults `trial` → "Upgrade to Pro", `pro` → "Upgrade to Enterprise", `enterprise` → none. `upgradeLabel` overrides (`false` hides), `upgradeHref` navigates, `upgrade` slot replaces it. It is a menu item (value `upgrade`), so arrow keys reach it.
|
|
15
|
+
- **Items:** `items` prop (`id`, `label`, `icon`, `href`, `variant`, `separatorBefore`), defaulting to Manage profile, Get desktop app, separator, Log out. The default slot replaces them with your own `DropdownMenuItem`s.
|
|
16
|
+
- `profile-menu:select` event (`{ id }`, bubbles, cancelable), fired for items and the upgrade button. Items with an `href` navigate unless it's cancelled (same contract as `sidebar-nav:workspace-change`).
|
|
17
|
+
- `defaultProfileMenuUpgradeLabels` and the `ProfileMenuItemData` / `ProfileMenuUpgradeLabels` types are exported.
|
|
18
|
+
|
|
19
|
+
### Generalised from the reference (cl-design `ProfileMenu.astro`)
|
|
20
|
+
|
|
21
|
+
- User, plan and the plan → label mapping come from props instead of the screen's mock data and `upgradeLabel()` helper.
|
|
22
|
+
- Items are data or slots instead of fixed markup; copy is props with the reference's labels as defaults.
|