Init Axo design system
This commit is contained in:
parent
7553a85b1c
commit
0d99f8bca2
35 changed files with 4785 additions and 210 deletions
60
.eslintrc.js
60
.eslintrc.js
|
@ -317,6 +317,66 @@ module.exports = {
|
|||
'func-names': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['ts/axo/**/*.tsx'],
|
||||
plugins: ['better-tailwindcss'],
|
||||
settings: {
|
||||
'better-tailwindcss': {
|
||||
entryPoint: './ts/axo/tailwind.css',
|
||||
callees: ['css'],
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// stylistic: Enforce consistent line wrapping for tailwind classes. (recommended, autofix)
|
||||
'better-tailwindcss/enforce-consistent-line-wrapping': 'off',
|
||||
// stylistic: Enforce a consistent order for tailwind classes. (recommended, autofix)
|
||||
'better-tailwindcss/enforce-consistent-class-order': 'error',
|
||||
// stylistic: Enforce consistent variable syntax. (autofix)
|
||||
'better-tailwindcss/enforce-consistent-variable-syntax': 'error',
|
||||
// stylistic: Enforce consistent position of the important modifier. (autofix)
|
||||
'better-tailwindcss/enforce-consistent-important-position': 'error',
|
||||
// stylistic: Enforce shorthand class names. (autofix)
|
||||
'better-tailwindcss/enforce-shorthand-classes': 'error',
|
||||
// stylistic: Remove duplicate classes. (autofix)
|
||||
'better-tailwindcss/no-duplicate-classes': 'error',
|
||||
// stylistic: Remove deprecated classes. (autofix)
|
||||
'better-tailwindcss/no-deprecated-classes': 'off',
|
||||
// stylistic: Disallow unnecessary whitespace in tailwind classes. (autofix)
|
||||
'better-tailwindcss/no-unnecessary-whitespace': 'error',
|
||||
// correctness: Report classes not registered with tailwindcss. (recommended)
|
||||
'better-tailwindcss/no-unregistered-classes': 'error',
|
||||
// correctness: Report classes that produce conflicting styles.
|
||||
'better-tailwindcss/no-conflicting-classes': 'error',
|
||||
// correctness: Disallow restricted classes. (autofix)
|
||||
'better-tailwindcss/no-restricted-classes': [
|
||||
'error',
|
||||
{
|
||||
restrict: [
|
||||
{
|
||||
pattern: '\\[#[a-fA-F0-9]{3,8}?\\]', // ex: "text-[#fff]"
|
||||
message: 'No arbitrary hex values',
|
||||
},
|
||||
{
|
||||
pattern: '\\[rgba?\\(.*\\)\\]', // ex: "text-[rgb(255,255,255)]"
|
||||
message: 'No arbitrary rgb values',
|
||||
},
|
||||
{
|
||||
pattern: '\\[hsla?\\(.*\\)\\]', // ex: "text-[hsl(255,255,255)]"
|
||||
message: 'No arbitrary hsl values',
|
||||
},
|
||||
{
|
||||
pattern: '^.*!$', // ex: "p-4!"
|
||||
message: 'No !important modifiers',
|
||||
},
|
||||
{
|
||||
pattern: '^\\*+:.*', // ex: "*:mx-0",
|
||||
message: 'No child variants',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
rules: {
|
||||
|
|
|
@ -17,6 +17,7 @@ ts/**/*.js
|
|||
ts/protobuf/*.d.ts
|
||||
ts/protobuf/*.js
|
||||
stylesheets/manifest.css
|
||||
stylesheets/tailwind.css
|
||||
ts/util/lint/exceptions.json
|
||||
storybook-static
|
||||
build/locale-display-names.json
|
||||
|
|
|
@ -1,8 +1,19 @@
|
|||
// Copyright 2018 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/** @type {import("prettier").Config} */
|
||||
module.exports = {
|
||||
singleQuote: true,
|
||||
arrowParens: 'avoid',
|
||||
trailingComma: 'es5',
|
||||
overrides: [
|
||||
{
|
||||
files: ['./ts/axo/**.tsx'],
|
||||
plugins: ['prettier-plugin-tailwindcss'],
|
||||
options: {
|
||||
tailwindStylesheet: './ts/axo/tailwind.css',
|
||||
tailwindFunctions: ['css'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -9,7 +9,7 @@ const config: StorybookConfig = {
|
|||
reactDocgen: false,
|
||||
},
|
||||
|
||||
stories: ['../ts/components/**/*.stories.tsx'],
|
||||
stories: ['../ts/axo/**/*.stories.tsx', '../ts/components/**/*.stories.tsx'],
|
||||
|
||||
addons: [
|
||||
'@storybook/addon-a11y',
|
||||
|
@ -69,6 +69,23 @@ const config: StorybookConfig = {
|
|||
],
|
||||
});
|
||||
|
||||
config.module!.rules!.push({
|
||||
test: /tailwind\.css$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'postcss-loader',
|
||||
options: {
|
||||
postcssOptions: {
|
||||
config: false,
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
config.node = { global: true };
|
||||
|
||||
config.externals = {
|
||||
|
|
|
@ -5,8 +5,8 @@ import '../ts/window.d.ts';
|
|||
|
||||
import React, { StrictMode } from 'react';
|
||||
|
||||
import 'sanitize.css';
|
||||
import '../stylesheets/manifest.scss';
|
||||
import '../ts/axo/tailwind.css';
|
||||
|
||||
import * as styles from './styles.scss';
|
||||
import messages from '../_locales/en/messages.json';
|
||||
|
|
|
@ -4818,6 +4818,30 @@ Signal Desktop makes use of the following open source projects.
|
|||
|
||||
License: MIT
|
||||
|
||||
## radix-ui
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 WorkOS
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## react
|
||||
|
||||
MIT License
|
||||
|
@ -5546,117 +5570,6 @@ Signal Desktop makes use of the following open source projects.
|
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## sanitize.css
|
||||
|
||||
# CC0 1.0 Universal
|
||||
|
||||
## Statement of Purpose
|
||||
|
||||
The laws of most jurisdictions throughout the world automatically confer
|
||||
exclusive Copyright and Related Rights (defined below) upon the creator and
|
||||
subsequent owner(s) (each and all, an “owner”) of an original work of
|
||||
authorship and/or a database (each, a “Work”).
|
||||
|
||||
Certain owners wish to permanently relinquish those rights to a Work for the
|
||||
purpose of contributing to a commons of creative, cultural and scientific works
|
||||
(“Commons”) that the public can reliably and without fear of later claims of
|
||||
infringement build upon, modify, incorporate in other works, reuse and
|
||||
redistribute as freely as possible in any form whatsoever and for any purposes,
|
||||
including without limitation commercial purposes. These owners may contribute
|
||||
to the Commons to promote the ideal of a free culture and the further
|
||||
production of creative, cultural and scientific works, or to gain reputation or
|
||||
greater distribution for their Work in part through the use and efforts of
|
||||
others.
|
||||
|
||||
For these and/or other purposes and motivations, and without any expectation of
|
||||
additional consideration or compensation, the person associating CC0 with a
|
||||
Work (the “Affirmer”), to the extent that he or she is an owner of Copyright
|
||||
and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and
|
||||
publicly distribute the Work under its terms, with knowledge of his or her
|
||||
Copyright and Related Rights in the Work and the meaning and intended legal
|
||||
effect of CC0 on those rights.
|
||||
|
||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||
protected by copyright and related or neighboring rights (“Copyright and
|
||||
Related Rights”). Copyright and Related Rights include, but are not limited
|
||||
to, the following:
|
||||
1. the right to reproduce, adapt, distribute, perform, display, communicate,
|
||||
and translate a Work;
|
||||
2. moral rights retained by the original author(s) and/or performer(s);
|
||||
3. publicity and privacy rights pertaining to a person’s image or likeness
|
||||
depicted in a Work;
|
||||
4. rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(i), below;
|
||||
5. rights protecting the extraction, dissemination, use and reuse of data in
|
||||
a Work;
|
||||
6. database rights (such as those arising under Directive 96/9/EC of the
|
||||
European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation thereof,
|
||||
including any amended or successor version of such directive); and
|
||||
7. other similar, equivalent or corresponding rights throughout the world
|
||||
based on applicable law or treaty, and any national implementations
|
||||
thereof.
|
||||
|
||||
2. Waiver. To the greatest extent permitted by, but not in contravention of,
|
||||
applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
|
||||
unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright
|
||||
and Related Rights and associated claims and causes of action, whether now
|
||||
known or unknown (including existing as well as future claims and causes of
|
||||
action), in the Work (i) in all territories worldwide, (ii) for the maximum
|
||||
duration provided by applicable law or treaty (including future time
|
||||
extensions), (iii) in any current or future medium and for any number of
|
||||
copies, and (iv) for any purpose whatsoever, including without limitation
|
||||
commercial, advertising or promotional purposes (the “Waiver”). Affirmer
|
||||
makes the Waiver for the benefit of each member of the public at large and
|
||||
to the detriment of Affirmer’s heirs and successors, fully intending that
|
||||
such Waiver shall not be subject to revocation, rescission, cancellation,
|
||||
termination, or any other legal or equitable action to disrupt the quiet
|
||||
enjoyment of the Work by the public as contemplated by Affirmer’s express
|
||||
Statement of Purpose.
|
||||
|
||||
3. Public License Fallback. Should any part of the Waiver for any reason be
|
||||
judged legally invalid or ineffective under applicable law, then the Waiver
|
||||
shall be preserved to the maximum extent permitted taking into account
|
||||
Affirmer’s express Statement of Purpose. In addition, to the extent the
|
||||
Waiver is so judged Affirmer hereby grants to each affected person a
|
||||
royalty-free, non transferable, non sublicensable, non exclusive,
|
||||
irrevocable and unconditional license to exercise Affirmer’s Copyright and
|
||||
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
||||
maximum duration provided by applicable law or treaty (including future time
|
||||
extensions), (iii) in any current or future medium and for any number of
|
||||
copies, and (iv) for any purpose whatsoever, including without limitation
|
||||
commercial, advertising or promotional purposes (the “License”). The License
|
||||
shall be deemed effective as of the date CC0 was applied by Affirmer to the
|
||||
Work. Should any part of the License for any reason be judged legally
|
||||
invalid or ineffective under applicable law, such partial invalidity or
|
||||
ineffectiveness shall not invalidate the remainder of the License, and in
|
||||
such case Affirmer hereby affirms that he or she will not (i) exercise any
|
||||
of his or her remaining Copyright and Related Rights in the Work or (ii)
|
||||
assert any associated claims and causes of action with respect to the Work,
|
||||
in either case contrary to Affirmer’s express Statement of Purpose.
|
||||
|
||||
4. Limitations and Disclaimers.
|
||||
1. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
2. Affirmer offers the Work as-is and makes no representations or warranties
|
||||
of any kind concerning the Work, express, implied, statutory or
|
||||
otherwise, including without limitation warranties of title,
|
||||
merchantability, fitness for a particular purpose, non infringement, or
|
||||
the absence of latent or other defects, accuracy, or the present or
|
||||
absence of errors, whether or not discoverable, all to the greatest
|
||||
extent permissible under applicable law.
|
||||
3. Affirmer disclaims responsibility for clearing rights of other persons
|
||||
that may apply to the Work or any use thereof, including without
|
||||
limitation any person’s Copyright and Related Rights in the Work.
|
||||
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||
consents, permissions or other rights required for any use of the Work.
|
||||
4. Affirmer understands and acknowledges that Creative Commons is not a
|
||||
party to this document and has no duty or obligation with respect to this
|
||||
CC0 or use of the Work.
|
||||
|
||||
For more information, please see
|
||||
http://creativecommons.org/publicdomain/zero/1.0/.
|
||||
|
||||
## semver
|
||||
|
||||
The ISC License
|
||||
|
|
|
@ -15,12 +15,8 @@
|
|||
script-src 'self';
|
||||
style-src 'self' 'unsafe-inline';"
|
||||
/>
|
||||
<link
|
||||
href="node_modules/sanitize.css/sanitize.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
|
||||
<link href="stylesheets/tailwind.css" rel="stylesheet" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
@ -76,6 +76,13 @@
|
|||
type="font/woff2"
|
||||
crossorigin
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="fonts/signal-symbols/SignalSymbolsVariable.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="images/logo-parts/base.svg"
|
||||
|
@ -89,18 +96,13 @@
|
|||
as="image"
|
||||
crossorigin
|
||||
/>
|
||||
|
||||
<link
|
||||
href="node_modules/sanitize.css/sanitize.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<link
|
||||
href="node_modules/@signalapp/quill-cjs/dist/quill.core.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
|
||||
<link href="stylesheets/tailwind.css" rel="stylesheet" type="text/css" />
|
||||
</head>
|
||||
<body class="overflow-hidden">
|
||||
<!-- Match ts/components/App.tsx -->
|
||||
|
|
|
@ -15,12 +15,8 @@
|
|||
script-src 'self';
|
||||
style-src 'self' 'unsafe-inline';"
|
||||
/>
|
||||
<link
|
||||
href="node_modules/sanitize.css/sanitize.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
|
||||
<link href="stylesheets/tailwind.css" rel="stylesheet" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
BIN
fonts/signal-symbols/SignalSymbolsVariable.woff2
Normal file
BIN
fonts/signal-symbols/SignalSymbolsVariable.woff2
Normal file
Binary file not shown.
|
@ -16,12 +16,8 @@
|
|||
script-src 'self';
|
||||
style-src 'self' 'unsafe-inline';"
|
||||
/>
|
||||
<link
|
||||
href="node_modules/sanitize.css/sanitize.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
|
||||
<link href="stylesheets/tailwind.css" rel="stylesheet" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-migration-screen app-loading-screen">
|
||||
|
|
23
package.json
23
package.json
|
@ -18,7 +18,7 @@
|
|||
"postinstall": "pnpm run build:acknowledgments && pnpm run electron:install-app-deps",
|
||||
"postuninstall": "pnpm run build:acknowledgments",
|
||||
"start": "electron .",
|
||||
"generate": "npm-run-all build-protobuf build:esbuild build:icu-types build:compact-locales sass get-expire-time copy-components",
|
||||
"generate": "npm-run-all build-protobuf build:esbuild build:icu-types build:compact-locales build:styles get-expire-time copy-components",
|
||||
"build-release": "pnpm run build",
|
||||
"sign-release": "node ts/updater/generateSignature.js",
|
||||
"notarize": "echo 'No longer necessary'",
|
||||
|
@ -30,7 +30,6 @@
|
|||
"mark-unusued-strings-deleted": "ts-node ./ts/scripts/mark-unused-strings-deleted.ts",
|
||||
"get-expire-time": "node ts/scripts/get-expire-time.js",
|
||||
"copy-components": "node ts/scripts/copy.js",
|
||||
"sass": "sass stylesheets/manifest.scss:stylesheets/manifest.css stylesheets/manifest_bridge.scss:stylesheets/manifest_bridge.css --fatal-deprecation=1.80.7",
|
||||
"build-module-protobuf": "pbjs --root='signal-desktop' --target static-module --force-long --no-typeurl --no-verify --no-create --no-convert --wrap commonjs --out ts/protobuf/compiled.js protos/*.proto && pbts --no-comments --out ts/protobuf/compiled.d.ts ts/protobuf/compiled.js",
|
||||
"clean-module-protobuf": "rm -f ts/protobuf/compiled.d.ts ts/protobuf/compiled.js",
|
||||
"build-protobuf": "pnpm run build-module-protobuf",
|
||||
|
@ -69,7 +68,9 @@
|
|||
"dev": "pnpm run build-protobuf && cross-env SIGNAL_ENV=storybook storybook dev --port 6006",
|
||||
"dev:transpile": "run-p \"check:types --watch\" dev:esbuild dev:icu-types dev:protobuf",
|
||||
"dev:esbuild": "node scripts/esbuild.js --watch",
|
||||
"dev:sass": "pnpm run sass --watch",
|
||||
"dev:styles": "pnpm run '/^dev:styles:(sass|tailwind)$/'",
|
||||
"dev:styles:sass": "pnpm run build:styles:sass --watch",
|
||||
"dev:styles:tailwind": "pnpm run build:styles:tailwind --watch",
|
||||
"dev:icu-types": "chokidar ./_locales/en/messages.json --initial --command \"pnpm run build:icu-types\"",
|
||||
"dev:protobuf": "chokidar ./protos/**/*.proto --command \"pnpm run build-protobuf\"",
|
||||
"build:storybook": "pnpm run build-protobuf && cross-env SIGNAL_ENV=storybook storybook build",
|
||||
|
@ -87,6 +88,9 @@
|
|||
"build:dev": "run-s --print-label generate build:esbuild:prod",
|
||||
"build:esbuild": "node scripts/esbuild.js",
|
||||
"build:esbuild:prod": "node scripts/esbuild.js --prod",
|
||||
"build:styles": "pnpm run \"/^build:styles:.*/\"",
|
||||
"build:styles:sass": "sass stylesheets/manifest.scss:stylesheets/manifest.css stylesheets/manifest_bridge.scss:stylesheets/manifest_bridge.css --fatal-deprecation=1.80.7",
|
||||
"build:styles:tailwind": "tailwindcss -i ./ts/axo/tailwind.css -o ./stylesheets/tailwind.css",
|
||||
"build:electron": "electron-builder --config.extraMetadata.environment=$SIGNAL_ENV",
|
||||
"build:release": "cross-env SIGNAL_ENV=production pnpm run build:electron --config.directories.output=release",
|
||||
"build:release-win32-all": "pnpm run build:release --arm64 --x64",
|
||||
|
@ -183,6 +187,7 @@
|
|||
"protobufjs": "7.3.2",
|
||||
"proxy-agent": "6.4.0",
|
||||
"qrcode-generator": "1.4.4",
|
||||
"radix-ui": "1.4.2",
|
||||
"react": "18.3.1",
|
||||
"react-aria": "3.35.1",
|
||||
"react-aria-components": "1.4.1",
|
||||
|
@ -199,7 +204,6 @@
|
|||
"redux-promise-middleware": "6.2.0",
|
||||
"redux-thunk": "3.1.0",
|
||||
"reselect": "5.1.1",
|
||||
"sanitize.css": "13.0.0",
|
||||
"semver": "7.6.3",
|
||||
"split2": "4.2.0",
|
||||
"tinykeys": "3.0.0",
|
||||
|
@ -244,6 +248,8 @@
|
|||
"@storybook/test": "8.4.4",
|
||||
"@storybook/test-runner": "0.22.0",
|
||||
"@storybook/types": "8.1.11",
|
||||
"@tailwindcss/cli": "4.1.7",
|
||||
"@tailwindcss/postcss": "4.1.7",
|
||||
"@types/backbone": "1.4.22",
|
||||
"@types/blueimp-load-image": "5.16.6",
|
||||
"@types/chai": "4.3.16",
|
||||
|
@ -305,6 +311,7 @@
|
|||
"eslint": "8.56.0",
|
||||
"eslint-config-airbnb-typescript-prettier": "5.0.0",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-plugin-better-tailwindcss": "3.7.2",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-local-rules": "1.3.2",
|
||||
"eslint-plugin-mocha": "10.1.0",
|
||||
|
@ -324,7 +331,10 @@
|
|||
"pixelmatch": "5.3.0",
|
||||
"playwright": "1.45.0",
|
||||
"pngjs": "7.0.0",
|
||||
"postcss": "8.5.3",
|
||||
"postcss-loader": "8.1.1",
|
||||
"prettier": "3.3.3",
|
||||
"prettier-plugin-tailwindcss": "0.6.11",
|
||||
"protobufjs-cli": "1.1.1",
|
||||
"react-devtools": "6.0.1",
|
||||
"react-devtools-core": "6.0.1",
|
||||
|
@ -340,6 +350,7 @@
|
|||
"stylelint-config-recommended-scss": "14.1.0",
|
||||
"stylelint-use-logical-spec": "5.0.1",
|
||||
"svgo": "3.3.2",
|
||||
"tailwindcss": "4.1.7",
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.6.3",
|
||||
|
@ -379,7 +390,8 @@
|
|||
"node-fetch@2.6.7": "patches/node-fetch+2.6.7.patch",
|
||||
"zod@3.23.8": "patches/zod+3.23.8.patch",
|
||||
"app-builder-lib": "patches/app-builder-lib.patch",
|
||||
"dmg-builder": "patches/dmg-builder.patch"
|
||||
"dmg-builder": "patches/dmg-builder.patch",
|
||||
"eslint-plugin-better-tailwindcss": "patches/eslint-plugin-better-tailwindcss.patch"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"@indutny/mac-screen-share",
|
||||
|
@ -390,6 +402,7 @@
|
|||
"@signalapp/ringrtc",
|
||||
"@signalapp/windows-ucv",
|
||||
"@swc/core",
|
||||
"@tailwindcss/oxide",
|
||||
"bufferutil",
|
||||
"electron",
|
||||
"esbuild",
|
||||
|
|
13
patches/eslint-plugin-better-tailwindcss.patch
Normal file
13
patches/eslint-plugin-better-tailwindcss.patch
Normal file
|
@ -0,0 +1,13 @@
|
|||
diff --git a/lib/cjs/tailwindcss/context.async.v4.js b/lib/cjs/tailwindcss/context.async.v4.js
|
||||
index 8464d9d949a0d82a53f3c6683c617b56770e5fba..ac392e6e8139f33650fc1691e5593660f87912cf 100644
|
||||
--- a/lib/cjs/tailwindcss/context.async.v4.js
|
||||
+++ b/lib/cjs/tailwindcss/context.async.v4.js
|
||||
@@ -59,7 +59,7 @@ const createTailwindContext = async (ctx) => (0, cache_js_1.withCache)("tailwind
|
||||
}
|
||||
const tailwindUrl = (0, platform_js_1.isWindows)() && (0, module_js_1.isESModule)() ? (0, node_url_1.pathToFileURL)(tailwindPath).toString() : tailwindPath;
|
||||
// eslint-disable-next-line eslint-plugin-typescript/naming-convention
|
||||
- const { __unstable__loadDesignSystem } = await Promise.resolve(`${tailwindUrl}`).then(s => __importStar(require(s)));
|
||||
+ const { __unstable__loadDesignSystem } = await import(`${tailwindUrl}`);
|
||||
let css = await (0, promises_1.readFile)(ctx.tailwindConfigPath, "utf-8");
|
||||
// Determine if the v4 API supports resolving `@import`
|
||||
let supportsImports = false;
|
|
@ -15,12 +15,8 @@
|
|||
script-src 'self';
|
||||
style-src 'self' 'unsafe-inline';"
|
||||
/>
|
||||
<link
|
||||
href="node_modules/sanitize.css/sanitize.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
|
||||
<link href="stylesheets/tailwind.css" rel="stylesheet" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
1886
pnpm-lock.yaml
generated
1886
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -15,12 +15,8 @@
|
|||
script-src 'self';
|
||||
style-src 'self' 'unsafe-inline';"
|
||||
/>
|
||||
<link
|
||||
href="node_modules/sanitize.css/sanitize.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
|
||||
<link href="stylesheets/tailwind.css" rel="stylesheet" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
@ -15,12 +15,8 @@
|
|||
script-src 'self';
|
||||
style-src 'self' 'unsafe-inline';"
|
||||
/>
|
||||
<link
|
||||
href="node_modules/sanitize.css/sanitize.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
|
||||
<link href="stylesheets/tailwind.css" rel="stylesheet" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
html {
|
||||
height: 100%;
|
||||
cursor: inherit;
|
||||
// Legacy style from sanitize.css:
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.light-theme {
|
||||
|
@ -157,10 +159,13 @@ audio {
|
|||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
font-size: inherit;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
button:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.grey {
|
||||
border-radius: 5px;
|
||||
border: solid 1px variables.$color-gray-25;
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
padding-inline: 12px 32px;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
color: variables.$color-black;
|
||||
|
||||
@include mixins.dark-theme {
|
||||
background-color: variables.$color-gray-90;
|
||||
|
|
|
@ -43,6 +43,7 @@ $input-padding-inline: 12px;
|
|||
$input-padding-inline + $icon-actual-size + $icon-margin-inline-start
|
||||
);
|
||||
@include mixins.font-body-1;
|
||||
color: light-dark(variables.$color-black, variables.$color-white);
|
||||
background: light-dark(variables.$color-gray-05, variables.$color-gray-80);
|
||||
|
||||
&:focus {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
<title>TextSecure test runner</title>
|
||||
<link rel="stylesheet" href="../node_modules/mocha/mocha.css" />
|
||||
<link rel="stylesheet" href="../stylesheets/manifest.css" />
|
||||
<link rel="stylesheet" href="../stylesheets/tailwind.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="mocha"></div>
|
||||
|
|
90
ts/axo/AxoButton.stories.tsx
Normal file
90
ts/axo/AxoButton.stories.tsx
Normal file
|
@ -0,0 +1,90 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React from 'react';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import {
|
||||
_getAllAxoButtonVariants,
|
||||
_getAllAxoButtonSizes,
|
||||
AxoButton,
|
||||
} from './AxoButton';
|
||||
|
||||
export default {
|
||||
title: 'Axo/AxoButton',
|
||||
} satisfies Meta;
|
||||
|
||||
export function Basic(): JSX.Element {
|
||||
const variants = _getAllAxoButtonVariants();
|
||||
const sizes = _getAllAxoButtonSizes();
|
||||
return (
|
||||
<div className="grid gap-1">
|
||||
{sizes.map(size => {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="type-title-medium">Size: {size}</h2>
|
||||
{variants.map(variant => {
|
||||
return (
|
||||
<div key={variant} className="flex gap-1">
|
||||
<AxoButton
|
||||
variant={variant}
|
||||
size={size}
|
||||
onClick={action('click')}
|
||||
>
|
||||
{variant}
|
||||
</AxoButton>
|
||||
|
||||
<AxoButton
|
||||
variant={variant}
|
||||
size={size}
|
||||
onClick={action('click')}
|
||||
disabled
|
||||
>
|
||||
Disabled
|
||||
</AxoButton>
|
||||
|
||||
<AxoButton
|
||||
symbol="info"
|
||||
variant={variant}
|
||||
size={size}
|
||||
onClick={action('click')}
|
||||
>
|
||||
Icon
|
||||
</AxoButton>
|
||||
|
||||
<AxoButton
|
||||
symbol="info"
|
||||
variant={variant}
|
||||
size={size}
|
||||
onClick={action('click')}
|
||||
disabled
|
||||
>
|
||||
Disabled
|
||||
</AxoButton>
|
||||
|
||||
<AxoButton
|
||||
arrow
|
||||
variant={variant}
|
||||
size={size}
|
||||
onClick={action('click')}
|
||||
>
|
||||
Arrow
|
||||
</AxoButton>
|
||||
|
||||
<AxoButton
|
||||
arrow
|
||||
variant={variant}
|
||||
size={size}
|
||||
onClick={action('click')}
|
||||
disabled
|
||||
>
|
||||
Disabled
|
||||
</AxoButton>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
195
ts/axo/AxoButton.tsx
Normal file
195
ts/axo/AxoButton.tsx
Normal file
|
@ -0,0 +1,195 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { memo, forwardRef } from 'react';
|
||||
import type { ButtonHTMLAttributes, FC, ForwardedRef, ReactNode } from 'react';
|
||||
import type { Styles } from './_internal/css';
|
||||
import { css } from './_internal/css';
|
||||
import { AxoSymbol, type AxoSymbolName } from './AxoSymbol';
|
||||
import { assert } from './_internal/assert';
|
||||
|
||||
const Namespace = 'AxoButton';
|
||||
|
||||
const baseAxoButtonStyles = css(
|
||||
'flex items-center-safe justify-center-safe gap-1 truncate rounded-full select-none',
|
||||
'outline-0 outline-border-focused focused:outline-[2.5px]'
|
||||
);
|
||||
|
||||
const AxoButtonTypes = {
|
||||
default: css(baseAxoButtonStyles),
|
||||
subtle: css(
|
||||
baseAxoButtonStyles,
|
||||
'bg-fill-secondary',
|
||||
'pressed:bg-fill-secondary-pressed'
|
||||
),
|
||||
floating: css(
|
||||
baseAxoButtonStyles,
|
||||
'bg-fill-floating',
|
||||
'shadow-elevation-1',
|
||||
'pressed:bg-fill-floating-pressed'
|
||||
),
|
||||
borderless: css(
|
||||
baseAxoButtonStyles,
|
||||
'bg-transparent',
|
||||
'hovered:bg-fill-secondary',
|
||||
'pressed:bg-fill-secondary-pressed'
|
||||
),
|
||||
} as const satisfies Record<string, Styles>;
|
||||
|
||||
const AxoButtonVariants = {
|
||||
// default
|
||||
secondary: css(
|
||||
AxoButtonTypes.default,
|
||||
'bg-fill-secondary text-label-primary',
|
||||
'pressed:bg-fill-secondary-pressed',
|
||||
'disabled:text-label-disabled'
|
||||
),
|
||||
primary: css(
|
||||
AxoButtonTypes.default,
|
||||
'bg-color-fill-primary text-label-primary-on-color',
|
||||
'pressed:bg-color-fill-primary-pressed',
|
||||
'disabled:text-label-disabled-on-color'
|
||||
),
|
||||
affirmative: css(
|
||||
AxoButtonTypes.default,
|
||||
'bg-color-fill-affirmative text-label-primary-on-color',
|
||||
'pressed:bg-color-fill-affirmative-pressed',
|
||||
'disabled:text-label-disabled-on-color'
|
||||
),
|
||||
destructive: css(
|
||||
AxoButtonTypes.default,
|
||||
'bg-color-fill-destructive text-label-primary-on-color',
|
||||
'pressed:bg-color-fill-destructive-pressed',
|
||||
'disabled:text-label-disabled-on-color'
|
||||
),
|
||||
|
||||
// subtle
|
||||
'subtle-primary': css(
|
||||
AxoButtonTypes.subtle,
|
||||
'text-color-label-primary',
|
||||
'disabled:text-color-label-primary-disabled'
|
||||
),
|
||||
'subtle-affirmative': css(
|
||||
AxoButtonTypes.subtle,
|
||||
'text-color-label-affirmative',
|
||||
'disabled:text-color-label-affirmative-disabled'
|
||||
),
|
||||
'subtle-destructive': css(
|
||||
AxoButtonTypes.subtle,
|
||||
'text-color-label-destructive',
|
||||
'disabled:text-color-label-destructive-disabled'
|
||||
),
|
||||
|
||||
// floating
|
||||
'floating-secondary': css(
|
||||
AxoButtonTypes.floating,
|
||||
'text-label-primary',
|
||||
'disabled:text-label-disabled'
|
||||
),
|
||||
'floating-primary': css(
|
||||
AxoButtonTypes.floating,
|
||||
'text-color-label-primary',
|
||||
'disabled:text-color-label-primary-disabled'
|
||||
),
|
||||
'floating-affirmative': css(
|
||||
AxoButtonTypes.floating,
|
||||
'text-color-label-affirmative',
|
||||
'disabled:text-color-label-affirmative-disabled'
|
||||
),
|
||||
'floating-destructive': css(
|
||||
AxoButtonTypes.floating,
|
||||
'text-color-label-destructive',
|
||||
'disabled:text-color-label-destructive-disabled'
|
||||
),
|
||||
|
||||
// borderless
|
||||
'borderless-secondary': css(
|
||||
AxoButtonTypes.borderless,
|
||||
'text-label-primary',
|
||||
'disabled:text-label-disabled'
|
||||
),
|
||||
'borderless-primary': css(
|
||||
AxoButtonTypes.borderless,
|
||||
'text-color-label-primary',
|
||||
'disabled:text-color-label-primary-disabled'
|
||||
),
|
||||
'borderless-affirmative': css(
|
||||
AxoButtonTypes.borderless,
|
||||
'text-color-label-affirmative',
|
||||
'disabled:text-color-label-affirmative-disabled'
|
||||
),
|
||||
'borderless-destructive': css(
|
||||
AxoButtonTypes.borderless,
|
||||
'text-color-label-destructive',
|
||||
'disabled:text-color-label-destructive-disabled'
|
||||
),
|
||||
};
|
||||
|
||||
const AxoButtonSizes = {
|
||||
large: css('px-4 py-2 type-body-medium font-medium'),
|
||||
medium: css('px-3 py-1.5 type-body-medium font-medium'),
|
||||
small: css('px-2 py-1 type-body-small font-medium'),
|
||||
} as const satisfies Record<string, Styles>;
|
||||
|
||||
type BaseButtonAttrs = Omit<
|
||||
ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
'className' | 'style' | 'children'
|
||||
>;
|
||||
|
||||
type AxoButtonVariant = keyof typeof AxoButtonVariants;
|
||||
type AxoButtonSize = keyof typeof AxoButtonSizes;
|
||||
|
||||
type AxoButtonProps = BaseButtonAttrs &
|
||||
Readonly<{
|
||||
variant: AxoButtonVariant;
|
||||
size: AxoButtonSize;
|
||||
symbol?: AxoSymbolName;
|
||||
arrow?: boolean;
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export function _getAllAxoButtonVariants(): ReadonlyArray<AxoButtonVariant> {
|
||||
return Object.keys(AxoButtonVariants) as Array<AxoButtonVariant>;
|
||||
}
|
||||
|
||||
export function _getAllAxoButtonSizes(): ReadonlyArray<AxoButtonSize> {
|
||||
return Object.keys(AxoButtonSizes) as Array<AxoButtonSize>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/export
|
||||
export const AxoButton: FC<AxoButtonProps> = memo(
|
||||
forwardRef((props, ref: ForwardedRef<HTMLButtonElement>) => {
|
||||
const { variant, size, symbol, arrow, children, ...rest } = props;
|
||||
const variantStyles = assert(
|
||||
AxoButtonVariants[variant],
|
||||
`${Namespace}: Invalid variant ${variant}`
|
||||
);
|
||||
const sizeStyles = assert(
|
||||
AxoButtonSizes[size],
|
||||
`${Namespace}: Invalid size ${size}`
|
||||
);
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type="button"
|
||||
className={css(variantStyles, sizeStyles)}
|
||||
{...rest}
|
||||
>
|
||||
{symbol != null && (
|
||||
<AxoSymbol.InlineGlyph symbol={symbol} label={null} />
|
||||
)}
|
||||
{children}
|
||||
{arrow && <AxoSymbol.InlineGlyph symbol="chevron-[end]" label={null} />}
|
||||
</button>
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
AxoButton.displayName = `${Namespace}`;
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace, @typescript-eslint/no-redeclare, import/export
|
||||
export namespace AxoButton {
|
||||
export type Variant = AxoButtonVariant;
|
||||
export type Size = AxoButtonSize;
|
||||
export type Props = AxoButtonProps;
|
||||
}
|
99
ts/axo/AxoContextMenu.stories.tsx
Normal file
99
ts/axo/AxoContextMenu.stories.tsx
Normal file
|
@ -0,0 +1,99 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { useState } from 'react';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { AxoContextMenu } from './AxoContextMenu';
|
||||
|
||||
export default {
|
||||
title: 'Axo/AxoContextMenu',
|
||||
} satisfies Meta;
|
||||
|
||||
export function Basic(): JSX.Element {
|
||||
const [showBookmarks, setShowBookmarks] = useState(true);
|
||||
const [showFullUrls, setShowFullUrls] = useState(false);
|
||||
const [selectedPerson, setSelectedPerson] = useState('jamie');
|
||||
return (
|
||||
<div className="flex h-96 w-full items-center justify-center">
|
||||
<AxoContextMenu.Root>
|
||||
<AxoContextMenu.Trigger>
|
||||
<div className="bg-fill-secondary p-12 text-color-label-primary">
|
||||
Right-Click
|
||||
</div>
|
||||
</AxoContextMenu.Trigger>
|
||||
<AxoContextMenu.Content>
|
||||
<AxoContextMenu.Item
|
||||
symbol="arrow-[start]"
|
||||
onSelect={action('back')}
|
||||
keyboardShortcut="⌘["
|
||||
>
|
||||
Back
|
||||
</AxoContextMenu.Item>
|
||||
<AxoContextMenu.Item
|
||||
disabled
|
||||
symbol="arrow-[end]"
|
||||
onSelect={action('forward')}
|
||||
keyboardShortcut="⌘]"
|
||||
>
|
||||
Forward
|
||||
</AxoContextMenu.Item>
|
||||
<AxoContextMenu.Item
|
||||
onSelect={action('reload')}
|
||||
keyboardShortcut="⌘R"
|
||||
>
|
||||
Reload
|
||||
</AxoContextMenu.Item>
|
||||
<AxoContextMenu.Sub>
|
||||
<AxoContextMenu.SubTrigger>More Tools</AxoContextMenu.SubTrigger>
|
||||
<AxoContextMenu.SubContent>
|
||||
<AxoContextMenu.Item
|
||||
onSelect={action('savePageAs')}
|
||||
keyboardShortcut="⌘S"
|
||||
>
|
||||
Save Page As...
|
||||
</AxoContextMenu.Item>
|
||||
<AxoContextMenu.Item onSelect={action('createShortcut')}>
|
||||
Create Shortcut...
|
||||
</AxoContextMenu.Item>
|
||||
<AxoContextMenu.Item onSelect={action('nameWindow')}>
|
||||
Name Window...
|
||||
</AxoContextMenu.Item>
|
||||
<AxoContextMenu.Separator />
|
||||
<AxoContextMenu.Item onSelect={action('developerTools')}>
|
||||
Developer Tools
|
||||
</AxoContextMenu.Item>
|
||||
</AxoContextMenu.SubContent>
|
||||
</AxoContextMenu.Sub>
|
||||
<AxoContextMenu.Separator />
|
||||
<AxoContextMenu.CheckboxItem
|
||||
checked={showBookmarks}
|
||||
onCheckedChange={setShowBookmarks}
|
||||
keyboardShortcut="⌘B"
|
||||
>
|
||||
Show Bookmarks
|
||||
</AxoContextMenu.CheckboxItem>
|
||||
<AxoContextMenu.CheckboxItem
|
||||
symbol="link"
|
||||
checked={showFullUrls}
|
||||
onCheckedChange={setShowFullUrls}
|
||||
>
|
||||
Show Full URLs
|
||||
</AxoContextMenu.CheckboxItem>
|
||||
<AxoContextMenu.Separator />
|
||||
<AxoContextMenu.Label>People</AxoContextMenu.Label>
|
||||
<AxoContextMenu.RadioGroup
|
||||
value={selectedPerson}
|
||||
onValueChange={setSelectedPerson}
|
||||
>
|
||||
<AxoContextMenu.RadioItem value="jamie">
|
||||
Jamie
|
||||
</AxoContextMenu.RadioItem>
|
||||
<AxoContextMenu.RadioItem value="tyler">
|
||||
Tyler
|
||||
</AxoContextMenu.RadioItem>
|
||||
</AxoContextMenu.RadioGroup>
|
||||
</AxoContextMenu.Content>
|
||||
</AxoContextMenu.Root>
|
||||
</div>
|
||||
);
|
||||
}
|
388
ts/axo/AxoContextMenu.tsx
Normal file
388
ts/axo/AxoContextMenu.tsx
Normal file
|
@ -0,0 +1,388 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { memo } from 'react';
|
||||
import { ContextMenu } from 'radix-ui';
|
||||
import type { FC } from 'react';
|
||||
import { AxoSymbol } from './AxoSymbol';
|
||||
import { AxoBaseMenu } from './_internal/AxoBaseMenu';
|
||||
|
||||
const Namespace = 'AxoContextMenu';
|
||||
|
||||
/**
|
||||
* Displays a menu located at the pointer, triggered by a right click or a long press.
|
||||
*
|
||||
* Note: For menus that are triggered by a normal button press, you should use
|
||||
* `AxoDropdownMenu`.
|
||||
*
|
||||
* @example Anatomy
|
||||
* ```tsx
|
||||
* import { AxoContextMenu } from "./axo/ContextMenu/AxoContentMenu.tsx";
|
||||
*
|
||||
* export default () => (
|
||||
* <AxoContextMenu.Root>
|
||||
* <AxoContextMenu.Trigger />
|
||||
*
|
||||
* <AxoContextMenu.Content>
|
||||
* <AxoContextMenu.Label />
|
||||
* <AxoContextMenu.Item />
|
||||
*
|
||||
* <AxoContextMenu.Group>
|
||||
* <AxoContextMenu.Item />
|
||||
* </AxoContextMenu.Group>
|
||||
*
|
||||
* <AxoContextMenu.CheckboxItem/>
|
||||
*
|
||||
* <AxoContextMenu.RadioGroup>
|
||||
* <AxoContextMenu.RadioItem/>
|
||||
* </AxoContextMenu.RadioGroup>
|
||||
*
|
||||
* <AxoContextMenu.Sub>
|
||||
* <AxoContextMenu.SubTrigger />
|
||||
* <AxoContextMenu.SubContent />
|
||||
* </AxoContextMenu.Sub>
|
||||
*
|
||||
* <AxoContextMenu.Separator />
|
||||
* </AxoContextMenu.Content>
|
||||
* </AxoContextMenu.Root>
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace AxoContextMenu {
|
||||
/**
|
||||
* Component: <AxoContextMenu.Root>
|
||||
* --------------------------------
|
||||
*/
|
||||
|
||||
export type RootProps = AxoBaseMenu.MenuRootProps;
|
||||
|
||||
export const Root: FC<RootProps> = memo(props => {
|
||||
return <ContextMenu.Root>{props.children}</ContextMenu.Root>;
|
||||
});
|
||||
|
||||
Root.displayName = `${Namespace}.Root`;
|
||||
|
||||
/**
|
||||
* Component: <AxoContextMenu.Trigger>
|
||||
* -----------------------------------
|
||||
*/
|
||||
|
||||
export type TriggerProps = AxoBaseMenu.MenuTriggerProps;
|
||||
|
||||
export const Trigger: FC<TriggerProps> = memo(props => {
|
||||
return <ContextMenu.Trigger>{props.children}</ContextMenu.Trigger>;
|
||||
});
|
||||
|
||||
Trigger.displayName = `${Namespace}.Trigger`;
|
||||
|
||||
/**
|
||||
* Component: <AxoContextMenu.Content>
|
||||
* -----------------------------------
|
||||
*/
|
||||
|
||||
export type ContentProps = AxoBaseMenu.MenuContentProps;
|
||||
|
||||
/**
|
||||
* The component that pops out in an open context menu.
|
||||
* Uses a portal to render the content part into the `body`.
|
||||
*/
|
||||
export const Content: FC<ContentProps> = memo(props => {
|
||||
return (
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Content
|
||||
className={AxoBaseMenu.menuContentStyles}
|
||||
alignOffset={-6}
|
||||
collisionPadding={6}
|
||||
>
|
||||
{props.children}
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Portal>
|
||||
);
|
||||
});
|
||||
|
||||
Content.displayName = `${Namespace}.Content`;
|
||||
|
||||
/**
|
||||
* Component: <AxoContextMenu.Item>
|
||||
* --------------------------------
|
||||
*/
|
||||
|
||||
export type ItemProps = AxoBaseMenu.MenuItemProps;
|
||||
|
||||
/**
|
||||
* The component that contains the context menu items.
|
||||
* @example
|
||||
* ```tsx
|
||||
* <AxoContextMenu.Item icon={<svg/>}>
|
||||
* {i18n("myContextMenuText")}
|
||||
* </AxoContentMenu.Item>
|
||||
* ````
|
||||
*/
|
||||
export const Item: FC<ItemProps> = memo(props => {
|
||||
return (
|
||||
<ContextMenu.Item
|
||||
disabled={props.disabled}
|
||||
textValue={props.textValue}
|
||||
onSelect={props.onSelect}
|
||||
className={AxoBaseMenu.menuItemStyles}
|
||||
>
|
||||
{props.symbol && (
|
||||
<AxoBaseMenu.ItemLeadingSlot>
|
||||
<AxoBaseMenu.ItemSymbol symbol={props.symbol} />
|
||||
</AxoBaseMenu.ItemLeadingSlot>
|
||||
)}
|
||||
<AxoBaseMenu.ItemContentSlot>
|
||||
<AxoBaseMenu.ItemText>{props.children}</AxoBaseMenu.ItemText>
|
||||
{props.keyboardShortcut && (
|
||||
<AxoBaseMenu.ItemKeyboardShortcut
|
||||
keyboardShortcut={props.keyboardShortcut}
|
||||
/>
|
||||
)}
|
||||
</AxoBaseMenu.ItemContentSlot>
|
||||
</ContextMenu.Item>
|
||||
);
|
||||
});
|
||||
|
||||
Item.displayName = `${Namespace}.Item`;
|
||||
|
||||
/**
|
||||
* Component: <AxoContextMenu.Group>
|
||||
* ---------------------------------
|
||||
*/
|
||||
|
||||
export type GroupProps = AxoBaseMenu.MenuGroupProps;
|
||||
|
||||
/**
|
||||
* Used to group multiple {@link AxoContextMenu.Item}'s.
|
||||
*/
|
||||
export const Group: FC<GroupProps> = memo(props => {
|
||||
return (
|
||||
<ContextMenu.Group className={AxoBaseMenu.menuGroupStyles}>
|
||||
{props.children}
|
||||
</ContextMenu.Group>
|
||||
);
|
||||
});
|
||||
|
||||
Group.displayName = `${Namespace}.Group`;
|
||||
|
||||
/**
|
||||
* Component: <AxoContextMenu.Label>
|
||||
* ---------------------------------
|
||||
*/
|
||||
|
||||
export type LabelProps = AxoBaseMenu.MenuLabelProps;
|
||||
|
||||
/**
|
||||
* Used to render a label. It won't be focusable using arrow keys.
|
||||
*/
|
||||
export const Label: FC<LabelProps> = memo(props => {
|
||||
return (
|
||||
<ContextMenu.Label className={AxoBaseMenu.menuLabelStyles}>
|
||||
<AxoBaseMenu.ItemContentSlot>
|
||||
<AxoBaseMenu.ItemText>{props.children}</AxoBaseMenu.ItemText>
|
||||
</AxoBaseMenu.ItemContentSlot>
|
||||
</ContextMenu.Label>
|
||||
);
|
||||
});
|
||||
|
||||
Label.displayName = `${Namespace}.Label`;
|
||||
|
||||
/**
|
||||
* Component: <AxoContextMenu.CheckboxItem>
|
||||
* ----------------------------------------
|
||||
*/
|
||||
|
||||
export type CheckboxItemProps = AxoBaseMenu.MenuCheckboxItemProps;
|
||||
|
||||
/**
|
||||
* An item that can be controlled and rendered like a checkbox.
|
||||
*/
|
||||
export const CheckboxItem: FC<CheckboxItemProps> = memo(props => {
|
||||
return (
|
||||
<ContextMenu.CheckboxItem
|
||||
textValue={props.textValue}
|
||||
disabled={props.disabled}
|
||||
checked={props.checked}
|
||||
onCheckedChange={props.onCheckedChange}
|
||||
onSelect={props.onSelect}
|
||||
className={AxoBaseMenu.menuCheckboxItemStyles}
|
||||
>
|
||||
<AxoBaseMenu.ItemLeadingSlot>
|
||||
<AxoBaseMenu.ItemCheckPlaceholder>
|
||||
<ContextMenu.ItemIndicator>
|
||||
<AxoBaseMenu.ItemCheck />
|
||||
</ContextMenu.ItemIndicator>
|
||||
</AxoBaseMenu.ItemCheckPlaceholder>
|
||||
</AxoBaseMenu.ItemLeadingSlot>
|
||||
<AxoBaseMenu.ItemContentSlot>
|
||||
{props.symbol && (
|
||||
<span className="mr-2">
|
||||
<AxoBaseMenu.ItemSymbol symbol={props.symbol} />
|
||||
</span>
|
||||
)}
|
||||
<AxoBaseMenu.ItemText>{props.children}</AxoBaseMenu.ItemText>
|
||||
{props.keyboardShortcut && (
|
||||
<AxoBaseMenu.ItemKeyboardShortcut
|
||||
keyboardShortcut={props.keyboardShortcut}
|
||||
/>
|
||||
)}
|
||||
</AxoBaseMenu.ItemContentSlot>
|
||||
</ContextMenu.CheckboxItem>
|
||||
);
|
||||
});
|
||||
|
||||
CheckboxItem.displayName = `${Namespace}.CheckboxItem`;
|
||||
|
||||
/**
|
||||
* Component: <AxoContextMenu.RadioGroup>
|
||||
* --------------------------------------
|
||||
*/
|
||||
|
||||
export type RadioGroupProps = AxoBaseMenu.MenuRadioGroupProps;
|
||||
|
||||
/**
|
||||
* Used to group multiple {@link AxoContextMenu.RadioItem}'s.
|
||||
*/
|
||||
export const RadioGroup: FC<RadioGroupProps> = memo(props => {
|
||||
return (
|
||||
<ContextMenu.RadioGroup
|
||||
value={props.value}
|
||||
onValueChange={props.onValueChange}
|
||||
className={AxoBaseMenu.menuRadioGroupStyles}
|
||||
>
|
||||
{props.children}
|
||||
</ContextMenu.RadioGroup>
|
||||
);
|
||||
});
|
||||
|
||||
RadioGroup.displayName = `${Namespace}.RadioGroup`;
|
||||
|
||||
/**
|
||||
* Component: <AxoContextMenu.RadioItem>
|
||||
* -------------------------------------
|
||||
*/
|
||||
|
||||
export type RadioItemProps = AxoBaseMenu.MenuRadioItemProps;
|
||||
|
||||
/**
|
||||
* An item that can be controlled and rendered like a radio.
|
||||
*/
|
||||
export const RadioItem: FC<RadioItemProps> = memo(props => {
|
||||
return (
|
||||
<ContextMenu.RadioItem
|
||||
value={props.value}
|
||||
className={AxoBaseMenu.menuRadioItemStyles}
|
||||
onSelect={props.onSelect}
|
||||
>
|
||||
<AxoBaseMenu.ItemLeadingSlot>
|
||||
<AxoBaseMenu.ItemCheckPlaceholder>
|
||||
<ContextMenu.ItemIndicator>
|
||||
<AxoBaseMenu.ItemCheck />
|
||||
</ContextMenu.ItemIndicator>
|
||||
</AxoBaseMenu.ItemCheckPlaceholder>
|
||||
</AxoBaseMenu.ItemLeadingSlot>
|
||||
<AxoBaseMenu.ItemContentSlot>
|
||||
{props.symbol && <AxoBaseMenu.ItemSymbol symbol={props.symbol} />}
|
||||
<AxoBaseMenu.ItemText>{props.children}</AxoBaseMenu.ItemText>
|
||||
{props.keyboardShortcut && (
|
||||
<AxoBaseMenu.ItemKeyboardShortcut
|
||||
keyboardShortcut={props.keyboardShortcut}
|
||||
/>
|
||||
)}
|
||||
</AxoBaseMenu.ItemContentSlot>
|
||||
</ContextMenu.RadioItem>
|
||||
);
|
||||
});
|
||||
|
||||
RadioItem.displayName = `${Namespace}.RadioItem`;
|
||||
|
||||
/**
|
||||
* Component: <AxoContextMenu.Separator>
|
||||
* -------------------------------------
|
||||
*/
|
||||
|
||||
export type SeparatorProps = AxoBaseMenu.MenuSeparatorProps;
|
||||
|
||||
/**
|
||||
* Used to visually separate items in the context menu.
|
||||
*/
|
||||
export const Separator: FC<SeparatorProps> = memo(() => {
|
||||
return (
|
||||
<ContextMenu.Separator className={AxoBaseMenu.menuSeparatorStyles} />
|
||||
);
|
||||
});
|
||||
|
||||
Separator.displayName = `${Namespace}.Separator`;
|
||||
|
||||
/**
|
||||
* Component: <AxoContextMenu.Sub>
|
||||
* -------------------------------
|
||||
*/
|
||||
|
||||
export type SubProps = AxoBaseMenu.MenuSubProps;
|
||||
|
||||
/**
|
||||
* Contains all the parts of a submenu.
|
||||
*/
|
||||
export const Sub: FC<SubProps> = memo(props => {
|
||||
return <ContextMenu.Sub>{props.children}</ContextMenu.Sub>;
|
||||
});
|
||||
|
||||
Sub.displayName = `${Namespace}.Sub`;
|
||||
|
||||
/**
|
||||
* Component: <AxoContextMenu.SubTrigger>
|
||||
* --------------------------------------
|
||||
*/
|
||||
|
||||
export type SubTriggerProps = AxoBaseMenu.MenuSubTriggerProps;
|
||||
|
||||
/**
|
||||
* An item that opens a submenu. Must be rendered inside
|
||||
* {@link ContextMenu.Sub}.
|
||||
*/
|
||||
export const SubTrigger: FC<SubTriggerProps> = memo(props => {
|
||||
return (
|
||||
<ContextMenu.SubTrigger className={AxoBaseMenu.menuSubTriggerStyles}>
|
||||
{props.symbol && (
|
||||
<AxoBaseMenu.ItemLeadingSlot>
|
||||
<AxoBaseMenu.ItemSymbol symbol={props.symbol} />
|
||||
</AxoBaseMenu.ItemLeadingSlot>
|
||||
)}
|
||||
<AxoBaseMenu.ItemContentSlot>
|
||||
<AxoBaseMenu.ItemText>{props.children}</AxoBaseMenu.ItemText>
|
||||
<span className="ml-auto">
|
||||
<AxoSymbol.Icon size={14} symbol="chevron-[end]" label={null} />
|
||||
</span>
|
||||
</AxoBaseMenu.ItemContentSlot>
|
||||
</ContextMenu.SubTrigger>
|
||||
);
|
||||
});
|
||||
|
||||
SubTrigger.displayName = `${Namespace}.SubTrigger`;
|
||||
|
||||
/**
|
||||
* Component: <AxoContextMenu.SubContent>
|
||||
* --------------------------------------
|
||||
*/
|
||||
|
||||
export type SubContentProps = AxoBaseMenu.MenuSubContentProps;
|
||||
|
||||
/**
|
||||
* The component that pops out when a submenu is open. Must be rendered
|
||||
* inside {@link AxoContextMenu.Sub}.
|
||||
*/
|
||||
export const SubContent: FC<SubContentProps> = memo(props => {
|
||||
return (
|
||||
<ContextMenu.SubContent
|
||||
alignOffset={-6}
|
||||
collisionPadding={6}
|
||||
className={AxoBaseMenu.menuSubContentStyles}
|
||||
>
|
||||
{props.children}
|
||||
</ContextMenu.SubContent>
|
||||
);
|
||||
});
|
||||
|
||||
SubContent.displayName = `${Namespace}.SubContent`;
|
||||
}
|
100
ts/axo/AxoDropdownMenu.stories.tsx
Normal file
100
ts/axo/AxoDropdownMenu.stories.tsx
Normal file
|
@ -0,0 +1,100 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { useState } from 'react';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { AxoDropdownMenu } from './AxoDropdownMenu';
|
||||
import { AxoButton } from './AxoButton';
|
||||
|
||||
export default {
|
||||
title: 'Axo/AxoDropdownMenu',
|
||||
} satisfies Meta;
|
||||
|
||||
export function Basic(): JSX.Element {
|
||||
const [showBookmarks, setShowBookmarks] = useState(true);
|
||||
const [showFullUrls, setShowFullUrls] = useState(false);
|
||||
const [selectedPerson, setSelectedPerson] = useState('jamie');
|
||||
return (
|
||||
<div className="flex h-96 w-full items-center justify-center">
|
||||
<AxoDropdownMenu.Root>
|
||||
<AxoDropdownMenu.Trigger>
|
||||
<AxoButton variant="secondary" size="medium">
|
||||
Open Dropdown Menu
|
||||
</AxoButton>
|
||||
</AxoDropdownMenu.Trigger>
|
||||
<AxoDropdownMenu.Content>
|
||||
<AxoDropdownMenu.Item
|
||||
symbol="arrow-[start]"
|
||||
onSelect={action('back')}
|
||||
keyboardShortcut="⌘["
|
||||
>
|
||||
Back
|
||||
</AxoDropdownMenu.Item>
|
||||
<AxoDropdownMenu.Item
|
||||
disabled
|
||||
symbol="arrow-[end]"
|
||||
onSelect={action('forward')}
|
||||
keyboardShortcut="⌘]"
|
||||
>
|
||||
Forward
|
||||
</AxoDropdownMenu.Item>
|
||||
<AxoDropdownMenu.Item
|
||||
onSelect={action('reload')}
|
||||
keyboardShortcut="⌘R"
|
||||
>
|
||||
Reload
|
||||
</AxoDropdownMenu.Item>
|
||||
<AxoDropdownMenu.Sub>
|
||||
<AxoDropdownMenu.SubTrigger>More Tools</AxoDropdownMenu.SubTrigger>
|
||||
<AxoDropdownMenu.SubContent>
|
||||
<AxoDropdownMenu.Item
|
||||
onSelect={action('savePageAs')}
|
||||
keyboardShortcut="⌘S"
|
||||
>
|
||||
Save Page As...
|
||||
</AxoDropdownMenu.Item>
|
||||
<AxoDropdownMenu.Item onSelect={action('createShortcut')}>
|
||||
Create Shortcut...
|
||||
</AxoDropdownMenu.Item>
|
||||
<AxoDropdownMenu.Item onSelect={action('nameWindow')}>
|
||||
Name Window...
|
||||
</AxoDropdownMenu.Item>
|
||||
<AxoDropdownMenu.Separator />
|
||||
<AxoDropdownMenu.Item onSelect={action('developerTools')}>
|
||||
Developer Tools
|
||||
</AxoDropdownMenu.Item>
|
||||
</AxoDropdownMenu.SubContent>
|
||||
</AxoDropdownMenu.Sub>
|
||||
<AxoDropdownMenu.Separator />
|
||||
<AxoDropdownMenu.CheckboxItem
|
||||
checked={showBookmarks}
|
||||
onCheckedChange={setShowBookmarks}
|
||||
keyboardShortcut="⌘B"
|
||||
>
|
||||
Show Bookmarks
|
||||
</AxoDropdownMenu.CheckboxItem>
|
||||
<AxoDropdownMenu.CheckboxItem
|
||||
symbol="link"
|
||||
checked={showFullUrls}
|
||||
onCheckedChange={setShowFullUrls}
|
||||
>
|
||||
Show Full URLs
|
||||
</AxoDropdownMenu.CheckboxItem>
|
||||
<AxoDropdownMenu.Separator />
|
||||
<AxoDropdownMenu.Label>People</AxoDropdownMenu.Label>
|
||||
<AxoDropdownMenu.RadioGroup
|
||||
value={selectedPerson}
|
||||
onValueChange={setSelectedPerson}
|
||||
>
|
||||
<AxoDropdownMenu.RadioItem value="jamie">
|
||||
Jamie
|
||||
</AxoDropdownMenu.RadioItem>
|
||||
<AxoDropdownMenu.RadioItem value="tyler">
|
||||
Tyler
|
||||
</AxoDropdownMenu.RadioItem>
|
||||
</AxoDropdownMenu.RadioGroup>
|
||||
</AxoDropdownMenu.Content>
|
||||
</AxoDropdownMenu.Root>
|
||||
</div>
|
||||
);
|
||||
}
|
402
ts/axo/AxoDropdownMenu.tsx
Normal file
402
ts/axo/AxoDropdownMenu.tsx
Normal file
|
@ -0,0 +1,402 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { memo } from 'react';
|
||||
import { DropdownMenu } from 'radix-ui';
|
||||
import type { FC } from 'react';
|
||||
import { AxoSymbol } from './AxoSymbol';
|
||||
import { AxoBaseMenu } from './_internal/AxoBaseMenu';
|
||||
|
||||
const Namespace = 'AxoDropdownMenu';
|
||||
|
||||
/**
|
||||
* Displays a menu to the user—such as a set of actions or functions—triggered
|
||||
* by a button.
|
||||
*
|
||||
* Note: For menus that are triggered by a right-click, you should use
|
||||
* `AxoContextMenu`.
|
||||
*
|
||||
* @example Anatomy
|
||||
* ```tsx
|
||||
* import { AxoDropdownMenu } from "./axo/DropdownMenu/AxoDropdownMenu.tsx";
|
||||
*
|
||||
* export default () => (
|
||||
* <AxoDropdownMenu.Root>
|
||||
* <AxoDropdownMenu.Trigger>
|
||||
* <button>Click Me</button>
|
||||
* </AxoDropdownMenu.Trigger>
|
||||
*
|
||||
* <AxoDropdownMenu.Content>
|
||||
* <AxoDropdownMenu.Label />
|
||||
* <AxoDropdownMenu.Item />
|
||||
*
|
||||
* <AxoDropdownMenu.Group>
|
||||
* <AxoDropdownMenu.Item />
|
||||
* </AxoDropdownMenu.Group>
|
||||
*
|
||||
* <AxoDropdownMenu.CheckboxItem/>
|
||||
*
|
||||
* <AxoDropdownMenu.RadioGroup>
|
||||
* <AxoDropdownMenu.RadioItem/>
|
||||
* </AxoDropdownMenu.RadioGroup>
|
||||
*
|
||||
* <AxoDropdownMenu.Sub>
|
||||
* <AxoDropdownMenu.SubTrigger />
|
||||
* <AxoDropdownMenu.SubContent />
|
||||
* </AxoDropdownMenu.Sub>
|
||||
*
|
||||
* <AxoDropdownMenu.Separator />
|
||||
* </AxoDropdownMenu.Content>
|
||||
* </AxoDropdownMenu.Root>
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace AxoDropdownMenu {
|
||||
/**
|
||||
* Component: <AxoDropdownMenu.Root>
|
||||
* ---------------------------------
|
||||
*/
|
||||
|
||||
export type RootProps = AxoBaseMenu.MenuRootProps;
|
||||
|
||||
/**
|
||||
* Contains all the parts of a dropdown menu.
|
||||
*/
|
||||
export const Root: FC<RootProps> = memo(props => {
|
||||
return <DropdownMenu.Root>{props.children}</DropdownMenu.Root>;
|
||||
});
|
||||
|
||||
Root.displayName = `${Namespace}.Root`;
|
||||
|
||||
/**
|
||||
* Component: <AxoDropdownMenu.Trigger>
|
||||
* ------------------------------------
|
||||
*/
|
||||
|
||||
export type TriggerProps = AxoBaseMenu.MenuTriggerProps;
|
||||
|
||||
/**
|
||||
* The button that toggles the dropdown menu.
|
||||
* By default, the {@link AxoDropdownMenu.Content} will position itself
|
||||
* against the trigger.
|
||||
*/
|
||||
export const Trigger: FC<TriggerProps> = memo(props => {
|
||||
return (
|
||||
<DropdownMenu.Trigger asChild>{props.children}</DropdownMenu.Trigger>
|
||||
);
|
||||
});
|
||||
|
||||
Trigger.displayName = `${Namespace}.Trigger`;
|
||||
|
||||
/**
|
||||
* Component: <AxoDropdownMenu.Content>
|
||||
* ------------------------------------
|
||||
*/
|
||||
|
||||
export type ContentProps = AxoBaseMenu.MenuContentProps;
|
||||
|
||||
/**
|
||||
* The component that pops out when the dropdown menu is open.
|
||||
* Uses a portal to render the content part into the `body`.
|
||||
*/
|
||||
export const Content: FC<ContentProps> = memo(props => {
|
||||
return (
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
sideOffset={4}
|
||||
align="start"
|
||||
collisionPadding={6}
|
||||
className={AxoBaseMenu.menuContentStyles}
|
||||
>
|
||||
{props.children}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
);
|
||||
});
|
||||
|
||||
Content.displayName = `${Namespace}.Content`;
|
||||
|
||||
/**
|
||||
* Component: <AxoDropdownMenu.Item>
|
||||
* ---------------------------------
|
||||
*/
|
||||
|
||||
export type ItemProps = AxoBaseMenu.MenuItemProps;
|
||||
|
||||
/**
|
||||
* The component that contains the dropdown menu items.
|
||||
* @example
|
||||
* ```tsx
|
||||
* <AxoDropdownMenu.Item icon={<svg/>}>
|
||||
* {i18n("myContextMenuText")}
|
||||
* </AxoContentMenu.Item>
|
||||
* ````
|
||||
*/
|
||||
export const Item: FC<ItemProps> = memo(props => {
|
||||
return (
|
||||
<DropdownMenu.Item
|
||||
disabled={props.disabled}
|
||||
textValue={props.textValue}
|
||||
onSelect={props.onSelect}
|
||||
className={AxoBaseMenu.menuItemStyles}
|
||||
>
|
||||
{props.symbol && (
|
||||
<AxoBaseMenu.ItemLeadingSlot>
|
||||
<AxoBaseMenu.ItemSymbol symbol={props.symbol} />
|
||||
</AxoBaseMenu.ItemLeadingSlot>
|
||||
)}
|
||||
<AxoBaseMenu.ItemContentSlot>
|
||||
<AxoBaseMenu.ItemText>{props.children}</AxoBaseMenu.ItemText>
|
||||
{props.keyboardShortcut && (
|
||||
<AxoBaseMenu.ItemKeyboardShortcut
|
||||
keyboardShortcut={props.keyboardShortcut}
|
||||
/>
|
||||
)}
|
||||
</AxoBaseMenu.ItemContentSlot>
|
||||
</DropdownMenu.Item>
|
||||
);
|
||||
});
|
||||
|
||||
Item.displayName = `${Namespace}.Item`;
|
||||
|
||||
/**
|
||||
* Component: <AxoDropdownMenu.Group>
|
||||
* ----------------------------------
|
||||
*/
|
||||
|
||||
export type GroupProps = AxoBaseMenu.MenuGroupProps;
|
||||
|
||||
/**
|
||||
* Used to group multiple {@link AxoDropdownMenu.Item}'s.
|
||||
*/
|
||||
export const Group: FC<GroupProps> = memo(props => {
|
||||
return (
|
||||
<DropdownMenu.Group className={AxoBaseMenu.menuGroupStyles}>
|
||||
{props.children}
|
||||
</DropdownMenu.Group>
|
||||
);
|
||||
});
|
||||
|
||||
Group.displayName = `${Namespace}.Group`;
|
||||
|
||||
/**
|
||||
* Component: <AxoDropdownMenu.Label>
|
||||
* ----------------------------------
|
||||
*/
|
||||
|
||||
export type LabelProps = AxoBaseMenu.MenuLabelProps;
|
||||
|
||||
/**
|
||||
* Used to render a label. It won't be focusable using arrow keys.
|
||||
*/
|
||||
export const Label: FC<LabelProps> = memo(props => {
|
||||
return (
|
||||
<DropdownMenu.Label className={AxoBaseMenu.menuLabelStyles}>
|
||||
<AxoBaseMenu.ItemContentSlot>
|
||||
<AxoBaseMenu.ItemText>{props.children}</AxoBaseMenu.ItemText>
|
||||
</AxoBaseMenu.ItemContentSlot>
|
||||
</DropdownMenu.Label>
|
||||
);
|
||||
});
|
||||
|
||||
Label.displayName = `${Namespace}.Label`;
|
||||
|
||||
/**
|
||||
* Component: <AxoDropdownMenu.CheckboxItem>
|
||||
* -----------------------------------------
|
||||
*/
|
||||
|
||||
export type CheckboxItemProps = AxoBaseMenu.MenuCheckboxItemProps;
|
||||
|
||||
/**
|
||||
* An item that can be controlled and rendered like a checkbox.
|
||||
*/
|
||||
export const CheckboxItem: FC<CheckboxItemProps> = memo(props => {
|
||||
return (
|
||||
<DropdownMenu.CheckboxItem
|
||||
textValue={props.textValue}
|
||||
disabled={props.disabled}
|
||||
checked={props.checked}
|
||||
onCheckedChange={props.onCheckedChange}
|
||||
onSelect={props.onSelect}
|
||||
className={AxoBaseMenu.menuCheckboxItemStyles}
|
||||
>
|
||||
<AxoBaseMenu.ItemLeadingSlot>
|
||||
<AxoBaseMenu.ItemCheckPlaceholder>
|
||||
<DropdownMenu.ItemIndicator>
|
||||
<AxoBaseMenu.ItemCheck />
|
||||
</DropdownMenu.ItemIndicator>
|
||||
</AxoBaseMenu.ItemCheckPlaceholder>
|
||||
</AxoBaseMenu.ItemLeadingSlot>
|
||||
<AxoBaseMenu.ItemContentSlot>
|
||||
{props.symbol && (
|
||||
<span className="mr-2">
|
||||
<AxoBaseMenu.ItemSymbol symbol={props.symbol} />
|
||||
</span>
|
||||
)}
|
||||
<AxoBaseMenu.ItemText>{props.children}</AxoBaseMenu.ItemText>
|
||||
{props.keyboardShortcut && (
|
||||
<AxoBaseMenu.ItemKeyboardShortcut
|
||||
keyboardShortcut={props.keyboardShortcut}
|
||||
/>
|
||||
)}
|
||||
</AxoBaseMenu.ItemContentSlot>
|
||||
</DropdownMenu.CheckboxItem>
|
||||
);
|
||||
});
|
||||
|
||||
CheckboxItem.displayName = `${Namespace}.CheckboxItem`;
|
||||
|
||||
/**
|
||||
* Component: <AxoDropdownMenu.RadioGroup>
|
||||
* ---------------------------------------
|
||||
*/
|
||||
|
||||
export type RadioGroupProps = AxoBaseMenu.MenuRadioGroupProps;
|
||||
|
||||
/**
|
||||
* Used to group multiple {@link AxoDropdownMenu.RadioItem}'s.
|
||||
*/
|
||||
export const RadioGroup: FC<RadioGroupProps> = memo(props => {
|
||||
return (
|
||||
<DropdownMenu.RadioGroup
|
||||
value={props.value}
|
||||
onValueChange={props.onValueChange}
|
||||
className={AxoBaseMenu.menuRadioGroupStyles}
|
||||
>
|
||||
{props.children}
|
||||
</DropdownMenu.RadioGroup>
|
||||
);
|
||||
});
|
||||
|
||||
RadioGroup.displayName = `${Namespace}.RadioGroup`;
|
||||
|
||||
/**
|
||||
* Component: <AxoDropdownMenu.RadioItem>
|
||||
* --------------------------------------
|
||||
*/
|
||||
|
||||
export type RadioItemProps = AxoBaseMenu.MenuRadioItemProps;
|
||||
|
||||
/**
|
||||
* An item that can be controlled and rendered like a radio.
|
||||
*/
|
||||
export const RadioItem: FC<RadioItemProps> = memo(props => {
|
||||
return (
|
||||
<DropdownMenu.RadioItem
|
||||
value={props.value}
|
||||
className={AxoBaseMenu.menuRadioItemStyles}
|
||||
onSelect={props.onSelect}
|
||||
>
|
||||
<AxoBaseMenu.ItemLeadingSlot>
|
||||
<AxoBaseMenu.ItemCheckPlaceholder>
|
||||
<DropdownMenu.ItemIndicator>
|
||||
<AxoBaseMenu.ItemCheck />
|
||||
</DropdownMenu.ItemIndicator>
|
||||
</AxoBaseMenu.ItemCheckPlaceholder>
|
||||
</AxoBaseMenu.ItemLeadingSlot>
|
||||
<AxoBaseMenu.ItemContentSlot>
|
||||
{props.symbol && <AxoBaseMenu.ItemSymbol symbol={props.symbol} />}
|
||||
<AxoBaseMenu.ItemText>{props.children}</AxoBaseMenu.ItemText>
|
||||
{props.keyboardShortcut && (
|
||||
<AxoBaseMenu.ItemKeyboardShortcut
|
||||
keyboardShortcut={props.keyboardShortcut}
|
||||
/>
|
||||
)}
|
||||
</AxoBaseMenu.ItemContentSlot>
|
||||
</DropdownMenu.RadioItem>
|
||||
);
|
||||
});
|
||||
|
||||
RadioItem.displayName = `${Namespace}.RadioItem`;
|
||||
|
||||
/**
|
||||
* Component: <AxoDropdownMenu.Separator>
|
||||
* --------------------------------------
|
||||
*/
|
||||
|
||||
export type SeparatorProps = AxoBaseMenu.MenuSeparatorProps;
|
||||
|
||||
/**
|
||||
* Used to visually separate items in the dropdown menu.
|
||||
*/
|
||||
export const Separator: FC<SeparatorProps> = memo(() => {
|
||||
return (
|
||||
<DropdownMenu.Separator className={AxoBaseMenu.menuSeparatorStyles} />
|
||||
);
|
||||
});
|
||||
|
||||
Separator.displayName = `${Namespace}.Separator`;
|
||||
|
||||
/**
|
||||
* Component: <AxoDropdownMenu.Sub>
|
||||
* -------------------------------
|
||||
*/
|
||||
|
||||
export type SubProps = AxoBaseMenu.MenuSubProps;
|
||||
|
||||
/**
|
||||
* Contains all the parts of a submenu.
|
||||
*/
|
||||
export const Sub: FC<SubProps> = memo(props => {
|
||||
return <DropdownMenu.Sub>{props.children}</DropdownMenu.Sub>;
|
||||
});
|
||||
|
||||
Sub.displayName = `${Namespace}.Sub`;
|
||||
|
||||
/**
|
||||
* Component: <AxoDropdownMenu.SubTrigger>
|
||||
* ---------------------------------------
|
||||
*/
|
||||
|
||||
export type SubTriggerProps = AxoBaseMenu.MenuSubTriggerProps;
|
||||
|
||||
/**
|
||||
* An item that opens a submenu. Must be rendered inside
|
||||
* {@link ContextMenu.Sub}.
|
||||
*/
|
||||
export const SubTrigger: FC<SubTriggerProps> = memo(props => {
|
||||
return (
|
||||
<DropdownMenu.SubTrigger className={AxoBaseMenu.menuSubTriggerStyles}>
|
||||
{props.symbol && (
|
||||
<AxoBaseMenu.ItemLeadingSlot>
|
||||
<AxoBaseMenu.ItemSymbol symbol={props.symbol} />
|
||||
</AxoBaseMenu.ItemLeadingSlot>
|
||||
)}
|
||||
<AxoBaseMenu.ItemContentSlot>
|
||||
<AxoBaseMenu.ItemText>{props.children}</AxoBaseMenu.ItemText>
|
||||
<span className="ml-auto">
|
||||
<AxoSymbol.Icon size={14} symbol="chevron-[end]" label={null} />
|
||||
</span>
|
||||
</AxoBaseMenu.ItemContentSlot>
|
||||
</DropdownMenu.SubTrigger>
|
||||
);
|
||||
});
|
||||
|
||||
SubTrigger.displayName = `${Namespace}.SubTrigger`;
|
||||
|
||||
/**
|
||||
* Component: <AxoDropdownMenu.SubContent>
|
||||
* ---------------------------------------
|
||||
*/
|
||||
|
||||
export type SubContentProps = AxoBaseMenu.MenuSubContentProps;
|
||||
|
||||
/**
|
||||
* The component that pops out when a submenu is open. Must be rendered
|
||||
* inside {@link AxoDropdownMenu.Sub}.
|
||||
*/
|
||||
export const SubContent: FC<SubContentProps> = memo(props => {
|
||||
return (
|
||||
<DropdownMenu.SubContent
|
||||
alignOffset={-6}
|
||||
collisionPadding={6}
|
||||
className={AxoBaseMenu.menuSubContentStyles}
|
||||
>
|
||||
{props.children}
|
||||
</DropdownMenu.SubContent>
|
||||
);
|
||||
});
|
||||
|
||||
SubContent.displayName = `${Namespace}.SubContent`;
|
||||
}
|
90
ts/axo/AxoSelect.stories.tsx
Normal file
90
ts/axo/AxoSelect.stories.tsx
Normal file
|
@ -0,0 +1,90 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { useState } from 'react';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { AxoSelect } from './AxoSelect';
|
||||
|
||||
export default {
|
||||
title: 'Axo/AxoSelect',
|
||||
} satisfies Meta;
|
||||
|
||||
function Template(props: {
|
||||
disabled?: boolean;
|
||||
triggerWidth?: AxoSelect.TriggerWidth;
|
||||
triggerVariant: AxoSelect.TriggerVariant;
|
||||
}) {
|
||||
const [value, setValue] = useState<string | null>(null);
|
||||
return (
|
||||
<AxoSelect.Root
|
||||
value={value}
|
||||
onValueChange={setValue}
|
||||
disabled={props.disabled}
|
||||
>
|
||||
<AxoSelect.Trigger
|
||||
variant={props.triggerVariant}
|
||||
width={props.triggerWidth}
|
||||
placeholder="Select an item..."
|
||||
/>
|
||||
<AxoSelect.Content>
|
||||
<AxoSelect.Group>
|
||||
<AxoSelect.Label>Fruits</AxoSelect.Label>
|
||||
<AxoSelect.Item value="apple">Apple</AxoSelect.Item>
|
||||
<AxoSelect.Item value="banana">Banana</AxoSelect.Item>
|
||||
<AxoSelect.Item value="blueberry">Blueberry</AxoSelect.Item>
|
||||
<AxoSelect.Item value="grapes">Grapes</AxoSelect.Item>
|
||||
<AxoSelect.Item value="pineapple">Pineapple</AxoSelect.Item>
|
||||
</AxoSelect.Group>
|
||||
<AxoSelect.Separator />
|
||||
<AxoSelect.Group>
|
||||
<AxoSelect.Label>Vegetables</AxoSelect.Label>
|
||||
<AxoSelect.Item value="aubergine">Aubergine</AxoSelect.Item>
|
||||
<AxoSelect.Item value="broccoli">Broccoli</AxoSelect.Item>
|
||||
<AxoSelect.Item value="carrot" disabled>
|
||||
Carrot
|
||||
</AxoSelect.Item>
|
||||
<AxoSelect.Item value="leek">Leek</AxoSelect.Item>
|
||||
</AxoSelect.Group>
|
||||
<AxoSelect.Separator />
|
||||
<AxoSelect.Group>
|
||||
<AxoSelect.Label>Meat</AxoSelect.Label>
|
||||
<AxoSelect.Item value="beef">Beef</AxoSelect.Item>
|
||||
<AxoSelect.Item value="chicken">Chicken</AxoSelect.Item>
|
||||
<AxoSelect.Item value="lamb">Lamb</AxoSelect.Item>
|
||||
<AxoSelect.Item value="pork">Pork</AxoSelect.Item>
|
||||
</AxoSelect.Group>
|
||||
</AxoSelect.Content>
|
||||
</AxoSelect.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export function Basic(): JSX.Element {
|
||||
return (
|
||||
<div className="flex h-96 w-full flex-col items-center justify-center gap-2">
|
||||
<div className="flex gap-2">
|
||||
<Template triggerVariant="default" />
|
||||
<Template triggerVariant="default" disabled />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Template triggerVariant="floating" />
|
||||
<Template triggerVariant="floating" disabled />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Template triggerVariant="borderless" />
|
||||
<Template triggerVariant="borderless" disabled />
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Template triggerWidth="fixed" triggerVariant="default" />
|
||||
<Template triggerWidth="fixed" triggerVariant="default" disabled />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Template triggerWidth="fixed" triggerVariant="floating" />
|
||||
<Template triggerWidth="fixed" triggerVariant="floating" disabled />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Template triggerWidth="fixed" triggerVariant="borderless" />
|
||||
<Template triggerWidth="fixed" triggerVariant="borderless" disabled />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
288
ts/axo/AxoSelect.tsx
Normal file
288
ts/axo/AxoSelect.tsx
Normal file
|
@ -0,0 +1,288 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { memo } from 'react';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import { Select } from 'radix-ui';
|
||||
import { AxoBaseMenu } from './_internal/AxoBaseMenu';
|
||||
import { AxoSymbol } from './AxoSymbol';
|
||||
import type { Styles } from './_internal/css';
|
||||
import { css } from './_internal/css';
|
||||
|
||||
const Namespace = 'AxoSelect';
|
||||
|
||||
/**
|
||||
* Displays a list of options for the user to pick from—triggered by a button.
|
||||
*
|
||||
* @example Anatomy
|
||||
* ```tsx
|
||||
* export default () => (
|
||||
* <AxoSelect.Root>
|
||||
* <AxoSelect.Trigger/>
|
||||
* <AxoSelect.Content>
|
||||
* <AxoSelect.Item/>
|
||||
* <AxoSelect.Separator/>
|
||||
* <AxoSelect.Group>
|
||||
* <AxoSelect.Label/>
|
||||
* <AxoSelect.Item/>
|
||||
* </AxoSelect.Group>
|
||||
* </AxoSelect.Content>
|
||||
* </Select.Root>
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace AxoSelect {
|
||||
/**
|
||||
* Component: <AxoSelect.Root>
|
||||
* ---------------------------
|
||||
*/
|
||||
|
||||
export type RootProps = Readonly<{
|
||||
name?: string;
|
||||
form?: string;
|
||||
autoComplete?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
value: string | null;
|
||||
onValueChange: (value: string) => void;
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Contains all the parts of a select.
|
||||
*/
|
||||
export const Root: FC<RootProps> = memo(props => {
|
||||
return (
|
||||
<Select.Root
|
||||
name={props.name}
|
||||
form={props.form}
|
||||
autoComplete={props.autoComplete}
|
||||
disabled={props.disabled}
|
||||
required={props.required}
|
||||
open={props.open}
|
||||
onOpenChange={props.onOpenChange}
|
||||
value={props.value ?? undefined}
|
||||
onValueChange={props.onValueChange}
|
||||
>
|
||||
{props.children}
|
||||
</Select.Root>
|
||||
);
|
||||
});
|
||||
|
||||
Root.displayName = `${Namespace}.Root`;
|
||||
|
||||
/**
|
||||
* Component: <AxoSelect.Trigger>
|
||||
* ---------------------------
|
||||
*/
|
||||
|
||||
const baseTriggerStyles = css(
|
||||
'flex',
|
||||
'rounded-full py-[5px] ps-3 pe-2.5 text-label-primary',
|
||||
'disabled:text-label-disabled',
|
||||
'outline-0 outline-border-focused focused:outline-[2.5px]'
|
||||
);
|
||||
|
||||
const TriggerVariants = {
|
||||
default: css(
|
||||
baseTriggerStyles,
|
||||
'bg-fill-secondary',
|
||||
'pressed:bg-fill-secondary-pressed'
|
||||
),
|
||||
floating: css(
|
||||
baseTriggerStyles,
|
||||
'bg-fill-floating',
|
||||
'shadow-elevation-1',
|
||||
'pressed:bg-fill-floating-pressed'
|
||||
),
|
||||
borderless: css(
|
||||
baseTriggerStyles,
|
||||
'bg-transparent',
|
||||
'hovered:bg-fill-secondary',
|
||||
'pressed:bg-fill-secondary-pressed'
|
||||
),
|
||||
} as const satisfies Record<string, Styles>;
|
||||
|
||||
const TriggerWidths = {
|
||||
hug: css(),
|
||||
fixed: css('w-[120px]'),
|
||||
};
|
||||
|
||||
export type TriggerVariant = keyof typeof TriggerVariants;
|
||||
export type TriggerWidth = keyof typeof TriggerWidths;
|
||||
|
||||
export type TriggerProps = Readonly<{
|
||||
variant?: TriggerVariant;
|
||||
width?: TriggerWidth;
|
||||
placeholder: string;
|
||||
children?: ReactNode;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* The button that toggles the select.
|
||||
* The {@link AxoSelect.Content} will position itself by aligning over the
|
||||
* trigger.
|
||||
*/
|
||||
export const Trigger: FC<TriggerProps> = memo(props => {
|
||||
const variant = props.variant ?? 'default';
|
||||
const width = props.width ?? 'hug';
|
||||
const variantStyles = TriggerVariants[variant];
|
||||
const widthStyles = TriggerWidths[width];
|
||||
return (
|
||||
<Select.Trigger className={css(variantStyles, widthStyles)}>
|
||||
<AxoBaseMenu.ItemText>
|
||||
<Select.Value placeholder={props.placeholder}>
|
||||
{props.children}
|
||||
</Select.Value>
|
||||
</AxoBaseMenu.ItemText>
|
||||
<Select.Icon className="ml-2">
|
||||
<AxoSymbol.Icon symbol="chevron-down" size={14} label={null} />
|
||||
</Select.Icon>
|
||||
</Select.Trigger>
|
||||
);
|
||||
});
|
||||
|
||||
Trigger.displayName = `${Namespace}.Trigger`;
|
||||
|
||||
/**
|
||||
* Component: <AxoSelect.Content>
|
||||
* ------------------------------
|
||||
*/
|
||||
|
||||
export type ContentProps = Readonly<{
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* The component that pops out when the select is open.
|
||||
* Uses a portal to render the content part into the `body`.
|
||||
*/
|
||||
export const Content: FC<ContentProps> = memo(props => {
|
||||
return (
|
||||
<Select.Portal>
|
||||
<Select.Content className={AxoBaseMenu.selectContentStyles}>
|
||||
<Select.ScrollUpButton className="flex items-center justify-center p-1 text-label-primary">
|
||||
<AxoSymbol.Icon symbol="chevron-up" size={14} label={null} />
|
||||
</Select.ScrollUpButton>
|
||||
<Select.Viewport className={AxoBaseMenu.selectContentViewportStyles}>
|
||||
{props.children}
|
||||
</Select.Viewport>
|
||||
<Select.ScrollDownButton className="flex items-center justify-center p-1 text-label-primary">
|
||||
<AxoSymbol.Icon symbol="chevron-down" size={14} label={null} />
|
||||
</Select.ScrollDownButton>
|
||||
</Select.Content>
|
||||
</Select.Portal>
|
||||
);
|
||||
});
|
||||
|
||||
Content.displayName = `${Namespace}.Content`;
|
||||
|
||||
/**
|
||||
* Component: <AxoSelect.Item>
|
||||
* ---------------------------
|
||||
*/
|
||||
|
||||
export type ItemProps = Readonly<{
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
textValue?: string;
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* The component that contains the select items.
|
||||
*/
|
||||
export const Item: FC<ItemProps> = memo(props => {
|
||||
return (
|
||||
<Select.Item
|
||||
value={props.value}
|
||||
disabled={props.disabled}
|
||||
textValue={props.textValue}
|
||||
className={AxoBaseMenu.selectItemStyles}
|
||||
>
|
||||
<AxoBaseMenu.ItemLeadingSlot>
|
||||
<AxoBaseMenu.ItemCheckPlaceholder>
|
||||
<Select.ItemIndicator>
|
||||
<AxoBaseMenu.ItemCheck />
|
||||
</Select.ItemIndicator>
|
||||
</AxoBaseMenu.ItemCheckPlaceholder>
|
||||
</AxoBaseMenu.ItemLeadingSlot>
|
||||
<AxoBaseMenu.ItemContentSlot>
|
||||
<AxoBaseMenu.ItemText>
|
||||
<Select.ItemText>{props.children}</Select.ItemText>
|
||||
</AxoBaseMenu.ItemText>
|
||||
</AxoBaseMenu.ItemContentSlot>
|
||||
</Select.Item>
|
||||
);
|
||||
});
|
||||
|
||||
Item.displayName = `${Namespace}.Content`;
|
||||
|
||||
/**
|
||||
* Component: <AxoSelect.Group>
|
||||
* ---------------------------
|
||||
*/
|
||||
|
||||
export type GroupProps = Readonly<{
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Used to group multiple items.
|
||||
* Use in conjunction with {@link AxoSelect.Label to ensure good accessibility
|
||||
* via automatic labelling.
|
||||
*/
|
||||
export const Group: FC<GroupProps> = memo(props => {
|
||||
return (
|
||||
<Select.Group className={AxoBaseMenu.selectGroupStyles}>
|
||||
{props.children}
|
||||
</Select.Group>
|
||||
);
|
||||
});
|
||||
|
||||
Group.displayName = `${Namespace}.Group`;
|
||||
|
||||
/**
|
||||
* Component: <AxoSelect.Label>
|
||||
* ---------------------------
|
||||
*/
|
||||
|
||||
export type LabelProps = Readonly<{
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Used to render the label of a group. It won't be focusable using arrow keys.
|
||||
*/
|
||||
export const Label: FC<LabelProps> = memo(props => {
|
||||
return (
|
||||
<Select.Label className={AxoBaseMenu.selectLabelStyles}>
|
||||
<AxoBaseMenu.ItemContentSlot>
|
||||
<AxoBaseMenu.ItemText>{props.children}</AxoBaseMenu.ItemText>
|
||||
</AxoBaseMenu.ItemContentSlot>
|
||||
</Select.Label>
|
||||
);
|
||||
});
|
||||
|
||||
Label.displayName = `${Namespace}.Label`;
|
||||
|
||||
/**
|
||||
* Component: <AxoSelect.Separator>
|
||||
* ---------------------------
|
||||
*/
|
||||
|
||||
export type SeparatorProps = Readonly<{
|
||||
// N/A
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Used to visually separate items in the select.
|
||||
*/
|
||||
export const Separator: FC<SeparatorProps> = memo(() => {
|
||||
return <Select.Separator className={AxoBaseMenu.selectSeperatorStyles} />;
|
||||
});
|
||||
|
||||
Separator.displayName = `${Namespace}.Separator`;
|
||||
}
|
102
ts/axo/AxoSymbol.stories.tsx
Normal file
102
ts/axo/AxoSymbol.stories.tsx
Normal file
|
@ -0,0 +1,102 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { memo, useMemo, useState } from 'react';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { Direction } from 'radix-ui';
|
||||
import Fuse from 'fuse.js';
|
||||
import type { AxoSymbolName } from './AxoSymbol';
|
||||
import { AxoSymbol, _getAllAxoSymbolNames, _getAxoSymbol } from './AxoSymbol';
|
||||
|
||||
export default {
|
||||
title: 'Axo/AxoSymbol',
|
||||
} satisfies Meta;
|
||||
|
||||
const SymbolInfo = memo(function SymbolInfo(props: {
|
||||
symbolName: AxoSymbolName;
|
||||
}): JSX.Element {
|
||||
const ltr = _getAxoSymbol(props.symbolName, 'ltr');
|
||||
const rtl = _getAxoSymbol(props.symbolName, 'rtl');
|
||||
|
||||
const variants =
|
||||
ltr === rtl
|
||||
? ([
|
||||
// same
|
||||
{ title: 'LTR/RTL', dir: 'ltr', text: ltr },
|
||||
] as const)
|
||||
: ([
|
||||
{ title: 'LTR', dir: 'ltr', text: ltr },
|
||||
{ title: 'RTL', dir: 'rtl', text: rtl },
|
||||
] as const);
|
||||
|
||||
return (
|
||||
<figure className="flex flex-col items-center gap-2 border border-border-primary bg-background-secondary p-4">
|
||||
<div className="flex w-full flex-1 flex-row justify-between">
|
||||
{variants.map(variant => {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col items-center gap-2">
|
||||
<span className="type-caption text-label-secondary">
|
||||
{variant.title}
|
||||
</span>
|
||||
<span className="text-[20px] leading-none">
|
||||
<Direction.DirectionProvider dir={variant.dir}>
|
||||
<AxoSymbol.InlineGlyph
|
||||
symbol={props.symbolName}
|
||||
label={null}
|
||||
/>
|
||||
</Direction.DirectionProvider>
|
||||
</span>
|
||||
<code className="type-caption text-label-secondary">
|
||||
{Array.from(variant.text, char => {
|
||||
const codePoint = char.codePointAt(0) ?? -1;
|
||||
return `U+${codePoint.toString(16).toUpperCase()}`;
|
||||
}).join(' ')}
|
||||
</code>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<figcaption className="w-full truncate border-t border-dotted border-border-primary pt-4 text-center type-body-medium text-color-label-primary">
|
||||
<code>{props.symbolName}</code>
|
||||
</figcaption>
|
||||
</figure>
|
||||
);
|
||||
});
|
||||
|
||||
const allAxoSymbolNames = _getAllAxoSymbolNames()
|
||||
.slice()
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
const fuse = new Fuse(allAxoSymbolNames);
|
||||
|
||||
export function All(): JSX.Element {
|
||||
const [input, setInput] = useState('');
|
||||
|
||||
const results = useMemo(() => {
|
||||
if (input.trim() !== '') {
|
||||
return fuse.search(input).map(result => {
|
||||
return result.item;
|
||||
});
|
||||
}
|
||||
return allAxoSymbolNames;
|
||||
}, [input]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="sticky top-4 mb-3 bg-elevated-background-primary p-4 shadow-elevation-2">
|
||||
<input
|
||||
type="search"
|
||||
value={input}
|
||||
placeholder="Search..."
|
||||
onChange={event => {
|
||||
setInput(event.currentTarget.value);
|
||||
}}
|
||||
className="w-full rounded bg-elevated-background-secondary p-3 type-body-medium"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6">
|
||||
{results.map(result => {
|
||||
return <SymbolInfo key={result} symbolName={result} />;
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
263
ts/axo/AxoSymbol.tsx
Normal file
263
ts/axo/AxoSymbol.tsx
Normal file
|
@ -0,0 +1,263 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { FC } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import { Direction } from 'radix-ui';
|
||||
import { VisuallyHidden } from 'react-aria';
|
||||
import { assert } from './_internal/assert';
|
||||
|
||||
const { useDirection } = Direction;
|
||||
|
||||
const Namespace = 'AxoSymbol';
|
||||
|
||||
type AxoSymbolDef = string | { ltr: string; rtl: string };
|
||||
|
||||
const AllAxoSymbolDefs = {
|
||||
logo: '\u{E000}',
|
||||
album: '\u{E001}',
|
||||
appearance: '\u{E031}',
|
||||
'arrow-[start]': { ltr: '\u{2190}', rtl: '\u{2192}' },
|
||||
'arrow-[end]': { ltr: '\u{2192}', rtl: '\u{2190}' },
|
||||
'arrow-up': '\u{2191}',
|
||||
'arrow-down': '\u{2193}',
|
||||
'arrow-up_[start]': { ltr: '\u{2196}', rtl: '\u{2197}' },
|
||||
'arrow-up_[end]': { ltr: '\u{2197}', rtl: '\u{2196}' },
|
||||
'arrow-down_[start]': { ltr: '\u{2199}', rtl: '\u{2198}' },
|
||||
'arrow-down_[end]': { ltr: '\u{2198}', rtl: '\u{2199}' },
|
||||
'arrow-circle-[start]': { ltr: '\u{E00B}', rtl: '\u{E00C}' },
|
||||
'arrow-circle-[end]': { ltr: '\u{E00C}', rtl: '\u{E00B}' },
|
||||
'arrow-circle-up': '\u{E00D}',
|
||||
'arrow-circle-down': '\u{E00E}',
|
||||
'arrow-circle-up_[start]': { ltr: '\u{E00F}', rtl: '\u{E010}' },
|
||||
'arrow-circle-up_[end]': { ltr: '\u{E010}', rtl: '\u{E00F}' },
|
||||
'arrow-circle-down_[start]': { ltr: '\u{E011}', rtl: '\u{E012}' },
|
||||
'arrow-circle-down_[end]': { ltr: '\u{E012}', rtl: '\u{E011}' },
|
||||
'arrow-square-[start]': { ltr: '\u{E013}', rtl: '\u{E014}' },
|
||||
'arrow-square-[end]': { ltr: '\u{E014}', rtl: '\u{E013}' },
|
||||
'arrow-square-up': '\u{E015}',
|
||||
'arrow-square-down': '\u{E016}',
|
||||
'arrow-square-up_[start]': { ltr: '\u{E017}', rtl: '\u{E018}' },
|
||||
'arrow-square-up_[end]': { ltr: '\u{E018}', rtl: '\u{E017}' },
|
||||
'arrow-square-down_[start]': { ltr: '\u{E019}', rtl: '\u{E01A}' },
|
||||
'arrow-square-down_[end]': { ltr: '\u{E01A}', rtl: '\u{E019}' },
|
||||
'arrow-dash-down': '\u{E021}',
|
||||
'arrow-circle-[start]-fill': { ltr: '\u{E003}', rtl: '\u{E004}' },
|
||||
'arrow-circle-[end]-fill': { ltr: '\u{E004}', rtl: '\u{E003}' },
|
||||
'arrow-circle-up-fill': '\u{E005}',
|
||||
'arrow-circle-down-fill': '\u{E006}',
|
||||
'arrow-circle-up_[start]-fill': { ltr: '\u{E007}', rtl: '\u{E008}' },
|
||||
'arrow-circle-up_[end]-fill': { ltr: '\u{E008}', rtl: '\u{E007}' },
|
||||
'arrow-circle-down_[start]-fill': { ltr: '\u{E009}', rtl: '\u{E00A}' },
|
||||
'arrow-circle-down_[end]-fill': { ltr: '\u{E00A}', rtl: '\u{E009}' },
|
||||
'arrow-square-[start]-fill': { ltr: '\u{E08A}', rtl: '\u{E08B}' },
|
||||
'arrow-square-[end]-fill': { ltr: '\u{E08B}', rtl: '\u{E08A}' },
|
||||
'arrow-square-up-fill': '\u{E08C}',
|
||||
'arrow-square-down-fill': '\u{E08D}',
|
||||
'arrow-square-up_[start]-fill': { ltr: '\u{E08E}', rtl: '\u{E08F}' },
|
||||
'arrow-square-up_[end]-fill': { ltr: '\u{E08F}', rtl: '\u{E08E}' },
|
||||
'arrow-square-down_[start]-fill': { ltr: '\u{E090}', rtl: '\u{E091}' },
|
||||
'arrow-square-down_[end]-fill': { ltr: '\u{E091}', rtl: '\u{E090}' },
|
||||
at: '\u{E01B}',
|
||||
attach: '\u{E058}',
|
||||
audio: '\u{E01C}',
|
||||
'audio-rectangle': '\u{E01D}',
|
||||
badge: '\u{E099}',
|
||||
'badge-fill': '\u{E09A}',
|
||||
bell: '\u{E01E}',
|
||||
'bell-slash': '\u{E01F}',
|
||||
'bell-ring': '\u{E020}',
|
||||
block: '\u{E002}',
|
||||
calender: '\u{E0A2}',
|
||||
'calender-blank': '\u{E0A3}',
|
||||
check: '\u{2713}',
|
||||
'check-circle': '\u{E022}',
|
||||
'check-square': '\u{E023}',
|
||||
'chevron-[start]': { ltr: '\u{E024}', rtl: '\u{E025}' },
|
||||
'chevron-[end]': { ltr: '\u{E025}', rtl: '\u{E024}' },
|
||||
'chevron-up': '\u{E026}',
|
||||
'chevron-down': '\u{E027}',
|
||||
'chevron-circle-[start]': { ltr: '\u{E028}', rtl: '\u{E029}' },
|
||||
'chevron-circle-[end]': { ltr: '\u{E029}', rtl: '\u{E028}' },
|
||||
'chevron-circle-up': '\u{E02A}',
|
||||
'chevron-circle-down': '\u{E02B}',
|
||||
'chevron-square-[start]': { ltr: '\u{E02C}', rtl: '\u{E02D}' },
|
||||
'chevron-square-[end]': { ltr: '\u{E02D}', rtl: '\u{E02C}' },
|
||||
'chevron-square-up': '\u{E02E}',
|
||||
'chevron-square-down': '\u{E02F}',
|
||||
'dropdown-down': '\u{E07F}',
|
||||
'dropdown-up': '\u{E080}',
|
||||
'dropdown-triangle-down': '\u{E082}',
|
||||
'dropdown-triangle-up': '\u{E083}',
|
||||
'dropdown-double': '\u{E081}',
|
||||
edit: '\u{E030}',
|
||||
emoji: '\u{263A}',
|
||||
error: '\u{E032}',
|
||||
'error-triangle': '\u{E092}',
|
||||
'error-fill': '\u{E093}',
|
||||
'error-triangle-fill': '\u{E094}',
|
||||
file: '\u{E034}',
|
||||
forward: '\u{E035}',
|
||||
'forward-fill': '\u{E036}',
|
||||
gif: '\u{E037}',
|
||||
'gif-rectangle': '\u{E097}',
|
||||
'gif-rectangle-fill': '\u{E098}',
|
||||
gift: '\u{E0B5}',
|
||||
globe: '\u{E0B6}',
|
||||
group: '\u{E038}',
|
||||
'group-x': '\u{E0AE}',
|
||||
heart: '\u{E039}',
|
||||
help: '\u{E0D8}',
|
||||
incoming: '\u{E03A}',
|
||||
info: '\u{E03B}',
|
||||
leave: { ltr: '\u{E03C}', rtl: '\u{E03D}' },
|
||||
link: '\u{E03E}',
|
||||
'link-android': '\u{E03F}',
|
||||
'link-broken': '\u{E057}',
|
||||
'link-slash': '\u{E040}',
|
||||
lock: '\u{E041}',
|
||||
'lock-open': '\u{E07D}',
|
||||
megaphone: '\u{E042}',
|
||||
merge: '\u{E043}',
|
||||
message: '\u{E0A6}',
|
||||
'message_status-sending': '\u{E044}',
|
||||
'message_status-sent': '\u{E045}',
|
||||
'message_status-read': '\u{E047}',
|
||||
'message_status-delivered': '\u{E046}',
|
||||
'message_timer-00': '\u{E048}',
|
||||
'message_timer-05': '\u{E049}',
|
||||
'message_timer-10': '\u{E04A}',
|
||||
'message_timer-15': '\u{E04B}',
|
||||
'message_timer-20': '\u{E04C}',
|
||||
'message_timer-25': '\u{E04D}',
|
||||
'message_timer-30': '\u{E04E}',
|
||||
'message_timer-35': '\u{E04F}',
|
||||
'message_timer-40': '\u{E050}',
|
||||
'message_timer-45': '\u{E051}',
|
||||
'message_timer-50': '\u{E052}',
|
||||
'message_timer-55': '\u{E053}',
|
||||
'message_timer-60': '\u{E054}',
|
||||
mic: '\u{E055}',
|
||||
'mic-slash': '\u{E056}',
|
||||
minus: '\u{2212}',
|
||||
'minus-circle': '\u{2296}',
|
||||
'minus-square': '\u{E059}',
|
||||
'missed-incoming': '\u{E05A}',
|
||||
'missed-outgoing': '\u{E05B}',
|
||||
note: { ltr: '\u{E095}', rtl: '\u{E096}' },
|
||||
official_badge: '\u{E086}',
|
||||
'official_badge-fill': '\u{E087}',
|
||||
outgoing: '\u{E05C}',
|
||||
person: '\u{E05D}',
|
||||
'person-circle': '\u{E05E}',
|
||||
'person-check': '\u{E05F}',
|
||||
'person-x': '\u{E060}',
|
||||
'person-plus': '\u{E061}',
|
||||
'person-minus': '\u{E062}',
|
||||
'person-question': '\u{E06A}',
|
||||
phone: '\u{E063}',
|
||||
'phone-fill': '\u{E064}',
|
||||
photo: '\u{E065}',
|
||||
'photo-slash': '\u{E066}',
|
||||
play: '\u{E067}',
|
||||
'play-circle': '\u{E068}',
|
||||
'play-square': '\u{E069}',
|
||||
plus: '\u{002B}',
|
||||
'plus-circle': '\u{2295}',
|
||||
'plus-square': '\u{E06C}',
|
||||
raise_hand: '\u{E07E}',
|
||||
'raise_hand-fill': '\u{E084}',
|
||||
refresh: '\u{E0C4}',
|
||||
reply: '\u{E06D}',
|
||||
'reply-fill': '\u{E06E}',
|
||||
safety_number: '\u{E06F}',
|
||||
spam: '\u{E033}',
|
||||
sticker: '\u{E070}',
|
||||
thread: '\u{E071}',
|
||||
'thread-fill': '\u{E072}',
|
||||
timer: '\u{E073}',
|
||||
'timer-slash': '\u{E074}',
|
||||
video_camera: '\u{E075}',
|
||||
'video_camera-slash': '\u{E076}',
|
||||
'video_camera-fill': '\u{E077}',
|
||||
video: '\u{E088}',
|
||||
'video-slash': '\u{E089}',
|
||||
view_once: '\u{E078}',
|
||||
'view_once-dash': '\u{E079}',
|
||||
'view_once-viewed': '\u{E07A}',
|
||||
x: '\u{00D7}',
|
||||
'x-circle': '\u{2297}',
|
||||
'x-square': '\u{2327}',
|
||||
space: '\u{0020}',
|
||||
} as const satisfies Record<string, AxoSymbolDef>;
|
||||
|
||||
export type AxoSymbolName = keyof typeof AllAxoSymbolDefs;
|
||||
|
||||
export function _getAllAxoSymbolNames(): ReadonlyArray<AxoSymbolName> {
|
||||
return Object.keys(AllAxoSymbolDefs) as Array<AxoSymbolName>;
|
||||
}
|
||||
|
||||
export function _getAxoSymbol(
|
||||
symbolName: AxoSymbolName,
|
||||
dir: 'ltr' | 'rtl'
|
||||
): string {
|
||||
const symbolDef = assert(
|
||||
AllAxoSymbolDefs[symbolName],
|
||||
`${Namespace}:Invalid name: ${symbolName}`
|
||||
);
|
||||
const symbol = typeof symbolDef === 'string' ? symbolDef : symbolDef[dir];
|
||||
return symbol;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace AxoSymbol {
|
||||
/**
|
||||
* Component: <AxoSymbol.InlineGlyph>
|
||||
* --------------------------------------
|
||||
*/
|
||||
|
||||
export type InlineGlyphProps = Readonly<{
|
||||
symbol: AxoSymbolName;
|
||||
label: string | null;
|
||||
}>;
|
||||
|
||||
export const InlineGlyph: FC<InlineGlyphProps> = memo(props => {
|
||||
const direction = useDirection();
|
||||
const symbol = _getAxoSymbol(props.symbol, direction);
|
||||
return (
|
||||
<>
|
||||
<span aria-hidden className="font-symbols select-none">
|
||||
{symbol}
|
||||
</span>
|
||||
{props.label != null && (
|
||||
<VisuallyHidden className="select-none">{props.label}</VisuallyHidden>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
InlineGlyph.displayName = `${Namespace}.InlineGlyph`;
|
||||
|
||||
/**
|
||||
* Component: <AxoSymbol.Icon>
|
||||
* --------------------------------------
|
||||
*/
|
||||
|
||||
export type IconProps = Readonly<{
|
||||
size: 14 | 16 | 20;
|
||||
symbol: AxoSymbolName;
|
||||
label: string | null;
|
||||
}>;
|
||||
|
||||
export const Icon: FC<IconProps> = memo(props => {
|
||||
return (
|
||||
<span
|
||||
className="inline-flex size-[1em] shrink-0 items-center justify-center"
|
||||
style={{ fontSize: props.size }}
|
||||
>
|
||||
<AxoSymbol.InlineGlyph symbol={props.symbol} label={props.label} />
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
Icon.displayName = `${Namespace}.Icon`;
|
||||
}
|
348
ts/axo/_internal/AxoBaseMenu.tsx
Normal file
348
ts/axo/_internal/AxoBaseMenu.tsx
Normal file
|
@ -0,0 +1,348 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { css } from './css';
|
||||
import { AxoSymbol, type AxoSymbolName } from '../AxoSymbol';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace AxoBaseMenu {
|
||||
// <Content/SubContent>
|
||||
const baseContentStyles = css(
|
||||
'max-w-[300px] min-w-[200px] p-1.5',
|
||||
'select-none',
|
||||
'rounded-xl bg-elevated-background-tertiary shadow-elevation-3',
|
||||
'data-[state=closed]:animate-fade-out'
|
||||
);
|
||||
|
||||
const baseContentGridStyles = css('grid grid-cols-[min-content_auto]');
|
||||
|
||||
// <Group/RadioGroup>
|
||||
const baseGroupStyles = css('col-span-full grid grid-cols-subgrid');
|
||||
|
||||
// <Item/RadioItem/CheckboxItem/SubTrigger/Label/Separator>
|
||||
const baseItemStyles = css(
|
||||
'col-span-full grid grid-cols-subgrid items-center'
|
||||
);
|
||||
|
||||
// <Item/RadioItem/CheckboxItem/SubTrigger/Label> (not Separator)
|
||||
const labeledItemStyles = css(baseItemStyles, 'truncate p-1.5');
|
||||
|
||||
// <Item/RadioItem/CheckboxItem/SubTrigger> (not Label/Separator)
|
||||
const navigableItemStyles = css(
|
||||
labeledItemStyles,
|
||||
'rounded-md type-body-medium',
|
||||
'outline-0 data-[highlighted]:bg-fill-secondary-pressed',
|
||||
'data-[disabled]:text-label-disabled',
|
||||
'outline-0 outline-border-focused focused:outline-[2.5px]'
|
||||
);
|
||||
|
||||
/**
|
||||
* <Item/RadioItem/CheckboxItem/SubTrigger> (not Label/Separator)
|
||||
*/
|
||||
type BaseNavigableItemProps = Readonly<{
|
||||
/**
|
||||
* When true, prevents the user from interacting with the item.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* Optional text used for typeahead purposes. By default the typeahead
|
||||
* behavior will use the .textContent of the item. Use this when the
|
||||
* content is complex, or you have non-textual content inside.
|
||||
*/
|
||||
textValue?: string;
|
||||
/**
|
||||
* An icon that should be rendered before the text.
|
||||
*/
|
||||
symbol?: AxoSymbolName;
|
||||
}>;
|
||||
|
||||
// <Item/RadioItem/CheckboxItem> (not SubTrigger/Label/Separator)
|
||||
const selectableItemStyles = css(navigableItemStyles);
|
||||
|
||||
/**
|
||||
* Used for any selectable content node such as Item, CheckboxItem, or RadioItem,
|
||||
* But not nodes like SubTrigger, Separator, Group, etc.
|
||||
*/
|
||||
type BaseSelectableItemProps = BaseNavigableItemProps &
|
||||
Readonly<{
|
||||
keyboardShortcut?: string;
|
||||
onSelect?: (event: Event) => void;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* AxoBaseMenu: Item Slots
|
||||
* -----------------------
|
||||
*/
|
||||
|
||||
export type ItemLeadingSlotProps = Readonly<{
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export function ItemLeadingSlot(props: ItemLeadingSlotProps): JSX.Element {
|
||||
return (
|
||||
<span className="col-start-1 col-end-1 me-1.5 flex items-center gap-1.5">
|
||||
{props.children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export type ItemContentSlotProps = Readonly<{
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export function ItemContentSlot(props: ItemContentSlotProps): JSX.Element {
|
||||
return (
|
||||
<span className="col-start-2 col-end-2 flex min-w-0 items-center">
|
||||
{props.children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* AxoBaseMenu: Item Parts
|
||||
* -----------------------
|
||||
*/
|
||||
|
||||
export const itemTextStyles = css('flex-1 truncate text-start');
|
||||
|
||||
export type ItemTextProps = Readonly<{
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export function ItemText(props: ItemTextProps): JSX.Element {
|
||||
return <span className={itemTextStyles}>{props.children}</span>;
|
||||
}
|
||||
|
||||
export type ItemCheckPlaceholderProps = Readonly<{
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export function ItemCheckPlaceholder(
|
||||
props: ItemCheckPlaceholderProps
|
||||
): JSX.Element {
|
||||
return <span className="w-3.5">{props.children}</span>;
|
||||
}
|
||||
|
||||
export function ItemCheck(): JSX.Element {
|
||||
return <AxoSymbol.Icon size={14} symbol="check" label={null} />;
|
||||
}
|
||||
|
||||
export function ItemSymbol(props: { symbol: AxoSymbolName }): JSX.Element {
|
||||
return <AxoSymbol.Icon size={16} symbol={props.symbol} label={null} />;
|
||||
}
|
||||
|
||||
export type ItemKeyboardShortcutProps = Readonly<{
|
||||
keyboardShortcut: string;
|
||||
}>;
|
||||
|
||||
export function ItemKeyboardShortcut(
|
||||
props: ItemKeyboardShortcutProps
|
||||
): JSX.Element {
|
||||
return (
|
||||
<span className="ml-auto px-1 type-body-medium text-label-secondary">
|
||||
{props.keyboardShortcut}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* AxoBaseMenu: Root
|
||||
* -----------------
|
||||
*/
|
||||
|
||||
export type MenuRootProps = Readonly<{
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* AxoBaseMenu: Trigger
|
||||
* --------------------
|
||||
*/
|
||||
|
||||
export type MenuTriggerProps = Readonly<{
|
||||
/**
|
||||
* When true, the context menu won't open when right-clicking.
|
||||
* Note that this will also restore the native context menu.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* AxoBaseMenu: Content
|
||||
* --------------------
|
||||
*/
|
||||
|
||||
export type MenuContentProps = Readonly<{
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export const menuContentStyles = css(
|
||||
baseContentStyles,
|
||||
baseContentGridStyles,
|
||||
'max-h-(--radix-popper-available-height) overflow-auto [scrollbar-width:none]',
|
||||
'overflow-auto [scrollbar-width:none]'
|
||||
);
|
||||
|
||||
export const selectContentStyles = css(baseContentStyles);
|
||||
export const selectContentViewportStyles = css(baseContentGridStyles);
|
||||
|
||||
/**
|
||||
* AxoBaseMenu: Item
|
||||
* -----------------
|
||||
*/
|
||||
|
||||
export type MenuItemProps = BaseSelectableItemProps &
|
||||
Readonly<{
|
||||
/**
|
||||
* Event handler called when the user selects an item (via mouse or
|
||||
* keyboard). Calling event.preventDefault in this handler will prevent the
|
||||
* context menu from closing when selecting that item.
|
||||
*/
|
||||
onSelect: (event: Event) => void;
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export const menuItemStyles = css(selectableItemStyles);
|
||||
export const selectItemStyles = css(selectableItemStyles);
|
||||
|
||||
/**
|
||||
* AxoBaseMenu: Group
|
||||
* ------------------
|
||||
*/
|
||||
|
||||
export type MenuGroupProps = Readonly<{
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export const menuGroupStyles = css(baseGroupStyles);
|
||||
export const selectGroupStyles = css(baseGroupStyles);
|
||||
|
||||
/**
|
||||
* AxoBaseMenu: Label
|
||||
* ------------------
|
||||
*/
|
||||
|
||||
export type MenuLabelProps = Readonly<{
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
const baseLabelStyles = css(
|
||||
labeledItemStyles,
|
||||
'type-body-small text-label-secondary'
|
||||
);
|
||||
|
||||
export const menuLabelStyles = css(baseLabelStyles);
|
||||
export const selectLabelStyles = css(baseLabelStyles);
|
||||
|
||||
/**
|
||||
* AxoBaseMenu: CheckboxItem
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
export type MenuCheckboxItemProps = BaseSelectableItemProps &
|
||||
Readonly<{
|
||||
/**
|
||||
* The controlled checked state of the item. Must be used in conjunction
|
||||
* with `onCheckedChange`.
|
||||
*/
|
||||
checked: boolean;
|
||||
/**
|
||||
* Event handler called when the checked state changes.
|
||||
*/
|
||||
onCheckedChange: (checked: boolean) => void;
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export const menuCheckboxItemStyles = css(selectableItemStyles);
|
||||
|
||||
/**
|
||||
* AxoBaseMenu: RadioGroup
|
||||
* -----------------------
|
||||
*/
|
||||
|
||||
export type MenuRadioGroupProps = Readonly<{
|
||||
/**
|
||||
* The value of the selected item in the group.
|
||||
*/
|
||||
value: string;
|
||||
|
||||
/**
|
||||
* Event handler called when the value changes.
|
||||
*/
|
||||
onValueChange: (value: string) => void;
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export const menuRadioGroupStyles = css(baseGroupStyles);
|
||||
|
||||
/**
|
||||
* AxoBaseMenu: RadioItem
|
||||
* ----------------------
|
||||
*/
|
||||
|
||||
export type MenuRadioItemProps = BaseSelectableItemProps &
|
||||
Readonly<{
|
||||
value: string;
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export const menuRadioItemStyles = css(selectableItemStyles);
|
||||
|
||||
/**
|
||||
* AxoBaseMenu: Separator
|
||||
* ----------------------
|
||||
*/
|
||||
|
||||
export type MenuSeparatorProps = Readonly<{
|
||||
// N/A
|
||||
}>;
|
||||
|
||||
const baseSeparatorStyles = css(
|
||||
baseItemStyles,
|
||||
'mx-0.5 my-1 border-t-[0.5px] border-border-primary'
|
||||
);
|
||||
|
||||
export const menuSeparatorStyles = css(baseSeparatorStyles);
|
||||
export const selectSeperatorStyles = css(baseSeparatorStyles);
|
||||
|
||||
/**
|
||||
* AxoBaseMenu: Sub
|
||||
* ----------------
|
||||
*/
|
||||
|
||||
export type MenuSubProps = Readonly<{
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* AxoBaseMenu: SubTrigger
|
||||
* -----------------------
|
||||
*/
|
||||
|
||||
export type MenuSubTriggerProps = BaseNavigableItemProps &
|
||||
Readonly<{
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export const menuSubTriggerStyles = css(
|
||||
navigableItemStyles,
|
||||
'data-[state=open]:not-data-[highlighted]:bg-fill-secondary'
|
||||
);
|
||||
|
||||
/**
|
||||
* AxoBaseMenu: SubContent
|
||||
* -----------------------
|
||||
*/
|
||||
|
||||
export type MenuSubContentProps = Readonly<{
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export const menuSubContentStyles = css(
|
||||
baseContentStyles,
|
||||
'max-h-(--radix-popper-available-height) overflow-auto [scrollbar-width:none]',
|
||||
baseContentGridStyles
|
||||
);
|
||||
}
|
17
ts/axo/_internal/assert.tsx
Normal file
17
ts/axo/_internal/assert.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
export class AssertionError extends TypeError {
|
||||
override name = 'AssertionError';
|
||||
}
|
||||
|
||||
export function assert(condition: boolean, message?: string): asserts condition;
|
||||
export function assert<T>(input: T, message?: string): NonNullable<T>;
|
||||
export function assert<T>(input: T, message?: string): NonNullable<T> {
|
||||
if (input === false || input == null) {
|
||||
// eslint-disable-next-line no-debugger
|
||||
debugger;
|
||||
throw new AssertionError(message ?? `input is ${input}`);
|
||||
}
|
||||
return input;
|
||||
}
|
27
ts/axo/_internal/css.tsx
Normal file
27
ts/axo/_internal/css.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
export type Styles = string & { __Styles: never };
|
||||
|
||||
export function css(
|
||||
...classNames: ReadonlyArray<Styles | string | boolean | null>
|
||||
): Styles {
|
||||
const { length } = classNames;
|
||||
|
||||
let result = '';
|
||||
let first = true;
|
||||
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
const className = classNames[index];
|
||||
if (typeof className === 'string') {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
result += ' ';
|
||||
}
|
||||
result += className;
|
||||
}
|
||||
}
|
||||
|
||||
return result as Styles;
|
||||
}
|
376
ts/axo/tailwind.css
Normal file
376
ts/axo/tailwind.css
Normal file
|
@ -0,0 +1,376 @@
|
|||
@import 'tailwindcss';
|
||||
|
||||
/**
|
||||
* Custom Variants
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
@custom-variant dark (&:where(.dark-theme, .dark-theme *));
|
||||
|
||||
@custom-variant hovered (&:hover:not(:disabled));
|
||||
@custom-variant pressed (&:active:not(:disabled));
|
||||
@custom-variant focused (.keyboard-mode &:focus);
|
||||
|
||||
/**
|
||||
* Color
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/* prettier-ignore */
|
||||
@theme {
|
||||
--color-*: initial; /* reset defaults */
|
||||
|
||||
/* Colors/Labels */
|
||||
--color-label-primary: light-dark(--alpha(#000 / 85%), --alpha(#FFF / 85%));
|
||||
--color-label-secondary: light-dark(--alpha(#000 / 55%), --alpha(#FFF / 55%));
|
||||
--color-label-placeholder: light-dark(--alpha(#000 / 30%), --alpha(#FFF / 30%));
|
||||
--color-label-disabled: light-dark(--alpha(#000 / 20%), --alpha(#FFF / 20%));
|
||||
|
||||
--color-label-primary-inverted: light-dark(--alpha(#FFF / 85%), /* */ #000 /* */);
|
||||
--color-label-secondary-inverted: light-dark(--alpha(#FFF / 55%), --alpha(#000 / 55%));
|
||||
--color-label-placeholder-inverted: light-dark(--alpha(#FFF / 30%), --alpha(#000 / 30%));
|
||||
--color-label-disabled-inverted: light-dark(--alpha(#FFF / 20%), --alpha(#000 / 20%));
|
||||
|
||||
--color-label-primary-on-color: light-dark(/* */ #FFF /* */, --alpha(#FFF / 90%));
|
||||
--color-label-secondary-on-color: light-dark(--alpha(#FFF / 80%), --alpha(#FFF / 70%));
|
||||
--color-label-placeholder-on-color: light-dark(--alpha(#FFF / 45%), --alpha(#FFF / 45%));
|
||||
--color-label-disabled-on-color: light-dark(--alpha(#FFF / 35%), --alpha(#FFF / 30%));
|
||||
|
||||
/* Colors/Color Label */
|
||||
--color-color-label-primary: light-dark(/* */ #030FFC /* */, /* */ #99A1FF /* */);
|
||||
--color-color-label-primary-disabled: light-dark(--alpha(#030FFC / 25%), --alpha(#99A1FF / 25%));
|
||||
--color-color-label-light: light-dark(/* */ #99A1FF /* */, /* */ #99A1FF /* */);
|
||||
--color-color-label-light-disabled: light-dark(--alpha(#99A1FF / 25%), --alpha(#99A1FF / 25%));
|
||||
--color-color-label-affirmative: light-dark(/* */ #00AD17 /* */, /* */ #30D150 /* */);
|
||||
--color-color-label-affirmative-disabled: light-dark(--alpha(#00AD17 / 25%), --alpha(#30D150 / 25%));
|
||||
--color-color-label-destructive: light-dark(/* */ #F21602 /* */, /* */ #FF4A3A /* */);
|
||||
--color-color-label-destructive-disabled: light-dark(--alpha(#F21602 / 25%), --alpha(#FF4A3A / 25%));
|
||||
|
||||
/* Colors/Background */
|
||||
--color-background-primary: light-dark(/* */ #FFFFFF /* */, /* */ #1A1A1A /* */);
|
||||
--color-background-secondary: light-dark(/* */ #F6F6F6 /* */, /* */ #262626 /* */);
|
||||
--color-background-overlay: light-dark(--alpha(#000000 / 20%), --alpha(#000000 / 40%));
|
||||
|
||||
/* Colors/Elevated Background */
|
||||
--color-elevated-background-primary: light-dark(#FAFAFA, #2A2A2A);
|
||||
--color-elevated-background-secondary: light-dark(#F2F2F2, #323232);
|
||||
--color-elevated-background-tertiary: light-dark(#EAEAEA, #3A3A3A);
|
||||
--color-elevated-background-quaternary: light-dark(#2A2A2A, #424242);
|
||||
|
||||
/* Colors/Fill */
|
||||
--color-fill-primary: light-dark(/* */ #FFFFFF /* */, --alpha(#808080 / 20%));
|
||||
--color-fill-primary-pressed: light-dark(/* */ #F6F6F6 /* */, --alpha(#808080 / 28%));
|
||||
--color-fill-secondary: light-dark(--alpha(#808080 / 12%), --alpha(#808080 / 20%));
|
||||
--color-fill-secondary-pressed: light-dark(--alpha(#808080 / 20%), --alpha(#808080 / 28%));
|
||||
--color-fill-selected: light-dark(--alpha(#808080 / 25%), --alpha(#808080 / 32%));
|
||||
--color-fill-inverted: light-dark(/* */ #424242 /* */, /* */ #DEDEDE /* */);
|
||||
--color-fill-inverted-pressed: light-dark(/* */ #4E4E4E /* */, /* */ #CACACA /* */);
|
||||
--color-fill-floating: light-dark(/* */ #FFFFFF /* */, /* */ #2A2A2A /* */);
|
||||
--color-fill-floating-pressed: light-dark(/* */ #F6F6F6 /* */, /* */ #323232 /* */);
|
||||
--color-fill-on-media: light-dark(--alpha(#000000 / 75%), --alpha(#000000 / 75%));
|
||||
--color-fill-on-media-pressed: light-dark(--alpha(#000000 / 83%), --alpha(#000000 / 83%));
|
||||
|
||||
/* Colors/Message Fill */
|
||||
--color-message-fill-incoming-primary: light-dark(/* */ #EAEAEA /* */, /* */ #3A3A3A /* */);
|
||||
--color-message-fill-incoming-secondary: light-dark(--alpha(#FFFFFF / 80%), --alpha(#FFFFFF / 20%));
|
||||
--color-message-fill-incoming-tertiary: light-dark(--alpha(#FFFFFF / 60%), --alpha(#FFFFFF / 12%));
|
||||
--color-message-fill-outgoing-primary: light-dark(/* */ #2267F5 /* */, /* */ #2267F5 /* */);
|
||||
--color-message-fill-outgoing-secondary: light-dark(--alpha(#FFFFFF / 60%), --alpha(#FFFFFF / 60%));
|
||||
--color-message-fill-outgoing-tertiary: light-dark(--alpha(#FFFFFF / 20%), --alpha(#FFFFFF / 20%));
|
||||
|
||||
/* Colors/Color Fill */
|
||||
--color-color-fill-primary: light-dark(#4655FF, #5563FF);
|
||||
--color-color-fill-primary-pressed: light-dark(#3B4AF4, #4856F2);
|
||||
--color-color-fill-affirmative: light-dark(#02C028, #02C529);
|
||||
--color-color-fill-affirmative-pressed: light-dark(#00B324, #00B725);
|
||||
--color-color-fill-warning: light-dark(#FFCC00, #FFD60A);
|
||||
--color-color-fill-warning-pressed: light-dark(#FFCC00, #F1C900);
|
||||
--color-color-fill-destructive: light-dark(#FD2512, #FB4332);
|
||||
--color-color-fill-destructive-pressed: light-dark(#EB1300, #E93120);
|
||||
|
||||
/* Colors/Border */
|
||||
--color-border-primary: light-dark(--alpha(#000000 / 16%), --alpha(#FFFFFF / 16%));
|
||||
--color-border-secondary: light-dark(--alpha(#000000 / 08%), --alpha(#FFFFFF / 08%));
|
||||
--color-border-focused: light-dark(/* */ #C1C7FE /* */, /* */ #C1C7FE /* */);
|
||||
--color-border-selected: light-dark(/* */ #4655FF /* */, /* */ #5563FF /* */);
|
||||
--color-border-selected-on-color: light-dark(/* */ #FFFFFF /* */, --alpha(#FFFFFF / 90%));
|
||||
--color-border-error: light-dark(/* */ #FD2512 /* */, /* */ #FB4332 /* */);
|
||||
|
||||
/* Colors/Shadow */
|
||||
--color-shadow-elevation-1: light-dark(--alpha(#000 / 08%), --alpha(#000 / 16%));
|
||||
--color-shadow-elevation-2: light-dark(--alpha(#000 / 08%), --alpha(#000 / 16%));
|
||||
--color-shadow-elevation-3: light-dark(--alpha(#000 / 10%), --alpha(#000 / 20%));
|
||||
--color-shadow-elevation-4: light-dark(--alpha(#000 / 12%), --alpha(#000 / 24%));
|
||||
--color-shadow-elevation-5: light-dark(--alpha(#000 / 20%), --alpha(#000 / 40%));
|
||||
--color-shadow-outline: light-dark(--alpha(#000 / 12%), /* */ transparent);
|
||||
--color-shadow-highlight: light-dark(/* */ transparent, --alpha(#FFF / 08%));
|
||||
}
|
||||
|
||||
@layer base {
|
||||
/* High Contrast Mode */
|
||||
/* prettier-ignore */
|
||||
@media (prefers-contrast: more) {
|
||||
/* Colors/Labels */
|
||||
--color-label-primary: light-dark(/* */ #000 /* */, /* */ #FFF /* */);
|
||||
--color-label-secondary: light-dark(--alpha(#000 / 70%), --alpha(#FFF / 70%));
|
||||
--color-label-placeholder: light-dark(--alpha(#000 / 50%), --alpha(#FFF / 50%));
|
||||
--color-label-disabled: light-dark(--alpha(#000 / 40%), --alpha(#FFF / 40%));
|
||||
|
||||
--color-label-primary-inverted: light-dark(/* */ #FFF /* */, /* */ #000 /* */);
|
||||
--color-label-secondary-inverted: light-dark(--alpha(#FFF / 70%), --alpha(#000 / 70%));
|
||||
--color-label-placeholder-inverted: light-dark(--alpha(#FFF / 50%), --alpha(#000 / 50%));
|
||||
--color-label-disabled-inverted: light-dark(--alpha(#FFF / 40%), --alpha(#000 / 40%));
|
||||
|
||||
--color-label-primary-on-color: light-dark(/* */ #FFF /* */, /* */ #FFF /* */);
|
||||
--color-label-secondary-on-color: light-dark(--alpha(#FFF / 90%), --alpha(#FFF / 90%));
|
||||
--color-label-placeholder-on-color: light-dark(--alpha(#FFF / 60%), --alpha(#FFF / 60%));
|
||||
--color-label-disabled-on-color: light-dark(--alpha(#FFF / 50%), --alpha(#FFF / 50%));
|
||||
|
||||
/* Colors/Color Label */
|
||||
--color-color-label-primary: light-dark(/* */ #000ECC /* */, /* */ #D5D9FF /* */);
|
||||
--color-color-label-primary-disabled: light-dark(--alpha(#000ECC / 40%), --alpha(#D5D9FF / 40%));
|
||||
--color-color-label-light: light-dark(/* */ #D5D9FF /* */, /* */ #D5D9FF /* */);
|
||||
--color-color-label-light-disabled: light-dark(--alpha(#D5D9FF / 40%), --alpha(#D5D9FF / 40%));
|
||||
--color-color-label-affirmative: light-dark(/* */ #004D0F /* */, /* */ #4CEF6D /* */);
|
||||
--color-color-label-affirmative-disabled: light-dark(--alpha(#004D0F / 40%), --alpha(#4CEF6D / 40%));
|
||||
--color-color-label-destructive: light-dark(/* */ #8A0B00 /* */, /* */ #FFC5C2 /* */);
|
||||
--color-color-label-destructive-disabled: light-dark(--alpha(#8A0B00 / 40%), --alpha(#FFC5C2 / 40%));
|
||||
|
||||
/* Colors/Background */
|
||||
--color-background-primary: light-dark(/* */ #FFFFFF /* */, /* */ #121212 /* */);
|
||||
--color-background-secondary: light-dark(/* */ #F6F6F6 /* */, /* */ #1E1E1E /* */);
|
||||
--color-background-overlay: light-dark(--alpha(#000000 / 40%), --alpha(#000000 / 60%));
|
||||
|
||||
/* Colors/Elevated Background */
|
||||
--color-elevated-background-primary: light-dark(#FFFFFF, #222222);
|
||||
--color-elevated-background-secondary: light-dark(#F2F2F2, #2A2A2A);
|
||||
--color-elevated-background-tertiary: light-dark(#EAEAEA, #323232);
|
||||
--color-elevated-background-quaternary: light-dark(#262626, #3A3A3A);
|
||||
|
||||
/* Colors/Fill */
|
||||
--color-fill-primary: light-dark(/* */ #FFFFFF /* */, --alpha(#808080 / 30%));
|
||||
--color-fill-primary-pressed: light-dark(/* */ #EAEAEA /* */, --alpha(#808080 / 38%));
|
||||
--color-fill-secondary: light-dark(--alpha(#808080 / 22%), --alpha(#808080 / 30%));
|
||||
--color-fill-secondary-pressed: light-dark(--alpha(#808080 / 30%), --alpha(#808080 / 38%));
|
||||
--color-fill-selected: light-dark(--alpha(#808080 / 34%), --alpha(#808080 / 42%));
|
||||
--color-fill-inverted: light-dark(/* */ #2A2A2A /* */, /* */ #F6F6F6 /* */);
|
||||
--color-fill-inverted-pressed: light-dark(/* */ #363636 /* */, /* */ #E2E2E2 /* */);
|
||||
--color-fill-floating: light-dark(/* */ #FFFFFF /* */, /* */ #323232 /* */);
|
||||
--color-fill-floating-pressed: light-dark(/* */ #EAEAEA /* */, /* */ #3A3A3A /* */);
|
||||
--color-fill-on-media: light-dark(--alpha(#000000 / 85%), --alpha(#000000 / 85%));
|
||||
--color-fill-on-media-pressed: light-dark(--alpha(#000000 / 93%), --alpha(#000000 / 93%));
|
||||
|
||||
/* Colors/Message Fill */
|
||||
--color-message-fill-incoming-primary: light-dark(/* */ #E0E0E0 /* */, /* */ #424242 /* */);
|
||||
--color-message-fill-incoming-secondary: light-dark(--alpha(#FFFFFF / 90%), --alpha(#FFFFFF / 30%));
|
||||
--color-message-fill-incoming-tertiary: light-dark(--alpha(#FFFFFF / 70%), --alpha(#FFFFFF / 22%));
|
||||
--color-message-fill-outgoing-primary: light-dark(/* */ #0842B9 /* */, /* */ #0842B9 /* */);
|
||||
--color-message-fill-outgoing-secondary: light-dark(--alpha(#FFFFFF / 70%), --alpha(#FFFFFF / 70%));
|
||||
--color-message-fill-outgoing-tertiary: light-dark(--alpha(#FFFFFF / 30%), --alpha(#FFFFFF / 30%));
|
||||
|
||||
/* Colors/Color Fill */
|
||||
--color-color-fill-primary: light-dark(#2B3BED, #2B3BED);
|
||||
--color-color-fill-primary-pressed: light-dark(#1E2EE0, #1E2EE0);
|
||||
--color-color-fill-affirmative: light-dark(#1D7A2F, #1D7A2F);
|
||||
--color-color-fill-affirmative-pressed: light-dark(#115E23, #116E23);
|
||||
--color-color-fill-warning: light-dark(#F0C000, #F0C000);
|
||||
--color-color-fill-warning-pressed: light-dark(#E4B600, #E4B600);
|
||||
--color-color-fill-destructive: light-dark(#B7271A, #B7271A);
|
||||
--color-color-fill-destructive-pressed: light-dark(#A61609, #A61609);
|
||||
|
||||
/* Colors/Border */
|
||||
--color-border-primary: light-dark(--alpha(#000000 / 32%), --alpha(#FFFFFF / 32%));
|
||||
--color-border-secondary: light-dark(--alpha(#000000 / 16%), --alpha(#FFFFFF / 16%));
|
||||
--color-border-focused: light-dark(/* */ #A0A7FE /* */, /* */ #A0A7FE /* */);
|
||||
--color-border-selected: light-dark(/* */ #2B3BED /* */, /* */ #5563FF /* */);
|
||||
--color-border-selected-on-color: light-dark(/* */ #FFFFFF /* */, /* */ #FFFFFF /* */);
|
||||
--color-border-error: light-dark(/* */ #B7271A /* */, /* */ #FB4332 /* */);
|
||||
|
||||
/* Colors/Shadow */
|
||||
--color-shadow-elevation-1: light-dark(--alpha(#000 / 08%), --alpha(#000 / 16%));
|
||||
--color-shadow-elevation-2: light-dark(--alpha(#000 / 08%), --alpha(#000 / 16%));
|
||||
--color-shadow-elevation-3: light-dark(--alpha(#000 / 10%), --alpha(#000 / 20%));
|
||||
--color-shadow-elevation-4: light-dark(--alpha(#000 / 12%), --alpha(#000 / 24%));
|
||||
--color-shadow-elevation-5: light-dark(--alpha(#000 / 20%), --alpha(#000 / 40%));
|
||||
--color-shadow-outline: light-dark(--alpha(#000 / 32%), /* */ transparent);
|
||||
--color-shadow-highlight: light-dark(/* */ transparent, --alpha(#FFF / 32%));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Font Family
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
@theme {
|
||||
--font-*: initial; /* reset defaults */
|
||||
/* Note: --font-sans also has language */
|
||||
--font-sans: Inter, 'Source Sans Pro', 'Source Han Sans', -apple-system,
|
||||
system-ui, 'Segoe UI', 'Noto Sans', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
/* Note: This font-family is checked for in matchMonospace, to support paste scenarios */
|
||||
--font-mono: 'SF Mono', SFMono-Regular, ui-monospace, 'DejaVu Sans Mono',
|
||||
Menlo, Consolas, monospace;
|
||||
--font-symbols: 'SignalSymbols';
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'SignalSymbols';
|
||||
font-style: normal;
|
||||
font-weight: 300 400 700;
|
||||
font-display: block;
|
||||
src: url('../../fonts/signal-symbols/SignalSymbolsVariable.woff2');
|
||||
}
|
||||
|
||||
@layer base {
|
||||
/* Japanese */
|
||||
:lang(ja) {
|
||||
--font-sans: Inter, 'SF Pro', 'SF Pro JP', 'BIZ UDGothic',
|
||||
'Hiragino Kaku Gothic Pro', 'ヒラギノ角ゴ Pro W3', メイリオ, Meiryo,
|
||||
'MS Pゴシック', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
/* Farsi (Persian) */
|
||||
:lang(fa) {
|
||||
--font-sans: 'Vazirmatn', -apple-system, system-ui, BlinkMacSystemFont,
|
||||
'Segoe UI', Tahoma, 'Noto Sans Arabic', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
/* Urdu */
|
||||
:lang(ur) {
|
||||
--font-sans: 'Noto Nastaliq Urdu', Gulzar, 'Jameel Noori Nastaleeq',
|
||||
'Faiz Lahori Nastaleeq', 'Urdu Typesetting', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Typography
|
||||
* ----------------------------------------------------------------------------
|
||||
* Should prefer to use the `type-*` utility when possible.
|
||||
*/
|
||||
@theme {
|
||||
/* text-size */
|
||||
--text-*: initial; /* reset defaults */
|
||||
--type-text-title-large: 1.5rem /* 24px */;
|
||||
--type-text-title-medium: 1.125rem /* 18px */;
|
||||
--type-text-title-small: 0.875rem /* 14px */;
|
||||
--type-text-body-large: 0.875rem /* 14px */;
|
||||
--type-text-body-medium: 0.8125rem /* 13px */;
|
||||
--type-text-body-small: 0.75rem /* 12px */;
|
||||
--type-text-caption: 0.6825rem /* 11px */;
|
||||
/* font-weight */
|
||||
--font-weight-*: initial; /* reset defaults */
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-regular: 400;
|
||||
--type-font-weight-title-large: var(--font-weight-semibold);
|
||||
--type-font-weight-title-medium: var(--font-weight-semibold);
|
||||
--type-font-weight-title-small: var(--font-weight-semibold);
|
||||
--type-font-weight-body-large: var(--font-weight-regular);
|
||||
--type-font-weight-body-medium: var(--font-weight-regular);
|
||||
--type-font-weight-body-small: var(--font-weight-medium);
|
||||
--type-font-weight-caption: var(--font-weight-regular);
|
||||
/* letter-spacing */
|
||||
--tracking-*: initial; /* reset defaults */
|
||||
--type-tracking-title-large: -0.019em /* (@ 24px) -0.46px */;
|
||||
--type-tracking-title-medium: -0.014em /* (@ 18px) -0.25px */;
|
||||
--type-tracking-title-small: -0.006em /* (@ 14px) -0.08px */;
|
||||
--type-tracking-body-large: -0.006em /* (@ 14px) -0.08px */;
|
||||
--type-tracking-body-medium: -0.003em /* (@ 13px) -0.04px */;
|
||||
--type-tracking-body-small: 0em /* (@ 12px) 0px */;
|
||||
--type-tracking-caption: 0.005em /* (@ 11px) 0.05px */;
|
||||
/* line-height */
|
||||
--leading-*: initial; /* reset defaults */
|
||||
--leading-none: 1;
|
||||
--type-leading-title-large: 2rem /* 32px */;
|
||||
--type-leading-title-medium: 1.5rem /* 24px */;
|
||||
--type-leading-title-small: 1.25rem /* 20px */;
|
||||
--type-leading-body-large: 1.25rem /* 20px */;
|
||||
--type-leading-body-medium: 1.125rem /* 18px */;
|
||||
--type-leading-body-small: 1rem /* 16px */;
|
||||
--type-leading-caption: 0.875rem /* 14px */;
|
||||
}
|
||||
|
||||
/* prettier-ignore */
|
||||
@utility type-* {
|
||||
font-size: --value(--type-text-*);
|
||||
font-weight: --value(--type-font-weight-*);
|
||||
letter-spacing: --value(--type-tracking-*);
|
||||
line-height: --value(--type-leading-*);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shadow
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
/* prettier-ignore */
|
||||
@theme {
|
||||
/* box-shadow */
|
||||
--shadow-*: initial; /* reset defaults */
|
||||
--shadow-elevation-0:
|
||||
0 1px 2px 0 var(--color-shadow-elevation-1);
|
||||
--shadow-elevation-1:
|
||||
0 0 0 0.5px var(--color-shadow-highlight) inset,
|
||||
0 0 0 0.5px var(--color-shadow-outline),
|
||||
0 2px 8px 0 var(--color-shadow-elevation-2);
|
||||
--shadow-elevation-2:
|
||||
0 0 0 0.5px var(--color-shadow-highlight) inset,
|
||||
0 0 0 0.5px var(--color-shadow-outline),
|
||||
0 4px 12px 0 var(--color-shadow-elevation-3);
|
||||
--shadow-elevation-3:
|
||||
0 0 0 0.5px var(--color-shadow-highlight) inset,
|
||||
0 0 0 0.5px var(--color-shadow-outline),
|
||||
0 6px 16px 0 var(--color-shadow-elevation-4);
|
||||
--shadow-elevation-4:
|
||||
0 0 0 0.5px var(--color-shadow-highlight) inset,
|
||||
0 0 0 0.5px var(--color-shadow-outline),
|
||||
0 12px 56px 0 var(--color-shadow-elevation-5);
|
||||
|
||||
/* box-shadow: inset */
|
||||
--inset-shadow-*: initial; /* reset defaults */
|
||||
|
||||
/* filter: drop-shadow() */
|
||||
--drop-shadow-*: initial; /* reset defaults */
|
||||
--drop-shadow-elevation-0: var(--shadow-elevation-0);
|
||||
--drop-shadow-elevation-1: var(--shadow-elevation-1);
|
||||
--drop-shadow-elevation-2: var(--shadow-elevation-2);
|
||||
--drop-shadow-elevation-3: var(--shadow-elevation-3);
|
||||
--drop-shadow-elevation-4: var(--shadow-elevation-4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Blur
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
@theme {
|
||||
/* filter/backdrop-filter: blur() */
|
||||
--blur-*: initial; /* reset defaults */
|
||||
--blur-thin: 10px;
|
||||
--blur-regular: 40px;
|
||||
--blur-thick: 80px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Easing
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
@theme {
|
||||
--ease-*: initial; /* reset defaults */
|
||||
--ease-in-cubic: cubic-bezier(0.32, 0, 0.67, 0);
|
||||
--ease-out-cubic: cubic-bezier(0.33, 1, 0.68, 1);
|
||||
--east-in-out-cubic: cubic-bezier(0.65, 0, 0.35, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Animations
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
@theme {
|
||||
--animate-*: initial; /* reset defaults */
|
||||
--animate-fade-out: animate-fade-out 120ms var(--ease-out-cubic);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@keyframes animate-fade-out {
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue