@cl-light-ds/ui 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +20 -18
- package/src/components/alert/Alert.astro +8 -2
- package/src/components/alert/CHANGELOG.md +9 -0
- package/src/components/button/Button.astro +78 -19
- package/src/components/button/CHANGELOG.md +22 -0
- package/src/components/button-group/ButtonGroup.astro +14 -3
- package/src/components/button-group/CHANGELOG.md +17 -0
- package/src/components/callout/CHANGELOG.md +32 -0
- package/src/components/callout/Callout.astro +147 -0
- package/src/components/callout/CalloutDescription.astro +16 -0
- package/src/components/callout/CalloutTitle.astro +16 -0
- package/src/components/callout/index.ts +3 -0
- package/src/components/card/CHANGELOG.md +9 -0
- package/src/components/card/Card.astro +22 -0
- package/src/components/combobox/CHANGELOG.md +14 -0
- package/src/components/combobox/ComboboxTrigger.astro +31 -1
- package/src/components/command-search/CHANGELOG.md +27 -0
- package/src/components/command-search/CommandSearch.astro +515 -0
- package/src/components/command-search/CommandSearchGroup.astro +32 -0
- package/src/components/command-search/CommandSearchItem.astro +64 -0
- package/src/components/command-search/classes.ts +18 -0
- package/src/components/command-search/index.ts +12 -0
- package/src/components/command-search/nothing-matches.svg +42 -0
- package/src/components/dialog/CHANGELOG.md +13 -0
- package/src/components/dialog/DialogClose.astro +26 -12
- package/src/components/dropdown-menu/CHANGELOG.md +7 -0
- package/src/components/dropdown-menu/dropdown-menu-item-variants.ts +1 -1
- package/src/components/empty-state/CHANGELOG.md +18 -0
- package/src/components/empty-state/EmptyState.astro +157 -0
- package/src/components/empty-state/illustrations/AllCaughtUp.astro +52 -0
- package/src/components/empty-state/illustrations/EmptyWorkspaces.astro +56 -0
- package/src/components/empty-state/illustrations/LoadError.astro +61 -0
- package/src/components/empty-state/illustrations/NoResults.astro +60 -0
- package/src/components/empty-state/illustrations/NothingMatches.astro +66 -0
- package/src/components/empty-state/illustrations/index.ts +27 -0
- package/src/components/empty-state/index.ts +7 -0
- package/src/components/favorite-toggle/CHANGELOG.md +13 -0
- package/src/components/favorite-toggle/FavoriteToggle.astro +94 -0
- package/src/components/favorite-toggle/index.ts +1 -0
- package/src/components/input/CHANGELOG.md +15 -0
- package/src/components/input/InputOTP.astro +211 -0
- package/src/components/input/index.ts +1 -1
- package/src/components/input-group/CHANGELOG.md +7 -0
- package/src/components/input-group/InputGroup.astro +2 -0
- package/src/components/loading-overlay/CHANGELOG.md +25 -0
- package/src/components/loading-overlay/LoadingOverlay.astro +179 -0
- package/src/components/loading-overlay/index.ts +8 -0
- package/src/components/logo/CHANGELOG.md +17 -0
- package/src/components/logo/Logo.astro +90 -0
- package/src/components/logo/index.ts +3 -0
- package/src/components/logo/logo-paths.ts +57 -0
- package/src/components/notifications-menu/CHANGELOG.md +22 -0
- package/src/components/notifications-menu/NotificationsMenu.astro +374 -0
- package/src/components/notifications-menu/NotificationsMenuItem.astro +129 -0
- package/src/components/notifications-menu/all-caught-up.svg +28 -0
- package/src/components/notifications-menu/index.ts +3 -0
- package/src/components/notifications-menu/types.ts +32 -0
- package/src/components/option-card/CHANGELOG.md +16 -0
- package/src/components/option-card/OptionCard.astro +135 -0
- package/src/components/option-card/index.ts +1 -0
- package/src/components/organisms/top-bar/CHANGELOG.md +21 -0
- package/src/components/organisms/top-bar/TopBar.astro +264 -0
- package/src/components/organisms/top-bar/index.ts +2 -0
- package/src/components/organisms/top-bar/types.ts +30 -0
- package/src/components/organisms/workspace/CHANGELOG.md +73 -0
- package/src/components/organisms/workspace/OrganisationOverview.astro +62 -0
- package/src/components/organisms/workspace/RecentWorkspaceCard.astro +235 -0
- package/src/components/organisms/workspace/RecentWorkspaces.astro +93 -0
- package/src/components/organisms/workspace/WorkspaceAvatar.astro +144 -0
- package/src/components/organisms/workspace/WorkspaceCard.astro +172 -0
- package/src/components/organisms/workspace/WorkspaceGlyph.astro +62 -0
- package/src/components/organisms/workspace/WorkspaceIcon.astro +42 -0
- package/src/components/organisms/workspace/WorkspaceIconPicker.astro +288 -0
- package/src/components/organisms/workspace/WorkspaceIconPickerGlyph.astro +44 -0
- package/src/components/organisms/workspace/icons.ts +98 -0
- package/src/components/organisms/workspace/index.ts +35 -0
- package/src/components/organisms/workspace/labels.ts +54 -0
- package/src/components/organisms/workspace/recent-card-rings-strong.png +0 -0
- package/src/components/organisms/workspace/types.ts +30 -0
- package/src/components/profile-menu/CHANGELOG.md +22 -0
- package/src/components/profile-menu/ProfileMenu.astro +236 -0
- package/src/components/profile-menu/index.ts +2 -0
- package/src/components/profile-menu/types.ts +27 -0
- package/src/components/search-field/CHANGELOG.md +14 -0
- package/src/components/search-field/SearchField.astro +163 -0
- package/src/components/search-field/index.ts +1 -0
- package/src/components/table/CHANGELOG.md +9 -0
- package/src/components/table/TableCell.astro +6 -0
- package/src/components/table/TableHead.astro +5 -1
- package/src/components/table/TableRow.astro +3 -1
- package/src/components/table/table-cell-variants.ts +2 -0
- package/src/components/toast/CHANGELOG.md +32 -0
- package/src/components/toast/Toast.astro +145 -0
- package/src/components/toast/Toaster.astro +322 -0
- package/src/components/toast/index.ts +9 -0
- package/src/components/toggle/CHANGELOG.md +7 -0
- package/src/lib/command-search.ts +73 -0
- package/src/lib/loading-overlay.ts +174 -0
- package/src/lib/toast.ts +127 -0
- package/src/lib/toggle-shared.ts +4 -1
- package/src/lib/workspace-palette.ts +119 -0
- package/src/styles/CHANGELOG.md +22 -0
- package/src/styles/globals.css +100 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Illustration "no-results" (280×200, with its own padding inside the viewBox).
|
|
4
|
+
* The designer's artwork; colours mapped to design tokens so it follows the theme.
|
|
5
|
+
* Decorative. Generated from the workspace selection handover assets (N15).
|
|
6
|
+
*/
|
|
7
|
+
import type { HTMLAttributes } from "astro/types";
|
|
8
|
+
|
|
9
|
+
type Props = Omit<HTMLAttributes<"svg">, "width" | "height">;
|
|
10
|
+
|
|
11
|
+
const props = Astro.props as Props;
|
|
12
|
+
// Unique ids per instance, so several illustrations on one page don't share filters.
|
|
13
|
+
const uid = `es-no-results-${Math.random().toString(36).slice(2, 8)}`;
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<svg
|
|
17
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
18
|
+
width="280"
|
|
19
|
+
height="200"
|
|
20
|
+
viewBox="-20 -20 280 200"
|
|
21
|
+
fill="none"
|
|
22
|
+
aria-hidden="true"
|
|
23
|
+
focusable="false"
|
|
24
|
+
{...props}
|
|
25
|
+
>
|
|
26
|
+
<defs>
|
|
27
|
+
<filter id={`${uid}-sBack`} x="-20%" y="-30%" width="140%" height="160%" color-interpolation-filters="sRGB">
|
|
28
|
+
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="#051729" flood-opacity="0.05"></feDropShadow>
|
|
29
|
+
</filter>
|
|
30
|
+
<filter id={`${uid}-sFront`} x="-30%" y="-40%" width="160%" height="190%" color-interpolation-filters="sRGB">
|
|
31
|
+
<feDropShadow dx="0" dy="8" stdDeviation="10" flood-color="#051729" flood-opacity="0.10"></feDropShadow>
|
|
32
|
+
</filter>
|
|
33
|
+
<filter id={`${uid}-sLens`} x="-60%" y="-60%" width="220%" height="220%" color-interpolation-filters="sRGB">
|
|
34
|
+
<feDropShadow dx="0" dy="6" stdDeviation="6" flood-opacity="0.28" style="flood-color:var(--primary)"></feDropShadow>
|
|
35
|
+
</filter>
|
|
36
|
+
</defs>
|
|
37
|
+
<circle cx="120" cy="80" r="69" stroke-width="20" style="stroke:color-mix(in oklab, var(--primary-50) 50%, var(--background))"></circle>
|
|
38
|
+
<circle cx="120" cy="80" r="40" stroke-width="16" style="stroke:var(--primary-50)"></circle>
|
|
39
|
+
<g transform="rotate(-9 94 70)" filter={`url(#${uid}-sBack)`}>
|
|
40
|
+
<rect x="34" y="34" width="120" height="72" rx="10" style="fill:var(--background)"></rect>
|
|
41
|
+
<rect x="34.5" y="34.5" width="119" height="71" rx="9.5" stroke-opacity="0.06" style="stroke:var(--foreground)"></rect>
|
|
42
|
+
</g>
|
|
43
|
+
<g transform="rotate(8 152 66)" filter={`url(#${uid}-sBack)`}>
|
|
44
|
+
<rect x="92" y="30" width="120" height="72" rx="10" style="fill:var(--background)"></rect>
|
|
45
|
+
<rect x="92.5" y="30.5" width="119" height="71" rx="9.5" stroke-opacity="0.06" style="stroke:var(--foreground)"></rect>
|
|
46
|
+
</g>
|
|
47
|
+
<g filter={`url(#${uid}-sFront)`}>
|
|
48
|
+
<rect x="50" y="52" width="140" height="84" rx="12" style="fill:var(--background)"></rect>
|
|
49
|
+
</g>
|
|
50
|
+
<rect x="50.5" y="52.5" width="139" height="83" rx="11.5" stroke-opacity="0.06" style="stroke:var(--foreground)"></rect>
|
|
51
|
+
<rect x="64.75" y="66.75" width="26.5" height="26.5" rx="7.25" stroke-width="1.5" stroke-dasharray="4 3" style="stroke:var(--color-zinc-300, #d4d4d8)"></rect>
|
|
52
|
+
<rect x="64" y="106" width="76" height="7" rx="3.5" style="fill:var(--border)"></rect>
|
|
53
|
+
<rect x="64" y="119" width="48" height="7" rx="3.5" style="fill:var(--muted)"></rect>
|
|
54
|
+
<g filter={`url(#${uid}-sLens)`}>
|
|
55
|
+
<line x1="183" y1="127" x2="197" y2="141" stroke-width="9" stroke-linecap="round" style="stroke:var(--primary-700)"></line>
|
|
56
|
+
<circle cx="168" cy="112" r="20" stroke-width="6" style="fill:var(--background);stroke:var(--primary)"></circle>
|
|
57
|
+
</g>
|
|
58
|
+
<line x1="162" y1="106" x2="174" y2="118" stroke-width="3" stroke-linecap="round" style="stroke:var(--primary-300)"></line>
|
|
59
|
+
<line x1="174" y1="106" x2="162" y2="118" stroke-width="3" stroke-linecap="round" style="stroke:var(--primary-300)"></line>
|
|
60
|
+
</svg>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Illustration "nothing-matches" (184×124, with its own padding inside the viewBox).
|
|
4
|
+
* The designer's artwork; colours mapped to design tokens so it follows the theme.
|
|
5
|
+
* Decorative. Generated from the workspace selection handover assets (N15).
|
|
6
|
+
*/
|
|
7
|
+
import type { HTMLAttributes } from "astro/types";
|
|
8
|
+
|
|
9
|
+
type Props = Omit<HTMLAttributes<"svg">, "width" | "height">;
|
|
10
|
+
|
|
11
|
+
const props = Astro.props as Props;
|
|
12
|
+
// Unique ids per instance, so several illustrations on one page don't share filters.
|
|
13
|
+
const uid = `es-nothing-matches-${Math.random().toString(36).slice(2, 8)}`;
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<svg
|
|
17
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
18
|
+
width="184"
|
|
19
|
+
height="124"
|
|
20
|
+
viewBox="-12 -12 184 124"
|
|
21
|
+
fill="none"
|
|
22
|
+
aria-hidden="true"
|
|
23
|
+
focusable="false"
|
|
24
|
+
{...props}
|
|
25
|
+
>
|
|
26
|
+
<defs>
|
|
27
|
+
<filter id={`${uid}-sBack`} x="-30%" y="-30%" width="160%" height="160%" color-interpolation-filters="sRGB">
|
|
28
|
+
<feDropShadow dx="0" dy="1.5" stdDeviation="2" flood-color="#051729" flood-opacity="0.05"></feDropShadow>
|
|
29
|
+
</filter>
|
|
30
|
+
<filter id={`${uid}-sFront`} x="-40%" y="-30%" width="180%" height="170%" color-interpolation-filters="sRGB">
|
|
31
|
+
<feDropShadow dx="0" dy="5" stdDeviation="6" flood-color="#051729" flood-opacity="0.10"></feDropShadow>
|
|
32
|
+
</filter>
|
|
33
|
+
<filter id={`${uid}-sLens`} x="-60%" y="-60%" width="220%" height="220%" color-interpolation-filters="sRGB">
|
|
34
|
+
<feDropShadow dx="0" dy="4" stdDeviation="4" flood-opacity="0.28" style="flood-color:var(--primary)"></feDropShadow>
|
|
35
|
+
</filter>
|
|
36
|
+
</defs>
|
|
37
|
+
<circle cx="80" cy="50" r="44" stroke-width="12" style="stroke:color-mix(in oklab, var(--primary-50) 50%, var(--background))"></circle>
|
|
38
|
+
<circle cx="80" cy="50" r="24" stroke-width="10" style="stroke:var(--primary-50)"></circle>
|
|
39
|
+
<g transform="rotate(-12 44 52)">
|
|
40
|
+
<g filter={`url(#${uid}-sBack)`}><rect x="22" y="26" width="44" height="52" rx="8" style="fill:var(--background)"></rect></g>
|
|
41
|
+
<rect x="22.5" y="26.5" width="43" height="51" rx="7.5" stroke-opacity="0.06" style="stroke:var(--foreground)"></rect>
|
|
42
|
+
<rect x="28" y="32" width="14" height="14" rx="4" style="fill:var(--primary-100)"></rect>
|
|
43
|
+
<rect x="28" y="54" width="24" height="4" rx="2" style="fill:var(--border)"></rect>
|
|
44
|
+
<rect x="28" y="62" width="16" height="4" rx="2" style="fill:var(--muted)"></rect>
|
|
45
|
+
</g>
|
|
46
|
+
<g transform="rotate(12 116 52)">
|
|
47
|
+
<g filter={`url(#${uid}-sBack)`}><rect x="94" y="26" width="44" height="52" rx="8" style="fill:var(--background)"></rect></g>
|
|
48
|
+
<rect x="94.5" y="26.5" width="43" height="51" rx="7.5" stroke-opacity="0.06" style="stroke:var(--foreground)"></rect>
|
|
49
|
+
<rect x="100" y="34" width="28" height="4" rx="2" style="fill:var(--border)"></rect>
|
|
50
|
+
<rect x="100" y="42" width="32" height="4" rx="2" style="fill:var(--muted)"></rect>
|
|
51
|
+
<rect x="100" y="50" width="22" height="4" rx="2" style="fill:var(--muted)"></rect>
|
|
52
|
+
<rect x="100" y="58" width="28" height="4" rx="2" style="fill:var(--muted)"></rect>
|
|
53
|
+
</g>
|
|
54
|
+
<g filter={`url(#${uid}-sFront)`}><rect x="56" y="18" width="48" height="58" rx="9" style="fill:var(--background)"></rect></g>
|
|
55
|
+
<rect x="56.5" y="18.5" width="47" height="57" rx="8.5" stroke-opacity="0.06" style="stroke:var(--foreground)"></rect>
|
|
56
|
+
<rect x="62" y="24" width="36" height="22" rx="5" style="fill:var(--primary-50)"></rect>
|
|
57
|
+
<path d="M77 30.5 L84.5 35 L77 39.5 Z" stroke-width="1.5" stroke-linejoin="round" style="fill:var(--primary-300);stroke:var(--primary-300)"></path>
|
|
58
|
+
<rect x="62" y="53" width="28" height="4" rx="2" style="fill:var(--border)"></rect>
|
|
59
|
+
<rect x="62" y="61" width="18" height="4" rx="2" style="fill:var(--muted)"></rect>
|
|
60
|
+
<g filter={`url(#${uid}-sLens)`}>
|
|
61
|
+
<line x1="113.5" y1="79.5" x2="122" y2="88" stroke-width="6" stroke-linecap="round" style="stroke:var(--primary-700)"></line>
|
|
62
|
+
<circle cx="104" cy="70" r="13" stroke-width="4" style="fill:var(--background);stroke:var(--primary)"></circle>
|
|
63
|
+
</g>
|
|
64
|
+
<line x1="100" y1="66" x2="108" y2="74" stroke-width="2.2" stroke-linecap="round" style="stroke:var(--primary-300)"></line>
|
|
65
|
+
<line x1="108" y1="66" x2="100" y2="74" stroke-width="2.2" stroke-linecap="round" style="stroke:var(--primary-300)"></line>
|
|
66
|
+
</svg>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import AllCaughtUp from "./AllCaughtUp.astro";
|
|
2
|
+
import EmptyWorkspaces from "./EmptyWorkspaces.astro";
|
|
3
|
+
import LoadError from "./LoadError.astro";
|
|
4
|
+
import NoResults from "./NoResults.astro";
|
|
5
|
+
import NothingMatches from "./NothingMatches.astro";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Built-in empty state illustrations. Each has its own padding inside the
|
|
9
|
+
* viewBox; `pull` is the negative vertical margin that takes it back out, so
|
|
10
|
+
* the drawing sits at the message's normal spacing.
|
|
11
|
+
*/
|
|
12
|
+
export const emptyStateIllustrations = {
|
|
13
|
+
/** Nothing created yet (280×200, page). */
|
|
14
|
+
empty: { component: EmptyWorkspaces, width: 280, height: 200, pull: "-my-6" },
|
|
15
|
+
/** A search found nothing (280×200, page). */
|
|
16
|
+
"no-results": { component: NoResults, width: 280, height: 200, pull: "-my-6" },
|
|
17
|
+
/** Something failed to load (280×200, page). */
|
|
18
|
+
error: { component: LoadError, width: 280, height: 200, pull: "-my-6" },
|
|
19
|
+
/** Nothing new, e.g. notifications (144×104, menu). */
|
|
20
|
+
"all-caught-up": { component: AllCaughtUp, width: 144, height: 104, pull: "-my-2" },
|
|
21
|
+
/** A search in a menu or command palette found nothing (184×124, menu). */
|
|
22
|
+
"nothing-matches": { component: NothingMatches, width: 184, height: 124, pull: "-my-3" },
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
export type EmptyStateIllustrationName = keyof typeof emptyStateIllustrations;
|
|
26
|
+
|
|
27
|
+
export const EMPTY_STATE_ILLUSTRATIONS = Object.keys(emptyStateIllustrations) as EmptyStateIllustrationName[];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `FavoriteToggle` component will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## Date 15th Oct 2026
|
|
6
|
+
## Workspace selection handover (N8)
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- `FavoriteToggle`: a `Toggle` with `variant="ghost"` and a Lucide `Star`. Pressed fills the star with the `favorite` token (was `badge-warning` in the screen, gap G3); no pressed background, the normal hover background on hover (C12).
|
|
11
|
+
- Props: `label` (the accessible name, required), `defaultPressed`, `disabled`, `size` (Toggle sizes, default `sm`, 32px), `class`, and any button attribute. State is `aria-pressed`; changes fire the Toggle runtime's `toggle:change`. `data-favorite-toggle` marks it for scripts.
|
|
12
|
+
- "Only on a favourite, or on hover or focus" is left to the consumer: documented as a `group/*` + `opacity-0 group-hover/*:opacity-100 group-focus-within/*:opacity-100 aria-pressed:opacity-100` recipe, so the star stays in the Tab order.
|
|
13
|
+
- Storybook: "Favorite Toggle" under Actions.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* @component FavoriteToggle
|
|
4
|
+
* @title Favorite Toggle
|
|
5
|
+
* @description A star that adds something to favourites or takes it out. A ghost `Toggle`: pressed shows as a star filled with the `favorite` colour, with no pressed background.
|
|
6
|
+
*
|
|
7
|
+
* @preview
|
|
8
|
+
*
|
|
9
|
+
* <div class="flex items-center gap-2">
|
|
10
|
+
* <FavoriteToggle label="Favourite Acme Logistics" />
|
|
11
|
+
* <FavoriteToggle label="Favourite Northwind" defaultPressed />
|
|
12
|
+
* </div>
|
|
13
|
+
*
|
|
14
|
+
* @usage
|
|
15
|
+
*
|
|
16
|
+
* ```astro nocollapse
|
|
17
|
+
* ---
|
|
18
|
+
* import { FavoriteToggle } from '@cl-light-ds/ui/components/favorite-toggle';
|
|
19
|
+
* ---
|
|
20
|
+
*
|
|
21
|
+
* <FavoriteToggle label="Favourite Acme Logistics" defaultPressed={item.starred} />
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* `label` names what is favourited ("Favourite Acme Logistics"); the state is
|
|
25
|
+
* announced through `aria-pressed`, so the label stays the same either way.
|
|
26
|
+
*
|
|
27
|
+
* ## Only when favourited, or on hover / focus
|
|
28
|
+
*
|
|
29
|
+
* Not built in. Put a `group/<name>` on the container and reveal the star with
|
|
30
|
+
* classes, so it is always there for keyboard and screen reader users:
|
|
31
|
+
*
|
|
32
|
+
* ```astro nocollapse
|
|
33
|
+
* <div class="group/card relative ...">
|
|
34
|
+
* <FavoriteToggle
|
|
35
|
+
* label="Favourite Acme Logistics"
|
|
36
|
+
* class="opacity-0 group-hover/card:opacity-100 group-focus-within/card:opacity-100 aria-pressed:opacity-100"
|
|
37
|
+
* />
|
|
38
|
+
* </div>
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* @api
|
|
42
|
+
*
|
|
43
|
+
* ### Events
|
|
44
|
+
*
|
|
45
|
+
* | Event | Detail | Description |
|
|
46
|
+
* |-------|--------|-------------|
|
|
47
|
+
* | `toggle:change` | `{ pressed: boolean }` | Fired by the Toggle runtime when the state changes |
|
|
48
|
+
*
|
|
49
|
+
* Set it from outside with `toggle:set` (`{ detail: { value: true } }`), as for `Toggle`.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
import type { HTMLAttributes } from "astro/types";
|
|
53
|
+
import { Star } from "@lucide/astro";
|
|
54
|
+
import { cn } from "@cl-light-ds/ui/lib/utils";
|
|
55
|
+
import type { ToggleSize } from "@cl-light-ds/ui/lib/toggle-shared";
|
|
56
|
+
import { Toggle } from "../toggle";
|
|
57
|
+
|
|
58
|
+
interface Props extends Omit<HTMLAttributes<"button">, "class"> {
|
|
59
|
+
/** Accessible name, e.g. "Favourite Acme Logistics". */
|
|
60
|
+
label: string;
|
|
61
|
+
/** Initially a favourite. */
|
|
62
|
+
defaultPressed?: boolean;
|
|
63
|
+
disabled?: boolean;
|
|
64
|
+
/** Toggle size; the star is 16px at every size. */
|
|
65
|
+
size?: ToggleSize;
|
|
66
|
+
class?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const {
|
|
70
|
+
label,
|
|
71
|
+
defaultPressed = false,
|
|
72
|
+
disabled = false,
|
|
73
|
+
size = "sm",
|
|
74
|
+
class: className = "",
|
|
75
|
+
...rest
|
|
76
|
+
} = Astro.props as Props;
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
<Toggle
|
|
80
|
+
{...rest}
|
|
81
|
+
variant="ghost"
|
|
82
|
+
size={size}
|
|
83
|
+
defaultPressed={defaultPressed}
|
|
84
|
+
disabled={disabled}
|
|
85
|
+
aria-label={label}
|
|
86
|
+
data-favorite-toggle
|
|
87
|
+
class={cn("transition-[color,box-shadow,opacity]", className)}
|
|
88
|
+
>
|
|
89
|
+
<Star
|
|
90
|
+
data-slot="favorite-toggle-icon"
|
|
91
|
+
class="size-4 group-aria-pressed/toggle:fill-favorite group-aria-pressed/toggle:text-favorite"
|
|
92
|
+
aria-hidden="true"
|
|
93
|
+
/>
|
|
94
|
+
</Toggle>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as FavoriteToggle } from "./FavoriteToggle.astro";
|
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the `Input` component will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 2026-10-06 — OTP Input (ClickLearn Lightning Design Library, node `7650-4177`)
|
|
6
|
+
|
|
7
|
+
Figma adds an "OTP Input" component set to the Input page with states Default, Focused, Filled, Disabled and Error: six joined 56×40 slots (outer corners `rounded-lg`, shared inner borders, 14px medium centered text), a label above and hint text below.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `InputOTP` (exported from `components/input`). Props: `length` (default 6), `name`, `value`, `size` (`sm` | `default` | `lg`; `default` is Figma's 56×40), `mode` (`numeric` | `alphanumeric`), `id` (applied to the first slot so `FieldLabel for` works), `disabled`, `required`, `autofocus`, `aria-invalid`.
|
|
12
|
+
- Joined value is submitted through a hidden input named by `name`.
|
|
13
|
+
- Inline script: auto-advance on type, Backspace to previous slot, arrow-key navigation, paste/OS autofill distributes characters across slots. Emits `input-otp:change` and `input-otp:complete` (`detail.value`).
|
|
14
|
+
|
|
15
|
+
### Notes
|
|
16
|
+
|
|
17
|
+
- States reuse Input's tokens exactly: `border-input`, `bg-card`, `shadow-xs`; Focus `border-ring ring-3 ring-ring/50`; Error `aria-invalid:border-destructive` (no outer ring, matching Figma); Disabled `bg-muted text-muted-foreground`. Hover not implemented (standing direction).
|
|
18
|
+
- Label and hint are composed with `Field` / `FieldLabel` / `FieldDescription` / `FieldError`, same as Input, rather than new props.
|
|
19
|
+
|
|
5
20
|
## 2026-09-09 — Figma alignment (ClickLearn Lightning Design Library, node `145-3`)
|
|
6
21
|
|
|
7
22
|
Figma documents Input across four pages: Variants (Default, Lead, Tail, Complex, Wide), Sizes (Default 32px, Large 40px), States (Default, Hover, Pressed, Focus, Disabled, Error), and Contents (Default, Lead Icon, Tail Icon, Label, Counter, Helper, Error text, All Details).
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* @component InputOTP
|
|
4
|
+
* @title OTP Input
|
|
5
|
+
* @description A row of single-character slots for entering one-time codes, with auto-advance, backspace and paste support.
|
|
6
|
+
* @figmaUrl https://www.figma.com/design/qXkzdqZ3QPQqgckU3AwIAH/ClickLearn-Lightning-Design-Library?node-id=7650-4177
|
|
7
|
+
* @status stable
|
|
8
|
+
* @since 0.4.0
|
|
9
|
+
*
|
|
10
|
+
* @preview
|
|
11
|
+
*
|
|
12
|
+
* <Field class="w-fit">
|
|
13
|
+
* <FieldLabel for="otp">Verification code</FieldLabel>
|
|
14
|
+
* <InputOTP id="otp" name="code" />
|
|
15
|
+
* <FieldDescription>Enter the 6-digit code we sent to your email.</FieldDescription>
|
|
16
|
+
* </Field>
|
|
17
|
+
*
|
|
18
|
+
* @usage
|
|
19
|
+
*
|
|
20
|
+
* ```astro nocollapse
|
|
21
|
+
* ---
|
|
22
|
+
* import { InputOTP } from '@cl-light-ds/ui/components/input';
|
|
23
|
+
* ---
|
|
24
|
+
* <InputOTP id="otp" name="code" length={6} />
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @api
|
|
28
|
+
*
|
|
29
|
+
* ### Events
|
|
30
|
+
*
|
|
31
|
+
* | Event | Detail | Description |
|
|
32
|
+
* |-------|--------|-------------|
|
|
33
|
+
* | `input-otp:change` | `{ value: string }` | Fired whenever the code changes |
|
|
34
|
+
* | `input-otp:complete` | `{ value: string }` | Fired when every slot is filled |
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { cn } from "@cl-light-ds/ui/lib/utils";
|
|
38
|
+
import type { HTMLAttributes } from "astro/types";
|
|
39
|
+
|
|
40
|
+
type InputOTPSize = "sm" | "default" | "lg";
|
|
41
|
+
type InputOTPMode = "numeric" | "alphanumeric";
|
|
42
|
+
|
|
43
|
+
interface Props extends Omit<HTMLAttributes<"div">, "id"> {
|
|
44
|
+
/** Number of slots. */
|
|
45
|
+
length?: number;
|
|
46
|
+
/** Name of the hidden input that carries the joined code on form submit. */
|
|
47
|
+
name?: string;
|
|
48
|
+
/** Initial value; characters fill slots from the left. */
|
|
49
|
+
value?: string;
|
|
50
|
+
size?: InputOTPSize;
|
|
51
|
+
/** `numeric` accepts digits only; `alphanumeric` accepts letters and digits. */
|
|
52
|
+
mode?: InputOTPMode;
|
|
53
|
+
/** Applied to the first slot so a `<label for>` / `FieldLabel` focuses it. */
|
|
54
|
+
id?: string;
|
|
55
|
+
disabled?: boolean;
|
|
56
|
+
required?: boolean;
|
|
57
|
+
autofocus?: boolean;
|
|
58
|
+
"aria-invalid"?: boolean | "true" | "false";
|
|
59
|
+
class?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const {
|
|
63
|
+
class: className = "",
|
|
64
|
+
length = 6,
|
|
65
|
+
name,
|
|
66
|
+
value = "",
|
|
67
|
+
size = "default",
|
|
68
|
+
mode = "numeric",
|
|
69
|
+
id,
|
|
70
|
+
disabled = false,
|
|
71
|
+
required = false,
|
|
72
|
+
autofocus = false,
|
|
73
|
+
"aria-invalid": ariaInvalid,
|
|
74
|
+
...rest
|
|
75
|
+
} = Astro.props as Props;
|
|
76
|
+
|
|
77
|
+
const allowed = mode === "alphanumeric" ? /[a-z0-9]/i : /\d/;
|
|
78
|
+
const chars = [...value].filter((char) => allowed.test(char)).slice(0, length);
|
|
79
|
+
const slots = Array.from({ length }, (_, index) => chars[index] ?? "");
|
|
80
|
+
const invalid = ariaInvalid === true || ariaInvalid === "true";
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
<div
|
|
84
|
+
{...rest}
|
|
85
|
+
role="group"
|
|
86
|
+
data-slot="input-otp"
|
|
87
|
+
data-size={size}
|
|
88
|
+
data-mode={mode}
|
|
89
|
+
data-length={length}
|
|
90
|
+
data-disabled={disabled ? "" : undefined}
|
|
91
|
+
data-invalid={invalid ? "" : undefined}
|
|
92
|
+
class={cn("flex w-fit items-center", className)}
|
|
93
|
+
>
|
|
94
|
+
{
|
|
95
|
+
slots.map((char, index) => (
|
|
96
|
+
<input
|
|
97
|
+
type="text"
|
|
98
|
+
id={index === 0 ? id : undefined}
|
|
99
|
+
data-slot="input-otp-slot"
|
|
100
|
+
data-size={size}
|
|
101
|
+
value={char}
|
|
102
|
+
inputmode={mode === "numeric" ? "numeric" : "text"}
|
|
103
|
+
autocomplete={index === 0 ? "one-time-code" : "off"}
|
|
104
|
+
autocapitalize="off"
|
|
105
|
+
spellcheck="false"
|
|
106
|
+
aria-label={`Character ${index + 1} of ${length}`}
|
|
107
|
+
aria-invalid={invalid ? "true" : undefined}
|
|
108
|
+
disabled={disabled}
|
|
109
|
+
autofocus={index === 0 && autofocus}
|
|
110
|
+
class={cn(
|
|
111
|
+
"relative -ml-px min-w-0 border border-input bg-card text-center font-medium text-foreground shadow-xs outline-none transition-[color,box-shadow] first:ml-0 first:rounded-l-lg last:rounded-r-lg",
|
|
112
|
+
"focus-visible:z-10 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:border-ring",
|
|
113
|
+
"aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 aria-invalid:focus-visible:ring-destructive/20",
|
|
114
|
+
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground",
|
|
115
|
+
"data-[size=sm]:h-8 data-[size=sm]:w-10 data-[size=sm]:text-xs",
|
|
116
|
+
"data-[size=default]:h-10 data-[size=default]:w-14 data-[size=default]:text-sm",
|
|
117
|
+
"data-[size=lg]:h-12 data-[size=lg]:w-16 data-[size=lg]:text-base",
|
|
118
|
+
)}
|
|
119
|
+
/>
|
|
120
|
+
))
|
|
121
|
+
}
|
|
122
|
+
<input
|
|
123
|
+
type="hidden"
|
|
124
|
+
data-slot="input-otp-value"
|
|
125
|
+
name={name}
|
|
126
|
+
value={chars.join("")}
|
|
127
|
+
required={required}
|
|
128
|
+
disabled={disabled}
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<script>
|
|
133
|
+
function initInputOTP(root: HTMLElement) {
|
|
134
|
+
if (root.dataset.initialized !== undefined) return;
|
|
135
|
+
root.dataset.initialized = "";
|
|
136
|
+
|
|
137
|
+
const slots = Array.from(root.querySelectorAll<HTMLInputElement>('[data-slot="input-otp-slot"]'));
|
|
138
|
+
const hidden = root.querySelector<HTMLInputElement>('[data-slot="input-otp-value"]');
|
|
139
|
+
const allowed = root.dataset.mode === "alphanumeric" ? /[a-z0-9]/i : /\d/;
|
|
140
|
+
const accept = (text: string) => [...text].filter((char) => allowed.test(char));
|
|
141
|
+
|
|
142
|
+
function sync() {
|
|
143
|
+
const value = slots.map((slot) => slot.value).join("");
|
|
144
|
+
if (hidden) hidden.value = value;
|
|
145
|
+
root.dispatchEvent(new CustomEvent("input-otp:change", { bubbles: true, detail: { value } }));
|
|
146
|
+
if (slots.every((slot) => slot.value)) {
|
|
147
|
+
root.dispatchEvent(new CustomEvent("input-otp:complete", { bubbles: true, detail: { value } }));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Writes characters into consecutive slots starting at `start`, then focuses the next empty one.
|
|
152
|
+
function fill(start: number, chars: string[]) {
|
|
153
|
+
let index = start;
|
|
154
|
+
for (const char of chars) {
|
|
155
|
+
const slot = slots[index];
|
|
156
|
+
if (!slot) break;
|
|
157
|
+
slot.value = char;
|
|
158
|
+
index++;
|
|
159
|
+
}
|
|
160
|
+
slots[Math.min(index, slots.length - 1)]?.focus();
|
|
161
|
+
sync();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
slots.forEach((slot, index) => {
|
|
165
|
+
slot.addEventListener("focus", () => slot.select());
|
|
166
|
+
|
|
167
|
+
let previousValue = slot.value;
|
|
168
|
+
slot.addEventListener("beforeinput", () => (previousValue = slot.value));
|
|
169
|
+
|
|
170
|
+
slot.addEventListener("input", (event) => {
|
|
171
|
+
const { data: typed, inputType } = event as InputEvent;
|
|
172
|
+
// Single keystroke: use only the typed character, so typing into a filled slot replaces it.
|
|
173
|
+
// Anything else (OS autofill, IME) carries the whole string in the value.
|
|
174
|
+
const chars = accept(typed && typed.length === 1 ? typed : slot.value);
|
|
175
|
+
if (chars.length) {
|
|
176
|
+
slot.value = "";
|
|
177
|
+
fill(index, chars);
|
|
178
|
+
} else {
|
|
179
|
+
// Rejected characters leave the slot as it was; deletions clear it.
|
|
180
|
+
slot.value = inputType.startsWith("insert") ? previousValue : "";
|
|
181
|
+
sync();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
slot.addEventListener("paste", (event) => {
|
|
186
|
+
event.preventDefault();
|
|
187
|
+
const chars = accept(event.clipboardData?.getData("text") ?? "");
|
|
188
|
+
if (chars.length) fill(index, chars);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
slot.addEventListener("keydown", (event) => {
|
|
192
|
+
const previous = slots[index - 1];
|
|
193
|
+
const next = slots[index + 1];
|
|
194
|
+
if (event.key === "Backspace" && !slot.value && previous) {
|
|
195
|
+
event.preventDefault();
|
|
196
|
+
previous.value = "";
|
|
197
|
+
previous.focus();
|
|
198
|
+
sync();
|
|
199
|
+
} else if (event.key === "ArrowLeft" && previous) {
|
|
200
|
+
event.preventDefault();
|
|
201
|
+
previous.focus();
|
|
202
|
+
} else if (event.key === "ArrowRight" && next) {
|
|
203
|
+
event.preventDefault();
|
|
204
|
+
next.focus();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
document.querySelectorAll<HTMLElement>('[data-slot="input-otp"]').forEach(initInputOTP);
|
|
211
|
+
</script>
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { default as Input } from "./Input.astro";
|
|
2
|
-
|
|
2
|
+
export { default as InputOTP } from "./InputOTP.astro";
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the `InputGroup` component family (`InputGroup`, `InputGroupAddon`, `InputGroupButton`, `InputGroupInput`, `InputGroupText`, `InputGroupTextarea`) are documented in this file.
|
|
4
4
|
|
|
5
|
+
## Date 15th Oct 2026
|
|
6
|
+
## Workspace selection handover (C5)
|
|
7
|
+
|
|
8
|
+
- An input directly after an `InputGroupAddon align="inline-start"` holding `InputGroupText` has no left padding, so a prefix such as `lightning.com/` runs straight into what's typed. Icon prefixes are unchanged. Needs the addon before the input in the markup.
|
|
9
|
+
- Storybook: "Text prefix" preview.
|
|
10
|
+
|
|
11
|
+
|
|
5
12
|
## 2026-09-10 — Figma alignment (ClickLearn Lightning Design Library, node `2282-6923`)
|
|
6
13
|
|
|
7
14
|
Figma documents a single "Default" composition with three example rows: a lead-icon search input with trailing supportive text, a plain input with a trailing icon, and a multi-line textarea with a toolbar (icon button + ghost text button + supportive text + separator + circular primary send button). No Sizes/States/Variants pages exist for this component in Figma (unlike `Input`) — scope is narrower accordingly.
|
|
@@ -99,6 +99,8 @@ const {
|
|
|
99
99
|
data-slot="input-group"
|
|
100
100
|
data-size={size}
|
|
101
101
|
class={cn(
|
|
102
|
+
// A text prefix ("…lightning.com/"): the input starts right after it, with no left padding.
|
|
103
|
+
"[&>[data-align=inline-start]:has(>[data-slot=input-group-text])+[data-slot=input-group-control]]:pl-0!",
|
|
102
104
|
"group/input-group border-input bg-card has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 data-[size=sm]:h-8 data-[size=default]:h-9 data-[size=lg]:h-10 rounded-lg border shadow-xs transition-[color,box-shadow] in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-3 has-[>[data-align=block-end]]:h-auto! has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto! has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5 relative flex w-full min-w-0 items-center outline-none has-[>textarea]:h-auto!",
|
|
103
105
|
className,
|
|
104
106
|
)}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `LoadingOverlay` component (and its controller in `@cl-light-ds/ui/lib/loading-overlay`) are documented in this file.
|
|
4
|
+
|
|
5
|
+
## Date 15th Oct 2026
|
|
6
|
+
## Workspace selection handover (N14)
|
|
7
|
+
|
|
8
|
+
New component, replacing the workspace-selection screen's hand-composed `OpeningOverlay.astro` and its `runOverlay` / `revealWorkspace` / `hideOverlay` code.
|
|
9
|
+
|
|
10
|
+
### `LoadingOverlay`
|
|
11
|
+
|
|
12
|
+
- The whole screen dimmed and blurred (`bg-foreground/60`, `backdrop-blur-sm`), a 48px `Spinner`, a 30px title (`font-heading text-3xl font-semibold`) and a status line (`text-base`, `background/80`) (D117, D120).
|
|
13
|
+
- Props: `title`, `lines` (status lines), `interval` (ms between lines, default 900, D135), `loop` (cycle instead of stopping on the last line), `open` (render shown).
|
|
14
|
+
- `behind` slot: content shown under the dimming, faded in with `setLoadingOverlayBehind(true)` or `showLoadingOverlay({ behind: true | ms })` (D135, D191). Generalises the screen's workspace iframe.
|
|
15
|
+
- Reveal (D143): `revealLoadingOverlay()` fades the dimming (700ms) and the spinner and text (500ms) away, then hides; returns a Promise.
|
|
16
|
+
- A11y: `aria-busy="true"` while running; the title and status line are a polite live region (`role="status"`); the spinner is hidden from assistive tech (the text says it). On show, focus moves to the overlay (off whatever it covers) and returns on hide.
|
|
17
|
+
- `prefers-reduced-motion`: no fades on reveal or behind.
|
|
18
|
+
|
|
19
|
+
### Controller (`@cl-light-ds/ui/lib/loading-overlay`)
|
|
20
|
+
|
|
21
|
+
- `showLoadingOverlay`, `setLoadingOverlayBehind`, `revealLoadingOverlay`, `hideLoadingOverlay`, and the matching `loading-overlay:*` events on the element; fires `loading-overlay:change` (`{ open }`).
|
|
22
|
+
|
|
23
|
+
### Storybook
|
|
24
|
+
|
|
25
|
+
- "Loading Overlay" section: run it with changing lines and a reveal, with content behind, and pinned open in a frame.
|