Matthew Trejo 1 місяць тому
батько
коміт
788d8a1fd6

+ 194 - 0
docs/A460200203F003049003.xml

@@ -0,0 +1,194 @@
+<?xml version="1.0" encoding="UTF-8"?><autorizacion>
+  <estado>AUTORIZADO</estado>
+  <numeroAutorizacion>0508202501120579845500120020030000000811505211214</numeroAutorizacion>
+  <fechaAutorizacion>05/08/2025 03:09:53</fechaAutorizacion>
+  <comprobante><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
+<factura id="comprobante" version="1.1.0">
+<infoTributaria>
+<ambiente>2</ambiente>
+<tipoEmision>1</tipoEmision>
+<razonSocial>TORRES TORRES GUILLERMO DAVID</razonSocial>
+<nombreComercial>TORRES TORRES GUILLERMO DAVID</nombreComercial>
+<ruc>1205798455001</ruc>
+<claveAcceso>0508202501120579845500120020030000000811505211214</claveAcceso>
+<codDoc>01</codDoc>
+<estab>002</estab>
+<ptoEmi>003</ptoEmi>
+<secuencial>000000081</secuencial>
+<dirMatriz>GUAYAQUIL</dirMatriz>
+</infoTributaria>
+<infoFactura>
+<fechaEmision>05/08/2025</fechaEmision>
+<dirEstablecimiento>AV REAL AUDIENCIA</dirEstablecimiento>
+<obligadoContabilidad>NO</obligadoContabilidad>
+<tipoIdentificacionComprador>04</tipoIdentificacionComprador>
+<razonSocialComprador>GOMEZ JUNCO JEREMY</razonSocialComprador>
+<identificacionComprador>1207687052001</identificacionComprador>
+<totalSinImpuestos>216.26</totalSinImpuestos>
+<totalDescuento>0.00</totalDescuento>
+<totalConImpuestos>
+<totalImpuesto>
+<codigo>2</codigo>
+<codigoPorcentaje>0</codigoPorcentaje>
+<baseImponible>138.00</baseImponible>
+<valor>0.00</valor>
+</totalImpuesto>
+<totalImpuesto>
+<codigo>2</codigo>
+<codigoPorcentaje>4</codigoPorcentaje>
+<baseImponible>78.26</baseImponible>
+<valor>11.74</valor>
+</totalImpuesto>
+</totalConImpuestos>
+<propina>0.00</propina>
+<importeTotal>228.00</importeTotal>
+<moneda>DOLAR</moneda>
+<pagos>
+<pago>
+<formaPago>01</formaPago>
+<total>228.00</total>
+<plazo>0</plazo>
+<unidadTiempo>dias</unidadTiempo>
+</pago>
+</pagos>
+</infoFactura>
+<detalles>
+<detalle>
+<codigoPrincipal>KIT3BGU002</codigoPrincipal>
+<codigoAuxiliar>KIT3BGU002</codigoAuxiliar>
+<descripcion>KIT PT 3 BGU</descripcion>
+<cantidad>1.00</cantidad>
+<precioUnitario>78.260000</precioUnitario>
+<descuento>0.00</descuento>
+<precioTotalSinImpuesto>78.26</precioTotalSinImpuesto>
+<impuestos>
+<impuesto>
+<codigo>2</codigo>
+<codigoPorcentaje>4</codigoPorcentaje>
+<tarifa>15</tarifa>
+<baseImponible>78.26</baseImponible>
+<valor>11.74</valor>
+</impuesto>
+</impuestos>
+</detalle>
+<detalle>
+<codigoPrincipal>KIT3BGU001</codigoPrincipal>
+<codigoAuxiliar>KIT3BGU001</codigoAuxiliar>
+<descripcion>KIT TERCERO DE BACHILLERATO COMPLETO</descripcion>
+<cantidad>1.00</cantidad>
+<precioUnitario>138.000000</precioUnitario>
+<descuento>0.00</descuento>
+<precioTotalSinImpuesto>138.00</precioTotalSinImpuesto>
+<impuestos>
+<impuesto>
+<codigo>2</codigo>
+<codigoPorcentaje>0</codigoPorcentaje>
+<tarifa>0</tarifa>
+<baseImponible>138.00</baseImponible>
+<valor>0.00</valor>
+</impuesto>
+</impuestos>
+</detalle>
+</detalles>
+<infoAdicional>
+<campoAdicional nombre="Direccion">BABAHOYO BARRIO LINDO</campoAdicional>
+<campoAdicional nombre="Email">jeremy.gomez@siselecvideo.com</campoAdicional>
+<campoAdicional nombre="Telefono">969549504</campoAdicional>
+</infoAdicional>
+<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:etsi="http://uri.etsi.org/01903/v1.3.2#" Id="Signature311372">
+<ds:SignedInfo Id="Signature-SignedInfo520705">
+<ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></ds:CanonicalizationMethod>
+<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"></ds:SignatureMethod>
+<ds:Reference Id="SignedPropertiesID1035786" Type="http://uri.etsi.org/01903#SignedProperties" URI="#Signature311372-SignedProperties667913">
+<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></ds:DigestMethod>
+<ds:DigestValue>RLdIjCwcI7amtthaAPrDSuUhVe8=</ds:DigestValue>
+</ds:Reference>
+<ds:Reference URI="#Certificate1195125">
+<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></ds:DigestMethod>
+<ds:DigestValue>qcjr2fXP1oQItM0mN3rHQYbNKN0=</ds:DigestValue>
+</ds:Reference>
+<ds:Reference Id="Reference-ID-43224" URI="#comprobante">
+<ds:Transforms>
+<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></ds:Transform>
+</ds:Transforms>
+<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></ds:DigestMethod>
+<ds:DigestValue>dVBVqntS56NCz4H4mvJneRLzI/g=</ds:DigestValue>
+</ds:Reference>
+</ds:SignedInfo>
+<ds:SignatureValue Id="SignatureValue866142">
+oxY4ZEVgWRREhbhPLwUBv4aug0kmmPa1hDwagTY19gNUpFDAgnqvViAkcdLObip9Wu3L0jyYkJFJ
+5nEt5cfKpnjqmCK0jhKMlNZVG/2akEJMTyz8Qi0BSAgOyf5VrV6Cg7DIvylAAF1q1DLOIVFVGBQG
+KkLjnswt+PggyYTB/P6j8gUfSX5pIWLpci1VPvmyIxF8ZR4lywrL30NzIJZTigLOOmOXQMrU5NcZ
+BfroYHVGZ8DvOglEwfcUEBglUPPs9Ku3dfGewEC3KUS/CxiLDngUJGovDTgIATZPNsXS5eBh/psq
+4RqVlrsb2JTdS3zmqXTkC0LLkhEoEFtIkyCWbw==
+</ds:SignatureValue>
+<ds:KeyInfo Id="Certificate1195125">
+<ds:X509Data>
+<ds:X509Certificate>
+MIIKzzCCCLegAwIBAgIMAzjIRvL+MySfbAyhMA0GCSqGSIb3DQEBCwUAMIG5MRYwFAYDVQQFEw0x
+NzkyNjAxMjE1MDAxMQswCQYDVQQGEwJFQzE2MDQGA1UEChMtQU5GQUMgQVVUT1JJREFEIERFIENF
+UlRJRklDQUNJT04gRUNVQURPUiBDLkEuMSUwIwYDVQQLExxBTkYgQXV0b3JpZGFkIGludGVybWVk
+aWEgIEVDMTMwMQYDVQQDEypBTkYgSGlnaCBBc3N1cmFuY2UgRWN1YWRvciBJbnRlcm1lZGlhdGUg
+Q0EwHhcNMjUwNDI0MTkzNTMwWhcNMjcwNDI0MTkzNTMwWjCCAZExJjAkBgNVBAoMHVRPUlJFUyBU
+T1JSRVMgR1VJTExFUk1PIERBVklEMRowGAYKKwYBBAGCpEIKBAwKMTIwNTc5ODQ1NTErMCkGA1UE
+CwwiQ2VydGlmaWNhZG8gUGVyc29uYSBOYXR1cmFsIFJVQyBFQzEYMBYGA1UEKgwPR1VJTExFUk1P
+IERBVklEMRYwFAYDVQRhEw0xMjA1Nzk4NDU1MDAxMQswCQYDVQQGEwJFQzETMBEGA1UEBRMKMTIw
+NTc5ODQ1NTEWMBQGA1UEBAwNVE9SUkVTIFRPUlJFUzExMC8GA1UEAwwoMTIwNTc5ODQ1NSBHVUlM
+TEVSTU8gREFWSUQgVE9SUkVTIFRPUlJFUzElMCMGCSqGSIb3DQEJARYWZnVuZGFmZWNjYkBob3Rt
+YWlsLmNvbTExMC8GA1UEDQwoQ2VydGlmaWNhZG8gcGFyYSBQZXJzb25hIE5hdHVyYWwgY29uIFJV
+QzESMBAGA1UECAwJTE9TIFLDjU9TMREwDwYDVQQHDAhCQUJBSE9ZTzCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBAKfBC6zcO6H33mIkx+0FSbyRqZ3jDAXJIYL1aLUSDaWsvWiOmydJ+5iF
+3Nl5iwTaTbWHNNlefQ30nScLeVJ7MpJJfscrIyOd6CSL2y4DxjlzlTyltwbIGbRvmbI/4E/WgCjs
+tFn8Kbsz0k2H9Gh88zd6Cu56WcQ3v9L47u/rLic5K98dawysKlCBy0BvHRnloD323tZmLf3CpCYo
+4CNSqSHCfyyRdksLQkHJ+ZmVpPgCAC/g+q1zEuHKndqVho5dDRyJbXVcO5RPaF9L2mkBR76u1YzZ
+6XKjRJkMdaX90c0YH7r8OBD+dI99DUqh5FO5wgjfjcrJkQxBT6KuZtkly9kCAwEAAaOCBPowggT2
+MB8GA1UdIwQYMBaAFJKu1MuCtJnG0XsRbGXqxU1oIfmsMB0GA1UdDgQWBBTSxvdrUIrE559TsKLb
+ZFTmGRKRCDCBsQYDVR0RBIGpMIGmgRZmdW5kYWZlY2NiQGhvdG1haWwuY29tpIGLMIGIMR8wHQYK
+KwYBBAGCpEIKAQwPR1VJTExFUk1PIERBVklEMRYwFAYKKwYBBAGCpEIKAgwGVE9SUkVTMRYwFAYK
+KwYBBAGCpEIKAwwGVE9SUkVTMRowGAYKKwYBBAGCpEIKBAwKMTIwNTc5ODQ1NTEZMBcGCSsGAQQB
+gY8cAQwKQS0yNDA0MjAyNTAOBgNVHQ8BAf8EBAMCBsAwHQYDVR0lBBYwFAYIKwYBBQUHAwQGCCsG
+AQUFBwMCMFAGA1UdHwRJMEcwRaBDoEGGP2h0dHA6Ly9jcmwuYW5mLmVzL2NybC9BTkZIaWdoQXNz
+dXJhbmNlRWN1YWRvckludGVybWVkaWF0ZUNBLmNybDCCAQ0GA1UdIASCAQQwggEAMIHnBgwrBgEE
+AYKkQgIFAQMwgdYwMAYIKwYBBQUHAgEWJGh0dHBzOi8vYW5mLmVzL2VjL3JlcG9zaXRvcmlvLWxl
+Z2FsLzCBoQYIKwYBBQUHAgIwgZQMgZFDZXJ0aWZpY2FkbyBjb25mb3JtZSBhIGxhIGxlZ2lzbGFj
+acOzbiBkZSBmaXJtYSBlbGVjdHLDs25pY2EuIEFudGVzIGRlIGFjZXB0YXJsbyBjb21wcnVlYmUg
+aW50ZWdyaWRhZCwgbGltaXRhY2lvbmVzLCB2aWdlbmNpYSB5IHVzb3MgYXV0b3JpemFkb3MuMAkG
+BwQAi+xAAQEwCQYHYIVUAQMFCDCBmwYIKwYBBQUHAQEEgY4wgYswJwYIKwYBBQUHMAGGG2h0dHA6
+Ly9vY3NwLmFuZi5lcy9zcGFpbi9BVjBgBggrBgEFBQcwAoZUaHR0cDovL3d3dy5hbmYuZXMvZXMv
+Y2VydGlmaWNhdGVzLWRvd25sb2FkL0FORkhpZ2hBc3N1cmFuY2VFY3VhZG9ySW50ZXJtZWRpYXRl
+Q0EuY2VyMAwGA1UdEwEB/wQCMAAwKAYJKwYBBAGBjxwTBBsMGTYzODMwMjcxNTg5OC00OTUwMTc2
+NTA0ODcwFQYKKwYBBAGBjxwTAgQHDAUzNzQ0MjApBgorBgEEAYKkQhMBBBsMGTQ5NTAxNzY1MDQ4
+Ny00ODcwNjUxMjIzNDgwGgYKKwYBBAGCpEIDAQQMDAoxMjA1Nzk4NDU1MC0GCisGAQQBgqRCAwoE
+HwwdVE9SUkVTIFRPUlJFUyBHVUlMTEVSTU8gREFWSUQwHQYKKwYBBAGCpEIDCwQPDA0xMjA1Nzk4
+NDU1MDAxMBIGCisGAQQBgqRCAwwEBAwCRUMwHwYKKwYBBAGCpEIDAgQRDA9HVUlMTEVSTU8gREFW
+SUQwHQYKKwYBBAGCpEIDAwQPDA1UT1JSRVMgVE9SUkVTMBgGCisGAQQBgqRCAwkECgwIQkFCQUhP
+WU8wFwYKKwYBBAGCpEIqAQQJDAdVSU8tMDI4MBMGCisGAQQBgqRCLgEEBQwDNzMwMFAGCisGAQQB
+gqRCLwEEQgxAYTEzNDNhMTFlYzliZTI4MzIxMmI5OTNlNDkzYzBhNDFlZTk2YmVhOWFlNmU3ZjI0
+MDIyNDY3NWQyZGMzNzFkNjANBgkqhkiG9w0BAQsFAAOCAgEAFc6yLoRqu+0mDLFKb3g6M5kRd8cy
+dWteApSU5JMRIQZHKZAr6dOo6a7W8bi8BF15G2qoMVnvctKEBg1+cVDjUlDEbPB+r+Op0fTfX0Su
+yFS01hHl7C8lXMZttORYwVUFaEbkBtg17aRHYn73OimF3gW7i9QEXXo0wwzkWSIbb86VSCOS9Rxr
+1gGTltdK4bpq6sR5IE3ubBOkGPPrbYyXLIHDNqYrmXs8xfgPnkxu6iIdL2HU/BfpUywLNm6jUP+O
+vofgR4dqBgRhWCg16oT8+kGBE/VOcdUO6Ii/RmHJ7cc4yrSMVmjtW+PcQW5Mhu43U71kSeMa8Tg5
+gUFLdivRtA8xoNRSnlT+CwJAN9bttsxHF+7q/tTuiqoRZsQ/PqGA7zLJfcMhbiJ+kPmXA7Ue8Zgy
+DRaMQhF4tjDLJeHJ7r9WAFBAgYmLDoMm4FYxICNooFx8+gYA90B73Fln5VNmOc9FMgay1qcKHlgs
+niLsUMNXUeI0A0Zjlnjc6yAd/1vPlD/aQVUA01B9b2Dk0Jd36O1nQ014Z5Yx0SRhZLiYxBdkdzPW
+D5ZH+ULry7MQ0U6/KH4YkWA0cK1YaQYjzR0bd3YEBT1nqlp6Egh1MrtimLzug+mVBBXB7apb3VKJ
+yzQZcajdHnVDKHmeCL4/AuYGi4TzfCYM7UoVEgf/U5qtIpU=
+</ds:X509Certificate>
+</ds:X509Data>
+<ds:KeyValue>
+<ds:RSAKeyValue>
+<ds:Modulus>
+p8ELrNw7offeYiTH7QVJvJGpneMMBckhgvVotRINpay9aI6bJ0n7mIXc2XmLBNpNtYc02V59DfSd
+Jwt5Unsykkl+xysjI53oJIvbLgPGOXOVPKW3BsgZtG+Zsj/gT9aAKOy0WfwpuzPSTYf0aHzzN3oK
+7npZxDe/0vju7+suJzkr3x1rDKwqUIHLQG8dGeWgPfbe1mYt/cKkJijgI1KpIcJ/LJF2SwtCQcn5
+mZWk+AIAL+D6rXMS4cqd2pWGjl0NHIltdVw7lE9oX0vaaQFHvq7VjNnpcqNEmQx1pf3RzRgfuvw4
+EP50j30NSqHkU7nCCN+NysmRDEFPoq5m2SXL2Q==
+</ds:Modulus>
+<ds:Exponent>AQAB</ds:Exponent>
+</ds:RSAKeyValue>
+</ds:KeyValue>
+</ds:KeyInfo>
+<ds:Object Id="Signature311372-Object225594"><etsi:QualifyingProperties Target="#Signature311372"><etsi:SignedProperties Id="Signature311372-SignedProperties667913"><etsi:SignedSignatureProperties><etsi:SigningTime>2025-08-05T15:09:50-05:00</etsi:SigningTime><etsi:SigningCertificate><etsi:Cert><etsi:CertDigest><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></ds:DigestMethod><ds:DigestValue>40qwBeGaoZabahu76qIwqBN7cT8=</ds:DigestValue></etsi:CertDigest><etsi:IssuerSerial><ds:X509IssuerName>CN=ANF High Assurance Ecuador Intermediate CA,OU=ANF Autoridad intermedia  EC,O=ANFAC AUTORIDAD DE CERTIFICACION ECUADOR C.A.,C=EC,2.5.4.5=#130d31373932363031323135303031</ds:X509IssuerName><ds:X509SerialNumber>997100657440602162318806177</ds:X509SerialNumber></etsi:IssuerSerial></etsi:Cert></etsi:SigningCertificate></etsi:SignedSignatureProperties><etsi:SignedDataObjectProperties><etsi:DataObjectFormat ObjectReference="#Reference-ID-43224"><etsi:Description>Documento de ejemplo</etsi:Description><etsi:MimeType>text/xml</etsi:MimeType></etsi:DataObjectFormat></etsi:SignedDataObjectProperties></etsi:SignedProperties></etsi:QualifyingProperties></ds:Object></ds:Signature></factura>]]></comprobante>
+  <mensajes/>
+</autorizacion>

+ 0 - 225
docs/factura.php

