@sole-ui/react 0.0.0-stage → 0.2.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 +98 -2
- package/dist/components/shell/app-frame.d.ts +14 -0
- package/dist/components/shell/app-frame.js +8 -0
- package/dist/components/shell/command-palette.d.ts +32 -0
- package/dist/components/shell/command-palette.js +30 -0
- package/dist/components/shell/index.d.ts +4 -0
- package/dist/components/shell/index.js +4 -0
- package/dist/components/shell/page.d.ts +25 -0
- package/dist/components/shell/page.js +22 -0
- package/dist/components/shell/sidebar.d.ts +81 -0
- package/dist/components/shell/sidebar.js +113 -0
- package/dist/components/sole/avatar.d.ts +19 -0
- package/dist/components/sole/avatar.js +37 -0
- package/dist/components/sole/banner.d.ts +24 -0
- package/dist/components/sole/banner.js +27 -0
- package/dist/components/sole/column-header.d.ts +15 -0
- package/dist/components/sole/column-header.js +15 -0
- package/dist/components/sole/copy-field.d.ts +10 -0
- package/dist/components/sole/copy-field.js +32 -0
- package/dist/components/sole/count-badge.d.ts +8 -0
- package/dist/components/sole/count-badge.js +8 -0
- package/dist/components/sole/field.d.ts +24 -0
- package/dist/components/sole/field.js +27 -0
- package/dist/components/sole/filters.d.ts +33 -0
- package/dist/components/sole/filters.js +48 -0
- package/dist/components/sole/index.d.ts +17 -0
- package/dist/components/sole/index.js +17 -0
- package/dist/components/sole/kbd.d.ts +7 -0
- package/dist/components/sole/kbd.js +14 -0
- package/dist/components/sole/label-tag.d.ts +18 -0
- package/dist/components/sole/label-tag.js +17 -0
- package/dist/components/sole/layout.d.ts +54 -0
- package/dist/components/sole/layout.js +48 -0
- package/dist/components/sole/logo.d.ts +13 -0
- package/dist/components/sole/logo.js +17 -0
- package/dist/components/sole/nav.d.ts +22 -0
- package/dist/components/sole/nav.js +32 -0
- package/dist/components/sole/person-chip.d.ts +8 -0
- package/dist/components/sole/person-chip.js +19 -0
- package/dist/components/sole/person-header.d.ts +21 -0
- package/dist/components/sole/person-header.js +22 -0
- package/dist/components/sole/pill.d.ts +9 -0
- package/dist/components/sole/pill.js +14 -0
- package/dist/components/sole/states.d.ts +48 -0
- package/dist/components/sole/states.js +53 -0
- package/dist/components/sole/table.d.ts +23 -0
- package/dist/components/sole/table.js +32 -0
- package/dist/components/ui/alert-dialog.d.ts +27 -0
- package/dist/components/ui/alert-dialog.js +33 -0
- package/dist/components/ui/badge.d.ts +11 -0
- package/dist/components/ui/badge.js +30 -0
- package/dist/components/ui/button.d.ts +13 -0
- package/dist/components/ui/button.js +49 -0
- package/dist/components/ui/calendar.d.ts +9 -0
- package/dist/components/ui/calendar.js +79 -0
- package/dist/components/ui/checkbox.d.ts +4 -0
- package/dist/components/ui/checkbox.js +11 -0
- package/dist/components/ui/command.d.ts +90 -0
- package/dist/components/ui/command.js +36 -0
- package/dist/components/ui/dialog.d.ts +18 -0
- package/dist/components/ui/dialog.js +33 -0
- package/dist/components/ui/dropdown-menu.d.ts +25 -0
- package/dist/components/ui/dropdown-menu.js +42 -0
- package/dist/components/ui/index.d.ts +23 -0
- package/dist/components/ui/index.js +23 -0
- package/dist/components/ui/input.d.ts +3 -0
- package/dist/components/ui/input.js +12 -0
- package/dist/components/ui/label.d.ts +4 -0
- package/dist/components/ui/label.js +10 -0
- package/dist/components/ui/popover.d.ts +10 -0
- package/dist/components/ui/popover.js +22 -0
- package/dist/components/ui/scroll-area.d.ts +5 -0
- package/dist/components/ui/scroll-area.js +14 -0
- package/dist/components/ui/select.d.ts +15 -0
- package/dist/components/ui/select.js +30 -0
- package/dist/components/ui/separator.d.ts +4 -0
- package/dist/components/ui/separator.js +10 -0
- package/dist/components/ui/sheet.d.ts +14 -0
- package/dist/components/ui/sheet.js +35 -0
- package/dist/components/ui/skeleton.d.ts +3 -0
- package/dist/components/ui/skeleton.js +10 -0
- package/dist/components/ui/sonner.d.ts +5 -0
- package/dist/components/ui/sonner.js +63 -0
- package/dist/components/ui/switch.d.ts +6 -0
- package/dist/components/ui/switch.js +10 -0
- package/dist/components/ui/tabs.d.ts +7 -0
- package/dist/components/ui/tabs.js +24 -0
- package/dist/components/ui/textarea.d.ts +3 -0
- package/dist/components/ui/textarea.js +9 -0
- package/dist/components/ui/toggle-group.d.ts +14 -0
- package/dist/components/ui/toggle-group.js +24 -0
- package/dist/components/ui/toggle.d.ts +12 -0
- package/dist/components/ui/toggle.js +28 -0
- package/dist/components/ui/tooltip.d.ts +7 -0
- package/dist/components/ui/tooltip.js +18 -0
- package/dist/hq/activity-line.d.ts +21 -0
- package/dist/hq/activity-line.js +26 -0
- package/dist/hq/board-card.d.ts +12 -0
- package/dist/hq/board-card.js +21 -0
- package/dist/hq/board-chip.d.ts +12 -0
- package/dist/hq/board-chip.js +20 -0
- package/dist/hq/checklist-item.d.ts +14 -0
- package/dist/hq/checklist-item.js +13 -0
- package/dist/hq/comment.d.ts +26 -0
- package/dist/hq/comment.js +15 -0
- package/dist/hq/due-chip.d.ts +11 -0
- package/dist/hq/due-chip.js +25 -0
- package/dist/hq/group-header.d.ts +27 -0
- package/dist/hq/group-header.js +22 -0
- package/dist/hq/inbox-row.d.ts +22 -0
- package/dist/hq/inbox-row.js +13 -0
- package/dist/hq/index.d.ts +21 -0
- package/dist/hq/index.js +21 -0
- package/dist/hq/link-row.d.ts +13 -0
- package/dist/hq/link-row.js +13 -0
- package/dist/hq/mention-chip.d.ts +23 -0
- package/dist/hq/mention-chip.js +28 -0
- package/dist/hq/page-row.d.ts +26 -0
- package/dist/hq/page-row.js +30 -0
- package/dist/hq/priority-icon.d.ts +9 -0
- package/dist/hq/priority-icon.js +23 -0
- package/dist/hq/property-type-icon.d.ts +7 -0
- package/dist/hq/property-type-icon.js +22 -0
- package/dist/hq/recent-item.d.ts +11 -0
- package/dist/hq/recent-item.js +12 -0
- package/dist/hq/review-badge.d.ts +37 -0
- package/dist/hq/review-badge.js +77 -0
- package/dist/hq/state-icon.d.ts +12 -0
- package/dist/hq/state-icon.js +41 -0
- package/dist/hq/state-pill.d.ts +9 -0
- package/dist/hq/state-pill.js +18 -0
- package/dist/hq/sticky.d.ts +13 -0
- package/dist/hq/sticky.js +22 -0
- package/dist/hq/template-card.d.ts +10 -0
- package/dist/hq/template-card.js +9 -0
- package/dist/hq/types.d.ts +49 -0
- package/dist/hq/types.js +7 -0
- package/dist/hq/work-item-row.d.ts +16 -0
- package/dist/hq/work-item-row.js +23 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +12 -0
- package/dist/lib/cn.d.ts +2 -0
- package/dist/lib/cn.js +15 -0
- package/dist/lib/colour.d.ts +8 -0
- package/dist/lib/colour.js +28 -0
- package/dist/lib/dates.d.ts +62 -0
- package/dist/lib/dates.js +182 -0
- package/dist/lib/hue.d.ts +3 -0
- package/dist/lib/hue.js +1 -0
- package/dist/lib/person.d.ts +16 -0
- package/dist/lib/person.js +17 -0
- package/dist/lib/platform.d.ts +4 -0
- package/dist/lib/platform.js +12 -0
- package/dist/lib/sole-provider.d.ts +35 -0
- package/dist/lib/sole-provider.js +52 -0
- package/dist/styles.css +64 -0
- package/dist/tokens.css +231 -0
- package/dist/tokens.generated.d.ts +219 -0
- package/dist/tokens.generated.js +679 -0
- package/dist/tokens.json +677 -0
- package/package.json +82 -4
package/README.md
CHANGED
|
@@ -1,3 +1,99 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @sole-ui/react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Sole's design system for React and Tailwind CSS v4: the design tokens (colour, label hues, type scale, radius, elevation, motion) and the shared and Sole HQ components, typed, tested and copied from Sole HQ with the same look and behaviour.
|
|
4
|
+
|
|
5
|
+
Light mode and desktop only. React 18.3 and 19.
|
|
6
|
+
|
|
7
|
+
Every part, live, with its variants: https://mindninjax.github.io/sole-design-system/components/
|
|
8
|
+
Every token: https://mindninjax.github.io/sole-design-system/
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install @sole-ui/react
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Components
|
|
17
|
+
|
|
18
|
+
Peer dependencies: `react`, `react-dom` (18.3 or 19), `radix-ui` and, for the styles, `tailwindcss` v4.
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm install @sole-ui/react radix-ui
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Wrap the app in `SoleProvider` once. It gives the parts your router's link, the work clock and the tooltip provider:
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { Link } from 'react-router';
|
|
28
|
+
import { Button, SoleProvider, Toaster } from '@sole-ui/react';
|
|
29
|
+
import { StatePill } from '@sole-ui/react/hq';
|
|
30
|
+
|
|
31
|
+
<SoleProvider
|
|
32
|
+
linkComponent={({ href, ...props }) => <Link to={href} {...props} />}
|
|
33
|
+
timeZone="Australia/Sydney"
|
|
34
|
+
personHref={(person) => `/people/${person.id}`}
|
|
35
|
+
>
|
|
36
|
+
<Button>Save</Button>
|
|
37
|
+
<StatePill state={{ name: 'In progress', group: 'started' }} />
|
|
38
|
+
<Toaster />
|
|
39
|
+
</SoleProvider>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- `@sole-ui/react` holds the shared shadcn parts (`Button`, `Dialog`, `Select`, `Tabs`, ...), the Sole parts (`Avatar`, `PersonChip`, `Pill`, `Table`, `FilterChip`, ...) and the app shell pieces (`AppFrame`, `Sidebar`, `TopBar`, `CommandPalette`).
|
|
43
|
+
- `@sole-ui/react/hq` holds the parts only Sole HQ uses (`StateIcon`, `DueChip`, `WorkItemRow`, `BoardCard`, ...).
|
|
44
|
+
- The parts are presentational: no router, no fetching. People are `SolePerson` (`id`, `name`, `avatarUrl?`, `isBot?`); links go through `SoleLink`. Pass `now` to `SoleProvider` to pin the clock in demos and tests.
|
|
45
|
+
- Each part that draws an element forwards its ref, and every module starts with `'use client'`, so they work in Next.js too.
|
|
46
|
+
|
|
47
|
+
## With Tailwind CSS v4
|
|
48
|
+
|
|
49
|
+
Tailwind v4 is an optional peer dependency (`^4.1.0`). In your main stylesheet:
|
|
50
|
+
|
|
51
|
+
```css
|
|
52
|
+
@import "tailwindcss";
|
|
53
|
+
@import "@sole-ui/react/styles.css";
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
That gives you:
|
|
57
|
+
|
|
58
|
+
- the Sole colours as utilities (`bg-primary`, `text-muted-foreground`, `border-border`), the label hues (`.hue-blue` with `bg-tag-bg`, `text-tag-fg`) and the radius, shadow and type scales (`text-body`, `text-strong`, `text-micro`);
|
|
59
|
+
- Inter Variable as the sans font, through `@fontsource-variable/inter`, which installs with the package;
|
|
60
|
+
- the base layer: page background and text colour, tabular numbers on `time` and `kbd`, and no motion for people who asked their system for less.
|
|
61
|
+
|
|
62
|
+
## Without Tailwind
|
|
63
|
+
|
|
64
|
+
`tokens.css` is plain CSS custom properties on `:root`, plus the `.hue-*` classes. It works in any project:
|
|
65
|
+
|
|
66
|
+
```css
|
|
67
|
+
@import "@sole-ui/react/tokens.css";
|
|
68
|
+
|
|
69
|
+
.card {
|
|
70
|
+
background: var(--card);
|
|
71
|
+
border: 1px solid var(--border);
|
|
72
|
+
border-radius: var(--radius);
|
|
73
|
+
box-shadow: var(--elevation-sm);
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
(The `@theme` and `@utility` blocks in the file are for Tailwind. Other tools ignore them.)
|
|
78
|
+
|
|
79
|
+
## In JavaScript
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
import { tokens } from "@sole-ui/react";
|
|
83
|
+
|
|
84
|
+
tokens.color.primary.$value; // "#0270d6"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`tokens` is the token tree as a plain object, typed so that `tokens.color.primary` is checked by the compiler.
|
|
88
|
+
|
|
89
|
+
## tokens.json
|
|
90
|
+
|
|
91
|
+
`@sole-ui/react/tokens.json` is the same tree in the [DTCG](https://www.designtokens.org/) format (`$value`, `$type`, `$description`), for design tools and other generators:
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
import tokens from "@sole-ui/react/tokens.json" with { type: "json" };
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Source
|
|
98
|
+
|
|
99
|
+
https://github.com/mindninjaX/sole-design-system. The values never change here first: if the look changes, it changes in Sole HQ, and the tokens follow.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface AppFrameProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'children'> {
|
|
3
|
+
/** A `Sidebar`. */
|
|
4
|
+
sidebar: React.ReactNode;
|
|
5
|
+
/** A panel docked beside the content, such as an assistant. Leave it out when closed. */
|
|
6
|
+
aside?: React.ReactNode;
|
|
7
|
+
/** A strip above the content panel. It sits in the 8px gap, so the panel gets no top gap of its own. */
|
|
8
|
+
banner?: React.ReactNode;
|
|
9
|
+
/** Classes for the content panel, for an app that restyles the screen beside its aside. */
|
|
10
|
+
contentClassName?: string;
|
|
11
|
+
/** The screen: usually a `Page`. */
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
export declare const AppFrame: React.ForwardRefExoticComponent<AppFrameProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { cn } from '../../lib/cn.js';
|
|
5
|
+
export const AppFrame = React.forwardRef(function AppFrame({ sidebar, aside, banner, contentClassName, children, className, ...props }, ref) {
|
|
6
|
+
return (_jsxs("div", { ref: ref, "data-app-shell": true, className: cn('flex h-dvh w-full min-w-[1024px] overflow-hidden bg-background', className), ...props, children: [sidebar, _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [banner, _jsxs("div", { className: cn('flex min-h-0 flex-1 gap-2 pr-2 pb-2', !banner && 'pt-2'), children: [_jsx("main", { id: "content", "data-pane": "content", "data-has-aside": aside ? true : undefined, className: cn('relative min-w-0 flex-1 overflow-hidden rounded-xl border border-border bg-card', contentClassName), children: children }), aside] })] })] }));
|
|
7
|
+
});
|
|
8
|
+
AppFrame.displayName = 'AppFrame';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface CommandPaletteItem {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
/** An icon element, drawn at 14px. */
|
|
6
|
+
icon?: React.ReactNode;
|
|
7
|
+
/** The key hint on the right, as words: "C" or "Cmd J". */
|
|
8
|
+
shortcut?: string;
|
|
9
|
+
/** Runs after the palette has closed. */
|
|
10
|
+
onSelect: () => void;
|
|
11
|
+
}
|
|
12
|
+
export interface CommandPaletteGroup {
|
|
13
|
+
heading: string;
|
|
14
|
+
items: CommandPaletteItem[];
|
|
15
|
+
}
|
|
16
|
+
export interface CommandPaletteProps {
|
|
17
|
+
groups: CommandPaletteGroup[];
|
|
18
|
+
open: boolean;
|
|
19
|
+
onOpenChange: (open: boolean) => void;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
/** Shown when nothing matches what was typed. */
|
|
22
|
+
empty?: string;
|
|
23
|
+
/** The palette's name for screen readers, and the search field's label. */
|
|
24
|
+
title?: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
/** false when the caller filters the rows itself. */
|
|
27
|
+
filter?: boolean;
|
|
28
|
+
/** The typed words, when the caller controls them. */
|
|
29
|
+
query?: string;
|
|
30
|
+
onQueryChange?: (query: string) => void;
|
|
31
|
+
}
|
|
32
|
+
export declare function CommandPalette({ groups, open, onOpenChange, placeholder, empty, title, description, filter, query, onQueryChange, }: CommandPaletteProps): React.JSX.Element;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { Kbd } from '../sole/kbd.js';
|
|
5
|
+
import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandShortcut } from '../ui/command.js';
|
|
6
|
+
/** 20.2's group headings: 26px from a heading to its first row, 31px from a last row to the next heading. */
|
|
7
|
+
const GROUP_CLASS = '[&_[cmdk-group-heading]]:pt-[7px] [&_[cmdk-group-heading]]:pb-0.5';
|
|
8
|
+
const matches = (text, term) => text.toLowerCase().includes(term.toLowerCase());
|
|
9
|
+
export function CommandPalette({ groups, open, onOpenChange, placeholder = 'Search or jump to', empty = 'Nothing matches that.', title = 'Command palette', description = 'Search, or jump to a screen', filter = true, query, onQueryChange, }) {
|
|
10
|
+
const [ownQuery, setOwnQuery] = React.useState('');
|
|
11
|
+
const text = query ?? ownQuery;
|
|
12
|
+
const setText = (value) => {
|
|
13
|
+
if (query === undefined)
|
|
14
|
+
setOwnQuery(value);
|
|
15
|
+
onQueryChange?.(value);
|
|
16
|
+
};
|
|
17
|
+
const term = text.trim();
|
|
18
|
+
const shown = groups
|
|
19
|
+
.map((group) => ({ ...group, items: filter && term ? group.items.filter((item) => matches(item.label, term)) : group.items }))
|
|
20
|
+
.filter((group) => group.items.length > 0);
|
|
21
|
+
return (_jsxs(CommandDialog, { open: open, onOpenChange: (next) => {
|
|
22
|
+
onOpenChange(next);
|
|
23
|
+
if (!next)
|
|
24
|
+
setOwnQuery('');
|
|
25
|
+
}, title: title, description: description, shouldFilter: false, children: [_jsx(CommandInput, { value: text, onValueChange: setText, placeholder: placeholder, "aria-label": title, trailing: _jsx(Kbd, { keys: ['Esc'] }) }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: empty }), shown.map((group) => (_jsx(CommandGroup, { heading: group.heading, className: GROUP_CLASS, children: group.items.map((item) => (_jsxs(CommandItem, { value: item.id, onSelect: () => {
|
|
26
|
+
onOpenChange(false);
|
|
27
|
+
setOwnQuery('');
|
|
28
|
+
item.onSelect();
|
|
29
|
+
}, children: [item.icon, _jsx("span", { className: "min-w-0 flex-1 truncate", children: item.label }), item.shortcut && _jsx(CommandShortcut, { children: item.shortcut })] }, item.id))) }, group.heading)))] })] }));
|
|
30
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface Crumb {
|
|
3
|
+
label: string;
|
|
4
|
+
/** An address in the app. The last crumb is the current page, so it never links. */
|
|
5
|
+
href?: string;
|
|
6
|
+
/** A board emoji or an icon before the label. */
|
|
7
|
+
icon?: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
export interface BreadcrumbsProps extends Omit<React.ComponentPropsWithoutRef<'nav'>, 'children'> {
|
|
10
|
+
crumbs: Crumb[];
|
|
11
|
+
}
|
|
12
|
+
export declare const Breadcrumbs: React.ForwardRefExoticComponent<BreadcrumbsProps & React.RefAttributes<HTMLElement>>;
|
|
13
|
+
export interface TopBarProps extends Omit<React.ComponentPropsWithoutRef<'header'>, 'children'> {
|
|
14
|
+
crumbs: Crumb[];
|
|
15
|
+
/** Buttons for the screen, on the right. */
|
|
16
|
+
actions?: React.ReactNode;
|
|
17
|
+
}
|
|
18
|
+
export declare const TopBar: React.ForwardRefExoticComponent<TopBarProps & React.RefAttributes<HTMLElement>>;
|
|
19
|
+
export interface PageProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'className' | 'title'> {
|
|
20
|
+
crumbs: Crumb[];
|
|
21
|
+
actions?: React.ReactNode;
|
|
22
|
+
/** Classes for the scrolling body under the top bar, not for the root. */
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare const Page: React.ForwardRefExoticComponent<PageProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { ChevronRight } from 'lucide-react';
|
|
5
|
+
import { cn } from '../../lib/cn.js';
|
|
6
|
+
import { SoleLink } from '../../lib/sole-provider.js';
|
|
7
|
+
export const Breadcrumbs = React.forwardRef(function Breadcrumbs({ crumbs, className, ...props }, ref) {
|
|
8
|
+
return (_jsx("nav", { ref: ref, "aria-label": "Breadcrumb", className: cn('min-w-0', className), ...props, children: _jsx("ol", { className: "flex min-w-0 items-center gap-1.5 text-body font-medium", children: crumbs.map((crumb, i) => {
|
|
9
|
+
const last = i === crumbs.length - 1;
|
|
10
|
+
const inner = (_jsxs(_Fragment, { children: [crumb.icon, _jsx("span", { className: "truncate", children: crumb.label })] }));
|
|
11
|
+
return (_jsxs(React.Fragment, { children: [i > 0 && _jsx(ChevronRight, { size: 14, "aria-hidden": "true", className: "shrink-0 text-faint" }), _jsx("li", { className: cn('flex min-w-0 items-center gap-1.5', last ? 'text-foreground' : 'text-muted-foreground'), "aria-current": last ? 'page' : undefined, children: crumb.href && !last ? (_jsx(SoleLink, { href: crumb.href, className: "flex min-w-0 items-center gap-1.5 rounded-sm outline-none hover:text-foreground focus-visible:shadow-[var(--focus-ring)]", children: inner })) : (inner) })] }, crumb.href ?? crumb.label));
|
|
12
|
+
}) }) }));
|
|
13
|
+
});
|
|
14
|
+
Breadcrumbs.displayName = 'Breadcrumbs';
|
|
15
|
+
export const TopBar = React.forwardRef(function TopBar({ crumbs, actions, className, ...props }, ref) {
|
|
16
|
+
return (_jsxs("header", { ref: ref, "data-topbar": true, className: cn('flex h-12 shrink-0 items-center gap-3 border-b border-border px-4', className), ...props, children: [_jsx(Breadcrumbs, { crumbs: crumbs }), actions && _jsx("div", { className: "ml-auto flex shrink-0 items-center gap-2", children: actions })] }));
|
|
17
|
+
});
|
|
18
|
+
TopBar.displayName = 'TopBar';
|
|
19
|
+
export const Page = React.forwardRef(function Page({ crumbs, actions, children, className, ...props }, ref) {
|
|
20
|
+
return (_jsxs("div", { ref: ref, className: "flex h-full min-h-0 flex-col", ...props, children: [_jsx(TopBar, { crumbs: crumbs, actions: actions }), _jsx("div", { "data-page-body": true, className: cn('min-h-0 flex-1 overflow-y-auto overflow-x-hidden', className), children: children })] }));
|
|
21
|
+
});
|
|
22
|
+
Page.displayName = 'Page';
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type SolePerson } from '../../lib/person.ts';
|
|
3
|
+
export interface SidebarProps extends React.ComponentPropsWithoutRef<'nav'> {
|
|
4
|
+
/** The 52px rail of icons instead of the 240px sidebar. Every part inside follows it. */
|
|
5
|
+
rail?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare const Sidebar: React.ForwardRefExoticComponent<SidebarProps & React.RefAttributes<HTMLElement>>;
|
|
8
|
+
export interface SidebarHeaderProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'children'> {
|
|
9
|
+
rail?: boolean;
|
|
10
|
+
/** Where the logo goes. */
|
|
11
|
+
href?: string;
|
|
12
|
+
/** The logo link's accessible name. */
|
|
13
|
+
label?: string;
|
|
14
|
+
/** The logo beside the collapse button. Defaults to the Sole lockup. */
|
|
15
|
+
logo?: React.ReactNode;
|
|
16
|
+
/** The logo in the rail, where it doubles as the expand button. Defaults to the Sole mark. */
|
|
17
|
+
mark?: React.ReactNode;
|
|
18
|
+
/** Collapses the sidebar, or expands the rail. Without it there is no button. */
|
|
19
|
+
onToggle?: () => void;
|
|
20
|
+
}
|
|
21
|
+
export declare const SidebarHeader: React.ForwardRefExoticComponent<SidebarHeaderProps & React.RefAttributes<HTMLDivElement>>;
|
|
22
|
+
/** A stack of rows: 2px apart, or 4px apart and centred in the rail. */
|
|
23
|
+
export declare const SidebarSection: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & {
|
|
24
|
+
rail?: boolean;
|
|
25
|
+
} & React.RefAttributes<HTMLDivElement>>;
|
|
26
|
+
/** The middle of the sidebar: it takes the spare height and scrolls when the rows do not fit. */
|
|
27
|
+
export declare const SidebarContent: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & {
|
|
28
|
+
rail?: boolean;
|
|
29
|
+
} & React.RefAttributes<HTMLDivElement>>;
|
|
30
|
+
/** The bottom of the sidebar (settings, the user row), pinned under the scrolling middle. */
|
|
31
|
+
export declare const SidebarFooter: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & {
|
|
32
|
+
rail?: boolean;
|
|
33
|
+
} & React.RefAttributes<HTMLDivElement>>;
|
|
34
|
+
export interface SidebarItemProps extends Omit<React.HTMLAttributes<HTMLElement>, 'children'> {
|
|
35
|
+
/** An address in the app: the row is a link. Without it the row is a button. */
|
|
36
|
+
href?: string;
|
|
37
|
+
/** Drawn at 16px; its stroke is 1.8, and 2.1 when active. An emoji works too. */
|
|
38
|
+
icon: React.ReactNode;
|
|
39
|
+
label: string;
|
|
40
|
+
/** The current page. Sets `aria-current`. The caller decides, so a row is never current by accident. */
|
|
41
|
+
active?: boolean;
|
|
42
|
+
/** A count: `text` is drawn, `label` is what a screen reader hears ("2 unread mentions"). */
|
|
43
|
+
badge?: {
|
|
44
|
+
text: string;
|
|
45
|
+
label: string;
|
|
46
|
+
};
|
|
47
|
+
/** Keys shown on hover, as `Kbd` takes them: `['mod', 'J']`. Hidden while there is a badge. */
|
|
48
|
+
shortcut?: string[];
|
|
49
|
+
/** What the rail's tooltip adds after the label. Defaults to the shortcut's keys, else the badge's label. */
|
|
50
|
+
hint?: string;
|
|
51
|
+
/** Something at the right edge, such as a count that shows on hover (`group-hover/nav:opacity-100`). */
|
|
52
|
+
trailing?: React.ReactNode;
|
|
53
|
+
rail?: boolean;
|
|
54
|
+
}
|
|
55
|
+
export declare const SidebarItem: React.ForwardRefExoticComponent<SidebarItemProps & React.RefAttributes<HTMLElement>>;
|
|
56
|
+
export interface SidebarGroupProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'children'> {
|
|
57
|
+
label: string;
|
|
58
|
+
/** Controlled. Leave it out and the group keeps its own state. */
|
|
59
|
+
open?: boolean;
|
|
60
|
+
defaultOpen?: boolean;
|
|
61
|
+
onOpenChange?: (open: boolean) => void;
|
|
62
|
+
rail?: boolean;
|
|
63
|
+
children: React.ReactNode;
|
|
64
|
+
}
|
|
65
|
+
/** A labelled, collapsible list of rows. In the rail the label becomes a short rule and the rows always show. */
|
|
66
|
+
export declare const SidebarGroup: React.ForwardRefExoticComponent<SidebarGroupProps & React.RefAttributes<HTMLDivElement>>;
|
|
67
|
+
export interface SidebarUserProps extends Omit<React.ComponentPropsWithoutRef<'button'>, 'children'> {
|
|
68
|
+
person: SolePerson;
|
|
69
|
+
/** The line under the name, such as "Admin". */
|
|
70
|
+
meta?: string;
|
|
71
|
+
rail?: boolean;
|
|
72
|
+
/** The menu's rows: `DropdownMenuItem`, `DropdownMenuSeparator` and the rest of `ui/dropdown-menu`. The name heads the menu. */
|
|
73
|
+
children?: React.ReactNode;
|
|
74
|
+
}
|
|
75
|
+
/** The user row and its menu (20.4-user-menu.png). */
|
|
76
|
+
export declare const SidebarUser: React.ForwardRefExoticComponent<SidebarUserProps & React.RefAttributes<HTMLButtonElement>>;
|
|
77
|
+
export interface SidebarSearchProps extends Omit<React.ComponentPropsWithoutRef<'button'>, 'children'> {
|
|
78
|
+
rail?: boolean;
|
|
79
|
+
}
|
|
80
|
+
/** The Search field that opens the command palette. It is a button: the palette has the real input. */
|
|
81
|
+
export declare const SidebarSearch: React.ForwardRefExoticComponent<SidebarSearchProps & React.RefAttributes<HTMLButtonElement>>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { ChevronDown, ChevronsUpDown, PanelLeft, Search } from 'lucide-react';
|
|
5
|
+
import { cn } from '../../lib/cn.js';
|
|
6
|
+
import { personName } from '../../lib/person.js';
|
|
7
|
+
import { SoleLink } from '../../lib/sole-provider.js';
|
|
8
|
+
import { Avatar } from '../sole/avatar.js';
|
|
9
|
+
import { CountBadge } from '../sole/count-badge.js';
|
|
10
|
+
import { Kbd, keysText } from '../sole/kbd.js';
|
|
11
|
+
import { SoleLockup, SoleMark } from '../sole/logo.js';
|
|
12
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuLabel, DropdownMenuTrigger } from '../ui/dropdown-menu.js';
|
|
13
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip.js';
|
|
14
|
+
// The sidebar (brand-and-ux 4.3, D2; hq/design/screens 30.1, 20.1 and 20.4): a header, the
|
|
15
|
+
// destinations, collapsible groups, then the user row. 240px wide, or a 52px rail of icons
|
|
16
|
+
// whose tooltips carry the label and the hint ("Inbox · 1 mention, 3 unread"). Everything
|
|
17
|
+
// arrives as props or children: the app owns its nav list, its data and its router.
|
|
18
|
+
/** `Sidebar` says whether it is a rail, so its parts do not each need `rail`. A part's own `rail` wins. */
|
|
19
|
+
const RailContext = React.createContext(false);
|
|
20
|
+
function useRail(own) {
|
|
21
|
+
const fromSidebar = React.useContext(RailContext);
|
|
22
|
+
return own ?? fromSidebar;
|
|
23
|
+
}
|
|
24
|
+
const itemClass = (active) => cn('flex h-8 w-full min-w-0 items-center gap-2 rounded-md px-2 text-body font-medium outline-none transition-colors duration-[120ms]', 'focus-visible:shadow-[var(--focus-ring)]', active ? 'bg-sidebar-accent text-sidebar-accent-foreground' : 'text-sidebar-foreground hover:bg-sidebar-hover hover:text-foreground');
|
|
25
|
+
/** A rail button: 36 by 32, centred in the 52px rail. */
|
|
26
|
+
const railClass = 'h-8 w-9 justify-center px-0';
|
|
27
|
+
/**
|
|
28
|
+
* The rail's tooltip (20.1): no arrow, its edge 13px from the button (4px past the rail),
|
|
29
|
+
* and the dot between the label and the hint set a little apart.
|
|
30
|
+
*/
|
|
31
|
+
function RailTip({ label, hint, children }) {
|
|
32
|
+
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: children }), _jsxs(TooltipContent, { side: "right", sideOffset: 13, className: "py-[4.5px] [&>span:has(>svg)]:hidden", children: [label, hint && (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", className: "px-[2.5px]", children: ' · ' }), hint] }))] })] }));
|
|
33
|
+
}
|
|
34
|
+
export const Sidebar = React.forwardRef(function Sidebar({ rail = false, className, children, ...props }, ref) {
|
|
35
|
+
return (_jsx(RailContext.Provider, { value: rail, children: _jsx("nav", { ref: ref, "aria-label": "Main", "data-pane": "sidebar", "data-rail": rail || undefined, className: cn('flex h-full shrink-0 flex-col bg-sidebar px-2 transition-[width] duration-200 ease-rise', rail ? 'w-[52px] items-center' : 'w-60', className), ...props, children: children }) }));
|
|
36
|
+
});
|
|
37
|
+
Sidebar.displayName = 'Sidebar';
|
|
38
|
+
export const SidebarHeader = React.forwardRef(function SidebarHeader({ rail: railProp, href = '/', label = 'Go to Home', logo, mark, onToggle, className, ...props }, ref) {
|
|
39
|
+
const rail = useRail(railProp);
|
|
40
|
+
if (rail) {
|
|
41
|
+
return (_jsx("div", { ref: ref, className: cn('flex h-[60px] shrink-0 items-center justify-center', className), ...props, children: onToggle ? (_jsx(RailTip, { label: "Expand the sidebar", children: _jsxs("button", { type: "button", onClick: onToggle, "aria-label": "Expand the sidebar", className: "group/mark inline-flex size-9 items-center justify-center rounded-md text-subtle-foreground outline-none hover:bg-sidebar-hover hover:text-foreground focus-visible:shadow-[var(--focus-ring)]", children: [_jsx("span", { className: "group-hover/mark:hidden group-focus-visible/mark:hidden", children: mark ?? _jsx(SoleMark, { size: 20 }) }), _jsx(PanelLeft, { size: 16, strokeWidth: 1.8, "aria-hidden": "true", className: "hidden group-hover/mark:block group-focus-visible/mark:block" })] }) })) : ((mark ?? _jsx(SoleMark, { size: 20 }))) }));
|
|
42
|
+
}
|
|
43
|
+
return (_jsxs("div", { ref: ref, className: cn('flex h-[66px] shrink-0 items-center justify-between pt-3 pl-2', className), ...props, children: [_jsx(SoleLink, { href: href, "aria-label": label, className: "rounded-md outline-none focus-visible:shadow-[var(--focus-ring)]", children: logo ?? _jsx(SoleLockup, { size: 20 }) }), onToggle && (_jsx(RailTip, { label: "Collapse the sidebar", children: _jsx("button", { type: "button", onClick: onToggle, "aria-label": "Collapse the sidebar", className: "inline-flex size-7 items-center justify-center rounded-md text-subtle-foreground outline-none hover:bg-sidebar-hover hover:text-foreground focus-visible:shadow-[var(--focus-ring)]", children: _jsx(PanelLeft, { size: 16, strokeWidth: 1.8, "aria-hidden": "true" }) }) }))] }));
|
|
44
|
+
});
|
|
45
|
+
SidebarHeader.displayName = 'SidebarHeader';
|
|
46
|
+
/** A stack of rows: 2px apart, or 4px apart and centred in the rail. */
|
|
47
|
+
export const SidebarSection = React.forwardRef(function SidebarSection({ rail: railProp, className, ...props }, ref) {
|
|
48
|
+
const rail = useRail(railProp);
|
|
49
|
+
return _jsx("div", { ref: ref, className: cn('flex w-full flex-col', rail ? 'items-center gap-1' : 'gap-0.5', className), ...props });
|
|
50
|
+
});
|
|
51
|
+
SidebarSection.displayName = 'SidebarSection';
|
|
52
|
+
/** The middle of the sidebar: it takes the spare height and scrolls when the rows do not fit. */
|
|
53
|
+
export const SidebarContent = React.forwardRef(function SidebarContent({ rail: railProp, className, ...props }, ref) {
|
|
54
|
+
const rail = useRail(railProp);
|
|
55
|
+
return _jsx("div", { ref: ref, className: cn('flex min-h-0 w-full flex-1 flex-col overflow-x-hidden overflow-y-auto', rail ? 'mt-4 items-center' : 'mt-3', className), ...props });
|
|
56
|
+
});
|
|
57
|
+
SidebarContent.displayName = 'SidebarContent';
|
|
58
|
+
/** The bottom of the sidebar (settings, the user row), pinned under the scrolling middle. */
|
|
59
|
+
export const SidebarFooter = React.forwardRef(function SidebarFooter({ rail: railProp, className, ...props }, ref) {
|
|
60
|
+
const rail = useRail(railProp);
|
|
61
|
+
return _jsx("div", { ref: ref, className: cn('flex w-full shrink-0 flex-col pt-2', rail ? 'items-center gap-1 pb-4' : 'gap-0.5 pb-5', className), ...props });
|
|
62
|
+
});
|
|
63
|
+
SidebarFooter.displayName = 'SidebarFooter';
|
|
64
|
+
export const SidebarItem = React.forwardRef(function SidebarItem({ href, icon, label, active = false, badge, shortcut, hint, trailing, rail: railProp, className, ...props }, ref) {
|
|
65
|
+
const rail = useRail(railProp);
|
|
66
|
+
const classes = cn(itemClass(active), 'group/nav relative', rail && railClass, className);
|
|
67
|
+
const common = {
|
|
68
|
+
'aria-current': active ? 'page' : undefined,
|
|
69
|
+
'aria-label': rail ? (badge ? `${label}, ${badge.label}` : label) : undefined,
|
|
70
|
+
className: classes,
|
|
71
|
+
...props,
|
|
72
|
+
};
|
|
73
|
+
const inner = (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", className: cn('flex shrink-0 items-center justify-center [&>svg]:size-4', active ? '[&>svg]:[stroke-width:2.1]' : '[&>svg]:[stroke-width:1.8]'), children: icon }), !rail && _jsx("span", { className: "flex-1 truncate text-left", children: label }), !rail && badge && _jsx(CountBadge, { label: badge.label, children: badge.text }), !rail && shortcut && !badge && _jsx(Kbd, { keys: shortcut, className: "opacity-0 transition-opacity group-hover/nav:opacity-100" }), !rail && trailing] }));
|
|
74
|
+
// One ref type for both elements; each branch narrows it to the element it renders.
|
|
75
|
+
const row = href ? (_jsx(SoleLink, { ref: ref, href: href, ...common, children: inner })) : (_jsx("button", { ref: ref, type: "button", ...common, children: inner }));
|
|
76
|
+
if (!rail)
|
|
77
|
+
return row;
|
|
78
|
+
// Counts are words in the tooltip, never a dot on the icon (brand-and-ux 4.3).
|
|
79
|
+
return (_jsx(RailTip, { label: label, hint: hint ?? (shortcut ? keysText(shortcut, ' ') : badge?.label), children: row }));
|
|
80
|
+
});
|
|
81
|
+
SidebarItem.displayName = 'SidebarItem';
|
|
82
|
+
/** A labelled, collapsible list of rows. In the rail the label becomes a short rule and the rows always show. */
|
|
83
|
+
export const SidebarGroup = React.forwardRef(function SidebarGroup({ label, open: openProp, defaultOpen = true, onOpenChange, rail: railProp, className, children, ...props }, ref) {
|
|
84
|
+
const rail = useRail(railProp);
|
|
85
|
+
const [own, setOwn] = React.useState(defaultOpen);
|
|
86
|
+
const open = openProp ?? own;
|
|
87
|
+
const toggle = () => {
|
|
88
|
+
if (openProp === undefined)
|
|
89
|
+
setOwn(!open);
|
|
90
|
+
onOpenChange?.(!open);
|
|
91
|
+
};
|
|
92
|
+
return (_jsxs("div", { ref: ref, className: cn('flex flex-col', rail ? 'mt-1 items-center gap-1' : 'gap-0.5', className), ...props, children: [rail ? (_jsx("div", { className: "my-1 h-px w-6 bg-border", "aria-hidden": "true" })) : (_jsxs("button", { type: "button", onClick: toggle, "aria-expanded": open, className: "mt-4 flex h-7 items-center justify-between rounded-md px-2 text-micro text-subtle-foreground outline-none hover:text-foreground focus-visible:shadow-[var(--focus-ring)]", children: [label, _jsx(ChevronDown, { size: 14, "aria-hidden": "true", className: cn('text-faint transition-transform duration-200', !open && '-rotate-90') })] })), (open || rail) && children] }));
|
|
93
|
+
});
|
|
94
|
+
SidebarGroup.displayName = 'SidebarGroup';
|
|
95
|
+
/** The user row and its menu (20.4-user-menu.png). */
|
|
96
|
+
export const SidebarUser = React.forwardRef(function SidebarUser({ person, meta, rail: railProp, className, children, ...props }, ref) {
|
|
97
|
+
const rail = useRail(railProp);
|
|
98
|
+
const name = personName(person);
|
|
99
|
+
const trigger = (_jsxs(DropdownMenuTrigger, { ref: ref, "aria-label": `${name}, open the menu`, className: cn('flex w-full min-w-0 items-center gap-2 rounded-md px-2 py-1 text-left outline-none transition-colors duration-[120ms] hover:bg-sidebar-hover focus-visible:shadow-[var(--focus-ring)]',
|
|
100
|
+
// The rail's avatar sits 4px closer to the row below it than the rows above it do (20.1).
|
|
101
|
+
rail && cn(railClass, '-mt-1'), className), ...props, children: [_jsx(Avatar, { person: person, size: 24 }), !rail && (_jsxs(_Fragment, { children: [_jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "truncate text-body leading-4 font-medium text-foreground", children: name }), meta && _jsx("span", { className: "truncate text-label leading-4 font-normal text-subtle-foreground", children: meta })] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": "true", className: "shrink-0 text-subtle-foreground" })] }))] }));
|
|
102
|
+
return (_jsxs(DropdownMenu, { children: [rail ? _jsx(RailTip, { label: name, children: trigger }) : trigger, _jsxs(DropdownMenuContent, { side: rail ? 'right' : 'top', align: rail ? 'end' : 'start', sideOffset: rail ? 8 : 0, className: "w-[223px]", children: [_jsx(DropdownMenuLabel, { className: "truncate text-micro text-subtle-foreground", children: name }), children] })] }));
|
|
103
|
+
});
|
|
104
|
+
SidebarUser.displayName = 'SidebarUser';
|
|
105
|
+
/** The Search field that opens the command palette. It is a button: the palette has the real input. */
|
|
106
|
+
export const SidebarSearch = React.forwardRef(function SidebarSearch({ rail: railProp, className, ...props }, ref) {
|
|
107
|
+
const rail = useRail(railProp);
|
|
108
|
+
const button = (_jsxs("button", { ref: ref, type: "button", "aria-label": rail ? 'Search' : undefined, "aria-keyshortcuts": "Meta+K Control+K", className: cn(rail
|
|
109
|
+
? cn(itemClass(false), railClass)
|
|
110
|
+
: 'flex h-8 w-full items-center gap-2 rounded-lg border border-border bg-card px-[7px] text-body text-subtle-foreground shadow-xs outline-none transition-colors duration-[120ms] hover:border-input hover:text-muted-foreground focus-visible:shadow-[var(--focus-ring)]', className), ...props, children: [_jsx(Search, { size: rail ? 16 : 14, strokeWidth: 1.8, "aria-hidden": "true", className: "shrink-0" }), !rail && (_jsxs(_Fragment, { children: [_jsx("span", { className: "flex-1 text-left", children: "Search" }), _jsx(Kbd, { keys: ['mod', 'K'] })] }))] }));
|
|
111
|
+
return rail ? (_jsx(RailTip, { label: "Search", hint: keysText(['mod', 'K'], ' '), children: button })) : (button);
|
|
112
|
+
});
|
|
113
|
+
SidebarSearch.displayName = 'SidebarSearch';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type SolePerson } from '../../lib/person.ts';
|
|
3
|
+
export type AvatarSize = 16 | 20 | 24 | 32 | 40 | 48 | 64;
|
|
4
|
+
export interface AvatarProps extends Omit<React.ComponentPropsWithoutRef<'span'>, 'children'> {
|
|
5
|
+
/** `null` or `undefined` draws a "?", for someone who has gone. A bot (`isBot`) draws the bot icon. */
|
|
6
|
+
person: SolePerson | null | undefined;
|
|
7
|
+
size?: AvatarSize;
|
|
8
|
+
/** Set when the avatar stands alone, so screen readers hear the name. */
|
|
9
|
+
label?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare const Avatar: React.ForwardRefExoticComponent<AvatarProps & React.RefAttributes<HTMLSpanElement>>;
|
|
12
|
+
export interface AvatarStackProps extends Omit<React.ComponentPropsWithoutRef<'span'>, 'children'> {
|
|
13
|
+
people: SolePerson[];
|
|
14
|
+
/** How many avatars to show before "+n". */
|
|
15
|
+
max?: number;
|
|
16
|
+
size?: AvatarSize;
|
|
17
|
+
}
|
|
18
|
+
/** Up to `max` avatars overlapping, then "+n". */
|
|
19
|
+
export declare const AvatarStack: React.ForwardRefExoticComponent<AvatarStackProps & React.RefAttributes<HTMLSpanElement>>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { Bot } from 'lucide-react';
|
|
5
|
+
import { cn } from '../../lib/cn.js';
|
|
6
|
+
import { avatarColour } from '../../lib/colour.js';
|
|
7
|
+
import { initials, personName } from '../../lib/person.js';
|
|
8
|
+
/** The picture in the circle, or the initials when there is none or it did not load. */
|
|
9
|
+
function Face({ url, name }) {
|
|
10
|
+
// The address that failed, so a new picture (a new address) gets its own try.
|
|
11
|
+
const [failed, setFailed] = React.useState(null);
|
|
12
|
+
if (url && failed !== url) {
|
|
13
|
+
return _jsx("img", { src: url, alt: "", draggable: false, decoding: "async", referrerPolicy: "no-referrer", onError: () => setFailed(url), className: "size-full rounded-full object-cover" });
|
|
14
|
+
}
|
|
15
|
+
return _jsx(_Fragment, { children: initials(name) });
|
|
16
|
+
}
|
|
17
|
+
export const Avatar = React.forwardRef(function Avatar({ person, size = 20, label, className, style, ...props }, ref) {
|
|
18
|
+
const name = personName(person);
|
|
19
|
+
const box = { width: size, height: size, fontSize: Math.round(size * 0.36) };
|
|
20
|
+
const a11y = label ? { role: 'img', 'aria-label': name, title: name } : { 'aria-hidden': true };
|
|
21
|
+
if (!person || person.isBot) {
|
|
22
|
+
return (_jsx("span", { ref: ref, ...a11y, style: { ...box, ...style }, className: cn('inline-flex shrink-0 items-center justify-center rounded-full border border-border bg-neutral-soft text-subtle-foreground select-none', className), ...props, children: person?.isBot ? _jsx(Bot, { size: Math.round(size * 0.6), strokeWidth: 1.8, "aria-hidden": "true" }) : '?' }));
|
|
23
|
+
}
|
|
24
|
+
const colour = avatarColour(person.id);
|
|
25
|
+
return (_jsx("span", { ref: ref, ...a11y, "data-colour": colour, style: { ...box, backgroundColor: `${colour}18`, borderColor: `${colour}33`, color: colour, ...style }, className: cn('inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full border leading-none font-bold tracking-tight select-none', className), ...props, children: _jsx(Face, { url: person.avatarUrl, name: name }) }));
|
|
26
|
+
});
|
|
27
|
+
Avatar.displayName = 'Avatar';
|
|
28
|
+
/** Up to `max` avatars overlapping, then "+n". */
|
|
29
|
+
export const AvatarStack = React.forwardRef(function AvatarStack({ people, max = 2, size = 20, className, ...props }, ref) {
|
|
30
|
+
if (!people.length)
|
|
31
|
+
return null;
|
|
32
|
+
const shown = people.slice(0, max);
|
|
33
|
+
const extra = people.length - shown.length;
|
|
34
|
+
const names = people.map((p) => personName(p)).join(', ');
|
|
35
|
+
return (_jsxs("span", { ref: ref, role: "img", "aria-label": names, title: names, className: cn('inline-flex shrink-0 items-center', className), ...props, children: [shown.map((p, i) => (_jsx(Avatar, { person: p, size: size, className: cn('ring-2 ring-card', i > 0 && '-ml-1') }, p.id))), extra > 0 && (_jsxs("span", { style: { height: size, minWidth: size, fontSize: Math.round(size * 0.45) }, className: "-ml-1 inline-flex items-center justify-center rounded-full border border-border bg-neutral-soft px-1 font-semibold text-neutral-text ring-2 ring-card", children: ["+", extra] }))] }));
|
|
36
|
+
});
|
|
37
|
+
AvatarStack.displayName = 'AvatarStack';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type LucideIcon } from 'lucide-react';
|
|
3
|
+
export type BannerTone = 'danger' | 'warning' | 'success' | 'info';
|
|
4
|
+
export interface BannerProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'role'> {
|
|
5
|
+
tone: BannerTone;
|
|
6
|
+
/** Overrides the tone's icon, e.g. CalendarX for a review that is due. */
|
|
7
|
+
icon?: LucideIcon;
|
|
8
|
+
/** Buttons on the right, 32px: the quiet one first, the main action last. */
|
|
9
|
+
actions?: React.ReactNode;
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
/** Defaults to alert for danger and status otherwise. */
|
|
12
|
+
role?: 'alert' | 'status';
|
|
13
|
+
}
|
|
14
|
+
export declare const Banner: React.ForwardRefExoticComponent<BannerProps & React.RefAttributes<HTMLDivElement>>;
|
|
15
|
+
export interface InlineErrorProps extends React.ComponentPropsWithoutRef<'p'> {
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
}
|
|
18
|
+
/** One line under a field when something failed: "That email or password is not right." */
|
|
19
|
+
export declare const InlineError: React.ForwardRefExoticComponent<InlineErrorProps & React.RefAttributes<HTMLParagraphElement>>;
|
|
20
|
+
export interface InlineSuccessProps extends React.ComponentPropsWithoutRef<'p'> {
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
}
|
|
23
|
+
/** One line after an action that worked: "The key works. Gemini answered in 1.2 s." */
|
|
24
|
+
export declare const InlineSuccess: React.ForwardRefExoticComponent<InlineSuccessProps & React.RefAttributes<HTMLParagraphElement>>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { CircleAlert, CircleCheck, Info, TriangleAlert } from 'lucide-react';
|
|
5
|
+
import { cn } from '../../lib/cn.js';
|
|
6
|
+
const TONES = {
|
|
7
|
+
danger: { box: 'border-danger-border bg-danger-soft text-danger-text', icon: 'text-danger-text', Icon: CircleAlert },
|
|
8
|
+
warning: { box: 'border-warning-border bg-warning-soft text-warning-text', icon: 'text-warning-text', Icon: TriangleAlert },
|
|
9
|
+
success: { box: 'border-success-border bg-success-soft text-success-text', icon: 'text-success', Icon: CircleCheck },
|
|
10
|
+
info: { box: 'border-brand/30 bg-accent text-accent-foreground', icon: 'text-accent-foreground', Icon: Info },
|
|
11
|
+
};
|
|
12
|
+
export const Banner = React.forwardRef(function Banner({ tone, icon: IconProp, actions, children, role, className, ...props }, ref) {
|
|
13
|
+
const t = TONES[tone];
|
|
14
|
+
const Icon = IconProp ?? t.Icon;
|
|
15
|
+
return (_jsxs("div", { ref: ref, role: role ?? (tone === 'danger' ? 'alert' : 'status'), "data-banner": tone, className: cn('flex items-center rounded-lg border py-2 pr-3 pl-2.5', t.box, className), ...props, children: [_jsx(Icon, { size: 16, strokeWidth: 1.8, "aria-hidden": "true", className: cn('mr-2.5 shrink-0', t.icon) }), _jsx("div", { className: "min-w-0 flex-1 py-px text-body", children: children }), actions && _jsx("div", { className: "ml-3 flex shrink-0 items-center gap-2", children: actions })] }));
|
|
16
|
+
});
|
|
17
|
+
Banner.displayName = 'Banner';
|
|
18
|
+
/** One line under a field when something failed: "That email or password is not right." */
|
|
19
|
+
export const InlineError = React.forwardRef(function InlineError({ children, className, ...props }, ref) {
|
|
20
|
+
return (_jsxs("p", { ref: ref, role: "alert", className: cn('flex items-start gap-1.5 text-label text-danger-text', className), ...props, children: [_jsx(CircleAlert, { size: 14, strokeWidth: 1.8, "aria-hidden": "true", className: "mt-px shrink-0" }), _jsx("span", { className: "min-w-0", children: children })] }));
|
|
21
|
+
});
|
|
22
|
+
InlineError.displayName = 'InlineError';
|
|
23
|
+
/** One line after an action that worked: "The key works. Gemini answered in 1.2 s." */
|
|
24
|
+
export const InlineSuccess = React.forwardRef(function InlineSuccess({ children, className, ...props }, ref) {
|
|
25
|
+
return (_jsxs("p", { ref: ref, role: "status", className: cn('flex items-start gap-1.5 text-label text-success-text', className), ...props, children: [_jsx(CircleCheck, { size: 14, strokeWidth: 1.8, "aria-hidden": "true", className: "mt-px shrink-0 text-success" }), _jsx("span", { className: "min-w-0", children: children })] }));
|
|
26
|
+
});
|
|
27
|
+
InlineSuccess.displayName = 'InlineSuccess';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Column header for kanban (50.3): an icon, 13/600 name, count, and a + on the right.
|
|
4
|
+
* The icon is the caller's (HQ passes its state icon), so the part knows no domain.
|
|
5
|
+
*/
|
|
6
|
+
export declare const ColumnHeader: React.ForwardRefExoticComponent<Omit<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref">, "children"> & {
|
|
7
|
+
name: string;
|
|
8
|
+
count?: number;
|
|
9
|
+
/** Before the name: a 16px decorative icon. */
|
|
10
|
+
icon?: React.ReactNode;
|
|
11
|
+
/** Shows the + button when given. */
|
|
12
|
+
onAdd?: () => void;
|
|
13
|
+
/** The + button's name for screen readers and its tooltip. Defaults to "Add to <name>". */
|
|
14
|
+
addLabel?: string;
|
|
15
|
+
} & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { Plus } from 'lucide-react';
|
|
5
|
+
import { cn } from '../../lib/cn.js';
|
|
6
|
+
import { Button } from '../ui/button.js';
|
|
7
|
+
/**
|
|
8
|
+
* Column header for kanban (50.3): an icon, 13/600 name, count, and a + on the right.
|
|
9
|
+
* The icon is the caller's (HQ passes its state icon), so the part knows no domain.
|
|
10
|
+
*/
|
|
11
|
+
export const ColumnHeader = React.forwardRef(function ColumnHeader({ name, count, icon, onAdd, addLabel, className, ...props }, ref) {
|
|
12
|
+
const label = addLabel ?? `Add to ${name}`;
|
|
13
|
+
return (_jsxs("div", { ref: ref, className: cn('flex h-8 min-w-0 items-center gap-2 pr-0.5 pl-1', className), ...props, children: [icon, _jsx("h3", { className: "truncate text-body font-semibold text-foreground", children: name }), count !== undefined && _jsx("span", { className: "tnum text-label font-normal text-subtle-foreground", children: count }), onAdd && (_jsx(Button, { variant: "ghost", size: "icon-xs", onClick: onAdd, "aria-label": label, title: label, className: "ml-auto text-faint hover:text-foreground", children: _jsx(Plus, { "aria-hidden": "true", className: "size-3.5" }) }))] }));
|
|
14
|
+
});
|
|
15
|
+
ColumnHeader.displayName = 'ColumnHeader';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface CopyFieldProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'children'> {
|
|
3
|
+
value: string;
|
|
4
|
+
/** A password shown once: monospace, 15px. */
|
|
5
|
+
secret?: boolean;
|
|
6
|
+
/** What is copied, for screen readers: "Temporary password", "Sign-up link". */
|
|
7
|
+
label: string;
|
|
8
|
+
onCopied?: () => void;
|
|
9
|
+
}
|
|
10
|
+
export declare const CopyField: React.ForwardRefExoticComponent<CopyFieldProps & React.RefAttributes<HTMLDivElement>>;
|