@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.
Files changed (91) hide show
  1. package/package.json +19 -17
  2. package/src/components/alert/Alert.astro +8 -2
  3. package/src/components/alert/CHANGELOG.md +9 -0
  4. package/src/components/button/Button.astro +78 -19
  5. package/src/components/button/CHANGELOG.md +22 -0
  6. package/src/components/button-group/ButtonGroup.astro +14 -3
  7. package/src/components/button-group/CHANGELOG.md +17 -0
  8. package/src/components/card/CHANGELOG.md +9 -0
  9. package/src/components/card/Card.astro +22 -0
  10. package/src/components/combobox/CHANGELOG.md +14 -0
  11. package/src/components/combobox/ComboboxTrigger.astro +31 -1
  12. package/src/components/command-search/CHANGELOG.md +27 -0
  13. package/src/components/command-search/CommandSearch.astro +515 -0
  14. package/src/components/command-search/CommandSearchGroup.astro +32 -0
  15. package/src/components/command-search/CommandSearchItem.astro +64 -0
  16. package/src/components/command-search/classes.ts +18 -0
  17. package/src/components/command-search/index.ts +12 -0
  18. package/src/components/command-search/nothing-matches.svg +42 -0
  19. package/src/components/dialog/CHANGELOG.md +13 -0
  20. package/src/components/dialog/DialogClose.astro +26 -12
  21. package/src/components/dropdown-menu/CHANGELOG.md +7 -0
  22. package/src/components/dropdown-menu/dropdown-menu-item-variants.ts +1 -1
  23. package/src/components/empty-state/CHANGELOG.md +18 -0
  24. package/src/components/empty-state/EmptyState.astro +157 -0
  25. package/src/components/empty-state/illustrations/AllCaughtUp.astro +52 -0
  26. package/src/components/empty-state/illustrations/EmptyWorkspaces.astro +56 -0
  27. package/src/components/empty-state/illustrations/LoadError.astro +61 -0
  28. package/src/components/empty-state/illustrations/NoResults.astro +60 -0
  29. package/src/components/empty-state/illustrations/NothingMatches.astro +66 -0
  30. package/src/components/empty-state/illustrations/index.ts +27 -0
  31. package/src/components/empty-state/index.ts +7 -0
  32. package/src/components/favorite-toggle/CHANGELOG.md +13 -0
  33. package/src/components/favorite-toggle/FavoriteToggle.astro +94 -0
  34. package/src/components/favorite-toggle/index.ts +1 -0
  35. package/src/components/input-group/CHANGELOG.md +7 -0
  36. package/src/components/input-group/InputGroup.astro +2 -0
  37. package/src/components/loading-overlay/CHANGELOG.md +25 -0
  38. package/src/components/loading-overlay/LoadingOverlay.astro +179 -0
  39. package/src/components/loading-overlay/index.ts +8 -0
  40. package/src/components/notifications-menu/CHANGELOG.md +22 -0
  41. package/src/components/notifications-menu/NotificationsMenu.astro +374 -0
  42. package/src/components/notifications-menu/NotificationsMenuItem.astro +129 -0
  43. package/src/components/notifications-menu/all-caught-up.svg +28 -0
  44. package/src/components/notifications-menu/index.ts +3 -0
  45. package/src/components/notifications-menu/types.ts +32 -0
  46. package/src/components/option-card/CHANGELOG.md +16 -0
  47. package/src/components/option-card/OptionCard.astro +135 -0
  48. package/src/components/option-card/index.ts +1 -0
  49. package/src/components/organisms/top-bar/CHANGELOG.md +21 -0
  50. package/src/components/organisms/top-bar/TopBar.astro +264 -0
  51. package/src/components/organisms/top-bar/index.ts +2 -0
  52. package/src/components/organisms/top-bar/types.ts +30 -0
  53. package/src/components/organisms/workspace/CHANGELOG.md +73 -0
  54. package/src/components/organisms/workspace/OrganisationOverview.astro +62 -0
  55. package/src/components/organisms/workspace/RecentWorkspaceCard.astro +235 -0
  56. package/src/components/organisms/workspace/RecentWorkspaces.astro +93 -0
  57. package/src/components/organisms/workspace/WorkspaceAvatar.astro +144 -0
  58. package/src/components/organisms/workspace/WorkspaceCard.astro +172 -0
  59. package/src/components/organisms/workspace/WorkspaceGlyph.astro +62 -0
  60. package/src/components/organisms/workspace/WorkspaceIcon.astro +42 -0
  61. package/src/components/organisms/workspace/WorkspaceIconPicker.astro +288 -0
  62. package/src/components/organisms/workspace/WorkspaceIconPickerGlyph.astro +44 -0
  63. package/src/components/organisms/workspace/icons.ts +98 -0
  64. package/src/components/organisms/workspace/index.ts +35 -0
  65. package/src/components/organisms/workspace/labels.ts +54 -0
  66. package/src/components/organisms/workspace/recent-card-rings-strong.png +0 -0
  67. package/src/components/organisms/workspace/types.ts +30 -0
  68. package/src/components/profile-menu/CHANGELOG.md +22 -0
  69. package/src/components/profile-menu/ProfileMenu.astro +236 -0
  70. package/src/components/profile-menu/index.ts +2 -0
  71. package/src/components/profile-menu/types.ts +27 -0
  72. package/src/components/search-field/CHANGELOG.md +14 -0
  73. package/src/components/search-field/SearchField.astro +163 -0
  74. package/src/components/search-field/index.ts +1 -0
  75. package/src/components/table/CHANGELOG.md +9 -0
  76. package/src/components/table/TableCell.astro +6 -0
  77. package/src/components/table/TableHead.astro +5 -1
  78. package/src/components/table/TableRow.astro +3 -1
  79. package/src/components/table/table-cell-variants.ts +2 -0
  80. package/src/components/toast/CHANGELOG.md +32 -0
  81. package/src/components/toast/Toast.astro +145 -0
  82. package/src/components/toast/Toaster.astro +322 -0
  83. package/src/components/toast/index.ts +9 -0
  84. package/src/components/toggle/CHANGELOG.md +7 -0
  85. package/src/lib/command-search.ts +73 -0
  86. package/src/lib/loading-overlay.ts +174 -0
  87. package/src/lib/toast.ts +127 -0
  88. package/src/lib/toggle-shared.ts +4 -1
  89. package/src/lib/workspace-palette.ts +119 -0
  90. package/src/styles/CHANGELOG.md +22 -0
  91. package/src/styles/globals.css +87 -0
