refactor: establish local frontend workspace boundaries
Frontend security / security (push) Successful in 9s
Frontend verify / verify (push) Successful in 5m27s
Frontend security / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Successful in 5m24s
Conventional commits / commits (pull_request) Failing after 26s

OpenSpec change: evolve-frontend-stack
This commit is contained in:
2026-09-22 22:11:02 +03:00
parent 5b2c6bb7cb
commit f876411a37
101 changed files with 1487 additions and 666 deletions
+45
View File
@@ -0,0 +1,45 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { AddIcon, DeleteIcon, EditIcon } from "./index";
const meta = {
title: "Design System/Icons",
component: AddIcon,
tags: ["autodocs"],
parameters: {
docs: {
description: {
component:
"Статичные SVG: size по умолчанию 24, strokeWidth 1.5, цвет currentColor. В кнопке иконка декоративная, имя принадлежит действию. Для самостоятельного изображения задайте aria-hidden={false}, role=img и aria-label либо aria-labelledby. Геометрия взята из Card/Cards; подписи и действия остаются у приложения.",
},
},
},
} satisfies Meta<typeof AddIcon>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Decorative: Story = {
render: () => (
<div style={{ display: "flex", gap: 16, color: "#2563eb" }}>
<button type="button" aria-label="Добавить пример">
<AddIcon size={20} strokeWidth={2} />
</button>
<button type="button" aria-label="Изменить пример">
<EditIcon size={18} />
</button>
<button type="button" aria-label="Удалить пример">
<DeleteIcon size={18} />
</button>
</div>
),
};
export const Meaningful: Story = {
render: () => (
<div style={{ display: "flex", gap: 16, color: "#2563eb" }}>
<AddIcon aria-hidden={false} role="img" aria-label="Добавление" />
<EditIcon aria-hidden={false} role="img" aria-label="Редактирование" />
<DeleteIcon aria-hidden={false} role="img" aria-label="Удаление" />
</div>
),
};
+34
View File
@@ -0,0 +1,34 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { AddIcon, DeleteIcon, EditIcon } from "./index";
describe("icon accessibility", () => {
it.each([AddIcon, EditIcon, DeleteIcon])(
"keeps decoration hidden and exposes explicitly named images: %s",
(Icon) => {
const { rerender } = render(
<button type="button" aria-label="Действие">
<Icon />
</button>,
);
expect(screen.queryByRole("img")).not.toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Действие" }).querySelector("svg"),
).toHaveAttribute("focusable", "false");
rerender(
<Icon
aria-hidden={false}
role="img"
aria-label="Пример"
size={18}
strokeWidth={2}
/>,
);
const icon = screen.getByRole("img", { name: "Пример" });
expect(icon).toHaveAttribute("width", "18");
expect(icon).toHaveAttribute("height", "18");
expect(icon).toHaveAttribute("stroke-width", "2");
expect(icon).toHaveAttribute("stroke", "currentColor");
},
);
});
+4
View File
@@ -0,0 +1,4 @@
export { AddIcon } from "./svg/AddIcon";
export { DeleteIcon } from "./svg/DeleteIcon";
export { EditIcon } from "./svg/EditIcon";
export type { IconProps } from "./types";
+24
View File
@@ -0,0 +1,24 @@
import type { FC } from "react";
import type { IconProps } from "../types";
const AddIcon: FC<IconProps> = (props) => {
const { size = "24", ...svgProps } = props;
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
aria-hidden="true"
focusable="false"
{...svgProps}
>
<path d="M12 5v14 M5 12h14" />
</svg>
);
};
export { AddIcon };
+25
View File
@@ -0,0 +1,25 @@
import type { FC } from "react";
import type { IconProps } from "../types";
const DeleteIcon: FC<IconProps> = (props) => {
const { size = "24", ...svgProps } = props;
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
focusable="false"
{...svgProps}
>
<path d="M3 6h18 M9 6V3h6v3 M5 6l1 15h12l1-15 M10 10v7 M14 10v7" />
</svg>
);
};
export { DeleteIcon };
+25
View File
@@ -0,0 +1,25 @@
import type { FC } from "react";
import type { IconProps } from "../types";
const EditIcon: FC<IconProps> = (props) => {
const { size = "24", ...svgProps } = props;
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
focusable="false"
{...svgProps}
>
<path d="m16 3 5 5-12 12-6 1 1-6L16 3Z M13 6l5 5" />
</svg>
);
};
export { EditIcon };
+10
View File
@@ -0,0 +1,10 @@
import type { ComponentPropsWithoutRef } from "react";
type IconProps = Omit<
ComponentPropsWithoutRef<"svg">,
"children" | "dangerouslySetInnerHTML"
> & {
size?: number | string;
};
export type { IconProps };