Skip to content

Commit 6f87fe1

Browse files
author
Chris Klimas
committed
Generalize tag components
1 parent 3ee2092 commit 6f87fe1

28 files changed

+575
-208
lines changed

.vscode/settings.json

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{
2+
}

src/components/control/icon-button-link.css

+11
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@
3030
margin-right: var(--control-inner-padding);
3131
}
3232

33+
.icon-button.icon-position-end,
34+
.icon-link.icon-position-end {
35+
flex-direction: row-reverse;
36+
}
37+
38+
.icon-button.icon-position-end:not(icon-only) .icon svg,
39+
.icon-link.icon-position-end:not(icon-only) .icon svg {
40+
margin-left: var(--control-inner-padding);
41+
margin-right: 0;
42+
}
43+
3344
.icon-link {
3445
text-decoration: none;
3546
}

src/components/story/story-card.css

+11
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@
99
padding: var(--grid-size);
1010
}
1111

12+
.story-card .card-body {
13+
padding-bottom: 0;
14+
}
15+
1216
.story-card .card-header {
17+
min-height: calc(0.85 * var(--control-height)); /* Height of <tag-button> */
1318
padding-top: 0;
1419
}
1520

@@ -19,6 +24,12 @@
1924
margin: var(--grid-size);
2025
}
2126

27+
.story-card .tags .tag-button {
28+
display: inline-block;
29+
margin-right: var(--grid-size);
30+
margin-bottom: var(--grid-size);
31+
}
32+
2233
.story-card .card-header,
2334
.story-import-card .card-header {
2435
margin-top: var(--grid-size);

src/components/story/story-card.tsx

+26-16
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,46 @@
11
import * as React from 'react';
22
import {useTranslation} from 'react-i18next';
3-
import {IconDots, IconEdit, IconPlus, IconPlayerPlay} from '@tabler/icons';
4-
import {
5-
Card,
6-
CardFooter,
7-
CardBody,
8-
CardHeader,
9-
CardProps
10-
} from '../container/card';
3+
import {IconDots, IconEdit, IconPlayerPlay} from '@tabler/icons';
4+
import {Card, CardBody, CardHeader, CardProps} from '../container/card';
115
import {ButtonBar} from '../container/button-bar';
126
import {MenuButton} from '../control/menu-button';
13-
import {ImageCard} from '../container/image-card';
147
import {IconButton} from '../control/icon-button';
8+
import {AddTagButton, TagButton} from '../tag';
159
import {StoryPreview} from './story-preview';
1610
import {Story} from '../../store/stories';
17-
import {hueString} from '../../util/hue-string';
11+
import {Color} from '../../util/color';
1812
import './story-card.css';
1913

2014
const dateFormatter = new Intl.DateTimeFormat([]);
2115

2216
export interface StoryCardProps extends CardProps {
2317
// onDelete: () => void;
2418
// onDuplicate: () => void;
19+
onAddTag: (name: string, color: Color) => void;
20+
onEditTag: (oldName: string, newName: string, newColor: Color) => void;
2521
onEdit: () => void;
2622
onPlay: () => void;
2723
onPublish: () => void;
2824
onRename: () => void;
2925
onTest: () => void;
3026
story: Story;
27+
storyTagColors: Record<string, Color>;
3128
}
3229

3330
// TODO: implement story delete
3431
// TODO: implement story duplicate
3532

3633
export const StoryCard: React.FC<StoryCardProps> = props => {
3734
const {
35+
onAddTag,
3836
onEdit,
37+
onEditTag,
3938
onPlay,
4039
onPublish,
4140
onRename,
4241
onTest,
4342
story,
43+
storyTagColors,
4444
...otherProps
4545
} = props;
4646
const {t} = useTranslation();
@@ -60,20 +60,30 @@ export const StoryCard: React.FC<StoryCardProps> = props => {
6060
count: story.passages.length
6161
})}
6262
</p>
63+
{story.tags && (
64+
<div className="tags">
65+
{story.tags.map(tag => (
66+
<TagButton
67+
color={storyTagColors[tag]}
68+
key={tag}
69+
name={tag}
70+
onDelete={() => {}}
71+
onEdit={(newName, newColor) =>
72+
onEditTag(tag, newName, newColor)
73+
}
74+
/>
75+
))}
76+
</div>
77+
)}
6378
</CardBody>
64-
<ButtonBar></ButtonBar>
6579
<ButtonBar>
6680
<IconButton
6781
icon={<IconEdit />}
6882
label={t('common.edit')}
6983
onClick={onEdit}
7084
variant="primary"
7185
/>
72-
<IconButton
73-
icon={<IconPlus />}
74-
label={t('common.tag')}
75-
onClick={onEdit}
76-
/>
86+
<AddTagButton onCreate={onAddTag} />
7787
<IconButton
7888
icon={<IconPlayerPlay />}
7989
label={t('common.play')}

