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
-
+
- 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)} - /> +
+ +
+ 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)} - /> +
+ +
+ !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 && } - -
-
    - {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}
    • ), - )} -
    - - 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;