@proteos/ui 0.31.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/LICENSE +105 -0
- package/dist/chunk-IKAX76MB.js +113 -0
- package/dist/chunk-IKAX76MB.js.map +1 -0
- package/dist/chunk-MWAXK66H.cjs +119 -0
- package/dist/chunk-MWAXK66H.cjs.map +1 -0
- package/dist/index.cjs +6246 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2126 -0
- package/dist/index.d.ts +2126 -0
- package/dist/index.js +6069 -0
- package/dist/index.js.map +1 -0
- package/dist/pdf-view-3ZMMS6ML.cjs +95 -0
- package/dist/pdf-view-3ZMMS6ML.cjs.map +1 -0
- package/dist/pdf-view-BV5ZD6OC.js +93 -0
- package/dist/pdf-view-BV5ZD6OC.js.map +1 -0
- package/package.json +96 -0
- package/src/components/attachment-chip/attachment-chip.tsx +129 -0
- package/src/components/attachment-chip/index.ts +2 -0
- package/src/components/attribute-type-icon/attribute-type-icon.stories.tsx +70 -0
- package/src/components/attribute-type-icon/attribute-type-icon.tsx +125 -0
- package/src/components/attribute-type-icon/index.ts +8 -0
- package/src/components/avatar/avatar.stories.tsx +42 -0
- package/src/components/avatar/avatar.tsx +48 -0
- package/src/components/avatar/index.ts +2 -0
- package/src/components/avatar-group/avatar-group.test.tsx +38 -0
- package/src/components/avatar-group/avatar-group.tsx +60 -0
- package/src/components/avatar-group/index.ts +2 -0
- package/src/components/badge/badge.stories.tsx +62 -0
- package/src/components/badge/badge.test.tsx +36 -0
- package/src/components/badge/badge.tsx +54 -0
- package/src/components/badge/index.ts +2 -0
- package/src/components/badge-multi-select/badge-multi-select.stories.tsx +177 -0
- package/src/components/badge-multi-select/badge-multi-select.test.tsx +155 -0
- package/src/components/badge-multi-select/badge-multi-select.tsx +272 -0
- package/src/components/badge-multi-select/index.ts +2 -0
- package/src/components/breadcrumbs/breadcrumbs.stories.tsx +43 -0
- package/src/components/breadcrumbs/breadcrumbs.tsx +86 -0
- package/src/components/breadcrumbs/index.ts +2 -0
- package/src/components/button/button.stories.tsx +290 -0
- package/src/components/button/button.test.tsx +59 -0
- package/src/components/button/button.tsx +86 -0
- package/src/components/button/index.ts +2 -0
- package/src/components/card/card.stories.tsx +351 -0
- package/src/components/card/card.test.tsx +141 -0
- package/src/components/card/card.tsx +173 -0
- package/src/components/card/index.ts +13 -0
- package/src/components/checkbox/checkbox-group.tsx +52 -0
- package/src/components/checkbox/checkbox.stories.tsx +48 -0
- package/src/components/checkbox/checkbox.test.tsx +50 -0
- package/src/components/checkbox/checkbox.tsx +71 -0
- package/src/components/checkbox/index.ts +4 -0
- package/src/components/combobox/combobox.stories.tsx +97 -0
- package/src/components/combobox/combobox.test.tsx +123 -0
- package/src/components/combobox/combobox.tsx +239 -0
- package/src/components/combobox/index.ts +2 -0
- package/src/components/command/command-hint.tsx +46 -0
- package/src/components/command/command.stories.tsx +88 -0
- package/src/components/command/command.test.tsx +102 -0
- package/src/components/command/command.tsx +76 -0
- package/src/components/command/index.ts +16 -0
- package/src/components/currency-input/currency-input.tsx +192 -0
- package/src/components/currency-input/index.ts +6 -0
- package/src/components/data-table/cells.stories.tsx +120 -0
- package/src/components/data-table/cells.tsx +165 -0
- package/src/components/data-table/data-table.stories.tsx +371 -0
- package/src/components/data-table/data-table.tsx +556 -0
- package/src/components/data-table/index.ts +22 -0
- package/src/components/data-table/make-actions-column.ts +30 -0
- package/src/components/data-table/states/data-table-empty.tsx +40 -0
- package/src/components/data-table/states/data-table-error.tsx +30 -0
- package/src/components/data-table/states/data-table-skeleton.tsx +42 -0
- package/src/components/data-table/use-infinite-scroll-sentinel.ts +51 -0
- package/src/components/date-picker/calendar-internals.tsx +138 -0
- package/src/components/date-picker/date-picker.stories.tsx +78 -0
- package/src/components/date-picker/date-picker.test.tsx +71 -0
- package/src/components/date-picker/date-picker.tsx +140 -0
- package/src/components/date-picker/index.ts +2 -0
- package/src/components/date-time-picker/date-time-picker.stories.tsx +96 -0
- package/src/components/date-time-picker/date-time-picker.test.tsx +87 -0
- package/src/components/date-time-picker/date-time-picker.tsx +203 -0
- package/src/components/date-time-picker/index.ts +2 -0
- package/src/components/empty-state/empty-state.tsx +63 -0
- package/src/components/empty-state/index.ts +2 -0
- package/src/components/error-animation/error-animation.stories.tsx +50 -0
- package/src/components/error-animation/error-animation.test.tsx +22 -0
- package/src/components/error-animation/error-animation.tsx +26 -0
- package/src/components/error-animation/index.ts +2 -0
- package/src/components/field/field.tsx +142 -0
- package/src/components/field/index.ts +8 -0
- package/src/components/file-field/file-field.tsx +285 -0
- package/src/components/file-field/index.ts +2 -0
- package/src/components/file-viewer/fallback-card.tsx +68 -0
- package/src/components/file-viewer/file-viewer.stories.tsx +67 -0
- package/src/components/file-viewer/file-viewer.test.tsx +81 -0
- package/src/components/file-viewer/file-viewer.tsx +116 -0
- package/src/components/file-viewer/image-view.tsx +38 -0
- package/src/components/file-viewer/index.ts +7 -0
- package/src/components/file-viewer/pdf-view.tsx +122 -0
- package/src/components/file-viewer/video-view.tsx +16 -0
- package/src/components/image-attachment/image-attachment.stories.tsx +39 -0
- package/src/components/image-attachment/image-attachment.test.tsx +54 -0
- package/src/components/image-attachment/image-attachment.tsx +77 -0
- package/src/components/image-attachment/index.ts +3 -0
- package/src/components/image-attachment/is-previewable-image-name.ts +14 -0
- package/src/components/image-lightbox/image-lightbox.stories.tsx +38 -0
- package/src/components/image-lightbox/image-lightbox.test.tsx +72 -0
- package/src/components/image-lightbox/image-lightbox.tsx +92 -0
- package/src/components/image-lightbox/index.ts +2 -0
- package/src/components/input/index.ts +2 -0
- package/src/components/input/input.stories.tsx +71 -0
- package/src/components/input/input.test.tsx +36 -0
- package/src/components/input/input.tsx +76 -0
- package/src/components/kbd/index.ts +2 -0
- package/src/components/kbd/kbd.stories.tsx +34 -0
- package/src/components/kbd/kbd.tsx +27 -0
- package/src/components/menu/index.ts +2 -0
- package/src/components/menu/menu.tsx +121 -0
- package/src/components/modal/index.ts +10 -0
- package/src/components/modal/modal.stories.tsx +153 -0
- package/src/components/modal/modal.test.tsx +93 -0
- package/src/components/modal/modal.tsx +149 -0
- package/src/components/multi-select/index.ts +2 -0
- package/src/components/multi-select/multi-select.stories.tsx +211 -0
- package/src/components/multi-select/multi-select.test.tsx +151 -0
- package/src/components/multi-select/multi-select.tsx +339 -0
- package/src/components/nav/index.ts +7 -0
- package/src/components/nav/nav.stories.tsx +139 -0
- package/src/components/nav/nav.tsx +246 -0
- package/src/components/popover/index.ts +8 -0
- package/src/components/popover/popover.stories.tsx +92 -0
- package/src/components/popover/popover.tsx +135 -0
- package/src/components/progress-bar/index.ts +2 -0
- package/src/components/progress-bar/progress-bar.stories.tsx +75 -0
- package/src/components/progress-bar/progress-bar.test.tsx +29 -0
- package/src/components/progress-bar/progress-bar.tsx +57 -0
- package/src/components/radio-group/index.ts +2 -0
- package/src/components/radio-group/radio-group.stories.tsx +113 -0
- package/src/components/radio-group/radio-group.test.tsx +87 -0
- package/src/components/radio-group/radio-group.tsx +104 -0
- package/src/components/record-filter/attribute-picker.tsx +74 -0
- package/src/components/record-filter/chip-value.tsx +171 -0
- package/src/components/record-filter/filter-row.tsx +262 -0
- package/src/components/record-filter/filter-value-input.tsx +351 -0
- package/src/components/record-filter/group-add-controls.tsx +77 -0
- package/src/components/record-filter/group-body.tsx +173 -0
- package/src/components/record-filter/group-mutations.test.ts +104 -0
- package/src/components/record-filter/group-mutations.ts +116 -0
- package/src/components/record-filter/index.ts +58 -0
- package/src/components/record-filter/nested-group.tsx +138 -0
- package/src/components/record-filter/operator-picker.tsx +62 -0
- package/src/components/record-filter/operators.test.ts +105 -0
- package/src/components/record-filter/operators.ts +97 -0
- package/src/components/record-filter/record-filter-builder-panel.tsx +236 -0
- package/src/components/record-filter/record-filter-button.tsx +126 -0
- package/src/components/record-filter/record-filter-chips.tsx +527 -0
- package/src/components/record-filter/record-filter-editor.tsx +91 -0
- package/src/components/record-filter/record-filter.stories.tsx +196 -0
- package/src/components/record-filter/record-filter.test.tsx +437 -0
- package/src/components/record-filter/show-details-store.ts +72 -0
- package/src/components/record-filter/types.ts +150 -0
- package/src/components/segmented-control/index.ts +6 -0
- package/src/components/segmented-control/segmented-control.stories.tsx +67 -0
- package/src/components/segmented-control/segmented-control.test.tsx +47 -0
- package/src/components/segmented-control/segmented-control.tsx +105 -0
- package/src/components/select/index.ts +2 -0
- package/src/components/select/select.stories.tsx +76 -0
- package/src/components/select/select.test.tsx +56 -0
- package/src/components/select/select.tsx +159 -0
- package/src/components/separator/index.ts +2 -0
- package/src/components/separator/separator.stories.tsx +73 -0
- package/src/components/separator/separator.test.tsx +29 -0
- package/src/components/separator/separator.tsx +51 -0
- package/src/components/skeleton/index.ts +2 -0
- package/src/components/skeleton/skeleton.stories.tsx +68 -0
- package/src/components/skeleton/skeleton.test.tsx +38 -0
- package/src/components/skeleton/skeleton.tsx +43 -0
- package/src/components/success-animation/index.ts +2 -0
- package/src/components/success-animation/success-animation.stories.tsx +50 -0
- package/src/components/success-animation/success-animation.test.tsx +22 -0
- package/src/components/success-animation/success-animation.tsx +26 -0
- package/src/components/switch/index.ts +2 -0
- package/src/components/switch/switch.stories.tsx +60 -0
- package/src/components/switch/switch.test.tsx +50 -0
- package/src/components/switch/switch.tsx +49 -0
- package/src/components/table/index.ts +25 -0
- package/src/components/table/table.stories.tsx +306 -0
- package/src/components/table/table.test.tsx +115 -0
- package/src/components/table/table.tsx +543 -0
- package/src/components/tabs/index.ts +8 -0
- package/src/components/tabs/tabs.tsx +141 -0
- package/src/components/tag-input/index.ts +2 -0
- package/src/components/tag-input/tag-input.stories.tsx +73 -0
- package/src/components/tag-input/tag-input.test.tsx +80 -0
- package/src/components/tag-input/tag-input.tsx +193 -0
- package/src/components/textarea/index.ts +2 -0
- package/src/components/textarea/textarea.stories.tsx +75 -0
- package/src/components/textarea/textarea.test.tsx +41 -0
- package/src/components/textarea/textarea.tsx +86 -0
- package/src/components/time-field/index.ts +2 -0
- package/src/components/time-field/time-field.stories.tsx +76 -0
- package/src/components/time-field/time-field.test.tsx +51 -0
- package/src/components/time-field/time-field.tsx +98 -0
- package/src/components/tooltip/index.ts +2 -0
- package/src/components/tooltip/tooltip.stories.tsx +176 -0
- package/src/components/tooltip/tooltip.test.tsx +52 -0
- package/src/components/tooltip/tooltip.tsx +56 -0
- package/src/index.ts +47 -0
- package/src/lib/cn.ts +6 -0
- package/src/lib/commands/discovery-store.test.tsx +76 -0
- package/src/lib/commands/discovery-store.ts +110 -0
- package/src/lib/commands/index.ts +9 -0
- package/src/lib/commands/match-event.test.ts +99 -0
- package/src/lib/commands/match-event.ts +31 -0
- package/src/lib/commands/parse-combo.test.ts +65 -0
- package/src/lib/commands/parse-combo.ts +48 -0
- package/src/lib/commands/platform.ts +27 -0
- package/src/lib/commands/protected-combos.test.ts +18 -0
- package/src/lib/commands/protected-combos.ts +38 -0
- package/src/lib/commands/registry.test.ts +97 -0
- package/src/lib/commands/registry.ts +59 -0
- package/src/lib/commands/use-command.test.tsx +91 -0
- package/src/lib/commands/use-command.ts +32 -0
- package/src/lib/commands/use-escape-key.test.tsx +97 -0
- package/src/lib/commands/use-escape-key.ts +62 -0
- package/src/styles.css +321 -0
- package/src/test/setup.ts +9 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
|
|
3
|
+
import { Avatar } from './avatar'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Avatar> = {
|
|
6
|
+
title: 'Components/Avatar',
|
|
7
|
+
component: Avatar,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] },
|
|
11
|
+
tone: { control: 'inline-radio', options: ['paper', 'indigo', 'moss', 'terra', 'slate'] },
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default meta
|
|
16
|
+
type Story = StoryObj<typeof Avatar>
|
|
17
|
+
|
|
18
|
+
export const Playground: Story = { args: { initials: 'JM' } }
|
|
19
|
+
|
|
20
|
+
export const Tones: Story = {
|
|
21
|
+
parameters: { controls: { hideNoControlsWarning: true } },
|
|
22
|
+
render: () => (
|
|
23
|
+
<div className="flex items-center gap-3">
|
|
24
|
+
<Avatar initials="JM" />
|
|
25
|
+
<Avatar tone="indigo" initials="N" />
|
|
26
|
+
<Avatar tone="moss" initials="M" />
|
|
27
|
+
<Avatar tone="terra" initials="A" />
|
|
28
|
+
<Avatar tone="slate" initials="S" />
|
|
29
|
+
</div>
|
|
30
|
+
),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const Sizes: Story = {
|
|
34
|
+
parameters: { controls: { hideNoControlsWarning: true } },
|
|
35
|
+
render: () => (
|
|
36
|
+
<div className="flex items-end gap-3">
|
|
37
|
+
<Avatar size="sm" initials="SM" />
|
|
38
|
+
<Avatar size="md" initials="MD" />
|
|
39
|
+
<Avatar size="lg" initials="LG" />
|
|
40
|
+
</div>
|
|
41
|
+
),
|
|
42
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'react'
|
|
2
|
+
import { tv, type VariantProps } from 'tailwind-variants'
|
|
3
|
+
|
|
4
|
+
import { cn } from '../../lib/cn'
|
|
5
|
+
|
|
6
|
+
/* =========================================================================
|
|
7
|
+
Avatar — round, paper-tinted initials chip. Mono glyph, hairline border.
|
|
8
|
+
For square colored "app tiles" use the AppSwitcher's internal tile —
|
|
9
|
+
Avatar is reserved for people.
|
|
10
|
+
========================================================================= */
|
|
11
|
+
|
|
12
|
+
export const avatarStyles = tv({
|
|
13
|
+
base: [
|
|
14
|
+
'inline-grid place-items-center shrink-0 select-none',
|
|
15
|
+
'rounded-full border',
|
|
16
|
+
'font-mono font-medium tracking-[-0.005em]',
|
|
17
|
+
],
|
|
18
|
+
variants: {
|
|
19
|
+
size: {
|
|
20
|
+
sm: 'size-[22px] text-[10px]',
|
|
21
|
+
md: 'size-7 text-[12px]',
|
|
22
|
+
lg: 'size-9 text-[13px]',
|
|
23
|
+
},
|
|
24
|
+
tone: {
|
|
25
|
+
paper: 'bg-surface-3 text-ink border-rule',
|
|
26
|
+
indigo: 'bg-accent text-[#f0efe8] border-transparent',
|
|
27
|
+
moss: 'bg-[#5b7052] text-[#f0efe8] border-transparent',
|
|
28
|
+
terra: 'bg-[#8a5a3f] text-[#f0efe8] border-transparent',
|
|
29
|
+
slate: 'bg-ink text-surface border-transparent',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: { size: 'md', tone: 'paper' },
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
export type AvatarVariants = VariantProps<typeof avatarStyles>
|
|
36
|
+
export type AvatarProps = HTMLAttributes<HTMLSpanElement> &
|
|
37
|
+
AvatarVariants & {
|
|
38
|
+
/** 1–2 character initials. Ignored if `children` is provided. */
|
|
39
|
+
initials?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function Avatar({ size, tone, initials, className, children, ...props }: AvatarProps) {
|
|
43
|
+
return (
|
|
44
|
+
<span {...props} className={cn(avatarStyles({ size, tone }), className)}>
|
|
45
|
+
{children ?? initials}
|
|
46
|
+
</span>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { AvatarGroup } from './avatar-group'
|
|
5
|
+
|
|
6
|
+
describe('AvatarGroup', () => {
|
|
7
|
+
it('renders one avatar per person up to max', () => {
|
|
8
|
+
render(
|
|
9
|
+
<AvatarGroup
|
|
10
|
+
avatars={[
|
|
11
|
+
{ initials: 'AB', key: 'a' },
|
|
12
|
+
{ initials: 'CD', key: 'b' },
|
|
13
|
+
]}
|
|
14
|
+
/>,
|
|
15
|
+
)
|
|
16
|
+
expect(screen.getByText('AB')).toBeInTheDocument()
|
|
17
|
+
expect(screen.getByText('CD')).toBeInTheDocument()
|
|
18
|
+
expect(screen.queryByText(/^\+/)).not.toBeInTheDocument()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('collapses the overflow into a +N chip', () => {
|
|
22
|
+
render(
|
|
23
|
+
<AvatarGroup
|
|
24
|
+
max={2}
|
|
25
|
+
avatars={[
|
|
26
|
+
{ initials: 'AB', key: 'a' },
|
|
27
|
+
{ initials: 'CD', key: 'b' },
|
|
28
|
+
{ initials: 'EF', key: 'c' },
|
|
29
|
+
{ initials: 'GH', key: 'd' },
|
|
30
|
+
]}
|
|
31
|
+
/>,
|
|
32
|
+
)
|
|
33
|
+
expect(screen.getByText('AB')).toBeInTheDocument()
|
|
34
|
+
expect(screen.getByText('CD')).toBeInTheDocument()
|
|
35
|
+
expect(screen.queryByText('EF')).not.toBeInTheDocument()
|
|
36
|
+
expect(screen.getByText('+2')).toBeInTheDocument()
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'react'
|
|
2
|
+
import { tv, type VariantProps } from 'tailwind-variants'
|
|
3
|
+
|
|
4
|
+
import { cn } from '../../lib/cn'
|
|
5
|
+
import { Avatar, type AvatarVariants } from '../avatar/avatar'
|
|
6
|
+
|
|
7
|
+
/* =========================================================================
|
|
8
|
+
AvatarGroup — a stack of overlapping people avatars with a +N overflow
|
|
9
|
+
chip. Used to render a conversation's roster: one avatar for a 1:1 thread,
|
|
10
|
+
several overlapping for a group. Each avatar wears a bg-colored ring so the
|
|
11
|
+
overlap reads as separate chips.
|
|
12
|
+
========================================================================= */
|
|
13
|
+
|
|
14
|
+
export const avatarGroupStyles = tv({
|
|
15
|
+
base: 'flex items-center',
|
|
16
|
+
variants: {
|
|
17
|
+
size: {
|
|
18
|
+
sm: '-space-x-1.5',
|
|
19
|
+
md: '-space-x-2',
|
|
20
|
+
lg: '-space-x-2.5',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
defaultVariants: { size: 'md' },
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export type AvatarGroupVariants = VariantProps<typeof avatarGroupStyles>
|
|
27
|
+
|
|
28
|
+
/** One person in the stack — initials plus an optional tone. */
|
|
29
|
+
export interface AvatarGroupItem {
|
|
30
|
+
initials: string
|
|
31
|
+
tone?: AvatarVariants['tone']
|
|
32
|
+
/** Stable key (e.g. the participant's external id); falls back to the index. */
|
|
33
|
+
key?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type AvatarGroupProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children'> &
|
|
37
|
+
AvatarGroupVariants & {
|
|
38
|
+
avatars: AvatarGroupItem[]
|
|
39
|
+
/** Max avatars shown before the rest collapse into a +N chip. Default 3. */
|
|
40
|
+
max?: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function AvatarGroup({ avatars, max = 3, size = 'md', className, ...props }: AvatarGroupProps) {
|
|
44
|
+
const shown = avatars.slice(0, max)
|
|
45
|
+
const overflow = avatars.length - shown.length
|
|
46
|
+
return (
|
|
47
|
+
<span {...props} className={cn(avatarGroupStyles({ size }), className)}>
|
|
48
|
+
{shown.map((avatar, index) => (
|
|
49
|
+
<span key={avatar.key ?? index} className="rounded-full ring-[1.5px] ring-surface">
|
|
50
|
+
<Avatar size={size} tone={avatar.tone ?? 'paper'} initials={avatar.initials} />
|
|
51
|
+
</span>
|
|
52
|
+
))}
|
|
53
|
+
{overflow > 0 && (
|
|
54
|
+
<span className="rounded-full ring-[1.5px] ring-surface">
|
|
55
|
+
<Avatar size={size} tone="paper" initials={`+${overflow}`} />
|
|
56
|
+
</span>
|
|
57
|
+
)}
|
|
58
|
+
</span>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
|
|
3
|
+
import { Badge } from './badge'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Badge> = {
|
|
6
|
+
title: 'Components/Badge',
|
|
7
|
+
component: Badge,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
args: {
|
|
10
|
+
children: 'in transit',
|
|
11
|
+
variant: 'active',
|
|
12
|
+
},
|
|
13
|
+
argTypes: {
|
|
14
|
+
variant: {
|
|
15
|
+
control: { type: 'inline-radio' },
|
|
16
|
+
options: ['default', 'neutral', 'active', 'success', 'warning', 'danger'],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default meta
|
|
22
|
+
type Story = StoryObj<typeof Badge>
|
|
23
|
+
|
|
24
|
+
export const Playground: Story = {}
|
|
25
|
+
|
|
26
|
+
export const AllVariants: Story = {
|
|
27
|
+
parameters: { controls: { hideNoControlsWarning: true } },
|
|
28
|
+
render: () => (
|
|
29
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
30
|
+
<Badge variant="active">in transit</Badge>
|
|
31
|
+
<Badge>quoted</Badge>
|
|
32
|
+
<Badge variant="neutral">dispatching</Badge>
|
|
33
|
+
<Badge variant="success">reconciled</Badge>
|
|
34
|
+
<Badge variant="warning">past due</Badge>
|
|
35
|
+
<Badge variant="danger">exception</Badge>
|
|
36
|
+
</div>
|
|
37
|
+
),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const InContext: Story = {
|
|
41
|
+
parameters: { controls: { hideNoControlsWarning: true } },
|
|
42
|
+
render: () => (
|
|
43
|
+
<div className="w-[480px] grid gap-3 font-mono text-[12.5px] text-ink-2">
|
|
44
|
+
<div className="flex items-center justify-between border-b border-dashed border-rule pb-3">
|
|
45
|
+
<span>SH-10482 · DAL → ATL</span>
|
|
46
|
+
<Badge variant="active">in transit</Badge>
|
|
47
|
+
</div>
|
|
48
|
+
<div className="flex items-center justify-between border-b border-dashed border-rule pb-3">
|
|
49
|
+
<span>SH-10483 · CHI → MSP</span>
|
|
50
|
+
<Badge variant="neutral">dispatching</Badge>
|
|
51
|
+
</div>
|
|
52
|
+
<div className="flex items-center justify-between border-b border-dashed border-rule pb-3">
|
|
53
|
+
<span>SH-10486 · MIA → JFK</span>
|
|
54
|
+
<Badge variant="warning">past due</Badge>
|
|
55
|
+
</div>
|
|
56
|
+
<div className="flex items-center justify-between">
|
|
57
|
+
<span>SH-10487 · SEA → BOI</span>
|
|
58
|
+
<Badge variant="danger">exception</Badge>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
),
|
|
62
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { Badge } from './badge'
|
|
5
|
+
|
|
6
|
+
describe('Badge', () => {
|
|
7
|
+
it('renders its content', () => {
|
|
8
|
+
render(<Badge>in transit</Badge>)
|
|
9
|
+
expect(screen.getByText('in transit')).toBeInTheDocument()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('applies the default variant classes', () => {
|
|
13
|
+
render(<Badge>quoted</Badge>)
|
|
14
|
+
expect(screen.getByText('quoted').className).toContain('text-ink-3')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('applies the active variant classes', () => {
|
|
18
|
+
render(<Badge variant="active">in transit</Badge>)
|
|
19
|
+
expect(screen.getByText('in transit').className).toContain('text-accent-ink')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('applies the danger variant classes', () => {
|
|
23
|
+
render(<Badge variant="danger">exception</Badge>)
|
|
24
|
+
expect(screen.getByText('exception').className).toContain('text-danger')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('merges custom classes', () => {
|
|
28
|
+
render(
|
|
29
|
+
<Badge variant="success" className="ml-3">
|
|
30
|
+
reconciled
|
|
31
|
+
</Badge>,
|
|
32
|
+
)
|
|
33
|
+
expect(screen.getByText('reconciled').className).toContain('ml-3')
|
|
34
|
+
expect(screen.getByText('reconciled').className).toContain('text-success')
|
|
35
|
+
})
|
|
36
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'react'
|
|
2
|
+
import { tv, type VariantProps } from 'tailwind-variants'
|
|
3
|
+
|
|
4
|
+
import { cn } from '../../lib/cn'
|
|
5
|
+
|
|
6
|
+
/* =========================================================================
|
|
7
|
+
Badge — mono, pill-shaped status chip with a leading dot.
|
|
8
|
+
Matches the Proteos spec's six registers: default, neutral, active (indigo),
|
|
9
|
+
success, warning, danger. Paper-compatible fills — never pure-color.
|
|
10
|
+
========================================================================= */
|
|
11
|
+
|
|
12
|
+
export const badgeStyles = tv({
|
|
13
|
+
base: [
|
|
14
|
+
'inline-flex items-center gap-1.5',
|
|
15
|
+
'font-mono text-[11.5px] tracking-[-0.005em]',
|
|
16
|
+
'px-2.5 py-0.5 rounded-pill border',
|
|
17
|
+
// Leading dot
|
|
18
|
+
'before:content-[""] before:block before:size-1.5 before:rounded-full',
|
|
19
|
+
],
|
|
20
|
+
variants: {
|
|
21
|
+
variant: {
|
|
22
|
+
default: 'border-rule bg-surface text-ink-3 before:bg-ink-4',
|
|
23
|
+
neutral: 'border-rule bg-surface-3 text-ink-2 before:bg-ink-3',
|
|
24
|
+
active: [
|
|
25
|
+
'text-accent-ink before:bg-accent',
|
|
26
|
+
'border-[color-mix(in_oklab,var(--color-accent)_35%,var(--color-rule))]',
|
|
27
|
+
'bg-[color-mix(in_oklab,var(--color-accent)_14%,var(--color-surface))]',
|
|
28
|
+
],
|
|
29
|
+
success: [
|
|
30
|
+
'text-success before:bg-success',
|
|
31
|
+
'border-[color-mix(in_oklab,var(--color-success)_35%,var(--color-rule))]',
|
|
32
|
+
'bg-[color-mix(in_oklab,var(--color-success)_12%,var(--color-surface))]',
|
|
33
|
+
],
|
|
34
|
+
warning: [
|
|
35
|
+
'text-warning before:bg-warning',
|
|
36
|
+
'border-[color-mix(in_oklab,var(--color-warning)_35%,var(--color-rule))]',
|
|
37
|
+
'bg-[color-mix(in_oklab,var(--color-warning)_14%,var(--color-surface))]',
|
|
38
|
+
],
|
|
39
|
+
danger: [
|
|
40
|
+
'text-danger before:bg-danger',
|
|
41
|
+
'border-[color-mix(in_oklab,var(--color-danger)_40%,var(--color-rule))]',
|
|
42
|
+
'bg-[color-mix(in_oklab,var(--color-danger)_12%,var(--color-surface))]',
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
defaultVariants: { variant: 'default' },
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
export type BadgeVariants = VariantProps<typeof badgeStyles>
|
|
50
|
+
export type BadgeProps = HTMLAttributes<HTMLSpanElement> & BadgeVariants
|
|
51
|
+
|
|
52
|
+
export function Badge({ variant, className, ...props }: BadgeProps) {
|
|
53
|
+
return <span {...props} className={cn(badgeStyles({ variant }), className)} />
|
|
54
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import { BadgeMultiSelect, type BadgeMultiSelectOption } from './badge-multi-select'
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof BadgeMultiSelect> = {
|
|
7
|
+
title: 'Components/BadgeMultiSelect',
|
|
8
|
+
component: BadgeMultiSelect,
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
decorators: [
|
|
11
|
+
(Story) => (
|
|
12
|
+
<div className="w-[440px]">
|
|
13
|
+
<Story />
|
|
14
|
+
</div>
|
|
15
|
+
),
|
|
16
|
+
],
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default meta
|
|
20
|
+
type Story = StoryObj<typeof BadgeMultiSelect>
|
|
21
|
+
|
|
22
|
+
const statuses: BadgeMultiSelectOption<string>[] = [
|
|
23
|
+
{ id: 'in-transit', label: 'in transit', variant: 'active' },
|
|
24
|
+
{ id: 'quoted', label: 'quoted' },
|
|
25
|
+
{ id: 'dispatching', label: 'dispatching', variant: 'neutral' },
|
|
26
|
+
{ id: 'reconciled', label: 'reconciled', variant: 'success' },
|
|
27
|
+
{ id: 'past-due', label: 'past due', variant: 'warning' },
|
|
28
|
+
{ id: 'exception', label: 'exception', variant: 'danger' },
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
export const Playground: Story = {
|
|
32
|
+
render: () => {
|
|
33
|
+
const [selected, setSelected] = useState<Set<string>>(new Set(['in-transit', 'past-due']))
|
|
34
|
+
return (
|
|
35
|
+
<BadgeMultiSelect
|
|
36
|
+
label="Statuses to include"
|
|
37
|
+
placeholder="Any status"
|
|
38
|
+
description="Filter the shipment board by one or more statuses."
|
|
39
|
+
options={statuses}
|
|
40
|
+
selectedKeys={selected}
|
|
41
|
+
onSelectionChange={setSelected}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const Empty: Story = {
|
|
48
|
+
render: () => {
|
|
49
|
+
const [selected, setSelected] = useState<Set<string>>(new Set())
|
|
50
|
+
return (
|
|
51
|
+
<BadgeMultiSelect
|
|
52
|
+
label="Statuses"
|
|
53
|
+
placeholder="Any status"
|
|
54
|
+
options={statuses}
|
|
55
|
+
selectedKeys={selected}
|
|
56
|
+
onSelectionChange={setSelected}
|
|
57
|
+
/>
|
|
58
|
+
)
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** `hideSelectedOptions` — already-selected items disappear from the
|
|
63
|
+
* dropdown. Removal happens only via the chip × in the trigger. Useful
|
|
64
|
+
* when the list is long and you want to see what's left to pick. */
|
|
65
|
+
export const HideSelected: Story = {
|
|
66
|
+
render: () => {
|
|
67
|
+
const [selected, setSelected] = useState<Set<string>>(new Set(['in-transit']))
|
|
68
|
+
return (
|
|
69
|
+
<BadgeMultiSelect
|
|
70
|
+
label="Statuses"
|
|
71
|
+
placeholder="Any status"
|
|
72
|
+
description="Selected statuses are hidden from the menu — remove them via × above."
|
|
73
|
+
options={statuses}
|
|
74
|
+
selectedKeys={selected}
|
|
75
|
+
onSelectionChange={setSelected}
|
|
76
|
+
hideSelectedOptions
|
|
77
|
+
/>
|
|
78
|
+
)
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const ManySelected: Story = {
|
|
83
|
+
render: () => {
|
|
84
|
+
const [selected, setSelected] = useState<Set<string>>(
|
|
85
|
+
new Set(['in-transit', 'quoted', 'reconciled', 'past-due', 'exception']),
|
|
86
|
+
)
|
|
87
|
+
return (
|
|
88
|
+
<BadgeMultiSelect
|
|
89
|
+
label="Statuses"
|
|
90
|
+
placeholder="Any status"
|
|
91
|
+
options={statuses}
|
|
92
|
+
selectedKeys={selected}
|
|
93
|
+
onSelectionChange={setSelected}
|
|
94
|
+
/>
|
|
95
|
+
)
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const Invalid: Story = {
|
|
100
|
+
render: () => {
|
|
101
|
+
const [selected, setSelected] = useState<Set<string>>(new Set())
|
|
102
|
+
return (
|
|
103
|
+
<BadgeMultiSelect
|
|
104
|
+
label="Statuses"
|
|
105
|
+
placeholder="Any status"
|
|
106
|
+
options={statuses}
|
|
107
|
+
selectedKeys={selected}
|
|
108
|
+
onSelectionChange={setSelected}
|
|
109
|
+
isInvalid
|
|
110
|
+
errorMessage="Pick at least one status."
|
|
111
|
+
/>
|
|
112
|
+
)
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const Disabled: Story = {
|
|
117
|
+
render: () => (
|
|
118
|
+
<BadgeMultiSelect
|
|
119
|
+
label="Statuses"
|
|
120
|
+
options={statuses}
|
|
121
|
+
selectedKeys={new Set(['in-transit', 'past-due'])}
|
|
122
|
+
onSelectionChange={() => {}}
|
|
123
|
+
isDisabled
|
|
124
|
+
/>
|
|
125
|
+
),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const AllStates: Story = {
|
|
129
|
+
parameters: { controls: { hideNoControlsWarning: true } },
|
|
130
|
+
render: () => {
|
|
131
|
+
const [a, setA] = useState<Set<string>>(new Set())
|
|
132
|
+
const [b, setB] = useState<Set<string>>(new Set(['in-transit']))
|
|
133
|
+
const [c, setC] = useState<Set<string>>(new Set(['in-transit', 'past-due', 'exception']))
|
|
134
|
+
return (
|
|
135
|
+
<div className="grid gap-5 w-[440px]">
|
|
136
|
+
<BadgeMultiSelect
|
|
137
|
+
label="Empty"
|
|
138
|
+
placeholder="Any status"
|
|
139
|
+
options={statuses}
|
|
140
|
+
selectedKeys={a}
|
|
141
|
+
onSelectionChange={setA}
|
|
142
|
+
/>
|
|
143
|
+
<BadgeMultiSelect
|
|
144
|
+
label="One selection"
|
|
145
|
+
placeholder="Any status"
|
|
146
|
+
options={statuses}
|
|
147
|
+
selectedKeys={b}
|
|
148
|
+
onSelectionChange={setB}
|
|
149
|
+
/>
|
|
150
|
+
<BadgeMultiSelect
|
|
151
|
+
label="Multiple selections"
|
|
152
|
+
placeholder="Any status"
|
|
153
|
+
options={statuses}
|
|
154
|
+
selectedKeys={c}
|
|
155
|
+
onSelectionChange={setC}
|
|
156
|
+
/>
|
|
157
|
+
<BadgeMultiSelect
|
|
158
|
+
label="Invalid"
|
|
159
|
+
placeholder="Any status"
|
|
160
|
+
options={statuses}
|
|
161
|
+
selectedKeys={new Set<string>()}
|
|
162
|
+
onSelectionChange={() => {}}
|
|
163
|
+
isInvalid
|
|
164
|
+
errorMessage="Pick at least one status."
|
|
165
|
+
/>
|
|
166
|
+
<BadgeMultiSelect
|
|
167
|
+
label="Disabled"
|
|
168
|
+
placeholder="Any status"
|
|
169
|
+
options={statuses}
|
|
170
|
+
selectedKeys={new Set(['in-transit'])}
|
|
171
|
+
onSelectionChange={() => {}}
|
|
172
|
+
isDisabled
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
)
|
|
176
|
+
},
|
|
177
|
+
}
|