diff --git a/.changeset/beige-cycles-spend.md b/.changeset/beige-cycles-spend.md new file mode 100644 index 0000000000..1284744fa5 --- /dev/null +++ b/.changeset/beige-cycles-spend.md @@ -0,0 +1,6 @@ +--- +"@stackoverflow/stacks": minor +"@stackoverflow/stacks-svelte": minor +--- + +Update User card to new SHINE design (part 1) \ No newline at end of file diff --git a/packages/stacks-classic/lib/components/user-card/user-card.less b/packages/stacks-classic/lib/components/user-card/user-card.less index 2238c53c4f..d42b16f8ed 100644 --- a/packages/stacks-classic/lib/components/user-card/user-card.less +++ b/packages/stacks-classic/lib/components/user-card/user-card.less @@ -2,12 +2,12 @@ --_uc-ai: center; --_uc-bg: unset; --_uc-bar: unset; - --_uc-d: grid; + --_uc-d: flex; --_uc-fc: unset; - --_uc-g: var(--su4) var(--su8); + --_uc-g: var(--su4); --_uc-p: var(--su8); - --_uc-info-ai: unset; - --_uc-info-fd: column; + --_uc-info-ai: center; + --_uc-info-fd: row; --_uc-link-fs: var(--fs-caption); --_uc-link-ws: unset; --_uc-rep-fc: unset; @@ -63,7 +63,13 @@ .list-reset; align-items: center; display: flex; - gap: var(--su6); + gap: var(--su4); + + // When li contains only s-bling__silver or s-bling__gold (no text content), reduce gap to 2px + li:has(> .s-bling__silver:only-child), + li:has(> .s-bling__gold:only-child) { + margin-right: calc(var(--su2) - var(--su4)); + } } & &--info { @@ -82,12 +88,16 @@ flex-wrap: wrap; min-width: 0; // Allow things to wrap overflow-wrap: break-word; + + // Reduce gap between adjacent s-badge elements from 6px to 4px + .s-badge:has(+ .s-badge) { + margin-right: calc(var(--su4) - var(--su6)); + } } & &--rep { color: var(--_uc-rep-fc); - - font-weight: 700; + font-weight: 600; } & &--tags { @@ -101,8 +111,6 @@ white-space: var(--_uc-time-ws); font-size: var(--fs-caption); - grid-column: ~"1 / 3"; - grid-row: ~"1 / 2"; } & &--type { @@ -121,9 +129,8 @@ border-radius: var(--_uc-bar); color: var(--_uc-fc); display: var(--_uc-d); + flex-direction: row; gap: var(--_uc-g); - - grid-template-columns: auto 1fr; line-height: 1; padding: var(--_uc-p); } diff --git a/packages/stacks-docs/_data/site-navigation.json b/packages/stacks-docs/_data/site-navigation.json index 8390db23bf..7494771f3b 100644 --- a/packages/stacks-docs/_data/site-navigation.json +++ b/packages/stacks-docs/_data/site-navigation.json @@ -354,7 +354,8 @@ }, { "title": "User cards", - "url": "/product/components/user-cards/" + "url": "/product/components/user-cards/", + "new": true } ] } diff --git a/packages/stacks-docs/_data/user-cards-component.json b/packages/stacks-docs/_data/user-cards-component.json index d9dcc52bfc..82698fa380 100644 --- a/packages/stacks-docs/_data/user-cards-component.json +++ b/packages/stacks-docs/_data/user-cards-component.json @@ -25,11 +25,6 @@ "applies": "N/A", "description": "Styles the link to the user’s profile appropriately." }, - { - "class": ".s-user-card--type", - "applies": "N/A", - "description": "An optional container for displaying various user types." - }, { "class": ".s-badge .s-badge__sm .s-badge__admin", "applies": "Child of .s-user-card--link", @@ -45,16 +40,6 @@ "applies": "Child of .s-user-card--link", "description": "Wraps and positions the staff user badge" }, - { - "class": ".s-user-card--location", - "applies": "N/A", - "description": "Styles the user’s location." - }, - { - "class": ".s-user-card--role", - "applies": "N/A", - "description": "Styles the user’s role." - }, { "class": ".s-user-card--awards", "applies": "N/A", @@ -65,35 +50,10 @@ "applies": "N/A", "description": "Styles the aggregate number of awards and activity properly." }, - { - "class": ".s-user-card--tags", - "applies": "N/A", - "description": "A container for a user’s most popular tags." - }, - { - "class": ".s-user-card__highlighted", - "applies": ".s-user-card", - "description": "Highlights the entire user card by adding a background color, some padding, and rounded corners." - }, - { - "class": ".s-user-card__full", - "applies": ".s-user-card", - "description": "Displays a larger avatar, best paired with additional tags meta data." - }, { "class": ".s-user-card__small", "applies": ".s-user-card", "description": "Pairs a small avatar with the reputation and awards." - }, - { - "class": ".s-user-card__minimal", - "applies": ".s-user-card", - "description": "Pairs a stripped down version of the reputation with a small avatar, and the time since the activity occurred." - }, - { - "class": ".s-user-card__deleted", - "applies": ".s-user-card", - "description": "When a user is deleted, we still need to show their name, but we strip the meta data" } ] } \ No newline at end of file diff --git a/packages/stacks-docs/product/components/user-cards.html b/packages/stacks-docs/product/components/user-cards.html index c5bb1ccf11..b2588d6313 100644 --- a/packages/stacks-docs/product/components/user-cards.html +++ b/packages/stacks-docs/product/components/user-cards.html @@ -31,568 +31,702 @@
{% header "h2", "Examples" %} {% header "h3", "Base" %} +

