banners: Add new redesigned banner component.

This commit adds the redesigned banner component to the codebase along
with a storybook-style page in /devtools/banners to view and test the
redesigned banner component.

Any banner using the new redesigned styles, requires two classes,
- First, the base `banner` class which defines the structure and
    behavior of the banner.
- Second, a modifier class like `banner-info` which defines the styles
    for the particular banner type.

The navbar alert banners also have a custom class `navbar-alert-banner`
which is used to define the specific style and structure for these
banner types.

This commit also makes the `banner`, `action-button` and `icon-button`
components into handlebar templates to maintain consistency in their
usage in the codebase.
This commit is contained in:
Sayam Samal
2024-12-31 13:18:20 +05:30
committed by Tim Abbott
parent c406060bb2
commit 6dabfa02cb
16 changed files with 1155 additions and 36 deletions

View File

@@ -0,0 +1,211 @@
{% extends "zerver/base.html" %}
{% set entrypoint = "dev-testing" %}
{% block title %}
<title>{{ doc_root_title }} | Zulip Dev</title>
{% endblock %}
{% block content %}
<div class="portico-container" data-platform="{{ platform }}">
<div class="portico-wrap">
{% include 'zerver/portico-header.html' %}
<div class="app portico-page">
<div class="banner-wrapper" id="dev_navbar_alerts_wrapper"></div>
<div class="app-main portico-page-container">
<div class="design-testing-wrapper">
<div class="banner-wrapper" id="dev_normal_banner_wrapper"></div>
<section class="design-controls-section">
<div class="design-testing-controls-label">Theme Settings</div>
<div class="design-testing-controls">
<div class="design-testing-control">
<span class="control-label">Dark Theme</span>
<div role="group" class="tab-picker control-setting">
<input type="radio" data-theme="dark" id="dev_enable_dark_theme_banners" class="tab-option" name="dark-theme-select"/>
<label role="menuitemradio" class="tab-option-content design-testing-control-element" for="dev_enable_dark_theme_banners" tabindex="0">
<span>Enable</span>
</label>
<input type="radio" data-theme="light" id="dev_disable_dark_theme_banners" class="tab-option" name="dark-theme-select" checked/>
<label role="menuitemradio" class="tab-option-content design-testing-control-element" for="dev_disable_dark_theme_banners" tabindex="0">
<span>Disable</span>
</label>
<span class="slider"></span>
</div>
</div>
<div class="design-testing-control">
<span class="control-label">Select Background</span>
<select class="design-testing-control-element control-setting select_background">
{% for background in background_colors %}
<option value="{{ background.css_var }}" {% if background.css_var == "--color-background" %}selected{% endif %}>{{ background.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="design-testing-controls-label">Banner Settings</div>
<div class="design-testing-controls">
<div class="design-testing-control">
<span class="control-label">Alert Banner Type</span>
<select class="design-testing-control-element control-setting" id="banner_select_type"></select>
</div>
<div class="design-testing-control">
<span class="control-label">Banner Intent</span>
<select class="design-testing-control-element control-setting" id="banner_select_intent"></select>
</div>
<div class="design-testing-control">
<span class="control-label">Banner Label</span>
<input class="design-testing-control-element control-setting" type="text" id="banner_label" />
</div>
<div class="design-testing-control">
<span class="control-label">Banner Close Button</span>
<div role="group" class="tab-picker control-setting">
<input type="radio" id="enable_banner_close_button" class="tab-option" name="banner-close-button-select"/>
<label role="menuitemradio" class="tab-option-content design-testing-control-element" for="enable_banner_close_button" tabindex="0">
<span>Enable</span>
</label>
<input type="radio" id="disable_banner_close_button" class="tab-option" name="banner-close-button-select" checked/>
<label role="menuitemradio" class="tab-option-content design-testing-control-element" for="disable_banner_close_button" tabindex="0">
<span>Disable</span>
</label>
<span class="slider"></span>
</div>
</div>
</div>
<div class="design-testing-controls-label">Primary Button Settings</div>
<div class="design-testing-controls">
<div class="design-testing-control">
<span class="control-label">Button</span>
<div role="group" class="tab-picker control-setting">
<input type="radio" id="enable_primary_button" class="tab-option" name="primary-button-select"/>
<label role="menuitemradio" class="tab-option-content design-testing-control-element" for="enable_primary_button" tabindex="0">
<span>Enable</span>
</label>
<input type="radio" id="disable_primary_button" class="tab-option" name="primary-button-select" checked/>
<label role="menuitemradio" class="tab-option-content design-testing-control-element" for="disable_primary_button" tabindex="0">
<span>Disable</span>
</label>
<span class="slider"></span>
</div>
</div>
<div class="design-testing-control">
<span class="control-label">Button Icon</span>
<div role="group" class="tab-picker control-setting">
<input type="radio" id="enable_primary_button_icon" class="tab-option" name="primary-button-icon-select"/>
<label role="menuitemradio" class="tab-option-content design-testing-control-element" for="enable_primary_button_icon" tabindex="0">
<span>Enable</span>
</label>
<input type="radio" id="disable_primary_button_icon" class="tab-option" name="primary-button-icon-select" checked/>
<label role="menuitemradio" class="tab-option-content design-testing-control-element" for="disable_primary_button_icon" tabindex="0">
<span>Disable</span>
</label>
<span class="slider"></span>
</div>
</div>
<div class="design-testing-control">
<span class="control-label">Select Icon</span>
<select class="design-testing-control-element control-setting" id="primary_button_select_icon">
{% for icon in icons %}
<option value="{{ icon }}" {% if icon == "move-alt" %}selected{% endif %}>{{ icon }}</option>
{% endfor %}
</select>
</div>
<div class="design-testing-control">
<span class="control-label">Button Text</span>
<input class="design-testing-control-element control-setting" type="text" id="primary_button_text" />
</div>
</div>
<div class="design-testing-controls-label">Quiet Button Settings</div>
<div class="design-testing-controls">
<div class="design-testing-control">
<span class="control-label">Button</span>
<div role="group" class="tab-picker control-setting">
<input type="radio" id="enable_quiet_button" class="tab-option" name="quiet-button-select"/>
<label role="menuitemradio" class="tab-option-content design-testing-control-element" for="enable_quiet_button" tabindex="0">
<span>Enable</span>
</label>
<input type="radio" id="disable_quiet_button" class="tab-option" name="quiet-button-select" checked/>
<label role="menuitemradio" class="tab-option-content design-testing-control-element" for="disable_quiet_button" tabindex="0">
<span>Disable</span>
</label>
<span class="slider"></span>
</div>
</div>
<div class="design-testing-control">
<span class="control-label">Button Icon</span>
<div role="group" class="tab-picker control-setting">
<input type="radio" id="enable_quiet_button_icon" class="tab-option" name="quiet-button-icon-select"/>
<label role="menuitemradio" class="tab-option-content design-testing-control-element" for="enable_quiet_button_icon" tabindex="0">
<span>Enable</span>
</label>
<input type="radio" id="disable_quiet_button_icon" class="tab-option" name="quiet-button-icon-select" checked/>
<label role="menuitemradio" class="tab-option-content design-testing-control-element" for="disable_quiet_button_icon" tabindex="0">
<span>Disable</span>
</label>
<span class="slider"></span>
</div>
</div>
<div class="design-testing-control">
<span class="control-label">Select Icon</span>
<select class="design-testing-control-element control-setting" id="quiet_button_select_icon">
{% for icon in icons %}
<option value="{{ icon }}" {% if icon == "move-alt" %}selected{% endif %}>{{ icon }}</option>
{% endfor %}
</select>
</div>
<div class="design-testing-control">
<span class="control-label">Button Text</span>
<input class="design-testing-control-element control-setting" type="text" id="quiet_button_text" />
</div>
</div>
<div class="design-testing-controls-label">Borderless Button Settings</div>
<div class="design-testing-controls">
<div class="design-testing-control">
<span class="control-label">Button</span>
<div role="group" class="tab-picker control-setting">
<input type="radio" id="enable_borderless_button" class="tab-option" name="borderless-button-select"/>
<label role="menuitemradio" class="tab-option-content design-testing-control-element" for="enable_borderless_button" tabindex="0">
<span>Enable</span>
</label>
<input type="radio" id="disable_borderless_button" class="tab-option" name="borderless-button-select" checked/>
<label role="menuitemradio" class="tab-option-content design-testing-control-element" for="disable_borderless_button" tabindex="0">
<span>Disable</span>
</label>
<span class="slider"></span>
</div>
</div>
<div class="design-testing-control">
<span class="control-label">Button Icon</span>
<div role="group" class="tab-picker control-setting">
<input type="radio" id="enable_borderless_button_icon" class="tab-option" name="borderless-button-icon-select"/>
<label role="menuitemradio" class="tab-option-content design-testing-control-element" for="enable_borderless_button_icon" tabindex="0">
<span>Enable</span>
</label>
<input type="radio" id="disable_borderless_button_icon" class="tab-option" name="borderless-button-icon-select" checked/>
<label role="menuitemradio" class="tab-option-content design-testing-control-element" for="disable_borderless_button_icon" tabindex="0">
<span>Disable</span>
</label>
<span class="slider"></span>
</div>
</div>
<div class="design-testing-control">
<span class="control-label">Select Icon</span>
<select class="design-testing-control-element control-setting" id="borderless_button_select_icon">
{% for icon in icons %}
<option value="{{ icon }}" {% if icon == "move-alt" %}selected{% endif %}>{{ icon }}</option>
{% endfor %}
</select>
</div>
<div class="design-testing-control">
<span class="control-label">Button Text</span>
<input class="design-testing-control-element control-setting" type="text" id="borderless_button_text" />
</div>
</div>
</section>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,8 +1,8 @@
{% extends "zerver/base.html" %}
{% set entrypoint = "dev-buttons" %}
{% set entrypoint = "dev-testing" %}
{% block title %}
<title>Button styles browser | Zulip Dev</title>
<title>{{ doc_root_title }} | Zulip Dev</title>
{% endblock %}
{% block content %}
@@ -11,7 +11,6 @@
{% include 'zerver/portico-header.html' %}
<div class="app portico-page">
<div class="app-main portico-page-container">
<div class="design-testing-title">Button styles browser</div>
<div class="design-testing-wrapper">
<div class="dev-buttons-grid">
<div class="dev-buttons-variant-group">
@@ -169,18 +168,18 @@
</div>
</div>
</div>
<section class="action-button-section">
<div class="section-heading">Controls</div>
<section class="design-controls-section">
<div class="design-testing-controls-label">Controls</div>
<div class="design-testing-controls">
<div class="design-testing-control">
<span class="control-label">Dark Theme</span>
<div role="group" class="tab-picker">
<input type="radio" id="enable_dark_theme" class="tab-option" name="dark-theme-select"/>
<label role="menuitemradio" class="tab-option-content design-testing-control-element" for="enable_dark_theme" tabindex="0">
<input type="radio" data-theme="dark" id="dev_enable_dark_theme_buttons" class="tab-option" name="dark-theme-select"/>
<label role="menuitemradio" class="tab-option-content design-testing-control-element" for="dev_enable_dark_theme_buttons" tabindex="0">
<span>Enable</span>
</label>
<input type="radio" id="disable_dark_theme" class="tab-option" name="dark-theme-select" checked/>
<label role="menuitemradio" class="tab-option-content design-testing-control-element" for="disable_dark_theme" tabindex="0">
<input type="radio" data-theme="light" id="dev_disable_dark_theme_buttons" class="tab-option" name="dark-theme-select" checked/>
<label role="menuitemradio" class="tab-option-content design-testing-control-element" for="dev_disable_dark_theme_buttons" tabindex="0">
<span>Disable</span>
</label>
<span class="slider"></span>
@@ -188,7 +187,7 @@
</div>
<div class="design-testing-control">
<span class="control-label">Select Background</span>
<select class="design-testing-control-element" id="button_select_background">
<select class="design-testing-control-element select_background">
{% for background in background_colors %}
<option value="{{ background.css_var }}" {% if background.css_var == "--color-background" %}selected{% endif %}>{{ background.name }}</option>
{% endfor %}

View File

@@ -84,6 +84,11 @@
<td>None needed</td>
<td>Test button styles</td>
</tr>
<tr>
<td><a href="/devtools/banners">/devtools/banners</a></td>
<td>None needed</td>
<td>Test banner styles</td>
</tr>
</tbody>
</table>
<h2>Useful management commands</h2>

View File

@@ -23,6 +23,9 @@
{% if page_is_policy_center %}
<span class="light portico-header-text"> | <a href="{{ root_domain_url }}/policies/">{{ doc_root_title }}</a></span>
{% endif %}
{% if page_is_design_testing %}
<span class="light portico-header-text"> | <a href="{{ root_domain_url }}/devtools/{{ design_component }}">{{ doc_root_title }}</a></span>
{% endif %}
</div>
{% endif %}
</div>

View File

@@ -0,0 +1,11 @@
import "./common.ts";
import "../portico/header.ts";
import "../portico/design-testing.ts";
// Import styles in the required order
import "../../styles/portico/portico_styles.css";
import "../../styles/portico/dev-testing.css";
import "../../styles/app_variables.css";
import "../../styles/buttons.css";
import "../../styles/banners.css";
import "../../styles/app_components.css";

View File

@@ -1,10 +1,299 @@
import Handlebars from "handlebars/runtime.js";
import $ from "jquery";
import {$t} from "../i18n.ts";
import render_banner from "../../templates/components/banner.hbs";
import {$t, $t_html} from "../i18n.ts";
type ComponentIntent = "neutral" | "brand" | "info" | "success" | "warning" | "danger";
type ActionButton = {
type: "primary" | "quiet" | "borderless";
intent: ComponentIntent;
label: string;
icon?: string | undefined;
};
type Banner = {
intent: ComponentIntent;
label: string | Handlebars.SafeString;
buttons: ActionButton[];
close_button: boolean;
custom_classes?: string;
};
type AlertBanner = Banner & {
process: string;
};
const component_intents: ComponentIntent[] = [
"neutral",
"brand",
"info",
"success",
"warning",
"danger",
];
const banner_html = (banner: Banner | AlertBanner): string => render_banner(banner);
const custom_normal_banner: Banner = {
intent: "neutral",
label: "This is a normal banner. Use the controls below to modify this banner.",
buttons: [
{
type: "quiet",
intent: "neutral",
label: "Quiet Button",
},
],
close_button: true,
};
const alert_banners: Record<string, AlertBanner> = {
"custom-banner": {
process: "custom-banner",
intent: "neutral",
label: "This is a navbar alerts banner. Use the controls below to modify this banner.",
buttons: [
{
type: "quiet",
intent: "neutral",
label: "Quiet Button",
},
],
close_button: true,
custom_classes: "navbar-alert-banner",
},
bankruptcy: {
process: "bankruptcy",
intent: "info",
label: "Welcome back! You have 12 unread messages. Do you want to mark them all as read?",
buttons: [
{
type: "quiet",
intent: "info",
label: "Yes, please!",
},
{
type: "borderless",
intent: "info",
label: "No, I'll catch up.",
},
],
close_button: true,
custom_classes: "navbar-alert-banner",
},
"email-server": {
process: "email-server",
intent: "warning",
label: "Zulip needs to send email to confirm users' addresses and send notifications.",
buttons: [
{
type: "quiet",
intent: "warning",
label: "Configuration instructions",
},
],
close_button: true,
custom_classes: "navbar-alert-banner",
},
"demo-organization-deadline": {
process: "demo-organization-deadline",
intent: "info",
label: new Handlebars.SafeString(
$t_html(
{
defaultMessage:
"This <demo_link>demo organization</demo_link> will be automatically deleted in 30 days, unless it's <convert_link>converted into a permanent organization</convert_link>.",
},
{
demo_link: (content_html) =>
`<a class="banner__link" href="https://zulip.com/help/demo-organizations" target="_blank" rel="noopener noreferrer">${content_html.join("")}</a>`,
convert_link: (content_html) =>
`<a class="banner__link" href="https://zulip.com/help/demo-organizations#convert-a-demo-organization-to-a-permanent-organization" target="_blank" rel="noopener noreferrer">${content_html.join("")}</a>`,
},
),
),
buttons: [],
close_button: true,
custom_classes: "navbar-alert-banner",
},
notifications: {
process: "notifications",
intent: "brand",
label: new Handlebars.SafeString(
$t_html(
{
defaultMessage:
"Zulip needs your permission to enable desktop notifications for messages you receive. You can <z-link>customize</z-link> what kinds of messages trigger notifications.",
},
{
"z-link": (content_html) =>
`<a class="banner__link" href="https://zulip.com/help/desktop-notifications#desktop-notifications" target="_blank" rel="noopener noreferrer">${content_html.join("")}</a>`,
},
),
),
buttons: [
{
type: "primary",
intent: "brand",
label: "Enable notifications",
},
{
type: "quiet",
intent: "brand",
label: "Ask me later",
},
{
type: "borderless",
intent: "brand",
label: "Never ask on this computer",
},
],
close_button: true,
custom_classes: "navbar-alert-banner",
},
"profile-missing-required": {
process: "profile-missing-required",
intent: "warning",
label: "Your profile is missing required fields.",
buttons: [
{
type: "quiet",
intent: "warning",
label: "Edit your profile",
},
],
close_button: true,
custom_classes: "navbar-alert-banner",
},
"insecure-desktop-app": {
process: "insecure-desktop-app",
intent: "danger",
label: "You are using an old version of the Zulip desktop app with known security bugs.",
buttons: [
{
type: "quiet",
intent: "danger",
label: "Download the latest version",
},
],
close_button: true,
custom_classes: "navbar-alert-banner",
},
"profile-incomplete": {
process: "profile-incomplete",
intent: "info",
label: "Complete your organization profile, which is displayed on your organization's registration and login pages.",
buttons: [
{
type: "quiet",
intent: "info",
label: "Edit profile",
},
],
close_button: true,
custom_classes: "navbar-alert-banner",
},
"server-needs-upgrade": {
process: "server-needs-upgrade",
intent: "danger",
label: "This Zulip server is running an old version and should be upgraded.",
buttons: [
{
type: "quiet",
intent: "danger",
label: "Learn more",
},
{
type: "borderless",
intent: "danger",
label: "Dismiss for a week",
},
],
close_button: true,
custom_classes: "navbar-alert-banner",
},
};
const sortButtons = (buttons: ActionButton[]): void => {
const sortOrder: Record<ActionButton["type"], number> = {
primary: 1,
quiet: 2,
borderless: 3,
};
buttons.sort((a, b) => sortOrder[a.type] - sortOrder[b.type]);
};
const update_buttons = (buttons: ActionButton[]): void => {
const primary_button = buttons.find((button) => button.type === "primary");
if (primary_button) {
$("#enable_primary_button").prop("checked", true);
$("#primary_button_text").val(primary_button.label);
if (primary_button.icon) {
$("#primary_button_select_icon").val(primary_button.icon);
$("#enable_primary_button_icon").prop("checked", true);
} else {
$("#disable_primary_button_icon").prop("checked", true);
}
} else {
$("#disable_primary_button").prop("checked", true);
$("#primary_button_text").val("");
$("#disable_primary_button_icon").prop("checked", true);
}
const quiet_button = buttons.find((button) => button.type === "quiet");
if (quiet_button) {
$("#enable_quiet_button").prop("checked", true);
$("#quiet_button_text").val(quiet_button.label);
if (quiet_button.icon) {
$("#quiet_button_select_icon").val(quiet_button.icon);
$("#enable_quiet_button_icon").prop("checked", true);
} else {
$("#disable_quiet_button_icon").prop("checked", true);
}
} else {
$("#disable_quiet_button").prop("checked", true);
$("#quiet_button_text").val("");
$("#disable_quiet_button_icon").prop("checked", true);
}
const borderless_button = buttons.find((button) => button.type === "borderless");
if (borderless_button) {
$("#enable_borderless_button").prop("checked", true);
$("#borderless_button_text").val(borderless_button.label);
if (borderless_button.icon) {
$("#borderless_button_select_icon").val(borderless_button.icon);
$("#enable_borderless_button_icon").prop("checked", true);
} else {
$("#disable_borderless_button_icon").prop("checked", true);
}
} else {
$("#disable_borderless_button").prop("checked", true);
$("#borderless_button_text").val("");
$("#disable_borderless_button_icon").prop("checked", true);
}
};
function update_banner(): void {
$("#dev_navbar_alerts_wrapper").html(banner_html(current_banner));
$("#dev_normal_banner_wrapper").html(banner_html(custom_normal_banner));
$("#banner_select_intent").val(current_banner.intent);
$("#banner_label").val(current_banner.label.toString());
update_buttons(current_banner.buttons);
if (current_banner.close_button) {
$("#enable_banner_close_button").prop("checked", true);
} else {
$("#disable_banner_close_button").prop("checked", true);
}
}
let current_banner = alert_banners["custom-banner"]!;
$(window).on("load", () => {
// Code for /devtools/buttons design testing page.
$("input[name='dark-theme-select']").on("change", (e) => {
if ($(e.target).attr("id") === "enable_dark_theme") {
if ($(e.target).data("theme") === "dark") {
$(":root").addClass("dark-theme");
} else {
$(":root").removeClass("dark-theme");
@@ -38,8 +327,314 @@ $(window).on("load", () => {
);
});
$("#button_select_background").on("change", function (this: HTMLElement) {
$(".select_background").on("change", function (this: HTMLElement) {
const background_var = $(this).val()?.toString() ?? "";
$("body").css("background-color", `var(${background_var})`);
});
// Code for /devtools/banners design testing page.
update_banner();
// Populate banner type select options
const $banner_select = $("#banner_select_type");
for (const key of Object.keys(alert_banners)) {
$banner_select.append($("<option>").val(key).text(key));
}
const $banner_intent_select = $("#banner_select_intent");
for (const intent of component_intents) {
$banner_intent_select.append($("<option>").val(intent).text(intent));
}
$("#banner_select_intent").on("change", function (this: HTMLElement) {
const selected_intent = $(this).val()?.toString();
if (selected_intent === undefined) {
return;
}
current_banner.intent =
component_intents.find((intent) => intent === selected_intent) ?? "neutral";
for (const button of current_banner.buttons) {
button.intent = current_banner.intent;
}
if (current_banner.process === "custom-banner") {
custom_normal_banner.intent = current_banner.intent;
for (const button of custom_normal_banner.buttons) {
button.intent = custom_normal_banner.intent;
}
$("#dev_normal_banner_wrapper").html(banner_html(custom_normal_banner));
}
$("#dev_navbar_alerts_wrapper").html(banner_html(current_banner));
});
$banner_select.on("change", function (this: HTMLElement) {
const banner_type = $(this).val()?.toString();
if (banner_type === undefined) {
return;
}
current_banner = alert_banners[banner_type]!;
update_banner();
});
$("input[name='banner-close-button-select']").on("change", (e) => {
if ($(e.target).attr("id") === "enable_banner_close_button") {
current_banner.close_button = true;
} else {
current_banner.close_button = false;
}
$("#dev_navbar_alerts_wrapper").html(banner_html(current_banner));
if (current_banner.process === "custom-banner") {
custom_normal_banner.close_button = current_banner.close_button;
$("#dev_normal_banner_wrapper").html(banner_html(custom_normal_banner));
}
});
$("#banner_label").on("input", function (this: HTMLElement) {
const banner_label = $(this).val()?.toString() ?? "";
current_banner.label = banner_label;
$("#dev_navbar_alerts_wrapper").html(banner_html(current_banner));
if (current_banner.process === "custom-banner") {
custom_normal_banner.label = banner_label;
$("#dev_normal_banner_wrapper").html(banner_html(custom_normal_banner));
}
});
$("input[name='primary-button-select']").on("change", (e) => {
if ($(e.target).attr("id") === "enable_primary_button") {
if (current_banner.buttons.some((button) => button.type === "primary")) {
return;
}
let label = $("#primary_button_text").val()?.toString();
if (!label) {
label = "Primary Button";
}
const is_icon_enabled = $("#enable_primary_button_icon").prop("checked") === true;
current_banner.buttons.push({
type: "primary",
intent: current_banner.intent,
label,
icon: is_icon_enabled
? $("#primary_button_select_icon").val()?.toString()
: undefined,
});
$("#primary_button_text").val(label);
} else {
current_banner.buttons = current_banner.buttons.filter(
(button) => button.type !== "primary",
);
}
sortButtons(current_banner.buttons);
$("#dev_navbar_alerts_wrapper").html(banner_html(current_banner));
if (current_banner.process === "custom-banner") {
custom_normal_banner.buttons = current_banner.buttons;
$("#dev_normal_banner_wrapper").html(banner_html(custom_normal_banner));
}
});
$("input[name='primary-button-icon-select']").on("change", (e) => {
const primary_button = current_banner.buttons.find((button) => button.type === "primary");
if (primary_button === undefined) {
return;
}
if ($(e.target).attr("id") === "enable_primary_button_icon") {
primary_button.icon = $("#primary_button_select_icon").val()?.toString() ?? "";
} else {
delete primary_button.icon;
}
$("#dev_navbar_alerts_wrapper").html(banner_html(current_banner));
if (current_banner.process === "custom-banner") {
custom_normal_banner.buttons = current_banner.buttons;
$("#dev_normal_banner_wrapper").html(banner_html(custom_normal_banner));
}
});
$("#primary_button_select_icon").on("change", function (this: HTMLElement) {
const primary_button = current_banner.buttons.find((button) => button.type === "primary");
if (primary_button === undefined) {
return;
}
if (!primary_button.icon) {
return;
}
primary_button.icon = $(this).val()?.toString() ?? "";
$("#dev_navbar_alerts_wrapper").html(banner_html(current_banner));
if (current_banner.process === "custom-banner") {
custom_normal_banner.buttons = current_banner.buttons;
$("#dev_normal_banner_wrapper").html(banner_html(custom_normal_banner));
}
});
$("#primary_button_text").on("input", function (this: HTMLElement) {
const primary_button = current_banner.buttons.find((button) => button.type === "primary");
if (primary_button === undefined) {
return;
}
primary_button.label = $(this).val()?.toString() ?? "";
$("#dev_navbar_alerts_wrapper").html(banner_html(current_banner));
if (current_banner.process === "custom-banner") {
custom_normal_banner.buttons = current_banner.buttons;
$("#dev_normal_banner_wrapper").html(banner_html(custom_normal_banner));
}
});
$("input[name='quiet-button-select']").on("change", (e) => {
if ($(e.target).attr("id") === "enable_quiet_button") {
if (current_banner.buttons.some((button) => button.type === "quiet")) {
return;
}
let label = $("#quiet_button_text").val()?.toString();
if (!label) {
label = "Quiet Button";
}
const is_icon_enabled = $("#enable_quiet_button_icon").prop("checked") === true;
current_banner.buttons.push({
type: "quiet",
intent: current_banner.intent,
label,
icon: is_icon_enabled
? $("#quiet_button_select_icon").val()?.toString()
: undefined,
});
$("#quiet_button_text").val(label);
sortButtons(current_banner.buttons);
} else {
current_banner.buttons = current_banner.buttons.filter(
(button) => button.type !== "quiet",
);
}
$("#dev_navbar_alerts_wrapper").html(banner_html(current_banner));
if (current_banner.process === "custom-banner") {
custom_normal_banner.buttons = current_banner.buttons;
$("#dev_normal_banner_wrapper").html(banner_html(custom_normal_banner));
}
});
$("input[name='quiet-button-icon-select']").on("change", (e) => {
const quiet_button = current_banner.buttons.find((button) => button.type === "quiet");
if (quiet_button === undefined) {
return;
}
if ($(e.target).attr("id") === "enable_quiet_button_icon") {
quiet_button.icon = $("#quiet_button_select_icon").val()?.toString() ?? "";
} else {
delete quiet_button.icon;
}
$("#dev_navbar_alerts_wrapper").html(banner_html(current_banner));
if (current_banner.process === "custom-banner") {
custom_normal_banner.buttons = current_banner.buttons;
$("#dev_normal_banner_wrapper").html(banner_html(custom_normal_banner));
}
});
$("#quiet_button_select_icon").on("change", function (this: HTMLElement) {
const quiet_button = current_banner.buttons.find((button) => button.type === "quiet");
if (quiet_button === undefined) {
return;
}
if (!quiet_button.icon) {
return;
}
quiet_button.icon = $(this).val()?.toString() ?? "";
$("#dev_navbar_alerts_wrapper").html(banner_html(current_banner));
if (current_banner.process === "custom-banner") {
custom_normal_banner.buttons = current_banner.buttons;
$("#dev_normal_banner_wrapper").html(banner_html(custom_normal_banner));
}
});
$("#quiet_button_text").on("input", function (this: HTMLElement) {
const quiet_button = current_banner.buttons.find((button) => button.type === "quiet");
if (quiet_button === undefined) {
return;
}
quiet_button.label = $(this).val()?.toString() ?? "";
$("#dev_navbar_alerts_wrapper").html(banner_html(current_banner));
if (current_banner.process === "custom-banner") {
custom_normal_banner.buttons = current_banner.buttons;
$("#dev_normal_banner_wrapper").html(banner_html(custom_normal_banner));
}
});
$("input[name='borderless-button-select']").on("change", function (this: HTMLElement) {
if ($(this).attr("id") === "enable_borderless_button") {
if (current_banner.buttons.some((button) => button.type === "borderless")) {
return;
}
let label = $("#borderless_button_text").val()?.toString();
if (!label) {
label = "Borderless Button";
}
const is_icon_enabled = $("#enable_borderless_button_icon").prop("checked") === true;
current_banner.buttons.push({
type: "borderless",
intent: current_banner.intent,
label,
icon: is_icon_enabled
? $("#borderless_button_select_icon").val()?.toString()
: undefined,
});
$("#borderless_button_text").val(label);
sortButtons(current_banner.buttons);
} else {
current_banner.buttons = current_banner.buttons.filter(
(button) => button.type !== "borderless",
);
}
$("#dev_navbar_alerts_wrapper").html(banner_html(current_banner));
if (current_banner.process === "custom-banner") {
custom_normal_banner.buttons = current_banner.buttons;
$("#dev_normal_banner_wrapper").html(banner_html(custom_normal_banner));
}
});
$("input[name='borderless-button-icon-select']").on("change", function (this: HTMLElement) {
const borderless_button = current_banner.buttons.find(
(button) => button.type === "borderless",
);
if (borderless_button === undefined) {
return;
}
if ($(this).attr("id") === "enable_borderless_button_icon") {
borderless_button.icon = $("#borderless_button_select_icon").val()?.toString() ?? "";
} else {
delete borderless_button.icon;
}
$("#dev_navbar_alerts_wrapper").html(banner_html(current_banner));
if (current_banner.process === "custom-banner") {
custom_normal_banner.buttons = current_banner.buttons;
$("#dev_normal_banner_wrapper").html(banner_html(custom_normal_banner));
}
});
$("#borderless_button_select_icon").on("change", function (this: HTMLElement) {
const borderless_button = current_banner.buttons.find(
(button) => button.type === "borderless",
);
if (borderless_button === undefined) {
return;
}
if (!borderless_button.icon) {
return;
}
borderless_button.icon = $(this).val()?.toString() ?? "";
$("#dev_navbar_alerts_wrapper").html(banner_html(current_banner));
if (current_banner.process === "custom-banner") {
custom_normal_banner.buttons = current_banner.buttons;
$("#dev_normal_banner_wrapper").html(banner_html(custom_normal_banner));
}
});
$("#borderless_button_text").on("input", function (this: HTMLElement) {
const borderless_button = current_banner.buttons.find(
(button) => button.type === "borderless",
);
if (borderless_button === undefined) {
return;
}
borderless_button.label = $(this).val()?.toString() ?? "";
$("#dev_navbar_alerts_wrapper").html(banner_html(current_banner));
if (current_banner.process === "custom-banner") {
custom_normal_banner.buttons = current_banner.buttons;
$("#dev_normal_banner_wrapper").html(banner_html(custom_normal_banner));
}
});
});

View File

@@ -591,6 +591,25 @@
oklch(70% 0.3 0deg) 100%
);
/* Banner grid layout variables */
--banner-horizontal-padding: 13px;
--banner-vertical-padding: 5px;
--banner-grid-template-areas-lg: ". . . . . . banner-close-btn banner-close-btn"
". . banner-label . banner-btns . banner-close-btn banner-close-btn"
". . . . . . banner-close-btn banner-close-btn";
--banner-grid-template-columns-lg: var(--banner-horizontal-padding) 0 auto
minmax(0, 1fr) auto 0 minmax(0, auto) var(--banner-horizontal-padding);
--banner-grid-template-rows-lg: 5px auto 5px;
--banner-grid-template-areas-md: ". . . . banner-close-btn banner-close-btn"
". . banner-label banner-label banner-close-btn banner-close-btn"
". banner-btns banner-btns banner-btns . .";
--banner-grid-template-columns-md: var(--banner-horizontal-padding) 0
minmax(auto, 1fr) 0 minmax(0, auto) var(--banner-horizontal-padding);
--banner-grid-template-rows-md: 5px auto auto 5px;
--banner-grid-template-areas-sm: ". . . . banner-close-btn banner-close-btn"
". . banner-label banner-label banner-close-btn banner-close-btn"
". banner-btns banner-btns banner-btns banner-btns .";
/* Colors used across the app */
--color-date: hsl(0deg 0% 15% / 75%);
--color-background-private-message-header: hsl(46deg 35% 93%);
@@ -1492,6 +1511,57 @@
hsl(359deg 93% 39%) 13%,
transparent
);
/* Banners */
--color-text-link-banner: hsl(210deg 94% 42%);
/* Banners - Neutral Variant */
--color-text-neutral-banner: hsl(229deg 12% 25%);
--color-border-neutral-banner: color-mix(
in oklch,
hsl(240deg 2% 30%) 40%,
transparent
);
--color-background-neutral-banner: hsl(240deg 7% 93%);
/* Banners - Brand Variant */
--color-text-brand-banner: hsl(264deg 95% 34%);
--color-border-brand-banner: color-mix(
in oklch,
hsl(254deg 60% 50%) 40%,
transparent
);
--color-background-brand-banner: hsl(254deg 42% 94%);
/* Banners - Info Variant */
--color-text-info-banner: hsl(241deg 95% 25%);
--color-border-info-banner: color-mix(
in oklch,
hsl(204deg 49% 29%) 40%,
transparent
);
--color-background-info-banner: hsl(204deg 58% 92%);
/* Banners - Success Variant */
--color-text-success-banner: hsl(144deg 88% 16%);
--color-border-success-banner: color-mix(
in oklch,
hsl(147deg 57% 25%) 40%,
transparent
);
--color-background-success-banner: hsl(147deg 43% 92%);
/* Banners - Warning Variant */
--color-text-warning-banner: hsl(34deg 89% 25%);
--color-border-warning-banner: color-mix(
in oklch,
hsl(38deg 44% 27%) 40%,
transparent
);
--color-background-warning-banner: hsl(50deg 75% 92%);
/* Banners - Danger Variant */
--color-text-danger-banner: hsl(359deg 94% 35%);
--color-border-danger-banner: color-mix(
in oklch,
hsl(3deg 57% 33%) 40%,
transparent
);
--color-background-danger-banner: hsl(0deg 35% 92%);
}
%dark-theme {
@@ -2414,6 +2484,56 @@
hsl(5deg 88% 60%) 17%,
transparent
);
/* Banners */
/* Banners - Neutral Variant */
--color-text-neutral-banner: hsl(231deg 11% 76%);
--color-border-neutral-banner: color-mix(
in oklch,
hsl(240deg 7% 66%) 40%,
transparent
);
--color-background-neutral-banner: hsl(240deg 7% 17%);
/* Banners - Brand Variant */
--color-text-brand-banner: hsl(244deg 96% 82%);
--color-border-brand-banner: color-mix(
in oklch,
hsl(253deg 70% 89%) 40%,
transparent
);
--color-background-brand-banner: hsl(254deg 49% 16%);
/* Banners - Info Variant */
--color-text-info-banner: hsl(221deg 93% 89%);
--color-border-info-banner: color-mix(
in oklch,
hsl(205deg 58% 69%) 40%,
transparent
);
--color-background-info-banner: hsl(204deg 100% 12%);
/* Banners - Success Variant */
--color-text-success-banner: hsl(135deg 56% 63%);
--color-border-success-banner: color-mix(
in oklch,
hsl(149deg 48% 52%) 40%,
transparent
);
--color-background-success-banner: hsl(146deg 90% 7%);
/* Banners - Warning Variant */
--color-text-warning-banner: hsl(40deg 94% 56%);
--color-border-warning-banner: color-mix(
in oklch,
hsl(44deg 44% 66%) 40%,
transparent
);
--color-background-warning-banner: hsl(50deg 100% 10%);
/* Banners - Danger Variant */
--color-text-danger-banner: hsl(7deg 100% 74%);
--color-border-danger-banner: color-mix(
in oklch,
hsl(3deg 73% 74%) 40%,
transparent
);
--color-background-danger-banner: hsl(0deg 52% 18%);
}
@media screen {

131
web/styles/banners.css Normal file
View File

@@ -0,0 +1,131 @@
.banner-wrapper {
container: banner / inline-size;
}
.banner {
box-sizing: border-box;
display: grid;
grid-template: var(--banner-grid-template-rows-lg) / var(
--banner-grid-template-columns-lg
);
grid-template-areas: var(--banner-grid-template-areas-lg);
place-items: start;
font-size: 0.9375em;
border: 1px solid;
border-radius: 6px;
}
.banner__link {
color: var(--color-text-link-banner);
}
.banner-label {
grid-area: banner-label;
padding: 0.3333em 0 0.2667em;
line-height: 1.2667;
}
.banner-action-buttons {
grid-area: banner-btns;
display: flex;
gap: 8px;
margin-left: 6px;
}
.banner-close-button {
display: flex;
grid-area: banner-close-btn;
padding: 0.6875em;
margin-left: 5px;
}
.navbar-alert-banner {
grid-template-columns:
var(--banner-horizontal-padding) minmax(0, 1fr)
auto 0 auto minmax(0, 1fr) minmax(0, auto) var(
--banner-horizontal-padding
);
border: unset;
border-bottom: 1px solid;
border-radius: 0;
place-items: start center;
}
.navbar-alert-banner .banner-action-buttons {
justify-content: center;
}
@container (width >= 44em) and (width < 63em) {
.navbar-alert-banner[data-process="notifications"] {
grid-template: var(--banner-grid-template-rows-md) / var(
--banner-grid-template-columns-md
);
grid-template-areas: var(--banner-grid-template-areas-md);
text-align: center;
}
}
@container (width < 44em) {
.banner {
grid-template: var(--banner-grid-template-rows-md) / var(
--banner-grid-template-columns-md
);
grid-template-areas: var(--banner-grid-template-areas-md);
}
.banner-action-buttons {
flex-wrap: wrap;
margin-left: 0;
}
.navbar-alert-banner {
text-align: center;
}
}
@container (width < 25em) {
.banner {
grid-template-areas: var(--banner-grid-template-areas-sm);
}
.banner-action-buttons {
flex-direction: column;
width: 100%;
}
}
.banner-neutral {
background-color: var(--color-background-neutral-banner);
color: var(--color-text-neutral-banner);
border-color: var(--color-border-neutral-banner);
}
.banner-brand {
background-color: var(--color-background-brand-banner);
color: var(--color-text-brand-banner);
border-color: var(--color-border-brand-banner);
}
.banner-info {
background-color: var(--color-background-info-banner);
color: var(--color-text-info-banner);
border-color: var(--color-border-info-banner);
}
.banner-success {
background-color: var(--color-background-success-banner);
color: var(--color-text-success-banner);
border-color: var(--color-border-success-banner);
}
.banner-warning {
background-color: var(--color-background-warning-banner);
color: var(--color-text-warning-banner);
border-color: var(--color-border-warning-banner);
}
.banner-danger {
background-color: var(--color-background-danger-banner);
color: var(--color-text-danger-banner);
border-color: var(--color-border-danger-banner);
}

View File

@@ -21,30 +21,27 @@ body {
display: flex;
flex-flow: column;
gap: 20px;
padding-bottom: 20px;
padding: 40px 20px;
}
.design-testing-title {
font-size: 2em;
text-align: center;
padding: 20px 0;
}
.action-button-section {
display: flex;
flex-flow: column;
gap: 5px;
.design-controls-section {
display: grid;
grid-template-columns: [control-name-start] max-content [control-name-end control-input-start] 1fr [control-input-end];
gap: 10px;
margin-top: 50px;
width: min(100%, 500px);
}
.design-testing-controls {
display: grid;
grid-template-columns: [control-name-start] max-content [control-name-end control-input-start] min-content [control-input-end];
grid-template-columns: subgrid;
grid-column: control-name-start / control-input-end;
grid-auto-rows: 1fr;
gap: 10px;
background-color: var(--color-background);
border: solid 1px;
padding: 10px;
width: fit-content;
width: 100%;
}
.design-testing-control {
@@ -79,7 +76,9 @@ body {
display: none !important;
}
.section-heading {
.design-testing-controls-label {
grid-column: control-name-start / control-input-end;
margin-top: 10px;
font-size: 1.4em;
font-weight: 600;
}
@@ -125,3 +124,12 @@ body {
font-size: 1.1em;
font-weight: 500;
}
#banner_select_intent {
text-transform: capitalize;
}
#dev_normal_banner_wrapper {
width: min(100%, 800px);
margin: 0 auto;
}

View File

@@ -0,0 +1,6 @@
<button class="{{#if custom_classes}}{{custom_classes}} {{/if}}action-button action-button-{{type}}-{{intent}}" tabindex="0">
{{#if icon}}
<i class="zulip-icon zulip-icon-{{icon}}"></i>
{{/if}}
<span class="action-button-label">{{label}}</span>
</button>

View File

@@ -0,0 +1,13 @@
<div {{#if process}}data-process="{{process}}"{{/if}} class="{{#if custom_classes}}{{custom_classes}} {{/if}}banner banner-{{intent}}">
<span class="banner-label">
{{label}}
</span>
<span class="banner-action-buttons">
{{#each buttons}}
{{> action-button .}}
{{/each}}
</span>
{{#if close_button}}
{{> icon-button custom_classes="banner-close-button" icon="close" intent=intent}}
{{/if}}
</div>

View File

@@ -0,0 +1,3 @@
<button class="{{#if custom_classes}}{{custom_classes}} {{/if}}icon-button icon-button-{{intent}}" tabindex="0">
<i class="zulip-icon zulip-icon-{{icon}}"></i>
</button>

View File

@@ -15,12 +15,5 @@
"./src/reload_state.ts",
"./src/channel.ts"
],
"dev-buttons": [
"./src/bundles/portico.ts",
"./src/portico/design-testing.ts",
"./styles/app_variables.css",
"./styles/portico/dev-buttons.css",
"./styles/buttons.css",
"./styles/app_components.css"
]
"dev-testing": ["./src/bundles/design-testing.ts"]
}

View File

@@ -59,6 +59,7 @@ class PublicURLTest(ZulipTestCase):
def test_design_testing_pages(self) -> None:
urls = {
"/devtools/buttons/": "Button styles browser",
"/devtools/banners/": "Banner styles browser",
}
for url, expected_content in urls.items():

View File

@@ -28,7 +28,23 @@ def dev_buttons_design_testing(request: HttpRequest) -> HttpResponse:
context = {
"background_colors": background_colors,
"icons": get_svg_filenames(),
"page_is_design_testing": True,
"design_component": "buttons",
"doc_root_title": "Button styles browser",
# We set isolated_page to avoid clutter from footer/header.
"isolated_page": True,
}
return render(request, "zerver/development/design_testing/buttons.html", context)
def dev_banners_design_testing(request: HttpRequest) -> HttpResponse:
context = {
"background_colors": background_colors,
"icons": get_svg_filenames(),
"page_is_design_testing": True,
"design_component": "banners",
"doc_root_title": "Banner styles browser",
# We set isolated_page to avoid clutter from footer/header.
"isolated_page": True,
}
return render(request, "zerver/development/design_testing/banners.html", context)

View File

@@ -13,7 +13,10 @@ from django.views.static import serve
from zerver.views.auth import login_page
from zerver.views.development.cache import remove_caches
from zerver.views.development.camo import handle_camo_url
from zerver.views.development.design_testing import dev_buttons_design_testing
from zerver.views.development.design_testing import (
dev_banners_design_testing,
dev_buttons_design_testing,
)
from zerver.views.development.dev_login import (
api_dev_fetch_api_key,
api_dev_list_users,
@@ -100,6 +103,7 @@ urls = [
path("external_content/<digest>/<received_url>", handle_camo_url),
# Endpoints for design testing.
path("devtools/buttons/", dev_buttons_design_testing),
path("devtools/banners/", dev_banners_design_testing),
]
v1_api_mobile_patterns = [