diff --git a/.scss-lint.yml b/.scss-lint.yml index 1cc029441..2477dfffb 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -283,3 +283,4 @@ exclude: - 'app/javascript/widget/assets/scss/sdk.css' - 'app/assets/stylesheets/administrate/reset/_normalize.scss' - 'app/javascript/shared/assets/stylesheets/*.scss' + - 'app/javascript/dashboard/assets/scss/_woot.scss' diff --git a/Gemfile b/Gemfile index 937aef4af..1e8605379 100644 --- a/Gemfile +++ b/Gemfile @@ -173,8 +173,11 @@ gem 'pgvector' # Convert Website HTML to Markdown gem 'reverse_markdown' +gem 'iso-639' gem 'ruby-openai' +gem 'shopify_api' + ### Gems required only in specific deployment environments ### ############################################################## diff --git a/Gemfile.lock b/Gemfile.lock index 74a59167a..bfe4b1970 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -352,6 +352,7 @@ GEM ruby2ruby (~> 2.4) ruby_parser (~> 3.10) hana (1.3.7) + hash_diff (1.1.1) hashdiff (1.1.0) hashie (5.0.0) html2text (0.4.0) @@ -377,6 +378,8 @@ GEM io-console (0.6.0) irb (1.7.2) reline (>= 0.3.6) + iso-639 (0.3.8) + csv jbuilder (2.11.5) actionview (>= 5.0.0) activesupport (>= 5.0.0) @@ -520,6 +523,9 @@ GEM rack (>= 1.2, < 4) snaky_hash (~> 2.0) version_gem (~> 1.1) + oj (3.16.10) + bigdecimal (>= 3.0) + ostruct (>= 0.2) omniauth (2.1.2) hashie (>= 3.4.6) rack (>= 2.2.3) @@ -711,6 +717,7 @@ GEM parser scss_lint (0.60.0) sass (~> 3.5, >= 3.5.5) + securerandom (0.4.1) seed_dump (3.3.1) activerecord (>= 4) activesupport (>= 4) @@ -725,6 +732,17 @@ GEM sentry-ruby (~> 5.19.0) sidekiq (>= 3.0) sexp_processor (4.17.0) + shopify_api (14.8.0) + activesupport + concurrent-ruby + hash_diff + httparty + jwt + oj + openssl + securerandom + sorbet-runtime + zeitwerk (~> 2.5) shoulda-matchers (5.3.0) activesupport (>= 5.2.0) sidekiq (7.3.1) @@ -757,6 +775,7 @@ GEM snaky_hash (2.0.1) hashie version_gem (~> 1.1, >= 1.1.1) + sorbet-runtime (0.5.11934) spring (4.1.1) spring-watcher-listen (2.1.0) listen (>= 2.7, < 4.0) @@ -896,6 +915,7 @@ DEPENDENCIES hashie html2text image_processing + iso-639 jbuilder json_refs json_schemer @@ -950,6 +970,7 @@ DEPENDENCIES sentry-rails (>= 5.19.0) sentry-ruby sentry-sidekiq (>= 5.19.0) + shopify_api shoulda-matchers sidekiq (>= 7.3.1) sidekiq-cron (>= 1.12.0) diff --git a/app/assets/stylesheets/administrate/components/_buttons.scss b/app/assets/stylesheets/administrate/components/_buttons.scss index a0c3699ba..1fceddc1f 100644 --- a/app/assets/stylesheets/administrate/components/_buttons.scss +++ b/app/assets/stylesheets/administrate/components/_buttons.scss @@ -9,7 +9,7 @@ input[type='submit']:not(.reset-base), border-radius: $base-border-radius; color: $white; cursor: pointer; - display: inline-block; + display: inline-flex; font-size: $font-size-small; -webkit-font-smoothing: antialiased; font-weight: $font-weight-medium; diff --git a/app/controllers/api/v1/accounts/integrations/shopify_controller.rb b/app/controllers/api/v1/accounts/integrations/shopify_controller.rb new file mode 100644 index 000000000..7fe31889b --- /dev/null +++ b/app/controllers/api/v1/accounts/integrations/shopify_controller.rb @@ -0,0 +1,111 @@ +class Api::V1::Accounts::Integrations::ShopifyController < Api::V1::Accounts::BaseController + include Shopify::IntegrationHelper + before_action :setup_shopify_context, only: [:orders] + before_action :fetch_hook, except: [:auth] + before_action :validate_contact, only: [:orders] + + def auth + shop_domain = params[:shop_domain] + return render json: { error: 'Shop domain is required' }, status: :unprocessable_entity if shop_domain.blank? + + state = generate_shopify_token(Current.account.id) + + auth_url = "https://#{shop_domain}/admin/oauth/authorize?" + auth_url += URI.encode_www_form( + client_id: client_id, + scope: REQUIRED_SCOPES.join(','), + redirect_uri: redirect_uri, + state: state + ) + + render json: { redirect_url: auth_url } + end + + def orders + customers = fetch_customers + return render json: { orders: [] } if customers.empty? + + orders = fetch_orders(customers.first['id']) + render json: { orders: orders } + rescue ShopifyAPI::Errors::HttpResponseError => e + render json: { error: e.message }, status: :unprocessable_entity + end + + def destroy + @hook.destroy! + head :ok + rescue StandardError => e + render json: { error: e.message }, status: :unprocessable_entity + end + + private + + def redirect_uri + "#{ENV.fetch('FRONTEND_URL', '')}/shopify/callback" + end + + def contact + @contact ||= Current.account.contacts.find_by(id: params[:contact_id]) + end + + def fetch_hook + @hook = Integrations::Hook.find_by!(account: Current.account, app_id: 'shopify') + end + + def fetch_customers + query = [] + query << "email:#{contact.email}" if contact.email.present? + query << "phone:#{contact.phone_number}" if contact.phone_number.present? + + shopify_client.get( + path: 'customers/search.json', + query: { + query: query.join(' OR '), + fields: 'id,email,phone' + } + ).body['customers'] || [] + end + + def fetch_orders(customer_id) + orders = shopify_client.get( + path: 'orders.json', + query: { + customer_id: customer_id, + status: 'any', + fields: 'id,email,created_at,total_price,currency,fulfillment_status,financial_status' + } + ).body['orders'] || [] + + orders.map do |order| + order.merge('admin_url' => "https://#{@hook.reference_id}/admin/orders/#{order['id']}") + end + end + + def setup_shopify_context + return if client_id.blank? || client_secret.blank? + + ShopifyAPI::Context.setup( + api_key: client_id, + api_secret_key: client_secret, + api_version: '2025-01'.freeze, + scope: REQUIRED_SCOPES.join(','), + is_embedded: true, + is_private: false + ) + end + + def shopify_session + ShopifyAPI::Auth::Session.new(shop: @hook.reference_id, access_token: @hook.access_token) + end + + def shopify_client + @shopify_client ||= ShopifyAPI::Clients::Rest::Admin.new(session: shopify_session) + end + + def validate_contact + return unless contact.blank? || (contact.email.blank? && contact.phone_number.blank?) + + render json: { error: 'Contact information missing' }, + status: :unprocessable_entity + end +end diff --git a/app/controllers/shopify/callbacks_controller.rb b/app/controllers/shopify/callbacks_controller.rb new file mode 100644 index 000000000..7fb8b5a47 --- /dev/null +++ b/app/controllers/shopify/callbacks_controller.rb @@ -0,0 +1,72 @@ +class Shopify::CallbacksController < ApplicationController + include Shopify::IntegrationHelper + + def show + verify_account! + + @response = oauth_client.auth_code.get_token( + params[:code], + redirect_uri: '/shopify/callback' + ) + + handle_response + rescue StandardError => e + Rails.logger.error("Shopify callback error: #{e.message}") + redirect_to "#{redirect_uri}?error=true" + end + + private + + def verify_account! + @account_id = verify_shopify_token(params[:state]) + raise StandardError, 'Invalid state parameter' if account.blank? + end + + def handle_response + account.hooks.create!( + app_id: 'shopify', + access_token: parsed_body['access_token'], + status: 'enabled', + reference_id: params[:shop], + settings: { + scope: parsed_body['scope'] + } + ) + + redirect_to shopify_integration_url + end + + def parsed_body + @parsed_body ||= @response.response.parsed + end + + def oauth_client + OAuth2::Client.new( + client_id, + client_secret, + { + site: "https://#{params[:shop]}", + authorize_url: '/admin/oauth/authorize', + token_url: '/admin/oauth/access_token' + } + ) + end + + def account + @account ||= Account.find(@account_id) + end + + def account_id + @account_id ||= params[:state].split('_').first + end + + def shopify_integration_url + "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/settings/integrations/shopify" + end + + def redirect_uri + return shopify_integration_url if account + + ENV.fetch('FRONTEND_URL', nil) + end +end diff --git a/app/controllers/super_admin/app_configs_controller.rb b/app/controllers/super_admin/app_configs_controller.rb index 43157fa0e..3e17a7369 100644 --- a/app/controllers/super_admin/app_configs_controller.rb +++ b/app/controllers/super_admin/app_configs_controller.rb @@ -35,6 +35,8 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController @allowed_configs = case @config when 'facebook' %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN FACEBOOK_API_VERSION ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT] + when 'shopify' + %w[SHOPIFY_CLIENT_ID SHOPIFY_CLIENT_SECRET] when 'microsoft' %w[AZURE_APP_ID AZURE_APP_SECRET] when 'email' diff --git a/app/helpers/shopify/integration_helper.rb b/app/helpers/shopify/integration_helper.rb new file mode 100644 index 000000000..6aad93211 --- /dev/null +++ b/app/helpers/shopify/integration_helper.rb @@ -0,0 +1,58 @@ +module Shopify::IntegrationHelper + REQUIRED_SCOPES = %w[read_customers read_orders read_fulfillments].freeze + + # Generates a signed JWT token for Shopify integration + # + # @param account_id [Integer] The account ID to encode in the token + # @return [String, nil] The encoded JWT token or nil if client secret is missing + def generate_shopify_token(account_id) + return if client_secret.blank? + + JWT.encode(token_payload(account_id), client_secret, 'HS256') + rescue StandardError => e + Rails.logger.error("Failed to generate Shopify token: #{e.message}") + nil + end + + def token_payload(account_id) + { + sub: account_id, + iat: Time.current.to_i + } + end + + # Verifies and decodes a Shopify JWT token + # + # @param token [String] The JWT token to verify + # @return [Integer, nil] The account ID from the token or nil if invalid + def verify_shopify_token(token) + return if token.blank? || client_secret.blank? + + decode_token(token, client_secret) + end + + private + + def client_id + @client_id ||= GlobalConfigService.load('SHOPIFY_CLIENT_ID', nil) + end + + def client_secret + @client_secret ||= GlobalConfigService.load('SHOPIFY_CLIENT_SECRET', nil) + end + + def decode_token(token, secret) + JWT.decode( + token, + secret, + true, + { + algorithm: 'HS256', + verify_expiration: true + } + ).first['sub'] + rescue StandardError => e + Rails.logger.error("Unexpected error verifying Shopify token: #{e.message}") + nil + end +end diff --git a/app/javascript/dashboard/api/integrations.js b/app/javascript/dashboard/api/integrations.js index 2b816e603..d4ffcbca3 100644 --- a/app/javascript/dashboard/api/integrations.js +++ b/app/javascript/dashboard/api/integrations.js @@ -32,6 +32,12 @@ class IntegrationsAPI extends ApiClient { deleteHook(hookId) { return axios.delete(`${this.baseUrl()}/integrations/hooks/${hookId}`); } + + connectShopify({ shopDomain }) { + return axios.post(`${this.baseUrl()}/integrations/shopify/auth`, { + shop_domain: shopDomain, + }); + } } export default new IntegrationsAPI(); diff --git a/app/javascript/dashboard/api/integrations/shopify.js b/app/javascript/dashboard/api/integrations/shopify.js new file mode 100644 index 000000000..0b6ce8ec1 --- /dev/null +++ b/app/javascript/dashboard/api/integrations/shopify.js @@ -0,0 +1,17 @@ +/* global axios */ + +import ApiClient from '../ApiClient'; + +class ShopifyAPI extends ApiClient { + constructor() { + super('integrations/shopify', { accountScoped: true }); + } + + getOrders(contactId) { + return axios.get(`${this.url}/orders`, { + params: { contact_id: contactId }, + }); + } +} + +export default new ShopifyAPI(); diff --git a/app/javascript/dashboard/assets/scss/_date-picker.scss b/app/javascript/dashboard/assets/scss/_date-picker.scss index 2fa577673..b22381ea0 100644 --- a/app/javascript/dashboard/assets/scss/_date-picker.scss +++ b/app/javascript/dashboard/assets/scss/_date-picker.scss @@ -10,7 +10,7 @@ &.no-margin { .mx-input { - @apply mb-0; + margin-bottom: 0 !important; } } diff --git a/app/javascript/dashboard/assets/scss/_next-colors.scss b/app/javascript/dashboard/assets/scss/_next-colors.scss new file mode 100644 index 000000000..48cfce921 --- /dev/null +++ b/app/javascript/dashboard/assets/scss/_next-colors.scss @@ -0,0 +1,208 @@ +// scss-lint:disable PropertySortOrder +@layer base { + // NEXT COLORS START + :root { + // slate + --slate-1: 252 252 253; + --slate-2: 249 249 251; + --slate-3: 240 240 243; + --slate-4: 232 232 236; + --slate-5: 224 225 230; + --slate-6: 217 217 224; + --slate-7: 205 206 214; + --slate-8: 185 187 198; + --slate-9: 139 141 152; + --slate-10: 128 131 141; + --slate-11: 96 100 108; + --slate-12: 28 32 36; + + --iris-1: 253 253 255; + --iris-2: 248 248 255; + --iris-3: 240 241 254; + --iris-4: 230 231 255; + --iris-5: 218 220 255; + --iris-6: 203 205 255; + --iris-7: 184 186 248; + --iris-8: 155 158 240; + --iris-9: 91 91 214; + --iris-10: 81 81 205; + --iris-11: 87 83 198; + --iris-12: 39 41 98; + + --ruby-1: 255 252 253; + --ruby-2: 255 247 248; + --ruby-3: 254 234 237; + --ruby-4: 255 220 225; + --ruby-5: 255 206 214; + --ruby-6: 248 191 200; + --ruby-7: 239 172 184; + --ruby-8: 229 146 163; + --ruby-9: 229 70 102; + --ruby-10: 220 59 93; + --ruby-11: 202 36 77; + --ruby-12: 100 23 43; + + --amber-1: 254 253 251; + --amber-2: 254 251 233; + --amber-3: 255 247 194; + --amber-4: 255 238 156; + --amber-5: 251 229 119; + --amber-6: 243 214 115; + --amber-7: 233 193 98; + --amber-8: 226 163 54; + --amber-9: 255 197 61; + --amber-10: 255 186 24; + --amber-11: 171 100 0; + --amber-12: 79 52 34; + + --teal-1: 250 254 253; + --teal-2: 243 251 249; + --teal-3: 224 248 243; + --teal-4: 204 243 234; + --teal-5: 184 234 224; + --teal-6: 161 222 210; + --teal-7: 131 205 193; + --teal-8: 83 185 171; + --teal-9: 18 165 148; + --teal-10: 13 155 138; + --teal-11: 0 133 115; + --teal-12: 13 61 56; + + --gray-1: 252 252 252; + --gray-2: 249 249 249; + --gray-3: 240 240 240; + --gray-4: 232 232 232; + --gray-5: 224 224 224; + --gray-6: 217 217 217; + --gray-7: 206 206 206; + --gray-8: 187 187 187; + --gray-9: 141 141 141; + --gray-10: 131 131 131; + --gray-11: 100 100 100; + --gray-12: 32 32 32; + + --background-color: 253 253 253; + --text-blue: 8 109 224; + --border-container: 236 236 236; + --border-strong: 235 235 235; + --border-weak: 234 234 234; + --solid-1: 255 255 255; + --solid-2: 255 255 255; + --solid-3: 255 255 255; + --solid-active: 255 255 255; + --solid-amber: 252 232 193; + --solid-blue: 218 236 255; + --solid-iris: 230 231 255; + + --alpha-1: 67, 67, 67, 0.06; + --alpha-2: 201, 202, 207, 0.15; + --alpha-3: 255, 255, 255, 0.96; + --black-alpha-1: 0, 0, 0, 0.12; + --black-alpha-2: 0, 0, 0, 0.04; + --border-blue: 39, 129, 246, 0.5; + --white-alpha: 255, 255, 255, 0.8; + } + + .dark { + // slate + --slate-1: 17 17 19; + --slate-2: 24 25 27; + --slate-3: 33 34 37; + --slate-4: 39 42 45; + --slate-5: 46 49 53; + --slate-6: 54 58 63; + --slate-7: 67 72 78; + --slate-8: 90 97 105; + --slate-9: 105 110 119; + --slate-10: 119 123 132; + --slate-11: 176 180 186; + --slate-12: 237 238 240; + + --iris-1: 19 19 30; + --iris-2: 23 22 37; + --iris-3: 32 34 72; + --iris-4: 38 42 101; + --iris-5: 48 51 116; + --iris-6: 61 62 130; + --iris-7: 74 74 149; + --iris-8: 89 88 177; + --iris-9: 91 91 214; + --iris-10: 84 114 228; + --iris-11: 158 177 255; + --iris-12: 224 223 254; + + --ruby-1: 25 17 19; + --ruby-2: 30 21 23; + --ruby-3: 58 20 30; + --ruby-4: 78 19 37; + --ruby-5: 94 26 46; + --ruby-6: 111 37 57; + --ruby-7: 136 52 71; + --ruby-8: 179 68 90; + --ruby-9: 229 70 102; + --ruby-10: 236 90 114; + --ruby-11: 255 148 157; + --ruby-12: 254 210 225; + + --amber-1: 22 18 12; + --amber-2: 29 24 15; + --amber-3: 48 32 8; + --amber-4: 63 39 0; + --amber-5: 77 48 0; + --amber-6: 92 61 5; + --amber-7: 113 79 25; + --amber-8: 143 100 36; + --amber-9: 255 197 61; + --amber-10: 255 214 10; + --amber-11: 255 202 22; + --amber-12: 255 231 179; + + --teal-1: 13 21 20; + --teal-2: 17 28 27; + --teal-3: 13 45 42; + --teal-4: 2 59 55; + --teal-5: 8 72 67; + --teal-6: 20 87 80; + --teal-7: 28 105 97; + --teal-8: 32 126 115; + --teal-9: 18 165 148; + --teal-10: 14 179 158; + --teal-11: 11 216 182; + --teal-12: 173 240 221; + + --gray-1: 17 17 17; + --gray-2: 25 25 25; + --gray-3: 34 34 34; + --gray-4: 42 42 42; + --gray-5: 49 49 49; + --gray-6: 58 58 58; + --gray-7: 72 72 72; + --gray-8: 96 96 96; + --gray-9: 110 110 110; + --gray-10: 123 123 123; + --gray-11: 180 180 180; + --gray-12: 238 238 238; + + --background-color: 18 18 19; + --border-strong: 52 52 52; + --border-weak: 38 38 42; + --solid-1: 23 23 26; + --solid-2: 29 30 36; + --solid-3: 44 45 54; + --solid-active: 53 57 66; + --solid-amber: 42 37 30; + --solid-blue: 16 49 91; + --solid-iris: 38 42 101; + --text-blue: 126 182 255; + + --alpha-1: 36, 36, 36, 0.8; + --alpha-2: 139, 147, 182, 0.15; + --alpha-3: 36, 38, 45, 0.9; + --black-alpha-1: 0, 0, 0, 0.3; + --black-alpha-2: 0, 0, 0, 0.2; + --border-blue: 39, 129, 246, 0.5; + --border-container: 236, 236, 236, 0; + --white-alpha: 255, 255, 255, 0.1; + } +} +// NEXT COLORS END diff --git a/app/javascript/dashboard/assets/scss/_woot.scss b/app/javascript/dashboard/assets/scss/_woot.scss index d736d7b5a..004d8bf59 100644 --- a/app/javascript/dashboard/assets/scss/_woot.scss +++ b/app/javascript/dashboard/assets/scss/_woot.scss @@ -5,6 +5,9 @@ @import 'shared/assets/fonts/InterDisplay/inter-display'; @import 'shared/assets/fonts/inter'; +// Next Colors +@import 'next-colors'; + @import 'shared/assets/stylesheets/animations'; @import 'shared/assets/stylesheets/colors'; @import 'shared/assets/stylesheets/spacing'; @@ -54,214 +57,19 @@ @apply text-n-blue-text; } +.custom-dashed-border { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25'%3E%3Crect x='0' y='0' width='100%25' height='100%25' fill='none' rx='16' ry='16' stroke='%23E2E3E7' stroke-width='2' stroke-dasharray='6, 8' stroke-dashoffset='0' stroke-linecap='round'/%3E%3C/svg%3E"); + background-position: center; + background-repeat: no-repeat; + background-size: 100% 100%; +} + +.dark .custom-dashed-border { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25'%3E%3Crect x='0' y='0' width='100%25' height='100%25' fill='none' rx='16' ry='16' stroke='%23343434' stroke-width='2' stroke-dasharray='6, 8' stroke-dashoffset='0' stroke-linecap='round'/%3E%3C/svg%3E"); +} + // scss-lint:disable PropertySortOrder @layer base { - /* NEXT COLORS START */ - :root { - /* slate */ - --slate-1: 252 252 253; - --slate-2: 249 249 251; - --slate-3: 240 240 243; - --slate-4: 232 232 236; - --slate-5: 224 225 230; - --slate-6: 217 217 224; - --slate-7: 205 206 214; - --slate-8: 185 187 198; - --slate-9: 139 141 152; - --slate-10: 128 131 141; - --slate-11: 96 100 108; - --slate-12: 28 32 36; - - --iris-1: 253 253 255; - --iris-2: 248 248 255; - --iris-3: 240 241 254; - --iris-4: 230 231 255; - --iris-5: 218 220 255; - --iris-6: 203 205 255; - --iris-7: 184 186 248; - --iris-8: 155 158 240; - --iris-9: 91 91 214; - --iris-10: 81 81 205; - --iris-11: 87 83 198; - --iris-12: 39 41 98; - - --ruby-1: 255 252 253; - --ruby-2: 255 247 248; - --ruby-3: 254 234 237; - --ruby-4: 255 220 225; - --ruby-5: 255 206 214; - --ruby-6: 248 191 200; - --ruby-7: 239 172 184; - --ruby-8: 229 146 163; - --ruby-9: 229 70 102; - --ruby-10: 220 59 93; - --ruby-11: 202 36 77; - --ruby-12: 100 23 43; - - --amber-1: 254 253 251; - --amber-2: 254 251 233; - --amber-3: 255 247 194; - --amber-4: 255 238 156; - --amber-5: 251 229 119; - --amber-6: 243 214 115; - --amber-7: 233 193 98; - --amber-8: 226 163 54; - --amber-9: 255 197 61; - --amber-10: 255 186 24; - --amber-11: 171 100 0; - --amber-12: 79 52 34; - - --teal-1: 250 254 253; - --teal-2: 243 251 249; - --teal-3: 224 248 243; - --teal-4: 204 243 234; - --teal-5: 184 234 224; - --teal-6: 161 222 210; - --teal-7: 131 205 193; - --teal-8: 83 185 171; - --teal-9: 18 165 148; - --teal-10: 13 155 138; - --teal-11: 0 133 115; - --teal-12: 13 61 56; - - --gray-1: 252 252 252; - --gray-2: 249 249 249; - --gray-3: 240 240 240; - --gray-4: 232 232 232; - --gray-5: 224 224 224; - --gray-6: 217 217 217; - --gray-7: 206 206 206; - --gray-8: 187 187 187; - --gray-9: 141 141 141; - --gray-10: 131 131 131; - --gray-11: 100 100 100; - --gray-12: 32 32 32; - - --background-color: 253 253 253; - --text-blue: 8 109 224; - --border-container: 236 236 236; - --border-strong: 235 235 235; - --border-weak: 234 234 234; - --solid-1: 255 255 255; - --solid-2: 255 255 255; - --solid-3: 255 255 255; - --solid-active: 255 255 255; - --solid-amber: 252 232 193; - --solid-blue: 218 236 255; - --solid-iris: 230 231 255; - - --alpha-1: 67, 67, 67, 0.06; - --alpha-2: 201, 202, 207, 0.15; - --alpha-3: 255, 255, 255, 0.96; - --black-alpha-1: 0, 0, 0, 0.12; - --black-alpha-2: 0, 0, 0, 0.04; - --border-blue: 39, 129, 246, 0.5; - --white-alpha: 255, 255, 255, 0.8; - } - - .dark { - /* slate */ - --slate-1: 17 17 19; - --slate-2: 24 25 27; - --slate-3: 33 34 37; - --slate-4: 39 42 45; - --slate-5: 46 49 53; - --slate-6: 54 58 63; - --slate-7: 67 72 78; - --slate-8: 90 97 105; - --slate-9: 105 110 119; - --slate-10: 119 123 132; - --slate-11: 176 180 186; - --slate-12: 237 238 240; - - --iris-1: 19 19 30; - --iris-2: 23 22 37; - --iris-3: 32 34 72; - --iris-4: 38 42 101; - --iris-5: 48 51 116; - --iris-6: 61 62 130; - --iris-7: 74 74 149; - --iris-8: 89 88 177; - --iris-9: 91 91 214; - --iris-10: 84 114 228; - --iris-11: 158 177 255; - --iris-12: 224 223 254; - - --ruby-1: 25 17 19; - --ruby-2: 30 21 23; - --ruby-3: 58 20 30; - --ruby-4: 78 19 37; - --ruby-5: 94 26 46; - --ruby-6: 111 37 57; - --ruby-7: 136 52 71; - --ruby-8: 179 68 90; - --ruby-9: 229 70 102; - --ruby-10: 236 90 114; - --ruby-11: 255 148 157; - --ruby-12: 254 210 225; - - --amber-1: 22 18 12; - --amber-2: 29 24 15; - --amber-3: 48 32 8; - --amber-4: 63 39 0; - --amber-5: 77 48 0; - --amber-6: 92 61 5; - --amber-7: 113 79 25; - --amber-8: 143 100 36; - --amber-9: 255 197 61; - --amber-10: 255 214 10; - --amber-11: 255 202 22; - --amber-12: 255 231 179; - - --teal-1: 13 21 20; - --teal-2: 17 28 27; - --teal-3: 13 45 42; - --teal-4: 2 59 55; - --teal-5: 8 72 67; - --teal-6: 20 87 80; - --teal-7: 28 105 97; - --teal-8: 32 126 115; - --teal-9: 18 165 148; - --teal-10: 14 179 158; - --teal-11: 11 216 182; - --teal-12: 173 240 221; - - --gray-1: 17 17 17; - --gray-2: 25 25 25; - --gray-3: 34 34 34; - --gray-4: 42 42 42; - --gray-5: 49 49 49; - --gray-6: 58 58 58; - --gray-7: 72 72 72; - --gray-8: 96 96 96; - --gray-9: 110 110 110; - --gray-10: 123 123 123; - --gray-11: 180 180 180; - --gray-12: 238 238 238; - - --background-color: 18 18 19; - --border-strong: 52 52 52; - --border-weak: 38 38 42; - --solid-1: 23 23 26; - --solid-2: 29 30 36; - --solid-3: 44 45 54; - --solid-active: 53 57 66; - --solid-amber: 42 37 30; - --solid-blue: 16 49 91; - --solid-iris: 38 42 101; - --text-blue: 126 182 255; - - --alpha-1: 36, 36, 36, 0.8; - --alpha-2: 139, 147, 182, 0.15; - --alpha-3: 36, 38, 45, 0.9; - --black-alpha-1: 0, 0, 0, 0.3; - --black-alpha-2: 0, 0, 0, 0.2; - --border-blue: 39, 129, 246, 0.5; - --border-container: 236, 236, 236, 0; - --white-alpha: 255, 255, 255, 0.1; - } - /* NEXT COLORS END */ - :root { --color-amber-25: 254 253 251; --color-amber-50: 255 249 237; diff --git a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss index 2f9c35c51..ffcfca545 100644 --- a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss +++ b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss @@ -1,13 +1,13 @@ @mixin label-multiselect-hover { &::after { - @apply text-woot-600 dark:text-woot-600; + @apply text-n-brand; } &:hover { - @apply bg-slate-50 dark:bg-slate-700; + @apply bg-n-slate-3; &::after { - @apply text-woot-500 dark:text-woot-500; + @apply text-n-blue-text; } } } @@ -18,20 +18,16 @@ } &.multiselect--disabled { - @apply opacity-50 border border-slate-200 dark:border-slate-600 rounded-md cursor-not-allowed; + @apply opacity-50 rounded-lg cursor-not-allowed pointer-events-auto; .multiselect__select { - @apply cursor-not-allowed bg-white dark:bg-slate-900 rounded-md; - } - - .multiselect__tags { - @apply border-0; + @apply cursor-not-allowed bg-transparent rounded-lg; } } .multiselect--active { > .multiselect__tags { - @apply border-woot-500 dark:border-woot-500; + @apply outline-n-blue-border; } } @@ -44,7 +40,7 @@ } .multiselect__content-wrapper { - @apply bg-white dark:bg-slate-900 border border-solid border-slate-200 dark:border-slate-600 text-slate-800 dark:text-slate-100; + @apply bg-n-alpha-black2 text-n-slate-12 backdrop-blur-[100px] border-0 border-none outline outline-1 outline-n-weak rounded-b-lg; } .multiselect__content { @@ -61,15 +57,19 @@ @apply mb-0; } + &::after { + @apply bottom-0 flex items-center justify-center text-center; + } + &.multiselect__option--highlight { - @apply bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-100; + @apply bg-n-alpha-black2 text-n-slate-12; } &.multiselect__option--highlight:hover { - @apply bg-woot-50 dark:bg-woot-600 text-slate-800 dark:text-slate-100; + @apply bg-n-brand/10 text-n-slate-12; &::after { - @apply bg-woot-50 dark:bg-woot-600 text-slate-600 dark:text-slate-200; + @apply bg-transparent text-center text-n-slate-12; } } @@ -78,17 +78,17 @@ } &.multiselect__option--selected { - @apply bg-woot-50 dark:bg-woot-600 text-slate-800 dark:text-slate-100; + @apply bg-n-brand/20 text-n-slate-12; + + &::after { + @apply bg-transparent; + } &.multiselect__option--highlight:hover { - @apply bg-woot-75 dark:bg-woot-600; - - &::after { - @apply bg-transparent; - } + @apply bg-transparent; &::after:hover { - @apply text-slate-800 dark:text-slate-100; + @apply text-n-slate-12 bg-transparent; } } } @@ -96,23 +96,27 @@ } .multiselect__tags { - @apply bg-white dark:bg-slate-900 border border-solid border-slate-200 dark:border-slate-600 m-0 min-h-[2.875rem] pt-0; + @apply bg-n-alpha-black2 border-0 grid items-center w-full border-none outline-1 outline outline-n-weak hover:outline-n-slate-6 m-0 min-h-[2.875rem] rounded-lg pt-0; input { - @apply border-0 border-none; + @apply border-0 border-none bg-transparent dark:bg-transparent text-n-slate-12 placeholder:text-n-slate-10; } } + .multiselect__spinner { + background-color: transparent; + } + .multiselect__tags-wrap { @apply inline-block leading-none mt-1; } .multiselect__placeholder { - @apply text-slate-400 dark:text-slate-400 font-normal pt-3; + @apply text-n-slate-10 font-normal pt-3; } .multiselect__tag { - @apply bg-slate-50 dark:bg-slate-800 mt-1 text-slate-800 dark:text-slate-100 pr-6 pl-2.5 py-1.5; + @apply bg-n-alpha-white mt-1 text-n-slate-12 pr-6 pl-2.5 py-1.5; } .multiselect__tag-icon { @@ -125,7 +129,7 @@ } .multiselect__single { - @apply bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 inline-block mb-0 py-3 px-2.5 overflow-hidden whitespace-nowrap text-ellipsis; + @apply bg-transparent text-n-slate-12 inline-block mb-0 py-3 px-2.5 overflow-hidden whitespace-nowrap text-ellipsis; } } @@ -143,11 +147,11 @@ } > .multiselect__tags { - @apply border-transparent; + @apply outline-transparent; } &.multiselect--active > .multiselect__tags { - @apply border-woot-500 dark:border-woot-500; + @apply outline-n-blue-border; } } } @@ -179,7 +183,7 @@ .multiselect__tags, .multiselect__input, .multiselect { - @apply bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 rounded-[5px] text-sm min-h-[2.5rem]; + @apply text-n-slate-12 rounded-lg text-sm min-h-[2.5rem]; } .multiselect__input { @@ -187,7 +191,7 @@ } .multiselect__single { - @apply items-center flex m-0 text-sm max-h-[2.375rem] text-slate-800 dark:text-slate-100 bg-white dark:bg-slate-900 py-3 px-0.5; + @apply items-center flex m-0 text-sm max-h-[2.375rem] bg-transparent text-n-slate-12 py-3 px-0.5; } .multiselect__placeholder { @@ -208,6 +212,24 @@ } } +.multiselect--disabled { + background-color: rgba(var(--black-alpha-2)) !important; + + .multiselect__tags { + @apply hover:outline-n-weak; + } +} + +.multiselect--active { + .multiselect__select::before { + @apply top-[62%]; + } +} + +.multiselect__select::before { + top: 60% !important; +} + .multiselect-wrap--medium { .multiselect__tags, .multiselect__input { @@ -217,16 +239,16 @@ .multiselect__tags, .multiselect__input, .multiselect { - @apply bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 text-sm h-12 min-h-[3rem]; + @apply bg-n-alpha-black2 text-n-slate-12 text-sm h-12 min-h-[3rem]; } .multiselect__input { @apply h-[2.875rem] min-h-[2.875rem]; - margin-bottom: 0px !important; + margin-bottom: 0 !important; } .multiselect__single { - @apply items-center flex m-0 text-sm py-1 px-0.5 text-slate-800 dark:text-slate-100 bg-white dark:bg-slate-900; + @apply items-center flex m-0 text-sm py-1 px-0.5 bg-transparent text-n-slate-12; } .multiselect__placeholder { diff --git a/app/javascript/dashboard/assets/scss/widgets/_base.scss b/app/javascript/dashboard/assets/scss/widgets/_base.scss index a8da23f3b..b2c271173 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_base.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_base.scss @@ -1,22 +1,22 @@ // scss-lint:disable QualifyingElement // Base typography +// ------------------------- h1, h2, h3, h4, h5, h6 { - @apply font-medium text-slate-800 dark:text-slate-50; + @apply font-medium text-n-slate-12; } p { text-rendering: optimizeLegibility; - @apply mb-2 leading-[1.65] text-sm; a { - @apply text-woot-500 dark:text-woot-500 cursor-pointer; + @apply text-n-brand dark:text-n-brand cursor-pointer; } } @@ -41,64 +41,82 @@ dl:not(.reset-base) { } // Form elements +// ------------------------- label { - @apply text-slate-800 dark:text-slate-200 block m-0 leading-7 text-sm font-medium; - - &.error { - input { - @apply mb-1; - } - } + @apply text-n-slate-12 block m-0 leading-7 text-sm font-medium; } .input-wrap, .help-text { - @apply text-slate-800 dark:text-slate-100 text-sm font-medium; - - .help-text { - @apply font-normal text-slate-600 dark:text-slate-400; - } + @apply text-n-slate-11 text-sm font-medium; } // Focus outline removal .button, -textarea, -input:focus { +textarea { outline: none; } -// Inputs -input[type='text']:not(.reset-base), -input[type='number']:not(.reset-base), -input[type='password']:not(.reset-base), -input[type='date']:not(.reset-base), -input[type='email']:not(.reset-base), -input[type='url']:not(.reset-base) { - @apply block box-border w-full transition-colors focus:border-woot-500 dark:focus:border-woot-600 duration-[0.25s] ease-[ease-in-out] h-10 appearance-none mx-0 mt-0 mb-4 p-2 rounded-md text-base font-normal bg-white dark:bg-slate-900 focus:bg-white focus:dark:bg-slate-900 text-slate-900 dark:text-slate-100 border border-solid border-slate-200 dark:border-slate-600; +// Field base styles (Input, TextArea, Select) +@layer components { + .field-base { + @apply block box-border w-full transition-colors duration-[0.25s] ease-[ease-in-out] focus:outline-n-brand dark:focus:outline-n-brand appearance-none mx-0 mt-0 mb-4 py-2 px-3 rounded-lg text-base font-normal bg-n-alpha-black2 placeholder:text-n-slate-10 dark:placeholder:text-n-slate-10 text-n-slate-12 border-none outline outline-1 outline-n-weak dark:outline-n-weak hover:outline-n-slate-6 dark:hover:outline-n-slate-6; + } - &[disabled] { - @apply bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-400 border-slate-200 dark:border-slate-600 cursor-not-allowed; + .field-disabled { + @apply opacity-50 outline-n-weak dark:outline-n-weak cursor-not-allowed; + } + + .field-error { + @apply outline outline-1 outline-n-ruby-8 dark:outline-n-ruby-8 hover:outline-n-ruby-9 dark:hover:outline-n-ruby-9 disabled:outline-n-ruby-8 dark:disabled:outline-n-ruby-8; } } -input[type='file'] { - @apply bg-n-background leading-[1.15] mb-4; +$form-input-selector: "input[type]:not([type='file']):not([type='checkbox']):not([type='radio']):not([type='range']):not([type='button']):not([type='submit']):not([type='reset']):not([type='color']):not([type='image']):not([type='hidden']):not(.reset-base):not(.multiselect__input):not(.no-margin)"; + +#{$form-input-selector} { + @apply field-base h-10; + + &[disabled] { + @apply field-disabled; + } + + &.error { + @apply field-error mb-1; + } +} + +input[type='file']:not(.multiselect__input) { + @apply leading-[1.15] mb-4 border-0 bg-transparent text-sm; } // Select select { background-image: url("data:image/svg+xml;utf8,"); - background-position: right -1rem center; background-size: 9px 6px; - @apply h-10 mx-0 mt-0 mb-4 bg-origin-content focus-visible:outline-none bg-no-repeat py-2 pr-6 pl-2 rounded-md w-full text-base font-normal appearance-none transition-colors focus:border-woot-500 dark:focus:border-woot-600 duration-[0.25s] ease-[ease-in-out] bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 border border-solid border-slate-200 dark:border-slate-600; + + @apply field-base h-10 bg-origin-content focus-visible:outline-none bg-no-repeat py-2 ltr:bg-[right_-1rem_center] rtl:bg-[left_-1rem_center] ltr:pr-6 rtl:pl-6 rtl:pr-3 ltr:pl-3; + + &[disabled] { + @apply field-disabled; + } } // Textarea textarea { - @apply block box-border w-full transition-colors focus:border-woot-500 dark:focus:border-woot-600 duration-[0.25s] ease-[ease-in-out] h-16 appearance-none mx-0 mt-0 mb-4 p-2 rounded-md text-base font-normal bg-white dark:bg-slate-900 focus:bg-white focus:dark:bg-slate-900 text-slate-900 dark:text-slate-100 border border-solid border-slate-200 dark:border-slate-600; + @apply field-base h-16; &[disabled] { - @apply bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-400 border-slate-200 dark:border-slate-600 cursor-not-allowed; + @apply field-disabled; + } +} + +// Add mb-1 when .help-text exists within the same label container +label:has(.help-text) { + input, + textarea, + select { + margin-bottom: 0.25rem !important; } } @@ -109,18 +127,41 @@ textarea { } } +// FormKit support +.formkit-outer[data-invalid='true'] { + #{$form-input-selector}, + textarea, + select { + @apply field-error; + } + + .formkit-message { + @apply text-n-ruby-9 dark:text-n-ruby-9 block text-sm mb-2.5 w-full; + } +} + .error { - input, + #{$form-input-selector}, input:not([type]), textarea, select, .multiselect > .multiselect__tags, .multiselect:not(.no-margin) { - @apply border border-solid border-red-400 dark:border-red-400 mb-1; + @apply field-error; + } + + // Only add mb-1 when .message exists within the same .error container + // And exclude no-margin from the margin-bottom + &:has(.message) { + input:not(.no-margin), + textarea, + select { + margin-bottom: 0.25rem !important; + } } .message { - @apply text-red-400 dark:text-red-400 block text-sm mb-2.5 w-full; + @apply text-n-ruby-9 dark:text-n-ruby-9 block text-sm mb-2.5 w-full; } } @@ -130,7 +171,7 @@ textarea { } .error { - @apply border-red-400 dark:border-red-400; + @apply text-n-ruby-9 dark:text-n-ruby-9; } } @@ -141,11 +182,11 @@ code { @apply text-xs border-0; &.hljs { - @apply bg-n-slate-3 dark:bg-n-solid-3 text-slate-800 dark:text-slate-50 rounded-lg p-5; + @apply bg-n-slate-3 dark:bg-n-solid-3 text-n-slate-12 rounded-lg p-5; .hljs-number, .hljs-string { - @apply text-red-800 dark:text-red-400; + @apply text-n-ruby-9 dark:text-n-ruby-9; } .hljs-name, diff --git a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss index 1213e7c26..133e07bd1 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss @@ -42,7 +42,7 @@ button { } } - &:hover:not(.secondary):not(.success):not(.alert):not(.warning):not( + &:hover:not(:disabled):not(.success):not(.alert):not(.warning):not( .clear ):not(.smooth):not(.hollow) { @apply bg-n-brand/80 dark:bg-n-brand/80; @@ -117,63 +117,63 @@ button { } &.hollow { - @apply border border-n-brand/40 bg-transparent text-n-blue-text hover:bg-n-brand/20; + @apply border border-n-brand/40 bg-transparent text-n-blue-text hover:enabled:bg-n-brand/20; &.secondary { - @apply text-n-slate-12 border-n-slate-5 hover:bg-n-slate-5; + @apply text-n-slate-12 border-n-slate-5 hover:enabled:bg-n-slate-5; } &.success { - @apply text-n-teal-9 border-n-teal-8 hover:bg-n-teal-5; + @apply text-n-teal-9 border-n-teal-8 hover:enabled:bg-n-teal-5; } &.alert { - @apply text-n-ruby-9 border-n-ruby-8 hover:bg-n-ruby-5; + @apply text-n-ruby-9 border-n-ruby-8 hover:enabled:bg-n-ruby-5; } &.warning { - @apply text-n-amber-9 border-n-amber-8 hover:bg-n-amber-5; + @apply text-n-amber-9 border-n-amber-8 hover:enabled:bg-n-amber-5; } } // Smooth style &.smooth { - @apply bg-n-brand/10 dark:bg-n-brand/30 text-n-blue-text hover:bg-n-brand/20 dark:hover:bg-n-brand/40; + @apply bg-n-brand/10 dark:bg-n-brand/30 text-n-blue-text hover:enabled:bg-n-brand/20 dark:hover:enabled:bg-n-brand/40; &.secondary { - @apply bg-n-slate-4 text-n-slate-11 hover:text-n-slate-11 hover:bg-n-slate-5; + @apply bg-n-slate-4 text-n-slate-11 hover:enabled:text-n-slate-11 hover:enabled:bg-n-slate-5; } &.success { - @apply bg-n-teal-4 text-n-teal-11 hover:text-n-teal-11 hover:bg-n-teal-5; + @apply bg-n-teal-4 text-n-teal-11 hover:enabled:text-n-teal-11 hover:enabled:bg-n-teal-5; } &.alert { - @apply bg-n-ruby-4 text-n-ruby-11 hover:text-n-ruby-11 hover:bg-n-ruby-5; + @apply bg-n-ruby-4 text-n-ruby-11 hover:enabled:text-n-ruby-11 hover:enabled:bg-n-ruby-5; } &.warning { - @apply bg-n-amber-4 text-n-amber-11 hover:text-n-amber-11 hover:bg-n-amber-5; + @apply bg-n-amber-4 text-n-amber-11 hover:enabled:text-n-amber-11 hover:enabled:bg-n-amber-5; } } &.clear { - @apply text-n-blue-text hover:bg-n-brand/10 dark:hover:bg-n-brand/30; + @apply text-n-blue-text hover:enabled:bg-n-brand/10 dark:hover:enabled:bg-n-brand/30; &.secondary { - @apply text-n-slate-12 hover:bg-n-slate-4; + @apply text-n-slate-12 hover:enabled:bg-n-slate-4; } &.success { - @apply text-n-teal-10 hover:bg-n-teal-4; + @apply text-n-teal-10 hover:enabled:bg-n-teal-4; } &.alert { - @apply text-n-ruby-11 hover:bg-n-ruby-4; + @apply text-n-ruby-11 hover:enabled:bg-n-ruby-4; } &.warning { - @apply text-n-amber-11 hover:bg-n-amber-4; + @apply text-n-amber-11 hover:enabled:bg-n-amber-4; } &:active { diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue b/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue index 4afca6269..cd8c767b8 100644 --- a/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue +++ b/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue @@ -247,10 +247,9 @@ defineExpose({ :placeholder="item.placeholder" class="[&>div>button]:h-8" :class="{ - '[&>div>button]:bg-n-alpha-black2 [&>div>button]:!outline-transparent': + '[&>div>button]:bg-n-alpha-black2 [&>div>button:not(.focused)]:!outline-transparent': !isDetailsView, - '[&>div>button]:!outline-n-weak [&>div>button]:hover:!outline-n-strong [&>div>button]:!bg-n-alpha-black2': - isDetailsView, + '[&>div>button]:!bg-n-alpha-black2': isDetailsView, }" @update:model-value="handleCountrySelection" /> @@ -266,7 +265,9 @@ defineExpose({ :placeholder="item.placeholder" :message-type="getMessageType(item.key)" :custom-input-class="`h-8 !pt-1 !pb-1 ${ - !isDetailsView ? '[&:not(.error,.focus)]:!border-transparent' : '' + !isDetailsView + ? '[&:not(.error,.focus)]:!outline-transparent' + : '' }`" class="w-full" @input=" @@ -303,7 +304,7 @@ defineExpose({ v-model=" state.additionalAttributes.socialProfiles[item.key.toLowerCase()] " - class="w-auto min-w-[100px] text-sm bg-transparent reset-base text-n-slate-12 dark:text-n-slate-12 placeholder:text-n-slate-10 dark:placeholder:text-n-slate-10" + class="w-auto min-w-[100px] text-sm bg-transparent outline-none reset-base text-n-slate-12 dark:text-n-slate-12 placeholder:text-n-slate-10 dark:placeholder:text-n-slate-10" :placeholder="item.placeholder" :size="item.placeholder.length" @input="emit('update', state)" diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/ContactCustomAttributes.vue b/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/ContactCustomAttributes.vue index 4964668df..039d2c709 100644 --- a/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/ContactCustomAttributes.vue +++ b/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/ContactCustomAttributes.vue @@ -135,7 +135,7 @@ const hasNoUsedAttributes = computed(() => usedAttributes.value.length === 0); :placeholder=" t('CONTACTS_LAYOUT.SIDEBAR.ATTRIBUTES.SEARCH_PLACEHOLDER') " - class="w-full h-8 py-2 pl-10 pr-2 text-sm border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12" + class="w-full h-8 py-2 pl-10 pr-2 text-sm reset-base outline-none border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12" />