@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
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { FileText, Lock } from 'lucide-react';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { AvatarStack } from '../components/sole/avatar.js';
|
|
6
|
+
import { cn } from '../lib/cn.js';
|
|
7
|
+
import { dateIn, formatDate } from '../lib/dates.js';
|
|
8
|
+
import { SoleLink, useSole, useToday } from '../lib/sole-provider.js';
|
|
9
|
+
import { ReviewBadge } from './review-badge.js';
|
|
10
|
+
const NO_TAGS = [];
|
|
11
|
+
const NO_PEOPLE = [];
|
|
12
|
+
export const PageRow = React.forwardRef(function PageRow({ href, title, isPrivate = false, reviewDue, spaceName, folderName, tags = NO_TAGS, commentCount = 0, owner, collaborators = NO_PEOPLE, updatedAt, today, actions, className, ...props }, ref) {
|
|
13
|
+
const current = useToday();
|
|
14
|
+
const { timeZone } = useSole();
|
|
15
|
+
const day = today ?? current;
|
|
16
|
+
const people = [owner, ...collaborators.filter((c) => c.id !== owner?.id)].filter((p) => !!p);
|
|
17
|
+
const meta = [];
|
|
18
|
+
if (spaceName)
|
|
19
|
+
meta.push({ key: 'space', text: spaceName });
|
|
20
|
+
meta.push({ key: 'folder', text: folderName ?? 'Unfiled' });
|
|
21
|
+
if (tags.length)
|
|
22
|
+
meta.push({ key: 'tags', text: tags.map((t) => t.name).join(', ') });
|
|
23
|
+
if (isPrivate)
|
|
24
|
+
meta.push({ key: 'private', text: 'Private to you' });
|
|
25
|
+
if (commentCount > 0)
|
|
26
|
+
meta.push({ key: 'comments', text: `${commentCount} ${commentCount === 1 ? 'comment' : 'comments'}` });
|
|
27
|
+
const Icon = isPrivate ? Lock : FileText;
|
|
28
|
+
return (_jsxs("div", { ...props, ref: ref, className: cn('group/page relative flex h-[52px] items-center gap-3 border-b border-hairline px-3 hover:bg-row-hover', className), children: [_jsx(Icon, { size: 16, strokeWidth: 1.8, "aria-hidden": "true", className: "shrink-0 text-subtle-foreground" }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx(SoleLink, { href: href, className: "truncate text-strong font-medium text-foreground outline-none after:absolute after:inset-0 focus-visible:underline", children: title || 'Untitled' }), _jsx(ReviewBadge, { reviewDue: reviewDue, today: day })] }), _jsx("p", { className: "truncate text-label font-normal text-subtle-foreground", children: meta.map((m, i) => (_jsxs("span", { children: [i > 0 && _jsx("span", { "aria-hidden": "true", className: "mx-[3px]", children: " \u00B7 " }), m.text] }, m.key))) })] }), _jsx(AvatarStack, { people: people, max: 3 }), _jsx("time", { dateTime: updatedAt, className: "tnum w-[84px] shrink-0 text-right text-label font-normal text-subtle-foreground", children: formatDate(dateIn(updatedAt, timeZone), day, { year: true }) }), actions && _jsx("div", { className: "relative z-10 opacity-0 group-hover/page:opacity-100 focus-within:opacity-100", children: actions })] }));
|
|
29
|
+
});
|
|
30
|
+
PageRow.displayName = 'PageRow';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { Priority } from './types.ts';
|
|
3
|
+
export interface PriorityIconProps extends Omit<React.ComponentPropsWithoutRef<'svg'>, 'children'> {
|
|
4
|
+
priority: Priority;
|
|
5
|
+
size?: number;
|
|
6
|
+
/** True when the priority is written next to it (menus, buttons). */
|
|
7
|
+
decorative?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare const PriorityIcon: React.ForwardRefExoticComponent<PriorityIconProps & React.RefAttributes<SVGSVGElement>>;
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
import { PRIORITY_LABELS } from './types.js';
|
|
6
|
+
// Only urgent is coloured (rule 5.3); the rest are 3 bars with 1, 2 or 3 filled, so
|
|
7
|
+
// priority is readable without colour (brand-and-ux 5.9).
|
|
8
|
+
const FILLED = { high: 3, medium: 2, low: 1, none: 0 };
|
|
9
|
+
export const PriorityIcon = React.forwardRef(function PriorityIcon({ priority, size = 16, decorative = false, className, ...props }, ref) {
|
|
10
|
+
const label = priority === 'none' ? PRIORITY_LABELS.none : `${PRIORITY_LABELS[priority]} priority`;
|
|
11
|
+
const a11y = decorative ? { 'aria-hidden': true } : { role: 'img', 'aria-label': label };
|
|
12
|
+
if (priority === 'urgent') {
|
|
13
|
+
return (_jsxs("svg", { ...props, ...a11y, ref: ref, width: size, height: size, viewBox: "0 0 16 16", className: cn('shrink-0', className), children: [!decorative && _jsx("title", { children: label }), _jsx("rect", { x: "1", y: "1", width: "14", height: "14", rx: "3", fill: "#DC2626" }), _jsx("path", { d: "M8 4.25v4.5", stroke: "#fff", strokeWidth: "1.8", strokeLinecap: "round" }), _jsx("circle", { cx: "8", cy: "11.4", r: "1.05", fill: "#fff" })] }));
|
|
14
|
+
}
|
|
15
|
+
const filled = FILLED[priority];
|
|
16
|
+
const bars = [
|
|
17
|
+
{ x: 3, h: 4 },
|
|
18
|
+
{ x: 7, h: 7 },
|
|
19
|
+
{ x: 11, h: 10 },
|
|
20
|
+
];
|
|
21
|
+
return (_jsxs("svg", { ...props, ...a11y, ref: ref, width: size, height: size, viewBox: "0 0 16 16", className: cn('shrink-0', className), children: [!decorative && _jsx("title", { children: label }), bars.map((bar, i) => (_jsx("rect", { x: bar.x, y: 13 - bar.h, width: "2", height: bar.h, rx: "0.75", className: i < filled ? 'fill-muted-foreground' : 'fill-faint' }, bar.x)))] }));
|
|
22
|
+
});
|
|
23
|
+
PriorityIcon.displayName = 'PriorityIcon';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { LucideProps } from 'lucide-react';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import type { PropertyType } from './types.ts';
|
|
4
|
+
export interface PropertyTypeIconProps extends Omit<LucideProps, 'ref'> {
|
|
5
|
+
type: PropertyType;
|
|
6
|
+
}
|
|
7
|
+
export declare const PropertyTypeIcon: React.ForwardRefExoticComponent<PropertyTypeIconProps & React.RefAttributes<SVGSVGElement>>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Calendar, CircleChevronDown, Hash, Link as LinkIcon, ListChecks, SquareCheck, Type, User } from 'lucide-react';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { cn } from '../lib/cn.js';
|
|
6
|
+
// One glyph per kind of custom property (0014), in the settings list and the type picker,
|
|
7
|
+
// so "Sprint" reads as a choice and "Story points" as a number at a glance.
|
|
8
|
+
const ICONS = {
|
|
9
|
+
text: Type,
|
|
10
|
+
number: Hash,
|
|
11
|
+
date: Calendar,
|
|
12
|
+
select: CircleChevronDown,
|
|
13
|
+
multi_select: ListChecks,
|
|
14
|
+
person: User,
|
|
15
|
+
checkbox: SquareCheck,
|
|
16
|
+
url: LinkIcon,
|
|
17
|
+
};
|
|
18
|
+
export const PropertyTypeIcon = React.forwardRef(function PropertyTypeIcon({ type, className, ...props }, ref) {
|
|
19
|
+
const Icon = ICONS[type];
|
|
20
|
+
return _jsx(Icon, { strokeWidth: 1.8, ...props, ref: ref, "aria-hidden": "true", className: cn('size-3.5 shrink-0 text-muted-foreground', className), "data-type-icon": type });
|
|
21
|
+
});
|
|
22
|
+
PropertyTypeIcon.displayName = 'PropertyTypeIcon';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { SoleLinkProps } from '../lib/sole-provider.tsx';
|
|
3
|
+
export interface RecentItemProps extends Omit<SoleLinkProps, 'children' | 'title'> {
|
|
4
|
+
kind: 'page' | 'work_item';
|
|
5
|
+
/** For a work item, "ENG-14 Fix the pricing copy on the plans page". */
|
|
6
|
+
title: React.ReactNode;
|
|
7
|
+
/** "Knowledge", "Engineering". */
|
|
8
|
+
where?: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
/** Recent item (30.1 Recently viewed): a page or work item icon, the title, where it lives. */
|
|
11
|
+
export declare const RecentItem: React.ForwardRefExoticComponent<RecentItemProps & React.RefAttributes<HTMLAnchorElement>>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { FileText, SquareCheck } from 'lucide-react';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { cn } from '../lib/cn.js';
|
|
6
|
+
import { SoleLink } from '../lib/sole-provider.js';
|
|
7
|
+
/** Recent item (30.1 Recently viewed): a page or work item icon, the title, where it lives. */
|
|
8
|
+
export const RecentItem = React.forwardRef(function RecentItem({ kind, title, where, className, ...props }, ref) {
|
|
9
|
+
const Icon = kind === 'page' ? FileText : SquareCheck;
|
|
10
|
+
return (_jsxs(SoleLink, { ref: ref, className: cn('flex min-w-0 items-center gap-2.5 rounded-lg px-2 py-2 outline-none transition-colors duration-[120ms] hover:bg-row-hover focus-visible:shadow-[inset_0_0_0_2px_var(--brand)]', className), ...props, children: [_jsx(Icon, { size: 16, strokeWidth: 1.8, "aria-hidden": "true", className: "shrink-0 text-subtle-foreground" }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "truncate text-body leading-[18px] font-medium text-foreground", children: title }), where && _jsx("span", { className: "truncate text-label font-normal text-subtle-foreground", children: where })] })] }));
|
|
11
|
+
});
|
|
12
|
+
RecentItem.displayName = 'RecentItem';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { DateOnly } from '../lib/dates.ts';
|
|
3
|
+
import type { ReviewState } from './types.ts';
|
|
4
|
+
/** A review date this many days away, or fewer, is "due soon". */
|
|
5
|
+
export declare const REVIEW_SOON_DAYS = 14;
|
|
6
|
+
/**
|
|
7
|
+
* current: more than 14 days away. due: today up to 14 days away. overdue: the date
|
|
8
|
+
* has passed. unknown: never reviewed, or no next date. The same rule runs in SQL as
|
|
9
|
+
* hq.page_review_state() in Sole HQ; keep the two in step.
|
|
10
|
+
*/
|
|
11
|
+
export declare function reviewState(reviewDue: DateOnly | null, today: DateOnly): ReviewState;
|
|
12
|
+
export interface ReviewBadgeModel {
|
|
13
|
+
tone: 'warning' | 'danger' | 'neutral';
|
|
14
|
+
label: string;
|
|
15
|
+
/** Tooltip, with the full date where there is one. */
|
|
16
|
+
title: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The short pill for list rows. Current pages get no pill at all: a badge on a third
|
|
20
|
+
* of the rows stops being read (U52).
|
|
21
|
+
*/
|
|
22
|
+
export declare function reviewBadge(reviewDue: DateOnly | null, today: DateOnly): ReviewBadgeModel | null;
|
|
23
|
+
export interface ReviewBadgeProps extends React.ComponentPropsWithoutRef<'span'> {
|
|
24
|
+
/** The page's next review date, or null if it was never reviewed. */
|
|
25
|
+
reviewDue: DateOnly | null;
|
|
26
|
+
/** Defaults to today on the provider's clock. */
|
|
27
|
+
today?: DateOnly;
|
|
28
|
+
}
|
|
29
|
+
export declare const ReviewBadge: React.ForwardRefExoticComponent<ReviewBadgeProps & React.RefAttributes<HTMLSpanElement>>;
|
|
30
|
+
export interface ReviewStateBadgeProps extends React.ComponentPropsWithoutRef<'span'> {
|
|
31
|
+
state: ReviewState | null | undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The same pill from a review state alone, where the date is not known (Ask Sole's
|
|
35
|
+
* sources carry hq.page_review_state, not the date). Quiet for a current page.
|
|
36
|
+
*/
|
|
37
|
+
export declare const ReviewStateBadge: React.ForwardRefExoticComponent<ReviewStateBadgeProps & React.RefAttributes<HTMLSpanElement>>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { CalendarCheck } from 'lucide-react';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { cn } from '../lib/cn.js';
|
|
6
|
+
import { daysBetween, formatDate, formatLongDate } from '../lib/dates.js';
|
|
7
|
+
import { useToday } from '../lib/sole-provider.js';
|
|
8
|
+
// Review pill for wiki rows (brand-and-ux 5.11). A current page shows nothing: the
|
|
9
|
+
// pill only appears when there is something to do.
|
|
10
|
+
/** A review date this many days away, or fewer, is "due soon". */
|
|
11
|
+
export const REVIEW_SOON_DAYS = 14;
|
|
12
|
+
/**
|
|
13
|
+
* current: more than 14 days away. due: today up to 14 days away. overdue: the date
|
|
14
|
+
* has passed. unknown: never reviewed, or no next date. The same rule runs in SQL as
|
|
15
|
+
* hq.page_review_state() in Sole HQ; keep the two in step.
|
|
16
|
+
*/
|
|
17
|
+
export function reviewState(reviewDue, today) {
|
|
18
|
+
if (!reviewDue)
|
|
19
|
+
return 'unknown';
|
|
20
|
+
if (reviewDue < today)
|
|
21
|
+
return 'overdue';
|
|
22
|
+
if (daysBetween(today, reviewDue) <= REVIEW_SOON_DAYS)
|
|
23
|
+
return 'due';
|
|
24
|
+
return 'current';
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The short pill for list rows. Current pages get no pill at all: a badge on a third
|
|
28
|
+
* of the rows stops being read (U52).
|
|
29
|
+
*/
|
|
30
|
+
export function reviewBadge(reviewDue, today) {
|
|
31
|
+
const state = reviewState(reviewDue, today);
|
|
32
|
+
switch (state) {
|
|
33
|
+
case 'current':
|
|
34
|
+
return null;
|
|
35
|
+
case 'due':
|
|
36
|
+
return {
|
|
37
|
+
tone: 'warning',
|
|
38
|
+
label: `Review by ${formatDate(reviewDue, today)}`,
|
|
39
|
+
title: `Review by ${formatLongDate(reviewDue)}`,
|
|
40
|
+
};
|
|
41
|
+
case 'overdue':
|
|
42
|
+
return { tone: 'danger', label: 'Review due', title: `Review was due ${formatLongDate(reviewDue)}` };
|
|
43
|
+
case 'unknown':
|
|
44
|
+
return { tone: 'neutral', label: 'Never reviewed', title: 'Nobody has confirmed this page.' };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// One icon for review, CalendarCheck (brand-and-ux 5.7); the tone says which state.
|
|
48
|
+
const TONES = {
|
|
49
|
+
warning: 'border-warning-border bg-warning-soft text-warning-text',
|
|
50
|
+
danger: 'border-danger-border bg-danger-soft text-danger-text',
|
|
51
|
+
neutral: 'border-border bg-transparent text-muted-foreground',
|
|
52
|
+
};
|
|
53
|
+
export const ReviewBadge = React.forwardRef(function ReviewBadge({ reviewDue, today, className, ...props }, ref) {
|
|
54
|
+
const current = useToday();
|
|
55
|
+
const badge = reviewBadge(reviewDue, today ?? current);
|
|
56
|
+
if (!badge)
|
|
57
|
+
return null;
|
|
58
|
+
const box = TONES[badge.tone];
|
|
59
|
+
return (_jsxs("span", { ...props, ref: ref, "data-review": badge.tone, title: badge.title, className: cn('inline-flex h-[22px] shrink-0 items-center gap-[5px] rounded-md border px-2 text-label whitespace-nowrap', box, className), children: [_jsx(CalendarCheck, { size: 12, strokeWidth: 1.8, "aria-hidden": "true" }), badge.label] }));
|
|
60
|
+
});
|
|
61
|
+
ReviewBadge.displayName = 'ReviewBadge';
|
|
62
|
+
const STATE_BADGE = {
|
|
63
|
+
overdue: { tone: 'danger', label: 'Review due', title: 'Past its review date' },
|
|
64
|
+
due: { tone: 'warning', label: 'Review soon', title: 'Due for review within 14 days' },
|
|
65
|
+
unknown: { tone: 'neutral', label: 'Never reviewed', title: 'Nobody has confirmed this page.' },
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* The same pill from a review state alone, where the date is not known (Ask Sole's
|
|
69
|
+
* sources carry hq.page_review_state, not the date). Quiet for a current page.
|
|
70
|
+
*/
|
|
71
|
+
export const ReviewStateBadge = React.forwardRef(function ReviewStateBadge({ state, className, ...props }, ref) {
|
|
72
|
+
if (!state || state === 'current')
|
|
73
|
+
return null;
|
|
74
|
+
const badge = STATE_BADGE[state];
|
|
75
|
+
return (_jsxs("span", { ...props, ref: ref, "data-review": badge.tone, title: badge.title, className: cn('inline-flex h-[22px] shrink-0 items-center gap-[5px] rounded-md border px-2 text-label whitespace-nowrap', TONES[badge.tone], className), children: [_jsx(CalendarCheck, { size: 12, strokeWidth: 1.8, "aria-hidden": "true" }), badge.label] }));
|
|
76
|
+
});
|
|
77
|
+
ReviewStateBadge.displayName = 'ReviewStateBadge';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { StateGroup } from './types.ts';
|
|
3
|
+
export declare const STATE_GROUP_LABELS: Record<StateGroup, string>;
|
|
4
|
+
export interface StateIconProps extends React.ComponentPropsWithoutRef<'span'> {
|
|
5
|
+
group: StateGroup;
|
|
6
|
+
/** The state's own name, for the tooltip and screen readers. Defaults to the group's name. */
|
|
7
|
+
name?: string;
|
|
8
|
+
size?: number;
|
|
9
|
+
/** True when the name is already written next to it. */
|
|
10
|
+
decorative?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare const StateIcon: React.ForwardRefExoticComponent<StateIconProps & React.RefAttributes<HTMLSpanElement>>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Circle, CircleCheck, CircleDashed, CircleX } from 'lucide-react';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { cn } from '../lib/cn.js';
|
|
6
|
+
// One icon shape per state group, so Backlog and Todo differ by shape and not only by
|
|
7
|
+
// colour (brand-and-ux 5.8, U29). Colour is by group, never by the state's own hex.
|
|
8
|
+
/** In progress: a ring with its left half filled (5.8, 02-components.png). Lucide has no such icon. */
|
|
9
|
+
function CircleHalf({ size = 24, strokeWidth = 2, ...props }) {
|
|
10
|
+
return (_jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: strokeWidth, ...props, children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("path", { d: "M12 6.5a5.5 5.5 0 0 0 0 11z", fill: "currentColor", stroke: "none" })] }));
|
|
11
|
+
}
|
|
12
|
+
const ICONS = {
|
|
13
|
+
backlog: CircleDashed,
|
|
14
|
+
unstarted: Circle,
|
|
15
|
+
started: CircleHalf,
|
|
16
|
+
completed: CircleCheck,
|
|
17
|
+
cancelled: CircleX,
|
|
18
|
+
};
|
|
19
|
+
const COLOURS = {
|
|
20
|
+
backlog: 'text-state-backlog',
|
|
21
|
+
unstarted: 'text-state-unstarted',
|
|
22
|
+
started: 'text-state-started',
|
|
23
|
+
completed: 'text-state-completed',
|
|
24
|
+
cancelled: 'text-state-cancelled',
|
|
25
|
+
};
|
|
26
|
+
export const STATE_GROUP_LABELS = {
|
|
27
|
+
backlog: 'Backlog',
|
|
28
|
+
unstarted: 'Todo',
|
|
29
|
+
started: 'In progress',
|
|
30
|
+
completed: 'Done',
|
|
31
|
+
cancelled: 'Cancelled',
|
|
32
|
+
};
|
|
33
|
+
export const StateIcon = React.forwardRef(function StateIcon({ group, name, size = 16, decorative = false, className, ...props }, ref) {
|
|
34
|
+
const Icon = ICONS[group];
|
|
35
|
+
const label = name ?? STATE_GROUP_LABELS[group];
|
|
36
|
+
if (decorative) {
|
|
37
|
+
return (_jsx("span", { ...props, ref: ref, "aria-hidden": "true", className: cn('inline-flex shrink-0', COLOURS[group], className), children: _jsx(Icon, { size: size, strokeWidth: 1.8 }) }));
|
|
38
|
+
}
|
|
39
|
+
return (_jsx("span", { ...props, ref: ref, role: "img", "aria-label": label, title: label, className: cn('inline-flex shrink-0', COLOURS[group], className), children: _jsx(Icon, { size: size, strokeWidth: 1.8, "aria-hidden": "true" }) }));
|
|
40
|
+
});
|
|
41
|
+
StateIcon.displayName = 'StateIcon';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { StateGroup } from './types.ts';
|
|
3
|
+
export interface StatePillProps extends React.ComponentPropsWithoutRef<'span'> {
|
|
4
|
+
state: {
|
|
5
|
+
name: string;
|
|
6
|
+
group: StateGroup;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export declare const StatePill: React.ForwardRefExoticComponent<StatePillProps & React.RefAttributes<HTMLSpanElement>>;
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
import { StateIcon } from './state-icon.js';
|
|
6
|
+
// State with its name, for properties and column headers (brand-and-ux 5.8). Rows use
|
|
7
|
+
// the icon alone.
|
|
8
|
+
const TONES = {
|
|
9
|
+
backlog: 'bg-neutral-soft text-neutral-text',
|
|
10
|
+
unstarted: 'bg-neutral-soft text-neutral-text',
|
|
11
|
+
started: 'bg-warning-soft text-warning-text',
|
|
12
|
+
completed: 'bg-success-soft text-success-text',
|
|
13
|
+
cancelled: 'bg-neutral-soft text-subtle-foreground',
|
|
14
|
+
};
|
|
15
|
+
export const StatePill = React.forwardRef(function StatePill({ state, className, ...props }, ref) {
|
|
16
|
+
return (_jsxs("span", { ...props, ref: ref, className: cn('inline-flex h-[22px] shrink-0 items-center gap-[5px] rounded-md px-2 text-label whitespace-nowrap', TONES[state.group], className), children: [_jsx(StateIcon, { group: state.group, size: 12, decorative: true }), state.name] }));
|
|
17
|
+
});
|
|
18
|
+
StatePill.displayName = 'StatePill';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
/** The eight note colours. */
|
|
3
|
+
import type { StickyColour } from './types.ts';
|
|
4
|
+
export interface StickyProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'title'> {
|
|
5
|
+
colour?: StickyColour;
|
|
6
|
+
title?: React.ReactNode;
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A sticky (30.1 Home): one of the 8 note colours, 8px corners, 12px padding, a 13/600
|
|
11
|
+
* title and the note at 13px. Pass `onClick` or wrap it to edit; the body keeps its line breaks.
|
|
12
|
+
*/
|
|
13
|
+
export declare const Sticky: React.ForwardRefExoticComponent<StickyProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
const STICKY_BG = {
|
|
6
|
+
gray: 'bg-[var(--sticky-gray)]',
|
|
7
|
+
peach: 'bg-[var(--sticky-peach)]',
|
|
8
|
+
pink: 'bg-[var(--sticky-pink)]',
|
|
9
|
+
orange: 'bg-[var(--sticky-orange)]',
|
|
10
|
+
green: 'bg-[var(--sticky-green)]',
|
|
11
|
+
'light-blue': 'bg-[var(--sticky-light-blue)]',
|
|
12
|
+
'dark-blue': 'bg-[var(--sticky-dark-blue)]',
|
|
13
|
+
purple: 'bg-[var(--sticky-purple)]',
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A sticky (30.1 Home): one of the 8 note colours, 8px corners, 12px padding, a 13/600
|
|
17
|
+
* title and the note at 13px. Pass `onClick` or wrap it to edit; the body keeps its line breaks.
|
|
18
|
+
*/
|
|
19
|
+
export const Sticky = React.forwardRef(function Sticky({ colour = 'gray', title, children, className, ...props }, ref) {
|
|
20
|
+
return (_jsxs("div", { ref: ref, "data-sticky": colour, className: cn('flex min-h-[132px] min-w-0 flex-col rounded-lg p-3 text-sticky-foreground', STICKY_BG[colour], className), ...props, children: [title && _jsx("p", { className: "truncate text-body leading-4 font-semibold", children: title }), children && _jsx("div", { className: cn('text-body leading-[19px] whitespace-pre-line text-secondary-foreground', title && 'mt-[5px]'), children: children })] }));
|
|
21
|
+
});
|
|
22
|
+
Sticky.displayName = 'Sticky';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LucideIcon } from 'lucide-react';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
export interface TemplateCardProps extends Omit<React.ComponentPropsWithoutRef<'button'>, 'title'> {
|
|
4
|
+
icon: LucideIcon;
|
|
5
|
+
title: string;
|
|
6
|
+
description: string;
|
|
7
|
+
selected?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** Template card (60.10 New page): icon, 13/600 name, the 12px hint; selected is blue. */
|
|
10
|
+
export declare const TemplateCard: React.ForwardRefExoticComponent<TemplateCardProps & React.RefAttributes<HTMLButtonElement>>;
|
|
@@ -0,0 +1,9 @@
|
|
|
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
|
+
/** Template card (60.10 New page): icon, 13/600 name, the 12px hint; selected is blue. */
|
|
6
|
+
export const TemplateCard = React.forwardRef(function TemplateCard({ icon: Icon, title, description, selected = false, className, ...props }, ref) {
|
|
7
|
+
return (_jsxs("button", { ref: ref, type: "button", role: "radio", "aria-checked": selected, className: cn('flex min-h-[108px] min-w-0 flex-col items-start rounded-lg border p-3 text-left outline-none transition-colors duration-[120ms] focus-visible:shadow-[var(--focus-ring)]', selected ? 'border-brand bg-accent shadow-[inset_0_0_0_0.5px_var(--brand)]' : 'border-border bg-card hover:bg-row-hover', className), ...props, children: [_jsx(Icon, { size: 16, strokeWidth: 1.8, "aria-hidden": "true", className: selected ? 'text-accent-foreground' : 'text-foreground' }), _jsx("span", { className: cn('mt-2 text-body font-semibold', selected ? 'text-accent-foreground' : 'text-foreground'), children: title }), _jsx("span", { className: "mt-0.5 text-label font-normal text-subtle-foreground", children: description })] }));
|
|
8
|
+
});
|
|
9
|
+
TemplateCard.displayName = 'TemplateCard';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { DateOnly } from '../lib/dates.ts';
|
|
2
|
+
import type { Hue } from '../lib/hue.ts';
|
|
3
|
+
import type { SolePerson } from '../lib/person.ts';
|
|
4
|
+
/** Where a work item's state sits in its life. Colour and icon are by group, never by the state's own hex. */
|
|
5
|
+
export type StateGroup = 'backlog' | 'unstarted' | 'started' | 'completed' | 'cancelled';
|
|
6
|
+
export type Priority = 'urgent' | 'high' | 'medium' | 'low' | 'none';
|
|
7
|
+
export declare const PRIORITY_LABELS: Record<Priority, string>;
|
|
8
|
+
/** Review state of a wiki page. `due` means due within 14 days. */
|
|
9
|
+
export type ReviewState = 'current' | 'due' | 'overdue' | 'unknown';
|
|
10
|
+
/** The kinds of custom property a work board can track. */
|
|
11
|
+
export type PropertyType = 'text' | 'number' | 'date' | 'select' | 'multi_select' | 'person' | 'checkbox' | 'url';
|
|
12
|
+
/** The colours a sticky note can take. */
|
|
13
|
+
export type StickyColour = 'gray' | 'peach' | 'pink' | 'orange' | 'green' | 'light-blue' | 'dark-blue' | 'purple';
|
|
14
|
+
/**
|
|
15
|
+
* Everything WorkItemRow and BoardCard read about a work item, resolved: the fields of HQ's
|
|
16
|
+
* `WorkItemView` that the two parts use. HQ builds one from a view; a demo writes one by hand.
|
|
17
|
+
*/
|
|
18
|
+
export interface WorkItemSummary {
|
|
19
|
+
/** "ENG-14". */
|
|
20
|
+
key: string;
|
|
21
|
+
/** Where the item opens, "/b/ENG/14". */
|
|
22
|
+
href: string;
|
|
23
|
+
title: string;
|
|
24
|
+
priority: Priority;
|
|
25
|
+
/** Not loaded yet or deleted: the parts draw the item as Todo, with no state name. */
|
|
26
|
+
state?: {
|
|
27
|
+
name: string;
|
|
28
|
+
group: StateGroup;
|
|
29
|
+
} | null;
|
|
30
|
+
labels: Array<{
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
hue: Hue;
|
|
34
|
+
}>;
|
|
35
|
+
dueDate: DateOnly | null;
|
|
36
|
+
/** In the order they were added. */
|
|
37
|
+
assignees: SolePerson[];
|
|
38
|
+
/** Completed or cancelled: the due date is a plain fact and never red. */
|
|
39
|
+
isDone: boolean;
|
|
40
|
+
}
|
|
41
|
+
/** A board's name and emoji, the two things BoardChip draws. */
|
|
42
|
+
export interface BoardSummary {
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
/** Data, not copy: none, or an empty one, draws a folder. */
|
|
46
|
+
emoji: string | null;
|
|
47
|
+
/** "ENG". Optional: the parts link by `href`, not by key. */
|
|
48
|
+
key?: string;
|
|
49
|
+
}
|
package/dist/hq/types.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { DateOnly } from '../lib/dates.ts';
|
|
3
|
+
import type { SoleLinkProps } from '../lib/sole-provider.tsx';
|
|
4
|
+
import type { WorkItemSummary } from './types.ts';
|
|
5
|
+
export interface WorkItemRowProps extends Omit<SoleLinkProps, 'href' | 'children' | 'onClick'> {
|
|
6
|
+
item: WorkItemSummary;
|
|
7
|
+
/** Defaults to today on the provider's clock. */
|
|
8
|
+
today?: DateOnly;
|
|
9
|
+
selected?: boolean;
|
|
10
|
+
showAssignees?: boolean;
|
|
11
|
+
/** Keep the assignee slot's width when it is empty. False drops it, as Home's My work does (30.1). */
|
|
12
|
+
assigneeSlot?: boolean;
|
|
13
|
+
/** Open somewhere other than the full page, e.g. the side sheet. Cmd-click still opens the page. */
|
|
14
|
+
onOpen?: (item: WorkItemSummary) => void;
|
|
15
|
+
}
|
|
16
|
+
export declare const WorkItemRow: React.ForwardRefExoticComponent<WorkItemRowProps & React.RefAttributes<HTMLAnchorElement>>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { AvatarStack } from '../components/sole/avatar.js';
|
|
5
|
+
import { LabelTags } from '../components/sole/label-tag.js';
|
|
6
|
+
import { cn } from '../lib/cn.js';
|
|
7
|
+
import { SoleLink } from '../lib/sole-provider.js';
|
|
8
|
+
import { DueChip } from './due-chip.js';
|
|
9
|
+
import { PriorityIcon } from './priority-icon.js';
|
|
10
|
+
import { StateIcon } from './state-icon.js';
|
|
11
|
+
export const WorkItemRow = React.forwardRef(function WorkItemRow({ item, today, selected = false, showAssignees = true, assigneeSlot = true, onOpen, className, ...props }, ref) {
|
|
12
|
+
const group = item.state?.group ?? 'unstarted';
|
|
13
|
+
const onClick = onOpen
|
|
14
|
+
? (e) => {
|
|
15
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0)
|
|
16
|
+
return;
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
onOpen(item);
|
|
19
|
+
}
|
|
20
|
+
: undefined;
|
|
21
|
+
return (_jsxs(SoleLink, { ...props, ref: ref, href: item.href, onClick: onClick, "data-work-item-row": item.key, "aria-current": selected || undefined, className: cn('group/row flex min-w-0 items-center gap-2.5 border-b border-hairline px-3 text-body transition-colors duration-[120ms] outline-none', 'h-9 hover:bg-row-hover focus-visible:shadow-[inset_0_0_0_2px_var(--brand)]', selected && 'bg-accent hover:bg-accent', className), children: [_jsx(PriorityIcon, { priority: item.priority }), _jsx("span", { className: "tnum w-[52px] shrink-0 truncate text-label text-subtle-foreground", children: item.key }), _jsx(StateIcon, { group: group, name: item.state?.name }), _jsx("span", { className: cn('min-w-0 flex-1 truncate font-medium text-foreground', group === 'completed' && 'text-muted-foreground', group === 'cancelled' && 'text-subtle-foreground line-through'), children: item.title }), _jsx(LabelTags, { labels: item.labels }), _jsx("span", { className: "flex min-w-[78px] shrink-0 justify-end", children: _jsx(DueChip, { due: item.dueDate, today: today, done: item.isDone }) }), assigneeSlot && _jsx("span", { className: "flex w-11 shrink-0 justify-end", children: showAssignees && _jsx(AvatarStack, { people: item.assignees }) })] }));
|
|
22
|
+
});
|
|
23
|
+
WorkItemRow.displayName = 'WorkItemRow';
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from './lib/cn.ts';
|
|
2
|
+
export * from './lib/person.ts';
|
|
3
|
+
export * from './lib/colour.ts';
|
|
4
|
+
export * from './lib/hue.ts';
|
|
5
|
+
export * from './lib/platform.ts';
|
|
6
|
+
export * from './lib/dates.ts';
|
|
7
|
+
export * from './lib/sole-provider.tsx';
|
|
8
|
+
export * from './components/ui/index.ts';
|
|
9
|
+
export * from './components/sole/index.ts';
|
|
10
|
+
export * from './components/shell/index.ts';
|
|
11
|
+
export { toast } from 'sonner';
|
|
12
|
+
export { tokens } from './tokens.generated.ts';
|
|
13
|
+
export type { Token, TokenType } from './tokens.generated.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './lib/cn.js';
|
|
2
|
+
export * from './lib/person.js';
|
|
3
|
+
export * from './lib/colour.js';
|
|
4
|
+
export * from './lib/hue.js';
|
|
5
|
+
export * from './lib/platform.js';
|
|
6
|
+
export * from './lib/dates.js';
|
|
7
|
+
export * from './lib/sole-provider.js';
|
|
8
|
+
export * from './components/ui/index.js';
|
|
9
|
+
export * from './components/sole/index.js';
|
|
10
|
+
export * from './components/shell/index.js';
|
|
11
|
+
export { toast } from 'sonner';
|
|
12
|
+
export { tokens } from './tokens.generated.js';
|
package/dist/lib/cn.d.ts
ADDED
package/dist/lib/cn.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { clsx } from 'clsx';
|
|
2
|
+
import { extendTailwindMerge } from 'tailwind-merge';
|
|
3
|
+
// The type scale (brand-and-ux 5.4) adds text-micro, text-label and friends. Without
|
|
4
|
+
// telling tailwind-merge they are font sizes it files them as colours, and
|
|
5
|
+
// cn('text-label', 'text-muted-foreground') silently drops the size.
|
|
6
|
+
const twMerge = extendTailwindMerge({
|
|
7
|
+
extend: {
|
|
8
|
+
classGroups: {
|
|
9
|
+
'font-size': [{ text: ['micro', 'label', 'body', 'strong', 'read', 'title', 'page', 'doc', 'metric'] }],
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
export function cn(...inputs) {
|
|
14
|
+
return twMerge(clsx(inputs));
|
|
15
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** FNV-1a, 32 bit. Stable across runtimes, used to pick colours from ids. */
|
|
2
|
+
export declare function fnv1a(value: string): number;
|
|
3
|
+
/** Dialer's 8 avatar colours (brand-and-ux 5.12). Hashed on the person id, so a rename keeps the colour. */
|
|
4
|
+
export declare const AVATAR_COLOURS: readonly ['#0485F7', '#0EA5A4', '#D97706', '#DB2777', '#2563EB', '#7C3AED', '#059669', '#EA580C'];
|
|
5
|
+
export declare function avatarColour(id: string): string;
|
|
6
|
+
/** Board colours: 8 muted two-stop gradients (brand-and-ux 5.13), for the header strip and chip dot. */
|
|
7
|
+
export declare const BOARD_GRADIENTS: readonly [readonly ['#3B5B8C', '#263B5C'], readonly ['#2F6E68', '#1E4844'], readonly ['#6B4C7A', '#463151'], readonly ['#8A5A2B', '#5A3A1C'], readonly ['#4E6B3A', '#334527'], readonly ['#4A4E8C', '#31345C'], readonly ['#8A4257', '#5A2B39'], readonly ['#46607A', '#2E3F50']];
|
|
8
|
+
export declare function boardGradient(id: string): readonly [string, string];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** FNV-1a, 32 bit. Stable across runtimes, used to pick colours from ids. */
|
|
2
|
+
export function fnv1a(value) {
|
|
3
|
+
let h = 0x811c9dc5;
|
|
4
|
+
for (let i = 0; i < value.length; i++) {
|
|
5
|
+
h ^= value.charCodeAt(i);
|
|
6
|
+
h = Math.imul(h, 0x01000193);
|
|
7
|
+
}
|
|
8
|
+
return h >>> 0;
|
|
9
|
+
}
|
|
10
|
+
/** Dialer's 8 avatar colours (brand-and-ux 5.12). Hashed on the person id, so a rename keeps the colour. */
|
|
11
|
+
export const AVATAR_COLOURS = ['#0485F7', '#0EA5A4', '#D97706', '#DB2777', '#2563EB', '#7C3AED', '#059669', '#EA580C'];
|
|
12
|
+
export function avatarColour(id) {
|
|
13
|
+
return AVATAR_COLOURS[fnv1a(id) % AVATAR_COLOURS.length];
|
|
14
|
+
}
|
|
15
|
+
/** Board colours: 8 muted two-stop gradients (brand-and-ux 5.13), for the header strip and chip dot. */
|
|
16
|
+
export const BOARD_GRADIENTS = [
|
|
17
|
+
['#3B5B8C', '#263B5C'],
|
|
18
|
+
['#2F6E68', '#1E4844'],
|
|
19
|
+
['#6B4C7A', '#463151'],
|
|
20
|
+
['#8A5A2B', '#5A3A1C'],
|
|
21
|
+
['#4E6B3A', '#334527'],
|
|
22
|
+
['#4A4E8C', '#31345C'],
|
|
23
|
+
['#8A4257', '#5A2B39'],
|
|
24
|
+
['#46607A', '#2E3F50'],
|
|
25
|
+
];
|
|
26
|
+
export function boardGradient(id) {
|
|
27
|
+
return BOARD_GRADIENTS[fnv1a(id) % BOARD_GRADIENTS.length];
|
|
28
|
+
}
|