@cl-light-ds/ui 0.3.0 → 0.4.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 +2 -2
- 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/input/CHANGELOG.md +15 -0
- package/src/components/input/InputOTP.astro +211 -0
- package/src/components/input/index.ts +1 -1
- 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/styles/globals.css +13 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cl-light-ds/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "ClickLearn Lightning Design System — Astro UI component library.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"exports": {
|
|
11
11
|
"./globals.css": "./src/styles/globals.css",
|
|
12
12
|
"./lib/*": "./src/lib/*.ts",
|
|
13
|
-
"./components/*": "./src/components
|
|
13
|
+
"./components/*": "./src/components/*/index.ts"
|
|
14
14
|
},
|
|
15
15
|
"publishConfig": {
|
|
16
16
|
"access": "public"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `Callout` component will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## 2026-10-06 — Initial implementation (ClickLearn Lightning Design Library, node `7650-5947`)
|
|
6
|
+
|
|
7
|
+
Figma documents Callout as a single component set (`7656:12714`) with one property, `Type = Default | Info | Warning | Danger`.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `Callout`, `CalloutTitle`, `CalloutDescription`.
|
|
12
|
+
- `variant: "default" | "info" | "warning" | "danger"` — maps 1:1 to Figma's `Type` (Figma's "Danger" kept as `danger`, its background token is `callout-background-error`).
|
|
13
|
+
- Default icon per variant, rendered in a 30px bordered white box, matching Figma: `Lightbulb` (foreground), `Info` (primary), `CircleAlert` (warning), `ShieldAlert` (destructive). Override with `icon={…}` or hide with `icon={false}`.
|
|
14
|
+
- `actions` named slot — right-aligned row under the copy. Figma's actions are small buttons, so pass `<Button size="sm">` (use `variant="destructive"` for the Danger primary action, as in Figma).
|
|
15
|
+
- `dismissible` (default `true`, Figma always shows the X). Visual only, same as `Alert`.
|
|
16
|
+
- `role="note"` — callouts are static, in-flow content, unlike `Alert`'s `role="alert"`.
|
|
17
|
+
|
|
18
|
+
### New tokens (`globals.css`)
|
|
19
|
+
|
|
20
|
+
| Token | Light | Dark | Figma variable |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| `--warning` / `bg-warning`, `text-warning` | `#eab308` | — | `badge-warning-backgground` |
|
|
23
|
+
| `--warning-foreground` | white | — | — |
|
|
24
|
+
| `--callout-background-info` / `bg-callout-info` | `var(--primary-50)` (`#ebf3fc`) | primary-500 15% on background | `callout-background-info` |
|
|
25
|
+
| `--callout-background-warning` / `bg-callout-warning` | `#fefce8` | `#eab308` 12% on background | `callout-background-warning` |
|
|
26
|
+
| `--callout-background-error` / `bg-callout-error` | `#fef2f2` | red-600 15% on background | `callout-background-error` |
|
|
27
|
+
|
|
28
|
+
### Notes
|
|
29
|
+
|
|
30
|
+
- Default variant uses `bg-popover` + `border-border`; tinted variants use a transparent border so all four keep identical dimensions.
|
|
31
|
+
- Radius is `rounded-md` (6px) and padding `p-3` per Figma's `radius/rounded-md` and `padding/p-3`.
|
|
32
|
+
- Dark values are not defined in Figma; they are tinted mixes chosen to keep contrast with `foreground`.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* @component Callout
|
|
4
|
+
* @title Callout
|
|
5
|
+
* @description An inline, in-flow message that highlights a tip, update, warning or problem, with optional actions.
|
|
6
|
+
* @figmaUrl https://www.figma.com/design/qXkzdqZ3QPQqgckU3AwIAH/ClickLearn-Lightning-Design-Library?node-id=7650-5947
|
|
7
|
+
* @status stable
|
|
8
|
+
* @since 0.4.0
|
|
9
|
+
*
|
|
10
|
+
* @preview
|
|
11
|
+
*
|
|
12
|
+
* <Callout variant="info" class="max-w-md">
|
|
13
|
+
* <CalloutTitle>Bookmark your organisation link</CalloutTitle>
|
|
14
|
+
* <CalloutDescription>Save your organisation URL to sign in directly next time.</CalloutDescription>
|
|
15
|
+
* <Fragment slot="actions">
|
|
16
|
+
* <Button size="sm">View updates</Button>
|
|
17
|
+
* <Button size="sm" variant="outline">Not now</Button>
|
|
18
|
+
* </Fragment>
|
|
19
|
+
* </Callout>
|
|
20
|
+
*
|
|
21
|
+
* @usage
|
|
22
|
+
*
|
|
23
|
+
* ```astro nocollapse
|
|
24
|
+
* ---
|
|
25
|
+
* import { Callout, CalloutTitle, CalloutDescription } from '@cl-light-ds/ui/components/callout';
|
|
26
|
+
* import { Button } from '@cl-light-ds/ui/components/button';
|
|
27
|
+
* ---
|
|
28
|
+
* <Callout variant="warning">
|
|
29
|
+
* <CalloutTitle>Your session is about to expire</CalloutTitle>
|
|
30
|
+
* <CalloutDescription>Save your work or extend the session.</CalloutDescription>
|
|
31
|
+
* <Fragment slot="actions">
|
|
32
|
+
* <Button size="sm">Extend session</Button>
|
|
33
|
+
* <Button size="sm" variant="outline">Save work</Button>
|
|
34
|
+
* </Fragment>
|
|
35
|
+
* </Callout>
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
import { cva } from "class-variance-authority";
|
|
39
|
+
import { CircleAlert, Info, Lightbulb, ShieldAlert, X as XIcon } from "@lucide/astro";
|
|
40
|
+
import type { AstroComponent } from "@lucide/astro";
|
|
41
|
+
import { cn } from "@cl-light-ds/ui/lib/utils";
|
|
42
|
+
import type { HTMLAttributes } from "astro/types";
|
|
43
|
+
|
|
44
|
+
type CalloutVariant = "default" | "info" | "warning" | "danger";
|
|
45
|
+
|
|
46
|
+
interface Props extends HTMLAttributes<"div"> {
|
|
47
|
+
variant?: CalloutVariant;
|
|
48
|
+
/** Icon component; defaults to the variant's icon. Pass `false` to hide it. */
|
|
49
|
+
icon?: AstroComponent | false;
|
|
50
|
+
dismissible?: boolean;
|
|
51
|
+
class?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const defaultIcons: Record<CalloutVariant, AstroComponent> = {
|
|
55
|
+
default: Lightbulb,
|
|
56
|
+
info: Info,
|
|
57
|
+
warning: CircleAlert,
|
|
58
|
+
danger: ShieldAlert,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const {
|
|
62
|
+
class: className = "",
|
|
63
|
+
variant = "default",
|
|
64
|
+
icon,
|
|
65
|
+
dismissible = true,
|
|
66
|
+
...rest
|
|
67
|
+
} = Astro.props as Props;
|
|
68
|
+
|
|
69
|
+
const Icon = icon === false ? undefined : (icon ?? defaultIcons[variant]);
|
|
70
|
+
const hasActions = Astro.slots.has("actions");
|
|
71
|
+
|
|
72
|
+
const calloutVariants = cva(
|
|
73
|
+
"group/callout relative flex w-full items-start gap-2 rounded-md border p-3",
|
|
74
|
+
{
|
|
75
|
+
variants: {
|
|
76
|
+
variant: {
|
|
77
|
+
default: "border-border bg-popover",
|
|
78
|
+
info: "border-transparent bg-callout-info",
|
|
79
|
+
warning: "border-transparent bg-callout-warning",
|
|
80
|
+
danger: "border-transparent bg-callout-error",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
defaultVariants: {
|
|
84
|
+
variant: "default",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const iconVariants = cva(
|
|
90
|
+
"flex size-7.5 shrink-0 items-center justify-center rounded-lg border border-border bg-background [&>svg]:size-[15px]",
|
|
91
|
+
{
|
|
92
|
+
variants: {
|
|
93
|
+
variant: {
|
|
94
|
+
default: "text-foreground",
|
|
95
|
+
info: "text-primary",
|
|
96
|
+
warning: "text-warning",
|
|
97
|
+
danger: "text-destructive",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
defaultVariants: {
|
|
101
|
+
variant: "default",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
<div
|
|
108
|
+
{...rest}
|
|
109
|
+
data-slot="callout"
|
|
110
|
+
data-variant={variant}
|
|
111
|
+
role="note"
|
|
112
|
+
class={cn(calloutVariants({ variant }), className)}
|
|
113
|
+
>
|
|
114
|
+
<div data-slot="callout-message" class="flex min-w-0 flex-1 items-start gap-3">
|
|
115
|
+
{
|
|
116
|
+
Icon && (
|
|
117
|
+
<div data-slot="callout-icon" class={iconVariants({ variant })}>
|
|
118
|
+
<Icon aria-hidden="true" />
|
|
119
|
+
</div>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
<div data-slot="callout-content" class="flex min-w-0 flex-1 flex-col gap-3 pt-0.5">
|
|
123
|
+
<div class="flex flex-col gap-1">
|
|
124
|
+
<slot />
|
|
125
|
+
</div>
|
|
126
|
+
{
|
|
127
|
+
hasActions && (
|
|
128
|
+
<div data-slot="callout-actions" class="flex w-full items-center justify-end gap-2">
|
|
129
|
+
<slot name="actions" />
|
|
130
|
+
</div>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
{
|
|
136
|
+
dismissible && (
|
|
137
|
+
<button
|
|
138
|
+
type="button"
|
|
139
|
+
data-slot="callout-dismiss"
|
|
140
|
+
class="flex shrink-0 items-center justify-center rounded-lg p-2 text-muted-foreground [&>svg]:size-4"
|
|
141
|
+
>
|
|
142
|
+
<XIcon aria-hidden="true" />
|
|
143
|
+
<span class="sr-only">Dismiss</span>
|
|
144
|
+
</button>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { cn } from "@cl-light-ds/ui/lib/utils";
|
|
3
|
+
import type { HTMLAttributes } from "astro/types";
|
|
4
|
+
|
|
5
|
+
interface Props extends HTMLAttributes<"div"> {}
|
|
6
|
+
|
|
7
|
+
const { class: className = "", ...rest } = Astro.props as Props;
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<div
|
|
11
|
+
{...rest}
|
|
12
|
+
data-slot="callout-description"
|
|
13
|
+
class={cn("text-sm leading-5 text-muted-foreground", className)}
|
|
14
|
+
>
|
|
15
|
+
<slot />
|
|
16
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { cn } from "@cl-light-ds/ui/lib/utils";
|
|
3
|
+
import type { HTMLAttributes } from "astro/types";
|
|
4
|
+
|
|
5
|
+
interface Props extends HTMLAttributes<"div"> {}
|
|
6
|
+
|
|
7
|
+
const { class: className = "", ...rest } = Astro.props as Props;
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<div
|
|
11
|
+
{...rest}
|
|
12
|
+
data-slot="callout-title"
|
|
13
|
+
class={cn("text-sm leading-5 font-semibold text-foreground", className)}
|
|
14
|
+
>
|
|
15
|
+
<slot />
|
|
16
|
+
</div>
|
|
@@ -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";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `Logo` component will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## 2026-10-06 — Initial implementation
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `Logo` — inline SVG ClickLearn logo. Props: `variant` (`full` | `mark` | `wordmark`), `tone` (`color` | `dark` | `white`), `size` (`sm` 24px | `default` 32px | `lg` 48px | `xl` 64px height, width auto), `title` (accessible name; `""` marks it decorative).
|
|
10
|
+
- `logoSvg(variant, tone)` — returns standalone SVG markup for the same artwork (used for the downloads on the Foundations → Logos page).
|
|
11
|
+
- `LOGO_COLORS` — mark `#3785E0` (`--primary-500`), wordmark `#112843` (`--primary-950`).
|
|
12
|
+
|
|
13
|
+
### Notes
|
|
14
|
+
|
|
15
|
+
- Artwork taken from the logo previously inlined in the storybook sidebar. The Figma "ClickLearn Logo" component set (library key `87ef4404…`) was not reachable through the Figma API at the time of writing; align variants/tones with it once a node link is available.
|
|
16
|
+
- Fills are fixed brand hex values (not theme tokens) so the logo stays on-brand regardless of theme; use `tone="white"` on dark or brand-coloured surfaces.
|
|
17
|
+
- No `clipPath`/`id`s are used, so multiple logos on a page never clash.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* @component Logo
|
|
4
|
+
* @title Logo
|
|
5
|
+
* @description The ClickLearn logo as an inline SVG: full lockup, mark only or wordmark only, in brand colour, dark or white.
|
|
6
|
+
* @status stable
|
|
7
|
+
* @since 0.4.0
|
|
8
|
+
*
|
|
9
|
+
* @preview
|
|
10
|
+
*
|
|
11
|
+
* <Logo />
|
|
12
|
+
*
|
|
13
|
+
* @usage
|
|
14
|
+
*
|
|
15
|
+
* ```astro nocollapse
|
|
16
|
+
* ---
|
|
17
|
+
* import { Logo } from '@cl-light-ds/ui/components/logo';
|
|
18
|
+
* ---
|
|
19
|
+
* <Logo variant="full" tone="color" size="default" />
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
import { cva } from "class-variance-authority";
|
|
23
|
+
import { cn } from "@cl-light-ds/ui/lib/utils";
|
|
24
|
+
import type { HTMLAttributes } from "astro/types";
|
|
25
|
+
import {
|
|
26
|
+
LOGO_VIEWBOX,
|
|
27
|
+
MARK_PATHS,
|
|
28
|
+
WORDMARK_PATHS,
|
|
29
|
+
logoFills,
|
|
30
|
+
type LogoTone,
|
|
31
|
+
type LogoVariant,
|
|
32
|
+
} from "./logo-paths";
|
|
33
|
+
|
|
34
|
+
type LogoSize = "sm" | "default" | "lg" | "xl";
|
|
35
|
+
|
|
36
|
+
interface Props extends HTMLAttributes<"svg"> {
|
|
37
|
+
variant?: LogoVariant;
|
|
38
|
+
tone?: LogoTone;
|
|
39
|
+
size?: LogoSize;
|
|
40
|
+
/** Accessible name. Pass an empty string when the logo is decorative (e.g. next to visible "ClickLearn" text). */
|
|
41
|
+
title?: string;
|
|
42
|
+
class?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const {
|
|
46
|
+
class: className = "",
|
|
47
|
+
variant = "full",
|
|
48
|
+
tone = "color",
|
|
49
|
+
size = "default",
|
|
50
|
+
title = "ClickLearn",
|
|
51
|
+
...rest
|
|
52
|
+
} = Astro.props as Props;
|
|
53
|
+
|
|
54
|
+
const logoVariants = cva("w-auto shrink-0", {
|
|
55
|
+
variants: {
|
|
56
|
+
size: {
|
|
57
|
+
sm: "h-6",
|
|
58
|
+
default: "h-8",
|
|
59
|
+
lg: "h-12",
|
|
60
|
+
xl: "h-16",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
defaultVariants: {
|
|
64
|
+
size: "default",
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const fills = logoFills(tone);
|
|
69
|
+
const showMark = variant !== "wordmark";
|
|
70
|
+
const showWordmark = variant !== "mark";
|
|
71
|
+
const decorative = title === "";
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
<svg
|
|
75
|
+
{...rest}
|
|
76
|
+
data-slot="logo"
|
|
77
|
+
data-variant={variant}
|
|
78
|
+
data-tone={tone}
|
|
79
|
+
viewBox={LOGO_VIEWBOX[variant]}
|
|
80
|
+
fill="none"
|
|
81
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
82
|
+
role={decorative ? undefined : "img"}
|
|
83
|
+
aria-label={decorative ? undefined : title}
|
|
84
|
+
aria-hidden={decorative ? "true" : undefined}
|
|
85
|
+
class={cn(logoVariants({ size }), className)}
|
|
86
|
+
>
|
|
87
|
+
{!decorative && <title>{title}</title>}
|
|
88
|
+
{showWordmark && WORDMARK_PATHS.map((d) => <path d={d} fill={fills.wordmark} />)}
|
|
89
|
+
{showMark && MARK_PATHS.map((d) => <path d={d} fill={fills.mark} />)}
|
|
90
|
+
</svg>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClickLearn logo artwork. Single source for the `Logo` component and for
|
|
3
|
+
* generating downloadable SVG files.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type LogoVariant = "full" | "mark" | "wordmark";
|
|
7
|
+
export type LogoTone = "color" | "dark" | "white";
|
|
8
|
+
|
|
9
|
+
/** Brand colours of the logo artwork (mark = primary-500, wordmark = primary-950). */
|
|
10
|
+
export const LOGO_COLORS = {
|
|
11
|
+
mark: "#3785E0",
|
|
12
|
+
wordmark: "#112843",
|
|
13
|
+
white: "#FFFFFF",
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
/** Paths for the speech-bubble mark, in the full lockup's coordinate space. */
|
|
17
|
+
export const MARK_PATHS = [
|
|
18
|
+
"M54.4204 27.2102C39.3935 27.2102 27.2102 39.3936 27.2102 54.4204C27.2102 69.4472 39.3935 81.6306 54.4204 81.6306C69.4472 81.6306 81.6306 69.4472 81.6306 54.4204C81.6306 39.3936 69.4472 27.2102 54.4204 27.2102ZM54.4204 68.0255C46.9172 68.0255 40.8153 61.9236 40.8153 54.4204C40.8153 46.9172 46.9172 40.8153 54.4204 40.8153C61.9236 40.8153 68.0255 46.9172 68.0255 54.4204C68.0255 61.9236 61.9236 68.0255 54.4204 68.0255Z",
|
|
19
|
+
"M54.4204 0C24.3633 0 0 24.3633 0 54.4204C0 79.8291 17.4104 101.169 40.9529 107.162C48.2346 109.015 54.4204 114.932 54.4204 122.446C77.8586 122.446 108.841 95.4465 108.841 54.4204C108.841 24.3633 84.4774 0 54.4204 0ZM54.4204 95.2357C31.9141 95.2357 13.6051 76.9266 13.6051 54.4204C13.6051 31.9142 31.9141 13.6051 54.4204 13.6051C76.9266 13.6051 95.2357 31.9142 95.2357 54.4204C95.2357 76.9266 76.9266 95.2357 54.4204 95.2357Z",
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
/** Paths for the "ClickLearn" wordmark, in the full lockup's coordinate space. */
|
|
23
|
+
export const WORDMARK_PATHS = [
|
|
24
|
+
"M136.051 54.4202C136.051 73.1851 151.22 88.433 170.002 88.433C176.325 88.433 182.192 86.8106 187.07 83.8311V69.7566C183.186 74.0864 177.227 76.7938 170.274 76.7938C157.812 76.7938 148.241 67.0492 148.241 54.4202C148.241 41.7913 157.812 32.0467 170.274 32.0467C177.227 32.0467 183.189 34.7541 187.07 39.0839V25.0094C182.192 22.0333 176.325 20.4075 170.002 20.4075C151.22 20.4075 136.051 35.6554 136.051 54.4202Z",
|
|
25
|
+
"M227.079 40.8152H215.175V87.0725H227.079V40.8152Z",
|
|
26
|
+
"M233.307 64.1139C233.307 78.0897 244.643 88.433 257.888 88.433C261.969 88.433 265.779 87.5248 268.68 85.8922V74.4572C265.867 76.7258 262.874 77.9945 258.612 77.9945C251.174 77.9945 244.735 72.2769 244.735 64.1105C244.735 55.944 251.174 50.3183 258.612 50.3183C262.874 50.3183 265.867 51.5904 268.68 53.7672V42.3321C265.779 40.6995 261.969 39.7914 257.888 39.7914C244.647 39.7914 233.307 50.138 233.307 64.1139Z",
|
|
27
|
+
"M384.334 39.7948C371.654 39.7948 360.514 50.2265 360.514 64.2057C360.514 78.1849 371.654 88.433 386.235 88.433C392.031 88.433 398.371 86.9806 402.99 83.8038V73.2769C399.003 76.5421 393.119 78.4502 387.684 78.4502C379.17 78.4502 373.011 74.3653 371.742 67.1988H406.34C406.432 66.1989 406.432 65.2907 406.432 64.3826C406.432 50.5904 399.007 39.7914 384.334 39.7914V39.7948ZM372.106 59.2126C373.735 53.223 378.354 49.4102 384.15 49.4102C390.946 49.4102 394.656 54.3148 395.201 59.2126H372.106Z",
|
|
28
|
+
"M193.872 87.0725H205.777V19.047C201.355 20.4075 198.634 20.7476 193.872 20.7476V87.0725Z",
|
|
29
|
+
"M317.614 40.8152H303.523L287.387 59.2603V19.047C282.965 20.4075 280.244 20.7476 275.483 20.7476V87.0725H287.387V66.09L304.155 87.0725H318.339L298.013 62.318L317.614 40.8152Z",
|
|
30
|
+
"M471.103 40.873H459.151V87.0725H471.056V64.5969C471.056 55.6311 476.447 51.9203 483.331 51.9203C484.236 51.9203 485.025 51.9475 486.021 52.0394V40.0873C478.879 39.4546 473.641 43.5906 471.103 48.0293V40.873Z",
|
|
31
|
+
"M517.004 39.7948C511.096 39.7948 506.279 42.5906 503.824 46.2912V40.1995H491.824V87.0725H503.728V60.6378C503.728 55.1345 507.184 50.5326 512.912 50.5326C518.64 50.5326 521.755 55.1345 521.755 60.5493V87.0759H534V57.393C534 47.1075 527.728 39.7982 517.004 39.7982V39.7948Z",
|
|
32
|
+
"M430.645 39.7948C424.584 39.7948 418.55 41.5396 414.931 43.5362V54.0801C418.642 51.8999 423.224 50.138 428.111 50.138C435.081 50.138 439.063 53.7706 439.063 58.4882V61.6888C437.162 59.6922 432.819 57.8521 427.57 57.8521C417.884 57.8521 409.829 63.9336 409.829 72.9164C409.829 82.4467 417.884 88.433 427.207 88.433C432.999 88.433 437.363 86.1916 439.08 84.0113V87.0725H450.645V57.9406C450.645 46.5973 443.043 39.7948 430.645 39.7948ZM429.921 79.5387C425.217 79.5387 420.782 77.1782 420.782 72.9164C420.782 68.6546 425.217 66.1989 429.921 66.1989C434.625 66.1989 439.244 68.6478 439.244 72.9164C439.244 77.185 434.536 79.5387 429.921 79.5387Z",
|
|
33
|
+
"M221.083 33.2065C224.84 33.2065 227.885 30.1609 227.885 26.404C227.885 22.647 224.84 19.6014 221.083 19.6014C217.326 19.6014 214.28 22.647 214.28 26.404C214.28 30.1609 217.326 33.2065 221.083 33.2065Z",
|
|
34
|
+
"M333.984 22.4483C329.563 23.8088 326.842 24.1489 322.08 24.1489V87.0725H359.494V76.8687H333.984V22.4483Z",
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
/** viewBox per variant — crops the shared coordinate space. */
|
|
38
|
+
export const LOGO_VIEWBOX: Record<LogoVariant, string> = {
|
|
39
|
+
full: "0 0 534 122.446",
|
|
40
|
+
mark: "0 0 108.841 122.446",
|
|
41
|
+
wordmark: "136.051 19.047 397.949 69.386",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function logoFills(tone: LogoTone) {
|
|
45
|
+
if (tone === "white") return { mark: LOGO_COLORS.white, wordmark: LOGO_COLORS.white };
|
|
46
|
+
if (tone === "dark") return { mark: LOGO_COLORS.wordmark, wordmark: LOGO_COLORS.wordmark };
|
|
47
|
+
return { mark: LOGO_COLORS.mark, wordmark: LOGO_COLORS.wordmark };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Standalone SVG markup, e.g. for download links. */
|
|
51
|
+
export function logoSvg(variant: LogoVariant, tone: LogoTone = "color"): string {
|
|
52
|
+
const fills = logoFills(tone);
|
|
53
|
+
const [, , width, height] = LOGO_VIEWBOX[variant].split(" ").map(Number);
|
|
54
|
+
const mark = variant === "wordmark" ? [] : MARK_PATHS.map((d) => `<path d="${d}" fill="${fills.mark}"/>`);
|
|
55
|
+
const word = variant === "mark" ? [] : WORDMARK_PATHS.map((d) => `<path d="${d}" fill="${fills.wordmark}"/>`);
|
|
56
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${LOGO_VIEWBOX[variant]}" width="${width}" height="${height}" fill="none"><title>ClickLearn</title>${[...word, ...mark].join("")}</svg>`;
|
|
57
|
+
}
|
package/src/styles/globals.css
CHANGED
|
@@ -37,6 +37,11 @@
|
|
|
37
37
|
--color-destructive-foreground: var(--destructive-foreground);
|
|
38
38
|
--color-success: var(--success);
|
|
39
39
|
--color-success-foreground: var(--success-foreground);
|
|
40
|
+
--color-warning: var(--warning);
|
|
41
|
+
--color-warning-foreground: var(--warning-foreground);
|
|
42
|
+
--color-callout-info: var(--callout-background-info);
|
|
43
|
+
--color-callout-warning: var(--callout-background-warning);
|
|
44
|
+
--color-callout-error: var(--callout-background-error);
|
|
40
45
|
--color-badge-success: var(--badge-success);
|
|
41
46
|
--color-badge-success-foreground: var(--badge-success-foreground);
|
|
42
47
|
--color-badge-warning: var(--badge-warning);
|
|
@@ -104,6 +109,11 @@
|
|
|
104
109
|
--destructive-foreground: var(--color-white);
|
|
105
110
|
--success: var(--color-green-600);
|
|
106
111
|
--success-foreground: var(--color-white);
|
|
112
|
+
--warning: #eab308;
|
|
113
|
+
--warning-foreground: var(--color-white);
|
|
114
|
+
--callout-background-info: var(--primary-50);
|
|
115
|
+
--callout-background-warning: #fefce8;
|
|
116
|
+
--callout-background-error: #fef2f2;
|
|
107
117
|
--badge-success: var(--color-green-700);
|
|
108
118
|
--badge-success-foreground: var(--color-white);
|
|
109
119
|
--badge-warning: var(--color-yellow-500);
|
|
@@ -152,7 +162,6 @@
|
|
|
152
162
|
--card-foreground: oklch(0.985 0 0);
|
|
153
163
|
--popover: oklch(0.205 0 0);
|
|
154
164
|
--popover-foreground: oklch(0.985 0 0);
|
|
155
|
-
--primary: var(--primary);
|
|
156
165
|
--primary-foreground: oklch(0.97 0.014 264.76);
|
|
157
166
|
--secondary: oklch(0.274 0.006 286.033);
|
|
158
167
|
--secondary-foreground: oklch(0.985 0 0);
|
|
@@ -161,6 +170,9 @@
|
|
|
161
170
|
--accent: oklch(0.371 0 0);
|
|
162
171
|
--accent-foreground: oklch(0.985 0 0);
|
|
163
172
|
--destructive: oklch(0.704 0.191 22.216);
|
|
173
|
+
--callout-background-info: color-mix(in oklab, var(--primary-500) 15%, var(--background));
|
|
174
|
+
--callout-background-warning: color-mix(in oklab, #eab308 12%, var(--background));
|
|
175
|
+
--callout-background-error: color-mix(in oklab, var(--color-red-600) 15%, var(--background));
|
|
164
176
|
--border: oklch(1 0 0 / 10%);
|
|
165
177
|
--input: oklch(1 0 0 / 15%);
|
|
166
178
|
--ring: oklch(0.556 0 0);
|