@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,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* @component OptionCard
|
|
4
|
+
* @title Option Card
|
|
5
|
+
* @description A radio choice drawn as a card: radio, bold title (with an optional badge), description. The chosen card has a primary border on the accent tint, and can open more content inside it. Goes inside a `RadioGroup`, which owns selection, keyboard and ARIA.
|
|
6
|
+
*
|
|
7
|
+
* @preview
|
|
8
|
+
*
|
|
9
|
+
* <RadioGroup name="mode" defaultValue="copy" aria-label="How content is brought across" class="gap-3">
|
|
10
|
+
* <OptionCard value="copy" title="Make independent copies" description="The new workspace gets its own copies.">
|
|
11
|
+
* <Badge slot="badge" variant="accent" size="sm" class="border-chart-1">Recommended</Badge>
|
|
12
|
+
* </OptionCard>
|
|
13
|
+
* <OptionCard value="link" title="Link to the original" description="Any change made in either workspace appears in both." />
|
|
14
|
+
* </RadioGroup>
|
|
15
|
+
*
|
|
16
|
+
* @usage
|
|
17
|
+
*
|
|
18
|
+
* ```astro nocollapse
|
|
19
|
+
* ---
|
|
20
|
+
* import { RadioGroup } from '@cl-light-ds/ui/components/radio-group';
|
|
21
|
+
* import { OptionCard } from '@cl-light-ds/ui/components/option-card';
|
|
22
|
+
* ---
|
|
23
|
+
*
|
|
24
|
+
* <RadioGroup name="export" defaultValue="full" aria-label="What to export" class="gap-3">
|
|
25
|
+
* <OptionCard value="full" title="Full workspace" description="Recordings, settings and team members." />
|
|
26
|
+
* <OptionCard value="choose" title="Choose what to include" description="Pick exactly which parts to export." content="panel">
|
|
27
|
+
* <!-- shown only while this option is chosen -->
|
|
28
|
+
* </OptionCard>
|
|
29
|
+
* </RadioGroup>
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* The title, badge and description are the radio's `<label>`, so the whole
|
|
33
|
+
* top of the card selects it. The default slot is outside the label (it may
|
|
34
|
+
* hold its own controls, e.g. checkboxes) and is shown only while the card is
|
|
35
|
+
* chosen:
|
|
36
|
+
*
|
|
37
|
+
* - `content="inset"`: lined up under the description, after a divider, small muted text (notes).
|
|
38
|
+
* - `content="panel"`: a white bordered panel inset in the card (a checklist).
|
|
39
|
+
* - `content="plain"`: no styling, only the show/hide.
|
|
40
|
+
*
|
|
41
|
+
* Listen for `radio-group:change` on the `RadioGroup` for the value.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import type { HTMLAttributes } from "astro/types";
|
|
45
|
+
import { cva } from "class-variance-authority";
|
|
46
|
+
import { cn } from "@cl-light-ds/ui/lib/utils";
|
|
47
|
+
import { RadioGroupItem } from "../radio-group";
|
|
48
|
+
|
|
49
|
+
type OptionCardContent = "inset" | "panel" | "plain";
|
|
50
|
+
|
|
51
|
+
interface Props extends Omit<HTMLAttributes<"div">, "class" | "title"> {
|
|
52
|
+
/** The radio value. */
|
|
53
|
+
value: string;
|
|
54
|
+
/** Bold title. Or use the `title` slot. */
|
|
55
|
+
title?: string;
|
|
56
|
+
/** Muted text under the title. Or use the `description` slot. */
|
|
57
|
+
description?: string;
|
|
58
|
+
/** How the nested (default slot) content is framed while chosen. */
|
|
59
|
+
content?: OptionCardContent;
|
|
60
|
+
disabled?: boolean;
|
|
61
|
+
/** id for the radio item. */
|
|
62
|
+
id?: string;
|
|
63
|
+
class?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const {
|
|
67
|
+
value,
|
|
68
|
+
title,
|
|
69
|
+
description,
|
|
70
|
+
content = "inset",
|
|
71
|
+
disabled = false,
|
|
72
|
+
id,
|
|
73
|
+
class: className = "",
|
|
74
|
+
...rest
|
|
75
|
+
} = Astro.props as Props;
|
|
76
|
+
|
|
77
|
+
const hasBadge = Astro.slots.has("badge");
|
|
78
|
+
const hasTitleSlot = Astro.slots.has("title");
|
|
79
|
+
const hasDescriptionSlot = Astro.slots.has("description");
|
|
80
|
+
const hasContent = Astro.slots.has("default");
|
|
81
|
+
|
|
82
|
+
const optionCardContentVariants = cva(
|
|
83
|
+
// Hidden until this card's own radio (not one nested in the content) is checked.
|
|
84
|
+
"hidden group-has-[>[data-slot=option-card-label]>[data-checked]]/option-card:block",
|
|
85
|
+
{
|
|
86
|
+
variants: {
|
|
87
|
+
content: {
|
|
88
|
+
inset: "-mt-2 mr-4 mb-4 ml-11 border-t pt-3 text-xs text-muted-foreground",
|
|
89
|
+
panel: "mx-3 mb-3 rounded-lg border bg-background",
|
|
90
|
+
plain: "",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
defaultVariants: { content: "inset" },
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
<div
|
|
99
|
+
{...rest}
|
|
100
|
+
data-slot="option-card"
|
|
101
|
+
data-value={value}
|
|
102
|
+
class={cn(
|
|
103
|
+
"group/option-card flex flex-col rounded-lg border bg-card transition-colors",
|
|
104
|
+
"has-[>[data-slot=option-card-label]>[data-checked]]:border-primary has-[>[data-slot=option-card-label]>[data-checked]]:bg-badge-accent/50",
|
|
105
|
+
"has-[>[data-slot=option-card-label]>[data-disabled]]:opacity-50",
|
|
106
|
+
className,
|
|
107
|
+
)}
|
|
108
|
+
>
|
|
109
|
+
<label
|
|
110
|
+
data-slot="option-card-label"
|
|
111
|
+
class="flex cursor-pointer items-start gap-3 p-4 text-sm text-foreground has-[>[data-disabled]]:cursor-not-allowed"
|
|
112
|
+
>
|
|
113
|
+
<RadioGroupItem value={value} id={id} disabled={disabled} class="mt-0.5" />
|
|
114
|
+
<span class="flex min-w-0 flex-col gap-1">
|
|
115
|
+
<span data-slot="option-card-title" class="flex flex-wrap items-center gap-2 font-semibold">
|
|
116
|
+
{hasTitleSlot ? <slot name="title" /> : title}
|
|
117
|
+
{hasBadge && <slot name="badge" />}
|
|
118
|
+
</span>
|
|
119
|
+
{
|
|
120
|
+
(hasDescriptionSlot || description) && (
|
|
121
|
+
<span data-slot="option-card-description" class="text-muted-foreground">
|
|
122
|
+
{hasDescriptionSlot ? <slot name="description" /> : description}
|
|
123
|
+
</span>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
</span>
|
|
127
|
+
</label>
|
|
128
|
+
{
|
|
129
|
+
hasContent && (
|
|
130
|
+
<div data-slot="option-card-content" class={optionCardContentVariants({ content })}>
|
|
131
|
+
<slot />
|
|
132
|
+
</div>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as OptionCard } from "./OptionCard.astro";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the top bar organism (`organisms/top-bar`) will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## Date 15th Oct 2026
|
|
6
|
+
## Workspace selection handover (N12, C1)
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- `TopBar` (N12): the organisation-level application bar, 41px including its bottom border (D23): 4px above and below the 32px controls. Sticky by default (`StickySurface`; `sticky={false}` for a plain bar).
|
|
11
|
+
- Left: a breadcrumb from `crumbs` (`{ label, href? }[]`, the last one current, crumbs without `href` as plain text), or the `breadcrumb` slot. It doesn't wrap; the current crumb truncates.
|
|
12
|
+
- Right, in order: the `search` slot (e.g. `CommandSearch`), the New workspace split button, the `notifications` slot (e.g. `NotificationsMenu`), the `profile` slot (e.g. `ProfileMenu`).
|
|
13
|
+
- Split button: primary `primaryLabel` ("New workspace", `primaryShortLabel` "New" on phones) + separator + chevron `DropdownMenu` with `menuItems` (default Import workspace, `id: "import"`). `splitButton={false}` hides it, the `actions` slot replaces it, `menuItems={[]}` drops the chevron. Uses the C2 split-button styles already in `ButtonGroup`.
|
|
14
|
+
- Event `top-bar:action` (`{ action }`, bubbles, cancelable) on the primary button (`primaryAction`, default `"new-workspace"`) or a row (its `id`); a row with `href` navigates unless cancelled. The button and rows also carry `data-action`.
|
|
15
|
+
- Holds the toast stack (N9): a `Toaster` with `top` = `TOP_BAR_HEIGHT` (41px), right-aligned with the bar's padding. The bar's surface is the containing block of the stack, so it hangs from the bar's right end and follows it. `toaster={false}` leaves it out; `toasterId`, `toasterOffset`.
|
|
16
|
+
- `types.ts`: `TopBarCrumb`, `TopBarMenuItem`, `TopBarActionDetail`, `TOP_BAR_HEIGHT`.
|
|
17
|
+
- Storybook: "Top Bar" under Organisms.
|
|
18
|
+
|
|
19
|
+
### Breadcrumb inside the bar (C1)
|
|
20
|
+
|
|
21
|
+
- Crumbs are 32px tall (`py-1.5`, was the pill's `py-2`, 36px) with 6px either side of the chevron (`px-1.5`, was `px-3`), so the bar stays 41px (D23, D97). The first crumb is pulled back by its padding so its text lines up with the bar's edge. Applied inside the organism; the generic `Breadcrumb` is unchanged.
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* @component TopBar
|
|
4
|
+
* @title Top Bar
|
|
5
|
+
* @description Organism. The organisation-level application bar, 41px with its bottom border: a breadcrumb on the left; on the right, in order, command search, a New workspace split button (with Import workspace), the notifications bell and the profile avatar. It hosts the toast stack under its right end. Sticky.
|
|
6
|
+
*
|
|
7
|
+
* @preview
|
|
8
|
+
*
|
|
9
|
+
* <TopBar crumbs={[{ label: "Acme Inc." }, { label: "Workspaces" }]} toaster={false}>
|
|
10
|
+
* <CommandSearch slot="search" triggerClass="hidden md:inline-flex" />
|
|
11
|
+
* <NotificationsMenu slot="notifications" notifications={notifications} />
|
|
12
|
+
* <ProfileMenu slot="profile" name="Wiley Fox" email="wiley.fox@northwind.com" plan="trial" />
|
|
13
|
+
* </TopBar>
|
|
14
|
+
*
|
|
15
|
+
* @usage
|
|
16
|
+
*
|
|
17
|
+
* ```astro nocollapse
|
|
18
|
+
* ---
|
|
19
|
+
* import { TopBar } from '@cl-light-ds/ui/components/organisms/top-bar';
|
|
20
|
+
* import { CommandSearch } from '@cl-light-ds/ui/components/command-search';
|
|
21
|
+
* import { NotificationsMenu } from '@cl-light-ds/ui/components/notifications-menu';
|
|
22
|
+
* import { ProfileMenu } from '@cl-light-ds/ui/components/profile-menu';
|
|
23
|
+
* ---
|
|
24
|
+
*
|
|
25
|
+
* <TopBar crumbs={[{ label: org.name }, { label: 'Workspaces' }]}>
|
|
26
|
+
* <CommandSearch slot="search" mode="remote" triggerClass="hidden md:inline-flex" />
|
|
27
|
+
* <NotificationsMenu slot="notifications" notifications={notifications} />
|
|
28
|
+
* <ProfileMenu slot="profile" name={user.name} email={user.email} plan={org.plan} />
|
|
29
|
+
* </TopBar>
|
|
30
|
+
*
|
|
31
|
+
* <script>
|
|
32
|
+
* document.querySelector('[data-slot="top-bar"]')?.addEventListener('top-bar:action', (event) => {
|
|
33
|
+
* const { action } = event.detail; // "new-workspace" | "import"
|
|
34
|
+
* });
|
|
35
|
+
* </script>
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @api
|
|
39
|
+
*
|
|
40
|
+
* ### Slots
|
|
41
|
+
*
|
|
42
|
+
* | Slot | Description |
|
|
43
|
+
* |------|-------------|
|
|
44
|
+
* | `breadcrumb` | Replaces the breadcrumb built from `crumbs`. |
|
|
45
|
+
* | `search` | The command search trigger, e.g. `CommandSearch`. |
|
|
46
|
+
* | `actions` | Replaces the split button. |
|
|
47
|
+
* | `notifications` | The bell, e.g. `NotificationsMenu`. |
|
|
48
|
+
* | `profile` | The avatar, e.g. `ProfileMenu`. |
|
|
49
|
+
*
|
|
50
|
+
* ### Events (on the `[data-slot="top-bar"]` root)
|
|
51
|
+
*
|
|
52
|
+
* | Event | Detail | Description |
|
|
53
|
+
* |-------|--------|-------------|
|
|
54
|
+
* | `top-bar:action` | `{ action }` | Bubbles, cancelable. The primary button (`primaryAction`, default `"new-workspace"`) or a menu row (its `id`, default `"import"`) was chosen. A row with an `href` navigates unless cancelled. |
|
|
55
|
+
*
|
|
56
|
+
* The primary button and the rows also carry `data-action`, for scripts that
|
|
57
|
+
* prefer to bind clicks themselves. To open a file picker on Import, call
|
|
58
|
+
* `input.click()` inside the `top-bar:action` handler: it still runs within the
|
|
59
|
+
* user's click.
|
|
60
|
+
*/
|
|
61
|
+
import { ChevronDown, Plus, Upload } from "@lucide/astro";
|
|
62
|
+
import type { HTMLAttributes } from "astro/types";
|
|
63
|
+
import { cva } from "class-variance-authority";
|
|
64
|
+
import { cn } from "@cl-light-ds/ui/lib/utils";
|
|
65
|
+
|
|
66
|
+
import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbSeparator } from "../../breadcrumb";
|
|
67
|
+
import { Button } from "../../button";
|
|
68
|
+
import { ButtonGroup, ButtonGroupSeparator } from "../../button-group";
|
|
69
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../../dropdown-menu";
|
|
70
|
+
import { StickySurface } from "../../sticky-surface";
|
|
71
|
+
import { Toaster } from "../../toast";
|
|
72
|
+
import { TOP_BAR_HEIGHT, type TopBarCrumb, type TopBarMenuItem } from "./types";
|
|
73
|
+
|
|
74
|
+
interface Props extends HTMLAttributes<"header"> {
|
|
75
|
+
/** Breadcrumb trail; the last one is the current page. The `breadcrumb` slot replaces it. */
|
|
76
|
+
crumbs?: TopBarCrumb[];
|
|
77
|
+
/** Accessible name of the breadcrumb nav. */
|
|
78
|
+
breadcrumbLabel?: string;
|
|
79
|
+
/** Show the split button. The `actions` slot replaces it. */
|
|
80
|
+
splitButton?: boolean;
|
|
81
|
+
/** Primary button label from `sm` up. */
|
|
82
|
+
primaryLabel?: string;
|
|
83
|
+
/** Primary button label on phones. */
|
|
84
|
+
primaryShortLabel?: string;
|
|
85
|
+
/** `data-action` of the primary button and the `action` it sends. */
|
|
86
|
+
primaryAction?: string;
|
|
87
|
+
/** Accessible name of the chevron. */
|
|
88
|
+
menuLabel?: string;
|
|
89
|
+
/** Rows of the chevron's menu. Default: Import workspace (`id: "import"`). Empty: no chevron. */
|
|
90
|
+
menuItems?: TopBarMenuItem[];
|
|
91
|
+
/** Stick to the top of the scrolling container. */
|
|
92
|
+
sticky?: boolean;
|
|
93
|
+
/** Render a `Toaster` hanging from the bar's right end. Turn off when the page has its own. */
|
|
94
|
+
toaster?: boolean;
|
|
95
|
+
/** Id of that Toaster, to target it with `toast({ toaster })`. */
|
|
96
|
+
toasterId?: string;
|
|
97
|
+
/** Gap between the bar and the toast stack. */
|
|
98
|
+
toasterOffset?: string;
|
|
99
|
+
class?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const defaultMenuItems: TopBarMenuItem[] = [{ id: "import", label: "Import workspace", icon: Upload }];
|
|
103
|
+
|
|
104
|
+
const {
|
|
105
|
+
crumbs = [],
|
|
106
|
+
breadcrumbLabel = "Breadcrumb",
|
|
107
|
+
splitButton = true,
|
|
108
|
+
primaryLabel = "New workspace",
|
|
109
|
+
primaryShortLabel = "New",
|
|
110
|
+
primaryAction = "new-workspace",
|
|
111
|
+
menuLabel = "More ways to add a workspace",
|
|
112
|
+
menuItems = defaultMenuItems,
|
|
113
|
+
sticky = true,
|
|
114
|
+
toaster = true,
|
|
115
|
+
toasterId,
|
|
116
|
+
toasterOffset,
|
|
117
|
+
class: className = "",
|
|
118
|
+
...rest
|
|
119
|
+
} = Astro.props as Props;
|
|
120
|
+
|
|
121
|
+
const hasBreadcrumbSlot = Astro.slots.has("breadcrumb");
|
|
122
|
+
const hasActionsSlot = Astro.slots.has("actions");
|
|
123
|
+
|
|
124
|
+
/*
|
|
125
|
+
* D23 — 41px: 4px above and below the 32px controls, plus the 1px border.
|
|
126
|
+
* The side padding matches the Toaster's default right inset (1rem, 1.5rem from md),
|
|
127
|
+
* so the stack lines up with the bar's right end.
|
|
128
|
+
*/
|
|
129
|
+
const topBarVariants = cva(
|
|
130
|
+
"bg-background relative flex items-center gap-4 border-b px-4 py-1 md:px-6",
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
/*
|
|
134
|
+
* C1 — the generic Breadcrumb pads crumbs as 36px pills (px-3 py-2). Here they are
|
|
135
|
+
* 32px (py-1.5), like the buttons, so the bar stays 41px, with 6px (px-1.5) either side
|
|
136
|
+
* of the chevron (D23, D97). The first crumb is pulled back by its padding so its text
|
|
137
|
+
* lines up with the bar's edge.
|
|
138
|
+
*/
|
|
139
|
+
const crumbClass =
|
|
140
|
+
"min-w-0 gap-0 [&_a]:px-1.5 [&_a]:py-1.5 [&_[aria-current=page]]:px-1.5 [&_[aria-current=page]]:py-1.5 first:-ms-1.5";
|
|
141
|
+
const lastIndex = crumbs.length - 1;
|
|
142
|
+
const Wrapper = sticky ? StickySurface : "div";
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
<Wrapper data-slot="top-bar-surface" class={sticky ? undefined : "relative z-50 w-full will-change-transform"}>
|
|
146
|
+
<header {...rest} data-slot="top-bar" class={cn(topBarVariants(), className)}>
|
|
147
|
+
<div data-slot="top-bar-start" class="flex min-w-0 flex-1 items-center">
|
|
148
|
+
{
|
|
149
|
+
hasBreadcrumbSlot ? (
|
|
150
|
+
<slot name="breadcrumb" />
|
|
151
|
+
) : (
|
|
152
|
+
crumbs.length > 0 && (
|
|
153
|
+
<Breadcrumb aria-label={breadcrumbLabel} class="min-w-0">
|
|
154
|
+
<BreadcrumbList class="flex-nowrap">
|
|
155
|
+
{crumbs.map((crumb, i) => (
|
|
156
|
+
<>
|
|
157
|
+
{i > 0 && <BreadcrumbSeparator />}
|
|
158
|
+
<BreadcrumbItem class={cn(crumbClass, i === lastIndex && "min-w-0")}>
|
|
159
|
+
{i === lastIndex ? (
|
|
160
|
+
<span aria-current="page" class="text-foreground max-w-full min-w-0">
|
|
161
|
+
<span class="min-w-0 truncate">{crumb.label}</span>
|
|
162
|
+
</span>
|
|
163
|
+
) : crumb.href ? (
|
|
164
|
+
<a href={crumb.href} class="whitespace-nowrap">
|
|
165
|
+
{crumb.label}
|
|
166
|
+
</a>
|
|
167
|
+
) : (
|
|
168
|
+
<span class="text-muted-foreground px-1.5 py-1.5 whitespace-nowrap">{crumb.label}</span>
|
|
169
|
+
)}
|
|
170
|
+
</BreadcrumbItem>
|
|
171
|
+
</>
|
|
172
|
+
))}
|
|
173
|
+
</BreadcrumbList>
|
|
174
|
+
</Breadcrumb>
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div data-slot="top-bar-end" class="flex shrink-0 items-center gap-2">
|
|
181
|
+
<slot name="search" />
|
|
182
|
+
|
|
183
|
+
{
|
|
184
|
+
hasActionsSlot ? (
|
|
185
|
+
<slot name="actions" />
|
|
186
|
+
) : (
|
|
187
|
+
splitButton && (
|
|
188
|
+
<ButtonGroup data-slot="top-bar-split-button">
|
|
189
|
+
<Button size="sm" type="button" data-action={primaryAction} data-top-bar-primary>
|
|
190
|
+
<Plus slot="lead-icon" />
|
|
191
|
+
<span class="hidden sm:inline">{primaryLabel}</span>
|
|
192
|
+
<span class="sm:hidden">{primaryShortLabel}</span>
|
|
193
|
+
</Button>
|
|
194
|
+
{menuItems.length > 0 && <ButtonGroupSeparator />}
|
|
195
|
+
{menuItems.length > 0 && (
|
|
196
|
+
<DropdownMenu lockScroll={false} class="flex" data-top-bar-menu>
|
|
197
|
+
<DropdownMenuTrigger asChild>
|
|
198
|
+
<Button size="icon-sm" type="button" aria-label={menuLabel}>
|
|
199
|
+
<ChevronDown />
|
|
200
|
+
</Button>
|
|
201
|
+
</DropdownMenuTrigger>
|
|
202
|
+
<DropdownMenuContent align="end" class="w-52">
|
|
203
|
+
{menuItems.map((item) => {
|
|
204
|
+
const Icon = item.icon;
|
|
205
|
+
return (
|
|
206
|
+
<DropdownMenuItem value={item.id} data-action={item.id} data-href={item.href}>
|
|
207
|
+
{Icon && <Icon slot="leading-icon" />}
|
|
208
|
+
{item.label}
|
|
209
|
+
</DropdownMenuItem>
|
|
210
|
+
);
|
|
211
|
+
})}
|
|
212
|
+
</DropdownMenuContent>
|
|
213
|
+
</DropdownMenu>
|
|
214
|
+
)}
|
|
215
|
+
</ButtonGroup>
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
<slot name="notifications" />
|
|
221
|
+
<slot name="profile" />
|
|
222
|
+
</div>
|
|
223
|
+
</header>
|
|
224
|
+
|
|
225
|
+
{
|
|
226
|
+
/*
|
|
227
|
+
N9, D160 — the toast stack hangs from the bar's right end. The surface has
|
|
228
|
+
will-change: transform, which makes it the containing block of the Toaster's
|
|
229
|
+
fixed viewport: the stack is placed against the bar and follows it.
|
|
230
|
+
*/
|
|
231
|
+
}
|
|
232
|
+
{toaster && <Toaster top={TOP_BAR_HEIGHT} offset={toasterOffset} id={toasterId} />}
|
|
233
|
+
</Wrapper>
|
|
234
|
+
|
|
235
|
+
<script>
|
|
236
|
+
import type { TopBarActionDetail } from "./types";
|
|
237
|
+
|
|
238
|
+
const emit = (root: HTMLElement, action: string) =>
|
|
239
|
+
root.dispatchEvent(
|
|
240
|
+
new CustomEvent<TopBarActionDetail>("top-bar:action", {
|
|
241
|
+
bubbles: true,
|
|
242
|
+
cancelable: true,
|
|
243
|
+
detail: { action },
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
document.querySelectorAll<HTMLElement>('[data-slot="top-bar"]').forEach((root) => {
|
|
248
|
+
if (root.dataset.topBarWired) return;
|
|
249
|
+
root.dataset.topBarWired = "";
|
|
250
|
+
|
|
251
|
+
root.querySelector<HTMLElement>("[data-top-bar-primary]")?.addEventListener("click", (event) => {
|
|
252
|
+
const action = (event.currentTarget as HTMLElement).dataset.action;
|
|
253
|
+
if (action) emit(root, action);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
root.querySelector<HTMLElement>("[data-top-bar-menu]")?.addEventListener("dropdown-menu:select", (event) => {
|
|
257
|
+
event.stopPropagation();
|
|
258
|
+
const { value, item } = (event as CustomEvent<{ value: string; item: HTMLElement | null }>).detail;
|
|
259
|
+
const proceed = emit(root, value);
|
|
260
|
+
const href = item?.dataset.href;
|
|
261
|
+
if (proceed && href) window.location.assign(href);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
</script>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AstroComponent } from "@lucide/astro";
|
|
2
|
+
|
|
3
|
+
/** One breadcrumb in the top bar. The last one is the current page. */
|
|
4
|
+
export interface TopBarCrumb {
|
|
5
|
+
label: string;
|
|
6
|
+
/** Link to that level. Left out: plain text (e.g. no organisation page yet). */
|
|
7
|
+
href?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** One row in the split button's menu (the chevron half). */
|
|
11
|
+
export interface TopBarMenuItem {
|
|
12
|
+
/** Sent as `action` in `top-bar:action`, and set as `data-action` on the row. */
|
|
13
|
+
id: string;
|
|
14
|
+
label: string;
|
|
15
|
+
icon?: AstroComponent;
|
|
16
|
+
/** Navigate here when chosen (unless `top-bar:action` is cancelled). */
|
|
17
|
+
href?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** `detail` of the `top-bar:action` event. */
|
|
21
|
+
export interface TopBarActionDetail {
|
|
22
|
+
/** The primary button's `primaryAction`, or a menu row's `id`. */
|
|
23
|
+
action: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The bar's height including its bottom border (D23): 4px + 32px controls +
|
|
28
|
+
* 4px + 1px border. The Toaster hangs from here.
|
|
29
|
+
*/
|
|
30
|
+
export const TOP_BAR_HEIGHT = "41px";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the workspace organisms (`organisms/workspace`) will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## Date 15th Oct 2026
|
|
6
|
+
## Workspace selection handover (N16)
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- `WorkspaceIconPicker`: a 64px soft square `WorkspaceAvatar` button (`PopoverTrigger`) opening a `Popover` (320px): the 12 `WORKSPACE_COLORS` as round swatches (the tint with a dot of the colour, ringed when chosen), a separator, then the initial and the 27 icons (`WORKSPACE_GLYPHS`) as tiles, on the chosen colour's tint with a ring when chosen. The avatar follows the choice live; the popover stays open while you pick.
|
|
11
|
+
- Props: `name` (for the initial), `color` (default blue), `icon` (default `initial`), `colorName` / `iconName` (hidden inputs for forms), `label` (the avatar button's name, default "Choose an icon and colour"), `colorLabel` / `iconLabel` (default "Colour" / "Icon"), `align` (default `start`), `class`; other attributes go on the root.
|
|
12
|
+
- Events on `[data-slot="workspace-icon-picker"]`: `workspace-icon-picker:change` (`{ color, icon }`, bubbling) when something is picked; dispatch `workspace-icon-picker:set` (`{ color?, icon?, name? }`) to set it from outside, e.g. to keep the initial in step with a name field (no `change` fired).
|
|
13
|
+
- Keyboard: every swatch and tile is a button with `aria-pressed`, in two named groups; Escape closes the popover and returns focus to the avatar (popover runtime). No focus trap of its own (Tab inside dialogs is fixed upstream).
|
|
14
|
+
- The initial renders the letter and the fallback icon side by side (one `hidden`), so a name typed after load (empty to start with) shows its letter; this is `WorkspaceIconPickerGlyph`, internal to the picker.
|
|
15
|
+
- Not taken over from the screen: the New workspace dialog's preview zoom (`data-name-zoom`) and its Tab handling inside the dialog (C7).
|
|
16
|
+
- Storybook: "Workspace Icon Picker" under Organisms.
|
|
17
|
+
|
|
18
|
+
## Date 15th Oct 2026
|
|
19
|
+
## Workspace selection handover (N3–N6)
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- `RecentWorkspaceCard`: a recent workspace as a shortcut card, one component with `size`:
|
|
24
|
+
- `lg` (N3): gradient in the workspace colour (`workspacePalette[color].gradient`, or `brandGradient` with `tone="brand"`), 16px corners, the ring artwork (`recent-card-rings-strong.png`, 140% of the card's height, pinned bottom right, decorative); 56px inverse avatar (brand tone with `tone="brand"`) top left, "Jump back in" (12px caps, with a clock icon) top right; the name (30px semibold) on a divider; "Opened … · Last edited by …" and the recordings (14px) under it; a white arrow centred on the right. At least 222px high.
|
|
25
|
+
- `md` (N4): white; 56px soft avatar; name (18px) and "Opened …" (16px) on the divider; "Last edited by …" and the recordings (14px); 20px arrow centred on the right. At least 222px high.
|
|
26
|
+
- `sm` (N5): 40px avatar, then the name (14px semibold) and "Opened …" (12px); 20px arrow centred on the right; 10px padding top and bottom.
|
|
27
|
+
- `xs` (N6): one row: 40px avatar, name and "Opened …", 16px arrow.
|
|
28
|
+
- Props: `name`, `href`, `color`, `icon`, `size` (default `lg`), `tone` (`workspace` | `brand`, `lg` only), `opened`, `lastEditedBy`, `recordings`, `labels`, `class`; other attributes go on the link.
|
|
29
|
+
- Every size is `Card as="a" interactive`: the whole card is one link with the card's focus ring (offset on the gradient card) and the hover / focus lift (C14). The full name is in `title`.
|
|
30
|
+
- `RecentWorkspaces`: lays out 1–4 recents (`items`, most recent first; more are ignored, none renders nothing): the large card, and beside it on `lg` screens a medium card (2), two small cards stacked (3) or three extra small cards stacked (4) sharing its 222px. One recent takes the row. Props: `items` (`RecentWorkspace[]`), `tone`, `label` (section name, default "Recent workspaces"), `labels`, `class`. `data-count` on the section.
|
|
31
|
+
- Types: `RecentWorkspace`, `RecentWorkspaceCardSize`, `RecentWorkspaceTone`.
|
|
32
|
+
- Generalised from the screen: sizes are a prop instead of following the section's `data-count` through group variants; copy through `labels`; the recents strip's own controls (show 1–4, `data-beyond`) are left to the app.
|
|
33
|
+
- Storybook: "Recent Workspaces" under Organisms.
|
|
34
|
+
|
|
35
|
+
## Date 15th Oct 2026
|
|
36
|
+
## Workspace selection handover (N7)
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- `WorkspaceCard`: a workspace in a grid. 40px soft square `WorkspaceAvatar`; a `FavoriteToggle` and an `actions` slot (for a ⋯ `DropdownMenu`) top right; the name (16px semibold, truncates, full name in `title`) and "Opened …" (14px); under a divider "Last edited by …" and the recordings (12px).
|
|
41
|
+
- Props: `name`, `href`, `color`, `icon`, `opened` (e.g. "2 hours ago"), `lastEditedBy`, `recordings` (number, or a string), `favorite`, `showFavorite` (default `true`), `labels`, `class`; other attributes go on the root (e.g. `data-workspace-id`, `hidden`).
|
|
42
|
+
- The name is a stretched link over the card, so the card opens from anywhere while the star and the actions stay separate controls above it (no interactive content inside the link). The focus ring goes round the whole card while the link has keyboard focus.
|
|
43
|
+
- The star shows only on a favourite, or while the card is hovered or has keyboard focus inside it; always on touch screens. Save changes from its `toggle:change` event.
|
|
44
|
+
- Lifts on hover and keyboard focus through `Card`'s new `interactive` prop (C14).
|
|
45
|
+
- `labels.ts`: `WorkspaceCardLabels` (`opened`, `lastEditedBy`, `recordings`, `recording`, `favorite`, `jumpBackIn` templates with `{time}`/`{name}`/`{count}`), `defaultWorkspaceCardLabels`, `fillLabel()`, `workspaceCardTexts()`; shared by the workspace cards.
|
|
46
|
+
- Not taken over from the screen: shortening editor names to "A.Silva" (D128); the card truncates instead, with the full text in `title`.
|
|
47
|
+
- Storybook: "Workspace Card" under Organisms.
|
|
48
|
+
|
|
49
|
+
## Date 15th Oct 2026
|
|
50
|
+
## Workspace selection handover (N17)
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- `OrganisationOverview`: "ORGANISATION" (12px, semibold, caps, muted) over the organisation name (14px semibold) and its plan (muted, after " · "), right-aligned (D113). Props: `name`, `plan` (optional), `label` (default "Organisation"), `align` (`end` default, or `start`), `class`. Hiding it on phones and placing it are left to the consumer. `data-slot="organisation-overview-plan"` wraps the plan for scripts.
|
|
55
|
+
- Storybook: "Organisation Overview" under Organisms.
|
|
56
|
+
|
|
57
|
+
## Date 15th Oct 2026
|
|
58
|
+
## Workspace selection handover (N2)
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
|
|
62
|
+
- `WorkspaceAvatar`: a workspace's placeholder icon or initial on its palette colour (`@cl-light-ds/ui/lib/workspace-palette`).
|
|
63
|
+
- `color`: one of the 12 `WORKSPACE_COLORS` (default `blue`).
|
|
64
|
+
- `tone`: `soft` (tint + coloured glyph, default), `solid` (colour + white glyph), `inverse` (white + coloured glyph), `brand` (white + primary glyph).
|
|
65
|
+
- `shape`: `circle` (default) or `square` (`rounded-lg`; `rounded-xl` at `lg`/`xl`).
|
|
66
|
+
- `size`: `sm` 32px / 16px glyph, `default` 40px / 20px, `lg` 56px / 28px (tall recent card), `xl` 64px / 28px (icon picker trigger).
|
|
67
|
+
- Decorative (`aria-hidden`) unless `label` is given, then `role="img"` with that name. A default slot replaces the glyph (e.g. a logo).
|
|
68
|
+
- A standalone `span` rather than `Avatar` with overrides: `Avatar` is 24/32/40px, round, bordered, and sizes its icon slot itself.
|
|
69
|
+
- `WorkspaceGlyph`: the icon, or the first letter of `name` when `icon="initial"`, centred in a box the icon's size. Falls back to the default icon when there is no name. `data-glyph` names the current glyph for scripts.
|
|
70
|
+
- `WorkspaceIcon`: one of the 27 placeholder Lucide icons by name; unknown names fall back to `building-2`.
|
|
71
|
+
- `icons.ts`, for an icon picker to reuse: `workspaceIcons` (name → Lucide component), `WORKSPACE_ICONS` (27 names, picker order), `WORKSPACE_GLYPHS` (`"initial"` + the 27), `WorkspaceIconName`, `WorkspaceGlyphName`, `DEFAULT_WORKSPACE_ICON`, `resolveWorkspaceIcon()`, `workspaceInitial()`, `workspaceGlyphLabel()`.
|
|
72
|
+
- `index.ts` also re-exports the workspace palette (`workspacePalette`, `WORKSPACE_COLORS`, `DEFAULT_WORKSPACE_COLOR`, `brandGradient`).
|
|
73
|
+
- Storybook: "Workspace Avatar" under Organisms.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* @component OrganisationOverview
|
|
4
|
+
* @title Organisation Overview
|
|
5
|
+
* @description Organism. Which organisation you are in and its plan: a small caps "ORGANISATION" label over "Acme Inc. · Enterprise". Right-aligned by default.
|
|
6
|
+
*
|
|
7
|
+
* @preview
|
|
8
|
+
*
|
|
9
|
+
* <OrganisationOverview name="Acme Inc." plan="Enterprise" />
|
|
10
|
+
*
|
|
11
|
+
* @usage
|
|
12
|
+
*
|
|
13
|
+
* ```astro nocollapse
|
|
14
|
+
* ---
|
|
15
|
+
* import { OrganisationOverview } from '@cl-light-ds/ui/components/organisms/workspace';
|
|
16
|
+
* ---
|
|
17
|
+
*
|
|
18
|
+
* <OrganisationOverview name={org.name} plan={org.plan} class="hidden md:flex" />
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* `data-slot="organisation-overview-plan"` holds the plan text, for scripts
|
|
22
|
+
* that update it after a plan change.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { HTMLAttributes } from "astro/types";
|
|
26
|
+
import { cn } from "@cl-light-ds/ui/lib/utils";
|
|
27
|
+
|
|
28
|
+
interface Props extends Omit<HTMLAttributes<"div">, "class"> {
|
|
29
|
+
/** Organisation name, in semibold. */
|
|
30
|
+
name: string;
|
|
31
|
+
/** Plan, after a middle dot, muted. Left out when empty. */
|
|
32
|
+
plan?: string;
|
|
33
|
+
/** The caps label above. */
|
|
34
|
+
label?: string;
|
|
35
|
+
/** Text alignment: `end` (right, default) or `start`. */
|
|
36
|
+
align?: "start" | "end";
|
|
37
|
+
class?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { name, plan, label = "Organisation", align = "end", class: className = "", ...rest } = Astro.props as Props;
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
<div
|
|
44
|
+
{...rest}
|
|
45
|
+
data-slot="organisation-overview"
|
|
46
|
+
class={cn("flex flex-col gap-0.5", align === "end" ? "items-end text-right" : "items-start text-left", className)}
|
|
47
|
+
>
|
|
48
|
+
<p data-slot="organisation-overview-label" class="text-xs font-semibold tracking-wide text-muted-foreground uppercase">
|
|
49
|
+
{label}
|
|
50
|
+
</p>
|
|
51
|
+
<p class="text-sm">
|
|
52
|
+
<span data-slot="organisation-overview-name" class="font-semibold text-foreground">{name}</span>
|
|
53
|
+
{
|
|
54
|
+
plan && (
|
|
55
|
+
<span class="text-muted-foreground">
|
|
56
|
+
{" · "}
|
|
57
|
+
<span data-slot="organisation-overview-plan">{plan}</span>
|
|
58
|
+
</span>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|