The Base style is the standard variant used to connect a user to their content, appearing most frequently in post-summary lists and on question pages. This view is flexible, allowing various metadata fields to be shown or hidden as needed.

+ {% tip, "info", "mb24" %} + Note on timestamps: Hovering over the timestamp displays a popover with precise dates and a link to the post's /timeline. For authors, this shows the post creation date; for editors, it shows the last modification date. + {% endtip %}
{% highlight html %}
- - +
- -
    -
  • -
  • -
  • -
  • -
+ +
+
+
    +
  • + + + + … +
  • +
  • + + + + … +
  • +
  • + + + + … +
  • +
  • + + + + … +
  • +
+
{% endhighlight %}
- - + demo avatar
- Paul Wright -
    -
  • 2,500
  • -
  • - - gold bling - - 5 -
  • -
  • - - silver bling - - 9 -
  • -
  • - - bronze bling - - 1 -
  • -
+ +
SofiaAlc
+
+
-
- - +
+ demo avatar
- -
Paul Wright
-
Mod
+
+
SofiaAlc
-
    -
  • 2,500
  • -
  • - - gold bling - - 5 -
  • -
  • - - silver bling - - 9 -
  • -
  • - - bronze bling - - 1 -
  • -
+
    +
  • + + reputation bling + + 1,775
  • +
+
-
-
-
-
- {% header "h3", "Highlighted" %} -

Highlights the entire user card by adding a background color, some padding, and rounded corners. Used for post authors and edits.

-
-{% highlight html %} -
- - - - -
- -
    -
  • -
  • -
  • -
  • -
-
- - -
- @Svg.StarVerifiedSm Partner -
-
-{% endhighlight %} -
-
- - +
+ demo avatar
- Paul Wright -
    -
  • 2,500
  • -
  • - - gold bling - - 5 -
  • -
  • - - silver bling - - 9 -
  • -
  • - - bronze bling - - 1 -
  • -
-
-
- -
-
-
- - - demo avatar - -
- Paul Wright -
    -
  • 2,500
  • -
  • - - gold bling - - 5 -
  • -
  • - - silver bling - - 9 -
  • -
  • - - bronze bling - - 1 -
  • -
-
-
- {% icon "StarVerifiedSm" %} Recognized by Hum -
-
-
- -
-
- - - demo avatar - -
- Paul Wright -
    -
  • 2,500
  • -
  • - - gold bling - - 5 -
  • -
  • - - silver bling - - 9 -
  • -
  • - - bronze bling - - 1 -
  • -
