diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx
index 16122eafd..82f5c2ab2 100644
--- a/client/components/combobox.jsx
+++ b/client/components/combobox.jsx
@@ -11,6 +11,7 @@ const Combobox = createReactClass({
trigger : 'hover',
default : '',
placeholder : '',
+ tooltip: '',
autoSuggest : {
clearAutoSuggestOnClick : true,
suggestMethod : 'includes',
@@ -70,7 +71,8 @@ const Combobox = createReactClass({
return (
{this.handleDropdown(true);} : undefined}
- onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined}>
+ onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined}
+ {...(this.props.tooltip ? { 'data-tooltip-bottom': this.props.tooltip } : {})}>
this.handleInput(e)}
diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx
index 721340079..301c088b7 100644
--- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx
+++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx
@@ -256,7 +256,7 @@ const MetadataEditor = createReactClass({
return
language
-
+
- Sets the HTML Lang property for your brew. May affect hyphenation or spellcheck.
;
@@ -339,14 +338,20 @@ const MetadataEditor = createReactClass({
{this.renderThumbnail()}
-
this.handleFieldChange('tags', e)}
- />
+
+
Tags
+
+ this.handleFieldChange('tags', e)}
+ tooltip='You may start tags with "type", "system", "group" or "meta" followed by a colon ":", these will be colored in your userpage.'
+ />
+
+
+
{this.renderLanguageDropdown()}
@@ -358,15 +363,22 @@ const MetadataEditor = createReactClass({
{this.renderAuthors()}
- !this.props.metadata.authors?.includes(v)]}
- placeholder='invite author' unique={true}
- values={this.props.metadata.invitedAuthors}
- smallText='Invited author usernames are case sensitive. After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.'
- onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}
- />
+
+
Invited authors
+
+ !this.props.metadata.authors?.includes(v)]}
+ placeholder='invite author' unique={true}
+ tooltip={`Invited author usernames are case sensitive.
+ After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.`}
+ values={this.props.metadata.invitedAuthors}
+ onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}
+ />
+
+
+
Privacy
diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.less b/client/homebrew/editor/metadataEditor/metadataEditor.less
index 304b85d30..0a3d670fa 100644
--- a/client/homebrew/editor/metadataEditor/metadataEditor.less
+++ b/client/homebrew/editor/metadataEditor/metadataEditor.less
@@ -44,8 +44,6 @@
gap : 10px;
}
-
-
.field {
position : relative;
display : flex;
@@ -62,6 +60,9 @@
& > .value {
flex : 1 1 auto;
width : 50px;
+ &[data-tooltip-right] {
+ max-width:380px;
+ }
&:invalid { background : #FFB9B9; }
small {
display : block;
@@ -110,7 +111,7 @@
}
}
- &.language .language-dropdown {
+ &.language .value {
z-index : 200;
max-width : 150px;
}
diff --git a/client/homebrew/editor/tagInput/tagInput.jsx b/client/homebrew/editor/tagInput/tagInput.jsx
index e9923eb6a..a61aa4435 100644
--- a/client/homebrew/editor/tagInput/tagInput.jsx
+++ b/client/homebrew/editor/tagInput/tagInput.jsx
@@ -1,71 +1,71 @@
-import './tagInput.less';
-import React, { useState, useEffect } from 'react';
-import Combobox from '../../../components/combobox.jsx';
+import "./tagInput.less";
+import React, { useState, useEffect } from "react";
+import Combobox from "../../../components/combobox.jsx";
-import tagSuggestionList from './curatedTagSuggestionList.js';
+import tagSuggestionList from "./curatedTagSuggestionList.js";
-const TagInput = ({ label, valuePatterns, values = [], unique = true, placeholder = '', smallText = '', onChange })=>{
+const TagInput = ({tooltip, label, valuePatterns, values = [], unique = true, placeholder = "", smallText = "", onChange }) => {
const [tagList, setTagList] = useState(
- values.map((value)=>({
+ values.map((value) => ({
value,
- editing : false,
- draft : '',
+ editing: false,
+ draft: "",
})),
);
- useEffect(()=>{
+ useEffect(() => {
const incoming = values || [];
- const current = tagList.map((t)=>t.value);
+ const current = tagList.map((t) => t.value);
- const changed = incoming.length !== current.length || incoming.some((v, i)=>v !== current[i]);
+ const changed = incoming.length !== current.length || incoming.some((v, i) => v !== current[i]);
- if(changed) {
+ if (changed) {
setTagList(
- incoming.map((value)=>({
+ incoming.map((value) => ({
value,
- editing : false,
+ editing: false,
})),
);
}
}, [values]);
- useEffect(()=>{
+ useEffect(() => {
onChange?.({
- target : { value: tagList.map((t)=>t.value) },
+ target: { value: tagList.map((t) => t.value) },
});
}, [tagList]);
// substrings to be normalized to the first value on the array
const duplicateGroups = [
- ['5e 2024', '5.5e', '5e\'24', '5.24', '5e24', '5.5'],
- ['5e', '5th Edition'],
- ['Dungeons & Dragons', 'Dungeons and Dragons', 'Dungeons n dragons'],
- ['D&D', 'DnD', 'dnd', 'Dnd', 'dnD', 'd&d', 'd&D', 'D&d'],
- ['P2e', 'p2e', 'P2E', 'Pathfinder 2e'],
+ ["5e 2024", "5.5e", "5e'24", "5.24", "5e24", "5.5"],
+ ["5e", "5th Edition"],
+ ["Dungeons & Dragons", "Dungeons and Dragons", "Dungeons n dragons"],
+ ["D&D", "DnD", "dnd", "Dnd", "dnD", "d&d", "d&D", "D&d"],
+ ["P2e", "p2e", "P2E", "Pathfinder 2e"],
];
- const normalizeValue = (input)=>{
+ const normalizeValue = (input) => {
const lowerInput = input.toLowerCase();
let normalizedTag = input;
for (const group of duplicateGroups) {
for (const tag of group) {
- if(!tag) continue;
+ if (!tag) continue;
const index = lowerInput.indexOf(tag.toLowerCase());
- if(index !== -1) {
+ if (index !== -1) {
normalizedTag = input.slice(0, index) + group[0] + input.slice(index + tag.length);
break;
}
}
}
- if(normalizedTag.includes(':')) {
- const [rawType, rawValue = ''] = normalizedTag.split(':');
+ if (normalizedTag.includes(":")) {
+ const [rawType, rawValue = ""] = normalizedTag.split(":");
const tagType = rawType.trim().toLowerCase();
const tagValue = rawValue.trim();
- if(tagValue.length > 0) {
+ if (tagValue.length > 0) {
normalizedTag = `${tagType}:${tagValue[0].toUpperCase()}${tagValue.slice(1)}`;
}
//trims spaces around colon and capitalizes the first word after the colon
@@ -75,56 +75,56 @@ const TagInput = ({ label, valuePatterns, values = [], unique = true, placeholde
return normalizedTag;
};
- const submitTag = (newValue, index = null)=>{
+ const submitTag = (newValue, index = null) => {
const trimmed = newValue?.trim();
- if(!trimmed) return;
- if(!valuePatterns.test(trimmed)) return;
+ if (!trimmed) return;
+ if (!valuePatterns.test(trimmed)) return;
const normalizedTag = normalizeValue(trimmed);
- setTagList((prev)=>{
- const existsIndex = prev.findIndex((t)=>t.value.toLowerCase() === normalizedTag.toLowerCase());
- if(unique && existsIndex !== -1) return prev;
- if(index !== null) {
- return prev.map((t, i)=>(i === index ? { ...t, value: normalizedTag, editing: false } : t));
+ setTagList((prev) => {
+ const existsIndex = prev.findIndex((t) => t.value.toLowerCase() === normalizedTag.toLowerCase());
+ if (unique && existsIndex !== -1) return prev;
+ if (index !== null) {
+ return prev.map((t, i) => (i === index ? { ...t, value: normalizedTag, editing: false } : t));
}
return [...prev, { value: normalizedTag, editing: false }];
});
};
- const removeTag = (index)=>{
- setTagList((prev)=>prev.filter((_, i)=>i !== index));
+ const removeTag = (index) => {
+ setTagList((prev) => prev.filter((_, i) => i !== index));
};
- const editTag = (index)=>{
- setTagList((prev)=>prev.map((t, i)=>(i === index ? { ...t, editing: true, draft: t.value } : t)));
+ const editTag = (index) => {
+ setTagList((prev) => prev.map((t, i) => (i === index ? { ...t, editing: true, draft: t.value } : t)));
};
- const stopEditing = (index)=>{
- setTagList((prev)=>prev.map((t, i)=>(i === index ? { ...t, editing: false, draft: '' } : t)));
+ const stopEditing = (index) => {
+ setTagList((prev) => prev.map((t, i) => (i === index ? { ...t, editing: false, draft: "" } : t)));
};
- const suggestionOptions = tagSuggestionList.map((tag)=>{
- const tagType = tag.split(':');
+ const suggestionOptions = tagSuggestionList.map((tag) => {
+ const tagType = tag.split(":");
- let classes = 'item';
+ let classes = "item";
switch (tagType[0]) {
- case 'type':
- classes = 'item type';
- break;
- case 'group':
- classes = 'item group';
- break;
- case 'meta':
- classes = 'item meta';
- break;
- case 'system':
- classes = 'item system';
- break;
- default:
- classes = 'item';
- break;
+ case "type":
+ classes = "item type";
+ break;
+ case "group":
+ classes = "item group";
+ break;
+ case "meta":
+ classes = "item meta";
+ break;
+ case "system":
+ classes = "item system";
+ break;
+ default:
+ classes = "item";
+ break;
}
return (
@@ -135,26 +135,54 @@ const TagInput = ({ label, valuePatterns, values = [], unique = true, placeholde
});
return (
-
- {label &&
{label} }
-
-
-
- {tagList.map((t, i)=>t.editing ? (
+
+
submitTag(value)}
+ onEntry={(e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ submitTag(e.target.value);
+ }
+ }}
+ />
+
+ {tagList.map((t, i) =>
+ t.editing ? (
setTagList((prev)=>prev.map((tag, idx)=>(idx === i ? { ...tag, draft: e.target.value } : tag)))}
- onKeyDown={(e)=>{
- if(e.key === 'Enter') {
+ onChange={(e) =>
+ setTagList((prev) =>
+ prev.map((tag, idx) => (idx === i ? { ...tag, draft: e.target.value } : tag)),
+ )
+ }
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
e.preventDefault();
submitTag(t.draft, i); // submit draft
- setTagList((prev)=>prev.map((tag, idx)=>(idx === i ? { ...tag, draft: '' } : tag)),
+ setTagList((prev) =>
+ prev.map((tag, idx) => (idx === i ? { ...tag, draft: "" } : tag)),
);
}
- if(e.key === 'Escape') {
+ if (e.key === "Escape") {
stopEditing(i);
e.target.blur();
}
@@ -162,47 +190,20 @@ const TagInput = ({ label, valuePatterns, values = [], unique = true, placeholde
autoFocus
/>
) : (
- editTag(i)}>
+ editTag(i)}>
{t.value}
{
+ type="button"
+ onClick={(e) => {
e.stopPropagation();
removeTag(i);
}}>
-
+
),
- )}
-
-
- submitTag(value)}
- onEntry={(e)=>{
- if(e.key === 'Enter') {
- e.preventDefault();
- submitTag(e.target.value);
- }
- }}
- />
- {smallText.length !== 0 && {smallText} }
-
+ )}
+
);
};
diff --git a/client/homebrew/editor/tagInput/tagInput.less b/client/homebrew/editor/tagInput/tagInput.less
index 8680bcd79..e986c6cc7 100644
--- a/client/homebrew/editor/tagInput/tagInput.less
+++ b/client/homebrew/editor/tagInput/tagInput.less
@@ -1,5 +1,11 @@
.tags {
+ .tagInputWrap {
+ display:grid;
+ grid-template-columns: 200px 3fr;
+ gap:10px;
+ }
+
.list input {
border-radius: 5px;
}
diff --git a/shared/naturalcrit/styles/tooltip.less b/shared/naturalcrit/styles/tooltip.less
index b21439486..c99cbf116 100644
--- a/shared/naturalcrit/styles/tooltip.less
+++ b/shared/naturalcrit/styles/tooltip.less
@@ -3,18 +3,23 @@
@arrowSize : 6px;
@arrowPosition : 18px;
[data-tooltip] {
+ position:relative;
.tooltip(attr(data-tooltip));
}
[data-tooltip-top] {
+ position:relative;
.tooltipTop(attr(data-tooltip-top));
}
[data-tooltip-bottom] {
+ position:relative;
.tooltipBottom(attr(data-tooltip-bottom));
}
[data-tooltip-left] {
+ position:relative;
.tooltipLeft(attr(data-tooltip-left));
}
[data-tooltip-right] {
+ position:relative;
.tooltipRight(attr(data-tooltip-right));
}
.tooltip(@content) {
@@ -75,8 +80,9 @@
}
&::after { margin-bottom : -14px;}
&::before, &::after {
- bottom : 50%;
+ top : 50%;
left : 100%;
+ translate:0 -50%;
}
&:hover::after, &:hover::before, &:focus::after, &:focus::before {
.transform(translateX(@arrowSize + 2));
@@ -106,9 +112,12 @@
font-size : 12px;
line-height : 12px;
color : white;
- white-space : nowrap;
content : @content;
background : @tooltipColor;
+ max-width : 50ch;
+ width :max-content;
+ word-break : break-word;
+ overflow-wrap : break-word;
}
&:hover::before, &:hover::after {
visibility : visible;