diff --git a/templates/zerver/development/design_testing/buttons.html b/templates/zerver/development/design_testing/buttons.html
new file mode 100644
index 0000000000..59b5317221
--- /dev/null
+++ b/templates/zerver/development/design_testing/buttons.html
@@ -0,0 +1,179 @@
+{% extends "zerver/base.html" %}
+{% set entrypoint = "dev-buttons" %}
+
+{% block title %}
+
Button styles browser | Zulip Dev
+{% endblock %}
+
+{% block content %}
+
+
+ {% include 'zerver/portico-header.html' %}
+
+
+
Button styles browser
+
+
+
+
+
+{% endblock %}
diff --git a/templates/zerver/development/dev_tools.html b/templates/zerver/development/dev_tools.html
index 84381701a5..b803ce4a0e 100644
--- a/templates/zerver/development/dev_tools.html
+++ b/templates/zerver/development/dev_tools.html
@@ -79,6 +79,11 @@
None needed |
Test incoming webhook integrations |
+
+ | /devtools/buttons |
+ None needed |
+ Test button styles |
+
Useful management commands
diff --git a/web/src/portico/design-testing.ts b/web/src/portico/design-testing.ts
new file mode 100644
index 0000000000..62bf91d35f
--- /dev/null
+++ b/web/src/portico/design-testing.ts
@@ -0,0 +1,43 @@
+import $ from "jquery";
+
+import {$t} from "../i18n.ts";
+
+$(window).on("load", () => {
+ $("input[name='dark-theme-select']").on("change", (e) => {
+ if ($(e.target).attr("id") === "enable_dark_theme") {
+ $(":root").addClass("dark-theme");
+ } else {
+ $(":root").removeClass("dark-theme");
+ }
+ });
+
+ $("input[name='button-icon-select']").on("change", (e) => {
+ if ($(e.target).attr("id") === "enable_button_icon") {
+ $(".action-button .zulip-icon").removeClass("hidden");
+ } else {
+ $(".action-button .zulip-icon").addClass("hidden");
+ }
+ });
+
+ $("#button_text").on("input", function (this: HTMLElement) {
+ const button_text = $(this).val()?.toString() ?? "";
+ $(".action-button-label").text(button_text);
+ });
+
+ $("#clear_button_text").on("click", () => {
+ $("#button_text").val("");
+ $(".action-button-label").text($t({defaultMessage: "Button joy"}));
+ });
+
+ $("#button_select_icon").on("change", function (this: HTMLElement) {
+ const icon_name = $(this).val()?.toString() ?? "";
+ $(".action-button .zulip-icon").attr("class", (_index, className) =>
+ className.replaceAll(/zulip-icon-[^\s]+/g, `zulip-icon-${icon_name}`),
+ );
+ });
+
+ $("#button_select_background").on("change", function (this: HTMLElement) {
+ const background_var = $(this).val()?.toString() ?? "";
+ $("body").css("background-color", `var(${background_var})`);
+ });
+});
diff --git a/web/styles/app_variables.css b/web/styles/app_variables.css
index 99f86369aa..61289d2dde 100644
--- a/web/styles/app_variables.css
+++ b/web/styles/app_variables.css
@@ -1106,6 +1106,209 @@
--background-color-active-dropdown-item: hsl(220deg 12% 4.9% / 5%);
--background-color-active-typeahead-item: hsl(221.14deg 89.74% 92.35%);
--color-typeahead-option-label: var(--grey-500);
+
+ /* Actions buttons */
+ --color-inner-shadow-action-button: color-mix(
+ in oklch,
+ #000 10%,
+ transparent
+ );
+ /* Action buttons -- Neutral Variant */
+ --color-text-neutral-primary-action-button: #fff;
+ --color-background-neutral-primary-action-button: #777a88;
+ --color-background-neutral-primary-action-button-hover: #707380;
+ --color-background-neutral-primary-action-button-active: #696b78;
+ --color-text-neutral-quiet-action-button: #393c49;
+ --color-background-neutral-quiet-action-button: color-mix(
+ in oklch,
+ #767988 12%,
+ transparent
+ );
+ --color-background-neutral-quiet-action-button-hover: color-mix(
+ in oklch,
+ #767988 17%,
+ transparent
+ );
+ --color-background-neutral-quiet-action-button-active: color-mix(
+ in oklch,
+ #767988 22%,
+ transparent
+ );
+ --color-text-neutral-borderless-action-button: #535663;
+ --color-background-neutral-borderless-action-button-hover: color-mix(
+ in oklch,
+ #535663 7%,
+ transparent
+ );
+ --color-background-neutral-borderless-action-button-active: color-mix(
+ in oklch,
+ #535663 11%,
+ transparent
+ );
+ /* Action buttons -- Brand Variant */
+ --color-text-brand-primary-action-button: #fff;
+ --color-background-brand-primary-action-button: #805bfe;
+ --color-background-brand-primary-action-button-hover: #7349ec;
+ --color-background-brand-primary-action-button-active: #612fd4;
+ --color-text-brand-quiet-action-button: #4704aa;
+ --color-background-brand-quiet-action-button: color-mix(
+ in oklch,
+ #805bfe 12%,
+ transparent
+ );
+ --color-background-brand-quiet-action-button-hover: color-mix(
+ in oklch,
+ #805bfe 17%,
+ transparent
+ );
+ --color-background-brand-quiet-action-button-active: color-mix(
+ in oklch,
+ #805bfe 22%,
+ transparent
+ );
+ --color-text-brand-borderless-action-button: #5f3cc5;
+ --color-background-brand-borderless-action-button-hover: color-mix(
+ in oklch,
+ #2c0070 5%,
+ transparent
+ );
+ --color-background-brand-borderless-action-button-active: color-mix(
+ in oklch,
+ #2c0070 10%,
+ transparent
+ );
+ /* Action buttons -- Info Variant */
+ --color-text-info-primary-action-button: #fff;
+ --color-background-info-primary-action-button: #3c6bff;
+ --color-background-info-primary-action-button-hover: #2e59eb;
+ --color-background-info-primary-action-button-active: #1e41d3;
+ --color-text-info-quiet-action-button: #1027a6;
+ --color-background-info-quiet-action-button: color-mix(
+ in oklch,
+ #3c6bff 12%,
+ transparent
+ );
+ --color-background-info-quiet-action-button-hover: color-mix(
+ in oklch,
+ #3c6bff 17%,
+ transparent
+ );
+ --color-background-info-quiet-action-button-active: color-mix(
+ in oklch,
+ #3c6bff 22%,
+ transparent
+ );
+ --color-text-info-borderless-action-button: #2347c6;
+ --color-background-info-borderless-action-button-hover: color-mix(
+ in oklch,
+ #06037c 5%,
+ transparent
+ );
+ --color-background-info-borderless-action-button-active: color-mix(
+ in oklch,
+ #06037c 9%,
+ transparent
+ );
+ /* Action buttons -- Success Variant */
+ --color-text-success-primary-action-button: #fff;
+ --color-background-success-primary-action-button: #07833c;
+ --color-background-success-primary-action-button-hover: #087736;
+ --color-background-success-primary-action-button-active: #09672e;
+ --color-text-success-quiet-action-button: #054f22;
+ --color-background-success-quiet-action-button: color-mix(
+ in oklch,
+ #07833c 13%,
+ transparent
+ );
+ --color-background-success-quiet-action-button-hover: color-mix(
+ in oklch,
+ #07833c 18%,
+ transparent
+ );
+ --color-background-success-quiet-action-button-active: color-mix(
+ in oklch,
+ #07833c 23%,
+ transparent
+ );
+ --color-text-success-borderless-action-button: #07833c;
+ --color-background-success-borderless-action-button-hover: color-mix(
+ in oklch,
+ #09672e 8%,
+ transparent
+ );
+ --color-background-success-borderless-action-button-active: color-mix(
+ in oklch,
+ #09672e 12%,
+ transparent
+ );
+ /* Action buttons -- Warning Variant */
+ --color-text-warning-primary-action-button: color-mix(
+ in oklch,
+ #000 88%,
+ transparent
+ );
+ --color-background-warning-primary-action-button: #febe3d;
+ --color-background-warning-primary-action-button-hover: #f8b325;
+ --color-background-warning-primary-action-button-active: #eba002;
+ --color-text-warning-quiet-action-button: #764607;
+ --color-background-warning-quiet-action-button: color-mix(
+ in oklch,
+ #eba002 18%,
+ transparent
+ );
+ --color-background-warning-quiet-action-button-hover: color-mix(
+ in oklch,
+ #eba002 23%,
+ transparent
+ );
+ --color-background-warning-quiet-action-button-active: color-mix(
+ in oklch,
+ #eba002 28%,
+ transparent
+ );
+ --color-text-warning-borderless-action-button: #a96a05;
+ --color-background-warning-borderless-action-button-hover: color-mix(
+ in oklch,
+ #a96a05 10%,
+ transparent
+ );
+ --color-background-warning-borderless-action-button-active: color-mix(
+ in oklch,
+ #a96a05 14%,
+ transparent
+ );
+ /* Action buttons -- Danger Variant */
+ --color-text-danger-primary-action-button: #fff;
+ --color-background-danger-primary-action-button: #e1392e;
+ --color-background-danger-primary-action-button-hover: #d22720;
+ --color-background-danger-primary-action-button-active: #c0070a;
+ --color-text-danger-quiet-action-button: #ac0508;
+ --color-background-danger-quiet-action-button: color-mix(
+ in oklch,
+ #e1392e 13%,
+ transparent
+ );
+ --color-background-danger-quiet-action-button-hover: color-mix(
+ in oklch,
+ #e1392e 18%,
+ transparent
+ );
+ --color-background-danger-quiet-action-button-active: color-mix(
+ in oklch,
+ #e1392e 23%,
+ transparent
+ );
+ --color-text-danger-borderless-action-button: #c0070a;
+ --color-background-danger-borderless-action-button-hover: color-mix(
+ in oklch,
+ #c0070a 9%,
+ transparent
+ );
+ --color-background-danger-borderless-action-button-active: color-mix(
+ in oklch,
+ #c0070a 13%,
+ transparent
+ );
}
%dark-theme {
@@ -1641,6 +1844,223 @@
226.35deg 82.53% 55.1% / 38.82%
);
--color-typeahead-option-label: var(--grey-400);
+
+ /* Action buttons -- Neutral Variant */
+ --color-text-neutral-primary-action-button: color-mix(
+ in oklch,
+ #fff 85%,
+ transparent
+ );
+ --color-background-neutral-primary-action-button: #535663;
+ --color-background-neutral-primary-action-button-hover: #626573;
+ --color-background-neutral-primary-action-button-active: #535663;
+ --color-text-neutral-quiet-action-button: #bbbdc8;
+ --color-background-neutral-quiet-action-button: color-mix(
+ in oklch,
+ #bbbdc8 12%,
+ transparent
+ );
+ --color-background-neutral-quiet-action-button-hover: color-mix(
+ in oklch,
+ #bbbdc8 17%,
+ transparent
+ );
+ --color-background-neutral-quiet-action-button-active: color-mix(
+ in oklch,
+ #bbbdc8 12%,
+ transparent
+ );
+ --color-text-neutral-borderless-action-button: #aaadba;
+ --color-background-neutral-borderless-action-button-hover: color-mix(
+ in oklch,
+ #9194a3 14%,
+ transparent
+ );
+ --color-background-neutral-borderless-action-button-active: color-mix(
+ in oklch,
+ #9194a3 18%,
+ transparent
+ );
+ /* Action buttons -- Brand Variant */
+ --color-text-brand-primary-action-button: color-mix(
+ in oklch,
+ #fff 85%,
+ transparent
+ );
+ --color-background-brand-primary-action-button: #5417c3;
+ --color-background-brand-primary-action-button-hover: #612fd4;
+ --color-background-brand-primary-action-button-active: #5417c3;
+ --color-text-brand-quiet-action-button: #aba5fd;
+ --color-background-brand-quiet-action-button: color-mix(
+ in oklch,
+ #9e94fd 12%,
+ transparent
+ );
+ --color-background-brand-quiet-action-button-hover: color-mix(
+ in oklch,
+ #9e94fd 17%,
+ transparent
+ );
+ --color-background-brand-quiet-action-button-active: color-mix(
+ in oklch,
+ #9e94fd 12%,
+ transparent
+ );
+ --color-text-brand-borderless-action-button: #9e94fd;
+ --color-background-brand-borderless-action-button-hover: color-mix(
+ in oklch,
+ #8a6fff 14%,
+ transparent
+ );
+ --color-background-brand-borderless-action-button-active: color-mix(
+ in oklch,
+ #8a6fff 18%,
+ transparent
+ );
+ /* Action buttons -- Info Variant */
+ --color-text-info-primary-action-button: color-mix(
+ in oklch,
+ #fff 85%,
+ transparent
+ );
+ --color-background-info-primary-action-button: #1e41d3;
+ --color-background-info-primary-action-button-hover: #2e59eb;
+ --color-background-info-primary-action-button-active: #1e41d3;
+ --color-text-info-quiet-action-button: #97b6fe;
+ --color-background-info-quiet-action-button: color-mix(
+ in oklch,
+ #97b6fe 12%,
+ transparent
+ );
+ --color-background-info-quiet-action-button-hover: color-mix(
+ in oklch,
+ #97b6fe 17%,
+ transparent
+ );
+ --color-background-info-quiet-action-button-active: color-mix(
+ in oklch,
+ #97b6fe 12%,
+ transparent
+ );
+ --color-text-info-borderless-action-button: #84a8fd;
+ --color-background-info-borderless-action-button-hover: color-mix(
+ in oklch,
+ #4d7bfd 12%,
+ transparent
+ );
+ --color-background-info-borderless-action-button-active: color-mix(
+ in oklch,
+ #4d7bfd 17%,
+ transparent
+ );
+ /* Action buttons -- Success Variant */
+ --color-text-success-primary-action-button: color-mix(
+ in oklch,
+ #fff 85%,
+ transparent
+ );
+ --color-background-success-primary-action-button: #09672e;
+ --color-background-success-primary-action-button-hover: #087736;
+ --color-background-success-primary-action-button-active: #09672e;
+ --color-text-success-quiet-action-button: #6bd586;
+ --color-background-success-quiet-action-button: color-mix(
+ in oklch,
+ #41ae61 12%,
+ transparent
+ );
+ --color-background-success-quiet-action-button-hover: color-mix(
+ in oklch,
+ #41ae61 17%,
+ transparent
+ );
+ --color-background-success-quiet-action-button-active: color-mix(
+ in oklch,
+ #41ae61 12%,
+ transparent
+ );
+ --color-text-success-borderless-action-button: #57c273;
+ --color-background-success-borderless-action-button-hover: color-mix(
+ in oklch,
+ #2f9f52 12%,
+ transparent
+ );
+ --color-background-success-borderless-action-button-active: color-mix(
+ in oklch,
+ #2f9f52 17%,
+ transparent
+ );
+ /* Action buttons -- Warning Variant */
+ --color-text-warning-primary-action-button: color-mix(
+ in oklch,
+ #000 90%,
+ transparent
+ );
+ --color-background-warning-primary-action-button: #db920d;
+ --color-background-warning-primary-action-button-hover: #eba002;
+ --color-background-warning-primary-action-button-active: #db920d;
+ --color-text-warning-quiet-action-button: #f8b325;
+ --color-background-warning-quiet-action-button: color-mix(
+ in oklch,
+ #db920d 12%,
+ transparent
+ );
+ --color-background-warning-quiet-action-button-hover: color-mix(
+ in oklch,
+ #db920d 17%,
+ transparent
+ );
+ --color-background-warning-quiet-action-button-active: color-mix(
+ in oklch,
+ #db920d 12%,
+ transparent
+ );
+ --color-text-warning-borderless-action-button: #eba002;
+ --color-background-warning-borderless-action-button-hover: color-mix(
+ in oklch,
+ #c8850d 12%,
+ transparent
+ );
+ --color-background-warning-borderless-action-button-active: color-mix(
+ in oklch,
+ #c8850d 17%,
+ transparent
+ );
+ /* Action buttons -- Danger Variant */
+ --color-text-danger-primary-action-button: color-mix(
+ in oklch,
+ #fff 85%,
+ transparent
+ );
+ --color-background-danger-primary-action-button: #d22720;
+ --color-background-danger-primary-action-button-hover: #e1392e;
+ --color-background-danger-primary-action-button-active: #d22720;
+ --color-text-danger-quiet-action-button: #ff8b7c;
+ --color-background-danger-quiet-action-button: color-mix(
+ in oklch,
+ #fd5f50 12%,
+ transparent
+ );
+ --color-background-danger-quiet-action-button-hover: color-mix(
+ in oklch,
+ #fd5f50 17%,
+ transparent
+ );
+ --color-background-danger-quiet-action-button-active: color-mix(
+ in oklch,
+ #fd5f50 12%,
+ transparent
+ );
+ --color-text-danger-borderless-action-button: #ff8b7c;
+ --color-background-danger-borderless-action-button-hover: color-mix(
+ in oklch,
+ #f34c3e 12%,
+ transparent
+ );
+ --color-background-danger-borderless-action-button-active: color-mix(
+ in oklch,
+ #f34c3e 17%,
+ transparent
+ );
}
@media screen {
diff --git a/web/styles/buttons.css b/web/styles/buttons.css
new file mode 100644
index 0000000000..dcbdfa962a
--- /dev/null
+++ b/web/styles/buttons.css
@@ -0,0 +1,356 @@
+.action-button {
+ display: flex;
+ gap: 0.5ch;
+ justify-content: center;
+ align-items: center;
+ line-height: 1.3333;
+ font-size: var(--base-font-size-px);
+ font-family: "Source Sans 3 VF", sans-serif;
+ font-weight: 550;
+ letter-spacing: 0.02ch;
+ padding: 0.2667em 0.6667em;
+ color: var(--color-text-neutral-quiet-action-button);
+ background-color: var(--color-background-neutral-quiet-action-button);
+ border-radius: 4px;
+ white-space: nowrap;
+ user-select: none;
+ border: none;
+ cursor: pointer;
+
+ &:hover {
+ background-color: var(
+ --color-background-neutral-quiet-action-button-hover
+ );
+ }
+
+ &:active {
+ background-color: var(
+ --color-background-neutral-quiet-action-button-active
+ );
+ scale: 0.96;
+ }
+}
+
+.action-button-label {
+ max-width: 32ch;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+/* Action buttons -- Neutral Intent */
+.action-button-primary-neutral {
+ color: var(--color-text-neutral-primary-action-button);
+ background-color: var(--color-background-neutral-primary-action-button);
+
+ &:hover {
+ background-color: var(
+ --color-background-neutral-primary-action-button-hover
+ );
+ }
+
+ &:active {
+ background-color: var(
+ --color-background-neutral-primary-action-button-active
+ );
+ }
+}
+
+.action-button-quiet-neutral {
+ color: var(--color-text-neutral-quiet-action-button);
+ background-color: var(--color-background-neutral-quiet-action-button);
+ box-shadow: 0 0 0.5px 0.5px var(--color-inner-shadow-action-button) inset;
+
+ &:hover {
+ background-color: var(
+ --color-background-neutral-quiet-action-button-hover
+ );
+ }
+
+ &:active {
+ background-color: var(
+ --color-background-neutral-quiet-action-button-active
+ );
+ }
+}
+
+.action-button-borderless-neutral {
+ color: var(--color-text-neutral-borderless-action-button);
+ background-color: transparent;
+
+ &:hover {
+ background-color: var(
+ --color-background-neutral-borderless-action-button-hover
+ );
+ }
+
+ &:active {
+ background-color: var(
+ --color-background-neutral-borderless-action-button-active
+ );
+ }
+}
+
+/* Action buttons -- Brand Intent */
+.action-button-primary-brand {
+ color: var(--color-text-brand-primary-action-button);
+ background-color: var(--color-background-brand-primary-action-button);
+
+ &:hover {
+ background-color: var(
+ --color-background-brand-primary-action-button-hover
+ );
+ }
+
+ &:active {
+ background-color: var(
+ --color-background-brand-primary-action-button-active
+ );
+ }
+}
+
+.action-button-quiet-brand {
+ color: var(--color-text-brand-quiet-action-button);
+ background-color: var(--color-background-brand-quiet-action-button);
+ box-shadow: 0 0 0.5px 0.5px var(--color-inner-shadow-action-button) inset;
+
+ &:hover {
+ background-color: var(
+ --color-background-brand-quiet-action-button-hover
+ );
+ }
+
+ &:active {
+ background-color: var(
+ --color-background-brand-quiet-action-button-active
+ );
+ }
+}
+
+.action-button-borderless-brand {
+ color: var(--color-text-brand-borderless-action-button);
+ background-color: transparent;
+
+ &:hover {
+ background-color: var(
+ --color-background-brand-borderless-action-button-hover
+ );
+ }
+
+ &:active {
+ background-color: var(
+ --color-background-brand-borderless-action-button-active
+ );
+ }
+}
+
+/* Action buttons -- Info Intent */
+.action-button-primary-info {
+ color: var(--color-text-info-primary-action-button);
+ background-color: var(--color-background-info-primary-action-button);
+
+ &:hover {
+ background-color: var(
+ --color-background-info-primary-action-button-hover
+ );
+ }
+
+ &:active {
+ background-color: var(
+ --color-background-info-primary-action-button-active
+ );
+ }
+}
+
+.action-button-quiet-info {
+ color: var(--color-text-info-quiet-action-button);
+ background-color: var(--color-background-info-quiet-action-button);
+ box-shadow: 0 0 0.5px 0.5px var(--color-inner-shadow-action-button) inset;
+
+ &:hover {
+ background-color: var(
+ --color-background-info-quiet-action-button-hover
+ );
+ }
+
+ &:active {
+ background-color: var(
+ --color-background-info-quiet-action-button-active
+ );
+ }
+}
+
+.action-button-borderless-info {
+ color: var(--color-text-info-borderless-action-button);
+ background-color: transparent;
+
+ &:hover {
+ background-color: var(
+ --color-background-info-borderless-action-button-hover
+ );
+ }
+
+ &:active {
+ background-color: var(
+ --color-background-info-borderless-action-button-active
+ );
+ }
+}
+
+/* Action buttons -- Success Intent */
+.action-button-primary-success {
+ color: var(--color-text-success-primary-action-button);
+ background-color: var(--color-background-success-primary-action-button);
+
+ &:hover {
+ background-color: var(
+ --color-background-success-primary-action-button-hover
+ );
+ }
+
+ &:active {
+ background-color: var(
+ --color-background-success-primary-action-button-active
+ );
+ }
+}
+
+.action-button-quiet-success {
+ color: var(--color-text-success-quiet-action-button);
+ background-color: var(--color-background-success-quiet-action-button);
+ box-shadow: 0 0 0.5px 0.5px var(--color-inner-shadow-action-button) inset;
+
+ &:hover {
+ background-color: var(
+ --color-background-success-quiet-action-button-hover
+ );
+ }
+
+ &:active {
+ background-color: var(
+ --color-background-success-quiet-action-button-active
+ );
+ }
+}
+
+.action-button-borderless-success {
+ color: var(--color-text-success-borderless-action-button);
+ background-color: transparent;
+
+ &:hover {
+ background-color: var(
+ --color-background-success-borderless-action-button-hover
+ );
+ }
+
+ &:active {
+ background-color: var(
+ --color-background-success-borderless-action-button-active
+ );
+ }
+}
+
+/* Action buttons -- Warning Intent */
+.action-button-primary-warning {
+ color: var(--color-text-warning-primary-action-button);
+ background-color: var(--color-background-warning-primary-action-button);
+
+ &:hover {
+ background-color: var(
+ --color-background-warning-primary-action-button-hover
+ );
+ }
+
+ &:active {
+ background-color: var(
+ --color-background-warning-primary-action-button-active
+ );
+ }
+}
+
+.action-button-quiet-warning {
+ color: var(--color-text-warning-quiet-action-button);
+ background-color: var(--color-background-warning-quiet-action-button);
+ box-shadow: 0 0 0.5px 0.5px var(--color-inner-shadow-action-button) inset;
+
+ &:hover {
+ background-color: var(
+ --color-background-warning-quiet-action-button-hover
+ );
+ }
+
+ &:active {
+ background-color: var(
+ --color-background-warning-quiet-action-button-active
+ );
+ }
+}
+
+.action-button-borderless-warning {
+ color: var(--color-text-warning-borderless-action-button);
+ background-color: transparent;
+
+ &:hover {
+ background-color: var(
+ --color-background-warning-borderless-action-button-hover
+ );
+ }
+
+ &:active {
+ background-color: var(
+ --color-background-warning-borderless-action-button-active
+ );
+ }
+}
+
+/* Action buttons -- Danger Intent */
+.action-button-primary-danger {
+ color: var(--color-text-danger-primary-action-button);
+ background-color: var(--color-background-danger-primary-action-button);
+
+ &:hover {
+ background-color: var(
+ --color-background-danger-primary-action-button-hover
+ );
+ }
+
+ &:active {
+ background-color: var(
+ --color-background-danger-primary-action-button-active
+ );
+ }
+}
+
+.action-button-quiet-danger {
+ color: var(--color-text-danger-quiet-action-button);
+ background-color: var(--color-background-danger-quiet-action-button);
+ box-shadow: 0 0 0.5px 0.5px var(--color-inner-shadow-action-button) inset;
+
+ &:hover {
+ background-color: var(
+ --color-background-danger-quiet-action-button-hover
+ );
+ }
+
+ &:active {
+ background-color: var(
+ --color-background-danger-quiet-action-button-active
+ );
+ }
+}
+
+.action-button-borderless-danger {
+ color: var(--color-text-danger-borderless-action-button);
+ background-color: transparent;
+
+ &:hover {
+ background-color: var(
+ --color-background-danger-borderless-action-button-hover
+ );
+ }
+
+ &:active {
+ background-color: var(
+ --color-background-danger-borderless-action-button-active
+ );
+ }
+}
diff --git a/web/styles/portico/dev-buttons.css b/web/styles/portico/dev-buttons.css
new file mode 100644
index 0000000000..9c95dc2b8b
--- /dev/null
+++ b/web/styles/portico/dev-buttons.css
@@ -0,0 +1,113 @@
+body {
+ --base-font-size-px: 16px;
+ width: 100%;
+ margin: 0;
+ padding: 0;
+ font-size: var(--base-font-size-px);
+ /* The line-height used in most of the UI should be at least
+ its legacy value, but should expand with larger base
+ line-height values. */
+ line-height: max(
+ var(--legacy-body-line-height-unitless),
+ var(--base-line-height-unitless)
+ );
+ font-family: "Source Sans 3 VF", sans-serif;
+ font-weight: unset; /* override value in portico.css */
+ color: var(--color-text-default);
+ background-color: var(--color-background);
+}
+
+.design-testing-header {
+ display: grid;
+ grid-template-columns: auto 1fr auto;
+ align-items: center;
+ padding: 15px;
+ background-color: hsl(0deg 0% 100%);
+ box-shadow: 0 0 4px hsla(0deg 0% 0% / 10%);
+
+ & .header__title {
+ color: hsl(0deg 0% 27%);
+ text-align: center;
+ font-size: 1.4em;
+ font-weight: 300;
+ }
+
+ & .header__link {
+ text-decoration: none;
+ color: hsl(0deg 0% 27%);
+ font-weight: 600;
+ }
+}
+
+.design-testing-wrapper {
+ display: flex;
+ flex-flow: column;
+ gap: 20px;
+ padding-bottom: 20px;
+}
+
+.design-testing-title {
+ font-size: 2em;
+ text-align: center;
+ padding: 20px 0;
+}
+
+.action-button-section {
+ display: flex;
+ flex-flow: column;
+ gap: 5px;
+}
+
+.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-auto-rows: 1fr;
+ gap: 10px;
+ background-color: var(--color-background);
+ border: solid 1px;
+ padding: 10px;
+ width: fit-content;
+}
+
+.design-testing-control {
+ display: grid;
+ grid-template-columns: subgrid;
+ grid-column: control-name-start / control-input-end;
+ gap: 10px;
+}
+
+.design-testing-control-element {
+ font-size: inherit;
+ font-family: inherit;
+}
+
+.control-label {
+ cursor: pointer;
+ grid-column: control-name-start / control-name-end;
+ align-self: center;
+}
+
+.control-setting {
+ grid-column: control-input-start / control-input-end;
+}
+
+.control-setting-multiple {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.hidden {
+ display: none !important;
+}
+
+.section-heading {
+ font-size: 1.4em;
+ font-weight: 600;
+}
+
+.action-button-group {
+ display: flex;
+ flex-flow: row wrap;
+ gap: 10px;
+}
diff --git a/web/webpack.dev-assets.json b/web/webpack.dev-assets.json
index d50b045065..b9ef8d7db7 100644
--- a/web/webpack.dev-assets.json
+++ b/web/webpack.dev-assets.json
@@ -14,5 +14,15 @@
"./styles/portico/email_log.css",
"./src/reload_state.ts",
"./src/channel.ts"
+ ],
+ "dev-buttons": [
+ "./src/bundles/common.ts",
+ "./src//portico/header.ts",
+ "./src/portico/design-testing.ts",
+ "./styles/portico/portico_styles.css",
+ "./styles/portico/dev-buttons.css",
+ "./styles/app_variables.css",
+ "./styles/app_components.css",
+ "./styles/buttons.css"
]
}
diff --git a/zerver/tests/test_urls.py b/zerver/tests/test_urls.py
index 08bb51ac8f..9e981d1670 100644
--- a/zerver/tests/test_urls.py
+++ b/zerver/tests/test_urls.py
@@ -56,6 +56,16 @@ class PublicURLTest(ZulipTestCase):
self.assertIn(expected_tag, response.content.decode())
self.assertEqual(response.status_code, 200)
+ def test_design_testing_pages(self) -> None:
+ urls = {
+ "/devtools/buttons/": "Button styles browser",
+ }
+
+ for url, expected_content in urls.items():
+ result = self.client_get(url)
+ self.assertEqual(result.status_code, 200)
+ self.assert_in_success_response([expected_content], result)
+
def test_public_urls(self) -> None:
"""
Test which views are accessible when not logged in.
diff --git a/zerver/views/development/design_testing.py b/zerver/views/development/design_testing.py
new file mode 100644
index 0000000000..3d9e0755f8
--- /dev/null
+++ b/zerver/views/development/design_testing.py
@@ -0,0 +1,34 @@
+import os
+
+from django.http import HttpRequest, HttpResponse
+from django.shortcuts import render
+
+background_colors = [
+ {"name": "Default background", "css_var": "--color-background"},
+ {"name": "Popover background", "css_var": "--color-background-popover-menu"},
+ {"name": "Modal background", "css_var": "--color-background-modal"},
+ {"name": "Compose background", "css_var": "--color-compose-box-background"},
+]
+
+
+def get_svg_filenames() -> list[str]:
+ icons_dir = os.path.join(os.path.dirname(__file__), "../../../web/shared/icons")
+
+ # Get all .svg file names from the directory
+ svg_files = [f for f in os.listdir(icons_dir) if f.endswith(".svg")]
+
+ # Remove the .svg extension from the file names
+ icon_names = [os.path.splitext(f)[0] for f in svg_files]
+
+ # Sort the list alphabetically
+ return sorted(icon_names)
+
+
+def dev_buttons_design_testing(request: HttpRequest) -> HttpResponse:
+ context = {
+ "background_colors": background_colors,
+ "icons": get_svg_filenames(),
+ # We set isolated_page to avoid clutter from footer/header.
+ "isolated_page": True,
+ }
+ return render(request, "zerver/development/design_testing/buttons.html", context)
diff --git a/zproject/dev_urls.py b/zproject/dev_urls.py
index 53ac877d0b..d8164bfb1d 100644
--- a/zproject/dev_urls.py
+++ b/zproject/dev_urls.py
@@ -13,6 +13,7 @@ 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.dev_login import (
api_dev_fetch_api_key,
api_dev_list_users,
@@ -97,6 +98,8 @@ urls = [
path("flush_caches", remove_caches),
# Redirect camo URLs for development
path("external_content//", handle_camo_url),
+ # Endpoints for design testing.
+ path("devtools/buttons/", dev_buttons_design_testing),
]
v1_api_mobile_patterns = [