src/components/story/story-preview.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@ export const StoryPreview: React.FC<StoryPreviewProps> = React.memo(props => {
2323
{
2424
key: passage.name,
2525
size: passage.text.length,
26-
x: passage.left,
27-
y: passage.top
26+
27+
// Jitter the passages off a strict grid.
28+
29+
x: passage.left + 50 - (hueString(passage.name) % 100),
30+
y: passage.top + 50 - (hueString(passage.name) % 100)
2831
}
2932
],
3033
maxSize: Math.max(passage.text.length, result.maxSize)

src/components/tag/add-tag-button.tsx

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import * as React from 'react';
2+
import {useTranslation} from 'react-i18next';
3+
import {Color} from '../../util/color';
4+
import {IconPlus} from '@tabler/icons';
5+
import {IconButton} from '../control/icon-button';
6+
import {TagModal} from './tag-modal';
7+
8+
export interface AddTagButtonProps {
9+
onCreate: (name: string, color: Color) => void;
10+
}
11+
12+
export const AddTagButton: React.FC<AddTagButtonProps> = props => {
13+
const [modalOpen, setModalOpen] = React.useState(false);
14+
const [newColor, setNewColor] = React.useState<Color>('none');
15+
const [newName, setNewName] = React.useState('');
16+
const {t} = useTranslation();
17+
18+
function openModal() {
19+
setNewColor('');
20+
setNewName('');
21+
setModalOpen(true);
22+
}
23+
24+
// TODO: move localization strings
25+
26+
return (
27+
<>
28+
<IconButton
29+
icon={<IconPlus />}
30+
label={t('common.tag')}
31+
onClick={openModal}
32+
/>
33+
<TagModal
34+
color={newColor}
35+
isOpen={modalOpen}
36+
message={t('passageEdit.tagToolbar.addTagHeader')}
37+
name={newName}
38+
onChangeName={setNewName}
39+
onChangeColor={setNewColor}
40+
onCancel={() => setModalOpen(false)}
41+
onSubmit={() => props.onCreate(newName, newColor)}
42+
/>
43+
</>
44+
);
45+
};

src/components/tag/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './add-tag-button';
2+
export * from './tag-button';
3+
export * from './tag-stripe';

src/components/tag/tag-button.css

+63-12
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,76 @@
11
@import '../../styles/colors.css';
2+
@import '../../styles/depth.css';
3+
@import '../../styles/metrics.css';
24

3-
.tag-button.color-red .menu-button .icon-button svg {
4-
color: var(--red);
5+
.tag-button .menu-button > .icon-button {
6+
background-color: var(--faint-gray);
7+
border: none;
8+
border-radius: var(--control-height);
9+
box-shadow: var(--shadow-small);
10+
height: var(--control-height);
11+
font-size: 85%;
512
}
613

7-
.tag-button.color-orange .menu-button .icon-button svg {
8-
color: var(--orange);
14+
.tag-button .menu-button > .icon-button:hover {
15+
background-color: var(--faint-gray);
916
}
1017

11-
.tag-button.color-yellow .menu-button .icon-button svg {
12-
color: var(--yellow);
18+
.tag-button.color-red .menu-button > .icon-button {
19+
background-color: var(--faint-red);
20+
box-shadow: 0 1px 1px var(--light-red);
21+
color: var(--dark-red);
1322
}
1423

15-
.tag-button.color-green .menu-button .icon-button svg {
16-
color: var(--green);
24+
.tag-button.color-red .menu-button > .icon-button:hover {
25+
background-color: var(--light-red);
1726
}
1827

19-
.tag-button.color-blue .menu-button .icon-button svg {
20-
color: var(--blue);
28+
.tag-button.color-orange .menu-button > .icon-button {
29+
background-color: var(--faint-orange);
30+
box-shadow: 0 1px 1px var(--light-orange);
31+
color: var(--dark-orange);
2132
}
2233

23-
.tag-button.color-purple .menu-button .icon-button svg {
24-
color: var(--purple);
34+
.tag-button.color-orange .menu-button > .icon-button:hover {
35+
background-color: var(--light-orange);
36+
}
37+
38+
.tag-button.color-yellow .menu-button > .icon-button {
39+
background-color: var(--faint-yellow);
40+
box-shadow: 0 1px 1px var(--light-yellow);
41+
color: var(--dark-yellow);
42+
}
43+
44+
.tag-button.color-yellow .menu-button > .icon-button:hover {
45+
background-color: var(--light-yellow);
46+
}
47+
48+
.tag-button.color-green .menu-button > .icon-button {
49+
background-color: var(--faint-green);
50+
box-shadow: 0 1px 1px var(--light-green);
51+
color: var(--dark-green);
52+
}
53+
54+
.tag-button.color-green .menu-button > .icon-button:hover {
55+
background-color: var(--light-green);
56+
}
57+
58+
.tag-button.color-blue .menu-button > .icon-button {
59+
background-color: var(--faint-blue);
60+
box-shadow: 0 1px 1px var(--light-blue);
61+
color: var(--dark-blue);
62+
}
63+
64+
.tag-button.color-blue .menu-button > .icon-button:hover {
65+
background-color: var(--light-blue);
66+
}
67+
68+
.tag-button.color-purple .menu-button > .icon-button {
69+
background-color: var(--faint-purple);
70+
box-shadow: 0 1px 1px var(--light-purple);
71+
color: var(--dark-purple);
72+
}
73+
74+
.tag-button.color-purple .menu-button > .icon-button:hover {
75+
background-color: var(--light-purple);
2576
}

src/components/tag/tag-button.tsx

+33-7
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,64 @@
11
import * as React from 'react';
22
import classNames from 'classnames';
33
import {useTranslation} from 'react-i18next';
4-
import {IconTagNub} from '../image/icon';
4+
import {IconChevronDown} from '@tabler/icons';
55
import {MenuButton} from '../control/menu-button';
6+
import {TagModal} from './tag-modal';
67
import {Color} from '../../util/color';
78
import './tag-button.css';
89

910
export interface TagButtonProps {
10-
color: Color;
11+
color?: Color;
1112
name: string;
1213
onDelete: () => void;
13-
onEdit: () => void;
14+
onEdit: (name: string, color: Color) => void;
1415
}
1516

1617
export const TagButton: React.FC<TagButtonProps> = props => {
18+
const [editColor, setEditColor] = React.useState<Color>('none');
19+
const [editName, setEditName] = React.useState(props.name);
20+
const [modalOpen, setModalOpen] = React.useState(false);
1721
const {t} = useTranslation();
1822

23+
function handleEdit() {
24+
props.onEdit(editName, editColor);
25+
setModalOpen(false);
26+
}
27+
28+
function openModal() {
29+
setEditName(props.name);
30+
setEditColor(props.color ?? 'none');
31+
setModalOpen(true);
32+
}
33+
1934
return (
2035
<span className={classNames('tag-button', `color-${props.color}`)}>
2136
<MenuButton
22-
icon={<IconTagNub />}
37+
icon={<IconChevronDown />}
38+
iconPosition="end"
2339
items={[
2440
{
2541
label: t('common.edit'),
26-
onClick: props.onEdit
42+
onClick: openModal
2743
},
2844
{
2945
label: t('common.delete'),
30-
onClick: props.onDelete,
31-
variant: 'danger'
46+
onClick: props.onDelete
3247
}
3348
]}
3449
label={props.name}
3550
/>
51+
<TagModal
52+
color={editColor}
53+
detail={t('passageEdit.tagToolbar.editTagDetail')}
54+
isOpen={modalOpen}
55+
message={t('passageEdit.tagToolbar.editTagHeader')}
56+
name={editName}
57+
onChangeName={setEditName}
58+
onChangeColor={setEditColor}
59+
onCancel={() => setModalOpen(false)}
60+
onSubmit={() => handleEdit()}
61+
/>
3662
</span>
3763
);
3864
};
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
@import '../../../../styles/depth.css';
2+
@import '../../../../styles/metrics.css';
23

34
.passage-dialog .card {
45
grid-template-rows: min-content 1fr;
56
height: 100%;
67
}
8+
9+
.passage-dialog .tags {
10+
padding: var(--grid-size);
11+
}

0 commit comments

Comments
 (0)