chore: Монорепо с общими пакетами
This commit is contained in:
140
packages/tach-typography/src/angular/index.ts
Normal file
140
packages/tach-typography/src/angular/index.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { Directive, ElementRef, Input, OnChanges, Renderer2, SimpleChanges } from "@angular/core";
|
||||
import { NzTypographyModule } from "ng-zorro-antd/typography";
|
||||
|
||||
import {
|
||||
tachTypographyClassList,
|
||||
tachTypographyClassName,
|
||||
tachTypographyEllipsisStyle,
|
||||
type EllipsisOptions,
|
||||
type TypographyClassOptions,
|
||||
type TypographyColor,
|
||||
type TypographyRenderOptions,
|
||||
type TypographyVariant,
|
||||
type TypographyWeight,
|
||||
} from "../core";
|
||||
|
||||
export type AngularTypographyClassInput = TypographyClassOptions;
|
||||
|
||||
export interface AngularTypographyRenderOptions extends TypographyRenderOptions {
|
||||
preserveStyle?: Record<string, string | number>;
|
||||
}
|
||||
|
||||
const camelToKebab = (value: string): string =>
|
||||
value.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`);
|
||||
|
||||
const toCssProperty = (styleKey: string): string => {
|
||||
if (styleKey.startsWith("Webkit")) {
|
||||
return `-webkit-${camelToKebab(styleKey.slice(6))}`;
|
||||
}
|
||||
|
||||
return camelToKebab(styleKey);
|
||||
};
|
||||
|
||||
export const tachAngularTypographyClassName = (
|
||||
options: AngularTypographyClassInput = {},
|
||||
): string => {
|
||||
return tachTypographyClassName(options);
|
||||
};
|
||||
|
||||
export const tachAngularTypographyClassList = (
|
||||
options: AngularTypographyClassInput = {},
|
||||
): string[] => {
|
||||
return tachTypographyClassList(options);
|
||||
};
|
||||
|
||||
export const tachAngularTypographyStyles = (
|
||||
ellipsis?: EllipsisOptions,
|
||||
preserveStyle: Record<string, string | number> = {},
|
||||
): Record<string, string | number> => {
|
||||
const ellipsisStyle = tachTypographyEllipsisStyle(ellipsis);
|
||||
|
||||
if (!ellipsisStyle) {
|
||||
return preserveStyle;
|
||||
}
|
||||
|
||||
return {
|
||||
...ellipsisStyle,
|
||||
...preserveStyle,
|
||||
};
|
||||
};
|
||||
|
||||
@Directive({
|
||||
selector: "[tachTypography]",
|
||||
standalone: true,
|
||||
})
|
||||
export class TachTypographyDirective implements OnChanges {
|
||||
@Input() tachTypography: TypographyVariant | "" | undefined;
|
||||
@Input() tachTypographyVariant: TypographyVariant = "Body";
|
||||
@Input() tachTypographyColor: TypographyColor = "primary";
|
||||
@Input() tachTypographyWeight: TypographyWeight = "normal";
|
||||
@Input() tachTypographyClickable = false;
|
||||
@Input() tachTypographyClassName: string | undefined;
|
||||
@Input() tachTypographyEllipsis: EllipsisOptions | undefined;
|
||||
|
||||
private readonly appliedClasses = new Set<string>();
|
||||
private readonly appliedStyleProperties = new Set<string>();
|
||||
|
||||
constructor(
|
||||
private readonly elementRef: ElementRef<HTMLElement>,
|
||||
private readonly renderer: Renderer2,
|
||||
) {}
|
||||
|
||||
ngOnChanges(_changes: SimpleChanges): void {
|
||||
this.syncClasses();
|
||||
this.syncEllipsisStyles();
|
||||
}
|
||||
|
||||
private syncClasses(): void {
|
||||
const nextClassList = tachTypographyClassList({
|
||||
variant: this.tachTypography || this.tachTypographyVariant,
|
||||
color: this.tachTypographyColor,
|
||||
weight: this.tachTypographyWeight,
|
||||
clickable: this.tachTypographyClickable,
|
||||
className: this.tachTypographyClassName,
|
||||
});
|
||||
|
||||
const nextSet = new Set(nextClassList);
|
||||
|
||||
for (const className of this.appliedClasses) {
|
||||
if (!nextSet.has(className)) {
|
||||
this.renderer.removeClass(this.elementRef.nativeElement, className);
|
||||
}
|
||||
}
|
||||
|
||||
for (const className of nextSet) {
|
||||
this.renderer.addClass(this.elementRef.nativeElement, className);
|
||||
}
|
||||
|
||||
this.appliedClasses.clear();
|
||||
for (const className of nextSet) {
|
||||
this.appliedClasses.add(className);
|
||||
}
|
||||
}
|
||||
|
||||
private syncEllipsisStyles(): void {
|
||||
const nextStyles = tachTypographyEllipsisStyle(this.tachTypographyEllipsis) || {};
|
||||
const nextStyleKeys = new Set(Object.keys(nextStyles));
|
||||
|
||||
for (const styleKey of this.appliedStyleProperties) {
|
||||
if (!nextStyleKeys.has(styleKey)) {
|
||||
this.renderer.removeStyle(this.elementRef.nativeElement, toCssProperty(styleKey));
|
||||
}
|
||||
}
|
||||
|
||||
for (const [styleKey, styleValue] of Object.entries(nextStyles)) {
|
||||
this.renderer.setStyle(this.elementRef.nativeElement, toCssProperty(styleKey), styleValue);
|
||||
}
|
||||
|
||||
this.appliedStyleProperties.clear();
|
||||
for (const styleKey of nextStyleKeys) {
|
||||
this.appliedStyleProperties.add(styleKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [NzTypographyModule, TachTypographyDirective],
|
||||
exports: [NzTypographyModule, TachTypographyDirective],
|
||||
})
|
||||
export class TachTypographyNzModule {}
|
||||
29
packages/tach-typography/src/core/classnames.ts
Normal file
29
packages/tach-typography/src/core/classnames.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { TypographyClassOptions } from "./types";
|
||||
|
||||
const BASE_CLASS = "tach-typography";
|
||||
|
||||
const join = (...parts: Array<string | undefined | null | false>): string =>
|
||||
parts.filter(Boolean).join(" ");
|
||||
|
||||
export const tachTypographyClassName = ({
|
||||
variant = "Body",
|
||||
color = "primary",
|
||||
weight = "normal",
|
||||
clickable = false,
|
||||
className,
|
||||
}: TypographyClassOptions = {}): string => {
|
||||
return join(
|
||||
BASE_CLASS,
|
||||
`${BASE_CLASS}--${variant}`,
|
||||
`${BASE_CLASS}--color-${color}`,
|
||||
weight === "bold" && `${BASE_CLASS}--bold`,
|
||||
clickable && `${BASE_CLASS}--pointer`,
|
||||
className,
|
||||
);
|
||||
};
|
||||
|
||||
export const tachTypographyClassList = (options: TypographyClassOptions = {}): string[] => {
|
||||
return tachTypographyClassName(options)
|
||||
.split(" ")
|
||||
.filter(Boolean);
|
||||
};
|
||||
21
packages/tach-typography/src/core/ellipsis.ts
Normal file
21
packages/tach-typography/src/core/ellipsis.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { EllipsisOptions } from "./types";
|
||||
|
||||
type StyleRecord = Record<string, string | number>;
|
||||
|
||||
export const tachTypographyEllipsisStyle = (
|
||||
ellipsis?: EllipsisOptions,
|
||||
): StyleRecord | undefined => {
|
||||
if (!ellipsis) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const rows = typeof ellipsis === "object" ? ellipsis.rows ?? 1 : 1;
|
||||
|
||||
return {
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
display: "-webkit-box",
|
||||
WebkitBoxOrient: "vertical",
|
||||
WebkitLineClamp: rows,
|
||||
};
|
||||
};
|
||||
56
packages/tach-typography/src/core/index.test.ts
Normal file
56
packages/tach-typography/src/core/index.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
tachTypographyClassList,
|
||||
tachTypographyClassName,
|
||||
tachTypographyEllipsisStyle,
|
||||
} from "./index";
|
||||
|
||||
describe("tachTypographyClassName", () => {
|
||||
it("builds className string with defaults", () => {
|
||||
expect(tachTypographyClassName()).toBe(
|
||||
"tach-typography tach-typography--Body tach-typography--color-primary",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds optional states", () => {
|
||||
expect(
|
||||
tachTypographyClassName({
|
||||
variant: "AccentH1",
|
||||
color: "link",
|
||||
weight: "bold",
|
||||
clickable: true,
|
||||
className: "custom",
|
||||
}),
|
||||
).toBe(
|
||||
"tach-typography tach-typography--AccentH1 tach-typography--color-link tach-typography--bold tach-typography--pointer custom",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns list helper", () => {
|
||||
expect(tachTypographyClassList({ variant: "Title1" })).toEqual([
|
||||
"tach-typography",
|
||||
"tach-typography--Title1",
|
||||
"tach-typography--color-primary",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("tachTypographyEllipsisStyle", () => {
|
||||
it("returns undefined without ellipsis", () => {
|
||||
expect(tachTypographyEllipsisStyle()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns one-line style for true", () => {
|
||||
expect(tachTypographyEllipsisStyle(true)).toMatchObject({
|
||||
WebkitLineClamp: 1,
|
||||
overflow: "hidden",
|
||||
});
|
||||
});
|
||||
|
||||
it("respects rows value", () => {
|
||||
expect(tachTypographyEllipsisStyle({ rows: 3 })).toMatchObject({
|
||||
WebkitLineClamp: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
3
packages/tach-typography/src/core/index.ts
Normal file
3
packages/tach-typography/src/core/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./types";
|
||||
export * from "./classnames";
|
||||
export * from "./ellipsis";
|
||||
61
packages/tach-typography/src/core/types.ts
Normal file
61
packages/tach-typography/src/core/types.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
export const TYPOGRAPHY_VARIANTS = [
|
||||
"LargeTitle",
|
||||
"Title1",
|
||||
"Title2",
|
||||
"Title3",
|
||||
"Headline",
|
||||
"Body",
|
||||
"Inputs",
|
||||
"Subheadline",
|
||||
"FootnoteUnderline",
|
||||
"Footnote",
|
||||
"Caption",
|
||||
"Caption2",
|
||||
"AccentH1",
|
||||
"AccentH2",
|
||||
"AccentSubttl",
|
||||
"AccentSubttl2",
|
||||
"AccentCaption",
|
||||
"AccentCaption2",
|
||||
"AccentRegularM",
|
||||
"AccentRegularS",
|
||||
"AccentLargeTtl",
|
||||
"AppMediumBody",
|
||||
"AppMediumSubtext",
|
||||
"AppMediumSubtextUnderline",
|
||||
] as const;
|
||||
|
||||
export const TYPOGRAPHY_COLORS = [
|
||||
"primary",
|
||||
"secondary",
|
||||
"tertiary",
|
||||
"quaternary",
|
||||
"link",
|
||||
"white",
|
||||
"dark",
|
||||
"alert",
|
||||
"malahit",
|
||||
"attantion",
|
||||
] as const;
|
||||
|
||||
export type TypographyVariant = (typeof TYPOGRAPHY_VARIANTS)[number];
|
||||
export type TypographyColor = (typeof TYPOGRAPHY_COLORS)[number];
|
||||
export type TypographyWeight = "normal" | "bold";
|
||||
|
||||
export interface TypographyClassOptions {
|
||||
variant?: TypographyVariant;
|
||||
color?: TypographyColor;
|
||||
weight?: TypographyWeight;
|
||||
clickable?: boolean;
|
||||
className?: string | undefined;
|
||||
}
|
||||
|
||||
export type EllipsisOptions =
|
||||
| boolean
|
||||
| {
|
||||
rows?: number;
|
||||
};
|
||||
|
||||
export interface TypographyRenderOptions extends TypographyClassOptions {
|
||||
ellipsis?: EllipsisOptions;
|
||||
}
|
||||
82
packages/tach-typography/src/react/index.tsx
Normal file
82
packages/tach-typography/src/react/index.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React from "react";
|
||||
|
||||
import { Typography } from "antd";
|
||||
import type { LinkProps } from "antd/lib/typography/Link";
|
||||
import type { ParagraphProps } from "antd/lib/typography/Paragraph";
|
||||
import type { TextProps } from "antd/lib/typography/Text";
|
||||
import type { TitleProps } from "antd/lib/typography/Title";
|
||||
|
||||
import {
|
||||
tachTypographyClassName,
|
||||
type TypographyColor,
|
||||
type TypographyVariant,
|
||||
type TypographyWeight,
|
||||
} from "../core";
|
||||
|
||||
interface AdditionalProps {
|
||||
color?: TypographyColor;
|
||||
weight?: TypographyWeight;
|
||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
||||
className?: string | undefined;
|
||||
}
|
||||
|
||||
const createTypographyVariant = <P extends object>(
|
||||
Component: React.ComponentType<P>,
|
||||
variant: TypographyVariant,
|
||||
) => {
|
||||
const Variant = React.forwardRef<HTMLElement, P & AdditionalProps>(
|
||||
({ color = "primary", weight = "normal", className, onClick, ...rest }, ref) => (
|
||||
<Component
|
||||
ref={ref as never}
|
||||
className={tachTypographyClassName({
|
||||
variant,
|
||||
color,
|
||||
weight,
|
||||
className,
|
||||
clickable: Boolean(onClick),
|
||||
})}
|
||||
onClick={onClick}
|
||||
{...(rest as P)}
|
||||
/>
|
||||
),
|
||||
);
|
||||
|
||||
Variant.displayName = String(variant);
|
||||
|
||||
return Variant;
|
||||
};
|
||||
|
||||
const createTypographyComponent = <P extends object>(Component: React.ComponentType<P>) => ({
|
||||
LargeTitle: createTypographyVariant(Component, "LargeTitle"),
|
||||
Title1: createTypographyVariant(Component, "Title1"),
|
||||
Title2: createTypographyVariant(Component, "Title2"),
|
||||
Title3: createTypographyVariant(Component, "Title3"),
|
||||
Headline: createTypographyVariant(Component, "Headline"),
|
||||
Body: createTypographyVariant(Component, "Body"),
|
||||
Inputs: createTypographyVariant(Component, "Inputs"),
|
||||
Subheadline: createTypographyVariant(Component, "Subheadline"),
|
||||
FootnoteUnderline: createTypographyVariant(Component, "FootnoteUnderline"),
|
||||
Footnote: createTypographyVariant(Component, "Footnote"),
|
||||
Caption: createTypographyVariant(Component, "Caption"),
|
||||
Caption2: createTypographyVariant(Component, "Caption2"),
|
||||
|
||||
AccentH1: createTypographyVariant(Component, "AccentH1"),
|
||||
AccentH2: createTypographyVariant(Component, "AccentH2"),
|
||||
AccentSubttl: createTypographyVariant(Component, "AccentSubttl"),
|
||||
AccentSubttl2: createTypographyVariant(Component, "AccentSubttl2"),
|
||||
AccentCaption: createTypographyVariant(Component, "AccentCaption"),
|
||||
AccentCaption2: createTypographyVariant(Component, "AccentCaption2"),
|
||||
AccentRegularM: createTypographyVariant(Component, "AccentRegularM"),
|
||||
AccentRegularS: createTypographyVariant(Component, "AccentRegularS"),
|
||||
AccentLargeTtl: createTypographyVariant(Component, "AccentLargeTtl"),
|
||||
AppMediumBody: createTypographyVariant(Component, "AppMediumBody"),
|
||||
AppMediumSubtext: createTypographyVariant(Component, "AppMediumSubtext"),
|
||||
AppMediumSubtextUnderline: createTypographyVariant(Component, "AppMediumSubtextUnderline"),
|
||||
});
|
||||
|
||||
export const TachTypography = {
|
||||
Text: createTypographyComponent<TextProps & Pick<ParagraphProps, "ellipsis">>(Typography.Text),
|
||||
Paragraph: createTypographyComponent<ParagraphProps>(Typography.Paragraph),
|
||||
Link: createTypographyComponent<LinkProps>(Typography.Link),
|
||||
Title: createTypographyComponent<TitleProps>(Typography.Title),
|
||||
};
|
||||
@@ -0,0 +1,183 @@
|
||||
import React from "react";
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { expect, fn, userEvent, within } from "@storybook/test";
|
||||
|
||||
import {
|
||||
TYPOGRAPHY_COLORS,
|
||||
TYPOGRAPHY_VARIANTS,
|
||||
type TypographyColor,
|
||||
type TypographyVariant,
|
||||
type TypographyWeight,
|
||||
} from "../core";
|
||||
import { TachTypography } from "../react";
|
||||
|
||||
type TypographyNamespace = keyof typeof TachTypography;
|
||||
|
||||
type VariantComponentProps = {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
color?: TypographyColor;
|
||||
ellipsis?: boolean | { rows?: number };
|
||||
href?: string;
|
||||
level?: 1 | 2 | 3 | 4 | 5;
|
||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
||||
weight?: TypographyWeight;
|
||||
};
|
||||
|
||||
type VariantComponent = React.ComponentType<VariantComponentProps>;
|
||||
|
||||
const getVariantComponent = (
|
||||
namespace: TypographyNamespace,
|
||||
variant: TypographyVariant,
|
||||
): VariantComponent => {
|
||||
return (TachTypography[namespace] as unknown as Record<TypographyVariant, VariantComponent>)[variant];
|
||||
};
|
||||
|
||||
interface PlaygroundArgs {
|
||||
namespace: TypographyNamespace;
|
||||
variant: TypographyVariant;
|
||||
color: TypographyColor;
|
||||
weight: TypographyWeight;
|
||||
children: string;
|
||||
clickable: boolean;
|
||||
ellipsisRows: number;
|
||||
href: string;
|
||||
titleLevel: 1 | 2 | 3 | 4 | 5;
|
||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
||||
}
|
||||
|
||||
const renderTypography = (args: PlaygroundArgs) => {
|
||||
const Component = getVariantComponent(args.namespace, args.variant);
|
||||
|
||||
const componentProps: VariantComponentProps = {
|
||||
color: args.color,
|
||||
weight: args.weight,
|
||||
};
|
||||
|
||||
if (args.clickable && args.onClick) {
|
||||
componentProps.onClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
args.onClick?.(event);
|
||||
};
|
||||
}
|
||||
|
||||
if (args.namespace === "Text" || args.namespace === "Paragraph") {
|
||||
componentProps.ellipsis = args.ellipsisRows > 0 ? { rows: args.ellipsisRows } : false;
|
||||
}
|
||||
|
||||
if (args.namespace === "Link") {
|
||||
componentProps.href = args.href;
|
||||
}
|
||||
|
||||
if (args.namespace === "Title") {
|
||||
componentProps.level = args.titleLevel;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tach-story-surface">
|
||||
<Component {...componentProps}>{args.children}</Component>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const meta: Meta<PlaygroundArgs> = {
|
||||
title: "TachTypography/Playground",
|
||||
tags: ["autodocs"],
|
||||
render: renderTypography,
|
||||
args: {
|
||||
namespace: "Text",
|
||||
variant: "Body",
|
||||
color: "primary",
|
||||
weight: "normal",
|
||||
children: "TachTypography playground text",
|
||||
clickable: false,
|
||||
ellipsisRows: 0,
|
||||
href: "https://example.com",
|
||||
titleLevel: 3,
|
||||
},
|
||||
argTypes: {
|
||||
namespace: {
|
||||
control: "select",
|
||||
options: ["Text", "Paragraph", "Link", "Title"],
|
||||
},
|
||||
variant: {
|
||||
control: "select",
|
||||
options: TYPOGRAPHY_VARIANTS,
|
||||
},
|
||||
color: {
|
||||
control: "select",
|
||||
options: TYPOGRAPHY_COLORS,
|
||||
},
|
||||
weight: {
|
||||
control: "inline-radio",
|
||||
options: ["normal", "bold"],
|
||||
},
|
||||
children: {
|
||||
control: "text",
|
||||
},
|
||||
clickable: {
|
||||
control: "boolean",
|
||||
},
|
||||
ellipsisRows: {
|
||||
control: {
|
||||
type: "number",
|
||||
min: 0,
|
||||
max: 5,
|
||||
},
|
||||
description: "Works for Text and Paragraph namespaces",
|
||||
},
|
||||
href: {
|
||||
control: "text",
|
||||
description: "Works for Link namespace",
|
||||
},
|
||||
titleLevel: {
|
||||
control: "inline-radio",
|
||||
options: [1, 2, 3, 4, 5],
|
||||
description: "Works for Title namespace",
|
||||
},
|
||||
onClick: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Interactive playground for all TachTypography namespaces with full token and prop controls.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Interactive: Story = {};
|
||||
|
||||
export const WithEllipsis: Story = {
|
||||
args: {
|
||||
namespace: "Paragraph",
|
||||
variant: "Body",
|
||||
ellipsisRows: 2,
|
||||
children:
|
||||
"This paragraph demonstrates multi-line truncation in Storybook. Increase or decrease ellipsisRows to validate visual behavior and clipping boundaries.",
|
||||
},
|
||||
};
|
||||
|
||||
export const ClickInteraction: Story = {
|
||||
args: {
|
||||
namespace: "Text",
|
||||
variant: "Subheadline",
|
||||
clickable: true,
|
||||
children: "Click this text to run interaction assertion",
|
||||
onClick: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(canvas.getByText(args.children));
|
||||
await expect(args.onClick as ReturnType<typeof fn>).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
import React from "react";
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
const meta = {
|
||||
title: "TachTypography/Reference",
|
||||
tags: ["autodocs"],
|
||||
} satisfies Meta;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const PropsMatrix: Story = {
|
||||
render: () => (
|
||||
<div className="tach-story-surface tach-story-stack">
|
||||
<table className="tach-story-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Prop</th>
|
||||
<th>Type</th>
|
||||
<th>React</th>
|
||||
<th>Angular</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>variant</td>
|
||||
<td>TypographyVariant</td>
|
||||
<td>TachTypography.[Text|Paragraph|Link|Title].Variant</td>
|
||||
<td>tachTypography / tachTypographyVariant</td>
|
||||
<td>Main typography token</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>color</td>
|
||||
<td>TypographyColor</td>
|
||||
<td>color</td>
|
||||
<td>tachTypographyColor</td>
|
||||
<td>Maps to CSS variables</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>weight</td>
|
||||
<td>"normal" | "bold"</td>
|
||||
<td>weight</td>
|
||||
<td>tachTypographyWeight</td>
|
||||
<td>Bold class modifier</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ellipsis</td>
|
||||
<td><code>{"boolean | { rows?: number }"}</code></td>
|
||||
<td>ellipsis (Text/Paragraph)</td>
|
||||
<td>tachTypographyEllipsis</td>
|
||||
<td>Applies line clamp styles</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>clickable</td>
|
||||
<td>boolean</td>
|
||||
<td>onClick adds pointer class</td>
|
||||
<td>tachTypographyClickable</td>
|
||||
<td>Visual affordance + cursor</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>className</td>
|
||||
<td>string</td>
|
||||
<td>className</td>
|
||||
<td>tachTypographyClassName</td>
|
||||
<td>Merges with token classes</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const AngularAdapter: Story = {
|
||||
render: () => (
|
||||
<div className="tach-story-surface tach-story-stack">
|
||||
<h4>Angular adapter usage</h4>
|
||||
<pre>
|
||||
<code>
|
||||
{`import { TachTypographyDirective, TachTypographyNzModule } from "@hublib-web/tach-typography/angular";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [TachTypographyNzModule, TachTypographyDirective],
|
||||
template: \
|
||||
\`<span
|
||||
nz-typography
|
||||
tachTypography
|
||||
tachTypography="Body"
|
||||
tachTypographyColor="link"
|
||||
tachTypographyWeight="bold"
|
||||
[tachTypographyEllipsis]="{ rows: 2 }"
|
||||
>
|
||||
Typography for Angular + NG-ZORRO
|
||||
</span>\`,
|
||||
})
|
||||
export class ExampleComponent {}
|
||||
`}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,189 @@
|
||||
import React from "react";
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import {
|
||||
TYPOGRAPHY_COLORS,
|
||||
TYPOGRAPHY_VARIANTS,
|
||||
type TypographyColor,
|
||||
type TypographyVariant,
|
||||
type TypographyWeight,
|
||||
} from "../core";
|
||||
import { TachTypography } from "../react";
|
||||
|
||||
type TypographyNamespace = keyof typeof TachTypography;
|
||||
|
||||
type VariantComponentProps = {
|
||||
children?: React.ReactNode;
|
||||
color?: TypographyColor;
|
||||
ellipsis?: boolean | { rows?: number };
|
||||
href?: string;
|
||||
level?: 1 | 2 | 3 | 4 | 5;
|
||||
weight?: TypographyWeight;
|
||||
};
|
||||
|
||||
type VariantComponent = React.ComponentType<VariantComponentProps>;
|
||||
|
||||
const getVariantComponent = (
|
||||
namespace: TypographyNamespace,
|
||||
variant: TypographyVariant,
|
||||
): VariantComponent => {
|
||||
return (TachTypography[namespace] as unknown as Record<TypographyVariant, VariantComponent>)[variant];
|
||||
};
|
||||
|
||||
interface VariantScaleArgs {
|
||||
namespace: TypographyNamespace;
|
||||
color: TypographyColor;
|
||||
weight: TypographyWeight;
|
||||
sampleText: string;
|
||||
}
|
||||
|
||||
interface ColorPaletteArgs {
|
||||
namespace: TypographyNamespace;
|
||||
variant: TypographyVariant;
|
||||
weight: TypographyWeight;
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: "TachTypography/Tokens",
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Token showcase for typography variants and colors. Useful for validating visual consistency against the design system.",
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Meta;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const VariantScale: StoryObj<VariantScaleArgs> = {
|
||||
args: {
|
||||
namespace: "Text",
|
||||
color: "primary",
|
||||
weight: "normal",
|
||||
sampleText: "The quick brown fox jumps over the lazy dog",
|
||||
},
|
||||
argTypes: {
|
||||
namespace: {
|
||||
control: "select",
|
||||
options: ["Text", "Paragraph", "Link", "Title"],
|
||||
},
|
||||
color: {
|
||||
control: "select",
|
||||
options: TYPOGRAPHY_COLORS,
|
||||
},
|
||||
weight: {
|
||||
control: "inline-radio",
|
||||
options: ["normal", "bold"],
|
||||
},
|
||||
sampleText: {
|
||||
control: "text",
|
||||
},
|
||||
},
|
||||
render: args => (
|
||||
<div className="tach-story-surface tach-story-stack">
|
||||
{TYPOGRAPHY_VARIANTS.map(variant => {
|
||||
const Component = getVariantComponent(args.namespace, variant);
|
||||
const componentProps: VariantComponentProps = {
|
||||
color: args.color,
|
||||
weight: args.weight,
|
||||
};
|
||||
|
||||
if (args.namespace === "Link") {
|
||||
componentProps.href = "https://example.com";
|
||||
}
|
||||
|
||||
if (args.namespace === "Title") {
|
||||
componentProps.level = 4;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tach-story-row" key={variant}>
|
||||
<span className="tach-story-label">{variant}</span>
|
||||
<Component {...componentProps}>{args.sampleText}</Component>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const ColorPalette: StoryObj<ColorPaletteArgs> = {
|
||||
args: {
|
||||
namespace: "Text",
|
||||
variant: "Body",
|
||||
weight: "normal",
|
||||
},
|
||||
argTypes: {
|
||||
namespace: {
|
||||
control: "select",
|
||||
options: ["Text", "Paragraph", "Link", "Title"],
|
||||
},
|
||||
variant: {
|
||||
control: "select",
|
||||
options: TYPOGRAPHY_VARIANTS,
|
||||
},
|
||||
weight: {
|
||||
control: "inline-radio",
|
||||
options: ["normal", "bold"],
|
||||
},
|
||||
},
|
||||
render: args => {
|
||||
const Component = getVariantComponent(args.namespace, args.variant);
|
||||
|
||||
return (
|
||||
<div className="tach-story-surface tach-story-grid tach-story-grid--colors">
|
||||
{TYPOGRAPHY_COLORS.map(color => {
|
||||
const componentProps: VariantComponentProps = {
|
||||
color,
|
||||
weight: args.weight,
|
||||
};
|
||||
|
||||
if (args.namespace === "Link") {
|
||||
componentProps.href = "https://example.com";
|
||||
}
|
||||
|
||||
if (args.namespace === "Title") {
|
||||
componentProps.level = 4;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tach-story-row" key={color}>
|
||||
<span className="tach-story-label">{color}</span>
|
||||
<Component {...componentProps}>Color token preview</Component>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const TokenReference: Story = {
|
||||
render: () => (
|
||||
<div className="tach-story-surface tach-story-stack">
|
||||
<div>
|
||||
<h4>Variants ({TYPOGRAPHY_VARIANTS.length})</h4>
|
||||
<ul className="tach-story-token-list">
|
||||
{TYPOGRAPHY_VARIANTS.map(token => (
|
||||
<li key={token}>{token}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4>Colors ({TYPOGRAPHY_COLORS.length})</h4>
|
||||
<ul className="tach-story-token-list">
|
||||
{TYPOGRAPHY_COLORS.map(token => (
|
||||
<li key={token}>{token}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
103
packages/tach-typography/src/styles/tach-typography.css
Normal file
103
packages/tach-typography/src/styles/tach-typography.css
Normal file
@@ -0,0 +1,103 @@
|
||||
.tach-typography {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ant-typography.tach-typography {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.tach-typography--pointer,
|
||||
.ant-typography.tach-typography--pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tach-typography--bold,
|
||||
.ant-typography.tach-typography--bold {
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.tach-typography--color-primary,
|
||||
.ant-typography.tach-typography--color-primary { color: var(--Text-Primary); }
|
||||
.tach-typography--color-secondary,
|
||||
.ant-typography.tach-typography--color-secondary { color: var(--Text-Secondary); }
|
||||
.tach-typography--color-tertiary,
|
||||
.ant-typography.tach-typography--color-tertiary { color: var(--Text-Tertiary); }
|
||||
.tach-typography--color-quaternary,
|
||||
.ant-typography.tach-typography--color-quaternary { color: var(--Text-Quaternary); }
|
||||
.tach-typography--color-link,
|
||||
.ant-typography.tach-typography--color-link { color: var(--System-HashtagsInPost); }
|
||||
.tach-typography--color-white,
|
||||
.ant-typography.tach-typography--color-white { color: var(--Default-White); }
|
||||
.tach-typography--color-dark,
|
||||
.ant-typography.tach-typography--color-dark { color: var(--Default-Dark); }
|
||||
.tach-typography--color-alert,
|
||||
.ant-typography.tach-typography--color-alert { color: var(--System-Alert); }
|
||||
.tach-typography--color-malahit,
|
||||
.ant-typography.tach-typography--color-malahit { color: var(--Accent-Malahit); }
|
||||
.tach-typography--color-attantion,
|
||||
.ant-typography.tach-typography--color-attantion { color: var(--System-Attantion); }
|
||||
|
||||
.tach-typography--LargeTitle,
|
||||
.ant-typography.tach-typography--LargeTitle { font-family: Inter, sans-serif; font-size: 38px; font-weight: 500; line-height: 46px; }
|
||||
.tach-typography--Title1,
|
||||
.ant-typography.tach-typography--Title1 { font-family: Inter, sans-serif; font-size: 28px; font-weight: 500; line-height: 34px; }
|
||||
.tach-typography--Title2,
|
||||
.ant-typography.tach-typography--Title2 { font-family: Inter, sans-serif; font-size: 22px; font-weight: 500; line-height: 28px; }
|
||||
.tach-typography--Title3,
|
||||
.ant-typography.tach-typography--Title3 { font-family: Inter, sans-serif; font-size: 20px; font-weight: 500; line-height: 26px; }
|
||||
.tach-typography--Headline,
|
||||
.ant-typography.tach-typography--Headline { font-family: Inter, sans-serif; font-size: 16px; font-weight: 500; line-height: 24px; }
|
||||
.tach-typography--Body,
|
||||
.tach-typography--AppMediumBody,
|
||||
.ant-typography.tach-typography--Body,
|
||||
.ant-typography.tach-typography--AppMediumBody { font-family: Inter, sans-serif; font-size: 14px; font-weight: 500; line-height: 20px; }
|
||||
.tach-typography--Inputs,
|
||||
.ant-typography.tach-typography--Inputs { font-family: Inter, sans-serif; font-size: 14px; font-weight: 500; line-height: 24px; }
|
||||
.tach-typography--Subheadline,
|
||||
.ant-typography.tach-typography--Subheadline { font-family: Inter, sans-serif; font-size: 14px; font-weight: 500; line-height: 18px; }
|
||||
.tach-typography--FootnoteUnderline,
|
||||
.ant-typography.tach-typography--FootnoteUnderline { font-family: Inter, sans-serif; font-size: 13px; font-weight: 500; line-height: 18px; text-decoration: underline; }
|
||||
.tach-typography--Footnote,
|
||||
.ant-typography.tach-typography--Footnote { font-family: Inter, sans-serif; font-size: 13px; font-weight: 500; line-height: 18px; }
|
||||
.tach-typography--Caption,
|
||||
.ant-typography.tach-typography--Caption { font-family: Inter, sans-serif; font-size: 10px; font-weight: 500; line-height: 12px; text-transform: uppercase; }
|
||||
.tach-typography--Caption2,
|
||||
.ant-typography.tach-typography--Caption2 { font-family: Inter, sans-serif; font-size: 8px; font-weight: 500; line-height: 10px; text-transform: uppercase; }
|
||||
|
||||
.tach-typography--AccentH1,
|
||||
.ant-typography.tach-typography--AccentH1 { font-family: Unbounded, sans-serif; font-size: 20px; font-weight: 700; line-height: 30px; }
|
||||
.tach-typography--AccentH2,
|
||||
.ant-typography.tach-typography--AccentH2 { font-family: Unbounded, sans-serif; font-size: 16px; font-weight: 700; line-height: 24px; }
|
||||
.tach-typography--AccentSubttl,
|
||||
.ant-typography.tach-typography--AccentSubttl { font-family: Unbounded, sans-serif; font-size: 14px; font-weight: 700; line-height: 22px; }
|
||||
.tach-typography--AccentSubttl2,
|
||||
.ant-typography.tach-typography--AccentSubttl2 { font-family: Unbounded, sans-serif; font-size: 12px; font-weight: 700; line-height: 20px; }
|
||||
.tach-typography--AccentCaption,
|
||||
.ant-typography.tach-typography--AccentCaption { font-family: Unbounded, sans-serif; font-size: 9px; font-weight: 700; line-height: 12px; text-transform: uppercase; }
|
||||
.tach-typography--AccentCaption2,
|
||||
.ant-typography.tach-typography--AccentCaption2 { font-family: Unbounded, sans-serif; font-size: 7px; font-weight: 700; line-height: 10px; text-transform: uppercase; }
|
||||
.tach-typography--AccentRegularM,
|
||||
.ant-typography.tach-typography--AccentRegularM { font-family: Unbounded, sans-serif; font-size: 14px; font-weight: 400; line-height: 22px; }
|
||||
.tach-typography--AccentRegularS,
|
||||
.ant-typography.tach-typography--AccentRegularS { font-family: Unbounded, sans-serif; font-size: 12px; font-weight: 400; line-height: 20px; }
|
||||
.tach-typography--AccentLargeTtl,
|
||||
.ant-typography.tach-typography--AccentLargeTtl { font-family: Unbounded, sans-serif; font-size: 38px; font-weight: 700; line-height: 52px; }
|
||||
.tach-typography--AppMediumSubtext,
|
||||
.ant-typography.tach-typography--AppMediumSubtext { text-align: center; font-family: Inter, sans-serif; font-size: 11px; font-weight: 400; line-height: 17px; }
|
||||
.tach-typography--AppMediumSubtextUnderline,
|
||||
.ant-typography.tach-typography--AppMediumSubtextUnderline { font-family: Inter, sans-serif; font-size: 11px; font-weight: 400; line-height: 17px; text-decoration: underline; }
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.tach-typography--AccentLargeTtl,
|
||||
.ant-typography.tach-typography--AccentLargeTtl {
|
||||
font-size: 20px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.tach-typography--AccentRegularM,
|
||||
.ant-typography.tach-typography--AccentRegularM {
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user