prime-ui-kit 0.11.0 → 0.12.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/README.md +2 -1
- package/bin/prime-ui-kit.js +0 -0
- package/dist/chunks/{chunk-GKX3QAUC.js → chunk-OTOIOHVE.js} +1 -1
- package/dist/chunks/{chunk-WDTSHOTD.js → chunk-VOZ7J72X.js} +7 -2
- package/dist/chunks/{chunk-CUH6DJ57.js → chunk-YJO3NJIJ.js} +716 -548
- package/dist/color-picker.js +2 -2
- package/dist/components/index.d.ts +2 -0
- package/dist/components/list/List.d.ts +105 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -2
- package/dist/internal/VisuallyHidden.d.ts +6 -1
- package/dist/skill/api-contract.md +1 -1
- package/dist/skill/cheatsheet.md +1 -1
- package/dist/skill/choosing.md +4 -2
- package/dist/skill/components.md +2 -1
- package/dist/skill/composition.md +8 -0
- package/dist/skill/layouts.md +2 -1
- package/dist/skill/patterns/app-shell.module.css +18 -0
- package/dist/skill/patterns/app-shell.tsx +301 -0
- package/dist/skill/patterns/chat.module.css +0 -16
- package/dist/skill/patterns/chat.tsx +16 -19
- package/dist/skill/patterns/kanban-board.module.css +28 -0
- package/dist/skill/patterns/kanban-board.tsx +618 -0
- package/dist/skill/patterns/media-library.module.css +66 -0
- package/dist/skill/patterns/media-library.tsx +638 -0
- package/dist/skill/patterns/schedule.module.css +22 -0
- package/dist/skill/patterns/schedule.tsx +710 -0
- package/dist/skill/reference/components/file-upload/COMPONENT.md +3 -1
- package/dist/skill/reference/components/list/COMPONENT.md +180 -0
- package/dist/skill/reference/components/list/examples/as-link.tsx +37 -0
- package/dist/skill/reference/components/list/examples/examples.module.css +18 -0
- package/dist/skill/reference/components/list/examples/overflow.tsx +46 -0
- package/dist/skill/reference/components/list/examples/overview.tsx +63 -0
- package/dist/skill/reference/components/list/examples/sizes.tsx +35 -0
- package/dist/skill/reference/components/list/examples/states.tsx +27 -0
- package/dist/skill/reference/components/list/examples/with-icon.tsx +60 -0
- package/dist/skill/reference/components/page-content/COMPONENT.md +2 -2
- package/dist/skill/reference/components/page-content/examples/examples.module.css +5 -12
- package/dist/skill/reference/components/page-content/examples/list-detail.tsx +13 -18
- package/dist/skill/reference/components/visually-hidden/COMPONENT.md +6 -3
- package/dist/skill/reference/components/visually-hidden/examples/overview.tsx +2 -2
- package/package.json +3 -3
- package/src/components/file-upload/COMPONENT.md +3 -1
- package/src/components/list/COMPONENT.md +180 -0
- package/src/components/list/examples/as-link.tsx +37 -0
- package/src/components/list/examples/examples.module.css +18 -0
- package/src/components/list/examples/overflow.tsx +46 -0
- package/src/components/list/examples/overview.tsx +63 -0
- package/src/components/list/examples/sizes.tsx +35 -0
- package/src/components/list/examples/states.tsx +27 -0
- package/src/components/list/examples/with-icon.tsx +60 -0
- package/src/components/page-content/COMPONENT.md +2 -2
- package/src/components/page-content/examples/examples.module.css +5 -12
- package/src/components/page-content/examples/list-detail.tsx +13 -18
- package/src/components/visually-hidden/COMPONENT.md +6 -3
- package/src/components/visually-hidden/examples/overview.tsx +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * from "./components/index";
|
|
|
2
2
|
export * from "./hooks/useTokenValue";
|
|
3
3
|
export * from "./hooks/useUploadQueue";
|
|
4
4
|
export * from "./icons/index";
|
|
5
|
+
export { formatFileSize } from "./internal/fileSize";
|
|
5
6
|
export { hueOf } from "./internal/palette";
|
|
6
7
|
export * from "./internal/states";
|
|
7
8
|
export type { TransferDirection, TransferHandler, TransferState } from "./internal/transfer";
|
package/dist/index.js
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
Kanban,
|
|
34
34
|
Kbd,
|
|
35
35
|
LinkButton,
|
|
36
|
+
List,
|
|
36
37
|
LoginForm,
|
|
37
38
|
MediaViewer,
|
|
38
39
|
Message,
|
|
@@ -67,6 +68,7 @@ import {
|
|
|
67
68
|
chartColors,
|
|
68
69
|
datepickerPresets,
|
|
69
70
|
formatDatepickerValue,
|
|
71
|
+
formatFileSize,
|
|
70
72
|
formatMessageDate,
|
|
71
73
|
formatMessageTime,
|
|
72
74
|
getWaveform,
|
|
@@ -81,7 +83,7 @@ import {
|
|
|
81
83
|
useSidebar,
|
|
82
84
|
useTokenValue,
|
|
83
85
|
useUploadQueue
|
|
84
|
-
} from "./chunks/chunk-
|
|
86
|
+
} from "./chunks/chunk-YJO3NJIJ.js";
|
|
85
87
|
import {
|
|
86
88
|
Avatar,
|
|
87
89
|
Badge,
|
|
@@ -113,7 +115,7 @@ import {
|
|
|
113
115
|
textTones,
|
|
114
116
|
tones,
|
|
115
117
|
variants
|
|
116
|
-
} from "./chunks/chunk-
|
|
118
|
+
} from "./chunks/chunk-VOZ7J72X.js";
|
|
117
119
|
import {
|
|
118
120
|
ControlSizeProvider,
|
|
119
121
|
useOptionalControlSize
|
|
@@ -164,6 +166,7 @@ export {
|
|
|
164
166
|
Kbd,
|
|
165
167
|
Label,
|
|
166
168
|
LinkButton,
|
|
169
|
+
List,
|
|
167
170
|
LoginForm,
|
|
168
171
|
MediaViewer,
|
|
169
172
|
Message,
|
|
@@ -210,6 +213,7 @@ export {
|
|
|
210
213
|
dataStates,
|
|
211
214
|
datepickerPresets,
|
|
212
215
|
formatDatepickerValue,
|
|
216
|
+
formatFileSize,
|
|
213
217
|
formatMessageDate,
|
|
214
218
|
formatMessageTime,
|
|
215
219
|
getPasswordStrength,
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type * as React from "react";
|
|
2
2
|
export type VisuallyHiddenProps = React.HTMLAttributes<HTMLSpanElement> & {
|
|
3
|
+
/**
|
|
4
|
+
* An announcement region: `polite` — `role="status"`, read after what is being said; `assertive`
|
|
5
|
+
* — `role="alert"`, read at once. Both are atomic: the whole text is read on every change.
|
|
6
|
+
*/
|
|
7
|
+
live?: "polite" | "assertive";
|
|
3
8
|
ref?: React.Ref<HTMLSpanElement>;
|
|
4
9
|
};
|
|
5
10
|
/** The sr-only recipe as a class, for an element that must stay itself (a focusable file input). */
|
|
@@ -9,7 +14,7 @@ export declare const visuallyHiddenClass: string;
|
|
|
9
14
|
* Public as `VisuallyHidden` (docs in `src/components/visually-hidden/`); inside the kit it replaces
|
|
10
15
|
* the per-component `.srOnly` / `.visuallyHidden` CSS copies.
|
|
11
16
|
*/
|
|
12
|
-
export declare function VisuallyHidden({ className, ...rest }: VisuallyHiddenProps): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
export declare function VisuallyHidden({ className, live, ...rest }: VisuallyHiddenProps): import("react/jsx-runtime").JSX.Element;
|
|
13
18
|
export declare namespace VisuallyHidden {
|
|
14
19
|
var displayName: string;
|
|
15
20
|
}
|
|
@@ -235,7 +235,7 @@ it saves).
|
|
|
235
235
|
| `Icon`, `createIcon` | kit icon set and domain glyphs — see Icons below |
|
|
236
236
|
| `chartColors` | the chart palette as CSS values for a chart library: `series` 1…8 in order, `other`, `grid`, `axis`, `scale` — follows the theme |
|
|
237
237
|
| `useTokenValue("--prime-space-7")` | a length token in px for geometry CSS cannot take (SVG coordinates, canvas, virtual rows); follows a theme switch |
|
|
238
|
-
| `VisuallyHidden` | text for screen readers only (an icon-only part's name, a spoken count); `
|
|
238
|
+
| `VisuallyHidden` | text for screen readers only (an icon-only part's name, a spoken count); `live="polite"` on it for an announcement (`"assertive"` only for an error that blocks) |
|
|
239
239
|
|
|
240
240
|
## Icons
|
|
241
241
|
|
package/dist/skill/cheatsheet.md
CHANGED
|
@@ -163,7 +163,7 @@ Full rules: [responsive.md](responsive.md).
|
|
|
163
163
|
- Breakpoints 640 · 768 · 1024 · 1280 only for the app frame; inside a page — intrinsic grids (`auto-fill, minmax`) and container queries.
|
|
164
164
|
- A wide table scrolls inside itself; offer `DataTable.ColumnsMenu` in its `toolbar` — never drop columns by breakpoint, never hand-build the chooser.
|
|
165
165
|
- A row that opens a record page is `getRowHref` (a real link: ⌘-click, new tab), a row that opens a drawer is `onRowClick` — never `navigate()` in `onRowClick`, never your own `event.target` checks for buttons in the row.
|
|
166
|
-
- A list + detail screen is `PageContent.Split` in a `PageContent.Root fill` page; a full-height table is `scrollHeight="fill"` — never a height computed from `100vh`.
|
|
166
|
+
- A list + detail screen is `PageContent.Split` in a `PageContent.Root fill` page, its records a `List` (`current` on the open one) — never ghost Buttons with a soft current; a full-height table is `scrollHeight="fill"` — never a height computed from `100vh`.
|
|
167
167
|
- App sections on phones: `BottomNav` in `AppShell.Footer` — not in Main, not `position: fixed`, no own media query: the shell hides the footer while the Sidebar rail is on screen; a short task from the bottom edge: `Sheet` — Select, Dropdown, Popover and Modal become sheets below 640 by themselves.
|
|
168
168
|
- Your hover styles inside `@media (hover: hover)`; nothing hover-only. Do not wrap kit fields to raise their text for iOS — they keep 16px on touch already.
|
|
169
169
|
- A long form saves through the panel action with a count («Сохранить 3 изменения») — not a sticky bar at the bottom of a phone.
|
package/dist/skill/choosing.md
CHANGED
|
@@ -13,7 +13,7 @@ confuse. Links: [components.md](components.md). Whole screens: [composition.md](
|
|
|
13
13
|
| see data: status labels, people, objects, numbers and their trend, properties of a record, rows, a board, a schedule, events, code, disclosed sections | Data display — Badge, Avatar, Thumbnail, Kbd, Card, DescriptionList, DataTable, Kanban, Gantt, Timeline, CodeBlock, Accordion, Sparkline, ChartFrame |
|
|
14
14
|
| hold a conversation: read and send chat messages of every kind | Messages — Chat, Message, Gallery, VoicePlayer, VideoNote, Composer |
|
|
15
15
|
| learn what happened, how far along it is, or watch a region load and change state; say it to a screen reader | Status and loading — Banner, Notification, ProgressBar, ProgressCircle, Spinner, Skeleton, Crossfade, EmptyPage, VisuallyHidden |
|
|
16
|
-
| move between places: app sections, panels of a screen, the path, pages, steps | Navigation — Tabs, Breadcrumb, Pagination, Stepper, Sidebar, BottomNav |
|
|
16
|
+
| move between places: app sections, panels of a screen, the records of a list, the path, pages, steps | Navigation — Tabs, Breadcrumb, Pagination, List, Stepper, Sidebar, BottomNav |
|
|
17
17
|
| see something on top of the page, or a photo or video full screen | Overlays — Tooltip, Popover, Dropdown, Modal, ConfirmDialog, Drawer, Sheet, CommandMenu, MediaViewer |
|
|
18
18
|
| get the app frame and the structure of a page | Page — AppShell, AppHeader, PageContent, PageToolbar, Divider, ScrollContainer |
|
|
19
19
|
| drag things by hand | Interaction — Dnd (a status board: Kanban in Data display) |
|
|
@@ -125,7 +125,8 @@ filter above a single table goes into the DataTable `toolbar` instead. A page th
|
|
|
125
125
|
height to the work (a board, a chat) may let `AppHeader` carry the page instead: `AppHeader.Title
|
|
126
126
|
as="h1"` and the PageToolbar in `AppHeader.Tools` — then main has no `PageContent.Header`. A record
|
|
127
127
|
opened from a table: its own page → DataTable `getRowHref`; in place → `onRowClick` + Drawer; a list
|
|
128
|
-
worked through record by record (mail, chat, tickets) → `PageContent.Split
|
|
128
|
+
worked through record by record (mail, chat, tickets) → `PageContent.Split` with a `List` of the records
|
|
129
|
+
(`current` on the open one). Sections of a long rail → `Sidebar.Group
|
|
129
130
|
collapsible`; a section with sub-pages → `Sidebar.Sub`; the signed-in user → `Sidebar.Account` in a
|
|
130
131
|
`Dropdown.Trigger` at the bottom of `Sidebar.Footer`.
|
|
131
132
|
|
|
@@ -133,6 +134,7 @@ collapsible`; a section with sub-pages → `Sidebar.Sub`; the signed-in user →
|
|
|
133
134
|
|
|
134
135
|
| Pair | Rule |
|
|
135
136
|
|---|---|
|
|
137
|
+
| List / Sidebar / Tabs / Card list / DataTable | List: the records of a list + detail screen (dialogs, tickets, mail), one open at a time (`current`), each row a button or a link with a text line, a preview, meta and a count. Sidebar: the sections of the **app**. Tabs: panels of **one screen**. `Card.List` (`variant="list"`): static facts in a card, nothing opens. DataTable: many records compared by columns, sorted, filtered, selected in bulk. |
|
|
136
138
|
| Tabs / SegmentedControl | Tabs switch **panels of content** (navigation inside a screen). SegmentedControl switches a **value or mode** of the same content (period, view mode, filter). |
|
|
137
139
|
| Modal / Drawer / Sheet / Popover / Tooltip | Modal: blocking decision or a short form (≤ 4 fields) that needs full attention. Drawer: long form, filters, record details beside the page, while keeping page context. Sheet: a short task that ends by itself — a pick, an action, a short note — raised from the bottom edge. Popover: small non-modal panel anchored to a control (quick edit, explanation with a link). Tooltip: a few words naming or explaining a control, no interactive content. |
|
|
138
140
|
| Select / NativeSelect | Select: the kit's floating list — icons, descriptions, groups, search, `multiple`, `clearable`. NativeSelect: a plain text list where the OS picker is better (phones) or the value must post with a form by `name`. |
|
package/dist/skill/components.md
CHANGED
|
@@ -103,13 +103,14 @@ Say what happens: system messages, progress, loading placeholders, state swaps,
|
|
|
103
103
|
|
|
104
104
|
## Navigation (`navigation`)
|
|
105
105
|
|
|
106
|
-
Move between places: app navigation (side rail, bottom bar on phones), tabs, the path, pages, steps.
|
|
106
|
+
Move between places: app navigation (side rail, bottom bar on phones), tabs, the records of a list, the path, pages, steps.
|
|
107
107
|
|
|
108
108
|
| Component | Purpose | Docs |
|
|
109
109
|
|---|---|---|
|
|
110
110
|
| Tabs | Tabs for navigating between content panels of one screen. | [COMPONENT.md](reference/components/tabs/COMPONENT.md) · [examples](reference/components/tabs/examples/) |
|
|
111
111
|
| Breadcrumb | Breadcrumbs: the path to the current page. | [COMPONENT.md](reference/components/breadcrumb/COMPONENT.md) · [examples](reference/components/breadcrumb/examples/) |
|
|
112
112
|
| Pagination | Page-by-page navigation: arrows, page numbers with ellipsis and a compact «3 / 12» view. | [COMPONENT.md](reference/components/pagination/COMPONENT.md) · [examples](reference/components/pagination/examples/) |
|
|
113
|
+
| List | A selectable list of rows — the list half of a list + detail screen: conversations, tickets, mail, records; the open one is `current`. | [COMPONENT.md](reference/components/list/COMPONENT.md) · [examples](reference/components/list/examples/) |
|
|
113
114
|
| Stepper | Steps of a multi-step process with pending, active, completed and danger statuses. | [COMPONENT.md](reference/components/stepper/COMPONENT.md) · [examples](reference/components/stepper/examples/) |
|
|
114
115
|
| Sidebar | App side navigation in three modes — expanded, compact, hidden — and an off-canvas panel on narrow screens. | [COMPONENT.md](reference/layout/sidebar/COMPONENT.md) · [examples](reference/layout/sidebar/examples/) |
|
|
115
116
|
| BottomNav | Phone navigation: 2–6 sections at the bottom, flat or a floating glass capsule, icons with or without labels, an optional round action. | [COMPONENT.md](reference/layout/bottom-nav/COMPONENT.md) · [examples](reference/layout/bottom-nav/examples/) |
|
|
@@ -18,6 +18,10 @@ is, and is shown live with its code in the playground (category «Компози
|
|
|
18
18
|
| Dashboard | [dashboard.tsx](patterns/dashboard.tsx) | period switch, KPI row that cross-fades to the new period, a Sparkline revenue trend, invoiced vs paid in a ChartFrame, panels with progress and a split, short table + link to the list |
|
|
19
19
|
| Screen states | [screen-states.tsx](patterns/screen-states.tsx) | page error in a Banner with retry, region loading with a Skeleton, Crossfade between states, table error in place, first-run EmptyPage |
|
|
20
20
|
| Messenger | [chat.tsx](patterns/chat.tsx) | PageContent.Split of dialogs and a `Chat`: `Chat.Header` with the back button, avatar, title, the status that turns into «печатает…» (`typing`) and the search button, `Chat.Search`, `Chat.Pinned`, `Chat.Messages` from `items` + a stable `renderMessage` (dates, runs, the unread band and service events from the data, history loaded above with `hasOlder` / `onLoadOlder`), every kind of Message (photos, file, contact, voice in a VoicePlayer with a transcript, round video), reactions, reply and edit from the menu through `Composer.Context value` (`replyTo` / `editOf`), the jump to the quoted message (`Message.Reply target`), optimistic sends whose messages rise from the composer while the client's answer slides in, «печатает…» both ways (`onActivityChange` → `Chat.Root typing`), unread counts from `onNewMessage` and `onRead`, selection with `Chat.SelectionBar`, delivery ticks, the Composer in `Chat.Footer` |
|
|
21
|
+
| Sales pipeline board | [kanban-board.tsx](patterns/kanban-board.tsx) | header with one primary action, PageToolbar with SmartFilter search and a board / list SegmentedControl, Crossfade between a Kanban and a DataTable of the same deals, column totals in `renderColumnMeta`, moves saved through an `onValueChange` promise that the server can refuse, movable stages with locked first and last, the deal in a Drawer with a DescriptionList and stage actions, skeleton first load |
|
|
22
|
+
| App frame for desktop and phone (root layout) | [app-shell.tsx](patterns/app-shell.tsx) | Sidebar rail on a wide screen, floating BottomNav in `AppShell.Footer` on a phone («Ещё» opens the Sidebar panel), AppHeader with the menu button, title and search to CommandMenu, counts on items, a home page that cross-fades between sections |
|
|
23
|
+
| Media library | [media-library.tsx](patterns/media-library.tsx) | PageToolbar with sections and counts, search (`SmartFilter` with `fields={[]}`), order and «Загрузить»; uploads through `useUploadQueue` in `FileUpload` tiles (`trigger="tile"`, `layout="grid"`) with progress, a failure and retry, a done file moving into the library; a grid of `Thumbnail` / `Thumbnail.Video` tiles that open `MediaViewer` out of the tile (`getOrigin`, `thumbnails`, `renderActions`) or, in selection mode, are picked (`checked`) for bulk download and a `ConfirmDialog` delete; a Skeleton first load and a filtered `EmptyPage` in a `Crossfade` |
|
|
24
|
+
| Resource schedule | [schedule.tsx](patterns/schedule.tsx) | full-height `Gantt` (`scrollHeight="fill"`) under a `PageToolbar` with search, role / client filters and team sections; people with avatars, client hues, lanes for overlaps, locked absences and milestones; moves saved through an async `onBarChange` or refused and glided back, new work drawn with `onBarCreate` and named in a Modal (cancel fades the ghost), `onSelect` details in a Drawer with a `DescriptionList` and a confirmed removal, `loading` and filtered `empty` |
|
|
21
25
|
|
|
22
26
|
Pick the closest one, rename the domain, keep the skeleton. Change structure only when the task needs
|
|
23
27
|
it, and then by the rules below.
|
|
@@ -176,6 +180,10 @@ Hierarchy comes from space and a handful of type roles, not from size jumps, col
|
|
|
176
180
|
|
|
177
181
|
- A list page is `DataTable` with the filters in its `toolbar` — `SmartFilter.Toolbar` and
|
|
178
182
|
`SmartFilter.Chips` inside `SmartFilter.Root` (or one SegmentedControl / Select for a single field).
|
|
183
|
+
- Where the filters go: tools of one table live in its `toolbar`; when the page itself carries
|
|
184
|
+
sections with counts, a view switch (board / list, grid / list) or filters that drive more than one
|
|
185
|
+
table — a board, a schedule, a media grid — the page gets `PageToolbar` and the table keeps only its
|
|
186
|
+
own tools (`DataTable.ColumnsMenu`).
|
|
179
187
|
- Paging is built in: `paging="pages"` (default, Pagination and the «Показано 1–6 из 10» range in the
|
|
180
188
|
footer), `paging="infinite"` for feeds, `paging="none"` for short lists on dashboards and details.
|
|
181
189
|
- When the server holds more than you load (a CRM list, a register): `source="server"` — `rows` is the
|
package/dist/skill/layouts.md
CHANGED
|
@@ -191,7 +191,8 @@ from what people do, not from how the backend is split. The full guide with the
|
|
|
191
191
|
`PageContent.Detail` in a filling page. Side by side from 48rem of the split's own width, one pane
|
|
192
192
|
at a time below it with `PageContent.BackButton` first in the detail's header; `open` is «a record
|
|
193
193
|
is chosen». Each pane gives its last child the free height: a Card with its own `ScrollContainer`
|
|
194
|
-
(`anchor="end"` for a thread).
|
|
194
|
+
(`anchor="end"` for a thread). The records in the list pane are a `List` (`current` on the open
|
|
195
|
+
one). Details that open over a table and close again are a Drawer instead.
|
|
195
196
|
|
|
196
197
|
## Which pattern to start from
|
|
197
198
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* App shell: the frame (Sidebar / BottomNav / AppHeader) lays itself out; this file only places the
|
|
2
|
+
home page's blocks. Blocks 40 apart come from PageContent.Body. */
|
|
3
|
+
|
|
4
|
+
/* KPI row: as many ~14rem tiles as fit, one column on a phone. */
|
|
5
|
+
.kpis {
|
|
6
|
+
display: grid;
|
|
7
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, calc(var(--prime-space-14) * 4)), 1fr));
|
|
8
|
+
gap: var(--prime-space-4);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* A list row: the text at the start, the meta (time, sum) at the end. */
|
|
12
|
+
.row {
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: space-between;
|
|
16
|
+
gap: var(--prime-space-4);
|
|
17
|
+
min-width: 0;
|
|
18
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/** The app frame for desktop and phone: the Sidebar rail on a wide screen, the floating BottomNav in the footer on a phone, the AppHeader with the menu button, the title and search that opens the CommandMenu, and a simple home page that swaps with the current section. */
|
|
2
|
+
import {
|
|
3
|
+
AppHeader,
|
|
4
|
+
AppShell,
|
|
5
|
+
BottomNav,
|
|
6
|
+
Card,
|
|
7
|
+
Checkbox,
|
|
8
|
+
CommandMenu,
|
|
9
|
+
Crossfade,
|
|
10
|
+
Icon,
|
|
11
|
+
type IconName,
|
|
12
|
+
PageContent,
|
|
13
|
+
Sidebar,
|
|
14
|
+
Typography,
|
|
15
|
+
} from "prime-ui-kit";
|
|
16
|
+
import * as React from "react";
|
|
17
|
+
|
|
18
|
+
import styles from "./app-shell.module.css";
|
|
19
|
+
|
|
20
|
+
type SectionId = "today" | "deals" | "clients" | "tasks" | "reports" | "settings";
|
|
21
|
+
type Section = { id: SectionId; label: string; icon: IconName };
|
|
22
|
+
type Row = { id: string; title: string; meta: string };
|
|
23
|
+
|
|
24
|
+
/** The main sections: the Sidebar's first group on a wide screen, the bar on a phone. */
|
|
25
|
+
const MAIN: Section[] = [
|
|
26
|
+
{ id: "today", label: "Сегодня", icon: "nav.home" },
|
|
27
|
+
{ id: "deals", label: "Сделки", icon: "object.funnel" },
|
|
28
|
+
{ id: "clients", label: "Клиенты", icon: "object.users" },
|
|
29
|
+
{ id: "tasks", label: "Задачи", icon: "object.tasks" },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/** The rest of the map: in the Sidebar footer; on a phone «Ещё» opens the Sidebar panel. */
|
|
33
|
+
const MORE: Section[] = [
|
|
34
|
+
{ id: "reports", label: "Отчёты", icon: "object.chart" },
|
|
35
|
+
{ id: "settings", label: "Настройки", icon: "action.settings" },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const SECTIONS = [...MAIN, ...MORE];
|
|
39
|
+
|
|
40
|
+
/** Open deals: an informational count, plain in the Sidebar. */
|
|
41
|
+
const OPEN_DEALS = 12;
|
|
42
|
+
|
|
43
|
+
const KPIS = [
|
|
44
|
+
{ label: "Сделки в работе", value: "37", delta: "+4 за неделю", tone: "success" },
|
|
45
|
+
{ label: "Выручка за октябрь", value: "4,8 млн ₽", delta: "+12% к сентябрю", tone: "success" },
|
|
46
|
+
{ label: "Ждут ответа", value: "5 клиентов", delta: "2 дольше суток", tone: "danger" },
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
49
|
+
const TASKS: Row[] = [
|
|
50
|
+
{ id: "t1", title: "Позвонить в ООО «Север» по договору поставки", meta: "10:30" },
|
|
51
|
+
{ id: "t2", title: "Отправить КП для АО «Меридиан»", meta: "12:00" },
|
|
52
|
+
{ id: "t3", title: "Согласовать скидку для ИП Котова", meta: "15:00" },
|
|
53
|
+
{ id: "t4", title: "Подготовить отчёт по воронке за неделю", meta: "17:30" },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/** What every section other than «Сегодня» lists. */
|
|
57
|
+
const PAGES: Record<Exclude<SectionId, "today">, { description: string; rows: Row[] }> = {
|
|
58
|
+
deals: {
|
|
59
|
+
description: "12 сделок в работе на 9,6 млн ₽.",
|
|
60
|
+
rows: [
|
|
61
|
+
{ id: "d1", title: "ООО «Север» · поставка оборудования", meta: "2 400 000 ₽" },
|
|
62
|
+
{ id: "d2", title: "АО «Меридиан» · годовой сервис", meta: "1 180 000 ₽" },
|
|
63
|
+
{ id: "d3", title: "ООО «Вектор» · внедрение CRM", meta: "860 000 ₽" },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
clients: {
|
|
67
|
+
description: "248 клиентов, 5 ждут ответа.",
|
|
68
|
+
rows: [
|
|
69
|
+
{ id: "c1", title: "ООО «Север»", meta: "4 сделки" },
|
|
70
|
+
{ id: "c2", title: "АО «Меридиан»", meta: "2 сделки" },
|
|
71
|
+
{ id: "c3", title: "ИП Котов", meta: "1 сделка" },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
tasks: {
|
|
75
|
+
description: "Все задачи команды по срокам.",
|
|
76
|
+
rows: TASKS.map((task) => ({ ...task, meta: `сегодня, ${task.meta}` })),
|
|
77
|
+
},
|
|
78
|
+
reports: {
|
|
79
|
+
description: "Сводки по продажам и работе менеджеров.",
|
|
80
|
+
rows: [
|
|
81
|
+
{ id: "r1", title: "Воронка продаж", meta: "обновлён сегодня" },
|
|
82
|
+
{ id: "r2", title: "План и факт по менеджерам", meta: "октябрь" },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
settings: {
|
|
86
|
+
description: "Компания, команда и интеграции.",
|
|
87
|
+
rows: [
|
|
88
|
+
{ id: "s1", title: "Команда и роли", meta: "8 человек" },
|
|
89
|
+
{ id: "s2", title: "Почта и телефония", meta: "подключены" },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const isMore = (id: SectionId) => MORE.some((section) => section.id === id);
|
|
95
|
+
|
|
96
|
+
export default function AppShellPattern() {
|
|
97
|
+
const [current, setCurrent] = React.useState<SectionId>("today");
|
|
98
|
+
const [menuOpen, setMenuOpen] = React.useState(false);
|
|
99
|
+
const [searchOpen, setSearchOpen] = React.useState(false);
|
|
100
|
+
const [done, setDone] = React.useState<ReadonlySet<string>>(() => new Set(["t1"]));
|
|
101
|
+
|
|
102
|
+
const openTasks = TASKS.length - done.size;
|
|
103
|
+
const section = SECTIONS.find((item) => item.id === current) ?? SECTIONS[0];
|
|
104
|
+
|
|
105
|
+
// One handler for every way into a section: the Sidebar, the bar and the search.
|
|
106
|
+
const go = (id: SectionId) => {
|
|
107
|
+
setCurrent(id);
|
|
108
|
+
setMenuOpen(false);
|
|
109
|
+
setSearchOpen(false);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const toggleTask = (id: string, checked: boolean) =>
|
|
113
|
+
setDone((prev) => {
|
|
114
|
+
const next = new Set(prev);
|
|
115
|
+
if (checked) next.add(id);
|
|
116
|
+
else next.delete(id);
|
|
117
|
+
return next;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ⌘K opens the same search as the header field.
|
|
121
|
+
React.useEffect(() => {
|
|
122
|
+
const onKey = (event: KeyboardEvent) => {
|
|
123
|
+
if (event.key === "k" && (event.metaKey || event.ctrlKey)) {
|
|
124
|
+
event.preventDefault();
|
|
125
|
+
setSearchOpen((open) => !open);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
document.addEventListener("keydown", onKey);
|
|
129
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
const countOf = (id: SectionId) => {
|
|
133
|
+
if (id === "deals") return <Sidebar.ItemCount>{OPEN_DEALS}</Sidebar.ItemCount>;
|
|
134
|
+
if (id === "tasks" && openTasks > 0) {
|
|
135
|
+
return <Sidebar.ItemCount color="red">{openTasks}</Sidebar.ItemCount>;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const sidebarItem = (item: Section) => (
|
|
141
|
+
<Sidebar.Item key={item.id} current={item.id === current} onClick={() => go(item.id)}>
|
|
142
|
+
<Sidebar.ItemIcon>
|
|
143
|
+
<Icon name={item.icon} />
|
|
144
|
+
</Sidebar.ItemIcon>
|
|
145
|
+
{item.label}
|
|
146
|
+
{countOf(item.id)}
|
|
147
|
+
</Sidebar.Item>
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<>
|
|
152
|
+
<AppShell.Template
|
|
153
|
+
fillViewport
|
|
154
|
+
scrollResetKey={current}
|
|
155
|
+
nav={
|
|
156
|
+
<Sidebar.Root open={menuOpen} onOpenChange={setMenuOpen}>
|
|
157
|
+
<Sidebar.Header>
|
|
158
|
+
<Sidebar.Brand description="Отдел продаж">
|
|
159
|
+
<Sidebar.BrandLogo>
|
|
160
|
+
<Icon name="object.funnel" />
|
|
161
|
+
</Sidebar.BrandLogo>
|
|
162
|
+
Acme CRM
|
|
163
|
+
</Sidebar.Brand>
|
|
164
|
+
<Sidebar.Toggle variant="header" />
|
|
165
|
+
</Sidebar.Header>
|
|
166
|
+
<Sidebar.Content>
|
|
167
|
+
<Sidebar.Group>{MAIN.map(sidebarItem)}</Sidebar.Group>
|
|
168
|
+
</Sidebar.Content>
|
|
169
|
+
<Sidebar.Footer>{MORE.map(sidebarItem)}</Sidebar.Footer>
|
|
170
|
+
</Sidebar.Root>
|
|
171
|
+
}
|
|
172
|
+
header={
|
|
173
|
+
<AppHeader.Root>
|
|
174
|
+
<AppHeader.Start>
|
|
175
|
+
<AppHeader.MenuButton aria-expanded={menuOpen} onClick={() => setMenuOpen(true)} />
|
|
176
|
+
<AppHeader.Title>{section.label}</AppHeader.Title>
|
|
177
|
+
</AppHeader.Start>
|
|
178
|
+
<AppHeader.Search onClick={() => setSearchOpen(true)}>
|
|
179
|
+
Поиск сделок и клиентов
|
|
180
|
+
</AppHeader.Search>
|
|
181
|
+
</AppHeader.Root>
|
|
182
|
+
}
|
|
183
|
+
footer={
|
|
184
|
+
<BottomNav.Root floating>
|
|
185
|
+
{MAIN.map((item) => (
|
|
186
|
+
<BottomNav.Item
|
|
187
|
+
key={item.id}
|
|
188
|
+
current={item.id === current}
|
|
189
|
+
onClick={() => go(item.id)}
|
|
190
|
+
>
|
|
191
|
+
<BottomNav.ItemIcon>
|
|
192
|
+
<Icon name={item.icon} />
|
|
193
|
+
</BottomNav.ItemIcon>
|
|
194
|
+
{item.id === "tasks" && openTasks > 0 ? (
|
|
195
|
+
<BottomNav.ItemCount>{openTasks}</BottomNav.ItemCount>
|
|
196
|
+
) : null}
|
|
197
|
+
{item.label}
|
|
198
|
+
</BottomNav.Item>
|
|
199
|
+
))}
|
|
200
|
+
{/* The rest of the map lives in the Sidebar: «Ещё» opens it as a panel. */}
|
|
201
|
+
<BottomNav.Item
|
|
202
|
+
current={isMore(current)}
|
|
203
|
+
aria-expanded={menuOpen}
|
|
204
|
+
onClick={() => setMenuOpen(true)}
|
|
205
|
+
>
|
|
206
|
+
<BottomNav.ItemIcon>
|
|
207
|
+
<Icon name="action.more" />
|
|
208
|
+
</BottomNav.ItemIcon>
|
|
209
|
+
Ещё
|
|
210
|
+
</BottomNav.Item>
|
|
211
|
+
</BottomNav.Root>
|
|
212
|
+
}
|
|
213
|
+
>
|
|
214
|
+
{/* Another section is another page: it cross-fades instead of flipping. */}
|
|
215
|
+
<Crossfade state={current}>
|
|
216
|
+
<PageContent.Section aria-labelledby="page-title">
|
|
217
|
+
<PageContent.Header>
|
|
218
|
+
<PageContent.Title id="page-title">{section.label}</PageContent.Title>
|
|
219
|
+
<PageContent.Description>
|
|
220
|
+
{current === "today"
|
|
221
|
+
? `Среда, 14 октября · открытых задач: ${openTasks}`
|
|
222
|
+
: PAGES[current].description}
|
|
223
|
+
</PageContent.Description>
|
|
224
|
+
</PageContent.Header>
|
|
225
|
+
<PageContent.Body>
|
|
226
|
+
{current === "today" ? (
|
|
227
|
+
<>
|
|
228
|
+
<div className={styles.kpis}>
|
|
229
|
+
{KPIS.map((kpi) => (
|
|
230
|
+
<Card.Root key={kpi.label} variant="stat-trend">
|
|
231
|
+
<Card.Label>{kpi.label}</Card.Label>
|
|
232
|
+
<Card.Value>{kpi.value}</Card.Value>
|
|
233
|
+
<Card.Delta tone={kpi.tone}>{kpi.delta}</Card.Delta>
|
|
234
|
+
</Card.Root>
|
|
235
|
+
))}
|
|
236
|
+
</div>
|
|
237
|
+
<Card.Root variant="list">
|
|
238
|
+
<Card.Header>
|
|
239
|
+
<Card.Title as="h2">Задачи на сегодня</Card.Title>
|
|
240
|
+
</Card.Header>
|
|
241
|
+
<Card.List>
|
|
242
|
+
{TASKS.map((task) => (
|
|
243
|
+
<Card.ListItem key={task.id}>
|
|
244
|
+
<span className={styles.row}>
|
|
245
|
+
<Checkbox.Root
|
|
246
|
+
checked={done.has(task.id)}
|
|
247
|
+
onCheckedChange={(checked) => toggleTask(task.id, checked)}
|
|
248
|
+
>
|
|
249
|
+
<Checkbox.Label>{task.title}</Checkbox.Label>
|
|
250
|
+
</Checkbox.Root>
|
|
251
|
+
<Typography as="span" variant="caption" tone="muted">
|
|
252
|
+
{task.meta}
|
|
253
|
+
</Typography>
|
|
254
|
+
</span>
|
|
255
|
+
</Card.ListItem>
|
|
256
|
+
))}
|
|
257
|
+
</Card.List>
|
|
258
|
+
</Card.Root>
|
|
259
|
+
</>
|
|
260
|
+
) : (
|
|
261
|
+
<Card.Root variant="list">
|
|
262
|
+
<Card.List>
|
|
263
|
+
{PAGES[current].rows.map((row) => (
|
|
264
|
+
<Card.ListItem key={row.id}>
|
|
265
|
+
<span className={styles.row}>
|
|
266
|
+
<Typography as="span" variant="body-m" truncate>
|
|
267
|
+
{row.title}
|
|
268
|
+
</Typography>
|
|
269
|
+
<Typography as="span" variant="caption" tone="muted">
|
|
270
|
+
{row.meta}
|
|
271
|
+
</Typography>
|
|
272
|
+
</span>
|
|
273
|
+
</Card.ListItem>
|
|
274
|
+
))}
|
|
275
|
+
</Card.List>
|
|
276
|
+
</Card.Root>
|
|
277
|
+
)}
|
|
278
|
+
</PageContent.Body>
|
|
279
|
+
</PageContent.Section>
|
|
280
|
+
</Crossfade>
|
|
281
|
+
</AppShell.Template>
|
|
282
|
+
|
|
283
|
+
<CommandMenu.Root open={searchOpen} onOpenChange={setSearchOpen} aria-label="Поиск">
|
|
284
|
+
<CommandMenu.Input placeholder="Раздел, сделка или клиент" />
|
|
285
|
+
<CommandMenu.List>
|
|
286
|
+
<CommandMenu.Empty />
|
|
287
|
+
<CommandMenu.Group label="Разделы">
|
|
288
|
+
{SECTIONS.map((item) => (
|
|
289
|
+
<CommandMenu.Item key={item.id} value={item.label} onSelect={() => go(item.id)}>
|
|
290
|
+
<CommandMenu.ItemIcon>
|
|
291
|
+
<Icon name={item.icon} />
|
|
292
|
+
</CommandMenu.ItemIcon>
|
|
293
|
+
{item.label}
|
|
294
|
+
</CommandMenu.Item>
|
|
295
|
+
))}
|
|
296
|
+
</CommandMenu.Group>
|
|
297
|
+
</CommandMenu.List>
|
|
298
|
+
</CommandMenu.Root>
|
|
299
|
+
</>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
@@ -13,21 +13,5 @@
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
.dialogs {
|
|
16
|
-
display: flex;
|
|
17
|
-
flex-direction: column;
|
|
18
|
-
gap: var(--prime-space-1);
|
|
19
|
-
margin: 0;
|
|
20
16
|
padding: var(--prime-space-2);
|
|
21
|
-
list-style: none;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/* A dialog row: avatar and name at the start, the unread count at the end. */
|
|
25
|
-
.dialog {
|
|
26
|
-
justify-content: flex-start;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.dialogText {
|
|
30
|
-
display: grid;
|
|
31
|
-
flex: 1;
|
|
32
|
-
min-width: 0;
|
|
33
17
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/** A support messenger on `Chat`: dialogs beside the conversation (one pane at a time on a phone), history that loads above as you scroll up, dates and runs from data, every kind of message, search, pinned messages, replies and edits from the menu with a jump to the quoted message, read receipts and unread counts from what was seen, the typing signal both ways, and the Composer whose messages rise into the list while the client answers. */
|
|
2
2
|
import {
|
|
3
3
|
Avatar,
|
|
4
|
-
Badge,
|
|
5
4
|
Button,
|
|
6
5
|
Card,
|
|
7
6
|
Chat,
|
|
@@ -13,6 +12,7 @@ import {
|
|
|
13
12
|
Dropdown,
|
|
14
13
|
type GalleryItem,
|
|
15
14
|
Icon,
|
|
15
|
+
List,
|
|
16
16
|
Message,
|
|
17
17
|
type MessageReaction,
|
|
18
18
|
type MessageStatus,
|
|
@@ -729,28 +729,25 @@ export default function ChatPattern() {
|
|
|
729
729
|
<PageContent.List aria-label="Диалоги">
|
|
730
730
|
<Card.Root className={styles.paneCard}>
|
|
731
731
|
<ScrollContainer className={styles.pane}>
|
|
732
|
-
<
|
|
732
|
+
<List.Root className={styles.dialogs}>
|
|
733
733
|
{DIALOGS.map((item) => (
|
|
734
|
-
<
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
onClick={() => setDialogId(item.id)}
|
|
742
|
-
>
|
|
743
|
-
<Avatar.Root size="s" color="blue">
|
|
734
|
+
<List.Item
|
|
735
|
+
key={item.id}
|
|
736
|
+
current={item.id === dialogId}
|
|
737
|
+
onClick={() => setDialogId(item.id)}
|
|
738
|
+
>
|
|
739
|
+
<List.ItemIcon>
|
|
740
|
+
<Avatar.Root color="blue">
|
|
744
741
|
<Avatar.Fallback>{item.initials}</Avatar.Fallback>
|
|
745
742
|
</Avatar.Root>
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
</
|
|
743
|
+
</List.ItemIcon>
|
|
744
|
+
<List.ItemText>{item.name}</List.ItemText>
|
|
745
|
+
{(unread[item.id] ?? 0) > 0 ? (
|
|
746
|
+
<List.ItemCount color="blue">{unread[item.id]}</List.ItemCount>
|
|
747
|
+
) : null}
|
|
748
|
+
</List.Item>
|
|
752
749
|
))}
|
|
753
|
-
</
|
|
750
|
+
</List.Root>
|
|
754
751
|
</ScrollContainer>
|
|
755
752
|
</Card.Root>
|
|
756
753
|
</PageContent.List>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/* Kanban board: the toolbar sits right above the view it filters; the page rhythm comes from
|
|
2
|
+
PageContent. */
|
|
3
|
+
|
|
4
|
+
.workspace {
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
gap: var(--prime-space-4);
|
|
8
|
+
min-width: 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* The board takes most of the screen; every column scrolls inside it, never the page. */
|
|
12
|
+
.board {
|
|
13
|
+
block-size: min(calc(var(--prime-space-24) * 7), 75dvh);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* Company cell: the name over the deal id. */
|
|
17
|
+
.company {
|
|
18
|
+
display: grid;
|
|
19
|
+
white-space: nowrap;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Owner: avatar, then the name. */
|
|
23
|
+
.owner {
|
|
24
|
+
display: inline-flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
gap: var(--prime-space-2);
|
|
27
|
+
white-space: nowrap;
|
|
28
|
+
}
|