feat: Tabs (0.7.0)
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@tnsu/ui-kit",
|
"name": "@tnsu/ui-kit",
|
||||||
"version": "0.6.0",
|
"version": "0.7.0",
|
||||||
"description": "TNSU shared UI components for SPFx web parts",
|
"description": "TNSU shared UI components for SPFx web parts",
|
||||||
"author": "Timur Gorodetskiy",
|
"author": "Timur Gorodetskiy",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
@import '../tokens/tokens';
|
||||||
|
|
||||||
|
// Светлая полоса вкладок; активная — navy с золотым подчёркиванием.
|
||||||
|
// На узких экранах — горизонтальная прокрутка (не ломается, не переносится «лесенкой»).
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: $sp-2;
|
||||||
|
border-bottom: 1px solid $gray-200;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $sp-2;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
color: $gray-500;
|
||||||
|
font-family: $font-sans;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: .2px;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color .18s, border-color .18s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $navy-800;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
color: $navy-900;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom-color: $gold-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
color: $gray-400;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Tabs, ITab } from './Tabs';
|
||||||
|
|
||||||
|
const QUEUE: ITab[] = [
|
||||||
|
{ id: 'pending', label: 'На согласовании у меня', count: 1 },
|
||||||
|
{ id: 'mine', label: 'Инициализированные мной', count: 71 },
|
||||||
|
{ id: 'approved', label: 'Согласованные мной', count: 4 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const meta: Meta<typeof Tabs> = {
|
||||||
|
title: 'Навигация/Tabs',
|
||||||
|
component: Tabs,
|
||||||
|
tags: ['autodocs']
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Tabs>;
|
||||||
|
|
||||||
|
// Вкладки-очереди реестра (как в дашборде myDocuments).
|
||||||
|
export const Queues: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [active, setActive] = useState<string | number>('pending');
|
||||||
|
return <Tabs tabs={QUEUE} activeId={active} onChange={setActive} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Вкладки категорий с иконками.
|
||||||
|
export const WithIcons: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [active, setActive] = useState<string | number>('rec');
|
||||||
|
return (
|
||||||
|
<Tabs
|
||||||
|
activeId={active}
|
||||||
|
onChange={setActive}
|
||||||
|
tabs={[
|
||||||
|
{ id: 'rec', label: 'Отдел рекрутинга', icon: 'people' },
|
||||||
|
{ id: 'it', label: 'IT', icon: 'monitor' },
|
||||||
|
{ id: 'buh', label: 'Бухгалтерия', icon: 'calculator' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import styles from './Tabs.module.scss';
|
||||||
|
import { Icon } from '../Icon/Icon';
|
||||||
|
import { IconName } from '../Icon/iconNames';
|
||||||
|
|
||||||
|
export interface ITab {
|
||||||
|
id: string | number;
|
||||||
|
label: React.ReactNode;
|
||||||
|
/** Счётчик после подписи, напр. «(71)». undefined/0 — не показывать. */
|
||||||
|
count?: number;
|
||||||
|
/** Иконка слева (необязательно). */
|
||||||
|
icon?: IconName;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITabsProps {
|
||||||
|
tabs: ITab[];
|
||||||
|
activeId: string | number;
|
||||||
|
onChange: (id: string | number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Горизонтальные вкладки: активная — navy с золотым подчёркиванием. Управляемый.
|
||||||
|
* На узких экранах прокручивается вбок (не ломается). Подходит и для очередей со
|
||||||
|
* счётчиками (реестр), и для категорий с иконками.
|
||||||
|
*/
|
||||||
|
export const Tabs: React.FC<ITabsProps> = ({ tabs, activeId, onChange }) => (
|
||||||
|
<nav className={styles.tabs} aria-label="Вкладки">
|
||||||
|
{tabs.map((t) => {
|
||||||
|
const isActive = t.id === activeId;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={t.id}
|
||||||
|
type="button"
|
||||||
|
className={[styles.tab, isActive ? styles.active : ''].filter(Boolean).join(' ')}
|
||||||
|
aria-current={isActive ? 'page' : undefined}
|
||||||
|
onClick={() => onChange(t.id)}
|
||||||
|
>
|
||||||
|
{t.icon && <Icon name={t.icon} size="sm" />}
|
||||||
|
<span>{t.label}</span>
|
||||||
|
{!!t.count && <span className={styles.count}>({t.count})</span>}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
@@ -50,6 +50,8 @@ export { confirmModal } from './ConfirmDialog/confirmModal';
|
|||||||
export type { IConfirmOptions } from './ConfirmDialog/confirmModal';
|
export type { IConfirmOptions } from './ConfirmDialog/confirmModal';
|
||||||
|
|
||||||
// --- Навигация ---
|
// --- Навигация ---
|
||||||
|
export { Tabs } from './Tabs/Tabs';
|
||||||
|
export type { ITab, ITabsProps } from './Tabs/Tabs';
|
||||||
export { Stepper, highestReachableIndex } from './Stepper/Stepper';
|
export { Stepper, highestReachableIndex } from './Stepper/Stepper';
|
||||||
export type { IStepperStep, IStepperProps } from './Stepper/Stepper';
|
export type { IStepperStep, IStepperProps } from './Stepper/Stepper';
|
||||||
export { Sidebar } from './Sidebar/Sidebar';
|
export { Sidebar } from './Sidebar/Sidebar';
|
||||||
|
|||||||
Reference in New Issue
Block a user