feat: classnames

This commit is contained in:
2023-10-16 15:48:56 +01:00
parent 7129ffa1c7
commit 2ce5bd153b
22 changed files with 91 additions and 80 deletions

View File

@ -18,6 +18,7 @@
"@types/use-sync-external-store": "^0.0.4", "@types/use-sync-external-store": "^0.0.4",
"@uidotdev/usehooks": "^2.3.1", "@uidotdev/usehooks": "^2.3.1",
"@void-cat/api": "^1.0.10", "@void-cat/api": "^1.0.10",
"classnames": "^2.3.2",
"debug": "^4.3.4", "debug": "^4.3.4",
"dexie": "^3.2.4", "dexie": "^3.2.4",
"emojilib": "^3.0.10", "emojilib": "^3.0.10",

View File

@ -1,4 +1,5 @@
import { useState, ReactNode } from "react"; import { useState, ReactNode } from "react";
import classNames from "classnames";
import Icon from "Icons/Icon"; import Icon from "Icons/Icon";
import ShowMore from "Element/Event/ShowMore"; import ShowMore from "Element/Event/ShowMore";
@ -38,15 +39,13 @@ interface CollapsedSectionProps {
export const CollapsedSection = ({ title, children, className }: CollapsedSectionProps) => { export const CollapsedSection = ({ title, children, className }: CollapsedSectionProps) => {
const [collapsed, setCollapsed] = useState(true); const [collapsed, setCollapsed] = useState(true);
const icon = ( const icon = (
<div className={`collapse-icon ${collapsed ? "" : "flip"}`}> <div className={classNames("collapse-icon", { flip: !collapsed })}>
<Icon name="arrowFront" /> <Icon name="arrowFront" />
</div> </div>
); );
return ( return (
<> <>
<div <div className={classNames("collapsable-section", className)} onClick={() => setCollapsed(!collapsed)}>
className={`collapsable-section${className ? ` ${className}` : ""}`}
onClick={() => setCollapsed(!collapsed)}>
{title} {title}
<CollapsedIcon icon={icon} collapsed={collapsed} /> <CollapsedIcon icon={icon} collapsed={collapsed} />
</div> </div>

View File

@ -1,4 +1,5 @@
import "./Copy.css"; import "./Copy.css";
import classNames from "classnames";
import Icon from "Icons/Icon"; import Icon from "Icons/Icon";
import { useCopy } from "useCopy"; import { useCopy } from "useCopy";
@ -13,7 +14,7 @@ export default function Copy({ text, maxSize = 32, className }: CopyProps) {
const trimmed = text.length > maxSize ? `${text.slice(0, sliceLength)}...${text.slice(-sliceLength)}` : text; const trimmed = text.length > maxSize ? `${text.slice(0, sliceLength)}...${text.slice(-sliceLength)}` : text;
return ( return (
<div className={`copy flex pointer g8${className ? ` ${className}` : ""}`} onClick={() => copy(text)}> <div className={classNames("copy flex pointer g8", className)} onClick={() => copy(text)}>
<span className="copy-body">{trimmed}</span> <span className="copy-body">{trimmed}</span>
<span className="icon" style={{ color: copied ? "var(--success)" : "var(--highlight)" }}> <span className="icon" style={{ color: copied ? "var(--success)" : "var(--highlight)" }}>
{copied ? <Icon name="check" size={14} /> : <Icon name="copy-solid" size={14} />} {copied ? <Icon name="check" size={14} /> : <Icon name="copy-solid" size={14} />}

View File

@ -3,6 +3,7 @@ import { useState } from "react";
import { useIntl, FormattedMessage } from "react-intl"; import { useIntl, FormattedMessage } from "react-intl";
import { useMemo } from "react"; import { useMemo } from "react";
import { decodeInvoice } from "@snort/shared"; import { decodeInvoice } from "@snort/shared";
import classNames from "classnames";
import SendSats from "Element/SendSats"; import SendSats from "Element/SendSats";
import Icon from "Icons/Icon"; import Icon from "Icons/Icon";
@ -60,7 +61,7 @@ export default function Invoice(props: InvoiceProps) {
return ( return (
<> <>
<div className={`note-invoice flex ${isExpired ? "expired" : ""} ${isPaid ? "paid" : ""}`}> <div className={classNames("note-invoice flex", { expired: isExpired, paid: isPaid })}>
<div className="invoice-header">{header()}</div> <div className="invoice-header">{header()}</div>
<p className="invoice-amount"> <p className="invoice-amount">

View File

@ -8,6 +8,7 @@ import useLogin from "Hooks/useLogin";
import Icon from "Icons/Icon"; import Icon from "Icons/Icon";
import { useNoteCreator } from "State/NoteCreator"; import { useNoteCreator } from "State/NoteCreator";
import { NoteCreator } from "./NoteCreator"; import { NoteCreator } from "./NoteCreator";
import classNames from "classnames";
export const NoteCreatorButton = ({ className }: { className?: string }) => { export const NoteCreatorButton = ({ className }: { className?: string }) => {
const buttonRef = useRef<HTMLButtonElement>(null); const buttonRef = useRef<HTMLButtonElement>(null);
@ -36,7 +37,7 @@ export const NoteCreatorButton = ({ className }: { className?: string }) => {
<> <>
<button <button
ref={buttonRef} ref={buttonRef}
className={`primary circle${className ? ` ${className}` : ""}`} className={classNames("primary circle", className)}
onClick={() => onClick={() =>
update(v => { update(v => {
v.replyTo = undefined; v.replyTo = undefined;

View File

@ -2,7 +2,9 @@ import { Link, useNavigate } from "react-router-dom";
import React, { ReactNode, useMemo, useState } from "react"; import React, { ReactNode, useMemo, useState } from "react";
import { useInView } from "react-intersection-observer"; import { useInView } from "react-intersection-observer";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import classNames from "classnames";
import { EventExt, EventKind, HexKey, Lists, NostrLink, NostrPrefix, TaggedNostrEvent } from "@snort/system"; import { EventExt, EventKind, HexKey, Lists, NostrLink, NostrPrefix, TaggedNostrEvent } from "@snort/system";
import { findTag, hexToBech32, profileLink } from "SnortUtils"; import { findTag, hexToBech32, profileLink } from "SnortUtils";
import useModeration from "Hooks/useModeration"; import useModeration from "Hooks/useModeration";
import useLogin from "Hooks/useLogin"; import useLogin from "Hooks/useLogin";
@ -27,7 +29,7 @@ import { chainKey } from "Hooks/useThreadContext";
export function NoteInner(props: NoteProps) { export function NoteInner(props: NoteProps) {
const { data: ev, related, highlight, options: opt, ignoreModeration = false, className } = props; const { data: ev, related, highlight, options: opt, ignoreModeration = false, className } = props;
const baseClassName = `note card${className ? ` ${className}` : ""}`; const baseClassName = classNames("note card", className);
const navigate = useNavigate(); const navigate = useNavigate();
const [showReactions, setShowReactions] = useState(false); const [showReactions, setShowReactions] = useState(false);
@ -328,7 +330,7 @@ export function NoteInner(props: NoteProps) {
} }
const note = ( const note = (
<div className={`${baseClassName}${highlight ? " active " : " "}`} onClick={e => goToEvent(e, ev)} ref={ref}> <div className={classNames(baseClassName, { active: highlight })} onClick={e => goToEvent(e, ev)} ref={ref}>
{content()} {content()}
</div> </div>
); );

View File

@ -3,6 +3,7 @@ import { useMemo, useState, ReactNode, useContext } from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { TaggedNostrEvent, u256, NostrPrefix, EventExt, parseNostrLink, NostrLink } from "@snort/system"; import { TaggedNostrEvent, u256, NostrPrefix, EventExt, parseNostrLink, NostrLink } from "@snort/system";
import classNames from "classnames";
import { getAllLinkReactions, getLinkReactions } from "SnortUtils"; import { getAllLinkReactions, getLinkReactions } from "SnortUtils";
import BackButton from "Element/BackButton"; import BackButton from "Element/BackButton";
@ -83,14 +84,17 @@ const ThreadNote = ({ active, note, isLast, isLastSubthread, related, chains, on
const [collapsed, setCollapsed] = useState(!activeInReplies); const [collapsed, setCollapsed] = useState(!activeInReplies);
const hasMultipleNotes = replies.length > 1; const hasMultipleNotes = replies.length > 1;
const isLastVisibleNote = isLastSubthread && isLast && !hasMultipleNotes; const isLastVisibleNote = isLastSubthread && isLast && !hasMultipleNotes;
const className = `subthread-container ${isLast && collapsed ? "subthread-last" : "subthread-multi subthread-mid"}`; const className = classNames(
"subthread-container",
isLast && collapsed ? "subthread-last" : "subthread-multi subthread-mid",
);
return ( return (
<> <>
<div className={className}> <div className={className}>
<Divider variant="small" /> <Divider variant="small" />
<Note <Note
highlight={active === note.id} highlight={active === note.id}
className={`thread-note ${isLastVisibleNote ? "is-last-note" : ""}`} className={classNames("thread-note", { "is-last-note": isLastVisibleNote })}
data={note} data={note}
key={note.id} key={note.id}
related={related} related={related}
@ -156,13 +160,15 @@ const TierThree = ({ active, isLastSubthread, notes, related, chains, onNavigate
return ( return (
<> <>
<div <div
className={`subthread-container ${hasMultipleNotes ? "subthread-multi" : ""} ${ className={classNames("subthread-container", {
isLast ? "subthread-last" : "subthread-mid" "subthread-multi": hasMultipleNotes,
}`}> "subthread-last": isLast,
"subthread-mid": !isLast,
})}>
<Divider variant="small" /> <Divider variant="small" />
<Note <Note
highlight={active === first.id} highlight={active === first.id}
className={`thread-note ${isLastSubthread && isLast ? "is-last-note" : ""}`} className={classNames("thread-note", { "is-last-note": isLastSubthread && isLast })}
data={first} data={first}
key={first.id} key={first.id}
related={related} related={related}
@ -188,12 +194,14 @@ const TierThree = ({ active, isLastSubthread, notes, related, chains, onNavigate
return ( return (
<div <div
key={r.id} key={r.id}
className={`subthread-container ${lastReply ? "" : "subthread-multi"} ${ className={classNames("subthread-container", {
lastReply ? "subthread-last" : "subthread-mid" "subthread-multi": !lastReply,
}`}> "subthread-last": !lastReply,
"subthread-mid": lastReply,
})}>
<Divider variant="small" /> <Divider variant="small" />
<Note <Note
className={`thread-note ${lastNote ? "is-last-note" : ""}`} className={classNames("thread-note", { "is-last-note": lastNote })}
highlight={active === r.id} highlight={active === r.id}
data={r} data={r}
key={r.id} key={r.id}

View File

@ -1,14 +1,19 @@
import classNames from "classnames";
import Icon, { IconProps } from "Icons/Icon";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
interface IconButtonProps { interface IconButtonProps {
onClick(): void; onClick?: () => void;
children: ReactNode; icon: IconProps;
className?: string;
children?: ReactNode;
} }
const IconButton = ({ onClick, children }: IconButtonProps) => { const IconButton = ({ onClick, icon, children, className }: IconButtonProps) => {
return ( return (
<button className="icon" type="button" onClick={onClick}> <button className={classNames("icon", className)} type="button" onClick={onClick}>
<div className="icon-wrapper">{children}</div> <Icon {...icon} />
{children}
</button> </button>
); );
}; };

View File

@ -1,6 +1,7 @@
import "./Text.css"; import "./Text.css";
import { ReactNode, useState } from "react"; import { ReactNode, useState } from "react";
import { HexKey, ParsedFragment } from "@snort/system"; import { HexKey, ParsedFragment } from "@snort/system";
import classNames from "classnames";
import Invoice from "Element/Embed/Invoice"; import Invoice from "Element/Embed/Invoice";
import Hashtag from "Element/Embed/Hashtag"; import Hashtag from "Element/Embed/Hashtag";
@ -276,7 +277,7 @@ export default function Text({
}; };
return ( return (
<div dir="auto" className={`text${className ? ` ${className}` : ""}`} onClick={onClick}> <div dir="auto" className={classNames("text", className)} onClick={onClick}>
{renderContent()} {renderContent()}
{showSpotlight && <SpotlightMediaModal images={images} onClose={() => setShowSpotlight(false)} idx={imageIdx} />} {showSpotlight && <SpotlightMediaModal images={images} onClose={() => setShowSpotlight(false)} idx={imageIdx} />}
</div> </div>

View File

@ -2,6 +2,7 @@ import "./Avatar.css";
import { CSSProperties, ReactNode, useEffect, useState } from "react"; import { CSSProperties, ReactNode, useEffect, useState } from "react";
import type { UserMetadata } from "@snort/system"; import type { UserMetadata } from "@snort/system";
import classNames from "classnames";
import useImgProxy from "Hooks/useImgProxy"; import useImgProxy from "Hooks/useImgProxy";
import { getDisplayName } from "Element/User/DisplayName"; import { getDisplayName } from "Element/User/DisplayName";
@ -44,7 +45,7 @@ const Avatar = ({ pubkey, user, size, onClick, image, imageOverlay, icons, class
<div <div
onClick={onClick} onClick={onClick}
style={style} style={style}
className={`avatar${imageOverlay ? " with-overlay" : ""} ${className ?? ""}`} className={classNames("avatar", { "with-overlay": imageOverlay }, className)}
data-domain={domain?.toLowerCase()} data-domain={domain?.toLowerCase()}
title={getDisplayName(user, "")}> title={getDisplayName(user, "")}>
{icons && <div className="icons">{icons}</div>} {icons && <div className="icons">{icons}</div>}

View File

@ -2,6 +2,7 @@ import "./NoteToSelf.css";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import FormattedMessage from "Element/FormattedMessage"; import FormattedMessage from "Element/FormattedMessage";
import { profileLink } from "SnortUtils"; import { profileLink } from "SnortUtils";
import classNames from "classnames";
import messages from "../messages"; import messages from "../messages";
import Icon from "Icons/Icon"; import Icon from "Icons/Icon";
@ -31,9 +32,9 @@ export default function NoteToSelf({ pubkey, clickable, className, link }: NoteT
}; };
return ( return (
<div className={`nts${className ? ` ${className}` : ""}`}> <div className={classNames("nts", className)}>
<div className="avatar-wrapper"> <div className="avatar-wrapper">
<div className={`avatar${clickable ? " clickable" : ""}`}> <div className={classNames("avatar", { clickable: clickable })}>
<Icon onClick={clickLink} name="book-closed" size={20} /> <Icon onClick={clickLink} name="book-closed" size={20} />
</div> </div>
</div> </div>

View File

@ -6,6 +6,7 @@ import { HexKey, UserMetadata } from "@snort/system";
import { useUserProfile } from "@snort/system-react"; import { useUserProfile } from "@snort/system-react";
import { useHover } from "@uidotdev/usehooks"; import { useHover } from "@uidotdev/usehooks";
import { ControlledMenu } from "@szhsin/react-menu"; import { ControlledMenu } from "@szhsin/react-menu";
import classNames from "classnames";
import { profileLink } from "SnortUtils"; import { profileLink } from "SnortUtils";
import Avatar from "Element/User/Avatar"; import Avatar from "Element/User/Avatar";
@ -157,7 +158,7 @@ export default function ProfileImage({
if (link === "") { if (link === "") {
return ( return (
<> <>
<div className={`pfp${className ? ` ${className}` : ""}`} onClick={handleClick}> <div className={classNames("pfp", className)} onClick={handleClick}>
{inner()} {inner()}
</div> </div>
{profileCard()} {profileCard()}
@ -167,7 +168,7 @@ export default function ProfileImage({
return ( return (
<> <>
<Link <Link
className={`pfp${className ? ` ${className}` : ""}`} className={classNames("pfp", className)}
to={link === undefined ? profileLink(pubkey) : link} to={link === undefined ? profileLink(pubkey) : link}
onClick={handleClick}> onClick={handleClick}>
{inner()} {inner()}

View File

@ -1,8 +1,6 @@
import IconProps from "./IconProps"; export const BlueWallet = (props: { width?: number; height?: number }) => {
export const BlueWallet = (props: IconProps) => {
return ( return (
<svg width="58px" height="58px" viewBox="0 0 58 58" version="1.1" xmlns="http://www.w3.org/2000/svg" {...props}> <svg viewBox="0 0 58 58" version="1.1" xmlns="http://www.w3.org/2000/svg" {...props}>
<title>logo-bluewallet</title> <title>logo-bluewallet</title>
<defs> <defs>
<filter x="-14.0%" y="-13.8%" width="128.1%" height="127.6%" filterUnits="objectBoundingBox" id="filter-1"> <filter x="-14.0%" y="-13.8%" width="128.1%" height="127.6%" filterUnits="objectBoundingBox" id="filter-1">

View File

@ -1,15 +1,15 @@
import { MouseEventHandler } from "react"; import { MouseEventHandler } from "react";
import IconsSvg from "public/icons.svg"; import IconsSvg from "public/icons.svg";
type Props = { export interface IconProps {
name: string; name: string;
size?: number; size?: number;
height?: number; height?: number;
className?: string; className?: string;
onClick?: MouseEventHandler<SVGSVGElement>; onClick?: MouseEventHandler<SVGSVGElement>;
}; }
const Icon = (props: Props) => { const Icon = (props: IconProps) => {
const size = props.size || 20; const size = props.size || 20;
const href = `${IconsSvg}#` + props.name; const href = `${IconsSvg}#` + props.name;

View File

@ -1,5 +0,0 @@
export default interface IconProps {
className?: string;
width?: number;
height?: number;
}

View File

@ -1,6 +1,4 @@
import IconProps from "./IconProps"; export default function NostrIcon(props: { width?: number; height?: number }) {
export default function NostrIcon(props: IconProps) {
return ( return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 116.446 84.924" {...props}> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 116.446 84.924" {...props}>
<path <path

View File

@ -1,7 +1,6 @@
import IconProps from "./IconProps";
import "./Spinner.css"; import "./Spinner.css";
const Spinner = (props: IconProps) => ( const Spinner = (props: { width?: number; height?: number; className?: string }) => (
<svg <svg
width={props.width ?? 20} width={props.width ?? 20}
height={props.height ?? 20} height={props.height ?? 20}

View File

@ -53,23 +53,6 @@
max-height: 300px; /* Cap images in notifications to 300px height */ max-height: 300px; /* Cap images in notifications to 300px height */
} }
.summary-icon {
padding: 4px;
border-radius: 8px;
display: flex;
align-items: center;
cursor: pointer;
color: var(--gray-light) !important;
}
.summary-icon:not(.active):hover {
background-color: var(--gray-dark);
}
.summary-icon.active {
background: rgba(255, 255, 255, 0.1);
}
.summary-tooltip { .summary-tooltip {
display: flex; display: flex;
gap: 12px; gap: 12px;

View File

@ -23,6 +23,7 @@ import ProfilePreview from "Element/User/ProfilePreview";
import { getDisplayName } from "Element/User/DisplayName"; import { getDisplayName } from "Element/User/DisplayName";
import { Day } from "Const"; import { Day } from "Const";
import Tabs, { Tab } from "Element/Tabs"; import Tabs, { Tab } from "Element/Tabs";
import classNames from "classnames";
function notificationContext(ev: TaggedNostrEvent) { function notificationContext(ev: TaggedNostrEvent) {
switch (ev.kind) { switch (ev.kind) {
@ -196,9 +197,12 @@ function NotificationSummary({ evs }: { evs: Array<TaggedNostrEvent> }) {
const filterIcon = (f: NotificationSummaryFilter, icon: string, iconActiveClass?: string) => { const filterIcon = (f: NotificationSummaryFilter, icon: string, iconActiveClass?: string) => {
const active = hasFlag(filter, f); const active = hasFlag(filter, f);
return ( return (
<div className={`summary-icon${active ? " active" : ""}`} onClick={() => setFilter(v => v ^ f)}> <button
type="button"
className={classNames("icon-sm transparent", { active: active })}
onClick={() => setFilter(v => v ^ f)}>
<Icon name={icon} className={active ? iconActiveClass : undefined} /> <Icon name={icon} className={active ? iconActiveClass : undefined} />
</div> </button>
); );
}; };

View File

@ -305,10 +305,8 @@ export default function ProfilePage({ id: propId }: ProfilePageProps) {
const link = encodeTLV(NostrPrefix.Profile, id); const link = encodeTLV(NostrPrefix.Profile, id);
return ( return (
<div className="icon-actions"> <>
<IconButton onClick={() => setShowProfileQr(true)}> <IconButton onClick={() => setShowProfileQr(true)} icon={{ name: "qr", size: 16 }} />
<Icon name="qr" size={16} />
</IconButton>
{showProfileQr && ( {showProfileQr && (
<Modal id="profile-qr" className="qr-modal" onClose={() => setShowProfileQr(false)}> <Modal id="profile-qr" className="qr-modal" onClose={() => setShowProfileQr(false)}>
<ProfileImage pubkey={id} /> <ProfileImage pubkey={id} />
@ -324,11 +322,7 @@ export default function ProfilePage({ id: propId }: ProfilePageProps) {
</> </>
) : ( ) : (
<> <>
{lnurl && ( {lnurl && <IconButton onClick={() => setShowLnQr(true)} icon={{ name: "zap", size: 16 }} />}
<IconButton onClick={() => setShowLnQr(true)}>
<Icon name="zap" size={16} />
</IconButton>
)}
{loginPubKey && !login.readonly && ( {loginPubKey && !login.readonly && (
<> <>
<IconButton <IconButton
@ -340,14 +334,14 @@ export default function ProfilePage({ id: propId }: ProfilePageProps) {
value: id, value: id,
})}`, })}`,
) )
}> }
<Icon name="envelope" size={16} /> icon={{ name: "envelope", size: 16 }}
</IconButton> />
</> </>
)} )}
</> </>
)} )}
</div> </>
); );
} }

View File

@ -324,6 +324,23 @@ button.icon:hover {
color: var(--highlight); color: var(--highlight);
} }
button.icon-sm {
padding: 4px;
border-radius: 8px;
display: flex;
align-items: center;
cursor: pointer;
color: var(--gray-light) !important;
}
button.icon-sm:not(.active):hover {
background-color: var(--gray-dark);
}
button.icon-sm.active {
background: rgba(255, 255, 255, 0.1);
}
.btn { .btn {
padding: 10px; padding: 10px;
border-radius: 5px; border-radius: 5px;

View File

@ -2720,6 +2720,7 @@ __metadata:
"@webpack-cli/generators": ^3.0.4 "@webpack-cli/generators": ^3.0.4
"@webscopeio/react-textarea-autocomplete": ^4.9.2 "@webscopeio/react-textarea-autocomplete": ^4.9.2
babel-loader: ^9.1.3 babel-loader: ^9.1.3
classnames: ^2.3.2
config: ^3.3.9 config: ^3.3.9
copy-webpack-plugin: ^11.0.0 copy-webpack-plugin: ^11.0.0
css-loader: ^6.7.3 css-loader: ^6.7.3
@ -5114,7 +5115,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"classnames@npm:^2.2.5": "classnames@npm:^2.2.5, classnames@npm:^2.3.2":
version: 2.3.2 version: 2.3.2
resolution: "classnames@npm:2.3.2" resolution: "classnames@npm:2.3.2"
checksum: 2c62199789618d95545c872787137262e741f9db13328e216b093eea91c85ef2bfb152c1f9e63027204e2559a006a92eb74147d46c800a9f96297ae1d9f96f4e checksum: 2c62199789618d95545c872787137262e741f9db13328e216b093eea91c85ef2bfb152c1f9e63027204e2559a006a92eb74147d46c800a9f96297ae1d9f96f4e