@@ -0,0 +1,235 @@
1
+ ---
2
+ /**
3
+ * @component RecentWorkspaceCard
4
+ * @title Recent Workspace Card
5
+ * @description Organism. A recently opened workspace as a shortcut card, in four sizes: `lg` (the large card on the workspace's gradient, "Jump back in"), `md` (tall, beside the large card when there are two recents), `sm` (two stacked) and `xs` (three stacked, one row). The whole card is the link.
6
+ *
7
+ * @preview
8
+ *
9
+ * <div class="grid w-full max-w-3xl gap-4 sm:grid-cols-3">
10
+ * <RecentWorkspaceCard
11
+ * class="sm:col-span-2"
12
+ * name="Acme Logistics"
13
+ * href="#"
14
+ * color="blue"
15
+ * icon="truck"
16
+ * opened="2 hours ago"
17
+ * lastEditedBy="Ana Silva"
18
+ * recordings={128}
19
+ * />
20
+ * <RecentWorkspaceCard size="md" name="Globex Finance" href="#" color="emerald" icon="wallet" opened="yesterday" lastEditedBy="You" recordings={42} />
21
+ * </div>
22
+ *
23
+ * @usage
24
+ *
25
+ * ```astro nocollapse
26
+ * ---
27
+ * import { RecentWorkspaceCard } from '@cl-light-ds/ui/components/organisms/workspace';
28
+ * ---
29
+ *
30
+ * <RecentWorkspaceCard size="lg" name={ws.name} href={ws.url} color={ws.color} icon={ws.icon} opened="2 hours ago" lastEditedBy="Ana Silva" recordings={128} />
31
+ * <RecentWorkspaceCard size="xs" name={ws.name} href={ws.url} color={ws.color} icon={ws.icon} opened="3 days ago" />
32
+ * ```
33
+ *
34
+ * Usually laid out by `RecentWorkspaces`, which picks the sizes from the
35
+ * number of recents. On their own:
36
+ *
37
+ * - `lg`: gradient in the workspace colour (or the brand gradient with
38
+ * `tone="brand"`), 16px corners, the ring artwork bottom right; 56px inverse
39
+ * avatar top left, "Jump back in" (12px caps) top right; the name (30px
40
+ * semibold) on a divider; "Opened … · Last edited by …" and the recordings
41
+ * (14px) under it; a white arrow centred on the right. At least 222px high.
42
+ * - `md`: white; 56px soft avatar; the name (18px) and "Opened …" (16px) on
43
+ * the divider; "Last edited by …" and recordings (14px); 20px arrow centred
44
+ * on the right. At least 222px high.
45
+ * - `sm`: 40px avatar, then the name (14px semibold), then "Opened …" (12px);
46
+ * 20px arrow centred on the right.
47
+ * - `xs`: one row: 40px avatar, name and "Opened …", 16px arrow.
48
+ *
49
+ * Every size is `Card as="a" interactive`: one link (no controls inside),
50
+ * with the card's focus ring and its hover / focus lift. The full name is in
51
+ * `title`, since long names truncate.
52
+ */
53
+
54
+ import type { HTMLAttributes } from "astro/types";
55
+ import { ArrowRight, RotateCcwClock } from "@lucide/astro";
56
+ import { cn } from "@cl-light-ds/ui/lib/utils";
57
+ import {
58
+ DEFAULT_WORKSPACE_COLOR,
59
+ brandGradient,
60
+ workspacePalette,
61
+ type WorkspaceColor,
62
+ } from "@cl-light-ds/ui/lib/workspace-palette";
63
+ import { Card } from "../../card";
64
+ import type { WorkspaceGlyphName } from "./icons";
65
+ import { workspaceCardTexts, type WorkspaceCardLabels } from "./labels";
66
+ import type { RecentWorkspaceCardSize, RecentWorkspaceTone } from "./types";
67
+ import WorkspaceAvatar from "./WorkspaceAvatar.astro";
68
+ import rings from "./recent-card-rings-strong.png?url";
69
+
70
+ interface Props extends Omit<HTMLAttributes<"a">, "class"> {
71
+ /** Workspace name (required). */
72
+ name: string;
73
+ /** The workspace's address (required): the whole card is this link. */
74
+ href: string;
75
+ /** Palette colour. Defaults to blue. */
76
+ color?: WorkspaceColor;
77
+ /** Placeholder icon, or `"initial"` (default). */
78
+ icon?: WorkspaceGlyphName | (string & {});
79
+ /** `lg` (default), `md`, `sm` or `xs`. */
80
+ size?: RecentWorkspaceCardSize;
81
+ /** `lg` only: the workspace's own gradient (default) or the brand gradient. */
82
+ tone?: RecentWorkspaceTone;
83
+ /** When it was last opened, e.g. "2 hours ago". */
84
+ opened?: string;
85
+ /** Who last edited it (`lg` and `md`). */
86
+ lastEditedBy?: string;
87
+ /** Number of recordings, or a ready-made string (`lg` and `md`). */
88
+ recordings?: number | string;
89
+ /** Copy overrides (see `WorkspaceCardLabels`). */
90
+ labels?: Partial<WorkspaceCardLabels>;
91
+ class?: string;
92
+ }
93
+
94
+ const {
95
+ name,
96
+ href,
97
+ color = DEFAULT_WORKSPACE_COLOR,
98
+ icon = "initial",
99
+ size = "lg",
100
+ tone = "workspace",
101
+ opened,
102
+ lastEditedBy,
103
+ recordings,
104
+ labels,
105
+ class: className = "",
106
+ ...rest
107
+ } = Astro.props as Props;
108
+
109
+ const text = workspaceCardTexts({ name, opened, lastEditedBy, recordings }, labels);
110
+ const swatch = workspacePalette[color] ?? workspacePalette[DEFAULT_WORKSPACE_COLOR];
111
+ const largeMeta = [text.opened, text.lastEditedBy].filter(Boolean).join(" · ");
112
+
113
+ const sizeClasses: Record<RecentWorkspaceCardSize, string> = {
114
+ lg: cn(
115
+ "isolate min-h-55.5 gap-0 rounded-2xl border-transparent bg-linear-to-br p-0 text-primary-foreground focus-visible:ring-3 focus-visible:ring-offset-2 focus-visible:ring-offset-background",
116
+ tone === "brand" ? brandGradient : swatch.gradient,
117
+ ),
118
+ md: "min-h-55.5 gap-0 p-0",
119
+ sm: "justify-center gap-1.5 px-3 py-2.5 pe-11",
120
+ xs: "flex-row items-center gap-3 p-3",
121
+ };
122
+ ---
123
+
124
+ <Card
125
+ {...rest}
126
+ as="a"
127
+ href={href}
128
+ title={name}
129
+ interactive
130
+ data-recent-workspace-card
131
+ data-recent-size={size}
132
+ data-color={color}
133
+ class={cn(sizeClasses[size], className)}
134
+ >
135
+ {
136
+ size === "lg" && (
137
+ <>
138
+ <div class="flex items-start justify-between gap-4 px-7 pt-6">
139
+ <WorkspaceAvatar
140
+ color={color}
141
+ icon={icon}
142
+ name={name}
143
+ size="lg"
144
+ shape="square"
145
+ tone={tone === "brand" ? "brand" : "inverse"}
146
+ />
147
+ <span class="flex items-center gap-2 text-xs font-semibold tracking-wide text-primary-foreground/90 uppercase">
148
+ <RotateCcwClock class="size-4" aria-hidden="true" />
149
+ {text.jumpBackIn}
150
+ </span>
151
+ </div>
152
+ <ArrowRight
153
+ class="absolute top-1/2 right-7 size-5 -translate-y-1/2 text-primary-foreground/90"
154
+ aria-hidden="true"
155
+ />
156
+ <div class="mt-auto flex min-w-0 flex-col px-7 pt-3 pb-3 pe-14">
157
+ <span data-slot="recent-workspace-card-name" class="block truncate font-heading text-3xl font-semibold">
158
+ {name}
159
+ </span>
160
+ </div>
161
+ {(largeMeta || text.recordings) && (
162
+ <div class="mx-7 flex items-center justify-between gap-4 border-t border-primary-foreground/25 py-3 text-sm text-primary-foreground/90">
163
+ <span class="min-w-0 truncate">{largeMeta}</span>
164
+ {text.recordings && <span class="shrink-0">{text.recordings}</span>}
165
+ </div>
166
+ )}
167
+ <img
168
+ src={rings}
169
+ alt=""
170
+ aria-hidden="true"
171
+ class="pointer-events-none absolute right-0 bottom-0 -z-10 h-7/5 w-auto max-w-none"
172
+ />
173
+ </>
174
+ )
175
+ }
176
+
177
+ {
178
+ size === "md" && (
179
+ <>
180
+ <div class="px-4 pt-4">
181
+ <WorkspaceAvatar color={color} icon={icon} name={name} size="lg" shape="square" />
182
+ </div>
183
+ <ArrowRight
184
+ class="absolute top-1/2 right-4 size-5 -translate-y-1/2 text-muted-foreground"
185
+ aria-hidden="true"
186
+ />
187
+ <div class="mt-auto flex min-w-0 flex-col gap-1 px-4 pt-3 pb-4 pe-10">
188
+ <span data-slot="recent-workspace-card-name" class="block truncate text-lg font-semibold text-foreground">
189
+ {name}
190
+ </span>
191
+ {text.opened && <span class="text-base text-muted-foreground">{text.opened}</span>}
192
+ </div>
193
+ {(text.lastEditedBy || text.recordings) && (
194
+ <div class="mx-4 flex items-center justify-between gap-2 border-t py-3 text-sm text-muted-foreground">
195
+ <span class="min-w-0 truncate">{text.lastEditedBy}</span>
196
+ {text.recordings && <span class="shrink-0">{text.recordings}</span>}
197
+ </div>
198
+ )}
199
+ </>
200
+ )
201
+ }
202
+
203
+ {
204
+ size === "sm" && (
205
+ <>
206
+ <WorkspaceAvatar color={color} icon={icon} name={name} shape="square" />
207
+ <span class="flex min-w-0 flex-col">
208
+ <span data-slot="recent-workspace-card-name" class="block truncate text-sm font-semibold text-foreground">
209
+ {name}
210
+ </span>
211
+ {text.opened && <span class="block truncate text-xs text-muted-foreground">{text.opened}</span>}
212
+ </span>
213
+ <ArrowRight
214
+ class="absolute top-1/2 right-3 size-5 -translate-y-1/2 text-muted-foreground"
215
+ aria-hidden="true"
216
+ />
217
+ </>
218
+ )
219
+ }
220
+
221
+ {
222
+ size === "xs" && (
223
+ <>
224
+ <WorkspaceAvatar color={color} icon={icon} name={name} shape="square" />
225
+ <span class="flex min-w-0 flex-1 flex-col">
226
+ <span data-slot="recent-workspace-card-name" class="block truncate text-sm font-semibold text-foreground">
227
+ {name}
228
+ </span>
229
+ {text.opened && <span class="block truncate text-xs text-muted-foreground">{text.opened}</span>}
230
+ </span>
231
+ <ArrowRight class="size-4 shrink-0 text-muted-foreground" aria-hidden="true" />
232
+ </>
233
+ )
234
+ }
235
+ </Card>
@@ -0,0 +1,93 @@
1
+ ---
2
+ /**
3
+ * @component RecentWorkspaces
4
+ * @title Recent Workspaces
5
+ * @description Organism. The 1–4 most recently opened workspaces: the latest as the large card, the others beside it, sized by how many there are. The large card is always 222px high.
6
+ *
7
+ * @preview
8
+ *
9
+ * <RecentWorkspaces
10
+ * class="w-full"
11
+ * items={[
12
+ * { name: "Acme Logistics", href: "#", color: "blue", icon: "truck", opened: "2 hours ago", lastEditedBy: "Ana Silva", recordings: 128 },
13
+ * { name: "Globex Finance", href: "#", color: "emerald", icon: "wallet", opened: "yesterday" },
14
+ * { name: "Initech Field Service", href: "#", color: "orange", icon: "wrench", opened: "3 days ago" },
15
+ * ]}
16
+ * />
17
+ *
18
+ * @usage
19
+ *
20
+ * ```astro nocollapse
21
+ * ---
22
+ * import { RecentWorkspaces } from '@cl-light-ds/ui/components/organisms/workspace';
23
+ * ---
24
+ *
25
+ * <RecentWorkspaces items={recents} />
26
+ * ```
27
+ *
28
+ * `items` is most recent first; only the first four are shown, and nothing is
29
+ * rendered without any. On wide screens (`lg`) the large card takes two of
30
+ * three columns and the others share the third:
31
+ *
32
+ * - 1 recent: the large card takes the row
33
+ * - 2: the large card + a medium card (`md`, as high as the large one)
34
+ * - 3: the large card + two small cards (`sm`) stacked, sharing its height
35
+ * - 4: the large card + three extra small cards (`xs`) stacked
36
+ *
37
+ * Below `lg` the cards stack. A `section` named by `label`, with no visible
38
+ * heading (add your own beside it if the page needs one).
39
+ */
40
+
41
+ import type { HTMLAttributes } from "astro/types";
42
+ import { cn } from "@cl-light-ds/ui/lib/utils";
43
+ import type { WorkspaceCardLabels } from "./labels";
44
+ import type { RecentWorkspace, RecentWorkspaceCardSize, RecentWorkspaceTone } from "./types";
45
+ import RecentWorkspaceCard from "./RecentWorkspaceCard.astro";
46
+
47
+ interface Props extends Omit<HTMLAttributes<"section">, "class"> {
48
+ /** The recents, most recent first. Up to four are shown. */
49
+ items: RecentWorkspace[];
50
+ /** The large card: the workspace's own gradient (default) or the brand gradient. */
51
+ tone?: RecentWorkspaceTone;
52
+ /** Accessible name of the section. */
53
+ label?: string;
54
+ /** Copy overrides for the cards (see `WorkspaceCardLabels`). */
55
+ labels?: Partial<WorkspaceCardLabels>;
56
+ class?: string;
57
+ }
58
+
59
+ const { items, tone = "workspace", label = "Recent workspaces", labels, class: className = "", ...rest } =
60
+ Astro.props as Props;
61
+
62
+ const shown = items.slice(0, 4);
63
+ const [latest, ...others] = shown;
64
+ const count = shown.length;
65
+ const otherSize: Record<number, RecentWorkspaceCardSize> = { 2: "md", 3: "sm", 4: "xs" };
66
+ ---
67
+
68
+ {
69
+ latest && (
70
+ <section
71
+ {...rest}
72
+ aria-label={label}
73
+ data-slot="recent-workspaces"
74
+ data-count={count}
75
+ class={cn("grid gap-4 lg:grid-cols-3", className)}
76
+ >
77
+ <RecentWorkspaceCard
78
+ {...latest}
79
+ size="lg"
80
+ tone={tone}
81
+ labels={labels}
82
+ class={count === 1 ? "lg:col-span-3" : "lg:col-span-2"}
83
+ />
84
+ {count > 1 && (
85
+ <div data-slot="recent-workspaces-others" class="flex min-w-0 flex-col gap-3">
86
+ {others.map((item) => (
87
+ <RecentWorkspaceCard {...item} size={otherSize[count]} labels={labels} class="min-w-0 flex-1" />
88
+ ))}
89
+ </div>
90
+ )}
91
+ </section>
92
+ )
93
+ }
@@ -0,0 +1,144 @@
1
+ ---
2
+ /**
3
+ * @component WorkspaceAvatar
4
+ * @title Workspace Avatar
5
+ * @description Organism. A workspace's placeholder icon, or the first letter of its name, on the workspace's colour from the workspace palette. Decorative by default, since the workspace name sits beside it.
6
+ *
7
+ * @preview
8
+ *
9
+ * <div class="flex items-center gap-3">
10
+ * <WorkspaceAvatar color="blue" icon="truck" />
11
+ * <WorkspaceAvatar color="emerald" icon="leaf" tone="solid" shape="square" />
12
+ * <WorkspaceAvatar color="purple" icon="initial" name="Northwind" size="lg" shape="square" />
13
+ * </div>
14
+ *
15
+ * @usage
16
+ *
17
+ * ```astro nocollapse
18
+ * ---
19
+ * import { WorkspaceAvatar } from '@cl-light-ds/ui/components/organisms/workspace';
20
+ * ---
21
+ *
22
+ * <WorkspaceAvatar color="teal" icon="rocket" shape="square" />
23
+ * ```
24
+ *
25
+ * - `tone="soft"` (default): the colour, strong, on a light tint of it
26
+ * - `tone="solid"`: a white glyph on the colour
27
+ * - `tone="inverse"`: the colour on white, e.g. on a coloured card
28
+ * - `tone="brand"`: the primary blue on white, on brand surfaces
29
+ * - `shape="square"`: a rounded square (8px; 14px at `lg` and `xl`)
30
+ * - sizes: `sm` 32px (16px glyph), `default` 40px (20px), `lg` 56px (28px),
31
+ * `xl` 64px (28px, the icon picker's trigger)
32
+ *
33
+ * Pass `label` to make it a named image (`role="img"`) when no name is
34
+ * beside it. A default slot replaces the glyph, e.g. with a logo `<img>`.
35
+ */
36
+
37
+ import type { HTMLAttributes } from "astro/types";
38
+ import { cva, type VariantProps } from "class-variance-authority";
39
+ import { cn } from "@cl-light-ds/ui/lib/utils";
40
+ import {
41
+ DEFAULT_WORKSPACE_COLOR,
42
+ workspacePalette,
43
+ type WorkspaceColor,
44
+ } from "@cl-light-ds/ui/lib/workspace-palette";
45
+ import type { WorkspaceGlyphName } from "./icons";
46
+ import type { WorkspaceAvatarSize, WorkspaceAvatarTone } from "./types";
47
+ import WorkspaceGlyph from "./WorkspaceGlyph.astro";
48
+
49
+ export const workspaceAvatarVariants = cva(
50
+ "relative inline-flex shrink-0 items-center justify-center overflow-hidden select-none",
51
+ {
52
+ variants: {
53
+ size: {
54
+ sm: "size-8",
55
+ default: "size-10",
56
+ lg: "size-14",
57
+ xl: "size-16",
58
+ },
59
+ shape: {
60
+ circle: "rounded-full",
61
+ square: "",
62
+ },
63
+ },
64
+ compoundVariants: [
65
+ { shape: "square", size: ["sm", "default"], class: "rounded-lg" },
66
+ { shape: "square", size: ["lg", "xl"], class: "rounded-xl" },
67
+ ],
68
+ defaultVariants: {
69
+ size: "default",
70
+ shape: "circle",
71
+ },
72
+ },
73
+ );
74
+
75
+ /** Glyph size per avatar size: the icon, or the letter's box and font size. */
76
+ const glyphSizes: Record<string, string> = {
77
+ sm: "size-4 text-sm",
78
+ default: "size-5 text-base",
79
+ lg: "size-7 text-2xl",
80
+ xl: "size-7 text-2xl",
81
+ };
82
+
83
+ interface Props
84
+ extends VariantProps<typeof workspaceAvatarVariants>,
85
+ Omit<HTMLAttributes<"span">, "class"> {
86
+ /** Palette colour. Defaults to `DEFAULT_WORKSPACE_COLOR` (blue). */
87
+ color?: WorkspaceColor;
88
+ /** An icon name from `WORKSPACE_ICONS`, or `"initial"`. */
89
+ icon?: WorkspaceGlyphName | (string & {});
90
+ /** The workspace name, for the initial. */
91
+ name?: string;
92
+ tone?: WorkspaceAvatarTone;
93
+ /** An accessible name. Without it the avatar is decorative (`aria-hidden`). */
94
+ label?: string;
95
+ class?: string;
96
+ /** Extra classes for the glyph (icon or letter). */
97
+ glyphClass?: string;
98
+ }
99
+
100
+ const {
101
+ color = DEFAULT_WORKSPACE_COLOR,
102
+ icon = "initial",
103
+ name = "",
104
+ tone = "soft",
105
+ size = "default",
106
+ shape = "circle",
107
+ label,
108
+ class: className = "",
109
+ glyphClass = "",
110
+ ...rest
111
+ } = Astro.props as Props;
112
+
113
+ const swatch = workspacePalette[color] ?? workspacePalette[DEFAULT_WORKSPACE_COLOR];
114
+ const toneClasses: Record<WorkspaceAvatarTone, string[]> = {
115
+ soft: [swatch.tint, swatch.text],
116
+ solid: [swatch.solid, "text-primary-foreground"],
117
+ inverse: ["bg-background", swatch.text],
118
+ brand: ["bg-background", "text-primary"],
119
+ };
120
+ const toneClass = toneClasses[tone as WorkspaceAvatarTone] ?? toneClasses.soft;
121
+ const glyphSize = glyphSizes[(size ?? "default") as WorkspaceAvatarSize];
122
+ const hasSlot = Astro.slots.has("default");
123
+ ---
124
+
125
+ <span
126
+ {...rest}
127
+ data-slot="workspace-avatar"
128
+ data-size={size}
129
+ data-shape={shape}
130
+ data-tone={tone}
131
+ data-color={color}
132
+ role={label ? "img" : undefined}
133
+ aria-label={label}
134
+ aria-hidden={label ? undefined : "true"}
135
+ class={cn(workspaceAvatarVariants({ size, shape }), toneClass, className)}
136
+ >
137
+ {
138
+ hasSlot ? (
139
+ <slot />
140
+ ) : (
141
+ <WorkspaceGlyph icon={icon} name={name} class={cn(glyphSize, glyphClass)} />
142
+ )
143
+ }
144
+ </span>
@@ -0,0 +1,172 @@
1
+ ---
2
+ /**
3
+ * @component WorkspaceCard
4
+ * @title Workspace Card
5
+ * @description Organism. A workspace in a grid: its soft square avatar with a favourite star and an actions menu top right, the name and when you last opened it, and under a divider who last edited it and how many recordings it has. The whole card opens the workspace and lifts on hover and keyboard focus.
6
+ *
7
+ * @preview
8
+ *
9
+ * <WorkspaceCard
10
+ * class="w-72"
11
+ * name="Acme Logistics"
12
+ * href="#"
13
+ * color="blue"
14
+ * icon="truck"
15
+ * opened="2 hours ago"
16
+ * lastEditedBy="Ana Silva"
17
+ * recordings={128}
18
+ * favorite
19
+ * />
20
+ *
21
+ * @usage
22
+ *
23
+ * ```astro nocollapse
24
+ * ---
25
+ * import { WorkspaceCard } from '@cl-light-ds/ui/components/organisms/workspace';
26
+ * import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@cl-light-ds/ui/components/dropdown-menu';
27
+ * import { Ellipsis } from '@lucide/astro';
28
+ * ---
29
+ *
30
+ * <WorkspaceCard
31
+ * name={ws.name}
32
+ * href={`/w/${ws.id}`}
33
+ * color={ws.color}
34
+ * icon={ws.icon}
35
+ * opened="2 hours ago"
36
+ * lastEditedBy="Ana Silva"
37
+ * recordings={128}
38
+ * favorite={ws.starred}
39
+ * data-workspace-id={ws.id}
40
+ * >
41
+ * <DropdownMenu slot="actions">
42
+ * <DropdownMenuTrigger variant="ghost" size="icon-sm" tailIcon={false} aria-label={`Actions for ${ws.name}`}>
43
+ * <Ellipsis />
44
+ * </DropdownMenuTrigger>
45
+ * <DropdownMenuContent align="end">
46
+ * <DropdownMenuItem>Duplicate</DropdownMenuItem>
47
+ * </DropdownMenuContent>
48
+ * </DropdownMenu>
49
+ * </WorkspaceCard>
50
+ * ```
51
+ *
52
+ * The name is the card's one link, stretched over it (`after:absolute
53
+ * after:inset-0`), so the card opens from anywhere; the star and the
54
+ * `actions` slot sit above it (`relative z-10`) and stay separate controls,
55
+ * with no interactive element nested in the link. The focus ring goes round
56
+ * the whole card while the link has keyboard focus.
57
+ *
58
+ * The star shows only on a favourite, or while the card is hovered or has
59
+ * keyboard focus; always on touch screens. It is a `FavoriteToggle`: listen
60
+ * for its `toggle:change` (`{ pressed }`, bubbling) to save the change.
61
+ *
62
+ * Long names and editors truncate, with the full text in `title`.
63
+ */
64
+
65
+ import type { HTMLAttributes } from "astro/types";
66
+ import { cn } from "@cl-light-ds/ui/lib/utils";
67
+ import { DEFAULT_WORKSPACE_COLOR, type WorkspaceColor } from "@cl-light-ds/ui/lib/workspace-palette";
68
+ import { Card, CardTitle } from "../../card";
69
+ import { FavoriteToggle } from "../../favorite-toggle";
70
+ import type { WorkspaceGlyphName } from "./icons";
71
+ import { workspaceCardTexts, type WorkspaceCardLabels } from "./labels";
72
+ import WorkspaceAvatar from "./WorkspaceAvatar.astro";
73
+
74
+ interface Props extends Omit<HTMLAttributes<"div">, "class"> {
75
+ /** Workspace name (required). */
76
+ name: string;
77
+ /** Where the card goes: the workspace. */
78
+ href: string;
79
+ /** Palette colour. Defaults to blue. */
80
+ color?: WorkspaceColor;
81
+ /** Placeholder icon, or `"initial"` (default). */
82
+ icon?: WorkspaceGlyphName | (string & {});
83
+ /** When it was last opened, e.g. "2 hours ago" ("Opened {time}"). */
84
+ opened?: string;
85
+ /** Who last edited it ("Last edited by {name}"). */
86
+ lastEditedBy?: string;
87
+ /** Number of recordings ("{count} recordings"), or a ready-made string. */
88
+ recordings?: number | string;
89
+ /** A favourite: the star is filled and always shown. */
90
+ favorite?: boolean;
91
+ /** Show the star at all. */
92
+ showFavorite?: boolean;
93
+ /** Copy overrides (see `WorkspaceCardLabels`). */
94
+ labels?: Partial<WorkspaceCardLabels>;
95
+ class?: string;
96
+ }
97
+
98
+ const {
99
+ name,
100
+ href,
101
+ color = DEFAULT_WORKSPACE_COLOR,
102
+ icon = "initial",
103
+ opened,
104
+ lastEditedBy,
105
+ recordings,
106
+ favorite = false,
107
+ showFavorite = true,
108
+ labels,
109
+ class: className = "",
110
+ ...rest
111
+ } = Astro.props as Props;
112
+
113
+ const text = workspaceCardTexts({ name, opened, lastEditedBy, recordings }, labels);
114
+ const hasActions = Astro.slots.has("actions");
115
+ const hasFooter = Boolean(text.lastEditedBy || text.recordings);
116
+ ---
117
+
118
+ <Card
119
+ {...rest}
120
+ interactive
121
+ data-workspace-card
122
+ data-color={color}
123
+ class={cn(
124
+ "gap-0 p-0",
125
+ // The stretched link's focus ring, round the whole card.
126
+ "has-[[data-slot=workspace-card-link]:focus-visible]:border-ring has-[[data-slot=workspace-card-link]:focus-visible]:ring-2 has-[[data-slot=workspace-card-link]:focus-visible]:ring-chart-1",
127
+ className,
128
+ )}
129
+ >
130
+ <div class="flex items-start justify-between gap-3 px-4 pt-4">
131
+ <WorkspaceAvatar color={color} icon={icon} name={name} shape="square" />
132
+ {
133
+ (showFavorite || hasActions) && (
134
+ <div data-slot="workspace-card-actions" class="relative z-10 flex items-center gap-1">
135
+ {showFavorite && (
136
+ <FavoriteToggle
137
+ label={text.favorite}
138
+ defaultPressed={favorite}
139
+ class="not-aria-pressed:opacity-0 group-hover/card:opacity-100 group-has-focus-visible/card:opacity-100 pointer-coarse:opacity-100"
140
+ />
141
+ )}
142
+ <slot name="actions" />
143
+ </div>
144
+ )
145
+ }
146
+ </div>
147
+
148
+ <div class="flex min-w-0 flex-col gap-1 px-4 pt-3 pb-4">
149
+ <CardTitle class="min-w-0 text-base leading-6">
150
+ <a
151
+ href={href}
152
+ title={name}
153
+ data-slot="workspace-card-link"
154
+ class="block truncate rounded-sm outline-none after:absolute after:inset-0 after:rounded-lg"
155
+ >
156
+ {name}
157
+ </a>
158
+ </CardTitle>
159
+ {text.opened && <p class="text-muted-foreground text-sm">{text.opened}</p>}
160
+ </div>
161
+
162
+ {
163
+ hasFooter && (
164
+ <div class="mx-4 flex items-center justify-between gap-2 border-t py-3 text-xs text-muted-foreground">
165
+ <p class="min-w-0 truncate" title={text.lastEditedBy || undefined}>
166
+ {text.lastEditedBy}
167
+ </p>
168
+ {text.recordings && <p class="shrink-0" data-slot="workspace-card-recordings">{text.recordings}</p>}
169
+ </div>
170
+ )
171
+ }
172
+ </Card>