@@ -1,225 +0,0 @@
-<?php
-// Función para generar el XML de la factura
-function generarFacturaXML($datos) {
-    // Crear documento XML
-    $xml = new DOMDocument('1.0', 'UTF-8');
-    $xml->formatOutput = true;
-    
-    // Raíz: factura
-    $factura = $xml->createElement('factura');
-    $factura->setAttribute('id', 'comprobante');
-    $factura->setAttribute('version', '1.1.0');
-    $xml->appendChild($factura);
-    
-    // infoTributaria
-    $infoTributaria = $xml->createElement('infoTributaria');
-    $factura->appendChild($infoTributaria);
-    
-    $infoTributaria->appendChild($xml->createElement('ambiente', $datos['ambiente'])); // 1=Pruebas, 2=Producción
-    $infoTributaria->appendChild($xml->createElement('tipoEmision', $datos['tipoEmision'])); // 1=Normal
-    $infoTributaria->appendChild($xml->createElement('razonSocial', $datos['razonSocial']));
-    $infoTributaria->appendChild($xml->createElement('nombreComercial', $datos['nombreComercial']));
-    $infoTributaria->appendChild($xml->createElement('ruc', $datos['ruc']));
-    $infoTributaria->appendChild($xml->createElement('claveAcceso', $datos['claveAcceso'])); // Genera una clave única (ver algoritmo SRI)
-    $infoTributaria->appendChild($xml->createElement('codDoc', '01')); // 01=Factura
-    $infoTributaria->appendChild($xml->createElement('estab', $datos['estab']));
-    $infoTributaria->appendChild($xml->createElement('ptoEmi', $datos['ptoEmi']));
-    $infoTributaria->appendChild($xml->createElement('secuencial', $datos['secuencial']));
-    $infoTributaria->appendChild($xml->createElement('dirMatriz', $datos['dirMatriz']));
-    
-    // infoFactura
-    $infoFactura = $xml->createElement('infoFactura');
-    $factura->appendChild($infoFactura);
-    
-    $infoFactura->appendChild($xml->createElement('fechaEmision', $datos['fechaEmision']));
-    $infoFactura->appendChild($xml->createElement('dirEstablecimiento', $datos['dirEstablecimiento']));
-    $infoFactura->appendChild($xml->createElement('contribuyenteEspecial', $datos['contribuyenteEspecial']));
-    $infoFactura->appendChild($xml->createElement('obligadoContabilidad', $datos['obligadoContabilidad']));
-    
-    // Identificación del comprador
-    $tipoIdentificacionComprador = $xml->createElement('tipoIdentificacionComprador', $datos['tipoIdentificacionComprador']);
-    $infoFactura->appendChild($tipoIdentificacionComprador);
-    $razonSocialComprador = $xml->createElement('razonSocialComprador', $datos['razonSocialComprador']);
-    $infoFactura->appendChild($razonSocialComprador);
-    $identificacionComprador = $xml->createElement('identificacionComprador', $datos['identificacionComprador']);
-    $infoFactura->appendChild($identificacionComprador);
-    $direccionComprador = $xml->createElement('direccionComprador', $datos['direccionComprador']);
-    $infoFactura->appendChild($direccionComprador);
-    
-    $infoFactura->appendChild($xml->createElement('totalSinImpuestos', $datos['totalSinImpuestos']));
-    $infoFactura->appendChild($xml->createElement('totalDescuento', $datos['totalDescuento']));
-    
-    // totalConImpuestos (simplificado: solo IVA)
-    $totalConImpuestos = $xml->createElement('totalConImpuestos');
-    $infoFactura->appendChild($totalConImpuestos);
-    $totalImpuesto = $xml->createElement('totalImpuesto');
-    $totalConImpuestos->appendChild($totalImpuesto);
-    $totalImpuesto->appendChild($xml->createElement('codigo', '2')); // IVA
-    $totalImpuesto->appendChild($xml->createElement('codigoPorcentaje', $datos['codigoPorcentaje']));
-    $totalImpuesto->appendChild($xml->createElement('baseImponible', $datos['baseImponible']));
-    $totalImpuesto->appendChild($xml->createElement('valor', $datos['valorImpuesto']));
-    
-    $infoFactura->appendChild($xml->createElement('propina', '0.00'));
-    $infoFactura->appendChild($xml->createElement('importeTotal', $datos['importeTotal']));
-    $infoFactura->appendChild($xml->createElement('moneda', 'DOLAR'));
-    
-    // pagos (simplificado)
-    $pagos = $xml->createElement('pagos');
-    $infoFactura->appendChild($pagos);
-    $pago = $xml->createElement('pago');
-    $pagos->appendChild($pago);
-    $pago->appendChild($xml->createElement('formaPago', $datos['formaPago']));
-    $pago->appendChild($xml->createElement('total', $datos['importeTotal']));
-    
-    // detalles
-    $detalles = $xml->createElement('detalles');
-    $factura->appendChild($detalles);
-    foreach ($datos['detalles'] as $item) {
-        $detalle = $xml->createElement('detalle');
-        $detalles->appendChild($detalle);
-        $detalle->appendChild($xml->createElement('codigoPrincipal', $item['codigoPrincipal']));
-        $detalle->appendChild($xml->createElement('descripcion', $item['descripcion']));
-        $detalle->appendChild($xml->createElement('cantidad', $item['cantidad']));
-        $detalle->appendChild($xml->createElement('precioUnitario', $item['precioUnitario']));
-        $detalle->appendChild($xml->createElement('descuento', $item['descuento']));
-        $detalle->appendChild($xml->createElement('precioTotalSinImpuesto', $item['precioTotalSinImpuesto']));
-        
-        // impuestos por detalle (IVA)
-        $impuestos = $xml->createElement('impuestos');
-        $detalle->appendChild($impuestos);
-        $impuesto = $xml->createElement('impuesto');
-        $impuestos->appendChild($impuesto);
-        $impuesto->appendChild($xml->createElement('codigo', '2'));
-        $impuesto->appendChild($xml->createElement('codigoPorcentaje', $item['codigoPorcentaje']));
-        $impuesto->appendChild($xml->createElement('tarifa', $item['tarifa']));
-        $impuesto->appendChild($xml->createElement('baseImponible', $item['baseImponible']));
-        $impuesto->appendChild($xml->createElement('valor', $item['valorImpuesto']));
-    }
-    
-    // infoAdicional (opcional)
-    $infoAdicional = $xml->createElement('infoAdicional');
-    $factura->appendChild($infoAdicional);
-    $campoAdicional = $xml->createElement('campoAdicional');
-    $campoAdicional->setAttribute('nombre', 'Email');
-    $campoAdicional->nodeValue = $datos['emailComprador'];
-    $infoAdicional->appendChild($campoAdicional);
-    
-    return $xml->saveXML();
-}
-
-// Procesar formulario
-$xmlGenerado = '';
-if ($_SERVER['REQUEST_METHOD'] == 'POST') {
-    // Recopilar datos del formulario
-    $datos = [
-        'ambiente' => $_POST['ambiente'],
-        'tipoEmision' => $_POST['tipoEmision'],
-        'razonSocial' => $_POST['razonSocial'],
-        'nombreComercial' => $_POST['nombreComercial'],
-        'ruc' => $_POST['ruc'],
-        'claveAcceso' => $_POST['claveAcceso'], // Debes generar esto dinámicamente
-        'estab' => $_POST['estab'],
-        'ptoEmi' => $_POST['ptoEmi'],
-        'secuencial' => $_POST['secuencial'],
-        'dirMatriz' => $_POST['dirMatriz'],
-        'fechaEmision' => $_POST['fechaEmision'],
-        'dirEstablecimiento' => $_POST['dirEstablecimiento'],
-        'contribuyenteEspecial' => $_POST['contribuyenteEspecial'],
-        'obligadoContabilidad' => $_POST['obligadoContabilidad'],
-        'tipoIdentificacionComprador' => $_POST['tipoIdentificacionComprador'],
-        'razonSocialComprador' => $_POST['razonSocialComprador'],
-        'identificacionComprador' => $_POST['identificacionComprador'],
-        'direccionComprador' => $_POST['direccionComprador'],
-        'totalSinImpuestos' => $_POST['totalSinImpuestos'],
-        'totalDescuento' => $_POST['totalDescuento'],
-        'codigoPorcentaje' => $_POST['codigoPorcentaje'],
-        'baseImponible' => $_POST['baseImponible'],
-        'valorImpuesto' => $_POST['valorImpuesto'],
-        'importeTotal' => $_POST['importeTotal'],
-        'formaPago' => $_POST['formaPago'],
-        'emailComprador' => $_POST['emailComprador'],
-        'detalles' => [
-            [
-                'codigoPrincipal' => $_POST['codigoPrincipal1'],
-                'descripcion' => $_POST['descripcion1'],
-                'cantidad' => $_POST['cantidad1'],
-                'precioUnitario' => $_POST['precioUnitario1'],
-                'descuento' => $_POST['descuento1'],
-                'precioTotalSinImpuesto' => $_POST['precioTotalSinImpuesto1'],
-                'codigoPorcentaje' => $_POST['codigoPorcentaje1'],
-                'tarifa' => $_POST['tarifa1'],
-                'baseImponible' => $_POST['baseImponible1'],
-                'valorImpuesto' => $_POST['valorImpuesto1']
-            ]
-            // Agrega más items si es necesario
-        ]
-    ];
-    
-    $xmlGenerado = generarFacturaXML($datos);
-    
-    // Opcional: Guardar en archivo
-    file_put_contents('factura_generada.xml', $xmlGenerado);
-}
-?>
-
-<!DOCTYPE html>
-<html lang="es">
-<head>
-    <meta charset="UTF-8">
-    <title>Generar Factura XML para SRI</title>
-</head>
-<body>
-    <h1>Formulario para Generar Factura Electrónica (SRI Ecuador)</h1>
-    <form method="post">
-        <h2>Info Tributaria</h2>
-        Ambiente: <input type="text" name="ambiente" value="1" required><br>
-        Tipo Emisión: <input type="text" name="tipoEmision" value="1" required><br>
-        Razón Social: <input type="text" name="razonSocial" required><br>
-        Nombre Comercial: <input type="text" name="nombreComercial" required><br>
-        RUC: <input type="text" name="ruc" required><br>
-        Clave de Acceso: <input type="text" name="claveAcceso" required> (Genera una única)<br>
-        Establecimiento: <input type="text" name="estab" required><br>
-        Punto de Emisión: <input type="text" name="ptoEmi" required><br>
-        Secuencial: <input type="text" name="secuencial" required><br>
-        Dirección Matriz: <input type="text" name="dirMatriz" required><br>
-        
-        <h2>Info Factura</h2>
-        Fecha Emisión: <input type="date" name="fechaEmision" required><br>
-        Dirección Establecimiento: <input type="text" name="dirEstablecimiento" required><br>
-        Contribuyente Especial: <input type="text" name="contribuyenteEspecial"><br>
-        Obligado Contabilidad: <input type="text" name="obligadoContabilidad" value="SI" required><br>
-        Tipo Identificación Comprador: <input type="text" name="tipoIdentificacionComprador" value="04" required> (04=RUC)<br>
-        Razón Social Comprador: <input type="text" name="razonSocialComprador" required><br>
-        Identificación Comprador: <input type="text" name="identificacionComprador" required><br>
-        Dirección Comprador: <input type="text" name="direccionComprador" required><br>
-        Total Sin Impuestos: <input type="text" name="totalSinImpuestos" required><br>
-        Total Descuento: <input type="text" name="totalDescuento" value="0.00" required><br>
-        Código Porcentaje IVA: <input type="text" name="codigoPorcentaje" value="2" required><br>
-        Base Imponible IVA: <input type="text" name="baseImponible" required><br>
-        Valor Impuesto: <input type="text" name="valorImpuesto" required><br>
-        Importe Total: <input type="text" name="importeTotal" required><br>
-        Forma de Pago: <input type="text" name="formaPago" value="01" required> (01=Efectivo)<br>
-        Email Comprador: <input type="email" name="emailComprador"><br>
-        
-        <h2>Detalles (Item 1)</h2>
-        Código Principal: <input type="text" name="codigoPrincipal1" required><br>
-        Descripción: <input type="text" name="descripcion1" required><br>
-        Cantidad: <input type="text" name="cantidad1" required><br>
-        Precio Unitario: <input type="text" name="precioUnitario1" required><br>
-        Descuento: <input type="text" name="descuento1" value="0.00" required><br>
-        Precio Total Sin Impuesto: <input type="text" name="precioTotalSinImpuesto1" required><br>
-        Código Porcentaje IVA: <input type="text" name="codigoPorcentaje1" value="2" required><br>
-        Tarifa IVA: <input type="text" name="tarifa1" value="15.00" required><br>
-        Base Imponible IVA: <input type="text" name="baseImponible1" required><br>
-        Valor Impuesto: <input type="text" name="valorImpuesto1" required><br>
-        
-        <button type="submit">Generar XML</button>
-    </form>
-    
-    <?php if ($xmlGenerado): ?>
-        <h2>XML Generado:</h2>
-        <textarea rows="20" cols="100"><?php echo htmlspecialchars($xmlGenerado); ?></textarea>
-        <br><a href="factura_generada.xml" download>Descargar XML</a>
-    <?php endif; ?>
-</body>
-</html>

+ 167 - 0
docs/invoice.xml

@@ -0,0 +1,167 @@
+<?xml version="1.0"?>
+<factura xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="comprobante" version="1.0.0">
+    <infoTributaria>
+        <ambiente>1</ambiente>
+        <tipoEmision>1</tipoEmision>
+        <razonSocial>razonSocial0</razonSocial>
+        <nombreComercial>nombreComercial0</nombreComercial>
+        <ruc>0000000000001</ruc>
+        <codDoc>01</codDoc>
+        <estab>000</estab>
+        <ptoEmi>000</ptoEmi>
+        <secuencial>000000000</secuencial>
+        <dirMatriz>dirMatriz0</dirMatriz>
+        <agenteRetencion>0</agenteRetencion>
+        <contribuyenteRimpe>CONTRIBUYENTE RÉGIMEN RIMPE</contribuyenteRimpe>
+        <claveAcceso>Sat Jan 01 2000 00:00:00 GMT-0500 (Ecuador
+            Time)0100000000000011000000000000000620270241NaN</claveAcceso>
+    </infoTributaria>
+    <infoFactura>
+        <fechaEmision>01/01/2000</fechaEmision>
+        <dirEstablecimiento>dirEstablecimiento0</dirEstablecimiento>
+        <contribuyenteEspecial>contribuyente</contribuyenteEspecial>
+        <obligadoContabilidad>SI</obligadoContabilidad>
+        <comercioExterior>EXPORTADOR</comercioExterior>
+        <incoTermFactura>A</incoTermFactura>
+        <lugarIncoTerm>lugarIncoTerm0</lugarIncoTerm>
+        <paisOrigen>000</paisOrigen>
+        <puertoEmbarque>puertoEmbarque0</puertoEmbarque>
+        <puertoDestino>puertoDestino0</puertoDestino>
+        <paisDestino>000</paisDestino>
+        <paisAdquisicion>000</paisAdquisicion>
+        <tipoIdentificacionComprador>04</tipoIdentificacionComprador>
+        <guiaRemision>000-000-000000000</guiaRemision>
+        <razonSocialComprador>razonSocialComprador0</razonSocialComprador>
+        <identificacionComprador>identificacionComprador0</identificacionComprador>
+        <direccionComprador>direccionComprador0</direccionComprador>
+        <totalSinImpuestos>50.00</totalSinImpuestos>
+        <totalSubsidio>50.00</totalSubsidio>
+        <incoTermTotalSinImpuestos>A</incoTermTotalSinImpuestos>
+        <totalDescuento>0.00</totalDescuento>
+        <codDocReembolso>00</codDocReembolso>
+        <totalComprobantesReembolso>50.00</totalComprobantesReembolso>
+        <totalBaseImponibleReembolso>50.00</totalBaseImponibleReembolso>
+        <totalImpuestoReembolso>50.00</totalImpuestoReembolso>
+        <totalConImpuestos>
+            <totalImpuesto>
+                <codigo>2</codigo>
+                <codigoPorcentaje>0</codigoPorcentaje>
+                <descuentoAdicional>0.00</descuentoAdicional>
+                <baseImponible>50.00</baseImponible>
+                <tarifa>49.50</tarifa>
+                <valor>50.00</valor>
+                <valorDevolucionIva>50.00</valorDevolucionIva>
+            </totalImpuesto>
+            <totalImpuesto>
+                <codigo>2</codigo>
+                <codigoPorcentaje>0</codigoPorcentaje>
+                <descuentoAdicional>0.00</descuentoAdicional>
+                <baseImponible>50.00</baseImponible>
+                <tarifa>49.50</tarifa>
+                <valor>50.00</valor>
+                <valorDevolucionIva>50.00</valorDevolucionIva>
+            </totalImpuesto>
+        </totalConImpuestos>
+        <compensaciones>
+            <compensacion>
+                <codigo>1</codigo>
+                <tarifa>49.50</tarifa>
+                <valor>50.00</valor>
+            </compensacion>
+            <compensacion>
+                <codigo>1</codigo>
+                <tarifa>49.50</tarifa>
+                <valor>50.00</valor>
+            </compensacion>
+        </compensaciones>
+        <propina>50.00</propina>
+        <fleteInternacional>50.00</fleteInternacional>
+        <seguroInternacional>50.00</seguroInternacional>
+        <gastosAduaneros>50.00</gastosAduaneros>
+        <gastosTransporteOtros>50.00</gastosTransporteOtros>
+        <importeTotal>50.00</importeTotal>
+        <moneda>moneda0</moneda>
+        <placa>placa0</placa>
+        <pagos>
+            <pago>
+                <formaPago>01</formaPago>
+                <total>50.00</total>
+                <plazo>50.00</plazo>
+                <unidadTiempo>unidadTiempo</unidadTiempo>
+            </pago>
+            <pago>
+                <formaPago>01</formaPago>
+                <total>50.00</total>
+                <plazo>50.00</plazo>
+                <unidadTiempo>unidadTiempo</unidadTiempo>
+            </pago>
+        </pagos>
+        <valorRetIva>50.00</valorRetIva>
+        <valorRetRenta>50.00</valorRetRenta>
+    </infoFactura>
+    <detalles>
+        <detalle>
+            <codigoPrincipal>codigoPrincipal0</codigoPrincipal>
+            <codigoAuxiliar>codigoAuxiliar0</codigoAuxiliar>
+            <descripcion>descripcion0</descripcion>
+            <unidadMedida>unidadMedida0</unidadMedida>
+            <cantidad>50.000000</cantidad>
+            <precioUnitario>50.000000</precioUnitario>
+            <precioSinSubsidio>50.000000</precioSinSubsidio>
+            <descuento>50.00</descuento>
+            <precioTotalSinImpuesto>50.00</precioTotalSinImpuesto>
+            <detallesAdicionales>
+                <detAdicional nombre="nombre0" valor="valor0" />
+                <detAdicional nombre="nombre1" valor="valor1" />
+            </detallesAdicionales>
+            <impuestos>
+                <impuesto>
+                    <codigo>2</codigo>
+                    <codigoPorcentaje>0</codigoPorcentaje>
+                    <tarifa>49.50</tarifa>
+                    <baseImponible>50.00</baseImponible>
+                    <valor>50.00</valor>
+                </impuesto>
+                <impuesto>
+                    <codigo>2</codigo>
+                    <codigoPorcentaje>0</codigoPorcentaje>
+                    <tarifa>49.50</tarifa>
+                    <baseImponible>50.00</baseImponible>
+                    <valor>50.00</valor>
+                </impuesto>
+            </impuestos>
+        </detalle>
+        <detalle>
+            <codigoPrincipal>codigoPrincipal1</codigoPrincipal>
+            <codigoAuxiliar>codigoAuxiliar1</codigoAuxiliar>
+            <descripcion>descripcion1</descripcion>
+            <unidadMedida>unidadMedida1</unidadMedida>
+            <cantidad>50.000000</cantidad>
+            <precioUnitario>50.000000</precioUnitario>
+            <precioSinSubsidio>50.000000</precioSinSubsidio>
+            <descuento>50.00</descuento>
+            <precioTotalSinImpuesto>50.00</precioTotalSinImpuesto>
+            <detallesAdicionales>
+                <detAdicional nombre="nombre0" valor="valor0" />
+                <detAdicional nombre="nombre1" valor="valor1" />
+            </detallesAdicionales>
+            <impuestos>
+                <impuesto>
+                    <codigo>2</codigo>
+                    <codigoPorcentaje>0</codigoPorcentaje>
+                    <tarifa>49.50</tarifa>
+                    <baseImponible>50.00</baseImponible>
+                    <valor>50.00</valor>
+                </impuesto>
+                <impuesto>
+                    <codigo>2</codigo>
+                    <codigoPorcentaje>0</codigoPorcentaje>
+                    <tarifa>49.50</tarifa>
+                    <baseImponible>50.00</baseImponible>
+                    <valor>50.00</valor>
+                </impuesto>
+            </impuestos>
+        </detalle>
+    </detalles>
+</factura>

