initial commit
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
/* ============================================================================
|
||||
ARROW ICON - figma `arrow-small-right` 84:4260 / `arrow-small-lift` 140:8078
|
||||
----------------------------------------------------------------------------
|
||||
The exported asset is at /assets/icons/arrow-small-right.svg. Its path is
|
||||
filled with a hard-coded black, so it is applied as a mask over
|
||||
`currentColor` instead of being dropped in an <img>: that is the only way the
|
||||
arrow can turn white on a Primary button, dark grey on a Secondary one and
|
||||
green on a Ghost one while remaining the exact exported bytes.
|
||||
|
||||
GEOMETRY, straight from Figma: a 24x24 clipped box (`--control-icon-size`)
|
||||
with the 12.9996 x 11.414 leaf inset 20.8333% from the left, 25% from the
|
||||
right and 26.22% top and bottom. The insets are physical on purpose - the
|
||||
glyph is asymmetric, which app.css names as the one case that cannot be
|
||||
expressed logically, so mirroring is done by flipping the whole box.
|
||||
|
||||
DIRECTION. Figma's left arrow is not a second drawing: node 140:8078 is this
|
||||
same asset with `-scale-x-100`. So `forward` (the arrow that follows reading
|
||||
order) is the exported right arrow in LTR and that same box mirrored in RTL,
|
||||
which is exactly what the Arabic home 85:5029 shows in its hero CTA.
|
||||
========================================================================= */
|
||||
|
||||
@layer theme, base, components, utilities;
|
||||
|
||||
@layer components {
|
||||
.sh-arrow-icon {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
flex: none;
|
||||
inline-size: var(--control-icon-size);
|
||||
block-size: var(--control-icon-size);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sh-arrow-icon__glyph {
|
||||
position: absolute;
|
||||
top: 26.22%;
|
||||
bottom: 26.22%;
|
||||
left: 20.8333%;
|
||||
right: 25%;
|
||||
background-color: currentColor;
|
||||
-webkit-mask-image: url("/assets/icons/arrow-small-right.svg");
|
||||
mask-image: url("/assets/icons/arrow-small-right.svg");
|
||||
-webkit-mask-size: 100% 100%;
|
||||
mask-size: 100% 100%;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
/* Mirror the box, not the glyph, so the 1px asymmetry of the inset mirrors
|
||||
with it - this reproduces node 140:8078 rather than approximating it. */
|
||||
.sh-arrow-icon[data-direction="backward"] {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
[dir="rtl"] .sh-arrow-icon[data-direction="forward"] {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
[dir="rtl"] .sh-arrow-icon[data-direction="backward"] {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import "./ArrowIcon.css";
|
||||
|
||||
export type ArrowIconDirection = "forward" | "backward";
|
||||
|
||||
export type ArrowIconProps = {
|
||||
/**
|
||||
* `forward` follows reading order - it points right in LTR and left in RTL,
|
||||
* which is what the Arabic home hero does. `backward` is the opposite, for
|
||||
* "previous" affordances such as pagination.
|
||||
*/
|
||||
direction?: ArrowIconDirection;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The arrow that rides along with the CTA label ("Discover SHARP-ER ->").
|
||||
*
|
||||
* Purely decorative: the label next to it carries the meaning, so the element
|
||||
* is hidden from assistive technology. It paints in `currentColor`, so it takes
|
||||
* the colour of whatever button or link it sits in with no props.
|
||||
*/
|
||||
export function ArrowIcon({ direction = "forward", className }: ArrowIconProps) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
data-direction={direction}
|
||||
className={className ? `sh-arrow-icon ${className}` : "sh-arrow-icon"}
|
||||
>
|
||||
<span className="sh-arrow-icon__glyph" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
/* ============================================================================
|
||||
BUTTON - figma frame 14:487 (Variant x Size x State)
|
||||
----------------------------------------------------------------------------
|
||||
Only TIER-2 semantic tokens are referenced here. The `--sh-btn-*` custom
|
||||
properties are component-local plumbing: each variant block binds them to a
|
||||
tier-2 token once, and the state rules below read them, so the whole 5x3x3
|
||||
matrix is expressed as five small blocks instead of forty-five selectors.
|
||||
|
||||
Everything lives in @layer components so a Tailwind utility passed through
|
||||
`className` (`w-full`, `mt-lg`, `text-text-secondary`, ...) still wins.
|
||||
The layer statement is a defensive no-op when app.css has already declared
|
||||
the same order, and makes this file correct on its own if it ever loads first.
|
||||
|
||||
HEIGHTS. Figma draws the stroke *inside* the frame, so a Secondary button is
|
||||
the same height as a Primary one. Borders are therefore subtracted from the
|
||||
padding, which lands every size on its designed box:
|
||||
sm 6.5*2 + 20 + 1.5*2 = 36 md 10.5*2 + 24 + 1.5*2 = 48
|
||||
lg 10.5*2 + 28 + 1.5*2 = 52
|
||||
The 20/24/28 line-heights come from the type role the component puts on the
|
||||
element (body-sm-bold / body-md-bold / body-lg-bold).
|
||||
|
||||
DEVIATIONS FROM FIGMA, deliberate:
|
||||
* Focus-visible is not drawn in Figma at all. WCAG 2.4.7 requires it, so the
|
||||
semantic focus ring is applied here; filled variants switch to the inverse
|
||||
ring colour because a green ring on a green fill is invisible.
|
||||
* "Ghost / Large / Default" is the one cell in the matrix with no padding
|
||||
while its own Hover and Disabled cells have it - an authoring slip that
|
||||
would make the button jump on hover. Padding is kept on all ghost states.
|
||||
* Disabled uses the opaque `-bg-disabled` / `-fg-disabled` tokens rather than
|
||||
Figma's 40% opacity, so the page never shows through a disabled control.
|
||||
`--control-opacity-disabled` still holds the 0.4 if that is ever wanted.
|
||||
========================================================================= */
|
||||
|
||||
@layer theme, base, components, utilities;
|
||||
|
||||
@layer components {
|
||||
/* ---------------------------------------------------------------------
|
||||
Base - shared by every variant and size.
|
||||
A one-cell grid stacks the label and the loading spinner without either
|
||||
transforms or absolute offsets, so it stays correct under dir="rtl".
|
||||
------------------------------------------------------------------ */
|
||||
.sh-btn {
|
||||
display: inline-grid;
|
||||
grid-template-areas: "sh-btn-stack";
|
||||
place-items: center;
|
||||
border-style: solid;
|
||||
border-width: var(--control-border-width);
|
||||
border-radius: var(--radius-control);
|
||||
background-color: var(--sh-btn-bg);
|
||||
color: var(--sh-btn-fg);
|
||||
border-color: var(--sh-btn-border);
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
transition:
|
||||
background-color var(--motion-duration-fast) var(--motion-ease-standard),
|
||||
border-color var(--motion-duration-fast) var(--motion-ease-standard),
|
||||
color var(--motion-duration-fast) var(--motion-ease-standard);
|
||||
}
|
||||
|
||||
.sh-btn--block {
|
||||
display: grid;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.sh-btn__content {
|
||||
grid-area: sh-btn-stack;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--control-gap);
|
||||
transition: opacity var(--motion-duration-fast) var(--motion-ease-standard);
|
||||
}
|
||||
|
||||
/* The 24px box the icon is drawn inside in Figma. Kept on the slot rather
|
||||
than on the icon so a consumer's own glyph inherits the same geometry. */
|
||||
.sh-btn__icon {
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
inline-size: var(--control-icon-size);
|
||||
block-size: var(--control-icon-size);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
Sizes - Figma: sm 16/8 (space.md/space.xs), md + lg 24/12 (space.lg/sm)
|
||||
------------------------------------------------------------------ */
|
||||
.sh-btn[data-size="sm"] {
|
||||
padding-inline: calc(var(--control-padding-inline-sm) - var(--control-border-width));
|
||||
padding-block: calc(var(--control-padding-block-sm) - var(--control-border-width));
|
||||
}
|
||||
|
||||
.sh-btn[data-size="md"] {
|
||||
padding-inline: calc(var(--control-padding-inline-md) - var(--control-border-width));
|
||||
padding-block: calc(var(--control-padding-block-md) - var(--control-border-width));
|
||||
}
|
||||
|
||||
.sh-btn[data-size="lg"] {
|
||||
padding-inline: calc(var(--control-padding-inline-lg) - var(--control-border-width));
|
||||
padding-block: calc(var(--control-padding-block-lg) - var(--control-border-width));
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
Variants - the only place a token name appears twice.
|
||||
------------------------------------------------------------------ */
|
||||
.sh-btn[data-variant="primary"] {
|
||||
--sh-btn-bg: var(--interactive-primary-bg);
|
||||
--sh-btn-bg-hover: var(--interactive-primary-bg-hover);
|
||||
--sh-btn-bg-active: var(--interactive-primary-bg-active);
|
||||
--sh-btn-bg-disabled: var(--interactive-primary-bg-disabled);
|
||||
--sh-btn-fg: var(--interactive-primary-fg);
|
||||
--sh-btn-fg-hover: var(--interactive-primary-fg-hover);
|
||||
--sh-btn-fg-disabled: var(--interactive-primary-fg-disabled);
|
||||
--sh-btn-border: var(--interactive-primary-border);
|
||||
--sh-btn-border-hover: var(--interactive-primary-border-hover);
|
||||
--sh-btn-border-disabled: var(--interactive-primary-border-disabled);
|
||||
}
|
||||
|
||||
.sh-btn[data-variant="secondary"] {
|
||||
--sh-btn-bg: var(--interactive-secondary-bg);
|
||||
--sh-btn-bg-hover: var(--interactive-secondary-bg-hover);
|
||||
--sh-btn-bg-active: var(--interactive-secondary-bg-active);
|
||||
--sh-btn-bg-disabled: var(--interactive-secondary-bg-disabled);
|
||||
--sh-btn-fg: var(--interactive-secondary-fg);
|
||||
--sh-btn-fg-hover: var(--interactive-secondary-fg-hover);
|
||||
--sh-btn-fg-disabled: var(--interactive-secondary-fg-disabled);
|
||||
--sh-btn-border: var(--interactive-secondary-border);
|
||||
--sh-btn-border-hover: var(--interactive-secondary-border-hover);
|
||||
--sh-btn-border-disabled: var(--interactive-secondary-border-disabled);
|
||||
}
|
||||
|
||||
.sh-btn[data-variant="dark"] {
|
||||
--sh-btn-bg: var(--interactive-dark-bg);
|
||||
--sh-btn-bg-hover: var(--interactive-dark-bg-hover);
|
||||
--sh-btn-bg-active: var(--interactive-dark-bg-active);
|
||||
--sh-btn-bg-disabled: var(--interactive-dark-bg-disabled);
|
||||
--sh-btn-fg: var(--interactive-dark-fg);
|
||||
--sh-btn-fg-hover: var(--interactive-dark-fg-hover);
|
||||
--sh-btn-fg-disabled: var(--interactive-dark-fg-disabled);
|
||||
--sh-btn-border: var(--interactive-dark-border);
|
||||
--sh-btn-border-hover: var(--interactive-dark-border-hover);
|
||||
--sh-btn-border-disabled: var(--interactive-dark-border-disabled);
|
||||
}
|
||||
|
||||
.sh-btn[data-variant="danger"] {
|
||||
--sh-btn-bg: var(--interactive-danger-bg);
|
||||
--sh-btn-bg-hover: var(--interactive-danger-bg-hover);
|
||||
--sh-btn-bg-active: var(--interactive-danger-bg-active);
|
||||
--sh-btn-bg-disabled: var(--interactive-danger-bg-disabled);
|
||||
--sh-btn-fg: var(--interactive-danger-fg);
|
||||
--sh-btn-fg-hover: var(--interactive-danger-fg-hover);
|
||||
--sh-btn-fg-disabled: var(--interactive-danger-fg-disabled);
|
||||
--sh-btn-border: var(--interactive-danger-border);
|
||||
--sh-btn-border-hover: var(--interactive-danger-border-hover);
|
||||
--sh-btn-border-disabled: var(--interactive-danger-border-disabled);
|
||||
}
|
||||
|
||||
.sh-btn[data-variant="ghost"] {
|
||||
--sh-btn-bg: var(--interactive-ghost-bg);
|
||||
--sh-btn-bg-hover: var(--interactive-ghost-bg-hover);
|
||||
--sh-btn-bg-active: var(--interactive-ghost-bg-active);
|
||||
--sh-btn-bg-disabled: var(--interactive-ghost-bg-disabled);
|
||||
--sh-btn-fg: var(--interactive-ghost-fg);
|
||||
--sh-btn-fg-hover: var(--interactive-ghost-fg-hover);
|
||||
--sh-btn-fg-disabled: var(--interactive-ghost-fg-disabled);
|
||||
--sh-btn-border: var(--interactive-ghost-border);
|
||||
--sh-btn-border-hover: var(--interactive-ghost-border-hover);
|
||||
--sh-btn-border-disabled: var(--interactive-ghost-border-disabled);
|
||||
}
|
||||
|
||||
/* Figma gives the ghost md/lg cells radius.md while every other cell uses
|
||||
radius.sm - reproduced rather than normalised. */
|
||||
.sh-btn[data-variant="ghost"][data-size="md"],
|
||||
.sh-btn[data-variant="ghost"][data-size="lg"] {
|
||||
border-radius: var(--radius-control-lg);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
States
|
||||
------------------------------------------------------------------ */
|
||||
@media (hover: hover) {
|
||||
.sh-btn:hover:not(:disabled, [aria-disabled="true"]) {
|
||||
background-color: var(--sh-btn-bg-hover);
|
||||
color: var(--sh-btn-fg-hover);
|
||||
border-color: var(--sh-btn-border-hover);
|
||||
}
|
||||
}
|
||||
|
||||
/* No `-fg-active` / `-border-active` exists in the token contract, so the
|
||||
pressed state deepens the fill only. */
|
||||
.sh-btn:active:not(:disabled, [aria-disabled="true"]) {
|
||||
background-color: var(--sh-btn-bg-active);
|
||||
}
|
||||
|
||||
.sh-btn:focus-visible {
|
||||
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
||||
outline-offset: var(--focus-ring-offset);
|
||||
}
|
||||
|
||||
/* A green ring on a green (or red, or near-black) fill reads as nothing. */
|
||||
.sh-btn[data-variant="primary"]:focus-visible,
|
||||
.sh-btn[data-variant="danger"]:focus-visible,
|
||||
.sh-btn[data-variant="dark"]:focus-visible {
|
||||
outline-color: var(--focus-ring-color-inverse);
|
||||
}
|
||||
|
||||
.sh-btn:disabled,
|
||||
.sh-btn[aria-disabled="true"] {
|
||||
background-color: var(--sh-btn-bg-disabled);
|
||||
color: var(--sh-btn-fg-disabled);
|
||||
border-color: var(--sh-btn-border-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
Loading - the label keeps its box (no reflow) and stays in the
|
||||
accessibility tree, which `visibility: hidden` would not allow.
|
||||
------------------------------------------------------------------ */
|
||||
.sh-btn[data-loading="true"] {
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
.sh-btn[data-loading="true"] .sh-btn__content {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sh-btn__spinner {
|
||||
grid-area: sh-btn-stack;
|
||||
inline-size: 1em;
|
||||
block-size: 1em;
|
||||
border: var(--control-border-width) solid currentColor;
|
||||
border-block-start-color: transparent;
|
||||
border-radius: var(--radius-pill);
|
||||
/* The contract publishes fast/base/slow only; two slows is the ~640ms a
|
||||
spinner wants without inventing a duration token. */
|
||||
animation: sh-btn-spin calc(var(--motion-duration-slow) * 2) linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sh-btn-spin {
|
||||
to {
|
||||
rotate: 1turn;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import type { ComponentPropsWithRef, ElementType, ReactNode, SyntheticEvent } from "react";
|
||||
|
||||
import "./Button.css";
|
||||
|
||||
export type ButtonVariant = "primary" | "secondary" | "dark" | "danger" | "ghost";
|
||||
|
||||
/** Figma calls these Small / Medium / Large; they resolve to 36 / 48 / 52px. */
|
||||
export type ButtonSize = "sm" | "md" | "lg";
|
||||
|
||||
/**
|
||||
* Each size owns a published type role rather than a raw font-size, and the
|
||||
* role's line-height is what makes the button land on its designed height.
|
||||
*/
|
||||
const TYPE_ROLE: Record<ButtonSize, string> = {
|
||||
sm: "body-sm-bold",
|
||||
md: "body-md-bold",
|
||||
lg: "body-lg-bold",
|
||||
};
|
||||
|
||||
type ButtonOwnProps = {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
/** Rendered in the inline-start 24px slot - the left in LTR, the right in RTL. */
|
||||
iconStart?: ReactNode;
|
||||
/** Rendered in the inline-end slot. Pass `<ArrowIcon />` for the CTA arrow. */
|
||||
iconEnd?: ReactNode;
|
||||
/** Keeps the label's box, overlays a spinner and sets `aria-busy`. */
|
||||
isLoading?: boolean;
|
||||
disabled?: boolean;
|
||||
/** Stretch to the width of the parent (`display: grid` instead of inline). */
|
||||
fullWidth?: boolean;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export type ButtonProps<E extends ElementType = "button"> = ButtonOwnProps & {
|
||||
/**
|
||||
* The element to render. Defaults to `button`; pass react-router's `Link`
|
||||
* (plus its `to`) for navigation, or `"a"` for an external href. Props of the
|
||||
* chosen element are type-checked, so `as={Link}` requires `to`.
|
||||
*/
|
||||
as?: E;
|
||||
} & Omit<ComponentPropsWithRef<E>, keyof ButtonOwnProps | "as">;
|
||||
|
||||
function cx(...parts: Array<string | false | null | undefined>): string {
|
||||
return parts.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
function blockActivation(event: SyntheticEvent): void {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* The SHARP-ER button - figma frame 14:487.
|
||||
*
|
||||
* Polymorphic: renders a native `<button>` by default and anything else through
|
||||
* `as`. `ref` is an ordinary prop in React 19, so it travels with the rest of
|
||||
* the element props and lands on whichever element `as` selected.
|
||||
*/
|
||||
export function Button<E extends ElementType = "button">({
|
||||
as,
|
||||
variant = "primary",
|
||||
size = "md",
|
||||
iconStart,
|
||||
iconEnd,
|
||||
isLoading = false,
|
||||
disabled = false,
|
||||
fullWidth = false,
|
||||
className,
|
||||
children,
|
||||
...rest
|
||||
}: ButtonProps<E>) {
|
||||
const Component = (as ?? "button") as ElementType;
|
||||
const isNativeButton = Component === "button";
|
||||
const isDisabled = disabled || isLoading;
|
||||
const elementProps = rest as Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* A native button gets the real `disabled` attribute. Anything else (a Link,
|
||||
* an anchor) has no such attribute, so it is taken out of the tab order and
|
||||
* its activation is swallowed instead. Both forms carry `aria-disabled`.
|
||||
*/
|
||||
const stateProps: Record<string, unknown> = isNativeButton
|
||||
? {
|
||||
// Default to a plain button so a button inside a form never submits it
|
||||
// by accident; pass type="submit" explicitly when that is the intent.
|
||||
type: (elementProps.type as string | undefined) ?? "button",
|
||||
disabled: isDisabled,
|
||||
}
|
||||
: isDisabled
|
||||
? { tabIndex: -1, onClick: blockActivation }
|
||||
: {};
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...elementProps}
|
||||
{...stateProps}
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
data-loading={isLoading ? "true" : undefined}
|
||||
aria-busy={isLoading || undefined}
|
||||
aria-disabled={isDisabled || undefined}
|
||||
className={cx("sh-btn", TYPE_ROLE[size], fullWidth && "sh-btn--block", className)}
|
||||
>
|
||||
<span className="sh-btn__content">
|
||||
{iconStart ? <span className="sh-btn__icon">{iconStart}</span> : null}
|
||||
<span className="sh-btn__label">{children}</span>
|
||||
{iconEnd ? <span className="sh-btn__icon">{iconEnd}</span> : null}
|
||||
</span>
|
||||
{isLoading ? <span className="sh-btn__spinner" aria-hidden="true" /> : null}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
USAGE (kept as a comment - the project has no Storybook)
|
||||
|
||||
import { Link } from "react-router";
|
||||
import { ArrowIcon, Button } from "~/components/ui/Button";
|
||||
|
||||
// The home hero CTA, Home EN 253:4799 / Home AR 85:5029.
|
||||
// The arrow sits inline-end, so RTL moves it to the left and mirrors it.
|
||||
<Button as={Link} to="/en/about" size="lg" iconEnd={<ArrowIcon />}>
|
||||
Discover SHARP-ER
|
||||
</Button>
|
||||
|
||||
// Every variant, all three sizes.
|
||||
<Button variant="primary">Apply</Button>
|
||||
<Button variant="secondary" size="sm">Read more</Button>
|
||||
<Button variant="dark" size="lg">Download the report</Button>
|
||||
<Button variant="danger">Withdraw application</Button>
|
||||
<Button variant="ghost" iconEnd={<ArrowIcon />}>All news</Button>
|
||||
|
||||
// Submitting a form: aria-busy is set and the label keeps its box.
|
||||
<Button type="submit" isLoading={isSubmitting} fullWidth>Send message</Button>
|
||||
|
||||
// Disabled - aria-disabled on both the native and the link form.
|
||||
<Button disabled>Applications closed</Button>
|
||||
<Button as={Link} to="/en/events" disabled>Registration closed</Button>
|
||||
|
||||
// A "previous" control, mirrored the other way.
|
||||
<Button variant="ghost" iconStart={<ArrowIcon direction="backward" />}>
|
||||
Previous
|
||||
</Button>
|
||||
|
||||
// An external link keeps anchor semantics and prop typing.
|
||||
<Button as="a" href="https://europa.eu" target="_blank" rel="noreferrer">
|
||||
European Union
|
||||
</Button>
|
||||
------------------------------------------------------------------------- */
|
||||
@@ -0,0 +1,4 @@
|
||||
export { Button } from "./Button";
|
||||
export type { ButtonProps, ButtonSize, ButtonVariant } from "./Button";
|
||||
export { ArrowIcon } from "./ArrowIcon";
|
||||
export type { ArrowIconDirection, ArrowIconProps } from "./ArrowIcon";
|
||||
Reference in New Issue
Block a user