-
-
- {% icon "StarVerifiedSm" %} Recognized by Hum -
-
+ +
SofiaAlc
+
-
-
-
-
- -
- {% header "h3", "Full" %} -

Displays a larger avatar, best paired with additional tags meta data. Currently used in user directories.

-
-{% highlight html %} -
- - - -
- -
-
-
-
    -
  • -
  • -
  • -
  • -
-
-
- - - -
-
- - -
- @Svg.StarVerifiedSm Recognized by -
-
-{% endhighlight %} -
-
- - demo avatar - -
- -
Paul Wright
-
Staff
-
Admin
-
    -
  • 2,500
  • +
  • + + reputation bling + + 1,775 +
  • gold bling - 5
  • silver bling - 9
  • bronze bling - 1
-
Paris, France
-
- js - node.js - graphQL -
+
-
-
- - demo avatar - -
- -
Nick Craver
-
Mod
+
+ + demo avatar +
    -
  • 17k
  • +
  • + + reputation bling + + 1,775 +
  • gold bling - 108 + 12
  • silver bling - 265 + 8
  • bronze bling - 812 + 4
-
Architecture Lead
-
North Carolina
-
- .net - sre -
+
-
-
- - demo avatar - -
- - Aaron Shekey +
+ + demo avatar -
    -
  • 768
  • -
  • - - silver bling - - 9 -
  • -
  • - - bronze bling - - 1 -
  • -
-
Senior Product Designer
-
Minneapolis, MN
-
- css - js - html +
+ +
SofiaAlc
+
Mod
+
+
    +
  • + + reputation bling + + 1,775 +
  • +
  • + + gold bling + + 12 +
  • +
  • + + silver bling + + 8 +
  • +
  • + + bronze bling + + 4 +
  • +
-
-
- -
- - demo avatar - -
- - Aaron Shekey - -
    -
  • 768
  • -
  • - - silver bling - - 9 -
  • -
  • - - bronze bling - - 1 -
  • -
-
Minneapolis, MN
-
-
- {% icon "StarVerifiedSm" %} Recognized by Hum +
- {% header "h3", "Small" %} + {% header "h3", "with user badges" %} +

Adds the User badge indicator to the usercard. Use this to signify the official role, status, or origin of the account (such as Moderator, Staff, or Bot) directly alongside the user's name.

{% highlight html %} -
+
+ + + + + +
+ +
+ + + + + +
+ + + +
+ + + +
+ +
+
+
    -
  • -
  • -
  • -
  • +
  • + + + + … +
  • +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
+ +
+ +
+ + + +
+ +
+
+
+
    +
  • + + + + … +
  • +
  • + + + + … +
  • +
  • + + + + … +
  • +
  • + + + + … +
  • +
+
+
{% endhighlight %}
-
+
+ + demo avatar + + + +
+ +
+ + demo avatar + + + +
+ +
demo avatar + +
+ +
+ + demo avatar + +
+ +
SofiaAlc
+
Mod
+
    -
  • 2,500
  • +
  • + + reputation bling + + 1,775 +
  • gold bling - 5
  • silver bling - 9
  • bronze bling - 1
+ +
+ +
+ + demo avatar + +
+ +
SofiaAlc
+
Staff
+
+
    +
  • + + reputation bling + + 1,775 +
  • +
  • + + gold bling + + 12 +
  • +
  • + + silver bling + + 8 +
  • +
  • + + bronze bling + + 4 +
  • +
+
+
- {% header "h3", "Minimal" %} -

- Minimal user cards reduce the amount of information down to who asked the question, how much rep they have, and when the action was taken. To read as plainly as possible, e.g. “Paul Wright modified 3 minutes ago,” we change the positioning of the time, and remove any reputation bling, leaving just the aggregated reputation. -

+ {% header "h3", "Sizes" %} + {% header "h4", "Small" %} +

Use the Small variant for space-constrained areas, such as post summaries, or to establish visual hierarchy for secondary content like comments and replies.