+ 2610 - 0
docs/sidebar.md

@@ -0,0 +1,2610 @@
+---
+title: Sidebar
+description: A composable, themeable and customizable sidebar component.
+component: true
+---
+
+<figure className="flex flex-col gap-4">
+  ```tsx
+import { AppSidebar } from "@/components/blocks/sidebar-07/components/app-sidebar"
+import {
+  Breadcrumb,
+  BreadcrumbItem,
+  BreadcrumbLink,
+  BreadcrumbList,
+  BreadcrumbPage,
+  BreadcrumbSeparator,
+} from "@/components/ui/breadcrumb"
+import { Separator } from "@/components/ui/separator"
+import {
+  SidebarInset,
+  SidebarProvider,
+  SidebarTrigger,
+} from "@/components/ui/sidebar"
+
+export function Page() {
+  return (
+    <SidebarProvider>
+      <AppSidebar />
+      <SidebarInset>
+        <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
+          <div className="flex items-center gap-2 px-4">
+            <SidebarTrigger className="-ml-1" />
+            <Separator
+              orientation="vertical"
+              className="mr-2 data-[orientation=vertical]:h-4"
+            />
+            <Breadcrumb>
+              <BreadcrumbList>
+                <BreadcrumbItem className="hidden md:block">
+                  <BreadcrumbLink href="#">
+                    Building Your Application
+                  </BreadcrumbLink>
+                </BreadcrumbItem>
+                <BreadcrumbSeparator className="hidden md:block" />
+                <BreadcrumbItem>
+                  <BreadcrumbPage>Data Fetching</BreadcrumbPage>
+                </BreadcrumbItem>
+              </BreadcrumbList>
+            </Breadcrumb>
+          </div>
+        </header>
+        <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
+          <div className="grid auto-rows-min gap-4 md:grid-cols-3">
+            <div className="bg-muted/50 aspect-video rounded-xl" />
+            <div className="bg-muted/50 aspect-video rounded-xl" />
+            <div className="bg-muted/50 aspect-video rounded-xl" />
+          </div>
+          <div className="bg-muted/50 min-h-[100vh] flex-1 rounded-xl md:min-h-min" />
+        </div>
+      </SidebarInset>
+    </SidebarProvider>
+  )
+}
+
+```
+  <figcaption className="text-center text-sm text-gray-500">
+    A sidebar that collapses to icons.
+  </figcaption>
+</figure>
+
+Sidebars are one of the most complex components to build. They are central
+to any application and often contain a lot of moving parts.
+
+I don't like building sidebars. So I built 30+ of them. All kinds of
+configurations. Then I extracted the core components into `sidebar.tsx`.
+
+We now have a solid foundation to build on top of. Composable. Themeable.
+Customizable.
+
+[Browse the Blocks Library](/blocks).
+
+## Installation
+
+<CodeTabs>
+
+<TabsList>
+  <TabsTrigger value="cli">CLI</TabsTrigger>
+  <TabsTrigger value="manual">Manual</TabsTrigger>
+</TabsList>
+<TabsContent value="cli">
+
+<Steps>
+
+<Step>Run the following command to install `sidebar.tsx`</Step>
+
+```bash
+npx shadcn@latest add sidebar
+```
+
+<Step>Add the following colors to your CSS file</Step>
+
+The command above should install the colors for you. If not, copy and paste the following in your CSS file.
+
+We'll go over the colors later in the [theming section](/docs/components/sidebar#theming).
+
+```css showLineNumbers title="app/globals.css"
+@layer base {
+  :root {
+    --sidebar: oklch(0.985 0 0);
+    --sidebar-foreground: oklch(0.145 0 0);
+    --sidebar-primary: oklch(0.205 0 0);
+    --sidebar-primary-foreground: oklch(0.985 0 0);
+    --sidebar-accent: oklch(0.97 0 0);
+    --sidebar-accent-foreground: oklch(0.205 0 0);
+    --sidebar-border: oklch(0.922 0 0);
+    --sidebar-ring: oklch(0.708 0 0);
+  }
+
+  .dark {
+    --sidebar: oklch(0.205 0 0);
+    --sidebar-foreground: oklch(0.985 0 0);
+    --sidebar-primary: oklch(0.488 0.243 264.376);
+    --sidebar-primary-foreground: oklch(0.985 0 0);
+    --sidebar-accent: oklch(0.269 0 0);
+    --sidebar-accent-foreground: oklch(0.985 0 0);
+    --sidebar-border: oklch(1 0 0 / 10%);
+    --sidebar-ring: oklch(0.439 0 0);
+  }
+}
+```
+
+</Steps>
+
+</TabsContent>
+
+<TabsContent value="manual">
+
+<Steps>
+
+<Step>Copy and paste the following code into your project.</Step>
+
+<ComponentSource name="sidebar" title="components/ui/sidebar.tsx" />
+
+<Step>Update the import paths to match your project setup.</Step>
+
+<Step>Add the following colors to your CSS file</Step>
+
+We'll go over the colors later in the [theming section](/docs/components/sidebar#theming).
+
+```css showLineNumbers title="app/globals.css"
+@layer base {
+  :root {
+    --sidebar: oklch(0.985 0 0);
+    --sidebar-foreground: oklch(0.145 0 0);
+    --sidebar-primary: oklch(0.205 0 0);
+    --sidebar-primary-foreground: oklch(0.985 0 0);
+    --sidebar-accent: oklch(0.97 0 0);
+    --sidebar-accent-foreground: oklch(0.205 0 0);
+    --sidebar-border: oklch(0.922 0 0);
+    --sidebar-ring: oklch(0.708 0 0);
+  }
+
+  .dark {
+    --sidebar: oklch(0.205 0 0);
+    --sidebar-foreground: oklch(0.985 0 0);
+    --sidebar-primary: oklch(0.488 0.243 264.376);
+    --sidebar-primary-foreground: oklch(0.985 0 0);
+    --sidebar-accent: oklch(0.269 0 0);
+    --sidebar-accent-foreground: oklch(0.985 0 0);
+    --sidebar-border: oklch(1 0 0 / 10%);
+    --sidebar-ring: oklch(0.439 0 0);
+  }
+}
+```
+
+</Steps>
+
+</TabsContent>
+
+</CodeTabs>
+
+## Structure
+
+A `Sidebar` component is composed of the following parts:
+
+- `SidebarProvider` - Handles collapsible state.
+- `Sidebar` - The sidebar container.
+- `SidebarHeader` and `SidebarFooter` - Sticky at the top and bottom of the sidebar.
+- `SidebarContent` - Scrollable content.
+- `SidebarGroup` - Section within the `SidebarContent`.
+- `SidebarTrigger` - Trigger for the `Sidebar`.
+
+<Image
+  src="/images/sidebar-structure.png"
+  width="716"
+  height="420"
+  alt="Sidebar Structure"
+  className="mt-6 w-full overflow-hidden rounded-lg border dark:hidden"
+/>
+<Image
+  src="/images/sidebar-structure-dark.png"
+  width="716"
+  height="420"
+  alt="Sidebar Structure"
+  className="mt-6 hidden w-full overflow-hidden rounded-lg border dark:block"
+/>
+
+## Usage
+
+```tsx showLineNumbers title="app/layout.tsx"
+import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
+import { AppSidebar } from "@/components/app-sidebar"
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+  return (
+    <SidebarProvider>
+      <AppSidebar />
+      <main>
+        <SidebarTrigger />
+        {children}
+      </main>
+    </SidebarProvider>
+  )
+}
+```
+
+```tsx showLineNumbers title="components/app-sidebar.tsx"
+import {
+  Sidebar,
+  SidebarContent,
+  SidebarFooter,
+  SidebarGroup,
+  SidebarHeader,
+} from "@/components/ui/sidebar"
+
+export function AppSidebar() {
+  return (
+    <Sidebar>
+      <SidebarHeader />
+      <SidebarContent>
+        <SidebarGroup />
+        <SidebarGroup />
+      </SidebarContent>
+      <SidebarFooter />
+    </Sidebar>
+  )
+}
+```
+
+## Your First Sidebar
+
+Let's start with the most basic sidebar. A collapsible sidebar with a menu.
+
+<Steps>
+
+<Step>
+  Add a `SidebarProvider` and `SidebarTrigger` at the root of your application.
+</Step>
+
+```tsx showLineNumbers title="app/layout.tsx"
+import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
+import { AppSidebar } from "@/components/app-sidebar"
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+  return (
+    <SidebarProvider>
+      <AppSidebar />
+      <main>
+        <SidebarTrigger />
+        {children}
+      </main>
+    </SidebarProvider>
+  )
+}
+```
+
+<Step>Create a new sidebar component at `components/app-sidebar.tsx`.</Step>
+
+```tsx showLineNumbers title="components/app-sidebar.tsx"
+import { Sidebar, SidebarContent } from "@/components/ui/sidebar"
+
+export function AppSidebar() {
+  return (
+    <Sidebar>
+      <SidebarContent />
+    </Sidebar>
+  )
+}
+```
+
+<Step>Now, let's add a `SidebarMenu` to the sidebar.</Step>
+
+We'll use the `SidebarMenu` component in a `SidebarGroup`.
+
+```tsx showLineNumbers title="components/app-sidebar.tsx"
+import { Calendar, Home, Inbox, Search, Settings } from "lucide-react"
+
+import {
+  Sidebar,
+  SidebarContent,
+  SidebarGroup,
+  SidebarGroupContent,
+  SidebarGroupLabel,
+  SidebarMenu,
+  SidebarMenuButton,
+  SidebarMenuItem,
+} from "@/components/ui/sidebar"
+
+// Menu items.
+const items = [
+  {
+    title: "Home",
+    url: "#",
+    icon: Home,
+  },
+  {
+    title: "Inbox",
+    url: "#",
+    icon: Inbox,
+  },
+  {
+    title: "Calendar",
+    url: "#",
+    icon: Calendar,
+  },
+  {
+    title: "Search",
+    url: "#",
+    icon: Search,
+  },
+  {
+    title: "Settings",
+    url: "#",
+    icon: Settings,
+  },
+]
+
+export function AppSidebar() {
+  return (
+    <Sidebar>
+      <SidebarContent>
+        <SidebarGroup>
+          <SidebarGroupLabel>Application</SidebarGroupLabel>
+          <SidebarGroupContent>
+            <SidebarMenu>
+              {items.map((item) => (
+                <SidebarMenuItem key={item.title}>
+                  <SidebarMenuButton asChild>
+                    <a href={item.url}>
+                      <item.icon />
+                      <span>{item.title}</span>
+                    </a>
+                  </SidebarMenuButton>
+                </SidebarMenuItem>
+              ))}
+            </SidebarMenu>
+          </SidebarGroupContent>
+        </SidebarGroup>
+      </SidebarContent>
+    </Sidebar>
+  )
+}
+```
+
+<Step>You've created your first sidebar.</Step>
+
+You should see something like this:
+
+<figure className="flex flex-col gap-4">
+  ```tsx
+"use client"
+
+import {
+  CalendarIcon,
+  HomeIcon,
+  InboxIcon,
+  SearchIcon,
+  SettingsIcon,
+} from "lucide-react"
+
+import {
+  Sidebar,
+  SidebarContent,
+  SidebarGroup,
+  SidebarGroupContent,
+  SidebarGroupLabel,
+  SidebarInset,
+  SidebarMenu,
+  SidebarMenuButton,
+  SidebarMenuItem,
+  SidebarProvider,
+  SidebarTrigger,
+} from "@/components/ui/sidebar"
+
+// Menu items.
+const items = [
+  {
+    title: "Home",
+    url: "#",
+    icon: HomeIcon,
+  },
+  {
+    title: "Inbox",
+    url: "#",
+    icon: InboxIcon,
+  },
+  {
+    title: "Calendar",
+    url: "#",
+    icon: CalendarIcon,
+  },
+  {
+    title: "Search",
+    url: "#",
+    icon: SearchIcon,
+  },
+  {
+    title: "Settings",
+    url: "#",
+    icon: SettingsIcon,
+  },
+]
+
+export function AppSidebar() {
+  return (
+    <SidebarProvider>
+      <Sidebar>
+        <SidebarContent>
+          <SidebarGroup>
+            <SidebarGroupLabel>Application</SidebarGroupLabel>
+            <SidebarGroupContent>
+              <SidebarMenu>
+                {items.map((item) => (
+                  <SidebarMenuItem key={item.title}>
+                    <SidebarMenuButton asChild>
+                      <a href={item.url}>
+                        <item.icon />
+                        <span>{item.title}</span>
+                      </a>
+                    </SidebarMenuButton>
+                  </SidebarMenuItem>
+                ))}
+              </SidebarMenu>
+            </SidebarGroupContent>
+          </SidebarGroup>
+        </SidebarContent>
+      </Sidebar>
+      <SidebarInset>
+        <header className="flex h-12 items-center justify-between px-4">
+          <SidebarTrigger />
+        </header>
+      </SidebarInset>
+    </SidebarProvider>
+  )
+}
+
+```
+  <figcaption className="text-center text-sm text-gray-500">
+    Your first sidebar.
+  </figcaption>
+</figure>
+
+</Steps>
+
+## Components
+
+The components in `sidebar.tsx` are built to be composable i.e you build your sidebar by putting the provided components together. They also compose well with other shadcn/ui components such as `DropdownMenu`, `Collapsible` or `Dialog` etc.
+
+**If you need to change the code in `sidebar.tsx`, you are encouraged to do so. The code is yours. Use `sidebar.tsx` as a starting point and build your own.**
+
+In the next sections, we'll go over each component and how to use them.
+
+## SidebarProvider
+
+The `SidebarProvider` component is used to provide the sidebar context to the `Sidebar` component. You should always wrap your application in a `SidebarProvider` component.
+
+### Props
+
+| Name           | Type                      | Description                                  |
+| -------------- | ------------------------- | -------------------------------------------- |
+| `defaultOpen`  | `boolean`                 | Default open state of the sidebar.           |
+| `open`         | `boolean`                 | Open state of the sidebar (controlled).      |
+| `onOpenChange` | `(open: boolean) => void` | Sets open state of the sidebar (controlled). |
+
+### Width
+
+If you have a single sidebar in your application, you can use the `SIDEBAR_WIDTH` and `SIDEBAR_WIDTH_MOBILE` variables in `sidebar.tsx` to set the width of the sidebar.
+
+```tsx showLineNumbers title="components/ui/sidebar.tsx"
+const SIDEBAR_WIDTH = "16rem"
+const SIDEBAR_WIDTH_MOBILE = "18rem"
+```
+
+For multiple sidebars in your application, you can use the `style` prop to set the width of the sidebar.
+
+To set the width of the sidebar, you can use the `--sidebar-width` and `--sidebar-width-mobile` CSS variables in the `style` prop.
+
+```tsx showLineNumbers title="components/ui/sidebar.tsx"
+<SidebarProvider
+  style={{
+    "--sidebar-width": "20rem",
+    "--sidebar-width-mobile": "20rem",
+  }}
+>
+  <Sidebar />
+</SidebarProvider>
+```
+
+This will handle the width of the sidebar but also the layout spacing.
+
+### Keyboard Shortcut
+
+The `SIDEBAR_KEYBOARD_SHORTCUT` variable is used to set the keyboard shortcut used to open and close the sidebar.
+
+To trigger the sidebar, you use the `cmd+b` keyboard shortcut on Mac and `ctrl+b` on Windows.
+
+You can change the keyboard shortcut by updating the `SIDEBAR_KEYBOARD_SHORTCUT` variable.
+
+```tsx showLineNumbers title="components/ui/sidebar.tsx"
+const SIDEBAR_KEYBOARD_SHORTCUT = "b"
+```
+
+### Persisted State
+
+The `SidebarProvider` supports persisting the sidebar state across page reloads and server-side rendering. It uses cookies to store the current state of the sidebar. When the sidebar state changes, a default cookie named `sidebar_state` is set with the current open/closed state. This cookie is then read on subsequent page loads to restore the sidebar state.
+
+To persist sidebar state in Next.js, set up your `SidebarProvider` in `app/layout.tsx` like this:
+
+```tsx showLineNumbers title="app/layout.tsx"
+import { cookies } from "next/headers"
+
+import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
+import { AppSidebar } from "@/components/app-sidebar"
+
+export async function Layout({ children }: { children: React.ReactNode }) {
+  const cookieStore = await cookies()
+  const defaultOpen = cookieStore.get("sidebar_state")?.value === "true"
+
+  return (
+    <SidebarProvider defaultOpen={defaultOpen}>
+      <AppSidebar />
+      <main>
+        <SidebarTrigger />
+        {children}
+      </main>
+    </SidebarProvider>
+  )
+}
+```
+
+You can change the name of the cookie by updating the `SIDEBAR_COOKIE_NAME` variable in `sidebar.tsx`.
+
+```tsx showLineNumbers title="components/ui/sidebar.tsx"
+const SIDEBAR_COOKIE_NAME = "sidebar_state"
+```
+
+## Sidebar
+
+The main `Sidebar` component used to render a collapsible sidebar.
+
+```tsx showLineNumbers
+import { Sidebar } from "@/components/ui/sidebar"
+
+export function AppSidebar() {
+  return <Sidebar />
+}
+```
+
+### Props
+
+| Property      | Type                              | Description                       |
+| ------------- | --------------------------------- | --------------------------------- |
+| `side`        | `left` or `right`                 | The side of the sidebar.          |
+| `variant`     | `sidebar`, `floating`, or `inset` | The variant of the sidebar.       |
+| `collapsible` | `offcanvas`, `icon`, or `none`    | Collapsible state of the sidebar. |
+
+### side
+
+Use the `side` prop to change the side of the sidebar.
+
+Available options are `left` and `right`.
+
+```tsx showLineNumbers
+import { Sidebar } from "@/components/ui/sidebar"
+
+export function AppSidebar() {
+  return <Sidebar side="left | right" />
+}
+```
+
+### variant
+
+Use the `variant` prop to change the variant of the sidebar.
+
+Available options are `sidebar`, `floating` and `inset`.
+
+```tsx showLineNumbers
+import { Sidebar } from "@/components/ui/sidebar"
+
+export function AppSidebar() {
+  return <Sidebar variant="sidebar | floating | inset" />
+}
+```
+
+<Callout>
+  **Note:** If you use the `inset` variant, remember to wrap your main content
+  in a `SidebarInset` component.
+</Callout>
+
+```tsx showLineNumbers
+<SidebarProvider>
+  <Sidebar variant="inset" />
+  <SidebarInset>
+    <main>{children}</main>
+  </SidebarInset>
+</SidebarProvider>
+```
+
+### collapsible
+
+Use the `collapsible` prop to make the sidebar collapsible.
+
+Available options are `offcanvas`, `icon` and `none`.
+
+```tsx showLineNumbers
+import { Sidebar } from "@/components/ui/sidebar"
+
+export function AppSidebar() {
+  return <Sidebar collapsible="offcanvas | icon | none" />
+}
+```
+
+| Prop        | Description                                                  |
+| ----------- | ------------------------------------------------------------ |
+| `offcanvas` | A collapsible sidebar that slides in from the left or right. |
+| `icon`      | A sidebar that collapses to icons.                           |
+| `none`      | A non-collapsible sidebar.                                   |
+
+## useSidebar
+
+The `useSidebar` hook is used to control the sidebar.
+
+```tsx showLineNumbers
+import { useSidebar } from "@/components/ui/sidebar"
+
+export function AppSidebar() {
+  const {
+    state,
+    open,
+    setOpen,
+    openMobile,
+    setOpenMobile,
+    isMobile,
+    toggleSidebar,
+  } = useSidebar()
+}
+```
+
+| Property        | Type                      | Description                                   |
+| --------------- | ------------------------- | --------------------------------------------- |
+| `state`         | `expanded` or `collapsed` | The current state of the sidebar.             |
+| `open`          | `boolean`                 | Whether the sidebar is open.                  |
+| `setOpen`       | `(open: boolean) => void` | Sets the open state of the sidebar.           |
+| `openMobile`    | `boolean`                 | Whether the sidebar is open on mobile.        |
+| `setOpenMobile` | `(open: boolean) => void` | Sets the open state of the sidebar on mobile. |
+| `isMobile`      | `boolean`                 | Whether the sidebar is on mobile.             |
+| `toggleSidebar` | `() => void`              | Toggles the sidebar. Desktop and mobile.      |
+
+## SidebarHeader
+
+Use the `SidebarHeader` component to add a sticky header to the sidebar.
+
+The following example adds a `<DropdownMenu>` to the `SidebarHeader`.
+
+<figure className="mt-6 flex flex-col gap-4">
+  ```tsx
+"use client"
+
+import { ChevronDownIcon } from "lucide-react"
+
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+import {
+  Sidebar,
+  SidebarHeader,
+  SidebarInset,
+  SidebarMenu,
+  SidebarMenuButton,
+  SidebarMenuItem,
+  SidebarProvider,
+  SidebarTrigger,
+} from "@/components/ui/sidebar"
+
+export function AppSidebar() {
+  return (
+    <SidebarProvider>
+      <Sidebar>
+        <SidebarHeader>
+          <SidebarMenu>
+            <SidebarMenuItem>
+              <DropdownMenu>
+                <DropdownMenuTrigger asChild>
+                  <SidebarMenuButton className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
+                    Select Workspace
+                    <ChevronDownIcon className="ml-auto" />
+                  </SidebarMenuButton>
+                </DropdownMenuTrigger>
+                <DropdownMenuContent className="w-(--radix-popper-anchor-width)">
+                  <DropdownMenuItem>
+                    <span>Acme Inc</span>
+                  </DropdownMenuItem>
+                  <DropdownMenuItem>
+                    <span>Acme Corp.</span>
+                  </DropdownMenuItem>
+                </DropdownMenuContent>
+              </DropdownMenu>
+            </SidebarMenuItem>
+          </SidebarMenu>
+        </SidebarHeader>
+      </Sidebar>
+      <SidebarInset>
+        <header className="flex h-12 items-center justify-between px-4">
+          <SidebarTrigger />
+        </header>
+      </SidebarInset>
+    </SidebarProvider>
+  )
+}
+
+```
+  <figcaption className="text-center text-sm text-gray-500">
+    A sidebar header with a dropdown menu.
+  </figcaption>
+</figure>
+
+```tsx showLineNumbers title="components/app-sidebar.tsx"
+<Sidebar>
+  <SidebarHeader>
+    <SidebarMenu>
+      <SidebarMenuItem>
+        <DropdownMenu>
+          <DropdownMenuTrigger asChild>
+            <SidebarMenuButton>
+              Select Workspace
+              <ChevronDown className="ml-auto" />
+            </SidebarMenuButton>
+          </DropdownMenuTrigger>
+          <DropdownMenuContent className="w-[--radix-popper-anchor-width]">
+            <DropdownMenuItem>
+              <span>Acme Inc</span>
+            </DropdownMenuItem>
+            <DropdownMenuItem>
+              <span>Acme Corp.</span>
+            </DropdownMenuItem>
+          </DropdownMenuContent>
+        </DropdownMenu>
+      </SidebarMenuItem>
+    </SidebarMenu>
+  </SidebarHeader>
+</Sidebar>
+```
+
+## SidebarFooter
+
+Use the `SidebarFooter` component to add a sticky footer to the sidebar.
+
+The following example adds a `<DropdownMenu>` to the `SidebarFooter`.
+
+<figure className="mt-6 flex flex-col gap-4">
+  ```tsx
+"use client"
+
+import { ChevronUpIcon } from "lucide-react"
+
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+import {
+  Sidebar,
+  SidebarContent,
+  SidebarFooter,
+  SidebarHeader,
+  SidebarInset,
+  SidebarMenu,
+  SidebarMenuButton,
+  SidebarMenuItem,
+  SidebarProvider,
+  SidebarTrigger,
+} from "@/components/ui/sidebar"
+
+export function AppSidebar() {
+  return (
+    <SidebarProvider>
+      <Sidebar>
+        <SidebarHeader />
+        <SidebarContent />
+        <SidebarFooter>
+          <SidebarMenu>
+            <SidebarMenuItem>
+              <DropdownMenu>
+                <DropdownMenuTrigger asChild>
+                  <SidebarMenuButton className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
+                    Username
+                    <ChevronUpIcon className="ml-auto" />
+                  </SidebarMenuButton>
+                </DropdownMenuTrigger>
+                <DropdownMenuContent
+                  side="top"
+                  className="w-(--radix-popper-anchor-width)"
+                >
+                  <DropdownMenuItem>
+                    <span>Account</span>
+                  </DropdownMenuItem>
+                  <DropdownMenuItem>
+                    <span>Billing</span>
+                  </DropdownMenuItem>
+                  <DropdownMenuItem>
+                    <span>Sign out</span>
+                  </DropdownMenuItem>
+                </DropdownMenuContent>
+              </DropdownMenu>
+            </SidebarMenuItem>
+          </SidebarMenu>
+        </SidebarFooter>
+      </Sidebar>
+      <SidebarInset>
+        <header className="flex h-12 items-center justify-between px-4">
+          <SidebarTrigger />
+        </header>
+      </SidebarInset>
+    </SidebarProvider>
+  )
+}
+
+```
+  <figcaption className="text-center text-sm text-gray-500">
+    A sidebar footer with a dropdown menu.
+  </figcaption>
+</figure>
+
+```tsx showLineNumbers title="components/app-sidebar.tsx"
+export function AppSidebar() {
+  return (
+    <SidebarProvider>
+      <Sidebar>
+        <SidebarHeader />
+        <SidebarContent />
+        <SidebarFooter>
+          <SidebarMenu>
+            <SidebarMenuItem>
+              <DropdownMenu>
+                <DropdownMenuTrigger asChild>
+                  <SidebarMenuButton>
+                    <User2 /> Username
+                    <ChevronUp className="ml-auto" />
+                  </SidebarMenuButton>
+                </DropdownMenuTrigger>
+                <DropdownMenuContent
+                  side="top"
+                  className="w-[--radix-popper-anchor-width]"
+                >
+                  <DropdownMenuItem>
+                    <span>Account</span>
+                  </DropdownMenuItem>
+                  <DropdownMenuItem>
+                    <span>Billing</span>
+                  </DropdownMenuItem>
+                  <DropdownMenuItem>
+                    <span>Sign out</span>
+                  </DropdownMenuItem>
+                </DropdownMenuContent>
+              </DropdownMenu>
+            </SidebarMenuItem>
+          </SidebarMenu>
+        </SidebarFooter>
+      </Sidebar>
+    </SidebarProvider>
+  )
+}
+```
+
+## SidebarContent
+
+The `SidebarContent` component is used to wrap the content of the sidebar. This is where you add your `SidebarGroup` components. It is scrollable.
+
+```tsx showLineNumbers
+import { Sidebar, SidebarContent } from "@/components/ui/sidebar"
+
+export function AppSidebar() {
+  return (
+    <Sidebar>
+      <SidebarContent>
+        <SidebarGroup />
+        <SidebarGroup />
+      </SidebarContent>
+    </Sidebar>
+  )
+}
+```
+
+## SidebarGroup
+
+Use the `SidebarGroup` component to create a section within the sidebar.
+
+A `SidebarGroup` has a `SidebarGroupLabel`, a `SidebarGroupContent` and an optional `SidebarGroupAction`.
+
+<figure className="mt-6 flex flex-col gap-4">
+  ```tsx
+"use client"
+
+import { LifeBuoyIcon, SendIcon } from "lucide-react"
+
+import {
+  Sidebar,
+  SidebarContent,
+  SidebarGroup,
+  SidebarGroupContent,
+  SidebarGroupLabel,
+  SidebarMenu,
+  SidebarMenuButton,
+  SidebarMenuItem,
+  SidebarProvider,
+} from "@/components/ui/sidebar"
+
+export function AppSidebar() {
+  return (
+    <SidebarProvider>
+      <Sidebar>
+        <SidebarContent>
+          <SidebarGroup>
+            <SidebarGroupLabel>Help</SidebarGroupLabel>
+            <SidebarGroupContent>
+              <SidebarMenu>
+                <SidebarMenuItem>
+                  <SidebarMenuButton>
+                    <LifeBuoyIcon />
+                    Support
+                  </SidebarMenuButton>
+                </SidebarMenuItem>
+                <SidebarMenuItem>
+                  <SidebarMenuButton>
+                    <SendIcon />
+                    Feedback
+                  </SidebarMenuButton>
+                </SidebarMenuItem>
+              </SidebarMenu>
+            </SidebarGroupContent>
+          </SidebarGroup>
+        </SidebarContent>
+      </Sidebar>
+    </SidebarProvider>
+  )
+}
+
+```
+  <figcaption className="text-center text-sm text-gray-500">
+    A sidebar group.
+  </figcaption>
+</figure>
+
+```tsx showLineNumbers
+import { Sidebar, SidebarContent, SidebarGroup } from "@/components/ui/sidebar"
+
+export function AppSidebar() {
+  return (
+    <Sidebar>
+      <SidebarContent>
+        <SidebarGroup>
+          <SidebarGroupLabel>Application</SidebarGroupLabel>
+          <SidebarGroupAction>
+            <Plus /> <span className="sr-only">Add Project</span>
+          </SidebarGroupAction>
+          <SidebarGroupContent></SidebarGroupContent>
+        </SidebarGroup>
+      </SidebarContent>
+    </Sidebar>
+  )
+}
+```
+
+## Collapsible SidebarGroup
+
+To make a `SidebarGroup` collapsible, wrap it in a `Collapsible`.
+
+<figure className="mt-6 flex flex-col gap-4">
+  ```tsx
+"use client"
+
+import { ChevronDownIcon, LifeBuoyIcon, SendIcon } from "lucide-react"
+
+import {
+  Collapsible,
+  CollapsibleContent,
+  CollapsibleTrigger,
+} from "@/components/ui/collapsible"
+import {
+  Sidebar,
+  SidebarContent,
+  SidebarGroup,
+  SidebarGroupContent,
+  SidebarGroupLabel,
+  SidebarMenu,
+  SidebarMenuButton,
+  SidebarMenuItem,
+  SidebarProvider,
+} from "@/components/ui/sidebar"
+
+export function AppSidebar() {
+  return (
+    <SidebarProvider>
+      <Sidebar>
+        <SidebarContent>
+          <Collapsible defaultOpen className="group/collapsible">
+            <SidebarGroup>
+              <SidebarGroupLabel
+                asChild
+                className="hover:bg-sidebar-accent hover:text-sidebar-accent-foreground text-sm"
+              >
+                <CollapsibleTrigger>
+                  Help
+                  <ChevronDownIcon className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
+                </CollapsibleTrigger>
+              </SidebarGroupLabel>
+              <CollapsibleContent>
+                <SidebarGroupContent>
+                  <SidebarMenu>
+                    <SidebarMenuItem>
+                      <SidebarMenuButton>
+                        <LifeBuoyIcon />
+                        Support
+                      </SidebarMenuButton>
+                    </SidebarMenuItem>
+                    <SidebarMenuItem>
+                      <SidebarMenuButton>
+                        <SendIcon />
+                        Feedback
+                      </SidebarMenuButton>
+                    </SidebarMenuItem>
+                  </SidebarMenu>
+                </SidebarGroupContent>
+              </CollapsibleContent>
+            </SidebarGroup>
+          </Collapsible>
+        </SidebarContent>
+      </Sidebar>
+    </SidebarProvider>
+  )
+}
+
+```
+  <figcaption className="text-center text-sm text-gray-500">
+    A collapsible sidebar group.
+  </figcaption>
+</figure>
+
+```tsx showLineNumbers
+export function AppSidebar() {
+  return (
+    <Collapsible defaultOpen className="group/collapsible">
+      <SidebarGroup>
+        <SidebarGroupLabel asChild>
+          <CollapsibleTrigger>
+            Help
+            <ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
+          </CollapsibleTrigger>
+        </SidebarGroupLabel>
+        <CollapsibleContent>
+          <SidebarGroupContent />
+        </CollapsibleContent>
+      </SidebarGroup>
+    </Collapsible>
+  )
+}
+```
+
+<Callout>
+  **Note:** We wrap the `CollapsibleTrigger` in a `SidebarGroupLabel` to render
+  a button.
+</Callout>
+
+## SidebarGroupAction
+
+Use the `SidebarGroupAction` component to add an action button to the `SidebarGroup`.
+
+<figure className="flex flex-col gap-4">
+  ```tsx
+"use client"
+
+import { FrameIcon, MapIcon, PieChartIcon, PlusIcon } from "lucide-react"
+import { toast, Toaster } from "sonner"
+
+import {
+  Sidebar,
+  SidebarContent,
+  SidebarGroup,
+  SidebarGroupAction,
+  SidebarGroupContent,
+  SidebarGroupLabel,
+  SidebarMenu,
+  SidebarMenuButton,
+  SidebarMenuItem,
+  SidebarProvider,
+} from "@/components/ui/sidebar"
+
+export function AppSidebar() {
+  return (
+    <SidebarProvider>
+      <Toaster
+        position="bottom-left"
+        toastOptions={{
+          className: "ml-[160px]",
+        }}
+      />
+      <Sidebar>
+        <SidebarContent>
+          <SidebarGroup>
+            <SidebarGroupLabel>Projects</SidebarGroupLabel>
+            <SidebarGroupAction
+              title="Add Project"
+              onClick={() => toast("You clicked the group action!")}
+            >
+              <PlusIcon /> <span className="sr-only">Add Project</span>
+            </SidebarGroupAction>
+            <SidebarGroupContent>
+              <SidebarMenu>
+                <SidebarMenuItem>
+                  <SidebarMenuButton asChild>
+                    <a href="#">
+                      <FrameIcon />
+                      <span>Design Engineering</span>
+                    </a>
+                  </SidebarMenuButton>
+                </SidebarMenuItem>
+                <SidebarMenuItem>
+                  <SidebarMenuButton asChild>
+                    <a href="#">
+                      <PieChartIcon />
+                      <span>Sales & Marketing</span>
+                    </a>
+                  </SidebarMenuButton>
+                </SidebarMenuItem>
+                <SidebarMenuItem>
+                  <SidebarMenuButton asChild>
+                    <a href="#">
+                      <MapIcon />
+                      <span>Travel</span>
+                    </a>
+                  </SidebarMenuButton>
+                </SidebarMenuItem>
+              </SidebarMenu>
+            </SidebarGroupContent>
+          </SidebarGroup>
+        </SidebarContent>
+      </Sidebar>
+    </SidebarProvider>
+  )
+}
+
+```
+  <figcaption className="text-center text-sm text-gray-500">
+    A sidebar group with an action button.
+  </figcaption>
+</figure>
+
+```tsx showLineNumbers {5-7}
+export function AppSidebar() {
+  return (
+    <SidebarGroup>
+      <SidebarGroupLabel asChild>Projects</SidebarGroupLabel>
+      <SidebarGroupAction title="Add Project">
+        <Plus /> <span className="sr-only">Add Project</span>
+      </SidebarGroupAction>
+      <SidebarGroupContent />
+    </SidebarGroup>
+  )
+}
+```
+
+## SidebarMenu
+
+The `SidebarMenu` component is used for building a menu within a `SidebarGroup`.
+
+A `SidebarMenu` component is composed of `SidebarMenuItem`, `SidebarMenuButton`, `<SidebarMenuAction />` and `<SidebarMenuSub />` components.
+
+<Image
+  src="/images/sidebar-menu.png"
+  width="716"
+  height="420"
+  alt="Sidebar Menu"
+  className="mt-6 w-full overflow-hidden rounded-lg border dark:hidden"
+/>
+<Image
+  src="/images/sidebar-menu-dark.png"
+  width="716"
+  height="420"
+  alt="Sidebar Menu"
+  className="mt-6 hidden w-full overflow-hidden rounded-lg border dark:block"
+/>
+
+Here's an example of a `SidebarMenu` component rendering a list of projects.
+
+<figure className="mt-6 flex flex-col gap-4">
+  ```tsx
+"use client"
+
+import {
+  FrameIcon,
+  LifeBuoyIcon,
+  MapIcon,
+  PieChartIcon,
+  SendIcon,
+} from "lucide-react"
+
+import {
+  Sidebar,
+  SidebarContent,
+  SidebarGroup,
+  SidebarGroupContent,
+  SidebarGroupLabel,
+  SidebarMenu,
+  SidebarMenuButton,
+  SidebarMenuItem,
+  SidebarProvider,
+} from "@/components/ui/sidebar"
+
+const projects = [
+  {
+    name: "Design Engineering",
+    url: "#",
+    icon: FrameIcon,
+  },
+  {
+    name: "Sales & Marketing",
+    url: "#",
+    icon: PieChartIcon,
+  },
+  {
+    name: "Travel",
+    url: "#",
+    icon: MapIcon,
+  },
+  {
+    name: "Support",
+    url: "#",
+    icon: LifeBuoyIcon,
+  },
+  {
+    name: "Feedback",
+    url: "#",
+    icon: SendIcon,
+  },
+]
+
+export function AppSidebar() {
+  return (
+    <SidebarProvider>
+      <Sidebar>
+        <SidebarContent>
+          <SidebarGroup>
+            <SidebarGroupLabel>Projects</SidebarGroupLabel>
+            <SidebarGroupContent>
+              <SidebarMenu>
+                {projects.map((project) => (
+                  <SidebarMenuItem key={project.name}>
+                    <SidebarMenuButton asChild>
+                      <a href={project.url}>
+                        <project.icon />
+                        <span>{project.name}</span>
+                      </a>
+                    </SidebarMenuButton>
+                  </SidebarMenuItem>
+                ))}
+              </SidebarMenu>
+            </SidebarGroupContent>
+          </SidebarGroup>
+        </SidebarContent>
+      </Sidebar>
+    </SidebarProvider>
+  )
+}
+
+```
+  <figcaption className="text-center text-sm text-gray-500">
+    A sidebar menu with a list of projects.
+  </figcaption>
+</figure>
+
+```tsx showLineNumbers
+<Sidebar>
+  <SidebarContent>
+    <SidebarGroup>
+      <SidebarGroupLabel>Projects</SidebarGroupLabel>
+      <SidebarGroupContent>
+        <SidebarMenu>
+          {projects.map((project) => (
+            <SidebarMenuItem key={project.name}>
+              <SidebarMenuButton asChild>
+                <a href={project.url}>
+                  <project.icon />
+                  <span>{project.name}</span>
+                </a>
+              </SidebarMenuButton>
+            </SidebarMenuItem>
+          ))}
+        </SidebarMenu>
+      </SidebarGroupContent>
+    </SidebarGroup>
+  </SidebarContent>
+</Sidebar>
+```
+
+## SidebarMenuButton
+
+The `SidebarMenuButton` component is used to render a menu button within a `SidebarMenuItem`.
+
+### Link or Anchor
+
+By default, the `SidebarMenuButton` renders a button but you can use the `asChild` prop to render a different component such as a `Link` or an `a` tag.
+
+```tsx showLineNumbers
+<SidebarMenuButton asChild>
+  <a href="#">Home</a>
+</SidebarMenuButton>
+```
+
+### Icon and Label
+
+You can render an icon and a truncated label inside the button. Remember to wrap the label in a `<span>`.
+
+```tsx showLineNumbers
+<SidebarMenuButton asChild>
+  <a href="#">
+    <Home />
+    <span>Home</span>
+  </a>
+</SidebarMenuButton>
+```
+
+### isActive
+
+Use the `isActive` prop to mark a menu item as active.
+
+```tsx showLineNumbers
+<SidebarMenuButton asChild isActive>
+  <a href="#">Home</a>
+</SidebarMenuButton>
+```
+
+## SidebarMenuAction
+
+The `SidebarMenuAction` component is used to render a menu action within a `SidebarMenuItem`.
+
+This button works independently of the `SidebarMenuButton` i.e you can have the `<SidebarMenuButton />` as a clickable link and the `<SidebarMenuAction />` as a button.
+
+```tsx showLineNumbers
+<SidebarMenuItem>
+  <SidebarMenuButton asChild>
+    <a href="#">
+      <Home />
+      <span>Home</span>
+    </a>
+  </SidebarMenuButton>
+  <SidebarMenuAction>
+    <Plus /> <span className="sr-only">Add Project</span>
+  </SidebarMenuAction>
+</SidebarMenuItem>
+```
+
+### DropdownMenu
+
+Here's an example of a `SidebarMenuAction` component rendering a `DropdownMenu`.
+
+<figure className="mt-6 flex flex-col gap-4">
+  ```tsx
+"use client"
+
+import {
+  FrameIcon,
+  LifeBuoyIcon,
+  MapIcon,
+  MoreHorizontalIcon,
+  PieChartIcon,
+  SendIcon,
+} from "lucide-react"
+
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+import {
+  Sidebar,
+  SidebarContent,
+  SidebarGroup,
+  SidebarGroupContent,
+  SidebarGroupLabel,
+  SidebarMenu,
+  SidebarMenuAction,
+  SidebarMenuButton,
+  SidebarMenuItem,
+  SidebarProvider,
+} from "@/components/ui/sidebar"
+
+const projects = [
+  {
+    name: "Design Engineering",
+    url: "#",
+    icon: FrameIcon,
+  },
+  {
+    name: "Sales & Marketing",
+    url: "#",
+    icon: PieChartIcon,
+  },
+  {
+    name: "Travel",
+    url: "#",
+    icon: MapIcon,
+  },
+  {
+    name: "Support",
+    url: "#",
+    icon: LifeBuoyIcon,
+  },
+  {
+    name: "Feedback",
+    url: "#",
+    icon: SendIcon,
+  },
+]
+
+export function AppSidebar() {
+  return (
+    <SidebarProvider>
+      <Sidebar>
+        <SidebarContent>
+          <SidebarGroup>
+            <SidebarGroupLabel>Projects</SidebarGroupLabel>
+            <SidebarGroupContent>
+              <SidebarMenu>
+                {projects.map((project) => (
+                  <SidebarMenuItem key={project.name}>
+                    <SidebarMenuButton
+                      asChild
+                      className="group-has-[[data-state=open]]/menu-item:bg-sidebar-accent"
+                    >
+                      <a href={project.url}>
+                        <project.icon />
+                        <span>{project.name}</span>
+                      </a>
+                    </SidebarMenuButton>
+                    <DropdownMenu>
+                      <DropdownMenuTrigger asChild>
+                        <SidebarMenuAction>
+                          <MoreHorizontalIcon />
+                          <span className="sr-only">More</span>
+                        </SidebarMenuAction>
+                      </DropdownMenuTrigger>
+                      <DropdownMenuContent side="right" align="start">
+                        <DropdownMenuItem>
+                          <span>Edit Project</span>
+                        </DropdownMenuItem>
+                        <DropdownMenuItem>
+                          <span>Delete Project</span>
+                        </DropdownMenuItem>
+                      </DropdownMenuContent>
+                    </DropdownMenu>
+                  </SidebarMenuItem>
+                ))}
+              </SidebarMenu>
+            </SidebarGroupContent>
+          </SidebarGroup>
+        </SidebarContent>
+      </Sidebar>
+    </SidebarProvider>
+  )
+}
+
+```
+  <figcaption className="text-center text-sm text-gray-500">
+    A sidebar menu action with a dropdown menu.
+  </figcaption>
+</figure>
+
+```tsx showLineNumbers
+<SidebarMenuItem>
+  <SidebarMenuButton asChild>
+    <a href="#">
+      <Home />
+      <span>Home</span>
+    </a>
+  </SidebarMenuButton>
+  <DropdownMenu>
+    <DropdownMenuTrigger asChild>
+      <SidebarMenuAction>
+        <MoreHorizontal />
+      </SidebarMenuAction>
+    </DropdownMenuTrigger>
+    <DropdownMenuContent side="right" align="start">
+      <DropdownMenuItem>
+        <span>Edit Project</span>
+      </DropdownMenuItem>
+      <DropdownMenuItem>
+        <span>Delete Project</span>
+      </DropdownMenuItem>
+    </DropdownMenuContent>
+  </DropdownMenu>
+</SidebarMenuItem>
+```
+
+## SidebarMenuSub
+
+The `SidebarMenuSub` component is used to render a submenu within a `SidebarMenu`.
+
+Use `<SidebarMenuSubItem />` and `<SidebarMenuSubButton />` to render a submenu item.
+
+<figure className="mt-6 flex flex-col gap-4">
+  ```tsx
+"use client"
+
+import {
+  Sidebar,
+  SidebarContent,
+  SidebarGroup,
+  SidebarGroupContent,
+  SidebarMenu,
+  SidebarMenuButton,
+  SidebarMenuItem,
+  SidebarMenuSub,
+  SidebarMenuSubButton,
+  SidebarMenuSubItem,
+  SidebarProvider,
+} from "@/components/ui/sidebar"
+
+const items = [
+  {
+    title: "Getting Started",
+    url: "#",
+    items: [
+      {
+        title: "Installation",
+        url: "#",
+      },
+      {
+        title: "Project Structure",
+        url: "#",
+      },
+    ],
+  },
+  {
+    title: "Building Your Application",
+    url: "#",
+    items: [
+      {
+        title: "Routing",
+        url: "#",
+      },
+      {
+        title: "Data Fetching",
+        url: "#",
+        isActive: true,
+      },
+      {
+        title: "Rendering",
+        url: "#",
+      },
+      {
+        title: "Caching",
+        url: "#",
+      },
+      {
+        title: "Styling",
+        url: "#",
+      },
+      {
+        title: "Optimizing",
+        url: "#",
+      },
+      {
+        title: "Configuring",
+        url: "#",
+      },
+      {
+        title: "Testing",
+        url: "#",
+      },
+      {
+        title: "Authentication",
+        url: "#",
+      },
+      {
+        title: "Deploying",
+        url: "#",
+      },
+      {
+        title: "Upgrading",
+        url: "#",
+      },
+      {
+        title: "Examples",
+        url: "#",
+      },
+    ],
+  },
+  {
+    title: "API Reference",
+    url: "#",
+    items: [
+      {
+        title: "Components",
+        url: "#",
+      },
+      {
+        title: "File Conventions",
+        url: "#",
+      },
+      {
+        title: "Functions",
+        url: "#",
+      },
+      {
+        title: "next.config.js Options",
+        url: "#",
+      },
+      {
+        title: "CLI",
+        url: "#",
+      },
+      {
+        title: "Edge Runtime",
+        url: "#",
+      },
+    ],
+  },
+  {
+    title: "Architecture",
+    url: "#",
+    items: [
+      {
+        title: "Accessibility",
+        url: "#",
+      },
+      {
+        title: "Fast Refresh",
+        url: "#",
+      },
+      {
+        title: "Next.js Compiler",
+        url: "#",
+      },
+      {
+        title: "Supported Browsers",
+        url: "#",
+      },
+      {
+        title: "Turbopack",
+        url: "#",
+      },
+    ],
+  },
+]
+
+export function AppSidebar() {
+  return (
+    <SidebarProvider>
+      <Sidebar>
+        <SidebarContent>
+          <SidebarGroup>
+            <SidebarGroupContent>
+              <SidebarMenu>
+                {items.map((item, index) => (
+                  <SidebarMenuItem key={index}>
+                    <SidebarMenuButton asChild>
+                      <a href={item.url}>
+                        <span>{item.title}</span>
+                      </a>
+                    </SidebarMenuButton>
+                    <SidebarMenuSub>
+                      {item.items.map((subItem, subIndex) => (
+                        <SidebarMenuSubItem key={subIndex}>
+                          <SidebarMenuSubButton asChild>
+                            <a href={subItem.url}>
+                              <span>{subItem.title}</span>
+                            </a>
+                          </SidebarMenuSubButton>
+                        </SidebarMenuSubItem>
+                      ))}
+                    </SidebarMenuSub>
+                  </SidebarMenuItem>
+                ))}
+              </SidebarMenu>
+            </SidebarGroupContent>
+          </SidebarGroup>
+        </SidebarContent>
+      </Sidebar>
+    </SidebarProvider>
+  )
+}
+
+```
+  <figcaption className="text-center text-sm text-gray-500">
+    A sidebar menu with a submenu.
+  </figcaption>
+</figure>
+
+```tsx showLineNumbers
+<SidebarMenuItem>
+  <SidebarMenuButton />
+  <SidebarMenuSub>
+    <SidebarMenuSubItem>
+      <SidebarMenuSubButton />
+    </SidebarMenuSubItem>
+    <SidebarMenuSubItem>
+      <SidebarMenuSubButton />
+    </SidebarMenuSubItem>
+  </SidebarMenuSub>
+</SidebarMenuItem>
+```
+
+## Collapsible SidebarMenu
+
+To make a `SidebarMenu` component collapsible, wrap it and the `SidebarMenuSub` components in a `Collapsible`.
+
+<figure className="mt-6 flex flex-col gap-4">
+  ```tsx
+"use client"
+
+import { ChevronRightIcon } from "lucide-react"
+
+import {
+  Collapsible,
+  CollapsibleContent,
+  CollapsibleTrigger,
+} from "@/components/ui/collapsible"
+import {
+  Sidebar,
+  SidebarContent,
+  SidebarGroup,
+  SidebarGroupContent,
+  SidebarMenu,
+  SidebarMenuButton,
+  SidebarMenuItem,
+  SidebarMenuSub,
+  SidebarMenuSubButton,
+  SidebarMenuSubItem,
+  SidebarProvider,
+} from "@/components/ui/sidebar"
+
+const items = [
+  {
+    title: "Getting Started",
+    url: "#",
+    items: [
+      {
+        title: "Installation",
+        url: "#",
+      },
+      {
+        title: "Project Structure",
+        url: "#",
+      },
+    ],
+  },
+  {
+    title: "Building Your Application",
+    url: "#",
+    items: [
+      {
+        title: "Routing",
+        url: "#",
+      },
+      {
+        title: "Data Fetching",
+        url: "#",
+        isActive: true,
+      },
+      {
+        title: "Rendering",
+        url: "#",
+      },
+      {
+        title: "Caching",
+        url: "#",
+      },
+      {
+        title: "Styling",
+        url: "#",
+      },
+      {
+        title: "Optimizing",
+        url: "#",
+      },
+      {
+        title: "Configuring",
+        url: "#",
+      },
+      {
+        title: "Testing",
+        url: "#",
+      },
+      {
+        title: "Authentication",
+        url: "#",
+      },
+      {
+        title: "Deploying",
+        url: "#",
+      },
+      {
+        title: "Upgrading",
+        url: "#",
+      },
+      {
+        title: "Examples",
+        url: "#",
+      },
+    ],
+  },
+  {
+    title: "API Reference",
+    url: "#",
+    items: [
+      {
+        title: "Components",
+        url: "#",
+      },
+      {
+        title: "File Conventions",
+        url: "#",
+      },
+      {
+        title: "Functions",
+        url: "#",
+      },
+      {
+        title: "next.config.js Options",
+        url: "#",
+      },
+      {
+        title: "CLI",
+        url: "#",
+      },
+      {
+        title: "Edge Runtime",
+        url: "#",
+      },
+    ],
+  },
+  {
+    title: "Architecture",
+    url: "#",
+    items: [
+      {
+        title: "Accessibility",
+        url: "#",
+      },
+      {
+        title: "Fast Refresh",
+        url: "#",
+      },
+      {
+        title: "Next.js Compiler",
+        url: "#",
+      },
+      {
+        title: "Supported Browsers",
+        url: "#",
+      },
+      {
+        title: "Turbopack",
+        url: "#",
+      },
+    ],
+  },
+]
+
+export function AppSidebar() {
+  return (
+    <SidebarProvider>
+      <Sidebar>
+        <SidebarContent>
+          <SidebarGroup>
+            <SidebarGroupContent>
+              <SidebarMenu>
+                {items.map((item, index) => (
+                  <Collapsible
+                    key={index}
+                    className="group/collapsible"
+                    defaultOpen={index === 0}
+                  >
+                    <SidebarMenuItem>
+                      <CollapsibleTrigger asChild>
+                        <SidebarMenuButton>
+                          <span>{item.title}</span>
+                          <ChevronRightIcon className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-90" />
+                        </SidebarMenuButton>
+                      </CollapsibleTrigger>
+                      <CollapsibleContent>
+                        <SidebarMenuSub>
+                          {item.items.map((subItem, subIndex) => (
+                            <SidebarMenuSubItem key={subIndex}>
+                              <SidebarMenuSubButton asChild>
+                                <a href={subItem.url}>
+                                  <span>{subItem.title}</span>
+                                </a>
+                              </SidebarMenuSubButton>
+                            </SidebarMenuSubItem>
+                          ))}
+                        </SidebarMenuSub>
+                      </CollapsibleContent>
+                    </SidebarMenuItem>
+                  </Collapsible>
+                ))}
+              </SidebarMenu>
+            </SidebarGroupContent>
+          </SidebarGroup>
+        </SidebarContent>
+      </Sidebar>
+    </SidebarProvider>
+  )
+}
+
+```
+  <figcaption className="text-center text-sm text-gray-500">
+    A collapsible sidebar menu.
+  </figcaption>
+</figure>
+
+```tsx showLineNumbers
+<SidebarMenu>
+  <Collapsible defaultOpen className="group/collapsible">
+    <SidebarMenuItem>
+      <CollapsibleTrigger asChild>
+        <SidebarMenuButton />
+      </CollapsibleTrigger>
+      <CollapsibleContent>
+        <SidebarMenuSub>
+          <SidebarMenuSubItem />
+        </SidebarMenuSub>
+      </CollapsibleContent>
+    </SidebarMenuItem>
+  </Collapsible>
+</SidebarMenu>
+```
+
+## SidebarMenuBadge
+
+The `SidebarMenuBadge` component is used to render a badge within a `SidebarMenuItem`.
+
+<figure className="mt-6 flex flex-col gap-4">
+  ```tsx
+"use client"
+
+import {
+  FrameIcon,
+  LifeBuoyIcon,
+  MapIcon,
+  PieChartIcon,
+  SendIcon,
+} from "lucide-react"
+
+import {
+  Sidebar,
+  SidebarContent,
+  SidebarGroup,
+  SidebarGroupContent,
+  SidebarGroupLabel,
+  SidebarMenu,
+  SidebarMenuBadge,
+  SidebarMenuButton,
+  SidebarMenuItem,
+  SidebarProvider,
+} from "@/components/ui/sidebar"
+
+const projects = [
+  {
+    name: "Design Engineering",
+    url: "#",
+    icon: FrameIcon,
+    badge: "24",
+  },
+  {
+    name: "Sales & Marketing",
+    url: "#",
+    icon: PieChartIcon,
+    badge: "12",
+  },
+  {
+    name: "Travel",
+    url: "#",
+    icon: MapIcon,
+    badge: "3",
+  },
+  {
+    name: "Support",
+    url: "#",
+    icon: LifeBuoyIcon,
+    badge: "21",
+  },
+  {
+    name: "Feedback",
+    url: "#",
+    icon: SendIcon,
+    badge: "8",
+  },
+]
+
+export function AppSidebar() {
+  return (
+    <SidebarProvider>
+      <Sidebar>
+        <SidebarContent>
+          <SidebarGroup>
+            <SidebarGroupLabel>Projects</SidebarGroupLabel>
+            <SidebarGroupContent>
+              <SidebarMenu>
+                {projects.map((project) => (
+                  <SidebarMenuItem key={project.name}>
+                    <SidebarMenuButton
+                      asChild
+                      className="group-has-[[data-state=open]]/menu-item:bg-sidebar-accent"
+                    >
+                      <a href={project.url}>
+                        <project.icon />
+                        <span>{project.name}</span>
+                      </a>
+                    </SidebarMenuButton>
+                    <SidebarMenuBadge>{project.badge}</SidebarMenuBadge>
+                  </SidebarMenuItem>
+                ))}
+              </SidebarMenu>
+            </SidebarGroupContent>
+          </SidebarGroup>
+        </SidebarContent>
+      </Sidebar>
+    </SidebarProvider>
+  )
+}
+
+```
+  <figcaption className="text-center text-sm text-gray-500">
+    A sidebar menu with a badge.
+  </figcaption>
+</figure>
+
+```tsx showLineNumbers
+<SidebarMenuItem>
+  <SidebarMenuButton />
+  <SidebarMenuBadge>24</SidebarMenuBadge>
+</SidebarMenuItem>
+```
+
+## SidebarMenuSkeleton
+
+The `SidebarMenuSkeleton` component is used to render a skeleton for a `SidebarMenu`. You can use this to show a loading state when using React Server Components, SWR or react-query.
+
+```tsx showLineNumbers
+function NavProjectsSkeleton() {
+  return (
+    <SidebarMenu>
+      {Array.from({ length: 5 }).map((_, index) => (
+        <SidebarMenuItem key={index}>
+          <SidebarMenuSkeleton />
+        </SidebarMenuItem>
+      ))}
+    </SidebarMenu>
+  )
+}
+```
+
+## SidebarSeparator
+
+The `SidebarSeparator` component is used to render a separator within a `Sidebar`.
+
+```tsx showLineNumbers
+<Sidebar>
+  <SidebarHeader />
+  <SidebarSeparator />
+  <SidebarContent>
+    <SidebarGroup />
+    <SidebarSeparator />
+    <SidebarGroup />
+  </SidebarContent>
+</Sidebar>
+```
+
+## SidebarTrigger
+
+Use the `SidebarTrigger` component to render a button that toggles the sidebar.
+
+The `SidebarTrigger` component must be used within a `SidebarProvider`.
+
+```tsx showLineNumbers
+<SidebarProvider>
+  <Sidebar />
+  <main>
+    <SidebarTrigger />
+  </main>
+</SidebarProvider>
+```
+
+### Custom Trigger
+
+To create a custom trigger, you can use the `useSidebar` hook.
+
+```tsx showLineNumbers
+import { useSidebar } from "@/components/ui/sidebar"
+
+export function CustomTrigger() {
+  const { toggleSidebar } = useSidebar()
+
+  return <button onClick={toggleSidebar}>Toggle Sidebar</button>
+}
+```
+
+## SidebarRail
+
+The `SidebarRail` component is used to render a rail within a `Sidebar`. This rail can be used to toggle the sidebar.
+
+```tsx showLineNumbers
+<Sidebar>
+  <SidebarHeader />
+  <SidebarContent>
+    <SidebarGroup />
+  </SidebarContent>
+  <SidebarFooter />
+  <SidebarRail />
+</Sidebar>
+```
+
+## Data Fetching
+
+### React Server Components
+
+Here's an example of a `SidebarMenu` component rendering a list of projects using React Server Components.
+
+<figure className="mt-6 flex flex-col gap-4">
+  ```tsx
+import * as React from "react"
+import {
+  FrameIcon,
+  LifeBuoyIcon,
+  MapIcon,
+  PieChartIcon,
+  SendIcon,
+} from "lucide-react"
+
+import {
+  Sidebar,
+  SidebarContent,
+  SidebarGroup,
+  SidebarGroupContent,
+  SidebarGroupLabel,
+  SidebarMenu,
+  SidebarMenuButton,
+  SidebarMenuItem,
+  SidebarMenuSkeleton,
+  SidebarProvider,
+} from "@/components/ui/sidebar"
+
+const projects = [
+  {
+    name: "Design Engineering",
+    url: "#",
+    icon: FrameIcon,
+    badge: "24",
+  },
+  {
+    name: "Sales & Marketing",
+    url: "#",
+    icon: PieChartIcon,
+    badge: "12",
+  },
+  {
+    name: "Travel",
+    url: "#",
+    icon: MapIcon,
+    badge: "3",
+  },
+  {
+    name: "Support",
+    url: "#",
+    icon: LifeBuoyIcon,
+    badge: "21",
+  },
+  {
+    name: "Feedback",
+    url: "#",
+    icon: SendIcon,
+    badge: "8",
+  },
+]
+
+// Dummy fetch function
+async function fetchProjects() {
+  await new Promise((resolve) => setTimeout(resolve, 3000))
+  return projects
+}
+
+export function AppSidebar() {
+  return (
+    <SidebarProvider>
+      <Sidebar>
+        <SidebarContent>
+          <SidebarGroup>
+            <SidebarGroupLabel>Projects</SidebarGroupLabel>
+            <SidebarGroupContent>
+              <React.Suspense fallback={<NavProjectsSkeleton />}>
+                <NavProjects />
+              </React.Suspense>
+            </SidebarGroupContent>
+          </SidebarGroup>
+        </SidebarContent>
+      </Sidebar>
+    </SidebarProvider>
+  )
+}
+
+function NavProjectsSkeleton() {
+  return (
+    <SidebarMenu>
+      {Array.from({ length: 5 }).map((_, index) => (
+        <SidebarMenuItem key={index}>
+          <SidebarMenuSkeleton showIcon />
+        </SidebarMenuItem>
+      ))}
+    </SidebarMenu>
+  )
+}
+
+async function NavProjects() {
+  const projects = await fetchProjects()
+
+  return (
+    <SidebarMenu>
+      {projects.map((project) => (
+        <SidebarMenuItem key={project.name}>
+          <SidebarMenuButton asChild>
+            <a href={project.url}>
+              <project.icon />
+              <span>{project.name}</span>
+            </a>
+          </SidebarMenuButton>
+        </SidebarMenuItem>
+      ))}
+    </SidebarMenu>
+  )
+}
+
+```
+  <figcaption className="text-center text-sm text-gray-500">
+    A sidebar menu using React Server Components.
+  </figcaption>
+</figure>
+
+```tsx showLineNumbers {6} title="Skeleton to show loading state."
+function NavProjectsSkeleton() {
+  return (
+    <SidebarMenu>
+      {Array.from({ length: 5 }).map((_, index) => (
+        <SidebarMenuItem key={index}>
+          <SidebarMenuSkeleton showIcon />
+        </SidebarMenuItem>
+      ))}
+    </SidebarMenu>
+  )
+}
+```
+
+```tsx showLineNumbers {2} title="Server component fetching data."
+async function NavProjects() {
+  const projects = await fetchProjects()
+
+  return (
+    <SidebarMenu>
+      {projects.map((project) => (
+        <SidebarMenuItem key={project.name}>
+          <SidebarMenuButton asChild>
+            <a href={project.url}>
+              <project.icon />
+              <span>{project.name}</span>
+            </a>
+          </SidebarMenuButton>
+        </SidebarMenuItem>
+      ))}
+    </SidebarMenu>
+  )
+}
+```
+
+```tsx showLineNumbers {8-10} title="Usage with React Suspense."
+function AppSidebar() {
+  return (
+    <Sidebar>
+      <SidebarContent>
+        <SidebarGroup>
+          <SidebarGroupLabel>Projects</SidebarGroupLabel>
+          <SidebarGroupContent>
+            <React.Suspense fallback={<NavProjectsSkeleton />}>
+              <NavProjects />
+            </React.Suspense>
+          </SidebarGroupContent>
+        </SidebarGroup>
+      </SidebarContent>
+    </Sidebar>
+  )
+}
+```
+
+### SWR and React Query
+
+You can use the same approach with [SWR](https://swr.vercel.app/) or [react-query](https://tanstack.com/query/latest/docs/framework/react/overview).
+
+```tsx showLineNumbers title="SWR"
+function NavProjects() {
+  const { data, isLoading } = useSWR("/api/projects", fetcher)
+
+  if (isLoading) {
+    return (
+      <SidebarMenu>
+        {Array.from({ length: 5 }).map((_, index) => (
+          <SidebarMenuItem key={index}>
+            <SidebarMenuSkeleton showIcon />
+          </SidebarMenuItem>
+        ))}
+      </SidebarMenu>
+    )
+  }
+
+  if (!data) {
+    return ...
+  }
+
+  return (
+    <SidebarMenu>
+      {data.map((project) => (
+        <SidebarMenuItem key={project.name}>
+          <SidebarMenuButton asChild>
+            <a href={project.url}>
+              <project.icon />
+              <span>{project.name}</span>
+            </a>
+          </SidebarMenuButton>
+        </SidebarMenuItem>
+      ))}
+    </SidebarMenu>
+  )
+}
+```
+
+```tsx showLineNumbers title="React Query"
+function NavProjects() {
+  const { data, isLoading } = useQuery()
+
+  if (isLoading) {
+    return (
+      <SidebarMenu>
+        {Array.from({ length: 5 }).map((_, index) => (
+          <SidebarMenuItem key={index}>
+            <SidebarMenuSkeleton showIcon />
+          </SidebarMenuItem>
+        ))}
+      </SidebarMenu>
+    )
+  }
+
+  if (!data) {
+    return ...
+  }
+
+  return (
+    <SidebarMenu>
+      {data.map((project) => (
+        <SidebarMenuItem key={project.name}>
+          <SidebarMenuButton asChild>
+            <a href={project.url}>
+              <project.icon />
+              <span>{project.name}</span>
+            </a>
+          </SidebarMenuButton>
+        </SidebarMenuItem>
+      ))}
+    </SidebarMenu>
+  )
+}
+```
+
+## Controlled Sidebar
+
+Use the `open` and `onOpenChange` props to control the sidebar.
+
+<figure className="mt-6 flex flex-col gap-4">
+  ```tsx
+"use client"
+
+import * as React from "react"
+import {
+  FrameIcon,
+  LifeBuoyIcon,
+  MapIcon,
+  PanelLeftCloseIcon,
+  PanelLeftOpenIcon,
+  PieChartIcon,
+  SendIcon,
+} from "lucide-react"
+
+import { Button } from "@/components/ui/button"
+import {
+  Sidebar,
+  SidebarContent,
+  SidebarGroup,
+  SidebarGroupContent,
+  SidebarGroupLabel,
+  SidebarInset,
+  SidebarMenu,
+  SidebarMenuButton,
+  SidebarMenuItem,
+  SidebarProvider,
+} from "@/components/ui/sidebar"
+
+const projects = [
+  {
+    name: "Design Engineering",
+    url: "#",
+    icon: FrameIcon,
+  },
+  {
+    name: "Sales & Marketing",
+    url: "#",
+    icon: PieChartIcon,
+  },
+  {
+    name: "Travel",
+    url: "#",
+    icon: MapIcon,
+  },
+  {
+    name: "Support",
+    url: "#",
+    icon: LifeBuoyIcon,
+  },
+  {
+    name: "Feedback",
+    url: "#",
+    icon: SendIcon,
+  },
+]
+
+export function AppSidebar() {
+  const [open, setOpen] = React.useState(true)
+
+  return (
+    <SidebarProvider open={open} onOpenChange={setOpen}>
+      <Sidebar>
+        <SidebarContent>
+          <SidebarGroup>
+            <SidebarGroupLabel>Projects</SidebarGroupLabel>
+            <SidebarGroupContent>
+              <SidebarMenu>
+                {projects.map((project) => (
+                  <SidebarMenuItem key={project.name}>
+                    <SidebarMenuButton asChild>
+                      <a href={project.url}>
+                        <project.icon />
+                        <span>{project.name}</span>
+                      </a>
+                    </SidebarMenuButton>
+                  </SidebarMenuItem>
+                ))}
+              </SidebarMenu>
+            </SidebarGroupContent>
+          </SidebarGroup>
+        </SidebarContent>
+      </Sidebar>
+      <SidebarInset>
+        <header className="flex h-12 items-center justify-between px-4">
+          <Button
+            onClick={() => setOpen((open) => !open)}
+            size="sm"
+            variant="ghost"
+          >
+            {open ? <PanelLeftCloseIcon /> : <PanelLeftOpenIcon />}
+            <span>{open ? "Close" : "Open"} Sidebar</span>
+          </Button>
+        </header>
+      </SidebarInset>
+    </SidebarProvider>
+  )
+}
+
+```
+  <figcaption className="text-center text-sm text-gray-500">
+    A controlled sidebar.
+  </figcaption>
+</figure>
+
+```tsx showLineNumbers
+export function AppSidebar() {
+  const [open, setOpen] = React.useState(false)
+
+  return (
+    <SidebarProvider open={open} onOpenChange={setOpen}>
+      <Sidebar />
+    </SidebarProvider>
+  )
+}
+```
+
+## Theming
+
+We use the following CSS variables to theme the sidebar.
+
+```css
+@layer base {
+  :root {
+    --sidebar-background: 0 0% 98%;
+    --sidebar-foreground: 240 5.3% 26.1%;
+    --sidebar-primary: 240 5.9% 10%;
+    --sidebar-primary-foreground: 0 0% 98%;
+    --sidebar-accent: 240 4.8% 95.9%;
+    --sidebar-accent-foreground: 240 5.9% 10%;
+    --sidebar-border: 220 13% 91%;
+    --sidebar-ring: 217.2 91.2% 59.8%;
+  }
+
+  .dark {
+    --sidebar-background: 240 5.9% 10%;
+    --sidebar-foreground: 240 4.8% 95.9%;
+    --sidebar-primary: 0 0% 98%;
+    --sidebar-primary-foreground: 240 5.9% 10%;
+    --sidebar-accent: 240 3.7% 15.9%;
+    --sidebar-accent-foreground: 240 4.8% 95.9%;
+    --sidebar-border: 240 3.7% 15.9%;
+    --sidebar-ring: 217.2 91.2% 59.8%;
+  }
+}
+```
+
+**We intentionally use different variables for the sidebar and the rest of the application** to make it easy to have a sidebar that is styled differently from the rest of the application. Think a sidebar with a darker shade from the main application.
+
+## Styling
+
+Here are some tips for styling the sidebar based on different states.
+
+- **Styling an element based on the sidebar collapsible state.** The following will hide the `SidebarGroup` when the sidebar is in `icon` mode.
+
+```tsx
+<Sidebar collapsible="icon">
+  <SidebarContent>
+    <SidebarGroup className="group-data-[collapsible=icon]:hidden" />
+  </SidebarContent>
+</Sidebar>
+```
+
+- **Styling a menu action based on the menu button active state.** The following will force the menu action to be visible when the menu button is active.
+
+```tsx
+<SidebarMenuItem>
+  <SidebarMenuButton />
+  <SidebarMenuAction className="peer-data-[active=true]/menu-button:opacity-100" />
+</SidebarMenuItem>
+```
+
+You can find more tips on using states for styling in this [Twitter thread](https://x.com/shadcn/status/1842329158879420864).
+
+## Changelog
+
+### 2024-10-30 Cookie handling in setOpen
+
+- [#5593](https://github.com/shadcn-ui/ui/pull/5593) - Improved setOpen callback logic in `<SidebarProvider>`.
+
+Update the `setOpen` callback in `<SidebarProvider>` as follows:
+
+```tsx showLineNumbers
+const setOpen = React.useCallback(
+  (value: boolean | ((value: boolean) => boolean)) => {
+    const openState = typeof value === "function" ? value(open) : value
+    if (setOpenProp) {
+      setOpenProp(openState)
+    } else {
+      _setOpen(openState)
+    }
+
+    // This sets the cookie to keep the sidebar state.
+    document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
+  },
+  [setOpenProp, open]
+)
+```
+
+### 2024-10-21 Fixed `text-sidebar-foreground`
+
+- [#5491](https://github.com/shadcn-ui/ui/pull/5491) - Moved `text-sidebar-foreground` from `<SidebarProvider>` to `<Sidebar>` component.
+
+### 2024-10-20 Typo in `useSidebar` hook.
+
+Fixed typo in `useSidebar` hook.
+
+```diff showLineNumbers title="sidebar.tsx"
+-  throw new Error("useSidebar must be used within a Sidebar.")
++  throw new Error("useSidebar must be used within a SidebarProvider.")
+```

+ 59 - 0
package-lock.json

@@ -13,8 +13,10 @@
         "@radix-ui/react-dropdown-menu": "^2.1.16",
         "@radix-ui/react-label": "^2.1.7",
         "@radix-ui/react-select": "^2.2.6",
+        "@radix-ui/react-separator": "^1.1.7",
         "@radix-ui/react-slot": "^1.2.3",
         "@radix-ui/react-switch": "^1.2.6",
+        "@radix-ui/react-tooltip": "^1.2.8",
         "class-variance-authority": "^0.7.1",
         "clsx": "^2.1.1",
         "lucide-react": "^0.548.0",
@@ -1792,6 +1794,29 @@
         }
       }
     },
+    "node_modules/@radix-ui/react-separator": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
+      "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-primitive": "2.1.3"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@radix-ui/react-slot": {
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
@@ -1839,6 +1864,40 @@
         }
       }
     },
+    "node_modules/@radix-ui/react-tooltip": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
+      "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/primitive": "1.1.3",
+        "@radix-ui/react-compose-refs": "1.1.2",
+        "@radix-ui/react-context": "1.1.2",
+        "@radix-ui/react-dismissable-layer": "1.1.11",
+        "@radix-ui/react-id": "1.1.1",
+        "@radix-ui/react-popper": "1.2.8",
+        "@radix-ui/react-portal": "1.1.9",
+        "@radix-ui/react-presence": "1.1.5",
+        "@radix-ui/react-primitive": "2.1.3",
+        "@radix-ui/react-slot": "1.2.3",
+        "@radix-ui/react-use-controllable-state": "1.2.2",
+        "@radix-ui/react-visually-hidden": "1.2.3"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@radix-ui/react-use-callback-ref": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",

+ 2 - 0
package.json

@@ -14,8 +14,10 @@
     "@radix-ui/react-dropdown-menu": "^2.1.16",
     "@radix-ui/react-label": "^2.1.7",
     "@radix-ui/react-select": "^2.2.6",
+    "@radix-ui/react-separator": "^1.1.7",
     "@radix-ui/react-slot": "^1.2.3",
     "@radix-ui/react-switch": "^1.2.6",
+    "@radix-ui/react-tooltip": "^1.2.8",
     "class-variance-authority": "^0.7.1",
     "clsx": "^2.1.1",
     "lucide-react": "^0.548.0",