{% highlight html %} -
+
+ + +
- -
    -
  • -
+ +
+
- +
-
+
- -
    -
  • -
+ +
+
+
+
    +
  • + + + + … +
  • +
  • + + + + … +
  • +
  • + + + + … +
  • +
  • + + + + … +
  • +
+ +
+ +
+ + + + - +
{% endhighlight %}
-
- - demo avatar - -
- - Paul Wright +
+
+ + demo avatar -
    -
  • 2,500
  • -
+ +
- -
-
-
- - Paul Wright +
+ + demo avatar +
    -
  • 2,500
  • +
  • + + reputation bling + + 1,775 +
  • +
  • + + gold bling + + 12 +
  • +
  • + + silver bling + + 8 +
  • +
  • + + bronze bling + + 4 +
+ +
+ +
+ + demo avatar + + +
-
-
- -
- {% header "h3", "Deleted" %} -

When a user is deleted, we still need to show their name, but we strip the meta data.

+ {% header "h4", "Default" %} +

Use the Default variant when the user needs a more primary focus of the content. This style features a larger avatar to establish top-level hierarchy like question and answer authors.

{% highlight html %} -
- -
+
+ + + -
+
-
-
+
+ -
+
- +
    +
  • + + + … -
+ +
  • + + + + … +
  • +
  • + + + + … +
  • +
  • + + + + … +
  • + + +
    + +
    + + + + - +
    {% endhighlight %}
    -
    - -
    - demo avatar -
    +
    + + demo avatar +
    - + +
    SofiaAlc
    +
    +
    -
    -
    - demo avatar +
    + + demo avatar + + +
      +
    • + + reputation bling + + 1,775 +
    • +
    • + + gold bling + + 12 +
    • +
    • + + silver bling + + 8 +
    • +
    • + + bronze bling + + 4 +
    • +
    + +
    + +
    + + demo avatar +
    - + +
    SofiaAlc
    +
    Mod
    +
    - +
    -
    + \ No newline at end of file diff --git a/packages/stacks-svelte/src/components/PostSummary/PostSummary.svelte b/packages/stacks-svelte/src/components/PostSummary/PostSummary.svelte index 3bc507526a..8fabf4f487 100644 --- a/packages/stacks-svelte/src/components/PostSummary/PostSummary.svelte +++ b/packages/stacks-svelte/src/components/PostSummary/PostSummary.svelte @@ -330,7 +330,7 @@ {/if} {i18nViewAnswersText} import { defineMeta } from "@storybook/addon-svelte-csf"; - import { createRawSnippet } from "svelte"; - import Tag from "../Tag/Tag.svelte"; import UserCard, { type Size } from "./UserCard.svelte"; - import Icon from "../Icon/Icon.svelte"; - import { IconStarVerifiedSm } from "@stackoverflow/stacks-icons-legacy/icons"; - const createSnippet = (markup = "") => - createRawSnippet(() => ({ - render: () => markup, - })); - - const UserCardSizes: (Size | undefined)[] = [ - undefined, - "full", - "small", - "minimal", - ]; + const UserCardSizes: (Size | undefined)[] = [undefined, "small"]; const baseArgs = { avatar: "https://picsum.photos/128", href: "#", - name: "Josephine Doe", - timestamp: "asked 2 hours ago", - reputation: "1,226", + name: "SofiaAlc", + timestamp: "asked 2 hr ago", + reputation: "1,775", gold: 12, - silver: 23, - bronze: 86, + silver: 8, + bronze: 4, }; const { Story } = defineMeta({ @@ -37,34 +23,13 @@ control: "select", options: UserCardSizes, }, - tags: { - control: "select", - options: ["no tags", "tags"], - mapping: { - "no tags": "", - "tags": 'javascript', - }, - }, - type: { - control: "select", - options: ["no type", "type"], - mapping: { - "no type": "", - "type": `${IconStarVerifiedSm} Recognized by Hum`, - }, - }, }, }); {#snippet template(args)} - {@const { tags, type, ...restArgs } = args} - + {/snippet} @@ -105,53 +70,6 @@ - -
    - -
    -
    - - -
    - -
    -
    - - -
    - - - - - - - - - - {#each ["role", "location", "both"] as prop (prop)} - - - - - {/each} - -
    PropertyExample
    - {prop} - - -
    -
    -
    -
    @@ -179,25 +97,3 @@
    - - -
    - - {#snippet tags()} - css - reactjs - javascript - {/snippet} - -
    -
    - - -
    - - {#snippet type()} - Recognized by Hum - {/snippet} - -
    -
    diff --git a/packages/stacks-svelte/src/components/UserCard/UserCard.svelte b/packages/stacks-svelte/src/components/UserCard/UserCard.svelte index eb11b1f7e7..b6e4dd9e34 100644 --- a/packages/stacks-svelte/src/components/UserCard/UserCard.svelte +++ b/packages/stacks-svelte/src/components/UserCard/UserCard.svelte @@ -1,14 +1,11 @@
    - {#if timestamp && size !== "minimal" && size !== "small"} - - {/if} - - {#if deleted} - - {:else} - - {/if} - -
    + + +
    {#if name} - {name} - {#if !deleted && moderator} -
    +
    {name}
    + {#if moderator} +
    Mod
    {/if} - {#if !deleted && staff} -
    Staff
    + {#if staff} +
    + Staff +
    {/if} - {#if !deleted && admin} -
    Admin
    + {#if admin} +
    + Admin +
    {/if} {/if} - - {#if !deleted && (reputation || gold || silver || bronze)} -
      - {#if reputation} -
    • {reputation}
    • - {/if} - {#if gold} - - {gold} - {/if} - {#if silver} - - {silver} - {/if} - {#if bronze} - - {bronze} - {/if} -
    - {/if} - - {#if !deleted && role} -
    {role}
    - {/if} - - {#if !deleted && location} -
    {location}
    - {/if} - - {#if timestamp && (size === "minimal" || size === "small")} - - {/if} - - {#if !deleted && tags} -
    - {@render tags()} -
    - {/if}
    - {#if !deleted && type} -
    - {@render type()} -
    + {#if reputation || gold || silver || bronze} +
      + {#if reputation} +
    • + + {reputation} +
    • + {/if} + {#if gold} +
    • + + {gold} gold awards +
    • + {/if} + {#if silver} +
    • + + {silver} silver awards +
    • + {/if} + {#if bronze} +
    • + + {bronze} bronze awards +
    • + {/if} +
    + {/if} + + {#if timestamp} + {/if}
    diff --git a/packages/stacks-svelte/src/components/UserCard/UserCard.test.ts b/packages/stacks-svelte/src/components/UserCard/UserCard.test.ts index 57ab8c3c92..e21ecc4158 100644 --- a/packages/stacks-svelte/src/components/UserCard/UserCard.test.ts +++ b/packages/stacks-svelte/src/components/UserCard/UserCard.test.ts @@ -1,17 +1,8 @@ -import { createRawSnippet } from "svelte"; import { expect } from "@open-wc/testing"; import { render, screen } from "@testing-library/svelte"; import UserCard from "./UserCard.svelte"; -const tags = createRawSnippet(() => ({ - render: () => 'JavaScript', -})); - -const type = createRawSnippet(() => ({ - render: () => "Recognized by Hum", -})); - describe("UserCard", () => { it("should render the user name", () => { render(UserCard, { @@ -31,24 +22,6 @@ describe("UserCard", () => { expect(avatarImg).to.have.attr("src", "https://picsum.photos/128"); }); - it("should render the user role", () => { - render(UserCard, { - name: "John Doe", - avatar: "https://picsum.photos/128", - role: "Developer", - }); - expect(screen.getByText("Developer")).to.exist; - }); - - it("should render the user location", () => { - render(UserCard, { - name: "John Doe", - avatar: "https://picsum.photos/128", - location: "New York", - }); - expect(screen.getByText("New York")).to.exist; - }); - it("should render the user reputation", () => { render(UserCard, { name: "John Doe", @@ -85,87 +58,11 @@ describe("UserCard", () => { render(UserCard, { name: "John Doe", avatar: "https://picsum.photos/128", - size: "full", + size: "small", }); expect( screen.getAllByText("John Doe")[1].closest(".s-user-card") - ).to.have.class("s-user-card__full"); - }); - - it("should render the user card with the highlighted class", () => { - render(UserCard, { - name: "John Doe", - avatar: "https://picsum.photos/128", - highlighted: true, - }); - expect( - screen.getAllByText("John Doe")[1].closest(".s-user-card") - ).to.have.class("s-user-card__highlighted"); - }); - - it("should render the user card with the deleted class", () => { - render(UserCard, { - name: "John Doe", - avatar: "https://picsum.photos/128", - deleted: true, - }); - expect( - screen.getAllByText("John Doe")[1].closest(".s-user-card") - ).to.have.class("s-user-card__deleted"); - }); - - it("should render the Icon UserFill instead of Avatar and name unlinked when deleted prop is true", () => { - render(UserCard, { - name: "John Doe", - avatar: "https://picsum.photos/128", - deleted: true, - href: "#", - }); - const avatarImg = screen - .getByTitle("John Doe") - .closest(".s-user-card--avatar"); - expect(avatarImg).to.have.class("IconUserFill"); - }); - - it("should not render many properties when the deleted prop is true", () => { - render(UserCard, { - name: "John Doe", - admin: true, - avatar: "https://picsum.photos/128", - bronze: 3333, - gold: 1111, - href: "#", - location: "Calgary", - moderator: true, - reputation: 1234, - role: "Staff Engineer", - silver: 2222, - staff: true, - timestamp: "Asked 2 hours ago", - deleted: true, - size: "full", - type, - tags, - }); - // Deleted user card should still render these properties - expect(screen.getByTitle("John Doe")).to.exist; // name - expect(screen.getByText("Asked 2 hours ago")).to.exist; // timestamp - - // Deleted user card should not render these properties - expect(screen.queryByText("Admin")).not.to.exist; // admin badge - expect(screen.queryByText("Mod")).not.to.exist; // moderator badge - expect(screen.queryByText("Staff")).not.to.exist; // staff badge - - expect(screen.queryByText("1111")).not.to.exist; // gold - expect(screen.queryByText("2222")).not.to.exist; // silver - expect(screen.queryByText("3333")).not.to.exist; // bronze - - expect(screen.queryByText("1234")).not.to.exist; // reputation - expect(screen.queryByText("Calgary")).not.to.exist; // location - expect(screen.queryByText("Staff Engineer")).not.to.exist; // role - - expect(screen.queryByText("Recognized by Hum")).not.to.exist; // type snippet text - expect(screen.queryByText("JavaScript")).not.to.exist; // tags snippet text + ).to.have.class("s-user-card__small"); }); it("should render the avatar and name as links", () => { @@ -177,7 +74,7 @@ describe("UserCard", () => { const avatarImg = screen.getByRole("presentation").parentElement; const name = screen.getAllByText("John Doe")[1]; expect(avatarImg).to.have.attr("href", "#"); - expect(name).to.have.attr("href", "#"); + expect(name.parentElement).to.have.attr("href", "#"); }); it("should render the user card with arbitrary classes", () => { @@ -203,27 +100,4 @@ describe("UserCard", () => { expect(screen.getByText("Mod")).to.exist; expect(screen.getByText("Staff")).to.exist; }); - - it("should render the user card with the tags slot within appropriate container", () => { - render(UserCard, { - name: "John Doe", - avatar: "https://picsum.photos/128", - tags, - }); - - expect(screen.getByText("JavaScript").closest(".s-user-card--tags")).to - .exist; - }); - - it("should render the user card with the type slot within appropriate container", () => { - render(UserCard, { - name: "John Doe", - avatar: "https://picsum.photos/128", - type, - }); - - expect( - screen.getByText("Recognized by Hum").closest(".s-user-card--type") - ).to.exist; - }); });