+ 8 - 10
src/app/factura/page.tsx

@@ -499,15 +499,14 @@ export default function FacturaPage() {
   }
 
   return (
-    <div className="min-h-screen bg-background p-8">
-      <div className="mx-auto max-w-6xl space-y-6">
-        {/* Header */}
-        <div className="text-center">
-          <h1 className="text-4xl font-bold tracking-tight">Factura Electrónica SRI Ecuador</h1>
-          <p className="mt-2 text-muted-foreground">
-            Generador de XML para facturación electrónica符合SRI
-          </p>
-        </div>
+    <div className="space-y-6">
+      {/* Header */}
+      <div className="text-center">
+        <h1 className="text-4xl font-bold tracking-tight">Factura Electrónica SRI Ecuador</h1>
+        <p className="mt-2 text-muted-foreground">
+          Generador de XML para facturación electrónica SRI
+        </p>
+      </div>
 
         {/* Info Tributaria */}
         <Card>
@@ -836,6 +835,5 @@ export default function FacturaPage() {
           </Card>
         )}
       </div>
-    </div>
   )
 }

+ 17 - 1
src/app/layout.tsx

@@ -3,6 +3,9 @@ import { Geist, Geist_Mono } from "next/font/google";
 import "./globals.css";
 import { ThemeProvider } from "@/components/theme-provider";
 import { Toaster } from "@/components/ui/sonner";
+import { SidebarProvider, SidebarInset, SidebarTrigger } from "@/components/ui/sidebar";
+import { AppSidebar } from "@/components/app-sidebar";
+import { Separator } from "@/components/ui/separator";
 
 const geistSans = Geist({
   variable: "--font-geist-sans",
@@ -35,7 +38,20 @@ export default function RootLayout({
           enableSystem
           disableTransitionOnChange
         >
-          {children}
+          <SidebarProvider>
+            <AppSidebar />
+            <SidebarInset>
+              <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
+                <div className="flex items-center gap-2 px-4">
+                  <SidebarTrigger className="-ml-1" />
+                  <Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
+                </div>
+              </header>
+              <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
+                {children}
+              </div>
+            </SidebarInset>
+          </SidebarProvider>
           <Toaster />
         </ThemeProvider>
       </body>

+ 211 - 216
src/app/page.tsx

@@ -33,252 +33,247 @@ export default function Home() {
   };
 
   return (
-    <div className="min-h-screen bg-background p-8">
-      <div className="mx-auto max-w-6xl space-y-8">
-        {/* Header */}
-        <div className="text-center">
-          <div className="flex justify-end mb-4">
-            <ModeToggle />
+    <div className="space-y-8">
+      {/* Header */}
+      <div className="text-center">
+        <h1 className="text-4xl font-bold tracking-tight">Sumire - shadcn/ui Demo</h1>
+        <p className="mt-2 text-muted-foreground">
+          Demostración completa de componentes shadcn/ui en Next.js
+        </p>
+        <div className="mt-4 flex justify-center gap-2">
+          <Badge variant="default">Next.js</Badge>
+          <Badge variant="secondary">shadcn/ui</Badge>
+          <Badge variant="outline">Tailwind CSS</Badge>
+        </div>
+      </div>
+
+      {/* Buttons Section */}
+      <Card>
+        <CardHeader>
+          <CardTitle>Botones</CardTitle>
+          <CardDescription>Diferentes variantes y estados de botones</CardDescription>
+        </CardHeader>
+        <CardContent className="space-y-4">
+          <div className="flex flex-wrap gap-2">
+            <Button>Botón Primario</Button>
+            <Button variant="secondary">Secundario</Button>
+            <Button variant="destructive">Destructivo</Button>
+            <Button variant="outline">Outline</Button>
+            <Button variant="ghost">Ghost</Button>
+            <Button variant="link">Link</Button>
           </div>
-          <h1 className="text-4xl font-bold tracking-tight">Sumire - shadcn/ui Demo</h1>
-          <p className="mt-2 text-muted-foreground">
-            Demostración completa de componentes shadcn/ui en Next.js
-          </p>
-          <div className="mt-4 flex justify-center gap-2">
-            <Badge variant="default">Next.js</Badge>
-            <Badge variant="secondary">shadcn/ui</Badge>
-            <Badge variant="outline">Tailwind CSS</Badge>
+          <div className="flex flex-wrap gap-2">
+            <Button size="sm">Pequeño</Button>
+            <Button size="default">Por Defecto</Button>
+            <Button size="lg">Grande</Button>
           </div>
-        </div>
+          <div className="flex flex-wrap gap-2">
+            <Button disabled>Deshabilitado</Button>
+            <Button variant="secondary">Cargando...</Button>
+          </div>
+        </CardContent>
+      </Card>
 
-        {/* Buttons Section */}
-        <Card>
-          <CardHeader>
-            <CardTitle>Botones</CardTitle>
-            <CardDescription>Diferentes variantes y estados de botones</CardDescription>
-          </CardHeader>
-          <CardContent className="space-y-4">
-            <div className="flex flex-wrap gap-2">
-              <Button>Botón Primario</Button>
-              <Button variant="secondary">Secundario</Button>
-              <Button variant="destructive">Destructivo</Button>
-              <Button variant="outline">Outline</Button>
-              <Button variant="ghost">Ghost</Button>
-              <Button variant="link">Link</Button>
+      {/* Form Section */}
+      <Card>
+        <CardHeader>
+          <CardTitle>Formulario</CardTitle>
+          <CardDescription>Ejemplo de formulario con diferentes campos</CardDescription>
+        </CardHeader>
+        <CardContent>
+          <form onSubmit={handleSubmit} className="space-y-4">
+            <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+              <div className="space-y-2">
+                <Label htmlFor="name">Nombre</Label>
+                <Input
+                  id="name"
+                  value={name}
+                  onChange={(e) => setName(e.target.value)}
+                  placeholder="Tu nombre"
+                />
+              </div>
+              <div className="space-y-2">
+                <Label htmlFor="email">Email</Label>
+                <Input
+                  id="email"
+                  type="email"
+                  value={email}
+                  onChange={(e) => setEmail(e.target.value)}
+                  placeholder="tu@email.com"
+                />
+              </div>
             </div>
-            <div className="flex flex-wrap gap-2">
-              <Button size="sm">Pequeño</Button>
-              <Button size="default">Por Defecto</Button>
-              <Button size="lg">Grande</Button>
+            
+            <div className="space-y-2">
+              <Label htmlFor="select">Opción</Label>
+              <Select value={selectedOption} onValueChange={setSelectedOption}>
+                <SelectTrigger>
+                  <SelectValue placeholder="Selecciona una opción" />
+                </SelectTrigger>
+                <SelectContent>
+                  <SelectItem value="option1">Opción 1</SelectItem>
+                  <SelectItem value="option2">Opción 2</SelectItem>
+                  <SelectItem value="option3">Opción 3</SelectItem>
+                </SelectContent>
+              </Select>
             </div>
-            <div className="flex flex-wrap gap-2">
-              <Button disabled>Deshabilitado</Button>
-              <Button variant="secondary">Cargando...</Button>
+
+            <div className="space-y-2">
+              <Label htmlFor="message">Mensaje</Label>
+              <Textarea
+                id="message"
+                value={message}
+                onChange={(e) => setMessage(e.target.value)}
+                placeholder="Escribe tu mensaje aquí..."
+                rows={3}
+              />
             </div>
-          </CardContent>
-        </Card>
 
-        {/* Form Section */}
-        <Card>
-          <CardHeader>
-            <CardTitle>Formulario</CardTitle>
-            <CardDescription>Ejemplo de formulario con diferentes campos</CardDescription>
-          </CardHeader>
-          <CardContent>
-            <form onSubmit={handleSubmit} className="space-y-4">
-              <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
-                <div className="space-y-2">
-                  <Label htmlFor="name">Nombre</Label>
-                  <Input
-                    id="name"
-                    value={name}
-                    onChange={(e) => setName(e.target.value)}
-                    placeholder="Tu nombre"
-                  />
-                </div>
-                <div className="space-y-2">
-                  <Label htmlFor="email">Email</Label>
-                  <Input
-                    id="email"
-                    type="email"
-                    value={email}
-                    onChange={(e) => setEmail(e.target.value)}
-                    placeholder="tu@email.com"
-                  />
-                </div>
-              </div>
-              
-              <div className="space-y-2">
-                <Label htmlFor="select">Opción</Label>
-                <Select value={selectedOption} onValueChange={setSelectedOption}>
-                  <SelectTrigger>
-                    <SelectValue placeholder="Selecciona una opción" />
-                  </SelectTrigger>
-                  <SelectContent>
-                    <SelectItem value="option1">Opción 1</SelectItem>
-                    <SelectItem value="option2">Opción 2</SelectItem>
-                    <SelectItem value="option3">Opción 3</SelectItem>
-                  </SelectContent>
-                </Select>
+            <div className="flex items-center space-x-4">
+              <div className="flex items-center space-x-2">
+                <Checkbox
+                  id="terms"
+                  checked={isChecked}
+                  onCheckedChange={(checked) => setIsChecked(checked as boolean)}
+                />
+                <Label htmlFor="terms">Acepto los términos y condiciones</Label>
               </div>
-
-              <div className="space-y-2">
-                <Label htmlFor="message">Mensaje</Label>
-                <Textarea
-                  id="message"
-                  value={message}
-                  onChange={(e) => setMessage(e.target.value)}
-                  placeholder="Escribe tu mensaje aquí..."
-                  rows={3}
+              <div className="flex items-center space-x-2">
+                <Switch
+                  id="notifications"
+                  checked={isSwitchOn}
+                  onCheckedChange={setIsSwitchOn}
                 />
+                <Label htmlFor="notifications">Notificaciones</Label>
               </div>
+            </div>
 
-              <div className="flex items-center space-x-4">
-                <div className="flex items-center space-x-2">
-                  <Checkbox
-                    id="terms"
-                    checked={isChecked}
-                    onCheckedChange={(checked) => setIsChecked(checked as boolean)}
-                  />
-                  <Label htmlFor="terms">Acepto los términos y condiciones</Label>
-                </div>
-                <div className="flex items-center space-x-2">
-                  <Switch
-                    id="notifications"
-                    checked={isSwitchOn}
-                    onCheckedChange={setIsSwitchOn}
-                  />
-                  <Label htmlFor="notifications">Notificaciones</Label>
-                </div>
-              </div>
+            <Button type="submit" className="w-full">
+              Enviar Formulario
+            </Button>
+          </form>
+        </CardContent>
+      </Card>
 
-              <Button type="submit" className="w-full">
-                Enviar Formulario
+      {/* Dialog Section */}
+      <Card>
+        <CardHeader>
+          <CardTitle>Dialog</CardTitle>
+          <CardDescription>Ejemplo de diálogo modal</CardDescription>
+        </CardHeader>
+        <CardContent>
+          <Dialog>
+            <DialogTrigger asChild>
+              <Button variant="outline" onClick={handleDialogOpen}>
+                Abrir Diálogo
               </Button>
-            </form>
-          </CardContent>
-        </Card>
+            </DialogTrigger>
+            <DialogContent>
+              <DialogHeader>
+                <DialogTitle>Diálogo de Ejemplo</DialogTitle>
+                <DialogDescription>
+                  Este es un ejemplo de diálogo modal usando shadcn/ui. 
+                  Puedes colocar cualquier contenido aquí.
+                </DialogDescription>
+              </DialogHeader>
+              <div className="py-4">
+                <p className="text-sm text-muted-foreground">
+                  Los diálogos son perfectos para confirmaciones, formularios 
+                  o mostrar información importante sin navegar away de la página actual.
+                </p>
+              </div>
+              <div className="flex justify-end gap-2">
+                <Button variant="outline">Cancelar</Button>
+                <Button>Confirmar</Button>
+              </div>
+            </DialogContent>
+          </Dialog>
+        </CardContent>
+      </Card>
 
-        {/* Dialog Section */}
+      {/* Status Cards */}
+      <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
         <Card>
-          <CardHeader>
-            <CardTitle>Dialog</CardTitle>
-            <CardDescription>Ejemplo de diálogo modal</CardDescription>
+          <CardHeader className="pb-3">
+            <CardTitle className="text-sm font-medium">Total Usuarios</CardTitle>
           </CardHeader>
           <CardContent>
-            <Dialog>
-              <DialogTrigger asChild>
-                <Button variant="outline" onClick={handleDialogOpen}>
-                  Abrir Diálogo
-                </Button>
-              </DialogTrigger>
-              <DialogContent>
-                <DialogHeader>
-                  <DialogTitle>Diálogo de Ejemplo</DialogTitle>
-                  <DialogDescription>
-                    Este es un ejemplo de diálogo modal usando shadcn/ui. 
-                    Puedes colocar cualquier contenido aquí.
-                  </DialogDescription>
-                </DialogHeader>
-                <div className="py-4">
-                  <p className="text-sm text-muted-foreground">
-                    Los diálogos son perfectos para confirmaciones, formularios 
-                    o mostrar información importante sin navegar away de la página actual.
-                  </p>
-                </div>
-                <div className="flex justify-end gap-2">
-                  <Button variant="outline">Cancelar</Button>
-                  <Button>Confirmar</Button>
-                </div>
-              </DialogContent>
-            </Dialog>
+            <div className="text-2xl font-bold">1,234</div>
+            <p className="text-xs text-muted-foreground">
+              <span className="text-green-600">+12%</span> desde el mes pasado
+            </p>
           </CardContent>
         </Card>
-
-        {/* Status Cards */}
-        <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
-          <Card>
-            <CardHeader className="pb-3">
-              <CardTitle className="text-sm font-medium">Total Usuarios</CardTitle>
-            </CardHeader>
-            <CardContent>
-              <div className="text-2xl font-bold">1,234</div>
-              <p className="text-xs text-muted-foreground">
-                <span className="text-green-600">+12%</span> desde el mes pasado
-              </p>
-            </CardContent>
-          </Card>
-          
-          <Card>
-            <CardHeader className="pb-3">
-              <CardTitle className="text-sm font-medium">Ingresos</CardTitle>
-            </CardHeader>
-            <CardContent>
-              <div className="text-2xl font-bold">$12,345</div>
-              <p className="text-xs text-muted-foreground">
-                <span className="text-green-600">+8%</span> desde el mes pasado
-              </p>
-            </CardContent>
-          </Card>
-          
-          <Card>
-            <CardHeader className="pb-3">
-              <CardTitle className="text-sm font-medium">Tasa de Conversión</CardTitle>
-            </CardHeader>
-            <CardContent>
-              <div className="text-2xl font-bold">3.2%</div>
-              <p className="text-xs text-muted-foreground">
-                <span className="text-red-600">-2%</span> desde el mes pasado
-              </p>
-            </CardContent>
-          </Card>
-        </div>
-
-        {/* Theme Toggle Demo */}
+        
         <Card>
-          <CardHeader>
-            <CardTitle>Selector de Tema</CardTitle>
-            <CardDescription>Cambia entre modo claro, oscuro y sistema</CardDescription>
+          <CardHeader className="pb-3">
+            <CardTitle className="text-sm font-medium">Ingresos</CardTitle>
           </CardHeader>
-          <CardContent className="flex items-center justify-between">
-            <div>
-              <p className="text-sm font-medium">Modo Actual</p>
-              <p className="text-sm text-muted-foreground">
-                Usa el botón para cambiar entre Light, Dark y System
-              </p>
-            </div>
-            <ModeToggle />
+          <CardContent>
+            <div className="text-2xl font-bold">$12,345</div>
+            <p className="text-xs text-muted-foreground">
+              <span className="text-green-600">+8%</span> desde el mes pasado
+            </p>
           </CardContent>
         </Card>
-
-        {/* Badges Showcase */}
+        
         <Card>
-          <CardHeader>
-            <CardTitle>Insignias (Badges)</CardTitle>
-            <CardDescription>Diferentes variantes de badges</CardDescription>
+          <CardHeader className="pb-3">
+            <CardTitle className="text-sm font-medium">Tasa de Conversión</CardTitle>
           </CardHeader>
           <CardContent>
-            <div className="flex flex-wrap gap-2">
-              <Badge>Default</Badge>
-              <Badge variant="secondary">Secondary</Badge>
-              <Badge variant="destructive">Destructive</Badge>
-              <Badge variant="outline">Outline</Badge>
-              <Badge className="bg-green-500 hover:bg-green-600">Success</Badge>
-              <Badge className="bg-yellow-500 hover:bg-yellow-600">Warning</Badge>
-              <Badge className="bg-blue-500 hover:bg-blue-600">Info</Badge>
-            </div>
+            <div className="text-2xl font-bold">3.2%</div>
+            <p className="text-xs text-muted-foreground">
+              <span className="text-red-600">-2%</span> desde el mes pasado
+            </p>
           </CardContent>
         </Card>
+      </div>
 
-        {/* Footer */}
-        <div className="text-center py-8 border-t">
-          <p className="text-muted-foreground">
-            Demo creado con ❤️ usando Next.js y shadcn/ui
-          </p>
-          <div className="mt-2 flex justify-center gap-2">
-            <Badge variant="outline">v1.0.0</Badge>
-            <Badge variant="outline">TypeScript</Badge>
+      {/* Theme Toggle Demo */}
+      <Card>
+        <CardHeader>
+          <CardTitle>Selector de Tema</CardTitle>
+          <CardDescription>Cambia entre modo claro, oscuro y sistema</CardDescription>
+        </CardHeader>
+        <CardContent className="flex items-center justify-between">
+          <div>
+            <p className="text-sm font-medium">Modo Actual</p>
+            <p className="text-sm text-muted-foreground">
+              Usa el botón para cambiar entre Light, Dark y System
+            </p>
           </div>
+          <ModeToggle />
+        </CardContent>
+      </Card>
+
+      {/* Badges Showcase */}
+      <Card>
+        <CardHeader>
+          <CardTitle>Insignias (Badges)</CardTitle>
+          <CardDescription>Diferentes variantes de badges</CardDescription>
+        </CardHeader>
+        <CardContent>
+          <div className="flex flex-wrap gap-2">
+            <Badge>Default</Badge>
+            <Badge variant="secondary">Secondary</Badge>
+            <Badge variant="destructive">Destructive</Badge>
+            <Badge variant="outline">Outline</Badge>
+            <Badge className="bg-green-500 hover:bg-green-600">Success</Badge>
+            <Badge className="bg-yellow-500 hover:bg-yellow-600">Warning</Badge>
+            <Badge className="bg-blue-500 hover:bg-blue-600">Info</Badge>
+          </div>
+        </CardContent>
+      </Card>
+
+      {/* Footer */}
+      <div className="text-center py-8 border-t">
+        <p className="text-muted-foreground">
+          Demo creado con ❤️ usando Next.js y shadcn/ui
+        </p>
+        <div className="mt-2 flex justify-center gap-2">
+          <Badge variant="outline">v1.0.0</Badge>
+          <Badge variant="outline">TypeScript</Badge>
         </div>
       </div>
     </div>

+ 118 - 0
src/components/app-sidebar.tsx

@@ -0,0 +1,118 @@
+"use client"
+
+import { ChevronUp, Home, FileText, Settings } from "lucide-react"
+
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+import {
+  Sidebar,
+  SidebarContent,
+  SidebarFooter,
+  SidebarGroup,
+  SidebarGroupContent,
+  SidebarGroupLabel,
+  SidebarHeader,
+  SidebarMenu,
+  SidebarMenuButton,
+  SidebarMenuItem,
+  SidebarProvider,
+  SidebarTrigger,
+} from "@/components/ui/sidebar"
+
+// Menu items
+const items = [
+  {
+    title: "Inicio",
+    url: "/",
+    icon: Home,
+  },
+  {
+    title: "Factura",
+    url: "/factura",
+    icon: FileText,
+  },
+  {
+    title: "Configuración",
+    url: "#",
+    icon: Settings,
+  },
+]
+
+export function AppSidebar() {
+  return (
+    <Sidebar>
+      <SidebarHeader>
+        <SidebarMenu>
+          <SidebarMenuItem>
+            <SidebarMenuButton size="lg" className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
+              <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
+                <FileText className="size-4" />
+              </div>
+              <div className="grid flex-1 text-left text-sm leading-tight">
+                <span className="truncate font-semibold">Sumire</span>
+                <span className="truncate text-xs">Sistema de Facturación</span>
+              </div>
+            </SidebarMenuButton>
+          </SidebarMenuItem>
+        </SidebarMenu>
+      </SidebarHeader>
+      <SidebarContent>
+        <SidebarGroup>
+          <SidebarGroupLabel>Navegación</SidebarGroupLabel>
+          <SidebarGroupContent>
+            <SidebarMenu>
+              {items.map((item) => (
+                <SidebarMenuItem key={item.title}>
+                  <SidebarMenuButton asChild>
+                    <a href={item.url}>
+                      <item.icon />
+                      <span>{item.title}</span>
+                    </a>
+                  </SidebarMenuButton>
+                </SidebarMenuItem>
+              ))}
+            </SidebarMenu>
+          </SidebarGroupContent>
+        </SidebarGroup>
+      </SidebarContent>
+      <SidebarFooter>
+        <SidebarMenu>
+          <SidebarMenuItem>
+            <DropdownMenu>
+              <DropdownMenuTrigger asChild>
+                <SidebarMenuButton
+                  size="sm"
+                  className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
+                >
+                  <div className="grid flex-1 text-left text-sm leading-tight">
+                    <span className="truncate font-semibold">Usuario</span>
+                    <span className="truncate text-xs">usuario@ejemplo.com</span>
+                  </div>
+                  <ChevronUp className="ml-auto transition-transform group-data-[state=open]/dropdown-menu:rotate-180" />
+                </SidebarMenuButton>
+              </DropdownMenuTrigger>
+              <DropdownMenuContent
+                side="top"
+                className="w-[--radix-popper-anchor-width]"
+              >
+                <DropdownMenuItem>
+                  <span>Mi Cuenta</span>
+                </DropdownMenuItem>
+                <DropdownMenuItem>
+                  <span>Configuración</span>
+                </DropdownMenuItem>
+                <DropdownMenuItem>
+                  <span>Cerrar Sesión</span>
+                </DropdownMenuItem>
+              </DropdownMenuContent>
+            </DropdownMenu>
+          </SidebarMenuItem>
+        </SidebarMenu>
+      </SidebarFooter>
+    </Sidebar>
+  )
+}

+ 28 - 0
src/components/ui/separator.tsx

@@ -0,0 +1,28 @@
+"use client"
+
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
+
+import { cn } from "@/lib/utils"
+
+function Separator({
+  className,
+  orientation = "horizontal",
+  decorative = true,
+  ...props
+}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
+  return (
+    <SeparatorPrimitive.Root
+      data-slot="separator"
+      decorative={decorative}
+      orientation={orientation}
+      className={cn(
+        "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+export { Separator }

+ 139 - 0
src/components/ui/sheet.tsx

@@ -0,0 +1,139 @@
+"use client"
+
+import * as React from "react"
+import * as SheetPrimitive from "@radix-ui/react-dialog"
+import { XIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
+  return <SheetPrimitive.Root data-slot="sheet" {...props} />
+}
+
+function SheetTrigger({
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
+  return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
+}
+
+function SheetClose({
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Close>) {
+  return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
+}
+
+function SheetPortal({
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
+  return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
+}
+
+function SheetOverlay({
+  className,
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
+  return (
+    <SheetPrimitive.Overlay
+      data-slot="sheet-overlay"
+      className={cn(
+        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function SheetContent({
+  className,
+  children,
+  side = "right",
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Content> & {
+  side?: "top" | "right" | "bottom" | "left"
+}) {
+  return (
+    <SheetPortal>
+      <SheetOverlay />
+      <SheetPrimitive.Content
+        data-slot="sheet-content"
+        className={cn(
+          "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
+          side === "right" &&
+            "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
+          side === "left" &&
+            "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
+          side === "top" &&
+            "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
+          side === "bottom" &&
+            "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
+          className
+        )}
+        {...props}
+      >
+        {children}
+        <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
+          <XIcon className="size-4" />
+          <span className="sr-only">Close</span>
+        </SheetPrimitive.Close>
+      </SheetPrimitive.Content>
+    </SheetPortal>
+  )
+}
+
+function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sheet-header"
+      className={cn("flex flex-col gap-1.5 p-4", className)}
+      {...props}
+    />
+  )
+}
+
+function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sheet-footer"
+      className={cn("mt-auto flex flex-col gap-2 p-4", className)}
+      {...props}
+    />
+  )
+}
+
+function SheetTitle({
+  className,
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Title>) {
+  return (
+    <SheetPrimitive.Title
+      data-slot="sheet-title"
+      className={cn("text-foreground font-semibold", className)}
+      {...props}
+    />
+  )
+}
+
+function SheetDescription({
+  className,
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Description>) {
+  return (
+    <SheetPrimitive.Description
+      data-slot="sheet-description"
+      className={cn("text-muted-foreground text-sm", className)}
+      {...props}
+    />
+  )
+}
+
+export {
+  Sheet,
+  SheetTrigger,
+  SheetClose,
+  SheetContent,
+  SheetHeader,
+  SheetFooter,
+  SheetTitle,
+  SheetDescription,
+}

+ 726 - 0
src/components/ui/sidebar.tsx

@@ -0,0 +1,726 @@
+"use client"
+
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+import { PanelLeftIcon } from "lucide-react"
+
+import { useIsMobile } from "@/hooks/use-mobile"
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Separator } from "@/components/ui/separator"
+import {
+  Sheet,
+  SheetContent,
+  SheetDescription,
+  SheetHeader,
+  SheetTitle,
+} from "@/components/ui/sheet"
+import { Skeleton } from "@/components/ui/skeleton"
+import {
+  Tooltip,
+  TooltipContent,
+  TooltipProvider,
+  TooltipTrigger,
+} from "@/components/ui/tooltip"
+
+const SIDEBAR_COOKIE_NAME = "sidebar_state"
+const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
+const SIDEBAR_WIDTH = "16rem"
+const SIDEBAR_WIDTH_MOBILE = "18rem"
+const SIDEBAR_WIDTH_ICON = "3rem"
+const SIDEBAR_KEYBOARD_SHORTCUT = "b"
+
+type SidebarContextProps = {
+  state: "expanded" | "collapsed"
+  open: boolean
+  setOpen: (open: boolean) => void
+  openMobile: boolean
+  setOpenMobile: (open: boolean) => void
+  isMobile: boolean
+  toggleSidebar: () => void
+}
+
+const SidebarContext = React.createContext<SidebarContextProps | null>(null)
+
+function useSidebar() {
+  const context = React.useContext(SidebarContext)
+  if (!context) {
+    throw new Error("useSidebar must be used within a SidebarProvider.")
+  }
+
+  return context
+}
+
+function SidebarProvider({
+  defaultOpen = true,
+  open: openProp,
+  onOpenChange: setOpenProp,
+  className,
+  style,
+  children,
+  ...props
+}: React.ComponentProps<"div"> & {
+  defaultOpen?: boolean
+  open?: boolean
+  onOpenChange?: (open: boolean) => void
+}) {
+  const isMobile = useIsMobile()
+  const [openMobile, setOpenMobile] = React.useState(false)
+
+  // This is the internal state of the sidebar.
+  // We use openProp and setOpenProp for control from outside the component.
+  const [_open, _setOpen] = React.useState(defaultOpen)
+  const open = openProp ?? _open
+  const setOpen = React.useCallback(
+    (value: boolean | ((value: boolean) => boolean)) => {
+      const openState = typeof value === "function" ? value(open) : value
+      if (setOpenProp) {
+        setOpenProp(openState)
+      } else {
+        _setOpen(openState)
+      }
+
+      // This sets the cookie to keep the sidebar state.
+      document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
+    },
+    [setOpenProp, open]
+  )
+
+  // Helper to toggle the sidebar.
+  const toggleSidebar = React.useCallback(() => {
+    return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
+  }, [isMobile, setOpen, setOpenMobile])
+
+  // Adds a keyboard shortcut to toggle the sidebar.
+  React.useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (
+        event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
+        (event.metaKey || event.ctrlKey)
+      ) {
+        event.preventDefault()
+        toggleSidebar()
+      }
+    }
+
+    window.addEventListener("keydown", handleKeyDown)
+    return () => window.removeEventListener("keydown", handleKeyDown)
+  }, [toggleSidebar])
+
+  // We add a state so that we can do data-state="expanded" or "collapsed".
+  // This makes it easier to style the sidebar with Tailwind classes.
+  const state = open ? "expanded" : "collapsed"
+
+  const contextValue = React.useMemo<SidebarContextProps>(
+    () => ({
+      state,
+      open,
+      setOpen,
+      isMobile,
+      openMobile,
+      setOpenMobile,
+      toggleSidebar,
+    }),
+    [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
+  )
+
+  return (
+    <SidebarContext.Provider value={contextValue}>
+      <TooltipProvider delayDuration={0}>
+        <div
+          data-slot="sidebar-wrapper"
+          style={
+            {
+              "--sidebar-width": SIDEBAR_WIDTH,
+              "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
+              ...style,
+            } as React.CSSProperties
+          }
+          className={cn(
+            "group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
+            className
+          )}
+          {...props}
+        >
+          {children}
+        </div>
+      </TooltipProvider>
+    </SidebarContext.Provider>
+  )
+}
+
+function Sidebar({
+  side = "left",
+  variant = "sidebar",
+  collapsible = "offcanvas",
+  className,
+  children,
+  ...props
+}: React.ComponentProps<"div"> & {
+  side?: "left" | "right"
+  variant?: "sidebar" | "floating" | "inset"
+  collapsible?: "offcanvas" | "icon" | "none"
+}) {
+  const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
+
+  if (collapsible === "none") {
+    return (
+      <div
+        data-slot="sidebar"
+        className={cn(
+          "bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
+          className
+        )}
+        {...props}
+      >
+        {children}
+      </div>
+    )
+  }
+
+  if (isMobile) {
+    return (
+      <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
+        <SheetContent
+          data-sidebar="sidebar"
+          data-slot="sidebar"
+          data-mobile="true"
+          className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
+          style={
+            {
+              "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
+            } as React.CSSProperties
+          }
+          side={side}
+        >
+          <SheetHeader className="sr-only">
+            <SheetTitle>Sidebar</SheetTitle>
+            <SheetDescription>Displays the mobile sidebar.</SheetDescription>
+          </SheetHeader>
+          <div className="flex h-full w-full flex-col">{children}</div>
+        </SheetContent>
+      </Sheet>
+    )
+  }
+
+  return (
+    <div
+      className="group peer text-sidebar-foreground hidden md:block"
+      data-state={state}
+      data-collapsible={state === "collapsed" ? collapsible : ""}
+      data-variant={variant}
+      data-side={side}
+      data-slot="sidebar"
+    >
+      {/* This is what handles the sidebar gap on desktop */}
+      <div
+        data-slot="sidebar-gap"
+        className={cn(
+          "relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
+          "group-data-[collapsible=offcanvas]:w-0",
+          "group-data-[side=right]:rotate-180",
+          variant === "floating" || variant === "inset"
+            ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
+            : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
+        )}
+      />
+      <div
+        data-slot="sidebar-container"
+        className={cn(
+          "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
+          side === "left"
+            ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
+            : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
+          // Adjust the padding for floating and inset variants.
+          variant === "floating" || variant === "inset"
+            ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
+            : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
+          className
+        )}
+        {...props}
+      >
+        <div
+          data-sidebar="sidebar"
+          data-slot="sidebar-inner"
+          className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
+        >
+          {children}
+        </div>
+      </div>
+    </div>
+  )
+}
+
+function SidebarTrigger({
+  className,
+  onClick,
+  ...props
+}: React.ComponentProps<typeof Button>) {
+  const { toggleSidebar } = useSidebar()
+
+  return (
+    <Button
+      data-sidebar="trigger"
+      data-slot="sidebar-trigger"
+      variant="ghost"
+      size="icon"
+      className={cn("size-7", className)}
+      onClick={(event) => {
+        onClick?.(event)
+        toggleSidebar()
+      }}
+      {...props}
+    >
+      <PanelLeftIcon />
+      <span className="sr-only">Toggle Sidebar</span>
+    </Button>
+  )
+}
+
+function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
+  const { toggleSidebar } = useSidebar()
+
+  return (
+    <button
+      data-sidebar="rail"
+      data-slot="sidebar-rail"
+      aria-label="Toggle Sidebar"
+      tabIndex={-1}
+      onClick={toggleSidebar}
+      title="Toggle Sidebar"
+      className={cn(
+        "hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
+        "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
+        "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
+        "hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
+        "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
+        "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
+  return (
+    <main
+      data-slot="sidebar-inset"
+      className={cn(
+        "bg-background relative flex w-full flex-1 flex-col",
+        "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function SidebarInput({
+  className,
+  ...props
+}: React.ComponentProps<typeof Input>) {
+  return (
+    <Input
+      data-slot="sidebar-input"
+      data-sidebar="input"
+      className={cn("bg-background h-8 w-full shadow-none", className)}
+      {...props}
+    />
+  )
+}
+
+function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sidebar-header"
+      data-sidebar="header"
+      className={cn("flex flex-col gap-2 p-2", className)}
+      {...props}
+    />
+  )
+}
+
+function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sidebar-footer"
+      data-sidebar="footer"
+      className={cn("flex flex-col gap-2 p-2", className)}
+      {...props}
+    />
+  )
+}
+
+function SidebarSeparator({
+  className,
+  ...props
+}: React.ComponentProps<typeof Separator>) {
+  return (
+    <Separator
+      data-slot="sidebar-separator"
+      data-sidebar="separator"
+      className={cn("bg-sidebar-border mx-2 w-auto", className)}
+      {...props}
+    />
+  )
+}
+
+function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sidebar-content"
+      data-sidebar="content"
+      className={cn(
+        "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sidebar-group"
+      data-sidebar="group"
+      className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
+      {...props}
+    />
+  )
+}
+
+function SidebarGroupLabel({
+  className,
+  asChild = false,
+  ...props
+}: React.ComponentProps<"div"> & { asChild?: boolean }) {
+  const Comp = asChild ? Slot : "div"
+
+  return (
+    <Comp
+      data-slot="sidebar-group-label"
+      data-sidebar="group-label"
+      className={cn(
+        "text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
+        "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function SidebarGroupAction({
+  className,
+  asChild = false,
+  ...props
+}: React.ComponentProps<"button"> & { asChild?: boolean }) {
+  const Comp = asChild ? Slot : "button"
+
+  return (
+    <Comp
+      data-slot="sidebar-group-action"
+      data-sidebar="group-action"
+      className={cn(
+        "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
+        // Increases the hit area of the button on mobile.
+        "after:absolute after:-inset-2 md:after:hidden",
+        "group-data-[collapsible=icon]:hidden",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function SidebarGroupContent({
+  className,
+  ...props
+}: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sidebar-group-content"
+      data-sidebar="group-content"
+      className={cn("w-full text-sm", className)}
+      {...props}
+    />
+  )
+}
+
+function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
+  return (
+    <ul
+      data-slot="sidebar-menu"
+      data-sidebar="menu"
+      className={cn("flex w-full min-w-0 flex-col gap-1", className)}
+      {...props}
+    />
+  )
+}
+
+function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
+  return (
+    <li
+      data-slot="sidebar-menu-item"
+      data-sidebar="menu-item"
+      className={cn("group/menu-item relative", className)}
+      {...props}
+    />
+  )
+}
+
+const sidebarMenuButtonVariants = cva(
+  "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+  {
+    variants: {
+      variant: {
+        default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
+        outline:
+          "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
+      },
+      size: {
+        default: "h-8 text-sm",
+        sm: "h-7 text-xs",
+        lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+      size: "default",
+    },
+  }
+)
+
+function SidebarMenuButton({
+  asChild = false,
+  isActive = false,
+  variant = "default",
+  size = "default",
+  tooltip,
+  className,
+  ...props
+}: React.ComponentProps<"button"> & {
+  asChild?: boolean
+  isActive?: boolean
+  tooltip?: string | React.ComponentProps<typeof TooltipContent>
+} & VariantProps<typeof sidebarMenuButtonVariants>) {
+  const Comp = asChild ? Slot : "button"
+  const { isMobile, state } = useSidebar()
+
+  const button = (
+    <Comp
+      data-slot="sidebar-menu-button"
+      data-sidebar="menu-button"
+      data-size={size}
+      data-active={isActive}
+      className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
+      {...props}
+    />
+  )
+
+  if (!tooltip) {
+    return button
+  }
+
+  if (typeof tooltip === "string") {
+    tooltip = {
+      children: tooltip,
+    }
+  }
+
+  return (
+    <Tooltip>
+      <TooltipTrigger asChild>{button}</TooltipTrigger>
+      <TooltipContent
+        side="right"
+        align="center"
+        hidden={state !== "collapsed" || isMobile}
+        {...tooltip}
+      />
+    </Tooltip>
+  )
+}
+
+function SidebarMenuAction({
+  className,
+  asChild = false,
+  showOnHover = false,
+  ...props
+}: React.ComponentProps<"button"> & {
+  asChild?: boolean
+  showOnHover?: boolean
+}) {
+  const Comp = asChild ? Slot : "button"
+
+  return (
+    <Comp
+      data-slot="sidebar-menu-action"
+      data-sidebar="menu-action"
+      className={cn(
+        "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
+        // Increases the hit area of the button on mobile.
+        "after:absolute after:-inset-2 md:after:hidden",
+        "peer-data-[size=sm]/menu-button:top-1",
+        "peer-data-[size=default]/menu-button:top-1.5",
+        "peer-data-[size=lg]/menu-button:top-2.5",
+        "group-data-[collapsible=icon]:hidden",
+        showOnHover &&
+          "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function SidebarMenuBadge({
+  className,
+  ...props
+}: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sidebar-menu-badge"
+      data-sidebar="menu-badge"
+      className={cn(
+        "text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
+        "peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
+        "peer-data-[size=sm]/menu-button:top-1",
+        "peer-data-[size=default]/menu-button:top-1.5",
+        "peer-data-[size=lg]/menu-button:top-2.5",
+        "group-data-[collapsible=icon]:hidden",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function SidebarMenuSkeleton({
+  className,
+  showIcon = false,
+  ...props
+}: React.ComponentProps<"div"> & {
+  showIcon?: boolean
+}) {
+  // Random width between 50 to 90%.
+  const width = React.useMemo(() => {
+    return `${Math.floor(Math.random() * 40) + 50}%`
+  }, [])
+
+  return (
+    <div
+      data-slot="sidebar-menu-skeleton"
+      data-sidebar="menu-skeleton"
+      className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
+      {...props}
+    >
+      {showIcon && (
+        <Skeleton
+          className="size-4 rounded-md"
+          data-sidebar="menu-skeleton-icon"
+        />
+      )}
+      <Skeleton
+        className="h-4 max-w-(--skeleton-width) flex-1"
+        data-sidebar="menu-skeleton-text"
+        style={
+          {
+            "--skeleton-width": width,
+          } as React.CSSProperties
+        }
+      />
+    </div>
+  )
+}
+
+function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
+  return (
+    <ul
+      data-slot="sidebar-menu-sub"
+      data-sidebar="menu-sub"
+      className={cn(
+        "border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
+        "group-data-[collapsible=icon]:hidden",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function SidebarMenuSubItem({
+  className,
+  ...props
+}: React.ComponentProps<"li">) {
+  return (
+    <li
+      data-slot="sidebar-menu-sub-item"
+      data-sidebar="menu-sub-item"
+      className={cn("group/menu-sub-item relative", className)}
+      {...props}
+    />
+  )
+}
+
+function SidebarMenuSubButton({
+  asChild = false,
+  size = "md",
+  isActive = false,
+  className,
+  ...props
+}: React.ComponentProps<"a"> & {
+  asChild?: boolean
+  size?: "sm" | "md"
+  isActive?: boolean
+}) {
+  const Comp = asChild ? Slot : "a"
+
+  return (
+    <Comp
+      data-slot="sidebar-menu-sub-button"
+      data-sidebar="menu-sub-button"
+      data-size={size}
+      data-active={isActive}
+      className={cn(
+        "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+        "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
+        size === "sm" && "text-xs",
+        size === "md" && "text-sm",
+        "group-data-[collapsible=icon]:hidden",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+export {
+  Sidebar,
+  SidebarContent,
+  SidebarFooter,
+  SidebarGroup,
+  SidebarGroupAction,
+  SidebarGroupContent,
+  SidebarGroupLabel,
+  SidebarHeader,
+  SidebarInput,
+  SidebarInset,
+  SidebarMenu,
+  SidebarMenuAction,
+  SidebarMenuBadge,
+  SidebarMenuButton,
+  SidebarMenuItem,
+  SidebarMenuSkeleton,
+  SidebarMenuSub,
+  SidebarMenuSubButton,
+  SidebarMenuSubItem,
+  SidebarProvider,
+  SidebarRail,
+  SidebarSeparator,
+  SidebarTrigger,
+  useSidebar,
+}

+ 13 - 0
src/components/ui/skeleton.tsx

@@ -0,0 +1,13 @@
+import { cn } from "@/lib/utils"
+
+function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="skeleton"
+      className={cn("bg-accent animate-pulse rounded-md", className)}
+      {...props}
+    />
+  )
+}
+
+export { Skeleton }

+ 61 - 0
src/components/ui/tooltip.tsx

@@ -0,0 +1,61 @@
+"use client"
+
+import * as React from "react"
+import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+
+import { cn } from "@/lib/utils"
+
+function TooltipProvider({
+  delayDuration = 0,
+  ...props
+}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
+  return (
+    <TooltipPrimitive.Provider
+      data-slot="tooltip-provider"
+      delayDuration={delayDuration}
+      {...props}
+    />
+  )
+}
+
+function Tooltip({
+  ...props
+}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
+  return (
+    <TooltipProvider>
+      <TooltipPrimitive.Root data-slot="tooltip" {...props} />
+    </TooltipProvider>
+  )
+}
+
+function TooltipTrigger({
+  ...props
+}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
+  return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
+}
+
+function TooltipContent({
+  className,
+  sideOffset = 0,
+  children,
+  ...props
+}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
+  return (
+    <TooltipPrimitive.Portal>
+      <TooltipPrimitive.Content
+        data-slot="tooltip-content"
+        sideOffset={sideOffset}
+        className={cn(
+          "bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
+          className
+        )}
+        {...props}
+      >
+        {children}
+        <TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
+      </TooltipPrimitive.Content>
+    </TooltipPrimitive.Portal>
+  )
+}
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

+ 19 - 0
src/hooks/use-mobile.ts

@@ -0,0 +1,19 @@
+import * as React from "react"
+
+const MOBILE_BREAKPOINT = 768
+
+export function useIsMobile() {
+  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
+
+  React.useEffect(() => {
+    const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
+    const onChange = () => {
+      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+    }
+    mql.addEventListener("change", onChange)
+    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+    return () => mql.removeEventListener("change", onChange)
+  }, [])
+
+  return !!isMobile
+}